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
3#include <algorithm>
4#include <cctype>
5#include <cstddef>
6#include <functional>
7#include <string>
8#include <unordered_set>
9#include <utility>
10#include <vector>
11
12#include "absl/strings/str_format.h"
14#include "app/gui/core/icons.h"
21#include "core/color.h"
22#include "imgui/imgui.h"
23
24namespace yaze {
25namespace editor {
26
28 std::function<bool()> is_dungeon_workbench_mode,
29 std::function<void(bool)> set_dungeon_workflow_mode)
30 : panel_manager_(panel_manager),
31 is_dungeon_workbench_mode_(std::move(is_dungeon_workbench_mode)),
32 set_dungeon_workflow_mode_(std::move(set_dungeon_workflow_mode)) {}
33
35 size_t session_id, const std::string& active_category,
36 const std::vector<std::string>& all_categories,
37 const std::unordered_set<std::string>& active_editor_categories,
38 std::function<bool()> has_rom) {
40 return;
41
42 // When the startup dashboard is active there are no meaningful left-panel
43 // cards; keep the activity rail visible but collapse the side panel.
44 const bool dashboard_active =
45 (active_category == PanelManager::kDashboardCategory);
46 if (dashboard_active && panel_manager_.IsPanelExpanded()) {
48 }
49
50 DrawActivityBarStrip(session_id, active_category, all_categories,
51 active_editor_categories, has_rom);
52
53 if (panel_manager_.IsPanelExpanded() && !dashboard_active) {
54 DrawSidePanel(session_id, active_category, has_rom);
55 }
56}
57
59 size_t session_id, const std::string& active_category,
60 const std::vector<std::string>& all_categories,
61 const std::unordered_set<std::string>& active_editor_categories,
62 std::function<bool()> has_rom) {
63
64 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
65 const ImGuiViewport* viewport = ImGui::GetMainViewport();
66 const float top_inset = gui::LayoutHelpers::GetTopInset();
67 const auto safe_area = gui::LayoutHelpers::GetSafeAreaInsets();
68 const float viewport_height =
69 std::max(0.0f, viewport->WorkSize.y - top_inset - safe_area.bottom);
70 const float bar_width = gui::UIConfig::kActivityBarWidth;
71
72 constexpr ImGuiWindowFlags kExtraFlags =
73 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoFocusOnAppearing |
74 ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoBringToFrontOnFocus;
75
77 "##ActivityBar",
78 ImVec2(viewport->WorkPos.x, viewport->WorkPos.y + top_inset),
79 ImVec2(bar_width, viewport_height),
80 {.bg = gui::ConvertColorToImVec4(theme.surface),
81 .border = gui::ConvertColorToImVec4(theme.text_disabled),
82 .padding = {0.0f, 8.0f},
83 .spacing = {0.0f, 8.0f},
84 .border_size = 1.0f},
85 kExtraFlags);
86
87 if (bar) {
88
89 // Global Search / Command Palette at top
91 "Global Search (Ctrl+Shift+F)", false,
92 ImVec4(0, 0, 0, 0), "activity_bar",
93 "search")) {
95 }
96
97 // Separator
98 ImGui::Spacing();
99 ImVec2 sep_p1 = ImGui::GetCursorScreenPos();
100 ImVec2 sep_p2 =
101 ImVec2(sep_p1.x + gui::UIConfig::kActivityBarWidth, sep_p1.y);
102 ImGui::GetWindowDrawList()->AddLine(
103 sep_p1, sep_p2,
104 ImGui::ColorConvertFloat4ToU32(gui::ConvertColorToImVec4(theme.border)),
105 1.0f);
106 ImGui::Spacing();
107
108 bool rom_loaded = has_rom ? has_rom() : false;
109
110 // Draw ALL editor categories (not just active ones)
111 for (const auto& cat : all_categories) {
113 continue;
114 }
115 bool is_selected = (cat == active_category);
116 bool panel_expanded = panel_manager_.IsPanelExpanded();
117 bool has_active_editor = active_editor_categories.count(cat) > 0;
118
119 // Emulator is always available, others require ROM
120 bool category_enabled =
121 rom_loaded || (cat == "Emulator") || (cat == "Agent");
122
123 // Get category-specific theme colors for expressive appearance
124 auto cat_theme = PanelManager::GetCategoryTheme(cat);
125 ImVec4 cat_color(cat_theme.r, cat_theme.g, cat_theme.b, cat_theme.a);
126 ImVec4 glow_color(cat_theme.glow_r, cat_theme.glow_g, cat_theme.glow_b,
127 1.0f);
128
129 // Active Indicator with category-specific colors
130 if (is_selected && category_enabled && panel_expanded) {
131 ImVec2 pos = ImGui::GetCursorScreenPos();
132
133 // Outer glow shadow (subtle, category color at 15% opacity)
134 ImVec4 outer_glow = glow_color;
135 outer_glow.w = 0.15f;
136 ImGui::GetWindowDrawList()->AddRectFilled(
137 ImVec2(pos.x - 1.0f, pos.y - 1.0f),
138 ImVec2(pos.x + 49.0f, pos.y + 41.0f),
139 ImGui::ColorConvertFloat4ToU32(outer_glow), 4.0f);
140
141 // Background highlight (category glow at 30% opacity)
142 ImVec4 highlight = glow_color;
143 highlight.w = 0.30f;
144 ImGui::GetWindowDrawList()->AddRectFilled(
145 pos, ImVec2(pos.x + 48.0f, pos.y + 40.0f),
146 ImGui::ColorConvertFloat4ToU32(highlight), 2.0f);
147
148 // Left accent border (4px wide, category-specific color)
149 ImGui::GetWindowDrawList()->AddRectFilled(
150 pos, ImVec2(pos.x + 4.0f, pos.y + 40.0f),
151 ImGui::ColorConvertFloat4ToU32(cat_color));
152 }
153
154 std::string icon = PanelManager::GetCategoryIcon(cat);
155
156 // Subtle indicator even when collapsed
157 if (is_selected && category_enabled && !panel_expanded) {
158 ImVec2 pos = ImGui::GetCursorScreenPos();
159 ImVec4 highlight = glow_color;
160 highlight.w = 0.15f;
161 ImGui::GetWindowDrawList()->AddRectFilled(
162 pos, ImVec2(pos.x + 48.0f, pos.y + 40.0f),
163 ImGui::ColorConvertFloat4ToU32(highlight), 2.0f);
164 ImVec4 accent = cat_color;
165 accent.w = 0.6f;
166 ImGui::GetWindowDrawList()->AddRectFilled(
167 pos, ImVec2(pos.x + 3.0f, pos.y + 40.0f),
168 ImGui::ColorConvertFloat4ToU32(accent));
169 }
170
171 // Always pass category color so inactive icons remain visible
172 ImVec4 icon_color = cat_color;
173 if (!category_enabled) {
174 ImGui::BeginDisabled();
175 }
177 nullptr, is_selected, icon_color,
178 "activity_bar", cat.c_str())) {
179 if (category_enabled) {
180 if (cat == active_category && panel_expanded) {
182 } else {
185 // Notify that a category was selected (dismisses dashboard)
187 }
188 }
189 }
190 if (!category_enabled) {
191 ImGui::EndDisabled();
192 }
193
194 // Tooltip with status information
195 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
196 ImGui::BeginTooltip();
197 ImGui::Text("%s %s", icon.c_str(), cat.c_str());
198 if (!category_enabled) {
199 gui::ColoredText("Open ROM required",
200 gui::ConvertColorToImVec4(theme.warning));
201 } else if (has_active_editor) {
202 gui::ColoredText("Editor open",
203 gui::ConvertColorToImVec4(theme.success));
204 } else {
205 gui::ColoredText("Click to view panels", gui::GetTextSecondaryVec4());
206 }
207 ImGui::EndTooltip();
208 }
209 }
210 }
211
212 // Draw "More Actions" button at the bottom
213 ImGui::SetCursorPosY(viewport_height - 48.0f);
214
216 nullptr, false, ImVec4(0, 0, 0, 0),
217 "activity_bar", "more_actions")) {
218 ImGui::OpenPopup("ActivityBarMoreMenu");
219 }
220
221 if (ImGui::BeginPopup("ActivityBarMoreMenu")) {
222 if (ImGui::MenuItem(ICON_MD_TERMINAL " Command Palette")) {
224 }
225 if (ImGui::MenuItem(ICON_MD_KEYBOARD " Keyboard Shortcuts")) {
227 }
228 ImGui::Separator();
229 if (ImGui::MenuItem(ICON_MD_FOLDER_OPEN " Open ROM / Project")) {
231 }
232 if (ImGui::MenuItem(ICON_MD_SETTINGS " Settings")) {
234 }
235 ImGui::Separator();
236 if (ImGui::MenuItem("Reset Layout")) {
238 }
239 ImGui::EndPopup();
240 }
241 // FixedPanel destructor handles End() + PopStyleVar/PopStyleColor
242}
243
244void ActivityBar::DrawSidePanel(size_t session_id, const std::string& category,
245 std::function<bool()> has_rom) {
246
247 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
248 const ImGuiViewport* viewport = ImGui::GetMainViewport();
249 const float bar_width = PanelManager::GetSidebarWidth();
250 const float panel_width =
251 panel_manager_.GetActiveSidePanelWidth(viewport->WorkSize.x);
252
253 const float top_inset = gui::LayoutHelpers::GetTopInset();
254 const auto safe_area = gui::LayoutHelpers::GetSafeAreaInsets();
255 const float panel_height =
256 std::max(0.0f, viewport->WorkSize.y - top_inset - safe_area.bottom);
257
258 gui::FixedPanel panel(
259 "##SidePanel",
260 ImVec2(viewport->WorkPos.x + bar_width, viewport->WorkPos.y + top_inset),
261 ImVec2(panel_width, panel_height),
262 {.bg = gui::ConvertColorToImVec4(theme.surface),
263 .padding = {8.0f, 8.0f},
264 .border_size = 1.0f,
265 .rounding = 0.0f},
266 ImGuiWindowFlags_NoFocusOnAppearing);
267
268 if (panel) {
269 // Header
270 ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]); // Use default font
271 ImGui::Text("%s", category.c_str());
272 ImGui::PopFont();
273
274 // Collapse button (right-aligned)
275 float avail_width = ImGui::GetContentRegionAvail().x;
276 const ImVec2 chrome_icon_size = gui::IconSize::Small();
277 ImGui::SameLine(ImGui::GetCursorPosX() + avail_width - chrome_icon_size.x);
279 "Collapse Panel", false, ImVec4(0, 0, 0, 0),
280 "activity_bar", "collapse_side_panel")) {
282 }
283
284 ImGui::Separator();
285 ImGui::Spacing();
286
287 // Search Bar
288 static char sidebar_search[256] = "";
289 const ImVec2 search_button_size = gui::IconSize::Small();
290 const float standard_spacing =
291 std::clamp(gui::LayoutHelpers::GetStandardSpacing(), 4.0f, 8.0f);
292 const float compact_spacing = std::max(4.0f, standard_spacing * 0.75f);
293 const float search_spacing = compact_spacing;
294 const float search_width =
295 std::max(140.0f, ImGui::GetContentRegionAvail().x -
296 search_button_size.x - search_spacing);
297 ImGui::SetNextItemWidth(search_width);
298 ImGui::InputTextWithHint("##SidebarSearch", ICON_MD_SEARCH " Filter...",
299 sidebar_search, sizeof(sidebar_search));
300 ImGui::SameLine(0.0f, search_spacing);
301 const bool filter_empty = sidebar_search[0] == '\0';
302 if (filter_empty) {
303 ImGui::BeginDisabled();
304 }
305 if (gui::TransparentIconButton(ICON_MD_CLEAR, search_button_size,
306 "Clear filter", false,
307 gui::GetTextSecondaryVec4(), "activity_bar",
308 "clear_sidebar_filter")) {
309 sidebar_search[0] = '\0';
310 }
311 if (filter_empty) {
312 ImGui::EndDisabled();
313 }
314
315 const auto is_dungeon_panel_mode_card =
316 [](const std::string& panel_id) -> bool {
317 const bool is_room_window = panel_id.rfind("dungeon.room_", 0) == 0;
318 return panel_id == "dungeon.room_selector" ||
319 panel_id == "dungeon.room_matrix" || is_room_window;
320 };
321 auto read_dungeon_workbench_mode = [&]() -> bool {
322 if (category != "Dungeon") {
323 return false;
324 }
327 }
328 return panel_manager_.IsPanelVisible(session_id, "dungeon.workbench");
329 };
330 bool dungeon_workbench_mode = read_dungeon_workbench_mode();
331
332 auto switch_to_dungeon_workbench_mode = [&]() -> bool {
333 if (category != "Dungeon") {
334 return false;
335 }
336 if (dungeon_workbench_mode) {
337 return false;
338 }
341 // Workflow switches are applied next frame; reflect requested state now.
342 dungeon_workbench_mode = true;
343 return true;
344 }
345 panel_manager_.ShowPanel(session_id, "dungeon.workbench");
346 for (const auto& descriptor :
347 panel_manager_.GetPanelsInCategory(session_id, "Dungeon")) {
348 const std::string& panel_id = descriptor.card_id;
349 if (panel_id == "dungeon.workbench") {
350 continue;
351 }
352 if (panel_manager_.IsPanelPinned(session_id, panel_id)) {
353 continue;
354 }
355 if (is_dungeon_panel_mode_card(panel_id)) {
356 panel_manager_.HidePanel(session_id, panel_id);
357 }
358 }
359 dungeon_workbench_mode = read_dungeon_workbench_mode();
360 return dungeon_workbench_mode;
361 };
362
363 auto switch_to_dungeon_panel_mode = [&]() -> bool {
364 if (category != "Dungeon") {
365 return false;
366 }
367 if (!dungeon_workbench_mode) {
368 return false;
369 }
372 // Workflow switches are applied next frame; reflect requested state now.
373 dungeon_workbench_mode = false;
374 return true;
375 }
376 panel_manager_.HidePanel(session_id, "dungeon.workbench");
377 panel_manager_.ShowPanel(session_id, "dungeon.room_selector");
378 panel_manager_.ShowPanel(session_id, "dungeon.room_matrix");
379 dungeon_workbench_mode = read_dungeon_workbench_mode();
380 return !dungeon_workbench_mode;
381 };
382
383 auto ensure_dungeon_panel_mode_for_card =
384 [&](const std::string& panel_id) -> bool {
385 if (category != "Dungeon" || !dungeon_workbench_mode ||
386 !is_dungeon_panel_mode_card(panel_id)) {
387 return false;
388 }
389 return switch_to_dungeon_panel_mode();
390 };
391
392 if (category == "Dungeon") {
393 ImGui::Spacing();
394 ImGui::TextDisabled(ICON_MD_WORKSPACES " Workflow");
395 ImGui::Spacing();
396
397 const float workflow_gap = compact_spacing;
398 const float workflow_min_button_width = 96.0f;
399 const float workflow_available_width =
400 std::max(1.0f, ImGui::GetContentRegionAvail().x);
401 const bool stack_workflow_buttons =
402 workflow_available_width <=
403 (workflow_min_button_width * 2.0f + workflow_gap);
404 const float workflow_button_width =
405 stack_workflow_buttons
406 ? workflow_available_width
407 : std::max(workflow_min_button_width,
408 (workflow_available_width - workflow_gap) * 0.5f);
409 const float workflow_button_height =
411 auto draw_workflow_button =
412 [&](const char* id, const char* icon, const char* label, bool active,
413 const char* tooltip, const ImVec2& button_size) -> bool {
414 const ImVec4 active_bg = gui::GetPrimaryVec4();
415 const ImVec4 inactive_bg = gui::GetSurfaceContainerHighVec4();
416 const ImVec4 inactive_hover = gui::GetSurfaceContainerHighestVec4();
418 {{ImGuiCol_Button, active ? active_bg : inactive_bg},
419 {ImGuiCol_ButtonHovered, active ? active_bg : inactive_hover},
420 {ImGuiCol_ButtonActive, active ? active_bg : inactive_hover}});
421 const std::string button_label =
422 absl::StrFormat("%s %s##%s", icon, label, id);
423 const bool clicked = ImGui::Button(button_label.c_str(), button_size);
424 if (ImGui::IsItemHovered() && tooltip && *tooltip) {
425 ImGui::SetTooltip("%s", tooltip);
426 }
427 return clicked;
428 };
429 const ImVec2 workflow_button_size(workflow_button_width,
430 workflow_button_height);
431
432 if (draw_workflow_button(
433 "workflow_workbench", ICON_MD_WORKSPACES, "Workbench",
434 dungeon_workbench_mode,
435 "Workbench mode: integrated room browser + inspector",
436 workflow_button_size)) {
437 switch_to_dungeon_workbench_mode();
438 }
439 if (!stack_workflow_buttons) {
440 ImGui::SameLine(0.0f, workflow_gap);
441 }
442 if (draw_workflow_button(
443 "workflow_panels", ICON_MD_VIEW_QUILT, "Panels",
444 !dungeon_workbench_mode,
445 "Panel mode: standalone Room List + Room Matrix + room windows",
446 workflow_button_size)) {
447 switch_to_dungeon_panel_mode();
448 }
449 ImGui::Spacing();
450 ImGui::Separator();
451 ImGui::Spacing();
452 }
453
454 ImGui::Spacing();
455
456 // Disable non-emulator categories when no ROM is loaded
457 const bool rom_loaded = has_rom ? has_rom() : true;
458 const bool disable_cards = !rom_loaded && category != "Emulator";
459
460 const auto category_cards =
461 panel_manager_.GetPanelsInCategory(session_id, category);
462 int visible_cards_in_category = 0;
463 for (const auto& category_card : category_cards) {
464 if (category_card.visibility_flag && *category_card.visibility_flag) {
465 ++visible_cards_in_category;
466 }
467 }
468
469 ImGui::TextDisabled("%d / %d visible", visible_cards_in_category,
470 static_cast<int>(category_cards.size()));
471 ImGui::Spacing();
472
473 const float action_gap = compact_spacing;
474 const float action_min_button_width = 84.0f;
475 const float action_available_width =
476 std::max(1.0f, ImGui::GetContentRegionAvail().x);
477 const bool stack_action_buttons =
478 action_available_width <=
479 (action_min_button_width * 3.0f + (action_gap * 2.0f));
480 const float action_button_width =
481 stack_action_buttons
482 ? action_available_width
483 : std::max(action_min_button_width,
484 (action_available_width - (action_gap * 2.0f)) / 3.0f);
485 const float action_button_height =
487 auto draw_action_button = [&](const char* id, const char* icon,
488 const char* label, const char* tooltip,
489 const ImVec2& button_size) -> bool {
490 const std::string button_label =
491 absl::StrFormat("%s %s##%s", icon, label, id);
492 const bool clicked = ImGui::Button(button_label.c_str(), button_size);
493 if (ImGui::IsItemHovered() && tooltip && *tooltip) {
494 ImGui::SetTooltip("%s", tooltip);
495 }
496 return clicked;
497 };
498 const ImVec2 action_button_size(action_button_width, action_button_height);
499
500 if (draw_action_button("open_panel_browser", ICON_MD_APPS, "Browser",
501 "Open Panel Browser", action_button_size)) {
503 }
504 if (!stack_action_buttons) {
505 ImGui::SameLine(0.0f, action_gap);
506 }
507
508 const bool bulk_actions_enabled =
509 !disable_cards && !(category == "Dungeon" && dungeon_workbench_mode);
510 bool bulk_action_hovered = false;
511 if (!bulk_actions_enabled) {
512 ImGui::BeginDisabled();
513 }
514 if (draw_action_button("show_category_panels", ICON_MD_VISIBILITY, "Show",
515 "Show all panels in this category",
516 action_button_size)) {
517 panel_manager_.ShowAllPanelsInCategory(session_id, category);
518 }
519 if (!stack_action_buttons) {
520 ImGui::SameLine(0.0f, action_gap);
521 }
522 if (!bulk_actions_enabled &&
523 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
524 bulk_action_hovered = true;
525 }
526 if (draw_action_button("hide_category_panels", ICON_MD_VISIBILITY_OFF,
527 "Hide", "Hide all panels in this category",
528 action_button_size)) {
529 panel_manager_.HideAllPanelsInCategory(session_id, category);
530 }
531 if (!bulk_actions_enabled) {
532 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
533 bulk_action_hovered = true;
534 }
535 if (bulk_action_hovered && category == "Dungeon" &&
536 dungeon_workbench_mode) {
537 ImGui::SetTooltip(
538 "Switch to Panel workflow to bulk-manage room panels.");
539 }
540 ImGui::EndDisabled();
541 }
542
543 ImGui::Spacing();
544
545 if (disable_cards) {
546 ImGui::TextUnformatted(ICON_MD_FOLDER_OPEN
547 " Open a ROM to enable this category");
548 ImGui::Spacing();
549 }
550
551 if (disable_cards) {
552 ImGui::BeginDisabled();
553 }
554
555 // Get pinned panels
556 const auto pinned_cards = panel_manager_.GetPinnedPanels();
557 const ImVec4 disabled_text =
558 ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled);
559 auto panel_text_color = [&](bool visible) -> ImVec4 {
560 if (disable_cards) {
561 return disabled_text;
562 }
564 };
565 const float pin_button_side =
567 const ImVec2 pin_button_size(pin_button_side, pin_button_side);
568 auto draw_pin_toggle_button = [&](const std::string& widget_id,
569 bool pinned) -> bool {
570 ImGui::PushID(widget_id.c_str());
571 // Use secondary text color for unpinned (more visible than text_disabled)
572 const ImVec4 pin_col = pinned ? gui::ConvertColorToImVec4(theme.primary)
574 const bool clicked = gui::TransparentIconButton(
575 pinned ? ICON_MD_PUSH_PIN : ICON_MD_PIN, pin_button_size,
576 pinned ? "Unpin panel" : "Pin panel", pinned, pin_col, "activity_bar",
577 widget_id.c_str());
578 ImGui::PopID();
579 return clicked;
580 };
581
582 // --- Pinned Section (panels that persist across editors) ---
583 bool pinned_section_open = false;
584 if (sidebar_search[0] == '\0' && !pinned_cards.empty()) {
585 bool has_pinned_in_category = false;
586 for (const auto& card_id : pinned_cards) {
587 const auto* card =
588 panel_manager_.GetPanelDescriptor(session_id, card_id);
589 if (card && card->category == category) {
590 has_pinned_in_category = true;
591 break;
592 }
593 }
594
595 if (has_pinned_in_category) {
596 pinned_section_open = ImGui::CollapsingHeader(
597 ICON_MD_PUSH_PIN " Pinned", ImGuiTreeNodeFlags_DefaultOpen);
598 if (pinned_section_open) {
599 for (const auto& card_id : pinned_cards) {
600 const auto* card =
601 panel_manager_.GetPanelDescriptor(session_id, card_id);
602 if (!card || card->category != category)
603 continue;
604
605 bool visible =
606 card->visibility_flag ? *card->visibility_flag : false;
607
608 // Unpin button
609 if (draw_pin_toggle_button("pin_" + card->card_id, true)) {
610 panel_manager_.SetPanelPinned(session_id, card->card_id, false);
611 }
612
613 ImGui::SameLine(0.0f, compact_spacing);
614
615 // Panel Item
616 std::string label = absl::StrFormat("%s %s", card->icon.c_str(),
617 card->display_name.c_str());
618 ImGui::PushID(
619 (std::string("pinned_select_") + card->card_id).c_str());
620 {
621 gui::StyleColorGuard text_color(ImGuiCol_Text,
622 panel_text_color(visible));
623 ImVec2 item_size(ImGui::GetContentRegionAvail().x,
624 pin_button_size.y);
625 if (ImGui::Selectable(label.c_str(), visible,
626 ImGuiSelectableFlags_None, item_size)) {
627 const bool switched_mode =
628 ensure_dungeon_panel_mode_for_card(card->card_id);
629 if (switched_mode) {
630 // Keep requested panel visible after implicit mode switch.
631 panel_manager_.ShowPanel(session_id, card->card_id);
632 } else {
633 panel_manager_.TogglePanel(session_id, card->card_id);
634 }
635
636 bool new_visible =
637 card->visibility_flag ? *card->visibility_flag : false;
638 if (new_visible) {
639 panel_manager_.TriggerPanelClicked(card->category);
640 const std::string window_name =
642 if (!window_name.empty()) {
643 ImGui::SetWindowFocus(window_name.c_str());
644 }
645 }
646 }
647 }
648 ImGui::PopID();
649 }
650 ImGui::Spacing();
651 ImGui::Separator();
652 ImGui::Spacing();
653 }
654 }
655 }
656
657 // Content - panels sorted by MRU (pinned first, then most recently used)
658 auto cards = panel_manager_.GetPanelsSortedByMRU(session_id, category);
659
660 // Panels section (minimum height so list never collapses)
661 const bool panel_content_open = gui::LayoutHelpers::BeginContentChild(
662 "##PanelContent", ImVec2(0.0f, gui::UIConfig::kContentMinHeightList),
663 false, ImGuiWindowFlags_None);
664 if (panel_content_open) {
665 for (const auto& card : cards) {
666 // Apply search filter
667 if (sidebar_search[0] != '\0') {
668 std::string search_str = sidebar_search;
669 std::string card_name = card.display_name;
670 std::transform(search_str.begin(), search_str.end(),
671 search_str.begin(), ::tolower);
672 std::transform(card_name.begin(), card_name.end(), card_name.begin(),
673 ::tolower);
674 if (card_name.find(search_str) == std::string::npos) {
675 continue;
676 }
677 }
678
679 const bool is_pinned = panel_manager_.IsPanelPinned(card.card_id);
680 // Avoid duplicate entries when pinned panels are already listed above.
681 if (pinned_section_open && is_pinned) {
682 continue;
683 }
684
685 bool visible = card.visibility_flag ? *card.visibility_flag : false;
686
687 // Pin toggle button
688 if (draw_pin_toggle_button("pin_" + card.card_id, is_pinned)) {
689 panel_manager_.SetPanelPinned(session_id, card.card_id, !is_pinned);
690 }
691 ImGui::SameLine(0.0f, compact_spacing);
692
693 // Panel Item with Icon
694 std::string label = absl::StrFormat("%s %s", card.icon.c_str(),
695 card.display_name.c_str());
696 ImGui::PushID((std::string("panel_select_") + card.card_id).c_str());
697 {
698 gui::StyleColorGuard text_color(ImGuiCol_Text,
699 panel_text_color(visible));
700 ImVec2 item_size(ImGui::GetContentRegionAvail().x, pin_button_size.y);
701 if (ImGui::Selectable(label.c_str(), visible,
702 ImGuiSelectableFlags_None, item_size)) {
703 const bool switched_mode =
704 ensure_dungeon_panel_mode_for_card(card.card_id);
705 if (switched_mode) {
706 // Keep requested panel visible after implicit mode switch.
707 panel_manager_.ShowPanel(session_id, card.card_id);
708 } else {
709 panel_manager_.TogglePanel(session_id, card.card_id);
710 }
711
712 bool new_visible =
713 card.visibility_flag ? *card.visibility_flag : false;
714 if (new_visible) {
716 panel_manager_.TriggerPanelClicked(card.category);
717 const std::string window_name =
719 if (!window_name.empty()) {
720 ImGui::SetWindowFocus(window_name.c_str());
721 }
722 }
723 }
724 }
725 ImGui::PopID();
726
727 // Shortcut hint on hover
728 if (ImGui::IsItemHovered() && !card.shortcut_hint.empty()) {
729 ImGui::SetTooltip("%s", card.shortcut_hint.c_str());
730 }
731 }
732 }
734
735 if (disable_cards) {
736 ImGui::EndDisabled();
737 }
738
739 // VSCode-style drag resize handle on the right edge of the side panel.
740 const float handle_width = 6.0f;
741 const ImVec2 panel_pos = ImGui::GetWindowPos();
742 const float panel_draw_height = ImGui::GetWindowHeight();
743 ImGui::SetCursorScreenPos(
744 ImVec2(panel_pos.x + panel_width - handle_width * 0.5f, panel_pos.y));
745 ImGui::InvisibleButton("##SidePanelResizeHandle",
746 ImVec2(handle_width, panel_draw_height));
747 const bool handle_hovered = ImGui::IsItemHovered();
748 const bool handle_active = ImGui::IsItemActive();
749 if (handle_hovered || handle_active) {
750 ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
751 }
752 if (handle_hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
754 }
755 if (handle_active) {
756 const float new_width = panel_width + ImGui::GetIO().MouseDelta.x;
757 panel_manager_.SetActiveSidePanelWidth(new_width, viewport->WorkSize.x);
758 ImGui::SetTooltip(
759 "Width: %.0f px",
760 panel_manager_.GetActiveSidePanelWidth(viewport->WorkSize.x));
761 }
762
763 ImVec4 handle_color = gui::GetOutlineVec4();
764 handle_color.w = handle_active ? 0.95f : (handle_hovered ? 0.72f : 0.35f);
765 ImGui::GetWindowDrawList()->AddLine(
766 ImVec2(panel_pos.x + panel_width - 1.0f, panel_pos.y),
767 ImVec2(panel_pos.x + panel_width - 1.0f,
768 panel_pos.y + panel_draw_height),
769 ImGui::GetColorU32(handle_color), handle_active ? 2.0f : 1.0f);
770 }
771 // FixedPanel destructor handles End() + PopStyleVar/PopStyleColor
772}
773
774void ActivityBar::DrawPanelBrowser(size_t session_id, bool* p_open) {
775 const ImGuiViewport* viewport = ImGui::GetMainViewport();
776 ImVec2 max_window_size(1600.0f, 1000.0f);
777 ImVec2 default_window_size(1080.0f, 700.0f);
778 if (viewport) {
779 max_window_size = ImVec2(std::max(760.0f, viewport->WorkSize.x * 0.95f),
780 std::max(520.0f, viewport->WorkSize.y * 0.95f));
781 default_window_size = ImVec2(
782 std::clamp(viewport->WorkSize.x * 0.72f, 880.0f, max_window_size.x),
783 std::clamp(viewport->WorkSize.y * 0.76f, 600.0f, max_window_size.y));
784 const ImVec2 center(viewport->WorkPos.x + viewport->WorkSize.x * 0.5f,
785 viewport->WorkPos.y + viewport->WorkSize.y * 0.5f);
786 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
787 }
788
789 ImGui::SetNextWindowSize(default_window_size, ImGuiCond_Appearing);
790 ImGui::SetNextWindowSizeConstraints(ImVec2(780, 520), max_window_size);
791
792 if (ImGui::Begin(
793 absl::StrFormat("%s Panel Browser", ICON_MD_DASHBOARD).c_str(),
794 p_open, ImGuiWindowFlags_NoDocking)) {
795 static char search_filter[256] = "";
796 static std::string category_filter = "All";
797
798 std::vector<std::string> categories =
800 std::sort(categories.begin(), categories.end());
801 if (category_filter != "All" &&
802 std::find(categories.begin(), categories.end(), category_filter) ==
803 categories.end()) {
804 category_filter = "All";
805 }
806
807 const auto all_cards = panel_manager_.GetPanelsInSession(session_id);
808 auto count_cards = [&](const std::string& category,
809 bool visible_only) -> int {
810 int count = 0;
811 for (const auto& card_id : all_cards) {
812 const auto* card =
813 panel_manager_.GetPanelDescriptor(session_id, card_id);
814 if (!card) {
815 continue;
816 }
817 if (category != "All" && card->category != category) {
818 continue;
819 }
820 const bool visible = card->visibility_flag && *card->visibility_flag;
821 if (!visible_only || visible) {
822 ++count;
823 }
824 }
825 return count;
826 };
827
828 ImGui::SetNextItemWidth(
829 std::max(280.0f, ImGui::GetContentRegionAvail().x * 0.45f));
830 ImGui::InputTextWithHint(
831 "##Search",
832 absl::StrFormat("%s Search panels...", ICON_MD_SEARCH).c_str(),
833 search_filter, sizeof(search_filter));
834 ImGui::SameLine();
835 if (ImGui::Button(ICON_MD_CLEAR " Clear")) {
836 search_filter[0] = '\0';
837 }
838 ImGui::SameLine();
839 if (category_filter == "All") {
840 if (ImGui::Button(ICON_MD_VISIBILITY " Show All")) {
842 }
843 ImGui::SameLine();
844 if (ImGui::Button(ICON_MD_VISIBILITY_OFF " Hide All")) {
846 }
847 } else {
848 if (ImGui::Button(ICON_MD_VISIBILITY " Show Category")) {
849 panel_manager_.ShowAllPanelsInCategory(session_id, category_filter);
850 }
851 ImGui::SameLine();
852 if (ImGui::Button(ICON_MD_VISIBILITY_OFF " Hide Category")) {
853 panel_manager_.HideAllPanelsInCategory(session_id, category_filter);
854 }
855 }
856
857 ImGui::Separator();
858
859 const float content_height = ImGui::GetContentRegionAvail().y;
860 const float max_sidebar_width =
861 std::max(220.0f, ImGui::GetContentRegionAvail().x * 0.50f);
862 float category_sidebar_width =
863 std::clamp(panel_manager_.GetPanelBrowserCategoryWidth(), 220.0f,
864 max_sidebar_width);
865
866 if (ImGui::BeginChild("##PanelBrowserCategories",
867 ImVec2(category_sidebar_width, content_height),
868 true)) {
869 const int visible_total = count_cards("All", true);
870 std::string all_label =
871 absl::StrFormat("%s All Panels (%d/%d)", ICON_MD_DASHBOARD,
872 visible_total, static_cast<int>(all_cards.size()));
873 if (ImGui::Selectable(all_label.c_str(), category_filter == "All")) {
874 category_filter = "All";
875 }
876 ImGui::Separator();
877
878 for (const auto& cat : categories) {
879 const int category_total = count_cards(cat, false);
880 if (category_total <= 0) {
881 continue;
882 }
883 const int visible_in_category = count_cards(cat, true);
884 const std::string icon = PanelManager::GetCategoryIcon(cat);
885 std::string label =
886 absl::StrFormat("%s %s (%d/%d)", icon.c_str(), cat.c_str(),
887 visible_in_category, category_total);
888 if (ImGui::Selectable(label.c_str(), category_filter == cat)) {
889 category_filter = cat;
890 }
891 }
892 }
893 ImGui::EndChild();
894
895 ImGui::SameLine(0.0f, 0.0f);
896
897 const float splitter_width = 6.0f;
898 const ImVec2 splitter_pos = ImGui::GetCursorScreenPos();
899 ImGui::InvisibleButton("##PanelBrowserSplitter",
900 ImVec2(splitter_width, content_height));
901 const bool splitter_hovered = ImGui::IsItemHovered();
902 const bool splitter_active = ImGui::IsItemActive();
903 if (splitter_hovered || splitter_active) {
904 ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
905 }
906 if (splitter_hovered &&
907 ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
910 category_sidebar_width =
911 std::clamp(panel_manager_.GetPanelBrowserCategoryWidth(), 220.0f,
912 max_sidebar_width);
913 }
914 if (splitter_active) {
915 category_sidebar_width =
916 std::clamp(category_sidebar_width + ImGui::GetIO().MouseDelta.x,
917 220.0f, max_sidebar_width);
918 panel_manager_.SetPanelBrowserCategoryWidth(category_sidebar_width);
919 ImGui::SetTooltip("Width: %.0f px", category_sidebar_width);
920 }
921
922 ImVec4 splitter_color = gui::GetOutlineVec4();
923 splitter_color.w =
924 splitter_active ? 0.95f : (splitter_hovered ? 0.72f : 0.35f);
925 ImGui::GetWindowDrawList()->AddLine(
926 ImVec2(splitter_pos.x + splitter_width * 0.5f, splitter_pos.y),
927 ImVec2(splitter_pos.x + splitter_width * 0.5f,
928 splitter_pos.y + content_height),
929 ImGui::GetColorU32(splitter_color), splitter_active ? 2.0f : 1.0f);
930
931 ImGui::SameLine(0.0f, 0.0f);
932
933 if (ImGui::BeginChild("##PanelBrowserTable", ImVec2(0, content_height),
934 false)) {
935 if (ImGui::BeginTable("##PanelTable", 5,
936 ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg |
937 ImGuiTableFlags_Borders |
938 ImGuiTableFlags_Resizable)) {
939 ImGui::TableSetupColumn("Visible", ImGuiTableColumnFlags_WidthFixed,
940 60);
941 ImGui::TableSetupColumn("Pin", ImGuiTableColumnFlags_WidthFixed, 36);
942 ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
943 ImGui::TableSetupColumn("Category", ImGuiTableColumnFlags_WidthFixed,
944 130);
945 ImGui::TableSetupColumn("Shortcut", ImGuiTableColumnFlags_WidthFixed,
946 110);
947 ImGui::TableHeadersRow();
948
949 auto cards =
950 (category_filter == "All") ? all_cards : std::vector<std::string>{};
951 if (category_filter != "All") {
952 auto category_cards =
953 panel_manager_.GetPanelsInCategory(session_id, category_filter);
954 cards.reserve(category_cards.size());
955 for (const auto& card : category_cards) {
956 cards.push_back(card.card_id);
957 }
958 }
959
960 for (const auto& card_id : cards) {
961 const auto* card =
962 panel_manager_.GetPanelDescriptor(session_id, card_id);
963 if (!card) {
964 continue;
965 }
966
967 std::string search_str = search_filter;
968 if (!search_str.empty()) {
969 std::string card_lower = card->display_name;
970 std::transform(card_lower.begin(), card_lower.end(),
971 card_lower.begin(), [](unsigned char c) {
972 return static_cast<char>(std::tolower(c));
973 });
974 std::transform(search_str.begin(), search_str.end(),
975 search_str.begin(), [](unsigned char c) {
976 return static_cast<char>(std::tolower(c));
977 });
978 if (card_lower.find(search_str) == std::string::npos) {
979 continue;
980 }
981 }
982
983 ImGui::TableNextRow();
984
985 ImGui::TableNextColumn();
986 if (card->visibility_flag) {
987 bool visible = *card->visibility_flag;
988 if (ImGui::Checkbox(
989 absl::StrFormat("##vis_%s", card->card_id.c_str()).c_str(),
990 &visible)) {
991 panel_manager_.TogglePanel(session_id, card->card_id);
992 }
993 }
994
995 ImGui::TableNextColumn();
996 const bool is_pinned = panel_manager_.IsPanelPinned(card->card_id);
997 const ImVec4 pin_color =
999 const float pin_side =
1001 ImGui::PushID(
1002 absl::StrFormat("browser_pin_%s", card->card_id).c_str());
1004 is_pinned ? ICON_MD_PUSH_PIN : ICON_MD_PIN,
1005 ImVec2(pin_side, pin_side),
1006 is_pinned ? "Unpin panel" : "Pin panel", is_pinned, pin_color,
1007 "panel_browser", card->card_id.c_str())) {
1008 panel_manager_.SetPanelPinned(session_id, card->card_id,
1009 !is_pinned);
1010 }
1011 ImGui::PopID();
1012
1013 ImGui::TableNextColumn();
1014 ImGui::Text("%s %s", card->icon.c_str(), card->display_name.c_str());
1015 if (ImGui::IsItemHovered() &&
1016 ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
1017 panel_manager_.ShowPanel(session_id, card->card_id);
1018 const std::string window_name =
1020 if (!window_name.empty()) {
1021 ImGui::SetWindowFocus(window_name.c_str());
1022 }
1023 }
1024
1025 ImGui::TableNextColumn();
1026 ImGui::TextUnformatted(card->category.c_str());
1027
1028 ImGui::TableNextColumn();
1029 if (card->shortcut_hint.empty()) {
1030 ImGui::TextDisabled("-");
1031 } else {
1032 ImGui::TextDisabled("%s", card->shortcut_hint.c_str());
1033 }
1034 }
1035
1036 ImGui::EndTable();
1037 }
1038 }
1039 ImGui::EndChild();
1040 }
1041 ImGui::End();
1042}
1043
1044} // namespace editor
1045} // namespace yaze
void DrawPanelBrowser(size_t session_id, bool *p_open)
void DrawSidePanel(size_t session_id, const std::string &category, std::function< bool()> has_rom)
std::function< void(bool)> set_dungeon_workflow_mode_
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)
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_dungeon_workbench_mode_
PanelManager & panel_manager_
ActivityBar(PanelManager &panel_manager, std::function< bool()> is_dungeon_workbench_mode={}, std::function< void(bool)> set_dungeon_workflow_mode={})
Central registry for all editor cards with session awareness and dependency injection.
float GetPanelBrowserCategoryWidth() const
std::vector< std::string > GetAllCategories(size_t session_id) const
void SetPanelExpanded(bool expanded, bool notify=true)
void TriggerCategorySelected(const std::string &category)
std::vector< std::string > GetPinnedPanels(size_t session_id) const
bool TogglePanel(size_t session_id, const std::string &base_card_id)
const PanelDescriptor * GetPanelDescriptor(size_t session_id, const std::string &base_card_id) const
void TriggerPanelClicked(const std::string &category)
void SetActiveSidePanelWidth(float width, float viewport_width=0.0f, bool notify=true)
void ResetSidePanelWidth(bool notify=true)
std::vector< std::string > GetPanelsInSession(size_t session_id) const
void SetActiveCategory(const std::string &category, bool notify=true)
void HideAllPanelsInSession(size_t session_id)
std::vector< PanelDescriptor > GetPanelsSortedByMRU(size_t session_id, const std::string &category) const
Get panels in category sorted by: pinned first, then MRU.
std::vector< PanelDescriptor > GetPanelsInCategory(size_t session_id, const std::string &category) const
bool ShowPanel(size_t session_id, const std::string &base_card_id)
static CategoryTheme GetCategoryTheme(const std::string &category)
bool IsPanelVisible(size_t session_id, const std::string &base_card_id) const
static constexpr float GetSidebarWidth()
void SetPanelBrowserCategoryWidth(float width, bool notify=true)
std::string GetPanelWindowName(size_t session_id, const std::string &base_card_id) const
Resolve the exact ImGui window name for a panel by base ID.
void ShowAllPanelsInCategory(size_t session_id, const std::string &category)
void MarkPanelRecentlyUsed(const std::string &card_id)
Record that a panel was used (for MRU ordering in sidebar)
static std::string GetCategoryIcon(const std::string &category)
static constexpr const char * kDashboardCategory
float GetActiveSidePanelWidth(float viewport_width) const
static constexpr float GetDefaultPanelBrowserCategoryWidth()
bool IsPanelPinned(size_t session_id, const std::string &base_card_id) const
bool HidePanel(size_t session_id, const std::string &base_card_id)
void SetPanelPinned(size_t session_id, const std::string &base_card_id, bool pinned)
void HideAllPanelsInCategory(size_t session_id, const std::string &category)
void ShowAllPanelsInSession(size_t session_id)
RAII for fixed-position panels (activity bar, side panel, status bar).
static bool BeginContentChild(const char *id, const ImVec2 &min_size, bool border=false, ImGuiWindowFlags flags=0)
static void EndContentChild()
static SafeAreaInsets GetSafeAreaInsets()
static float GetStandardWidgetHeight()
static float GetStandardSpacing()
RAII guard for ImGui style colors.
Definition style_guard.h:27
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_SETTINGS
Definition icons.h:1699
#define ICON_MD_VIEW_QUILT
Definition icons.h:2094
#define ICON_MD_APPS
Definition icons.h:168
#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_VISIBILITY_OFF
Definition icons.h:2102
#define ICON_MD_KEYBOARD
Definition icons.h:1028
#define ICON_MD_CHEVRON_LEFT
Definition icons.h:405
#define ICON_MD_TERMINAL
Definition icons.h:1951
#define ICON_MD_PIN
Definition icons.h:1470
#define ICON_MD_CLEAR
Definition icons.h:416
#define ICON_MD_DASHBOARD
Definition icons.h:517
#define ICON_MD_PUSH_PIN
Definition icons.h:1529
#define ICON_MD_WORKSPACES
Definition icons.h:2186
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:134
void ColoredText(const char *text, const ImVec4 &color)
ImVec4 GetSurfaceContainerHighestVec4()
ImVec4 GetPrimaryVec4()
ImVec4 GetTextDisabledVec4()
ImVec4 GetTextSecondaryVec4()
ImVec4 GetSurfaceContainerHighVec4()
ImVec4 GetOutlineVec4()
ImVec4 GetOnSurfaceVec4()
static constexpr float kActivityBarWidth
Definition ui_config.h:18
static constexpr float kContentMinHeightList
Definition ui_config.h:54