1#define IMGUI_DEFINE_MATH_OPERATORS
10#include "absl/strings/str_format.h"
26#include "imgui/imgui.h"
27#include "imgui/imgui_internal.h"
48 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,
49 ImVec2(4, ImGui::GetStyle().ItemSpacing.y));
52 std::optional<gui::StyleColorGuard> selected;
55 if (ImGui::Button(tr(
"Sheets")))
58 HOVER_HINT(visible ?
"Hide Sheet Browser to expand the canvas"
59 :
"Show Sheet Browser in its previous position");
64 const float button_size = ImGui::GetFrameHeight();
66 ImGui::OpenPopup(
"View options");
69 HOVER_HINT(
"Display overlays and sheet overview");
70 if (ImGui::BeginPopup(
"View options")) {
77 ImGui::OpenPopup(
"Edit options");
81 if (ImGui::BeginPopup(
"Edit options")) {
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();
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)
112 ImGui::SetNextWindowScroll(ImVec2(0, 0));
114 ImGui::BeginChild(
"##CanvasArea", canvas_size,
false,
115 ImGuiWindowFlags_HorizontalScrollbar);
122 "##SourceUsagePreview",
123 beside ? ImVec2(preview_width, 0) : ImVec2(0, preview_height),
false);
133 return absl::OkStatus();
138 return target->palette;
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);
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();
179 if (palette_changed) {
197 ImGui::BeginDisabled(!context.return_to_source);
198 const bool return_to_source = ImGui::SmallButton(tr(
"Return to source"));
200 ImGui::EndDisabled();
201 if (return_to_source) {
203 auto navigate = context.return_to_source;
215 ImGui::OpenPopup(
"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());
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."));
234 if (!context.limitation.empty()) {
235 ImGui::TextWrapped(
"%s", context.limitation.c_str());
237 ImGui::PopTextWrapPos();
240 const bool manual = ImGui::SmallButton(tr(
"Use manual palette"));
242 ImGui::EndDisabled();
244 ImGui::CloseCurrentPopup();
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);
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),
265 draw->AddText(text_pos, ImGui::GetColorU32(ImGuiCol_Text),
266 context.label.c_str());
268 if (clipped && label_size.x >= ellipsis_width) {
269 draw->AddText(ImVec2(text_right, text_pos.y),
270 ImGui::GetColorU32(ImGuiCol_Text),
"...");
272 ImGui::Dummy(label_size);
273 if (ImGui::IsItemHovered())
274 ImGui::SetTooltip(
"%s", context.label.c_str());
276 if (!context.palette_error.empty()) {
277 ImGui::TextWrapped(tr(
"Source colors could not refresh: %s"),
278 context.palette_error.c_str());
280 if (context.targets.empty())
282 const auto& target = context.targets[context.selected_target];
284 ImGui::SetNextItemWidth(-1);
285 const bool open = ImGui::BeginCombo(
"##SourceTile", target.label.c_str());
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)) {
295 absl::StrFormat(
"Source tile %zu", i));
300 ImGui::EndDisabled();
305 if (ImGui::Button(
"Scratch assets"))
309 "Keep editable sheets and BIN assets before assigning them to the ROM");
316 const Tool tools[] = {
327 const float button_size = ImGui::GetFrameHeight();
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);
339 for (
const auto& tool : tools) {
347 for (
const auto& tool : tools) {
348 std::optional<gui::StyleColorGuard> selected;
356 if (ImGui::IsItemHovered())
357 ImGui::SetTooltip(
"%s", tool.label);
358 if (&tool != &tools[8])
366 ImGui::SetNextItemWidth(60);
368 if (ImGui::SliderInt(
"##BrushSize", &brush, 1, 8,
"%d px")) {
380 history_error_ = status.ok() ?
"" : std::string(status.message());
384 ImGui::EndDisabled();
392 history_error_ = status.ok() ?
"" : std::string(status.message());
396 ImGui::EndDisabled();
406 if (ImGui::CollapsingHeader(tr(
"Sheet overview"))) {
419 tr(
"No sheet selected. Select a sheet from the browser."));
425 ImGuiTabBarFlags_AutoSelectNewTabs |
426 ImGuiTabBarFlags_Reorderable |
427 ImGuiTabBarFlags_TabListPopupButton)) {
428 std::vector<uint16_t> sheets_to_close;
432 std::string tab_label = absl::StrFormat(
"%02X", sheet_id);
438 if (ImGui::BeginTabItem(tab_label.c_str(), &open,
439 requested ? ImGuiTabItemFlags_SetSelected : 0)) {
450 if (!sheet.is_active()) {
451 ImGui::TextDisabled(tr(
"Sheet %02X is not active"), sheet_id);
459 const ImVec2 available = ImGui::GetContentRegionAvail();
460 state_->
SetZoom(std::floor(std::min(available.x / sheet.width(),
461 available.y / sheet.height())));
478 if (sheet.texture()) {
483 ImVec2(0, 0), ImVec2(1, 1),
484 ImGui::GetColorU32(ImVec4(1, 1, 1, 1)));
504 sel_min, sel_max, ImGui::GetColorU32(IM_COL32(255, 255, 0, 255)),
509 sel_min, sel_max, ImGui::GetColorU32(IM_COL32(0, 0, 0, 128)),
524 start, end, ImGui::GetColorU32(IM_COL32(255, 255, 0, 200)),
530 ImMin(start, end), ImMax(start, end),
531 ImGui::GetColorU32(IM_COL32(255, 255, 0, 200)), 0.0f, 2.0f);
561 sheets_to_close.push_back(sheet_id);
566 for (uint16_t sheet_id : sheets_to_close) {
575 float canvas_height) {
576 const float cell_size = 8.0f;
577 const ImU32 color1 = ImGui::GetColorU32(IM_COL32(180, 180, 180, 255));
578 const ImU32 color2 = ImGui::GetColorU32(IM_COL32(220, 220, 220, 255));
581 int cols =
static_cast<int>(canvas_width / cell_size) + 1;
582 int rows =
static_cast<int>(canvas_height / cell_size) + 1;
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));
591 is_light ? color1 : color2);
602 ImVec2 v_end(cursor_screen.x + pixel_size / 2,
605 v_start, v_end, ImGui::GetColorU32(IM_COL32(255, 100, 100, 100)), 1.0f);
610 cursor_screen.y + pixel_size / 2);
612 h_start, h_end, ImGui::GetColorU32(IM_COL32(255, 100, 100, 100)), 1.0f);
615 ImVec2 pixel_min = cursor_screen;
616 ImVec2 pixel_max(cursor_screen.x + pixel_size, cursor_screen.y + pixel_size);
618 ImGui::GetColorU32(IM_COL32(255, 255, 255, 200)),
626 int half = brush / 2;
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);
636 ? ImGui::GetColorU32(IM_COL32(255, 0, 0, 50))
637 : ImGui::GetColorU32(IM_COL32(0, 255, 0, 50));
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);
648 if (cursor_x_ < 0 || cursor_x_ >= sheet.
width() ||
cursor_y_ < 0 ||
656 ImGui::BeginTooltip();
659 ImGui::Text(tr(
"Index: %d"), color_index);
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,
668 if (color_index == 0) {
670 ImGui::TextDisabled(tr(
"(Transparent)"));
684 const double now = ImGui::GetTime();
691 const int tiles_per_row = sheet.
width() / 8;
692 if (tiles_per_row <= 0) {
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);
700 0.5f + 0.5f * std::sin(
static_cast<float>(elapsed) * 6.0f);
701 const float alpha = 0.25f + (pulse * 0.35f);
708 const ImU32 fill_color = ImGui::GetColorU32(sel);
710 sel_outline.w = 0.9f;
711 const ImU32 outline_color = ImGui::GetColorU32(sel_outline);
715 if (ImGui::IsMouseHoveringRect(min, max)) {
716 ImGui::BeginTooltip();
717 ImGui::Text(tr(
"Focus tile: %d (sheet %02X)"), tile_index,
727 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,
728 ImVec2(4, ImGui::GetStyle().ItemSpacing.y));
730 ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x;
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 <=
744 ImGui::TextDisabled(tr(
"No sheet"));
746 ImGui::PopStyleVar();
754 const auto display_color = [&palette](
int index) {
756 color.w = index == 0 ? 0.0f : 1.0f;
759 constexpr auto kSwatchFlags = ImGuiColorEditFlags_NoTooltip |
760 ImGuiColorEditFlags_NoBorder |
761 ImGuiColorEditFlags_AlphaPreview;
764 for (
int i = 0; i < static_cast<int>(palette.size()) && i < 8; i++) {
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))) {
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),
782 ImGui::GetStyle().FrameRounding, 1.5f);
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));
793 ImGui::Text(tr(
"(Transparent)"));
800 ImGui::SameLine(0, ImGui::GetStyle().ItemSpacing.x * 2);
802 if (index >= 8 || index >= palette.size()) {
803 ImGui::TextDisabled(
"?");
804 HOVER_HINT(
"Selected palette index is unavailable");
806 ImGui::PopStyleVar();
809 ImGui::ColorButton(
"##CurrentColor", display_color(index), kSwatchFlags,
810 ImVec2(swatch_size, swatch_size));
812 if (ImGui::IsItemHovered()) {
813 ImGui::SetTooltip(tr(
"Current color: %d%s"), index,
814 index == 0 ? tr(
" (transparent)") :
"");
817 ImGui::PopStyleVar();
823 const bool show_swap = ImGui::CollapsingHeader(tr(
"Swap color IDs"));
827 ImGui::TextWrapped(
"%s",
828 tr(
"Swap matching pixels across the selection. "
829 "Palette colors stay fixed; ID 0 stays transparent."));
831 "Selected pixels\0Object source tiles\0");
833 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
837 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
845 const bool has_source =
850 ImGui::BeginDisabled(!ready);
851 if (ImGui::Button(tr(
"Swap selected color IDs"))) {
852 std::vector<PixelIndexTarget> targets;
860 targets = std::move(*selected);
862 status = selected.status();
865 targets.push_back({target.sheet_id, target.tile_index});
875 status.ok() ? tr(
"Color IDs swapped. Undo restores the pixels.")
876 : std::string(status.message());
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."));
888 ImGui::TextDisabled(tr(
"No sheet"));
893 if (!sheet.texture())
897 float mini_scale = 0.5f;
898 float mini_width = sheet.width() * mini_scale;
899 float mini_height = sheet.height() * mini_scale;
901 ImVec2 pos = ImGui::GetCursorScreenPos();
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)));
911 ImGui::Dummy(ImVec2(mini_width, mini_height));
916 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,
917 ImVec2(4, ImGui::GetStyle().ItemSpacing.y));
918 const float button_size = ImGui::GetFrameHeight();
925 ImGui::SetNextItemWidth(ImGui::CalcTextSize(
"16x").x + button_size + 8);
926 if (ImGui::BeginCombo(
"##PixelZoom",
928 for (
int zoom = 1; zoom <= 16; ++zoom) {
929 if (ImGui::Selectable(absl::StrFormat(
"%dx", zoom).c_str(),
943 if (ImGui::Button(tr(
"Fit"))) {
947 HOVER_HINT(
"Fit this sheet at an integer zoom and reset panning");
950 std::string status = absl::StrFormat(
959 absl::StrAppendFormat(&status,
" | %s %d", tr(
"Focus"),
963 status += tr(
" | Modified");
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());
977 ImGui::PopStyleVar();
981 if (!ImGui::IsItemHovered()) {
987 ImVec2 mouse_pos = ImGui::GetMousePos();
990 cursor_x_ =
static_cast<int>(pixel_pos.x);
991 cursor_y_ =
static_cast<int>(pixel_pos.y);
1005 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
1044 if (ImGui::IsMouseDragging(ImGuiMouseButton_Left) &&
is_drawing_) {
1069 if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) &&
is_drawing_)
1074 if (
is_drawing_ && !ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
1113 if (edit ==
nullptr) {
1121 if (edit ==
nullptr) {
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++) {
1137 if (edit ==
nullptr) {
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);
1153 if (edit ==
nullptr) {
1156 const int width = edit->width();
1157 const int height = edit->height();
1158 if (x < 0 || x >= width || y < 0 || y >= height)
1161 uint8_t target_color = edit->Get(x, y);
1164 if (target_color == fill_color)
1168 std::queue<std::pair<int, int>> queue;
1169 std::vector<bool> visited(width * height,
false);
1172 visited[y * width + x] =
true;
1174 while (!queue.empty()) {
1175 auto [cx, cy] = queue.front();
1178 edit->Set(cx, cy, fill_color);
1181 const int dx[] = {0, 0, -1, 1};
1182 const int dy[] = {-1, 1, 0, 0};
1184 for (
int i = 0; i < 4; i++) {
1185 int nx = cx + dx[i];
1186 int ny = cy + dy[i];
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});
1204 if (x >= 0 && x < sheet.width() && y >= 0 && y < sheet.height()) {
1211 if (edit ==
nullptr) {
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;
1221 bool changed =
false;
1226 if (x1 == x2 && y1 == y2)
1246 if (edit ==
nullptr) {
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);
1255 bool changed =
false;
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);
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);
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);
1311 if (src_x >= 0 && src_x < sheet.width() && src_y >= 0 &&
1312 src_y < sheet.height()) {
1314 sheet.GetPixel(src_x, src_y);
1333 if (edit ==
nullptr) {
1336 bool changed =
false;
1341 changed |= edit->Set(x + dx, y + dy, pixel);
1383 if (store ==
nullptr) {
1411 const uint16_t sheet =
stroke_->sheet();
1423 absl::StrFormat(
"Edit pixels on sheet %02X", sheet)));
1429 const bool middle = ImGui::IsMouseDown(ImGuiMouseButton_Middle);
1430 if (!middle && !(hand && ImGui::IsMouseDown(ImGuiMouseButton_Left))) {
1434 const ImVec2 delta = ImGui::GetIO().MouseDelta;
1435 ImGui::SetScrollX(ImGui::GetScrollX() - delta.x);
1436 ImGui::SetScrollY(ImGui::GetScrollY() - delta.y);
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,
1449 for (
float y = step; y < canvas_height; y += step) {
1450 draw_list->AddLineH(origin.x, origin.x + canvas_width, origin.y + y,
1455 lines(zoom, ImGui::GetColorU32(IM_COL32(255, 255, 255, 28)), 1.0f);
1464 ImGui::AlignTextToFramePadding();
1465 ImGui::TextDisabled(
"%s", tr(
"Source colors"));
1466 HOVER_HINT(
"Colors resolved by the source editor");
1476 for (
size_t i = 0; i < colors.size(); ++i) {
1477 if (i < palette.size()) {
1478 const ImVec4 rgb = palette[i].rgb();
1479 colors[i] = {
static_cast<uint8_t
>(rgb.x),
static_cast<uint8_t
>(rgb.y),
1480 static_cast<uint8_t
>(rgb.z)};
1482 const auto gray =
static_cast<uint8_t
>(i * 255 / 7);
1483 colors[i] = {gray, gray, gray};
1494 !ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) ||
1495 ImGui::GetIO().WantTextInput) {
1498 if (ImGui::IsKeyPressed(ImGuiKey_Enter) ||
1499 ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) {
1503 if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
1507 const int step = ImGui::GetIO().KeyShift ? 8 : 1;
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;
1522 ImGui::BeginDisabled(!image_clipboard);
1524 "Copy selection, or the whole sheet, as a PNG "
1530 "Paste a PNG from the clipboard as a floating "
1531 "selection (Cmd+V)")) {
1534 ImGui::EndDisabled();
1535 if (!image_clipboard) {
1537 ImGui::TextDisabled(
"%s", tr(
"(image clipboard: macOS only)"));
1544 HOVER_HINT(
"Write the floating paste into the sheet (Enter)");
1552 HOVER_HINT(
"Leave pixels under color 0 of the paste unchanged");
1563 if (!sheet.is_active()) {
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) {
1577 width = selection.width;
1578 height = selection.height;
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);
1592 const absl::Status status =
1595 status.ok() ? absl::StrFormat(
"Copied %dx%d from sheet %02X", width,
1597 : std::string(status.message());
1603 if (!selection.is_active || selection.is_floating || selection.width <= 0 ||
1604 selection.height <= 0) {
1615 if (edit ==
nullptr) {
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);
1629 absl::StrFormat(
"Cut %dx%d from sheet %02X", selection.width,
1635 if (!sheet.is_active()) {
1642 sheet.width(), sheet.height())
1643 : absl::StatusOr<ClipboardPaste>(png.status());
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;
1658 selection.is_active =
true;
1659 selection.is_floating =
true;
1663 (paste->above_depth > 0 ?
" (colors above 7 were mapped by color)" :
"") +
1664 ". Drag or use arrows, Enter to commit, Esc to cancel.";
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));
1679 if (!selection.is_floating) {
1684 if (edit ==
nullptr) {
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];
1696 changed |= edit->Set(selection.x + dx, selection.y + dy, pixel);
1701 selection.is_floating =
false;
1704 absl::StrFormat(
"Pasted %dx%d at %d,%d", selection.width,
1705 selection.height, selection.x, selection.y);
1718 if (!selection.is_floating) {
1724 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
1739 if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
1747 if (!selection.is_floating) {
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];
1758 const auto& rgb = colors[pixel & 7];
1759 const ImVec2 p0 =
PixelToScreen(selection.x + dx, selection.y + dy);
1764 draw_list->PrimReserve(6, 4);
1765 draw_list->PrimRect(
1766 p0, p1, ImGui::GetColorU32(IM_COL32(rgb[0], rgb[1], rgb[2], 230)));
1771 selection.y + selection.height),
1778 return ImVec2(px, py);
bool show_tile_boundaries
void MarkSheetModified(uint16_t sheet_id)
Mark a sheet as modified for save tracking.
uint8_t current_color_index
std::optional< GraphicsEditContext > source_context
std::optional< uint16_t > requested_sheet_tab
bool show_pixel_info_tooltip
bool show_transparency_grid
void SetZoom(float zoom)
Set zoom level with clamping.
std::set< uint16_t > modified_sheets
bool show_cursor_crosshair
uint16_t current_sheet_id
std::set< uint16_t > open_sheets
const char * GetToolName() const
Get tool name for status display.
zelda3::GameData * game_data
void CloseSheet(uint16_t sheet_id)
Close a sheet tab.
void SelectSourceTarget(size_t index)
void ClearSourceContext()
TileHighlight tile_highlight
const GraphicsEditTarget * SourceTarget() const
void SetTool(PixelTool tool)
Set the current editing tool.
void Draw(GraphicsEditorState &state, const Apply &apply, bool busy)
bool HasActiveStroke() const
UndoManager * undo_manager_
bool show_source_preview_
void EndSelection()
Finalize the selection.
PaletteReorderControl palette_reorder_
gfx::Bitmap & DisplaySheet()
void DrawIndexSwapControls()
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 ClampFloatingPaste()
bool HandleFloatingPasteInput()
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.
void HandleFloatingPasteKeys()
bool fit_canvas_requested_
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 PasteFromSystemClipboard()
void DrawToolbar()
Draw the toolbar with tool selection.
absl::Status Update()
Legacy Update method for backward compatibility.
void CopyToSystemClipboard()
ImVec2 PixelToScreen(int x, int y)
Convert pixel coordinates to screen coordinates.
std::string index_swap_status_
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.
bool clipboard_status_is_error_
void FinishReleasedGesture()
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.
void DrawClipboardControls()
std::string history_error_
std::function< void()> open_scratch_
std::string clipboard_status_
void DrawPixelGrids(float canvas_width, float canvas_height)
void ShowStrokeChanges(bool changed)
After pixel writes: refresh the Arena copy and mark the sheet modified.
GraphicsEditorState * state_
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
void CommitFloatingPaste()
std::function< bool()> sheet_browser_visible_
void CancelFloatingPaste()
gfx::Bitmap context_sheet_
void DrawRectangle(int x1, int y1, int x2, int y2, bool filled)
Draw rectangle from start to end.
void CutToSystemClipboard()
SheetColors CurrentSheetColors() const
void DrawCanvas()
Draw the main editing canvas.
PaletteReorderControl::Apply apply_palette_reorder_
void DrawPaletteRowPicker()
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)
auto mutable_gfx_sheets()
Get mutable reference to all graphics sheets.
GraphicsSheets & gfx_sheets()
Represents a bitmap image optimized for SNES ROM hacking.
const SnesPalette & palette() const
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
TextureHandle texture() const
const std::vector< uint8_t > & vector() const
void set_data(const std::vector< uint8_t > &data)
uint8_t GetPixel(int x, int y) const
Get the palette index at the given x,y coordinates.
void SetPaletteWithTransparent(const SnesPalette &palette, size_t index, int length=7)
Set the palette with a transparent color.
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
void DrawBackground(ImVec2 canvas_size=ImVec2(0, 0), std::optional< ImVec2 > viewport_size=std::nullopt)
void DrawGrid(float grid_step=64.0f, int tile_id_offset=8)
The Graphics editor's path for sheet pixel writes.
#define ICON_MD_INFO_OUTLINE
#define ICON_MD_FORMAT_COLOR_FILL
#define ICON_MD_MORE_HORIZ
#define ICON_MD_AUTO_FIX_HIGH
#define ICON_MD_CONTENT_PASTE
#define ICON_MD_CROP_SQUARE
#define ICON_MD_HORIZONTAL_RULE
#define ICON_MD_SELECT_ALL
#define ICON_MD_CONTENT_COPY
PNG images on the system clipboard. SDL's clipboard is text-only.
#define HOVER_HINT(string)
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()
ImVec4 ConvertSnesColorToImVec4(const gfx::SnesColor &color)
Convert SnesColor to standard ImVec4 for display.
bool ToolbarIconButton(const char *icon, const char *tooltip, bool is_active)
Convenience wrapper for toolbar-sized icon buttons.
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.