294 std::function<
bool()> has_rom) {
296 const ImGuiViewport* viewport = ImGui::GetMainViewport();
298 const float panel_width =
303 const float bottom_reserved =
307 const float panel_height =
308 std::max(0.0f, viewport->WorkSize.y - top_inset - safe_area.bottom -
313 ImVec2(viewport->WorkPos.x + bar_width, viewport->WorkPos.y + top_inset),
314 ImVec2(panel_width, panel_height),
316 .padding = {10.0f, 10.0f},
319 ImGuiWindowFlags_NoFocusOnAppearing);
325 ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]);
326 ImGui::Text(
"%s", category.c_str());
329 float avail_width = ImGui::GetContentRegionAvail().x;
331 ImGui::SameLine(ImGui::GetCursorPosX() + avail_width - chrome_icon_size.x);
333 "Collapse Sidebar",
false, ImVec4(0, 0, 0, 0),
334 "window_sidebar",
"collapse_side_panel")) {
342 const float standard_spacing =
344 const float compact_spacing = std::max(4.0f, standard_spacing * 0.75f);
345 const float search_spacing = compact_spacing;
347 auto read_dungeon_workbench_mode = [&]() ->
bool {
348 if (category !=
"Dungeon") {
356 bool dungeon_workbench_mode = read_dungeon_workbench_mode();
357 const bool rom_loaded = has_rom ? has_rom() :
true;
358 const bool disable_windows = !rom_loaded && category !=
"Emulator";
359 const bool bulk_actions_enabled =
360 !disable_windows && !(category ==
"Dungeon" && dungeon_workbench_mode);
362 const float search_width = std::max(96.0f, ImGui::GetContentRegionAvail().x -
363 (search_button_size.x * 2.0f) -
364 (search_spacing * 2.0f));
365 ImGui::SetNextItemWidth(search_width);
366 ImGui::InputTextWithHint(
"##SidebarSearch",
ICON_MD_SEARCH " Filter...",
368 ImGui::SameLine(0.0f, search_spacing);
371 ImGui::BeginDisabled();
374 "Clear filter",
false,
376 "clear_sidebar_filter")) {
380 ImGui::EndDisabled();
382 ImGui::SameLine(0.0f, search_spacing);
384 "Window actions",
false,
386 "open_sidebar_actions")) {
387 ImGui::OpenPopup(
"##WindowSidebarActions");
389 if (ImGui::BeginPopup(
"##WindowSidebarActions")) {
390 const auto category_windows =
392 int visible_windows = 0;
393 int listed_windows = 0;
394 for (
const auto& category_window : category_windows) {
395 if (!category_window.IsListedInWindowBrowser()) {
399 if (category_window.visibility_flag && *category_window.visibility_flag) {
403 ImGui::TextDisabled(tr(
"%d of %zu visible"), visible_windows,
404 static_cast<size_t>(listed_windows));
411 bulk_actions_enabled)) {
414 const bool show_all_hovered =
415 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled);
417 bulk_actions_enabled)) {
420 const bool hide_all_hovered =
421 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled);
422 if (!bulk_actions_enabled && (show_all_hovered || hide_all_hovered) &&
423 category ==
"Dungeon" && dungeon_workbench_mode) {
425 tr(
"Switch to Window workflow to bulk-manage room windows."));
430 auto switch_to_dungeon_workbench_mode = [&]() ->
bool {
431 if (category !=
"Dungeon" || dungeon_workbench_mode) {
436 dungeon_workbench_mode =
true;
444 for (
const auto& descriptor :
446 const std::string& window_id = descriptor.card_id;
447 if (window_id ==
"dungeon.workbench") {
453 if (IsDungeonPanelModeWindow(window_id)) {
457 dungeon_workbench_mode = read_dungeon_workbench_mode();
458 return dungeon_workbench_mode;
461 auto switch_to_dungeon_window_mode = [&]() ->
bool {
462 if (category !=
"Dungeon" || !dungeon_workbench_mode) {
467 dungeon_workbench_mode =
false;
473 dungeon_workbench_mode = read_dungeon_workbench_mode();
474 return !dungeon_workbench_mode;
477 auto ensure_dungeon_window_mode_for_window =
478 [&](
const std::string& window_id) ->
bool {
479 if (category !=
"Dungeon" || !dungeon_workbench_mode ||
480 !IsDungeonPanelModeWindow(window_id)) {
483 return switch_to_dungeon_window_mode();
486 if (category ==
"Dungeon") {
487 const float workflow_gap = compact_spacing;
488 const float workflow_min_button_width = 96.0f;
489 const float workflow_available_width =
490 std::max(1.0f, ImGui::GetContentRegionAvail().x);
491 const bool stack_workflow_buttons =
492 workflow_available_width <=
493 (workflow_min_button_width * 2.0f + workflow_gap);
494 const float workflow_button_width =
495 stack_workflow_buttons
496 ? workflow_available_width
497 : std::max(workflow_min_button_width,
498 (workflow_available_width - workflow_gap) * 0.5f);
499 const float workflow_button_height =
501 auto draw_workflow_button =
502 [&](
const char* id,
const char* icon,
const char* label,
bool active,
503 const char* tooltip,
const ImVec2& button_size) ->
bool {
508 {{ImGuiCol_Button, active ? active_bg : inactive_bg},
509 {ImGuiCol_ButtonHovered, active ? active_bg : inactive_hover},
510 {ImGuiCol_ButtonActive, active ? active_bg : inactive_hover}});
511 const std::string button_label =
512 absl::StrFormat(
"%s %s##%s", icon, label,
id);
513 const bool clicked = ImGui::Button(button_label.c_str(), button_size);
514 if (ImGui::IsItemHovered() && tooltip && *tooltip) {
515 ImGui::SetTooltip(
"%s", tooltip);
519 const ImVec2 workflow_button_size(workflow_button_width,
520 workflow_button_height);
522 if (draw_workflow_button(
524 dungeon_workbench_mode,
525 "Workbench mode: integrated room browser + inspector",
526 workflow_button_size)) {
527 switch_to_dungeon_workbench_mode();
529 if (!stack_workflow_buttons) {
530 ImGui::SameLine(0.0f, workflow_gap);
532 if (draw_workflow_button(
534 !dungeon_workbench_mode,
535 "Window mode: standalone Room List + Room Matrix + room windows",
536 workflow_button_size)) {
537 switch_to_dungeon_window_mode();
544 if (disable_windows) {
546 " Open a ROM to enable this category");
550 if (disable_windows) {
551 ImGui::BeginDisabled();
555 const ImVec4 disabled_text = ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled);
556 auto window_text_color = [&](
bool visible) -> ImVec4 {
557 if (disable_windows) {
558 return disabled_text;
563 auto draw_pin_toggle_button = [&](
const std::string& widget_id,
564 bool pinned) ->
bool {
565 ImGui::PushID(widget_id.c_str());
573 pinned ?
"Unpin window" :
"Pin window", pinned, pin_col,
574 "window_sidebar", widget_id.c_str());
579 bool pinned_section_open =
false;
581 bool has_pinned_in_category =
false;
582 for (
const auto& window_id : pinned_windows) {
585 if (window && window->category == category && window->IsAdmitted()) {
586 has_pinned_in_category =
true;
591 if (has_pinned_in_category) {
592 pinned_section_open = ImGui::CollapsingHeader(
594 if (pinned_section_open) {
595 for (
const auto& window_id : pinned_windows) {
598 if (!window || window->category != category ||
599 !window->IsAdmitted()) {
606 std::string label = absl::StrFormat(
"%s %s", window->icon.c_str(),
607 window->display_name.c_str());
608 const float selectable_width =
609 std::max(1.0f, ImGui::GetContentRegionAvail().x -
610 pin_button_size.x - compact_spacing);
612 (std::string(
"pinned_select_") + window->card_id).c_str());
615 window_text_color(visible));
616 ImVec2 item_size(selectable_width, pin_button_size.y);
617 if (ImGui::Selectable(label.c_str(), visible,
618 ImGuiSelectableFlags_None, item_size)) {
619 const bool switched_mode =
620 ensure_dungeon_window_mode_for_window(window->card_id);
627 const bool new_visible =
628 window->visibility_flag ? *window->visibility_flag :
false;
631 const std::string window_name =
633 if (!window_name.empty()) {
634 ImGui::SetWindowFocus(window_name.c_str());
640 ImGui::SameLine(0.0f, compact_spacing);
641 if (draw_pin_toggle_button(
"pin_" + window->card_id,
true)) {
657 std::map<std::string, std::vector<WindowDescriptor>> sections;
658 for (
const auto& window : windows) {
659 if (!window.IsListedInWindowBrowser()) {
666 window.card_id, window.shortcut_hint)) {
670 if (pinned_section_open && is_pinned) {
677 for (
auto& [section_name, section_windows] : sections) {
679 std::sort(section_windows.begin(), section_windows.end(),
681 if (a.priority != b.priority) {
682 return a.priority < b.priority;
688 constexpr const char* kPreferredSectionOrder[] = {
694 std::vector<std::string> section_order;
695 for (
const char* preferred : kPreferredSectionOrder) {
696 if (sections.count(preferred)) {
697 section_order.emplace_back(preferred);
700 for (
const auto& [name, _] : sections) {
701 if (std::find(section_order.begin(), section_order.end(), name) ==
702 section_order.end()) {
703 section_order.push_back(name);
707 auto draw_window_row = [&](
const WindowDescriptor& window) {
708 const bool is_pinned = window_manager_.IsWindowPinned(window.card_id);
710 window.visibility_flag ? *window.visibility_flag :
false;
712 std::string label = absl::StrFormat(
"%s %s", window.icon.c_str(),
713 window.display_name.c_str());
714 const float selectable_width =
715 std::max(1.0f, ImGui::GetContentRegionAvail().x - pin_button_size.x -
717 bool window_row_hovered =
false;
718 ImGui::PushID((std::string(
"window_select_") + window.card_id).c_str());
720 gui::StyleColorGuard text_color(ImGuiCol_Text,
721 window_text_color(visible));
722 ImVec2 item_size(selectable_width, pin_button_size.y);
723 if (ImGui::Selectable(label.c_str(), visible, ImGuiSelectableFlags_None,
725 const bool switched_mode =
726 ensure_dungeon_window_mode_for_window(window.card_id);
728 window_manager_.OpenWindow(session_id, window.card_id);
730 window_manager_.ToggleWindow(session_id, window.card_id);
733 const bool new_visible =
734 window.visibility_flag ? *window.visibility_flag :
false;
736 window_manager_.MarkWindowRecentlyUsed(window.card_id);
737 window_manager_.TriggerWindowClicked(window.category);
738 const std::string window_name =
739 window_manager_.GetWorkspaceWindowName(window);
740 if (!window_name.empty()) {
741 ImGui::SetWindowFocus(window_name.c_str());
745 window_row_hovered = ImGui::IsItemHovered();
748 ImGui::SameLine(0.0f, compact_spacing);
749 if (draw_pin_toggle_button(
"pin_" + window.card_id, is_pinned)) {
750 window_manager_.SetWindowPinned(session_id, window.card_id, !is_pinned);
753 if (window_row_hovered && !window.shortcut_hint.empty()) {
754 ImGui::SetTooltip(
"%s", window.shortcut_hint.c_str());
758 const EditorContextSnapshot context_snapshot =
759 context_provider_ ? context_provider_(category) : EditorContextSnapshot{};
760 const bool has_context = HasRenderableContext(context_snapshot);
762 ImGui::EndDisabled();
763 const bool window_content_open =
764 ImGui::BeginChild(
"##WindowContent", ImVec2(0.0f, 0.0f),
false);
765 if (window_content_open) {
766 ImGui::BeginDisabled(disable_windows);
767 for (
const std::string& section_name : section_order) {
768 auto it = sections.find(section_name);
769 if (it == sections.end() || it->second.empty()) {
776 for (
const auto& window : it->second) {
777 draw_window_row(window);
784 ImGui::SeparatorText(section_name.c_str());
785 for (
const auto& window : it->second)
786 draw_window_row(window);
788 ImGui::EndDisabled();
790 if (window_content_open && has_context)
791 DrawEditorContext(session_id, category, context_snapshot);
794 const float handle_width = 6.0f;
795 const ImVec2 panel_pos = ImGui::GetWindowPos();
796 const float panel_draw_height = ImGui::GetWindowHeight();
797 ImGui::SetCursorScreenPos(
798 ImVec2(panel_pos.x + panel_width - handle_width * 0.5f, panel_pos.y));
799 ImGui::InvisibleButton(
"##SidePanelResizeHandle",
800 ImVec2(handle_width, panel_draw_height));
801 const bool handle_hovered = ImGui::IsItemHovered();
802 const bool handle_active = ImGui::IsItemActive();
803 if (handle_hovered || handle_active) {
804 ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
806 if (handle_hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
807 window_manager_.ResetSidePanelWidth();
810 const float new_width = panel_width + ImGui::GetIO().MouseDelta.x;
811 window_manager_.SetActiveSidePanelWidth(new_width, viewport->WorkSize.x);
813 tr(
"Width: %.0f px"),
814 window_manager_.GetActiveSidePanelWidth(viewport->WorkSize.x));
818 handle_color.w = handle_active ? 0.95f : (handle_hovered ? 0.72f : 0.35f);
819 ImGui::GetWindowDrawList()->AddLine(
820 ImVec2(panel_pos.x + panel_width - 1.0f, panel_pos.y),
821 ImVec2(panel_pos.x + panel_width - 1.0f, panel_pos.y + panel_draw_height),
822 ImGui::GetColorU32(handle_color), handle_active ? 2.0f : 1.0f);