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 if (show_placement_summary)
226 else
228}
229
233
235 // In agent mode, we might force tabs open or change layout
236 (void)enabled;
237}
238
239void ObjectSelectorContent::SetPlacementError(const std::string& message) {
240 // Avoid refreshing the timer for repeated identical errors; keeps the
241 // message stable during rapid blocked clicks.
242 if (message == last_placement_error_ && placement_error_time_ >= 0.0) {
243 double elapsed = ImGui::GetTime() - placement_error_time_;
244 if (elapsed < kPlacementErrorDuration) {
245 return;
246 }
247 }
248 last_placement_error_ = message;
249 placement_error_time_ = ImGui::GetTime();
250 if (toast_manager_) {
251 toast_manager_->Show(message, ToastType::kError, 4.0f);
252 }
253}
254
255void ObjectSelectorContent::DrawObjectSelector(float minimum_grid_height) {
256 // Delegate to the DungeonObjectSelector component
257 object_selector_.DrawObjectAssetBrowser(minimum_grid_height);
258}
259
261 bool show_placement_summary) {
262 const auto& theme = AgentUI::GetTheme();
263 auto* viewer = ResolveCanvasViewer();
264 const auto snapshot = viewer != nullptr
266 viewer->object_interaction(), viewer->rooms(),
267 viewer->current_room_id())
269
270 bool drew_primary_status = false;
271 if (!last_placement_error_.empty()) {
272 double elapsed = ImGui::GetTime() - placement_error_time_;
273 if (!toast_manager_ && elapsed < kPlacementErrorDuration) {
274 ImGui::TextColored(theme.status_error, ICON_MD_WARNING " %s",
275 last_placement_error_.c_str());
276 drew_primary_status = true;
277 } else if (elapsed >= kPlacementErrorDuration) {
278 last_placement_error_.clear();
279 }
280 }
281
282 const auto* preview =
283 viewer ? viewer->object_interaction().GetPlacementPreview() : nullptr;
284 const bool is_placing = preview != nullptr;
285 if (!is_placing && has_preview_object_) {
286 has_preview_object_ = false;
287 }
288
289 if (is_placing && show_placement_summary) {
290 if (drew_primary_status) {
291 ImGui::Spacing();
292 }
293 ImGui::TextColored(theme.status_warning,
294 ICON_MD_ADD_CIRCLE " Queued 0x%03X %s", preview->id_,
295 zelda3::GetObjectName(preview->id_).c_str());
296 const char* cancel_label = ICON_MD_CANCEL " Cancel";
297 const float cancel_width = ImGui::CalcTextSize(cancel_label).x +
298 ImGui::GetStyle().FramePadding.x * 2.0f;
299 const float next_x = ImGui::GetItemRectMax().x +
300 ImGui::GetStyle().ItemSpacing.x + cancel_width;
301 const float line_right =
302 ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
303 if (next_x <= line_right) {
304 ImGui::SameLine();
305 }
306 if (ImGui::SmallButton(cancel_label)) {
308 }
309 drew_primary_status = true;
310 }
311
312 // Capacity and validation detail belongs in the wider standalone picker.
313 // A full-height inspector is still narrow, so preserve that space for the
314 // object grid while keeping placement and error feedback visible above.
315 const ImVec2 available = ImGui::GetContentRegionAvail();
316 const bool show_secondary_status =
317 available.x >= 420.0f && available.y >= 260.0f;
318 if (!is_placing && show_secondary_status &&
319 snapshot.kind == DungeonSelectionKind::ObjectSingle) {
320 ImGui::TextColored(theme.status_success,
321 ICON_MD_CHECK_CIRCLE " 1 object selected");
322 DrawInlineInspectButton(open_object_editor_callback_);
323 } else if (!is_placing && show_secondary_status &&
324 snapshot.kind == DungeonSelectionKind::ObjectMulti) {
325 ImGui::TextColored(theme.status_success,
326 ICON_MD_SELECT_ALL " %zu objects selected",
327 snapshot.count);
328 DrawInlineInspectButton(open_object_editor_callback_);
329 } else if (!is_placing && show_secondary_status &&
330 (snapshot.kind == DungeonSelectionKind::Door ||
331 snapshot.kind == DungeonSelectionKind::Sprite ||
332 snapshot.kind == DungeonSelectionKind::Item)) {
333 ImGui::TextColored(theme.status_success,
334 ICON_MD_MANAGE_SEARCH " 1 %s selected",
335 GetDungeonSelectionKindLabel(snapshot.kind));
336 DrawInlineInspectButton(open_object_editor_callback_);
337 } else if (!is_placing && show_secondary_status &&
338 (snapshot.kind == DungeonSelectionKind::EntityMulti ||
339 snapshot.kind == DungeonSelectionKind::Mixed)) {
340 ImGui::TextColored(theme.status_success, ICON_MD_SELECT_ALL " %s",
341 GetDungeonSelectionSummaryText(snapshot).c_str());
342 DrawInlineInspectButton(open_object_editor_callback_);
343 }
344
345 if (!show_secondary_status) {
346 return;
347 }
348
349 auto* rooms = object_selector_.get_rooms();
350 if (rooms && current_room_id_ >= 0 &&
352 const auto& room = (*rooms)[current_room_id_];
353 size_t object_count = room.GetTileObjects().size();
354 size_t sprite_count = room.GetSprites().size();
355 size_t door_count = room.GetDoors().size();
356 int chest_count = 0;
357 for (const auto& obj : room.GetTileObjects()) {
360 chest_count++;
361 }
362 }
363
364 const int kMaxObjects = static_cast<int>(zelda3::kMaxTileObjects);
365 const int kMaxSprites = static_cast<int>(zelda3::kMaxTotalSprites);
366 const int kMaxDoors = static_cast<int>(zelda3::kMaxDoors);
367 const int kMaxChests = static_cast<int>(zelda3::kMaxChests);
368
369 auto usage_color = [&](size_t count, int max_val,
370 bool exact_capacity_warning) -> ImVec4 {
371 if (exact_capacity_warning) {
372 return GetPlacementSummaryColor(theme, count, max_val,
373 theme.text_secondary_gray);
374 }
375 float ratio = static_cast<float>(count) / static_cast<float>(max_val);
376 if (ratio >= 1.0f) {
377 return theme.status_error;
378 }
379 if (ratio >= 0.75f) {
380 return theme.status_warning;
381 }
382 return theme.text_secondary_gray;
383 };
384
385 zelda3::DungeonValidator validator;
386 auto result = validator.ValidateRoom(room);
387
388 ImGui::Spacing();
389 DrawUsageChips(
390 {{ICON_MD_WIDGETS, absl::StrFormat("%zu/%d", object_count, kMaxObjects),
391 usage_color(object_count, kMaxObjects, true)},
393 absl::StrFormat("%zu/%d", sprite_count, kMaxSprites),
394 usage_color(sprite_count, kMaxSprites, true)},
395 {ICON_MD_DOOR_FRONT, absl::StrFormat("%zu/%d", door_count, kMaxDoors),
396 usage_color(door_count, kMaxDoors, true)},
398 absl::StrFormat("%d/%d", chest_count, kMaxChests),
399 usage_color(chest_count, kMaxChests, false)}});
400
401 if (!result.errors.empty() || !result.warnings.empty()) {
402 ImGui::TextColored(
403 result.errors.empty() ? theme.status_warning : theme.status_error,
404 tr("%s %zu issue%s"),
405 result.errors.empty() ? ICON_MD_WARNING : ICON_MD_ERROR,
406 result.errors.size() + result.warnings.size(),
407 (result.errors.size() + result.warnings.size()) == 1 ? "" : "s");
408 if (ImGui::IsItemHovered()) {
409 ImGui::BeginTooltip();
410 for (const auto& err : result.errors) {
411 ImGui::TextColored(theme.status_error, ICON_MD_ERROR " %s",
412 err.c_str());
413 }
414 for (const auto& warn : result.warnings) {
415 ImGui::TextColored(theme.status_warning, ICON_MD_WARNING " %s",
416 warn.c_str());
417 }
418 ImGui::EndTooltip();
419 }
420 }
421 }
422}
423
431
441
442} // namespace editor
443} // 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:52
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