yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
room_matrix_content.h
Go to the documentation of this file.
1#ifndef YAZE_APP_EDITOR_DUNGEON_WORKSPACE_ROOM_MATRIX_CONTENT_H_
2#define YAZE_APP_EDITOR_DUNGEON_WORKSPACE_ROOM_MATRIX_CONTENT_H_
3
4#include <algorithm>
5#include <cctype>
6#include <cmath>
7#include <functional>
8#include <memory>
9#include <optional>
10#include <string>
11#include <vector>
12#include "util/i18n/tr.h"
13
14#include "absl/strings/str_format.h"
26#include "app/gui/core/icons.h"
27#include "imgui/imgui.h"
28#include "zelda3/dungeon/room.h"
31
32namespace yaze {
33namespace editor {
34
52 public:
60 RoomMatrixContent(int* current_room_id, ImVector<int>* active_rooms,
61 std::function<void(int)> on_room_selected,
62 std::function<void(int, int)> on_room_swap = nullptr,
63 DungeonRoomStore* rooms = nullptr)
64 : current_room_id_(current_room_id),
65 active_rooms_(active_rooms),
66 rooms_(rooms),
67 on_room_selected_(std::move(on_room_selected)),
68 on_room_swap_(std::move(on_room_swap)) {}
69
70 // ==========================================================================
71 // WindowContent Identity
72 // ==========================================================================
73
74 std::string GetId() const override { return "dungeon.room_matrix"; }
75 std::string GetDisplayName() const override { return "Room Matrix"; }
76 std::string GetIcon() const override { return ICON_MD_GRID_VIEW; }
77 std::string GetEditorCategory() const override { return "Dungeon"; }
78 int GetPriority() const override { return 30; }
79 std::string GetWorkflowGroup() const override { return "Core"; }
80 float GetPreferredWidth() const override { return 440.0f; }
81
83 std::function<void(int, RoomSelectionIntent)> callback) {
84 on_room_intent_ = std::move(callback);
85 }
86
87 // Resolve the graphics owner without changing a shared room or selection.
89 std::function<uint8_t(int)> resolver) {
90 preview_entrance_blockset_resolver_ = std::move(resolver);
91 }
92
93 // The parent owns this region's size. Short regions scroll the matrix while
94 // the retained hover preview stays in a separate, bounded child below it.
95 void DrawInWorkbench(float preview_height = 144.0f) {
97 requested_preview_height_ = std::max(0.0f, preview_height);
98 Draw(nullptr);
99 fixed_preview_mode_ = false;
100 }
101
102 int preview_room_id() const { return preview_room_id_; }
103
104 // A successful ROM refresh can replace bytes and decoded resources in place
105 // without changing the room ID, pointers or authored document.
112
113 // Project dungeon ownership and warp tags for the census overlay.
119 // The window manager can retain this panel after its editor is destroyed.
121 census_.SetEnabled(false);
123 SetRooms(nullptr);
124 current_room_id_ = nullptr;
125 active_rooms_ = nullptr;
127 on_room_swap_ = {};
128 on_room_intent_ = {};
130 preview_room_id_ = -1;
132 }
134 if (!navigation_shortcut_focus_ || ImGui::GetCurrentContext() == nullptr) {
135 return false;
136 }
137 return navigation_shortcut_last_draw_frame_ >= ImGui::GetFrameCount() - 1;
138 }
139
140 // ==========================================================================
141 // WindowContent Drawing
142 // ==========================================================================
143
144 void Draw(bool* p_open) override {
146 return;
147 navigation_shortcut_last_draw_frame_ = ImGui::GetFrameCount();
149 ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows);
150
151 const auto& theme = AgentUI::GetTheme();
152
153 // 16 wide x 19 tall = 304 cells (296 rooms + 8 empty)
154 constexpr int kRoomsPerRow = 16;
155 constexpr int kRoomsPerCol = 19;
156 constexpr int kTotalRooms = 0x128; // 296 rooms (0x00-0x127)
157 constexpr float kCellSpacing = 1.0f;
158
159 Rom* census_rom = rooms_ ? rooms_->rom() : nullptr;
160 census_.Update(census_rom);
161 DrawMatrixHeader(census_rom);
162 const bool census_on = census_.enabled() && census_.census() != nullptr;
163
164 float preview_height = 0.0f;
166 if (preview_room_id_ < 0)
167 preview_room_id_ = std::clamp(*current_room_id_, 0, kTotalRooms - 1);
168 const float height = std::max(1.0f, ImGui::GetContentRegionAvail().y);
169 preview_height = std::min(
171 std::max(0.0f, height - 64.0f - ImGui::GetStyle().ItemSpacing.y));
172 const float grid_height = std::max(
173 1.0f, height - preview_height - ImGui::GetStyle().ItemSpacing.y);
174 if (!ImGui::BeginChild("##RoomMatrixGrid", ImVec2(0, grid_height),
175 false)) {
176 ImGui::EndChild();
177 DrawFixedPreview(preview_height);
178 return;
179 }
180 }
181
182 const ImVec2 avail = ImGui::GetContentRegionAvail();
183 const float panel_width = std::max(1.0f, avail.x);
184 const float panel_height =
185 std::max(1.0f, avail.y - ImGui::GetStyle().ItemSpacing.y);
186 const float cell_size_by_width =
187 (panel_width - kCellSpacing * (kRoomsPerRow - 1)) / kRoomsPerRow;
188 const float cell_size_by_height =
189 (panel_height - kCellSpacing * (kRoomsPerCol - 1)) / kRoomsPerCol;
190 const float cell_size =
191 std::max(fixed_preview_mode_ ? 8.0f : 1.0f,
192 std::floor(std::min({cell_size_by_width, cell_size_by_height,
193 24.0f * ImGui::GetFontSize() / 13.0f})));
194
195 const float grid_width =
196 kRoomsPerRow * cell_size + kCellSpacing * (kRoomsPerRow - 1);
197 const float grid_height =
198 kRoomsPerCol * cell_size + kCellSpacing * (kRoomsPerCol - 1);
199 const float x_offset = std::max(0.0f, (panel_width - grid_width) * 0.5f);
200 const float y_offset = std::max(0.0f, (panel_height - grid_height) * 0.5f);
201
202 ImDrawList* draw_list = ImGui::GetWindowDrawList();
203 const ImVec2 grid_origin = ImGui::GetCursorScreenPos();
204 ImVec2 canvas_pos = grid_origin;
205 canvas_pos.x += x_offset;
206 canvas_pos.y += y_offset;
207
208 int room_index = 0;
209 for (int row = 0; row < kRoomsPerCol; row++) {
210 for (int col = 0; col < kRoomsPerRow; col++) {
211 int room_id = room_index;
212 bool is_valid_room = (room_id < kTotalRooms);
213 const bool matches_filter =
214 MatchesSearchFilter(room_id) &&
215 (!census_on || census_.MatchesChips(room_id));
216
217 ImVec2 cell_min =
218 ImVec2(canvas_pos.x + col * (cell_size + kCellSpacing),
219 canvas_pos.y + row * (cell_size + kCellSpacing));
220 ImVec2 cell_max =
221 ImVec2(cell_min.x + cell_size, cell_min.y + cell_size);
222
223 if (is_valid_room) {
224 // Keep the palette hue stable whether a room is loaded or unopened.
225 ImU32 bg_color = census_on ? census_.FillColor(room_id)
226 : GetRoomColor(room_id, theme);
227 if (!matches_filter) {
228 bg_color = BlendRoomColor(
229 bg_color, ImGui::ColorConvertFloat4ToU32(theme.panel_bg_darker),
230 0.72f);
231 }
232
233 bool is_current = (*current_room_id_ == room_id);
234 bool is_open = rooms_ && rooms_->GetIfLoaded(room_id);
235 for (int i = 0; i < active_rooms_->Size; i++) {
236 if ((*active_rooms_)[i] == room_id) {
237 is_open = true;
238 break;
239 }
240 }
241
242 if ((is_open || is_current) && matches_filter) {
243 ImVec4 color = ImGui::ColorConvertU32ToFloat4(bg_color);
244 float h, s, v;
245 ImGui::ColorConvertRGBtoHSV(color.x, color.y, color.z, h, s, v);
246 ImGui::ColorConvertHSVtoRGB(h, s, v + (1.0f - v) * 0.22f, color.x,
247 color.y, color.z);
248 bg_color = ImGui::ColorConvertFloat4ToU32(color);
249 }
250 draw_list->AddRectFilled(cell_min, cell_max, bg_color);
251 // An inset outline stays inside the cell, including the last row.
252 if (is_current || is_open) {
253 const ImU32 outline =
254 is_current ? ImGui::ColorConvertFloat4ToU32(
255 theme.dungeon_selection_primary)
256 : IM_COL32(255, 255, 255, matches_filter ? 130 : 45);
257 draw_list->AddRect(ImVec2(cell_min.x + 1, cell_min.y + 1),
258 ImVec2(cell_max.x - 1, cell_max.y - 1), outline,
259 0, 0, is_current ? 2.0f : 1.0f);
260 }
261
262 if (census_on) {
263 DrawCensusCellMarks(draw_list, room_id, cell_min, cell_max,
264 matches_filter, theme);
265 }
266
267 // Scale the three-digit IDs to the actual cell; never spill into
268 // adjacent rooms. Tiny docks retain full IDs in the hover tooltip.
269 if (cell_size >= 14.0f) {
270 const std::string label = absl::StrFormat("%03X", room_id);
271 const ImVec2 normal_size = ImGui::CalcTextSize(label.c_str());
272 const float scale =
273 std::min(1.0f, (cell_size - 2.0f) / normal_size.x);
274 const ImVec2 text_pos(
275 cell_min.x + (cell_size - normal_size.x * scale) * 0.5f,
276 cell_min.y + (cell_size - normal_size.y * scale) * 0.5f);
277 const ImVec4 color = ImGui::ColorConvertU32ToFloat4(bg_color);
278 const float luminance =
279 color.x * 0.2126f + color.y * 0.7152f + color.z * 0.0722f;
280 const ImU32 ink = luminance > 0.55f ? IM_COL32(15, 15, 15, 255)
281 : IM_COL32(245, 245, 245, 255);
282 draw_list->AddText(ImGui::GetFont(), ImGui::GetFontSize() * scale,
283 text_pos, ink, label.c_str());
284 }
285
286 // Handle clicks
287 ImGui::SetCursorScreenPos(cell_min);
288 char btn_id[32];
289 snprintf(btn_id, sizeof(btn_id), "##room%d", room_id);
290 ImGui::InvisibleButton(btn_id, ImVec2(cell_size, cell_size));
292 absl::StrFormat("Room 0x%03X", room_id));
293
294 const bool cell_hovered = ImGui::IsItemHovered();
295 if (fixed_preview_mode_ && cell_hovered)
296 preview_room_id_ = room_id;
297
298 if (ImGui::IsItemClicked()) {
299 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
300 // Double-click: open as standalone panel
301 if (on_room_intent_) {
303 } else if (on_room_selected_) {
304 on_room_selected_(room_id);
305 }
306 } else if (on_room_selected_) {
307 on_room_selected_(room_id);
308 }
309 }
310
311 gui::ItemContextMenu(nullptr, [&, room_id]() {
312 const bool can_swap =
314 *current_room_id_ < kTotalRooms && *current_room_id_ != room_id;
315
316 std::vector<gui::MenuItemSpec> items;
317 items.emplace_back(is_open ? "Focus Room" : "Open in Workbench",
318 ICON_MD_ARROW_FORWARD, [this, room_id]() {
319 if (on_room_intent_) {
321 room_id,
323 } else if (on_room_selected_) {
324 on_room_selected_(room_id);
325 }
326 });
327 items.emplace_back(
328 tr("Open as Panel"), ICON_MD_OPEN_IN_NEW, [this, room_id]() {
329 if (on_room_intent_) {
330 on_room_intent_(room_id,
332 } else if (on_room_selected_) {
333 on_room_selected_(room_id);
334 }
335 });
336 items.back().separator_after = true;
337
338 items.push_back(gui::CopyToClipboardItem(
339 tr("Copy Room ID"), absl::StrFormat("0x%03X", room_id)));
340 items.push_back(gui::CopyToClipboardItem(
341 tr("Copy Room Name"), zelda3::GetRoomLabel(room_id)));
342 items.back().separator_after = true;
343
344 items.push_back(gui::MenuItemSpec::Conditional(
345 tr("Swap With Current Room"),
346 [this, room_id]() {
348 },
349 [can_swap]() { return can_swap; }));
350 items.back().icon = ICON_MD_SWAP_HORIZ;
351 return items;
352 });
353
354 // Tooltip with room info and thumbnail preview
355 if (cell_hovered && !fixed_preview_mode_) {
356 ImGui::BeginTooltip();
357 // Use unified ResourceLabelProvider for room names
358 ImGui::Text("%s", zelda3::GetRoomLabel(room_id).c_str());
359 ImGui::TextDisabled(tr("Room 0x%02X"), room_id);
360 if (census_on) {
361 ImGui::Separator();
362 census_.DrawTooltip(room_id);
363 ImGui::Separator();
364 }
365
366 if (is_current) {
367 ImGui::TextColored(theme.dungeon_selection_primary,
368 ICON_MD_MY_LOCATION " Current workbench room");
369 } else if (is_open) {
370 ImGui::TextColored(theme.status_success,
371 ICON_MD_TAB " Open in editor");
372 }
373
374 if (rooms_) {
375 zelda3::Room* room = HoverPreviewRoom(room_id);
376 if (room != nullptr) {
377 ImGui::TextDisabled(
378 "Palette: 0x%02X | Blockset: 0x%02X | Spriteset: 0x%02X",
379 room->palette(), room->blockset(), room->spriteset());
380 ImGui::TextDisabled(
381 "Objects: %zu | Sprites: %zu | Doors: %zu | Chests: %zu",
382 room->GetTileObjects().size(), room->GetSprites().size(),
383 room->GetDoors().size(), room->GetChests().size());
384
385 auto& preview_bitmap = PrepareCanonicalRoomComposite(
389 if (preview_bitmap.is_active() &&
390 preview_bitmap.texture() != 0) {
391 ImGui::Separator();
392 constexpr float kThumbnailSize = 144.0f;
393 ImGui::Image((ImTextureID)(intptr_t)preview_bitmap.texture(),
394 ImVec2(kThumbnailSize, kThumbnailSize));
395 }
396 }
397 }
398
399 ImGui::EndTooltip();
400 }
401 } else {
402 // Empty cell
403 draw_list->AddRectFilled(
404 cell_min, cell_max,
405 ImGui::ColorConvertFloat4ToU32(theme.panel_bg_darker));
406 }
407
408 room_index++;
409 }
410 }
411
412 // Cell buttons move the cursor. Reserve the grid once from its origin,
413 // rather than adding another full grid below the last cell.
414 ImGui::SetCursorScreenPos(grid_origin);
415 ImGui::Dummy(ImVec2(panel_width, grid_height + y_offset));
417 ImGui::EndChild();
418 DrawFixedPreview(preview_height);
419 }
420 }
421
423 if (rooms_ != rooms) {
424 rooms_ = rooms;
427 }
428 }
429
430 private:
432
433 void DrawFixedPreview(float height) {
434 if (height <= 0.0f)
435 return;
436 const bool open = ImGui::BeginChild(
437 "##RoomMatrixFixedPreview", ImVec2(0, height), false,
438 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
439 if (open) {
440 const std::string caption = absl::StrFormat(
442 const ImVec2 position = ImGui::GetCursorScreenPos();
443 const ImVec2 label_size(ImGui::GetContentRegionAvail().x,
444 ImGui::GetTextLineHeight());
445 auto* draw_list = ImGui::GetWindowDrawList();
446 draw_list->PushClipRect(
447 position,
448 ImVec2(position.x + label_size.x, position.y + label_size.y), true);
449 draw_list->AddText(position, ImGui::GetColorU32(ImGuiCol_TextDisabled),
450 caption.c_str());
451 draw_list->PopClipRect();
452 ImGui::Dummy(label_size);
453 const ImVec2 available = ImGui::GetContentRegionAvail();
454 const float size = std::max(0.0f, std::min(available.x, available.y));
455 if (size > 0.0f) {
456 if (auto* room = HoverPreviewRoom(preview_room_id_)) {
457 auto& bitmap =
461 if (bitmap.is_active() && bitmap.texture() != 0) {
462 ImGui::SetCursorPosX(ImGui::GetCursorPosX() +
463 (available.x - size) * 0.5f);
464 ImGui::Image((ImTextureID)(intptr_t)bitmap.texture(),
465 ImVec2(size, size));
466 }
467 }
468 }
469 }
470 ImGui::EndChild();
471 }
472
473 // Every preview owns its room. Loaded rooms contribute their current authored
474 // document, while unopened rooms load privately without losing pending imports
475 // or materializing a room in the shared store.
477 Rom* rom = rooms_ ? rooms_->rom() : nullptr;
478 if (rom == nullptr || !rom->is_loaded() || room_id < 0 ||
479 room_id >= static_cast<int>(DungeonRoomStore::kRoomCount)) {
480 return nullptr;
481 }
482 const auto* source = rooms_->GetIfMaterialized(room_id);
483 std::optional<DungeonRoomDocument> document;
484 if (source && source->IsLoaded())
485 document = CaptureDungeonRoomDocument(*source);
486 const bool document_changed =
487 document.has_value() != hover_preview_document_.has_value() ||
488 (document &&
490 if (!hover_preview_room_ || hover_preview_room_->id() != room_id ||
491 hover_preview_room_->rom() != rom || document_changed) {
494 std::make_unique<zelda3::Room>(zelda3::LoadRoomFromRom(rom, room_id));
495 hover_preview_room_->SetGameData(rooms_->game_data());
496 if (document)
498 hover_preview_document_ = std::move(document);
499 }
500 if (hover_preview_room_->game_data() != rooms_->game_data()) {
502 hover_preview_room_->SetGameData(rooms_->game_data());
503 hover_preview_room_->MarkGraphicsDirty();
504 }
505 if (source) {
506 if (hover_preview_room_->graphics_sheet_overrides() !=
507 source->graphics_sheet_overrides()) {
508 hover_preview_room_->SetGraphicsSheetOverrides(
509 source->graphics_sheet_overrides());
510 }
511 } else if (!hover_preview_room_->graphics_sheet_overrides().empty()) {
512 hover_preview_room_->SetGraphicsSheetOverrides({});
513 }
514 if (source && !source->IsLoaded()) {
516 if (source->custom_collision_dirty() || source->has_custom_collision())
517 pending.collision = source->custom_collision();
518 if (source->water_fill_dirty() || source->water_fill_zone().has_data)
519 pending.water = source->water_fill_zone();
521 }
522 const uint8_t blockset = preview_entrance_blockset_resolver_
524 : source ? source->render_entrance_blockset()
525 : 0xFF;
526 hover_preview_room_->SetRenderEntranceBlockset(blockset);
527 return hover_preview_room_.get();
528 }
529
530 std::function<uint8_t(int)> preview_entrance_blockset_resolver_;
531 std::unique_ptr<zelda3::Room> hover_preview_room_;
532 std::optional<DungeonRoomDocument> hover_preview_document_;
537 const float button_width = ImGui::GetFrameHeight();
538 const float spacing = ImGui::GetStyle().ItemSpacing.x;
539 const bool has_filter = search_filter_[0] != '\0';
540 ImGui::SetNextItemWidth(
541 std::max(1.0f, ImGui::GetContentRegionAvail().x -
542 (button_width + spacing) * (has_filter ? 2 : 1)));
543 ImGui::InputTextWithHint("##RoomSearch", "Room ID or name...",
544 search_filter_, IM_ARRAYSIZE(search_filter_));
545 if (has_filter) {
546 ImGui::SameLine();
547 if (ImGui::Button(ICON_MD_CLOSE "##ClearFilter", ImVec2(button_width, 0)))
548 search_filter_[0] = '\0';
549 if (ImGui::IsItemHovered())
550 ImGui::SetTooltip(tr("Clear room filter"));
551 }
552 ImGui::SameLine();
553 if (ImGui::Button(ICON_MD_MORE_HORIZ "##MatrixOptions",
554 ImVec2(button_width, 0)))
555 ImGui::OpenPopup("##MatrixOptionsPopup");
556 gui::AutoRegisterLastItem("button", "Room matrix options");
557 if (ImGui::IsItemHovered()) {
558 ImGui::BeginTooltip();
559 ImGui::TextUnformatted(tr("Room colors and census"));
560 ImGui::TextDisabled("%d loaded rooms",
561 rooms_ ? static_cast<int>(rooms_->LoadedCount())
562 : active_rooms_->Size);
563 if (const auto meta = GetRoomMetadata(*current_room_id_))
564 ImGui::TextDisabled("Palette %02X / Room GFX %02X", meta->first,
565 meta->second);
566 ImGui::TextDisabled(
567 "Click a room to open. Double-click for a separate panel.");
568 ImGui::EndTooltip();
569 }
570 if (ImGui::BeginPopup("##MatrixOptionsPopup")) {
571 ImGui::TextDisabled("Room palette colors (same swatch as ZScream)");
572 ImGui::TextUnformatted("Brighter + thin outline: open room");
573 ImGui::TextUnformatted("Accent outline: current room");
574 ImGui::Separator();
576 ImGui::EndPopup();
577 }
578 }
579
580 // Census marks: free/reclaimable outline, largest-free-block halo, orphan
581 // corner.
582 void DrawCensusCellMarks(ImDrawList* draw_list, int room_id,
583 const ImVec2& cell_min, const ImVec2& cell_max,
584 bool matches_filter,
585 const AgentUITheme& theme) const {
586 const ImU32 dim_target =
587 ImGui::ColorConvertFloat4ToU32(theme.panel_bg_darker);
588 if (census_.InLargestFreeBlock(room_id)) {
589 draw_list->AddRect(
590 ImVec2(cell_min.x - 1.5f, cell_min.y - 1.5f),
591 ImVec2(cell_max.x + 1.5f, cell_max.y + 1.5f),
592 RoomMatrixCensusOverlay::BlockHaloColor(matches_filter ? 0.5f : 0.2f),
593 0.0f, 0, 2.0f);
594 }
595 if (auto outline = census_.StatusOutline(room_id); outline.has_value()) {
596 ImU32 color = *outline;
597 if (!matches_filter) {
598 color = BlendRoomColor(color, dim_target, 0.55f);
599 }
600 draw_list->AddRect(ImVec2(cell_min.x + 1.0f, cell_min.y + 1.0f),
601 ImVec2(cell_max.x - 1.0f, cell_max.y - 1.0f), color,
602 0.0f, 0, 2.0f);
603 }
604 if (census_.IsOrphan(room_id)) {
605 const float corner = std::max(4.0f, (cell_max.x - cell_min.x) * 0.3f);
607 if (!matches_filter) {
608 color = BlendRoomColor(color, dim_target, 0.55f);
609 }
610 draw_list->AddTriangleFilled(ImVec2(cell_max.x - corner, cell_min.y),
611 ImVec2(cell_max.x, cell_min.y),
612 ImVec2(cell_max.x, cell_min.y + corner),
613 color);
614 }
615 }
616
617 bool MatchesSearchFilter(int room_id) const {
618 if (search_filter_[0] == '\0') {
619 return true;
620 }
621
622 const std::string filter = NormalizeForSearch(search_filter_);
623 char id_buf[16];
624 snprintf(id_buf, sizeof(id_buf), "%02X", room_id);
625 char hex_buf[16];
626 snprintf(hex_buf, sizeof(hex_buf), "0x%02X", room_id);
627
628 const std::string room_label =
630 const std::string id_text = NormalizeForSearch(id_buf);
631 const std::string hex_text = NormalizeForSearch(hex_buf);
632 return room_label.find(filter) != std::string::npos ||
633 id_text.find(filter) != std::string::npos ||
634 hex_text.find(filter) != std::string::npos;
635 }
636
637 static std::string NormalizeForSearch(const std::string& value) {
638 std::string lowered;
639 lowered.reserve(value.size());
640 for (unsigned char ch : value) {
641 lowered.push_back(static_cast<char>(std::tolower(ch)));
642 }
643 return lowered;
644 }
645
646 static ImU32 BlendRoomColor(ImU32 source, ImU32 target, float blend) {
647 ImVec4 src = ImGui::ColorConvertU32ToFloat4(source);
648 ImVec4 dst = ImGui::ColorConvertU32ToFloat4(target);
649 src.x = (src.x * (1.0f - blend)) + (dst.x * blend);
650 src.y = (src.y * (1.0f - blend)) + (dst.y * blend);
651 src.z = (src.z * (1.0f - blend)) + (dst.z * blend);
652 src.w = 1.0f;
653 return ImGui::ColorConvertFloat4ToU32(src);
654 }
655
656 std::optional<std::pair<int, int>> GetRoomMetadata(int room_id) const {
657 if (!rooms_ || room_id < 0 ||
658 room_id >= static_cast<int>(DungeonRoomStore::kRoomCount)) {
659 return std::nullopt;
660 }
661
662 if (const auto* room = rooms_->GetIfLoaded(room_id); room != nullptr) {
663 return std::make_pair(static_cast<int>(room->palette()),
664 static_cast<int>(room->blockset()));
665 }
666
667 Rom* rom = rooms_->rom();
668 if (rom == nullptr || !rom->is_loaded()) {
669 return std::nullopt;
670 }
671
672 zelda3::Room header_room = zelda3::LoadRoomHeaderFromRom(rom, room_id);
673 return std::make_pair(static_cast<int>(header_room.palette()),
674 static_cast<int>(header_room.blockset()));
675 }
676
677 // ZScream's MapPicturebox uses CGRAM [4, 2]: the fourth opaque
678 // color in the first dungeon row (stored index 3; transparent is implicit).
679 // Resolve the palette through the same two-level lookup as Room rendering.
680 // Header-only reads avoid materializing/rendering all 296 rooms, and reading
681 // GameData each frame keeps unsaved palette edits visible immediately.
682 ImU32 GetRoomColor(int room_id, const AgentUITheme& theme) const {
683 if (rooms_ && rooms_->rom() && rooms_->rom()->is_loaded() &&
684 rooms_->game_data() && room_id >= 0 &&
685 room_id < static_cast<int>(DungeonRoomStore::kRoomCount)) {
686 const auto& palettes = rooms_->game_data()->palette_groups.dungeon_main;
687 if (!palettes.empty()) {
688 int palette_id;
689 if (const auto* room = rooms_->GetIfLoaded(room_id)) {
690 palette_id = room->ResolveDungeonPaletteId();
691 } else {
692 auto header = zelda3::LoadRoomHeaderFromRom(rooms_->rom(), room_id);
693 header.SetGameData(rooms_->game_data());
694 palette_id = header.ResolveDungeonPaletteId();
695 }
696 const auto& palette = palettes.palette_ref(palette_id);
697 if (palette.size() > 3) {
698 const auto color = palette[3].rgb();
699 return IM_COL32(color.x, color.y, color.z, 255);
700 }
701 }
702 }
703 return ImGui::ColorConvertFloat4ToU32(theme.panel_bg_color);
704 }
705
706 int* current_room_id_ = nullptr;
707 ImVector<int>* active_rooms_ = nullptr;
709 std::function<void(int)> on_room_selected_;
710 std::function<void(int, int)> on_room_swap_;
711 std::function<void(int, RoomSelectionIntent)> on_room_intent_;
716 char search_filter_[64] = "";
717};
718
719} // namespace editor
720} // namespace yaze
721
722#endif // YAZE_APP_EDITOR_DUNGEON_WORKSPACE_ROOM_MATRIX_CONTENT_H_
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
bool is_loaded() const
Definition rom.h:199
zelda3::Room * GetIfMaterialized(int room_id)
static constexpr size_t kRoomCount
zelda3::Room * GetIfLoaded(int room_id)
zelda3::GameData * game_data() const
"Census" overlay for the Room Matrix.
const zelda3::RoomCensus * census() const
std::function< const core::HackManifest *()> ManifestProvider
std::optional< ImU32 > StatusOutline(int room_id) const
void SetManifestProvider(ManifestProvider provider)
WindowContent for displaying a visual 16x19 grid of all dungeon rooms.
std::function< void(int)> on_room_selected_
static std::string NormalizeForSearch(const std::string &value)
std::function< void(int, RoomSelectionIntent)> on_room_intent_
std::function< uint8_t(int)> preview_entrance_blockset_resolver_
void SetCensusManifestProvider(RoomMatrixCensusOverlay::ManifestProvider provider)
static ImU32 BlendRoomColor(ImU32 source, ImU32 target, float blend)
std::string GetIcon() const override
Material Design icon for this panel.
std::optional< std::pair< int, int > > GetRoomMetadata(int room_id) const
void SetRooms(DungeonRoomStore *rooms)
std::string GetEditorCategory() const override
Editor category this panel belongs to.
void DrawCensusCellMarks(ImDrawList *draw_list, int room_id, const ImVec2 &cell_min, const ImVec2 &cell_max, bool matches_filter, const AgentUITheme &theme) const
std::optional< DungeonRoomDocument > hover_preview_document_
float GetPreferredWidth() const override
Get preferred width for this panel (optional)
void DrawInWorkbench(float preview_height=144.0f)
std::string GetWorkflowGroup() const override
Optional workflow group for hack-centric actions.
void SetPreviewEntranceBlocksetResolver(std::function< uint8_t(int)> resolver)
int GetPriority() const override
Get display priority for menu ordering.
std::string GetDisplayName() const override
Human-readable name shown in menus and title bars.
RoomMatrixContent(int *current_room_id, ImVector< int > *active_rooms, std::function< void(int)> on_room_selected, std::function< void(int, int)> on_room_swap=nullptr, DungeonRoomStore *rooms=nullptr)
Construct a room matrix panel.
void Draw(bool *p_open) override
Draw the panel content.
std::unique_ptr< zelda3::Room > hover_preview_room_
bool MatchesSearchFilter(int room_id) const
std::string GetId() const override
Unique identifier for this panel.
std::function< void(int, int)> on_room_swap_
ImU32 GetRoomColor(int room_id, const AgentUITheme &theme) const
RoomCompositeOutput tooltip_composite_output_
RoomMatrixCensusOverlay & census_overlay()
zelda3::Room * HoverPreviewRoom(int room_id)
void SetRoomIntentCallback(std::function< void(int, RoomSelectionIntent)> callback)
Base interface for all logical window content components.
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:211
static Arena & Get()
Definition arena.cc:24
const std::vector< chest_data > & GetChests() const
Definition room.h:323
uint8_t blockset() const
Definition room.h:1015
const std::vector< Door > & GetDoors() const
Definition room.h:413
uint8_t palette() const
Definition room.h:1019
uint8_t spriteset() const
Definition room.h:1018
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:316
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:445
#define ICON_MD_GRID_VIEW
Definition icons.h:897
#define ICON_MD_MY_LOCATION
Definition icons.h:1270
#define ICON_MD_ARROW_FORWARD
Definition icons.h:184
#define ICON_MD_SWAP_HORIZ
Definition icons.h:1896
#define ICON_MD_MORE_HORIZ
Definition icons.h:1241
#define ICON_MD_TAB
Definition icons.h:1930
#define ICON_MD_OPEN_IN_NEW
Definition icons.h:1354
#define ICON_MD_CLOSE
Definition icons.h:418
const AgentUITheme & GetTheme()
gfx::Bitmap & PrepareCanonicalRoomComposite(zelda3::Room &room, RoomCompositeOutput &output)
bool SameDungeonRoomDocument(const DungeonRoomDocument &a, const DungeonRoomDocument &b)
RoomSelectionIntent
Intent for room selection in the dungeon editor.
void ApplyDungeonRoomDocument(zelda3::Room &room, const DungeonRoomDocument &document)
DungeonRoomDocument CaptureDungeonRoomDocument(const zelda3::Room &room)
void EnsureCompositeBitmapTextureQueued(Bitmap &composite)
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
bool ItemContextMenu(const char *id, const std::function< std::vector< MenuItemSpec >()> &build, ImGuiPopupFlags flags)
Right-click menu for the last submitted ImGui item.
MenuItemSpec CopyToClipboardItem(const std::string &label, const std::string &text)
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
Definition room.cc:680
std::string GetRoomLabel(int id)
Convenience function to get a room label.
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:655
Centralized theme colors for Agent UI components.
Definition agent_theme.h:19
static CanvasMenuItem Conditional(const std::string &lbl, std::function< void()> cb, std::function< bool()> condition)
gfx::PaletteGroupMap palette_groups
Definition game_data.h:95
Automatic widget registration helpers for ImGui Test Engine integration.