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"
19#include "imgui/imgui.h"
20
21namespace yaze {
22namespace editor {
23
25 // Canvas is initialized via member initializer list
26}
27
28void PixelEditorView::Draw(bool* p_open) {
29 // WindowContent interface - delegate to Update()
30 Update().IgnoreError();
31}
32
34 // Top toolbar
36 ImGui::SameLine();
38
39 ImGui::Separator();
40
41 constexpr float kColorPickerWidth = 200.0f;
42 constexpr float kStatusBarHeight = 24.0f;
43
44 // Main content area with canvas and side panels
45 ImGui::BeginChild("##PixelEditorContent", ImVec2(0, -kStatusBarHeight),
46 false);
47
48 // Color picker on the left
49 ImGui::BeginChild("##ColorPickerSide", ImVec2(kColorPickerWidth, 0), true);
51 ImGui::Separator();
53 ImGui::EndChild();
54
55 ImGui::SameLine();
56
57 // Main canvas
58 ImGui::BeginChild("##CanvasArea", ImVec2(0, 0), true,
59 ImGuiWindowFlags_HorizontalScrollbar);
60 DrawCanvas();
61 ImGui::EndChild();
62
63 ImGui::EndChild();
64
65 // Status bar
67
68 return absl::OkStatus();
69}
70
72 // Tool selection buttons
73 auto tool_button = [this](PixelTool tool, const char* icon,
74 const char* tooltip) {
75 bool is_selected = state_->current_tool == tool;
76 std::optional<gui::StyleColorGuard> sel_guard;
77 if (is_selected) {
78 sel_guard.emplace(ImGuiCol_Button, gui::GetPrimaryVec4());
79 }
80 if (ImGui::Button(icon)) {
81 state_->SetTool(tool);
82 }
83 sel_guard.reset();
84 if (ImGui::IsItemHovered()) {
85 ImGui::SetTooltip("%s", tooltip);
86 }
87 ImGui::SameLine();
88 };
89
90 tool_button(PixelTool::kSelect, ICON_MD_SELECT_ALL, "Select (V)");
91 tool_button(PixelTool::kPencil, ICON_MD_DRAW, "Pencil (B)");
92 tool_button(PixelTool::kBrush, ICON_MD_BRUSH, "Brush (B)");
93 tool_button(PixelTool::kEraser, ICON_MD_AUTO_FIX_HIGH, "Eraser (E)");
94 tool_button(PixelTool::kFill, ICON_MD_FORMAT_COLOR_FILL, "Fill (G)");
95 tool_button(PixelTool::kLine, ICON_MD_HORIZONTAL_RULE, "Line");
96 tool_button(PixelTool::kRectangle, ICON_MD_CROP_SQUARE, "Rectangle");
97 tool_button(PixelTool::kEyedropper, ICON_MD_COLORIZE, "Eyedropper (I)");
98
99 ImGui::SameLine();
100 ImGui::Text("|");
101 ImGui::SameLine();
102
103 // Brush size for pencil/brush/eraser
107 ImGui::SetNextItemWidth(80);
108 int brush = state_->brush_size;
109 if (ImGui::SliderInt("##BrushSize", &brush, 1, 8, "%d px")) {
110 state_->brush_size = static_cast<uint8_t>(brush);
111 }
112 HOVER_HINT("Brush size");
113 ImGui::SameLine();
114 }
115
116 // Undo/Redo buttons
117 ImGui::Text("|");
118 ImGui::SameLine();
119
120 ImGui::BeginDisabled(!undo_manager_ || !undo_manager_->CanUndo());
121 if (gui::ToolbarIconButton(ICON_MD_UNDO, "Undo (Ctrl+Z)") && undo_manager_) {
122 undo_manager_->Undo().IgnoreError();
123 }
124 ImGui::EndDisabled();
125
126 ImGui::SameLine();
127
128 ImGui::BeginDisabled(!undo_manager_ || !undo_manager_->CanRedo());
129 if (gui::ToolbarIconButton(ICON_MD_REDO, "Redo (Ctrl+Y)") && undo_manager_) {
130 undo_manager_->Redo().IgnoreError();
131 }
132 ImGui::EndDisabled();
133}
134
136 // Zoom controls
137 if (gui::ToolbarIconButton(ICON_MD_ZOOM_OUT, "Zoom out (-)")) {
138 state_->ZoomOut();
139 }
140 ImGui::SameLine();
141
142 ImGui::SetNextItemWidth(100);
143 float zoom = state_->zoom_level;
144 if (ImGui::SliderFloat("##Zoom", &zoom, 1.0f, 16.0f, "%.0fx")) {
145 state_->SetZoom(zoom);
146 }
147 ImGui::SameLine();
148
149 if (gui::ToolbarIconButton(ICON_MD_ZOOM_IN, "Zoom in (+)")) {
150 state_->ZoomIn();
151 }
152
153 ImGui::SameLine();
154 ImGui::Text("|");
155 ImGui::SameLine();
156
157 // View overlay toggles
158 ImGui::Checkbox(ICON_MD_GRID_ON, &state_->show_grid);
159 HOVER_HINT("Toggle grid (Ctrl+G)");
160 ImGui::SameLine();
161
162 ImGui::Checkbox(ICON_MD_ADD, &state_->show_cursor_crosshair);
163 HOVER_HINT("Toggle cursor crosshair");
164 ImGui::SameLine();
165
166 ImGui::Checkbox(ICON_MD_BRUSH, &state_->show_brush_preview);
167 HOVER_HINT("Toggle brush preview");
168 ImGui::SameLine();
169
171 HOVER_HINT("Toggle transparency grid");
172}
173
175 if (state_->open_sheets.empty()) {
177 opts.icon = ICON_MD_TEXTURE;
178 opts.title = "Select a sheet";
179 opts.detail = "Open a graphics sheet from the browser to edit pixels here.";
180 opts.compact = true;
182 return;
183 }
184
185 // Tab bar for open sheets
186 if (gui::BeginThemedTabBar("##SheetTabs",
187 ImGuiTabBarFlags_AutoSelectNewTabs |
188 ImGuiTabBarFlags_Reorderable |
189 ImGuiTabBarFlags_TabListPopupButton)) {
190 std::vector<uint16_t> sheets_to_close;
191
192 for (uint16_t sheet_id : state_->open_sheets) {
193 bool open = true;
194 std::string tab_label = absl::StrFormat("%02X", sheet_id);
195 if (state_->modified_sheets.count(sheet_id) > 0) {
196 tab_label += "*";
197 }
198
199 if (ImGui::BeginTabItem(tab_label.c_str(), &open)) {
200 state_->current_sheet_id = sheet_id;
201
202 // Get the current sheet bitmap
203 auto& sheet = gfx::Arena::Get().mutable_gfx_sheets()->at(
205
206 if (!sheet.is_active()) {
207 ImGui::TextDisabled(tr("Sheet %02X is not active"), sheet_id);
208 ImGui::EndTabItem();
209 continue;
210 }
211
212 // Calculate canvas size based on zoom
213 float canvas_width = sheet.width() * state_->zoom_level;
214 float canvas_height = sheet.height() * state_->zoom_level;
215
216 // Draw canvas background
217 canvas_.DrawBackground(ImVec2(canvas_width, canvas_height));
218
219 // Draw transparency checkerboard background if enabled
221 DrawTransparencyGrid(canvas_width, canvas_height);
222 }
223
224 // Draw the sheet texture
225 if (sheet.texture()) {
226 canvas_.draw_list()->AddImage(
227 (ImTextureID)(intptr_t)sheet.texture(), canvas_.zero_point(),
228 ImVec2(canvas_.zero_point().x + canvas_width,
229 canvas_.zero_point().y + canvas_height));
230 }
231
232 // Draw grid if enabled
233 if (state_->show_grid) {
235 }
236
237 // Draw transient tile highlight (e.g., from "Edit Graphics" jump)
238 DrawTileHighlight(sheet);
239
240 // Draw selection rectangle if active
242 ImVec2 sel_min =
244 ImVec2 sel_max =
247 canvas_.draw_list()->AddRect(
248 sel_min, sel_max, IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f);
249
250 // Marching ants effect (simplified)
251 canvas_.draw_list()->AddRect(sel_min, sel_max, IM_COL32(0, 0, 0, 128),
252 0.0f, 0, 1.0f);
253 }
254
255 // Draw tool preview (line/rectangle)
257 ImVec2 start = PixelToScreen(static_cast<int>(tool_start_pixel_.x),
258 static_cast<int>(tool_start_pixel_.y));
259 ImVec2 end = PixelToScreen(static_cast<int>(preview_end_.x),
260 static_cast<int>(preview_end_.y));
261
263 canvas_.draw_list()->AddLine(start, end, IM_COL32(255, 255, 0, 200),
264 2.0f);
266 canvas_.draw_list()->AddRect(start, end, IM_COL32(255, 255, 0, 200),
267 0.0f, 0, 2.0f);
268 }
269 }
270
271 // Draw cursor crosshair overlay if enabled and cursor in canvas
274 }
275
276 // Draw brush preview if using brush/eraser tool
281 }
282
284
285 // Handle mouse input
287
288 // Show pixel info tooltip if enabled
291 }
292
293 ImGui::EndTabItem();
294 }
295
296 if (!open) {
297 sheets_to_close.push_back(sheet_id);
298 }
299 }
300
301 // Close tabs that were requested
302 for (uint16_t sheet_id : sheets_to_close) {
303 state_->CloseSheet(sheet_id);
304 }
305
307 }
308}
309
311 float canvas_height) {
312 const float cell_size = 8.0f; // Checkerboard cell size
313 const ImU32 color1 = IM_COL32(180, 180, 180, 255);
314 const ImU32 color2 = IM_COL32(220, 220, 220, 255);
315
316 ImVec2 origin = canvas_.zero_point();
317 int cols = static_cast<int>(canvas_width / cell_size) + 1;
318 int rows = static_cast<int>(canvas_height / cell_size) + 1;
319
320 for (int row = 0; row < rows; row++) {
321 for (int col = 0; col < cols; col++) {
322 bool is_light = (row + col) % 2 == 0;
323 ImVec2 p_min(origin.x + col * cell_size, origin.y + row * cell_size);
324 ImVec2 p_max(std::min(p_min.x + cell_size, origin.x + canvas_width),
325 std::min(p_min.y + cell_size, origin.y + canvas_height));
326 canvas_.draw_list()->AddRectFilled(p_min, p_max,
327 is_light ? color1 : color2);
328 }
329 }
330}
331
333 ImVec2 cursor_screen = PixelToScreen(cursor_x_, cursor_y_);
334 float pixel_size = state_->zoom_level;
335
336 // Vertical line through cursor pixel
337 ImVec2 v_start(cursor_screen.x + pixel_size / 2, canvas_.zero_point().y);
338 ImVec2 v_end(cursor_screen.x + pixel_size / 2,
340 canvas_.draw_list()->AddLine(v_start, v_end, IM_COL32(255, 100, 100, 100),
341 1.0f);
342
343 // Horizontal line through cursor pixel
344 ImVec2 h_start(canvas_.zero_point().x, cursor_screen.y + pixel_size / 2);
345 ImVec2 h_end(canvas_.zero_point().x + canvas_.canvas_size().x,
346 cursor_screen.y + pixel_size / 2);
347 canvas_.draw_list()->AddLine(h_start, h_end, IM_COL32(255, 100, 100, 100),
348 1.0f);
349
350 // Highlight current pixel with a bright outline
351 ImVec2 pixel_min = cursor_screen;
352 ImVec2 pixel_max(cursor_screen.x + pixel_size, cursor_screen.y + pixel_size);
353 canvas_.draw_list()->AddRect(pixel_min, pixel_max,
354 IM_COL32(255, 255, 255, 200), 0.0f, 0, 2.0f);
355}
356
358 ImVec2 cursor_screen = PixelToScreen(cursor_x_, cursor_y_);
359 float pixel_size = state_->zoom_level;
360 int brush = state_->brush_size;
361 int half = brush / 2;
362
363 // Draw preview of brush area
364 ImVec2 brush_min(cursor_screen.x - half * pixel_size,
365 cursor_screen.y - half * pixel_size);
366 ImVec2 brush_max(cursor_screen.x + (brush - half) * pixel_size,
367 cursor_screen.y + (brush - half) * pixel_size);
368
369 // Fill with semi-transparent color preview
370 ImU32 preview_color = (state_->current_tool == PixelTool::kEraser)
371 ? IM_COL32(255, 0, 0, 50)
372 : IM_COL32(0, 255, 0, 50);
373 canvas_.draw_list()->AddRectFilled(brush_min, brush_max, preview_color);
374
375 // Outline
376 ImU32 outline_color = (state_->current_tool == PixelTool::kEraser)
377 ? IM_COL32(255, 100, 100, 200)
378 : IM_COL32(100, 255, 100, 200);
379 canvas_.draw_list()->AddRect(brush_min, brush_max, outline_color, 0.0f, 0,
380 1.0f);
381}
382
384 if (cursor_x_ < 0 || cursor_x_ >= sheet.width() || cursor_y_ < 0 ||
385 cursor_y_ >= sheet.height()) {
386 return;
387 }
388
389 uint8_t color_index = sheet.GetPixel(cursor_x_, cursor_y_);
390 auto palette = sheet.palette();
391
392 ImGui::BeginTooltip();
393 ImGui::Text(tr("Pos: %d, %d"), cursor_x_, cursor_y_);
394 ImGui::Text(tr("Tile: %d, %d"), cursor_x_ / 8, cursor_y_ / 8);
395 ImGui::Text(tr("Index: %d"), color_index);
396
397 if (color_index < palette.size()) {
398 ImGui::Text(tr("SNES: $%04X"), palette[color_index].snes());
399 ImVec4 color(palette[color_index].rgb().x / 255.0f,
400 palette[color_index].rgb().y / 255.0f,
401 palette[color_index].rgb().z / 255.0f, 1.0f);
402 ImGui::ColorButton("##ColorPreview", color, ImGuiColorEditFlags_NoTooltip,
403 ImVec2(24, 24));
404 if (color_index == 0) {
405 ImGui::SameLine();
406 ImGui::TextDisabled(tr("(Transparent)"));
407 }
408 }
409 ImGui::EndTooltip();
410}
411
414 return;
415 }
417 return;
418 }
419
420 const double now = ImGui::GetTime();
421 const double elapsed = now - state_->tile_highlight.start_time;
422 if (elapsed > state_->tile_highlight.duration) {
424 return;
425 }
426
427 const int tiles_per_row = sheet.width() / 8;
428 if (tiles_per_row <= 0) {
429 return;
430 }
431 const uint16_t tile_index = state_->tile_highlight.tile_index;
432 const int tile_x = static_cast<int>(tile_index % tiles_per_row);
433 const int tile_y = static_cast<int>(tile_index / tiles_per_row);
434
435 const float pulse =
436 0.5f + 0.5f * std::sin(static_cast<float>(elapsed) * 6.0f);
437 const float alpha = 0.25f + (pulse * 0.35f);
438
439 ImVec2 min = PixelToScreen(tile_x * 8, tile_y * 8);
440 ImVec2 max = PixelToScreen(tile_x * 8 + 8, tile_y * 8 + 8);
441
442 ImVec4 sel = gui::GetSelectedColor();
443 sel.w = alpha;
444 const ImU32 fill_color = ImGui::GetColorU32(sel);
445 ImVec4 sel_outline = gui::GetSelectedColor();
446 sel_outline.w = 0.9f;
447 const ImU32 outline_color = ImGui::GetColorU32(sel_outline);
448 canvas_.draw_list()->AddRectFilled(min, max, fill_color);
449 canvas_.draw_list()->AddRect(min, max, outline_color, 0.0f, 0, 2.0f);
450
451 if (ImGui::IsMouseHoveringRect(min, max)) {
452 ImGui::BeginTooltip();
453 ImGui::Text(tr("Focus tile: %d (sheet %02X)"), tile_index,
455 if (!state_->tile_highlight.label.empty()) {
456 ImGui::Text("%s", state_->tile_highlight.label.c_str());
457 }
458 ImGui::EndTooltip();
459 }
460}
461
463 ImGui::Text(tr("Colors"));
464
465 if (state_->open_sheets.empty()) {
466 ImGui::TextDisabled(tr("No sheet"));
467 return;
468 }
469
470 auto& sheet =
472 auto palette = sheet.palette();
473
474 // Draw palette colors in 4x4 grid (16 colors)
475 for (int i = 0; i < static_cast<int>(palette.size()) && i < 16; i++) {
476 if (i > 0 && i % 4 == 0) {
477 // New row
478 } else if (i > 0) {
479 ImGui::SameLine();
480 }
481
482 ImVec4 color(palette[i].rgb().x / 255.0f, palette[i].rgb().y / 255.0f,
483 palette[i].rgb().z / 255.0f, 1.0f);
484
485 bool is_selected = state_->current_color_index == i;
486 std::optional<gui::StyleVarGuard> border_var;
487 std::optional<gui::StyleColorGuard> border_color;
488 if (is_selected) {
489 border_var.emplace(ImGuiStyleVar_FrameBorderSize, 2.0f);
490 border_color.emplace(ImGuiCol_Border, gui::GetWarningColor());
491 }
492
493 std::string id = absl::StrFormat("##Color%d", i);
494 if (ImGui::ColorButton(
495 id.c_str(), color,
496 ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoBorder,
497 ImVec2(24, 24))) {
498 state_->current_color_index = static_cast<uint8_t>(i);
499 state_->current_color = color;
500 }
501
502 if (is_selected) {
503 border_color.reset();
504 border_var.reset();
505 }
506
507 if (ImGui::IsItemHovered()) {
508 ImGui::BeginTooltip();
509 ImGui::Text(tr("Index: %d"), i);
510 ImGui::Text(tr("SNES: $%04X"), palette[i].snes());
511 ImGui::Text(tr("RGB: %d, %d, %d"), static_cast<int>(palette[i].rgb().x),
512 static_cast<int>(palette[i].rgb().y),
513 static_cast<int>(palette[i].rgb().z));
514 if (i == 0) {
515 ImGui::Text(tr("(Transparent)"));
516 }
517 ImGui::EndTooltip();
518 }
519 }
520
521 ImGui::Separator();
522
523 // Current color preview
524 ImGui::Text(tr("Current:"));
525 ImGui::ColorButton("##CurrentColor", state_->current_color,
526 ImGuiColorEditFlags_NoTooltip, ImVec2(40, 40));
527 ImGui::SameLine();
528 ImGui::Text(tr("Index: %d"), state_->current_color_index);
529}
530
532 ImGui::Text(tr("Navigator"));
533
534 if (state_->open_sheets.empty()) {
535 ImGui::TextDisabled(tr("No sheet"));
536 return;
537 }
538
539 auto& sheet =
541 if (!sheet.texture())
542 return;
543
544 // Draw mini version of the sheet
545 float mini_scale = 0.5f;
546 float mini_width = sheet.width() * mini_scale;
547 float mini_height = sheet.height() * mini_scale;
548
549 ImVec2 pos = ImGui::GetCursorScreenPos();
550
551 ImGui::GetWindowDrawList()->AddImage(
552 (ImTextureID)(intptr_t)sheet.texture(), pos,
553 ImVec2(pos.x + mini_width, pos.y + mini_height));
554
555 // Draw viewport rectangle
556 // TODO: Calculate actual viewport bounds based on scroll position
557
558 ImGui::Dummy(ImVec2(mini_width, mini_height));
559}
560
562 ImGui::Separator();
563
564 // Tool name
565 ImGui::Text("%s", state_->GetToolName());
566 ImGui::SameLine();
567
568 // Cursor position
569 if (cursor_in_canvas_) {
570 ImGui::Text(tr("Pos: %d, %d"), cursor_x_, cursor_y_);
571 ImGui::SameLine();
572
573 // Tile coordinates
574 int tile_x = cursor_x_ / 8;
575 int tile_y = cursor_y_ / 8;
576 ImGui::Text(tr("Tile: %d, %d"), tile_x, tile_y);
577 ImGui::SameLine();
578 }
579
580 // Sheet info
581 ImGui::Text(tr("Sheet: %02X"), state_->current_sheet_id);
582 ImGui::SameLine();
583
586 ImGui::TextColored(gui::GetWarningColor(), tr("Focus: %d"),
588 ImGui::SameLine();
589 }
590
591 // Modified indicator
593 ImGui::TextColored(gui::GetModifiedColor(), tr("(Modified)"));
594 }
595
596 // Zoom level
597 ImGui::SameLine();
598 ImGui::SetCursorPosX(ImGui::GetWindowWidth() - 80);
599 ImGui::Text(tr("Zoom: %.0fx"), state_->zoom_level);
600}
601
603 if (!ImGui::IsItemHovered()) {
604 cursor_in_canvas_ = false;
605 return;
606 }
607
608 cursor_in_canvas_ = true;
609 ImVec2 mouse_pos = ImGui::GetMousePos();
610 ImVec2 pixel_pos = ScreenToPixel(mouse_pos);
611
612 cursor_x_ = static_cast<int>(pixel_pos.x);
613 cursor_y_ = static_cast<int>(pixel_pos.y);
614
615 auto& sheet =
617
618 // Clamp to sheet bounds
619 cursor_x_ = std::clamp(cursor_x_, 0, sheet.width() - 1);
620 cursor_y_ = std::clamp(cursor_y_, 0, sheet.height() - 1);
621
622 // Mouse button handling
623 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
624 is_drawing_ = true;
626 ImVec2(static_cast<float>(cursor_x_), static_cast<float>(cursor_y_));
628
629 // Save undo state before starting to draw
631
632 // Handle tools that need start position
633 switch (state_->current_tool) {
636 break;
639 break;
642 break;
643 case PixelTool::kFill:
645 break;
648 break;
651 break;
652 case PixelTool::kLine:
654 show_tool_preview_ = true;
655 break;
656 default:
657 break;
658 }
659 }
660
661 if (ImGui::IsMouseDragging(ImGuiMouseButton_Left) && is_drawing_) {
663 ImVec2(static_cast<float>(cursor_x_), static_cast<float>(cursor_y_));
664
665 switch (state_->current_tool) {
668 break;
671 break;
674 break;
677 break;
678 default:
679 break;
680 }
681
683 ImVec2(static_cast<float>(cursor_x_), static_cast<float>(cursor_y_));
684 }
685
686 if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) && is_drawing_) {
687 is_drawing_ = false;
688
689 switch (state_->current_tool) {
690 case PixelTool::kLine:
691 DrawLine(static_cast<int>(tool_start_pixel_.x),
692 static_cast<int>(tool_start_pixel_.y), cursor_x_, cursor_y_);
693 break;
695 DrawRectangle(static_cast<int>(tool_start_pixel_.x),
696 static_cast<int>(tool_start_pixel_.y), cursor_x_,
697 cursor_y_, false);
698 break;
700 EndSelection();
701 break;
702 default:
703 break;
704 }
705
706 // Finalize undo action after the edit stroke completes
708
709 show_tool_preview_ = false;
710 }
711}
712
714 auto& sheet =
716
717 if (x >= 0 && x < sheet.width() && y >= 0 && y < sheet.height()) {
718 sheet.WriteToPixel(x, y, state_->current_color_index);
721 }
722}
723
724void PixelEditorView::ApplyBrush(int x, int y) {
725 auto& sheet =
727 int size = state_->brush_size;
728 int half = size / 2;
729
730 for (int dy = -half; dy < size - half; dy++) {
731 for (int dx = -half; dx < size - half; dx++) {
732 int px = x + dx;
733 int py = y + dy;
734 if (px >= 0 && px < sheet.width() && py >= 0 && py < sheet.height()) {
735 sheet.WriteToPixel(px, py, state_->current_color_index);
736 }
737 }
738 }
739
742}
743
745 auto& sheet =
747 int size = state_->brush_size;
748 int half = size / 2;
749
750 for (int dy = -half; dy < size - half; dy++) {
751 for (int dx = -half; dx < size - half; dx++) {
752 int px = x + dx;
753 int py = y + dy;
754 if (px >= 0 && px < sheet.width() && py >= 0 && py < sheet.height()) {
755 sheet.WriteToPixel(px, py, 0); // Index 0 = transparent
756 }
757 }
758 }
759
762}
763
764void PixelEditorView::ApplyFill(int x, int y) {
765 auto& sheet =
767
768 if (x < 0 || x >= sheet.width() || y < 0 || y >= sheet.height())
769 return;
770
771 uint8_t target_color = sheet.GetPixel(x, y);
772 uint8_t fill_color = state_->current_color_index;
773
774 if (target_color == fill_color)
775 return; // Nothing to fill
776
777 // BFS flood fill
778 std::queue<std::pair<int, int>> queue;
779 std::vector<bool> visited(sheet.width() * sheet.height(), false);
780
781 queue.push({x, y});
782 visited[y * sheet.width() + x] = true;
783
784 while (!queue.empty()) {
785 auto [cx, cy] = queue.front();
786 queue.pop();
787
788 sheet.WriteToPixel(cx, cy, fill_color);
789
790 // Check 4-connected neighbors
791 const int dx[] = {0, 0, -1, 1};
792 const int dy[] = {-1, 1, 0, 0};
793
794 for (int i = 0; i < 4; i++) {
795 int nx = cx + dx[i];
796 int ny = cy + dy[i];
797
798 if (nx >= 0 && nx < sheet.width() && ny >= 0 && ny < sheet.height()) {
799 int idx = ny * sheet.width() + nx;
800 if (!visited[idx] && sheet.GetPixel(nx, ny) == target_color) {
801 visited[idx] = true;
802 queue.push({nx, ny});
803 }
804 }
805 }
806 }
807
810}
811
814
815 if (x >= 0 && x < sheet.width() && y >= 0 && y < sheet.height()) {
816 state_->current_color_index = sheet.GetPixel(x, y);
817
818 // Update current color display
819 auto palette = sheet.palette();
820 if (state_->current_color_index < palette.size()) {
821 auto& color = palette[state_->current_color_index];
823 ImVec4(color.rgb().x / 255.0f, color.rgb().y / 255.0f,
824 color.rgb().z / 255.0f, 1.0f);
825 }
826 }
827}
828
829void PixelEditorView::DrawLine(int x1, int y1, int x2, int y2) {
830 auto& sheet =
832
833 // Bresenham's line algorithm
834 int dx = std::abs(x2 - x1);
835 int dy = std::abs(y2 - y1);
836 int sx = x1 < x2 ? 1 : -1;
837 int sy = y1 < y2 ? 1 : -1;
838 int err = dx - dy;
839
840 while (true) {
841 if (x1 >= 0 && x1 < sheet.width() && y1 >= 0 && y1 < sheet.height()) {
842 sheet.WriteToPixel(x1, y1, state_->current_color_index);
843 }
844
845 if (x1 == x2 && y1 == y2)
846 break;
847
848 int e2 = 2 * err;
849 if (e2 > -dy) {
850 err -= dy;
851 x1 += sx;
852 }
853 if (e2 < dx) {
854 err += dx;
855 y1 += sy;
856 }
857 }
858
861}
862
863void PixelEditorView::DrawRectangle(int x1, int y1, int x2, int y2,
864 bool filled) {
865 auto& sheet =
867
868 int min_x = std::min(x1, x2);
869 int max_x = std::max(x1, x2);
870 int min_y = std::min(y1, y2);
871 int max_y = std::max(y1, y2);
872
873 if (filled) {
874 for (int y = min_y; y <= max_y; y++) {
875 for (int x = min_x; x <= max_x; x++) {
876 if (x >= 0 && x < sheet.width() && y >= 0 && y < sheet.height()) {
877 sheet.WriteToPixel(x, y, state_->current_color_index);
878 }
879 }
880 }
881 } else {
882 // Top and bottom edges
883 for (int x = min_x; x <= max_x; x++) {
884 if (x >= 0 && x < sheet.width()) {
885 if (min_y >= 0 && min_y < sheet.height())
886 sheet.WriteToPixel(x, min_y, state_->current_color_index);
887 if (max_y >= 0 && max_y < sheet.height())
888 sheet.WriteToPixel(x, max_y, state_->current_color_index);
889 }
890 }
891 // Left and right edges
892 for (int y = min_y; y <= max_y; y++) {
893 if (y >= 0 && y < sheet.height()) {
894 if (min_x >= 0 && min_x < sheet.width())
895 sheet.WriteToPixel(min_x, y, state_->current_color_index);
896 if (max_x >= 0 && max_x < sheet.width())
897 sheet.WriteToPixel(max_x, y, state_->current_color_index);
898 }
899 }
900 }
901
904}
905
907 state_->selection.x = x;
908 state_->selection.y = y;
911 state_->selection.is_active = true;
912 state_->is_selecting = true;
913}
914
916 int start_x = static_cast<int>(tool_start_pixel_.x);
917 int start_y = static_cast<int>(tool_start_pixel_.y);
918
919 state_->selection.x = std::min(start_x, x);
920 state_->selection.y = std::min(start_y, y);
921 state_->selection.width = std::abs(x - start_x) + 1;
922 state_->selection.height = std::abs(y - start_y) + 1;
923}
924
926 state_->is_selecting = false;
927
928 // Copy pixel data for the selection
929 if (state_->selection.width > 0 && state_->selection.height > 0) {
933
934 for (int y = 0; y < state_->selection.height; y++) {
935 for (int x = 0; x < state_->selection.width; x++) {
936 int src_x = state_->selection.x + x;
937 int src_y = state_->selection.y + y;
938 if (src_x >= 0 && src_x < sheet.width() && src_y >= 0 &&
939 src_y < sheet.height()) {
941 sheet.GetPixel(src_x, src_y);
942 }
943 }
944 }
945
946 state_->selection.palette = sheet.palette();
947 }
948}
949
951 // Selection data is already in state_->selection
952}
953
955 if (state_->selection.pixel_data.empty())
956 return;
957
958 auto& sheet =
960
962
963 for (int dy = 0; dy < state_->selection.height; dy++) {
964 for (int dx = 0; dx < state_->selection.width; dx++) {
965 int dest_x = x + dx;
966 int dest_y = y + dy;
967 if (dest_x >= 0 && dest_x < sheet.width() && dest_y >= 0 &&
968 dest_y < sheet.height()) {
969 uint8_t pixel =
971 sheet.WriteToPixel(dest_x, dest_y, pixel);
972 }
973 }
974 }
975
979}
980
982 if (state_->selection.pixel_data.empty())
983 return;
984
985 std::vector<uint8_t> flipped(state_->selection.pixel_data.size());
986 for (int y = 0; y < state_->selection.height; y++) {
987 for (int x = 0; x < state_->selection.width; x++) {
988 int src_idx = y * state_->selection.width + x;
989 int dst_idx =
990 y * state_->selection.width + (state_->selection.width - 1 - x);
991 flipped[dst_idx] = state_->selection.pixel_data[src_idx];
992 }
993 }
994 state_->selection.pixel_data = std::move(flipped);
995}
996
998 if (state_->selection.pixel_data.empty())
999 return;
1000
1001 std::vector<uint8_t> flipped(state_->selection.pixel_data.size());
1002 for (int y = 0; y < state_->selection.height; y++) {
1003 for (int x = 0; x < state_->selection.width; x++) {
1004 int src_idx = y * state_->selection.width + x;
1005 int dst_idx =
1006 (state_->selection.height - 1 - y) * state_->selection.width + x;
1007 flipped[dst_idx] = state_->selection.pixel_data[src_idx];
1008 }
1009 }
1010 state_->selection.pixel_data = std::move(flipped);
1011}
1012
1014 if (!undo_manager_)
1015 return;
1018 pending_undo_before_data_ = sheet.vector();
1019 has_pending_undo_ = true;
1020}
1021
1024 has_pending_undo_ = false;
1025 return;
1026 }
1027
1029 auto after_data = sheet.vector();
1030
1031 // Only push if the data actually changed
1032 if (after_data != pending_undo_before_data_) {
1033 auto description =
1034 absl::StrFormat("Edit pixels on sheet %02X", pending_undo_sheet_id_);
1035 undo_manager_->Push(std::make_unique<GraphicsPixelEditAction>(
1037 std::move(after_data), std::move(description),
1038 [state = state_](uint16_t sheet_id) {
1039 state->MarkSheetModified(sheet_id);
1040 }));
1041 }
1042
1043 has_pending_undo_ = false;
1045}
1046
1047ImVec2 PixelEditorView::ScreenToPixel(ImVec2 screen_pos) {
1048 float px = (screen_pos.x - canvas_.zero_point().x) / state_->zoom_level;
1049 float py = (screen_pos.y - canvas_.zero_point().y) / state_->zoom_level;
1050 return ImVec2(px, py);
1051}
1052
1054 return ImVec2(canvas_.zero_point().x + x * state_->zoom_level,
1055 canvas_.zero_point().y + y * state_->zoom_level);
1056}
1057
1058} // namespace editor
1059} // 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:205
std::array< gfx::Bitmap, 223 > & gfx_sheets()
Get reference to all graphics sheets.
Definition arena.h:177
void NotifySheetModified(int sheet_index)
Notify Arena that a graphics sheet has been modified.
Definition arena.cc:526
static Arena & Get()
Definition arena.cc:24
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:69
const SnesPalette & palette() const
Definition bitmap.h:391
int height() const
Definition bitmap.h:397
int width() const
Definition bitmap.h:396
uint8_t GetPixel(int x, int y) const
Get the palette index at the given x,y coordinates.
Definition bitmap.h:276
auto draw_list() const
Definition canvas.h:353
auto canvas_size() const
Definition canvas.h:363
auto zero_point() const
Definition canvas.h:354
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
#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()
bool DrawEmptyState(const EmptyStateOptions &options)
Draw a centered empty-state block.
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
Shared empty / disabled panel presentation.
Definition empty_state.h:15
bool compact
Tighter vertical rhythm for drawers / inspectors.
Definition empty_state.h:22