yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
window_sidebar.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <cctype>
6#include <map>
7#include <string>
8#include <vector>
9
10#include "absl/strings/str_format.h"
13#include "app/gui/core/icons.h"
20#include "core/color.h"
21#include "imgui/imgui.h"
22
23namespace yaze {
24namespace editor {
25
26namespace {
27
28std::string LowercaseCopy(std::string value) {
29 std::transform(
30 value.begin(), value.end(), value.begin(),
31 [](unsigned char c) { return static_cast<char>(::tolower(c)); });
32 return value;
33}
34
35bool MatchesWindowSearch(const std::string& query,
36 const WindowDescriptor& window) {
38 query, window.display_name, window.card_id, window.shortcut_hint);
39}
40
41bool IsDungeonRoomWindow(const std::string& window_id) {
42 constexpr char kPrefix[] = "dungeon.room_";
43 constexpr size_t kPrefixLength = sizeof(kPrefix) - 1;
44 if (window_id.rfind(kPrefix, 0) != 0 || window_id.size() <= kPrefixLength) {
45 return false;
46 }
47 // DungeonEditorV2 registers dynamic room windows with a decimal room number.
48 // Other room_* IDs, such as room_graphics and room_tags, are standalone tools.
49 return std::all_of(window_id.begin() + kPrefixLength, window_id.end(),
50 [](char ch) { return ch >= '0' && ch <= '9'; });
51}
52
53bool IsDungeonPanelModeWindow(const std::string& window_id) {
55}
56
57} // namespace
58
60 WorkspaceWindowManager& window_manager,
61 std::function<bool()> is_dungeon_workbench_mode,
62 std::function<void(bool)> set_dungeon_workflow_mode,
63 std::function<float()> get_bottom_reserved_height,
64 std::function<EditorContextSnapshot(const std::string&)> context_provider)
65 : window_manager_(window_manager),
66 is_dungeon_workbench_mode_(std::move(is_dungeon_workbench_mode)),
67 set_dungeon_workflow_mode_(std::move(set_dungeon_workflow_mode)),
68 get_bottom_reserved_height_(std::move(get_bottom_reserved_height)),
69 context_provider_(std::move(context_provider)) {}
70
72 const EditorContextSnapshot& snapshot) {
73 return !snapshot.subtitle.empty() || !snapshot.metadata.empty() ||
74 !snapshot.counts.empty() || !snapshot.diagnostics.empty() ||
75 !snapshot.actions.empty() || snapshot.has_pending_changes ||
76 snapshot.experiment.experimental;
77}
78
80 const EditorContextAction& action) {
81 if (!action.enabled || action.target.empty()) {
82 return;
83 }
85 return;
86 }
87 if (!window_manager_.OpenWindow(session_id, action.target)) {
88 return;
89 }
91 if (const auto* descriptor =
92 window_manager_.GetWindowDescriptor(session_id, action.target)) {
93 window_manager_.TriggerWindowClicked(descriptor->category);
94 const std::string window_name =
96 if (!window_name.empty()) {
97 ImGui::SetWindowFocus(window_name.c_str());
98 }
99 }
100}
101
102void WindowSidebar::DrawEditorContext(size_t session_id,
103 const std::string& category,
104 const EditorContextSnapshot& snapshot) {
105 if (!HasRenderableContext(snapshot)) {
106 return;
107 }
108
109 ImGui::PushID(category.c_str());
110 ImGui::SeparatorText(tr("Context"));
111 if (!snapshot.title.empty())
112 ImGui::TextWrapped("%s", snapshot.title.c_str());
113 if (snapshot.experiment.experimental) {
114 const std::string label =
115 "Experimental · " + snapshot.experiment.save_posture;
116 gui::ColoredText(label.c_str(), gui::GetWarningVec4());
117 }
118 if (snapshot.has_pending_changes)
119 gui::ColoredText(snapshot.pending_label.empty()
120 ? "Pending changes"
121 : snapshot.pending_label.c_str(),
123 for (const auto& diagnostic : snapshot.diagnostics) {
124 if (diagnostic.severity == EditorContextDiagnosticSeverity::kInfo)
125 continue;
126 ImGui::TextWrapped("%s", diagnostic.message.c_str());
127 }
128 if (ImGui::Button(tr("Context details")))
129 context_details_category_ = category;
130 ImGui::PopID();
131}
132
134 if (context_details_category_.empty())
135 return;
136 const EditorContextSnapshot snapshot =
139 if (!HasRenderableContext(snapshot)) {
141 return;
142 }
143 bool open = true;
144 ImGui::SetNextWindowSize(ImVec2(520, 480), ImGuiCond_FirstUseEver);
145 const std::string title =
146 context_details_category_ + " Context###EditorContextDetails";
147 if (ImGui::Begin(title.c_str(), &open))
148 DrawEditorContextDetails(session_id, snapshot);
149 ImGui::End();
150 if (!open)
152}
153
155 size_t session_id, const EditorContextSnapshot& snapshot) {
156 if (!snapshot.title.empty()) {
157 ImGui::TextWrapped("%s", snapshot.title.c_str());
158 }
159 if (!snapshot.subtitle.empty()) {
160 ImGui::TextDisabled("%s", snapshot.subtitle.c_str());
161 }
162 if (snapshot.experiment.experimental) {
163 const std::string experiment_label =
164 absl::StrFormat("Experimental · %s", snapshot.experiment.save_posture);
165 gui::ColoredText(experiment_label.c_str(), snapshot.experiment.acknowledged
168 }
169 if (snapshot.has_pending_changes) {
170 gui::ColoredText(snapshot.pending_label.empty()
171 ? ICON_MD_EDIT " Pending changes"
172 : snapshot.pending_label.c_str(),
174 }
175
176 const auto draw_values = [](const char* table_id,
177 const std::vector<EditorContextValue>& values) {
178 if (values.empty()) {
179 return;
180 }
181 if (ImGui::BeginTable(
182 table_id, 2,
183 ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_PadOuterX)) {
184 ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthFixed);
185 ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch);
186 for (const EditorContextValue& value : values) {
187 ImGui::TableNextRow();
188 ImGui::TableNextColumn();
189 ImGui::TextDisabled("%s", value.label.c_str());
190 ImGui::TableNextColumn();
191 ImGui::TextWrapped("%s", value.value.c_str());
192 }
193 ImGui::EndTable();
194 }
195 };
196
197 if (!snapshot.metadata.empty()) {
198 ImGui::SeparatorText(tr("Metadata"));
199 draw_values("##ContextMetadata", snapshot.metadata);
200 }
201 if (!snapshot.counts.empty()) {
202 ImGui::SeparatorText(tr("Contents"));
203 draw_values("##ContextCounts", snapshot.counts);
204 }
205
206 if (!snapshot.diagnostics.empty()) {
207 ImGui::SeparatorText(tr("Attention"));
208 ImGui::PushTextWrapPos(0.0f);
209 for (const EditorContextDiagnostic& diagnostic : snapshot.diagnostics) {
210 switch (diagnostic.severity) {
213 diagnostic.message.c_str());
214 break;
217 diagnostic.message.c_str());
218 break;
220 default:
222 diagnostic.message.c_str());
223 break;
224 }
225 }
226 ImGui::PopTextWrapPos();
227 }
228
229 if (!snapshot.actions.empty()) {
230 ImGui::SeparatorText(tr("Quick Actions"));
231 for (const EditorContextAction& action : snapshot.actions) {
232 ImGui::PushID(action.id.c_str());
233 ImGui::BeginDisabled(!action.enabled);
234 if (ImGui::Button(action.label.c_str(), ImVec2(-1.0f, 0.0f))) {
235 DispatchContextAction(session_id, action);
236 }
237 if (!action.enabled && !action.disabled_reason.empty() &&
238 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
239 ImGui::SetTooltip("%s", action.disabled_reason.c_str());
240 }
241 ImGui::EndDisabled();
242 ImGui::PopID();
243 }
244 }
245}
246
247bool WindowSidebar::MatchesWindowSearch(const std::string& query,
248 const std::string& display_name,
249 const std::string& window_id,
250 const std::string& shortcut_hint) {
251 if (query.empty()) {
252 return true;
253 }
254
255 const std::string search_str = LowercaseCopy(query);
256 return LowercaseCopy(display_name).find(search_str) != std::string::npos ||
257 LowercaseCopy(window_id).find(search_str) != std::string::npos ||
258 LowercaseCopy(shortcut_hint).find(search_str) != std::string::npos;
259}
260
261bool WindowSidebar::IsDungeonWindowModeTarget(const std::string& window_id) {
262 return window_id == "dungeon.room_selector" ||
263 window_id == "dungeon.room_matrix" ||
264 window_id == "dungeon.entrance_list" ||
265 window_id == "dungeon.entrance_properties" ||
266 IsDungeonRoomWindow(window_id);
267}
268
270 const std::string& workflow_group) {
271 if (workflow_group.empty() || workflow_group == "Windows") {
272 return "Editors";
273 }
274 return workflow_group;
275}
276
278 if (!window.workflow_group.empty() && window.workflow_group != "Windows") {
279 return window.workflow_group;
280 }
281 // Dynamic per-room windows may omit workflow_group at registration time.
282 if (IsDungeonRoomWindow(window.card_id)) {
283 return "Rooms";
284 }
285 return SidebarSectionFor(window.workflow_group);
286}
287
288bool WindowSidebar::ShouldOmitWindowInSidebar(const std::string& window_id,
289 bool dungeon_workbench_mode) {
290 return dungeon_workbench_mode && IsDungeonWindowModeTarget(window_id);
291}
292
293void WindowSidebar::Draw(size_t session_id, const std::string& category,
294 std::function<bool()> has_rom) {
295 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
296 const ImGuiViewport* viewport = ImGui::GetMainViewport();
297 const float bar_width = WorkspaceWindowManager::GetSidebarWidth();
298 const float panel_width =
299 window_manager_.GetActiveSidePanelWidth(viewport->WorkSize.x);
300
301 const float top_inset = gui::LayoutHelpers::GetTopInset();
302 const auto safe_area = gui::LayoutHelpers::GetSafeAreaInsets();
303 const float bottom_reserved =
305 ? std::max(0.0f, get_bottom_reserved_height_())
306 : 0.0f;
307 const float panel_height =
308 std::max(0.0f, viewport->WorkSize.y - top_inset - safe_area.bottom -
309 bottom_reserved);
310
311 gui::FixedPanel panel(
312 "##SidePanel",
313 ImVec2(viewport->WorkPos.x + bar_width, viewport->WorkPos.y + top_inset),
314 ImVec2(panel_width, panel_height),
315 {.bg = gui::ConvertColorToImVec4(theme.surface),
316 .padding = {10.0f, 10.0f},
317 .border_size = 1.0f,
318 .rounding = 0.0f},
319 ImGuiWindowFlags_NoFocusOnAppearing);
320
321 if (!panel) {
322 return;
323 }
324
325 ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]);
326 ImGui::Text("%s", category.c_str());
327 ImGui::PopFont();
328
329 float avail_width = ImGui::GetContentRegionAvail().x;
330 const ImVec2 chrome_icon_size = gui::IconSize::Small();
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")) {
336 }
337
338 ImGui::Separator();
339 ImGui::Spacing();
340
341 const ImVec2 search_button_size = gui::IconSize::Small();
342 const float standard_spacing =
343 std::clamp(gui::LayoutHelpers::GetStandardSpacing(), 4.0f, 8.0f);
344 const float compact_spacing = std::max(4.0f, standard_spacing * 0.75f);
345 const float search_spacing = compact_spacing;
346
347 auto read_dungeon_workbench_mode = [&]() -> bool {
348 if (category != "Dungeon") {
349 return false;
350 }
353 }
354 return window_manager_.IsWindowOpen(session_id, "dungeon.workbench");
355 };
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);
361
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);
369 const bool filter_empty = sidebar_search_[0] == '\0';
370 if (filter_empty) {
371 ImGui::BeginDisabled();
372 }
373 if (gui::TransparentIconButton(ICON_MD_CLEAR, search_button_size,
374 "Clear filter", false,
375 gui::GetTextSecondaryVec4(), "window_sidebar",
376 "clear_sidebar_filter")) {
377 sidebar_search_[0] = '\0';
378 }
379 if (filter_empty) {
380 ImGui::EndDisabled();
381 }
382 ImGui::SameLine(0.0f, search_spacing);
383 if (gui::TransparentIconButton(ICON_MD_MORE_HORIZ, search_button_size,
384 "Window actions", false,
385 gui::GetTextSecondaryVec4(), "window_sidebar",
386 "open_sidebar_actions")) {
387 ImGui::OpenPopup("##WindowSidebarActions");
388 }
389 if (ImGui::BeginPopup("##WindowSidebarActions")) {
390 const auto category_windows =
391 window_manager_.GetWindowsInCategory(session_id, category);
392 int visible_windows = 0;
393 int listed_windows = 0;
394 for (const auto& category_window : category_windows) {
395 if (!category_window.IsListedInWindowBrowser()) {
396 continue;
397 }
398 ++listed_windows;
399 if (category_window.visibility_flag && *category_window.visibility_flag) {
400 ++visible_windows;
401 }
402 }
403 ImGui::TextDisabled(tr("%d of %zu visible"), visible_windows,
404 static_cast<size_t>(listed_windows));
405 ImGui::Separator();
406 if (ImGui::MenuItem(ICON_MD_APPS " Window Browser")) {
408 }
409 ImGui::Separator();
410 if (ImGui::MenuItem(ICON_MD_VISIBILITY " Show all", nullptr, false,
411 bulk_actions_enabled)) {
412 window_manager_.ShowAllWindowsInCategory(session_id, category);
413 }
414 const bool show_all_hovered =
415 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled);
416 if (ImGui::MenuItem(ICON_MD_VISIBILITY_OFF " Hide all", nullptr, false,
417 bulk_actions_enabled)) {
418 window_manager_.HideAllWindowsInCategory(session_id, category);
419 }
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) {
424 ImGui::SetTooltip(
425 tr("Switch to Window workflow to bulk-manage room windows."));
426 }
427 ImGui::EndPopup();
428 }
429
430 auto switch_to_dungeon_workbench_mode = [&]() -> bool {
431 if (category != "Dungeon" || dungeon_workbench_mode) {
432 return false;
433 }
436 dungeon_workbench_mode = true;
437 return true;
438 }
439 window_manager_.OpenWindow(session_id, "dungeon.workbench");
440 // Workbench-local tool windows (Object/Door/Sprite/Item Selector, Palette,
441 // Room Graphics/Tags, Custom Collision, Water Fill, Minecart) are NOT
442 // auto-closed here. Users can keep a standalone copy open alongside the
443 // Workbench drawer's embedded copy if they prefer that layout.
444 for (const auto& descriptor :
445 window_manager_.GetWindowsInCategory(session_id, "Dungeon")) {
446 const std::string& window_id = descriptor.card_id;
447 if (window_id == "dungeon.workbench") {
448 continue;
449 }
450 if (window_manager_.IsWindowPinned(session_id, window_id)) {
451 continue;
452 }
453 if (IsDungeonPanelModeWindow(window_id)) {
454 window_manager_.CloseWindow(session_id, window_id);
455 }
456 }
457 dungeon_workbench_mode = read_dungeon_workbench_mode();
458 return dungeon_workbench_mode;
459 };
460
461 auto switch_to_dungeon_window_mode = [&]() -> bool {
462 if (category != "Dungeon" || !dungeon_workbench_mode) {
463 return false;
464 }
467 dungeon_workbench_mode = false;
468 return true;
469 }
470 window_manager_.CloseWindow(session_id, "dungeon.workbench");
471 window_manager_.OpenWindow(session_id, "dungeon.room_selector");
472 window_manager_.OpenWindow(session_id, "dungeon.room_matrix");
473 dungeon_workbench_mode = read_dungeon_workbench_mode();
474 return !dungeon_workbench_mode;
475 };
476
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)) {
481 return false;
482 }
483 return switch_to_dungeon_window_mode();
484 };
485
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 {
504 const ImVec4 active_bg = gui::GetPrimaryVec4();
505 const ImVec4 inactive_bg = gui::GetSurfaceContainerHighVec4();
506 const ImVec4 inactive_hover = gui::GetSurfaceContainerHighestVec4();
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);
516 }
517 return clicked;
518 };
519 const ImVec2 workflow_button_size(workflow_button_width,
520 workflow_button_height);
521
522 if (draw_workflow_button(
523 "workflow_workbench", ICON_MD_WORKSPACES, "Workbench",
524 dungeon_workbench_mode,
525 "Workbench mode: integrated room browser + inspector",
526 workflow_button_size)) {
527 switch_to_dungeon_workbench_mode();
528 }
529 if (!stack_workflow_buttons) {
530 ImGui::SameLine(0.0f, workflow_gap);
531 }
532 if (draw_workflow_button(
533 "workflow_windows", ICON_MD_VIEW_QUILT, "Windows",
534 !dungeon_workbench_mode,
535 "Window mode: standalone Room List + Room Matrix + room windows",
536 workflow_button_size)) {
537 switch_to_dungeon_window_mode();
538 }
539 ImGui::Separator();
540 }
541
542 ImGui::Spacing();
543
544 if (disable_windows) {
545 ImGui::TextUnformatted(ICON_MD_FOLDER_OPEN
546 " Open a ROM to enable this category");
547 ImGui::Spacing();
548 }
549
550 if (disable_windows) {
551 ImGui::BeginDisabled();
552 }
553
554 const auto pinned_windows = window_manager_.GetPinnedWindows();
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;
559 }
561 };
562 const ImVec2 pin_button_size = gui::ScaledSize(30.0f, 30.0f);
563 auto draw_pin_toggle_button = [&](const std::string& widget_id,
564 bool pinned) -> bool {
565 ImGui::PushID(widget_id.c_str());
566 ImVec4 pin_col = pinned ? gui::ConvertColorToImVec4(theme.primary)
568 if (!pinned) {
569 pin_col.w *= 0.72f;
570 }
571 const bool clicked = gui::TransparentIconButton(
572 ICON_MD_PUSH_PIN, pin_button_size,
573 pinned ? "Unpin window" : "Pin window", pinned, pin_col,
574 "window_sidebar", widget_id.c_str());
575 ImGui::PopID();
576 return clicked;
577 };
578
579 bool pinned_section_open = false;
580 if (sidebar_search_[0] == '\0' && !pinned_windows.empty()) {
581 bool has_pinned_in_category = false;
582 for (const auto& window_id : pinned_windows) {
583 const auto* window =
584 window_manager_.GetWindowDescriptor(session_id, window_id);
585 if (window && window->category == category && window->IsAdmitted()) {
586 has_pinned_in_category = true;
587 break;
588 }
589 }
590
591 if (has_pinned_in_category) {
592 pinned_section_open = ImGui::CollapsingHeader(
593 ICON_MD_PUSH_PIN " Pinned Windows", ImGuiTreeNodeFlags_DefaultOpen);
594 if (pinned_section_open) {
595 for (const auto& window_id : pinned_windows) {
596 const auto* window =
597 window_manager_.GetWindowDescriptor(session_id, window_id);
598 if (!window || window->category != category ||
599 !window->IsAdmitted()) {
600 continue;
601 }
602
603 const bool visible =
604 window->visibility_flag ? *window->visibility_flag : false;
605
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);
611 ImGui::PushID(
612 (std::string("pinned_select_") + window->card_id).c_str());
613 {
614 gui::StyleColorGuard text_color(ImGuiCol_Text,
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);
621 if (switched_mode) {
622 window_manager_.OpenWindow(session_id, window->card_id);
623 } else {
624 window_manager_.ToggleWindow(session_id, window->card_id);
625 }
626
627 const bool new_visible =
628 window->visibility_flag ? *window->visibility_flag : false;
629 if (new_visible) {
630 window_manager_.TriggerWindowClicked(window->category);
631 const std::string window_name =
633 if (!window_name.empty()) {
634 ImGui::SetWindowFocus(window_name.c_str());
635 }
636 }
637 }
638 }
639 ImGui::PopID();
640 ImGui::SameLine(0.0f, compact_spacing);
641 if (draw_pin_toggle_button("pin_" + window->card_id, true)) {
642 window_manager_.SetWindowPinned(session_id, window->card_id, false);
643 }
644 }
645 ImGui::Spacing();
646 ImGui::Separator();
647 ImGui::Spacing();
648 }
649 }
650 }
651
652 auto windows = window_manager_.GetWindowsSortedByMRU(session_id, category);
653 const bool filtering = sidebar_search_[0] != '\0';
654
655 // Bucket windows into sidebar sections (Core / Editors / Rooms / Advanced /
656 // other). Pinned rows that already appear in the Pinned header are skipped.
657 std::map<std::string, std::vector<WindowDescriptor>> sections;
658 for (const auto& window : windows) {
659 if (!window.IsListedInWindowBrowser()) {
660 continue;
661 }
662 if (ShouldOmitWindowInSidebar(window.card_id, dungeon_workbench_mode)) {
663 continue;
664 }
665 if (!MatchesWindowSearch(sidebar_search_, window.display_name,
666 window.card_id, window.shortcut_hint)) {
667 continue;
668 }
669 const bool is_pinned = window_manager_.IsWindowPinned(window.card_id);
670 if (pinned_section_open && is_pinned) {
671 continue;
672 }
673 sections[SidebarSectionFor(window)].push_back(window);
674 }
675
676 // Within each section: priority ascending, then display name.
677 for (auto& [section_name, section_windows] : sections) {
678 (void)section_name;
679 std::sort(section_windows.begin(), section_windows.end(),
680 [](const WindowDescriptor& a, const WindowDescriptor& b) {
681 if (a.priority != b.priority) {
682 return a.priority < b.priority;
683 }
684 return a.display_name < b.display_name;
685 });
686 }
687
688 constexpr const char* kPreferredSectionOrder[] = {
689 "Core",
690 "Editors",
691 "Rooms",
692 "Advanced",
693 };
694 std::vector<std::string> section_order;
695 for (const char* preferred : kPreferredSectionOrder) {
696 if (sections.count(preferred)) {
697 section_order.emplace_back(preferred);
698 }
699 }
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);
704 }
705 }
706
707 auto draw_window_row = [&](const WindowDescriptor& window) {
708 const bool is_pinned = window_manager_.IsWindowPinned(window.card_id);
709 const bool visible =
710 window.visibility_flag ? *window.visibility_flag : false;
711
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 -
716 compact_spacing);
717 bool window_row_hovered = false;
718 ImGui::PushID((std::string("window_select_") + window.card_id).c_str());
719 {
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,
724 item_size)) {
725 const bool switched_mode =
726 ensure_dungeon_window_mode_for_window(window.card_id);
727 if (switched_mode) {
728 window_manager_.OpenWindow(session_id, window.card_id);
729 } else {
730 window_manager_.ToggleWindow(session_id, window.card_id);
731 }
732
733 const bool new_visible =
734 window.visibility_flag ? *window.visibility_flag : false;
735 if (new_visible) {
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());
742 }
743 }
744 }
745 window_row_hovered = ImGui::IsItemHovered();
746 }
747 ImGui::PopID();
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);
751 }
752
753 if (window_row_hovered && !window.shortcut_hint.empty()) {
754 ImGui::SetTooltip("%s", window.shortcut_hint.c_str());
755 }
756 };
757
758 const EditorContextSnapshot context_snapshot =
759 context_provider_ ? context_provider_(category) : EditorContextSnapshot{};
760 const bool has_context = HasRenderableContext(context_snapshot);
761 if (disable_windows)
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()) {
770 continue;
771 }
772
773 // Search results are flat so a previously collapsed section cannot hide
774 // a match. Do not change the ordinary section's saved expansion state.
775 if (filtering) {
776 for (const auto& window : it->second) {
777 draw_window_row(window);
778 }
779 continue;
780 }
781
782 // Flat sections: the sidebar already provides search and one scroll
783 // surface. A saved closed header must not hide an editing tool.
784 ImGui::SeparatorText(section_name.c_str());
785 for (const auto& window : it->second)
786 draw_window_row(window);
787 }
788 ImGui::EndDisabled();
789 }
790 if (window_content_open && has_context)
791 DrawEditorContext(session_id, category, context_snapshot);
792 ImGui::EndChild();
793
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);
805 }
806 if (handle_hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
807 window_manager_.ResetSidePanelWidth();
808 }
809 if (handle_active) {
810 const float new_width = panel_width + ImGui::GetIO().MouseDelta.x;
811 window_manager_.SetActiveSidePanelWidth(new_width, viewport->WorkSize.x);
812 ImGui::SetTooltip(
813 tr("Width: %.0f px"),
814 window_manager_.GetActiveSidePanelWidth(viewport->WorkSize.x));
815 }
816
817 ImVec4 handle_color = gui::GetOutlineVec4();
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);
823}
824
825} // namespace editor
826} // namespace yaze
void Draw(size_t session_id, const std::string &category, std::function< bool()> has_rom)
std::function< void(bool)> set_dungeon_workflow_mode_
void DrawEditorContextDetails(size_t session_id, const EditorContextSnapshot &snapshot)
static bool HasRenderableContext(const EditorContextSnapshot &snapshot)
std::function< bool()> is_dungeon_workbench_mode_
std::function< EditorContextSnapshot(const std::string &) context_provider_)
static bool IsDungeonWindowModeTarget(const std::string &window_id)
void DrawContextDetailsWindow(size_t session_id)
void DispatchContextAction(size_t session_id, const EditorContextAction &action)
WorkspaceWindowManager & window_manager_
WindowSidebar(WorkspaceWindowManager &window_manager, std::function< bool()> is_dungeon_workbench_mode={}, std::function< void(bool)> set_dungeon_workflow_mode={}, std::function< float()> get_bottom_reserved_height={}, std::function< EditorContextSnapshot(const std::string &)> context_provider={})
void DrawEditorContext(size_t session_id, const std::string &category, const EditorContextSnapshot &snapshot)
std::function< float()> get_bottom_reserved_height_
static std::string SidebarSectionFor(const std::string &workflow_group)
static bool ShouldOmitWindowInSidebar(const std::string &window_id, bool dungeon_workbench_mode)
static bool MatchesWindowSearch(const std::string &query, const std::string &display_name, const std::string &window_id, const std::string &shortcut_hint)
Central registry for all editor cards with session awareness and dependency injection.
void HideAllWindowsInCategory(size_t session_id, const std::string &category)
std::string GetWorkspaceWindowName(size_t session_id, const std::string &base_window_id) const
Resolve the exact ImGui window name for a panel by base ID.
std::vector< WindowDescriptor > GetWindowsInCategory(size_t session_id, const std::string &category) const
const WindowDescriptor * GetWindowDescriptor(size_t session_id, const std::string &base_window_id) const
void SetWindowPinned(size_t session_id, const std::string &base_window_id, bool pinned)
bool CloseWindow(size_t session_id, const std::string &base_window_id)
void TriggerWindowClicked(const std::string &category)
std::vector< std::string > GetPinnedWindows(size_t session_id) const
bool IsWindowOpen(size_t session_id, const std::string &base_window_id) const
float GetActiveSidePanelWidth(float viewport_width) const
bool OpenWindow(size_t session_id, const std::string &base_window_id)
std::vector< WindowDescriptor > GetWindowsSortedByMRU(size_t session_id, const std::string &category) const
bool IsWindowPinned(size_t session_id, const std::string &base_window_id) const
bool ToggleWindow(size_t session_id, const std::string &base_window_id)
void SetSidebarExpanded(bool expanded, bool notify=true)
void ShowAllWindowsInCategory(size_t session_id, const std::string &category)
void MarkWindowRecentlyUsed(const std::string &window_id)
RAII for fixed-position panels (activity bar, side panel, status bar).
static SafeAreaInsets GetSafeAreaInsets()
static float GetStandardWidgetHeight()
static float GetStandardSpacing()
RAII guard for ImGui style colors.
Definition style_guard.h:27
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_VIEW_QUILT
Definition icons.h:2094
#define ICON_MD_APPS
Definition icons.h:168
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_SEARCH
Definition icons.h:1673
#define ICON_MD_VISIBILITY
Definition icons.h:2101
#define ICON_MD_EDIT
Definition icons.h:645
#define ICON_MD_MORE_HORIZ
Definition icons.h:1241
#define ICON_MD_ERROR
Definition icons.h:686
#define ICON_MD_VISIBILITY_OFF
Definition icons.h:2102
#define ICON_MD_CHEVRON_LEFT
Definition icons.h:405
#define ICON_MD_CLEAR
Definition icons.h:416
#define ICON_MD_PUSH_PIN
Definition icons.h:1529
#define ICON_MD_WORKSPACES
Definition icons.h:2186
bool IsDungeonRoomWindow(const std::string &window_id)
bool IsDungeonPanelModeWindow(const std::string &window_id)
bool TransparentIconButton(const char *icon, const ImVec2 &size, const char *tooltip, bool is_active, const ImVec4 &active_color, const char *panel_id, const char *anim_id)
Draw a transparent icon button (hover effect only).
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:140
void ColoredText(const char *text, const ImVec4 &color)
ImVec4 GetErrorVec4()
ImVec4 GetSurfaceContainerHighestVec4()
ImVec4 GetPrimaryVec4()
ImVec2 ScaledSize(float base_width, float base_height)
Returns a DPI-scaled version of a fixed pixel size.
Definition ui_config.h:148
ImVec4 GetTextSecondaryVec4()
void ColoredTextF(const ImVec4 &color, const char *fmt,...)
ImVec4 GetSurfaceContainerHighVec4()
ImVec4 GetOutlineVec4()
ImVec4 GetOnSurfaceVec4()
ImVec4 GetWarningVec4()
const char * tr(const char *source)
Definition translator.cc:50
std::vector< EditorContextValue > counts
std::vector< EditorContextAction > actions
std::vector< EditorContextDiagnostic > diagnostics
std::vector< EditorContextValue > metadata
Metadata for a dockable editor window (formerly PanelInfo)