yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_workbench_content.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <array>
6#include <cctype>
7#include <cmath>
8#include <cstdint>
9#include <cstdio>
10#include <cstring>
11#include <utility>
12#include <vector>
13
14#include "absl/strings/str_format.h"
33#include "app/gui/core/icons.h"
34#include "app/gui/core/input.h"
39#include "core/features.h"
40#include "core/project.h"
41#include "imgui/imgui.h"
42#include "rom/rom.h"
52
53namespace yaze::editor {
54
55namespace {
56
57// Object type category names based on ID range
58const char* GetObjectCategory(int object_id) {
59 if (object_id < 0x100)
60 return "Standard";
61 if (object_id < 0x200)
62 return "Extended";
63 if (object_id >= 0xF80)
64 return "Special";
65 return "Unknown";
66}
67
68const char* GetObjectStreamName(int layer_value) {
69 switch (layer_value) {
70 case 0:
71 return "Primary";
72 case 1:
73 return "BG2 overlay";
74 case 2:
75 return "BG1 overlay";
76 default:
77 return "Unknown";
78 }
79}
80
81const char* GetSpecialLayerName(int layer_value) {
82 switch (layer_value) {
83 case 0:
84 return "Upper layer (BG1)";
85 case 1:
86 return "Lower layer (BG2)";
87 default:
88 return "Unknown";
89 }
90}
91
92const char* GetBg2ModeName(int value) {
93 static constexpr const char* kNames[] = {
94 "Off", "Parallax", "Dark", "On top", "Translucent",
95 "Addition", "Normal", "Transparent", "Dark room"};
96 constexpr int kNameCount = sizeof(kNames) / sizeof(kNames[0]);
97 return (value >= 0 && value < kNameCount) ? kNames[value] : "Unknown";
98}
99
100const char* GetCollisionName(int value) {
101 static constexpr const char* kNames[] = {"One", "Both", "Both + Scroll",
102 "Moving Floor", "Moving Water"};
103 constexpr int kNameCount = sizeof(kNames) / sizeof(kNames[0]);
104 return (value >= 0 && value < kNameCount) ? kNames[value] : "Unknown";
105}
106
107// Pot item names for the inspector
108const char* GetPotItemName(uint8_t item) {
109 static const char* kNames[] = {
110 "Nothing", "Green Rupee", "Rock", "Bee",
111 "Heart (4)", "Bomb (4)", "Heart", "Blue Rupee",
112 "Key", "Arrow (5)", "Bomb (1)", "Heart",
113 "Magic (Small)", "Full Magic", "Cucco", "Green Soldier",
114 "Bush Stal", "Blue Soldier", "Landmine", "Heart",
115 "Fairy", "Heart", "Nothing (22)", "Hole",
116 "Warp", "Staircase", "Bombable", "Switch",
117 };
118 constexpr size_t kCount = sizeof(kNames) / sizeof(kNames[0]);
119 return item < kCount ? kNames[item] : "Unknown";
120}
121
122float ClampWorkbenchPaneWidth(float desired_width, float min_width,
123 float max_width) {
124 return std::clamp(desired_width, min_width, std::max(min_width, max_width));
125}
126
127constexpr float kCompactLeftSidebarMinWidth = 224.0f;
128constexpr float kCompactRightSidebarMinWidth = 272.0f;
129constexpr float kCompactLeftSidebarScale = 0.72f;
130constexpr float kCompactRightSidebarScale = 0.84f;
131
132float GetCompactSidebarWidth(bool right_sidebar, float min_sidebar_width) {
133 return std::max(
134 right_sidebar ? kCompactRightSidebarMinWidth
136 min_sidebar_width * (right_sidebar ? kCompactRightSidebarScale
138}
139
142 rect.visible = true;
143 const ImVec2 min = ImGui::GetItemRectMin();
144 const ImVec2 max = ImGui::GetItemRectMax();
145 rect.min_x = min.x;
146 rect.min_y = min.y;
147 rect.max_x = max.x;
148 rect.max_y = max.y;
149 return rect;
150}
151
152} // namespace
153
154bool ResolveCompactInspectorDetailRequest(bool compact, bool detail_requested) {
155 return compact && detail_requested;
156}
157
159 float total_width, float min_canvas_width, float min_sidebar_width,
160 float splitter_width, bool want_left, bool want_right) {
162 result.show_left = want_left;
163 result.show_right = want_right;
164
165 auto required_width = [&](bool left, bool right, bool compact_left,
166 bool compact_right) {
167 float required = min_canvas_width;
168 required +=
169 left ? (compact_left ? GetCompactSidebarWidth(false, min_sidebar_width)
170 : min_sidebar_width)
171 : 0.0f;
172 required +=
173 right ? (compact_right ? GetCompactSidebarWidth(true, min_sidebar_width)
174 : min_sidebar_width)
175 : 0.0f;
176 if (left) {
177 required += splitter_width;
178 }
179 if (right) {
180 required += splitter_width;
181 }
182 return required;
183 };
184
185 if (result.show_left &&
186 total_width <
187 required_width(result.show_left, result.show_right, false, false)) {
188 result.compact_left = true;
189 }
190 if (result.show_right &&
191 total_width < required_width(result.show_left, result.show_right,
192 result.compact_left, false)) {
193 result.compact_right = true;
194 }
195 if (result.show_left &&
196 total_width < required_width(result.show_left, result.show_right,
197 result.compact_left, result.compact_right)) {
198 result.show_left = false;
199 result.compact_left = false;
200 }
201 if (result.show_right &&
202 total_width < required_width(result.show_left, result.show_right,
203 result.compact_left, result.compact_right)) {
204 result.show_right = false;
205 result.compact_right = false;
206 }
207
208 return result;
209}
210
212 float total_width, float min_canvas_width, float min_sidebar_width,
213 float splitter_width, float stored_left_width, float stored_right_width,
214 bool want_left, bool want_right) {
217 total_width, min_canvas_width, min_sidebar_width, splitter_width,
218 want_left, want_right);
219
220 const float compact_left_width =
221 GetCompactSidebarWidth(false, min_sidebar_width);
222 const float compact_right_width =
223 GetCompactSidebarWidth(true, min_sidebar_width);
224 layout.min_left_width =
225 layout.responsive.compact_left ? compact_left_width : min_sidebar_width;
226 layout.min_right_width =
227 layout.responsive.compact_right ? compact_right_width : min_sidebar_width;
228
229 float left_width = layout.responsive.show_left
230 ? (layout.responsive.compact_left ? compact_left_width
231 : stored_left_width)
232 : 0.0f;
233 float right_width =
235 ? (layout.responsive.compact_right ? compact_right_width
236 : stored_right_width)
237 : 0.0f;
238
239 const float max_left_width =
240 total_width - right_width - min_canvas_width -
241 (layout.responsive.show_left ? splitter_width : 0.0f) -
242 (layout.responsive.show_right ? splitter_width : 0.0f);
243 const float max_right_width =
244 total_width - left_width - min_canvas_width -
245 (layout.responsive.show_left ? splitter_width : 0.0f) -
246 (layout.responsive.show_right ? splitter_width : 0.0f);
247 if (layout.responsive.show_left) {
248 left_width = ClampWorkbenchPaneWidth(
249 left_width, layout.min_left_width,
250 std::max(layout.min_left_width, max_left_width));
251 } else {
252 left_width = 0.0f;
253 }
254 if (layout.responsive.show_right) {
255 right_width = ClampWorkbenchPaneWidth(
256 right_width, layout.min_right_width,
257 std::max(layout.min_right_width, max_right_width));
258 } else {
259 right_width = 0.0f;
260 }
261
262 float center_width = total_width - left_width - right_width;
263 if (layout.responsive.show_left) {
264 center_width -= splitter_width;
265 }
266 if (layout.responsive.show_right) {
267 center_width -= splitter_width;
268 }
269 if (center_width < min_canvas_width) {
270 float deficit = min_canvas_width - center_width;
271 if (layout.responsive.show_left) {
272 const float shrink =
273 std::min(deficit, left_width - layout.min_left_width);
274 left_width -= shrink;
275 deficit -= shrink;
276 }
277 if (deficit > 0.0f && layout.responsive.show_right) {
278 const float shrink =
279 std::min(deficit, right_width - layout.min_right_width);
280 right_width -= shrink;
281 deficit -= shrink;
282 }
283 center_width = std::max(
284 1.0f, total_width - left_width - right_width -
285 (layout.responsive.show_left ? splitter_width : 0.0f) -
286 (layout.responsive.show_right ? splitter_width : 0.0f));
287 }
288
289 layout.left_width = left_width;
290 layout.center_width = center_width;
291 layout.right_width = right_width;
292 return layout;
293}
294
296 DungeonRoomSelector* room_selector, int* current_room_id,
297 std::function<void(int)> on_room_selected,
298 std::function<void(int, RoomSelectionIntent)> on_room_selected_with_intent,
299 std::function<void(int)> on_save_room,
300 std::function<void()> on_save_all_rooms,
301 std::function<DungeonCanvasViewer*()> get_viewer,
302 std::function<DungeonCanvasViewer*(int)> get_compare_viewer,
303 std::function<const std::deque<int>&()> get_recent_rooms,
304 std::function<void(int)> forget_recent_room,
305 std::function<void(bool)> set_workflow_mode, Rom* rom)
306 : room_selector_(room_selector),
307 current_room_id_(current_room_id),
308 on_room_selected_(std::move(on_room_selected)),
309 on_room_selected_with_intent_(std::move(on_room_selected_with_intent)),
310 on_save_room_(std::move(on_save_room)),
311 on_save_all_rooms_(std::move(on_save_all_rooms)),
312 get_viewer_(std::move(get_viewer)),
313 get_compare_viewer_(std::move(get_compare_viewer)),
314 get_recent_rooms_(std::move(get_recent_rooms)),
315 forget_recent_room_(std::move(forget_recent_room)),
316 set_workflow_mode_(std::move(set_workflow_mode)),
317 rom_(rom) {}
318
320
322 return "dungeon.workbench";
323}
325 return "Dungeon Workbench";
326}
328 return ICON_MD_WORKSPACES;
329}
331 return "Dungeon";
332}
334 return 10;
335}
336
338 rom_ = rom;
339 room_dungeon_cache_.clear();
341}
342
344 RoomTagEditorPanel* room_tags, CustomCollisionPanel* custom_collision,
345 WaterFillPanel* water_fill, MinecartTrackEditorPanel* minecart_tracks) {
346 room_tag_panel_ = room_tags;
347 custom_collision_panel_ = custom_collision;
348 water_fill_panel_ = water_fill;
349 minecart_track_panel_ = minecart_tracks;
350}
351
353 WindowContent* object_selector, WindowContent* door_editor,
354 WindowContent* sprite_editor, WindowContent* item_editor,
355 WindowContent* room_graphics, WindowContent* palette_editor) {
356 object_selector_content_ = object_selector;
357 door_editor_content_ = door_editor;
358 sprite_editor_content_ = sprite_editor;
359 item_editor_content_ = item_editor;
360 room_graphics_content_ = room_graphics;
361 palette_editor_content_ = palette_editor;
362}
363
369
375
380
385
389
393
397
401
405
409
413
417
421
425
429
433
435 switch (inspector_mode_) {
437 return "room";
439 return "selection";
441 return "tools";
442 }
443 return "unknown";
444}
445
449
450void DungeonWorkbenchContent::DrawSidebarPane(float width, float height,
451 float button_size, bool compact) {
452 const bool sidebar_open = ImGui::BeginChild("##DungeonWorkbenchSidebar",
453 ImVec2(width, height), true);
454 if (sidebar_open) {
455 DrawSidebarHeader(button_size, compact);
457 }
458 ImGui::EndChild();
459}
460
462 bool compact) {
463 const bool can_open_overview = true;
464 const float collapse_w =
466 const float menu_w = can_open_overview ? workbench::CalcIconButtonWidth(
467 ICON_MD_MORE_HORIZ, button_size)
468 : 0.0f;
469 const float spacing = ImGui::GetStyle().ItemSpacing.x;
470 const float header_width = ImGui::GetContentRegionAvail().x;
471 const bool stack_mode_switch = compact && header_width < 220.0f;
472 const float action_cluster_w =
473 collapse_w + (can_open_overview ? (spacing + menu_w) : 0.0f);
474
476 "##DungeonWorkbenchSidebarHeader", ICON_MD_VIEW_SIDEBAR, "Browse",
477 "Browse", nullptr, compact, action_cluster_w, [&]() {
478 if (can_open_overview) {
479 if (workbench::DrawHeaderIconAction("SidebarQuickActions",
480 ICON_MD_MORE_HORIZ, button_size,
481 "Open room review tools", true)) {
482 ImGui::OpenPopup("##WorkbenchSidebarQuickActions");
483 }
484 if (ImGui::BeginPopup("##WorkbenchSidebarQuickActions")) {
485 if (ImGui::MenuItem(ICON_MD_VIEW_QUILT " Stitched Rooms")) {
487 }
488 if (ImGui::MenuItem(ICON_MD_MAP " Dungeon Map")) {
490 }
491 ImGui::EndPopup();
492 }
493 ImGui::SameLine();
494 }
495 if (workbench::DrawHeaderIconAction("CollapseRooms",
496 ICON_MD_CHEVRON_LEFT, button_size,
497 "Collapse navigation pane")) {
499 }
500 });
501
502 ImGui::Dummy(ImVec2(0.0f, 2.0f));
503 DrawSidebarModeTabs(stack_mode_switch, std::max(button_size, 26.0f));
504 ImGui::Separator();
505}
506
508 float segment_height) {
509 // Compact icon-only segmented selector. Each button is square (size = height)
510 // so the cluster takes ~70px instead of stretching to ~170px with labels.
511 // Tooltips carry the full label.
512 (void)stacked;
513 const float spacing = ImGui::GetStyle().ItemSpacing.x;
514 const ImVec2 button_size(segment_height, segment_height);
515
516 if (gui::ToggleButton(ICON_MD_VIEW_LIST "##NavRooms",
517 sidebar_mode_ == SidebarMode::Rooms, button_size)) {
519 }
520 if (ImGui::IsItemHovered()) {
521 ImGui::SetTooltip(tr("Rooms"));
522 }
523 ImGui::SameLine(0.0f, spacing);
524 if (gui::ToggleButton(ICON_MD_DOOR_FRONT "##NavEntrances",
525 sidebar_mode_ == SidebarMode::Entrances, button_size)) {
527 }
528 if (ImGui::IsItemHovered()) {
529 ImGui::SetTooltip(tr("Entrances"));
530 }
531}
532
534 if (!room_selector_) {
535 ImGui::TextDisabled(tr("Room navigation unavailable"));
536 return;
537 }
538
539 ImGui::PushID("WorkbenchSidebarMode");
540 switch (sidebar_mode_) {
543 break;
546 break;
547 }
548 ImGui::PopID();
549}
550
552 (void)p_open;
553 const auto& theme = AgentUI::GetTheme();
554
555 if (!rom_ || !rom_->is_loaded()) {
556 ImGui::TextDisabled(ICON_MD_INFO " Load a ROM to edit dungeon rooms.");
557 return;
558 }
560 ImGui::TextColored(theme.text_error_red, tr("Dungeon Workbench not wired"));
561 return;
562 }
563
564 DungeonCanvasViewer* primary_viewer = get_viewer_ ? get_viewer_() : nullptr;
565 DungeonCanvasViewer* compare_viewer =
567 const float splitter_w = gui::UIConfig::kSplitterWidth;
568 const float total_w = std::max(ImGui::GetContentRegionAvail().x, 1.0f);
569 // Relaxed from 320 px to 260 px so the Inspect pane can breathe at widths
570 // where the previous floor forced a too-wide right rail. Room browser on the
571 // left also benefits since its matrix cells scale down gracefully.
572 const float min_sidebar_w =
574 const float min_canvas_w = std::max(420.0f, min_sidebar_w + 96.0f);
575 const DungeonWorkbenchPaneLayout pane_layout =
577 total_w, min_canvas_w, min_sidebar_w, splitter_w,
580 const bool show_left = pane_layout.responsive.show_left;
581 const bool show_right = pane_layout.responsive.show_right;
582
583 if (current_room_id_) {
585 params.layout = &layout_state_;
586 params.left_sidebar_visible = show_left;
591 params.primary_viewer = primary_viewer;
592 params.compare_viewer = compare_viewer;
596 params.open_room_matrix = [this]() {
598 };
600 params.on_request_dungeon_map = [this]() {
602 };
605 const bool request_panel_workflow = DungeonWorkbenchToolbar::Draw(params);
606 if (request_panel_workflow && set_workflow_mode_) {
607 // Defer panel visibility mutation until toolbar child/table scopes closed.
608 set_workflow_mode_(false);
609 return;
610 }
611 }
612
614 const float total_h = std::max(ImGui::GetContentRegionAvail().y, 1.0f);
615 const float left_w = pane_layout.left_width;
616 const float right_w = pane_layout.right_width;
617 const float center_w = pane_layout.center_width;
618 if (show_left && !pane_layout.responsive.compact_left) {
619 layout_state_.left_width = left_w;
620 }
621 if (show_right && !pane_layout.responsive.compact_right) {
622 layout_state_.right_width = right_w;
623 }
624
625 // Mirror toggle: when inspector_on_left_, swap render order so the inspector
626 // visually sits on the left of the canvas (ZScream-style). Width state is
627 // preserved (right_width is always the inspector's, left_width always the
628 // sidebar's) so splitter drags still affect the correct pane regardless of
629 // which side it's on.
630 if (inspector_on_left_) {
631 if (show_right) {
632 DrawInspectorPane(right_w, total_h, btn,
633 pane_layout.responsive.compact_right, primary_viewer);
634 ImGui::SameLine(0.0f, 0.0f);
636 "##DungeonWorkbenchLeftSplitter", total_h,
638 total_w - left_w - min_canvas_w - (show_left ? splitter_w : 0.0f),
639 false)) {
641 }
642 ImGui::SameLine(0.0f, 0.0f);
643 }
644 DrawCanvasPane(center_w, total_h, primary_viewer, show_left);
645 if (show_left) {
646 ImGui::SameLine(0.0f, 0.0f);
648 "##DungeonWorkbenchRightSplitter", total_h,
650 total_w - right_w - min_canvas_w -
651 (show_right ? splitter_w : 0.0f),
652 true)) {
654 }
655 ImGui::SameLine(0.0f, 0.0f);
656 DrawSidebarPane(left_w, total_h, btn,
657 pane_layout.responsive.compact_left);
658 }
659 } else {
660 if (show_left) {
661 DrawSidebarPane(left_w, total_h, btn,
662 pane_layout.responsive.compact_left);
663 }
664 if (show_left) {
665 ImGui::SameLine(0.0f, 0.0f);
667 "##DungeonWorkbenchLeftSplitter", total_h,
669 total_w - right_w - min_canvas_w -
670 (show_right ? splitter_w : 0.0f),
671 false)) {
673 }
674 }
675
676 if (show_left) {
677 ImGui::SameLine(0.0f, 0.0f);
678 }
679 DrawCanvasPane(center_w, total_h, primary_viewer, show_left);
680
681 if (show_right) {
682 ImGui::SameLine(0.0f, 0.0f);
684 "##DungeonWorkbenchRightSplitter", total_h,
686 total_w - left_w - min_canvas_w - (show_left ? splitter_w : 0.0f),
687 true)) {
689 }
690 ImGui::SameLine(0.0f, 0.0f);
691 }
692 if (show_right) {
693 DrawInspectorPane(right_w, total_h, btn,
694 pane_layout.responsive.compact_right, primary_viewer);
695 }
696 }
697 if (primary_viewer) {
698 DrawDungeonMapPopup(*primary_viewer);
699 }
700}
701
703 float width, float height, DungeonCanvasViewer* primary_viewer,
704 bool left_sidebar_visible) {
705 const bool canvas_open = ImGui::BeginChild("##DungeonWorkbenchCanvas",
706 ImVec2(width, height), false);
707 if (canvas_open) {
708 if (primary_viewer) {
709 const bool show_recent_tabs =
710 split_view_enabled_ || !left_sidebar_visible;
711 if (show_recent_tabs) {
713 }
715 DrawSelectionShelf(*primary_viewer);
716 }
717
718 // Reserve a fixed strip at the bottom for the status bar so neither
719 // single-room, split, nor connected-mode rendering can run past it and
720 // clip against the outer window chrome. Inner scrolling (wheel zoom in
721 // connected mode, canvas pan in single-room) happens inside this body
722 // child — the outer ##DungeonWorkbenchCanvas never needs a scrollbar.
723 const float status_bar_reserved =
724 std::max(
725 ImGui::GetFontSize() + ImGui::GetStyle().FramePadding.y * 2.0f,
727 ImGui::GetStyle().ItemSpacing.y;
728
729 const bool connected_mode = layout_state_.show_connected_canvas_view;
730 ImGuiWindowFlags body_flags = 0;
731 if (connected_mode) {
732 // Body child owns the connected-mode scroll container; matrix no
733 // longer nests its own BeginChild for scrolling.
734 const ImVec2 content_size =
736 const float scale = primary_viewer->ConnectedCanvasScale();
737 ImGui::SetNextWindowContentSize(
738 ImVec2(content_size.x * scale, content_size.y * scale));
739 body_flags |= ImGuiWindowFlags_HorizontalScrollbar |
740 ImGuiWindowFlags_NoScrollWithMouse;
741 }
742 const bool body_open = ImGui::BeginChild(
743 "##DungeonCanvasBody", ImVec2(0.0f, -status_bar_reserved), false,
744 body_flags);
745 if (body_open) {
746 if (connected_mode) {
747 split_view_enabled_ = false;
748 if (primary_viewer->DrawConnectedRoomMatrix(*current_room_id_)
749 .has_value()) {
751 }
752 } else if (split_view_enabled_) {
753 DrawSplitView(*primary_viewer);
754 } else {
755 primary_viewer->DrawDungeonCanvas(*current_room_id_);
756 }
757 }
758 ImGui::EndChild();
759
760 const char* tool_mode =
761 primary_viewer->object_interaction().mode_manager().GetModeName();
762 bool room_dirty = false;
763 if (auto* rooms = primary_viewer->rooms();
764 rooms && current_room_id_ && *current_room_id_ >= 0) {
765 if (const auto* room = rooms->GetIfMaterialized(*current_room_id_)) {
766 room_dirty = room->HasUnsavedChanges();
767 }
768 }
769 auto status =
770 DungeonStatusBar::BuildState(*primary_viewer, tool_mode, room_dirty);
771 status.workflow_mode = layout_state_.show_connected_canvas_view
774 status.workflow_primary = true;
775 if (can_undo_)
776 status.can_undo = can_undo_();
777 if (can_redo_)
778 status.can_redo = can_redo_();
779 if (undo_desc_) {
780 static std::string s_undo_desc;
781 s_undo_desc = undo_desc_();
782 status.undo_desc = s_undo_desc.empty() ? nullptr : s_undo_desc.c_str();
783 }
784 if (redo_desc_) {
785 static std::string s_redo_desc;
786 s_redo_desc = redo_desc_();
787 status.redo_desc = s_redo_desc.empty() ? nullptr : s_redo_desc.c_str();
788 }
789 if (undo_depth_)
790 status.undo_depth = undo_depth_();
791 status.on_undo = on_undo_;
792 status.on_redo = on_redo_;
794 } else {
795 ImGui::TextDisabled(tr("No active viewer"));
796 }
797 }
798 ImGui::EndChild();
799}
800
802 auto& interaction = viewer.object_interaction();
804 interaction, viewer.rooms(), viewer.current_room_id());
805 const size_t object_count = interaction.GetSelectionCount();
806 const bool has_entity = interaction.HasEntitySelection();
807 if (object_count == 0 && !has_entity) {
808 return;
809 }
810
811 // Gentle compaction only; the start_action lambda's per-button SameLine
812 // already controls inter-button spacing within a row, so don't shrink
813 // ItemSpacing.x below the theme default here.
814 gui::StyleVarGuard frame_padding_guard(
815 ImGuiStyleVar_FramePadding,
816 ImVec2(std::max(4.0f, ImGui::GetStyle().FramePadding.x),
817 std::max(3.0f, ImGui::GetStyle().FramePadding.y - 1.0f)));
818 gui::StyleVarGuard item_spacing_guard(
819 ImGuiStyleVar_ItemSpacing,
820 ImVec2(std::max(ImGui::GetStyle().ItemSpacing.x, 4.0f),
821 std::max(3.0f, ImGui::GetStyle().ItemSpacing.y - 1.0f)));
822
823 const float spacing = ImGui::GetStyle().ItemSpacing.x;
824 bool first_button = true;
825 auto start_action = [&](const char* label) {
826 const float button_width = ImGui::CalcTextSize(label).x +
827 (ImGui::GetStyle().FramePadding.x * 2.0f) + 8.0f;
828 if (!first_button) {
829 const float next_x = ImGui::GetItemRectMax().x + spacing + button_width;
830 const float max_x =
831 ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
832 if (next_x <= max_x) {
833 ImGui::SameLine(0.0f, spacing);
834 }
835 }
836 first_button = false;
837 };
838
839 auto draw_action = [&](const char* label, bool enabled,
840 const auto& on_press) {
841 start_action(label);
842 if (!enabled) {
843 ImGui::BeginDisabled();
844 }
845 if (workbench::DrawActionButton(label, ImVec2(0.0f, 0.0f)) && enabled) {
846 on_press();
847 }
848 if (!enabled) {
849 ImGui::EndDisabled();
850 }
851 };
852
853 ImGui::TextColored(AgentUI::GetTheme().text_secondary_gray,
854 ICON_MD_SELECT_ALL " %s",
855 GetDungeonSelectionSummaryText(snapshot).c_str());
856
857 const SelectedEntity selection = interaction.GetSelectedEntity();
858 const char* local_tool_label = nullptr;
860 if (object_count == 0 && has_entity) {
861 switch (selection.type) {
862 case EntityType::Door:
863 local_tool = WorkbenchTool::DoorEditor;
864 local_tool_label = ICON_MD_DOOR_FRONT " Door Tools";
865 break;
867 local_tool = WorkbenchTool::SpriteEditor;
868 local_tool_label = ICON_MD_PERSON " Sprite Tools";
869 break;
870 case EntityType::Item:
871 local_tool = WorkbenchTool::ItemEditor;
872 local_tool_label = ICON_MD_INVENTORY " Item Tools";
873 break;
874 default:
875 break;
876 }
877 }
878
879 const bool can_copy_selection = snapshot.object_count > 0 ||
880 snapshot.sprite_count > 0 ||
881 snapshot.item_count > 0;
882 draw_action(ICON_MD_CONTENT_COPY " Copy", can_copy_selection,
883 [&]() { interaction.HandleCopySelected(); });
884 draw_action(ICON_MD_CONTENT_PASTE " Paste", interaction.HasClipboardData(),
885 [&]() { interaction.HandlePasteObjects(); });
886 draw_action(ICON_MD_DELETE " Delete", true,
887 [&]() { interaction.HandleDeleteSelected(); });
888 draw_action(ICON_MD_CLEAR " Clear", true, [&]() {
889 interaction.ClearSelection();
890 interaction.ClearEntitySelection();
891 });
892 draw_action(ICON_MD_TUNE " Inspector", true,
893 [&]() { FocusSelectionInspector(); });
894 if (local_tool_label != nullptr) {
895 draw_action(local_tool_label, true, [&]() { OpenTool(local_tool); });
896 }
897
898 ImGui::Dummy(ImVec2(0.0f, 2.0f));
899}
900
901void DungeonWorkbenchContent::DrawInspectorPane(float width, float height,
902 float button_size, bool compact,
903 DungeonCanvasViewer* viewer) {
904 const bool inspector_open = ImGui::BeginChild("##DungeonWorkbenchInspector",
905 ImVec2(width, height), true);
906 if (inspector_open) {
907 DrawInspectorHeader(button_size, compact);
908 if (viewer) {
909 DrawInspector(*viewer, compact);
910 } else {
911 ImGui::TextDisabled(tr("No active viewer"));
912 }
913 }
914 ImGui::EndChild();
915}
916
918 bool compact) {
919 // Chevron flips with mirror state so the collapse arrow always points
920 // toward the inspector's exit edge (off-right when on right, off-left when
921 // on left).
922 const char* collapse_icon =
924 const float collapse_w =
925 workbench::CalcIconButtonWidth(collapse_icon, button_size);
926 const float swap_w =
928 const float spacing = ImGui::GetStyle().ItemSpacing.x;
929 const float action_cluster_w = swap_w + spacing + collapse_w;
930
932 "##DungeonWorkbenchInspectorHeader", ICON_MD_TUNE, "Inspect", "Inspect",
933 nullptr, compact, action_cluster_w, [&]() {
935 "SwapInspectorSide", ICON_MD_SWAP_HORIZ, button_size,
936 inspector_on_left_ ? "Move inspector to the right side"
937 : "Move inspector to the left side "
938 "(ZScream layout)")) {
942 }
943 }
944 ImGui::SameLine(0.0f, spacing);
945 if (workbench::DrawHeaderIconAction("CollapseInspector", collapse_icon,
946 button_size,
947 "Collapse inspector")) {
949 }
950 });
951
952 ImGui::Dummy(ImVec2(0.0f, 2.0f));
953 DrawInspectorPrimarySelector(std::max(button_size, 26.0f));
954 if (current_room_id_ && *current_room_id_ >= 0) {
955 ImGui::AlignTextToFramePadding();
956 ImGui::TextUnformatted(tr("Room"));
957 ImGui::SameLine();
958 uint16_t requested_room_id = static_cast<uint16_t>(*current_room_id_);
959 ImGui::SetNextItemWidth(82.0f);
960 const bool can_jump_room = static_cast<bool>(on_room_selected_);
961 if (!can_jump_room) {
962 ImGui::BeginDisabled();
963 }
964 bool room_changed = false;
965 {
966 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
967 room_changed = gui::InputScalarDeferred(
968 "##WorkbenchRoomId", ImGuiDataType_U16, &requested_room_id, "%04X",
969 ImGuiInputTextFlags_CharsHexadecimal,
970 {reinterpret_cast<uintptr_t>(current_room_id_)});
971 gui::AutoRegisterLastItem("input_scalar", "room_id",
972 "Current dungeon room ID");
973 }
974 if (!can_jump_room) {
975 ImGui::EndDisabled();
976 }
977 if (room_changed && on_room_selected_) {
978 const int target_room_id = std::clamp(static_cast<int>(requested_room_id),
980 if (target_room_id != *current_room_id_) {
981 *current_room_id_ = target_room_id;
982 on_room_selected_(target_room_id);
983 }
984 }
985 if (ImGui::IsItemHovered()) {
986 const int preview_room_id = std::clamp(
987 static_cast<int>(requested_room_id), 0, zelda3::kNumberOfRooms - 1);
988 ImGui::SetTooltip(tr("Open room 0x%03X"), preview_room_id);
989 }
990 }
991 ImGui::Separator();
992}
993
996 return;
997 }
998
999 DungeonRoomStore* rooms = nullptr;
1000 if (auto* viewer = get_viewer_ ? get_viewer_() : nullptr) {
1001 rooms = viewer->rooms();
1002 }
1003
1004 const auto& recent = get_recent_rooms_();
1005 if (recent.empty()) {
1006 return;
1007 }
1008 // Copy IDs up-front so we can safely mutate the underlying MRU list (close
1009 // tabs) without invalidating iterators mid-loop.
1010 std::vector<int> recent_ids(recent.begin(), recent.end());
1011 std::vector<int> to_forget;
1012
1013 constexpr ImGuiTabBarFlags kFlags = ImGuiTabBarFlags_AutoSelectNewTabs |
1014 ImGuiTabBarFlags_FittingPolicyScroll |
1015 ImGuiTabBarFlags_TabListPopupButton;
1016
1017 // Adaptive frame padding: larger tabs on touch/iPad for easier tapping
1018 const ImVec2 frame_pad = ImGui::GetStyle().FramePadding;
1019 const bool is_touch = gui::LayoutHelpers::IsTouchDevice();
1020 const float extra_y = is_touch ? 6.0f : 1.0f;
1021 const float extra_x = is_touch ? 4.0f : 0.0f;
1022 gui::StyleVarGuard pad_guard(
1023 ImGuiStyleVar_FramePadding,
1024 ImVec2(frame_pad.x + extra_x, frame_pad.y + extra_y));
1025 const project::YazeProject* label_project =
1026 get_viewer_ && get_viewer_() ? get_viewer_()->project() : nullptr;
1027
1028 if (gui::BeginThemedTabBar("##DungeonRecentRooms", kFlags)) {
1029 for (int room_id : recent_ids) {
1030 bool open = true;
1031 const ImGuiTabItemFlags tab_flags =
1032 (room_id == *current_room_id_) ? ImGuiTabItemFlags_SetSelected : 0;
1033 const auto room_name =
1034 dungeon_project_labels::GetRoomLabel(label_project, room_id);
1035 const bool room_dirty =
1036 rooms != nullptr && rooms->GetIfMaterialized(room_id) != nullptr &&
1037 rooms->GetIfMaterialized(room_id)->HasUnsavedChanges();
1038 char tab_label[64];
1039 if (room_name.empty() || room_name == "Unknown") {
1040 snprintf(tab_label, sizeof(tab_label), "%03X%s##recent_%03X", room_id,
1041 room_dirty ? "*" : "", room_id);
1042 } else {
1043 snprintf(tab_label, sizeof(tab_label), "%03X%s %.12s##recent_%03X",
1044 room_id, room_dirty ? "*" : "", room_name.c_str(), room_id);
1045 }
1046 const bool selected = ImGui::BeginTabItem(tab_label, &open, tab_flags);
1047
1048 if (!open && forget_recent_room_) {
1049 to_forget.push_back(room_id);
1050 }
1051
1052 if (ImGui::IsItemHovered()) {
1053 const auto label =
1054 dungeon_project_labels::GetRoomLabel(label_project, room_id);
1055 ImGui::SetTooltip("[%03X] %s%s", room_id, label.c_str(),
1056 room_dirty ? "\nPending room changes" : "");
1057 }
1058
1059 if (ImGui::IsItemActivated() && room_id != *current_room_id_) {
1060 on_room_selected_(room_id);
1061 }
1062
1063 if (ImGui::BeginPopupContextItem()) {
1064 if (ImGui::MenuItem(ICON_MD_COMPARE_ARROWS " Compare")) {
1065 split_view_enabled_ = true;
1066 compare_room_id_ = room_id;
1067 }
1069 ImGui::MenuItem(ICON_MD_OPEN_IN_NEW " Open as Panel")) {
1072 }
1073 if (forget_recent_room_ && ImGui::MenuItem(ICON_MD_CLOSE " Close")) {
1074 to_forget.push_back(room_id);
1075 }
1076 ImGui::EndPopup();
1077 }
1078
1079 if (selected) {
1080 ImGui::EndTabItem();
1081 }
1082 }
1083
1085 }
1086
1087 if (!to_forget.empty() && forget_recent_room_) {
1088 for (int rid : to_forget) {
1090 }
1091 }
1092}
1093
1095 DungeonCanvasViewer& primary_viewer) {
1097 if (split_view_enabled_) {
1098 split_view_enabled_ = false;
1099 }
1100 return;
1101 }
1102
1103 // Choose a sensible default compare room (most-recent non-current).
1105 if (get_recent_rooms_) {
1106 for (int rid : get_recent_rooms_()) {
1107 if (rid != *current_room_id_) {
1108 compare_room_id_ = rid;
1109 break;
1110 }
1111 }
1112 }
1113 }
1114
1115 if (compare_room_id_ < 0) {
1116 // Nothing to compare yet.
1117 split_view_enabled_ = false;
1118 primary_viewer.DrawDungeonCanvas(*current_room_id_);
1119 return;
1120 }
1121
1122 constexpr ImGuiTableFlags kSplitFlags =
1123 ImGuiTableFlags_Resizable | ImGuiTableFlags_NoPadOuterX |
1124 ImGuiTableFlags_NoPadInnerX | ImGuiTableFlags_BordersInnerV;
1125
1126 if (!ImGui::BeginTable("##DungeonWorkbenchSplit", 2, kSplitFlags)) {
1127 primary_viewer.DrawDungeonCanvas(*current_room_id_);
1128 return;
1129 }
1130
1131 ImGui::TableSetupColumn("Active", ImGuiTableColumnFlags_WidthStretch);
1132 ImGui::TableSetupColumn("Compare", ImGuiTableColumnFlags_WidthStretch);
1133 ImGui::TableNextRow();
1134
1135 DungeonCanvasViewer* compare_viewer =
1137
1138 // Active pane (minimum height so canvas never collapses)
1139 ImGui::TableNextColumn();
1140 ImGui::AlignTextToFramePadding();
1141 const project::YazeProject* active_project = primary_viewer.project();
1142 ImGui::TextDisabled(
1143 ICON_MD_CROP_FREE " Active [%03X] %s", *current_room_id_,
1145 .c_str());
1146 ImGui::Separator();
1147 const bool split_active_open = gui::LayoutHelpers::BeginContentChild(
1148 "##SplitActive", ImVec2(0.0f, gui::UIConfig::kContentMinHeightCanvas));
1149 if (split_active_open) {
1150 primary_viewer.DrawDungeonCanvas(*current_room_id_);
1151 }
1153
1154 // Compare pane
1155 ImGui::TableNextColumn();
1156 ImGui::AlignTextToFramePadding();
1157 const project::YazeProject* compare_project =
1158 compare_viewer ? compare_viewer->project() : active_project;
1159 ImGui::TextDisabled(
1160 ICON_MD_COMPARE_ARROWS " Compare [%03X] %s", compare_room_id_,
1162 .c_str());
1163 ImGui::Separator();
1164 const bool split_compare_open = gui::LayoutHelpers::BeginContentChild(
1165 "##SplitCompare", ImVec2(0.0f, gui::UIConfig::kContentMinHeightCanvas));
1166 if (split_compare_open) {
1167 if (compare_viewer) {
1169 compare_viewer->canvas().ApplyScaleSnapshot(
1170 primary_viewer.canvas().GetConfig());
1171 }
1172 compare_viewer->DrawDungeonCanvas(compare_room_id_);
1173 } else {
1174 ImGui::TextDisabled(tr("No compare viewer"));
1175 }
1176 }
1178
1179 ImGui::EndTable();
1180}
1181
1183 room_dungeon_cache_.clear();
1184 room_dungeon_cache_built_ = true; // Always set, even if ROM missing.
1185 if (!rom_ || !rom_->is_loaded())
1186 return;
1187
1188 // Short dungeon names for display in the inspector badge.
1189 // Indices 0-13 = vanilla ALTTP dungeons; higher indices = custom/Oracle.
1190 static const char* const kShortNames[] = {
1191 "Sewers", "HC", "Eastern", "Desert", "A-Tower", "Swamp", "PoD",
1192 "Misery", "Skull", "Ice", "Hera", "Thieves", "Turtle", "GT",
1193 };
1194 constexpr int kVanillaCount =
1195 static_cast<int>(sizeof(kShortNames) / sizeof(kShortNames[0]));
1196
1197 auto AddRoom = [&](int room_id, int dungeon_id) {
1198 if (room_id < 0)
1199 return;
1200 if (room_dungeon_cache_.contains(room_id))
1201 return; // Entrance wins over spawn.
1202 if (dungeon_id >= 0 && dungeon_id < kVanillaCount) {
1203 room_dungeon_cache_[room_id] = kShortNames[dungeon_id];
1204 } else {
1205 char buf[16];
1206 snprintf(buf, sizeof(buf), "Dungeon %02X", dungeon_id);
1207 room_dungeon_cache_[room_id] = buf;
1208 }
1209 };
1210
1211 // Standard entrances (0x00–0x83) — authoritative dungeon assignment.
1212 for (int i = 0; i < 0x84; ++i) {
1213 zelda3::RoomEntrance ent(rom_, static_cast<uint8_t>(i), false);
1214 int did = ent.dungeon_id_;
1215 if (did >= 0 && did < kVanillaCount) {
1216 room_dungeon_cache_[ent.room_] = kShortNames[did];
1217 } else {
1218 char buf[16];
1219 snprintf(buf, sizeof(buf), "Dungeon %02X", did);
1220 room_dungeon_cache_[ent.room_] = buf;
1221 }
1222 }
1223
1224 // Spawn points (0x00–0x13) — fill in rooms not covered by entrances.
1225 for (int i = 0; i < 0x14; ++i) {
1226 zelda3::RoomEntrance ent(rom_, static_cast<uint8_t>(i), true);
1227 AddRoom(static_cast<int>(ent.room_), static_cast<int>(ent.dungeon_id_));
1228 }
1229}
1230
1232 bool compact) {
1233 // Gentle compaction only; inspector sections own their own internal
1234 // spacing (collapsing-header headers, action buttons), so let the theme
1235 // defaults shape inter-section breathing room here.
1236 gui::StyleVarGuard item_spacing_guard(
1237 ImGuiStyleVar_ItemSpacing,
1238 ImVec2(std::max(ImGui::GetStyle().ItemSpacing.x, 4.0f),
1239 std::max(4.0f, ImGui::GetStyle().ItemSpacing.y - 1.0f)));
1240 DrawInspectorShelf(viewer, compact);
1241}
1242
1244 auto& flags = core::FeatureFlags::get().dungeon;
1245 flags.kSaveObjects = value;
1246 flags.kSaveSprites = value;
1247 flags.kSaveRoomHeaders = value;
1248 flags.kSaveChests = value;
1249 flags.kSavePotItems = value;
1250 flags.kSavePalettes = value;
1251 flags.kSaveCollision = value;
1252 flags.kSaveBlocks = value;
1253 flags.kSaveTorches = value;
1254 flags.kSavePits = value;
1255}
1256
1258 auto& flags = core::FeatureFlags::get().dungeon;
1259 bool use_workbench = flags.kUseWorkbench;
1260 if (ImGui::Checkbox(tr("Single-window Workbench"), &use_workbench)) {
1261 flags.kUseWorkbench = use_workbench;
1262 if (set_workflow_mode_) {
1263 set_workflow_mode_(use_workbench);
1264 }
1265 }
1266 if (ImGui::IsItemHovered()) {
1267 ImGui::SetTooltip(tr(
1268 "Keep Dungeon editing in the integrated Workbench instead of separate "
1269 "high-level room panels."));
1270 }
1271
1272 ImGui::Separator();
1273 ImGui::TextDisabled(tr("Data written by Apply Room / Apply Loaded Rooms"));
1274 constexpr ImGuiTableFlags kFlags =
1275 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
1276 if (ImGui::BeginTable("##WorkbenchApplyScopeFlags", 2, kFlags)) {
1277 auto draw_checkbox = [](const char* label, bool* value) {
1278 ImGui::TableNextColumn();
1279 ImGui::Checkbox(label, value);
1280 };
1281 ImGui::TableNextRow();
1282 draw_checkbox("Room Objects", &flags.kSaveObjects);
1283 draw_checkbox("Sprites", &flags.kSaveSprites);
1284 ImGui::TableNextRow();
1285 draw_checkbox("Room Headers", &flags.kSaveRoomHeaders);
1286 draw_checkbox("Chests", &flags.kSaveChests);
1287 ImGui::TableNextRow();
1288 draw_checkbox("Pot Items", &flags.kSavePotItems);
1289 draw_checkbox("Palettes", &flags.kSavePalettes);
1290 ImGui::TableNextRow();
1291 draw_checkbox("Collision Maps", &flags.kSaveCollision);
1292 ImGui::TableNextColumn();
1293 ImGui::BeginDisabled();
1294 ImGui::Checkbox("Water Fill", &flags.kSaveWaterFillZones);
1295 ImGui::EndDisabled();
1296 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1297 ImGui::SetTooltip(
1298 "Water Fill save scope is fixed when the project opens. Change "
1299 "save_dungeon_water_fill_zones in the .yaze file, then reopen the "
1300 "project.");
1301 }
1302 ImGui::TableNextRow();
1303 draw_checkbox("Blocks", &flags.kSaveBlocks);
1304 draw_checkbox("Torches", &flags.kSaveTorches);
1305 ImGui::TableNextRow();
1306 draw_checkbox("Pits", &flags.kSavePits);
1307 ImGui::TableNextColumn();
1308 ImGui::EndTable();
1309 }
1310
1311 if (ImGui::SmallButton(tr("Select All##WorkbenchApplyScope"))) {
1312 SetAllSaveFlags(true);
1313 }
1314 ImGui::SameLine();
1315 if (ImGui::SmallButton(tr("Select None##WorkbenchApplyScope"))) {
1316 SetAllSaveFlags(false);
1317 }
1318
1319 ImGui::Separator();
1320 if (on_save_room_ && room_id >= 0 &&
1321 workbench::DrawActionButton(ICON_MD_SAVE " Apply Current Room",
1322 ImVec2(-1, 0))) {
1323 on_save_room_(room_id);
1324 }
1325 if (on_save_all_rooms_ &&
1326 workbench::DrawActionButton(ICON_MD_SAVE_ALT " Apply Loaded Rooms",
1327 ImVec2(-1, 0))) {
1329 }
1330}
1331
1334 const auto& theme = AgentUI::GetTheme();
1335 zelda3::PitDamageTable* table =
1337 if (!table) {
1338 ImGui::TextDisabled(
1339 tr("Pit damage table is unavailable until ROM data loads."));
1340 return;
1341 }
1342 const auto membership = BuildPitDamageMembershipState(
1343 table, room_id, pit_damage_replacement_room_id_,
1345 if (!membership.room_valid) {
1346 ImGui::TextDisabled(tr("No valid room selected."));
1347 return;
1348 }
1349
1350 const uint16_t current_room = membership.room_id;
1351 const bool room_deals_damage = membership.deals_damage;
1352 ImGui::TextColored(
1353 room_deals_damage ? theme.status_warning : theme.text_secondary_gray,
1354 tr("%s Room 0x%03X %s pit damage"),
1355 room_deals_damage ? ICON_MD_WARNING : ICON_MD_INFO, room_id,
1356 room_deals_damage ? "deals" : "does not deal");
1357 ImGui::TextWrapped(tr(
1358 "RoomsWithPitDamage is fixed-capacity in vanilla. Editing replaces one "
1359 "listed room with another instead of growing or shrinking the table."));
1360
1361 if (membership.dirty) {
1362 ImGui::TextColored(theme.status_warning,
1363 ICON_MD_EDIT " Pending pit table change");
1364 }
1365
1366 auto show_status = [&]() {
1367 if (pit_damage_status_message_.empty()) {
1368 return;
1369 }
1370 ImGui::TextColored(
1371 pit_damage_status_error_ ? theme.status_error : theme.status_success,
1372 "%s %s",
1375 };
1376
1377 ImGui::Separator();
1378 if (room_deals_damage) {
1379 if (!membership.suggested_replacement_room.has_value()) {
1380 ImGui::TextDisabled(tr("No available non-damaging room slot."));
1381 show_status();
1382 return;
1383 }
1384 pit_damage_replacement_room_id_ = *membership.suggested_replacement_room;
1385
1386 ImGui::TextDisabled(tr("Remove current room by replacing it with:"));
1387 ImGui::SetNextItemWidth(88.0f);
1388 auto edit_value = pit_damage_replacement_room_id_;
1389 if (auto res = gui::InputHexWordEx("##PitDamageReplacementRoom",
1390 &edit_value, 88.0f, true);
1391 res.ShouldApply()) {
1393 std::min<uint16_t>(edit_value, zelda3::kNumberOfRooms - 1);
1394 }
1395 if (ImGui::IsItemHovered()) {
1396 ImGui::SetTooltip(
1397 tr("Replacement room must not already be in the table."));
1398 }
1399 ImGui::SameLine();
1400 const bool replace_clicked =
1401 ImGui::Button(tr("Replace current##PitDamageReplaceCurrent"));
1402 pit_damage_control_rects_.replace_current = CaptureLastItemRectForTesting();
1403 if (replace_clicked) {
1404 auto status = RemoveCurrentRoomFromPitDamage(
1405 table, current_room, pit_damage_replacement_room_id_);
1406 pit_damage_status_error_ = !status.ok();
1408 status.ok()
1409 ? absl::StrFormat("Room 0x%03X removed; slot now 0x%03X",
1410 current_room, pit_damage_replacement_room_id_)
1411 : std::string(status.message());
1412 }
1413 } else {
1414 if (!membership.suggested_victim_room.has_value()) {
1415 ImGui::TextDisabled(tr("No existing pit-damage slot is available."));
1416 show_status();
1417 return;
1418 }
1419 pit_damage_victim_room_id_ = *membership.suggested_victim_room;
1420
1421 ImGui::TextDisabled(tr("Add current room by replacing listed room:"));
1422 ImGui::SetNextItemWidth(88.0f);
1423 auto edit_value = pit_damage_victim_room_id_;
1424 if (auto res = gui::InputHexWordEx("##PitDamageVictimRoom", &edit_value,
1425 88.0f, true);
1426 res.ShouldApply()) {
1428 std::min<uint16_t>(edit_value, zelda3::kNumberOfRooms - 1);
1429 }
1430 if (ImGui::IsItemHovered()) {
1431 ImGui::SetTooltip(tr("This room will stop dealing pit damage."));
1432 }
1433 ImGui::SameLine();
1434 const bool add_clicked =
1435 ImGui::Button(tr("Add current##PitDamageAddCurrent"));
1436 pit_damage_control_rects_.add_current = CaptureLastItemRectForTesting();
1437 if (add_clicked) {
1438 auto status = AddCurrentRoomToPitDamage(table, current_room,
1440 pit_damage_status_error_ = !status.ok();
1442 status.ok()
1443 ? absl::StrFormat("Room 0x%03X added; replaced 0x%03X",
1444 current_room, pit_damage_victim_room_id_)
1445 : std::string(status.message());
1446 }
1447 }
1448
1449 show_status();
1450 ImGui::TextDisabled(
1451 tr("Apply Current Room / Apply Loaded Rooms writes this via "
1452 "SaveAllPits when the Pits scope is enabled."));
1453}
1454
1456 DungeonCanvasViewer& viewer, int room_id) {
1457 if (room_id < 0) {
1458 ImGui::TextDisabled(tr("No active room"));
1459 return;
1460 }
1461
1462 auto& layer_manager = viewer.GetRoomLayerManager(room_id);
1463 auto draw_blend_combo = [&](const char* combo_id,
1464 zelda3::LayerType layer_type) {
1465 zelda3::LayerBlendMode current_mode =
1466 layer_manager.GetLayerBlendMode(layer_type);
1467 const char* current_name =
1469
1470 ImGui::SetNextItemWidth(-1);
1471 if (ImGui::BeginCombo(combo_id, current_name)) {
1472 for (int mode_int = 0; mode_int <= 4; ++mode_int) {
1473 const auto mode = static_cast<zelda3::LayerBlendMode>(mode_int);
1474 const char* mode_name =
1476 const bool selected = current_mode == mode;
1477 if (ImGui::Selectable(mode_name, selected)) {
1478 layer_manager.SetLayerBlendMode(layer_type, mode);
1479 }
1480 if (selected) {
1481 ImGui::SetItemDefaultFocus();
1482 }
1483 }
1484 ImGui::EndCombo();
1485 }
1486 };
1487
1488 struct BlendRow {
1489 const char* label;
1490 const char* combo_id;
1491 zelda3::LayerType layer_type;
1492 };
1493 static constexpr BlendRow kBlendRows[] = {
1494 {"BG1 Layout", "##WorkbenchBlendBG1Layout",
1496 {"BG1 Objects", "##WorkbenchBlendBG1Objects",
1498 {"BG2 Layout", "##WorkbenchBlendBG2Layout",
1500 {"BG2 Objects", "##WorkbenchBlendBG2Objects",
1502 };
1503
1504 constexpr ImGuiTableFlags kBlendTableFlags =
1505 ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_NoPadOuterX;
1506 if (ImGui::BeginTable("##WorkbenchLayerBlend", 2, kBlendTableFlags)) {
1507 ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, 92.0f);
1508 ImGui::TableSetupColumn("##combo", ImGuiTableColumnFlags_WidthStretch);
1509 for (const auto& row : kBlendRows) {
1510 ImGui::TableNextRow();
1511 ImGui::TableNextColumn();
1512 ImGui::AlignTextToFramePadding();
1513 ImGui::TextUnformatted(row.label);
1514 ImGui::TableNextColumn();
1515 draw_blend_combo(row.combo_id, row.layer_type);
1516 }
1517 ImGui::EndTable();
1518 }
1519
1520 if (workbench::DrawActionButton(ICON_MD_REFRESH " Reset Layer Blend",
1521 ImVec2(-1, 0))) {
1522 layer_manager.SetLayerBlendMode(zelda3::LayerType::BG1_Layout,
1524 layer_manager.SetLayerBlendMode(zelda3::LayerType::BG1_Objects,
1526 layer_manager.SetLayerBlendMode(zelda3::LayerType::BG2_Layout,
1528 layer_manager.SetLayerBlendMode(zelda3::LayerType::BG2_Objects,
1530 }
1531}
1532
1534 DungeonCanvasViewer& viewer) {
1536 return nullptr;
1537 }
1538 if (!embedded_dungeon_map_) {
1539 embedded_dungeon_map_ = std::make_unique<DungeonMapPanel>(
1541 on_room_selected_, viewer.rooms());
1543 if (*current_room_id_ >= 0) {
1545 }
1546 }
1547 embedded_dungeon_map_->SetRooms(viewer.rooms());
1548 if (const auto* project = viewer.project()) {
1549 embedded_dungeon_map_->SetHackManifest(&project->hack_manifest);
1550 } else {
1551 embedded_dungeon_map_->SetHackManifest(nullptr);
1552 }
1553 return embedded_dungeon_map_.get();
1554}
1555
1557 constexpr const char* kPopupId = "Dungeon Map##WorkbenchDungeonMapPopup";
1559 ImGui::SetNextWindowSize(ImVec2(660.0f, 520.0f), ImGuiCond_Appearing);
1560 ImGui::OpenPopup(kPopupId);
1562 }
1563
1564 bool popup_open = true;
1565 if (ImGui::BeginPopupModal(kPopupId, &popup_open,
1566 ImGuiWindowFlags_NoSavedSettings)) {
1567 if (!popup_open) {
1568 ImGui::CloseCurrentPopup();
1569 ImGui::EndPopup();
1570 return;
1571 }
1572 if (auto* map = GetEmbeddedDungeonMap(viewer)) {
1573 const ImVec2 map_size =
1574 ImVec2(std::max(360.0f, ImGui::GetContentRegionAvail().x),
1575 std::max(260.0f, ImGui::GetContentRegionAvail().y - 34.0f));
1576 if (ImGui::BeginChild("##WorkbenchDungeonMapBody", map_size, false)) {
1577 map->Draw(nullptr);
1578 }
1579 ImGui::EndChild();
1580 } else {
1581 ImGui::TextDisabled(tr("Dungeon map unavailable"));
1582 }
1583
1584 if (workbench::DrawActionButton(ICON_MD_CLOSE " Close", ImVec2(-1, 0))) {
1585 ImGui::CloseCurrentPopup();
1586 }
1587 ImGui::EndPopup();
1588 }
1589}
1590
1592 if (tool == WorkbenchTool::None) {
1593 return;
1594 }
1595 active_tool_ = tool;
1598}
1599
1601 WorkbenchTool tool) const {
1602 switch (tool) {
1604 return room_tag_panel_ != nullptr;
1606 return custom_collision_panel_ != nullptr;
1608 return water_fill_panel_ != nullptr;
1610 return minecart_track_panel_ != nullptr;
1612 return object_selector_content_ != nullptr;
1614 return door_editor_content_ != nullptr;
1616 return sprite_editor_content_ != nullptr;
1618 return item_editor_content_ != nullptr;
1620 return room_graphics_content_ != nullptr;
1622 return palette_editor_content_ != nullptr;
1624 return false;
1625 }
1626 return false;
1627}
1628
1630 WorkbenchTool tool) const {
1631 switch (tool) {
1633 return "room_tags";
1635 return "custom_collision";
1637 return "water_fill";
1639 return "minecart";
1641 return "object_selector";
1643 return "door";
1645 return "sprite";
1647 return "item";
1649 return "room_graphics";
1651 return "palette";
1653 return "none";
1654 }
1655 return "unknown";
1656}
1657
1659 WorkbenchTool tool) const {
1660 switch (tool) {
1662 return ICON_MD_LABEL " Room Tags";
1664 return ICON_MD_GRID_ON " Custom Collision";
1666 return ICON_MD_WATER_DROP " Water Fill";
1668 return ICON_MD_TRAIN " Minecart Tracks";
1670 return ICON_MD_CATEGORY " Object Selector";
1672 return ICON_MD_DOOR_FRONT " Door Tools";
1674 return ICON_MD_PERSON " Sprite Tools";
1676 return ICON_MD_INVENTORY " Item Tools";
1678 return ICON_MD_IMAGE " Room Graphics";
1680 return ICON_MD_PALETTE " Palette";
1682 return ICON_MD_BUILD " Tool";
1683 }
1684 return ICON_MD_BUILD " Tool";
1685}
1686
1688 WorkbenchTool tool) const {
1689 switch (tool) {
1691 return ICON_MD_LABEL;
1693 return ICON_MD_GRID_ON;
1695 return ICON_MD_WATER_DROP;
1697 return ICON_MD_TRAIN;
1699 return ICON_MD_CATEGORY;
1701 return ICON_MD_DOOR_FRONT;
1703 return ICON_MD_PERSON;
1705 return ICON_MD_INVENTORY;
1707 return ICON_MD_IMAGE;
1709 return ICON_MD_PALETTE;
1711 return ICON_MD_BUILD;
1712 }
1713 return ICON_MD_BUILD;
1714}
1715
1717 WorkbenchTool tool) const {
1718 switch (tool) {
1720 return "Room Tags";
1722 return "Custom Collision";
1724 return "Water Fill";
1726 return "Minecart Tracks";
1728 return "Object Selector";
1730 return "Door Tools";
1732 return "Sprite Tools";
1734 return "Item Tools";
1736 return "Room Graphics";
1738 return "Palette";
1740 return "Tool";
1741 }
1742 return "Tool";
1743}
1744
1746 WorkbenchTool tool) const {
1747 switch (tool) {
1749 return "Room tag tools are not available.";
1751 return "Custom collision tools are not available.";
1753 return "Water fill tools are not available.";
1755 return "Minecart track tools are not available.";
1757 return "Object selector is not available.";
1759 return "Door tools are not available.";
1761 return "Sprite tools are not available.";
1763 return "Item tools are not available.";
1765 return "Room graphics tools are not available.";
1767 return "Palette tools are not available.";
1769 return "No Workbench tool selected.";
1770 }
1771 return "Tool is not available.";
1772}
1773
1775 WorkbenchTool tool) {
1776 const int room_id = viewer.current_room_id();
1777 auto draw_window_content = [](WindowContent* content,
1778 const char* unavailable_message) {
1779 if (!content) {
1780 ImGui::TextDisabled("%s", unavailable_message);
1781 return;
1782 }
1783 content->Draw(nullptr);
1784 };
1785
1786 ImGui::PushID(GetWorkbenchToolId(tool));
1787 switch (tool) {
1789 if (!room_tag_panel_) {
1790 ImGui::TextDisabled("%s", GetWorkbenchToolUnavailableMessage(tool));
1791 break;
1792 }
1794 room_tag_panel_->Draw(nullptr);
1795 break;
1798 ImGui::TextDisabled("%s", GetWorkbenchToolUnavailableMessage(tool));
1799 break;
1800 }
1803 custom_collision_panel_->Draw(nullptr);
1804 break;
1806 if (!water_fill_panel_) {
1807 ImGui::TextDisabled("%s", GetWorkbenchToolUnavailableMessage(tool));
1808 break;
1809 }
1812 water_fill_panel_->Draw(nullptr);
1813 break;
1815 if (!minecart_track_panel_) {
1816 ImGui::TextDisabled("%s", GetWorkbenchToolUnavailableMessage(tool));
1817 break;
1818 }
1819 minecart_track_panel_->Draw(nullptr);
1820 break;
1822 draw_window_content(object_selector_content_,
1824 break;
1826 draw_window_content(door_editor_content_,
1828 break;
1830 draw_window_content(sprite_editor_content_,
1832 break;
1834 draw_window_content(item_editor_content_,
1836 break;
1838 draw_window_content(room_graphics_content_,
1840 break;
1842 draw_window_content(palette_editor_content_,
1844 break;
1846 ImGui::TextDisabled("%s", GetWorkbenchToolUnavailableMessage(tool));
1847 break;
1848 }
1849 ImGui::PopID();
1850}
1851
1853 // Two-row icon strip lets users swap tools in one click without scrolling
1854 // past the active body. Row 1 holds entity/selection tools; row 2 holds
1855 // room-data tools. Active tool is highlighted via gui::ToggleButton accent.
1856 static constexpr WorkbenchTool kStrip[2][5] = {
1863 };
1864
1865 constexpr ImGuiTableFlags kFlags =
1866 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
1867 if (!ImGui::BeginTable("##WorkbenchToolStrip", 5, kFlags)) {
1868 return;
1869 }
1870 for (int row = 0; row < 2; ++row) {
1871 ImGui::TableNextRow();
1872 for (int col = 0; col < 5; ++col) {
1873 ImGui::TableNextColumn();
1874 const WorkbenchTool tool = kStrip[row][col];
1875 const bool enabled = IsWorkbenchToolAvailable(tool);
1876 const bool active = active_tool_ == tool;
1877 char btn_id[48];
1878 std::snprintf(btn_id, sizeof(btn_id), "%s##StripTool_%s",
1880 if (!enabled) {
1881 ImGui::BeginDisabled();
1882 }
1883 const bool pressed = gui::ToggleButton(btn_id, active, ImVec2(-1, 0));
1884 {
1885 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
1887 "button", absl::StrFormat("tool_%s", GetWorkbenchToolId(tool)),
1888 "Open a Dungeon Workbench tool");
1889 }
1890 if (pressed && enabled) {
1891 OpenTool(tool);
1892 }
1893 if (!enabled) {
1894 ImGui::EndDisabled();
1895 }
1896 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1897 if (enabled) {
1898 ImGui::SetTooltip("%s", GetWorkbenchToolShortLabel(tool));
1899 } else {
1900 ImGui::SetTooltip("%s\n(%s)", GetWorkbenchToolShortLabel(tool),
1902 }
1903 }
1904 }
1905 }
1906 ImGui::EndTable();
1907}
1908
1910 DungeonCanvasViewer& viewer) {
1911 // Quick-switch strip first: users can hop between tools without leaving the
1912 // drawer. The inspector primary segmented selector handles Room/Selection
1913 // returns, so no in-drawer back button is needed.
1915
1916 ImGui::Dummy(ImVec2(0.0f, 2.0f));
1918
1919 const bool available = IsWorkbenchToolAvailable(active_tool_);
1920 if (!available) {
1921 ImGui::TextDisabled("%s", GetWorkbenchToolUnavailableMessage(active_tool_));
1922 return;
1923 }
1924
1925 // The strip + section header take ~70-90px depending on font scale. Anything
1926 // remaining in the inspector belongs to the tool body, with a small floor so
1927 // narrow inspectors still leave room for at least a few rows of controls.
1928 const float available_h = std::max(180.0f, ImGui::GetContentRegionAvail().y);
1929 const bool body_open =
1930 ImGui::BeginChild("##WorkbenchToolDrawerBody", ImVec2(0.0f, available_h),
1931 true, ImGuiWindowFlags_HorizontalScrollbar);
1932 if (body_open) {
1934 }
1935 ImGui::EndChild();
1936}
1937
1939 float segment_height) {
1940 const float spacing = ImGui::GetStyle().ItemSpacing.x;
1941 const float room_width =
1942 workbench::CalcIconButtonWidth("Room", segment_height);
1943 const float selection_width =
1944 workbench::CalcIconButtonWidth("Selection", segment_height);
1945 const float tools_width =
1946 workbench::CalcIconButtonWidth("Tools", segment_height);
1947 const float available_width = ImGui::GetContentRegionAvail().x;
1948 const bool stack = available_width < (room_width + selection_width +
1949 tools_width + spacing * 2.0f);
1950 auto draw_mode = [&](const char* label, float width, InspectorMode mode) {
1951 const ImVec2 size(stack ? ImGui::GetContentRegionAvail().x : width,
1952 segment_height);
1953 const bool pressed =
1954 gui::ToggleButton(label, inspector_mode_ == mode, size);
1955 {
1956 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
1958 "button",
1959 mode == InspectorMode::Room ? "mode_room" : "mode_selection",
1960 "Switch Dungeon Workbench inspector mode");
1961 }
1962 if (pressed) {
1963 inspector_mode_ = mode;
1965 }
1966 if (!stack) {
1967 ImGui::SameLine(0.0f, spacing);
1968 }
1969 };
1970
1971 draw_mode("Room", room_width, InspectorMode::Room);
1972 draw_mode("Selection", selection_width, InspectorMode::Selection);
1973 const ImVec2 tools_size(
1974 stack ? ImGui::GetContentRegionAvail().x : tools_width, segment_height);
1975 const bool tools_pressed = gui::ToggleButton(
1976 "Tools", inspector_mode_ == InspectorMode::Tools, tools_size);
1977 {
1978 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
1979 gui::AutoRegisterLastItem("button", "mode_tools",
1980 "Switch Dungeon Workbench inspector mode");
1981 }
1982 if (tools_pressed) {
1985 }
1986}
1987
1989 DungeonCanvasViewer& viewer) {
1990 const int room_id = (viewer.current_room_id() >= 0)
1991 ? viewer.current_room_id()
1993 const auto& interaction = viewer.object_interaction();
1994 const size_t selected_objects = interaction.GetSelectionCount();
1995 const bool has_entity = interaction.HasEntitySelection();
1996
1997 ImGui::TextDisabled(ICON_MD_SUMMARIZE " Summary");
1998 if (room_id >= 0) {
1999 ImGui::Text("[%03X] %s", room_id,
2001 .c_str());
2002 } else {
2003 ImGui::TextDisabled(tr("No room selected"));
2004 }
2005
2006 ImGui::Dummy(ImVec2(0.0f, 2.0f));
2007 if (selected_objects > 0 || has_entity) {
2008 ImGui::TextDisabled(ICON_MD_SELECT_ALL " Focus");
2009 if (has_entity) {
2010 ImGui::BulletText(tr("Entity selected"));
2011 }
2012 if (selected_objects > 0) {
2013 ImGui::BulletText(tr("%zu object%s selected"), selected_objects,
2014 selected_objects == 1 ? "" : "s");
2015 }
2017 ImVec2(-1, 0))) {
2020 }
2021 } else {
2022 ImGui::TextDisabled(tr("Nothing selected"));
2023 }
2024
2025 // Apply Room, View overlays, and Tools quick-grid all live elsewhere now:
2026 // Apply Room is on the canvas toolbar, the overlay checkboxes live in the
2027 // toolbar's View Options popup, and Tools have their own inspector primary
2028 // mode (the segmented selector at the inspector header). Compact summary
2029 // stays focused on what's selected.
2030}
2031
2033 bool compact) {
2034 const auto& interaction = viewer.object_interaction();
2035 const bool has_selection =
2036 interaction.GetSelectionCount() > 0 || interaction.HasEntitySelection();
2037 if (has_selection && !inspector_selection_was_active_ &&
2040 }
2041 inspector_selection_was_active_ = has_selection;
2042
2045
2046 // Use the resolved pane layout instead of GetContentRegionAvail().x. The
2047 // latter changes when a vertical scrollbar appears, which can make the
2048 // inspector alternate between full and compact content every frame.
2049 if (compact && inspector_mode_ != InspectorMode::Tools &&
2052 return;
2053 }
2054
2055 switch (inspector_mode_) {
2057 DrawInspectorShelfRoom(viewer);
2058 break;
2061 break;
2064 return;
2065 }
2066
2067 // Stitched Rooms, View Options, and Tools collapsibles intentionally
2068 // removed: the canvas toolbar exposes the Stitched Rooms toggle directly,
2069 // the View Options popup off the toolbar's eye icon owns all 11 overlay
2070 // checkboxes, and the inspector primary segmented selector at the header
2071 // already routes between Room / Selection / Tools modes.
2072}
2073
2075 DungeonCanvasViewer& viewer) {
2076 const auto& theme = AgentUI::GetTheme();
2077
2078 int room_id = viewer.current_room_id();
2079 if (room_id < 0 && current_room_id_) {
2080 room_id = *current_room_id_;
2081 }
2082
2083 const std::string room_label =
2084 (room_id >= 0)
2086 : std::string("None");
2087
2088 // Room badge: hex ID + copy button (only for valid room IDs).
2090 if (room_id >= 0) {
2091 ImGui::AlignTextToFramePadding();
2092 ImGui::Text(tr("Room 0x%03X"), room_id);
2093 ImGui::SameLine();
2094 if (ImGui::SmallButton(ICON_MD_CONTENT_COPY "##CopyRoomId")) {
2095 char buf[16];
2096 snprintf(buf, sizeof(buf), "0x%03X", room_id);
2097 ImGui::SetClipboardText(buf);
2098 }
2099 if (ImGui::IsItemHovered()) {
2100 ImGui::SetTooltip(tr("Copy room ID (0x%03X) to clipboard"), room_id);
2101 }
2102
2103 if (auto* rooms = viewer.rooms();
2104 rooms && room_id < static_cast<int>(rooms->size())) {
2105 auto& objects = (*rooms)[room_id].GetTileObjects();
2106 if (!objects.empty()) {
2107 const auto selected_indices =
2109 int requested_object_index =
2110 selected_indices.size() == 1
2111 ? static_cast<int>(selected_indices.front())
2112 : 0;
2113 ImGui::AlignTextToFramePadding();
2114 ImGui::TextUnformatted(tr("Object"));
2115 ImGui::SameLine();
2116 ImGui::SetNextItemWidth(74.0f);
2117 bool object_index_changed = false;
2118 {
2119 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
2120 object_index_changed = gui::InputScalarDeferred(
2121 "##WorkbenchObjectIndex", ImGuiDataType_S32,
2122 &requested_object_index, "%d", 0,
2123 {reinterpret_cast<uintptr_t>(rooms),
2124 static_cast<uint64_t>(room_id)});
2125 gui::AutoRegisterLastItem("input_int", "object_index",
2126 "Select and locate a room object by index");
2127 }
2128 if (object_index_changed) {
2129 const size_t target_index = static_cast<size_t>(std::clamp(
2130 requested_object_index, 0, static_cast<int>(objects.size()) - 1));
2131 viewer.object_interaction().SetSelectedObjects({target_index});
2132 viewer.ScrollToTile(objects[target_index].x(),
2133 objects[target_index].y());
2135 }
2136 if (ImGui::IsItemHovered()) {
2137 ImGui::SetTooltip(tr("Select object index 0-%zu"),
2138 objects.size() - 1);
2139 }
2140 }
2141 }
2142 } else {
2143 ImGui::TextUnformatted(tr("Room: None"));
2144 }
2145
2146 bool room_dirty = false;
2147 if (auto* rooms = viewer.rooms(); rooms && room_id >= 0) {
2148 if (const auto* room = rooms->GetIfMaterialized(room_id)) {
2149 room_dirty = room->HasUnsavedChanges();
2150 }
2151 }
2152 if (room_dirty) {
2153 ImGui::TextColored(theme.status_warning,
2154 ICON_MD_EDIT " Pending room changes");
2155 } else if (room_id >= 0) {
2156 ImGui::TextDisabled(ICON_MD_CHECK " Room matches ROM buffer");
2157 }
2158
2159 // Dungeon group context: prefer ROM entrance-based lookup (accurate for
2160 // custom Oracle dungeons); fall back to blockset-derived name.
2163 }
2164 if (room_id >= 0) {
2165 std::string project_group_name =
2167 room_id);
2168 const char* group_name =
2169 project_group_name.empty() ? nullptr : project_group_name.c_str();
2170 if (!group_name) {
2171 auto cache_it = room_dungeon_cache_.find(room_id);
2172 if (cache_it != room_dungeon_cache_.end() && !cache_it->second.empty()) {
2173 group_name = cache_it->second.c_str();
2174 }
2175 }
2176 if (!group_name) {
2177 auto* rooms = viewer.rooms();
2178 if (rooms && room_id < static_cast<int>(rooms->size())) {
2180 (*rooms)[room_id].blockset());
2181 }
2182 }
2183 if (group_name) {
2184 ImGui::TextDisabled(ICON_MD_CASTLE " %s – %s", group_name,
2185 room_label.c_str());
2186 } else {
2187 ImGui::TextDisabled("%s", room_label.c_str());
2188 }
2189 } else {
2190 ImGui::TextDisabled("%s", room_label.c_str());
2191 }
2192
2193 // Apply Room and Dungeon Map have moved to the canvas toolbar (Save and
2194 // Map icons). Apply Scope and Layer Compositing remain here as
2195 // collapsibles since they're rarely-touched per-room batch settings.
2197 false)) {
2198 DrawApplyScopeControls(room_id);
2199 }
2200
2201 if (workbench::BeginInspectorSection(ICON_MD_WARNING " Pit Damage", false)) {
2202 DrawPitDamageControls(room_id);
2203 }
2204
2205 if (workbench::BeginInspectorSection(ICON_MD_LAYERS " Layer Compositing",
2206 false)) {
2207 DrawLayerCompositingControls(viewer, room_id);
2208 }
2209
2210 // ZScream-style compact room header: keep the raw ROM fields together so
2211 // experienced dungeon authors can scan and edit them without panel hopping.
2213 if (auto* rooms = viewer.rooms();
2214 rooms && room_id >= 0 && room_id < static_cast<int>(rooms->size())) {
2215 auto& room = (*rooms)[room_id];
2216
2217 uint8_t layout_val = room.layout_id();
2218 uint8_t blockset_val = room.blockset();
2219 uint8_t floor1_val = room.floor1();
2220 uint8_t floor2_val = room.floor2();
2221 uint8_t palette_val = room.palette();
2222 uint8_t spriteset_val = room.spriteset();
2223 uint16_t message_val = room.message_id();
2224 uint8_t bg2_val = static_cast<uint8_t>(room.bg2());
2225 uint8_t effect_val = static_cast<uint8_t>(room.effect());
2226 uint8_t collision_val = static_cast<uint8_t>(room.collision());
2227 uint8_t tag1_val = static_cast<uint8_t>(room.tag1());
2228 uint8_t tag2_val = static_cast<uint8_t>(room.tag2());
2229
2230 auto render_room_graphics = [&]() {
2231 if (room.rom() && room.rom()->is_loaded()) {
2232 room.RenderRoomGraphics();
2233 }
2234 };
2235
2236 constexpr float kHexW = 54.0f;
2237 constexpr ImGuiTableFlags kHeaderFlags = ImGuiTableFlags_BordersInnerV |
2238 ImGuiTableFlags_RowBg |
2239 ImGuiTableFlags_NoPadOuterX;
2240 auto draw_label = [](const char* label) {
2241 ImGui::AlignTextToFramePadding();
2242 ImGui::TextUnformatted(label);
2243 };
2244 auto draw_byte_field = [&](const char* label, const char* id, uint8_t value,
2245 uint8_t max_value, const std::string& tooltip,
2246 auto apply) {
2247 ImGui::TableNextColumn();
2248 draw_label(label);
2249 ImGui::TableNextColumn();
2250 ImGui::SetNextItemWidth(kHexW);
2251 uint8_t edit_value = value;
2252 if (auto res =
2253 gui::InputHexByteEx(id, &edit_value, max_value, kHexW, true);
2254 res.ShouldApply()) {
2255 apply(edit_value);
2256 }
2257 if (ImGui::IsItemHovered()) {
2258 ImGui::SetTooltip("%s", tooltip.c_str());
2259 }
2260 };
2261 auto draw_word_field = [&](const char* label, const char* id,
2262 uint16_t value, uint16_t max_value,
2263 const std::string& tooltip, auto apply) {
2264 ImGui::TableNextColumn();
2265 draw_label(label);
2266 ImGui::TableNextColumn();
2267 ImGui::SetNextItemWidth(kHexW + 12.0f);
2268 uint16_t edit_value = value;
2269 if (auto res = gui::InputHexWordEx(id, &edit_value, kHexW + 12.0f, true);
2270 res.ShouldApply()) {
2271 apply(std::min<uint16_t>(edit_value, max_value));
2272 }
2273 if (ImGui::IsItemHovered()) {
2274 ImGui::SetTooltip("%s", tooltip.c_str());
2275 }
2276 };
2277
2278 if (ImGui::BeginTable("##WorkbenchRoomHeader", 4, kHeaderFlags)) {
2279 ImGui::TableSetupColumn("L1", ImGuiTableColumnFlags_WidthFixed, 44.0f);
2280 ImGui::TableSetupColumn("V1", ImGuiTableColumnFlags_WidthFixed, 66.0f);
2281 ImGui::TableSetupColumn("L2", ImGuiTableColumnFlags_WidthFixed, 44.0f);
2282 ImGui::TableSetupColumn("V2", ImGuiTableColumnFlags_WidthStretch);
2283
2284 ImGui::TableNextRow();
2285 draw_byte_field("Lay", "##RoomHeaderLayout", layout_val, 0x07,
2286 "Layout (0-7)", [&](uint8_t value) {
2287 room.SetLayoutId(value);
2288 room.MarkLayoutDirty();
2289 render_room_graphics();
2290 });
2291 draw_byte_field("Blk", "##RoomHeaderBlockset", blockset_val, 0x51,
2292 "Blockset (0-51)", [&](uint8_t value) {
2293 room.SetBlockset(value);
2294 render_room_graphics();
2295 });
2296
2297 ImGui::TableNextRow();
2298 draw_byte_field("F1", "##RoomHeaderFloor1", floor1_val, 0x0F,
2299 "BG1 floor graphics (0-F)", [&](uint8_t value) {
2300 room.set_floor1(value);
2301 render_room_graphics();
2302 });
2303 draw_byte_field("F2", "##RoomHeaderFloor2", floor2_val, 0x0F,
2304 "BG2 floor graphics (0-F)", [&](uint8_t value) {
2305 room.set_floor2(value);
2306 render_room_graphics();
2307 });
2308
2309 ImGui::TableNextRow();
2310 draw_byte_field("Pal", "##RoomHeaderPalette", palette_val, 0x47,
2311 "Palette set (0-47)", [&](uint8_t value) {
2312 room.SetPalette(value);
2313 render_room_graphics();
2314 if (on_room_selected_) {
2315 on_room_selected_(room_id);
2316 }
2317 });
2318 draw_byte_field("Spr", "##RoomHeaderSpriteset", spriteset_val, 0x8F,
2319 "Sprite graphics set (0-8F)", [&](uint8_t value) {
2320 room.SetSpriteset(value);
2321 render_room_graphics();
2322 });
2323
2324 ImGui::TableNextRow();
2325 draw_word_field("Msg", "##RoomHeaderMessage", message_val, 0x0FFF,
2326 "Dungeon message ID (0-FFF)",
2327 [&](uint16_t value) { room.SetMessageId(value); });
2328 draw_byte_field("BG2", "##RoomHeaderBg2", bg2_val, 0x08,
2329 std::string("BG2 mode: ") + GetBg2ModeName(bg2_val),
2330 [&](uint8_t value) {
2331 room.SetBg2(static_cast<background2>(value));
2332 render_room_graphics();
2333 });
2334
2335 ImGui::TableNextRow();
2336 const char* effect_name =
2337 effect_val < 8 ? zelda3::RoomEffect[effect_val].c_str() : "Unknown";
2338 draw_byte_field("FX", "##RoomHeaderEffect", effect_val, 0x07,
2339 std::string("Effect: ") + effect_name,
2340 [&](uint8_t value) {
2341 room.SetEffect(static_cast<zelda3::EffectKey>(value));
2342 render_room_graphics();
2343 });
2344 draw_byte_field(
2345 "Coll", "##RoomHeaderCollision", collision_val, 0x04,
2346 std::string("Collision: ") + GetCollisionName(collision_val),
2347 [&](uint8_t value) {
2348 room.SetCollision(static_cast<zelda3::CollisionKey>(value));
2349 });
2350
2351 ImGui::TableNextRow();
2352 draw_byte_field("Tag1", "##RoomHeaderTag1", tag1_val, 0x40,
2353 std::string("Tag1: ") + zelda3::GetRoomTagLabel(tag1_val),
2354 [&](uint8_t value) {
2355 room.SetTag1(static_cast<zelda3::TagKey>(value));
2356 render_room_graphics();
2357 });
2358 draw_byte_field("Tag2", "##RoomHeaderTag2", tag2_val, 0x40,
2359 std::string("Tag2: ") + zelda3::GetRoomTagLabel(tag2_val),
2360 [&](uint8_t value) {
2361 room.SetTag2(static_cast<zelda3::TagKey>(value));
2362 render_room_graphics();
2363 });
2364
2365 ImGui::EndTable();
2366 }
2367
2368 ImGui::Dummy(ImVec2(0.0f, 2.0f));
2370 if (ImGui::BeginTable("##WorkbenchRoomDestinations", 4, kHeaderFlags)) {
2371 ImGui::TableSetupColumn("L1", ImGuiTableColumnFlags_WidthFixed, 44.0f);
2372 ImGui::TableSetupColumn("V1", ImGuiTableColumnFlags_WidthFixed, 66.0f);
2373 ImGui::TableSetupColumn("L2", ImGuiTableColumnFlags_WidthFixed, 44.0f);
2374 ImGui::TableSetupColumn("V2", ImGuiTableColumnFlags_WidthStretch);
2375
2376 ImGui::TableNextRow();
2377 draw_byte_field("Pit", "##RoomHeaderPit", room.holewarp(), 0xFF,
2378 "Pit/holewarp destination room",
2379 [&](uint8_t value) { room.SetHolewarp(value); });
2380 draw_byte_field("St1", "##RoomHeaderStair1", room.staircase_room(0), 0xFF,
2381 "Stair destination slot 1",
2382 [&](uint8_t value) { room.SetStaircaseRoom(0, value); });
2383
2384 ImGui::TableNextRow();
2385 draw_byte_field("St2", "##RoomHeaderStair2", room.staircase_room(1), 0xFF,
2386 "Stair destination slot 2",
2387 [&](uint8_t value) { room.SetStaircaseRoom(1, value); });
2388 draw_byte_field("St3", "##RoomHeaderStair3", room.staircase_room(2), 0xFF,
2389 "Stair destination slot 3",
2390 [&](uint8_t value) { room.SetStaircaseRoom(2, value); });
2391
2392 ImGui::TableNextRow();
2393 draw_byte_field("St4", "##RoomHeaderStair4", room.staircase_room(3), 0xFF,
2394 "Stair destination slot 4",
2395 [&](uint8_t value) { room.SetStaircaseRoom(3, value); });
2396 draw_byte_field("P1", "##RoomHeaderStairPlane1", room.staircase_plane(0),
2397 0x03, "Stair 1 target layer/plane (0-3)",
2398 [&](uint8_t value) { room.SetStaircasePlane(0, value); });
2399
2400 ImGui::TableNextRow();
2401 draw_byte_field("P2", "##RoomHeaderStairPlane2", room.staircase_plane(1),
2402 0x03, "Stair 2 target layer/plane (0-3)",
2403 [&](uint8_t value) { room.SetStaircasePlane(1, value); });
2404 draw_byte_field("P3", "##RoomHeaderStairPlane3", room.staircase_plane(2),
2405 0x03, "Stair 3 target layer/plane (0-3)",
2406 [&](uint8_t value) { room.SetStaircasePlane(2, value); });
2407
2408 ImGui::TableNextRow();
2409 draw_byte_field("P4", "##RoomHeaderStairPlane4", room.staircase_plane(3),
2410 0x03, "Stair 4 target layer/plane (0-3)",
2411 [&](uint8_t value) { room.SetStaircasePlane(3, value); });
2412 ImGui::TableNextColumn();
2413 ImGui::TableNextColumn();
2414
2415 ImGui::EndTable();
2416 }
2417 } else {
2418 ImGui::TextDisabled(tr("Room header unavailable"));
2419 }
2420
2422 auto& interaction = viewer.object_interaction();
2423 const bool placing = interaction.mode_manager().IsPlacementActive();
2424 if (placing) {
2425 ImGui::TextColored(theme.text_info, tr("Placement active"));
2426 ImGui::SameLine();
2427 if (ImGui::SmallButton(ICON_MD_CLOSE " Cancel")) {
2428 interaction.mode_manager().CancelCurrentMode();
2429 }
2430 }
2431}
2432
2434 DungeonCanvasViewer& viewer) {
2435 auto& interaction = viewer.object_interaction();
2436 const auto& theme = AgentUI::GetTheme();
2437
2438 const int room_id = viewer.current_room_id();
2439 const size_t obj_count = interaction.GetSelectionCount();
2440 const bool has_entity = interaction.HasEntitySelection();
2441
2442 if (!has_entity && obj_count == 0) {
2444 ImGui::TextDisabled(tr("Click an object or entity to inspect"));
2445 return;
2446 }
2447
2448 // ── Tile Object Selection ──
2449 if (obj_count > 0) {
2451 ImGui::TextColored(theme.text_primary, ICON_MD_WIDGETS " %zu object%s",
2452 obj_count, obj_count == 1 ? "" : "s");
2453 if (obj_count == 1) {
2454 ImGui::SameLine();
2455 ImGui::TextDisabled(tr("Focused selection"));
2456 }
2457
2458 const auto indices = interaction.GetSelectedObjectIndices();
2459
2460 // Multi-object summary + inline bulk editor (O1)
2461 if (indices.size() > 1 && room_id >= 0 && viewer.rooms()) {
2462 auto& room = (*viewer.rooms())[room_id];
2463 auto& objects = room.GetTileObjects();
2465 " Multi-selection");
2466 for (size_t i = 0; i < indices.size() && i < 8; ++i) {
2467 size_t idx = indices[i];
2468 if (idx < objects.size()) {
2469 auto& obj = objects[idx];
2470 std::string name = zelda3::GetObjectName(obj.id_);
2471 ImGui::BulletText(tr("0x%03X %s"), obj.id_, name.c_str());
2472 }
2473 }
2474 if (indices.size() > 8) {
2475 ImGui::TextDisabled(tr(" ... and %zu more"), indices.size() - 8);
2476 }
2477
2478 // Bulk editor: nudge/layer/stacking/size + destructive duplicate/delete.
2479 // Operations route through tile_handler / interaction, so every action
2480 // captures its own undo snapshot.
2481 auto& tile_handler = interaction.entity_coordinator().tile_handler();
2482 const std::vector<size_t> selection_copy(indices.begin(), indices.end());
2483 bool has_room_stream_object = false;
2484 bool has_special_layer_object = false;
2485 for (const size_t index : selection_copy) {
2486 if (index >= objects.size()) {
2487 continue;
2488 }
2489 if (zelda3::UsesRoomObjectStream(objects[index])) {
2490 has_room_stream_object = true;
2491 } else {
2492 has_special_layer_object = true;
2493 }
2494 }
2495 const bool has_editable_size =
2496 workbench::HasEditableRoomObjectSize(objects, selection_copy);
2497
2499
2500 // Nudge grid (arrow buttons around a tile-delta drag).
2501 static int bulk_nudge_dx = 0;
2502 static int bulk_nudge_dy = 0;
2503 ImGui::TextDisabled(tr("Nudge (tiles)"));
2504 ImGui::PushButtonRepeat(true);
2505 if (ImGui::Button(ICON_MD_ARROW_UPWARD "##BulkNudgeUp"))
2506 tile_handler.MoveObjects(room_id, selection_copy, 0, -1);
2507 ImGui::SameLine();
2508 if (ImGui::Button(ICON_MD_ARROW_DOWNWARD "##BulkNudgeDown"))
2509 tile_handler.MoveObjects(room_id, selection_copy, 0, 1);
2510 ImGui::SameLine();
2511 if (ImGui::Button(ICON_MD_ARROW_BACK "##BulkNudgeLeft"))
2512 tile_handler.MoveObjects(room_id, selection_copy, -1, 0);
2513 ImGui::SameLine();
2514 if (ImGui::Button(ICON_MD_ARROW_FORWARD "##BulkNudgeRight"))
2515 tile_handler.MoveObjects(room_id, selection_copy, 1, 0);
2516 ImGui::PopButtonRepeat();
2517
2518 ImGui::SetNextItemWidth(60);
2519 ImGui::DragInt("##BulkNudgeDx", &bulk_nudge_dx, 0.25f, -63, 63, "Δx:%d");
2520 ImGui::SameLine();
2521 ImGui::SetNextItemWidth(60);
2522 ImGui::DragInt("##BulkNudgeDy", &bulk_nudge_dy, 0.25f, -63, 63, "Δy:%d");
2523 ImGui::SameLine();
2524 if (ImGui::Button(tr("Apply##BulkNudgeApply")) &&
2525 (bulk_nudge_dx != 0 || bulk_nudge_dy != 0)) {
2526 tile_handler.MoveObjects(room_id, selection_copy, bulk_nudge_dx,
2527 bulk_nudge_dy);
2528 bulk_nudge_dx = 0;
2529 bulk_nudge_dy = 0;
2530 }
2531
2532 ImGui::Spacing();
2533 if (has_room_stream_object && !has_special_layer_object) {
2534 ImGui::TextDisabled(tr("Object stream (set all)"));
2535 if (ImGui::SmallButton(tr("Primary##BulkPlacement0")))
2536 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 0);
2537 ImGui::SameLine();
2538 if (ImGui::SmallButton(tr("BG2 overlay##BulkPlacement1")))
2539 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 1);
2540 ImGui::SameLine();
2541 if (ImGui::SmallButton(tr("BG1 overlay##BulkPlacement2")))
2542 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 2);
2543 } else if (has_special_layer_object && !has_room_stream_object) {
2544 ImGui::TextDisabled(tr("Special layer (set all)"));
2545 if (ImGui::SmallButton(tr("Upper (BG1)##BulkPlacement0")))
2546 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 0);
2547 ImGui::SameLine();
2548 if (ImGui::SmallButton(tr("Lower (BG2)##BulkPlacement1")))
2549 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 1);
2550 } else {
2551 ImGui::TextDisabled(tr("Stored placement (mixed selection)"));
2552 if (ImGui::SmallButton(tr("Primary / Upper (BG1)##BulkPlacement0")))
2553 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 0);
2554 ImGui::SameLine();
2555 if (ImGui::SmallButton(tr("BG2 overlay / Lower (BG2)##BulkPlacement1")))
2556 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 1);
2557 }
2558
2559 ImGui::Spacing();
2560 ImGui::TextDisabled(tr("Stacking"));
2561 if (ImGui::SmallButton(ICON_MD_FLIP_TO_FRONT " Front##BulkFront"))
2562 tile_handler.SendToFront(room_id, selection_copy);
2563 ImGui::SameLine();
2564 if (ImGui::SmallButton(ICON_MD_FLIP_TO_BACK " Back##BulkBack"))
2565 tile_handler.SendToBack(room_id, selection_copy);
2566 ImGui::SameLine();
2567 if (ImGui::SmallButton(ICON_MD_KEYBOARD_ARROW_UP "##BulkFwd"))
2568 tile_handler.MoveForward(room_id, selection_copy);
2569 ImGui::SameLine();
2570 if (ImGui::SmallButton(ICON_MD_KEYBOARD_ARROW_DOWN "##BulkBwd"))
2571 tile_handler.MoveBackward(room_id, selection_copy);
2572
2573 ImGui::Spacing();
2574 ImGui::TextDisabled(tr("Size"));
2575 if (!has_editable_size) {
2576 ImGui::BeginDisabled();
2577 }
2578 if (ImGui::SmallButton(ICON_MD_REMOVE "##BulkSizeDec"))
2579 tile_handler.ResizeObjects(room_id, selection_copy, -1);
2580 ImGui::SameLine();
2581 if (ImGui::SmallButton(ICON_MD_ADD "##BulkSizeInc"))
2582 tile_handler.ResizeObjects(room_id, selection_copy, 1);
2583 if (!has_editable_size) {
2584 ImGui::EndDisabled();
2585 }
2586
2587 ImGui::Spacing();
2588 if (ImGui::SmallButton(ICON_MD_CONTENT_COPY " Duplicate##BulkDup"))
2589 (void)tile_handler.DuplicateObjects(room_id, selection_copy, 1, 1);
2590 ImGui::SameLine();
2591 {
2592 gui::StyleColorGuard danger_colors({
2593 {ImGuiCol_Button, theme.status_error},
2594 {ImGuiCol_ButtonHovered,
2595 ImVec4(theme.status_error.x + 0.1f, theme.status_error.y + 0.05f,
2596 theme.status_error.z + 0.05f, 1.0f)},
2597 });
2598 if (ImGui::SmallButton(ICON_MD_DELETE " Delete##BulkDel"))
2599 ImGui::OpenPopup("##BulkDeleteConfirm");
2600 }
2601
2602 if (ImGui::BeginPopup("##BulkDeleteConfirm")) {
2603 ImGui::TextColored(
2604 theme.status_error, ICON_MD_WARNING " Delete %zu object%s?",
2605 selection_copy.size(), selection_copy.size() == 1 ? "" : "s");
2606 ImGui::Separator();
2607 if (ImGui::Button(tr("Cancel##BulkDelCancel"))) {
2608 ImGui::CloseCurrentPopup();
2609 }
2610 ImGui::SameLine();
2611 {
2612 gui::StyleColorGuard confirm_colors({
2613 {ImGuiCol_Button, theme.status_error},
2614 });
2615 if (ImGui::Button(ICON_MD_DELETE " Confirm##BulkDelConfirm")) {
2616 tile_handler.DeleteObjects(room_id, selection_copy);
2617 ImGui::CloseCurrentPopup();
2618 }
2619 }
2620 ImGui::EndPopup();
2621 }
2622 }
2623
2624 // Single-object detailed inspector
2625 if (indices.size() == 1 && room_id >= 0 && viewer.rooms()) {
2626 auto& room = (*viewer.rooms())[room_id];
2627 auto& objects = room.GetTileObjects();
2628 const size_t idx = indices.front();
2629 if (idx < objects.size()) {
2630 auto& obj = objects[idx];
2631 const std::string obj_name = zelda3::GetObjectName(obj.id_);
2632 const int subtype = zelda3::GetObjectSubtype(obj.id_);
2633 const bool uses_room_stream = zelda3::UsesRoomObjectStream(obj);
2634 int displayed_layer = obj.GetLayerValue();
2635 const int pixel_x = obj.x_ * 8;
2636 const int pixel_y = obj.y_ * 8;
2637
2638 // Name + category header
2640 " Focused Object");
2641 ImGui::TextColored(theme.text_primary, "%s", obj_name.c_str());
2642 ImGui::TextDisabled(tr("%s (Type %d) #%zu in list"),
2643 GetObjectCategory(obj.id_), subtype, idx);
2644
2645 // Property table
2646 constexpr ImGuiTableFlags kPropsFlags = ImGuiTableFlags_BordersInnerV |
2647 ImGuiTableFlags_RowBg |
2648 ImGuiTableFlags_NoPadOuterX;
2649 if (ImGui::BeginTable("##SelObjProps", 2, kPropsFlags)) {
2650 ImGui::TableSetupColumn("Prop", ImGuiTableColumnFlags_WidthFixed,
2651 56.0f);
2652 ImGui::TableSetupColumn("Val", ImGuiTableColumnFlags_WidthStretch);
2653
2654 // ID
2656 uint16_t obj_id = static_cast<uint16_t>(obj.id_ & 0x0FFF);
2657 if (auto res =
2658 gui::InputHexWordEx("##SelObjId", &obj_id, 80.0f, true);
2659 res.ShouldApply()) {
2660 obj_id &= 0x0FFF;
2661 interaction.SetObjectId(idx, static_cast<int16_t>(obj_id));
2662 }
2663 });
2664
2665 // Position
2666 gui::LayoutHelpers::PropertyRow("Pos", [&]() {
2667 int pos_x = obj.x_;
2668 int pos_y = obj.y_;
2669 ImGui::SetNextItemWidth(60);
2670 bool x_changed =
2671 ImGui::DragInt("##SelObjX", &pos_x, 0.1f, 0, 63, "X:%d");
2672 {
2673 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
2674 gui::AutoRegisterLastItem("drag_int", "selected_object_x",
2675 "Selected room object X coordinate");
2676 }
2677 ImGui::SameLine();
2678 ImGui::SetNextItemWidth(60);
2679 bool y_changed =
2680 ImGui::DragInt("##SelObjY", &pos_y, 0.1f, 0, 63, "Y:%d");
2681 {
2682 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
2683 gui::AutoRegisterLastItem("drag_int", "selected_object_y",
2684 "Selected room object Y coordinate");
2685 }
2686 if (x_changed || y_changed) {
2687 int delta_x = pos_x - obj.x_;
2688 int delta_y = pos_y - obj.y_;
2689 interaction.entity_coordinator().tile_handler().MoveObjects(
2690 room_id, {idx}, delta_x, delta_y);
2691 }
2692 });
2693
2694 // Size
2695 gui::LayoutHelpers::PropertyRow("Size", [&]() {
2696 uint8_t size = obj.size_ & 0x0F;
2697 const bool size_editable =
2699 if (!size_editable) {
2700 ImGui::BeginDisabled();
2701 }
2702 if (auto res = gui::InputHexByteEx("##SelObjSize", &size, 0x0F,
2703 60.0f, true);
2704 size_editable && res.ShouldApply()) {
2705 interaction.SetObjectSize(idx, size);
2706 }
2707 if (!size_editable) {
2708 ImGui::EndDisabled();
2709 }
2710 });
2711
2713 uses_room_stream ? "Stream" : "Layer", [&]() {
2714 int layer = displayed_layer;
2715 const char* stream_names[] = {"Primary", "BG2 overlay",
2716 "BG1 overlay"};
2717 const char* special_layer_names[] = {"Upper layer (BG1)",
2718 "Lower layer (BG2)"};
2719 ImGui::SetNextItemWidth(-1);
2720 if (ImGui::Combo(
2721 "##SelObjLayer", &layer,
2722 uses_room_stream ? stream_names : special_layer_names,
2723 uses_room_stream ? IM_ARRAYSIZE(stream_names)
2724 : IM_ARRAYSIZE(special_layer_names))) {
2725 const int max_layer = uses_room_stream ? 2 : 1;
2726 layer = std::clamp(layer, 0, max_layer);
2727 if (interaction.SetObjectLayer(
2728 idx,
2729 static_cast<zelda3::RoomObject::LayerType>(layer))) {
2730 displayed_layer = layer;
2731 }
2732 }
2733 });
2734 gui::LayoutHelpers::PropertyRow("Route", [&]() {
2735 ImGui::TextDisabled(
2736 "%s", uses_room_stream ? GetObjectStreamName(displayed_layer)
2737 : GetSpecialLayerName(displayed_layer));
2738 });
2739
2740 // Pixel coords (read-only info)
2741 gui::LayoutHelpers::PropertyRow("Pixel", [&]() {
2742 ImGui::TextDisabled("(%d, %d)", pixel_x, pixel_y);
2743 });
2744
2745 ImGui::EndTable();
2746 }
2747 }
2748 }
2749 }
2750
2751 // ── Entity Selection (Doors, Sprites, Items) ──
2752 if (has_entity && room_id >= 0 && viewer.rooms()) {
2753 const auto sel = interaction.GetSelectedEntity();
2754 auto& room = (*viewer.rooms())[room_id];
2756 " Entity Selection");
2757
2758 switch (sel.type) {
2759 case EntityType::Door: {
2760 const auto& doors = room.GetDoors();
2761 if (sel.index < doors.size()) {
2762 const auto& door = doors[sel.index];
2763 std::string type_name(zelda3::GetDoorTypeName(door.type));
2764 std::string dir_name(zelda3::GetDoorDirectionName(door.direction));
2765
2766 ImGui::TextColored(theme.text_primary, ICON_MD_DOOR_FRONT " %s",
2767 type_name.c_str());
2768 ImGui::TextDisabled(tr("Direction: %s Position: 0x%02X"),
2769 dir_name.c_str(), door.position);
2770
2771 auto [tile_x, tile_y] = door.GetTileCoords();
2772 auto [pixel_x, pixel_y] = door.GetPixelCoords();
2773 ImGui::TextDisabled(tr("Tile: (%d, %d) Pixel: (%d, %d)"), tile_x,
2774 tile_y, pixel_x, pixel_y);
2775 }
2776 break;
2777 }
2778 case EntityType::Sprite: {
2779 const auto& sprites = room.GetSprites();
2780 if (sel.index < sprites.size()) {
2781 const auto& sprite = sprites[sel.index];
2782 std::string sprite_name = zelda3::GetSpriteLabel(sprite.id());
2783
2784 ImGui::TextColored(theme.text_primary, ICON_MD_PERSON " %s",
2785 sprite_name.c_str());
2786 ImGui::TextDisabled(tr("ID: 0x%02X Subtype: %d Layer: %d"),
2787 sprite.id(), sprite.subtype(), sprite.layer());
2788 ImGui::TextDisabled(tr("Pos: (%d, %d) Pixel: (%d, %d)"), sprite.x(),
2789 sprite.y(), sprite.x() * 16, sprite.y() * 16);
2790
2791 // Overlord check
2792 if (sprite.subtype() == 0x07 && sprite.id() >= 0x01 &&
2793 sprite.id() <= 0x1A) {
2794 std::string overlord_name = zelda3::GetOverlordLabel(sprite.id());
2795 ImGui::TextColored(theme.text_warning_yellow,
2796 ICON_MD_STAR " Overlord: %s",
2797 overlord_name.c_str());
2798 }
2799 }
2800 break;
2801 }
2802 case EntityType::Item: {
2803 const auto& items = room.GetPotItems();
2804 if (sel.index < items.size()) {
2805 const auto& pot_item = items[sel.index];
2806 const char* item_name = GetPotItemName(pot_item.item);
2807
2808 ImGui::TextColored(theme.text_primary, ICON_MD_INVENTORY_2 " %s",
2809 item_name);
2810 ImGui::TextDisabled(tr("Item ID: 0x%02X Raw Pos: 0x%04X"),
2811 pot_item.item, pot_item.position);
2812 ImGui::TextDisabled(tr("Pixel: (%d, %d) Tile: (%d, %d)"),
2813 pot_item.GetPixelX(), pot_item.GetPixelY(),
2814 pot_item.GetTileX(), pot_item.GetTileY());
2815 }
2816 break;
2817 }
2818 default:
2819 break;
2820 }
2821 }
2822}
2823
2825 DungeonCanvasViewer& viewer) {
2826 (void)viewer;
2828 constexpr ImGuiTableFlags kFlags =
2829 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
2830 if (!ImGui::BeginTable("##WorkbenchToolsGrid", 2, kFlags)) {
2831 return;
2832 }
2833
2834 auto draw_tool_button = [&](const char* label, WorkbenchTool tool,
2835 bool enabled = true) {
2836 if (!enabled) {
2837 ImGui::BeginDisabled();
2838 }
2839 const bool active =
2841 if (gui::ToggleButton(label, active, ImVec2(-1, 0)) && enabled) {
2842 OpenTool(tool);
2843 }
2844 if (!enabled) {
2845 ImGui::EndDisabled();
2846 }
2847 };
2848
2849 // Edit row holds entity tools only. Mode switches (Selection / Room Details)
2850 // moved out — the inspector primary segmented selector at the inspector
2851 // header is the canonical mode switch.
2852 ImGui::TableNextRow();
2853 ImGui::TableNextColumn();
2854 draw_tool_button(ICON_MD_CATEGORY " Selector", WorkbenchTool::ObjectSelector,
2855 object_selector_content_ != nullptr);
2856 ImGui::TableNextColumn();
2857 draw_tool_button(ICON_MD_DOOR_FRONT " Doors", WorkbenchTool::DoorEditor,
2858 door_editor_content_ != nullptr);
2859
2860 ImGui::TableNextRow();
2861 ImGui::TableNextColumn();
2862 draw_tool_button(ICON_MD_PERSON " Sprites", WorkbenchTool::SpriteEditor,
2863 sprite_editor_content_ != nullptr);
2864 ImGui::TableNextColumn();
2865 draw_tool_button(ICON_MD_INVENTORY " Items", WorkbenchTool::ItemEditor,
2866 item_editor_content_ != nullptr);
2867
2868 ImGui::EndTable();
2869
2871 if (ImGui::BeginTable("##WorkbenchRoomToolsGrid", 2, kFlags)) {
2872 auto room_tool_button = [&](const char* label, WorkbenchTool tool,
2873 bool enabled) {
2874 ImGui::TableNextColumn();
2875 if (!enabled) {
2876 ImGui::BeginDisabled();
2877 }
2878 const bool active =
2880 if (gui::ToggleButton(label, active, ImVec2(-1, 0)) && enabled) {
2881 OpenTool(tool);
2882 }
2883 if (!enabled) {
2884 ImGui::EndDisabled();
2885 }
2886 };
2887
2888 ImGui::TableNextRow();
2889 room_tool_button(ICON_MD_LABEL " Room Tags", WorkbenchTool::RoomTags,
2890 room_tag_panel_ != nullptr);
2891 room_tool_button(ICON_MD_GRID_ON " Collision",
2893 custom_collision_panel_ != nullptr);
2894 ImGui::TableNextRow();
2895 room_tool_button(ICON_MD_WATER_DROP " Water Fill", WorkbenchTool::WaterFill,
2896 water_fill_panel_ != nullptr);
2897 room_tool_button(ICON_MD_TRAIN " Minecart", WorkbenchTool::MinecartTracks,
2898 minecart_track_panel_ != nullptr);
2899 ImGui::EndTable();
2900 }
2901
2903 if (ImGui::BeginTable("##WorkbenchReviewGrid", 2, kFlags)) {
2904 ImGui::TableNextRow();
2905 ImGui::TableNextColumn();
2907 ImVec2(-1, 0))) {
2909 }
2910 ImGui::TableNextColumn();
2911 if (workbench::DrawActionButton(ICON_MD_MAP " Dungeon Map",
2912 ImVec2(-1, 0))) {
2914 }
2915
2916 ImGui::TableNextRow();
2917 ImGui::TableNextColumn();
2919 ImVec2(-1, 0))) {
2921 }
2922 ImGui::TableNextColumn();
2923 draw_tool_button(ICON_MD_PALETTE " Palette", WorkbenchTool::Palette,
2924 palette_editor_content_ != nullptr);
2925 ImGui::EndTable();
2926 }
2927
2929 if (workbench::DrawActionButton(ICON_MD_KEYBOARD " Keyboard Shortcuts",
2930 ImVec2(-1, 0))) {
2931 show_shortcut_legend_ = true;
2932 }
2934}
2935
2936} // namespace yaze::editor
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
bool is_loaded() const
Definition rom.h:155
static Flags & get()
Definition features.h:119
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetInteraction(DungeonObjectInteraction *interaction)
void Draw(bool *p_open) override
Draw the panel content.
std::optional< int > DrawConnectedRoomMatrix(int center_room_id)
DungeonObjectInteraction & object_interaction()
zelda3::RoomLayerManager & GetRoomLayerManager(int room_id)
const project::YazeProject * project() const
void ScrollToTile(int tile_x, int tile_y)
WindowContent for displaying multiple rooms in a spatial dungeon layout.
void SetSelectedObjects(const std::vector< size_t > &indices)
std::vector< size_t > GetSelectedObjectIndices() const
Handles room and entrance selection UI.
void DrawRoomBrowser(RoomSelectionIntent single_click_intent=RoomSelectionIntent::kFocusInWorkbench)
static const char * GetBlocksetGroupName(uint8_t blockset)
zelda3::Room * GetIfMaterialized(int room_id)
static void Draw(const DungeonStatusBarState &state)
static DungeonStatusBarState BuildState(const DungeonCanvasViewer &viewer, const char *tool_mode, bool room_dirty)
void DrawInspector(DungeonCanvasViewer &viewer, bool compact)
void DrawInspectorShelfSelection(DungeonCanvasViewer &viewer)
DungeonWorkbenchContent(DungeonRoomSelector *room_selector, int *current_room_id, std::function< void(int)> on_room_selected, std::function< void(int, RoomSelectionIntent)> on_room_selected_with_intent, std::function< void(int)> on_save_room, std::function< void()> on_save_all_rooms, std::function< DungeonCanvasViewer *()> get_viewer, std::function< DungeonCanvasViewer *(int)> get_compare_viewer, std::function< const std::deque< int > &()> get_recent_rooms, std::function< void(int)> forget_recent_room, std::function< void(bool)> set_workflow_mode, Rom *rom=nullptr)
int GetPriority() const override
Get display priority for menu ordering.
DungeonWorkbenchPitDamageControlRects pit_damage_control_rects_
void DrawSelectionShelf(DungeonCanvasViewer &viewer)
bool IsWorkbenchToolAvailable(WorkbenchTool tool) const
void DrawInspectorHeader(float button_size, bool compact)
void DrawInspectorCompactSummary(DungeonCanvasViewer &viewer)
void DrawSidebarModeTabs(bool stacked, float segment_height)
void DrawSidebarPane(float width, float height, float button_size, bool compact)
DungeonMapPanel * GetEmbeddedDungeonMap(DungeonCanvasViewer &viewer)
void DrawInspectorPane(float width, float height, float button_size, bool compact, DungeonCanvasViewer *viewer)
const char * GetWorkbenchToolIcon(WorkbenchTool tool) const
void DrawInspectorShelfRoom(DungeonCanvasViewer &viewer)
std::string GetEditorCategory() const override
Editor category this panel belongs to.
void DrawInspectorShelfTools(DungeonCanvasViewer &viewer)
void DrawCanvasPane(float width, float height, DungeonCanvasViewer *primary_viewer, bool left_sidebar_visible)
void DrawWorkbenchTool(DungeonCanvasViewer &viewer, WorkbenchTool tool)
void DrawSidebarHeader(float button_size, bool compact)
void DrawInspectorToolDrawer(DungeonCanvasViewer &viewer)
const char * GetWorkbenchToolShortLabel(WorkbenchTool tool) const
void DrawDungeonMapPopup(DungeonCanvasViewer &viewer)
std::unique_ptr< DungeonMapPanel > embedded_dungeon_map_
std::function< void(int, RoomSelectionIntent)> on_room_selected_with_intent_
void SetEmbeddedEditorPanels(WindowContent *object_selector, WindowContent *door_editor, WindowContent *sprite_editor, WindowContent *item_editor, WindowContent *room_graphics, WindowContent *palette_editor)
void SetEmbeddedToolPanels(RoomTagEditorPanel *room_tags, CustomCollisionPanel *custom_collision, WaterFillPanel *water_fill, MinecartTrackEditorPanel *minecart_tracks)
const char * GetWorkbenchToolId(WorkbenchTool tool) const
const char * GetWorkbenchToolUnavailableMessage(WorkbenchTool tool) const
std::string GetId() const override
Unique identifier for this panel.
void DrawSplitView(DungeonCanvasViewer &primary_viewer)
std::function< zelda3::PitDamageTable *()> get_pit_damage_table_
const char * GetWorkbenchToolTitle(WorkbenchTool tool) const
std::string GetIcon() const override
Material Design icon for this panel.
std::function< DungeonCanvasViewer *(int)> get_compare_viewer_
std::unordered_map< int, std::string > room_dungeon_cache_
std::string GetDisplayName() const override
Human-readable name shown in menus and title bars.
std::function< void(bool)> on_inspector_side_changed_
void Draw(bool *p_open) override
Draw the panel content.
std::function< const std::deque< int > &()> get_recent_rooms_
void DrawInspectorShelf(DungeonCanvasViewer &viewer, bool compact)
void DrawLayerCompositingControls(DungeonCanvasViewer &viewer, int room_id)
std::function< DungeonCanvasViewer *()> get_viewer_
static bool Draw(const DungeonWorkbenchToolbarParams &params)
const char * GetModeName() const
Get mode name for debugging/UI.
bool IsPlacementActive() const
Check if any placement mode is active.
void Draw(bool *p_open) override
Draw the panel content.
WindowContent showing all room tag slots and their usage across rooms.
void Draw(bool *p_open) override
Draw the panel content.
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetInteraction(DungeonObjectInteraction *interaction)
void Draw(bool *p_open) override
Draw the panel content.
Base interface for all logical window content components.
virtual void Draw(bool *p_open)=0
Draw the panel content.
RAII scope that enables automatic widget registration.
void ApplyScaleSnapshot(const CanvasConfig &snapshot)
Definition canvas.cc:944
CanvasConfig & GetConfig()
Definition canvas.h:229
static float GetTouchSafeWidgetHeight()
static bool BeginContentChild(const char *id, const ImVec2 &min_size, bool border=false, ImGuiWindowFlags flags=0)
static void EndContentChild()
static void PropertyRow(const char *label, std::function< void()> widget_callback)
RAII guard for ImGui style colors.
Definition style_guard.h:27
RAII guard for ImGui style vars.
Definition style_guard.h:68
Dungeon Room Entrance or Spawn Point.
static const char * GetBlendModeName(LayerBlendMode mode)
Get blend mode name.
LayerBlendMode GetLayerBlendMode(LayerType layer) const
bool HasUnsavedChanges() const
Definition room.h:589
zelda3_bg2_effect
Background layer 2 effects.
Definition zelda.h:369
#define ICON_MD_GRID_VIEW
Definition icons.h:897
#define ICON_MD_SAVE_ALT
Definition icons.h:1645
#define ICON_MD_SUMMARIZE
Definition icons.h:1885
#define ICON_MD_VIEW_QUILT
Definition icons.h:2094
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_ALT_ROUTE
Definition icons.h:151
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_VIEW_LIST
Definition icons.h:2092
#define ICON_MD_TRAIN
Definition icons.h:2005
#define ICON_MD_STAR
Definition icons.h:1848
#define ICON_MD_ARROW_FORWARD
Definition icons.h:184
#define ICON_MD_CHECK
Definition icons.h:397
#define ICON_MD_SWAP_HORIZ
Definition icons.h:1896
#define ICON_MD_COMPARE_ARROWS
Definition icons.h:448
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_OPEN_WITH
Definition icons.h:1356
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_OPEN_IN_FULL
Definition icons.h:1353
#define ICON_MD_MAP
Definition icons.h:1173
#define ICON_MD_FLIP_TO_FRONT
Definition icons.h:802
#define ICON_MD_LABEL
Definition icons.h:1053
#define ICON_MD_ARROW_DOWNWARD
Definition icons.h:180
#define ICON_MD_WIDGETS
Definition icons.h:2156
#define ICON_MD_EDIT
Definition icons.h:645
#define ICON_MD_MORE_HORIZ
Definition icons.h:1241
#define ICON_MD_TRAVEL_EXPLORE
Definition icons.h:2012
#define ICON_MD_CASTLE
Definition icons.h:380
#define ICON_MD_CONTENT_PASTE
Definition icons.h:467
#define ICON_MD_GRID_ON
Definition icons.h:896
#define ICON_MD_KEYBOARD_ARROW_DOWN
Definition icons.h:1030
#define ICON_MD_LAYERS
Definition icons.h:1068
#define ICON_MD_ADD
Definition icons.h:86
#define ICON_MD_INVENTORY
Definition icons.h:1011
#define ICON_MD_KEYBOARD
Definition icons.h:1028
#define ICON_MD_DOOR_FRONT
Definition icons.h:613
#define ICON_MD_REMOVE
Definition icons.h:1574
#define ICON_MD_CHEVRON_LEFT
Definition icons.h:405
#define ICON_MD_ARROW_UPWARD
Definition icons.h:189
#define ICON_MD_IMAGE
Definition icons.h:982
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_CLEAR
Definition icons.h:416
#define ICON_MD_PERSON
Definition icons.h:1415
#define ICON_MD_BUILD
Definition icons.h:328
#define ICON_MD_KEYBOARD_ARROW_UP
Definition icons.h:1033
#define ICON_MD_ARROW_BACK
Definition icons.h:173
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_SELECT_ALL
Definition icons.h:1680
#define ICON_MD_DELETE
Definition icons.h:530
#define ICON_MD_EDIT_NOTE
Definition icons.h:650
#define ICON_MD_PALETTE
Definition icons.h:1370
#define ICON_MD_OPEN_IN_NEW
Definition icons.h:1354
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_INVENTORY_2
Definition icons.h:1012
#define ICON_MD_WATER_DROP
Definition icons.h:2131
#define ICON_MD_CLOSE
Definition icons.h:418
#define ICON_MD_CATEGORY
Definition icons.h:382
#define ICON_MD_VIEW_SIDEBAR
Definition icons.h:2095
#define ICON_MD_CHEVRON_RIGHT
Definition icons.h:406
#define ICON_MD_FLIP_TO_BACK
Definition icons.h:801
#define ICON_MD_WORKSPACES
Definition icons.h:2186
#define ICON_MD_CROP_FREE
Definition icons.h:495
const AgentUITheme & GetTheme()
float GetCompactSidebarWidth(bool right_sidebar, float min_sidebar_width)
float ClampWorkbenchPaneWidth(float desired_width, float min_width, float max_width)
std::string GetRoomLabel(const project::YazeProject *project, int room_id)
std::string GetDungeonNameForRoom(const project::YazeProject *project, int room_id)
bool BeginInspectorSection(const char *label, bool default_open)
void DrawPaneHeader(const char *table_id, const char *icon, const char *title, const char *compact_title, const char *subtitle, bool compact, float action_width, const std::function< void()> &draw_actions)
float CalcIconButtonWidth(const char *icon, float button_height)
bool DrawActionButton(const char *label, const ImVec2 &size)
bool HasEditableRoomObjectSize(std::span< const zelda3::RoomObject > objects, std::span< const size_t > selected_indices)
bool DrawHeaderIconAction(const char *id, const char *icon, float button_size, const char *tooltip, bool active=false)
constexpr const char * kConnected
constexpr const char * kWorkbench
Editors are the view controllers for the application.
DungeonWorkbenchPaneLayout ResolveDungeonWorkbenchPaneLayout(float total_width, float min_canvas_width, float min_sidebar_width, float splitter_width, float stored_left_width, float stored_right_width, bool want_left, bool want_right)
DungeonSelectionSnapshot BuildDungeonSelectionSnapshot(const DungeonObjectInteraction &interaction, const DungeonRoomStore *rooms, int room_id)
PitDamageMembershipState BuildPitDamageMembershipState(const zelda3::PitDamageTable *table, int room_id, uint16_t replacement_fallback, uint16_t victim_fallback)
bool ResolveCompactInspectorDetailRequest(bool compact, bool detail_requested)
absl::Status RemoveCurrentRoomFromPitDamage(zelda3::PitDamageTable *table, uint16_t current_room_id, uint16_t replacement_room_id)
RoomSelectionIntent
Intent for room selection in the dungeon editor.
std::string GetDungeonSelectionSummaryText(const DungeonSelectionSnapshot &snapshot)
DungeonWorkbenchResponsiveLayout ResolveDungeonWorkbenchResponsiveLayout(float total_width, float min_canvas_width, float min_sidebar_width, float splitter_width, bool want_left, bool want_right)
bool DrawDungeonWorkbenchVerticalSplitter(const char *id, float height, float *pane_width, float min_width, float max_width, bool resize_from_left_edge, float collapse_threshold)
absl::Status AddCurrentRoomToPitDamage(zelda3::PitDamageTable *table, uint16_t current_room_id, uint16_t victim_room_id)
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
void EndThemedTabBar()
bool ToggleButton(const char *label, bool active, const ImVec2 &size)
bool InputScalarDeferred(const char *label, ImGuiDataType data_type, void *data, const char *format, ImGuiInputTextFlags flags, InputScalarTargetIdentity target_identity)
Definition input.cc:388
InputHexResult InputHexByteEx(const char *label, uint8_t *data, float input_width, bool no_step)
Definition input.cc:529
InputHexResult InputHexWordEx(const char *label, uint16_t *data, float input_width, bool no_step)
Definition input.cc:557
std::string GetRoomTagLabel(int id)
Convenience function to get a room tag label.
LayerBlendMode
Layer blend modes for compositing.
std::string GetSpriteLabel(int id)
Convenience function to get a sprite label.
bool IsRoomObjectSizeEditable(int object_id)
int GetObjectSubtype(int object_id)
std::string GetOverlordLabel(int id)
Convenience function to get an overlord label.
constexpr std::string_view GetDoorDirectionName(DoorDirection dir)
Get human-readable name for door direction.
Definition door_types.h:204
LayerType
Layer types for the 4-way visibility system.
bool UsesRoomObjectStream(const RoomObject &object)
std::string GetObjectName(int object_id)
constexpr std::string_view GetDoorTypeName(DoorType type)
Get human-readable name for door type.
Definition door_types.h:110
constexpr int kNumberOfRooms
const std::string RoomEffect[8]
Definition room.cc:135
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
std::function< const std::deque< int > &()> get_recent_rooms
Represents a selected entity in the dungeon editor.
bool ShouldApply() const
Definition input.h:82
static constexpr float kStatusBarHeight
Definition ui_config.h:21
static constexpr float kContentMinHeightCanvas
Definition ui_config.h:56
static constexpr float kSplitterWidth
Definition ui_config.h:76
static constexpr float kContentMinWidthSidebar
Definition ui_config.h:58
Modern project structure with comprehensive settings consolidation.
Definition project.h:172
Automatic widget registration helpers for ImGui Test Engine integration.