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#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <cstdio>
6
8
9namespace yaze::gui {
10
11namespace {
12
13std::string_view TrimAsciiWhitespace(std::string_view s) {
14 while (!s.empty() && (s.front() == ' ' || s.front() == '\t' ||
15 s.front() == '\n' || s.front() == '\r')) {
16 s.remove_prefix(1);
17 }
18 while (!s.empty() && (s.back() == ' ' || s.back() == '\t' ||
19 s.back() == '\n' || s.back() == '\r')) {
20 s.remove_suffix(1);
21 }
22 return s;
23}
24
25// Parse tile-id text using hex by default, with explicit decimal support via
26// "d:<value>".
27bool ParseTileIdText(std::string_view input, int* out_value) {
28 if (!out_value) {
29 return false;
30 }
31
32 std::string_view trimmed = TrimAsciiWhitespace(input);
33 if (trimmed.empty()) {
34 return false;
35 }
36
37 bool decimal_mode = false;
38 if (trimmed.size() >= 2 && (trimmed[0] == 'd' || trimmed[0] == 'D') &&
39 trimmed[1] == ':') {
40 decimal_mode = true;
41 trimmed.remove_prefix(2);
42 } else if (trimmed.size() >= 2 && trimmed[0] == '0' &&
43 (trimmed[1] == 'x' || trimmed[1] == 'X')) {
44 trimmed.remove_prefix(2);
45 }
46
47 if (trimmed.empty()) {
48 return false;
49 }
50
51 std::string text(trimmed);
52 unsigned int parsed = 0;
53 char trailing = '\0';
54 const char* format = decimal_mode ? "%u%c" : "%x%c";
55 if (std::sscanf(text.c_str(), format, &parsed, &trailing) != 1) {
56 return false;
57 }
58
59 *out_value = static_cast<int>(parsed);
60 return true;
61}
62
63} // namespace
64
66 : config_(),
67 total_tiles_(config_.total_tiles),
68 widget_id_(std::move(widget_id)) {}
69
70TileSelectorWidget::TileSelectorWidget(std::string widget_id, Config config)
71 : config_(config),
72 total_tiles_(config.total_tiles),
73 widget_id_(std::move(widget_id)) {}
74
76 canvas_ = canvas;
77}
78
89
96
98 const int tile_display_size =
99 static_cast<int>(config_.tile_size * config_.display_scale);
100 const int num_rows =
102 return ImVec2(
103 config_.tiles_per_row * tile_display_size + config_.draw_offset.x * 2,
104 num_rows * tile_display_size + config_.draw_offset.y * 2);
105}
106
108 const float grid_width = GetGridContentSize().x;
109 // Leave enough room for the jump/range controls before the filter bar wraps,
110 // while still allowing a narrower compact layout when the dock is squeezed.
111 return std::max(grid_width + 18.0f, 332.0f);
112}
113
115 bool atlas_ready) {
116 RenderResult result;
117
118 if (!canvas_) {
119 return result;
120 }
121
122 const int tile_display_size =
123 static_cast<int>(config_.tile_size * config_.display_scale);
124 const ImVec2 content_size = GetGridContentSize();
125
126 // Set content size for ImGui child window (must be called before
127 // DrawBackground)
128 ImGui::SetCursorPos(ImVec2(0, 0));
129 ImGui::Dummy(content_size);
130 ImGui::SetCursorPos(ImVec2(0, 0));
131
132 // Handle pending scroll (deferred from ScrollToTile call outside render
133 // context)
134 if (pending_scroll_tile_id_ >= 0) {
136 const ImVec2 target = TileOrigin(pending_scroll_tile_id_);
138 const ImVec2 window_size = ImGui::GetWindowSize();
139 float scroll_x =
140 target.x - (window_size.x / 2.0f) + (tile_display_size / 2.0f);
141 float scroll_y =
142 target.y - (window_size.y / 2.0f) + (tile_display_size / 2.0f);
143 scroll_x = std::max(0.0f, scroll_x);
144 scroll_y = std::max(0.0f, scroll_y);
145 ImGui::SetScrollX(scroll_x);
146 ImGui::SetScrollY(scroll_y);
147 }
148 }
149 pending_scroll_tile_id_ = -1; // Clear pending scroll
150 }
151
153
154 if (atlas_ready && atlas.is_active()) {
155 canvas_->DrawBitmap(atlas, static_cast<int>(config_.draw_offset.x),
156 static_cast<int>(config_.draw_offset.y),
158
159 result = HandleInteraction(tile_display_size);
160
161 // Hover tooltip: show tile ID and zoomed preview
162 if (config_.show_hover_tooltip && ImGui::IsItemHovered()) {
163 int hovered_tile = ResolveTileAtCursor(tile_display_size);
164 if (IsValidTileId(hovered_tile)) {
165 ImGui::BeginTooltip();
166 ImGui::Text(tr("Tile %d (0x%03X)"), hovered_tile, hovered_tile);
167
168 // Extract and draw a zoomed preview of the hovered tile
169 int tile_col = hovered_tile % config_.tiles_per_row;
170 int tile_row = hovered_tile / config_.tiles_per_row;
171 int src_x = tile_col * config_.tile_size;
172 int src_y = tile_row * config_.tile_size;
173
174 // Use ImGui texture coords for the atlas to show the tile zoomed
175 auto* texture_id = atlas.texture();
176 if (texture_id != nullptr) {
177 float atlas_w = static_cast<float>(atlas.width());
178 float atlas_h = static_cast<float>(atlas.height());
179 if (atlas_w > 0 && atlas_h > 0) {
180 ImVec2 uv0(src_x / atlas_w, src_y / atlas_h);
181 ImVec2 uv1((src_x + config_.tile_size) / atlas_w,
182 (src_y + config_.tile_size) / atlas_h);
183 float preview_size = config_.tile_size * 4.0f;
184 ImGui::Image((ImTextureID)(intptr_t)texture_id,
185 ImVec2(preview_size, preview_size), uv0, uv1);
186 }
187 }
188
189 ImGui::EndTooltip();
190 }
191 }
192
194 DrawTileIdLabels(tile_display_size);
195 }
196
197 DrawHighlight(tile_display_size);
198 }
199
201 canvas_->DrawGrid();
203
204 return result;
205}
206
208 int tile_display_size) {
209 RenderResult result;
210
211 if (!ImGui::IsItemHovered()) {
212 return result;
213 }
214
215 const bool clicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
216 const bool double_clicked =
217 ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
218 const bool right_clicked = ImGui::IsMouseClicked(ImGuiMouseButton_Right);
219
220 if (clicked || double_clicked || right_clicked) {
221 const int hovered_tile = ResolveTileAtCursor(tile_display_size);
222 if (IsValidTileId(hovered_tile) && IsInFilterRange(hovered_tile)) {
223 result.tile_clicked = clicked;
224 result.tile_double_clicked = double_clicked;
225 result.tile_right_clicked = right_clicked;
226 if (hovered_tile != selected_tile_id_) {
227 selected_tile_id_ = hovered_tile;
228 result.selection_changed = true;
229 }
231 }
232 }
233
234 // Emit drag source for the selected tile when dragging is enabled
236 result.tile_dragging =
238 }
239
240 return result;
241}
242
243int TileSelectorWidget::ResolveTileAtCursor(int tile_display_size) const {
244 if (!canvas_) {
245 return -1;
246 }
247
248 const ImVec2 screen_pos = ImGui::GetIO().MousePos;
249 const ImVec2 origin = canvas_->zero_point();
250 const ImVec2 scroll = canvas_->scrolling();
251
252 // Convert screen position to canvas content position (accounting for scroll)
253 ImVec2 local =
254 ImVec2(screen_pos.x - origin.x - config_.draw_offset.x - scroll.x,
255 screen_pos.y - origin.y - config_.draw_offset.y - scroll.y);
256
257 if (local.x < 0.0f || local.y < 0.0f) {
258 return -1;
259 }
260
261 const int column = static_cast<int>(local.x / tile_display_size);
262 const int row = static_cast<int>(local.y / tile_display_size);
263
264 return row * config_.tiles_per_row + column;
265}
266
267void TileSelectorWidget::DrawHighlight(int tile_display_size) const {
269 return;
270 }
271
272 const int column = selected_tile_id_ % config_.tiles_per_row;
273 const int row = selected_tile_id_ / config_.tiles_per_row;
274
275 const float x = config_.draw_offset.x + column * tile_display_size;
276 const float y = config_.draw_offset.y + row * tile_display_size;
277
278 canvas_->DrawOutlineWithColor(static_cast<int>(x), static_cast<int>(y),
279 tile_display_size, tile_display_size,
281}
282
284 // Future enhancement: draw ImGui text overlay with tile indices.
285}
286
287void TileSelectorWidget::ScrollToTile(int tile_id, bool use_imgui_scroll) {
288 if (!canvas_ || !IsValidTileId(tile_id)) {
289 return;
290 }
291
292 // Defer scroll until next render (when we're in the correct ImGui window
293 // context)
294 pending_scroll_tile_id_ = tile_id;
295 pending_scroll_use_imgui_ = use_imgui_scroll;
296}
297
298ImVec2 TileSelectorWidget::TileOrigin(int tile_id) const {
299 if (!IsValidTileId(tile_id)) {
300 return ImVec2(-1, -1);
301 }
302 const int tile_display_size =
303 static_cast<int>(config_.tile_size * config_.display_scale);
304 const int column = tile_id % config_.tiles_per_row;
305 const int row = tile_id / config_.tiles_per_row;
306 return ImVec2(config_.draw_offset.x + column * tile_display_size,
307 config_.draw_offset.y + row * tile_display_size);
308}
309
311 std::string_view input) {
312 int tile_id = -1;
313 if (!ParseTileIdText(input, &tile_id)) {
315 return last_jump_result_;
316 }
317
318 if (!IsValidTileId(tile_id)) {
320 return last_jump_result_;
321 }
322
323 selected_tile_id_ = tile_id;
324 ScrollToTile(tile_id, true);
326 return last_jump_result_;
327}
328
330 bool jumped = false;
331 const int max_tile_id = GetMaxTileId();
332 const float available_width =
333 std::max(ImGui::GetContentRegionAvail().x, 1.0f);
334 const bool compact_layout = available_width < 420.0f;
335 const float jump_input_width =
336 compact_layout ? std::clamp(available_width * 0.28f, 56.0f, 88.0f)
337 : 64.0f;
338 const float range_input_width =
339 compact_layout
340 ? std::clamp((available_width - 110.0f) * 0.5f, 56.0f, 88.0f)
341 : 64.0f;
342
343 constexpr ImGuiInputTextFlags kHexFlags =
344 ImGuiInputTextFlags_CharsHexadecimal |
345 ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll;
346
347 ImGui::PushID(widget_id_.c_str());
348
349 // Jump-to-ID input
350 ImGui::AlignTextToFramePadding();
351 ImGui::TextUnformatted(tr("Go:"));
352 ImGui::SameLine();
353
354 ImGui::SetNextItemWidth(jump_input_width);
355 if (ImGui::InputText("##TileFilterID", filter_buf_, sizeof(filter_buf_),
356 kHexFlags)) {
359 jumped = true;
360 break;
363 break;
364 }
365 }
366 if (ImGui::IsItemHovered()) {
367 ImGui::SetTooltip(
368 tr("Enter tile ID and press Enter:\n"
369 "hex: 1A or 0x1A\n"
370 "decimal: d:26"));
371 }
372
373 ImGui::SameLine();
374 ImGui::TextDisabled(tr("/ 0x%03X"), max_tile_id);
375
376 if (compact_layout) {
377 ImGui::NewLine();
378 } else {
380 ImGui::SameLine(0, 8.0f);
381 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), tr("Invalid hex ID"));
383 ImGui::SameLine(0, 8.0f);
384 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
385 tr("Out of range (max: 0x%03X)"), max_tile_id);
386 }
387 }
388
389 // Range filter inputs
390 if (!compact_layout) {
391 ImGui::SameLine(0, 12.0f);
392 }
393 ImGui::TextUnformatted(tr("Range:"));
394 ImGui::SameLine();
395
396 bool range_changed = false;
397 ImGui::SetNextItemWidth(range_input_width);
398 if (ImGui::InputText("##RangeMin", filter_min_buf_, sizeof(filter_min_buf_),
399 kHexFlags)) {
400 range_changed = true;
401 }
402 if (ImGui::IsItemHovered()) {
403 ImGui::SetTooltip(
404 tr("Min tile ID. Press Enter to apply.\n"
405 "hex: 1A or 0x1A\n"
406 "decimal: d:26"));
407 }
408 ImGui::SameLine();
409 ImGui::TextUnformatted("-");
410 ImGui::SameLine();
411 ImGui::SetNextItemWidth(range_input_width);
412 if (ImGui::InputText("##RangeMax", filter_max_buf_, sizeof(filter_max_buf_),
413 kHexFlags)) {
414 range_changed = true;
415 }
416 if (ImGui::IsItemHovered()) {
417 ImGui::SetTooltip(
418 tr("Max tile ID. Press Enter to apply.\n"
419 "hex: 1A or 0x1A\n"
420 "decimal: d:26"));
421 }
422 if (!compact_layout) {
423 ImGui::SameLine();
424 ImGui::TextDisabled(tr("(hex, d:dec)"));
425 } else {
426 ImGui::NewLine();
427 ImGui::TextDisabled(tr("hex or d:dec"));
428 }
429
430 if (range_changed) {
431 int parsed_min = 0;
432 int parsed_max = 0;
433 bool has_min = ParseTileIdText(filter_min_buf_, &parsed_min);
434 bool has_max = ParseTileIdText(filter_max_buf_, &parsed_max);
435
436 if (has_min && has_max && parsed_min <= parsed_max) {
437 SetRangeFilter(parsed_min, parsed_max);
438 filter_range_error_ = false;
440 filter_out_of_range_ = false;
442 } else {
443 // SetRangeFilter returned early: both values exceeded total_tiles_.
445 }
446 } else if (!has_min && !has_max) {
448 filter_range_error_ = false;
449 filter_out_of_range_ = false;
450 } else if (has_min && has_max && parsed_min > parsed_max) {
451 // Invalid range: min must be ≤ max
452 filter_range_error_ = true;
453 filter_out_of_range_ = false;
454 }
455 }
456
457 // Clear button when range is active
459 if (!compact_layout) {
460 ImGui::SameLine();
461 }
462 if (ImGui::SmallButton(tr("X##ClearRange"))) {
464 filter_min_buf_[0] = '\0';
465 filter_max_buf_[0] = '\0';
466 filter_range_error_ = false;
467 }
468 if (ImGui::IsItemHovered()) {
469 ImGui::SetTooltip(tr("Clear range filter"));
470 }
471 }
472
473 // Validation feedback (shown inline after the filter inputs)
475 if (!compact_layout) {
476 ImGui::SameLine(0, 8.0f);
477 }
478 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
479 tr("Min must be <= Max"));
480 } else if (filter_out_of_range_) {
481 if (!compact_layout) {
482 ImGui::SameLine(0, 8.0f);
483 }
484 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
485 tr("Out of range (max: 0x%03X)"), GetMaxTileId());
486 } else if (filter_range_active_) {
487 int range_count = filter_range_max_ - filter_range_min_ + 1;
488 if (range_count <= 0) {
489 if (!compact_layout) {
490 ImGui::SameLine(0, 8.0f);
491 }
492 ImGui::TextDisabled(tr("(no tiles in range)"));
493 }
494 }
495
496 ImGui::PopID();
497
498 return jumped;
499}
500
501void TileSelectorWidget::SetRangeFilter(int min_id, int max_id) {
502 if (min_id < 0)
503 min_id = 0;
504 if (max_id >= total_tiles_)
505 max_id = total_tiles_ - 1;
506 if (min_id > max_id)
507 return;
508
510 filter_range_min_ = min_id;
511 filter_range_max_ = max_id;
512 filter_range_error_ = false;
513}
514
522
523bool TileSelectorWidget::IsValidTileId(int tile_id) const {
524 return tile_id >= 0 && tile_id < total_tiles_;
525}
526
527bool TileSelectorWidget::IsInFilterRange(int tile_id) const {
529 return true;
530 return tile_id >= filter_range_min_ && tile_id <= filter_range_max_;
531}
532
533} // namespace yaze::gui
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
TextureHandle texture() const
Definition bitmap.h:401
bool is_active() const
Definition bitmap.h:405
int height() const
Definition bitmap.h:395
int width() const
Definition bitmap.h:394
Modern, robust canvas for drawing and manipulating graphics.
Definition canvas.h:64
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1162
void DrawOutlineWithColor(int x, int y, int w, int h, ImVec4 color)
Definition canvas.cc:1231
void DrawContextMenu()
Definition canvas.cc:692
auto zero_point() const
Definition canvas.h:348
auto scrolling() const
Definition canvas.h:350
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0))
Definition canvas.cc:602
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1484
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:65