yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
pixel_editor_panel.cc
Go to the documentation of this file.
1#define IMGUI_DEFINE_MATH_OPERATORS
3#include "util/i18n/tr.h"
4
5#include <algorithm>
6#include <cmath>
7#include <memory>
8#include <queue>
9
10#include "absl/strings/str_format.h"
17#include "app/gui/core/color.h"
18#include "app/gui/core/icons.h"
19#include "app/gui/core/style.h"
26#include "imgui/imgui.h"
27#include "imgui/imgui_internal.h"
29
30namespace yaze {
31namespace editor {
32
34 // Canvas is initialized via member initializer list
35}
36
37void PixelEditorPanel::Draw(bool* p_open) {
38 // WindowContent interface - delegate to Update()
39 Update().IgnoreError();
40}
41
45
46 // Match every control to the current font/frame size. Mixing toolbar icon
47 // presets and normal buttons made Undo taller than the drawing tools.
48 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,
49 ImVec2(4, ImGui::GetStyle().ItemSpacing.y));
51 const bool visible = sheet_browser_visible_();
52 std::optional<gui::StyleColorGuard> selected;
53 if (visible)
54 selected.emplace(ImGuiCol_Button, gui::GetPrimaryVec4());
55 if (ImGui::Button(tr("Sheets")))
57 gui::AutoRegisterLastItem("button", "Toggle Sheet Browser");
58 HOVER_HINT(visible ? "Hide Sheet Browser to expand the canvas"
59 : "Show Sheet Browser in its previous position");
60 ImGui::SameLine();
61 }
63 ImGui::SameLine();
64 const float button_size = ImGui::GetFrameHeight();
65 if (gui::CenteredIconButton(ICON_MD_TUNE "##ViewOptions", button_size)) {
66 ImGui::OpenPopup("View options");
67 }
68 gui::AutoRegisterLastItem("button", "View options");
69 HOVER_HINT("Display overlays and sheet overview");
70 if (ImGui::BeginPopup("View options")) {
72 ImGui::EndPopup();
73 }
74 ImGui::SameLine();
75 if (gui::CenteredIconButton(ICON_MD_MORE_HORIZ "##PixelEditOptions",
76 button_size)) {
77 ImGui::OpenPopup("Edit options");
78 }
79 gui::AutoRegisterLastItem("button", "Edit options");
80 HOVER_HINT("Copy and paste a PNG");
81 if (ImGui::BeginPopup("Edit options")) {
83 ImGui::EndPopup();
84 }
85 ImGui::PopStyleVar();
88 }
89
91
92 const float status_height =
93 ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y + 1;
94 ImGui::BeginChild("##PixelEditorContent", ImVec2(0, -status_height), false);
95 const ImVec2 available = ImGui::GetContentRegionAvail();
96 const float font = ImGui::GetFontSize();
97 const bool preview = show_source_preview_ && draw_source_preview_ &&
100 state_->source_context->render_preview;
101 const bool beside = preview && available.x >= 48 * font;
102 const float preview_width =
103 std::clamp(available.x * 0.30f, 14 * font, 22 * font);
104 const float preview_height = std::min(available.y * 0.38f, 18 * font);
105 const float gap = ImGui::GetStyle().ItemSpacing.x;
106 const ImVec2 canvas_size(
107 beside ? std::max(1.0f, available.x - preview_width - gap) : 0,
108 preview && !beside ? std::max(1.0f, available.y - preview_height -
109 ImGui::GetStyle().ItemSpacing.y)
110 : 0);
112 ImGui::SetNextWindowScroll(ImVec2(0, 0));
113 }
114 ImGui::BeginChild("##CanvasArea", canvas_size, false,
115 ImGuiWindowFlags_HorizontalScrollbar);
116 DrawCanvas();
117 ImGui::EndChild();
118 if (preview) {
119 if (beside)
120 ImGui::SameLine();
121 ImGui::BeginChild(
122 "##SourceUsagePreview",
123 beside ? ImVec2(preview_width, 0) : ImVec2(0, preview_height), false);
125 ImGui::EndChild();
126 }
127 ImGui::EndChild();
128
130 // Status bar
132
133 return absl::OkStatus();
134}
135
137 if (const auto* target = state_->SourceTarget()) {
138 return target->palette;
139 }
140 const auto& sheet = gfx::Arena::Get().gfx_sheets()[state_->current_sheet_id];
141 const auto* palette = platform::GetSurfacePalette(sheet.surface());
142 if (!palette) {
143 return {};
144 }
145
146 // Bitmap::palette() retains the full source bank. Manual row selection
147 // applies a slice to the surface, inserting transparent index zero. Pixel
148 // tools and PNG transfer must use those displayed three-bit indices.
149 std::vector<gfx::SnesColor> colors;
150 const int count = std::min(8, palette->ncolors);
151 colors.reserve(count);
152 for (int i = 0; i < count; ++i) {
153 const auto& color = palette->colors[i];
154 colors.emplace_back(color.r, color.g, color.b);
155 colors.back().set_transparent(i == 0);
156 }
157 return gfx::SnesPalette(colors);
158}
159
161 auto& sheet =
163 if (state_->SourceTarget() == nullptr || !sheet.is_active()) {
164 return sheet;
165 }
166 bool changed = !context_sheet_.is_active();
167 if (!context_sheet_.is_active()) {
168 context_sheet_.Create(sheet.width(), sheet.height(), 8, sheet.vector());
169 } else if (context_sheet_.vector() != sheet.vector()) {
170 context_sheet_.set_data(sheet.vector());
171 changed = true;
172 }
173 const auto& palette = DisplayPalette();
174 const auto& old = context_sheet_.palette();
175 bool palette_changed = old.size() != palette.size();
176 for (size_t i = 0; !palette_changed && i < palette.size(); ++i) {
177 palette_changed = old[i].snes() != palette[i].snes();
178 }
179 if (palette_changed) {
181 }
182 if (changed || palette_changed || !context_sheet_.texture()) {
187 }
188 return context_sheet_;
189}
190
192 if (!state_->source_context ||
194 return;
195 }
196 auto& context = *state_->source_context;
197 ImGui::BeginDisabled(!context.return_to_source);
198 const bool return_to_source = ImGui::SmallButton(tr("Return to source"));
199 gui::AutoRegisterLastItem("button", "Return to source");
200 ImGui::EndDisabled();
201 if (return_to_source) {
202 // Copy before invoking: navigation may replace editor state.
203 auto navigate = context.return_to_source;
205 navigate();
206 return;
207 }
208 if (draw_source_preview_ && context.render_preview) {
209 ImGui::SameLine();
210 ImGui::Checkbox(tr("Preview"), &show_source_preview_);
211 gui::AutoRegisterLastItem("checkbox", "Show source preview");
212 }
213 ImGui::SameLine();
214 if (ImGui::SmallButton(ICON_MD_INFO_OUTLINE "##SourceDetails")) {
215 ImGui::OpenPopup("Source details");
216 }
217 gui::AutoRegisterLastItem("button", "Source details");
218 HOVER_HINT("Source, drawing pattern and palette details");
219 if (ImGui::BeginPopup("Source details")) {
220 ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 28 * ImGui::GetFontSize());
221 ImGui::TextWrapped("%s", context.label.c_str());
222 if (!context.drawing_description.empty()) {
223 ImGui::TextWrapped(tr("Drawing: %s"),
224 context.drawing_description.c_str());
225 }
226 if (const auto* target = state_->SourceTarget();
227 target && target->source_orientation_mask != 0) {
228 ImGui::TextWrapped(tr("Pattern orientations: %s"),
229 target->SourceOrientationLabel().c_str());
230 ImGui::TextWrapped("%s",
231 tr("Paint the unflipped source; all uses of this "
232 "sheet share its pixels."));
233 }
234 if (!context.limitation.empty()) {
235 ImGui::TextWrapped("%s", context.limitation.c_str());
236 }
237 ImGui::PopTextWrapPos();
238 ImGui::Separator();
239 ImGui::BeginDisabled(state_->selection.is_floating);
240 const bool manual = ImGui::SmallButton(tr("Use manual palette"));
241 gui::AutoRegisterLastItem("button", "Use manual palette");
242 ImGui::EndDisabled();
243 if (manual)
244 ImGui::CloseCurrentPopup();
245 ImGui::EndPopup();
246 if (manual) {
248 return;
249 }
250 }
251 ImGui::SameLine();
252 const ImVec2 label_pos = ImGui::GetCursorScreenPos();
253 const ImVec2 label_size(std::max(1.0f, ImGui::GetContentRegionAvail().x),
254 ImGui::GetFrameHeight());
255 const ImVec2 text_pos(label_pos.x,
256 label_pos.y + ImGui::GetStyle().FramePadding.y);
257 const bool clipped =
258 ImGui::CalcTextSize(context.label.c_str()).x > label_size.x;
259 const float ellipsis_width = clipped ? ImGui::CalcTextSize("...").x : 0;
260 const float text_right =
261 label_pos.x + std::max(0.0f, label_size.x - ellipsis_width);
262 auto* draw = ImGui::GetWindowDrawList();
263 draw->PushClipRect(label_pos, ImVec2(text_right, label_pos.y + label_size.y),
264 true);
265 draw->AddText(text_pos, ImGui::GetColorU32(ImGuiCol_Text),
266 context.label.c_str());
267 draw->PopClipRect();
268 if (clipped && label_size.x >= ellipsis_width) {
269 draw->AddText(ImVec2(text_right, text_pos.y),
270 ImGui::GetColorU32(ImGuiCol_Text), "...");
271 }
272 ImGui::Dummy(label_size);
273 if (ImGui::IsItemHovered())
274 ImGui::SetTooltip("%s", context.label.c_str());
275
276 if (!context.palette_error.empty()) {
277 ImGui::TextWrapped(tr("Source colors could not refresh: %s"),
278 context.palette_error.c_str());
279 }
280 if (context.targets.empty())
281 return;
282 const auto& target = context.targets[context.selected_target];
283 ImGui::BeginDisabled(state_->selection.is_floating);
284 ImGui::SetNextItemWidth(-1);
285 const bool open = ImGui::BeginCombo("##SourceTile", target.label.c_str());
286 gui::AutoRegisterLastItem("combo", "Source tile");
287 if (open) {
288 for (size_t i = 0; i < context.targets.size(); ++i) {
289 ImGui::PushID(static_cast<int>(i));
290 if (ImGui::Selectable(context.targets[i].label.c_str(),
291 i == context.selected_target)) {
293 }
294 gui::AutoRegisterLastItem("selectable",
295 absl::StrFormat("Source tile %zu", i));
296 ImGui::PopID();
297 }
298 ImGui::EndCombo();
299 }
300 ImGui::EndDisabled();
301}
302
304 if (open_scratch_) {
305 if (ImGui::Button("Scratch assets"))
307 gui::AutoRegisterLastItem("button", "Scratch assets");
309 "Keep editable sheets and BIN assets before assigning them to the ROM");
310 }
311 struct Tool {
312 PixelTool tool;
313 const char* icon;
314 const char* label;
315 };
316 const Tool tools[] = {
317 {PixelTool::kSelect, ICON_MD_SELECT_ALL, "Select (M)"},
318 {PixelTool::kHand, ICON_MD_PAN_TOOL, "Hand (H)"},
319 {PixelTool::kPencil, ICON_MD_DRAW, "Pencil (B)"},
320 {PixelTool::kBrush, ICON_MD_BRUSH, "Brush (P)"},
324 {PixelTool::kRectangle, ICON_MD_CROP_SQUARE, "Rectangle (R)"},
325 {PixelTool::kEyedropper, ICON_MD_COLORIZE, "Eyedropper (I)"},
326 };
327 const float button_size = ImGui::GetFrameHeight();
328 const bool uses_brush = state_->current_tool == PixelTool::kPencil ||
331 // Nine tools, Undo/Redo, View and Edit. Only collapse on genuinely narrow
332 // detached panels; normal laptop workspaces keep the drawing tools visible.
333 const float toolbar_width =
334 13 * button_size + 12 * ImGui::GetStyle().ItemSpacing.x +
335 (uses_brush ? 60 + ImGui::GetStyle().ItemSpacing.x : 0);
336 if (ImGui::GetContentRegionAvail().x < toolbar_width) {
337 ImGui::SetNextItemWidth(125);
338 if (ImGui::BeginCombo("##PixelTool", state_->GetToolName())) {
339 for (const auto& tool : tools) {
340 if (ImGui::Selectable(tool.label, state_->current_tool == tool.tool)) {
341 state_->SetTool(tool.tool);
342 }
343 }
344 ImGui::EndCombo();
345 }
346 } else {
347 for (const auto& tool : tools) {
348 std::optional<gui::StyleColorGuard> selected;
349 if (state_->current_tool == tool.tool) {
350 selected.emplace(ImGuiCol_Button, gui::GetPrimaryVec4());
351 }
352 if (gui::CenteredIconButton(tool.icon, button_size)) {
353 state_->SetTool(tool.tool);
354 }
355 gui::AutoRegisterLastItem("button", tool.label);
356 if (ImGui::IsItemHovered())
357 ImGui::SetTooltip("%s", tool.label);
358 if (&tool != &tools[8])
359 ImGui::SameLine();
360 }
361 }
362 ImGui::SameLine();
363
364 // Brush size for pencil/brush/eraser
365 if (uses_brush) {
366 ImGui::SetNextItemWidth(60);
367 int brush = state_->brush_size;
368 if (ImGui::SliderInt("##BrushSize", &brush, 1, 8, "%d px")) {
369 state_->brush_size = static_cast<uint8_t>(brush);
370 }
371 HOVER_HINT("Brush size");
372 ImGui::SameLine();
373 }
374
375 // Undo/Redo buttons
376 ImGui::BeginDisabled(HasActiveStroke() || !undo_manager_ ||
379 const auto status = undo_ ? undo_() : undo_manager_->Undo();
380 history_error_ = status.ok() ? "" : std::string(status.message());
381 }
382 gui::AutoRegisterLastItem("button", "Undo pixels");
383 HOVER_HINT("Undo (Cmd/Ctrl+Z)");
384 ImGui::EndDisabled();
385
386 ImGui::SameLine();
387
388 ImGui::BeginDisabled(HasActiveStroke() || !undo_manager_ ||
391 const auto status = redo_ ? redo_() : undo_manager_->Redo();
392 history_error_ = status.ok() ? "" : std::string(status.message());
393 }
394 gui::AutoRegisterLastItem("button", "Redo pixels");
395 HOVER_HINT("Redo (Cmd/Ctrl+Shift+Z)");
396 ImGui::EndDisabled();
397}
398
400 ImGui::Checkbox(tr("Pixel grid (6x zoom)"), &state_->show_pixel_grid);
401 ImGui::Checkbox(tr("8x8 tile grid"), &state_->show_grid);
402 ImGui::Checkbox(tr("16x16 block grid"), &state_->show_tile_boundaries);
403 ImGui::Checkbox(tr("Cursor crosshair"), &state_->show_cursor_crosshair);
404 ImGui::Checkbox(tr("Brush preview"), &state_->show_brush_preview);
405 ImGui::Checkbox(tr("Transparency grid"), &state_->show_transparency_grid);
406 if (ImGui::CollapsingHeader(tr("Sheet overview"))) {
407 DrawMiniMap();
408 }
409}
410
412 // Show the sheet the browser has selected rather than an empty editor.
413 if (state_->open_sheets.empty() &&
416 }
417 if (state_->open_sheets.empty()) {
418 ImGui::TextDisabled(
419 tr("No sheet selected. Select a sheet from the browser."));
420 return;
421 }
422
423 // Tab bar for open sheets
424 if (gui::BeginThemedTabBar("##SheetTabs",
425 ImGuiTabBarFlags_AutoSelectNewTabs |
426 ImGuiTabBarFlags_Reorderable |
427 ImGuiTabBarFlags_TabListPopupButton)) {
428 std::vector<uint16_t> sheets_to_close;
429
430 for (uint16_t sheet_id : state_->open_sheets) {
431 bool open = true;
432 std::string tab_label = absl::StrFormat("%02X", sheet_id);
433 if (state_->modified_sheets.count(sheet_id) > 0) {
434 tab_label += "*";
435 }
436
437 const bool requested = state_->requested_sheet_tab == sheet_id;
438 if (ImGui::BeginTabItem(tab_label.c_str(), &open,
439 requested ? ImGuiTabItemFlags_SetSelected : 0)) {
440 if (state_->current_sheet_id != sheet_id)
442 state_->current_sheet_id = sheet_id;
443 if (requested) {
445 }
446
447 // Get the current sheet bitmap
448 auto& sheet = DisplaySheet();
449
450 if (!sheet.is_active()) {
451 ImGui::TextDisabled(tr("Sheet %02X is not active"), sheet_id);
452 ImGui::EndTabItem();
453 continue;
454 }
455
457 // Fit once, after the sheet tabs establish the actual canvas space.
458 // Resizing the window must not silently change an artist's zoom.
459 const ImVec2 available = ImGui::GetContentRegionAvail();
460 state_->SetZoom(std::floor(std::min(available.x / sheet.width(),
461 available.y / sheet.height())));
462 fit_canvas_requested_ = false;
463 }
464
465 // Calculate canvas size based on zoom
466 float canvas_width = sheet.width() * state_->zoom_level;
467 float canvas_height = sheet.height() * state_->zoom_level;
468
469 // Draw canvas background
470 canvas_.DrawBackground(ImVec2(canvas_width, canvas_height));
471
472 // Draw transparency checkerboard background if enabled
474 DrawTransparencyGrid(canvas_width, canvas_height);
475 }
476
477 // Draw the sheet texture
478 if (sheet.texture()) {
479 canvas_.draw_list()->AddImage(
480 (ImTextureID)(intptr_t)sheet.texture(), canvas_.zero_point(),
481 ImVec2(canvas_.zero_point().x + canvas_width,
482 canvas_.zero_point().y + canvas_height),
483 ImVec2(0, 0), ImVec2(1, 1),
484 ImGui::GetColorU32(ImVec4(1, 1, 1, 1)));
485 }
486
487 // Draw grid if enabled
488 if (state_->show_grid) {
490 }
491 DrawPixelGrids(canvas_width, canvas_height);
492
493 // Draw transient tile highlight (e.g., from "Edit Graphics" jump)
494 DrawTileHighlight(sheet);
495
496 // Draw selection rectangle if active
498 ImVec2 sel_min =
500 ImVec2 sel_max =
503 canvas_.draw_list()->AddRect(
504 sel_min, sel_max, ImGui::GetColorU32(IM_COL32(255, 255, 0, 255)),
505 0.0f, 2.0f);
506
507 // Marching ants effect (simplified)
508 canvas_.draw_list()->AddRect(
509 sel_min, sel_max, ImGui::GetColorU32(IM_COL32(0, 0, 0, 128)),
510 0.0f, 1.0f);
511 }
512
514
515 // Draw tool preview (line/rectangle)
517 ImVec2 start = PixelToScreen(static_cast<int>(tool_start_pixel_.x),
518 static_cast<int>(tool_start_pixel_.y));
519 ImVec2 end = PixelToScreen(static_cast<int>(preview_end_.x),
520 static_cast<int>(preview_end_.y));
521
523 canvas_.draw_list()->AddLine(
524 start, end, ImGui::GetColorU32(IM_COL32(255, 255, 0, 200)),
525 2.0f);
527 // Dragging up or left puts end before start; ImGui 1.93 draws
528 // nothing for an inverted rect.
529 canvas_.draw_list()->AddRect(
530 ImMin(start, end), ImMax(start, end),
531 ImGui::GetColorU32(IM_COL32(255, 255, 0, 200)), 0.0f, 2.0f);
532 }
533 }
534
535 // Draw cursor crosshair overlay if enabled and cursor in canvas
538 }
539
540 // Draw brush preview if using brush/eraser tool
545 }
546
548
549 // Handle mouse input
551
552 // Show pixel info tooltip if enabled
555 }
556
557 ImGui::EndTabItem();
558 }
559
560 if (!open) {
561 sheets_to_close.push_back(sheet_id);
562 }
563 }
564
565 // Close tabs that were requested
566 for (uint16_t sheet_id : sheets_to_close) {
567 state_->CloseSheet(sheet_id);
568 }
569
571 }
572}
573
575 float canvas_height) {
576 const float cell_size = 8.0f; // Checkerboard cell size
577 const ImU32 color1 = ImGui::GetColorU32(IM_COL32(180, 180, 180, 255));
578 const ImU32 color2 = ImGui::GetColorU32(IM_COL32(220, 220, 220, 255));
579
580 ImVec2 origin = canvas_.zero_point();
581 int cols = static_cast<int>(canvas_width / cell_size) + 1;
582 int rows = static_cast<int>(canvas_height / cell_size) + 1;
583
584 for (int row = 0; row < rows; row++) {
585 for (int col = 0; col < cols; col++) {
586 bool is_light = (row + col) % 2 == 0;
587 ImVec2 p_min(origin.x + col * cell_size, origin.y + row * cell_size);
588 ImVec2 p_max(std::min(p_min.x + cell_size, origin.x + canvas_width),
589 std::min(p_min.y + cell_size, origin.y + canvas_height));
590 canvas_.draw_list()->AddRectFilled(p_min, p_max,
591 is_light ? color1 : color2);
592 }
593 }
594}
595
597 ImVec2 cursor_screen = PixelToScreen(cursor_x_, cursor_y_);
598 float pixel_size = state_->zoom_level;
599
600 // Vertical line through cursor pixel
601 ImVec2 v_start(cursor_screen.x + pixel_size / 2, canvas_.zero_point().y);
602 ImVec2 v_end(cursor_screen.x + pixel_size / 2,
604 canvas_.draw_list()->AddLine(
605 v_start, v_end, ImGui::GetColorU32(IM_COL32(255, 100, 100, 100)), 1.0f);
606
607 // Horizontal line through cursor pixel
608 ImVec2 h_start(canvas_.zero_point().x, cursor_screen.y + pixel_size / 2);
609 ImVec2 h_end(canvas_.zero_point().x + canvas_.canvas_size().x,
610 cursor_screen.y + pixel_size / 2);
611 canvas_.draw_list()->AddLine(
612 h_start, h_end, ImGui::GetColorU32(IM_COL32(255, 100, 100, 100)), 1.0f);
613
614 // Highlight current pixel with a bright outline
615 ImVec2 pixel_min = cursor_screen;
616 ImVec2 pixel_max(cursor_screen.x + pixel_size, cursor_screen.y + pixel_size);
617 canvas_.draw_list()->AddRect(pixel_min, pixel_max,
618 ImGui::GetColorU32(IM_COL32(255, 255, 255, 200)),
619 0.0f, 2.0f);
620}
621
623 ImVec2 cursor_screen = PixelToScreen(cursor_x_, cursor_y_);
624 float pixel_size = state_->zoom_level;
625 int brush = state_->brush_size;
626 int half = brush / 2;
627
628 // Draw preview of brush area
629 ImVec2 brush_min(cursor_screen.x - half * pixel_size,
630 cursor_screen.y - half * pixel_size);
631 ImVec2 brush_max(cursor_screen.x + (brush - half) * pixel_size,
632 cursor_screen.y + (brush - half) * pixel_size);
633
634 // Fill with semi-transparent color preview
635 ImU32 preview_color = (state_->current_tool == PixelTool::kEraser)
636 ? ImGui::GetColorU32(IM_COL32(255, 0, 0, 50))
637 : ImGui::GetColorU32(IM_COL32(0, 255, 0, 50));
638 canvas_.draw_list()->AddRectFilled(brush_min, brush_max, preview_color);
639
640 // Outline
641 ImU32 outline_color = (state_->current_tool == PixelTool::kEraser)
642 ? ImGui::GetColorU32(IM_COL32(255, 100, 100, 200))
643 : ImGui::GetColorU32(IM_COL32(100, 255, 100, 200));
644 canvas_.draw_list()->AddRect(brush_min, brush_max, outline_color, 0.0f, 1.0f);
645}
646
648 if (cursor_x_ < 0 || cursor_x_ >= sheet.width() || cursor_y_ < 0 ||
649 cursor_y_ >= sheet.height()) {
650 return;
651 }
652
653 uint8_t color_index = sheet.GetPixel(cursor_x_, cursor_y_);
654 const auto& palette = DisplayPalette();
655
656 ImGui::BeginTooltip();
657 ImGui::Text(tr("Pos: %d, %d"), cursor_x_, cursor_y_);
658 ImGui::Text(tr("Tile: %d, %d"), cursor_x_ / 8, cursor_y_ / 8);
659 ImGui::Text(tr("Index: %d"), color_index);
660
661 if (color_index < palette.size()) {
662 ImGui::Text(tr("SNES: $%04X"), palette[color_index].snes());
663 ImVec4 color(palette[color_index].rgb().x / 255.0f,
664 palette[color_index].rgb().y / 255.0f,
665 palette[color_index].rgb().z / 255.0f, 1.0f);
666 ImGui::ColorButton("##ColorPreview", color, ImGuiColorEditFlags_NoTooltip,
667 ImVec2(24, 24));
668 if (color_index == 0) {
669 ImGui::SameLine();
670 ImGui::TextDisabled(tr("(Transparent)"));
671 }
672 }
673 ImGui::EndTooltip();
674}
675
678 return;
679 }
681 return;
682 }
683
684 const double now = ImGui::GetTime();
685 const double elapsed = now - state_->tile_highlight.start_time;
686 if (elapsed > state_->tile_highlight.duration) {
688 return;
689 }
690
691 const int tiles_per_row = sheet.width() / 8;
692 if (tiles_per_row <= 0) {
693 return;
694 }
695 const uint16_t tile_index = state_->tile_highlight.tile_index;
696 const int tile_x = static_cast<int>(tile_index % tiles_per_row);
697 const int tile_y = static_cast<int>(tile_index / tiles_per_row);
698
699 const float pulse =
700 0.5f + 0.5f * std::sin(static_cast<float>(elapsed) * 6.0f);
701 const float alpha = 0.25f + (pulse * 0.35f);
702
703 ImVec2 min = PixelToScreen(tile_x * 8, tile_y * 8);
704 ImVec2 max = PixelToScreen(tile_x * 8 + 8, tile_y * 8 + 8);
705
706 ImVec4 sel = gui::GetSelectedColor();
707 sel.w = alpha;
708 const ImU32 fill_color = ImGui::GetColorU32(sel);
709 ImVec4 sel_outline = gui::GetSelectedColor();
710 sel_outline.w = 0.9f;
711 const ImU32 outline_color = ImGui::GetColorU32(sel_outline);
712 canvas_.draw_list()->AddRectFilled(min, max, fill_color);
713 canvas_.draw_list()->AddRect(min, max, outline_color, 0.0f, 2.0f);
714
715 if (ImGui::IsMouseHoveringRect(min, max)) {
716 ImGui::BeginTooltip();
717 ImGui::Text(tr("Focus tile: %d (sheet %02X)"), tile_index,
719 if (!state_->tile_highlight.label.empty()) {
720 ImGui::Text("%s", state_->tile_highlight.label.c_str());
721 }
722 ImGui::EndTooltip();
723 }
724}
725
727 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,
728 ImVec2(4, ImGui::GetStyle().ItemSpacing.y));
729 const float right =
730 ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x;
731 ImGui::BeginGroup();
733 ImGui::EndGroup();
734 const float swatch_size = ImGui::GetFrameHeight();
735 const float swatch_width =
736 9 * swatch_size + 9 * ImGui::GetStyle().ItemSpacing.x;
737 if (ImGui::GetItemRectMax().x + swatch_width +
738 ImGui::GetStyle().ItemSpacing.x <=
739 right) {
740 ImGui::SameLine();
741 }
742 ImGui::BeginGroup();
743 if (state_->open_sheets.empty()) {
744 ImGui::TextDisabled(tr("No sheet"));
745 ImGui::EndGroup();
746 ImGui::PopStyleVar();
747 return;
748 }
749
750 const auto& palette = DisplayPalette();
751
752 // Selection is an index, not a cached RGB value. Resolve it again when the
753 // sheet, palette row, or source context changes. Index zero stays transparent.
754 const auto display_color = [&palette](int index) {
755 ImVec4 color = gui::ConvertSnesColorToImVec4(palette[index]);
756 color.w = index == 0 ? 0.0f : 1.0f;
757 return color;
758 };
759 constexpr auto kSwatchFlags = ImGuiColorEditFlags_NoTooltip |
760 ImGuiColorEditFlags_NoBorder |
761 ImGuiColorEditFlags_AlphaPreview;
762
763 // Sheet editing is indexed 3bpp; rendered 4bpp expansion is not authored.
764 for (int i = 0; i < static_cast<int>(palette.size()) && i < 8; i++) {
765 if (i > 0) {
766 ImGui::SameLine();
767 }
768
769 std::string id = absl::StrFormat("##Color%d", i);
770 if (ImGui::ColorButton(id.c_str(), display_color(i), kSwatchFlags,
771 ImVec2(swatch_size, swatch_size))) {
772 state_->current_color_index = static_cast<uint8_t>(i);
773 }
774 gui::AutoRegisterLastItem("color", absl::StrFormat("Palette index %d", i));
775
776 if (state_->current_color_index == i) {
777 const auto min = ImGui::GetItemRectMin();
778 const auto max = ImGui::GetItemRectMax();
779 ImGui::GetWindowDrawList()->AddRect(
780 ImVec2(min.x - 1, min.y - 1), ImVec2(max.x + 1, max.y + 1),
781 ImGui::GetColorU32(gui::GetSelectedColor()),
782 ImGui::GetStyle().FrameRounding, 1.5f);
783 }
784
785 if (ImGui::IsItemHovered()) {
786 ImGui::BeginTooltip();
787 ImGui::Text(tr("Index: %d"), i);
788 ImGui::Text(tr("SNES: $%04X"), palette[i].snes());
789 ImGui::Text(tr("RGB: %d, %d, %d"), static_cast<int>(palette[i].rgb().x),
790 static_cast<int>(palette[i].rgb().y),
791 static_cast<int>(palette[i].rgb().z));
792 if (i == 0) {
793 ImGui::Text(tr("(Transparent)"));
794 }
795 ImGui::EndTooltip();
796 }
797 }
798
799 // Keep the foreground swatch adjacent to the indexed colors.
800 ImGui::SameLine(0, ImGui::GetStyle().ItemSpacing.x * 2);
801 const int index = state_->current_color_index;
802 if (index >= 8 || index >= palette.size()) {
803 ImGui::TextDisabled("?");
804 HOVER_HINT("Selected palette index is unavailable");
805 ImGui::EndGroup();
806 ImGui::PopStyleVar();
807 return;
808 }
809 ImGui::ColorButton("##CurrentColor", display_color(index), kSwatchFlags,
810 ImVec2(swatch_size, swatch_size));
811 gui::AutoRegisterLastItem("color", "Current color");
812 if (ImGui::IsItemHovered()) {
813 ImGui::SetTooltip(tr("Current color: %d%s"), index,
814 index == 0 ? tr(" (transparent)") : "");
815 }
816 ImGui::EndGroup();
817 ImGui::PopStyleVar();
820}
821
823 const bool show_swap = ImGui::CollapsingHeader(tr("Swap color IDs"));
824 gui::AutoRegisterLastItem("header", "Swap color IDs");
825 if (!show_swap)
826 return;
827 ImGui::TextWrapped("%s",
828 tr("Swap matching pixels across the selection. "
829 "Palette colors stay fixed; ID 0 stays transparent."));
830 ImGui::Combo(tr("Swap scope"), &swap_scope_,
831 "Selected pixels\0Object source tiles\0");
832 gui::AutoRegisterLastItem("combo", "Swap scope");
833 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
834 ImGui::InputInt(tr("From ID"), &swap_from_id_, 0, 0);
835 gui::AutoRegisterLastItem("input", "Swap from color ID");
836 ImGui::SameLine();
837 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
838 ImGui::InputInt(tr("To ID"), &swap_to_id_, 0, 0);
839 gui::AutoRegisterLastItem("input", "Swap to color ID");
840 swap_from_id_ = std::clamp(swap_from_id_, 1, 7);
841 swap_to_id_ = std::clamp(swap_to_id_, 1, 7);
842 const bool has_selection = state_->selection.is_active &&
843 state_->selection.width > 0 &&
845 const bool has_source =
846 state_->source_context && !state_->source_context->targets.empty();
847 const bool ready = undo_manager_ && !state_->selection.is_floating &&
849 (swap_scope_ == 0 ? has_selection : has_source);
850 ImGui::BeginDisabled(!ready);
851 if (ImGui::Button(tr("Swap selected color IDs"))) {
852 std::vector<PixelIndexTarget> targets;
853 absl::Status status;
854 if (swap_scope_ == 0) {
855 const auto& selection = state_->selection;
856 auto selected = MakePixelIndexRectangleSelection(
857 state_->current_sheet_id, selection.x, selection.y, selection.width,
858 selection.height);
859 if (selected.ok())
860 targets = std::move(*selected);
861 else
862 status = selected.status();
863 } else {
864 for (const auto& target : state_->source_context->targets) {
865 targets.push_back({target.sheet_id, target.tile_index});
866 }
867 }
868 if (status.ok()) {
869 PixelIndexPermutation permutation{0, 1, 2, 3, 4, 5, 6, 7};
870 std::swap(permutation[swap_from_id_], permutation[swap_to_id_]);
872 permutation);
873 }
875 status.ok() ? tr("Color IDs swapped. Undo restores the pixels.")
876 : std::string(status.message());
877 }
878 gui::AutoRegisterLastItem("button", "Swap selected color IDs");
879 ImGui::EndDisabled();
880 ImGui::TextWrapped("%s", tr("These are shared sheet pixels. Other objects "
881 "using the same tiles also use the edited art."));
882 if (!index_swap_status_.empty())
883 ImGui::TextWrapped("%s", index_swap_status_.c_str());
884}
885
887 if (state_->open_sheets.empty()) {
888 ImGui::TextDisabled(tr("No sheet"));
889 return;
890 }
891
892 auto& sheet = DisplaySheet();
893 if (!sheet.texture())
894 return;
895
896 // Draw mini version of the sheet
897 float mini_scale = 0.5f;
898 float mini_width = sheet.width() * mini_scale;
899 float mini_height = sheet.height() * mini_scale;
900
901 ImVec2 pos = ImGui::GetCursorScreenPos();
902
903 ImGui::GetWindowDrawList()->AddImage(
904 (ImTextureID)(intptr_t)sheet.texture(), pos,
905 ImVec2(pos.x + mini_width, pos.y + mini_height), ImVec2(0, 0),
906 ImVec2(1, 1), ImGui::GetColorU32(ImVec4(1, 1, 1, 1)));
907
908 // Draw viewport rectangle
909 // TODO: Calculate actual viewport bounds based on scroll position
910
911 ImGui::Dummy(ImVec2(mini_width, mini_height));
912}
913
915 ImGui::Separator();
916 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,
917 ImVec2(4, ImGui::GetStyle().ItemSpacing.y));
918 const float button_size = ImGui::GetFrameHeight();
919 if (gui::CenteredIconButton(ICON_MD_ZOOM_OUT, button_size)) {
920 state_->ZoomOut();
921 }
922 gui::AutoRegisterLastItem("button", "Zoom out");
923 HOVER_HINT("Zoom out (-)");
924 ImGui::SameLine();
925 ImGui::SetNextItemWidth(ImGui::CalcTextSize("16x").x + button_size + 8);
926 if (ImGui::BeginCombo("##PixelZoom",
927 absl::StrFormat("%.0fx", state_->zoom_level).c_str())) {
928 for (int zoom = 1; zoom <= 16; ++zoom) {
929 if (ImGui::Selectable(absl::StrFormat("%dx", zoom).c_str(),
930 state_->zoom_level == zoom)) {
931 state_->SetZoom(zoom);
932 }
933 }
934 ImGui::EndCombo();
935 }
936 ImGui::SameLine();
937 if (gui::CenteredIconButton(ICON_MD_ZOOM_IN, button_size)) {
938 state_->ZoomIn();
939 }
940 gui::AutoRegisterLastItem("button", "Zoom in");
941 HOVER_HINT("Zoom in (+)");
942 ImGui::SameLine();
943 if (ImGui::Button(tr("Fit"))) {
945 }
946 gui::AutoRegisterLastItem("button", "Fit sheet");
947 HOVER_HINT("Fit this sheet at an integer zoom and reset panning");
948 ImGui::SameLine();
949
950 std::string status = absl::StrFormat(
951 "%s | %02X | %s %d", state_->GetToolName(), state_->current_sheet_id,
952 tr("Color"), state_->current_color_index);
953 if (cursor_in_canvas_) {
954 absl::StrAppendFormat(&status, " | %d,%d | %s %d", cursor_x_, cursor_y_,
955 tr("Tile"), cursor_y_ / 8 * 16 + cursor_x_ / 8);
956 }
959 absl::StrAppendFormat(&status, " | %s %d", tr("Focus"),
961 }
963 status += tr(" | Modified");
964 }
965 if (!history_error_.empty())
966 status += " | " + history_error_;
967 // Long cursor/source metadata must not push view controls off a small panel.
968 ImGui::BeginChild(
969 "##PixelStatus", ImVec2(0, button_size), false,
970 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
971 ImGui::AlignTextToFramePadding();
972 ImGui::TextUnformatted(status.c_str());
973 if (ImGui::IsWindowHovered()) {
974 ImGui::SetTooltip("%s", status.c_str());
975 }
976 ImGui::EndChild();
977 ImGui::PopStyleVar();
978}
979
981 if (!ImGui::IsItemHovered()) {
982 cursor_in_canvas_ = false;
983 return;
984 }
985
986 cursor_in_canvas_ = true;
987 ImVec2 mouse_pos = ImGui::GetMousePos();
988 ImVec2 pixel_pos = ScreenToPixel(mouse_pos);
989
990 cursor_x_ = static_cast<int>(pixel_pos.x);
991 cursor_y_ = static_cast<int>(pixel_pos.y);
992
993 auto& sheet =
995
996 // Clamp to sheet bounds
997 cursor_x_ = std::clamp(cursor_x_, 0, sheet.width() - 1);
998 cursor_y_ = std::clamp(cursor_y_, 0, sheet.height() - 1);
999
1001 return;
1002 }
1003
1004 // Mouse button handling
1005 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
1006 is_drawing_ = true;
1009 ImVec2(static_cast<float>(cursor_x_), static_cast<float>(cursor_y_));
1011
1012 // Pixel writes until mouse-up form one stroke (one undo step).
1013 BeginStroke();
1014
1015 // Handle tools that need start position
1016 switch (state_->current_tool) {
1017 case PixelTool::kPencil:
1019 break;
1020 case PixelTool::kBrush:
1022 break;
1023 case PixelTool::kEraser:
1025 break;
1026 case PixelTool::kFill:
1028 break;
1031 break;
1032 case PixelTool::kSelect:
1034 break;
1035 case PixelTool::kLine:
1037 show_tool_preview_ = true;
1038 break;
1039 default:
1040 break;
1041 }
1042 }
1043
1044 if (ImGui::IsMouseDragging(ImGuiMouseButton_Left) && is_drawing_) {
1045 preview_end_ =
1046 ImVec2(static_cast<float>(cursor_x_), static_cast<float>(cursor_y_));
1047
1048 switch (state_->current_tool) {
1049 case PixelTool::kPencil:
1051 break;
1052 case PixelTool::kBrush:
1054 break;
1055 case PixelTool::kEraser:
1057 break;
1058 case PixelTool::kSelect:
1060 break;
1061 default:
1062 break;
1063 }
1064
1066 ImVec2(static_cast<float>(cursor_x_), static_cast<float>(cursor_y_));
1067 }
1068
1069 if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) && is_drawing_)
1070 FinishGesture();
1071}
1072
1074 if (is_drawing_ && !ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
1075 // The canvas release path already handles the current endpoint. This is
1076 // the outside/hidden-panel fallback, so retain the last valid drag point.
1077 cursor_x_ = static_cast<int>(last_mouse_pixel_.x);
1078 cursor_y_ = static_cast<int>(last_mouse_pixel_.y);
1079 FinishGesture();
1080 }
1081}
1082
1084 if ((is_drawing_ || stroke_) && ActiveSheetStore(*state_) == nullptr)
1085 return;
1086 if (is_drawing_) {
1087 // Use the last point inside the canvas, never the control released over.
1088 // Explicit sheet navigation calls this before changing the selected sheet.
1089 switch (gesture_tool_) {
1090 case PixelTool::kLine:
1091 DrawLine(static_cast<int>(tool_start_pixel_.x),
1092 static_cast<int>(tool_start_pixel_.y), cursor_x_, cursor_y_);
1093 break;
1095 DrawRectangle(static_cast<int>(tool_start_pixel_.x),
1096 static_cast<int>(tool_start_pixel_.y), cursor_x_,
1097 cursor_y_, false);
1098 break;
1099 case PixelTool::kSelect:
1100 EndSelection();
1101 break;
1102 default:
1103 break;
1104 }
1105 }
1106 is_drawing_ = false;
1107 show_tool_preview_ = false;
1108 EndStroke();
1109}
1110
1112 auto* edit = StrokeEdit();
1113 if (edit == nullptr) {
1114 return;
1115 }
1116 ShowStrokeChanges(edit->Set(x, y, state_->current_color_index));
1117}
1118
1120 auto* edit = StrokeEdit();
1121 if (edit == nullptr) {
1122 return;
1123 }
1124 int size = state_->brush_size;
1125 int half = size / 2;
1126 bool changed = false;
1127 for (int dy = -half; dy < size - half; dy++) {
1128 for (int dx = -half; dx < size - half; dx++) {
1129 changed |= edit->Set(x + dx, y + dy, state_->current_color_index);
1130 }
1131 }
1132 ShowStrokeChanges(changed);
1133}
1134
1136 auto* edit = StrokeEdit();
1137 if (edit == nullptr) {
1138 return;
1139 }
1140 int size = state_->brush_size;
1141 int half = size / 2;
1142 bool changed = false;
1143 for (int dy = -half; dy < size - half; dy++) {
1144 for (int dx = -half; dx < size - half; dx++) {
1145 changed |= edit->Set(x + dx, y + dy, 0); // Index 0 = transparent
1146 }
1147 }
1148 ShowStrokeChanges(changed);
1149}
1150
1152 auto* edit = StrokeEdit();
1153 if (edit == nullptr) {
1154 return;
1155 }
1156 const int width = edit->width();
1157 const int height = edit->height();
1158 if (x < 0 || x >= width || y < 0 || y >= height)
1159 return;
1160
1161 uint8_t target_color = edit->Get(x, y);
1162 uint8_t fill_color = state_->current_color_index;
1163
1164 if (target_color == fill_color)
1165 return; // Nothing to fill
1166
1167 // BFS flood fill
1168 std::queue<std::pair<int, int>> queue;
1169 std::vector<bool> visited(width * height, false);
1170
1171 queue.push({x, y});
1172 visited[y * width + x] = true;
1173
1174 while (!queue.empty()) {
1175 auto [cx, cy] = queue.front();
1176 queue.pop();
1177
1178 edit->Set(cx, cy, fill_color);
1179
1180 // Check 4-connected neighbors
1181 const int dx[] = {0, 0, -1, 1};
1182 const int dy[] = {-1, 1, 0, 0};
1183
1184 for (int i = 0; i < 4; i++) {
1185 int nx = cx + dx[i];
1186 int ny = cy + dy[i];
1187
1188 if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
1189 int idx = ny * width + nx;
1190 if (!visited[idx] && edit->Get(nx, ny) == target_color) {
1191 visited[idx] = true;
1192 queue.push({nx, ny});
1193 }
1194 }
1195 }
1196 }
1197
1198 ShowStrokeChanges(true);
1199}
1200
1203
1204 if (x >= 0 && x < sheet.width() && y >= 0 && y < sheet.height()) {
1205 state_->current_color_index = sheet.GetPixel(x, y);
1206 }
1207}
1208
1209void PixelEditorPanel::DrawLine(int x1, int y1, int x2, int y2) {
1210 auto* edit = StrokeEdit();
1211 if (edit == nullptr) {
1212 return;
1213 }
1214
1215 // Bresenham's line algorithm
1216 int dx = std::abs(x2 - x1);
1217 int dy = std::abs(y2 - y1);
1218 int sx = x1 < x2 ? 1 : -1;
1219 int sy = y1 < y2 ? 1 : -1;
1220 int err = dx - dy;
1221 bool changed = false;
1222
1223 while (true) {
1224 changed |= edit->Set(x1, y1, state_->current_color_index);
1225
1226 if (x1 == x2 && y1 == y2)
1227 break;
1228
1229 int e2 = 2 * err;
1230 if (e2 > -dy) {
1231 err -= dy;
1232 x1 += sx;
1233 }
1234 if (e2 < dx) {
1235 err += dx;
1236 y1 += sy;
1237 }
1238 }
1239
1240 ShowStrokeChanges(changed);
1241}
1242
1243void PixelEditorPanel::DrawRectangle(int x1, int y1, int x2, int y2,
1244 bool filled) {
1245 auto* edit = StrokeEdit();
1246 if (edit == nullptr) {
1247 return;
1248 }
1249
1250 int min_x = std::min(x1, x2);
1251 int max_x = std::max(x1, x2);
1252 int min_y = std::min(y1, y2);
1253 int max_y = std::max(y1, y2);
1254 const uint8_t color = state_->current_color_index;
1255 bool changed = false;
1256
1257 if (filled) {
1258 for (int y = min_y; y <= max_y; y++) {
1259 for (int x = min_x; x <= max_x; x++) {
1260 changed |= edit->Set(x, y, color);
1261 }
1262 }
1263 } else {
1264 // Top and bottom edges
1265 for (int x = min_x; x <= max_x; x++) {
1266 changed |= edit->Set(x, min_y, color);
1267 changed |= edit->Set(x, max_y, color);
1268 }
1269 // Left and right edges
1270 for (int y = min_y; y <= max_y; y++) {
1271 changed |= edit->Set(min_x, y, color);
1272 changed |= edit->Set(max_x, y, color);
1273 }
1274 }
1275
1276 ShowStrokeChanges(changed);
1277}
1278
1280 state_->selection.x = x;
1281 state_->selection.y = y;
1282 state_->selection.width = 1;
1283 state_->selection.height = 1;
1284 state_->selection.is_active = true;
1285 state_->is_selecting = true;
1286}
1287
1289 int start_x = static_cast<int>(tool_start_pixel_.x);
1290 int start_y = static_cast<int>(tool_start_pixel_.y);
1291
1292 state_->selection.x = std::min(start_x, x);
1293 state_->selection.y = std::min(start_y, y);
1294 state_->selection.width = std::abs(x - start_x) + 1;
1295 state_->selection.height = std::abs(y - start_y) + 1;
1296}
1297
1299 state_->is_selecting = false;
1300
1301 // Copy pixel data for the selection
1302 if (state_->selection.width > 0 && state_->selection.height > 0) {
1306
1307 for (int y = 0; y < state_->selection.height; y++) {
1308 for (int x = 0; x < state_->selection.width; x++) {
1309 int src_x = state_->selection.x + x;
1310 int src_y = state_->selection.y + y;
1311 if (src_x >= 0 && src_x < sheet.width() && src_y >= 0 &&
1312 src_y < sheet.height()) {
1314 sheet.GetPixel(src_x, src_y);
1315 }
1316 }
1317 }
1318
1320 }
1321}
1322
1324 // Selection data is already in state_->selection
1325}
1326
1328 if (state_->selection.pixel_data.empty())
1329 return;
1330
1331 BeginStroke();
1332 auto* edit = StrokeEdit();
1333 if (edit == nullptr) {
1334 return;
1335 }
1336 bool changed = false;
1337 for (int dy = 0; dy < state_->selection.height; dy++) {
1338 for (int dx = 0; dx < state_->selection.width; dx++) {
1339 uint8_t pixel =
1341 changed |= edit->Set(x + dx, y + dy, pixel);
1342 }
1343 }
1344 ShowStrokeChanges(changed);
1345 EndStroke();
1346}
1347
1349 if (state_->selection.pixel_data.empty())
1350 return;
1351
1352 std::vector<uint8_t> flipped(state_->selection.pixel_data.size());
1353 for (int y = 0; y < state_->selection.height; y++) {
1354 for (int x = 0; x < state_->selection.width; x++) {
1355 int src_idx = y * state_->selection.width + x;
1356 int dst_idx =
1357 y * state_->selection.width + (state_->selection.width - 1 - x);
1358 flipped[dst_idx] = state_->selection.pixel_data[src_idx];
1359 }
1360 }
1361 state_->selection.pixel_data = std::move(flipped);
1362}
1363
1365 if (state_->selection.pixel_data.empty())
1366 return;
1367
1368 std::vector<uint8_t> flipped(state_->selection.pixel_data.size());
1369 for (int y = 0; y < state_->selection.height; y++) {
1370 for (int x = 0; x < state_->selection.width; x++) {
1371 int src_idx = y * state_->selection.width + x;
1372 int dst_idx =
1373 (state_->selection.height - 1 - y) * state_->selection.width + x;
1374 flipped[dst_idx] = state_->selection.pixel_data[src_idx];
1375 }
1376 }
1377 state_->selection.pixel_data = std::move(flipped);
1378}
1379
1381 EndStroke(); // a stroke left open ends here
1382 auto* store = ActiveSheetStore(*state_);
1383 if (store == nullptr) {
1384 return;
1385 }
1386 stroke_.emplace(*store, state_->current_sheet_id);
1387 if (!stroke_->ok()) {
1388 stroke_.reset();
1389 }
1390}
1391
1393 if (!stroke_ || stroke_->sheet() != state_->current_sheet_id) {
1394 BeginStroke();
1395 }
1396 return stroke_ ? &*stroke_ : nullptr;
1397}
1398
1400 if (!stroke_ || !changed) {
1401 return;
1402 }
1403 RefreshArenaSheet(*state_, stroke_->sheet());
1404 state_->MarkSheetModified(stroke_->sheet());
1405}
1406
1408 if (!stroke_) {
1409 return;
1410 }
1411 const uint16_t sheet = stroke_->sheet();
1412 SheetPixelDiff diff =
1413 MakeSheetPixelDiff(sheet, stroke_->before(), stroke_->pixels());
1414 stroke_.reset(); // commits: one revision for the whole stroke
1415 RefreshArenaSheet(*state_, sheet);
1416 if (diff.empty()) {
1417 return;
1418 }
1419 state_->MarkSheetModified(sheet);
1420 if (undo_manager_ != nullptr) {
1421 undo_manager_->Push(std::make_unique<GraphicsPixelEditAction>(
1422 state_, std::move(diff),
1423 absl::StrFormat("Edit pixels on sheet %02X", sheet)));
1424 }
1425}
1426
1428 const bool hand = state_->current_tool == PixelTool::kHand;
1429 const bool middle = ImGui::IsMouseDown(ImGuiMouseButton_Middle);
1430 if (!middle && !(hand && ImGui::IsMouseDown(ImGuiMouseButton_Left))) {
1431 return hand; // the Hand tool never paints
1432 }
1433 // DrawCanvas runs inside the scrolling "##CanvasArea" child.
1434 const ImVec2 delta = ImGui::GetIO().MouseDelta;
1435 ImGui::SetScrollX(ImGui::GetScrollX() - delta.x);
1436 ImGui::SetScrollY(ImGui::GetScrollY() - delta.y);
1437 return true;
1438}
1439
1440void PixelEditorPanel::DrawPixelGrids(float canvas_width, float canvas_height) {
1441 auto* draw_list = canvas_.draw_list();
1442 const ImVec2 origin = canvas_.zero_point();
1443 const float zoom = state_->zoom_level;
1444 auto lines = [&](float step, ImU32 color, float thickness) {
1445 for (float x = step; x < canvas_width; x += step) {
1446 draw_list->AddLineV(origin.x + x, origin.y, origin.y + canvas_height,
1447 color, thickness);
1448 }
1449 for (float y = step; y < canvas_height; y += step) {
1450 draw_list->AddLineH(origin.x, origin.x + canvas_width, origin.y + y,
1451 color, thickness);
1452 }
1453 };
1454 if (state_->show_pixel_grid && zoom >= 6.0f) {
1455 lines(zoom, ImGui::GetColorU32(IM_COL32(255, 255, 255, 28)), 1.0f);
1456 }
1458 lines(16.0f * zoom, ImGui::GetColorU32(gui::GetInfoColor()), 1.5f);
1459 }
1460}
1461
1463 if (state_->SourceTarget()) {
1464 ImGui::AlignTextToFramePadding();
1465 ImGui::TextDisabled("%s", tr("Source colors"));
1466 HOVER_HINT("Colors resolved by the source editor");
1467 return;
1468 }
1471}
1472
1474 SheetColors colors{};
1475 const auto& palette = DisplayPalette();
1476 for (size_t i = 0; i < colors.size(); ++i) {
1477 if (i < palette.size()) {
1478 const ImVec4 rgb = palette[i].rgb(); // 0-255 components
1479 colors[i] = {static_cast<uint8_t>(rgb.x), static_cast<uint8_t>(rgb.y),
1480 static_cast<uint8_t>(rgb.z)};
1481 } else {
1482 const auto gray = static_cast<uint8_t>(i * 255 / 7);
1483 colors[i] = {gray, gray, gray};
1484 }
1485 }
1486 return colors;
1487}
1488
1489// Cmd+C/X/V reach Copy/Cut/Paste through the editor shortcuts
1490// (GraphicsEditor::Copy and friends); only the floating paste's own keys are
1491// handled here.
1494 !ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) ||
1495 ImGui::GetIO().WantTextInput) {
1496 return;
1497 }
1498 if (ImGui::IsKeyPressed(ImGuiKey_Enter) ||
1499 ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) {
1501 return;
1502 }
1503 if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
1505 return;
1506 }
1507 const int step = ImGui::GetIO().KeyShift ? 8 : 1;
1508 auto& selection = state_->selection;
1509 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow))
1510 selection.x -= step;
1511 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow))
1512 selection.x += step;
1513 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow))
1514 selection.y -= step;
1515 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow))
1516 selection.y += step;
1518}
1519
1521 const bool image_clipboard = util::ImageClipboardSupported();
1522 ImGui::BeginDisabled(!image_clipboard);
1524 "Copy selection, or the whole sheet, as a PNG "
1525 "(Cmd+C)")) {
1527 }
1528 ImGui::SameLine();
1530 "Paste a PNG from the clipboard as a floating "
1531 "selection (Cmd+V)")) {
1533 }
1534 ImGui::EndDisabled();
1535 if (!image_clipboard) {
1536 ImGui::SameLine();
1537 ImGui::TextDisabled("%s", tr("(image clipboard: macOS only)"));
1538 }
1540 ImGui::SameLine();
1541 if (ImGui::SmallButton(ICON_MD_CHECK " Commit")) {
1543 }
1544 HOVER_HINT("Write the floating paste into the sheet (Enter)");
1545 ImGui::SameLine();
1546 if (ImGui::SmallButton(ICON_MD_CLOSE " Cancel")) {
1548 }
1549 HOVER_HINT("Drop the floating paste (Esc)");
1550 ImGui::SameLine();
1551 ImGui::Checkbox(tr("Skip color 0"), &paste_skip_color0_);
1552 HOVER_HINT("Leave pixels under color 0 of the paste unchanged");
1553 }
1554 if (!clipboard_status_.empty()) {
1555 ImGui::TextColored(
1557 "%s", clipboard_status_.c_str());
1558 }
1559}
1560
1562 const auto& sheet = gfx::Arena::Get().gfx_sheets()[state_->current_sheet_id];
1563 if (!sheet.is_active()) {
1564 clipboard_status_ = "No sheet to copy";
1566 return;
1567 }
1568 const auto& selection = state_->selection;
1569 int x0 = 0;
1570 int y0 = 0;
1571 int width = sheet.width();
1572 int height = sheet.height();
1573 if (selection.is_active && !selection.is_floating && selection.width > 0 &&
1574 selection.height > 0) {
1575 x0 = selection.x;
1576 y0 = selection.y;
1577 width = selection.width;
1578 height = selection.height;
1579 }
1580 // Read the current pixels; the selection's copy may predate later edits.
1581 std::vector<uint8_t> pixels(static_cast<size_t>(width) * height, 0);
1582 for (int y = 0; y < height; ++y) {
1583 for (int x = 0; x < width; ++x) {
1584 const int sx = x0 + x;
1585 const int sy = y0 + y;
1586 if (sx >= 0 && sx < sheet.width() && sy >= 0 && sy < sheet.height()) {
1587 pixels[y * width + x] = sheet.GetPixel(sx, sy);
1588 }
1589 }
1590 }
1591 auto png = EncodeClipboardPng(pixels, width, height, CurrentSheetColors());
1592 const absl::Status status =
1593 png.ok() ? util::SetClipboardPng(*png) : png.status();
1595 status.ok() ? absl::StrFormat("Copied %dx%d from sheet %02X", width,
1596 height, state_->current_sheet_id)
1597 : std::string(status.message());
1598 clipboard_status_is_error_ = !status.ok();
1599}
1600
1602 const auto& selection = state_->selection;
1603 if (!selection.is_active || selection.is_floating || selection.width <= 0 ||
1604 selection.height <= 0) {
1605 clipboard_status_ = "Select pixels to cut";
1607 return;
1608 }
1611 return;
1612 }
1613 BeginStroke();
1614 auto* edit = StrokeEdit();
1615 if (edit == nullptr) {
1616 clipboard_status_ = "Copied, but this sheet cannot be edited here";
1618 return;
1619 }
1620 bool changed = false;
1621 for (int y = selection.y; y < selection.y + selection.height; ++y) {
1622 for (int x = selection.x; x < selection.x + selection.width; ++x) {
1623 changed |= edit->Set(x, y, 0);
1624 }
1625 }
1626 ShowStrokeChanges(changed);
1627 EndStroke();
1629 absl::StrFormat("Cut %dx%d from sheet %02X", selection.width,
1630 selection.height, state_->current_sheet_id);
1631}
1632
1634 const auto& sheet = gfx::Arena::Get().gfx_sheets()[state_->current_sheet_id];
1635 if (!sheet.is_active()) {
1636 clipboard_status_ = "Open a sheet before pasting";
1638 return;
1639 }
1640 auto png = util::GetClipboardPng();
1641 auto paste = png.ok() ? MapClipboardPng(*png, CurrentSheetColors(),
1642 sheet.width(), sheet.height())
1643 : absl::StatusOr<ClipboardPaste>(png.status());
1644 if (!paste.ok()) {
1645 clipboard_status_ = std::string(paste.status().message());
1647 return;
1648 }
1649 auto& selection = state_->selection;
1650 // Float over the current selection's corner, or the sheet's top left.
1651 const int x = selection.is_active ? selection.x : 0;
1652 const int y = selection.is_active ? selection.y : 0;
1653 selection.pixel_data = paste->indices;
1654 selection.width = paste->width;
1655 selection.height = paste->height;
1656 selection.x = x;
1657 selection.y = y;
1658 selection.is_active = true;
1659 selection.is_floating = true;
1662 "Paste " + DescribeClipboardPaste(*paste) +
1663 (paste->above_depth > 0 ? " (colors above 7 were mapped by color)" : "") +
1664 ". Drag or use arrows, Enter to commit, Esc to cancel.";
1666}
1667
1669 const auto& sheet = gfx::Arena::Get().gfx_sheets()[state_->current_sheet_id];
1670 auto& selection = state_->selection;
1671 selection.x =
1672 std::clamp(selection.x, 0, std::max(0, sheet.width() - selection.width));
1673 selection.y = std::clamp(selection.y, 0,
1674 std::max(0, sheet.height() - selection.height));
1675}
1676
1678 auto& selection = state_->selection;
1679 if (!selection.is_floating) {
1680 return;
1681 }
1682 BeginStroke();
1683 auto* edit = StrokeEdit();
1684 if (edit == nullptr) {
1685 clipboard_status_ = "This sheet cannot be edited here";
1687 return;
1688 }
1689 bool changed = false;
1690 for (int dy = 0; dy < selection.height; ++dy) {
1691 for (int dx = 0; dx < selection.width; ++dx) {
1692 const uint8_t pixel = selection.pixel_data[dy * selection.width + dx];
1693 if (paste_skip_color0_ && pixel == 0) {
1694 continue;
1695 }
1696 changed |= edit->Set(selection.x + dx, selection.y + dy, pixel);
1697 }
1698 }
1699 ShowStrokeChanges(changed);
1700 EndStroke();
1701 selection.is_floating = false;
1702 dragging_paste_ = false;
1704 absl::StrFormat("Pasted %dx%d at %d,%d", selection.width,
1705 selection.height, selection.x, selection.y);
1707}
1708
1711 dragging_paste_ = false;
1712 clipboard_status_ = "Paste cancelled";
1714}
1715
1717 auto& selection = state_->selection;
1718 if (!selection.is_floating) {
1719 return false;
1720 }
1721 const bool inside =
1722 cursor_x_ >= selection.x && cursor_x_ < selection.x + selection.width &&
1723 cursor_y_ >= selection.y && cursor_y_ < selection.y + selection.height;
1724 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
1725 if (inside) {
1726 dragging_paste_ = true;
1727 paste_drag_dx_ = cursor_x_ - selection.x;
1728 paste_drag_dy_ = cursor_y_ - selection.y;
1729 } else {
1731 }
1732 return true;
1733 }
1734 if (dragging_paste_ && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
1735 selection.x = cursor_x_ - paste_drag_dx_;
1736 selection.y = cursor_y_ - paste_drag_dy_;
1738 }
1739 if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
1740 dragging_paste_ = false;
1741 }
1742 return true;
1743}
1744
1746 const auto& selection = state_->selection;
1747 if (!selection.is_floating) {
1748 return;
1749 }
1750 const SheetColors colors = CurrentSheetColors();
1751 auto* draw_list = canvas_.draw_list();
1752 for (int dy = 0; dy < selection.height; ++dy) {
1753 for (int dx = 0; dx < selection.width; ++dx) {
1754 const uint8_t pixel = selection.pixel_data[dy * selection.width + dx];
1755 if (paste_skip_color0_ && pixel == 0) {
1756 continue;
1757 }
1758 const auto& rgb = colors[pixel & 7];
1759 const ImVec2 p0 = PixelToScreen(selection.x + dx, selection.y + dy);
1760 const ImVec2 p1 =
1761 PixelToScreen(selection.x + dx + 1, selection.y + dy + 1);
1762 // Keep adjacent pixels hard-edged at fractional zoom, as in dungeon
1763 // sprite previews. AddRectFilled adds an AA fringe to fractional bounds.
1764 draw_list->PrimReserve(6, 4);
1765 draw_list->PrimRect(
1766 p0, p1, ImGui::GetColorU32(IM_COL32(rgb[0], rgb[1], rgb[2], 230)));
1767 }
1768 }
1769 draw_list->AddRect(PixelToScreen(selection.x, selection.y),
1770 PixelToScreen(selection.x + selection.width,
1771 selection.y + selection.height),
1772 ImGui::GetColorU32(gui::GetInfoColor()), 0.0f, 2.0f);
1773}
1774
1775ImVec2 PixelEditorPanel::ScreenToPixel(ImVec2 screen_pos) {
1776 float px = (screen_pos.x - canvas_.zero_point().x) / state_->zoom_level;
1777 float py = (screen_pos.y - canvas_.zero_point().y) / state_->zoom_level;
1778 return ImVec2(px, py);
1779}
1780
1782 return ImVec2(canvas_.zero_point().x + x * state_->zoom_level,
1783 canvas_.zero_point().y + y * state_->zoom_level);
1784}
1785
1786} // namespace editor
1787} // namespace yaze
void MarkSheetModified(uint16_t sheet_id)
Mark a sheet as modified for save tracking.
std::optional< GraphicsEditContext > source_context
std::optional< uint16_t > requested_sheet_tab
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.
const GraphicsEditTarget * SourceTarget() const
void SetTool(PixelTool tool)
Set the current editing tool.
void Draw(GraphicsEditorState &state, const Apply &apply, bool busy)
void EndSelection()
Finalize the selection.
PaletteReorderControl palette_reorder_
void DrawLine(int x1, int y1, int x2, int y2)
Draw line from start to end.
void DrawColorPicker()
Draw the color palette picker.
void ApplyEraser(int x, int y)
Apply eraser tool at position.
std::optional< zelda3::GraphicsSheetStore::Edit > stroke_
void DrawPixelInfoTooltip(const gfx::Bitmap &sheet)
Draw tooltip with pixel information.
void DrawCursorCrosshair()
Draw crosshair at cursor position.
void DrawMiniMap()
Draw the mini navigation map.
void Draw(bool *p_open) override
Draw the pixel editor UI (WindowContent interface)
void HandleCanvasInput()
Handle canvas mouse input for current tool.
ImVec2 ScreenToPixel(ImVec2 screen_pos)
Convert screen coordinates to pixel coordinates.
std::function< void(bool)> set_sheet_browser_visible_
void DrawBrushPreview()
Draw brush size preview circle.
void ApplyEyedropper(int x, int y)
Apply eyedropper tool at position.
void CopySelection()
Copy selection to clipboard.
void UpdateSelection(int x, int y)
Update selection during drag.
void FlipSelectionVertical()
Flip selection vertically.
void ApplyBrush(int x, int y)
Apply brush tool at position.
void Initialize()
Initialize the panel.
void FlipSelectionHorizontal()
Flip selection horizontally.
void DrawViewControls()
Draw zoom and view controls.
void ApplyPencil(int x, int y)
Apply pencil tool at position.
void DrawToolbar()
Draw the toolbar with tool selection.
absl::Status Update()
Legacy Update method for backward compatibility.
ImVec2 PixelToScreen(int x, int y)
Convert pixel coordinates to screen coordinates.
void BeginSelection(int x, int y)
Start a new selection.
std::function< absl::Status()> redo_
std::function< absl::Status()> undo_
void DrawStatusBar()
Draw the status bar with cursor position.
void DrawTransparencyGrid(float canvas_width, float canvas_height)
Draw checkerboard pattern for transparent pixels.
void EndStroke()
Ends the stroke: the sheet's store revision changes once and one GraphicsPixelEditAction (the changed...
void BeginStroke()
Starts a stroke on the current sheet. Pixel writes go to the session's GraphicsSheetStore until EndSt...
void DrawTileHighlight(const gfx::Bitmap &sheet)
Draw a transient highlight for a target tile.
std::function< void()> open_scratch_
void DrawPixelGrids(float canvas_width, float canvas_height)
void ShowStrokeChanges(bool changed)
After pixel writes: refresh the Arena copy and mark the sheet modified.
std::function< void()> draw_source_preview_
void ApplyFill(int x, int y)
Apply flood fill starting at position.
void PasteSelection(int x, int y)
Paste clipboard at position.
gfx::SnesPalette DisplayPalette() const
std::function< bool()> sheet_browser_visible_
void DrawRectangle(int x1, int y1, int x2, int y2, bool filled)
Draw rectangle from start to end.
void DrawCanvas()
Draw the main editing canvas.
PaletteReorderControl::Apply apply_palette_reorder_
zelda3::GraphicsSheetStore::Edit * StrokeEdit()
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.
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:39
auto mutable_gfx_sheets()
Get mutable reference to all graphics sheets.
Definition arena.h:206
GraphicsSheets & gfx_sheets()
Definition arena.h:178
static Arena & Get()
Definition arena.cc:24
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:70
const SnesPalette & palette() const
Definition bitmap.h:393
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:198
TextureHandle texture() const
Definition bitmap.h:410
const std::vector< uint8_t > & vector() const
Definition bitmap.h:411
bool is_active() const
Definition bitmap.h:414
int height() const
Definition bitmap.h:399
void set_data(const std::vector< uint8_t > &data)
Definition bitmap.cc:901
int width() const
Definition bitmap.h:398
uint8_t GetPixel(int x, int y) const
Get the palette index at the given x,y coordinates.
Definition bitmap.h:277
void SetPaletteWithTransparent(const SnesPalette &palette, size_t index, int length=7)
Set the palette with a transparent color.
Definition bitmap.cc:524
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
auto draw_list() const
Definition canvas.h:363
auto canvas_size() const
Definition canvas.h:373
auto zero_point() const
Definition canvas.h:364
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0), std::optional< ImVec2 > viewport_size=std::nullopt)
Definition canvas.cc:703
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
Definition canvas.cc:1627
The Graphics editor's path for sheet pixel writes.
#define ICON_MD_COLORIZE
Definition icons.h:441
#define ICON_MD_INFO_OUTLINE
Definition icons.h:994
#define ICON_MD_CHECK
Definition icons.h:397
#define ICON_MD_DRAW
Definition icons.h:625
#define ICON_MD_BRUSH
Definition icons.h:325
#define ICON_MD_TUNE
Definition icons.h:2022
#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_MORE_HORIZ
Definition icons.h:1241
#define ICON_MD_AUTO_FIX_HIGH
Definition icons.h:218
#define ICON_MD_CONTENT_PASTE
Definition icons.h:467
#define ICON_MD_PAN_TOOL
Definition icons.h:1371
#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_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_CLOSE
Definition icons.h:418
#define ICON_MD_UNDO
Definition icons.h:2039
PNG images on the system clipboard. SDL's clipboard is text-only.
#define HOVER_HINT(string)
Definition macro.h:24
zelda3::GraphicsSheetStore * ActiveSheetStore(const GraphicsEditorState &state)
void DrawGraphicsPreviewPaletteSelector(GraphicsEditorState &state, GraphicsPaletteSelectorLayout layout, GraphicsPaletteSelectorScope scope)
absl::StatusOr< ClipboardPaste > MapClipboardPng(const std::vector< uint8_t > &png_bytes, const SheetColors &colors, int max_width, int max_height)
absl::Status ApplyPixelIndexPermutation(GraphicsEditorState &state, UndoManager &history, const std::vector< PixelIndexTarget > &targets, const PixelIndexPermutation &permutation)
std::array< std::array< uint8_t, 3 >, 8 > SheetColors
The 8 colors a 3bpp sheet shows (index 0 is transparent in game).
void RefreshArenaSheet(GraphicsEditorState &state, uint16_t sheet)
absl::StatusOr< std::vector< uint8_t > > EncodeClipboardPng(const std::vector< uint8_t > &indices, int width, int height, const SheetColors &colors)
std::string DescribeClipboardPaste(const ClipboardPaste &paste)
"16x16: 200 kept" / "16x8: 90 exact, 38 remapped, 4 above color 7".
PixelTool
Pixel editing tool types for the graphics editor.
absl::StatusOr< std::vector< PixelIndexTarget > > MakePixelIndexRectangleSelection(uint16_t sheet_id, int x, int y, int width, int height)
std::array< uint8_t, 8 > PixelIndexPermutation
SheetPixelDiff MakeSheetPixelDiff(uint16_t sheet, absl::Span< const uint8_t > before, absl::Span< const uint8_t > after)
The pixels that differ between two 4096-byte sheets.
bool CenteredIconButton(const char *label, float button_size)
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
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 ConvertSnesColorToImVec4(const gfx::SnesColor &color)
Convert SnesColor to standard ImVec4 for display.
Definition color.cc:24
ImVec4 GetErrorColor()
Definition ui_helpers.cc:59
ImVec4 GetInfoColor()
Definition ui_helpers.cc:64
bool ToolbarIconButton(const char *icon, const char *tooltip, bool is_active)
Convenience wrapper for toolbar-sized icon buttons.
SDL_Palette * GetSurfacePalette(SDL_Surface *surface)
Get the palette attached to a surface.
Definition sdl_compat.h:398
absl::StatusOr< std::vector< uint8_t > > GetClipboardPng(const std::string &)
bool ImageClipboardSupported()
absl::Status SetClipboardPng(const std::vector< uint8_t > &, const std::string &)
Replaces the clipboard contents with one PNG image.
SDL2/SDL3 compatibility layer.
std::vector< uint8_t > pixel_data
The pixels one undo step changes in one sheet.
Automatic widget registration helpers for ImGui Test Engine integration.