yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
ui_coordinator.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <filesystem>
5#include <functional>
6#include <memory>
7#include <string>
8#include <vector>
9
10#include "absl/strings/match.h"
11#include "absl/strings/str_format.h"
12
13#ifdef __EMSCRIPTEN__
14#include <emscripten.h>
15#endif
16#include "app/application.h"
18#include "app/editor/editor.h"
32#include "app/gui/core/icons.h"
33#include "app/gui/core/input.h"
35#include "app/gui/core/style.h"
40#include "core/project.h"
41#include "imgui/imgui.h"
42#include "util/file_util.h"
43#include "util/platform_paths.h"
44
45namespace yaze {
46namespace editor {
47
49 EditorManager* editor_manager, RomFileManager& rom_manager,
50 ProjectManager& project_manager, EditorRegistry& editor_registry,
51 WorkspaceWindowManager& window_manager,
52 SessionCoordinator& session_coordinator, WindowDelegate& window_delegate,
53 ToastManager& toast_manager, PopupManager& popup_manager,
54 ShortcutManager& shortcut_manager)
55 : editor_manager_(editor_manager),
56 rom_manager_(rom_manager),
57 project_manager_(project_manager),
58 editor_registry_(editor_registry),
59 window_manager_(window_manager),
60 session_coordinator_(session_coordinator),
61 window_delegate_(window_delegate),
62 toast_manager_(toast_manager),
63 popup_manager_(popup_manager),
64 shortcut_manager_(shortcut_manager) {
65 // Initialize welcome screen with proper callbacks
66 welcome_screen_ = std::make_unique<WelcomeScreen>();
67
68 // Bind persistent prefs so animation tweaks survive restart.
69 if (editor_manager_) {
70 welcome_screen_->SetUserSettings(&editor_manager_->user_settings());
71 }
72
73 // Wire welcome screen callbacks to EditorManager
74 welcome_screen_->SetOpenRomCallback([this]() {
75#ifdef __EMSCRIPTEN__
76 // In web builds, trigger the file input element directly
77 // The file input handler in app.js will handle the file selection
78 // and call LoadRomFromWeb, which will update the ROM
79 EM_ASM({
80 var romInput = document.getElementById('rom-input');
81 if (romInput) {
82 romInput.click();
83 }
84 });
85 // Don't hide welcome screen yet - it will be hidden when ROM loads
86 // (DrawWelcomeScreen auto-transitions to Dashboard on ROM load)
87#else
88 if (editor_manager_) {
89 auto status = editor_manager_->LoadRom();
90 if (!status.ok()) {
91 toast_manager_.Show(
92 absl::StrFormat("Failed to load ROM: %s", status.message()),
93 ToastType::kError);
94 }
95#if !(defined(__APPLE__) && TARGET_OS_IOS == 1)
96 else {
97 // Transition to Dashboard on successful ROM load
98 SetStartupSurface(StartupSurface::kDashboard);
99 }
100#endif
101 }
102#endif
103 });
104
105 welcome_screen_->SetNewProjectCallback([this]() {
106 if (editor_manager_) {
107 auto status = editor_manager_->CreateNewProject();
108 if (!status.ok()) {
109 toast_manager_.Show(
110 absl::StrFormat("Failed to create project: %s", status.message()),
111 ToastType::kError);
112 } else {
113 // Transition to Dashboard on successful project creation
114 SetStartupSurface(StartupSurface::kDashboard);
115 }
116 }
117 });
118
119 // Template-driven creation now opens the guided dialog instead of chaining
120 // straight into CreateNewProject -> LoadRom. The dialog calls back into
121 // EditorManager once the user has a ROM path and project name picked.
122 welcome_screen_->SetNewProjectWithTemplateCallback(
123 [this](const std::string& template_name) {
124 new_project_dialog_.Open(template_name);
125 });
126
127 new_project_dialog_.SetCreateCallback(
128 [this](const std::string& template_name, const std::string& rom_path,
129 const std::string& project_name) {
130 if (!editor_manager_)
131 return;
132 auto status = editor_manager_->CreateNewProject(template_name);
133 if (!status.ok()) {
134 toast_manager_.Show(
135 absl::StrFormat("Failed to create project: %s", status.message()),
136 ToastType::kError);
137 return;
138 }
139 // Project shell is in place; now open the user's ROM. OpenRomOrProject
140 // handles both ROM and project files, and emits its own toasts on failure.
141 auto rom_status = editor_manager_->OpenRomOrProject(rom_path);
142 if (!rom_status.ok()) {
143 toast_manager_.Show(
144 absl::StrFormat("Project created but ROM failed to load: %s",
145 rom_status.message()),
146 ToastType::kWarning);
147 return;
148 }
149 toast_manager_.Show(absl::StrFormat("Created project \"%s\" from %s",
150 project_name, template_name),
151 ToastType::kSuccess);
152 SetStartupSurface(StartupSurface::kDashboard);
153 });
154
155 welcome_screen_->SetOpenProjectCallback([this](const std::string& filepath) {
156 if (editor_manager_) {
157 auto status = editor_manager_->OpenRomOrProject(filepath);
158 if (!status.ok()) {
159 toast_manager_.Show(
160 absl::StrFormat("Failed to open project: %s", status.message()),
161 ToastType::kError);
162 } else {
163 // Transition to Dashboard on successful project open
164 SetStartupSurface(StartupSurface::kDashboard);
165 }
166 }
167 });
168
169 welcome_screen_->SetOpenAgentCallback([this]() {
170 if (editor_manager_) {
171#ifdef YAZE_BUILD_AGENT_UI
172 editor_manager_->ShowAIAgent();
173#endif
174 // Exit welcome so the agent panels can be interacted with
175 SetStartupSurface(StartupSurface::kEditor);
176 }
177 });
178
179 welcome_screen_->SetOpenProjectDialogCallback([this]() {
180 if (editor_manager_) {
181 auto status = editor_manager_->OpenProject();
182 if (!status.ok()) {
183 toast_manager_.Show(
184 absl::StrFormat("Failed to open project: %s", status.message()),
185 ToastType::kError);
186 } else {
187 SetStartupSurface(StartupSurface::kDashboard);
188 }
189 }
190 });
191
192 welcome_screen_->SetOpenProjectManagementCallback([this]() {
193 if (editor_manager_) {
194 editor_manager_->ShowProjectManagement();
195 SetStartupSurface(StartupSurface::kDashboard);
196 }
197 });
198
199 welcome_screen_->SetOpenProjectFileEditorCallback([this]() {
200 if (!editor_manager_) {
201 return;
202 }
203 const auto* project = editor_manager_->GetCurrentProject();
204 if (!project || project->filepath.empty()) {
205 toast_manager_.Show("No project file to edit", ToastType::kInfo);
206 return;
207 }
208 editor_manager_->ShowProjectFileEditor();
209 SetStartupSurface(StartupSurface::kDashboard);
210 });
211
212 welcome_screen_->SetOpenPrototypeResearchCallback([this]() {
213 if (!editor_manager_) {
214 return;
215 }
216 editor_manager_->SwitchToEditor(EditorType::kGraphics, true);
217 window_manager_.OpenWindow("graphics.prototype_viewer");
218 SetWelcomeScreenVisible(false);
219 SetWelcomeScreenManuallyClosed(true);
220 });
221
222 welcome_screen_->SetOpenAssemblyEditorNoRomCallback([this]() {
223 if (!editor_manager_) {
224 return;
225 }
226 editor_manager_->SwitchToEditor(EditorType::kAssembly, true);
227 window_manager_.OpenWindow("assembly.code_editor");
228 SetWelcomeScreenVisible(false);
229 SetWelcomeScreenManuallyClosed(true);
230 });
231}
232
233void UICoordinator::SetWelcomeScreenBehavior(StartupVisibility mode) {
234 welcome_behavior_override_ = mode;
235 if (mode == StartupVisibility::kHide) {
236 welcome_screen_manually_closed_ = true;
237 // If hiding welcome, transition to appropriate state
238 if (current_startup_surface_ == StartupSurface::kWelcome) {
239 SetStartupSurface(StartupSurface::kDashboard);
240 }
241 } else if (mode == StartupVisibility::kShow) {
242 welcome_screen_manually_closed_ = false;
243 SetStartupSurface(StartupSurface::kWelcome);
244 }
245}
246
247void UICoordinator::SetDashboardBehavior(StartupVisibility mode) {
248 if (dashboard_behavior_override_ == mode) {
249 return;
250 }
251 dashboard_behavior_override_ = mode;
252 if (mode == StartupVisibility::kShow) {
253 // Only transition to dashboard if we're not in welcome
254 if (current_startup_surface_ != StartupSurface::kWelcome) {
255 SetStartupSurface(StartupSurface::kDashboard);
256 }
257 } else if (mode == StartupVisibility::kHide) {
258 // If hiding dashboard, transition to editor state
259 if (current_startup_surface_ == StartupSurface::kDashboard) {
260 SetStartupSurface(StartupSurface::kEditor);
261 }
262 }
263}
264
265void UICoordinator::DrawBackground() {
266 if (!ImGui::GetCurrentContext()) {
267 return;
268 }
269
270 const ImGuiViewport* viewport = ImGui::GetMainViewport();
271 if (!viewport) {
272 return;
273 }
274
275 ImDrawList* bg_draw_list =
276 ImGui::GetBackgroundDrawList(const_cast<ImGuiViewport*>(viewport));
277
278 auto& theme_manager = gui::ThemeManager::Get();
279 auto current_theme = theme_manager.GetCurrentTheme();
280 auto& bg_renderer = gui::BackgroundRenderer::Get();
281
282 // Draw grid covering the entire main viewport
283 ImVec2 grid_pos = viewport->WorkPos;
284 ImVec2 grid_size = viewport->WorkSize;
285 bg_renderer.RenderDockingBackground(bg_draw_list, grid_pos, grid_size,
286 current_theme.primary);
287}
288
289void UICoordinator::DrawAllUI() {
290 // Note: Theme styling is applied by ThemeManager, not here
291 // This is called from EditorManager::Update() - don't call menu bar stuff
292 // here
293
294 // Draw UI windows and dialogs
295 // Session dialogs are drawn by SessionCoordinator separately to avoid
296 // duplication
297 DrawCommandPalette(); // Ctrl+Shift+P
298 DrawPanelFinder(); // Ctrl+P
299 DrawGlobalSearch(); // Ctrl+Shift+K
300 DrawWorkspacePresetDialogs(); // Save/Load workspace dialogs
301 DrawLayoutPresets(); // Layout preset dialogs
302 DrawWelcomeScreen(); // Welcome screen
303 DrawProjectHelp(); // Project help
304 DrawWindowManagementUI(); // Window management
305
306#ifdef YAZE_BUILD_AGENT_UI
307 if (show_ai_agent_) {
308 if (editor_manager_) {
309 editor_manager_->ShowAIAgent();
310 }
311 show_ai_agent_ = false;
312 }
313
314 if (show_chat_history_) {
315 if (editor_manager_) {
316 editor_manager_->ShowChatHistory();
317 }
318 show_chat_history_ = false;
319 }
320
321 if (show_proposal_drawer_) {
322 if (editor_manager_) {
323 if (auto* right_panel = editor_manager_->right_drawer_manager()) {
324 right_panel->OpenDrawer(RightDrawerManager::DrawerType::kProposals);
325 }
326 }
327 show_proposal_drawer_ = false;
328 }
329#endif
330
331 // Draw popups and toasts
332 DrawAllPopups();
333 toast_manager_.Draw();
334}
335
336bool UICoordinator::IsCompactLayout() const {
337 const ImGuiViewport* viewport = ImGui::GetMainViewport();
338 if (!viewport) {
339 return false;
340 }
341 const float width = viewport->WorkSize.x;
342#if defined(__APPLE__) && TARGET_OS_IOS == 1
343 // Use hysteresis to avoid layout thrash while iPad windows are being
344 // interactively resized around the compact breakpoint.
345 static bool compact_mode = false;
346 constexpr float kEnterCompactWidth = 900.0f;
347 constexpr float kExitCompactWidth = 940.0f;
348 compact_mode =
349 compact_mode ? (width < kExitCompactWidth) : (width < kEnterCompactWidth);
350 return compact_mode;
351#else
352 return width < 900.0f;
353#endif
354}
355
356// =============================================================================
357// Menu Bar Helpers
358// =============================================================================
359
360bool UICoordinator::DrawMenuBarIconButton(const char* icon, const char* tooltip,
361 bool is_active) {
362 // Consistent button styling: transparent background, themed text
363 gui::StyleColorGuard btn_guard(
364 {{ImGuiCol_Button, ImVec4(0, 0, 0, 0)},
365 {ImGuiCol_ButtonHovered, gui::GetSurfaceContainerHighVec4()},
366 {ImGuiCol_ButtonActive, gui::GetSurfaceContainerHighestVec4()},
367 {ImGuiCol_Text,
368 is_active ? gui::GetPrimaryVec4() : gui::GetTextSecondaryVec4()}});
369
370 bool clicked = ImGui::SmallButton(icon);
371
372 if (tooltip && ImGui::IsItemHovered()) {
373 ImGui::SetTooltip("%s", tooltip);
374 }
375
376 return clicked;
377}
378
379float UICoordinator::GetMenuBarIconButtonWidth() {
380 // SmallButton width = text width + frame padding * 2
381 const float frame_padding = ImGui::GetStyle().FramePadding.x;
382 // Use a standard icon width (Material Design icons are uniform)
383 const float icon_width = ImGui::CalcTextSize(ICON_MD_SETTINGS).x;
384 return icon_width + frame_padding * 2.0f;
385}
386
387void UICoordinator::DrawMenuBarExtras() {
388 // Right-aligned status cluster: Version, dirty indicator, session, bell, panel toggles
389 // Panel toggles are positioned using SCREEN coordinates (from viewport) so they
390 // stay fixed even when the dockspace resizes due to panel open/close.
391 //
392 // Layout: [v0.x.x][●][📄▾][🔔] [panels][⬆]
393 // ^^^ shifts with dockspace ^^^ ^^^ fixed screen position ^^^
394
395 auto* current_rom = editor_manager_->GetCurrentRom();
396 const std::string full_version =
397 absl::StrFormat("v%s", editor_manager_->version().c_str());
398
399 const float item_spacing = 6.0f;
400 const float padding = 8.0f;
401
402 auto CalcSmallButtonWidth = [](const char* label) -> float {
403 // SmallButton width = text width + frame padding * 2
404 const float frame_padding = ImGui::GetStyle().FramePadding.x;
405 const float text_w = ImGui::CalcTextSize(label).x;
406 return text_w + frame_padding * 2.0f;
407 };
408
409 // Get TRUE viewport dimensions (not affected by dockspace resize)
410 const ImGuiViewport* viewport = ImGui::GetMainViewport();
411 const float true_viewport_right = viewport->WorkPos.x + viewport->WorkSize.x;
412
413 // Calculate panel toggle region width
414 // Keep this in sync with RightDrawerManager::DrawDrawerToggleButtons().
415 const bool has_panel_toggles =
416 editor_manager_->right_drawer_manager() != nullptr;
417 float panel_buttons_width = 0.0f;
418 if (has_panel_toggles) {
419 const char* kIcons[] = {
420 ICON_MD_FOLDER_SPECIAL, // Project
421 ICON_MD_SMART_TOY, // Agent
422 ICON_MD_HELP_OUTLINE, // Help
423 ICON_MD_SETTINGS, // Settings
424 ICON_MD_LIST_ALT, // Properties
425 };
426 constexpr size_t kIconCount = sizeof(kIcons) / sizeof(kIcons[0]);
427
428 for (size_t i = 0; i < kIconCount; ++i) {
429 panel_buttons_width += CalcSmallButtonWidth(kIcons[i]);
430 if (i + 1 < kIconCount) {
431 panel_buttons_width += item_spacing;
432 }
433 }
434 }
435
436 // Reserve only the real button footprint so compact icon toggles do not
437 // leave a dead gap before the right edge cluster.
438 float panel_region_width = panel_buttons_width;
439#ifdef __EMSCRIPTEN__
440 // WASM hide menu bar toggle (drawn inline after panel buttons).
441 panel_region_width +=
442 CalcSmallButtonWidth(ICON_MD_EXPAND_LESS) + item_spacing;
443#endif
444
445 // Calculate screen X position for panel toggles (fixed at viewport right edge)
446 float panel_screen_x = true_viewport_right - panel_region_width;
447 if (editor_manager_->right_drawer_manager() &&
448 editor_manager_->right_drawer_manager()->IsDrawerExpanded()) {
449 panel_screen_x -= editor_manager_->right_drawer_manager()->GetDrawerWidth();
450 }
451
452 // Calculate available space for status cluster (version, dirty, session, bell)
453 // This ends where the panel toggle region begins
454 const float window_width = ImGui::GetWindowWidth();
455 const float window_screen_x = ImGui::GetWindowPos().x;
456 const float menu_items_end = ImGui::GetCursorPosX() + 16.0f;
457
458 // Convert panel screen X to window-local coordinates for space calculation
459 float panel_local_x = panel_screen_x - window_screen_x;
460 float region_end =
461 std::min(window_width - padding, panel_local_x - item_spacing);
462
463 // Calculate what elements to show - progressive hiding when space is tight
464 bool has_dirty_rom =
465 current_rom && current_rom->is_loaded() && current_rom->dirty();
466 bool has_multiple_sessions = session_coordinator_.HasMultipleSessions();
467
468 float version_width = ImGui::CalcTextSize(full_version.c_str()).x;
469 float dirty_width =
470 ImGui::CalcTextSize(ICON_MD_FIBER_MANUAL_RECORD).x + item_spacing;
471 const float session_width = CalcSmallButtonWidth(ICON_MD_LAYERS);
472
473 const float available_width = region_end - menu_items_end - padding;
474
475 // Minimum required width: just the bell (always visible)
476 float required_width = CalcSmallButtonWidth(ICON_MD_NOTIFICATIONS);
477
478 // Progressive show/hide based on available space
479 // Priority (highest to lowest): Bell > Dirty > Session > Version
480
481 // Try to fit version (lowest priority - hide first when tight)
482 bool show_version =
483 (required_width + version_width + item_spacing) <= available_width;
484 if (show_version) {
485 required_width += version_width + item_spacing;
486 }
487
488 // Try to fit session button (medium priority)
489 bool show_session =
490 has_multiple_sessions &&
491 (required_width + session_width + item_spacing) <= available_width;
492 if (show_session) {
493 required_width += session_width + item_spacing;
494 }
495
496 // Try to fit dirty indicator (high priority - only hide if extremely tight)
497 bool show_dirty =
498 has_dirty_rom && (required_width + dirty_width) <= available_width;
499 if (show_dirty) {
500 required_width += dirty_width;
501 }
502
503 // Calculate start position (right-align within available space)
504 float start_pos = std::max(menu_items_end, region_end - required_width);
505
506 // =========================================================================
507 // DRAW STATUS CLUSTER (shifts with dockspace)
508 // =========================================================================
509 ImGui::SameLine(start_pos);
510 gui::StyleVarGuard item_spacing_guard(ImGuiStyleVar_ItemSpacing,
511 ImVec2(item_spacing, 0.0f));
512
513 // 1. Version - subdued gray text
514 if (show_version) {
515 gui::ColoredText(full_version.c_str(), gui::GetTextDisabledVec4());
516 ImGui::SameLine();
517 }
518
519 // 2. Dirty badge - warning color dot
520 if (show_dirty) {
521 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
522 gui::ColoredText(ICON_MD_FIBER_MANUAL_RECORD,
523 gui::ConvertColorToImVec4(theme.warning));
524 if (ImGui::IsItemHovered()) {
525 ImGui::SetTooltip("Unsaved changes: %s",
526 current_rom->short_name().c_str());
527 }
528 ImGui::SameLine();
529 }
530
531 // 3. Session button - layers icon
532 if (show_session) {
533 DrawSessionButton();
534 ImGui::SameLine();
535 }
536
537 // 4. Notification bell (pass visibility flags for enhanced tooltip)
538 DrawNotificationBell(show_dirty, has_dirty_rom, show_session,
539 has_multiple_sessions);
540
541 // =========================================================================
542 // DRAW PANEL TOGGLES (fixed screen position, unaffected by dockspace resize)
543 // =========================================================================
544 if (has_panel_toggles) {
545 // Get current Y position within menu bar
546 float menu_bar_y = ImGui::GetCursorScreenPos().y;
547
548 // Position at fixed screen coordinates
549 ImGui::SetCursorScreenPos(ImVec2(panel_screen_x, menu_bar_y));
550
551 // Draw panel toggle buttons
552 editor_manager_->right_drawer_manager()->DrawDrawerToggleButtons();
553 }
554
555#ifdef __EMSCRIPTEN__
556 // WASM toggle button - also at fixed position
557 ImGui::SameLine();
558 if (DrawMenuBarIconButton(ICON_MD_EXPAND_LESS,
559 "Hide menu bar (Alt to restore)")) {
560 show_menu_bar_ = false;
561 }
562#endif
563}
564
565void UICoordinator::DrawMenuBarRestoreButton() {
566 // Only draw when menu bar is hidden (primarily for WASM builds)
567 if (show_menu_bar_) {
568 return;
569 }
570
571 // Small floating button in top-left corner to restore menu bar
572 ImGuiWindowFlags flags =
573 ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
574 ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
575 ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize |
576 ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoSavedSettings;
577
578 ImGui::SetNextWindowPos(ImVec2(8, 8));
579 ImGui::SetNextWindowBgAlpha(0.7f);
580
581 if (ImGui::Begin("##MenuBarRestore", nullptr, flags)) {
582 gui::StyleColorGuard btn_guard(
583 {{ImGuiCol_Button, gui::GetSurfaceContainerVec4()},
584 {ImGuiCol_ButtonHovered, gui::GetSurfaceContainerHighVec4()},
585 {ImGuiCol_ButtonActive, gui::GetSurfaceContainerHighestVec4()},
586 {ImGuiCol_Text, gui::GetPrimaryVec4()}});
587
588 if (ImGui::Button(ICON_MD_FULLSCREEN_EXIT, ImVec2(32, 32))) {
589 show_menu_bar_ = true;
590 }
591
592 if (ImGui::IsItemHovered()) {
593 ImGui::SetTooltip("Show menu bar (Alt)");
594 }
595 }
596 ImGui::End();
597
598 // Also check for Alt key to restore menu bar
599 if (ImGui::IsKeyPressed(ImGuiKey_LeftAlt) ||
600 ImGui::IsKeyPressed(ImGuiKey_RightAlt)) {
601 show_menu_bar_ = true;
602 }
603}
604
605void UICoordinator::DrawNotificationBell(bool show_dirty, bool has_dirty_rom,
606 bool show_session,
607 bool has_multiple_sessions) {
608 size_t unread = toast_manager_.GetUnreadCount();
609 auto* current_rom = editor_manager_->GetCurrentRom();
610 auto* right_panel = editor_manager_->right_drawer_manager();
611
612 // Check if notifications panel is active
613 bool is_active =
614 right_panel && right_panel->IsDrawerActive(
615 RightDrawerManager::DrawerType::kNotifications);
616
617 // Bell icon with accent color when there are unread notifications or panel is active
618 ImVec4 bell_text_color = (unread > 0 || is_active)
619 ? gui::GetPrimaryVec4()
620 : gui::GetTextSecondaryVec4();
621 gui::StyleColorGuard bell_guard(
622 {{ImGuiCol_Text, bell_text_color},
623 {ImGuiCol_Button, ImVec4(0, 0, 0, 0)},
624 {ImGuiCol_ButtonHovered, gui::GetSurfaceContainerHighVec4()},
625 {ImGuiCol_ButtonActive, gui::GetSurfaceContainerHighestVec4()}});
626
627 // Bell button - opens notifications panel in right sidebar
628 if (ImGui::SmallButton(ICON_MD_NOTIFICATIONS)) {
629 if (right_panel) {
630 right_panel->ToggleDrawer(RightDrawerManager::DrawerType::kNotifications);
631 toast_manager_.MarkAllRead();
632 }
633 }
634
635 // Enhanced tooltip showing notifications + hidden status items
636 if (ImGui::IsItemHovered()) {
637 ImGui::BeginTooltip();
638
639 // Notifications
640 if (unread > 0) {
641 gui::ColoredTextF(gui::GetPrimaryVec4(), "%s %zu new notification%s",
642 ICON_MD_NOTIFICATIONS, unread, unread == 1 ? "" : "s");
643 } else {
644 gui::ColoredText(ICON_MD_NOTIFICATIONS " No new notifications",
645 gui::GetTextSecondaryVec4());
646 }
647
648 ImGui::TextDisabled("Click to open Notifications panel");
649
650 // Show hidden status items if any
651 if (!show_dirty && has_dirty_rom) {
652 ImGui::Separator();
653 gui::ColoredTextF(gui::ConvertColorToImVec4(
654 gui::ThemeManager::Get().GetCurrentTheme().warning),
655 ICON_MD_FIBER_MANUAL_RECORD " Unsaved changes: %s",
656 current_rom->short_name().c_str());
657 }
658
659 if (!show_session && has_multiple_sessions) {
660 if (!show_dirty && has_dirty_rom) {
661 // Already had a separator
662 } else {
663 ImGui::Separator();
664 }
665 gui::ColoredTextF(gui::GetTextSecondaryVec4(),
666 ICON_MD_LAYERS " %zu sessions active",
667 session_coordinator_.GetActiveSessionCount());
668 }
669
670 ImGui::EndTooltip();
671 }
672}
673
674void UICoordinator::DrawSessionButton() {
675 auto* current_rom = editor_manager_->GetCurrentRom();
676
677 // Consistent button styling with other menubar buttons
678 gui::StyleColorGuard session_btn_guard(
679 {{ImGuiCol_Button, ImVec4(0, 0, 0, 0)},
680 {ImGuiCol_ButtonHovered, gui::GetSurfaceContainerHighVec4()},
681 {ImGuiCol_ButtonActive, gui::GetSurfaceContainerHighestVec4()},
682 {ImGuiCol_Text, gui::GetTextSecondaryVec4()}});
683
684 // Store button position for popup anchoring
685 ImVec2 button_min = ImGui::GetCursorScreenPos();
686
687 if (ImGui::SmallButton(ICON_MD_LAYERS)) {
688 ImGui::OpenPopup("##SessionSwitcherPopup");
689 }
690
691 ImVec2 button_max = ImGui::GetItemRectMax();
692
693 if (ImGui::IsItemHovered()) {
694 std::string tooltip = current_rom && current_rom->is_loaded()
695 ? current_rom->short_name()
696 : "No ROM loaded";
697 ImGui::SetTooltip("%s\n%zu sessions open (Ctrl+Tab)", tooltip.c_str(),
698 session_coordinator_.GetActiveSessionCount());
699 }
700
701 // Anchor popup to right edge - position so right edge aligns with button
702 const float popup_width = 250.0f;
703 const float screen_width = ImGui::GetIO().DisplaySize.x;
704 const float popup_x =
705 std::min(button_min.x, screen_width - popup_width - 10.0f);
706
707 ImGui::SetNextWindowPos(ImVec2(popup_x, button_max.y + 2.0f),
708 ImGuiCond_Appearing);
709
710 // Session switcher popup
711 if (ImGui::BeginPopup("##SessionSwitcherPopup")) {
712 ImGui::Text(ICON_MD_LAYERS " Sessions");
713 ImGui::Separator();
714
715 for (size_t i = 0; i < session_coordinator_.GetTotalSessionCount(); ++i) {
716 if (session_coordinator_.IsSessionClosed(i))
717 continue;
718
719 auto* session =
720 static_cast<RomSession*>(session_coordinator_.GetSession(i));
721 if (!session)
722 continue;
723
724 Rom* rom = &session->rom;
725 ImGui::PushID(static_cast<int>(i));
726
727 bool is_current = (rom == current_rom);
728 std::optional<gui::StyleColorGuard> current_guard;
729 if (is_current) {
730 current_guard.emplace(ImGuiCol_Text, gui::GetPrimaryVec4());
731 }
732
733 std::string label =
734 rom->is_loaded()
735 ? absl::StrFormat("%s %s", ICON_MD_DESCRIPTION,
736 rom->short_name().c_str())
737 : absl::StrFormat("%s Session %zu", ICON_MD_DESCRIPTION, i + 1);
738
739 if (ImGui::Selectable(label.c_str(), is_current)) {
740 editor_manager_->SwitchToSession(i);
741 }
742
743 ImGui::PopID();
744 }
745
746 ImGui::EndPopup();
747 }
748}
749
750// ============================================================================
751// Session UI Delegation
752// ============================================================================
753// All session-related UI is now managed by SessionCoordinator to eliminate
754// duplication. UICoordinator methods delegate to SessionCoordinator.
755
756void UICoordinator::ShowSessionSwitcher() {
757 session_coordinator_.ShowSessionSwitcher();
758}
759
760bool UICoordinator::IsSessionSwitcherVisible() const {
761 return session_coordinator_.IsSessionSwitcherVisible();
762}
763
764void UICoordinator::SetSessionSwitcherVisible(bool visible) {
765 if (visible) {
766 session_coordinator_.ShowSessionSwitcher();
767 } else {
768 session_coordinator_.HideSessionSwitcher();
769 }
770}
771
772// Emulator visibility delegates to WorkspaceWindowManager (single source of truth)
773bool UICoordinator::IsEmulatorVisible() const {
774 size_t session_id = session_coordinator_.GetActiveSessionIndex();
775 auto emulator_windows =
776 window_manager_.GetWindowsInCategory(session_id, "Emulator");
777 for (const auto& window : emulator_windows) {
778 if (window.visibility_flag && *window.visibility_flag) {
779 return true;
780 }
781 }
782 return false;
783}
784
785void UICoordinator::SetEmulatorVisible(bool visible) {
786 size_t session_id = session_coordinator_.GetActiveSessionIndex();
787 if (visible) {
788 auto default_windows =
789 LayoutPresets::GetDefaultWindows(EditorType::kEmulator);
790 for (const auto& window_id : default_windows) {
791 window_manager_.OpenWindow(session_id, window_id);
792 }
793 } else {
794 window_manager_.HideAllWindowsInCategory(session_id, "Emulator");
795 }
796}
797
798// Assembly editor visibility: same delegation pattern as the emulator. The
799// `show_asm_editor_` boolean was a parallel source of truth that could drift
800// when the user closed a single Assembly panel via its window close button;
801// reading through the category means the user's last interaction always wins.
802bool UICoordinator::IsAsmEditorVisible() const {
803 size_t session_id = session_coordinator_.GetActiveSessionIndex();
804 auto windows = window_manager_.GetWindowsInCategory(session_id, "Assembly");
805 for (const auto& window : windows) {
806 if (window.visibility_flag && *window.visibility_flag) {
807 return true;
808 }
809 }
810 return false;
811}
812
813void UICoordinator::SetAsmEditorVisible(bool visible) {
814 size_t session_id = session_coordinator_.GetActiveSessionIndex();
815 if (visible) {
816 auto default_windows =
817 LayoutPresets::GetDefaultWindows(EditorType::kAssembly);
818 for (const auto& window_id : default_windows) {
819 window_manager_.OpenWindow(session_id, window_id);
820 }
821 } else {
822 window_manager_.HideAllWindowsInCategory(session_id, "Assembly");
823 }
824}
825
826// ============================================================================
827// Layout and Window Management UI
828// ============================================================================
829
830void UICoordinator::DrawLayoutPresets() {
831 // TODO: [EditorManagerRefactor] Implement full layout preset UI with
832 // save/load For now, this is accessed via Window menu items that call
833 // workspace_manager directly
834}
835
836void UICoordinator::DrawWelcomeScreen() {
837 // ============================================================================
838 // CENTRALIZED WELCOME SCREEN LOGIC (using StartupSurface state)
839 // ============================================================================
840 // Uses ShouldShowWelcome() as single source of truth
841 // Auto-transitions to Dashboard on ROM load
842 // Activity Bar hidden when welcome is visible
843 // ============================================================================
844
845 if (!editor_manager_) {
846 LOG_ERROR("UICoordinator",
847 "EditorManager is null - cannot check ROM state");
848 return;
849 }
850
851 if (!welcome_screen_) {
852 LOG_ERROR("UICoordinator", "WelcomeScreen object is null - cannot render");
853 return;
854 }
855
856 // Check ROM state and update startup surface accordingly
857 auto* current_rom = editor_manager_->GetCurrentRom();
858 bool rom_is_loaded = current_rom && current_rom->is_loaded();
859
860 // Auto-transition: ROM loaded -> Dashboard or Editor
861 if (rom_is_loaded && current_startup_surface_ == StartupSurface::kWelcome) {
862 if (welcome_screen_manually_closed_) {
863 SetStartupSurface(StartupSurface::kEditor);
864 } else {
865 SetStartupSurface(StartupSurface::kDashboard);
866 }
867 }
868
869 // Auto-transition: ROM unloaded -> Welcome (reset to welcome state)
870 if (!rom_is_loaded && current_startup_surface_ != StartupSurface::kWelcome &&
871 !welcome_screen_manually_closed_) {
872 SetStartupSurface(StartupSurface::kWelcome);
873 }
874
875 // Use centralized visibility check
876 if (!ShouldShowWelcome()) {
877 return;
878 }
879
880 // Provide context state for gating actions
881 welcome_screen_->SetContextState(rom_is_loaded,
882 project_manager_.HasActiveProject());
883
884 // Update recent projects before showing (cheap no-op when the
885 // RecentFilesManager generation counter hasn't changed).
886 welcome_screen_->RefreshRecentProjects();
887
888 // Pass layout offsets so welcome screen centers within dockspace region
889 // Note: Activity Bar is hidden when welcome is shown, so left_offset = 0
890 float left_offset =
891 ShouldShowActivityBar() ? editor_manager_->GetLeftLayoutOffset() : 0.0f;
892 float right_offset = editor_manager_->GetRightLayoutOffset();
893 welcome_screen_->SetLayoutOffsets(left_offset, right_offset);
894
895 // Show the welcome screen window
896 bool is_open = true;
897 welcome_screen_->Show(&is_open);
898
899 // Dialog is drawn after the welcome screen so its popup layers above the
900 // welcome window. Kept outside the ShouldShowWelcome short-circuit above
901 // because the dialog stays owned here even when welcome is hidden.
902 new_project_dialog_.Draw();
903
904 // If user closed it via X button, respect that and transition to appropriate state
905 if (!is_open) {
906 welcome_screen_manually_closed_ = true;
907 // Transition to Dashboard if ROM loaded, stay in Editor state otherwise
908 if (rom_is_loaded) {
909 SetStartupSurface(StartupSurface::kDashboard);
910 }
911 }
912}
913
914void UICoordinator::DrawProjectHelp() {
915 // TODO: [EditorManagerRefactor] Implement project help dialog
916 // Show context-sensitive help based on current editor and ROM state
917}
918
919void UICoordinator::DrawWorkspacePresetDialogs() {
920 if (show_save_workspace_preset_) {
921 ImGui::Begin("Save Workspace Preset", &show_save_workspace_preset_,
922 ImGuiWindowFlags_AlwaysAutoResize);
923 static char preset_name[128] = "";
924 ImGui::InputText("Name", preset_name, IM_ARRAYSIZE(preset_name));
925 if (ImGui::Button("Save", gui::kDefaultModalSize)) {
926 if (strlen(preset_name) > 0) {
927 editor_manager_->SaveWorkspacePreset(preset_name);
928 toast_manager_.Show("Preset saved", editor::ToastType::kSuccess);
929 show_save_workspace_preset_ = false;
930 preset_name[0] = '\0';
931 }
932 }
933 ImGui::SameLine();
934 if (ImGui::Button("Cancel", gui::kDefaultModalSize)) {
935 show_save_workspace_preset_ = false;
936 preset_name[0] = '\0';
937 }
938 ImGui::End();
939 }
940
941 if (show_load_workspace_preset_) {
942 ImGui::Begin("Load Workspace Preset", &show_load_workspace_preset_,
943 ImGuiWindowFlags_AlwaysAutoResize);
944
945 // Lazy load workspace presets when UI is accessed
946 editor_manager_->RefreshWorkspacePresets();
947
948 if (auto* workspace_manager = editor_manager_->workspace_manager()) {
949 for (const auto& name : workspace_manager->workspace_presets()) {
950 if (ImGui::Selectable(name.c_str())) {
951 editor_manager_->LoadWorkspacePreset(name);
952 toast_manager_.Show("Preset loaded", editor::ToastType::kSuccess);
953 show_load_workspace_preset_ = false;
954 }
955 }
956 if (workspace_manager->workspace_presets().empty())
957 ImGui::Text("No presets found");
958 }
959 ImGui::End();
960 }
961}
962
963void UICoordinator::DrawWindowManagementUI() {
964 // TODO: [EditorManagerRefactor] Implement window management dialog
965 // Provide UI for toggling window visibility, managing docking, etc.
966}
967
968void UICoordinator::DrawAllPopups() {
969 // Draw all registered popups
970 popup_manager_.DrawPopups();
971}
972
973void UICoordinator::ShowPopup(const std::string& popup_name) {
974 popup_manager_.Show(popup_name.c_str());
975}
976
977void UICoordinator::HidePopup(const std::string& popup_name) {
978 popup_manager_.Hide(popup_name.c_str());
979}
980
981void UICoordinator::ShowDisplaySettings() {
982 // Display Settings is now a popup managed by PopupManager
983 // Delegate directly to PopupManager instead of UICoordinator
984 popup_manager_.Show(PopupID::kDisplaySettings);
985}
986
987// ============================================================================
988// Sidebar visibility delegates to WorkspaceWindowManager
989// ============================================================================
990
991void UICoordinator::TogglePanelSidebar() {
992 window_manager_.ToggleSidebarVisibility();
993}
994
995bool UICoordinator::IsPanelSidebarVisible() const {
996 return window_manager_.IsSidebarVisible();
997}
998
999void UICoordinator::SetPanelSidebarVisible(bool visible) {
1000 window_manager_.SetSidebarVisible(visible);
1001}
1002
1003void UICoordinator::ShowAllWindows() {
1004 window_delegate_.ShowAllWindows();
1005}
1006
1007void UICoordinator::HideAllWindows() {
1008 window_delegate_.HideAllWindows();
1009}
1010
1011// Material Design component helpers
1012void UICoordinator::DrawMaterialButton(const std::string& text,
1013 const std::string& icon,
1014 const ImVec4& color,
1015 std::function<void()> callback,
1016 bool enabled) {
1017 std::optional<gui::StyleColorGuard> disabled_guard;
1018 if (!enabled) {
1019 disabled_guard.emplace(std::initializer_list<gui::StyleColorGuard::Entry>{
1020 {ImGuiCol_Button, gui::GetSurfaceContainerHighestVec4()},
1021 {ImGuiCol_Text, gui::GetOnSurfaceVariantVec4()}});
1022 }
1023
1024 std::string button_text =
1025 absl::StrFormat("%s %s", icon.c_str(), text.c_str());
1026 if (ImGui::Button(button_text.c_str())) {
1027 if (enabled && callback) {
1028 callback();
1029 }
1030 }
1031}
1032
1033// Layout and positioning helpers
1034void UICoordinator::CenterWindow(const std::string& window_name) {
1035 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
1036 ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
1037}
1038
1039void UICoordinator::PositionWindow(const std::string& window_name, float x,
1040 float y) {
1041 ImGui::SetNextWindowPos(ImVec2(x, y), ImGuiCond_Appearing);
1042}
1043
1044void UICoordinator::SetWindowSize(const std::string& window_name, float width,
1045 float height) {
1046 ImGui::SetNextWindowSize(ImVec2(width, height), ImGuiCond_FirstUseEver);
1047}
1048
1049void UICoordinator::DrawCommandPalette() {
1050 if (!show_command_palette_)
1051 return;
1052
1053 // Initialize command palette on first use
1054 if (!command_palette_initialized_) {
1055 InitializeCommandPalette(0); // Default session
1056 }
1057
1058 using namespace ImGui;
1059 auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
1060
1061 SetNextWindowPos(GetMainViewport()->GetCenter(), ImGuiCond_Appearing,
1062 ImVec2(0.5f, 0.5f));
1063 SetNextWindowSize(ImVec2(800, 600), ImGuiCond_FirstUseEver);
1064
1065 bool show_palette = true;
1066 if (Begin(absl::StrFormat("%s Command Palette", ICON_MD_SEARCH).c_str(),
1067 &show_palette, ImGuiWindowFlags_NoCollapse)) {
1068 // Search input with focus management
1069 SetNextItemWidth(-100);
1070 if (IsWindowAppearing()) {
1071 SetKeyboardFocusHere();
1072 command_palette_selected_idx_ = 0;
1073 }
1074
1075 bool input_changed = InputTextWithHint(
1076 "##cmd_query",
1077 absl::StrFormat("%s Search commands (fuzzy matching enabled)...",
1079 .c_str(),
1080 command_palette_query_, IM_ARRAYSIZE(command_palette_query_));
1081
1082 SameLine();
1083 if (Button(absl::StrFormat("%s Clear", ICON_MD_CLEAR).c_str())) {
1084 command_palette_query_[0] = '\0';
1085 input_changed = true;
1086 command_palette_selected_idx_ = 0;
1087 }
1088
1089 Separator();
1090
1091 // Unified command list structure
1092 struct ScoredCommand {
1093 int score;
1094 std::string name;
1095 std::string category;
1096 std::string shortcut;
1097 std::function<void()> callback;
1098 };
1099 std::vector<ScoredCommand> scored_commands;
1100
1101 std::string query_lower = command_palette_query_;
1102 std::transform(query_lower.begin(), query_lower.end(), query_lower.begin(),
1103 ::tolower);
1104
1105 auto score_text = [&query_lower](const std::string& text) -> int {
1106 std::string text_lower = text;
1107 std::transform(text_lower.begin(), text_lower.end(), text_lower.begin(),
1108 ::tolower);
1109
1110 if (query_lower.empty())
1111 return 1;
1112 if (text_lower.find(query_lower) == 0)
1113 return 1000;
1114 if (text_lower.find(query_lower) != std::string::npos)
1115 return 500;
1116
1117 // Fuzzy match
1118 size_t text_idx = 0, query_idx = 0;
1119 int score = 0;
1120 while (text_idx < text_lower.length() &&
1121 query_idx < query_lower.length()) {
1122 if (text_lower[text_idx] == query_lower[query_idx]) {
1123 score += 10;
1124 query_idx++;
1125 }
1126 text_idx++;
1127 }
1128 return (query_idx == query_lower.length()) ? score : 0;
1129 };
1130
1131 // Add shortcuts from ShortcutManager
1132 for (const auto& [name, shortcut] : shortcut_manager_.GetShortcuts()) {
1133 int score = score_text(name);
1134 if (score > 0) {
1135 std::string shortcut_text =
1136 shortcut.keys.empty()
1137 ? ""
1138 : absl::StrFormat("(%s)", PrintShortcut(shortcut.keys).c_str());
1139 scored_commands.push_back(
1140 {score, name, "Shortcuts", shortcut_text, shortcut.callback});
1141 }
1142 }
1143
1144 // Add commands from CommandPalette
1145 for (const auto& entry : command_palette_.GetAllCommands()) {
1146 int score = score_text(entry.name);
1147 // Also search category and description
1148 score += score_text(entry.category) / 2;
1149 score += score_text(entry.description) / 4;
1150
1151 if (score > 0) {
1152 scored_commands.push_back({score, entry.name, entry.category,
1153 entry.shortcut, entry.callback});
1154 }
1155 }
1156
1157 // Sort by score descending
1158 std::sort(scored_commands.begin(), scored_commands.end(),
1159 [](const auto& a, const auto& b) { return a.score > b.score; });
1160
1161 // Display results with categories
1162 if (gui::BeginThemedTabBar("CommandCategories")) {
1163 if (BeginTabItem(
1164 absl::StrFormat("%s All Commands", ICON_MD_LIST).c_str())) {
1165 if (gui::LayoutHelpers::BeginTableWithTheming(
1166 "CommandPaletteTable", 4,
1167 ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg |
1168 ImGuiTableFlags_SizingStretchProp,
1169 ImVec2(0, -30))) {
1170 TableSetupColumn("Command", ImGuiTableColumnFlags_WidthStretch,
1171 0.45f);
1172 TableSetupColumn("Category", ImGuiTableColumnFlags_WidthStretch,
1173 0.2f);
1174 TableSetupColumn("Shortcut", ImGuiTableColumnFlags_WidthStretch,
1175 0.2f);
1176 TableSetupColumn("Score", ImGuiTableColumnFlags_WidthStretch, 0.15f);
1177 TableHeadersRow();
1178
1179 for (size_t i = 0; i < scored_commands.size(); ++i) {
1180 const auto& cmd = scored_commands[i];
1181
1182 TableNextRow();
1183 TableNextColumn();
1184
1185 PushID(static_cast<int>(i));
1186 bool is_selected =
1187 (static_cast<int>(i) == command_palette_selected_idx_);
1188 if (Selectable(cmd.name.c_str(), is_selected,
1189 ImGuiSelectableFlags_SpanAllColumns)) {
1190 command_palette_selected_idx_ = i;
1191 if (cmd.callback) {
1192 cmd.callback();
1193 show_command_palette_ = false;
1194 // Record usage for frecency
1195 command_palette_.RecordUsage(cmd.name);
1196 }
1197 }
1198 PopID();
1199
1200 TableNextColumn();
1201 gui::ColoredText(cmd.category.c_str(),
1202 gui::ConvertColorToImVec4(theme.text_secondary));
1203
1204 TableNextColumn();
1205 gui::ColoredText(cmd.shortcut.c_str(),
1206 gui::ConvertColorToImVec4(theme.text_secondary));
1207
1208 TableNextColumn();
1209 gui::ColoredTextF(gui::ConvertColorToImVec4(theme.text_disabled),
1210 "%d", cmd.score);
1211 }
1212
1213 gui::LayoutHelpers::EndTableWithTheming();
1214 }
1215 EndTabItem();
1216 }
1217
1218 if (BeginTabItem(absl::StrFormat("%s Recent", ICON_MD_HISTORY).c_str())) {
1219 auto recent = command_palette_.GetRecentCommands(10);
1220 if (recent.empty()) {
1221 Text("No recent commands yet.");
1222 } else {
1223 for (const auto& entry : recent) {
1224 if (Selectable(entry.name.c_str())) {
1225 if (entry.callback) {
1226 entry.callback();
1227 show_command_palette_ = false;
1228 command_palette_.RecordUsage(entry.name);
1229 }
1230 }
1231 }
1232 }
1233 EndTabItem();
1234 }
1235
1236 if (BeginTabItem(absl::StrFormat("%s Frequent", ICON_MD_STAR).c_str())) {
1237 auto frequent = command_palette_.GetFrequentCommands(10);
1238 if (frequent.empty()) {
1239 Text("No frequently used commands yet.");
1240 } else {
1241 for (const auto& entry : frequent) {
1242 if (Selectable(absl::StrFormat("%s (%d uses)", entry.name,
1243 entry.usage_count)
1244 .c_str())) {
1245 if (entry.callback) {
1246 entry.callback();
1247 show_command_palette_ = false;
1248 command_palette_.RecordUsage(entry.name);
1249 }
1250 }
1251 }
1252 }
1253 EndTabItem();
1254 }
1255
1256 gui::EndThemedTabBar();
1257 }
1258
1259 // Status bar with tips
1260 Separator();
1261 Text("%s %zu commands | Score: fuzzy match", ICON_MD_INFO,
1262 scored_commands.size());
1263 SameLine();
1264 gui::ColoredText("| ↑↓=Navigate | Enter=Execute | Esc=Close",
1265 gui::ConvertColorToImVec4(theme.text_disabled));
1266 }
1267 End();
1268
1269 // Update visibility state - save history when closing
1270 if (!show_palette) {
1271 show_command_palette_ = false;
1272 // Save command usage history on close
1273 auto config_dir = util::PlatformPaths::GetConfigDirectory();
1274 if (config_dir.ok()) {
1275 std::filesystem::path history_file = *config_dir / "command_history.json";
1276 command_palette_.SaveHistory(history_file.string());
1277 }
1278 }
1279}
1280
1281void UICoordinator::DrawPanelFinder() {
1282 if (!show_panel_finder_)
1283 return;
1284
1285 using namespace ImGui;
1286 const size_t session_id = window_manager_.GetActiveSessionId();
1287
1288 // B6: Responsive modal sizing with dim overlay
1289 const ImGuiViewport* viewport = GetMainViewport();
1290 ImDrawList* bg_list = GetBackgroundDrawList();
1291 bg_list->AddRectFilled(viewport->WorkPos,
1292 ImVec2(viewport->WorkPos.x + viewport->WorkSize.x,
1293 viewport->WorkPos.y + viewport->WorkSize.y),
1294 IM_COL32(0, 0, 0, 100));
1295
1296 SetNextWindowPos(viewport->GetCenter(), ImGuiCond_Appearing,
1297 ImVec2(0.5f, 0.3f));
1298 if (IsCompactLayout()) {
1299 SetNextWindowSize(
1300 ImVec2(viewport->WorkSize.x * 0.95f, viewport->WorkSize.y * 0.70f),
1301 ImGuiCond_Appearing);
1302 } else {
1303 SetNextWindowSize(ImVec2(600, 420), ImGuiCond_Appearing);
1304 }
1305
1306 const ImGuiWindowFlags finder_flags =
1307 ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove |
1308 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoDocking |
1309 ImGuiWindowFlags_NoSavedSettings;
1310
1311 bool show = true;
1312 if (Begin(ICON_MD_DASHBOARD " Window Finder", &show, finder_flags)) {
1313 // Auto-focus search on open
1314 if (IsWindowAppearing()) {
1315 SetKeyboardFocusHere();
1316 panel_finder_selected_idx_ = 0;
1317 }
1318
1319 SetNextItemWidth(-1);
1320 bool input_changed = InputTextWithHint(
1321 "##panel_finder_query", ICON_MD_SEARCH " Find window...",
1322 panel_finder_query_, IM_ARRAYSIZE(panel_finder_query_));
1323
1324 if (input_changed) {
1325 panel_finder_selected_idx_ = 0;
1326 }
1327
1328 Separator();
1329
1330 // Build filtered + scored list
1331 struct WindowEntry {
1332 std::string card_id;
1333 std::string display_name;
1334 std::string icon;
1335 std::string category;
1336 bool visible;
1337 bool pinned;
1338 int score;
1339 };
1340 std::vector<WindowEntry> entries;
1341
1342 std::string query(panel_finder_query_);
1343
1344 // B2: Fuzzy scoring via CommandPalette::FuzzyScore
1345 for (const auto& card_id :
1346 window_manager_.GetWindowsInSession(session_id)) {
1347 const auto* desc =
1348 window_manager_.GetWindowDescriptor(session_id, card_id);
1349 if (!desc) {
1350 continue;
1351 }
1352 int score = 0;
1353 if (!query.empty()) {
1354 score = CommandPalette::FuzzyScore(desc->display_name, query) +
1355 CommandPalette::FuzzyScore(desc->category, query) / 2;
1356 if (score <= 0)
1357 continue;
1358 }
1359
1360 bool vis = desc->visibility_flag ? *desc->visibility_flag : false;
1361 bool pin = window_manager_.IsWindowPinned(session_id, card_id);
1362 entries.push_back({card_id, desc->display_name, desc->icon,
1363 desc->category, vis, pin, score});
1364 }
1365
1366 // B3: MRU + fuzzy sort
1367 if (query.empty()) {
1368 // Empty query: pinned first, then MRU order (higher time = more recent)
1369 std::sort(
1370 entries.begin(), entries.end(),
1371 [this](const WindowEntry& lhs, const WindowEntry& rhs) {
1372 if (lhs.pinned != rhs.pinned)
1373 return lhs.pinned > rhs.pinned;
1374 uint64_t lhs_t = window_manager_.GetWindowMRUTime(lhs.card_id);
1375 uint64_t rhs_t = window_manager_.GetWindowMRUTime(rhs.card_id);
1376 if (lhs_t != rhs_t)
1377 return lhs_t > rhs_t;
1378 return lhs.display_name < rhs.display_name;
1379 });
1380 } else {
1381 // With query: sort by score descending, pinned tiebreaker
1382 std::sort(entries.begin(), entries.end(),
1383 [](const WindowEntry& lhs, const WindowEntry& rhs) {
1384 if (lhs.score != rhs.score)
1385 return lhs.score > rhs.score;
1386 if (lhs.pinned != rhs.pinned)
1387 return lhs.pinned > rhs.pinned;
1388 return lhs.display_name < rhs.display_name;
1389 });
1390 }
1391
1392 // Keyboard navigation
1393 if (IsKeyPressed(ImGuiKey_DownArrow) &&
1394 panel_finder_selected_idx_ < static_cast<int>(entries.size()) - 1) {
1395 panel_finder_selected_idx_++;
1396 }
1397 if (IsKeyPressed(ImGuiKey_UpArrow) && panel_finder_selected_idx_ > 0) {
1398 panel_finder_selected_idx_--;
1399 }
1400 bool enter_pressed = IsKeyPressed(ImGuiKey_Enter);
1401
1402 // B5: Touch-aware item sizing
1403 const bool is_touch = gui::LayoutHelpers::IsTouchDevice();
1404 if (is_touch) {
1405 PushStyleVar(ImGuiStyleVar_ItemSpacing,
1406 ImVec2(GetStyle().ItemSpacing.x, 10.0f));
1407 }
1408
1409 // Draw panel list
1410 BeginChild("##PanelFinderList");
1411 for (int i = 0; i < static_cast<int>(entries.size()); ++i) {
1412 const auto& entry = entries[i];
1413 bool is_selected = (i == panel_finder_selected_idx_);
1414
1415 // B4: Visibility indicator icon
1416 const char* vis_icon =
1418
1419 // Dim hidden panels
1420 std::optional<gui::StyleColorGuard> dim_guard;
1421 if (!entry.visible) {
1422 ImVec4 dimmed = GetStyleColorVec4(ImGuiCol_Text);
1423 dimmed.w *= 0.5f;
1424 dim_guard.emplace(ImGuiCol_Text, dimmed);
1425 }
1426
1427 std::string label =
1428 absl::StrFormat("%s %s %s", vis_icon, entry.icon.c_str(),
1429 entry.display_name.c_str());
1430
1431 // Touch: ensure selectable meets 44px minimum height
1432 float item_h =
1433 is_touch ? std::max(GetTextLineHeightWithSpacing(), 44.0f) : 0.0f;
1434
1435 PushID(entry.card_id.c_str());
1436 if (Selectable(label.c_str(), is_selected, ImGuiSelectableFlags_None,
1437 ImVec2(0, item_h))) {
1438 window_manager_.OpenWindow(session_id, entry.card_id);
1439 window_manager_.MarkWindowRecentlyUsed(entry.card_id);
1440 show_panel_finder_ = false;
1441 }
1442 // Show category badge on the same line
1443 SameLine(GetContentRegionAvail().x - 80);
1444 TextDisabled("%s", entry.category.c_str());
1445 if (entry.pinned) {
1446 SameLine();
1447 TextDisabled(ICON_MD_PUSH_PIN);
1448 }
1449 PopID();
1450
1451 // Enter to activate selected
1452 if (is_selected && enter_pressed) {
1453 window_manager_.OpenWindow(session_id, entry.card_id);
1454 window_manager_.MarkWindowRecentlyUsed(entry.card_id);
1455 show_panel_finder_ = false;
1456 }
1457
1458 // Scroll selected into view
1459 if (is_selected && (IsKeyPressed(ImGuiKey_DownArrow) ||
1460 IsKeyPressed(ImGuiKey_UpArrow))) {
1461 SetScrollHereY();
1462 }
1463 }
1464 EndChild();
1465
1466 if (is_touch) {
1467 PopStyleVar();
1468 }
1469 }
1470 End();
1471
1472 // Escape or close button
1473 if (!show || ImGui::IsKeyPressed(ImGuiKey_Escape)) {
1474 show_panel_finder_ = false;
1475 panel_finder_query_[0] = '\0';
1476 }
1477}
1478
1479void UICoordinator::InitializeCommandPalette(size_t session_id) {
1480 command_palette_.Clear();
1481 RefreshWorkflowActions();
1482
1483 // Register panel commands
1484 command_palette_.RegisterProvider(
1485 std::make_unique<PanelCommandsProvider>(&window_manager_, session_id));
1486 command_palette_.RegisterProvider(
1487 std::make_unique<WorkflowCommandsProvider>(&window_manager_, session_id));
1488 if (editor_manager_) {
1489 command_palette_.RegisterProvider(std::make_unique<SidebarCommandsProvider>(
1490 &window_manager_, &editor_manager_->user_settings(), session_id));
1491 }
1492
1493 // Register editor switch commands
1494 command_palette_.RegisterProvider(std::make_unique<EditorCommandsProvider>(
1495 [this](const std::string& category) {
1496 auto type = EditorRegistry::GetEditorTypeFromCategory(category);
1497 if (type != EditorType::kSettings && editor_manager_) {
1498 editor_manager_->SwitchToEditor(type);
1499 }
1500 }));
1501
1502 // Register layout/profile commands
1503 command_palette_.AddCommand("Apply: Minimal Layout", CommandCategory::kLayout,
1504 "Switch to essential cards only", "", [this]() {
1505 if (editor_manager_) {
1506 editor_manager_->ApplyLayoutPreset("Minimal");
1507 }
1508 });
1509
1510 command_palette_.AddCommand(
1511 "Apply: Logic Debugger Layout", CommandCategory::kLayout,
1512 "Switch to debug and development focused layout", "", [this]() {
1513 if (editor_manager_) {
1514 editor_manager_->ApplyLayoutPreset("Logic Debugger");
1515 }
1516 });
1517
1518 command_palette_.AddCommand(
1519 "Apply: Overworld Artist Layout", CommandCategory::kLayout,
1520 "Switch to visual and overworld focused layout", "", [this]() {
1521 if (editor_manager_) {
1522 editor_manager_->ApplyLayoutPreset("Overworld Artist");
1523 }
1524 });
1525
1526 command_palette_.AddCommand(
1527 "Apply: Dungeon Master Layout", CommandCategory::kLayout,
1528 "Switch to comprehensive dungeon editing layout", "", [this]() {
1529 if (editor_manager_) {
1530 editor_manager_->ApplyLayoutPreset("Dungeon Master");
1531 }
1532 });
1533
1534 command_palette_.AddCommand(
1535 "Apply: Audio Engineer Layout", CommandCategory::kLayout,
1536 "Switch to music and sound editing layout", "", [this]() {
1537 if (editor_manager_) {
1538 editor_manager_->ApplyLayoutPreset("Audio Engineer");
1539 }
1540 });
1541
1542 // Register recent files commands
1543 command_palette_.RegisterProvider(
1544 std::make_unique<RecentFilesCommandsProvider>(
1545 [this](const std::string& filepath) {
1546 if (editor_manager_) {
1547 auto status = editor_manager_->OpenRomOrProject(filepath);
1548 if (!status.ok()) {
1549 toast_manager_.Show(
1550 absl::StrFormat("Failed to open: %s", status.message()),
1551 ToastType::kError);
1552 }
1553 }
1554 }));
1555
1556 // Dungeon navigation helpers (room jump by id/label).
1557 command_palette_.RegisterProvider(
1558 std::make_unique<DungeonRoomCommandsProvider>(session_id));
1559
1560 // Welcome-screen-scoped commands: per-entry pin/remove, undo, template
1561 // creation, and visibility toggles. Recent-entry iteration pulls from the
1562 // same model the welcome screen renders, so the palette and the cards stay
1563 // in sync without a separate refresh path. We rebuild these whenever
1564 // RefreshCommandPalette() is invoked after recents mutate.
1565 if (welcome_screen_) {
1566 WelcomeCommandsProvider::Callbacks welcome_callbacks;
1567 welcome_callbacks.model = &welcome_screen_->recent_projects();
1568 welcome_callbacks.template_names = {
1569 "Vanilla ROM Hack", "ZSCustomOverworld v3", "ZSCustomOverworld v2",
1570 "Randomizer Compatible"};
1571 welcome_callbacks.remove = [this](const std::string& path) {
1572 if (!welcome_screen_)
1573 return;
1574 welcome_screen_->recent_projects().RemoveRecent(path);
1575 welcome_screen_->RefreshRecentProjects(/*force=*/true);
1576 toast_manager_.Show(
1577 absl::StrFormat("Removed %s from recents",
1578 std::filesystem::path(path).filename().string()),
1579 ToastType::kInfo);
1580 };
1581 welcome_callbacks.toggle_pin = [this](const std::string& path) {
1582 if (!welcome_screen_)
1583 return;
1584 auto& model = welcome_screen_->recent_projects();
1585 bool currently_pinned = false;
1586 for (const auto& entry : model.entries()) {
1587 if (entry.filepath == path) {
1588 currently_pinned = entry.pinned;
1589 break;
1590 }
1591 }
1592 model.SetPinned(path, !currently_pinned);
1593 welcome_screen_->RefreshRecentProjects(/*force=*/true);
1594 };
1595 welcome_callbacks.undo_remove = [this]() {
1596 if (!welcome_screen_)
1597 return;
1598 if (!welcome_screen_->recent_projects().UndoLastRemoval()) {
1599 toast_manager_.Show("Nothing to undo — the last removal has expired.",
1600 ToastType::kWarning);
1601 return;
1602 }
1603 welcome_screen_->RefreshRecentProjects(/*force=*/true);
1604 };
1605 welcome_callbacks.clear_recents = [this]() {
1606 if (!welcome_screen_)
1607 return;
1608 welcome_screen_->recent_projects().ClearAll();
1609 welcome_screen_->RefreshRecentProjects(/*force=*/true);
1610 };
1611 welcome_callbacks.create_from_template =
1612 [this](const std::string& template_name) {
1613 if (!editor_manager_)
1614 return;
1615 auto status = editor_manager_->CreateNewProject(template_name);
1616 if (!status.ok()) {
1617 toast_manager_.Show(absl::StrFormat("Failed to create project: %s",
1618 status.message()),
1619 ToastType::kError);
1620 } else {
1621 SetStartupSurface(StartupSurface::kDashboard);
1622 }
1623 };
1624 welcome_callbacks.dismiss_welcome = [this]() {
1625 welcome_screen_manually_closed_ = true;
1626 if (current_startup_surface_ == StartupSurface::kWelcome) {
1627 SetStartupSurface(StartupSurface::kDashboard);
1628 }
1629 };
1630 welcome_callbacks.show_welcome = [this]() {
1631 welcome_screen_manually_closed_ = false;
1632 SetStartupSurface(StartupSurface::kWelcome);
1633 };
1634 command_palette_.RegisterProvider(std::make_unique<WelcomeCommandsProvider>(
1635 std::move(welcome_callbacks)));
1636 }
1637
1638 // Load command usage history
1639 auto config_dir = util::PlatformPaths::GetConfigDirectory();
1640 if (config_dir.ok()) {
1641 std::filesystem::path history_file = *config_dir / "command_history.json";
1642 command_palette_.LoadHistory(history_file.string());
1643 }
1644
1645 command_palette_initialized_ = true;
1646}
1647
1648void UICoordinator::RefreshWorkflowActions() {
1649 ContentRegistry::WorkflowActions::Clear();
1650
1651 if (editor_manager_ && editor_manager_->GetCurrentProject() &&
1652 editor_manager_->GetCurrentProject()->project_opened()) {
1653 ContentRegistry::WorkflowActions::Register(
1654 {.id = "workflow.project.build",
1655 .group = "Build & Run",
1656 .label = "Build Project",
1657 .description = "Run the active project's configured build command",
1658 .shortcut = "",
1659 .priority = 5,
1660 .callback = [this]() {
1661 if (editor_manager_) {
1662 editor_manager_->QueueBuildCurrentProject();
1663 }
1664 }});
1665 ContentRegistry::WorkflowActions::Register(
1666 {.id = "workflow.project.run",
1667 .group = "Build & Run",
1668 .label = "Run Project Output",
1669 .description = "Open the active project's configured run target in a "
1670 "test session",
1671 .shortcut = "",
1672 .priority = 10,
1673 .callback = [this]() {
1674 if (editor_manager_) {
1675 (void)editor_manager_->RunCurrentProject();
1676 }
1677 }});
1678 }
1679
1680#ifdef YAZE_WITH_GRPC
1681 auto* emu_backend = Application::Instance().GetEmulatorBackend();
1682 if (emu_backend) {
1683 ContentRegistry::WorkflowActions::Register(
1684 {.id = "workflow.mesen.connect",
1685 .group = "Live Debugging",
1686 .label = "Connect Mesen2",
1687 .description =
1688 "Auto-discover and connect to the active Mesen2 backend",
1689 .shortcut = "",
1690 .priority = 10,
1691 .callback = [this]() {
1692 auto* backend = Application::Instance().GetEmulatorBackend();
1693 if (backend) {
1694 toast_manager_.Show("Mesen2 connection attempt queued",
1695 ToastType::kInfo);
1696 }
1697 }});
1698 ContentRegistry::WorkflowActions::Register(
1699 {.id = "workflow.mesen.step_over",
1700 .group = "Live Debugging",
1701 .label = "Mesen2 Step Over",
1702 .description = "Execute one instruction without entering subroutines",
1703 .shortcut = "F10",
1704 .priority = 20,
1705 .callback = [emu_backend]() { emu_backend->StepOver(); }});
1706 ContentRegistry::WorkflowActions::Register(
1707 {.id = "workflow.mesen.step_out",
1708 .group = "Live Debugging",
1709 .label = "Mesen2 Step Out",
1710 .description = "Run until return from the current subroutine",
1711 .shortcut = "Shift+F11",
1712 .priority = 30,
1713 .callback = [emu_backend]() { emu_backend->StepOut(); }});
1714 ContentRegistry::WorkflowActions::Register(
1715 {.id = "workflow.mesen.overlay_on",
1716 .group = "Live Debugging",
1717 .label = "Enable Collision Overlay",
1718 .description = "Show collision overlays in the active Mesen2 backend",
1719 .shortcut = "",
1720 .priority = 40,
1721 .callback = [emu_backend]() {
1722 emu_backend->SetCollisionOverlay(true);
1723 }});
1724 ContentRegistry::WorkflowActions::Register(
1725 {.id = "workflow.mesen.overlay_off",
1726 .group = "Live Debugging",
1727 .label = "Disable Collision Overlay",
1728 .description = "Hide collision overlays in the active Mesen2 backend",
1729 .shortcut = "",
1730 .priority = 50,
1731 .callback = [emu_backend]() {
1732 emu_backend->SetCollisionOverlay(false);
1733 }});
1734 }
1735#endif
1736}
1737
1738void UICoordinator::RefreshCommandPalette(size_t session_id) {
1739 InitializeCommandPalette(session_id);
1740}
1741
1742void UICoordinator::DrawGlobalSearch() {
1743 if (!show_global_search_)
1744 return;
1745
1746 ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
1747 ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
1748 ImGui::SetNextWindowSize(ImVec2(800, 600), ImGuiCond_FirstUseEver);
1749
1750 bool show_search = true;
1751 if (ImGui::Begin(
1752 absl::StrFormat("%s Global Search", ICON_MD_MANAGE_SEARCH).c_str(),
1753 &show_search, ImGuiWindowFlags_NoCollapse)) {
1754 // Enhanced search input with focus management
1755 ImGui::SetNextItemWidth(-100);
1756 if (ImGui::IsWindowAppearing()) {
1757 ImGui::SetKeyboardFocusHere();
1758 }
1759
1760 bool input_changed = ImGui::InputTextWithHint(
1761 "##global_query",
1762 absl::StrFormat("%s Search everything...", ICON_MD_SEARCH).c_str(),
1763 global_search_query_, IM_ARRAYSIZE(global_search_query_));
1764
1765 ImGui::SameLine();
1766 if (ImGui::Button(absl::StrFormat("%s Clear", ICON_MD_CLEAR).c_str())) {
1767 global_search_query_[0] = '\0';
1768 input_changed = true;
1769 }
1770
1771 ImGui::Separator();
1772
1773 // Tabbed search results for better organization
1774 if (gui::BeginThemedTabBar("SearchResultTabs")) {
1775 // Recent Files Tab
1776 if (ImGui::BeginTabItem(
1777 absl::StrFormat("%s Recent Files", ICON_MD_HISTORY).c_str())) {
1778 auto& manager = project::RecentFilesManager::GetInstance();
1779 auto recent_files = manager.GetRecentFiles();
1780
1781 if (ImGui::BeginTable("RecentFilesTable", 3,
1782 ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg |
1783 ImGuiTableFlags_SizingStretchProp)) {
1784 ImGui::TableSetupColumn("File", ImGuiTableColumnFlags_WidthStretch,
1785 0.6f);
1786 ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed,
1787 80.0f);
1788 ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthFixed,
1789 100.0f);
1790 ImGui::TableHeadersRow();
1791
1792 for (const auto& file : recent_files) {
1793 if (global_search_query_[0] != '\0' &&
1794 file.find(global_search_query_) == std::string::npos)
1795 continue;
1796
1797 ImGui::TableNextRow();
1798 ImGui::TableNextColumn();
1799 ImGui::Text("%s", util::GetFileName(file).c_str());
1800
1801 ImGui::TableNextColumn();
1802 std::string ext = util::GetFileExtension(file);
1803 if (ext == "sfc" || ext == "smc") {
1804 ImGui::TextColored(ImVec4(0.2f, 0.8f, 0.2f, 1.0f), "%s ROM",
1806 } else if (ext == "yaze") {
1807 ImGui::TextColored(ImVec4(0.2f, 0.6f, 0.8f, 1.0f), "%s Project",
1809 } else {
1810 ImGui::Text("%s File", ICON_MD_DESCRIPTION);
1811 }
1812
1813 ImGui::TableNextColumn();
1814 ImGui::PushID(file.c_str());
1815 if (ImGui::Button("Open")) {
1816 auto status = editor_manager_->OpenRomOrProject(file);
1817 if (!status.ok()) {
1818 toast_manager_.Show(
1819 absl::StrCat("Failed to open: ", status.message()),
1820 ToastType::kError);
1821 }
1822 SetGlobalSearchVisible(false);
1823 }
1824 ImGui::PopID();
1825 }
1826
1827 ImGui::EndTable();
1828 }
1829 ImGui::EndTabItem();
1830 }
1831
1832 // Labels Tab (only if ROM is loaded)
1833 auto* current_rom = editor_manager_->GetCurrentRom();
1834 if (current_rom && current_rom->resource_label()) {
1835 if (ImGui::BeginTabItem(
1836 absl::StrFormat("%s Labels", ICON_MD_LABEL).c_str())) {
1837 auto& labels = current_rom->resource_label()->labels_;
1838
1839 if (ImGui::BeginTable("LabelsTable", 3,
1840 ImGuiTableFlags_ScrollY |
1841 ImGuiTableFlags_RowBg |
1842 ImGuiTableFlags_SizingStretchProp)) {
1843 ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed,
1844 100.0f);
1845 ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthStretch,
1846 0.4f);
1847 ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch,
1848 0.6f);
1849 ImGui::TableHeadersRow();
1850
1851 for (const auto& type_pair : labels) {
1852 for (const auto& kv : type_pair.second) {
1853 if (global_search_query_[0] != '\0' &&
1854 kv.first.find(global_search_query_) == std::string::npos &&
1855 kv.second.find(global_search_query_) == std::string::npos)
1856 continue;
1857
1858 ImGui::TableNextRow();
1859 ImGui::TableNextColumn();
1860 ImGui::Text("%s", type_pair.first.c_str());
1861
1862 ImGui::TableNextColumn();
1863 if (ImGui::Selectable(kv.first.c_str(), false,
1864 ImGuiSelectableFlags_SpanAllColumns)) {
1865 // Future: navigate to related editor/location
1866 }
1867
1868 ImGui::TableNextColumn();
1869 ImGui::TextDisabled("%s", kv.second.c_str());
1870 }
1871 }
1872
1873 ImGui::EndTable();
1874 }
1875 ImGui::EndTabItem();
1876 }
1877 }
1878
1879 // Sessions Tab
1880 if (session_coordinator_.GetActiveSessionCount() > 1) {
1881 if (ImGui::BeginTabItem(
1882 absl::StrFormat("%s Sessions", ICON_MD_TAB).c_str())) {
1883 ImGui::Text("Search and switch between active sessions:");
1884
1885 for (size_t i = 0; i < session_coordinator_.GetTotalSessionCount();
1886 ++i) {
1887 std::string session_info =
1888 session_coordinator_.GetSessionDisplayName(i);
1889 if (session_info == "[CLOSED SESSION]")
1890 continue;
1891
1892 if (global_search_query_[0] != '\0' &&
1893 session_info.find(global_search_query_) == std::string::npos)
1894 continue;
1895
1896 bool is_current =
1897 (i == session_coordinator_.GetActiveSessionIndex());
1898 std::optional<gui::StyleColorGuard> current_guard;
1899 if (is_current) {
1900 current_guard.emplace(ImGuiCol_Text,
1901 ImVec4(0.2f, 0.8f, 0.2f, 1.0f));
1902 }
1903
1904 if (ImGui::Selectable(absl::StrFormat("%s %s %s", ICON_MD_TAB,
1905 session_info.c_str(),
1906 is_current ? "(Current)" : "")
1907 .c_str())) {
1908 if (!is_current) {
1909 editor_manager_->SwitchToSession(i);
1910 SetGlobalSearchVisible(false);
1911 }
1912 }
1913 }
1914 ImGui::EndTabItem();
1915 }
1916 }
1917
1918 gui::EndThemedTabBar();
1919 }
1920
1921 // Status bar
1922 ImGui::Separator();
1923 ImGui::Text("%s Global search across all YAZE data", ICON_MD_INFO);
1924 }
1925 ImGui::End();
1926
1927 // Update visibility state
1928 if (!show_search) {
1929 SetGlobalSearchVisible(false);
1930 }
1931}
1932
1933// ============================================================================
1934// Startup Surface Management (Single Source of Truth)
1935// ============================================================================
1936
1937void UICoordinator::SetStartupSurface(StartupSurface surface) {
1938 StartupSurface old_surface = current_startup_surface_;
1939 current_startup_surface_ = surface;
1940
1941 // Log state transitions for debugging
1942 const char* surface_names[] = {"Welcome", "Dashboard", "Editor"};
1943 LOG_INFO("UICoordinator", "Startup surface: %s -> %s",
1944 surface_names[static_cast<int>(old_surface)],
1945 surface_names[static_cast<int>(surface)]);
1946
1947 // Update dependent visibility flags
1948 switch (surface) {
1949 case StartupSurface::kWelcome:
1950 show_welcome_screen_ = true;
1951 show_editor_selection_ = false; // Dashboard hidden
1952 // Activity Bar will be hidden (checked via ShouldShowActivityBar)
1953 break;
1954 case StartupSurface::kDashboard:
1955 show_welcome_screen_ = false;
1956 show_editor_selection_ = true; // Dashboard shown
1957 break;
1958 case StartupSurface::kEditor:
1959 show_welcome_screen_ = false;
1960 show_editor_selection_ = false; // Dashboard hidden
1961 break;
1962 }
1963}
1964
1965bool UICoordinator::ShouldShowWelcome() const {
1966 // Respect CLI overrides
1967 if (welcome_behavior_override_ == StartupVisibility::kHide) {
1968 return false;
1969 }
1970 if (welcome_behavior_override_ == StartupVisibility::kShow) {
1971 return true;
1972 }
1973
1974 // Default: show welcome only when in welcome state and not manually closed
1975 return current_startup_surface_ == StartupSurface::kWelcome &&
1976 !welcome_screen_manually_closed_;
1977}
1978
1979bool UICoordinator::ShouldShowDashboard() const {
1980 // Respect CLI overrides
1981 if (dashboard_behavior_override_ == StartupVisibility::kHide) {
1982 return false;
1983 }
1984 if (dashboard_behavior_override_ == StartupVisibility::kShow) {
1985 return true;
1986 }
1987
1988 // Default: show dashboard only when in dashboard state
1989 return current_startup_surface_ == StartupSurface::kDashboard;
1990}
1991
1992bool UICoordinator::ShouldShowActivityBar() const {
1993 // Sidebar would consume the entire screen on compact (iPhone portrait)
1994 if (IsCompactLayout()) {
1995 return false;
1996 }
1997
1998 // Activity Bar hidden on cold start (welcome screen)
1999 // Only show after ROM is loaded
2000 if (current_startup_surface_ == StartupSurface::kWelcome) {
2001 return false;
2002 }
2003
2004 // Check if ROM is actually loaded
2005 if (editor_manager_) {
2006 auto* current_rom = editor_manager_->GetCurrentRom();
2007 if (!current_rom || !current_rom->is_loaded()) {
2008 return false;
2009 }
2010 }
2011
2012 return true;
2013}
2014
2015} // namespace editor
2016} // namespace yaze
auto short_name() const
Definition rom.h:147
bool is_loaded() const
Definition rom.h:132
UICoordinator(EditorManager *editor_manager, RomFileManager &rom_manager, ProjectManager &project_manager, EditorRegistry &editor_registry, WorkspaceWindowManager &card_registry, SessionCoordinator &session_coordinator, WindowDelegate &window_delegate, ToastManager &toast_manager, PopupManager &popup_manager, ShortcutManager &shortcut_manager)
#define ICON_MD_NOTIFICATIONS
Definition icons.h:1335
#define ICON_MD_SETTINGS
Definition icons.h:1699
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_FOLDER_SPECIAL
Definition icons.h:815
#define ICON_MD_SEARCH
Definition icons.h:1673
#define ICON_MD_STAR
Definition icons.h:1848
#define ICON_MD_FULLSCREEN_EXIT
Definition icons.h:862
#define ICON_MD_EXPAND_LESS
Definition icons.h:702
#define ICON_MD_LABEL
Definition icons.h:1053
#define ICON_MD_LIST_ALT
Definition icons.h:1095
#define ICON_MD_VIDEOGAME_ASSET
Definition icons.h:2076
#define ICON_MD_VISIBILITY
Definition icons.h:2101
#define ICON_MD_LIST
Definition icons.h:1094
#define ICON_MD_MANAGE_SEARCH
Definition icons.h:1172
#define ICON_MD_VISIBILITY_OFF
Definition icons.h:2102
#define ICON_MD_LAYERS
Definition icons.h:1068
#define ICON_MD_CLEAR
Definition icons.h:416
#define ICON_MD_DESCRIPTION
Definition icons.h:539
#define ICON_MD_HELP_OUTLINE
Definition icons.h:935
#define ICON_MD_DASHBOARD
Definition icons.h:517
#define ICON_MD_TAB
Definition icons.h:1930
#define ICON_MD_FOLDER
Definition icons.h:809
#define ICON_MD_PUSH_PIN
Definition icons.h:1529
#define ICON_MD_FIBER_MANUAL_RECORD
Definition icons.h:739
#define ICON_MD_SMART_TOY
Definition icons.h:1781
#define ICON_MD_HISTORY
Definition icons.h:946
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define LOG_INFO(category, format,...)
Definition log.h:105
Definition input.cc:22
Rom * rom()
Get the current ROM instance.
StartupSurface
Represents the current startup surface state.
std::string PrintShortcut(const std::vector< ImGuiKey > &keys)
ImVec4 GetTextSecondaryVec4()
bool IsKeyPressed(KeyboardState state, SDL_Scancode scancode)
Check if a key is pressed using the keyboard state.
Definition sdl_compat.h:142