yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
tile_selector_widget.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstdio>
5
7
8namespace yaze::gui {
9
10namespace {
11
12std::string_view TrimAsciiWhitespace(std::string_view s) {
13 while (!s.empty() &&
14 (s.front() == ' ' || s.front() == '\t' || s.front() == '\n' ||
15 s.front() == '\r')) {
16 s.remove_prefix(1);
17 }
18 while (!s.empty() &&
19 (s.back() == ' ' || s.back() == '\t' || s.back() == '\n' ||
20 s.back() == '\r')) {
21 s.remove_suffix(1);
22 }
23 return s;
24}
25
26// Parse tile-id text using hex by default, with explicit decimal support via
27// "d:<value>".
28bool ParseTileIdText(std::string_view input, int* out_value) {
29 if (!out_value) {
30 return false;
31 }
32
33 std::string_view trimmed = TrimAsciiWhitespace(input);
34 if (trimmed.empty()) {
35 return false;
36 }
37
38 bool decimal_mode = false;
39 if (trimmed.size() >= 2 &&
40 (trimmed[0] == 'd' || trimmed[0] == 'D') &&
41 trimmed[1] == ':') {
42 decimal_mode = true;
43 trimmed.remove_prefix(2);
44 } else if (trimmed.size() >= 2 && trimmed[0] == '0' &&
45 (trimmed[1] == 'x' || trimmed[1] == 'X')) {
46 trimmed.remove_prefix(2);
47 }
48
49 if (trimmed.empty()) {
50 return false;
51 }
52
53 std::string text(trimmed);
54 unsigned int parsed = 0;
55 char trailing = '\0';
56 const char* format = decimal_mode ? "%u%c" : "%x%c";
57 if (std::sscanf(text.c_str(), format, &parsed, &trailing) != 1) {
58 return false;
59 }
60
61 *out_value = static_cast<int>(parsed);
62 return true;
63}
64
65} // namespace
66
68 : config_(),
69 total_tiles_(config_.total_tiles),
70 widget_id_(std::move(widget_id)) {}
71
72TileSelectorWidget::TileSelectorWidget(std::string widget_id, Config config)
73 : config_(config),
74 total_tiles_(config.total_tiles),
75 widget_id_(std::move(widget_id)) {}
76
78 canvas_ = canvas;
79}
80
91
98
100 bool atlas_ready) {
101 RenderResult result;
102
103 if (!canvas_) {
104 return result;
105 }
106
107 const int tile_display_size =
108 static_cast<int>(config_.tile_size * config_.display_scale);
109
110 // Calculate total content size for ImGui child window scrolling
111 const int num_rows =
113 const ImVec2 content_size(
114 config_.tiles_per_row * tile_display_size + config_.draw_offset.x * 2,
115 num_rows * tile_display_size + config_.draw_offset.y * 2);
116
117 // Set content size for ImGui child window (must be called before
118 // DrawBackground)
119 ImGui::SetCursorPos(ImVec2(0, 0));
120 ImGui::Dummy(content_size);
121 ImGui::SetCursorPos(ImVec2(0, 0));
122
123 // Handle pending scroll (deferred from ScrollToTile call outside render
124 // context)
125 if (pending_scroll_tile_id_ >= 0) {
127 const ImVec2 target = TileOrigin(pending_scroll_tile_id_);
129 const ImVec2 window_size = ImGui::GetWindowSize();
130 float scroll_x =
131 target.x - (window_size.x / 2.0f) + (tile_display_size / 2.0f);
132 float scroll_y =
133 target.y - (window_size.y / 2.0f) + (tile_display_size / 2.0f);
134 scroll_x = std::max(0.0f, scroll_x);
135 scroll_y = std::max(0.0f, scroll_y);
136 ImGui::SetScrollX(scroll_x);
137 ImGui::SetScrollY(scroll_y);
138 }
139 }
140 pending_scroll_tile_id_ = -1; // Clear pending scroll
141 }
142
145
146 if (atlas_ready && atlas.is_active()) {
147 canvas_->DrawBitmap(atlas, static_cast<int>(config_.draw_offset.x),
148 static_cast<int>(config_.draw_offset.y),
150
151 result = HandleInteraction(tile_display_size);
152
153 // Hover tooltip: show tile ID and zoomed preview
154 if (config_.show_hover_tooltip && ImGui::IsItemHovered()) {
155 int hovered_tile = ResolveTileAtCursor(tile_display_size);
156 if (IsValidTileId(hovered_tile)) {
157 ImGui::BeginTooltip();
158 ImGui::Text("Tile %d (0x%03X)", hovered_tile, hovered_tile);
159
160 // Extract and draw a zoomed preview of the hovered tile
161 int tile_col = hovered_tile % config_.tiles_per_row;
162 int tile_row = hovered_tile / config_.tiles_per_row;
163 int src_x = tile_col * config_.tile_size;
164 int src_y = tile_row * config_.tile_size;
165
166 // Use ImGui texture coords for the atlas to show the tile zoomed
167 auto* texture_id = atlas.texture();
168 if (texture_id != nullptr) {
169 float atlas_w = static_cast<float>(atlas.width());
170 float atlas_h = static_cast<float>(atlas.height());
171 if (atlas_w > 0 && atlas_h > 0) {
172 ImVec2 uv0(src_x / atlas_w, src_y / atlas_h);
173 ImVec2 uv1((src_x + config_.tile_size) / atlas_w,
174 (src_y + config_.tile_size) / atlas_h);
175 float preview_size = config_.tile_size * 4.0f;
176 ImGui::Image((ImTextureID)(intptr_t)texture_id,
177 ImVec2(preview_size, preview_size), uv0, uv1);
178 }
179 }
180
181 ImGui::EndTooltip();
182 }
183 }
184
186 DrawTileIdLabels(tile_display_size);
187 }
188
189 DrawHighlight(tile_display_size);
190 }
191
192 canvas_->DrawGrid();
194
195 return result;
196}
197
199 int tile_display_size) {
200 RenderResult result;
201
202 if (!ImGui::IsItemHovered()) {
203 return result;
204 }
205
206 const bool clicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
207 const bool double_clicked =
208 ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
209
210 if (clicked || double_clicked) {
211 const int hovered_tile = ResolveTileAtCursor(tile_display_size);
212 if (IsValidTileId(hovered_tile) && IsInFilterRange(hovered_tile)) {
213 result.tile_clicked = clicked;
214 result.tile_double_clicked = double_clicked;
215 if (hovered_tile != selected_tile_id_) {
216 selected_tile_id_ = hovered_tile;
217 result.selection_changed = true;
218 }
220 }
221 }
222
223 // Emit drag source for the selected tile when dragging is enabled
227 }
228
229 return result;
230}
231
232int TileSelectorWidget::ResolveTileAtCursor(int tile_display_size) const {
233 if (!canvas_) {
234 return -1;
235 }
236
237 const ImVec2 screen_pos = ImGui::GetIO().MousePos;
238 const ImVec2 origin = canvas_->zero_point();
239 const ImVec2 scroll = canvas_->scrolling();
240
241 // Convert screen position to canvas content position (accounting for scroll)
242 ImVec2 local =
243 ImVec2(screen_pos.x - origin.x - config_.draw_offset.x - scroll.x,
244 screen_pos.y - origin.y - config_.draw_offset.y - scroll.y);
245
246 if (local.x < 0.0f || local.y < 0.0f) {
247 return -1;
248 }
249
250 const int column = static_cast<int>(local.x / tile_display_size);
251 const int row = static_cast<int>(local.y / tile_display_size);
252
253 return row * config_.tiles_per_row + column;
254}
255
256void TileSelectorWidget::DrawHighlight(int tile_display_size) const {
258 return;
259 }
260
261 const int column = selected_tile_id_ % config_.tiles_per_row;
262 const int row = selected_tile_id_ / config_.tiles_per_row;
263
264 const float x = config_.draw_offset.x + column * tile_display_size;
265 const float y = config_.draw_offset.y + row * tile_display_size;
266
267 canvas_->DrawOutlineWithColor(static_cast<int>(x), static_cast<int>(y),
268 tile_display_size, tile_display_size,
270}
271
273 // Future enhancement: draw ImGui text overlay with tile indices.
274}
275
276void TileSelectorWidget::ScrollToTile(int tile_id, bool use_imgui_scroll) {
277 if (!canvas_ || !IsValidTileId(tile_id)) {
278 return;
279 }
280
281 // Defer scroll until next render (when we're in the correct ImGui window
282 // context)
283 pending_scroll_tile_id_ = tile_id;
284 pending_scroll_use_imgui_ = use_imgui_scroll;
285}
286
287ImVec2 TileSelectorWidget::TileOrigin(int tile_id) const {
288 if (!IsValidTileId(tile_id)) {
289 return ImVec2(-1, -1);
290 }
291 const int tile_display_size =
292 static_cast<int>(config_.tile_size * config_.display_scale);
293 const int column = tile_id % config_.tiles_per_row;
294 const int row = tile_id / config_.tiles_per_row;
295 return ImVec2(config_.draw_offset.x + column * tile_display_size,
296 config_.draw_offset.y + row * tile_display_size);
297}
298
300 std::string_view input) {
301 int tile_id = -1;
302 if (!ParseTileIdText(input, &tile_id)) {
304 return last_jump_result_;
305 }
306
307 if (!IsValidTileId(tile_id)) {
309 return last_jump_result_;
310 }
311
312 selected_tile_id_ = tile_id;
313 ScrollToTile(tile_id, true);
315 return last_jump_result_;
316}
317
319 bool jumped = false;
320 const int max_tile_id = GetMaxTileId();
321
322 constexpr ImGuiInputTextFlags kHexFlags =
323 ImGuiInputTextFlags_CharsHexadecimal |
324 ImGuiInputTextFlags_EnterReturnsTrue |
325 ImGuiInputTextFlags_AutoSelectAll;
326
327 ImGui::PushID(widget_id_.c_str());
328
329 // Jump-to-ID input
330 ImGui::PushItemWidth(64);
331 ImGui::AlignTextToFramePadding();
332 ImGui::TextUnformatted("Go:");
333 ImGui::SameLine();
334
335 if (ImGui::InputText("##TileFilterID", filter_buf_, sizeof(filter_buf_),
336 kHexFlags)) {
339 jumped = true;
340 break;
343 break;
344 }
345 }
346 if (ImGui::IsItemHovered()) {
347 ImGui::SetTooltip(
348 "Enter tile ID and press Enter:\n"
349 "hex: 1A or 0x1A\n"
350 "decimal: d:26");
351 }
352
353 ImGui::SameLine();
354 ImGui::TextDisabled("/ 0x%03X", max_tile_id);
356 ImGui::SameLine(0, 8.0f);
357 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Invalid hex ID");
359 ImGui::SameLine(0, 8.0f);
360 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
361 "Out of range (max: 0x%03X)", max_tile_id);
362 }
363
364 // Range filter inputs
365 ImGui::SameLine(0, 12.0f);
366 ImGui::TextUnformatted("Range:");
367 ImGui::SameLine();
368
369 bool range_changed = false;
370 if (ImGui::InputText("##RangeMin", filter_min_buf_, sizeof(filter_min_buf_),
371 kHexFlags)) {
372 range_changed = true;
373 }
374 if (ImGui::IsItemHovered()) {
375 ImGui::SetTooltip(
376 "Min tile ID. Press Enter to apply.\n"
377 "hex: 1A or 0x1A\n"
378 "decimal: d:26");
379 }
380 ImGui::SameLine();
381 ImGui::TextUnformatted("-");
382 ImGui::SameLine();
383 if (ImGui::InputText("##RangeMax", filter_max_buf_, sizeof(filter_max_buf_),
384 kHexFlags)) {
385 range_changed = true;
386 }
387 if (ImGui::IsItemHovered()) {
388 ImGui::SetTooltip(
389 "Max tile ID. Press Enter to apply.\n"
390 "hex: 1A or 0x1A\n"
391 "decimal: d:26");
392 }
393 ImGui::SameLine();
394 ImGui::TextDisabled("(hex, d:dec)");
395
396 if (range_changed) {
397 int parsed_min = 0;
398 int parsed_max = 0;
399 bool has_min = ParseTileIdText(filter_min_buf_, &parsed_min);
400 bool has_max = ParseTileIdText(filter_max_buf_, &parsed_max);
401
402 if (has_min && has_max && parsed_min <= parsed_max) {
403 SetRangeFilter(parsed_min, parsed_max);
404 filter_range_error_ = false;
406 filter_out_of_range_ = false;
408 } else {
409 // SetRangeFilter returned early: both values exceeded total_tiles_.
411 }
412 } else if (!has_min && !has_max) {
414 filter_range_error_ = false;
415 filter_out_of_range_ = false;
416 } else if (has_min && has_max && parsed_min > parsed_max) {
417 // Invalid range: min must be ≤ max
418 filter_range_error_ = true;
419 filter_out_of_range_ = false;
420 }
421 }
422
423 // Clear button when range is active
425 ImGui::SameLine();
426 if (ImGui::SmallButton("X##ClearRange")) {
428 filter_min_buf_[0] = '\0';
429 filter_max_buf_[0] = '\0';
430 filter_range_error_ = false;
431 }
432 if (ImGui::IsItemHovered()) {
433 ImGui::SetTooltip("Clear range filter");
434 }
435 }
436
437 // Validation feedback (shown inline after the filter inputs)
439 ImGui::SameLine(0, 8.0f);
440 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Min must be <= Max");
441 } else if (filter_out_of_range_) {
442 ImGui::SameLine(0, 8.0f);
443 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
444 "Out of range (max: 0x%03X)", GetMaxTileId());
445 } else if (filter_range_active_) {
446 int range_count = filter_range_max_ - filter_range_min_ + 1;
447 if (range_count <= 0) {
448 ImGui::SameLine(0, 8.0f);
449 ImGui::TextDisabled("(no tiles in range)");
450 }
451 }
452
453 ImGui::PopItemWidth();
454 ImGui::PopID();
455
456 return jumped;
457}
458
459void TileSelectorWidget::SetRangeFilter(int min_id, int max_id) {
460 if (min_id < 0) min_id = 0;
461 if (max_id >= total_tiles_) max_id = total_tiles_ - 1;
462 if (min_id > max_id) return;
463
465 filter_range_min_ = min_id;
466 filter_range_max_ = max_id;
467 filter_range_error_ = false;
468}
469
477
478bool TileSelectorWidget::IsValidTileId(int tile_id) const {
479 return tile_id >= 0 && tile_id < total_tiles_;
480}
481
482bool TileSelectorWidget::IsInFilterRange(int tile_id) const {
483 if (!filter_range_active_) return true;
484 return tile_id >= filter_range_min_ && tile_id <= filter_range_max_;
485}
486
487} // namespace yaze::gui
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
TextureHandle texture() const
Definition bitmap.h:380
bool is_active() const
Definition bitmap.h:384
int height() const
Definition bitmap.h:374
int width() const
Definition bitmap.h:373
Modern, robust canvas for drawing and manipulating graphics.
Definition canvas.h:150
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1157
void DrawOutlineWithColor(int x, int y, int w, int h, ImVec4 color)
Definition canvas.cc:1226
void DrawContextMenu()
Definition canvas.cc:684
auto zero_point() const
Definition canvas.h:443
auto scrolling() const
Definition canvas.h:445
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0))
Definition canvas.cc:590
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1480
JumpToTileResult JumpToTileFromInput(std::string_view input)
int ResolveTileAtCursor(int tile_display_size) const
RenderResult Render(gfx::Bitmap &atlas, bool atlas_ready)
bool IsValidTileId(int tile_id) const
void SetRangeFilter(int min_id, int max_id)
ImVec2 TileOrigin(int tile_id) const
RenderResult HandleInteraction(int tile_display_size)
void ScrollToTile(int tile_id, bool use_imgui_scroll=true)
void DrawTileIdLabels(int tile_display_size) const
void DrawHighlight(int tile_display_size) const
bool IsInFilterRange(int tile_id) const
TileSelectorWidget(std::string widget_id)
bool ParseTileIdText(std::string_view input, int *out_value)
Graphical User Interface (GUI) components for the application.
bool BeginTileDragSource(int tile_id, int map_id)
Definition drag_drop.h:54