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 <cstdint>
8#include <cstdio>
9#include <cstring>
10#include <utility>
11#include <vector>
12
13#include "absl/strings/str_format.h"
31#include "app/gui/core/icons.h"
32#include "app/gui/core/input.h"
37#include "core/features.h"
38#include "core/project.h"
39#include "imgui/imgui.h"
40#include "rom/rom.h"
50
51namespace yaze::editor {
52
53namespace {
54
55// Object type category names based on ID range
56const char* GetObjectCategory(int object_id) {
57 if (object_id < 0x100)
58 return "Standard";
59 if (object_id < 0x200)
60 return "Extended";
61 if (object_id >= 0xF80)
62 return "Special";
63 return "Unknown";
64}
65
66const char* GetObjectStreamName(int layer_value) {
67 switch (layer_value) {
68 case 0:
69 return "Primary";
70 case 1:
71 return "BG2 overlay";
72 case 2:
73 return "BG1 overlay";
74 default:
75 return "Unknown";
76 }
77}
78
79const char* GetSpecialLayerName(int layer_value) {
80 switch (layer_value) {
81 case 0:
82 return "Upper layer (BG1)";
83 case 1:
84 return "Lower layer (BG2)";
85 default:
86 return "Unknown";
87 }
88}
89
90const char* GetBg2ModeName(int value) {
91 static constexpr const char* kNames[] = {
92 "Off", "Parallax", "Dark", "On top", "Translucent",
93 "Addition", "Normal", "Transparent", "Dark room"};
94 constexpr int kNameCount = sizeof(kNames) / sizeof(kNames[0]);
95 return (value >= 0 && value < kNameCount) ? kNames[value] : "Unknown";
96}
97
98const char* GetCollisionName(int value) {
99 static constexpr const char* kNames[] = {"One", "Both", "Both + Scroll",
100 "Moving Floor", "Moving Water"};
101 constexpr int kNameCount = sizeof(kNames) / sizeof(kNames[0]);
102 return (value >= 0 && value < kNameCount) ? kNames[value] : "Unknown";
103}
104
105// Pot item names for the inspector
106const char* GetPotItemName(uint8_t item) {
107 static const char* kNames[] = {
108 "Nothing", "Green Rupee", "Rock", "Bee",
109 "Heart (4)", "Bomb (4)", "Heart", "Blue Rupee",
110 "Key", "Arrow (5)", "Bomb (1)", "Heart",
111 "Magic (Small)", "Full Magic", "Cucco", "Green Soldier",
112 "Bush Stal", "Blue Soldier", "Landmine", "Heart",
113 "Fairy", "Heart", "Nothing (22)", "Hole",
114 "Warp", "Staircase", "Bombable", "Switch",
115 };
116 constexpr size_t kCount = sizeof(kNames) / sizeof(kNames[0]);
117 return item < kCount ? kNames[item] : "Unknown";
118}
119
120float ClampWorkbenchPaneWidth(float desired_width, float min_width,
121 float max_width) {
122 return std::clamp(desired_width, min_width, std::max(min_width, max_width));
123}
124
125constexpr float kCompactLeftSidebarMinWidth = 224.0f;
126constexpr float kCompactRightSidebarMinWidth = 272.0f;
127constexpr float kCompactLeftSidebarScale = 0.72f;
128constexpr float kCompactRightSidebarScale = 0.84f;
129float GetCompactSidebarWidth(bool right_sidebar, float min_sidebar_width) {
130 return std::max(
131 right_sidebar ? kCompactRightSidebarMinWidth
133 min_sidebar_width * (right_sidebar ? kCompactRightSidebarScale
135}
136
139 rect.visible = true;
140 const ImVec2 min = ImGui::GetItemRectMin();
141 const ImVec2 max = ImGui::GetItemRectMax();
142 rect.min_x = min.x;
143 rect.min_y = min.y;
144 rect.max_x = max.x;
145 rect.max_y = max.y;
146 return rect;
147}
148
149} // namespace
150
151bool ResolveCompactInspectorDetailRequest(bool compact, bool detail_requested) {
152 return compact && detail_requested;
153}
154
161
163 float total_width, float min_canvas_width, float min_sidebar_width,
164 float splitter_width, bool want_left, bool want_right) {
166 result.show_left = want_left;
167 result.show_right = want_right;
168
169 auto required_width = [&](bool left, bool right, bool compact_left,
170 bool compact_right) {
171 float required = min_canvas_width;
172 required +=
173 left ? (compact_left ? GetCompactSidebarWidth(false, min_sidebar_width)
174 : min_sidebar_width)
175 : 0.0f;
176 required +=
177 right ? (compact_right ? GetCompactSidebarWidth(true, min_sidebar_width)
178 : min_sidebar_width)
179 : 0.0f;
180 if (left) {
181 required += splitter_width;
182 }
183 if (right) {
184 required += splitter_width;
185 }
186 return required;
187 };
188
189 if (result.show_left &&
190 total_width <
191 required_width(result.show_left, result.show_right, false, false)) {
192 result.compact_left = true;
193 }
194 if (result.show_right &&
195 total_width < required_width(result.show_left, result.show_right,
196 result.compact_left, false)) {
197 result.compact_right = true;
198 }
199 if (result.show_left &&
200 total_width < required_width(result.show_left, result.show_right,
201 result.compact_left, result.compact_right)) {
202 result.show_left = false;
203 result.compact_left = false;
204 }
205 if (result.show_right &&
206 total_width < required_width(result.show_left, result.show_right,
207 result.compact_left, result.compact_right)) {
208 result.show_right = false;
209 result.compact_right = false;
210 }
211
212 return result;
213}
214
216 float total_width, float min_canvas_width, float min_sidebar_width,
217 float splitter_width, float stored_left_width, float stored_right_width,
218 bool want_left, bool want_right) {
221 total_width, min_canvas_width, min_sidebar_width, splitter_width,
222 want_left, want_right);
223
224 const float compact_left_width =
225 GetCompactSidebarWidth(false, min_sidebar_width);
226 const float compact_right_width =
227 GetCompactSidebarWidth(true, min_sidebar_width);
228 layout.min_left_width =
229 layout.responsive.compact_left ? compact_left_width : min_sidebar_width;
230 layout.min_right_width =
231 layout.responsive.compact_right ? compact_right_width : min_sidebar_width;
232
233 float left_width = layout.responsive.show_left
234 ? (layout.responsive.compact_left ? compact_left_width
235 : stored_left_width)
236 : 0.0f;
237 float right_width =
239 ? (layout.responsive.compact_right ? compact_right_width
240 : stored_right_width)
241 : 0.0f;
242
243 const float max_left_width =
244 total_width - right_width - min_canvas_width -
245 (layout.responsive.show_left ? splitter_width : 0.0f) -
246 (layout.responsive.show_right ? splitter_width : 0.0f);
247 const float max_right_width =
248 total_width - left_width - min_canvas_width -
249 (layout.responsive.show_left ? splitter_width : 0.0f) -
250 (layout.responsive.show_right ? splitter_width : 0.0f);
251 if (layout.responsive.show_left) {
252 left_width = ClampWorkbenchPaneWidth(
253 left_width, layout.min_left_width,
254 std::max(layout.min_left_width, max_left_width));
255 } else {
256 left_width = 0.0f;
257 }
258 if (layout.responsive.show_right) {
259 right_width = ClampWorkbenchPaneWidth(
260 right_width, layout.min_right_width,
261 std::max(layout.min_right_width, max_right_width));
262 } else {
263 right_width = 0.0f;
264 }
265
266 float center_width = total_width - left_width - right_width;
267 if (layout.responsive.show_left) {
268 center_width -= splitter_width;
269 }
270 if (layout.responsive.show_right) {
271 center_width -= splitter_width;
272 }
273 if (center_width < min_canvas_width) {
274 float deficit = min_canvas_width - center_width;
275 if (layout.responsive.show_left) {
276 const float shrink =
277 std::min(deficit, left_width - layout.min_left_width);
278 left_width -= shrink;
279 deficit -= shrink;
280 }
281 if (deficit > 0.0f && layout.responsive.show_right) {
282 const float shrink =
283 std::min(deficit, right_width - layout.min_right_width);
284 right_width -= shrink;
285 deficit -= shrink;
286 }
287 center_width = std::max(
288 1.0f, total_width - left_width - right_width -
289 (layout.responsive.show_left ? splitter_width : 0.0f) -
290 (layout.responsive.show_right ? splitter_width : 0.0f));
291 }
292
293 layout.left_width = left_width;
294 layout.center_width = center_width;
295 layout.right_width = right_width;
296 return layout;
297}
298
300 DungeonRoomSelector* room_selector, int* current_room_id,
301 std::function<void(int)> on_room_selected,
302 std::function<void(int, RoomSelectionIntent)> on_room_selected_with_intent,
303 std::function<void(int)> on_save_room,
304 std::function<void()> on_save_all_rooms,
305 std::function<DungeonCanvasViewer*()> get_viewer,
306 std::function<DungeonCanvasViewer*(int)> get_compare_viewer,
307 std::function<const std::deque<int>&()> get_recent_rooms,
308 std::function<void(int)> forget_recent_room,
309 std::function<void(bool)> set_workflow_mode, Rom* rom)
310 : room_selector_(room_selector),
311 current_room_id_(current_room_id),
312 on_room_selected_(std::move(on_room_selected)),
313 on_room_selected_with_intent_(std::move(on_room_selected_with_intent)),
314 on_save_room_(std::move(on_save_room)),
315 on_save_all_rooms_(std::move(on_save_all_rooms)),
316 get_viewer_(std::move(get_viewer)),
317 get_compare_viewer_(std::move(get_compare_viewer)),
318 get_recent_rooms_(std::move(get_recent_rooms)),
319 forget_recent_room_(std::move(forget_recent_room)),
320 set_workflow_mode_(std::move(set_workflow_mode)),
321 rom_(rom) {}
322
324
326 return "dungeon.workbench";
327}
329 return "Dungeon Workbench";
330}
332 return ICON_MD_WORKSPACES;
333}
335 return "Dungeon";
336}
338 return 10;
339}
340
342 rom_ = rom;
343 room_dungeon_cache_.clear();
345}
346
348 RoomTagEditorPanel* room_tags, CustomCollisionPanel* custom_collision,
349 WaterFillPanel* water_fill, MinecartTrackEditorPanel* minecart_tracks) {
350 room_tag_panel_ = room_tags;
351 custom_collision_panel_ = custom_collision;
352 water_fill_panel_ = water_fill;
353 minecart_track_panel_ = minecart_tracks;
354}
355
357 WindowContent* object_selector, WindowContent* door_editor,
358 WindowContent* sprite_editor, WindowContent* item_editor,
359 WindowContent* room_graphics, WindowContent* palette_editor) {
360 object_selector_content_ = object_selector;
361 door_editor_content_ = door_editor;
362 sprite_editor_content_ = sprite_editor;
363 item_editor_content_ = item_editor;
364 room_graphics_content_ = room_graphics;
365 palette_editor_content_ = palette_editor;
366}
367
374
381
386
391
395
399
403
407
411
415
419
423
427
431
435
439
441 switch (inspector_mode_) {
443 return "room";
445 return "selection";
447 return "tools";
448 }
449 return "unknown";
450}
451
455
456void DungeonWorkbenchContent::DrawSidebarPane(float width, float height,
457 float button_size, bool compact) {
458 const bool sidebar_open = ImGui::BeginChild("##DungeonWorkbenchSidebar",
459 ImVec2(width, height), true);
460 if (sidebar_open) {
461 DrawSidebarHeader(button_size, compact);
463 }
464 ImGui::EndChild();
465}
466
468 bool compact) {
469 const bool can_open_overview = true;
470 const float collapse_w =
472 const float menu_w = can_open_overview ? workbench::CalcIconButtonWidth(
473 ICON_MD_MORE_HORIZ, button_size)
474 : 0.0f;
475 const float spacing = ImGui::GetStyle().ItemSpacing.x;
476 const float header_width = ImGui::GetContentRegionAvail().x;
477 const bool stack_mode_switch = compact && header_width < 220.0f;
478 const float action_cluster_w =
479 collapse_w + (can_open_overview ? (spacing + menu_w) : 0.0f);
480
482 "##DungeonWorkbenchSidebarHeader", ICON_MD_VIEW_SIDEBAR, "Browse",
483 "Browse", nullptr, compact, action_cluster_w, [&]() {
484 if (can_open_overview) {
485 if (workbench::DrawHeaderIconAction("SidebarQuickActions",
486 ICON_MD_MORE_HORIZ, button_size,
487 "Open room review tools", true)) {
488 ImGui::OpenPopup("##WorkbenchSidebarQuickActions");
489 }
490 if (ImGui::BeginPopup("##WorkbenchSidebarQuickActions")) {
491 if (ImGui::MenuItem(ICON_MD_VIEW_QUILT " Stitched Rooms")) {
493 }
494 if (ImGui::MenuItem(ICON_MD_MAP " Dungeon Map")) {
496 }
497 ImGui::Separator();
498 const bool can_open_shortcuts =
499 static_cast<bool>(open_keyboard_shortcuts_);
500 if (ImGui::MenuItem(ICON_MD_KEYBOARD " Keyboard Shortcuts", "?",
501 false, can_open_shortcuts) &&
504 }
505 ImGui::EndPopup();
506 }
507 ImGui::SameLine();
508 }
509 if (workbench::DrawHeaderIconAction("CollapseRooms",
510 ICON_MD_CHEVRON_LEFT, button_size,
511 "Collapse navigation pane")) {
513 }
514 });
515
516 ImGui::Dummy(ImVec2(0.0f, 2.0f));
517 DrawSidebarModeTabs(stack_mode_switch, std::max(button_size, 26.0f));
518 ImGui::Separator();
519}
520
522 float segment_height) {
523 // Compact icon-only segmented selector. Each button is square (size = height)
524 // so the cluster takes ~70px instead of stretching to ~170px with labels.
525 // Tooltips carry the full label.
526 (void)stacked;
527 const float spacing = ImGui::GetStyle().ItemSpacing.x;
528 const ImVec2 button_size(segment_height, segment_height);
529
530 if (gui::ToggleButton(ICON_MD_VIEW_LIST "##NavRooms",
531 sidebar_mode_ == SidebarMode::Rooms, button_size)) {
533 }
534 if (ImGui::IsItemHovered()) {
535 ImGui::SetTooltip(tr("Rooms"));
536 }
537 ImGui::SameLine(0.0f, spacing);
538 if (gui::ToggleButton(ICON_MD_DOOR_FRONT "##NavEntrances",
539 sidebar_mode_ == SidebarMode::Entrances, button_size)) {
541 }
542 if (ImGui::IsItemHovered()) {
543 ImGui::SetTooltip(tr("Entrances"));
544 }
545}
546
548 if (!room_selector_) {
549 ImGui::TextDisabled(tr("Room navigation unavailable"));
550 return;
551 }
552
553 ImGui::PushID("WorkbenchSidebarMode");
554 switch (sidebar_mode_) {
557 break;
560 break;
561 }
562 ImGui::PopID();
563}
564
566 (void)p_open;
567 const auto& theme = AgentUI::GetTheme();
568
569 if (!rom_ || !rom_->is_loaded()) {
570 ImGui::TextDisabled(ICON_MD_INFO " Load a ROM to edit dungeon rooms.");
571 return;
572 }
574 ImGui::TextColored(theme.text_error_red, tr("Dungeon Workbench not wired"));
575 return;
576 }
577
578 DungeonCanvasViewer* primary_viewer = get_viewer_ ? get_viewer_() : nullptr;
579 DungeonCanvasViewer* compare_viewer =
581 const float splitter_w = gui::UIConfig::kSplitterWidth;
582 const float total_w = std::max(ImGui::GetContentRegionAvail().x, 1.0f);
583 // Relaxed from 320 px to 260 px so the Inspect pane can breathe at widths
584 // where the previous floor forced a too-wide right rail. Room browser on the
585 // left also benefits since its matrix cells scale down gracefully.
586 const float min_sidebar_w =
588 const float min_canvas_w = std::max(420.0f, min_sidebar_w + 96.0f);
589 const DungeonWorkbenchPaneLayout pane_layout =
591 total_w, min_canvas_w, min_sidebar_w, splitter_w,
594 const bool show_left = pane_layout.responsive.show_left;
595 const bool show_right = pane_layout.responsive.show_right;
596
597 if (current_room_id_) {
599 params.layout = &layout_state_;
604 params.primary_viewer = primary_viewer;
605 params.compare_viewer = compare_viewer;
609 params.on_open_room_panel = [this](int room_id) {
612 };
613 }
617 params.on_request_dungeon_map = [this]() {
619 };
622 const bool request_panel_workflow = DungeonWorkbenchToolbar::Draw(params);
623 if (request_panel_workflow && set_workflow_mode_) {
624 // Defer panel visibility mutation until toolbar child/table scopes closed.
625 set_workflow_mode_(false);
626 return;
627 }
628 }
629
631 const float total_h = std::max(ImGui::GetContentRegionAvail().y, 1.0f);
632 const float left_w = pane_layout.left_width;
633 const float right_w = pane_layout.right_width;
634 const float center_w = pane_layout.center_width;
635 if (show_left && !pane_layout.responsive.compact_left) {
636 layout_state_.left_width = left_w;
637 }
638 if (show_right && !pane_layout.responsive.compact_right) {
639 layout_state_.right_width = right_w;
640 }
641
642 // Mirror toggle: when inspector_on_left_, swap render order so the inspector
643 // visually sits on the left of the canvas (ZScream-style). Width state is
644 // preserved (right_width is always the inspector's, left_width always the
645 // sidebar's) so splitter drags still affect the correct pane regardless of
646 // which side it's on.
647 if (inspector_on_left_) {
648 if (show_right) {
649 DrawInspectorPane(right_w, total_h, btn,
650 pane_layout.responsive.compact_right, primary_viewer);
651 ImGui::SameLine(0.0f, 0.0f);
653 "##DungeonWorkbenchLeftSplitter", total_h,
655 total_w - left_w - min_canvas_w - (show_left ? splitter_w : 0.0f),
656 false)) {
658 }
659 ImGui::SameLine(0.0f, 0.0f);
660 }
661 DrawCanvasPane(center_w, total_h, primary_viewer);
662 if (show_left) {
663 ImGui::SameLine(0.0f, 0.0f);
665 "##DungeonWorkbenchRightSplitter", total_h,
667 total_w - right_w - min_canvas_w -
668 (show_right ? splitter_w : 0.0f),
669 true)) {
671 }
672 ImGui::SameLine(0.0f, 0.0f);
673 DrawSidebarPane(left_w, total_h, btn,
674 pane_layout.responsive.compact_left);
675 }
676 } else {
677 if (show_left) {
678 DrawSidebarPane(left_w, total_h, btn,
679 pane_layout.responsive.compact_left);
680 }
681 if (show_left) {
682 ImGui::SameLine(0.0f, 0.0f);
684 "##DungeonWorkbenchLeftSplitter", total_h,
686 total_w - right_w - min_canvas_w -
687 (show_right ? splitter_w : 0.0f),
688 false)) {
690 }
691 }
692
693 if (show_left) {
694 ImGui::SameLine(0.0f, 0.0f);
695 }
696 DrawCanvasPane(center_w, total_h, primary_viewer);
697
698 if (show_right) {
699 ImGui::SameLine(0.0f, 0.0f);
701 "##DungeonWorkbenchRightSplitter", total_h,
703 total_w - left_w - min_canvas_w - (show_left ? splitter_w : 0.0f),
704 true)) {
706 }
707 ImGui::SameLine(0.0f, 0.0f);
708 }
709 if (show_right) {
710 DrawInspectorPane(right_w, total_h, btn,
711 pane_layout.responsive.compact_right, primary_viewer);
712 }
713 }
714 if (primary_viewer) {
715 DrawDungeonMapPopup(*primary_viewer);
716 }
717}
718
720 float width, float height, DungeonCanvasViewer* primary_viewer) {
721 const bool canvas_open = ImGui::BeginChild("##DungeonWorkbenchCanvas",
722 ImVec2(width, height), false);
723 if (canvas_open) {
724 if (primary_viewer) {
725 // Reserve a fixed strip at the bottom for the status bar so neither
726 // single-room, split, nor connected-mode rendering can run past it and
727 // clip against the outer window chrome. Inner scrolling (wheel zoom in
728 // connected mode, canvas pan in single-room) happens inside this body
729 // child — the outer ##DungeonWorkbenchCanvas never needs a scrollbar.
730 const float status_bar_reserved =
731 std::max(
732 ImGui::GetFontSize() + ImGui::GetStyle().FramePadding.y * 2.0f,
734 ImGui::GetStyle().ItemSpacing.y;
735
736 const bool connected_mode = layout_state_.show_connected_canvas_view;
737 ImGuiWindowFlags body_flags = 0;
738 if (connected_mode) {
739 // Body child owns the connected-mode scroll container; matrix no
740 // longer nests its own BeginChild for scrolling.
741 const ImVec2 content_size =
743 const float scale = primary_viewer->ConnectedCanvasScale();
744 ImGui::SetNextWindowContentSize(
745 ImVec2(content_size.x * scale, content_size.y * scale));
746 body_flags |= ImGuiWindowFlags_HorizontalScrollbar |
747 ImGuiWindowFlags_NoScrollWithMouse;
748 }
749 const bool body_open = ImGui::BeginChild(
750 "##DungeonCanvasBody", ImVec2(0.0f, -status_bar_reserved), false,
751 body_flags);
752 if (body_open) {
753 if (connected_mode) {
754 split_view_enabled_ = false;
755 if (primary_viewer->DrawConnectedRoomMatrix(*current_room_id_)
756 .has_value()) {
758 }
759 } else if (split_view_enabled_) {
760 DrawSplitView(*primary_viewer);
761 } else {
762 primary_viewer->DrawDungeonCanvas(*current_room_id_);
763 }
765 on_primary_canvas_drawn_(*primary_viewer);
766 }
767 }
768 ImGui::EndChild();
769
770 const char* tool_mode =
771 primary_viewer->object_interaction().mode_manager().GetModeName();
772 bool room_dirty = false;
773 if (auto* rooms = primary_viewer->rooms();
774 rooms && current_room_id_ && *current_room_id_ >= 0) {
775 if (const auto* room = rooms->GetIfMaterialized(*current_room_id_)) {
776 room_dirty = room->HasUnsavedChanges();
777 }
778 }
779 auto status =
780 DungeonStatusBar::BuildState(*primary_viewer, tool_mode, room_dirty);
781 status.workflow_mode = layout_state_.show_connected_canvas_view
783 : nullptr;
784 status.workflow_primary = true;
785 if (can_undo_)
786 status.can_undo = can_undo_();
787 if (can_redo_)
788 status.can_redo = can_redo_();
789 if (undo_desc_) {
790 static std::string s_undo_desc;
791 s_undo_desc = undo_desc_();
792 status.undo_desc = s_undo_desc.empty() ? nullptr : s_undo_desc.c_str();
793 }
794 if (redo_desc_) {
795 static std::string s_redo_desc;
796 s_redo_desc = redo_desc_();
797 status.redo_desc = s_redo_desc.empty() ? nullptr : s_redo_desc.c_str();
798 }
799 if (undo_depth_)
800 status.undo_depth = undo_depth_();
801 status.on_undo = on_undo_;
802 status.on_redo = on_redo_;
804 status.on_selection = [this]() {
806 };
807 }
809 } else {
810 ImGui::TextDisabled(tr("No active viewer"));
811 }
812 }
813 ImGui::EndChild();
814}
815
816void DungeonWorkbenchContent::DrawInspectorPane(float width, float height,
817 float button_size, bool compact,
818 DungeonCanvasViewer* viewer) {
819 const bool inspector_open = ImGui::BeginChild("##DungeonWorkbenchInspector",
820 ImVec2(width, height), true);
821 if (inspector_open) {
822 DrawInspectorHeader(button_size, compact);
823 if (viewer) {
824 DrawInspector(*viewer, compact);
825 } else {
826 ImGui::TextDisabled(tr("No active viewer"));
827 }
828 }
829 ImGui::EndChild();
830}
831
833 bool compact) {
834 // Chevron flips with mirror state so the collapse arrow always points
835 // toward the inspector's exit edge (off-right when on right, off-left when
836 // on left).
837 const char* collapse_icon =
839 const float collapse_w =
840 workbench::CalcIconButtonWidth(collapse_icon, button_size);
841 const float swap_w =
843 const float spacing = ImGui::GetStyle().ItemSpacing.x;
844 const float action_cluster_w = swap_w + spacing + collapse_w;
845
846 const bool tools_mode = inspector_mode_ == InspectorMode::Tools;
848 "##DungeonWorkbenchInspectorHeader",
849 tools_mode ? ICON_MD_BUILD : ICON_MD_TUNE,
850 tools_mode ? "Tools" : "Inspect",
851 tools_mode ? GetWorkbenchToolShortLabel(active_tool_) : "Inspect",
852 nullptr, compact, action_cluster_w, [&]() {
854 "SwapInspectorSide", ICON_MD_SWAP_HORIZ, button_size,
855 inspector_on_left_ ? "Move inspector to the right side"
856 : "Move inspector to the left side "
857 "(ZScream layout)")) {
861 }
862 }
863 ImGui::SameLine(0.0f, spacing);
864 if (workbench::DrawHeaderIconAction("CollapseInspector", collapse_icon,
865 button_size,
866 "Collapse inspector")) {
868 }
869 });
870
871 ImGui::Dummy(ImVec2(0.0f, 2.0f));
872 DrawInspectorPrimarySelector(std::max(button_size, 26.0f));
874 *current_room_id_ >= 0) {
875 ImGui::AlignTextToFramePadding();
876 ImGui::TextUnformatted(tr("Room"));
877 ImGui::SameLine();
878 uint16_t requested_room_id = static_cast<uint16_t>(*current_room_id_);
879 ImGui::SetNextItemWidth(82.0f);
880 const bool can_jump_room = static_cast<bool>(on_room_selected_);
881 if (!can_jump_room) {
882 ImGui::BeginDisabled();
883 }
884 bool room_changed = false;
885 {
886 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
887 room_changed = gui::InputScalarDeferred(
888 "##WorkbenchRoomId", ImGuiDataType_U16, &requested_room_id, "%04X",
889 ImGuiInputTextFlags_CharsHexadecimal,
890 {reinterpret_cast<uintptr_t>(current_room_id_)});
891 gui::AutoRegisterLastItem("input_scalar", "room_id",
892 "Current dungeon room ID");
893 }
894 if (!can_jump_room) {
895 ImGui::EndDisabled();
896 }
897 if (room_changed && on_room_selected_) {
898 const int target_room_id = std::clamp(static_cast<int>(requested_room_id),
900 if (target_room_id != *current_room_id_) {
901 *current_room_id_ = target_room_id;
902 on_room_selected_(target_room_id);
903 }
904 }
905 if (ImGui::IsItemHovered()) {
906 const int preview_room_id = std::clamp(
907 static_cast<int>(requested_room_id), 0, zelda3::kNumberOfRooms - 1);
908 ImGui::SetTooltip(tr("Open room 0x%03X"), preview_room_id);
909 }
910 }
911 ImGui::Separator();
912}
913
915 DungeonCanvasViewer& primary_viewer) {
918 split_view_enabled_ = false;
919 }
920 return;
921 }
922
923 // Choose a sensible default compare room (most-recent non-current).
925 if (get_recent_rooms_) {
926 for (int rid : get_recent_rooms_()) {
927 if (rid != *current_room_id_) {
928 compare_room_id_ = rid;
929 break;
930 }
931 }
932 }
933 }
934
935 if (compare_room_id_ < 0) {
936 // Nothing to compare yet.
937 split_view_enabled_ = false;
938 primary_viewer.DrawDungeonCanvas(*current_room_id_);
939 return;
940 }
941
942 constexpr ImGuiTableFlags kSplitFlags =
943 ImGuiTableFlags_Resizable | ImGuiTableFlags_NoPadOuterX |
944 ImGuiTableFlags_NoPadInnerX | ImGuiTableFlags_BordersInnerV;
945
946 if (!ImGui::BeginTable("##DungeonWorkbenchSplit", 2, kSplitFlags)) {
947 primary_viewer.DrawDungeonCanvas(*current_room_id_);
948 return;
949 }
950
951 ImGui::TableSetupColumn("Active", ImGuiTableColumnFlags_WidthStretch);
952 ImGui::TableSetupColumn("Compare", ImGuiTableColumnFlags_WidthStretch);
953 ImGui::TableNextRow();
954
955 DungeonCanvasViewer* compare_viewer =
957
958 // Active pane (minimum height so canvas never collapses)
959 ImGui::TableNextColumn();
960 ImGui::AlignTextToFramePadding();
961 const project::YazeProject* active_project = primary_viewer.project();
962 ImGui::TextDisabled(
963 ICON_MD_CROP_FREE " Active [%03X] %s", *current_room_id_,
965 .c_str());
966 ImGui::Separator();
967 const bool split_active_open = gui::LayoutHelpers::BeginContentChild(
968 "##SplitActive", ImVec2(0.0f, gui::UIConfig::kContentMinHeightCanvas));
969 if (split_active_open) {
970 primary_viewer.DrawDungeonCanvas(*current_room_id_);
971 }
973
974 // Compare pane
975 ImGui::TableNextColumn();
976 ImGui::AlignTextToFramePadding();
977 const project::YazeProject* compare_project =
978 compare_viewer ? compare_viewer->project() : active_project;
979 ImGui::TextDisabled(
980 ICON_MD_COMPARE_ARROWS " Compare [%03X] %s", compare_room_id_,
982 .c_str());
983 ImGui::Separator();
984 const bool split_compare_open = gui::LayoutHelpers::BeginContentChild(
985 "##SplitCompare", ImVec2(0.0f, gui::UIConfig::kContentMinHeightCanvas));
986 if (split_compare_open) {
987 if (compare_viewer) {
989 compare_viewer->canvas().ApplyScaleSnapshot(
990 primary_viewer.canvas().GetConfig());
991 }
992 compare_viewer->DrawDungeonCanvas(compare_room_id_);
993 } else {
994 ImGui::TextDisabled(tr("No compare viewer"));
995 }
996 }
998
999 ImGui::EndTable();
1000}
1001
1003 room_dungeon_cache_.clear();
1004 room_dungeon_cache_built_ = true; // Always set, even if ROM missing.
1005 if (!rom_ || !rom_->is_loaded())
1006 return;
1007
1008 // Short dungeon names for display in the inspector badge.
1009 // Indices 0-13 = vanilla ALTTP dungeons; higher indices = custom/Oracle.
1010 static const char* const kShortNames[] = {
1011 "Sewers", "HC", "Eastern", "Desert", "A-Tower", "Swamp", "PoD",
1012 "Misery", "Skull", "Ice", "Hera", "Thieves", "Turtle", "GT",
1013 };
1014 constexpr int kVanillaCount =
1015 static_cast<int>(sizeof(kShortNames) / sizeof(kShortNames[0]));
1016
1017 auto AddRoom = [&](int room_id, int dungeon_id) {
1018 if (room_id < 0)
1019 return;
1020 if (room_dungeon_cache_.contains(room_id))
1021 return; // Entrance wins over spawn.
1022 if (dungeon_id >= 0 && dungeon_id < kVanillaCount) {
1023 room_dungeon_cache_[room_id] = kShortNames[dungeon_id];
1024 } else {
1025 char buf[16];
1026 snprintf(buf, sizeof(buf), "Dungeon %02X", dungeon_id);
1027 room_dungeon_cache_[room_id] = buf;
1028 }
1029 };
1030
1031 // Standard entrances (0x00–0x83) — authoritative dungeon assignment.
1032 for (int i = 0; i < 0x84; ++i) {
1033 zelda3::RoomEntrance ent(rom_, static_cast<uint8_t>(i), false);
1034 int did = ent.dungeon_id_;
1035 if (did >= 0 && did < kVanillaCount) {
1036 room_dungeon_cache_[ent.room_] = kShortNames[did];
1037 } else {
1038 char buf[16];
1039 snprintf(buf, sizeof(buf), "Dungeon %02X", did);
1040 room_dungeon_cache_[ent.room_] = buf;
1041 }
1042 }
1043
1044 // Spawn points (0x00–0x13) — fill in rooms not covered by entrances.
1045 for (int i = 0; i < 0x14; ++i) {
1046 zelda3::RoomEntrance ent(rom_, static_cast<uint8_t>(i), true);
1047 AddRoom(static_cast<int>(ent.room_), static_cast<int>(ent.dungeon_id_));
1048 }
1049}
1050
1052 bool compact) {
1053 // Gentle compaction only; inspector sections own their own internal
1054 // spacing (collapsing-header headers, action buttons), so let the theme
1055 // defaults shape inter-section breathing room here.
1056 gui::StyleVarGuard item_spacing_guard(
1057 ImGuiStyleVar_ItemSpacing,
1058 ImVec2(std::max(ImGui::GetStyle().ItemSpacing.x, 4.0f),
1059 std::max(4.0f, ImGui::GetStyle().ItemSpacing.y - 1.0f)));
1060 DrawInspectorShelf(viewer, compact);
1061}
1062
1064 auto& flags = core::FeatureFlags::get().dungeon;
1065 flags.kSaveObjects = value;
1066 flags.kSaveSprites = value;
1067 flags.kSaveRoomHeaders = value;
1068 flags.kSaveChests = value;
1069 flags.kSavePotItems = value;
1070 flags.kSaveEntrances = value;
1071 flags.kSavePalettes = value;
1072 flags.kSaveCollision = value;
1073 flags.kSaveBlocks = value;
1074 flags.kSaveTorches = value;
1075 flags.kSavePits = value;
1076}
1077
1079 auto& flags = core::FeatureFlags::get().dungeon;
1080 bool use_workbench = flags.kUseWorkbench;
1081 if (ImGui::Checkbox(tr("Single-window Workbench"), &use_workbench)) {
1082 flags.kUseWorkbench = use_workbench;
1083 if (set_workflow_mode_) {
1084 set_workflow_mode_(use_workbench);
1085 }
1086 }
1087 if (ImGui::IsItemHovered()) {
1088 ImGui::SetTooltip(tr(
1089 "Keep Dungeon editing in the integrated Workbench instead of separate "
1090 "high-level room panels."));
1091 }
1092
1093 ImGui::Separator();
1094 ImGui::TextDisabled(tr("Data written by Apply Room / Apply Loaded Rooms"));
1095 constexpr ImGuiTableFlags kFlags =
1096 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
1097 if (ImGui::BeginTable("##WorkbenchApplyScopeFlags", 2, kFlags)) {
1098 auto draw_checkbox = [](const char* label, bool* value) {
1099 ImGui::TableNextColumn();
1100 ImGui::Checkbox(label, value);
1101 };
1102 ImGui::TableNextRow();
1103 draw_checkbox("Room Objects", &flags.kSaveObjects);
1104 draw_checkbox("Sprites", &flags.kSaveSprites);
1105 ImGui::TableNextRow();
1106 draw_checkbox("Room Headers", &flags.kSaveRoomHeaders);
1107 draw_checkbox("Chests", &flags.kSaveChests);
1108 ImGui::TableNextRow();
1109 draw_checkbox("Pot Items", &flags.kSavePotItems);
1110 draw_checkbox("Palettes", &flags.kSavePalettes);
1111 ImGui::TableNextRow();
1112 draw_checkbox("Collision Maps", &flags.kSaveCollision);
1113 ImGui::TableNextColumn();
1114 ImGui::BeginDisabled();
1115 ImGui::Checkbox("Water Fill", &flags.kSaveWaterFillZones);
1116 ImGui::EndDisabled();
1117 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1118 ImGui::SetTooltip(
1119 "Water Fill save scope is fixed when the project opens. Change "
1120 "save_dungeon_water_fill_zones in the .yaze file, then reopen the "
1121 "project.");
1122 }
1123 ImGui::TableNextRow();
1124 draw_checkbox("Blocks", &flags.kSaveBlocks);
1125 draw_checkbox("Torches", &flags.kSaveTorches);
1126 ImGui::TableNextRow();
1127 draw_checkbox("Pits", &flags.kSavePits);
1128 draw_checkbox("Entrances", &flags.kSaveEntrances);
1129 ImGui::EndTable();
1130 }
1131
1132 if (ImGui::SmallButton(tr("Select All##WorkbenchApplyScope"))) {
1133 SetAllSaveFlags(true);
1134 }
1135 ImGui::SameLine();
1136 if (ImGui::SmallButton(tr("Select None##WorkbenchApplyScope"))) {
1137 SetAllSaveFlags(false);
1138 }
1139
1140 ImGui::Separator();
1141 if (on_save_room_ && room_id >= 0 &&
1142 workbench::DrawActionButton(ICON_MD_SAVE " Apply Current Room",
1143 ImVec2(-1, 0))) {
1144 on_save_room_(room_id);
1145 }
1146 if (on_save_all_rooms_ &&
1147 workbench::DrawActionButton(ICON_MD_SAVE_ALT " Apply Loaded Rooms",
1148 ImVec2(-1, 0))) {
1150 }
1151}
1152
1155 const auto& theme = AgentUI::GetTheme();
1156 zelda3::PitDamageTable* table =
1158 if (!table) {
1159 ImGui::TextDisabled(
1160 tr("Pit damage table is unavailable until ROM data loads."));
1161 return;
1162 }
1163 const auto membership = BuildPitDamageMembershipState(
1164 table, room_id, pit_damage_replacement_room_id_,
1166 if (!membership.room_valid) {
1167 ImGui::TextDisabled(tr("No valid room selected."));
1168 return;
1169 }
1170
1171 const uint16_t current_room = membership.room_id;
1172 const bool room_deals_damage = membership.deals_damage;
1173 ImGui::TextColored(
1174 room_deals_damage ? theme.status_warning : theme.text_secondary_gray,
1175 tr("%s Room 0x%03X %s pit damage"),
1176 room_deals_damage ? ICON_MD_WARNING : ICON_MD_INFO, room_id,
1177 room_deals_damage ? "deals" : "does not deal");
1178 ImGui::TextWrapped(tr(
1179 "RoomsWithPitDamage is fixed-capacity in vanilla. Editing replaces one "
1180 "listed room with another instead of growing or shrinking the table."));
1181
1182 if (membership.dirty) {
1183 ImGui::TextColored(theme.status_warning,
1184 ICON_MD_EDIT " Pending pit table change");
1185 }
1186
1187 auto show_status = [&]() {
1188 if (pit_damage_status_message_.empty()) {
1189 return;
1190 }
1191 ImGui::TextColored(
1192 pit_damage_status_error_ ? theme.status_error : theme.status_success,
1193 "%s %s",
1196 };
1197
1198 ImGui::Separator();
1199 if (room_deals_damage) {
1200 if (!membership.suggested_replacement_room.has_value()) {
1201 ImGui::TextDisabled(tr("No available non-damaging room slot."));
1202 show_status();
1203 return;
1204 }
1205 pit_damage_replacement_room_id_ = *membership.suggested_replacement_room;
1206
1207 ImGui::TextDisabled(tr("Remove current room by replacing it with:"));
1208 ImGui::SetNextItemWidth(88.0f);
1209 auto edit_value = pit_damage_replacement_room_id_;
1210 if (auto res = gui::InputHexWordEx("##PitDamageReplacementRoom",
1211 &edit_value, 88.0f, true);
1212 res.ShouldApply()) {
1214 std::min<uint16_t>(edit_value, zelda3::kNumberOfRooms - 1);
1215 }
1216 if (ImGui::IsItemHovered()) {
1217 ImGui::SetTooltip(
1218 tr("Replacement room must not already be in the table."));
1219 }
1220 ImGui::SameLine();
1221 const bool replace_clicked =
1222 ImGui::Button(tr("Replace current##PitDamageReplaceCurrent"));
1223 pit_damage_control_rects_.replace_current = CaptureLastItemRectForTesting();
1224 if (replace_clicked) {
1225 auto status = RemoveCurrentRoomFromPitDamage(
1226 table, current_room, pit_damage_replacement_room_id_);
1227 pit_damage_status_error_ = !status.ok();
1229 status.ok()
1230 ? absl::StrFormat("Room 0x%03X removed; slot now 0x%03X",
1231 current_room, pit_damage_replacement_room_id_)
1232 : std::string(status.message());
1233 }
1234 } else {
1235 if (!membership.suggested_victim_room.has_value()) {
1236 ImGui::TextDisabled(tr("No existing pit-damage slot is available."));
1237 show_status();
1238 return;
1239 }
1240 pit_damage_victim_room_id_ = *membership.suggested_victim_room;
1241
1242 ImGui::TextDisabled(tr("Add current room by replacing listed room:"));
1243 ImGui::SetNextItemWidth(88.0f);
1244 auto edit_value = pit_damage_victim_room_id_;
1245 if (auto res = gui::InputHexWordEx("##PitDamageVictimRoom", &edit_value,
1246 88.0f, true);
1247 res.ShouldApply()) {
1249 std::min<uint16_t>(edit_value, zelda3::kNumberOfRooms - 1);
1250 }
1251 if (ImGui::IsItemHovered()) {
1252 ImGui::SetTooltip(tr("This room will stop dealing pit damage."));
1253 }
1254 ImGui::SameLine();
1255 const bool add_clicked =
1256 ImGui::Button(tr("Add current##PitDamageAddCurrent"));
1257 pit_damage_control_rects_.add_current = CaptureLastItemRectForTesting();
1258 if (add_clicked) {
1259 auto status = AddCurrentRoomToPitDamage(table, current_room,
1261 pit_damage_status_error_ = !status.ok();
1263 status.ok()
1264 ? absl::StrFormat("Room 0x%03X added; replaced 0x%03X",
1265 current_room, pit_damage_victim_room_id_)
1266 : std::string(status.message());
1267 }
1268 }
1269
1270 show_status();
1271 ImGui::TextDisabled(
1272 tr("Apply Current Room / Apply Loaded Rooms writes this via "
1273 "SaveAllPits when the Pits scope is enabled."));
1274}
1275
1277 DungeonCanvasViewer& viewer, int room_id) {
1278 if (room_id < 0) {
1279 ImGui::TextDisabled(tr("No active room"));
1280 return;
1281 }
1282
1283 auto& layer_manager = viewer.GetRoomLayerManager(room_id);
1284 auto draw_blend_combo = [&](const char* combo_id,
1285 zelda3::LayerType layer_type) {
1286 zelda3::LayerBlendMode current_mode =
1287 layer_manager.GetLayerBlendMode(layer_type);
1288 const char* current_name =
1290
1291 ImGui::SetNextItemWidth(-1);
1292 if (ImGui::BeginCombo(combo_id, current_name)) {
1293 for (int mode_int = 0; mode_int <= 4; ++mode_int) {
1294 const auto mode = static_cast<zelda3::LayerBlendMode>(mode_int);
1295 const char* mode_name =
1297 const bool selected = current_mode == mode;
1298 if (ImGui::Selectable(mode_name, selected)) {
1299 layer_manager.SetLayerBlendMode(layer_type, mode);
1300 }
1301 if (selected) {
1302 ImGui::SetItemDefaultFocus();
1303 }
1304 }
1305 ImGui::EndCombo();
1306 }
1307 };
1308
1309 struct BlendRow {
1310 const char* label;
1311 const char* combo_id;
1312 zelda3::LayerType layer_type;
1313 };
1314 static constexpr BlendRow kBlendRows[] = {
1315 {"BG1 Layout", "##WorkbenchBlendBG1Layout",
1317 {"BG1 Objects", "##WorkbenchBlendBG1Objects",
1319 {"BG2 Layout", "##WorkbenchBlendBG2Layout",
1321 {"BG2 Objects", "##WorkbenchBlendBG2Objects",
1323 };
1324
1325 constexpr ImGuiTableFlags kBlendTableFlags =
1326 ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_NoPadOuterX;
1327 if (ImGui::BeginTable("##WorkbenchLayerBlend", 2, kBlendTableFlags)) {
1328 ImGui::TableSetupColumn("##label", ImGuiTableColumnFlags_WidthFixed, 92.0f);
1329 ImGui::TableSetupColumn("##combo", ImGuiTableColumnFlags_WidthStretch);
1330 for (const auto& row : kBlendRows) {
1331 ImGui::TableNextRow();
1332 ImGui::TableNextColumn();
1333 ImGui::AlignTextToFramePadding();
1334 ImGui::TextUnformatted(row.label);
1335 ImGui::TableNextColumn();
1336 draw_blend_combo(row.combo_id, row.layer_type);
1337 }
1338 ImGui::EndTable();
1339 }
1340
1341 if (workbench::DrawActionButton(ICON_MD_REFRESH " Reset Layer Blend",
1342 ImVec2(-1, 0))) {
1343 layer_manager.SetLayerBlendMode(zelda3::LayerType::BG1_Layout,
1345 layer_manager.SetLayerBlendMode(zelda3::LayerType::BG1_Objects,
1347 layer_manager.SetLayerBlendMode(zelda3::LayerType::BG2_Layout,
1349 layer_manager.SetLayerBlendMode(zelda3::LayerType::BG2_Objects,
1351 }
1352}
1353
1355 DungeonCanvasViewer& viewer) {
1357 return nullptr;
1358 }
1359 if (!embedded_dungeon_map_) {
1360 embedded_dungeon_map_ = std::make_unique<DungeonMapPanel>(
1362 on_room_selected_, viewer.rooms());
1364 if (*current_room_id_ >= 0) {
1366 }
1367 }
1368 embedded_dungeon_map_->SetRooms(viewer.rooms());
1369 if (const auto* project = viewer.project()) {
1370 embedded_dungeon_map_->SetHackManifest(&project->hack_manifest);
1371 } else {
1372 embedded_dungeon_map_->SetHackManifest(nullptr);
1373 }
1374 return embedded_dungeon_map_.get();
1375}
1376
1378 constexpr const char* kPopupId = "Dungeon Map##WorkbenchDungeonMapPopup";
1380 ImGui::SetNextWindowSize(ImVec2(660.0f, 520.0f), ImGuiCond_Appearing);
1381 ImGui::OpenPopup(kPopupId);
1383 }
1384
1385 bool popup_open = true;
1386 if (ImGui::BeginPopupModal(kPopupId, &popup_open,
1387 ImGuiWindowFlags_NoSavedSettings)) {
1388 if (!popup_open) {
1389 ImGui::CloseCurrentPopup();
1390 ImGui::EndPopup();
1391 return;
1392 }
1393 if (auto* map = GetEmbeddedDungeonMap(viewer)) {
1394 const ImVec2 map_size =
1395 ImVec2(std::max(360.0f, ImGui::GetContentRegionAvail().x),
1396 std::max(260.0f, ImGui::GetContentRegionAvail().y - 34.0f));
1397 if (ImGui::BeginChild("##WorkbenchDungeonMapBody", map_size, false)) {
1398 map->Draw(nullptr);
1399 }
1400 ImGui::EndChild();
1401 } else {
1402 ImGui::TextDisabled(tr("Dungeon map unavailable"));
1403 }
1404
1405 if (workbench::DrawActionButton(ICON_MD_CLOSE " Close", ImVec2(-1, 0))) {
1406 ImGui::CloseCurrentPopup();
1407 }
1408 ImGui::EndPopup();
1409 }
1410}
1411
1413 if (tool == WorkbenchTool::None) {
1414 return;
1415 }
1416 active_tool_ = tool;
1417
1420 // A pinned or explicitly popped-out tool remains the presentation owner.
1421 // Re-open/focus it, and never draw the same WindowContent twice.
1424 if (WindowContent* content = GetWorkbenchToolContent(tool)) {
1425 (void)open_and_focus_standalone_tool_(content->GetId());
1426 }
1427 }
1428 return;
1429 }
1430
1433 }
1437}
1438
1467
1469 WorkbenchTool tool) const {
1470 return GetWorkbenchToolContent(tool) != nullptr;
1471}
1472
1474 WindowContent* content = GetWorkbenchToolContent(tool);
1475 if (!content || !is_standalone_tool_open_) {
1476 return false;
1477 }
1478 const std::string standalone_id = content->GetId();
1479 return !standalone_id.empty() && is_standalone_tool_open_(standalone_id);
1480}
1481
1487
1490 if (!content || !open_and_focus_standalone_tool_) {
1491 return false;
1492 }
1493
1494 const std::string standalone_id = content->GetId();
1495 if (standalone_id.empty() ||
1496 !open_and_focus_standalone_tool_(standalone_id)) {
1497 return false;
1498 }
1499
1501 return true;
1502}
1503
1505 WorkbenchTool tool) const {
1506 switch (tool) {
1508 return "room_tags";
1510 return "custom_collision";
1512 return "water_fill";
1514 return "minecart";
1516 return "object_selector";
1518 return "door";
1520 return "sprite";
1522 return "item";
1524 return "room_graphics";
1526 return "palette";
1528 return "none";
1529 }
1530 return "unknown";
1531}
1532
1534 WorkbenchTool tool) const {
1535 switch (tool) {
1537 return "Room Tags";
1539 return "Custom Collision";
1541 return "Water Fill";
1543 return "Minecart Tracks";
1545 return "Object Selector";
1547 return "Door Tools";
1549 return "Sprite Tools";
1551 return "Item Tools";
1553 return "Room Graphics";
1555 return "Palette";
1557 return "Tool";
1558 }
1559 return "Tool";
1560}
1561
1563 WorkbenchTool tool) const {
1564 switch (tool) {
1566 return "Room tag tools are not available.";
1568 return "Custom collision tools are not available.";
1570 return "Water fill tools are not available.";
1572 return "Minecart track tools are not available.";
1574 return "Object selector is not available.";
1576 return "Door tools are not available.";
1578 return "Sprite tools are not available.";
1580 return "Item tools are not available.";
1582 return "Room graphics tools are not available.";
1584 return "Palette tools are not available.";
1586 return "No Workbench tool selected.";
1587 }
1588 return "Tool is not available.";
1589}
1590
1592 WorkbenchTool tool) {
1593 const int room_id = viewer.current_room_id();
1594 auto draw_window_content = [](WindowContent* content,
1595 const char* unavailable_message) {
1596 if (!content) {
1597 ImGui::TextDisabled("%s", unavailable_message);
1598 return;
1599 }
1600 content->Draw(nullptr);
1601 };
1602
1603 ImGui::PushID(GetWorkbenchToolId(tool));
1604 switch (tool) {
1606 if (!room_tag_panel_) {
1607 ImGui::TextDisabled("%s", GetWorkbenchToolUnavailableMessage(tool));
1608 break;
1609 }
1611 room_tag_panel_->Draw(nullptr);
1612 break;
1615 ImGui::TextDisabled("%s", GetWorkbenchToolUnavailableMessage(tool));
1616 break;
1617 }
1620 custom_collision_panel_->Draw(nullptr);
1621 break;
1623 if (!water_fill_panel_) {
1624 ImGui::TextDisabled("%s", GetWorkbenchToolUnavailableMessage(tool));
1625 break;
1626 }
1629 water_fill_panel_->Draw(nullptr);
1630 break;
1632 if (!minecart_track_panel_) {
1633 ImGui::TextDisabled("%s", GetWorkbenchToolUnavailableMessage(tool));
1634 break;
1635 }
1636 minecart_track_panel_->Draw(nullptr);
1637 break;
1639 draw_window_content(object_selector_content_,
1641 break;
1643 draw_window_content(door_editor_content_,
1645 break;
1647 draw_window_content(sprite_editor_content_,
1649 break;
1651 draw_window_content(item_editor_content_,
1653 break;
1655 draw_window_content(room_graphics_content_,
1657 break;
1659 draw_window_content(palette_editor_content_,
1661 break;
1663 ImGui::TextDisabled("%s", GetWorkbenchToolUnavailableMessage(tool));
1664 break;
1665 }
1666 ImGui::PopID();
1667}
1668
1670 static constexpr std::array<WorkbenchTool, 10> kTools = {
1676 };
1677
1678 const ImGuiStyle& style = ImGui::GetStyle();
1679 const bool active_tool_is_standalone = IsStandaloneToolOpen(active_tool_);
1680 const char* window_action_label = active_tool_is_standalone
1681 ? "Show window##WorkbenchToolPopOut"
1682 : "Pop out##WorkbenchToolPopOut";
1683 const float pop_out_width =
1684 ImGui::CalcTextSize(window_action_label, nullptr, true).x +
1685 style.FramePadding.x * 2.0f;
1686 const float picker_width =
1687 std::max(1.0f, ImGui::GetContentRegionAvail().x - pop_out_width -
1688 style.ItemSpacing.x);
1689 const std::string picker_label = absl::StrFormat(
1690 "%s %s##WorkbenchToolPicker", GetWorkbenchToolShortLabel(active_tool_),
1692 const bool picker_requested = workbench::DrawActionButton(
1693 picker_label.c_str(), ImVec2(picker_width, 0.0f));
1694 {
1695 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
1696 gui::AutoRegisterLastItem("button", "tool_picker",
1697 "Choose a Dungeon Workbench tool");
1698 }
1699 if (picker_requested) {
1700 ImGui::OpenPopup("##WorkbenchToolPickerPopup");
1701 }
1702 if (ImGui::BeginPopup("##WorkbenchToolPickerPopup")) {
1703 for (size_t index = 0; index < kTools.size(); ++index) {
1704 if (index == 0) {
1705 ImGui::SeparatorText(tr("Edit"));
1706 } else if (index == 4) {
1707 ImGui::SeparatorText(tr("Room"));
1708 } else if (index == 6) {
1709 ImGui::SeparatorText(tr("Review"));
1710 }
1711
1712 const WorkbenchTool tool = kTools[index];
1713 const bool available = IsWorkbenchToolAvailable(tool);
1714 if (!available) {
1715 ImGui::BeginDisabled();
1716 }
1717 const bool selected = ImGui::Selectable(GetWorkbenchToolShortLabel(tool),
1718 active_tool_ == tool);
1719 {
1720 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
1722 "button", absl::StrFormat("tool_%s", GetWorkbenchToolId(tool)),
1723 "Open a Dungeon Workbench tool");
1724 }
1725 if (!available) {
1726 ImGui::EndDisabled();
1727 }
1728 if (selected && available) {
1729 OpenTool(tool);
1730 }
1731 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) &&
1732 !available) {
1733 ImGui::SetTooltip("%s", GetWorkbenchToolUnavailableMessage(tool));
1734 }
1735 }
1736 ImGui::EndPopup();
1737 }
1738
1739 ImGui::SameLine(0.0f, style.ItemSpacing.x);
1740 const bool can_pop_out =
1742 if (!can_pop_out) {
1743 ImGui::BeginDisabled();
1744 }
1745 const bool pop_out_requested = workbench::DrawActionButton(
1746 window_action_label, ImVec2(pop_out_width, 0.0f));
1747 {
1748 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
1750 "button", "pop_out_tool",
1751 "Open the active Workbench tool in its standalone window");
1752 }
1753 if (!can_pop_out) {
1754 ImGui::EndDisabled();
1755 }
1756 if (pop_out_requested && can_pop_out) {
1757 (void)PopOutActiveTool();
1758 }
1759}
1760
1762 DungeonCanvasViewer& viewer) {
1765 return;
1766 }
1767
1768 ImGui::Separator();
1770 ImGui::TextDisabled(tr("%s is open in a standalone window."),
1772 ImGui::TextDisabled(tr("Choose another tool or select Show window."));
1773 return;
1774 }
1776 ImGui::TextDisabled("%s", GetWorkbenchToolUnavailableMessage(active_tool_));
1777 return;
1778 }
1779
1780 const bool body_open = ImGui::BeginChild("##WorkbenchToolInspectorBody",
1781 ImVec2(0.0f, 0.0f), false);
1782 if (body_open) {
1784 }
1785 ImGui::EndChild();
1786}
1787
1789 float segment_height) {
1790 const float spacing = ImGui::GetStyle().ItemSpacing.x;
1791 const float room_width =
1792 workbench::CalcIconButtonWidth("Room", segment_height);
1793 const float selection_width =
1794 workbench::CalcIconButtonWidth("Selection", segment_height);
1795 const float tools_width =
1796 workbench::CalcIconButtonWidth("Tools", segment_height);
1797 const float available_width = ImGui::GetContentRegionAvail().x;
1798 const bool stack = available_width < (room_width + selection_width +
1799 tools_width + spacing * 2.0f);
1800 auto draw_mode = [&](const char* label, const char* automation_key,
1801 float width, InspectorMode mode) {
1802 const ImVec2 size(stack ? ImGui::GetContentRegionAvail().x : width,
1803 segment_height);
1804 const bool pressed =
1805 gui::ToggleButton(label, inspector_mode_ == mode, size);
1806 {
1807 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
1808 gui::AutoRegisterLastItem("button", automation_key,
1809 "Switch Dungeon Workbench inspector mode");
1810 }
1811 if (pressed) {
1812 if (mode != InspectorMode::Tools) {
1814 }
1815 inspector_mode_ = mode;
1817 }
1818 };
1819
1820 draw_mode("Room", "mode_room", room_width, InspectorMode::Room);
1821 if (!stack) {
1822 ImGui::SameLine(0.0f, spacing);
1823 }
1824 draw_mode("Selection", "mode_selection", selection_width,
1826 if (!stack) {
1827 ImGui::SameLine(0.0f, spacing);
1828 }
1829 draw_mode("Tools", "mode_tools", tools_width, InspectorMode::Tools);
1830}
1831
1833 DungeonCanvasViewer& viewer) {
1834 const int room_id = (viewer.current_room_id() >= 0)
1835 ? viewer.current_room_id()
1837 const auto& interaction = viewer.object_interaction();
1838 const size_t selected_objects = interaction.GetSelectionCount();
1839 const bool has_entity = interaction.HasEntitySelection();
1840
1841 ImGui::TextDisabled(ICON_MD_SUMMARIZE " Summary");
1842 if (room_id >= 0) {
1843 ImGui::Text("[%03X] %s", room_id,
1845 .c_str());
1846 } else {
1847 ImGui::TextDisabled(tr("No room selected"));
1848 }
1849
1850 ImGui::Dummy(ImVec2(0.0f, 2.0f));
1851 if (selected_objects > 0 || has_entity) {
1852 ImGui::TextDisabled(ICON_MD_SELECT_ALL " Focus");
1853 if (has_entity) {
1854 ImGui::BulletText(tr("Entity selected"));
1855 }
1856 if (selected_objects > 0) {
1857 ImGui::BulletText(tr("%zu object%s selected"), selected_objects,
1858 selected_objects == 1 ? "" : "s");
1859 }
1861 ImVec2(-1, 0))) {
1864 }
1865 } else {
1866 ImGui::TextDisabled(tr("Nothing selected"));
1867 }
1868
1869 // Apply Room, View overlays, and tool switching all live elsewhere now:
1870 // Apply Room is on the canvas toolbar, the overlay checkboxes live in the
1871 // toolbar's View Options popup, and the Tools segment opens the inspector's
1872 // tool surface. Compact summary stays focused on what's selected.
1873}
1874
1876 bool compact) {
1877 const auto& interaction = viewer.object_interaction();
1878 const bool has_selection =
1879 interaction.GetSelectionCount() > 0 || interaction.HasEntitySelection();
1880 if (has_selection && !inspector_selection_was_active_ &&
1883 }
1884 inspector_selection_was_active_ = has_selection;
1885
1888
1889 // Use the resolved pane layout instead of GetContentRegionAvail().x. The
1890 // latter changes when a vertical scrollbar appears, which can make the
1891 // inspector alternate between full and compact content every frame.
1892 if (compact && inspector_mode_ != InspectorMode::Tools &&
1895 return;
1896 }
1897
1898 switch (inspector_mode_) {
1900 DrawInspectorShelfRoom(viewer);
1901 break;
1904 break;
1906 DrawInspectorToolPanel(viewer);
1907 return;
1908 }
1909
1910 // Stitched Rooms, View Options, and the old Tools collapsible intentionally
1911 // removed: the canvas toolbar exposes the Stitched Rooms toggle directly,
1912 // the View Options popup off the toolbar's eye icon owns all 11 overlay
1913 // checkboxes, and Tools is a peer inspector mode instead of another pane.
1914}
1915
1917 DungeonCanvasViewer& viewer) {
1918 const auto& theme = AgentUI::GetTheme();
1919
1920 int room_id = viewer.current_room_id();
1921 if (room_id < 0 && current_room_id_) {
1922 room_id = *current_room_id_;
1923 }
1924
1925 const std::string room_label =
1926 (room_id >= 0)
1928 : std::string("None");
1929
1930 // Room badge: hex ID + copy button (only for valid room IDs).
1932 if (room_id >= 0) {
1933 ImGui::AlignTextToFramePadding();
1934 ImGui::Text(tr("Room 0x%03X"), room_id);
1935 ImGui::SameLine();
1936 if (ImGui::SmallButton(ICON_MD_CONTENT_COPY "##CopyRoomId")) {
1937 char buf[16];
1938 snprintf(buf, sizeof(buf), "0x%03X", room_id);
1939 ImGui::SetClipboardText(buf);
1940 }
1941 if (ImGui::IsItemHovered()) {
1942 ImGui::SetTooltip(tr("Copy room ID (0x%03X) to clipboard"), room_id);
1943 }
1944
1945 if (auto* rooms = viewer.rooms();
1946 rooms && room_id < static_cast<int>(rooms->size())) {
1947 auto& objects = (*rooms)[room_id].GetTileObjects();
1948 if (!objects.empty()) {
1949 const auto selected_indices =
1951 int requested_object_index =
1952 selected_indices.size() == 1
1953 ? static_cast<int>(selected_indices.front())
1954 : 0;
1955 ImGui::AlignTextToFramePadding();
1956 ImGui::TextUnformatted(tr("Object"));
1957 ImGui::SameLine();
1958 ImGui::SetNextItemWidth(74.0f);
1959 bool object_index_changed = false;
1960 {
1961 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
1962 object_index_changed = gui::InputScalarDeferred(
1963 "##WorkbenchObjectIndex", ImGuiDataType_S32,
1964 &requested_object_index, "%d", 0,
1965 {reinterpret_cast<uintptr_t>(rooms),
1966 static_cast<uint64_t>(room_id)});
1967 gui::AutoRegisterLastItem("input_int", "object_index",
1968 "Select and locate a room object by index");
1969 }
1970 if (object_index_changed) {
1971 const size_t target_index = static_cast<size_t>(std::clamp(
1972 requested_object_index, 0, static_cast<int>(objects.size()) - 1));
1973 viewer.object_interaction().SetSelectedObjects({target_index});
1974 viewer.ScrollToTile(objects[target_index].x(),
1975 objects[target_index].y());
1977 }
1978 if (ImGui::IsItemHovered()) {
1979 ImGui::SetTooltip(tr("Select object index 0-%zu"),
1980 objects.size() - 1);
1981 }
1982 }
1983 }
1984 } else {
1985 ImGui::TextUnformatted(tr("Room: None"));
1986 }
1987
1988 bool room_dirty = false;
1989 if (auto* rooms = viewer.rooms(); rooms && room_id >= 0) {
1990 if (const auto* room = rooms->GetIfMaterialized(room_id)) {
1991 room_dirty = room->HasUnsavedChanges();
1992 }
1993 }
1994 if (room_dirty) {
1995 ImGui::TextColored(theme.status_warning,
1996 ICON_MD_EDIT " Pending room changes");
1997 } else if (room_id >= 0) {
1998 ImGui::TextDisabled(ICON_MD_CHECK " Room matches ROM buffer");
1999 }
2000
2001 // Dungeon group context: prefer ROM entrance-based lookup (accurate for
2002 // custom Oracle dungeons); fall back to blockset-derived name.
2005 }
2006 if (room_id >= 0) {
2007 std::string project_group_name =
2009 room_id);
2010 const char* group_name =
2011 project_group_name.empty() ? nullptr : project_group_name.c_str();
2012 if (!group_name) {
2013 auto cache_it = room_dungeon_cache_.find(room_id);
2014 if (cache_it != room_dungeon_cache_.end() && !cache_it->second.empty()) {
2015 group_name = cache_it->second.c_str();
2016 }
2017 }
2018 if (!group_name) {
2019 auto* rooms = viewer.rooms();
2020 if (rooms && room_id < static_cast<int>(rooms->size())) {
2022 (*rooms)[room_id].blockset());
2023 }
2024 }
2025 if (group_name) {
2026 ImGui::TextDisabled(ICON_MD_CASTLE " %s – %s", group_name,
2027 room_label.c_str());
2028 } else {
2029 ImGui::TextDisabled("%s", room_label.c_str());
2030 }
2031 } else {
2032 ImGui::TextDisabled("%s", room_label.c_str());
2033 }
2034
2035 // Apply Room and Dungeon Map have moved to the canvas toolbar (Save and
2036 // Map icons). Apply Scope and Layer Compositing remain here as
2037 // collapsibles since they're rarely-touched per-room batch settings.
2039 false)) {
2040 DrawApplyScopeControls(room_id);
2041 }
2042
2043 if (workbench::BeginInspectorSection(ICON_MD_WARNING " Pit Damage", false)) {
2044 DrawPitDamageControls(room_id);
2045 }
2046
2047 if (workbench::BeginInspectorSection(ICON_MD_LAYERS " Layer Compositing",
2048 false)) {
2049 DrawLayerCompositingControls(viewer, room_id);
2050 }
2051
2052 // Preserve the ZScream-style raw ROM controls, but keep them collapsed until
2053 // an experienced author asks for them. Normal room navigation and selection
2054 // work should not begin with a wall of header bytes.
2055 if (auto* rooms = viewer.rooms();
2056 rooms && room_id >= 0 && room_id < static_cast<int>(rooms->size())) {
2057 auto& room = (*rooms)[room_id];
2058
2059 uint8_t layout_val = room.layout_id();
2060 uint8_t blockset_val = room.blockset();
2061 uint8_t floor1_val = room.floor1();
2062 uint8_t floor2_val = room.floor2();
2063 uint8_t palette_val = room.palette();
2064 uint8_t spriteset_val = room.spriteset();
2065 uint16_t message_val = room.message_id();
2066 uint8_t bg2_val = static_cast<uint8_t>(room.bg2());
2067 uint8_t effect_val = static_cast<uint8_t>(room.effect());
2068 uint8_t collision_val = static_cast<uint8_t>(room.collision());
2069 uint8_t tag1_val = static_cast<uint8_t>(room.tag1());
2070 uint8_t tag2_val = static_cast<uint8_t>(room.tag2());
2071
2072 auto render_room_graphics = [&]() {
2073 if (room.rom() && room.rom()->is_loaded()) {
2074 room.RenderRoomGraphics();
2075 }
2076 };
2077
2078 constexpr float kHexW = 54.0f;
2079 constexpr ImGuiTableFlags kHeaderFlags = ImGuiTableFlags_BordersInnerV |
2080 ImGuiTableFlags_RowBg |
2081 ImGuiTableFlags_NoPadOuterX;
2082 auto draw_label = [](const char* label) {
2083 ImGui::AlignTextToFramePadding();
2084 ImGui::TextUnformatted(label);
2085 };
2086 auto draw_byte_field = [&](const char* label, const char* id, uint8_t value,
2087 uint8_t max_value, const std::string& tooltip,
2088 auto apply) {
2089 ImGui::TableNextColumn();
2090 draw_label(label);
2091 ImGui::TableNextColumn();
2092 ImGui::SetNextItemWidth(kHexW);
2093 uint8_t edit_value = value;
2094 if (auto res =
2095 gui::InputHexByteEx(id, &edit_value, max_value, kHexW, true);
2096 res.ShouldApply()) {
2097 apply(edit_value);
2098 }
2099 if (ImGui::IsItemHovered()) {
2100 ImGui::SetTooltip("%s", tooltip.c_str());
2101 }
2102 };
2103 auto draw_word_field = [&](const char* label, const char* id,
2104 uint16_t value, uint16_t max_value,
2105 const std::string& tooltip, auto apply) {
2106 ImGui::TableNextColumn();
2107 draw_label(label);
2108 ImGui::TableNextColumn();
2109 ImGui::SetNextItemWidth(kHexW + 12.0f);
2110 uint16_t edit_value = value;
2111 if (auto res = gui::InputHexWordEx(id, &edit_value, kHexW + 12.0f, true);
2112 res.ShouldApply()) {
2113 apply(std::min<uint16_t>(edit_value, max_value));
2114 }
2115 if (ImGui::IsItemHovered()) {
2116 ImGui::SetTooltip("%s", tooltip.c_str());
2117 }
2118 };
2119
2120 if (workbench::BeginInspectorSection(ICON_MD_TUNE " Room Header", false) &&
2121 ImGui::BeginTable("##WorkbenchRoomHeader", 4, kHeaderFlags)) {
2122 ImGui::TableSetupColumn("L1", ImGuiTableColumnFlags_WidthFixed, 44.0f);
2123 ImGui::TableSetupColumn("V1", ImGuiTableColumnFlags_WidthFixed, 66.0f);
2124 ImGui::TableSetupColumn("L2", ImGuiTableColumnFlags_WidthFixed, 44.0f);
2125 ImGui::TableSetupColumn("V2", ImGuiTableColumnFlags_WidthStretch);
2126
2127 ImGui::TableNextRow();
2128 draw_byte_field("Lay", "##RoomHeaderLayout", layout_val, 0x07,
2129 "Layout (0-7)", [&](uint8_t value) {
2130 room.SetLayoutId(value);
2131 room.MarkLayoutDirty();
2132 render_room_graphics();
2133 });
2134 draw_byte_field("Blk", "##RoomHeaderBlockset", blockset_val, 0x51,
2135 "Blockset (0-51)", [&](uint8_t value) {
2136 room.SetBlockset(value);
2137 render_room_graphics();
2138 });
2139
2140 ImGui::TableNextRow();
2141 draw_byte_field("F1", "##RoomHeaderFloor1", floor1_val, 0x0F,
2142 "BG1 floor graphics (0-F)", [&](uint8_t value) {
2143 room.set_floor1(value);
2144 render_room_graphics();
2145 });
2146 draw_byte_field("F2", "##RoomHeaderFloor2", floor2_val, 0x0F,
2147 "BG2 floor graphics (0-F)", [&](uint8_t value) {
2148 room.set_floor2(value);
2149 render_room_graphics();
2150 });
2151
2152 ImGui::TableNextRow();
2153 draw_byte_field("Pal", "##RoomHeaderPalette", palette_val, 0x47,
2154 "Palette set (0-47)", [&](uint8_t value) {
2155 room.SetPalette(value);
2156 render_room_graphics();
2157 if (on_room_selected_) {
2158 on_room_selected_(room_id);
2159 }
2160 });
2161 draw_byte_field("Spr", "##RoomHeaderSpriteset", spriteset_val, 0x8F,
2162 "Sprite graphics set (0-8F)", [&](uint8_t value) {
2163 room.SetSpriteset(value);
2164 render_room_graphics();
2165 });
2166
2167 ImGui::TableNextRow();
2168 draw_word_field("Msg", "##RoomHeaderMessage", message_val, 0x0FFF,
2169 "Dungeon message ID (0-FFF)",
2170 [&](uint16_t value) { room.SetMessageId(value); });
2171 draw_byte_field("BG2", "##RoomHeaderBg2", bg2_val, 0x08,
2172 std::string("BG2 mode: ") + GetBg2ModeName(bg2_val),
2173 [&](uint8_t value) {
2174 room.SetBg2(static_cast<background2>(value));
2175 render_room_graphics();
2176 });
2177
2178 ImGui::TableNextRow();
2179 const char* effect_name =
2180 effect_val < 8 ? zelda3::RoomEffect[effect_val].c_str() : "Unknown";
2181 draw_byte_field("FX", "##RoomHeaderEffect", effect_val, 0x07,
2182 std::string("Effect: ") + effect_name,
2183 [&](uint8_t value) {
2184 room.SetEffect(static_cast<zelda3::EffectKey>(value));
2185 render_room_graphics();
2186 });
2187 draw_byte_field(
2188 "Coll", "##RoomHeaderCollision", collision_val, 0x04,
2189 std::string("Collision: ") + GetCollisionName(collision_val),
2190 [&](uint8_t value) {
2191 room.SetCollision(static_cast<zelda3::CollisionKey>(value));
2192 });
2193
2194 ImGui::TableNextRow();
2195 draw_byte_field("Tag1", "##RoomHeaderTag1", tag1_val, 0x40,
2196 std::string("Tag1: ") + zelda3::GetRoomTagLabel(tag1_val),
2197 [&](uint8_t value) {
2198 room.SetTag1(static_cast<zelda3::TagKey>(value));
2199 render_room_graphics();
2200 });
2201 draw_byte_field("Tag2", "##RoomHeaderTag2", tag2_val, 0x40,
2202 std::string("Tag2: ") + zelda3::GetRoomTagLabel(tag2_val),
2203 [&](uint8_t value) {
2204 room.SetTag2(static_cast<zelda3::TagKey>(value));
2205 render_room_graphics();
2206 });
2207
2208 ImGui::EndTable();
2209 }
2210
2212 false) &&
2213 ImGui::BeginTable("##WorkbenchRoomDestinations", 4, kHeaderFlags)) {
2214 ImGui::TableSetupColumn("L1", ImGuiTableColumnFlags_WidthFixed, 44.0f);
2215 ImGui::TableSetupColumn("V1", ImGuiTableColumnFlags_WidthFixed, 66.0f);
2216 ImGui::TableSetupColumn("L2", ImGuiTableColumnFlags_WidthFixed, 44.0f);
2217 ImGui::TableSetupColumn("V2", ImGuiTableColumnFlags_WidthStretch);
2218
2219 ImGui::TableNextRow();
2220 draw_byte_field("Pit", "##RoomHeaderPit", room.holewarp(), 0xFF,
2221 "Pit/holewarp destination room",
2222 [&](uint8_t value) { room.SetHolewarp(value); });
2223 draw_byte_field("St1", "##RoomHeaderStair1", room.staircase_room(0), 0xFF,
2224 "Stair destination slot 1",
2225 [&](uint8_t value) { room.SetStaircaseRoom(0, value); });
2226
2227 ImGui::TableNextRow();
2228 draw_byte_field("St2", "##RoomHeaderStair2", room.staircase_room(1), 0xFF,
2229 "Stair destination slot 2",
2230 [&](uint8_t value) { room.SetStaircaseRoom(1, value); });
2231 draw_byte_field("St3", "##RoomHeaderStair3", room.staircase_room(2), 0xFF,
2232 "Stair destination slot 3",
2233 [&](uint8_t value) { room.SetStaircaseRoom(2, value); });
2234
2235 ImGui::TableNextRow();
2236 draw_byte_field("St4", "##RoomHeaderStair4", room.staircase_room(3), 0xFF,
2237 "Stair destination slot 4",
2238 [&](uint8_t value) { room.SetStaircaseRoom(3, value); });
2239 draw_byte_field("P1", "##RoomHeaderStairPlane1", room.staircase_plane(0),
2240 0x03, "Stair 1 target layer/plane (0-3)",
2241 [&](uint8_t value) { room.SetStaircasePlane(0, value); });
2242
2243 ImGui::TableNextRow();
2244 draw_byte_field("P2", "##RoomHeaderStairPlane2", room.staircase_plane(1),
2245 0x03, "Stair 2 target layer/plane (0-3)",
2246 [&](uint8_t value) { room.SetStaircasePlane(1, value); });
2247 draw_byte_field("P3", "##RoomHeaderStairPlane3", room.staircase_plane(2),
2248 0x03, "Stair 3 target layer/plane (0-3)",
2249 [&](uint8_t value) { room.SetStaircasePlane(2, value); });
2250
2251 ImGui::TableNextRow();
2252 draw_byte_field("P4", "##RoomHeaderStairPlane4", room.staircase_plane(3),
2253 0x03, "Stair 4 target layer/plane (0-3)",
2254 [&](uint8_t value) { room.SetStaircasePlane(3, value); });
2255 ImGui::TableNextColumn();
2256 ImGui::TableNextColumn();
2257
2258 ImGui::EndTable();
2259 }
2260 } else {
2261 ImGui::TextDisabled(tr("Room header unavailable"));
2262 }
2263
2264 auto& interaction = viewer.object_interaction();
2265 const bool placing = interaction.mode_manager().IsPlacementActive();
2266 if (placing) {
2268 ImGui::TextColored(theme.text_info, tr("Placement active"));
2269 ImGui::SameLine();
2270 if (ImGui::SmallButton(ICON_MD_CLOSE " Cancel")) {
2271 interaction.mode_manager().CancelCurrentMode();
2272 }
2273 }
2274}
2275
2277 DungeonCanvasViewer& viewer) {
2278 auto& interaction = viewer.object_interaction();
2279 const auto& theme = AgentUI::GetTheme();
2280
2281 const int room_id = viewer.current_room_id();
2282 const size_t obj_count = interaction.GetSelectionCount();
2283 const bool has_entity = interaction.HasEntitySelection();
2284
2285 if (!has_entity && obj_count == 0) {
2287 ImGui::TextDisabled(tr("Click an object or entity to inspect"));
2288 return;
2289 }
2290
2291 // ── Tile Object Selection ──
2292 if (obj_count > 0) {
2294 ImGui::TextColored(theme.text_primary, ICON_MD_WIDGETS " %zu object%s",
2295 obj_count, obj_count == 1 ? "" : "s");
2296 if (obj_count == 1) {
2297 ImGui::SameLine();
2298 ImGui::TextDisabled(tr("Focused selection"));
2299 }
2300
2301 const auto indices = interaction.GetSelectedObjectIndices();
2302
2303 // Multi-object summary + inline bulk editor (O1)
2304 if (indices.size() > 1 && room_id >= 0 && viewer.rooms()) {
2305 auto& room = (*viewer.rooms())[room_id];
2306 auto& objects = room.GetTileObjects();
2308 " Multi-selection");
2309 for (size_t i = 0; i < indices.size() && i < 8; ++i) {
2310 size_t idx = indices[i];
2311 if (idx < objects.size()) {
2312 auto& obj = objects[idx];
2313 std::string name = zelda3::GetObjectName(obj.id_);
2314 ImGui::BulletText(tr("0x%03X %s"), obj.id_, name.c_str());
2315 }
2316 }
2317 if (indices.size() > 8) {
2318 ImGui::TextDisabled(tr(" ... and %zu more"), indices.size() - 8);
2319 }
2320
2321 // Bulk editor: nudge/layer/stacking/size + destructive duplicate/delete.
2322 // Operations route through tile_handler / interaction, so every action
2323 // captures its own undo snapshot.
2324 auto& tile_handler = interaction.entity_coordinator().tile_handler();
2325 const std::vector<size_t> selection_copy(indices.begin(), indices.end());
2326 bool has_room_stream_object = false;
2327 bool has_special_layer_object = false;
2328 for (const size_t index : selection_copy) {
2329 if (index >= objects.size()) {
2330 continue;
2331 }
2332 if (zelda3::UsesRoomObjectStream(objects[index])) {
2333 has_room_stream_object = true;
2334 } else {
2335 has_special_layer_object = true;
2336 }
2337 }
2338 const bool has_editable_size =
2339 workbench::HasEditableRoomObjectSize(objects, selection_copy);
2340
2342
2343 // Nudge grid (arrow buttons around a tile-delta drag).
2344 static int bulk_nudge_dx = 0;
2345 static int bulk_nudge_dy = 0;
2346 ImGui::TextDisabled(tr("Nudge (tiles)"));
2347 ImGui::PushButtonRepeat(true);
2348 if (ImGui::Button(ICON_MD_ARROW_UPWARD "##BulkNudgeUp"))
2349 tile_handler.MoveObjects(room_id, selection_copy, 0, -1);
2350 ImGui::SameLine();
2351 if (ImGui::Button(ICON_MD_ARROW_DOWNWARD "##BulkNudgeDown"))
2352 tile_handler.MoveObjects(room_id, selection_copy, 0, 1);
2353 ImGui::SameLine();
2354 if (ImGui::Button(ICON_MD_ARROW_BACK "##BulkNudgeLeft"))
2355 tile_handler.MoveObjects(room_id, selection_copy, -1, 0);
2356 ImGui::SameLine();
2357 if (ImGui::Button(ICON_MD_ARROW_FORWARD "##BulkNudgeRight"))
2358 tile_handler.MoveObjects(room_id, selection_copy, 1, 0);
2359 ImGui::PopButtonRepeat();
2360
2361 ImGui::SetNextItemWidth(60);
2362 ImGui::DragInt("##BulkNudgeDx", &bulk_nudge_dx, 0.25f, -63, 63, "Δx:%d");
2363 ImGui::SameLine();
2364 ImGui::SetNextItemWidth(60);
2365 ImGui::DragInt("##BulkNudgeDy", &bulk_nudge_dy, 0.25f, -63, 63, "Δy:%d");
2366 ImGui::SameLine();
2367 if (ImGui::Button(tr("Apply##BulkNudgeApply")) &&
2368 (bulk_nudge_dx != 0 || bulk_nudge_dy != 0)) {
2369 tile_handler.MoveObjects(room_id, selection_copy, bulk_nudge_dx,
2370 bulk_nudge_dy);
2371 bulk_nudge_dx = 0;
2372 bulk_nudge_dy = 0;
2373 }
2374
2375 ImGui::Spacing();
2376 if (has_room_stream_object && !has_special_layer_object) {
2377 ImGui::TextDisabled(tr("Object stream (set all)"));
2378 if (ImGui::SmallButton(tr("Primary##BulkPlacement0")))
2379 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 0);
2380 ImGui::SameLine();
2381 if (ImGui::SmallButton(tr("BG2 overlay##BulkPlacement1")))
2382 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 1);
2383 ImGui::SameLine();
2384 if (ImGui::SmallButton(tr("BG1 overlay##BulkPlacement2")))
2385 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 2);
2386 } else if (has_special_layer_object && !has_room_stream_object) {
2387 ImGui::TextDisabled(tr("Special layer (set all)"));
2388 if (ImGui::SmallButton(tr("Upper (BG1)##BulkPlacement0")))
2389 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 0);
2390 ImGui::SameLine();
2391 if (ImGui::SmallButton(tr("Lower (BG2)##BulkPlacement1")))
2392 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 1);
2393 } else {
2394 ImGui::TextDisabled(tr("Stored placement (mixed selection)"));
2395 if (ImGui::SmallButton(tr("Primary / Upper (BG1)##BulkPlacement0")))
2396 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 0);
2397 ImGui::SameLine();
2398 if (ImGui::SmallButton(tr("BG2 overlay / Lower (BG2)##BulkPlacement1")))
2399 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 1);
2400 }
2401
2402 ImGui::Spacing();
2403 ImGui::TextDisabled(tr("Stacking"));
2404 if (ImGui::SmallButton(ICON_MD_FLIP_TO_FRONT " Front##BulkFront"))
2405 tile_handler.SendToFront(room_id, selection_copy);
2406 ImGui::SameLine();
2407 if (ImGui::SmallButton(ICON_MD_FLIP_TO_BACK " Back##BulkBack"))
2408 tile_handler.SendToBack(room_id, selection_copy);
2409 ImGui::SameLine();
2410 if (ImGui::SmallButton(ICON_MD_KEYBOARD_ARROW_UP "##BulkFwd"))
2411 tile_handler.MoveForward(room_id, selection_copy);
2412 ImGui::SameLine();
2413 if (ImGui::SmallButton(ICON_MD_KEYBOARD_ARROW_DOWN "##BulkBwd"))
2414 tile_handler.MoveBackward(room_id, selection_copy);
2415
2416 ImGui::Spacing();
2417 ImGui::TextDisabled(tr("Size"));
2418 if (!has_editable_size) {
2419 ImGui::BeginDisabled();
2420 }
2421 if (ImGui::SmallButton(ICON_MD_REMOVE "##BulkSizeDec"))
2422 tile_handler.ResizeObjects(room_id, selection_copy, -1);
2423 ImGui::SameLine();
2424 if (ImGui::SmallButton(ICON_MD_ADD "##BulkSizeInc"))
2425 tile_handler.ResizeObjects(room_id, selection_copy, 1);
2426 if (!has_editable_size) {
2427 ImGui::EndDisabled();
2428 }
2429
2430 ImGui::Spacing();
2431 if (ImGui::SmallButton(ICON_MD_CONTENT_COPY " Duplicate##BulkDup"))
2432 (void)tile_handler.DuplicateObjects(room_id, selection_copy, 1, 1);
2433 ImGui::SameLine();
2434 {
2435 gui::StyleColorGuard danger_colors({
2436 {ImGuiCol_Button, theme.status_error},
2437 {ImGuiCol_ButtonHovered,
2438 ImVec4(theme.status_error.x + 0.1f, theme.status_error.y + 0.05f,
2439 theme.status_error.z + 0.05f, 1.0f)},
2440 });
2441 if (ImGui::SmallButton(ICON_MD_DELETE " Delete##BulkDel"))
2442 ImGui::OpenPopup("##BulkDeleteConfirm");
2443 }
2444
2445 if (ImGui::BeginPopup("##BulkDeleteConfirm")) {
2446 ImGui::TextColored(
2447 theme.status_error, ICON_MD_WARNING " Delete %zu object%s?",
2448 selection_copy.size(), selection_copy.size() == 1 ? "" : "s");
2449 ImGui::Separator();
2450 if (ImGui::Button(tr("Cancel##BulkDelCancel"))) {
2451 ImGui::CloseCurrentPopup();
2452 }
2453 ImGui::SameLine();
2454 {
2455 gui::StyleColorGuard confirm_colors({
2456 {ImGuiCol_Button, theme.status_error},
2457 });
2458 if (ImGui::Button(ICON_MD_DELETE " Confirm##BulkDelConfirm")) {
2459 tile_handler.DeleteObjects(room_id, selection_copy);
2460 ImGui::CloseCurrentPopup();
2461 }
2462 }
2463 ImGui::EndPopup();
2464 }
2465 }
2466
2467 // Single-object detailed inspector
2468 if (indices.size() == 1 && room_id >= 0 && viewer.rooms()) {
2469 auto& room = (*viewer.rooms())[room_id];
2470 auto& objects = room.GetTileObjects();
2471 const size_t idx = indices.front();
2472 if (idx < objects.size()) {
2473 auto& obj = objects[idx];
2474 const std::string obj_name = zelda3::GetObjectName(obj.id_);
2475 const int subtype = zelda3::GetObjectSubtype(obj.id_);
2476 const bool uses_room_stream = zelda3::UsesRoomObjectStream(obj);
2477 int displayed_layer = obj.GetLayerValue();
2478 const int pixel_x = obj.x_ * 8;
2479 const int pixel_y = obj.y_ * 8;
2480
2481 // Name + category header
2483 " Focused Object");
2484 ImGui::TextColored(theme.text_primary, "%s", obj_name.c_str());
2485 ImGui::TextDisabled(tr("%s (Type %d) #%zu in list"),
2486 GetObjectCategory(obj.id_), subtype, idx);
2487
2488 // Property table
2489 constexpr ImGuiTableFlags kPropsFlags = ImGuiTableFlags_BordersInnerV |
2490 ImGuiTableFlags_RowBg |
2491 ImGuiTableFlags_NoPadOuterX;
2492 if (ImGui::BeginTable("##SelObjProps", 2, kPropsFlags)) {
2493 ImGui::TableSetupColumn("Prop", ImGuiTableColumnFlags_WidthFixed,
2494 56.0f);
2495 ImGui::TableSetupColumn("Val", ImGuiTableColumnFlags_WidthStretch);
2496
2497 // ID
2499 uint16_t obj_id = static_cast<uint16_t>(obj.id_ & 0x0FFF);
2500 if (auto res =
2501 gui::InputHexWordEx("##SelObjId", &obj_id, 80.0f, true);
2502 res.ShouldApply()) {
2503 obj_id &= 0x0FFF;
2504 interaction.SetObjectId(idx, static_cast<int16_t>(obj_id));
2505 }
2506 });
2507
2508 // Position
2509 gui::LayoutHelpers::PropertyRow("Pos", [&]() {
2510 int pos_x = obj.x_;
2511 int pos_y = obj.y_;
2512 ImGui::SetNextItemWidth(60);
2513 bool x_changed =
2514 ImGui::DragInt("##SelObjX", &pos_x, 0.1f, 0, 63, "X:%d");
2515 {
2516 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
2517 gui::AutoRegisterLastItem("drag_int", "selected_object_x",
2518 "Selected room object X coordinate");
2519 }
2520 ImGui::SameLine();
2521 ImGui::SetNextItemWidth(60);
2522 bool y_changed =
2523 ImGui::DragInt("##SelObjY", &pos_y, 0.1f, 0, 63, "Y:%d");
2524 {
2525 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
2526 gui::AutoRegisterLastItem("drag_int", "selected_object_y",
2527 "Selected room object Y coordinate");
2528 }
2529 if (x_changed || y_changed) {
2530 int delta_x = pos_x - obj.x_;
2531 int delta_y = pos_y - obj.y_;
2532 interaction.entity_coordinator().tile_handler().MoveObjects(
2533 room_id, {idx}, delta_x, delta_y);
2534 }
2535 });
2536
2537 uint8_t requested_size = obj.size_;
2538 if (workbench::DrawObjectSizeControls(obj, &requested_size)) {
2539 interaction.SetObjectSize(idx, requested_size);
2540 }
2541
2543 uses_room_stream ? "Stream" : "Layer", [&]() {
2544 int layer = displayed_layer;
2545 const char* stream_names[] = {"Primary", "BG2 overlay",
2546 "BG1 overlay"};
2547 const char* special_layer_names[] = {"Upper layer (BG1)",
2548 "Lower layer (BG2)"};
2549 ImGui::SetNextItemWidth(-1);
2550 if (ImGui::Combo(
2551 "##SelObjLayer", &layer,
2552 uses_room_stream ? stream_names : special_layer_names,
2553 uses_room_stream ? IM_ARRAYSIZE(stream_names)
2554 : IM_ARRAYSIZE(special_layer_names))) {
2555 const int max_layer = uses_room_stream ? 2 : 1;
2556 layer = std::clamp(layer, 0, max_layer);
2557 if (interaction.SetObjectLayer(
2558 idx,
2559 static_cast<zelda3::RoomObject::LayerType>(layer))) {
2560 displayed_layer = layer;
2561 }
2562 }
2563 });
2564 gui::LayoutHelpers::PropertyRow("Route", [&]() {
2565 ImGui::TextDisabled(
2566 "%s", uses_room_stream ? GetObjectStreamName(displayed_layer)
2567 : GetSpecialLayerName(displayed_layer));
2568 });
2569
2570 // Pixel coords (read-only info)
2571 gui::LayoutHelpers::PropertyRow("Pixel", [&]() {
2572 ImGui::TextDisabled("(%d, %d)", pixel_x, pixel_y);
2573 });
2574
2575 ImGui::EndTable();
2576 }
2577 }
2578 }
2579 }
2580
2581 // ── Entity Selection (Doors, Sprites, Items) ──
2582 if (has_entity && room_id >= 0 && viewer.rooms()) {
2583 const auto sel = interaction.GetSelectedEntity();
2584 auto& room = (*viewer.rooms())[room_id];
2586 " Entity Selection");
2587
2588 switch (sel.type) {
2589 case EntityType::Door: {
2590 const auto& doors = room.GetDoors();
2591 if (sel.index < doors.size()) {
2592 const auto& door = doors[sel.index];
2593 std::string type_name(zelda3::GetDoorTypeName(door.type));
2594 std::string dir_name(zelda3::GetDoorDirectionName(door.direction));
2595
2596 ImGui::TextColored(theme.text_primary, ICON_MD_DOOR_FRONT " %s",
2597 type_name.c_str());
2598 ImGui::TextDisabled(tr("Direction: %s Position: 0x%02X"),
2599 dir_name.c_str(), door.position);
2600
2601 auto [tile_x, tile_y] = door.GetTileCoords();
2602 auto [pixel_x, pixel_y] = door.GetPixelCoords();
2603 ImGui::TextDisabled(tr("Tile: (%d, %d) Pixel: (%d, %d)"), tile_x,
2604 tile_y, pixel_x, pixel_y);
2605 }
2606 break;
2607 }
2608 case EntityType::Sprite: {
2609 const auto& sprites = room.GetSprites();
2610 if (sel.index < sprites.size()) {
2611 const auto& sprite = sprites[sel.index];
2612 std::string sprite_name = zelda3::GetSpriteLabel(sprite.id());
2613
2614 ImGui::TextColored(theme.text_primary, ICON_MD_PERSON " %s",
2615 sprite_name.c_str());
2616 ImGui::TextDisabled(tr("ID: 0x%02X Subtype: %d Layer: %d"),
2617 sprite.id(), sprite.subtype(), sprite.layer());
2618 ImGui::TextDisabled(tr("Pos: (%d, %d) Pixel: (%d, %d)"), sprite.x(),
2619 sprite.y(), sprite.x() * 16, sprite.y() * 16);
2620
2621 // Overlord check
2622 if (sprite.subtype() == 0x07 && sprite.id() >= 0x01 &&
2623 sprite.id() <= 0x1A) {
2624 std::string overlord_name = zelda3::GetOverlordLabel(sprite.id());
2625 ImGui::TextColored(theme.text_warning_yellow,
2626 ICON_MD_STAR " Overlord: %s",
2627 overlord_name.c_str());
2628 }
2629 }
2630 break;
2631 }
2632 case EntityType::Item: {
2633 const auto& items = room.GetPotItems();
2634 if (sel.index < items.size()) {
2635 const auto& pot_item = items[sel.index];
2636 const char* item_name = GetPotItemName(pot_item.item);
2637
2638 ImGui::TextColored(theme.text_primary, ICON_MD_INVENTORY_2 " %s",
2639 item_name);
2640 ImGui::TextDisabled(tr("Item ID: 0x%02X Raw Pos: 0x%04X"),
2641 pot_item.item, pot_item.position);
2642 ImGui::TextDisabled(tr("Pixel: (%d, %d) Tile: (%d, %d)"),
2643 pot_item.GetPixelX(), pot_item.GetPixelY(),
2644 pot_item.GetTileX(), pot_item.GetTileY());
2645 }
2646 break;
2647 }
2648 default:
2649 break;
2650 }
2651 }
2652}
2653
2654} // 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)
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)
bool IsStandaloneToolOpen(WorkbenchTool tool) const
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)
std::function< bool(const std::string &) is_standalone_tool_open_)
int GetPriority() const override
Get display priority for menu ordering.
DungeonWorkbenchPitDamageControlRects pit_damage_control_rects_
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)
void DrawInspectorShelfRoom(DungeonCanvasViewer &viewer)
WindowContent * GetWorkbenchToolContent(WorkbenchTool tool) const
std::string GetEditorCategory() const override
Editor category this panel belongs to.
void DrawWorkbenchTool(DungeonCanvasViewer &viewer, WorkbenchTool tool)
void DrawSidebarHeader(float button_size, bool compact)
const char * GetWorkbenchToolShortLabel(WorkbenchTool tool) const
void DrawDungeonMapPopup(DungeonCanvasViewer &viewer)
std::unique_ptr< DungeonMapPanel > embedded_dungeon_map_
void DrawInspectorToolPanel(DungeonCanvasViewer &viewer)
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)
void DrawCanvasPane(float width, float height, DungeonCanvasViewer *primary_viewer)
std::function< void(DungeonCanvasViewer &) on_primary_canvas_drawn_)
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_
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< bool(const std::string &) open_and_focus_standalone_tool_)
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.
virtual std::string GetId() const =0
Unique identifier for this panel.
RAII scope that enables automatic widget registration.
void ApplyScaleSnapshot(const CanvasConfig &snapshot)
Definition canvas.cc:944
CanvasConfig & GetConfig()
Definition canvas.h:233
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
zelda3_bg2_effect
Background layer 2 effects.
Definition zelda.h:369
#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_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_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_CASTLE
Definition icons.h:380
#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_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_CHECK_CIRCLE
Definition icons.h:400
#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_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_INVENTORY_2
Definition icons.h:1012
#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
#define ICON_MD_ARROW_DROP_DOWN
Definition icons.h:181
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 DrawObjectSizeControls(const zelda3::RoomObject &object, uint8_t *requested_size)
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
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)
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.
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)
DungeonWorkbenchToolRequestTarget ResolveDungeonWorkbenchToolRequestTarget(bool standalone_window_open)
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 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:392
InputHexResult InputHexByteEx(const char *label, uint8_t *data, float input_width, bool no_step)
Definition input.cc:533
InputHexResult InputHexWordEx(const char *label, uint16_t *data, float input_width, bool no_step)
Definition input.cc:561
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.
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:262
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
std::function< const std::deque< int > &()> get_recent_rooms
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.