yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
object_selector_content.cc
Go to the documentation of this file.
1// Related header
3#include <algorithm>
4#include <cstddef>
5#include <cstdint>
6#include <functional>
7#include <initializer_list>
8#include <memory>
9#include <string>
10#include <vector>
11#include "util/i18n/tr.h"
12
13// Third-party library headers
14#include "absl/strings/str_format.h"
17#include "imgui/imgui.h"
18
19// Project headers
24#include "app/gui/core/icons.h"
26#include "rom/rom.h"
32
33namespace yaze {
34namespace editor {
35
36namespace {
37
38struct UsageChip {
39 const char* icon = "";
40 std::string value;
41 ImVec4 color;
42};
43
44void DrawInlineInspectButton(const std::function<void()>& callback) {
45 if (!callback) {
46 return;
47 }
48
49 const char* label = ICON_MD_OPEN_IN_NEW " Inspect";
50 const ImGuiStyle& style = ImGui::GetStyle();
51 const float button_width =
52 ImGui::CalcTextSize(label).x + style.FramePadding.x * 2.0f;
53 const float next_x =
54 ImGui::GetItemRectMax().x + style.ItemSpacing.x + button_width;
55 const float line_right =
56 ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
57 if (next_x <= line_right) {
58 ImGui::SameLine();
59 }
60 if (ImGui::SmallButton(label)) {
61 callback();
62 }
63}
64
65void DrawUsageChips(std::initializer_list<UsageChip> chips) {
66 if (chips.size() == 0) {
67 return;
68 }
69
70 const int columns = ImGui::GetContentRegionAvail().x < 330.0f ? 2 : 4;
71 constexpr ImGuiTableFlags kFlags =
72 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_NoPadOuterX;
73 if (!ImGui::BeginTable("##ObjectSelectorUsageChips", columns, kFlags)) {
74 return;
75 }
76
77 for (const UsageChip& chip : chips) {
78 ImGui::TableNextColumn();
79 ImGui::TextColored(chip.color, "%s %s", chip.icon, chip.value.c_str());
80 }
81 ImGui::EndTable();
82}
83
84} // namespace
85
87 Rom* rom, DungeonCanvasViewer* canvas_viewer,
88 std::shared_ptr<zelda3::DungeonObjectEditor> object_editor,
89 ToastManager* toast_manager)
90 : rom_(rom),
91 canvas_viewer_(canvas_viewer),
92 object_selector_(rom),
93 object_editor_(object_editor),
94 toast_manager_(toast_manager) {
95 // Wire up object selector callback
97 [this](const zelda3::RoomObject& obj) {
98 preview_object_ = obj;
100 if (canvas_viewer_) {
103 }
104
105 // Sync with backend editor if available
106 if (object_editor_) {
108 object_editor_->SetCurrentObjectType(obj.id_);
109 }
110 });
112 [this]() { CancelPlacement(); });
113}
114
121
122void ObjectSelectorContent::Draw(bool* p_open) {
123 (void)p_open;
124 DrawContent(true);
125}
126
131
135
136void ObjectSelectorContent::DrawContent(bool show_placement_summary) {
138
139 const int max_objects = static_cast<int>(zelda3::kMaxTileObjects);
140 const int max_sprites = static_cast<int>(zelda3::kMaxTotalSprites);
141 const int max_doors = static_cast<int>(zelda3::kMaxDoors);
142
143 // Check if placement was blocked by ROM limits
144 if (canvas_viewer_) {
145 auto& coordinator =
147
148 auto& tile_handler = coordinator.tile_handler();
149 if (tile_handler.was_placement_blocked()) {
150 const auto reason = tile_handler.placement_block_reason();
151 tile_handler.clear_placement_blocked();
152 switch (reason) {
154 SetPlacementError(absl::StrFormat(
155 "Object limit reached (%d max) - placement blocked",
156 max_objects));
157 break;
159 SetPlacementError("Invalid room target - placement blocked");
160 break;
163 std::string(tile_handler.mutation_status().message()));
164 break;
166 default:
167 SetPlacementError("Object placement blocked");
168 break;
169 }
170 }
171
172 auto& sprite_handler = coordinator.sprite_handler();
173 if (sprite_handler.was_placement_blocked()) {
174 const auto reason = sprite_handler.placement_block_reason();
175 sprite_handler.clear_placement_blocked();
176 switch (reason) {
178 SetPlacementError(absl::StrFormat(
179 "Sprite limit reached (%d max) - placement blocked",
180 max_sprites));
181 break;
183 SetPlacementError("Invalid room target - sprite placement blocked");
184 break;
187 std::string(sprite_handler.mutation_status().message()));
188 break;
190 default:
191 SetPlacementError("Sprite placement blocked");
192 break;
193 }
194 }
195
196 auto& door_handler = coordinator.door_handler();
197 if (door_handler.was_placement_blocked()) {
198 const auto reason = door_handler.placement_block_reason();
199 door_handler.clear_placement_blocked();
200 switch (reason) {
202 SetPlacementError(absl::StrFormat(
203 "Door limit reached (%d max) - placement blocked", max_doors));
204 break;
206 SetPlacementError("Invalid door position - must be near a wall");
207 break;
209 SetPlacementError("Invalid room target - door placement blocked");
210 break;
213 std::string(door_handler.mutation_status().message()));
214 break;
216 default:
217 SetPlacementError("Door placement blocked");
218 break;
219 }
220 }
221 }
222
223 DrawInteractionSummary(show_placement_summary);
224 // Let the Workbench's parent scroll when placement controls consume the
225 // viewport; the asset grid must remain large enough to choose another object.
226 DrawObjectSelector(show_placement_summary
227 ? 1.0f
228 : ImGui::GetTextLineHeightWithSpacing() * 8.0f);
229}
230
234
236 // In agent mode, we might force tabs open or change layout
237 (void)enabled;
238}
239
240void ObjectSelectorContent::SetPlacementError(const std::string& message) {
241 // Avoid refreshing the timer for repeated identical errors; keeps the
242 // message stable during rapid blocked clicks.
243 if (message == last_placement_error_ && placement_error_time_ >= 0.0) {
244 double elapsed = ImGui::GetTime() - placement_error_time_;
245 if (elapsed < kPlacementErrorDuration) {
246 return;
247 }
248 }
249 last_placement_error_ = message;
250 placement_error_time_ = ImGui::GetTime();
251 if (toast_manager_) {
252 toast_manager_->Show(message, ToastType::kError, 4.0f);
253 }
254}
255
256void ObjectSelectorContent::DrawObjectSelector(float minimum_grid_height) {
257 // Delegate to the DungeonObjectSelector component
258 object_selector_.DrawObjectAssetBrowser(minimum_grid_height);
259}
260
262 bool show_placement_summary) {
263 const auto& theme = AgentUI::GetTheme();
264 auto* viewer = ResolveCanvasViewer();
265 const auto snapshot = viewer != nullptr
267 viewer->object_interaction(), viewer->rooms(),
268 viewer->current_room_id())
270
271 bool drew_primary_status = false;
272 if (!last_placement_error_.empty()) {
273 double elapsed = ImGui::GetTime() - placement_error_time_;
274 if (!toast_manager_ && elapsed < kPlacementErrorDuration) {
275 ImGui::TextColored(theme.status_error, ICON_MD_WARNING " %s",
276 last_placement_error_.c_str());
277 drew_primary_status = true;
278 } else if (elapsed >= kPlacementErrorDuration) {
279 last_placement_error_.clear();
280 }
281 }
282
283 const auto* preview =
284 viewer ? viewer->object_interaction().GetPlacementPreview() : nullptr;
285 const bool is_placing = preview != nullptr;
286 if (!is_placing && has_preview_object_) {
287 has_preview_object_ = false;
288 }
289
290 if (is_placing && show_placement_summary) {
291 if (drew_primary_status) {
292 ImGui::Spacing();
293 }
294 ImGui::TextColored(theme.status_warning,
295 ICON_MD_ADD_CIRCLE " Queued 0x%03X %s", preview->id_,
296 zelda3::GetObjectName(preview->id_).c_str());
297 const char* cancel_label = ICON_MD_CANCEL " Cancel";
298 const float cancel_width = ImGui::CalcTextSize(cancel_label).x +
299 ImGui::GetStyle().FramePadding.x * 2.0f;
300 const float next_x = ImGui::GetItemRectMax().x +
301 ImGui::GetStyle().ItemSpacing.x + cancel_width;
302 const float line_right =
303 ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
304 if (next_x <= line_right) {
305 ImGui::SameLine();
306 }
307 if (ImGui::SmallButton(cancel_label)) {
309 }
310 drew_primary_status = true;
311 }
312
313 // Capacity and validation detail belongs in the wider standalone picker.
314 // A full-height inspector is still narrow, so preserve that space for the
315 // object grid while keeping placement and error feedback visible above.
316 const ImVec2 available = ImGui::GetContentRegionAvail();
317 const bool show_secondary_status =
318 available.x >= 420.0f && available.y >= 260.0f;
319 if (!is_placing && show_secondary_status &&
320 snapshot.kind == DungeonSelectionKind::ObjectSingle) {
321 ImGui::TextColored(theme.status_success,
322 ICON_MD_CHECK_CIRCLE " 1 object selected");
323 DrawInlineInspectButton(open_object_editor_callback_);
324 } else if (!is_placing && show_secondary_status &&
325 snapshot.kind == DungeonSelectionKind::ObjectMulti) {
326 ImGui::TextColored(theme.status_success,
327 ICON_MD_SELECT_ALL " %zu objects selected",
328 snapshot.count);
329 DrawInlineInspectButton(open_object_editor_callback_);
330 } else if (!is_placing && show_secondary_status &&
331 (snapshot.kind == DungeonSelectionKind::Door ||
332 snapshot.kind == DungeonSelectionKind::Sprite ||
333 snapshot.kind == DungeonSelectionKind::Item)) {
334 ImGui::TextColored(theme.status_success,
335 ICON_MD_MANAGE_SEARCH " 1 %s selected",
336 GetDungeonSelectionKindLabel(snapshot.kind));
337 DrawInlineInspectButton(open_object_editor_callback_);
338 } else if (!is_placing && show_secondary_status &&
339 (snapshot.kind == DungeonSelectionKind::EntityMulti ||
340 snapshot.kind == DungeonSelectionKind::Mixed)) {
341 ImGui::TextColored(theme.status_success, ICON_MD_SELECT_ALL " %s",
342 GetDungeonSelectionSummaryText(snapshot).c_str());
343 DrawInlineInspectButton(open_object_editor_callback_);
344 }
345
346 if (!show_secondary_status) {
347 return;
348 }
349
350 auto* rooms = object_selector_.get_rooms();
351 if (rooms && current_room_id_ >= 0 &&
353 const auto& room = (*rooms)[current_room_id_];
354 size_t object_count = room.GetTileObjects().size();
355 size_t sprite_count = room.GetSprites().size();
356 size_t door_count = room.GetDoors().size();
357 int chest_count = 0;
358 for (const auto& obj : room.GetTileObjects()) {
361 chest_count++;
362 }
363 }
364
365 const int kMaxObjects = static_cast<int>(zelda3::kMaxTileObjects);
366 const int kMaxSprites = static_cast<int>(zelda3::kMaxTotalSprites);
367 const int kMaxDoors = static_cast<int>(zelda3::kMaxDoors);
368 const int kMaxChests = static_cast<int>(zelda3::kMaxChests);
369
370 auto usage_color = [&](size_t count, int max_val,
371 bool exact_capacity_warning) -> ImVec4 {
372 if (exact_capacity_warning) {
373 return GetPlacementSummaryColor(theme, count, max_val,
374 theme.text_secondary_gray);
375 }
376 float ratio = static_cast<float>(count) / static_cast<float>(max_val);
377 if (ratio >= 1.0f) {
378 return theme.status_error;
379 }
380 if (ratio >= 0.75f) {
381 return theme.status_warning;
382 }
383 return theme.text_secondary_gray;
384 };
385
386 zelda3::DungeonValidator validator;
387 auto result = validator.ValidateRoom(room);
388
389 ImGui::Spacing();
390 DrawUsageChips(
391 {{ICON_MD_WIDGETS, absl::StrFormat("%zu/%d", object_count, kMaxObjects),
392 usage_color(object_count, kMaxObjects, true)},
394 absl::StrFormat("%zu/%d", sprite_count, kMaxSprites),
395 usage_color(sprite_count, kMaxSprites, true)},
396 {ICON_MD_DOOR_FRONT, absl::StrFormat("%zu/%d", door_count, kMaxDoors),
397 usage_color(door_count, kMaxDoors, true)},
399 absl::StrFormat("%d/%d", chest_count, kMaxChests),
400 usage_color(chest_count, kMaxChests, false)}});
401
402 if (!result.errors.empty() || !result.warnings.empty()) {
403 ImGui::TextColored(
404 result.errors.empty() ? theme.status_warning : theme.status_error,
405 tr("%s %zu issue%s"),
406 result.errors.empty() ? ICON_MD_WARNING : ICON_MD_ERROR,
407 result.errors.size() + result.warnings.size(),
408 (result.errors.size() + result.warnings.size()) == 1 ? "" : "s");
409 if (ImGui::IsItemHovered()) {
410 ImGui::BeginTooltip();
411 for (const auto& err : result.errors) {
412 ImGui::TextColored(theme.status_error, ICON_MD_ERROR " %s",
413 err.c_str());
414 }
415 for (const auto& warn : result.warnings) {
416 ImGui::TextColored(theme.status_warning, ICON_MD_WARNING " %s",
417 warn.c_str());
418 }
419 ImGui::EndTooltip();
420 }
421 }
422 }
423}
424
432
442
443} // namespace editor
444} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:49
DungeonObjectInteraction & object_interaction()
void SetPreviewObject(const zelda3::RoomObject &object)
InteractionCoordinator & entity_coordinator()
Get the interaction coordinator for entity handling.
void DrawObjectAssetBrowser(float minimum_grid_height=1.0f)
void SelectObject(int obj_id, int subtype=-1)
void SetPlacementInvalidatedCallback(std::function< void()> callback)
void SetObjectSelectedCallback(std::function< void(const zelda3::RoomObject &)> callback)
void DrawObjectSelector(float minimum_grid_height=1.0f)
void SetPlacementError(const std::string &message)
std::function< DungeonCanvasViewer *()> canvas_viewer_provider_
std::shared_ptr< zelda3::DungeonObjectEditor > object_editor_
void DrawContent(bool show_placement_summary)
ObjectSelectorContent(Rom *rom, DungeonCanvasViewer *canvas_viewer, std::shared_ptr< zelda3::DungeonObjectEditor > object_editor=nullptr, ToastManager *toast_manager=nullptr)
void DrawInteractionSummary(bool show_placement_summary)
void Draw(bool *p_open) override
Draw the panel content.
PlacementBlockReason placement_block_reason() const
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f, std::string action_label={}, std::function< void()> on_action=nullptr)
ValidationResult ValidateRoom(const Room &room)
#define ICON_MD_CANCEL
Definition icons.h:364
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_WIDGETS
Definition icons.h:2156
#define ICON_MD_ERROR
Definition icons.h:686
#define ICON_MD_MANAGE_SEARCH
Definition icons.h:1172
#define ICON_MD_DOOR_FRONT
Definition icons.h:613
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_PEST_CONTROL
Definition icons.h:1429
#define ICON_MD_SELECT_ALL
Definition icons.h:1680
#define ICON_MD_OPEN_IN_NEW
Definition icons.h:1354
#define ICON_MD_INVENTORY_2
Definition icons.h:1012
#define ICON_MD_ADD_CIRCLE
Definition icons.h:95
const AgentUITheme & GetTheme()
void DrawInlineInspectButton(const std::function< void()> &callback)
void DrawUsageChips(std::initializer_list< UsageChip > chips)
ImVec4 GetPlacementSummaryColor(const AgentUITheme &theme, size_t current_count, size_t max_count, const ImVec4 &normal_color)
DungeonSelectionSnapshot BuildDungeonSelectionSnapshot(const DungeonObjectInteraction &interaction, const DungeonRoomStore *rooms, int room_id)
const char * GetDungeonSelectionKindLabel(DungeonSelectionKind kind)
std::string GetDungeonSelectionSummaryText(const DungeonSelectionSnapshot &snapshot)
constexpr size_t kMaxTileObjects
bool UsesRoomObjectStream(const RoomObject &object)
constexpr size_t kMaxDoors
std::string GetObjectName(int object_id)
constexpr int kNumberOfRooms
constexpr bool IsStatefulChestObjectId(int object_id)
constexpr size_t kMaxChests
constexpr size_t kMaxTotalSprites