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;
125
126 const int max_objects = static_cast<int>(zelda3::kMaxTileObjects);
127 const int max_sprites = static_cast<int>(zelda3::kMaxTotalSprites);
128 const int max_doors = static_cast<int>(zelda3::kMaxDoors);
129
130 // Check if placement was blocked by ROM limits
131 if (canvas_viewer_) {
132 auto& coordinator =
134
135 auto& tile_handler = coordinator.tile_handler();
136 if (tile_handler.was_placement_blocked()) {
137 const auto reason = tile_handler.placement_block_reason();
138 tile_handler.clear_placement_blocked();
139 switch (reason) {
141 SetPlacementError(absl::StrFormat(
142 "Object limit reached (%d max) - placement blocked",
143 max_objects));
144 break;
146 SetPlacementError("Invalid room target - placement blocked");
147 break;
149 default:
150 SetPlacementError("Object placement blocked");
151 break;
152 }
153 }
154
155 auto& sprite_handler = coordinator.sprite_handler();
156 if (sprite_handler.was_placement_blocked()) {
157 const auto reason = sprite_handler.placement_block_reason();
158 sprite_handler.clear_placement_blocked();
159 switch (reason) {
161 SetPlacementError(absl::StrFormat(
162 "Sprite limit reached (%d max) - placement blocked",
163 max_sprites));
164 break;
166 SetPlacementError("Invalid room target - sprite placement blocked");
167 break;
169 default:
170 SetPlacementError("Sprite placement blocked");
171 break;
172 }
173 }
174
175 auto& door_handler = coordinator.door_handler();
176 if (door_handler.was_placement_blocked()) {
177 const auto reason = door_handler.placement_block_reason();
178 door_handler.clear_placement_blocked();
179 switch (reason) {
181 SetPlacementError(absl::StrFormat(
182 "Door limit reached (%d max) - placement blocked", max_doors));
183 break;
185 SetPlacementError("Invalid door position - must be near a wall");
186 break;
188 SetPlacementError("Invalid room target - door placement blocked");
189 break;
191 default:
192 SetPlacementError("Door placement blocked");
193 break;
194 }
195 }
196 }
197
200}
201
205
207 // In agent mode, we might force tabs open or change layout
208 (void)enabled;
209}
210
211void ObjectSelectorContent::SetPlacementError(const std::string& message) {
212 // Avoid refreshing the timer for repeated identical errors; keeps the
213 // message stable during rapid blocked clicks.
214 if (message == last_placement_error_ && placement_error_time_ >= 0.0) {
215 double elapsed = ImGui::GetTime() - placement_error_time_;
216 if (elapsed < kPlacementErrorDuration) {
217 return;
218 }
219 }
220 last_placement_error_ = message;
221 placement_error_time_ = ImGui::GetTime();
222 if (toast_manager_) {
223 toast_manager_->Show(message, ToastType::kError, 4.0f);
224 }
225}
226
228 // Delegate to the DungeonObjectSelector component
230}
231
233 const auto& theme = AgentUI::GetTheme();
234 auto* viewer = ResolveCanvasViewer();
235 const auto snapshot = viewer != nullptr
237 viewer->object_interaction(), viewer->rooms(),
238 viewer->current_room_id())
240
241 bool drew_primary_status = false;
242 if (!last_placement_error_.empty()) {
243 double elapsed = ImGui::GetTime() - placement_error_time_;
244 if (!toast_manager_ && elapsed < kPlacementErrorDuration) {
245 ImGui::TextColored(theme.status_error, ICON_MD_WARNING " %s",
246 last_placement_error_.c_str());
247 drew_primary_status = true;
248 } else if (elapsed >= kPlacementErrorDuration) {
249 last_placement_error_.clear();
250 }
251 }
252
253 bool is_placing = has_preview_object_ && canvas_viewer_ &&
255 if (!is_placing && has_preview_object_) {
256 has_preview_object_ = false;
257 }
258
259 if (is_placing) {
260 if (drew_primary_status) {
261 ImGui::Spacing();
262 }
263 ImGui::TextColored(theme.status_warning,
264 ICON_MD_ADD_CIRCLE " Queued 0x%03X %s",
267 const char* cancel_label = ICON_MD_CANCEL " Cancel";
268 const float cancel_width = ImGui::CalcTextSize(cancel_label).x +
269 ImGui::GetStyle().FramePadding.x * 2.0f;
270 const float next_x = ImGui::GetItemRectMax().x +
271 ImGui::GetStyle().ItemSpacing.x + cancel_width;
272 const float line_right =
273 ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
274 if (next_x <= line_right) {
275 ImGui::SameLine();
276 }
277 if (ImGui::SmallButton(cancel_label)) {
279 }
280 drew_primary_status = true;
281 }
282
283 // Capacity and validation detail belongs in the wider standalone picker.
284 // A full-height inspector is still narrow, so preserve that space for the
285 // object grid while keeping placement and error feedback visible above.
286 const ImVec2 available = ImGui::GetContentRegionAvail();
287 const bool show_secondary_status =
288 available.x >= 420.0f && available.y >= 260.0f;
289 if (!is_placing && show_secondary_status &&
290 snapshot.kind == DungeonSelectionKind::ObjectSingle) {
291 ImGui::TextColored(theme.status_success,
292 ICON_MD_CHECK_CIRCLE " 1 object selected");
293 DrawInlineInspectButton(open_object_editor_callback_);
294 } else if (!is_placing && show_secondary_status &&
295 snapshot.kind == DungeonSelectionKind::ObjectMulti) {
296 ImGui::TextColored(theme.status_success,
297 ICON_MD_SELECT_ALL " %zu objects selected",
298 snapshot.count);
299 DrawInlineInspectButton(open_object_editor_callback_);
300 } else if (!is_placing && show_secondary_status &&
301 (snapshot.kind == DungeonSelectionKind::Door ||
302 snapshot.kind == DungeonSelectionKind::Sprite ||
303 snapshot.kind == DungeonSelectionKind::Item)) {
304 ImGui::TextColored(theme.status_success,
305 ICON_MD_MANAGE_SEARCH " 1 %s selected",
306 GetDungeonSelectionKindLabel(snapshot.kind));
307 DrawInlineInspectButton(open_object_editor_callback_);
308 } else if (!is_placing && show_secondary_status &&
309 (snapshot.kind == DungeonSelectionKind::EntityMulti ||
310 snapshot.kind == DungeonSelectionKind::Mixed)) {
311 ImGui::TextColored(theme.status_success, ICON_MD_SELECT_ALL " %s",
312 GetDungeonSelectionSummaryText(snapshot).c_str());
313 DrawInlineInspectButton(open_object_editor_callback_);
314 }
315
316 if (!show_secondary_status) {
317 return;
318 }
319
320 auto* rooms = object_selector_.get_rooms();
321 if (rooms && current_room_id_ >= 0 &&
323 const auto& room = (*rooms)[current_room_id_];
324 size_t object_count = room.GetTileObjects().size();
325 size_t sprite_count = room.GetSprites().size();
326 size_t door_count = room.GetDoors().size();
327 int chest_count = 0;
328 for (const auto& obj : room.GetTileObjects()) {
331 chest_count++;
332 }
333 }
334
335 const int kMaxObjects = static_cast<int>(zelda3::kMaxTileObjects);
336 const int kMaxSprites = static_cast<int>(zelda3::kMaxTotalSprites);
337 const int kMaxDoors = static_cast<int>(zelda3::kMaxDoors);
338 const int kMaxChests = static_cast<int>(zelda3::kMaxChests);
339
340 auto usage_color = [&](size_t count, int max_val,
341 bool exact_capacity_warning) -> ImVec4 {
342 if (exact_capacity_warning) {
343 return GetPlacementSummaryColor(theme, count, max_val,
344 theme.text_secondary_gray);
345 }
346 float ratio = static_cast<float>(count) / static_cast<float>(max_val);
347 if (ratio >= 1.0f) {
348 return theme.status_error;
349 }
350 if (ratio >= 0.75f) {
351 return theme.status_warning;
352 }
353 return theme.text_secondary_gray;
354 };
355
356 zelda3::DungeonValidator validator;
357 auto result = validator.ValidateRoom(room);
358
359 ImGui::Spacing();
360 DrawUsageChips(
361 {{ICON_MD_WIDGETS, absl::StrFormat("%zu/%d", object_count, kMaxObjects),
362 usage_color(object_count, kMaxObjects, true)},
364 absl::StrFormat("%zu/%d", sprite_count, kMaxSprites),
365 usage_color(sprite_count, kMaxSprites, true)},
366 {ICON_MD_DOOR_FRONT, absl::StrFormat("%zu/%d", door_count, kMaxDoors),
367 usage_color(door_count, kMaxDoors, true)},
369 absl::StrFormat("%d/%d", chest_count, kMaxChests),
370 usage_color(chest_count, kMaxChests, false)}});
371
372 if (!result.errors.empty() || !result.warnings.empty()) {
373 ImGui::TextColored(
374 result.errors.empty() ? theme.status_warning : theme.status_error,
375 tr("%s %zu issue%s"),
376 result.errors.empty() ? ICON_MD_WARNING : ICON_MD_ERROR,
377 result.errors.size() + result.warnings.size(),
378 (result.errors.size() + result.warnings.size()) == 1 ? "" : "s");
379 if (ImGui::IsItemHovered()) {
380 ImGui::BeginTooltip();
381 for (const auto& err : result.errors) {
382 ImGui::TextColored(theme.status_error, ICON_MD_ERROR " %s",
383 err.c_str());
384 }
385 for (const auto& warn : result.warnings) {
386 ImGui::TextColored(theme.status_warning, ICON_MD_WARNING " %s",
387 warn.c_str());
388 }
389 ImGui::EndTooltip();
390 }
391 }
392 }
393}
394
402
412
413} // namespace editor
414} // 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:28
DungeonObjectInteraction & object_interaction()
void SetPreviewObject(const zelda3::RoomObject &object)
InteractionCoordinator & entity_coordinator()
Get the interaction coordinator for entity handling.
void SelectObject(int obj_id, int subtype=-1)
void SetPlacementInvalidatedCallback(std::function< void()> callback)
void SetObjectSelectedCallback(std::function< void(const zelda3::RoomObject &)> callback)
void SetPlacementError(const std::string &message)
std::function< DungeonCanvasViewer *()> canvas_viewer_provider_
std::shared_ptr< zelda3::DungeonObjectEditor > object_editor_
ObjectSelectorContent(Rom *rom, DungeonCanvasViewer *canvas_viewer, std::shared_ptr< zelda3::DungeonObjectEditor > object_editor=nullptr, ToastManager *toast_manager=nullptr)
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)
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