yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
pixel_editor_view.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <cmath>
6#include <memory>
7#include <queue>
8
9#include "absl/strings/str_format.h"
12#include "app/gui/core/icons.h"
13#include "app/gui/core/style.h"
18#include "imgui/imgui.h"
19
20namespace yaze {
21namespace editor {
22
24 // Canvas is initialized via member initializer list
25}
26
27void PixelEditorView::Draw(bool* p_open) {
28 // WindowContent interface - delegate to Update()
29 Update().IgnoreError();
30}
31
33 // Top toolbar
35 ImGui::SameLine();
37
38 ImGui::Separator();
39
40 constexpr float kColorPickerWidth = 200.0f;
41 constexpr float kStatusBarHeight = 24.0f;
42
43 // Main content area with canvas and side panels
44 ImGui::BeginChild("##PixelEditorContent", ImVec2(0, -kStatusBarHeight),
45 false);
46
47 // Color picker on the left
48 ImGui::BeginChild("##ColorPickerSide", ImVec2(kColorPickerWidth, 0), true);
50 ImGui::Separator();
52 ImGui::EndChild();
53
54 ImGui::SameLine();
55
56 // Main canvas
57 ImGui::BeginChild("##CanvasArea", ImVec2(0, 0), true,
58 ImGuiWindowFlags_HorizontalScrollbar);
59 DrawCanvas();
60 ImGui::EndChild();
61
62 ImGui::EndChild();
63
64 // Status bar
66
67 return absl::OkStatus();
68}
69
71 // Tool selection buttons
72 auto tool_button = [this](PixelTool tool, const char* icon,
73 const char* tooltip) {
74 bool is_selected = state_->current_tool == tool;
75 std::optional<gui::StyleColorGuard> sel_guard;
76 if (is_selected) {
77 sel_guard.emplace(ImGuiCol_Button, gui::GetPrimaryVec4());
78 }
79 if (ImGui::Button(icon)) {
80 state_->SetTool(tool);
81 }
82 sel_guard.reset();
83 if (ImGui::IsItemHovered()) {
84 ImGui::SetTooltip("%s", tooltip);
85 }
86 ImGui::SameLine();
87 };
88
89 tool_button(PixelTool::kSelect, ICON_MD_SELECT_ALL, "Select (V)");
90 tool_button(PixelTool::kPencil, ICON_MD_DRAW, "Pencil (B)");
91 tool_button(PixelTool::kBrush, ICON_MD_BRUSH, "Brush (B)");
92 tool_button(PixelTool::kEraser, ICON_MD_AUTO_FIX_HIGH, "Eraser (E)");
93 tool_button(PixelTool::kFill, ICON_MD_FORMAT_COLOR_FILL, "Fill (G)");
94 tool_button(PixelTool::kLine, ICON_MD_HORIZONTAL_RULE, "Line");
95 tool_button(PixelTool::kRectangle, ICON_MD_CROP_SQUARE, "Rectangle");
96 tool_button(PixelTool::kEyedropper, ICON_MD_COLORIZE, "Eyedropper (I)");
97
98 ImGui::SameLine();
99 ImGui::Text("|");
100 ImGui::SameLine();
101
102 // Brush size for pencil/brush/eraser
106 ImGui::SetNextItemWidth(80);
107 int brush = state_->brush_size;
108 if (ImGui::SliderInt("##BrushSize", &brush, 1, 8, "%d px")) {
109 state_->brush_size = static_cast<uint8_t>(brush);
110 }
111 HOVER_HINT("Brush size");
112 ImGui::SameLine();
113 }
114
115 // Undo/Redo buttons
116 ImGui::Text("|");
117 ImGui::SameLine();
118
119 ImGui::BeginDisabled(!undo_manager_ || !undo_manager_->CanUndo());
120 if (gui::ToolbarIconButton(ICON_MD_UNDO, "Undo (Ctrl+Z)") && undo_manager_) {
121 undo_manager_->Undo().IgnoreError();
122 }
123 ImGui::EndDisabled();
124
125 ImGui::SameLine();
126
127 ImGui::BeginDisabled(!undo_manager_ || !undo_manager_->CanRedo());
128 if (gui::ToolbarIconButton(ICON_MD_REDO, "Redo (Ctrl+Y)") && undo_manager_) {
129 undo_manager_->Redo().IgnoreError();
130 }
131 ImGui::EndDisabled();
132}
133
135 // Zoom controls
136 if (gui::ToolbarIconButton(ICON_MD_ZOOM_OUT, "Zoom out (-)")) {
137 state_->ZoomOut();
138 }
139 ImGui::SameLine();
140
141 ImGui::SetNextItemWidth(100);
142 float zoom = state_->zoom_level;
143 if (ImGui::SliderFloat("##Zoom", &zoom, 1.0f, 16.0f, "%.0fx")) {
144 state_->SetZoom(zoom);
145 }
146 ImGui::SameLine();
147
148 if (gui::ToolbarIconButton(ICON_MD_ZOOM_IN, "Zoom in (+)")) {
149 state_->ZoomIn();
150 }
151
152 ImGui::SameLine();
153 ImGui::Text("|");
154 ImGui::SameLine();
155
156 // View overlay toggles
157 ImGui::Checkbox(ICON_MD_GRID_ON, &state_->show_grid);
158 HOVER_HINT("Toggle grid (Ctrl+G)");
159 ImGui::SameLine();
160
161 ImGui::Checkbox(ICON_MD_ADD, &state_->show_cursor_crosshair);
162 HOVER_HINT("Toggle cursor crosshair");
163 ImGui::SameLine();
164
165 ImGui::Checkbox(ICON_MD_BRUSH, &state_->show_brush_preview);
166 HOVER_HINT("Toggle brush preview");
167 ImGui::SameLine();
168
170 HOVER_HINT("Toggle transparency grid");
171}
172
174 if (state_->open_sheets.empty()) {
175 ImGui::TextDisabled(
176 tr("No sheet selected. Select a sheet from the browser."));
177 return;
178 }
179
180 // Tab bar for open sheets
181 if (gui::BeginThemedTabBar("##SheetTabs",
182 ImGuiTabBarFlags_AutoSelectNewTabs |
183 ImGuiTabBarFlags_Reorderable |
184 ImGuiTabBarFlags_TabListPopupButton)) {
185 std::vector<uint16_t> sheets_to_close;
186
187 for (uint16_t sheet_id : state_->open_sheets) {
188 bool open = true;
189 std::string tab_label = absl::StrFormat("%02X", sheet_id);
190 if (state_->modified_sheets.count(sheet_id) > 0) {
191 tab_label += "*";
192 }
193
194 if (ImGui::BeginTabItem(tab_label.c_str(), &open)) {
195 state_->current_sheet_id = sheet_id;
196
197 // Get the current sheet bitmap
198 auto& sheet = gfx::Arena::Get().mutable_gfx_sheets()->at(
200
201 if (!sheet.is_active()) {
202 ImGui::TextDisabled(tr("Sheet %02X is not active"), sheet_id);
203 ImGui::EndTabItem();
204 continue;
205 }
206
207 // Calculate canvas size based on zoom
208 float canvas_width = sheet.width() * state_->zoom_level;
209 float canvas_height = sheet.height() * state_->zoom_level;
210
211 // Draw canvas background
212 canvas_.DrawBackground(ImVec2(canvas_width, canvas_height));
213
214 // Draw transparency checkerboard background if enabled
216 DrawTransparencyGrid(canvas_width, canvas_height);
217 }
218
219 // Draw the sheet texture
220 if (sheet.texture()) {
221 canvas_.draw_list()->AddImage(
222 (ImTextureID)(intptr_t)sheet.texture(), canvas_.zero_point(),
223 ImVec2(canvas_.zero_point().x + canvas_width,
224 canvas_.zero_point().y + canvas_height));
225 }
226
227 // Draw grid if enabled
228 if (state_->show_grid) {
230 }
231
232 // Draw transient tile highlight (e.g., from "Edit Graphics" jump)
233 DrawTileHighlight(sheet);
234
235 // Draw selection rectangle if active
237 ImVec2 sel_min =
239 ImVec2 sel_max =
242 canvas_.draw_list()->AddRect(
243 sel_min, sel_max, IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f);
244
245 // Marching ants effect (simplified)
246 canvas_.draw_list()->AddRect(sel_min, sel_max, IM_COL32(0, 0, 0, 128),
247 0.0f, 0, 1.0f);
248 }
249
250 // Draw tool preview (line/rectangle)
252 ImVec2 start = PixelToScreen(static_cast<int>(tool_start_pixel_.x),
253 static_cast<int>(tool_start_pixel_.y));
254 ImVec2 end = PixelToScreen(static_cast<int>(preview_end_.x),
255 static_cast<int>(preview_end_.y));
256
258 canvas_.draw_list()->AddLine(start, end, IM_COL32(255, 255, 0, 200),
259 2.0f);
261 canvas_.draw_list()->AddRect(start, end, IM_COL32(255, 255, 0, 200),
262 0.0f, 0, 2.0f);
263 }
264 }
265
266 // Draw cursor crosshair overlay if enabled and cursor in canvas
269 }
270
271 // Draw brush preview if using brush/eraser tool
276 }
277
279
280 // Handle mouse input
282
283 // Show pixel info tooltip if enabled
286 }
287
288 ImGui::EndTabItem();
289 }
290
291 if (!open) {
292 sheets_to_close.push_back(sheet_id);
293 }
294 }
295
296 // Close tabs that were requested
297 for (uint16_t sheet_id : sheets_to_close) {
298 state_->CloseSheet(sheet_id);
299 }
300
302 }
303}
304
306 float canvas_height) {
307 const float cell_size = 8.0f; // Checkerboard cell size
308 const ImU32 color1 = IM_COL32(180, 180, 180, 255);
309 const ImU32 color2 = IM_COL32(220, 220, 220, 255);
310
311 ImVec2 origin = canvas_.zero_point();
312 int cols = static_cast<int>(canvas_width / cell_size) + 1;
313 int rows = static_cast<int>(canvas_height / cell_size) + 1;
314
315 for (int row = 0; row < rows; row++) {
316 for (int col = 0; col < cols; col++) {
317 bool is_light = (row + col) % 2 == 0;
318 ImVec2 p_min(origin.x + col * cell_size, origin.y + row * cell_size);
319 ImVec2 p_max(std::min(p_min.x + cell_size, origin.x + canvas_width),
320 std::min(p_min.y + cell_size, origin.y + canvas_height));
321 canvas_.draw_list()->AddRectFilled(p_min, p_max,
322 is_light ? color1 : color2);
323 }
324 }
325}
326
328 ImVec2 cursor_screen = PixelToScreen(cursor_x_, cursor_y_);
329 float pixel_size = state_->zoom_level;
330
331 // Vertical line through cursor pixel
332 ImVec2 v_start(cursor_screen.x + pixel_size / 2, canvas_.zero_point().y);
333 ImVec2 v_end(cursor_screen.x + pixel_size / 2,
335 canvas_.draw_list()->AddLine(v_start, v_end, IM_COL32(255, 100, 100, 100),
336 1.0f);
337
338 // Horizontal line through cursor pixel
339 ImVec2 h_start(canvas_.zero_point().x, cursor_screen.y + pixel_size / 2);
340 ImVec2 h_end(canvas_.zero_point().x + canvas_.canvas_size().x,
341 cursor_screen.y + pixel_size / 2);
342 canvas_.draw_list()->AddLine(h_start, h_end, IM_COL32(255, 100, 100, 100),
343 1.0f);
344
345 // Highlight current pixel with a bright outline
346 ImVec2 pixel_min = cursor_screen;
347 ImVec2 pixel_max(cursor_screen.x + pixel_size, cursor_screen.y + pixel_size);
348 canvas_.draw_list()->AddRect(pixel_min, pixel_max,
349 IM_COL32(255, 255, 255, 200), 0.0f, 0, 2.0f);
350}
351
353 ImVec2 cursor_screen = PixelToScreen(cursor_x_, cursor_y_);
354 float pixel_size = state_->zoom_level;
355 int brush = state_->brush_size;
356 int half = brush / 2;
357
358 // Draw preview of brush area
359 ImVec2 brush_min(cursor_screen.x - half * pixel_size,
360 cursor_screen.y - half * pixel_size);
361 ImVec2 brush_max(cursor_screen.x + (brush - half) * pixel_size,
362 cursor_screen.y + (brush - half) * pixel_size);
363
364 // Fill with semi-transparent color preview
365 ImU32 preview_color = (state_->current_tool == PixelTool::kEraser)
366 ? IM_COL32(255, 0, 0, 50)
367 : IM_COL32(0, 255, 0, 50);
368 canvas_.draw_list()->AddRectFilled(brush_min, brush_max, preview_color);
369
370 // Outline
371 ImU32 outline_color = (state_->current_tool == PixelTool::kEraser)
372 ? IM_COL32(255, 100, 100, 200)
373 : IM_COL32(100, 255, 100, 200);
374 canvas_.draw_list()->AddRect(brush_min, brush_max, outline_color, 0.0f, 0,
375 1.0f);
376}
377
379 if (cursor_x_ < 0 || cursor_x_ >= sheet.width() || cursor_y_ < 0 ||
380 cursor_y_ >= sheet.height()) {
381 return;
382 }
383
384 uint8_t color_index = sheet.GetPixel(cursor_x_, cursor_y_);
385 auto palette = sheet.palette();
386
387 ImGui::BeginTooltip();
388 ImGui::Text(tr("Pos: %d, %d"), cursor_x_, cursor_y_);
389 ImGui::Text(tr("Tile: %d, %d"), cursor_x_ / 8, cursor_y_ / 8);
390 ImGui::Text(tr("Index: %d"), color_index);
391
392 if (color_index < palette.size()) {
393 ImGui::Text(tr("SNES: $%04X"), palette[color_index].snes());
394 ImVec4 color(palette[color_index].rgb().x / 255.0f,
395 palette[color_index].rgb().y / 255.0f,
396 palette[color_index].rgb().z / 255.0f, 1.0f);
397 ImGui::ColorButton("##ColorPreview", color, ImGuiColorEditFlags_NoTooltip,
398 ImVec2(24, 24));
399 if (color_index == 0) {
400 ImGui::SameLine();
401 ImGui::TextDisabled(tr("(Transparent)"));
402 }
403 }
404 ImGui::EndTooltip();
405}
406
409 return;
410 }
412 return;
413 }
414
415 const double now = ImGui::GetTime();
416 const double elapsed = now - state_->tile_highlight.start_time;
417 if (elapsed > state_->tile_highlight.duration) {
419 return;
420 }
421
422 const int tiles_per_row = sheet.width() / 8;
423 if (tiles_per_row <= 0) {
424 return;
425 }
426 const uint16_t tile_index = state_->tile_highlight.tile_index;
427 const int tile_x = static_cast<int>(tile_index % tiles_per_row);
428 const int tile_y = static_cast<int>(tile_index / tiles_per_row);
429
430 const float pulse =
431 0.5f + 0.5f * std::sin(static_cast<float>(elapsed) * 6.0f);
432 const float alpha = 0.25f + (pulse * 0.35f);
433
434 ImVec2 min = PixelToScreen(tile_x * 8, tile_y * 8);
435 ImVec2 max = PixelToScreen(tile_x * 8 + 8, tile_y * 8 + 8);
436
437 ImVec4 sel = gui::GetSelectedColor();
438 sel.w = alpha;
439 const ImU32 fill_color = ImGui::GetColorU32(sel);
440 ImVec4 sel_outline = gui::GetSelectedColor();
441 sel_outline.w = 0.9f;
442 const ImU32 outline_color = ImGui::GetColorU32(sel_outline);
443 canvas_.draw_list()->AddRectFilled(min, max, fill_color);
444 canvas_.draw_list()->AddRect(min, max, outline_color, 0.0f, 0, 2.0f);
445
446 if (ImGui::IsMouseHoveringRect(min, max)) {
447 ImGui::BeginTooltip();
448 ImGui::Text(tr("Focus tile: %d (sheet %02X)"), tile_index,
450 if (!state_->tile_highlight.label.empty()) {
451 ImGui::Text("%s", state_->tile_highlight.label.c_str());
452 }
453 ImGui::EndTooltip();
454 }
455}
456
458 ImGui::Text(tr("Colors"));
459
460 if (state_->open_sheets.empty()) {
461 ImGui::TextDisabled(tr("No sheet"));
462 return;
463 }
464
465 auto& sheet =
467 auto palette = sheet.palette();
468
469 // Draw palette colors in 4x4 grid (16 colors)
470 for (int i = 0; i < static_cast<int>(palette.size()) && i < 16; i++) {
471 if (i > 0 && i % 4 == 0) {
472 // New row
473 } else if (i > 0) {
474 ImGui::SameLine();
475 }
476
477 ImVec4 color(palette[i].rgb().x / 255.0f, palette[i].rgb().y / 255.0f,
478 palette[i].rgb().z / 255.0f, 1.0f);
479
480 bool is_selected = state_->current_color_index == i;
481 std::optional<gui::StyleVarGuard> border_var;
482 std::optional<gui::StyleColorGuard> border_color;
483 if (is_selected) {
484 border_var.emplace(ImGuiStyleVar_FrameBorderSize, 2.0f);
485 border_color.emplace(ImGuiCol_Border, gui::GetWarningColor());
486 }
487
488 std::string id = absl::StrFormat("##Color%d", i);
489 if (ImGui::ColorButton(
490 id.c_str(), color,
491 ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoBorder,
492 ImVec2(24, 24))) {
493 state_->current_color_index = static_cast<uint8_t>(i);
494 state_->current_color = color;
495 }
496
497 if (is_selected) {
498 border_color.reset();
499 border_var.reset();
500 }
501
502 if (ImGui::IsItemHovered()) {
503 ImGui::BeginTooltip();
504 ImGui::Text(tr("Index: %d"), i);
505 ImGui::Text(tr("SNES: $%04X"), palette[i].snes());
506 ImGui::Text(tr("RGB: %d, %d, %d"), static_cast<int>(palette[i].rgb().x),
507 static_cast<int>(palette[i].rgb().y),
508 static_cast<int>(palette[i].rgb().z));
509 if (i == 0) {
510 ImGui::Text(tr("(Transparent)"));
511 }
512 ImGui::EndTooltip();
513 }
514 }
515
516 ImGui::Separator();
517
518 // Current color preview
519 ImGui::Text(tr("Current:"));
520 ImGui::ColorButton("##CurrentColor", state_->current_color,
521 ImGuiColorEditFlags_NoTooltip, ImVec2(40, 40));
522 ImGui::SameLine();
523 ImGui::Text(tr("Index: %d"), state_->current_color_index);
524}
525
527 ImGui::Text(tr("Navigator"));
528
529 if (state_->open_sheets.empty()) {
530 ImGui::TextDisabled(tr("No sheet"));
531 return;
532 }
533
534 auto& sheet =
536 if (!sheet.texture())
537 return;
538
539 // Draw mini version of the sheet
540 float mini_scale = 0.5f;
541 float mini_width = sheet.width() * mini_scale;
542 float mini_height = sheet.height() * mini_scale;
543
544 ImVec2 pos = ImGui::GetCursorScreenPos();
545
546 ImGui::GetWindowDrawList()->AddImage(
547 (ImTextureID)(intptr_t)sheet.texture(), pos,
548 ImVec2(pos.x + mini_width, pos.y + mini_height));
549
550 // Draw viewport rectangle
551 // TODO: Calculate actual viewport bounds based on scroll position
552
553 ImGui::Dummy(ImVec2(mini_width, mini_height));
554}
555
557 ImGui::Separator();
558
559 // Tool name
560 ImGui::Text("%s", state_->GetToolName());
561 ImGui::SameLine();
562
563 // Cursor position
564 if (cursor_in_canvas_) {
565 ImGui::Text(tr("Pos: %d, %d"), cursor_x_, cursor_y_);
566 ImGui::SameLine();
567
568 // Tile coordinates
569 int tile_x = cursor_x_ / 8;
570 int tile_y = cursor_y_ / 8;
571 ImGui::Text(tr("Tile: %d, %d"), tile_x, tile_y);
572 ImGui::SameLine();
573 }
574
575 // Sheet info
576 ImGui::Text(tr("Sheet: %02X"), state_->current_sheet_id);
577 ImGui::SameLine();
578
581 ImGui::TextColored(gui::GetWarningColor(), tr("Focus: %d"),
583 ImGui::SameLine();
584 }
585
586 // Modified indicator
588 ImGui::TextColored(gui::GetModifiedColor(), tr("(Modified)"));
589 }
590
591 // Zoom level
592 ImGui::SameLine();
593 ImGui::SetCursorPosX(ImGui::GetWindowWidth() - 80);
594 ImGui::Text(tr("Zoom: %.0fx"), state_->zoom_level);
595}
596
598 if (!ImGui::IsItemHovered()) {
599 cursor_in_canvas_ = false;
600 return;
601 }
602
603 cursor_in_canvas_ = true;
604 ImVec2 mouse_pos = ImGui::GetMousePos();
605 ImVec2 pixel_pos = ScreenToPixel(mouse_pos);
606
607 cursor_x_ = static_cast<int>(pixel_pos.x);
608 cursor_y_ = static_cast<int>(pixel_pos.y);
609
610 auto& sheet =
612
613 // Clamp to sheet bounds
614 cursor_x_ = std::clamp(cursor_x_, 0, sheet.width() - 1);
615 cursor_y_ = std::clamp(cursor_y_, 0, sheet.height() - 1);
616
617 // Mouse button handling
618 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
619 is_drawing_ = true;
621 ImVec2(static_cast<float>(cursor_x_), static_cast<float>(cursor_y_));
623
624 // Save undo state before starting to draw
626
627 // Handle tools that need start position
628 switch (state_->current_tool) {
631 break;
634 break;
637 break;
638 case PixelTool::kFill:
640 break;
643 break;
646 break;
647 case PixelTool::kLine:
649 show_tool_preview_ = true;
650 break;
651 default:
652 break;
653 }
654 }
655
656 if (ImGui::IsMouseDragging(ImGuiMouseButton_Left) && is_drawing_) {
658 ImVec2(static_cast<float>(cursor_x_), static_cast<float>(cursor_y_));
659
660 switch (state_->current_tool) {
663 break;
666 break;
669 break;
672 break;
673 default:
674 break;
675 }
676
678 ImVec2(static_cast<float>(cursor_x_), static_cast<float>(cursor_y_));
679 }
680
681 if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) && is_drawing_) {
682 is_drawing_ = false;
683
684 switch (state_->current_tool) {
685 case PixelTool::kLine:
686 DrawLine(static_cast<int>(tool_start_pixel_.x),
687 static_cast<int>(tool_start_pixel_.y), cursor_x_, cursor_y_);
688 break;
690 DrawRectangle(static_cast<int>(tool_start_pixel_.x),
691 static_cast<int>(tool_start_pixel_.y), cursor_x_,
692 cursor_y_, false);
693 break;
695 EndSelection();
696 break;
697 default:
698 break;
699 }
700
701 // Finalize undo action after the edit stroke completes
703
704 show_tool_preview_ = false;
705 }
706}
707
709 auto& sheet =
711
712 if (x >= 0 && x < sheet.width() && y >= 0 && y < sheet.height()) {
713 sheet.WriteToPixel(x, y, state_->current_color_index);
716 }
717}
718
719void PixelEditorView::ApplyBrush(int x, int y) {
720 auto& sheet =
722 int size = state_->brush_size;
723 int half = size / 2;
724
725 for (int dy = -half; dy < size - half; dy++) {
726 for (int dx = -half; dx < size - half; dx++) {
727 int px = x + dx;
728 int py = y + dy;
729 if (px >= 0 && px < sheet.width() && py >= 0 && py < sheet.height()) {
730 sheet.WriteToPixel(px, py, state_->current_color_index);
731 }
732 }
733 }
734
737}
738
740 auto& sheet =
742 int size = state_->brush_size;
743 int half = size / 2;
744
745 for (int dy = -half; dy < size - half; dy++) {
746 for (int dx = -half; dx < size - half; dx++) {
747 int px = x + dx;
748 int py = y + dy;
749 if (px >= 0 && px < sheet.width() && py >= 0 && py < sheet.height()) {
750 sheet.WriteToPixel(px, py, 0); // Index 0 = transparent
751 }
752 }
753 }
754
757}
758
759void PixelEditorView::ApplyFill(int x, int y) {
760 auto& sheet =
762
763 if (x < 0 || x >= sheet.width() || y < 0 || y >= sheet.height())
764 return;
765
766 uint8_t target_color = sheet.GetPixel(x, y);
767 uint8_t fill_color = state_->current_color_index;
768
769 if (target_color == fill_color)
770 return; // Nothing to fill
771
772 // BFS flood fill
773 std::queue<std::pair<int, int>> queue;
774 std::vector<bool> visited(sheet.width() * sheet.height(), false);
775
776 queue.push({x, y});
777 visited[y * sheet.width() + x] = true;
778
779 while (!queue.empty()) {
780 auto [cx, cy] = queue.front();
781 queue.pop();
782
783 sheet.WriteToPixel(cx, cy, fill_color);
784
785 // Check 4-connected neighbors
786 const int dx[] = {0, 0, -1, 1};
787 const int dy[] = {-1, 1, 0, 0};
788
789 for (int i = 0; i < 4; i++) {
790 int nx = cx + dx[i];
791 int ny = cy + dy[i];
792
793 if (nx >= 0 && nx < sheet.width() && ny >= 0 && ny < sheet.height()) {
794 int idx = ny * sheet.width() + nx;
795 if (!visited[idx] && sheet.GetPixel(nx, ny) == target_color) {
796 visited[idx] = true;
797 queue.push({nx, ny});
798 }
799 }
800 }
801 }
802
805}
806
809
810 if (x >= 0 && x < sheet.width() && y >= 0 && y < sheet.height()) {
811 state_->current_color_index = sheet.GetPixel(x, y);
812
813 // Update current color display
814 auto palette = sheet.palette();
815 if (state_->current_color_index < palette.size()) {
816 auto& color = palette[state_->current_color_index];
818 ImVec4(color.rgb().x / 255.0f, color.rgb().y / 255.0f,
819 color.rgb().z / 255.0f, 1.0f);
820 }
821 }
822}
823
824void PixelEditorView::DrawLine(int x1, int y1, int x2, int y2) {
825 auto& sheet =
827
828 // Bresenham's line algorithm
829 int dx = std::abs(x2 - x1);
830 int dy = std::abs(y2 - y1);
831 int sx = x1 < x2 ? 1 : -1;
832 int sy = y1 < y2 ? 1 : -1;
833 int err = dx - dy;
834
835 while (true) {
836 if (x1 >= 0 && x1 < sheet.width() && y1 >= 0 && y1 < sheet.height()) {
837 sheet.WriteToPixel(x1, y1, state_->current_color_index);
838 }
839
840 if (x1 == x2 && y1 == y2)
841 break;
842
843 int e2 = 2 * err;
844 if (e2 > -dy) {
845 err -= dy;
846 x1 += sx;
847 }
848 if (e2 < dx) {
849 err += dx;
850 y1 += sy;
851 }
852 }
853
856}
857
858void PixelEditorView::DrawRectangle(int x1, int y1, int x2, int y2,
859 bool filled) {
860 auto& sheet =
862
863 int min_x = std::min(x1, x2);
864 int max_x = std::max(x1, x2);
865 int min_y = std::min(y1, y2);
866 int max_y = std::max(y1, y2);
867
868 if (filled) {
869 for (int y = min_y; y <= max_y; y++) {
870 for (int x = min_x; x <= max_x; x++) {
871 if (x >= 0 && x < sheet.width() && y >= 0 && y < sheet.height()) {
872 sheet.WriteToPixel(x, y, state_->current_color_index);
873 }
874 }
875 }
876 } else {
877 // Top and bottom edges
878 for (int x = min_x; x <= max_x; x++) {
879 if (x >= 0 && x < sheet.width()) {
880 if (min_y >= 0 && min_y < sheet.height())
881 sheet.WriteToPixel(x, min_y, state_->current_color_index);
882 if (max_y >= 0 && max_y < sheet.height())
883 sheet.WriteToPixel(x, max_y, state_->current_color_index);
884 }
885 }
886 // Left and right edges
887 for (int y = min_y; y <= max_y; y++) {
888 if (y >= 0 && y < sheet.height()) {
889 if (min_x >= 0 && min_x < sheet.width())
890 sheet.WriteToPixel(min_x, y, state_->current_color_index);
891 if (max_x >= 0 && max_x < sheet.width())
892 sheet.WriteToPixel(max_x, y, state_->current_color_index);
893 }
894 }
895 }
896
899}
900
902 state_->selection.x = x;
903 state_->selection.y = y;
906 state_->selection.is_active = true;
907 state_->is_selecting = true;
908}
909
911 int start_x = static_cast<int>(tool_start_pixel_.x);
912 int start_y = static_cast<int>(tool_start_pixel_.y);
913
914 state_->selection.x = std::min(start_x, x);
915 state_->selection.y = std::min(start_y, y);
916 state_->selection.width = std::abs(x - start_x) + 1;
917 state_->selection.height = std::abs(y - start_y) + 1;
918}
919
921 state_->is_selecting = false;
922
923 // Copy pixel data for the selection
924 if (state_->selection.width > 0 && state_->selection.height > 0) {
928
929 for (int y = 0; y < state_->selection.height; y++) {
930 for (int x = 0; x < state_->selection.width; x++) {
931 int src_x = state_->selection.x + x;
932 int src_y = state_->selection.y + y;
933 if (src_x >= 0 && src_x < sheet.width() && src_y >= 0 &&
934 src_y < sheet.height()) {
936 sheet.GetPixel(src_x, src_y);
937 }
938 }
939 }
940
941 state_->selection.palette = sheet.palette();
942 }
943}
944
946 // Selection data is already in state_->selection
947}
948
950 if (state_->selection.pixel_data.empty())
951 return;
952
953 auto& sheet =
955
957
958 for (int dy = 0; dy < state_->selection.height; dy++) {
959 for (int dx = 0; dx < state_->selection.width; dx++) {
960 int dest_x = x + dx;
961 int dest_y = y + dy;
962 if (dest_x >= 0 && dest_x < sheet.width() && dest_y >= 0 &&
963 dest_y < sheet.height()) {
964 uint8_t pixel =
966 sheet.WriteToPixel(dest_x, dest_y, pixel);
967 }
968 }
969 }
970
974}
975
977 if (state_->selection.pixel_data.empty())
978 return;
979
980 std::vector<uint8_t> flipped(state_->selection.pixel_data.size());
981 for (int y = 0; y < state_->selection.height; y++) {
982 for (int x = 0; x < state_->selection.width; x++) {
983 int src_idx = y * state_->selection.width + x;
984 int dst_idx =
985 y * state_->selection.width + (state_->selection.width - 1 - x);
986 flipped[dst_idx] = state_->selection.pixel_data[src_idx];
987 }
988 }
989 state_->selection.pixel_data = std::move(flipped);
990}
991
993 if (state_->selection.pixel_data.empty())
994 return;
995
996 std::vector<uint8_t> flipped(state_->selection.pixel_data.size());
997 for (int y = 0; y < state_->selection.height; y++) {
998 for (int x = 0; x < state_->selection.width; x++) {
999 int src_idx = y * state_->selection.width + x;
1000 int dst_idx =
1001 (state_->selection.height - 1 - y) * state_->selection.width + x;
1002 flipped[dst_idx] = state_->selection.pixel_data[src_idx];
1003 }
1004 }
1005 state_->selection.pixel_data = std::move(flipped);
1006}
1007
1009 if (!undo_manager_)
1010 return;
1013 pending_undo_before_data_ = sheet.vector();
1014 has_pending_undo_ = true;
1015}
1016
1019 has_pending_undo_ = false;
1020 return;
1021 }
1022
1024 auto after_data = sheet.vector();
1025
1026 // Only push if the data actually changed
1027 if (after_data != pending_undo_before_data_) {
1028 auto description =
1029 absl::StrFormat("Edit pixels on sheet %02X", pending_undo_sheet_id_);
1030 undo_manager_->Push(std::make_unique<GraphicsPixelEditAction>(
1032 std::move(after_data), std::move(description)));
1033 }
1034
1035 has_pending_undo_ = false;
1037}
1038
1039ImVec2 PixelEditorView::ScreenToPixel(ImVec2 screen_pos) {
1040 float px = (screen_pos.x - canvas_.zero_point().x) / state_->zoom_level;
1041 float py = (screen_pos.y - canvas_.zero_point().y) / state_->zoom_level;
1042 return ImVec2(px, py);
1043}
1044
1046 return ImVec2(canvas_.zero_point().x + x * state_->zoom_level,
1047 canvas_.zero_point().y + y * state_->zoom_level);
1048}
1049
1050} // namespace editor
1051} // namespace yaze
void MarkSheetModified(uint16_t sheet_id)
Mark a sheet as modified for save tracking.
void SetZoom(float zoom)
Set zoom level with clamping.
const char * GetToolName() const
Get tool name for status display.
void CloseSheet(uint16_t sheet_id)
Close a sheet tab.
void SetTool(PixelTool tool)
Set the current editing tool.
void ApplyFill(int x, int y)
Apply flood fill starting at position.
void DrawBrushPreview()
Draw brush size preview circle.
void DrawCursorCrosshair()
Draw crosshair at cursor position.
absl::Status Update()
Legacy Update method for backward compatibility.
void DrawCanvas()
Draw the main editing canvas.
void ApplyPencil(int x, int y)
Apply pencil tool at position.
std::vector< uint8_t > pending_undo_before_data_
void FinalizeUndoAction()
Finalize the current undo action by capturing the after-snapshot and pushing a GraphicsPixelEditActio...
void BeginSelection(int x, int y)
Start a new selection.
void HandleCanvasInput()
Handle canvas mouse input for current tool.
void DrawTransparencyGrid(float canvas_width, float canvas_height)
Draw checkerboard pattern for transparent pixels.
void Initialize()
Initialize the view.
void ApplyEyedropper(int x, int y)
Apply eyedropper tool at position.
ImVec2 PixelToScreen(int x, int y)
Convert pixel coordinates to screen coordinates.
void DrawPixelInfoTooltip(const gfx::Bitmap &sheet)
Draw tooltip with pixel information.
void FlipSelectionHorizontal()
Flip selection horizontally.
ImVec2 ScreenToPixel(ImVec2 screen_pos)
Convert screen coordinates to pixel coordinates.
void DrawTileHighlight(const gfx::Bitmap &sheet)
Draw a transient highlight for a target tile.
void PasteSelection(int x, int y)
Paste clipboard at position.
void Draw(bool *p_open) override
Draw the pixel editor UI (WindowContent interface)
void DrawLine(int x1, int y1, int x2, int y2)
Draw line from start to end.
void CopySelection()
Copy selection to clipboard.
void UpdateSelection(int x, int y)
Update selection during drag.
void FlipSelectionVertical()
Flip selection vertically.
void DrawToolbar()
Draw the toolbar with tool selection.
void DrawViewControls()
Draw zoom and view controls.
void SaveUndoState()
Save current state for undo (captures before-snapshot)
void DrawRectangle(int x1, int y1, int x2, int y2, bool filled)
Draw rectangle from start to end.
void EndSelection()
Finalize the selection.
void ApplyBrush(int x, int y)
Apply brush tool at position.
void DrawStatusBar()
Draw the status bar with cursor position.
void ApplyEraser(int x, int y)
Apply eraser tool at position.
void DrawColorPicker()
Draw the color palette picker.
void DrawMiniMap()
Draw the mini navigation map.
void Push(std::unique_ptr< UndoAction > action)
absl::Status Redo()
Redo the top action. Returns error if stack is empty.
absl::Status Undo()
Undo the top action. Returns error if stack is empty.
auto mutable_gfx_sheets()
Get mutable reference to all graphics sheets.
Definition arena.h:178
std::array< gfx::Bitmap, 223 > & gfx_sheets()
Get reference to all graphics sheets.
Definition arena.h:152
void NotifySheetModified(int sheet_index)
Notify Arena that a graphics sheet has been modified.
Definition arena.cc:393
static Arena & Get()
Definition arena.cc:21
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
const SnesPalette & palette() const
Definition bitmap.h:389
int height() const
Definition bitmap.h:395
int width() const
Definition bitmap.h:394
uint8_t GetPixel(int x, int y) const
Get the palette index at the given x,y coordinates.
Definition bitmap.h:274
auto draw_list() const
Definition canvas.h:347
auto canvas_size() const
Definition canvas.h:357
auto zero_point() const
Definition canvas.h:348
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
#define ICON_MD_COLORIZE
Definition icons.h:441
#define ICON_MD_TEXTURE
Definition icons.h:1965
#define ICON_MD_DRAW
Definition icons.h:625
#define ICON_MD_BRUSH
Definition icons.h:325
#define ICON_MD_ZOOM_OUT
Definition icons.h:2196
#define ICON_MD_REDO
Definition icons.h:1570
#define ICON_MD_FORMAT_COLOR_FILL
Definition icons.h:830
#define ICON_MD_AUTO_FIX_HIGH
Definition icons.h:218
#define ICON_MD_GRID_ON
Definition icons.h:896
#define ICON_MD_ADD
Definition icons.h:86
#define ICON_MD_CROP_SQUARE
Definition icons.h:500
#define ICON_MD_HORIZONTAL_RULE
Definition icons.h:960
#define ICON_MD_ZOOM_IN
Definition icons.h:2194
#define ICON_MD_SELECT_ALL
Definition icons.h:1680
#define ICON_MD_UNDO
Definition icons.h:2039
#define HOVER_HINT(string)
Definition macro.h:24
PixelTool
Pixel editing tool types for the graphics editor.
bool BeginThemedTabBar(const char *id, ImGuiTabBarFlags flags)
A stylized tab bar with "Mission Control" branding.
ImVec4 GetSelectedColor()
Definition ui_helpers.cc:99
void EndThemedTabBar()
ImVec4 GetPrimaryVec4()
ImVec4 GetWarningColor()
Definition ui_helpers.cc:54
ImVec4 GetModifiedColor()
bool ToolbarIconButton(const char *icon, const char *tooltip, bool is_active)
Convenience wrapper for toolbar-sized icon buttons.
std::vector< uint8_t > pixel_data