yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
activity_bar.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <cctype>
6#include <cstddef>
7#include <cstring>
8#include <functional>
9#include <string>
10#include <unordered_set>
11#include <utility>
12#include <vector>
13
14#include "absl/strings/str_format.h"
21#include "app/gui/core/icons.h"
28#include "core/color.h"
29#include "imgui/imgui.h"
30
31namespace yaze {
32namespace editor {
33
34namespace {
35constexpr const char* kSidebarDragPayload = "YAZE_SIDEBAR_CAT";
36
38 if (!settings)
39 return;
40 (void)settings->Save();
41}
42} // namespace
43
45 WorkspaceWindowManager& window_manager,
46 std::function<bool()> is_dungeon_workbench_mode,
47 std::function<void(bool)> set_dungeon_workflow_mode,
48 std::function<EditorContextSnapshot(const std::string&)> context_provider)
49 : window_manager_(window_manager),
50 window_browser_(window_manager),
51 window_sidebar_(
52 window_manager, std::move(is_dungeon_workbench_mode),
53 std::move(set_dungeon_workflow_mode),
54 [this]() { return GetBottomReservedHeight(); },
55 std::move(context_provider)),
56 actions_registry_(std::make_unique<MoreActionsRegistry>()) {}
57
59
62 return 0.0f;
63 }
65}
66
67std::vector<std::string> ActivityBar::SortCategories(
68 const std::vector<std::string>& input,
69 const std::vector<std::string>& order,
70 const std::unordered_set<std::string>& pinned,
71 const std::unordered_set<std::string>& hidden) {
72 // visible preserves input order, filters hidden.
73 std::vector<std::string> visible;
74 visible.reserve(input.size());
75 std::unordered_set<std::string> visible_set;
76 visible_set.reserve(input.size());
77 for (const auto& c : input) {
78 if (hidden.count(c))
79 continue;
80 visible.push_back(c);
81 visible_set.insert(c);
82 }
83
84 // Pinned ∩ visible, in input order.
85 std::vector<std::string> pinned_visible;
86 std::unordered_set<std::string> pinned_visible_set;
87 for (const auto& c : input) {
88 if (visible_set.count(c) && pinned.count(c)) {
89 pinned_visible.push_back(c);
90 pinned_visible_set.insert(c);
91 }
92 }
93
94 // When the user has never customized the order, preserve the canonical
95 // input ordering for every non-pinned visible entry. Only once `order` has
96 // content do we split into "explicit order" + "true newcomers alphabetical".
97 std::vector<std::string> ordered;
98 std::vector<std::string> newcomers;
99
100 if (order.empty()) {
101 for (const auto& c : visible) {
102 if (pinned_visible_set.count(c))
103 continue;
104 ordered.push_back(c);
105 }
106 } else {
107 std::unordered_set<std::string> ordered_set;
108 for (const auto& c : order) {
109 if (!visible_set.count(c))
110 continue;
111 if (pinned_visible_set.count(c))
112 continue;
113 ordered.push_back(c);
114 ordered_set.insert(c);
115 }
116 std::unordered_set<std::string> order_set(order.begin(), order.end());
117 for (const auto& c : visible) {
118 if (pinned_visible_set.count(c))
119 continue;
120 if (order_set.count(c))
121 continue;
122 newcomers.push_back(c);
123 }
124 std::sort(newcomers.begin(), newcomers.end());
125 }
126
127 std::vector<std::string> result;
128 result.reserve(pinned_visible.size() + ordered.size() + newcomers.size());
129 result.insert(result.end(), pinned_visible.begin(), pinned_visible.end());
130 result.insert(result.end(), ordered.begin(), ordered.end());
131 result.insert(result.end(), newcomers.begin(), newcomers.end());
132 return result;
133}
134
136 size_t session_id, const std::string& active_category,
137 const std::vector<std::string>& all_categories,
138 const std::unordered_set<std::string>& active_editor_categories,
139 std::function<bool()> has_rom, std::function<bool()> is_rom_dirty,
140 std::function<int()> pending_dungeon_rooms) {
143 return;
144
145 // When the startup dashboard is active there are no meaningful left-panel
146 // cards; keep the activity rail visible but collapse the side panel.
147 const bool dashboard_active =
149 if (dashboard_active && window_manager_.IsSidebarExpanded()) {
151 }
152
154 session_id, active_category, all_categories, active_editor_categories,
155 has_rom, std::move(is_rom_dirty), std::move(pending_dungeon_rooms));
156
157 if (window_manager_.IsSidebarExpanded() && !dashboard_active) {
158 DrawSidePanel(session_id, active_category, has_rom);
159 }
160}
161
162void ActivityBar::DrawCategoryContextMenu(const std::string& category) {
163 if (!user_settings_)
164 return;
165
166 // ImGui generates a stable popup id from the last item by default, but we
167 // pass an explicit id so the popup survives ImGui::PushID changes.
168 std::string popup_id = absl::StrFormat("##SidebarCtx_%s", category);
169 if (!ImGui::BeginPopupContextItem(popup_id.c_str()))
170 return;
171
172 auto& prefs = user_settings_->prefs();
173 const bool is_pinned = prefs.sidebar_pinned.count(category) > 0;
174 const bool is_hidden = prefs.sidebar_hidden.count(category) > 0;
175
176 std::vector<gui::MenuItemSpec> items;
177 items.emplace_back(is_pinned ? "Unpin from Top" : "Pin to Top",
178 ICON_MD_PUSH_PIN, [&prefs, &category, is_pinned, this]() {
179 if (is_pinned) {
180 prefs.sidebar_pinned.erase(category);
181 } else {
182 prefs.sidebar_pinned.insert(category);
183 }
184 PersistSettings(user_settings_);
185 });
186 items.emplace_back(is_hidden ? "Show on Sidebar" : "Hide from Sidebar",
188 [&prefs, &category, is_hidden, this]() {
189 if (is_hidden) {
190 prefs.sidebar_hidden.erase(category);
191 } else {
192 prefs.sidebar_hidden.insert(category);
193 }
194 PersistSettings(user_settings_);
195 });
196 items.back().separator_after = true;
197 items.emplace_back(tr("Reset Sidebar Order"), ICON_MD_RESTORE,
198 [&prefs, this]() {
199 prefs.sidebar_order.clear();
200 PersistSettings(user_settings_);
201 });
202 items.emplace_back(tr("Show All Categories"), ICON_MD_VISIBILITY,
203 [&prefs, this]() {
204 prefs.sidebar_hidden.clear();
205 PersistSettings(user_settings_);
206 });
208
209 ImGui::EndPopup();
210}
211
212void ActivityBar::HandleReorderDragAndDrop(const std::string& category) {
213 if (!user_settings_)
214 return;
215 auto& prefs = user_settings_->prefs();
216
217 // Pinned items participate in pin grouping but not in drag-reorder — the
218 // pin block's order is driven by the registry's canonical order.
219 const bool is_pinned = prefs.sidebar_pinned.count(category) > 0;
220
221 if (!is_pinned &&
222 ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) {
223 ImGui::SetDragDropPayload(kSidebarDragPayload, category.data(),
224 category.size());
225 ImGui::TextUnformatted(category.c_str());
226 ImGui::EndDragDropSource();
227 }
228
229 if (ImGui::BeginDragDropTarget()) {
230 const ImGuiPayload* payload =
231 ImGui::AcceptDragDropPayload(kSidebarDragPayload);
232 if (payload != nullptr && payload->Data != nullptr) {
233 std::string src(static_cast<const char*>(payload->Data),
234 static_cast<size_t>(payload->DataSize));
235 if (src != category && !prefs.sidebar_pinned.count(src) &&
236 !prefs.sidebar_pinned.count(category)) {
237 auto& order = prefs.sidebar_order;
238 auto rm = std::remove(order.begin(), order.end(), src);
239 if (rm != order.end()) {
240 order.erase(rm, order.end());
241 }
242 auto dst = std::find(order.begin(), order.end(), category);
243 if (dst == order.end()) {
244 // Target wasn't tracked yet; keep moves local by appending.
245 order.push_back(src);
246 } else {
247 order.insert(dst, src);
248 }
249 PersistSettings(user_settings_);
250 }
251 }
252 ImGui::EndDragDropTarget();
253 }
254}
255
257 size_t session_id, const std::string& active_category,
258 const std::vector<std::string>& all_categories,
259 const std::unordered_set<std::string>& active_editor_categories,
260 std::function<bool()> has_rom, std::function<bool()> is_rom_dirty,
261 std::function<int()> pending_dungeon_rooms) {
262
263 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
264 const ImGuiViewport* viewport = ImGui::GetMainViewport();
265 const float top_inset = gui::LayoutHelpers::GetTopInset();
266 const auto safe_area = gui::LayoutHelpers::GetSafeAreaInsets();
267 const float bottom_reserved = GetBottomReservedHeight();
268 const float viewport_height =
269 std::max(0.0f, viewport->WorkSize.y - top_inset - safe_area.bottom -
270 bottom_reserved);
271 const float bar_width = gui::UIConfig::kActivityBarWidth;
272
273 constexpr ImGuiWindowFlags kExtraFlags =
274 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoFocusOnAppearing |
275 ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoBringToFrontOnFocus;
276
277 gui::FixedPanel bar(
278 "##ActivityBar",
279 ImVec2(viewport->WorkPos.x, viewport->WorkPos.y + top_inset),
280 ImVec2(bar_width, viewport_height),
281 {.bg = gui::ConvertColorToImVec4(theme.surface),
282 .border = gui::ConvertColorToImVec4(theme.text_disabled),
283 .padding = {0.0f, 6.0f},
284 .spacing = {0.0f, 4.0f},
285 .border_size = 0.0f},
286 kExtraFlags);
287
288 if (bar) {
289
290 // Global Search / Command Palette at top
292 "Global Search (Ctrl+Shift+F)", false,
293 ImVec4(0, 0, 0, 0), "activity_bar",
294 "search")) {
296 }
297
298 // Separator
299 ImGui::Spacing();
300 ImVec2 sep_p1 = ImGui::GetCursorScreenPos();
301 ImVec2 sep_p2 =
302 ImVec2(sep_p1.x + gui::UIConfig::kActivityBarWidth, sep_p1.y);
303 ImGui::GetWindowDrawList()->AddLine(
304 sep_p1, sep_p2,
305 ImGui::ColorConvertFloat4ToU32(gui::ConvertColorToImVec4(theme.border)),
306 1.0f);
307 ImGui::Spacing();
308
309 bool rom_loaded = has_rom ? has_rom() : false;
310
311 // Apply per-user pin/order/hide prefs if available. Dashboard category
312 // always stays excluded regardless of prefs so we strip it first.
313 std::vector<std::string> filtered_input;
314 filtered_input.reserve(all_categories.size());
315 for (const auto& cat : all_categories) {
317 continue;
318 filtered_input.push_back(cat);
319 }
320
321 std::vector<std::string> effective = filtered_input;
322 if (user_settings_) {
323 const auto& prefs = user_settings_->prefs();
324 effective = SortCategories(filtered_input, prefs.sidebar_order,
325 prefs.sidebar_pinned, prefs.sidebar_hidden);
326
327 // Empty-state guard: if the user hid every category, silently reset
328 // the hidden set so the rail stays usable.
329 if (effective.empty() && !filtered_input.empty()) {
331 PersistSettings(user_settings_);
332 effective = filtered_input;
333 }
334 }
335
336 // Draw categories in effective order.
337 for (const auto& cat : effective) {
338 bool is_selected = (cat == active_category);
339 bool panel_expanded = window_manager_.IsSidebarExpanded();
340 bool has_active_editor = active_editor_categories.count(cat) > 0;
341
342 // Emulator, Agent and Planning work without a ROM.
343 bool category_enabled = rom_loaded || (cat == "Emulator") ||
344 (cat == "Agent") || (cat == "Planning");
345
346 const EditorType editor_type =
348 const bool experiments_enabled =
350 const bool experimental_warning =
352 editor_type, experiments_enabled);
353
354 // Get category-specific theme colors for expressive appearance
355 auto cat_theme = WorkspaceWindowManager::GetCategoryTheme(cat);
356 ImVec4 cat_color(cat_theme.r, cat_theme.g, cat_theme.b, cat_theme.a);
357 ImVec4 glow_color(cat_theme.glow_r, cat_theme.glow_g, cat_theme.glow_b,
358 1.0f);
359
360 // Active Indicator with category-specific colors
361 if (is_selected && category_enabled && panel_expanded) {
362 ImVec2 pos = ImGui::GetCursorScreenPos();
363
364 // Outer glow shadow (subtle, category color at 15% opacity)
365 ImVec4 outer_glow = glow_color;
366 outer_glow.w = 0.15f;
367 ImGui::GetWindowDrawList()->AddRectFilled(
368 ImVec2(pos.x - 1.0f, pos.y - 1.0f),
369 ImVec2(pos.x + 49.0f, pos.y + 41.0f),
370 ImGui::ColorConvertFloat4ToU32(outer_glow), 4.0f);
371
372 // Background highlight (category glow at 30% opacity)
373 ImVec4 highlight = glow_color;
374 highlight.w = 0.30f;
375 ImGui::GetWindowDrawList()->AddRectFilled(
376 pos, ImVec2(pos.x + 48.0f, pos.y + 40.0f),
377 ImGui::ColorConvertFloat4ToU32(highlight), 2.0f);
378
379 // Left accent border (4px wide, category-specific color)
380 ImGui::GetWindowDrawList()->AddRectFilled(
381 pos, ImVec2(pos.x + 4.0f, pos.y + 40.0f),
382 ImGui::ColorConvertFloat4ToU32(cat_color));
383 }
384
385 std::string icon = WorkspaceWindowManager::GetCategoryIcon(cat);
386
387 // Subtle indicator even when collapsed
388 if (is_selected && category_enabled && !panel_expanded) {
389 ImVec2 pos = ImGui::GetCursorScreenPos();
390 ImVec4 highlight = glow_color;
391 highlight.w = 0.15f;
392 ImGui::GetWindowDrawList()->AddRectFilled(
393 pos, ImVec2(pos.x + 48.0f, pos.y + 40.0f),
394 ImGui::ColorConvertFloat4ToU32(highlight), 2.0f);
395 ImVec4 accent = cat_color;
396 accent.w = 0.6f;
397 ImGui::GetWindowDrawList()->AddRectFilled(
398 pos, ImVec2(pos.x + 3.0f, pos.y + 40.0f),
399 ImGui::ColorConvertFloat4ToU32(accent));
400 }
401
402 // Dim indicator for categories whose editor is open but not currently
403 // selected. Makes "what's open" readable at a glance without competing
404 // with the full selection glow above.
405 if (!is_selected && category_enabled && has_active_editor) {
406 ImVec2 pos = ImGui::GetCursorScreenPos();
407 ImVec4 dim_accent = cat_color;
408 dim_accent.w = 0.35f;
409 ImGui::GetWindowDrawList()->AddRectFilled(
410 ImVec2(pos.x + 45.0f, pos.y + 8.0f),
411 ImVec2(pos.x + 48.0f, pos.y + 32.0f),
412 ImGui::ColorConvertFloat4ToU32(dim_accent), 1.5f);
413 }
414
415 // Pinned badge — small tick in the top-left corner.
416 bool is_pinned = user_settings_ &&
417 user_settings_->prefs().sidebar_pinned.count(cat) > 0;
418
419 // Always pass category color so inactive icons remain visible
420 ImVec4 icon_color = cat_color;
421 if (!category_enabled) {
422 ImGui::BeginDisabled();
423 }
425 nullptr, is_selected, icon_color,
426 "activity_bar", cat.c_str())) {
427 if (category_enabled) {
428 if (cat == active_category) {
429 // Explicit toggle only — selecting a category must not auto-open
430 // WindowSidebar (ActivityBar-only default).
432 } else {
435 }
436 }
437 }
438 if (!category_enabled) {
439 ImGui::EndDisabled();
440 }
441
442 // Context menu + drag-drop anchor on the icon's last-drawn rect.
445
446 if (is_pinned) {
447 ImVec2 pin_min = ImGui::GetItemRectMin();
448 ImVec4 pin_color = cat_color;
449 pin_color.w = 0.85f;
450 ImGui::GetWindowDrawList()->AddCircleFilled(
451 ImVec2(pin_min.x + 6.0f, pin_min.y + 6.0f), 2.5f,
452 ImGui::ColorConvertFloat4ToU32(pin_color));
453 }
454
455 const int pending_rooms =
456 pending_dungeon_rooms ? pending_dungeon_rooms() : 0;
457 const bool dungeon_pending = cat == "Dungeon" && pending_rooms > 0;
458
459 // Dirty-ROM dot badge on the currently selected category's icon.
460 // We draw after the button so it paints on top.
461 const bool rom_dirty = is_rom_dirty ? is_rom_dirty() : false;
462 if (is_selected && category_enabled && rom_dirty) {
463 ImVec2 last_min = ImGui::GetItemRectMin();
464 ImVec2 last_max = ImGui::GetItemRectMax();
465 ImVec2 dot_center(last_max.x - 7.0f, last_min.y + 7.0f);
466 ImVec4 dot_color = gui::ConvertColorToImVec4(theme.warning);
467 ImGui::GetWindowDrawList()->AddCircleFilled(
468 dot_center, 3.5f, ImGui::ColorConvertFloat4ToU32(dot_color));
469 }
470
471 if (category_enabled && dungeon_pending) {
472 ImVec2 last_min = ImGui::GetItemRectMin();
473 ImVec2 pending_center(last_min.x + 7.0f, last_min.y + 7.0f);
474 ImVec4 pending_color = gui::ConvertColorToImVec4(theme.warning);
475 ImGui::GetWindowDrawList()->AddCircleFilled(
476 pending_center, 3.0f,
477 ImGui::ColorConvertFloat4ToU32(pending_color));
478 }
479
480 // Tooltip with status information
481 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
482 ImGui::BeginTooltip();
483 ImGui::Text("%s %s", icon.c_str(), cat.c_str());
484 if (!category_enabled) {
485 gui::ColoredText("Open ROM required",
486 gui::ConvertColorToImVec4(theme.warning));
487 } else if (experimental_warning) {
489 "Experimental — available with defensive save safeguards",
490 gui::ConvertColorToImVec4(theme.warning));
491 } else if (has_active_editor) {
493 is_selected ? "Active editor" : "Editor open (click to focus)",
494 gui::ConvertColorToImVec4(theme.success));
495 } else {
496 gui::ColoredText("Click to view windows",
498 }
499 if (is_pinned) {
500 gui::ColoredText("Pinned (right-click to unpin)",
502 } else if (user_settings_) {
503 gui::ColoredText("Right-click for options • drag to reorder",
505 }
506 if (is_selected && rom_dirty) {
507 gui::ColoredText("ROM has unsaved changes",
508 gui::ConvertColorToImVec4(theme.warning));
509 }
510 if (dungeon_pending) {
512 "%d dungeon room%s pending apply", pending_rooms,
513 pending_rooms == 1 ? "" : "s");
514 }
515 ImGui::EndTooltip();
516 }
517 }
518 }
519
520 // Draw "More Actions" button at the bottom
521 ImGui::SetCursorPosY(viewport_height - 48.0f);
522
524 nullptr, false, ImVec4(0, 0, 0, 0),
525 "activity_bar", "more_actions")) {
526 ImGui::OpenPopup("ActivityBarMoreMenu");
527 }
528
529 if (ImGui::BeginPopup("ActivityBarMoreMenu")) {
530 if (actions_registry_ && !actions_registry_->empty()) {
531 actions_registry_->ForEach([](const MoreAction& action) {
532 std::string label;
533 if (action.icon != nullptr) {
534 label = absl::StrFormat("%s %s", action.icon, action.label);
535 } else {
536 label = action.label;
537 }
538 bool enabled = !action.enabled_fn || action.enabled_fn();
539 if (ImGui::MenuItem(label.c_str(), /*shortcut=*/nullptr,
540 /*selected=*/false, enabled)) {
541 if (action.on_invoke)
542 action.on_invoke();
543 }
544 });
545 } else {
546 ImGui::TextDisabled(tr("No actions available"));
547 }
548 ImGui::EndPopup();
549 }
550 // FixedPanel destructor handles End() + PopStyleVar/PopStyleColor
551}
552
553void ActivityBar::DrawSidePanel(size_t session_id, const std::string& category,
554 std::function<bool()> has_rom) {
555 window_sidebar_.Draw(session_id, category, std::move(has_rom));
556}
557
558void ActivityBar::DrawWindowBrowser(size_t session_id, bool* p_open) {
559 window_browser_.Draw(session_id, p_open);
560}
561
562} // namespace editor
563} // namespace yaze
void DrawSidePanel(size_t session_id, const std::string &category, std::function< bool()> has_rom)
void DrawWindowBrowser(size_t session_id, bool *p_open)
WindowBrowser window_browser_
UserSettings * user_settings_
std::unique_ptr< MoreActionsRegistry > actions_registry_
void Render(size_t session_id, const std::string &active_category, const std::vector< std::string > &all_categories, const std::unordered_set< std::string > &active_editor_categories, std::function< bool()> has_rom, std::function< bool()> is_rom_dirty={}, std::function< int()> pending_dungeon_rooms={})
WindowSidebar window_sidebar_
void DrawCategoryContextMenu(const std::string &category)
WorkspaceWindowManager & window_manager_
ActivityBar(WorkspaceWindowManager &window_manager, std::function< bool()> is_dungeon_workbench_mode={}, std::function< void(bool)> set_dungeon_workflow_mode={}, std::function< EditorContextSnapshot(const std::string &)> context_provider={})
static std::vector< std::string > SortCategories(const std::vector< std::string > &input, const std::vector< std::string > &order, const std::unordered_set< std::string > &pinned, const std::unordered_set< std::string > &hidden)
void HandleReorderDragAndDrop(const std::string &category)
void DrawActivityBarStrip(size_t session_id, const std::string &active_category, const std::vector< std::string > &all_categories, const std::unordered_set< std::string > &active_editor_categories, std::function< bool()> has_rom, std::function< bool()> is_rom_dirty, std::function< int()> pending_dungeon_rooms)
float GetBottomReservedHeight() const
static EditorType GetEditorTypeFromCategory(const std::string &category)
static bool ShouldWarnAboutExperimentalEditor(EditorType type, bool experiments_enabled)
static constexpr float kStatusBarHeight
Definition status_bar.h:241
Manages user preferences and settings persistence.
void Draw(size_t session_id, bool *p_open)
void Draw(size_t session_id, const std::string &category, std::function< bool()> has_rom)
void DrawContextDetailsWindow(size_t session_id)
Central registry for all editor cards with session awareness and dependency injection.
static CategoryTheme GetCategoryTheme(const std::string &category)
void SetActiveCategory(const std::string &category, bool notify=true)
static std::string GetCategoryIcon(const std::string &category)
static constexpr const char * kDashboardCategory
void TriggerCategorySelected(const std::string &category)
void SetSidebarExpanded(bool expanded, bool notify=true)
RAII for fixed-position panels (activity bar, side panel, status bar).
static SafeAreaInsets GetSafeAreaInsets()
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
#define ICON_MD_SEARCH
Definition icons.h:1673
#define ICON_MD_VISIBILITY
Definition icons.h:2101
#define ICON_MD_MORE_HORIZ
Definition icons.h:1241
#define ICON_MD_RESTORE
Definition icons.h:1605
#define ICON_MD_VISIBILITY_OFF
Definition icons.h:2102
#define ICON_MD_PUSH_PIN
Definition icons.h:1529
bool TransparentIconButton(const char *icon, const ImVec2 &size, const char *tooltip, bool is_active, const ImVec4 &active_color, const char *panel_id, const char *anim_id)
Draw a transparent icon button (hover effect only).
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:140
void ColoredText(const char *text, const ImVec4 &color)
ImVec4 GetTextSecondaryVec4()
void ColoredTextF(const ImVec4 &color, const char *fmt,...)
void RenderMenuItems(const std::vector< MenuItemSpec > &items)
std::unordered_set< std::string > sidebar_pinned
std::unordered_set< std::string > sidebar_hidden
static constexpr float kActivityBarWidth
Definition ui_config.h:18