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 const int tile_display_size =
101 static_cast<int>(config_.tile_size * config_.display_scale);
102 const int num_rows =
104 return ImVec2(
105 config_.tiles_per_row * tile_display_size + config_.draw_offset.x * 2,
106 num_rows * tile_display_size + config_.draw_offset.y * 2);
107}
108
110 const float grid_width = GetGridContentSize().x;
111 // Leave enough room for the jump/range controls before the filter bar wraps,
112 // while still allowing a narrower compact layout when the dock is squeezed.
113 return std::max(grid_width + 18.0f, 332.0f);
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
156
157 if (atlas_ready && atlas.is_active()) {
158 canvas_->DrawBitmap(atlas, static_cast<int>(config_.draw_offset.x),
159 static_cast<int>(config_.draw_offset.y),
161
162 result = HandleInteraction(tile_display_size);
163
164 // Hover tooltip: show tile ID and zoomed preview
165 if (config_.show_hover_tooltip && ImGui::IsItemHovered()) {
166 int hovered_tile = ResolveTileAtCursor(tile_display_size);
167 if (IsValidTileId(hovered_tile)) {
168 ImGui::BeginTooltip();
169 ImGui::Text("Tile %d (0x%03X)", hovered_tile, hovered_tile);
170
171 // Extract and draw a zoomed preview of the hovered tile
172 int tile_col = hovered_tile % config_.tiles_per_row;
173 int tile_row = hovered_tile / config_.tiles_per_row;
174 int src_x = tile_col * config_.tile_size;
175 int src_y = tile_row * config_.tile_size;
176
177 // Use ImGui texture coords for the atlas to show the tile zoomed
178 auto* texture_id = atlas.texture();
179 if (texture_id != nullptr) {
180 float atlas_w = static_cast<float>(atlas.width());
181 float atlas_h = static_cast<float>(atlas.height());
182 if (atlas_w > 0 && atlas_h > 0) {
183 ImVec2 uv0(src_x / atlas_w, src_y / atlas_h);
184 ImVec2 uv1((src_x + config_.tile_size) / atlas_w,
185 (src_y + config_.tile_size) / atlas_h);
186 float preview_size = config_.tile_size * 4.0f;
187 ImGui::Image((ImTextureID)(intptr_t)texture_id,
188 ImVec2(preview_size, preview_size), uv0, uv1);
189 }
190 }
191
192 ImGui::EndTooltip();
193 }
194 }
195
197 DrawTileIdLabels(tile_display_size);
198 }
199
200 DrawHighlight(tile_display_size);
201 }
202
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
221 if (clicked || double_clicked) {
222 const int hovered_tile = ResolveTileAtCursor(tile_display_size);
223 if (IsValidTileId(hovered_tile) && IsInFilterRange(hovered_tile)) {
224 result.tile_clicked = clicked;
225 result.tile_double_clicked = double_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
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 = std::max(ImGui::GetContentRegionAvail().x, 1.0f);
333 const bool compact_layout = available_width < 420.0f;
334 const float jump_input_width =
335 compact_layout ? std::clamp(available_width * 0.28f, 56.0f, 88.0f)
336 : 64.0f;
337 const float range_input_width =
338 compact_layout ? std::clamp((available_width - 110.0f) * 0.5f, 56.0f, 88.0f)
339 : 64.0f;
340
341 constexpr ImGuiInputTextFlags kHexFlags =
342 ImGuiInputTextFlags_CharsHexadecimal |
343 ImGuiInputTextFlags_EnterReturnsTrue |
344 ImGuiInputTextFlags_AutoSelectAll;
345
346 ImGui::PushID(widget_id_.c_str());
347
348 // Jump-to-ID input
349 ImGui::AlignTextToFramePadding();
350 ImGui::TextUnformatted("Go:");
351 ImGui::SameLine();
352
353 ImGui::SetNextItemWidth(jump_input_width);
354 if (ImGui::InputText("##TileFilterID", filter_buf_, sizeof(filter_buf_),
355 kHexFlags)) {
358 jumped = true;
359 break;
362 break;
363 }
364 }
365 if (ImGui::IsItemHovered()) {
366 ImGui::SetTooltip(
367 "Enter tile ID and press Enter:\n"
368 "hex: 1A or 0x1A\n"
369 "decimal: d:26");
370 }
371
372 ImGui::SameLine();
373 ImGui::TextDisabled("/ 0x%03X", max_tile_id);
374
375 if (compact_layout) {
376 ImGui::NewLine();
377 } else {
379 ImGui::SameLine(0, 8.0f);
380 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Invalid hex ID");
382 ImGui::SameLine(0, 8.0f);
383 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
384 "Out of range (max: 0x%03X)", max_tile_id);
385 }
386 }
387
388 // Range filter inputs
389 if (!compact_layout) {
390 ImGui::SameLine(0, 12.0f);
391 }
392 ImGui::TextUnformatted("Range:");
393 ImGui::SameLine();
394
395 bool range_changed = false;
396 ImGui::SetNextItemWidth(range_input_width);
397 if (ImGui::InputText("##RangeMin", filter_min_buf_, sizeof(filter_min_buf_),
398 kHexFlags)) {
399 range_changed = true;
400 }
401 if (ImGui::IsItemHovered()) {
402 ImGui::SetTooltip(
403 "Min tile ID. Press Enter to apply.\n"
404 "hex: 1A or 0x1A\n"
405 "decimal: d:26");
406 }
407 ImGui::SameLine();
408 ImGui::TextUnformatted("-");
409 ImGui::SameLine();
410 ImGui::SetNextItemWidth(range_input_width);
411 if (ImGui::InputText("##RangeMax", filter_max_buf_, sizeof(filter_max_buf_),
412 kHexFlags)) {
413 range_changed = true;
414 }
415 if (ImGui::IsItemHovered()) {
416 ImGui::SetTooltip(
417 "Max tile ID. Press Enter to apply.\n"
418 "hex: 1A or 0x1A\n"
419 "decimal: d:26");
420 }
421 if (!compact_layout) {
422 ImGui::SameLine();
423 ImGui::TextDisabled("(hex, d:dec)");
424 } else {
425 ImGui::NewLine();
426 ImGui::TextDisabled("hex or d:dec");
427 }
428
429 if (range_changed) {
430 int parsed_min = 0;
431 int parsed_max = 0;
432 bool has_min = ParseTileIdText(filter_min_buf_, &parsed_min);
433 bool has_max = ParseTileIdText(filter_max_buf_, &parsed_max);
434
435 if (has_min && has_max && parsed_min <= parsed_max) {
436 SetRangeFilter(parsed_min, parsed_max);
437 filter_range_error_ = false;
439 filter_out_of_range_ = false;
441 } else {
442 // SetRangeFilter returned early: both values exceeded total_tiles_.
444 }
445 } else if (!has_min && !has_max) {
447 filter_range_error_ = false;
448 filter_out_of_range_ = false;
449 } else if (has_min && has_max && parsed_min > parsed_max) {
450 // Invalid range: min must be ≤ max
451 filter_range_error_ = true;
452 filter_out_of_range_ = false;
453 }
454 }
455
456 // Clear button when range is active
458 if (!compact_layout) {
459 ImGui::SameLine();
460 }
461 if (ImGui::SmallButton("X##ClearRange")) {
463 filter_min_buf_[0] = '\0';
464 filter_max_buf_[0] = '\0';
465 filter_range_error_ = false;
466 }
467 if (ImGui::IsItemHovered()) {
468 ImGui::SetTooltip("Clear range filter");
469 }
470 }
471
472 // Validation feedback (shown inline after the filter inputs)
474 if (!compact_layout) {
475 ImGui::SameLine(0, 8.0f);
476 }
477 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Min must be <= Max");
478 } else if (filter_out_of_range_) {
479 if (!compact_layout) {
480 ImGui::SameLine(0, 8.0f);
481 }
482 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
483 "Out of range (max: 0x%03X)", GetMaxTileId());
484 } else if (filter_range_active_) {
485 int range_count = filter_range_max_ - filter_range_min_ + 1;
486 if (range_count <= 0) {
487 if (!compact_layout) {
488 ImGui::SameLine(0, 8.0f);
489 }
490 ImGui::TextDisabled("(no tiles in range)");
491 }
492 }
493
494 ImGui::PopID();
495
496 return jumped;
497}
498
499void TileSelectorWidget::SetRangeFilter(int min_id, int max_id) {
500 if (min_id < 0) min_id = 0;
501 if (max_id >= total_tiles_) max_id = total_tiles_ - 1;
502 if (min_id > max_id) return;
503
505 filter_range_min_ = min_id;
506 filter_range_max_ = max_id;
507 filter_range_error_ = false;
508}
509
517
518bool TileSelectorWidget::IsValidTileId(int tile_id) const {
519 return tile_id >= 0 && tile_id < total_tiles_;
520}
521
522bool TileSelectorWidget::IsInFilterRange(int tile_id) const {
523 if (!filter_range_active_) return true;
524 return tile_id >= filter_range_min_ && tile_id <= filter_range_max_;
525}
526
527} // 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