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