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 // Prefer honest grid + scrollbar chrome so docks can auto-size. Filter-bar
110 // controls wrap compactly when the available width is tight (see
111 // DrawFilterBar); they must not inflate the preferred dock width.
112 constexpr float kScrollbarChrome = 18.0f;
113 return grid_width + kScrollbarChrome;
114}
115
117 bool atlas_ready) {
118 RenderResult result;
119
120 if (!canvas_) {
121 return result;
122 }
123
124 const int tile_display_size =
125 static_cast<int>(config_.tile_size * config_.display_scale);
126 const ImVec2 content_size = GetGridContentSize();
127
128 // Set content size for ImGui child window (must be called before
129 // DrawBackground)
130 ImGui::SetCursorPos(ImVec2(0, 0));
131 ImGui::Dummy(content_size);
132 ImGui::SetCursorPos(ImVec2(0, 0));
133
134 // Handle pending scroll (deferred from ScrollToTile call outside render
135 // context)
136 if (pending_scroll_tile_id_ >= 0) {
138 const ImVec2 target = TileOrigin(pending_scroll_tile_id_);
140 const ImVec2 window_size = ImGui::GetWindowSize();
141 float scroll_x =
142 target.x - (window_size.x / 2.0f) + (tile_display_size / 2.0f);
143 float scroll_y =
144 target.y - (window_size.y / 2.0f) + (tile_display_size / 2.0f);
145 scroll_x = std::max(0.0f, scroll_x);
146 scroll_y = std::max(0.0f, scroll_y);
147 ImGui::SetScrollX(scroll_x);
148 ImGui::SetScrollY(scroll_y);
149 }
150 }
151 pending_scroll_tile_id_ = -1; // Clear pending scroll
152 }
153
155
156 if (atlas_ready && atlas.is_active()) {
157 canvas_->DrawBitmap(atlas, static_cast<int>(config_.draw_offset.x),
158 static_cast<int>(config_.draw_offset.y),
160
161 result = HandleInteraction(tile_display_size);
162
163 // Hover tooltip: show tile ID and zoomed preview
164 if (config_.show_hover_tooltip && ImGui::IsItemHovered()) {
165 int hovered_tile = ResolveTileAtCursor(tile_display_size);
166 if (IsValidTileId(hovered_tile)) {
167 ImGui::BeginTooltip();
168 ImGui::Text(tr("Tile %d (0x%03X)"), hovered_tile, hovered_tile);
169
170 // Extract and draw a zoomed preview of the hovered tile
171 int tile_col = hovered_tile % config_.tiles_per_row;
172 int tile_row = hovered_tile / config_.tiles_per_row;
173 int src_x = tile_col * config_.tile_size;
174 int src_y = tile_row * config_.tile_size;
175
176 // Use ImGui texture coords for the atlas to show the tile zoomed
177 auto* texture_id = atlas.texture();
178 if (texture_id != nullptr) {
179 float atlas_w = static_cast<float>(atlas.width());
180 float atlas_h = static_cast<float>(atlas.height());
181 if (atlas_w > 0 && atlas_h > 0) {
182 ImVec2 uv0(src_x / atlas_w, src_y / atlas_h);
183 ImVec2 uv1((src_x + config_.tile_size) / atlas_w,
184 (src_y + config_.tile_size) / atlas_h);
185 float preview_size = config_.tile_size * 4.0f;
186 ImGui::Image((ImTextureID)(intptr_t)texture_id,
187 ImVec2(preview_size, preview_size), uv0, uv1);
188 }
189 }
190
191 ImGui::EndTooltip();
192 }
193 }
194
196 DrawTileIdLabels(tile_display_size);
197 }
198
199 DrawHighlight(tile_display_size);
200 }
201
203 canvas_->DrawGrid();
205
206 return result;
207}
208
210 int tile_display_size) {
211 RenderResult result;
212
213 if (!ImGui::IsItemHovered()) {
214 return result;
215 }
216
217 const bool clicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
218 const bool double_clicked =
219 ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
220 const bool right_clicked = ImGui::IsMouseClicked(ImGuiMouseButton_Right);
221
222 if (clicked || double_clicked || right_clicked) {
223 const int hovered_tile = ResolveTileAtCursor(tile_display_size);
224 if (IsValidTileId(hovered_tile) && IsInFilterRange(hovered_tile)) {
225 result.tile_clicked = clicked;
226 result.tile_double_clicked = double_clicked;
227 result.tile_right_clicked = right_clicked;
228 if (hovered_tile != selected_tile_id_) {
229 selected_tile_id_ = hovered_tile;
230 result.selection_changed = true;
231 }
233 }
234 }
235
236 // Emit drag source for the selected tile when dragging is enabled
238 result.tile_dragging =
240 }
241
242 return result;
243}
244
245int TileSelectorWidget::ResolveTileAtCursor(int tile_display_size) const {
246 if (!canvas_) {
247 return -1;
248 }
249
250 const ImVec2 screen_pos = ImGui::GetIO().MousePos;
251 const ImVec2 origin = canvas_->zero_point();
252 const ImVec2 scroll = canvas_->scrolling();
253
254 // Convert screen position to canvas content position (accounting for scroll)
255 ImVec2 local =
256 ImVec2(screen_pos.x - origin.x - config_.draw_offset.x - scroll.x,
257 screen_pos.y - origin.y - config_.draw_offset.y - scroll.y);
258
259 if (local.x < 0.0f || local.y < 0.0f) {
260 return -1;
261 }
262
263 const int column = static_cast<int>(local.x / tile_display_size);
264 const int row = static_cast<int>(local.y / tile_display_size);
265
266 return row * config_.tiles_per_row + column;
267}
268
269void TileSelectorWidget::DrawHighlight(int tile_display_size) const {
271 return;
272 }
273
274 const int column = selected_tile_id_ % config_.tiles_per_row;
275 const int row = selected_tile_id_ / config_.tiles_per_row;
276
277 const float x = config_.draw_offset.x + column * tile_display_size;
278 const float y = config_.draw_offset.y + row * tile_display_size;
279
280 canvas_->DrawOutlineWithColor(static_cast<int>(x), static_cast<int>(y),
281 tile_display_size, tile_display_size,
283}
284
286 // Future enhancement: draw ImGui text overlay with tile indices.
287}
288
289void TileSelectorWidget::ScrollToTile(int tile_id, bool use_imgui_scroll) {
290 if (!canvas_ || !IsValidTileId(tile_id)) {
291 return;
292 }
293
294 // Defer scroll until next render (when we're in the correct ImGui window
295 // context)
296 pending_scroll_tile_id_ = tile_id;
297 pending_scroll_use_imgui_ = use_imgui_scroll;
298}
299
300ImVec2 TileSelectorWidget::TileOrigin(int tile_id) const {
301 if (!IsValidTileId(tile_id)) {
302 return ImVec2(-1, -1);
303 }
304 const int tile_display_size =
305 static_cast<int>(config_.tile_size * config_.display_scale);
306 const int column = tile_id % config_.tiles_per_row;
307 const int row = tile_id / config_.tiles_per_row;
308 return ImVec2(config_.draw_offset.x + column * tile_display_size,
309 config_.draw_offset.y + row * tile_display_size);
310}
311
313 std::string_view input) {
314 int tile_id = -1;
315 if (!ParseTileIdText(input, &tile_id)) {
317 return last_jump_result_;
318 }
319
320 if (!IsValidTileId(tile_id)) {
322 return last_jump_result_;
323 }
324
325 selected_tile_id_ = tile_id;
326 ScrollToTile(tile_id, true);
328 return last_jump_result_;
329}
330
332 bool jumped = false;
333 const int max_tile_id = GetMaxTileId();
334 const float available_width =
335 std::max(ImGui::GetContentRegionAvail().x, 1.0f);
336 const bool compact_layout = available_width < 420.0f;
337 const float jump_input_width =
338 compact_layout ? std::clamp(available_width * 0.28f, 56.0f, 88.0f)
339 : 64.0f;
340 const float range_input_width =
341 compact_layout
342 ? std::clamp((available_width - 110.0f) * 0.5f, 56.0f, 88.0f)
343 : 64.0f;
344
345 constexpr ImGuiInputTextFlags kHexFlags =
346 ImGuiInputTextFlags_CharsHexadecimal |
347 ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll;
348
349 ImGui::PushID(widget_id_.c_str());
350
351 // Jump-to-ID input
352 ImGui::AlignTextToFramePadding();
353 ImGui::TextUnformatted(tr("Go:"));
354 ImGui::SameLine();
355
356 ImGui::SetNextItemWidth(jump_input_width);
357 if (ImGui::InputText("##TileFilterID", filter_buf_, sizeof(filter_buf_),
358 kHexFlags)) {
361 jumped = true;
362 break;
365 break;
366 }
367 }
368 if (ImGui::IsItemHovered()) {
369 ImGui::SetTooltip(
370 tr("Enter tile ID and press Enter:\n"
371 "hex: 1A or 0x1A\n"
372 "decimal: d:26"));
373 }
374
375 ImGui::SameLine();
376 ImGui::TextDisabled(tr("/ 0x%03X"), max_tile_id);
377
378 if (compact_layout) {
379 ImGui::NewLine();
380 } else {
382 ImGui::SameLine(0, 8.0f);
383 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), tr("Invalid hex ID"));
385 ImGui::SameLine(0, 8.0f);
386 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
387 tr("Out of range (max: 0x%03X)"), max_tile_id);
388 }
389 }
390
391 // Range filter inputs
392 if (!compact_layout) {
393 ImGui::SameLine(0, 12.0f);
394 }
395 ImGui::TextUnformatted(tr("Range:"));
396 ImGui::SameLine();
397
398 bool range_changed = false;
399 ImGui::SetNextItemWidth(range_input_width);
400 if (ImGui::InputText("##RangeMin", filter_min_buf_, sizeof(filter_min_buf_),
401 kHexFlags)) {
402 range_changed = true;
403 }
404 if (ImGui::IsItemHovered()) {
405 ImGui::SetTooltip(
406 tr("Min tile ID. Press Enter to apply.\n"
407 "hex: 1A or 0x1A\n"
408 "decimal: d:26"));
409 }
410 ImGui::SameLine();
411 ImGui::TextUnformatted("-");
412 ImGui::SameLine();
413 ImGui::SetNextItemWidth(range_input_width);
414 if (ImGui::InputText("##RangeMax", filter_max_buf_, sizeof(filter_max_buf_),
415 kHexFlags)) {
416 range_changed = true;
417 }
418 if (ImGui::IsItemHovered()) {
419 ImGui::SetTooltip(
420 tr("Max tile ID. Press Enter to apply.\n"
421 "hex: 1A or 0x1A\n"
422 "decimal: d:26"));
423 }
424 if (!compact_layout) {
425 ImGui::SameLine();
426 ImGui::TextDisabled(tr("(hex, d:dec)"));
427 } else {
428 ImGui::NewLine();
429 ImGui::TextDisabled(tr("hex or d:dec"));
430 }
431
432 if (range_changed) {
433 int parsed_min = 0;
434 int parsed_max = 0;
435 bool has_min = ParseTileIdText(filter_min_buf_, &parsed_min);
436 bool has_max = ParseTileIdText(filter_max_buf_, &parsed_max);
437
438 if (has_min && has_max && parsed_min <= parsed_max) {
439 SetRangeFilter(parsed_min, parsed_max);
440 filter_range_error_ = false;
442 filter_out_of_range_ = false;
444 } else {
445 // SetRangeFilter returned early: both values exceeded total_tiles_.
447 }
448 } else if (!has_min && !has_max) {
450 filter_range_error_ = false;
451 filter_out_of_range_ = false;
452 } else if (has_min && has_max && parsed_min > parsed_max) {
453 // Invalid range: min must be ≤ max
454 filter_range_error_ = true;
455 filter_out_of_range_ = false;
456 }
457 }
458
459 // Clear button when range is active
461 if (!compact_layout) {
462 ImGui::SameLine();
463 }
464 if (ImGui::SmallButton(tr("X##ClearRange"))) {
466 filter_min_buf_[0] = '\0';
467 filter_max_buf_[0] = '\0';
468 filter_range_error_ = false;
469 }
470 if (ImGui::IsItemHovered()) {
471 ImGui::SetTooltip(tr("Clear range filter"));
472 }
473 }
474
475 // Validation feedback (shown inline after the filter inputs)
477 if (!compact_layout) {
478 ImGui::SameLine(0, 8.0f);
479 }
480 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
481 tr("Min must be <= Max"));
482 } else if (filter_out_of_range_) {
483 if (!compact_layout) {
484 ImGui::SameLine(0, 8.0f);
485 }
486 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
487 tr("Out of range (max: 0x%03X)"), GetMaxTileId());
488 } else if (filter_range_active_) {
489 int range_count = filter_range_max_ - filter_range_min_ + 1;
490 if (range_count <= 0) {
491 if (!compact_layout) {
492 ImGui::SameLine(0, 8.0f);
493 }
494 ImGui::TextDisabled(tr("(no tiles in range)"));
495 }
496 }
497
498 ImGui::PopID();
499
500 return jumped;
501}
502
503void TileSelectorWidget::SetRangeFilter(int min_id, int max_id) {
504 if (min_id < 0)
505 min_id = 0;
506 if (max_id >= total_tiles_)
507 max_id = total_tiles_ - 1;
508 if (min_id > max_id)
509 return;
510
512 filter_range_min_ = min_id;
513 filter_range_max_ = max_id;
514 filter_range_error_ = false;
515}
516
524
525bool TileSelectorWidget::IsValidTileId(int tile_id) const {
526 return tile_id >= 0 && tile_id < total_tiles_;
527}
528
529bool TileSelectorWidget::IsInFilterRange(int tile_id) const {
531 return true;
532 return tile_id >= filter_range_min_ && tile_id <= filter_range_max_;
533}
534
535} // namespace yaze::gui
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:69
TextureHandle texture() const
Definition bitmap.h:403
bool is_active() const
Definition bitmap.h:407
int height() const
Definition bitmap.h:397
int width() const
Definition bitmap.h:396
Modern, robust canvas for drawing and manipulating graphics.
Definition canvas.h:64
void DrawBitmap(Bitmap &bitmap, int border_offset, float scale)
Definition canvas.cc:1173
void DrawOutlineWithColor(int x, int y, int w, int h, ImVec4 color)
Definition canvas.cc:1242
void DrawContextMenu()
Definition canvas.cc:703
auto zero_point() const
Definition canvas.h:354
auto scrolling() const
Definition canvas.h:356
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0))
Definition canvas.cc:613
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1495
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