yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_workbench_room_inspector.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstdint>
5#include <cstdio>
6#include <string>
7
8#include "absl/strings/str_format.h"
20#include "app/gui/core/icons.h"
21#include "app/gui/core/input.h"
24#include "imgui/imgui.h"
25#include "rom/rom.h"
26#include "util/i18n/tr.h"
31
32namespace yaze::editor {
33namespace {
34
35const char* GetObjectCategory(int object_id) {
36 if (object_id < 0x100)
37 return "Standard";
38 if (object_id < 0x200)
39 return "Extended";
40 if (object_id >= 0xF80)
41 return "Special";
42 return "Unknown";
43}
44
45const char* GetObjectStreamName(int layer_value) {
46 switch (layer_value) {
47 case 0:
48 return "Primary";
49 case 1:
50 return "BG2 overlay";
51 case 2:
52 return "BG1 overlay";
53 default:
54 return "Unknown";
55 }
56}
57
58const char* GetSpecialLayerName(int layer_value) {
59 switch (layer_value) {
60 case 0:
61 return "Upper layer (BG1)";
62 case 1:
63 return "Lower layer (BG2)";
64 default:
65 return "Unknown";
66 }
67}
68
70 auto* rooms = viewer.rooms();
71 const int room_id = viewer.current_room_id();
72 if (!rooms || room_id < 0 || room_id >= static_cast<int>(rooms->size())) {
73 return;
74 }
75 auto& objects = (*rooms)[room_id].GetTileObjects();
76 if (objects.empty()) {
77 return;
78 }
79
80 auto& interaction = viewer.object_interaction();
81 const auto selected = interaction.GetSelectedObjectIndices();
82 int requested_object_index =
83 selected.size() == 1 ? static_cast<int>(selected.front()) : 0;
84
85 ImGui::AlignTextToFramePadding();
86 ImGui::TextUnformatted(tr("Jump to object"));
87 ImGui::SameLine();
88 ImGui::SetNextItemWidth(74.0f);
89 bool object_index_changed = false;
90 {
91 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
92 object_index_changed = gui::InputScalarDeferred(
93 "##WorkbenchObjectIndex", ImGuiDataType_S32, &requested_object_index,
94 "%d", 0,
95 {reinterpret_cast<uintptr_t>(rooms), static_cast<uint64_t>(room_id)});
96 gui::AutoRegisterLastItem("input_int", "object_index",
97 "Select and locate a room object by index");
98 }
99 if (ImGui::IsItemHovered()) {
100 ImGui::SetTooltip(tr("Select object index 0-%zu"), objects.size() - 1);
101 }
102 if (object_index_changed) {
103 const size_t target_index = static_cast<size_t>(std::clamp(
104 requested_object_index, 0, static_cast<int>(objects.size()) - 1));
105 interaction.SetSelectedObjects({target_index});
106 viewer.ScrollToTile(objects[target_index].x(), objects[target_index].y());
107 }
108}
109
110} // namespace
111
113 bool /*compact*/) {
114 auto& interaction = viewer.object_interaction();
115 ImGui::PushID("InspectedSelection");
116 const bool placing_object = DrawObjectPlacementInspector(viewer);
117 if (!placing_object && interaction.mode_manager().IsPlacementActive()) {
118 ImGui::TextUnformatted(interaction.mode_manager().GetModeName());
119 if (ImGui::Button("Done placing (Esc)")) {
120 interaction.CancelPlacement();
121 }
122 {
123 gui::AutoWidgetScope scope("Dungeon/Workbench");
124 gui::AutoRegisterLastItem("button", "finish_placement");
125 }
126 } else if (!placing_object) {
128 }
129 ImGui::PopID();
130}
131
133 DungeonCanvasViewer& viewer) {
134 auto& interaction = viewer.object_interaction();
135 const auto* active_preview = interaction.GetPlacementPreview();
136 if (!active_preview) {
137 return false;
138 }
139 // Setters may invalidate preview caches; draw from a stable value this frame.
140 const auto object = *active_preview;
141 const auto description = workbench::DescribeObjectSize(object);
142 const auto& theme = AgentUI::GetTheme();
143 ImGui::PushID("PlacementInspector");
145 {
146 gui::StyleColorGuard name_color(ImGuiCol_Text, theme.text_primary);
147 ImGui::TextWrapped("%s", zelda3::GetObjectName(object.id_).c_str());
148 }
149 ImGui::TextDisabled("0x%03X", object.id_);
150
151 constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_BordersInnerV |
152 ImGuiTableFlags_RowBg |
153 ImGuiTableFlags_NoPadOuterX;
154 if (ImGui::BeginTable("Properties", 2, kFlags)) {
155 ImGui::TableSetupColumn("Property", ImGuiTableColumnFlags_WidthFixed,
156 64.0f);
157 ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch);
158 uint8_t requested_size = object.size_;
159 if (workbench::DrawObjectSizeControls(object, &requested_size)) {
160 interaction.SetPlacementPreviewSize(requested_size);
161 }
162 const bool uses_stream = zelda3::UsesRoomObjectStream(object);
163 gui::LayoutHelpers::PropertyRow(uses_stream ? "Stream" : "Layer", [&]() {
164 int layer = object.GetLayerValue();
165 const char* stream_names[] = {"Primary", "BG2 overlay", "BG1 overlay"};
166 const char* layer_names[] = {"Upper layer (BG1)", "Lower layer (BG2)"};
167 ImGui::SetNextItemWidth(-1);
168 if (ImGui::Combo("##Layer", &layer,
169 uses_stream ? stream_names : layer_names,
170 uses_stream ? 3 : 2)) {
171 interaction.SetPlacementPreviewLayer(layer);
172 }
173 gui::AutoWidgetScope scope("Dungeon/Workbench");
174 gui::AutoRegisterLastItem("combo", "placement_layer",
175 "Layer for the next placed object");
176 });
177 ImGui::EndTable();
178 }
179
180 bool repeat = interaction.GetPlacementPolicy() ==
181 DungeonObjectInteraction::PlacementPolicy::kRepeat;
182 if (ImGui::Checkbox(tr("Keep placing copies"), &repeat)) {
183 interaction.SetPlacementPolicy(
184 repeat ? DungeonObjectInteraction::PlacementPolicy::kRepeat
185 : DungeonObjectInteraction::PlacementPolicy::kOnce);
186 }
187 {
188 gui::AutoWidgetScope scope("Dungeon/Workbench");
189 gui::AutoRegisterLastItem("checkbox", "repeat_placement",
190 "Keep placing copies after each click");
191 }
192 ImGui::TextWrapped("%s", repeat
193 ? tr("Click the canvas to place each copy.")
194 : tr("Click the canvas to place, then edit."));
195 if (!description.wheel_hint.empty()) {
196 ImGui::TextWrapped("%s", description.wheel_hint.c_str());
197 }
198 if (workbench::DrawActionButton(ICON_MD_CHECK " Done placing (Esc)",
199 ImVec2(-1, 0))) {
200 interaction.CancelPlacement();
201 if (interaction.GetSelectionCount() > 0) {
203 }
204 }
205 {
206 gui::AutoWidgetScope scope("Dungeon/Workbench");
208 "button", "finish_placement",
209 "Finish placement and inspect the selected object");
210 }
211 ImGui::Separator();
212 ImGui::PopID();
213 return true;
214}
215
217 DungeonCanvasViewer& viewer, size_t index) {
218 auto& interaction = viewer.object_interaction();
219 auto& handler = interaction.entity_coordinator().tile_handler();
220 const int room_id = viewer.current_room_id();
221 const std::vector<size_t> indices = {index};
222 ImGui::Spacing();
224 ImVec2(-1, 0))) {
225 if (interaction.BeginPlacementFromSelection()) {
227 }
228 }
229 {
230 gui::AutoWidgetScope scope("Dungeon/Workbench");
231 gui::AutoRegisterLastItem("button", "place_another",
232 "Place another object with this size and layer");
233 }
234 constexpr ImGuiTableFlags kFlags =
235 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
236 if (ImGui::BeginTable("##SelectedObjectActions", 2, kFlags)) {
237 auto action = [](const char* label, const char* id) {
238 ImGui::TableNextColumn();
239 const bool clicked = workbench::DrawActionButton(label, ImVec2(-1, 0));
240 gui::AutoWidgetScope scope("Dungeon/Workbench");
241 gui::AutoRegisterLastItem("button", id, label);
242 return clicked;
243 };
244 if (action(ICON_MD_CONTENT_COPY " Duplicate", "duplicate_object")) {
245 (void)interaction.HandleDuplicateSelected();
246 }
247 if (action(ICON_MD_DELETE " Delete", "delete_object")) {
248 handler.DeleteObjects(room_id, indices);
249 }
250 if (action(ICON_MD_FLIP_TO_FRONT " To front", "object_to_front")) {
251 handler.SendToFront(room_id, indices);
252 }
253 if (action(ICON_MD_FLIP_TO_BACK " To back", "object_to_back")) {
254 handler.SendToBack(room_id, indices);
255 }
256 ImGui::EndTable();
257 }
258}
259
261 DungeonCanvasViewer& viewer) {
262 auto& interaction = viewer.object_interaction();
263 const auto& theme = AgentUI::GetTheme();
264
265 const int room_id = viewer.current_room_id();
266 const size_t obj_count = interaction.GetSelectionCount();
267 const bool has_entity = interaction.HasEntitySelection();
268
269 if (!has_entity && obj_count == 0) {
271 ImGui::TextDisabled(tr("Click an object or entity to inspect"));
272 ImGui::Spacing();
273 DrawObjectIndexJump(viewer);
274 return;
275 }
276
277 // ── Tile Object Selection ──
278 if (obj_count > 0) {
279 const auto indices = interaction.GetSelectedObjectIndices();
280
281 // Multi-object summary + inline bulk editor (O1)
282 if (indices.size() > 1 && room_id >= 0 && viewer.rooms()) {
283 auto& room = (*viewer.rooms())[room_id];
284 auto& objects = room.GetTileObjects();
286 " Multi-selection");
287 for (size_t i = 0; i < indices.size() && i < 8; ++i) {
288 size_t idx = indices[i];
289 if (idx < objects.size()) {
290 auto& obj = objects[idx];
291 std::string name = zelda3::GetObjectName(obj.id_);
292 ImGui::BulletText(tr("0x%03X %s"), obj.id_, name.c_str());
293 }
294 }
295 if (indices.size() > 8) {
296 ImGui::TextDisabled(tr(" ... and %zu more"), indices.size() - 8);
297 }
298
299 // Bulk editor: nudge/layer/stacking/size + destructive duplicate/delete.
300 // Operations route through tile_handler / interaction, so every action
301 // captures its own undo snapshot.
302 auto& tile_handler = interaction.entity_coordinator().tile_handler();
303 const std::vector<size_t> selection_copy(indices.begin(), indices.end());
304 bool has_room_stream_object = false;
305 bool has_special_layer_object = false;
306 for (const size_t index : selection_copy) {
307 if (index >= objects.size()) {
308 continue;
309 }
310 if (zelda3::UsesRoomObjectStream(objects[index])) {
311 has_room_stream_object = true;
312 } else {
313 has_special_layer_object = true;
314 }
315 }
316 const bool has_editable_size =
317 workbench::HasEditableRoomObjectSize(objects, selection_copy);
318
320
321 // Nudge grid (arrow buttons around a tile-delta drag).
322 static int bulk_nudge_dx = 0;
323 static int bulk_nudge_dy = 0;
324 ImGui::TextDisabled(
325 tr("Nudge selection (%d px steps)"),
326 interaction.entity_coordinator().SelectionMoveStepPixels());
327 ImGui::PushButtonRepeat(true);
328 if (ImGui::Button(ICON_MD_ARROW_UPWARD "##BulkNudgeUp"))
329 interaction.NudgeSelected(0, -1);
330 ImGui::SameLine();
331 if (ImGui::Button(ICON_MD_ARROW_DOWNWARD "##BulkNudgeDown"))
332 interaction.NudgeSelected(0, 1);
333 ImGui::SameLine();
334 if (ImGui::Button(ICON_MD_ARROW_BACK "##BulkNudgeLeft"))
335 interaction.NudgeSelected(-1, 0);
336 ImGui::SameLine();
337 if (ImGui::Button(ICON_MD_ARROW_FORWARD "##BulkNudgeRight"))
338 interaction.NudgeSelected(1, 0);
339 ImGui::PopButtonRepeat();
340
341 ImGui::SetNextItemWidth(60);
342 ImGui::DragInt("##BulkNudgeDx", &bulk_nudge_dx, 0.25f, -63, 63, "Δx:%d");
343 ImGui::SameLine();
344 ImGui::SetNextItemWidth(60);
345 ImGui::DragInt("##BulkNudgeDy", &bulk_nudge_dy, 0.25f, -63, 63, "Δy:%d");
346 ImGui::SameLine();
347 if (ImGui::Button(tr("Apply##BulkNudgeApply")) &&
348 (bulk_nudge_dx != 0 || bulk_nudge_dy != 0)) {
349 interaction.NudgeSelected(bulk_nudge_dx, bulk_nudge_dy);
350 bulk_nudge_dx = 0;
351 bulk_nudge_dy = 0;
352 }
353
354 ImGui::Spacing();
355 if (has_room_stream_object && !has_special_layer_object) {
356 ImGui::TextDisabled(tr("Object stream (set all)"));
357 if (ImGui::SmallButton(tr("Primary##BulkPlacement0")))
358 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 0);
359 ImGui::SameLine();
360 if (ImGui::SmallButton(tr("BG2 overlay##BulkPlacement1")))
361 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 1);
362 ImGui::SameLine();
363 if (ImGui::SmallButton(tr("BG1 overlay##BulkPlacement2")))
364 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 2);
365 } else if (has_special_layer_object && !has_room_stream_object) {
366 ImGui::TextDisabled(tr("Special layer (set all)"));
367 if (ImGui::SmallButton(tr("Upper (BG1)##BulkPlacement0")))
368 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 0);
369 ImGui::SameLine();
370 if (ImGui::SmallButton(tr("Lower (BG2)##BulkPlacement1")))
371 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 1);
372 } else {
373 ImGui::TextDisabled(tr("Stored placement (mixed selection)"));
374 if (ImGui::SmallButton(tr("Primary / Upper (BG1)##BulkPlacement0")))
375 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 0);
376 ImGui::SameLine();
377 if (ImGui::SmallButton(tr("BG2 overlay / Lower (BG2)##BulkPlacement1")))
378 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 1);
379 }
380
381 ImGui::Spacing();
382 ImGui::TextDisabled(tr("Stacking"));
383 if (ImGui::SmallButton(ICON_MD_FLIP_TO_FRONT " Front##BulkFront"))
384 tile_handler.SendToFront(room_id, selection_copy);
385 ImGui::SameLine();
386 if (ImGui::SmallButton(ICON_MD_FLIP_TO_BACK " Back##BulkBack"))
387 tile_handler.SendToBack(room_id, selection_copy);
388 ImGui::SameLine();
389 if (ImGui::SmallButton(ICON_MD_KEYBOARD_ARROW_UP "##BulkFwd"))
390 tile_handler.MoveForward(room_id, selection_copy);
391 ImGui::SameLine();
392 if (ImGui::SmallButton(ICON_MD_KEYBOARD_ARROW_DOWN "##BulkBwd"))
393 tile_handler.MoveBackward(room_id, selection_copy);
394
395 ImGui::Spacing();
396 ImGui::TextDisabled(tr("Size"));
397 if (!has_editable_size) {
398 ImGui::BeginDisabled();
399 }
400 if (ImGui::SmallButton(ICON_MD_REMOVE "##BulkSizeDec"))
401 tile_handler.ResizeObjects(room_id, selection_copy, -1);
402 ImGui::SameLine();
403 if (ImGui::SmallButton(ICON_MD_ADD "##BulkSizeInc"))
404 tile_handler.ResizeObjects(room_id, selection_copy, 1);
405 if (!has_editable_size) {
406 ImGui::EndDisabled();
407 }
408
409 ImGui::Spacing();
410 if (ImGui::SmallButton(ICON_MD_CONTENT_COPY " Duplicate##BulkDup"))
411 (void)interaction.HandleDuplicateSelected();
412 ImGui::SameLine();
413 {
414 gui::StyleColorGuard danger_colors({
415 {ImGuiCol_Button, theme.status_error},
416 {ImGuiCol_ButtonHovered,
417 ImVec4(theme.status_error.x + 0.1f, theme.status_error.y + 0.05f,
418 theme.status_error.z + 0.05f, 1.0f)},
419 });
420 if (ImGui::SmallButton(ICON_MD_DELETE " Delete##BulkDel"))
421 ImGui::OpenPopup("##BulkDeleteConfirm");
422 }
423
424 if (ImGui::BeginPopup("##BulkDeleteConfirm")) {
425 ImGui::TextColored(
426 theme.status_error, ICON_MD_WARNING " Delete %zu object%s?",
427 selection_copy.size(), selection_copy.size() == 1 ? "" : "s");
428 ImGui::Separator();
429 if (ImGui::Button(tr("Cancel##BulkDelCancel"))) {
430 ImGui::CloseCurrentPopup();
431 }
432 ImGui::SameLine();
433 {
434 gui::StyleColorGuard confirm_colors({
435 {ImGuiCol_Button, theme.status_error},
436 });
437 if (ImGui::Button(ICON_MD_DELETE " Confirm##BulkDelConfirm")) {
438 tile_handler.DeleteObjects(room_id, selection_copy);
439 ImGui::CloseCurrentPopup();
440 }
441 }
442 ImGui::EndPopup();
443 }
444 }
445
446 // Single-object detailed inspector
447 if (indices.size() == 1 && room_id >= 0 && viewer.rooms()) {
448 auto& room = (*viewer.rooms())[room_id];
449 auto& objects = room.GetTileObjects();
450 const size_t idx = indices.front();
451 if (idx < objects.size()) {
452 // Stream changes can reorder and replace the room's object vector.
453 // Keep this frame's property values independent of that storage.
454 const auto obj = objects[idx];
455 const std::string obj_name = zelda3::GetObjectName(obj.id_);
456 const bool uses_room_stream = zelda3::UsesRoomObjectStream(obj);
457 int displayed_layer = obj.GetLayerValue();
458 const auto chest_index = zelda3::ChestIndexForObject(objects, idx);
459 const bool editable =
460 viewer.IsObjectInteractionEnabled() && !viewer.header_read_only();
461
462 ImGui::TextWrapped("%s ($%03X)", obj_name.c_str(), obj.id_);
463 if (ImGui::IsItemHovered()) {
464 ImGui::BeginTooltip();
465 ImGui::Text("Object %zu, %s (Type %d)", idx,
466 GetObjectCategory(obj.id_),
467 zelda3::GetObjectSubtype(obj.id_));
468 ImGui::Text("Pixel (%d, %d), %s", obj.x_ * 8, obj.y_ * 8,
469 uses_room_stream ? GetObjectStreamName(displayed_layer)
470 : GetSpecialLayerName(displayed_layer));
471 const auto description = workbench::DescribeObjectSize(obj);
472 if (!description.wheel_hint.empty())
473 ImGui::TextUnformatted(description.wheel_hint.c_str());
474 ImGui::EndTooltip();
475 }
476
477 if (chest_index) {
478 const bool has_contents =
479 room.AreChestsLoaded() && *chest_index < room.GetChests().size();
480 if (has_contents) {
481 const auto chest = room.GetChests()[*chest_index];
482 ImGui::TextWrapped(
483 "Contents: %s",
484 GetDungeonChestItemLabel(chest.id, viewer.project()).c_str());
485 ImGui::Text("Size: %s", chest.size ? "Big" : "Small");
486 } else {
487 ImGui::TextDisabled("Contents unavailable");
488 ImGui::Text("Size: %s", obj.id_ == 0xFB1 ? "Big" : "Small");
489 }
490 ImGui::BeginDisabled(!editable || !has_contents);
491 if (ImGui::Button("Edit Chest...", ImVec2(-1, 0))) {
492 const auto status = viewer.OpenChestEditor(room_id, *chest_index);
493 viewer.chest_editor_state().error =
494 status.ok() ? "" : std::string(status.message());
495 }
496 {
497 gui::AutoWidgetScope scope("Dungeon/Workbench");
498 gui::AutoRegisterLastItem("button", "edit_chest");
499 }
500 ImGui::EndDisabled();
501 if (!viewer.chest_editor_state().error.empty())
502 ImGui::TextWrapped("%s", viewer.chest_editor_state().error.c_str());
503 }
504
505 const bool show_properties =
506 !chest_index ||
507 ImGui::CollapsingHeader("Properties##ChestProperties");
508 if (chest_index) {
509 gui::AutoWidgetScope scope("Dungeon/Workbench");
510 gui::AutoRegisterLastItem("collapsing_header", "chest_properties");
511 }
512 if (show_properties) {
513 ImGui::BeginDisabled(!editable);
514 // Property edits retain the existing handlers and validation.
515 constexpr ImGuiTableFlags kPropsFlags =
516 ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_RowBg |
517 ImGuiTableFlags_NoPadOuterX;
518 if (ImGui::BeginTable("##SelObjProps", 2, kPropsFlags)) {
519 ImGui::TableSetupColumn("Prop", ImGuiTableColumnFlags_WidthFixed,
520 56.0f);
521 ImGui::TableSetupColumn("Val", ImGuiTableColumnFlags_WidthStretch);
522
523 // ID
525 uint16_t obj_id = static_cast<uint16_t>(obj.id_ & 0x0FFF);
526 if (auto res =
527 gui::InputHexWordEx("##SelObjId", &obj_id, 80.0f, true);
528 res.ShouldApply()) {
529 obj_id &= 0x0FFF;
530 interaction.SetObjectId(idx, static_cast<int16_t>(obj_id));
531 }
532 });
533
534 // Position
536 int pos_x = obj.x_;
537 int pos_y = obj.y_;
538 ImGui::SetNextItemWidth(60);
539 bool x_changed =
540 ImGui::DragInt("##SelObjX", &pos_x, 0.1f, 0, 63, "X:%d");
541 {
542 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
543 gui::AutoRegisterLastItem("drag_int", "selected_object_x",
544 "Selected room object X coordinate");
545 }
546 ImGui::SameLine();
547 ImGui::SetNextItemWidth(60);
548 bool y_changed =
549 ImGui::DragInt("##SelObjY", &pos_y, 0.1f, 0, 63, "Y:%d");
550 {
551 gui::AutoWidgetScope automation_scope("Dungeon/Workbench");
552 gui::AutoRegisterLastItem("drag_int", "selected_object_y",
553 "Selected room object Y coordinate");
554 }
555 if (x_changed || y_changed) {
556 int delta_x = pos_x - obj.x_;
557 int delta_y = pos_y - obj.y_;
558 interaction.entity_coordinator().tile_handler().MoveObjects(
559 room_id, {idx}, delta_x, delta_y);
560 }
561 });
562
563 uint8_t requested_size = obj.size_;
564 if (workbench::DrawObjectSizeControls(obj, &requested_size)) {
565 interaction.SetObjectSize(idx, requested_size);
566 }
567
569 uses_room_stream ? "Stream" : "Layer", [&]() {
570 int layer = displayed_layer;
571 const char* stream_names[] = {"Primary", "BG2 overlay",
572 "BG1 overlay"};
573 const char* special_layer_names[] = {"Upper layer (BG1)",
574 "Lower layer (BG2)"};
575 ImGui::SetNextItemWidth(-1);
576 if (ImGui::Combo(
577 "##SelObjLayer", &layer,
578 uses_room_stream ? stream_names : special_layer_names,
579 uses_room_stream
580 ? IM_ARRAYSIZE(stream_names)
581 : IM_ARRAYSIZE(special_layer_names))) {
582 const int max_layer = uses_room_stream ? 2 : 1;
583 layer = std::clamp(layer, 0, max_layer);
584 if (interaction.SetObjectLayer(
585 idx, static_cast<zelda3::RoomObject::LayerType>(
586 layer))) {
587 displayed_layer = layer;
588 }
589 }
590 });
591 ImGui::EndTable();
592 }
593 // Actions can reallocate the room's object vector; no object references
594 // are used after this point.
595 const auto current_indices = interaction.GetSelectedObjectIndices();
596 if (current_indices.size() == 1) {
597 DrawSelectedObjectActions(viewer, current_indices.front());
598 }
599 ImGui::EndDisabled();
600 if (chest_index ||
601 ImGui::CollapsingHeader("Details##SelectionDetails"))
602 DrawObjectIndexJump(viewer);
603 }
604 }
605 }
606 }
607
608 if (has_entity) {
610 " Entity Selection");
612 }
613}
614
616 DungeonCanvasViewer& viewer) {
617 const auto& theme = AgentUI::GetTheme();
618
619 int room_id = viewer.current_room_id();
620 if (room_id < 0 && current_room_id_) {
621 room_id = *current_room_id_;
622 }
623
624 const std::string room_label =
625 (room_id >= 0)
627 : std::string("None");
628
629 bool room_dirty = false;
630 if (auto* rooms = viewer.rooms(); rooms && room_id >= 0) {
631 if (const auto* room = rooms->GetIfMaterialized(room_id)) {
632 room_dirty = room->HasUnsavedChanges();
633 }
634 }
635
636 // Room header: hex ID + copy button + room and dungeon name unified.
637 if (room_id >= 0) {
638 ImGui::AlignTextToFramePadding();
639 ImGui::Text(ICON_MD_CASTLE " Room 0x%03X", room_id);
640 ImGui::SameLine();
641 if (ImGui::SmallButton(ICON_MD_CONTENT_COPY "##CopyRoomId")) {
642 char buf[16];
643 snprintf(buf, sizeof(buf), "0x%03X", room_id);
644 ImGui::SetClipboardText(buf);
645 }
646 if (ImGui::IsItemHovered()) {
647 ImGui::SetTooltip(tr("Copy room ID (0x%03X) to clipboard"), room_id);
648 }
649
650 if (room_label_edit_callback_ && viewer.project() != nullptr) {
651 ImGui::SameLine();
652 if (ImGui::SmallButton(ICON_MD_EDIT "##RenameRoom")) {
653 snprintf(room_rename_buffer_, sizeof(room_rename_buffer_), "%s",
654 room_label.c_str());
655 room_rename_error_.clear();
656 ImGui::OpenPopup("RenameRoomModal");
657 }
658 if (ImGui::IsItemHovered()) {
659 ImGui::SetTooltip(tr("Rename Room 0x%03X for this project"), room_id);
660 }
661 gui::AutoRegisterLastItem("button", "rename_room_button");
662 }
663
664 if (ImGui::BeginPopup("RenameRoomModal")) {
665 const auto apply = [&](std::optional<std::string> label) {
666 const absl::Status status =
668 ? room_label_edit_callback_(room_id, std::move(label))
669 : absl::FailedPreconditionError("Room names are read-only");
670 if (status.ok()) {
671 ImGui::CloseCurrentPopup();
672 } else {
673 room_rename_error_ = std::string(status.message());
674 }
675 };
676
677 ImGui::Text(ICON_MD_EDIT " Rename Room 0x%03X", room_id);
678 ImGui::Separator();
679 ImGui::Spacing();
680
681 ImGui::TextDisabled(tr("Project Room Name:"));
682 ImGui::SetNextItemWidth(260.0f);
683 const bool enter_pressed = ImGui::InputText(
684 "##RoomNameInput", room_rename_buffer_, sizeof(room_rename_buffer_),
685 ImGuiInputTextFlags_EnterReturnsTrue);
686 gui::AutoRegisterLastItem("input_text", "rename_room_input");
687
688 ImGui::Spacing();
689 const bool has_name = room_rename_buffer_[0] != '\0';
690 ImGui::BeginDisabled(!has_name);
691 const bool save_clicked = ImGui::Button(ICON_MD_SAVE " Save");
692 ImGui::EndDisabled();
693 gui::AutoRegisterLastItem("button", "rename_room_save");
694 if (has_name && (save_clicked || enter_pressed)) {
695 apply(std::string(room_rename_buffer_));
696 }
697
698 ImGui::SameLine();
700 room_id)) {
701 if (ImGui::Button(ICON_MD_RESTORE " Reset")) {
702 apply(std::nullopt);
703 }
704 gui::AutoRegisterLastItem("button", "rename_room_reset");
705 if (ImGui::IsItemHovered()) {
706 ImGui::SetTooltip(tr("Reset to default/registry name"));
707 }
708 ImGui::SameLine();
709 }
710
711 if (ImGui::Button(ICON_MD_CANCEL " Cancel")) {
712 ImGui::CloseCurrentPopup();
713 }
714 gui::AutoRegisterLastItem("button", "rename_room_cancel");
715
716 if (!room_rename_error_.empty()) {
717 ImGui::TextColored(theme.status_error, "%s",
718 room_rename_error_.c_str());
719 }
720
721 ImGui::EndPopup();
722 }
723
724 if (room_dirty) {
725 ImGui::SameLine();
726 ImGui::TextColored(theme.status_warning, "Unsaved");
727 }
728
729 // Dungeon group context: prefer ROM entrance-based lookup; fall back to blockset.
732 }
733 std::string project_group_name =
735 room_id);
736 const char* group_name =
737 project_group_name.empty() ? nullptr : project_group_name.c_str();
738 if (!group_name) {
739 auto cache_it = room_dungeon_cache_.find(room_id);
740 if (cache_it != room_dungeon_cache_.end() && !cache_it->second.empty()) {
741 group_name = cache_it->second.c_str();
742 }
743 }
744 if (!group_name) {
745 auto* rooms = viewer.rooms();
746 if (rooms && room_id < static_cast<int>(rooms->size())) {
748 (*rooms)[room_id].blockset());
749 }
750 }
751 const bool is_custom_name =
753 ImGui::PushTextWrapPos(0.0f);
754 if (group_name) {
755 ImGui::TextDisabled("%s – %s", group_name, room_label.c_str());
756 } else {
757 ImGui::TextDisabled("%s", room_label.c_str());
758 }
759 if (is_custom_name) {
760 ImGui::SameLine();
761 ImGui::TextColored(theme.text_info, "(custom)");
762 }
763 ImGui::PopTextWrapPos();
764 } else {
765 ImGui::TextUnformatted(tr("Room: None"));
766 }
767}
768
770 auto* rooms = viewer.rooms();
771 const int room_id = viewer.current_room_id();
772 if (!rooms || room_id < 0 || room_id >= static_cast<int>(rooms->size())) {
773 return;
774 }
775 const bool wide =
776 ImGui::GetContentRegionAvail().x >= ImGui::GetFontSize() * 55.0f;
777 ImGui::PushID("WorkbenchRoomContext");
778 if (ImGui::BeginTable("##RoomContextRow", wide ? 2 : 1,
779 ImGuiTableFlags_BordersOuterH |
780 ImGuiTableFlags_NoPadOuterX |
781 ImGuiTableFlags_SizingStretchProp)) {
782 if (wide) {
783 ImGui::TableSetupColumn("Room", ImGuiTableColumnFlags_WidthFixed,
784 ImGui::GetFontSize() * 19.0f);
785 ImGui::TableSetupColumn("Properties", ImGuiTableColumnFlags_WidthStretch);
786 }
787 ImGui::TableNextColumn();
788 DrawRoomContextHeader(viewer);
789 ImGui::TableNextColumn();
790 DrawDungeonRoomPropertyFields(viewer, (*rooms)[room_id], room_id,
792 DrawPitDamageControls(room_id, true);
793 });
794 ImGui::EndTable();
795 }
796 ImGui::PopID();
797}
798
800 DungeonCanvasViewer& viewer) {
801 int room_id = viewer.current_room_id();
802 if (room_id < 0 && current_room_id_) {
803 room_id = *current_room_id_;
804 }
806 DrawRoomContextHeader(viewer);
807 }
808
809 // Room properties share the editor's validated metadata edit boundary.
810 if (auto* rooms = viewer.rooms();
811 rooms && room_id >= 0 && room_id < static_cast<int>(rooms->size())) {
812 auto& room = (*rooms)[room_id];
813
815 ImGui::Spacing();
817 viewer, room, room_id, RoomPropertyLayout::kCoreGraphicsRow,
818 [&]() { DrawPitDamageControls(room_id, true); });
819 }
820
821 ImGui::Spacing();
823 DrawDungeonRoomPropertyFields(viewer, room, room_id,
825 ImGui::Spacing();
828 if (viewer.current_room_id() != room_id)
829 return;
830 } else {
831 ImGui::TextDisabled(tr("Room properties unavailable"));
832 }
833
835 ICON_MD_CONTENT_COPY " Clone / Import Room", false)) {
836 if (viewer.room_transfer_state().popup_open) {
837 ImGui::TextWrapped("Room transfer is open in the canvas dialog.");
838 } else {
840 }
841 }
842}
843
844} // namespace yaze::editor
bool is_loaded() const
Definition rom.h:199
DungeonChestEditorState & chest_editor_state()
absl::Status OpenChestEditor(int room_id, size_t index)
DungeonObjectInteraction & object_interaction()
DungeonRoomTransferEditorState & room_transfer_state()
const project::YazeProject * project() const
void ScrollToTile(int tile_x, int tile_y)
const zelda3::RoomObject * GetPlacementPreview() const
std::vector< size_t > GetSelectedObjectIndices() const
InteractionCoordinator & entity_coordinator()
Get the interaction coordinator for entity handling.
static const char * GetBlocksetGroupName(uint8_t blockset)
void DrawPitDamageControls(int room_id, bool compact=false)
void DrawInspectorShelfSelection(DungeonCanvasViewer &viewer)
void DrawSelectedObjectActions(DungeonCanvasViewer &viewer, size_t index)
void DrawInspectorShelfRoom(DungeonCanvasViewer &viewer)
void DrawRoomContextHeader(DungeonCanvasViewer &viewer)
bool DrawObjectPlacementInspector(DungeonCanvasViewer &viewer)
std::unordered_map< int, std::string > room_dungeon_cache_
void DrawInspectorShelf(DungeonCanvasViewer &viewer, bool compact)
RAII scope that enables automatic widget registration.
static void PropertyRow(const char *label, std::function< void()> widget_callback)
RAII guard for ImGui style colors.
Definition style_guard.h:29
#define ICON_MD_SUMMARIZE
Definition icons.h:1885
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_CANCEL
Definition icons.h:364
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_ARROW_FORWARD
Definition icons.h:184
#define ICON_MD_CHECK
Definition icons.h:397
#define ICON_MD_OPEN_WITH
Definition icons.h:1356
#define ICON_MD_FLIP_TO_FRONT
Definition icons.h:802
#define ICON_MD_ARROW_DOWNWARD
Definition icons.h:180
#define ICON_MD_EDIT
Definition icons.h:645
#define ICON_MD_CASTLE
Definition icons.h:380
#define ICON_MD_RESTORE
Definition icons.h:1605
#define ICON_MD_KEYBOARD_ARROW_DOWN
Definition icons.h:1030
#define ICON_MD_ADD
Definition icons.h:86
#define ICON_MD_REMOVE
Definition icons.h:1574
#define ICON_MD_ARROW_UPWARD
Definition icons.h:189
#define ICON_MD_KEYBOARD_ARROW_UP
Definition icons.h:1033
#define ICON_MD_ARROW_BACK
Definition icons.h:173
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_SELECT_ALL
Definition icons.h:1680
#define ICON_MD_DELETE
Definition icons.h:530
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_FLIP_TO_BACK
Definition icons.h:801
#define ICON_MD_ADD_CIRCLE
Definition icons.h:95
const AgentUITheme & GetTheme()
bool HasCustomRoomLabel(const project::YazeProject *project, int room_id)
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)
ObjectSizeDescription DescribeObjectSize(const zelda3::RoomObject &object)
bool DrawObjectSizeControls(const zelda3::RoomObject &object, uint8_t *requested_size)
bool DrawActionButton(const char *label, const ImVec2 &size)
bool HasEditableRoomObjectSize(std::span< const zelda3::RoomObject > objects, std::span< const size_t > selected_indices)
Editors are the view controllers for the application.
void DrawDungeonEntityInspector(DungeonCanvasViewer &viewer, const std::function< void(int, size_t)> &jump_to_reciprocal)
std::string GetDungeonChestItemLabel(uint8_t item_id)
void DrawDungeonRoomTransferEditor(DungeonCanvasViewer &viewer)
void DrawDungeonDestinationEditor(DungeonCanvasViewer &viewer)
void DrawDungeonRoomPropertyFields(DungeonCanvasViewer &viewer, zelda3::Room &room, int room_id, RoomPropertyLayout layout, const std::function< void()> &draw_pit_damage)
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
bool InputScalarDeferred(const char *label, ImGuiDataType data_type, void *data, const char *format, ImGuiInputTextFlags flags, InputScalarTargetIdentity target_identity)
Definition input.cc:360
InputHexResult InputHexWordEx(const char *label, uint16_t *data, float input_width, bool no_step)
Definition input.cc:544
std::optional< size_t > ChestIndexForObject(const std::vector< RoomObject > &objects, size_t object_index)
Definition chest_edit.cc:63
int GetObjectSubtype(int object_id)
bool UsesRoomObjectStream(const RoomObject &object)
std::string GetObjectName(int object_id)
Treasure chest.
Definition zelda.h:425
bool ShouldApply() const
Definition input.h:87
Automatic widget registration helpers for ImGui Test Engine integration.