112 std::function<
bool()> has_rom) {
114 const ImGuiViewport* viewport = ImGui::GetMainViewport();
116 const float panel_width =
121 const float bottom_reserved =
125 const float panel_height =
126 std::max(0.0f, viewport->WorkSize.y - top_inset - safe_area.bottom -
131 ImVec2(viewport->WorkPos.x + bar_width, viewport->WorkPos.y + top_inset),
132 ImVec2(panel_width, panel_height),
134 .padding = {8.0f, 8.0f},
137 ImGuiWindowFlags_NoFocusOnAppearing);
143 ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]);
144 ImGui::Text(
"%s", category.c_str());
147 float avail_width = ImGui::GetContentRegionAvail().x;
149 ImGui::SameLine(ImGui::GetCursorPosX() + avail_width - chrome_icon_size.x);
151 "Collapse Sidebar",
false, ImVec4(0, 0, 0, 0),
152 "window_sidebar",
"collapse_side_panel")) {
160 const float standard_spacing =
162 const float compact_spacing = std::max(4.0f, standard_spacing * 0.75f);
163 const float search_spacing = compact_spacing;
165 auto read_dungeon_workbench_mode = [&]() ->
bool {
166 if (category !=
"Dungeon") {
174 bool dungeon_workbench_mode = read_dungeon_workbench_mode();
175 const bool rom_loaded = has_rom ? has_rom() :
true;
176 const bool disable_windows = !rom_loaded && category !=
"Emulator";
177 const bool bulk_actions_enabled =
178 !disable_windows && !(category ==
"Dungeon" && dungeon_workbench_mode);
180 const float search_width = std::max(96.0f, ImGui::GetContentRegionAvail().x -
181 (search_button_size.x * 2.0f) -
182 (search_spacing * 2.0f));
183 ImGui::SetNextItemWidth(search_width);
184 ImGui::InputTextWithHint(
"##SidebarSearch",
ICON_MD_SEARCH " Filter...",
186 ImGui::SameLine(0.0f, search_spacing);
189 ImGui::BeginDisabled();
192 "Clear filter",
false,
194 "clear_sidebar_filter")) {
198 ImGui::EndDisabled();
200 ImGui::SameLine(0.0f, search_spacing);
202 "Window actions",
false,
204 "open_sidebar_actions")) {
205 ImGui::OpenPopup(
"##WindowSidebarActions");
207 if (ImGui::BeginPopup(
"##WindowSidebarActions")) {
208 const auto category_windows =
210 int visible_windows = 0;
211 for (
const auto& category_window : category_windows) {
212 if (category_window.visibility_flag && *category_window.visibility_flag) {
216 ImGui::TextDisabled(tr(
"%d of %zu visible"), visible_windows,
217 category_windows.size());
224 bulk_actions_enabled)) {
227 const bool show_all_hovered =
228 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled);
230 bulk_actions_enabled)) {
233 const bool hide_all_hovered =
234 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled);
235 if (!bulk_actions_enabled && (show_all_hovered || hide_all_hovered) &&
236 category ==
"Dungeon" && dungeon_workbench_mode) {
238 tr(
"Switch to Window workflow to bulk-manage room windows."));
243 auto switch_to_dungeon_workbench_mode = [&]() ->
bool {
244 if (category !=
"Dungeon" || dungeon_workbench_mode) {
249 dungeon_workbench_mode =
true;
257 for (
const auto& descriptor :
259 const std::string& window_id = descriptor.card_id;
260 if (window_id ==
"dungeon.workbench") {
266 if (IsDungeonPanelModeWindow(window_id)) {
270 dungeon_workbench_mode = read_dungeon_workbench_mode();
271 return dungeon_workbench_mode;
274 auto switch_to_dungeon_window_mode = [&]() ->
bool {
275 if (category !=
"Dungeon" || !dungeon_workbench_mode) {
280 dungeon_workbench_mode =
false;
286 dungeon_workbench_mode = read_dungeon_workbench_mode();
287 return !dungeon_workbench_mode;
290 auto ensure_dungeon_window_mode_for_window =
291 [&](
const std::string& window_id) ->
bool {
292 if (category !=
"Dungeon" || !dungeon_workbench_mode ||
293 !IsDungeonPanelModeWindow(window_id)) {
296 return switch_to_dungeon_window_mode();
299 if (category ==
"Dungeon") {
300 const float workflow_gap = compact_spacing;
301 const float workflow_min_button_width = 96.0f;
302 const float workflow_available_width =
303 std::max(1.0f, ImGui::GetContentRegionAvail().x);
304 const bool stack_workflow_buttons =
305 workflow_available_width <=
306 (workflow_min_button_width * 2.0f + workflow_gap);
307 const float workflow_button_width =
308 stack_workflow_buttons
309 ? workflow_available_width
310 : std::max(workflow_min_button_width,
311 (workflow_available_width - workflow_gap) * 0.5f);
312 const float workflow_button_height =
314 auto draw_workflow_button =
315 [&](
const char* id,
const char* icon,
const char* label,
bool active,
316 const char* tooltip,
const ImVec2& button_size) ->
bool {
321 {{ImGuiCol_Button, active ? active_bg : inactive_bg},
322 {ImGuiCol_ButtonHovered, active ? active_bg : inactive_hover},
323 {ImGuiCol_ButtonActive, active ? active_bg : inactive_hover}});
324 const std::string button_label =
325 absl::StrFormat(
"%s %s##%s", icon, label,
id);
326 const bool clicked = ImGui::Button(button_label.c_str(), button_size);
327 if (ImGui::IsItemHovered() && tooltip && *tooltip) {
328 ImGui::SetTooltip(
"%s", tooltip);
332 const ImVec2 workflow_button_size(workflow_button_width,
333 workflow_button_height);
335 if (draw_workflow_button(
337 dungeon_workbench_mode,
338 "Workbench mode: integrated room browser + inspector",
339 workflow_button_size)) {
340 switch_to_dungeon_workbench_mode();
342 if (!stack_workflow_buttons) {
343 ImGui::SameLine(0.0f, workflow_gap);
345 if (draw_workflow_button(
347 !dungeon_workbench_mode,
348 "Window mode: standalone Room List + Room Matrix + room windows",
349 workflow_button_size)) {
350 switch_to_dungeon_window_mode();
357 if (disable_windows) {
359 " Open a ROM to enable this category");
363 if (disable_windows) {
364 ImGui::BeginDisabled();
368 const ImVec4 disabled_text = ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled);
369 auto window_text_color = [&](
bool visible) -> ImVec4 {
370 if (disable_windows) {
371 return disabled_text;
375 const float pin_button_side =
377 const ImVec2 pin_button_size(pin_button_side, pin_button_side);
378 auto draw_pin_toggle_button = [&](
const std::string& widget_id,
379 bool pinned) ->
bool {
380 ImGui::PushID(widget_id.c_str());
385 pinned ?
"Unpin window" :
"Pin window", pinned, pin_col,
386 "window_sidebar", widget_id.c_str());
391 bool pinned_section_open =
false;
393 bool has_pinned_in_category =
false;
394 for (
const auto& window_id : pinned_windows) {
397 if (window && window->category == category) {
398 has_pinned_in_category =
true;
403 if (has_pinned_in_category) {
404 pinned_section_open = ImGui::CollapsingHeader(
406 if (pinned_section_open) {
407 for (
const auto& window_id : pinned_windows) {
410 if (!window || window->category != category) {
417 if (draw_pin_toggle_button(
"pin_" + window->card_id,
true)) {
421 ImGui::SameLine(0.0f, compact_spacing);
423 std::string label = absl::StrFormat(
"%s %s", window->icon.c_str(),
424 window->display_name.c_str());
426 (std::string(
"pinned_select_") + window->card_id).c_str());
429 window_text_color(visible));
430 ImVec2 item_size(ImGui::GetContentRegionAvail().x,
432 if (ImGui::Selectable(label.c_str(), visible,
433 ImGuiSelectableFlags_None, item_size)) {
434 const bool switched_mode =
435 ensure_dungeon_window_mode_for_window(window->card_id);
442 const bool new_visible =
443 window->visibility_flag ? *window->visibility_flag :
false;
446 const std::string window_name =
448 if (!window_name.empty()) {
449 ImGui::SetWindowFocus(window_name.c_str());
468 std::map<std::string, std::vector<WindowDescriptor>> sections;
469 for (
const auto& window : windows) {
474 window.card_id, window.shortcut_hint)) {
478 if (pinned_section_open && is_pinned) {
485 for (
auto& [section_name, section_windows] : sections) {
487 std::sort(section_windows.begin(), section_windows.end(),
489 if (a.priority != b.priority) {
490 return a.priority < b.priority;
496 constexpr const char* kPreferredSectionOrder[] = {
502 std::vector<std::string> section_order;
503 for (
const char* preferred : kPreferredSectionOrder) {
504 if (sections.count(preferred)) {
505 section_order.emplace_back(preferred);
508 for (
const auto& [name, _] : sections) {
509 if (std::find(section_order.begin(), section_order.end(), name) ==
510 section_order.end()) {
511 section_order.push_back(name);
515 auto draw_window_row = [&](
const WindowDescriptor& window) {
516 const bool is_pinned = window_manager_.IsWindowPinned(window.card_id);
518 window.visibility_flag ? *window.visibility_flag :
false;
520 if (draw_pin_toggle_button(
"pin_" + window.card_id, is_pinned)) {
521 window_manager_.SetWindowPinned(session_id, window.card_id, !is_pinned);
523 ImGui::SameLine(0.0f, compact_spacing);
525 std::string label = absl::StrFormat(
"%s %s", window.icon.c_str(),
526 window.display_name.c_str());
527 ImGui::PushID((std::string(
"window_select_") + window.card_id).c_str());
529 gui::StyleColorGuard text_color(ImGuiCol_Text,
530 window_text_color(visible));
531 ImVec2 item_size(ImGui::GetContentRegionAvail().x, pin_button_size.y);
532 if (ImGui::Selectable(label.c_str(), visible, ImGuiSelectableFlags_None,
534 const bool switched_mode =
535 ensure_dungeon_window_mode_for_window(window.card_id);
537 window_manager_.OpenWindow(session_id, window.card_id);
539 window_manager_.ToggleWindow(session_id, window.card_id);
542 const bool new_visible =
543 window.visibility_flag ? *window.visibility_flag :
false;
545 window_manager_.MarkWindowRecentlyUsed(window.card_id);
546 window_manager_.TriggerWindowClicked(window.category);
547 const std::string window_name =
548 window_manager_.GetWorkspaceWindowName(window);
549 if (!window_name.empty()) {
550 ImGui::SetWindowFocus(window_name.c_str());
557 if (ImGui::IsItemHovered() && !window.shortcut_hint.empty()) {
558 ImGui::SetTooltip(
"%s", window.shortcut_hint.c_str());
564 if (window_content_open) {
565 for (
const std::string& section_name : section_order) {
566 auto it = sections.find(section_name);
567 if (it == sections.end() || it->second.empty()) {
574 for (
const auto& window : it->second) {
575 draw_window_row(window);
580 ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_None;
581 if (section_name ==
"Core") {
582 flags |= ImGuiTreeNodeFlags_DefaultOpen;
585 const std::string header = absl::StrFormat(
586 "%s##sidebar_section_%s", section_name.c_str(), section_name.c_str());
587 if (ImGui::CollapsingHeader(header.c_str(), flags)) {
588 for (
const auto& window : it->second) {
589 draw_window_row(window);
596 if (disable_windows) {
597 ImGui::EndDisabled();
600 const float handle_width = 6.0f;
601 const ImVec2 panel_pos = ImGui::GetWindowPos();
602 const float panel_draw_height = ImGui::GetWindowHeight();
603 ImGui::SetCursorScreenPos(
604 ImVec2(panel_pos.x + panel_width - handle_width * 0.5f, panel_pos.y));
605 ImGui::InvisibleButton(
"##SidePanelResizeHandle",
606 ImVec2(handle_width, panel_draw_height));
607 const bool handle_hovered = ImGui::IsItemHovered();
608 const bool handle_active = ImGui::IsItemActive();
609 if (handle_hovered || handle_active) {
610 ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW);
612 if (handle_hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
613 window_manager_.ResetSidePanelWidth();
616 const float new_width = panel_width + ImGui::GetIO().MouseDelta.x;
617 window_manager_.SetActiveSidePanelWidth(new_width, viewport->WorkSize.x);
619 tr(
"Width: %.0f px"),
620 window_manager_.GetActiveSidePanelWidth(viewport->WorkSize.x));
624 handle_color.w = handle_active ? 0.95f : (handle_hovered ? 0.72f : 0.35f);
625 ImGui::GetWindowDrawList()->AddLine(
626 ImVec2(panel_pos.x + panel_width - 1.0f, panel_pos.y),
627 ImVec2(panel_pos.x + panel_width - 1.0f, panel_pos.y + panel_draw_height),
628 ImGui::GetColorU32(handle_color), handle_active ? 2.0f : 1.0f);