yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_workbench_content.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <array>
6#include <cctype>
7#include <cstdint>
8#include <cstdio>
9#include <cstring>
10#include <fstream>
11#include <sstream>
12#include <utility>
13#include <vector>
14
15#include "absl/strings/str_format.h"
40#include "app/gui/core/icons.h"
41#include "app/gui/core/input.h"
48#include "core/features.h"
49#include "core/project.h"
50#include "imgui/imgui.h"
51#include "rom/rom.h"
52#include "util/file_util.h"
62
63namespace yaze::editor {
64
65namespace {
66
67float ClampWorkbenchPaneWidth(float desired_width, float min_width,
68 float max_width) {
69 return std::clamp(desired_width, min_width, std::max(min_width, max_width));
70}
71
72constexpr float kCompactLeftSidebarMinWidth = 224.0f;
73constexpr float kCompactRightSidebarMinWidth = 272.0f;
74constexpr float kCompactLeftSidebarScale = 0.72f;
75constexpr float kCompactRightSidebarScale = 0.84f;
76float GetCompactSidebarWidth(bool right_sidebar, float min_sidebar_width) {
77 return std::max(
78 right_sidebar ? kCompactRightSidebarMinWidth
80 min_sidebar_width * (right_sidebar ? kCompactRightSidebarScale
82}
83
86 rect.visible = true;
87 const ImVec2 min = ImGui::GetItemRectMin();
88 const ImVec2 max = ImGui::GetItemRectMax();
89 rect.min_x = min.x;
90 rect.min_y = min.y;
91 rect.max_x = max.x;
92 rect.max_y = max.y;
93 return rect;
94}
95
96} // namespace
97
104
106 float total_width, float min_canvas_width, float min_sidebar_width,
107 float splitter_width, bool want_left, bool want_right) {
109 result.show_left = want_left;
110 result.show_right = want_right;
111
112 auto required_width = [&](bool left, bool right, bool compact_left,
113 bool compact_right) {
114 float required = min_canvas_width;
115 required +=
116 left ? (compact_left ? GetCompactSidebarWidth(false, min_sidebar_width)
117 : min_sidebar_width)
118 : 0.0f;
119 required +=
120 right ? (compact_right ? GetCompactSidebarWidth(true, min_sidebar_width)
121 : min_sidebar_width)
122 : 0.0f;
123 if (left) {
124 required += splitter_width;
125 }
126 if (right) {
127 required += splitter_width;
128 }
129 return required;
130 };
131
132 if (result.show_left &&
133 total_width <
134 required_width(result.show_left, result.show_right, false, false)) {
135 result.compact_left = true;
136 }
137 if (result.show_right &&
138 total_width < required_width(result.show_left, result.show_right,
139 result.compact_left, false)) {
140 result.compact_right = true;
141 }
142 // Keep the entrance selector, object browser, and room navigation available
143 // when only one pane fits. Toolbar pane toggles still allow either alone.
144 if (result.show_right &&
145 total_width < required_width(result.show_left, result.show_right,
146 result.compact_left, result.compact_right)) {
147 result.show_right = false;
148 result.compact_right = false;
149 }
150 if (result.show_left &&
151 total_width < required_width(result.show_left, result.show_right,
152 result.compact_left, result.compact_right)) {
153 result.show_left = false;
154 result.compact_left = false;
155 }
156
157 return result;
158}
159
161 float total_width, float min_canvas_width, float min_sidebar_width,
162 float splitter_width, float stored_left_width, float stored_right_width,
163 bool want_left, bool want_right) {
166 total_width, min_canvas_width, min_sidebar_width, splitter_width,
167 want_left, want_right);
168
169 const float compact_left_width =
170 GetCompactSidebarWidth(false, min_sidebar_width);
171 const float compact_right_width =
172 GetCompactSidebarWidth(true, min_sidebar_width);
173 layout.min_left_width =
174 layout.responsive.compact_left ? compact_left_width : min_sidebar_width;
175 layout.min_right_width =
176 layout.responsive.compact_right ? compact_right_width : min_sidebar_width;
177
178 float left_width = layout.responsive.show_left
179 ? (layout.responsive.compact_left ? compact_left_width
180 : stored_left_width)
181 : 0.0f;
182 float right_width =
184 ? (layout.responsive.compact_right ? compact_right_width
185 : stored_right_width)
186 : 0.0f;
187
188 const float max_left_width =
189 total_width - right_width - min_canvas_width -
190 (layout.responsive.show_left ? splitter_width : 0.0f) -
191 (layout.responsive.show_right ? splitter_width : 0.0f);
192 const float max_right_width =
193 total_width - left_width - min_canvas_width -
194 (layout.responsive.show_left ? splitter_width : 0.0f) -
195 (layout.responsive.show_right ? splitter_width : 0.0f);
196 if (layout.responsive.show_left) {
197 left_width = ClampWorkbenchPaneWidth(
198 left_width, layout.min_left_width,
199 std::max(layout.min_left_width, max_left_width));
200 } else {
201 left_width = 0.0f;
202 }
203 if (layout.responsive.show_right) {
204 right_width = ClampWorkbenchPaneWidth(
205 right_width, layout.min_right_width,
206 std::max(layout.min_right_width, max_right_width));
207 } else {
208 right_width = 0.0f;
209 }
210
211 float center_width = total_width - left_width - right_width;
212 if (layout.responsive.show_left) {
213 center_width -= splitter_width;
214 }
215 if (layout.responsive.show_right) {
216 center_width -= splitter_width;
217 }
218 if (center_width < min_canvas_width) {
219 float deficit = min_canvas_width - center_width;
220 if (layout.responsive.show_left) {
221 const float shrink =
222 std::min(deficit, left_width - layout.min_left_width);
223 left_width -= shrink;
224 deficit -= shrink;
225 }
226 if (deficit > 0.0f && layout.responsive.show_right) {
227 const float shrink =
228 std::min(deficit, right_width - layout.min_right_width);
229 right_width -= shrink;
230 deficit -= shrink;
231 }
232 center_width = std::max(
233 1.0f, total_width - left_width - right_width -
234 (layout.responsive.show_left ? splitter_width : 0.0f) -
235 (layout.responsive.show_right ? splitter_width : 0.0f));
236 }
237
238 layout.left_width = left_width;
239 layout.center_width = center_width;
240 layout.right_width = right_width;
241 return layout;
242}
243
245 DungeonRoomSelector* room_selector, int* current_room_id,
246 std::function<void(int)> on_room_selected,
247 std::function<void(int, RoomSelectionIntent)> on_room_selected_with_intent,
248 std::function<void(int)> on_save_room,
249 std::function<void()> on_save_all_rooms,
250 std::function<DungeonCanvasViewer*()> get_viewer,
251 std::function<DungeonCanvasViewer*(int)> get_compare_viewer,
252 std::function<const std::deque<int>&()> get_recent_rooms,
253 std::function<void(int)> forget_recent_room,
254 std::function<void(bool)> set_workflow_mode, Rom* rom)
255 : room_selector_(room_selector),
256 current_room_id_(current_room_id),
257 on_room_selected_(std::move(on_room_selected)),
258 on_room_selected_with_intent_(std::move(on_room_selected_with_intent)),
259 on_save_room_(std::move(on_save_room)),
260 on_save_all_rooms_(std::move(on_save_all_rooms)),
261 get_viewer_(std::move(get_viewer)),
262 get_compare_viewer_(std::move(get_compare_viewer)),
263 get_recent_rooms_(std::move(get_recent_rooms)),
264 forget_recent_room_(std::move(forget_recent_room)),
265 set_workflow_mode_(std::move(set_workflow_mode)),
266 rom_(rom) {}
267
269
271 return "dungeon.workbench";
272}
274 return "Dungeon Workbench";
275}
277 return ICON_MD_WORKSPACES;
278}
280 return "Dungeon";
281}
283 return 10;
284}
285
287 rom_ = rom;
288 room_dungeon_cache_.clear();
290}
291
293 RoomTagEditorPanel* room_tags, CustomCollisionPanel* custom_collision,
294 WaterFillPanel* water_fill, MinecartTrackEditorPanel* minecart_tracks) {
295 room_tag_panel_ = room_tags;
296 custom_collision_panel_ = custom_collision;
297 water_fill_panel_ = water_fill;
298 minecart_track_panel_ = minecart_tracks;
299}
300
302 WindowContent* object_selector, WindowContent* door_editor,
303 WindowContent* sprite_editor, WindowContent* item_editor,
304 WindowContent* room_graphics, WindowContent* palette_editor) {
305 object_selector_content_ = object_selector;
306 door_editor_content_ = door_editor;
307 sprite_editor_content_ = sprite_editor;
308 item_editor_content_ = item_editor;
309 room_graphics_content_ = room_graphics;
310 palette_editor_content_ = palette_editor;
311}
312
318
322
327
333
338
342
346
347bool DungeonWorkbenchContent::LoadProposalOverlay(const std::string& path) {
348 std::snprintf(proposal_path_, sizeof(proposal_path_), "%s", path.c_str());
349 std::ifstream file(path, std::ios::binary);
350 if (!file) {
351 proposal_error_ = absl::StrFormat("Cannot open %s", path);
352 return false;
353 }
354 std::stringstream buffer;
355 buffer << file.rdbuf();
356 auto overlay = ParseDungeonProposalOverlay(buffer.str());
357 if (!overlay.ok()) {
358 // Keep the previously loaded overlay visible; show why the new one failed.
359 proposal_error_ = std::string(overlay.status().message());
360 return false;
361 }
363 for (const auto& layer : overlay->layers) {
364 proposal_layer_visible_.push_back(layer.visible ? 1 : 0);
365 }
366 proposal_overlay_ = std::move(*overlay);
368 proposal_error_.clear();
369 return true;
370}
371
373 DungeonCanvasViewer& viewer) {
375 return;
376 }
377 ImGui::SetNextWindowSize(ImVec2(1120.0f, 700.0f), ImGuiCond_FirstUseEver);
378 if (!ImGui::Begin(ICON_MD_PREVIEW " Proposal Preview##DungeonProposalPreview",
380 ImGui::End();
381 return;
382 }
383
384 // File row.
385 util::FileDialogOptions json_options;
386 json_options.filters.push_back({"Proposal Overlay", "json"});
387 json_options.filters.push_back({"All Files", "*"});
388 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Open...")) {
389 const std::string path =
391 if (!path.empty()) {
392 (void)LoadProposalOverlay(path);
393 }
394 }
395 ImGui::SameLine();
396 ImGui::BeginDisabled(proposal_path_[0] == '\0');
397 if (ImGui::Button(ICON_MD_REFRESH " Reload")) {
399 }
400 ImGui::EndDisabled();
401 ImGui::SameLine();
402 ImGui::SetNextItemWidth(-1.0f);
403 if (ImGui::InputTextWithHint("##ProposalPath", tr("overlay .json path"),
405 ImGuiInputTextFlags_EnterReturnsTrue)) {
407 }
408 if (!proposal_error_.empty()) {
409 gui::StyleColorGuard error_color(ImGuiCol_Text,
410 ImVec4(1.0f, 0.45f, 0.4f, 1.0f));
411 ImGui::TextWrapped("%s", proposal_error_.c_str());
412 }
413
414 if (!proposal_overlay_.has_value()) {
415 ImGui::Spacing();
416 ImGui::TextWrapped(
417 "%s",
418 tr("Open a proposal overlay (.json) to render its rooms from the "
419 "loaded ROM with the proposed changes drawn on top. This window "
420 "is read-only: it never edits rooms or the ROM."));
421 ImGui::End();
422 return;
423 }
425
426 if (!overlay.title.empty()) {
427 ImGui::TextUnformatted(overlay.title.c_str());
428 }
429 if (!overlay.status.empty()) {
430 ImGui::SameLine();
431 ImGui::TextDisabled("(%s)", overlay.status.c_str());
432 }
433 ImGui::TextDisabled("%s",
434 tr("Read-only preview. Nothing here edits the ROM."));
435
436 // Layer and view controls.
437 ImGui::Checkbox(tr("Show proposal"), &proposal_show_overlay_);
438 ImGui::BeginDisabled(!proposal_show_overlay_);
439 for (size_t i = 0; i < overlay.layers.size(); ++i) {
440 const auto& layer = overlay.layers[i];
441 ImGui::SameLine();
442 const uint32_t c = layer.color_rgba;
443 ImGui::ColorButton(
444 absl::StrFormat("##ProposalLayerSwatch%d", i).c_str(),
445 ImVec4(((c >> 24) & 0xFF) / 255.0f, ((c >> 16) & 0xFF) / 255.0f,
446 ((c >> 8) & 0xFF) / 255.0f, 1.0f),
447 ImGuiColorEditFlags_NoTooltip, ImVec2(12, 12));
448 ImGui::SameLine(0, 4);
449 bool visible =
451 if (ImGui::Checkbox(
452 absl::StrFormat("%s##ProposalLayer%d", layer.label, i).c_str(),
453 &visible) &&
454 i < proposal_layer_visible_.size()) {
455 proposal_layer_visible_[i] = visible ? 1 : 0;
456 }
457 }
458 ImGui::EndDisabled();
459
460 ImGui::SetNextItemWidth(160.0f);
461 ImGui::SliderFloat(tr("Zoom"), &proposal_scale_, 0.5f, 2.0f, "%.2fx");
462 if (on_room_selected_) {
463 for (const auto& room : overlay.rooms) {
464 ImGui::SameLine();
465 if (ImGui::SmallButton(
466 absl::StrFormat("Open $%02X##ProposalOpenRoom", room.room_id)
467 .c_str())) {
468 on_room_selected_(room.room_id);
469 }
470 }
471 }
472
473 // Rooms with the overlay. Reserve space for notes below.
474 const float notes_height =
475 overlay.notes.empty()
476 ? 0.0f
477 : std::min(140.0f, ImGui::GetTextLineHeightWithSpacing() *
478 (overlay.notes.size() + 1.5f));
479 if (ImGui::BeginChild("##ProposalPreviewBody", ImVec2(0.0f, -notes_height),
480 true, ImGuiWindowFlags_HorizontalScrollbar)) {
483 }
484 ImGui::EndChild();
485
486 if (!overlay.notes.empty() &&
487 ImGui::BeginChild("##ProposalPreviewNotes", ImVec2(0.0f, 0.0f), false)) {
488 for (const auto& note : overlay.notes) {
489 ImGui::BulletText("%s", note.c_str());
490 }
491 }
492 if (!overlay.notes.empty()) {
493 ImGui::EndChild();
494 }
495 ImGui::End();
496}
497
501
505
509
513
517
521
525
529
533
537
541
545
547 switch (inspector_mode_) {
549 return "inspect";
551 return "tools";
552 }
553 return "unknown";
554}
555
559
561 switch (sidebar_mode_) {
563 return "room";
565 return "objects";
567 return "sprites";
569 return "doors";
571 return "items";
573 return "entrance";
576 }
577 return "unknown";
578}
579
580void DungeonWorkbenchContent::DrawSidebarPane(float width, float height,
581 float button_size, bool compact) {
582 const bool open = ImGui::BeginChild(
583 "##DungeonWorkbenchSidebar", ImVec2(width, height), false,
584 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
585 if (open) {
586 DrawSidebarHeader(button_size, compact);
587 const float rail_width = ImGui::GetFrameHeight() + 8.0f;
588 if (ImGui::BeginChild("##WorkbenchRail", ImVec2(rail_width, 0), false))
590 ImGui::EndChild();
591 ImGui::SameLine();
592 if (ImGui::BeginChild("##NavigatorTools", ImVec2(0, 0), false))
594 ImGui::EndChild();
595 }
596 ImGui::EndChild();
597}
598
600 bool compact) {
601 const char* label = "Room";
602 switch (sidebar_mode_) {
604 break;
606 label = "Objects";
607 break;
609 label = "Sprites";
610 break;
612 label = "Doors";
613 break;
615 label = "Items";
616 break;
618 label = "Entrance";
619 break;
622 break;
623 }
624 const float action_width =
627 ImGui::GetStyle().ItemSpacing.x;
629 "##DungeonWorkbenchSidebarHeader", "", label, label, nullptr, compact,
630 action_width, [&]() {
631 if (workbench::DrawHeaderIconAction("SidebarQuickActions",
632 ICON_MD_MORE_HORIZ, button_size,
633 "Room tools"))
634 ImGui::OpenPopup("##WorkbenchSidebarQuickActions");
635 if (ImGui::BeginPopup("##WorkbenchSidebarQuickActions")) {
636 if (ImGui::MenuItem("Dungeon overview"))
638 if (ImGui::MenuItem("Dungeon map"))
640 if (ImGui::MenuItem("Proposal Preview..."))
642 ImGui::Separator();
643 if (ImGui::MenuItem("Keyboard Shortcuts", nullptr, false,
644 static_cast<bool>(open_keyboard_shortcuts_)))
646 ImGui::EndPopup();
647 }
648 ImGui::SameLine();
649 if (workbench::DrawHeaderIconAction("CollapseRooms",
650 ICON_MD_CHEVRON_LEFT, button_size,
651 "Collapse browser"))
653 });
654}
655
657 gui::AutoWidgetScope scope("Dungeon/WorkbenchRail");
658 const float size = ImGui::GetFrameHeight() + 4.0f;
659 const auto draw = [&](const char* id, const char* icon, const char* label,
660 bool selected, auto action) {
662 ImGuiCol_Button,
663 ImGui::GetStyleColorVec4(selected ? ImGuiCol_ButtonActive
664 : ImGuiCol_Button));
665 ImGui::PushID(id);
666 ImGui::SetCursorPosX(
667 ImGui::GetCursorPosX() +
668 std::max(0.0f, (ImGui::GetContentRegionAvail().x - size) * 0.5f));
669 if (gui::CenteredIconButton(icon, size))
670 action();
671 gui::AutoRegisterLastItem("button", id, label);
672 if (selected) {
673 const ImVec2 p = ImGui::GetItemRectMin();
674 ImGui::GetWindowDrawList()->AddLine(
675 p, ImVec2(p.x, p.y + size), ImGui::GetColorU32(ImGuiCol_CheckMark),
676 2.0f);
677 }
678 if (ImGui::IsItemHovered())
679 ImGui::SetTooltip("%s", label);
680 ImGui::PopID();
681 };
682 draw("room", ICON_MD_TUNE, "Room", sidebar_mode_ == SidebarMode::Room,
683 [this]() { FocusRoomInspector(); });
684 struct Choice {
685 const char* id;
686 const char* icon;
687 const char* label;
688 WorkbenchTool tool;
689 SidebarMode mode;
690 };
691 constexpr Choice choices[] = {
692 {"objects", ICON_MD_WIDGETS, "Objects", WorkbenchTool::ObjectSelector,
700 };
701 for (const auto& choice : choices)
702 draw(choice.id, choice.icon, choice.label, sidebar_mode_ == choice.mode,
703 [this, &choice]() { OpenTool(choice.tool); });
704 ImGui::Separator();
705 draw("entrance", ICON_MD_LOGIN, "Entrance",
707 [this]() { FocusEntranceBrowser(); });
708 constexpr Choice tools[] = {
709 {"palette", ICON_MD_PALETTE, "Palette", WorkbenchTool::Palette,
711 {"minecart", ICON_MD_TRAIN, "Tracks", WorkbenchTool::MinecartTracks,
713 {"custom_collision", ICON_MD_GRID_ON, "Collision",
715 };
716 for (const auto& choice : tools)
717 draw(choice.id, choice.icon, choice.label,
718 sidebar_mode_ == SidebarMode::Tools && active_tool_ == choice.tool,
719 [this, &choice]() { OpenTool(choice.tool); });
720}
721
723 DungeonCanvasViewer* viewer = get_viewer_ ? get_viewer_() : nullptr;
724 if (!viewer) {
725 ImGui::TextDisabled("No active room");
726 return;
727 }
728 ImGui::PushID("WorkbenchSidebarMode");
730 DrawInspectorShelfRoom(*viewer);
731 } else if (sidebar_mode_ == SidebarMode::Entrance) {
733 ImGui::TextDisabled("Entrance editor unavailable");
734 } else if (is_standalone_tool_open_ &&
736 if (ImGui::Button("Focus Entrance Window") &&
739 } else {
741 ImGui::GetContentRegionAvail().x);
742 ImGui::Separator();
743 ImGui::BeginDisabled(viewer->header_read_only() ||
744 !viewer->IsObjectInteractionEnabled());
746 ImGui::EndDisabled();
747 }
748 } else if (sidebar_mode_ == SidebarMode::Tools) {
749 DrawInspectorToolPanel(*viewer);
750 } else {
751 WindowContent* content = nullptr;
752 switch (sidebar_mode_) {
754 content = object_selector_content_;
755 break;
757 content = sprite_editor_content_;
758 break;
760 content = door_editor_content_;
761 break;
763 content = item_editor_content_;
764 break;
765 default:
766 break;
767 }
768 if (!content) {
769 ImGui::TextDisabled("Browser unavailable");
770 } else if (is_standalone_tool_open_ &&
771 is_standalone_tool_open_(content->GetId())) {
772 if (ImGui::Button("Focus Window") && open_and_focus_standalone_tool_)
774 } else if (auto* selector = dynamic_cast<ObjectSelectorContent*>(content)) {
775 selector->DrawInWorkbench();
776 } else if (auto* sprites = dynamic_cast<SpriteEditorPanel*>(content)) {
777 sprites->DrawInWorkbench();
778 } else if (auto* doors = dynamic_cast<DoorEditorContent*>(content)) {
779 doors->DrawInWorkbench();
780 } else if (auto* items = dynamic_cast<ItemEditorPanel*>(content)) {
781 items->DrawInWorkbench();
782 } else {
783 content->Draw(nullptr);
784 }
785 }
786 ImGui::PopID();
787}
788
790 (void)p_open;
791 const auto& theme = AgentUI::GetTheme();
792
793 if (!rom_ || !rom_->is_loaded()) {
794 ImGui::TextDisabled(ICON_MD_INFO " Load a ROM to edit dungeon rooms.");
795 return;
796 }
798 ImGui::TextColored(theme.text_error_red, tr("Dungeon Workbench not wired"));
799 return;
800 }
801
802 DungeonCanvasViewer* primary_viewer = get_viewer_ ? get_viewer_() : nullptr;
803 DungeonCanvasViewer* compare_viewer =
805 const float splitter_w = gui::UIConfig::kSplitterWidth;
806 const float total_w = std::max(ImGui::GetContentRegionAvail().x, 1.0f);
807 // Relaxed from 320 px to 260 px so the Inspect pane can breathe at widths
808 // where the previous floor forced a too-wide right rail. Room browser on the
809 // left also benefits since its matrix cells scale down gracefully.
810 const float min_sidebar_w =
812 const float min_canvas_w = std::max(420.0f, min_sidebar_w + 96.0f);
813 if (current_room_id_) {
815 params.layout = &layout_state_;
820 params.primary_viewer = primary_viewer;
821 params.compare_viewer = compare_viewer;
825 params.on_open_room_panel = [this](int room_id) {
828 };
829 }
833 params.draw_tools = [this]() {
835 };
836 params.draw_save_content = [this]() {
838 };
839 params.draw_layout_options = [this]() {
841 };
842 params.on_request_dungeon_map = [this]() {
844 };
847 params.draw_entrance_context = [this](float width) {
850 };
851 const bool request_panel_workflow = DungeonWorkbenchToolbar::Draw(params);
852 if (request_panel_workflow && set_workflow_mode_) {
853 // Defer panel visibility mutation until toolbar child/table scopes closed.
854 set_workflow_mode_(false);
855 return;
856 }
857 }
858
859 if (primary_viewer && layout_state_.show_room_context_row) {
860 DrawRoomContextRow(*primary_viewer);
861 }
862
863 // Display controls can change widths or visibility in this frame.
864 const DungeonWorkbenchPaneLayout pane_layout =
866 total_w, min_canvas_w, min_sidebar_w, splitter_w,
869 const bool show_left = pane_layout.responsive.show_left;
870 const bool show_right = pane_layout.responsive.show_right;
871
873 const float total_h = std::max(ImGui::GetContentRegionAvail().y, 1.0f);
874 const float left_w = pane_layout.left_width;
875 const float right_w = pane_layout.right_width;
876 const float center_w = pane_layout.center_width;
877 if (show_left && !pane_layout.responsive.compact_left) {
878 layout_state_.left_width = left_w;
879 }
880 if (show_right && !pane_layout.responsive.compact_right) {
881 layout_state_.right_width = right_w;
882 }
883
884 if (show_left) {
885 DrawSidebarPane(left_w, total_h, btn, pane_layout.responsive.compact_left);
886 ImGui::SameLine(0, 0);
888 "##DungeonWorkbenchLeftSplitter", total_h,
890 total_w - right_w - min_canvas_w - (show_right ? splitter_w : 0),
891 false))
893 ImGui::SameLine(0, 0);
894 }
895 DrawCanvasPane(center_w, total_h, primary_viewer);
896 if (show_right) {
897 ImGui::SameLine(0, 0);
899 "##DungeonWorkbenchRightSplitter", total_h,
901 total_w - left_w - min_canvas_w - (show_left ? splitter_w : 0),
902 true))
904 ImGui::SameLine(0, 0);
905 DrawInspectorPane(right_w, total_h, btn,
906 pane_layout.responsive.compact_right, primary_viewer);
907 }
908 if (primary_viewer) {
909 DrawDungeonMapPopup(*primary_viewer);
910 primary_viewer->DrawChestEditorPopup();
911 }
912}
913
915 gui::AutoWidgetScope automation_scope("Dungeon/WorkbenchLayout");
916 ImGui::TextUnformatted(tr("Workbench layout"));
917 ImGui::Checkbox(tr("Room properties above panes"),
919 ImGui::Checkbox(tr("Nearby rooms in spare canvas space"),
921 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 16.0f);
922 ImGui::SliderFloat(tr("Browser width"), &layout_state_.left_width, 224.0f,
923 600.0f, "%.0f px");
924 gui::AutoRegisterLastItem("slider", "Browser width");
925 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 16.0f);
926 ImGui::SliderFloat(tr("Inspector width"), &layout_state_.right_width, 272.0f,
927 600.0f, "%.0f px");
928 gui::AutoRegisterLastItem("slider", "Inspector width");
929 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 16.0f);
930 ImGui::SliderFloat(tr("Selection / matrix split"), &navigator_tools_fraction_,
931 0.20f, 0.65f, "%.2f");
932 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 16.0f);
933 ImGui::SliderFloat(tr("Nearby preview height"),
934 &layout_state_.nearby_rooms_height, 80.0f, 180.0f,
935 "%.0f px");
936 if (ImGui::Button(tr("Reset workbench layout"))) {
937 layout_state_ = {};
939 }
940 ImGui::TextDisabled(tr("Layout changes apply immediately for this session."));
941}
942
944 float width, float height, DungeonCanvasViewer* primary_viewer) {
945 const bool canvas_open = ImGui::BeginChild(
946 "##DungeonWorkbenchCanvas", ImVec2(width, height), false,
947 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
948 if (canvas_open) {
949 if (primary_viewer) {
950 // Reserve a fixed strip at the bottom for the status bar so neither
951 // single-room, split, nor connected-mode rendering can run past it and
952 // clip against the outer window chrome. Inner scrolling (wheel zoom in
953 // connected mode, canvas pan in single-room) happens inside this body
954 // child — the outer ##DungeonWorkbenchCanvas never needs a scrollbar.
955 const float status_bar_reserved =
956 std::max(
957 ImGui::GetFontSize() + ImGui::GetStyle().FramePadding.y * 2.0f,
959 ImGui::GetStyle().ItemSpacing.y;
960
961 const bool connected_mode = layout_state_.show_connected_canvas_view;
962 const ImVec2 body_available = ImGui::GetContentRegionAvail();
963 const float nearby_height =
964 layout_state_.show_nearby_rooms && !connected_mode &&
967 body_available.x - ImGui::GetStyle().WindowPadding.x * 2,
968 body_available.y - status_bar_reserved -
969 ImGui::GetStyle().WindowPadding.y * 2,
971 ImGui::GetStyle().ItemSpacing.y)
972 : 0.0f;
973 const float nearby_reserved =
974 nearby_height > 0.0f ? nearby_height + ImGui::GetStyle().ItemSpacing.y
975 : 0.0f;
976 ImGuiWindowFlags body_flags =
977 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
978 if (connected_mode) {
979 // Body child owns the connected-mode scroll container; matrix no
980 // longer nests its own BeginChild for scrolling.
981 const ImVec2 content_size =
983 const float scale = primary_viewer->ConnectedCanvasScale();
984 ImGui::SetNextWindowContentSize(
985 ImVec2(content_size.x * scale, content_size.y * scale));
986 body_flags = ImGuiWindowFlags_HorizontalScrollbar |
987 ImGuiWindowFlags_NoScrollWithMouse;
988 }
989 const bool body_open = ImGui::BeginChild(
990 "##DungeonCanvasBody",
991 ImVec2(0.0f, -status_bar_reserved - nearby_reserved), false,
992 body_flags);
993 if (body_open) {
994 if (connected_mode) {
995 split_view_enabled_ = false;
996 if (primary_viewer->DrawConnectedRoomMatrix(*current_room_id_)
997 .has_value()) {
999 }
1000 } else if (split_view_enabled_) {
1001 DrawSplitView(*primary_viewer);
1002 } else {
1003 primary_viewer->DrawDungeonCanvas(*current_room_id_);
1004 }
1006 on_primary_canvas_drawn_(*primary_viewer);
1007 }
1008 }
1009 ImGui::EndChild();
1010
1011 if (nearby_height > 0.0f) {
1012 const float preview_y = ImGui::GetCursorPosY();
1014 nearby_height);
1015 if (ImGui::GetCursorPosY() == preview_y) {
1016 ImGui::Dummy(ImVec2(0.0f, nearby_height));
1017 }
1018 }
1019
1020 const char* tool_mode =
1021 primary_viewer->object_interaction().mode_manager().GetModeName();
1022 bool room_dirty = false;
1023 if (auto* rooms = primary_viewer->rooms();
1024 rooms && current_room_id_ && *current_room_id_ >= 0) {
1025 if (const auto* room = rooms->GetIfMaterialized(*current_room_id_)) {
1026 room_dirty = room->HasUnsavedChanges();
1027 }
1028 }
1029 auto status =
1030 DungeonStatusBar::BuildState(*primary_viewer, tool_mode, room_dirty);
1031 status.workflow_mode = layout_state_.show_connected_canvas_view
1033 : nullptr;
1034 status.workflow_primary = true;
1035 if (can_undo_)
1036 status.can_undo = can_undo_();
1037 if (can_redo_)
1038 status.can_redo = can_redo_();
1039 if (undo_desc_) {
1040 static std::string s_undo_desc;
1041 s_undo_desc = undo_desc_();
1042 status.undo_desc = s_undo_desc.empty() ? nullptr : s_undo_desc.c_str();
1043 }
1044 if (redo_desc_) {
1045 static std::string s_redo_desc;
1046 s_redo_desc = redo_desc_();
1047 status.redo_desc = s_redo_desc.empty() ? nullptr : s_redo_desc.c_str();
1048 }
1049 if (undo_depth_)
1050 status.undo_depth = undo_depth_();
1051 status.on_undo = on_undo_;
1052 status.on_redo = on_redo_;
1054 status.on_selection = [this]() {
1056 };
1057 }
1058 DungeonStatusBar::Draw(status);
1059 } else {
1060 ImGui::TextDisabled(tr("No active viewer"));
1061 }
1062 }
1063 ImGui::EndChild();
1064 // Floating, independent window; drawn here so it stays available while the
1065 // navigation sidebar is collapsed.
1066 if (primary_viewer) {
1067 DrawProposalPreviewWindow(*primary_viewer);
1068 }
1069}
1070
1071void DungeonWorkbenchContent::DrawInspectorPane(float width, float height,
1072 float button_size, bool compact,
1073 DungeonCanvasViewer* viewer) {
1074 const bool open = ImGui::BeginChild(
1075 "##DungeonWorkbenchInspector", ImVec2(width, height), true,
1076 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
1077 if (open) {
1078 DrawInspectorHeader(button_size, compact);
1079 const float available = std::max(1.0f, ImGui::GetContentRegionAvail().y);
1080 const float spacing = ImGui::GetStyle().ItemSpacing.y;
1081 const float toggle_height = ImGui::GetFrameHeight();
1082 const float selection_height =
1084 ? std::max(1.0f, available * navigator_tools_fraction_)
1085 : std::max(1.0f, available - toggle_height - 2.0f * spacing);
1086 if (ImGui::BeginChild("##WorkbenchSelection", ImVec2(0, selection_height),
1087 false)) {
1088 if (viewer)
1089 DrawInspector(*viewer, compact);
1090 else
1091 ImGui::TextDisabled("No active room");
1092 }
1093 ImGui::EndChild();
1094 if (!show_room_matrix_) {
1095 if (ImGui::Button(ICON_MD_GRID_VIEW " Show Room Matrix",
1096 ImVec2(-1, toggle_height)))
1097 show_room_matrix_ = true;
1098 } else {
1099 const ImVec2 divider = ImGui::GetCursorScreenPos();
1100 ImGui::InvisibleButton("##NavigatorSplit", ImVec2(-1, 6));
1101 if (ImGui::IsItemHovered() || ImGui::IsItemActive())
1102 ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
1103 if (ImGui::IsItemActive())
1104 navigator_tools_fraction_ = std::clamp(
1105 navigator_tools_fraction_ + ImGui::GetIO().MouseDelta.y / available,
1106 .20f, .65f);
1107 ImGui::GetWindowDrawList()->AddLine(
1108 ImVec2(divider.x, divider.y + 3),
1109 ImVec2(ImGui::GetItemRectMax().x, divider.y + 3),
1110 ImGui::GetColorU32(ImGuiCol_Separator));
1111 if (ImGui::SmallButton(ICON_MD_EXPAND_MORE "##CollapseMatrix"))
1112 show_room_matrix_ = false;
1113 ImGui::SameLine();
1114 ImGui::TextDisabled("Room Matrix");
1115 if (ImGui::BeginChild("##NavigatorMatrix", ImVec2(0, 0), false,
1116 ImGuiWindowFlags_NoScrollbar |
1117 ImGuiWindowFlags_NoScrollWithMouse)) {
1119 is_standalone_tool_open_("dungeon.room_matrix")) {
1120 if (ImGui::Button("Focus Matrix Window") &&
1122 open_and_focus_standalone_tool_("dungeon.room_matrix");
1123 } else if (room_matrix_content_) {
1125 } else {
1126 ImGui::TextDisabled("Room Matrix unavailable");
1127 }
1128 }
1129 ImGui::EndChild();
1130 }
1131 }
1132 ImGui::EndChild();
1133}
1134
1136 bool compact) {
1138 "##DungeonWorkbenchInspectorHeader", ICON_MD_SELECT_ALL, "Selection",
1139 "Selection", nullptr, compact,
1141 [&]() {
1143 "CollapseInspector", ICON_MD_CHEVRON_RIGHT, button_size,
1144 "Collapse selection and room matrix"))
1146 });
1147}
1148
1150 DungeonCanvasViewer& primary_viewer) {
1152 if (split_view_enabled_) {
1153 split_view_enabled_ = false;
1154 }
1155 return;
1156 }
1157
1158 // Choose a sensible default compare room (most-recent non-current).
1160 if (get_recent_rooms_) {
1161 for (int rid : get_recent_rooms_()) {
1162 if (rid != *current_room_id_) {
1163 compare_room_id_ = rid;
1164 break;
1165 }
1166 }
1167 }
1168 }
1169
1170 if (compare_room_id_ < 0) {
1171 // Nothing to compare yet.
1172 split_view_enabled_ = false;
1173 primary_viewer.DrawDungeonCanvas(*current_room_id_);
1174 return;
1175 }
1176
1177 constexpr ImGuiTableFlags kSplitFlags =
1178 ImGuiTableFlags_Resizable | ImGuiTableFlags_NoPadOuterX |
1179 ImGuiTableFlags_NoPadInnerX | ImGuiTableFlags_BordersInnerV;
1180
1181 if (!ImGui::BeginTable("##DungeonWorkbenchSplit", 2, kSplitFlags)) {
1182 primary_viewer.DrawDungeonCanvas(*current_room_id_);
1183 return;
1184 }
1185
1186 ImGui::TableSetupColumn("Active", ImGuiTableColumnFlags_WidthStretch);
1187 ImGui::TableSetupColumn("Compare", ImGuiTableColumnFlags_WidthStretch);
1188 ImGui::TableNextRow();
1189
1190 DungeonCanvasViewer* compare_viewer =
1192
1193 // Active pane (minimum height so canvas never collapses)
1194 ImGui::TableNextColumn();
1195 ImGui::AlignTextToFramePadding();
1196 const project::YazeProject* active_project = primary_viewer.project();
1197 ImGui::TextDisabled(
1198 ICON_MD_CROP_FREE " Active [%03X] %s", *current_room_id_,
1200 .c_str());
1201 ImGui::Separator();
1202 const bool split_active_open = gui::LayoutHelpers::BeginContentChild(
1203 "##SplitActive", ImVec2(1.0f, 1.0f), false,
1204 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
1205 if (split_active_open) {
1206 primary_viewer.DrawDungeonCanvas(*current_room_id_);
1207 }
1209
1210 // Compare pane
1211 ImGui::TableNextColumn();
1212 ImGui::AlignTextToFramePadding();
1213 const project::YazeProject* compare_project =
1214 compare_viewer ? compare_viewer->project() : active_project;
1215 ImGui::TextDisabled(
1216 ICON_MD_COMPARE_ARROWS " Compare [%03X] %s", compare_room_id_,
1218 .c_str());
1219 ImGui::Separator();
1220 const bool split_compare_open = gui::LayoutHelpers::BeginContentChild(
1221 "##SplitCompare", ImVec2(1.0f, 1.0f), false,
1222 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
1223 if (split_compare_open) {
1224 if (compare_viewer) {
1226 compare_viewer->canvas().ApplyScaleSnapshot(
1227 primary_viewer.canvas().GetConfig());
1228 }
1229 compare_viewer->DrawDungeonCanvas(compare_room_id_);
1230 } else {
1231 ImGui::TextDisabled(tr("No compare viewer"));
1232 }
1233 }
1235
1236 ImGui::EndTable();
1237}
1238
1240 room_dungeon_cache_.clear();
1241 room_dungeon_cache_built_ = true; // Always set, even if ROM missing.
1242 if (!rom_ || !rom_->is_loaded() ||
1244 return;
1245
1246 // Short dungeon names for display in the inspector badge.
1247 // Indices 0-13 = vanilla ALTTP dungeons; higher indices = custom/Oracle.
1248 static const char* const kShortNames[] = {
1249 "Sewers", "HC", "Eastern", "Desert", "A-Tower", "Swamp", "PoD",
1250 "Misery", "Skull", "Ice", "Hera", "Thieves", "Turtle", "GT",
1251 };
1252 constexpr int kVanillaCount =
1253 static_cast<int>(sizeof(kShortNames) / sizeof(kShortNames[0]));
1254
1255 auto AddRoom = [&](int room_id, int dungeon_id) {
1256 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms)
1257 return;
1258 if (room_dungeon_cache_.contains(room_id))
1259 return; // Entrance wins over spawn.
1260 if (dungeon_id >= 0 && dungeon_id < kVanillaCount) {
1261 room_dungeon_cache_[room_id] = kShortNames[dungeon_id];
1262 } else {
1263 char buf[16];
1264 snprintf(buf, sizeof(buf), "Dungeon %02X", dungeon_id);
1265 room_dungeon_cache_[room_id] = buf;
1266 }
1267 };
1268
1269 // Standard entrances — authoritative dungeon assignment.
1270 for (int i = 0; i < zelda3::kNumRegularDungeonEntrances; ++i) {
1271 zelda3::RoomEntrance ent(rom_, static_cast<uint8_t>(i), false);
1272 if (!ent.load_status().ok() || ent.room_ < 0 ||
1274 continue;
1275 int did = ent.dungeon_id_;
1276 if (did >= 0 && did < kVanillaCount) {
1277 room_dungeon_cache_[ent.room_] = kShortNames[did];
1278 } else {
1279 char buf[16];
1280 snprintf(buf, sizeof(buf), "Dungeon %02X", did);
1281 room_dungeon_cache_[ent.room_] = buf;
1282 }
1283 }
1284
1285 // Spawn points — fill in rooms not covered by entrances.
1286 for (int i = 0; i < zelda3::kNumDungeonSpawnPoints; ++i) {
1287 zelda3::RoomEntrance ent(rom_, static_cast<uint8_t>(i), true);
1288 if (ent.load_status().ok())
1289 AddRoom(static_cast<int>(ent.room_), static_cast<int>(ent.dungeon_id_));
1290 }
1291}
1292
1294 bool compact) {
1295 // Gentle compaction only; inspector sections own their own internal
1296 // spacing (collapsing-header headers, action buttons), so let the theme
1297 // defaults shape inter-section breathing room here.
1298 gui::StyleVarGuard item_spacing_guard(
1299 ImGuiStyleVar_ItemSpacing,
1300 ImVec2(std::max(ImGui::GetStyle().ItemSpacing.x, 4.0f),
1301 std::max(4.0f, ImGui::GetStyle().ItemSpacing.y - 1.0f)));
1302 DrawInspectorShelf(viewer, compact);
1303}
1304
1306 auto& flags = core::FeatureFlags::get().dungeon;
1307 flags.kSaveObjects = value;
1308 flags.kSaveSprites = value;
1309 flags.kSaveRoomHeaders = value;
1310 flags.kSaveChests = value;
1311 flags.kSavePotItems = value;
1312 flags.kSaveEntrances = value;
1313 flags.kSavePalettes = value;
1314 flags.kSaveCollision = value;
1315 flags.kSaveBlocks = value;
1316 flags.kSaveTorches = value;
1317 flags.kSavePits = value;
1318}
1319
1321 auto& flags = core::FeatureFlags::get().dungeon;
1322 ImGui::TextUnformatted(tr("Room content to apply"));
1323 constexpr ImGuiTableFlags kFlags =
1324 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
1325 if (ImGui::BeginTable("##WorkbenchSaveContentFlags", 2, kFlags,
1326 ImVec2(320.0f, 0.0f))) {
1327 auto draw_checkbox = [](const char* label, bool* value) {
1328 ImGui::TableNextColumn();
1329 ImGui::Checkbox(label, value);
1330 };
1331 ImGui::TableNextRow();
1332 draw_checkbox("Room objects", &flags.kSaveObjects);
1333 draw_checkbox("Room sprites", &flags.kSaveSprites);
1334 ImGui::TableNextRow();
1335 draw_checkbox("Room properties", &flags.kSaveRoomHeaders);
1336 draw_checkbox("Chest contents", &flags.kSaveChests);
1337 ImGui::TableNextRow();
1338 draw_checkbox("Items under pots", &flags.kSavePotItems);
1339 draw_checkbox("Palettes", &flags.kSavePalettes);
1340 ImGui::TableNextRow();
1341 draw_checkbox("Custom collision", &flags.kSaveCollision);
1342 ImGui::TableNextColumn();
1343 ImGui::BeginDisabled();
1344 ImGui::Checkbox("Water Fill", &flags.kSaveWaterFillZones);
1345 ImGui::EndDisabled();
1346 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1347 ImGui::SetTooltip(
1348 "Water Fill saving is fixed when the project opens. Change "
1349 "save_dungeon_water_fill_zones in the .yaze file, then reopen the "
1350 "project.");
1351 }
1352 ImGui::TableNextRow();
1353 draw_checkbox("Push blocks", &flags.kSaveBlocks);
1354 draw_checkbox("Torches", &flags.kSaveTorches);
1355 ImGui::TableNextRow();
1356 draw_checkbox("Pit damage", &flags.kSavePits);
1357 draw_checkbox("Entrances", &flags.kSaveEntrances);
1358 ImGui::EndTable();
1359 }
1360
1361 if (ImGui::SmallButton(tr("Select all##WorkbenchSaveContent"))) {
1362 SetAllSaveFlags(true);
1363 }
1364 ImGui::SameLine();
1365 if (ImGui::SmallButton(tr("Select none##WorkbenchSaveContent"))) {
1366 SetAllSaveFlags(false);
1367 }
1368
1369 ImGui::Separator();
1370 const auto apply_room_label =
1371 absl::StrFormat(ICON_MD_SAVE " Apply room 0x%03X", room_id);
1372 if (on_save_room_ && room_id >= 0 &&
1373 workbench::DrawActionButton(apply_room_label.c_str(), ImVec2(-1, 0))) {
1374 on_save_room_(room_id);
1375 }
1376 if (on_save_all_rooms_ &&
1377 workbench::DrawActionButton(ICON_MD_SAVE_ALT " Apply all loaded rooms",
1378 ImVec2(-1, 0))) {
1380 }
1381 ImGui::TextWrapped(
1382 tr("Apply uses the checked content. File > Save ROM "
1383 "writes the applied changes to disk."));
1384}
1385
1386void DungeonWorkbenchContent::DrawPitDamageControls(int room_id, bool compact) {
1388 if (pit_damage_chooser_room_id_ >= 0 &&
1389 pit_damage_chooser_room_id_ != room_id) {
1390 if (ImGui::BeginPopup("##PitDamageSlotChooser")) {
1391 ImGui::CloseCurrentPopup();
1392 ImGui::EndPopup();
1393 }
1395 }
1396 const auto& theme = AgentUI::GetTheme();
1397 zelda3::PitDamageTable* table =
1399 if (!table) {
1400 bool enabled = false;
1401 ImGui::BeginDisabled();
1402 ImGui::Checkbox(
1403 compact ? "##PitDamageEnabled" : tr("Pit damage##PitDamageEnabled"),
1404 &enabled);
1405 ImGui::EndDisabled();
1406 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1407 ImGui::SetTooltip(tr("Available after room data loads."));
1408 }
1409 return;
1410 }
1411 const auto membership = BuildPitDamageMembershipState(
1412 table, room_id, pit_damage_replacement_room_id_,
1414 if (!membership.room_valid) {
1415 ImGui::TextDisabled(tr("No valid room selected."));
1416 return;
1417 }
1418
1419 const uint16_t current_room = membership.room_id;
1420 bool room_deals_damage = membership.deals_damage;
1421 if (ImGui::Checkbox(
1422 compact ? "##PitDamageEnabled" : tr("Pit damage##PitDamageEnabled"),
1423 &room_deals_damage)) {
1425 ImGui::OpenPopup("##PitDamageSlotChooser");
1426 }
1427 {
1428 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
1429 gui::AutoRegisterLastItem("checkbox", "PitDamageEnabled");
1430 }
1431 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
1432 ImGui::BeginTooltip();
1433 ImGui::TextUnformatted(
1434 "Choose which room gives up or receives a pit-damage slot.");
1435 if (membership.dirty) {
1436 ImGui::TextColored(theme.status_warning, "Pending pit damage change");
1437 }
1438 if (!pit_damage_status_message_.empty()) {
1439 ImGui::TextWrapped("%s", pit_damage_status_message_.c_str());
1440 }
1441 ImGui::EndTooltip();
1442 }
1443
1444 auto show_status = [&]() {
1445 if (pit_damage_status_message_.empty()) {
1446 return;
1447 }
1448 ImGui::PushTextWrapPos(0.0f);
1449 ImGui::TextColored(
1450 pit_damage_status_error_ ? theme.status_error : theme.status_success,
1451 "%s %s",
1454 ImGui::PopTextWrapPos();
1455 };
1456
1457 if (!ImGui::BeginPopup("##PitDamageSlotChooser")) {
1458 return;
1459 }
1460
1461 // Toggling requests a membership change. Only the explicit confirmation
1462 // transfers a fixed-capacity slot; closing the chooser leaves it untouched.
1463 if (membership.deals_damage) {
1464 if (!membership.suggested_replacement_room.has_value()) {
1465 ImGui::PushTextWrapPos(0.0f);
1466 ImGui::TextDisabled(tr("No available non-damaging room slot."));
1467 ImGui::PopTextWrapPos();
1468 show_status();
1469 ImGui::EndPopup();
1470 return;
1471 }
1472 pit_damage_replacement_room_id_ = *membership.suggested_replacement_room;
1473
1474 ImGui::TextUnformatted(tr("Move pit damage to room:"));
1475 ImGui::SetNextItemWidth(88.0f);
1476 auto edit_value = pit_damage_replacement_room_id_;
1477 if (auto res = gui::InputHexWordEx("##PitDamageReplacementRoom",
1478 &edit_value, 88.0f, true);
1479 res.ShouldApply()) {
1481 std::min<uint16_t>(edit_value, zelda3::kNumberOfRooms - 1);
1482 }
1483 if (ImGui::IsItemHovered()) {
1484 ImGui::SetTooltip(
1485 tr("Replacement room must not already be in the table."));
1486 }
1487 const bool replace_clicked =
1488 ImGui::Button(tr("Disable pit damage##PitDamageReplaceCurrent"));
1489 pit_damage_control_rects_.replace_current = CaptureLastItemRectForTesting();
1490 if (replace_clicked) {
1491 auto status = RemoveCurrentRoomFromPitDamage(
1492 table, current_room, pit_damage_replacement_room_id_);
1493 pit_damage_status_error_ = !status.ok();
1495 status.ok()
1496 ? absl::StrFormat("Room 0x%03X removed; slot now 0x%03X",
1497 current_room, pit_damage_replacement_room_id_)
1498 : std::string(status.message());
1499 if (status.ok()) {
1502 }
1504 ImGui::CloseCurrentPopup();
1505 }
1506 }
1507 } else {
1508 if (!membership.suggested_victim_room.has_value()) {
1509 ImGui::PushTextWrapPos(0.0f);
1510 ImGui::TextDisabled(tr("No existing pit-damage slot is available."));
1511 ImGui::PopTextWrapPos();
1512 show_status();
1513 ImGui::EndPopup();
1514 return;
1515 }
1516 pit_damage_victim_room_id_ = *membership.suggested_victim_room;
1517
1518 ImGui::TextUnformatted(tr("Take pit damage from room:"));
1519 ImGui::SetNextItemWidth(88.0f);
1520 auto edit_value = pit_damage_victim_room_id_;
1521 if (auto res = gui::InputHexWordEx("##PitDamageVictimRoom", &edit_value,
1522 88.0f, true);
1523 res.ShouldApply()) {
1525 std::min<uint16_t>(edit_value, zelda3::kNumberOfRooms - 1);
1526 }
1527 if (ImGui::IsItemHovered()) {
1528 ImGui::SetTooltip(tr("This room will stop dealing pit damage."));
1529 }
1530 const bool add_clicked =
1531 ImGui::Button(tr("Enable pit damage##PitDamageAddCurrent"));
1532 pit_damage_control_rects_.add_current = CaptureLastItemRectForTesting();
1533 if (add_clicked) {
1534 auto status = AddCurrentRoomToPitDamage(table, current_room,
1536 pit_damage_status_error_ = !status.ok();
1538 status.ok()
1539 ? absl::StrFormat("Room 0x%03X added; replaced 0x%03X",
1540 current_room, pit_damage_victim_room_id_)
1541 : std::string(status.message());
1542 if (status.ok()) {
1545 }
1547 ImGui::CloseCurrentPopup();
1548 }
1549 }
1550 }
1551
1552 ImGui::SameLine();
1553 if (ImGui::Button(tr("Cancel##PitDamageCancel"))) {
1555 ImGui::CloseCurrentPopup();
1556 }
1557 {
1558 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
1559 gui::AutoRegisterLastItem("button", "PitDamageCancel");
1560 }
1561 show_status();
1562 ImGui::EndPopup();
1563}
1564
1566 DungeonCanvasViewer& viewer) {
1568 return nullptr;
1569 }
1570 if (!embedded_dungeon_map_) {
1571 embedded_dungeon_map_ = std::make_unique<DungeonMapPanel>(
1573 on_room_selected_, viewer.rooms());
1575 if (*current_room_id_ >= 0) {
1577 }
1578 }
1579 embedded_dungeon_map_->SetRooms(viewer.rooms());
1580 embedded_dungeon_map_->SetRenderContextCallback(prepare_room_render_context_);
1581 if (const auto* project = viewer.project()) {
1582 embedded_dungeon_map_->SetHackManifest(&project->hack_manifest);
1583 } else {
1584 embedded_dungeon_map_->SetHackManifest(nullptr);
1585 }
1586 return embedded_dungeon_map_.get();
1587}
1588
1590 constexpr const char* kPopupId = "Dungeon Map##WorkbenchDungeonMapPopup";
1592 const ImVec2 viewport = ImGui::GetMainViewport()->WorkSize;
1593 ImGui::SetNextWindowSize(ImVec2(std::min(1040.0f, viewport.x * 0.85f),
1594 std::min(780.0f, viewport.y * 0.85f)),
1595 ImGuiCond_Appearing);
1596 ImGui::OpenPopup(kPopupId);
1598 }
1599
1600 bool popup_open = true;
1601 if (ImGui::BeginPopupModal(kPopupId, &popup_open,
1602 ImGuiWindowFlags_NoSavedSettings)) {
1603 if (!popup_open) {
1604 ImGui::CloseCurrentPopup();
1605 ImGui::EndPopup();
1606 return;
1607 }
1608 if (auto* map = GetEmbeddedDungeonMap(viewer)) {
1609 const ImVec2 map_size =
1610 ImVec2(std::max(1.0f, ImGui::GetContentRegionAvail().x),
1611 std::max(1.0f, ImGui::GetContentRegionAvail().y -
1612 ImGui::GetFrameHeightWithSpacing()));
1613 if (ImGui::BeginChild("##WorkbenchDungeonMapBody", map_size, false)) {
1614 map->Draw(nullptr);
1615 }
1616 ImGui::EndChild();
1617 } else {
1618 ImGui::TextDisabled(tr("Dungeon map unavailable"));
1619 }
1620
1621 if (workbench::DrawActionButton(ICON_MD_CLOSE " Close", ImVec2(-1, 0))) {
1622 ImGui::CloseCurrentPopup();
1623 }
1624 ImGui::EndPopup();
1625 }
1626}
1627
1629 if (tool == WorkbenchTool::None) {
1630 return;
1631 }
1632 active_tool_ = tool;
1638 }
1639
1642 // A pinned or explicitly popped-out tool remains the presentation owner.
1643 // Re-open/focus it, and never draw the same WindowContent twice.
1646 if (WindowContent* content = GetWorkbenchToolContent(tool)) {
1647 (void)open_and_focus_standalone_tool_(content->GetId());
1648 }
1649 }
1650 return;
1651 }
1652
1653 if (tool == WorkbenchTool::ObjectSelector ||
1663 return;
1664 }
1665
1669}
1670
1701
1703 WorkbenchTool tool) const {
1704 return GetWorkbenchToolContent(tool) != nullptr;
1705}
1706
1708 WindowContent* content = GetWorkbenchToolContent(tool);
1709 if (!content || !is_standalone_tool_open_) {
1710 return false;
1711 }
1712 const std::string standalone_id = content->GetId();
1713 return !standalone_id.empty() && is_standalone_tool_open_(standalone_id);
1714}
1715
1722 }
1723 if (get_viewer_) {
1724 if (auto* viewer = get_viewer_()) {
1725 viewer->object_interaction().HandleMouseRelease();
1726 viewer->object_interaction().CancelPlacement();
1727 viewer->object_interaction().mode_manager().CancelCurrentMode();
1728 }
1729 }
1730 }
1734}
1735
1738 if (!content || !open_and_focus_standalone_tool_) {
1739 return false;
1740 }
1741
1742 const std::string standalone_id = content->GetId();
1743 if (standalone_id.empty() ||
1744 !open_and_focus_standalone_tool_(standalone_id)) {
1745 return false;
1746 }
1747
1749 return true;
1750}
1751
1753 WorkbenchTool tool) const {
1754 switch (tool) {
1756 return "room_tags";
1758 return "custom_collision";
1760 return "water_fill";
1762 return "minecart";
1764 return "object_coverage";
1766 return "object_selector";
1768 return "door";
1770 return "sprite";
1772 return "item";
1774 return "room_graphics";
1776 return "palette";
1778 return "none";
1779 }
1780 return "unknown";
1781}
1782
1784 WorkbenchTool tool) const {
1785 switch (tool) {
1787 return "Room Tags";
1789 return "Custom Collision";
1791 return "Water Fill";
1793 return "Minecart Tracks";
1795 return "Object Coverage";
1797 return "Object Selector";
1799 return "Door Tools";
1801 return "Sprite Tools";
1803 return "Item Tools";
1805 return "Room Graphics";
1807 return "Palette";
1809 return "Tool";
1810 }
1811 return "Tool";
1812}
1813
1815 WorkbenchTool tool) const {
1816 switch (tool) {
1818 return "Room tag tools are not available.";
1820 return "Custom collision tools are not available.";
1822 return "Water fill tools are not available.";
1824 return "Minecart track tools are not available.";
1826 return "Object coverage is not available.";
1828 return "Object selector is not available.";
1830 return "Door tools are not available.";
1832 return "Sprite tools are not available.";
1834 return "Item tools are not available.";
1836 return "Room graphics tools are not available.";
1838 return "Palette tools are not available.";
1840 return "No Workbench tool selected.";
1841 }
1842 return "Tool is not available.";
1843}
1844
1846 WorkbenchTool tool) {
1847 const int room_id = viewer.current_room_id();
1848 auto draw_window_content = [](WindowContent* content,
1849 const char* unavailable_message) {
1850 if (!content) {
1851 ImGui::TextDisabled("%s", unavailable_message);
1852 return;
1853 }
1854 content->Draw(nullptr);
1855 };
1856
1857 ImGui::PushID(GetWorkbenchToolId(tool));
1858 switch (tool) {
1860 if (!room_tag_panel_) {
1861 ImGui::TextDisabled("%s", GetWorkbenchToolUnavailableMessage(tool));
1862 break;
1863 }
1865 room_tag_panel_->Draw(nullptr);
1866 break;
1869 ImGui::TextDisabled("%s", GetWorkbenchToolUnavailableMessage(tool));
1870 break;
1871 }
1874 custom_collision_panel_->Draw(nullptr);
1875 break;
1877 if (!water_fill_panel_) {
1878 ImGui::TextDisabled("%s", GetWorkbenchToolUnavailableMessage(tool));
1879 break;
1880 }
1883 water_fill_panel_->Draw(nullptr);
1884 break;
1886 if (!minecart_track_panel_) {
1887 ImGui::TextDisabled("%s", GetWorkbenchToolUnavailableMessage(tool));
1888 break;
1889 }
1890 minecart_track_panel_->Draw(nullptr);
1891 break;
1893 draw_window_content(object_coverage_content_,
1895 break;
1897 if (auto* selector =
1899 selector->DrawInWorkbench();
1900 } else {
1901 draw_window_content(object_selector_content_,
1903 }
1904 break;
1906 draw_window_content(door_editor_content_,
1908 break;
1910 draw_window_content(sprite_editor_content_,
1912 break;
1914 draw_window_content(item_editor_content_,
1916 break;
1918 draw_window_content(room_graphics_content_,
1920 break;
1922 draw_window_content(palette_editor_content_,
1924 break;
1926 ImGui::TextDisabled("%s", GetWorkbenchToolUnavailableMessage(tool));
1927 break;
1928 }
1929 ImGui::PopID();
1930}
1931
1933 constexpr WorkbenchTool tools[] = {
1938 const bool pressed = ImGui::Button("Room tools");
1939 {
1940 gui::AutoWidgetScope scope("Dungeon/Workbench");
1941 gui::AutoRegisterLastItem("button", "tool_picker",
1942 "Specialized room tools");
1943 }
1944 if (pressed)
1945 ImGui::OpenPopup("##WorkbenchToolPickerPopup");
1946 if (ImGui::BeginPopup("##WorkbenchToolPickerPopup")) {
1947 for (const auto tool : tools) {
1948 const bool available = IsWorkbenchToolAvailable(tool);
1949 const bool selected = ImGui::MenuItem(
1950 GetWorkbenchToolShortLabel(tool), nullptr,
1952 available);
1953 {
1954 gui::AutoWidgetScope scope("Dungeon/Workbench");
1956 "menu_item", absl::StrFormat("tool_%s", GetWorkbenchToolId(tool)));
1957 }
1958 if (selected)
1959 OpenTool(tool);
1960 }
1961 ImGui::EndPopup();
1962 }
1963}
1964
1966 DungeonCanvasViewer& viewer) {
1967 const bool back = ImGui::Button("Room");
1968 {
1969 gui::AutoWidgetScope scope("Dungeon/Workbench");
1970 gui::AutoRegisterLastItem("button", "back_to_inspect");
1971 }
1972 if (back) {
1974 return;
1975 }
1976 ImGui::SameLine();
1977 const bool can_pop_out =
1979 ImGui::BeginDisabled(!can_pop_out);
1980 const bool pop_out = ImGui::Button("Pop out");
1981 {
1982 gui::AutoWidgetScope scope("Dungeon/Workbench");
1983 gui::AutoRegisterLastItem("button", "pop_out_tool");
1984 }
1985 ImGui::EndDisabled();
1986 if (pop_out && can_pop_out) {
1987 (void)PopOutActiveTool();
1988 return;
1989 }
1991 ImGui::TextDisabled("%s is open in a separate window.",
1993 return;
1994 }
1996 ImGui::TextDisabled("%s", GetWorkbenchToolUnavailableMessage(active_tool_));
1997 return;
1998 }
1999 ImGui::SeparatorText(GetWorkbenchToolShortLabel(active_tool_));
2000 const bool body_open = ImGui::BeginChild("##WorkbenchToolInspectorBody",
2001 ImVec2(0.0f, 0.0f), false);
2002 if (body_open)
2004 ImGui::EndChild();
2005}
2006
2007} // 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:52
const auto & vector() const
Definition rom.h:217
bool is_loaded() const
Definition rom.h:199
static Flags & get()
Definition features.h:122
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()
const project::YazeProject * project() const
void DrawProposalPreview(const DungeonProposalOverlay &overlay, const std::vector< char > &layer_visible, bool show_overlay, float scale)
void DrawNearbyRoomPreviews(int room_id, float height)
std::string GetId() const override
Unique identifier for this panel.
WindowContent for displaying multiple rooms in a spatial dungeon layout.
Handles room and entrance selection UI.
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 DrawPitDamageControls(int room_id, bool compact=false)
bool IsStandaloneToolOpen(WorkbenchTool tool) const
DungeonWorkbenchContent(DungeonRoomSelector *room_selector, int *current_room_id, std::function< void(int)> on_room_selected, std::function< void(int, RoomSelectionIntent)> on_room_selected_with_intent, std::function< void(int)> on_save_room, std::function< void()> on_save_all_rooms, std::function< DungeonCanvasViewer *()> get_viewer, std::function< DungeonCanvasViewer *(int)> get_compare_viewer, std::function< const std::deque< int > &()> get_recent_rooms, std::function< void(int)> forget_recent_room, std::function< void(bool)> set_workflow_mode, Rom *rom=nullptr)
void DrawProposalPreviewWindow(DungeonCanvasViewer &viewer)
std::function< bool(const std::string &) is_standalone_tool_open_)
int GetPriority() const override
Get display priority for menu ordering.
DungeonWorkbenchPitDamageControlRects pit_damage_control_rects_
bool IsWorkbenchToolAvailable(WorkbenchTool tool) const
void DrawInspectorHeader(float button_size, bool compact)
void DrawSidebarPane(float width, float height, float button_size, bool compact)
DungeonMapPanel * GetEmbeddedDungeonMap(DungeonCanvasViewer &viewer)
void DrawInspectorPane(float width, float height, float button_size, bool compact, DungeonCanvasViewer *viewer)
void DrawInspectorShelfRoom(DungeonCanvasViewer &viewer)
WindowContent * GetWorkbenchToolContent(WorkbenchTool tool) const
std::string GetEditorCategory() const override
Editor category this panel belongs to.
std::optional< DungeonProposalOverlay > proposal_overlay_
void DrawWorkbenchTool(DungeonCanvasViewer &viewer, WorkbenchTool tool)
void DrawSidebarHeader(float button_size, bool compact)
const char * GetWorkbenchToolShortLabel(WorkbenchTool tool) const
void DrawDungeonMapPopup(DungeonCanvasViewer &viewer)
std::unique_ptr< DungeonMapPanel > embedded_dungeon_map_
void DrawInspectorToolPanel(DungeonCanvasViewer &viewer)
std::function< void(int, RoomSelectionIntent)> on_room_selected_with_intent_
void SetEmbeddedEditorPanels(WindowContent *object_selector, WindowContent *door_editor, WindowContent *sprite_editor, WindowContent *item_editor, WindowContent *room_graphics, WindowContent *palette_editor)
void SetEmbeddedToolPanels(RoomTagEditorPanel *room_tags, CustomCollisionPanel *custom_collision, WaterFillPanel *water_fill, MinecartTrackEditorPanel *minecart_tracks)
void DrawCanvasPane(float width, float height, DungeonCanvasViewer *primary_viewer)
std::function< void(DungeonCanvasViewer &) on_primary_canvas_drawn_)
const char * GetWorkbenchToolId(WorkbenchTool tool) const
const char * GetWorkbenchToolUnavailableMessage(WorkbenchTool tool) const
std::string GetId() const override
Unique identifier for this panel.
bool LoadProposalOverlay(const std::string &path)
void DrawSplitView(DungeonCanvasViewer &primary_viewer)
std::function< zelda3::PitDamageTable *()> get_pit_damage_table_
std::string GetIcon() const override
Material Design icon for this panel.
std::function< DungeonCanvasViewer *(int)> get_compare_viewer_
std::unordered_map< int, std::string > room_dungeon_cache_
std::string GetDisplayName() const override
Human-readable name shown in menus and title bars.
void Draw(bool *p_open) override
Draw the panel content.
std::function< bool(const std::string &) open_and_focus_standalone_tool_)
std::function< const std::deque< int > &()> get_recent_rooms_
void DrawInspectorShelf(DungeonCanvasViewer &viewer, bool compact)
std::function< DungeonCanvasViewer *()> get_viewer_
static bool Draw(const DungeonWorkbenchToolbarParams &params)
const char * GetModeName() const
Get mode name for debugging/UI.
WindowContent for placing and managing dungeon pot items.
void Draw(bool *p_open) override
Draw the panel content.
Browse and place dungeon objects.
void DrawInWorkbench(float preview_height=144.0f)
WindowContent showing all room tag slots and their usage across rooms.
void Draw(bool *p_open) override
Draw the panel content.
WindowContent for placing and managing dungeon sprites.
void SetCanvasViewer(DungeonCanvasViewer *viewer)
void SetInteraction(DungeonObjectInteraction *interaction)
void Draw(bool *p_open) override
Draw the panel content.
Base interface for all logical window content components.
virtual void Draw(bool *p_open)=0
Draw the panel content.
virtual std::string GetId() const =0
Unique identifier for this panel.
RAII scope that enables automatic widget registration.
void ApplyScaleSnapshot(const CanvasConfig &snapshot)
Definition canvas.cc:1063
CanvasConfig & GetConfig()
Definition canvas.h:243
static float GetTouchSafeWidgetHeight()
static bool BeginContentChild(const char *id, const ImVec2 &min_size, bool border=false, ImGuiWindowFlags flags=0)
static void EndContentChild()
RAII guard for ImGui style colors.
Definition style_guard.h:29
RAII guard for ImGui style vars.
Definition style_guard.h:71
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
Dungeon Room Entrance or Spawn Point.
const absl::Status & load_status() const
#define ICON_MD_GRID_VIEW
Definition icons.h:897
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_SAVE_ALT
Definition icons.h:1645
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_TRAIN
Definition icons.h:2005
#define ICON_MD_COMPARE_ARROWS
Definition icons.h:448
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_WIDGETS
Definition icons.h:2156
#define ICON_MD_MORE_HORIZ
Definition icons.h:1241
#define ICON_MD_LOGIN
Definition icons.h:1146
#define ICON_MD_GRID_ON
Definition icons.h:896
#define ICON_MD_DOOR_FRONT
Definition icons.h:613
#define ICON_MD_CHEVRON_LEFT
Definition icons.h:405
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_PEST_CONTROL
Definition icons.h:1429
#define ICON_MD_PREVIEW
Definition icons.h:1512
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_SELECT_ALL
Definition icons.h:1680
#define ICON_MD_PALETTE
Definition icons.h:1370
#define ICON_MD_INVENTORY_2
Definition icons.h:1012
#define ICON_MD_CLOSE
Definition icons.h:418
#define ICON_MD_CHEVRON_RIGHT
Definition icons.h:406
#define ICON_MD_WORKSPACES
Definition icons.h:2186
#define ICON_MD_CROP_FREE
Definition icons.h:495
#define ICON_MD_EXPAND_MORE
Definition icons.h:703
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)
void DrawPaneHeader(const char *table_id, const char *icon, const char *title, const char *compact_title, const char *subtitle, bool compact, float action_width, const std::function< void()> &draw_actions)
float CalcIconButtonWidth(const char *icon, float button_height)
bool DrawActionButton(const char *label, const ImVec2 &size)
bool DrawHeaderIconAction(const char *id, const char *icon, float button_size, const char *tooltip, bool active=false)
constexpr const char * kConnected
Editors are the view controllers for the application.
absl::StatusOr< DungeonProposalOverlay > ParseDungeonProposalOverlay(std::string_view json_text, int max_room_id)
DungeonWorkbenchPaneLayout ResolveDungeonWorkbenchPaneLayout(float total_width, float min_canvas_width, float min_sidebar_width, float splitter_width, float stored_left_width, float stored_right_width, bool want_left, bool want_right)
PitDamageMembershipState BuildPitDamageMembershipState(const zelda3::PitDamageTable *table, int room_id, uint16_t replacement_fallback, uint16_t victim_fallback)
absl::Status RemoveCurrentRoomFromPitDamage(zelda3::PitDamageTable *table, uint16_t current_room_id, uint16_t replacement_room_id)
RoomSelectionIntent
Intent for room selection in the dungeon editor.
DungeonWorkbenchResponsiveLayout ResolveDungeonWorkbenchResponsiveLayout(float total_width, float min_canvas_width, float min_sidebar_width, float splitter_width, bool want_left, bool want_right)
float ResolveDungeonWorkbenchNearbyHeight(float width, float height, float requested_height, float item_spacing)
bool DrawDungeonWorkbenchVerticalSplitter(const char *id, float height, float *pane_width, float min_width, float max_width, bool resize_from_left_edge, float collapse_threshold)
DungeonWorkbenchToolRequestTarget ResolveDungeonWorkbenchToolRequestTarget(bool standalone_window_open)
absl::Status AddCurrentRoomToPitDamage(zelda3::PitDamageTable *table, uint16_t current_room_id, uint16_t victim_room_id)
bool CenteredIconButton(const char *label, float button_size)
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
InputHexResult InputHexWordEx(const char *label, uint16_t *data, float input_width, bool no_step)
Definition input.cc:544
constexpr int kNumRegularDungeonEntrances
constexpr int kNumDungeonSpawnPoints
constexpr int kNumberOfRooms
absl::StatusOr< EntranceLayout > ResolveEntranceLayout(std::span< const uint8_t > data)
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
std::function< const std::deque< int > &()> get_recent_rooms
bool ShouldApply() const
Definition input.h:87
static constexpr float kStatusBarHeight
Definition ui_config.h:21
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:290
std::vector< FileDialogFilter > filters
Definition file_util.h:18
Automatic widget registration helpers for ImGui Test Engine integration.