1#define IMGUI_DEFINE_MATH_OPERATORS
10#include "absl/strings/str_format.h"
24#include "imgui/imgui.h"
25#include "imgui/imgui_internal.h"
36 const bool pressed = [&] {
38 return ImGui::Button(label, ImVec2(button_size, button_size));
40 if (!ImGui::IsItemVisible())
43 unsigned int codepoint = 0;
44 ImTextCharFromUtf8(&codepoint, label,
nullptr);
45 auto* baked = ImGui::GetFontBaked();
46 const auto* glyph = baked->FindGlyph(
static_cast<ImWchar
>(codepoint));
47 if (!glyph || !glyph->Visible)
50 const float font_scale = ImGui::GetFontSize() / baked->Size;
51 const float width = (glyph->X1 - glyph->X0) * font_scale;
52 const float height = (glyph->Y1 - glyph->Y0) * font_scale;
53 if (width <= 0 || height <= 0)
55 const ImVec2 min = ImGui::GetItemRectMin();
56 const ImVec2 max = ImGui::GetItemRectMax();
58 const float inset = ImGui::GetFontSize() / 8.0f;
59 const float available = std::max(1.0f, button_size - 2 * inset);
60 const float fit = std::min({1.0f, available / width, available / height});
61 const ImVec2 glyph_size(width * fit, height * fit);
62 const ImVec2 glyph_min((min.x + max.x - glyph_size.x) * 0.5f,
63 (min.y + max.y - glyph_size.y) * 0.5f);
64 ImGui::GetWindowDrawList()->AddImage(
65 ImGui::GetFont()->OwnerAtlas->TexRef, glyph_min,
66 ImVec2(glyph_min.x + glyph_size.x, glyph_min.y + glyph_size.y),
67 ImVec2(glyph->U0, glyph->V0), ImVec2(glyph->U1, glyph->V1),
68 ImGui::GetColorU32(ImGuiCol_Text));
89 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,
90 ImVec2(4, ImGui::GetStyle().ItemSpacing.y));
93 std::optional<gui::StyleColorGuard> selected;
96 if (ImGui::Button(tr(
"Sheets")))
99 HOVER_HINT(visible ?
"Hide Sheet Browser to expand the canvas"
100 :
"Show Sheet Browser in its previous position");
105 const float button_size = ImGui::GetFrameHeight();
106 if (PixelIconButton(
ICON_MD_TUNE "##ViewOptions", button_size)) {
107 ImGui::OpenPopup(
"View options");
110 HOVER_HINT(
"Display overlays and sheet overview");
111 if (ImGui::BeginPopup(
"View options")) {
117 ImGui::OpenPopup(
"Edit options");
121 if (ImGui::BeginPopup(
"Edit options")) {
125 ImGui::PopStyleVar();
132 const float status_height =
133 ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y + 1;
134 ImGui::BeginChild(
"##PixelEditorContent", ImVec2(0, -status_height),
false);
135 const ImVec2 available = ImGui::GetContentRegionAvail();
136 const float font = ImGui::GetFontSize();
141 const bool beside = preview && available.x >= 48 * font;
142 const float preview_width =
143 std::clamp(available.x * 0.30f, 14 * font, 22 * font);
144 const float preview_height = std::min(available.y * 0.38f, 18 * font);
145 const float gap = ImGui::GetStyle().ItemSpacing.x;
146 const ImVec2 canvas_size(
147 beside ? std::max(1.0f, available.x - preview_width - gap) : 0,
148 preview && !beside ? std::max(1.0f, available.y - preview_height -
149 ImGui::GetStyle().ItemSpacing.y)
152 ImGui::SetNextWindowScroll(ImVec2(0, 0));
154 ImGui::BeginChild(
"##CanvasArea", canvas_size,
false,
155 ImGuiWindowFlags_HorizontalScrollbar);
162 "##SourceUsagePreview",
163 beside ? ImVec2(preview_width, 0) : ImVec2(0, preview_height),
false);
173 return absl::OkStatus();
178 return target->palette;
189 std::vector<gfx::SnesColor> colors;
190 const int count = std::min(8, palette->ncolors);
191 colors.reserve(count);
192 for (
int i = 0; i < count; ++i) {
193 const auto& color = palette->colors[i];
194 colors.emplace_back(color.r, color.g, color.b);
195 colors.back().set_transparent(i == 0);
215 bool palette_changed = old.
size() != palette.size();
216 for (
size_t i = 0; !palette_changed && i < palette.size(); ++i) {
217 palette_changed = old[i].snes() != palette[i].snes();
219 if (palette_changed) {
237 ImGui::BeginDisabled(!context.return_to_source);
238 const bool return_to_source = ImGui::SmallButton(tr(
"Return to source"));
240 ImGui::EndDisabled();
241 if (return_to_source) {
243 auto navigate = context.return_to_source;
255 ImGui::OpenPopup(
"Source details");
258 HOVER_HINT(
"Source, drawing pattern and palette details");
259 if (ImGui::BeginPopup(
"Source details")) {
260 ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 28 * ImGui::GetFontSize());
261 ImGui::TextWrapped(
"%s", context.label.c_str());
262 if (!context.drawing_description.empty()) {
263 ImGui::TextWrapped(tr(
"Drawing: %s"),
264 context.drawing_description.c_str());
267 target && target->source_orientation_mask != 0) {
268 ImGui::TextWrapped(tr(
"Pattern orientations: %s"),
269 target->SourceOrientationLabel().c_str());
270 ImGui::TextWrapped(
"%s",
271 tr(
"Paint the unflipped source; all uses of this "
272 "sheet share its pixels."));
274 if (!context.limitation.empty()) {
275 ImGui::TextWrapped(
"%s", context.limitation.c_str());
277 ImGui::PopTextWrapPos();
280 const bool manual = ImGui::SmallButton(tr(
"Use manual palette"));
282 ImGui::EndDisabled();
284 ImGui::CloseCurrentPopup();
292 const ImVec2 label_pos = ImGui::GetCursorScreenPos();
293 const ImVec2 label_size(std::max(1.0f, ImGui::GetContentRegionAvail().x),
294 ImGui::GetFrameHeight());
295 const ImVec2 text_pos(label_pos.x,
296 label_pos.y + ImGui::GetStyle().FramePadding.y);
298 ImGui::CalcTextSize(context.label.c_str()).x > label_size.x;
299 const float ellipsis_width = clipped ? ImGui::CalcTextSize(
"...").x : 0;
300 const float text_right =
301 label_pos.x + std::max(0.0f, label_size.x - ellipsis_width);
302 auto* draw = ImGui::GetWindowDrawList();
303 draw->PushClipRect(label_pos, ImVec2(text_right, label_pos.y + label_size.y),
305 draw->AddText(text_pos, ImGui::GetColorU32(ImGuiCol_Text),
306 context.label.c_str());
308 if (clipped && label_size.x >= ellipsis_width) {
309 draw->AddText(ImVec2(text_right, text_pos.y),
310 ImGui::GetColorU32(ImGuiCol_Text),
"...");
312 ImGui::Dummy(label_size);
313 if (ImGui::IsItemHovered())
314 ImGui::SetTooltip(
"%s", context.label.c_str());
316 if (!context.palette_error.empty()) {
317 ImGui::TextWrapped(tr(
"Source colors could not refresh: %s"),
318 context.palette_error.c_str());
320 if (context.targets.empty())
322 const auto& target = context.targets[context.selected_target];
324 ImGui::SetNextItemWidth(-1);
325 const bool open = ImGui::BeginCombo(
"##SourceTile", target.label.c_str());
328 for (
size_t i = 0; i < context.targets.size(); ++i) {
329 ImGui::PushID(
static_cast<int>(i));
330 if (ImGui::Selectable(context.targets[i].label.c_str(),
331 i == context.selected_target)) {
335 absl::StrFormat(
"Source tile %zu", i));
340 ImGui::EndDisabled();
349 const Tool tools[] = {
360 const float button_size = ImGui::GetFrameHeight();
366 const float toolbar_width =
367 13 * button_size + 12 * ImGui::GetStyle().ItemSpacing.x +
368 (uses_brush ? 60 + ImGui::GetStyle().ItemSpacing.x : 0);
369 if (ImGui::GetContentRegionAvail().x < toolbar_width) {
370 ImGui::SetNextItemWidth(125);
372 for (
const auto& tool : tools) {
380 for (
const auto& tool : tools) {
381 std::optional<gui::StyleColorGuard> selected;
385 if (PixelIconButton(tool.icon, button_size)) {
389 if (ImGui::IsItemHovered())
390 ImGui::SetTooltip(
"%s", tool.label);
391 if (&tool != &tools[8])
399 ImGui::SetNextItemWidth(60);
401 if (ImGui::SliderInt(
"##BrushSize", &brush, 1, 8,
"%d px")) {
413 history_error_ = status.ok() ?
"" : std::string(status.message());
417 ImGui::EndDisabled();
425 history_error_ = status.ok() ?
"" : std::string(status.message());
429 ImGui::EndDisabled();
439 if (ImGui::CollapsingHeader(tr(
"Sheet overview"))) {
452 tr(
"No sheet selected. Select a sheet from the browser."));
458 ImGuiTabBarFlags_AutoSelectNewTabs |
459 ImGuiTabBarFlags_Reorderable |
460 ImGuiTabBarFlags_TabListPopupButton)) {
461 std::vector<uint16_t> sheets_to_close;
465 std::string tab_label = absl::StrFormat(
"%02X", sheet_id);
471 if (ImGui::BeginTabItem(tab_label.c_str(), &open,
472 requested ? ImGuiTabItemFlags_SetSelected : 0)) {
483 if (!sheet.is_active()) {
484 ImGui::TextDisabled(tr(
"Sheet %02X is not active"), sheet_id);
492 const ImVec2 available = ImGui::GetContentRegionAvail();
493 state_->
SetZoom(std::floor(std::min(available.x / sheet.width(),
494 available.y / sheet.height())));
511 if (sheet.texture()) {
516 ImVec2(0, 0), ImVec2(1, 1),
517 ImGui::GetColorU32(ImVec4(1, 1, 1, 1)));
537 sel_min, sel_max, ImGui::GetColorU32(IM_COL32(255, 255, 0, 255)),
542 sel_min, sel_max, ImGui::GetColorU32(IM_COL32(0, 0, 0, 128)),
557 start, end, ImGui::GetColorU32(IM_COL32(255, 255, 0, 200)),
561 start, end, ImGui::GetColorU32(IM_COL32(255, 255, 0, 200)),
592 sheets_to_close.push_back(sheet_id);
597 for (uint16_t sheet_id : sheets_to_close) {
606 float canvas_height) {
607 const float cell_size = 8.0f;
608 const ImU32 color1 = ImGui::GetColorU32(IM_COL32(180, 180, 180, 255));
609 const ImU32 color2 = ImGui::GetColorU32(IM_COL32(220, 220, 220, 255));
612 int cols =
static_cast<int>(canvas_width / cell_size) + 1;
613 int rows =
static_cast<int>(canvas_height / cell_size) + 1;
615 for (
int row = 0; row < rows; row++) {
616 for (
int col = 0; col < cols; col++) {
617 bool is_light = (row + col) % 2 == 0;
618 ImVec2 p_min(origin.x + col * cell_size, origin.y + row * cell_size);
619 ImVec2 p_max(std::min(p_min.x + cell_size, origin.x + canvas_width),
620 std::min(p_min.y + cell_size, origin.y + canvas_height));
622 is_light ? color1 : color2);
633 ImVec2 v_end(cursor_screen.x + pixel_size / 2,
636 v_start, v_end, ImGui::GetColorU32(IM_COL32(255, 100, 100, 100)), 1.0f);
641 cursor_screen.y + pixel_size / 2);
643 h_start, h_end, ImGui::GetColorU32(IM_COL32(255, 100, 100, 100)), 1.0f);
646 ImVec2 pixel_min = cursor_screen;
647 ImVec2 pixel_max(cursor_screen.x + pixel_size, cursor_screen.y + pixel_size);
649 ImGui::GetColorU32(IM_COL32(255, 255, 255, 200)),
657 int half = brush / 2;
660 ImVec2 brush_min(cursor_screen.x - half * pixel_size,
661 cursor_screen.y - half * pixel_size);
662 ImVec2 brush_max(cursor_screen.x + (brush - half) * pixel_size,
663 cursor_screen.y + (brush - half) * pixel_size);
667 ? ImGui::GetColorU32(IM_COL32(255, 0, 0, 50))
668 : ImGui::GetColorU32(IM_COL32(0, 255, 0, 50));
673 ? ImGui::GetColorU32(IM_COL32(255, 100, 100, 200))
674 : ImGui::GetColorU32(IM_COL32(100, 255, 100, 200));
680 if (cursor_x_ < 0 || cursor_x_ >= sheet.
width() ||
cursor_y_ < 0 ||
688 ImGui::BeginTooltip();
691 ImGui::Text(tr(
"Index: %d"), color_index);
693 if (color_index < palette.size()) {
694 ImGui::Text(tr(
"SNES: $%04X"), palette[color_index].snes());
695 ImVec4 color(palette[color_index].rgb().x / 255.0f,
696 palette[color_index].rgb().y / 255.0f,
697 palette[color_index].rgb().z / 255.0f, 1.0f);
698 ImGui::ColorButton(
"##ColorPreview", color, ImGuiColorEditFlags_NoTooltip,
700 if (color_index == 0) {
702 ImGui::TextDisabled(tr(
"(Transparent)"));
716 const double now = ImGui::GetTime();
723 const int tiles_per_row = sheet.
width() / 8;
724 if (tiles_per_row <= 0) {
728 const int tile_x =
static_cast<int>(tile_index % tiles_per_row);
729 const int tile_y =
static_cast<int>(tile_index / tiles_per_row);
732 0.5f + 0.5f * std::sin(
static_cast<float>(elapsed) * 6.0f);
733 const float alpha = 0.25f + (pulse * 0.35f);
740 const ImU32 fill_color = ImGui::GetColorU32(sel);
742 sel_outline.w = 0.9f;
743 const ImU32 outline_color = ImGui::GetColorU32(sel_outline);
747 if (ImGui::IsMouseHoveringRect(min, max)) {
748 ImGui::BeginTooltip();
749 ImGui::Text(tr(
"Focus tile: %d (sheet %02X)"), tile_index,
759 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,
760 ImVec2(4, ImGui::GetStyle().ItemSpacing.y));
762 ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x;
766 const float swatch_size = ImGui::GetFrameHeight();
767 const float swatch_width =
768 9 * swatch_size + 9 * ImGui::GetStyle().ItemSpacing.x;
769 if (ImGui::GetItemRectMax().x + swatch_width +
770 ImGui::GetStyle().ItemSpacing.x <=
776 ImGui::TextDisabled(tr(
"No sheet"));
778 ImGui::PopStyleVar();
786 const auto display_color = [&palette](
int index) {
788 color.w = index == 0 ? 0.0f : 1.0f;
791 constexpr auto kSwatchFlags = ImGuiColorEditFlags_NoTooltip |
792 ImGuiColorEditFlags_NoBorder |
793 ImGuiColorEditFlags_AlphaPreview;
796 for (
int i = 0; i < static_cast<int>(palette.size()) && i < 8; i++) {
801 std::string
id = absl::StrFormat(
"##Color%d", i);
802 if (ImGui::ColorButton(
id.c_str(), display_color(i), kSwatchFlags,
803 ImVec2(swatch_size, swatch_size))) {
809 const auto min = ImGui::GetItemRectMin();
810 const auto max = ImGui::GetItemRectMax();
811 ImGui::GetWindowDrawList()->AddRect(
812 ImVec2(min.x - 1, min.y - 1), ImVec2(max.x + 1, max.y + 1),
814 ImGui::GetStyle().FrameRounding, 0, 1.5f);
817 if (ImGui::IsItemHovered()) {
818 ImGui::BeginTooltip();
819 ImGui::Text(tr(
"Index: %d"), i);
820 ImGui::Text(tr(
"SNES: $%04X"), palette[i].snes());
821 ImGui::Text(tr(
"RGB: %d, %d, %d"),
static_cast<int>(palette[i].rgb().x),
822 static_cast<int>(palette[i].rgb().y),
823 static_cast<int>(palette[i].rgb().z));
825 ImGui::Text(tr(
"(Transparent)"));
832 ImGui::SameLine(0, ImGui::GetStyle().ItemSpacing.x * 2);
834 if (index >= 8 || index >= palette.size()) {
835 ImGui::TextDisabled(
"?");
836 HOVER_HINT(
"Selected palette index is unavailable");
838 ImGui::PopStyleVar();
841 ImGui::ColorButton(
"##CurrentColor", display_color(index), kSwatchFlags,
842 ImVec2(swatch_size, swatch_size));
844 if (ImGui::IsItemHovered()) {
845 ImGui::SetTooltip(tr(
"Current color: %d%s"), index,
846 index == 0 ? tr(
" (transparent)") :
"");
849 ImGui::PopStyleVar();
855 ImGui::TextDisabled(tr(
"No sheet"));
860 if (!sheet.texture())
864 float mini_scale = 0.5f;
865 float mini_width = sheet.width() * mini_scale;
866 float mini_height = sheet.height() * mini_scale;
868 ImVec2 pos = ImGui::GetCursorScreenPos();
870 ImGui::GetWindowDrawList()->AddImage(
871 (ImTextureID)(intptr_t)sheet.texture(), pos,
872 ImVec2(pos.x + mini_width, pos.y + mini_height), ImVec2(0, 0),
873 ImVec2(1, 1), ImGui::GetColorU32(ImVec4(1, 1, 1, 1)));
878 ImGui::Dummy(ImVec2(mini_width, mini_height));
883 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,
884 ImVec2(4, ImGui::GetStyle().ItemSpacing.y));
885 const float button_size = ImGui::GetFrameHeight();
892 ImGui::SetNextItemWidth(ImGui::CalcTextSize(
"16x").x + button_size + 8);
893 if (ImGui::BeginCombo(
"##PixelZoom",
895 for (
int zoom = 1; zoom <= 16; ++zoom) {
896 if (ImGui::Selectable(absl::StrFormat(
"%dx", zoom).c_str(),
910 if (ImGui::Button(tr(
"Fit"))) {
914 HOVER_HINT(
"Fit this sheet at an integer zoom and reset panning");
917 std::string status = absl::StrFormat(
926 absl::StrAppendFormat(&status,
" | %s %d", tr(
"Focus"),
930 status += tr(
" | Modified");
936 "##PixelStatus", ImVec2(0, button_size),
false,
937 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
938 ImGui::AlignTextToFramePadding();
939 ImGui::TextUnformatted(status.c_str());
940 if (ImGui::IsWindowHovered()) {
941 ImGui::SetTooltip(
"%s", status.c_str());
944 ImGui::PopStyleVar();
948 if (!ImGui::IsItemHovered()) {
954 ImVec2 mouse_pos = ImGui::GetMousePos();
957 cursor_x_ =
static_cast<int>(pixel_pos.x);
958 cursor_y_ =
static_cast<int>(pixel_pos.y);
972 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
1011 if (ImGui::IsMouseDragging(ImGuiMouseButton_Left) &&
is_drawing_) {
1036 if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) &&
is_drawing_)
1041 if (
is_drawing_ && !ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
1080 if (edit ==
nullptr) {
1088 if (edit ==
nullptr) {
1092 int half = size / 2;
1093 bool changed =
false;
1094 for (
int dy = -half; dy < size - half; dy++) {
1095 for (
int dx = -half; dx < size - half; dx++) {
1104 if (edit ==
nullptr) {
1108 int half = size / 2;
1109 bool changed =
false;
1110 for (
int dy = -half; dy < size - half; dy++) {
1111 for (
int dx = -half; dx < size - half; dx++) {
1112 changed |= edit->Set(x + dx, y + dy, 0);
1120 if (edit ==
nullptr) {
1123 const int width = edit->width();
1124 const int height = edit->height();
1125 if (x < 0 || x >= width || y < 0 || y >= height)
1128 uint8_t target_color = edit->Get(x, y);
1131 if (target_color == fill_color)
1135 std::queue<std::pair<int, int>> queue;
1136 std::vector<bool> visited(width * height,
false);
1139 visited[y * width + x] =
true;
1141 while (!queue.empty()) {
1142 auto [cx, cy] = queue.front();
1145 edit->Set(cx, cy, fill_color);
1148 const int dx[] = {0, 0, -1, 1};
1149 const int dy[] = {-1, 1, 0, 0};
1151 for (
int i = 0; i < 4; i++) {
1152 int nx = cx + dx[i];
1153 int ny = cy + dy[i];
1155 if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
1156 int idx = ny * width + nx;
1157 if (!visited[idx] && edit->Get(nx, ny) == target_color) {
1158 visited[idx] =
true;
1159 queue.push({nx, ny});
1171 if (x >= 0 && x < sheet.width() && y >= 0 && y < sheet.height()) {
1178 if (edit ==
nullptr) {
1183 int dx = std::abs(x2 - x1);
1184 int dy = std::abs(y2 - y1);
1185 int sx = x1 < x2 ? 1 : -1;
1186 int sy = y1 < y2 ? 1 : -1;
1188 bool changed =
false;
1193 if (x1 == x2 && y1 == y2)
1213 if (edit ==
nullptr) {
1217 int min_x = std::min(x1, x2);
1218 int max_x = std::max(x1, x2);
1219 int min_y = std::min(y1, y2);
1220 int max_y = std::max(y1, y2);
1222 bool changed =
false;
1225 for (
int y = min_y; y <= max_y; y++) {
1226 for (
int x = min_x; x <= max_x; x++) {
1227 changed |= edit->Set(x, y, color);
1232 for (
int x = min_x; x <= max_x; x++) {
1233 changed |= edit->Set(x, min_y, color);
1234 changed |= edit->Set(x, max_y, color);
1237 for (
int y = min_y; y <= max_y; y++) {
1238 changed |= edit->Set(min_x, y, color);
1239 changed |= edit->Set(max_x, y, color);
1278 if (src_x >= 0 && src_x < sheet.width() && src_y >= 0 &&
1279 src_y < sheet.height()) {
1281 sheet.GetPixel(src_x, src_y);
1300 if (edit ==
nullptr) {
1303 bool changed =
false;
1308 changed |= edit->Set(x + dx, y + dy, pixel);
1350 if (store ==
nullptr) {
1378 const uint16_t sheet =
stroke_->sheet();
1390 absl::StrFormat(
"Edit pixels on sheet %02X", sheet)));
1396 const bool middle = ImGui::IsMouseDown(ImGuiMouseButton_Middle);
1397 if (!middle && !(hand && ImGui::IsMouseDown(ImGuiMouseButton_Left))) {
1401 const ImVec2 delta = ImGui::GetIO().MouseDelta;
1402 ImGui::SetScrollX(ImGui::GetScrollX() - delta.x);
1403 ImGui::SetScrollY(ImGui::GetScrollY() - delta.y);
1411 auto lines = [&](
float step, ImU32 color,
float thickness) {
1412 for (
float x = step; x < canvas_width; x += step) {
1413 draw_list->AddLine(ImVec2(origin.x + x, origin.y),
1414 ImVec2(origin.x + x, origin.y + canvas_height), color,
1417 for (
float y = step; y < canvas_height; y += step) {
1418 draw_list->AddLine(ImVec2(origin.x, origin.y + y),
1419 ImVec2(origin.x + canvas_width, origin.y + y), color,
1424 lines(zoom, ImGui::GetColorU32(IM_COL32(255, 255, 255, 28)), 1.0f);
1433 ImGui::AlignTextToFramePadding();
1434 ImGui::TextDisabled(
"%s", tr(
"Source colors"));
1435 HOVER_HINT(
"Colors resolved by the source editor");
1445 for (
size_t i = 0; i < colors.size(); ++i) {
1446 if (i < palette.size()) {
1447 const ImVec4 rgb = palette[i].rgb();
1448 colors[i] = {
static_cast<uint8_t
>(rgb.x),
static_cast<uint8_t
>(rgb.y),
1449 static_cast<uint8_t
>(rgb.z)};
1451 const auto gray =
static_cast<uint8_t
>(i * 255 / 7);
1452 colors[i] = {gray, gray, gray};
1463 !ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) ||
1464 ImGui::GetIO().WantTextInput) {
1467 if (ImGui::IsKeyPressed(ImGuiKey_Enter) ||
1468 ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) {
1472 if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
1476 const int step = ImGui::GetIO().KeyShift ? 8 : 1;
1478 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow))
1479 selection.
x -= step;
1480 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow))
1481 selection.x += step;
1482 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow))
1483 selection.y -= step;
1484 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow))
1485 selection.y += step;
1491 ImGui::BeginDisabled(!image_clipboard);
1493 "Copy selection, or the whole sheet, as a PNG "
1499 "Paste a PNG from the clipboard as a floating "
1500 "selection (Cmd+V)")) {
1503 ImGui::EndDisabled();
1504 if (!image_clipboard) {
1506 ImGui::TextDisabled(
"%s", tr(
"(image clipboard: macOS only)"));
1513 HOVER_HINT(
"Write the floating paste into the sheet (Enter)");
1521 HOVER_HINT(
"Leave pixels under color 0 of the paste unchanged");
1532 if (!sheet.is_active()) {
1540 int width = sheet.
width();
1541 int height = sheet.height();
1542 if (selection.is_active && !selection.is_floating && selection.width > 0 &&
1543 selection.height > 0) {
1546 width = selection.width;
1547 height = selection.height;
1550 std::vector<uint8_t> pixels(
static_cast<size_t>(width) * height, 0);
1551 for (
int y = 0; y < height; ++y) {
1552 for (
int x = 0; x < width; ++x) {
1553 const int sx = x0 + x;
1554 const int sy = y0 + y;
1555 if (sx >= 0 && sx < sheet.width() && sy >= 0 && sy < sheet.height()) {
1556 pixels[y * width + x] = sheet.GetPixel(sx, sy);
1561 const absl::Status status =
1564 status.ok() ? absl::StrFormat(
"Copied %dx%d from sheet %02X", width,
1566 : std::string(status.message());
1572 if (!selection.is_active || selection.is_floating || selection.width <= 0 ||
1573 selection.height <= 0) {
1584 if (edit ==
nullptr) {
1589 bool changed =
false;
1590 for (
int y = selection.y; y < selection.y + selection.height; ++y) {
1591 for (
int x = selection.x; x < selection.x + selection.width; ++x) {
1592 changed |= edit->Set(x, y, 0);
1598 absl::StrFormat(
"Cut %dx%d from sheet %02X", selection.width,
1604 if (!sheet.is_active()) {
1611 sheet.width(), sheet.height())
1612 : absl::StatusOr<ClipboardPaste>(png.status());
1620 const int x = selection.
is_active ? selection.x : 0;
1621 const int y = selection.is_active ? selection.y : 0;
1622 selection.pixel_data = paste->indices;
1623 selection.width = paste->width;
1624 selection.height = paste->height;
1627 selection.is_active =
true;
1628 selection.is_floating =
true;
1632 (paste->above_depth > 0 ?
" (colors above 7 were mapped by color)" :
"") +
1633 ". Drag or use arrows, Enter to commit, Esc to cancel.";
1641 std::clamp(selection.x, 0, std::max(0, sheet.width() - selection.width));
1642 selection.y = std::clamp(selection.y, 0,
1643 std::max(0, sheet.height() - selection.height));
1648 if (!selection.is_floating) {
1653 if (edit ==
nullptr) {
1658 bool changed =
false;
1659 for (
int dy = 0; dy < selection.height; ++dy) {
1660 for (
int dx = 0; dx < selection.width; ++dx) {
1661 const uint8_t pixel = selection.pixel_data[dy * selection.width + dx];
1665 changed |= edit->Set(selection.x + dx, selection.y + dy, pixel);
1670 selection.is_floating =
false;
1673 absl::StrFormat(
"Pasted %dx%d at %d,%d", selection.width,
1674 selection.height, selection.x, selection.y);
1687 if (!selection.is_floating) {
1693 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
1708 if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
1716 if (!selection.is_floating) {
1721 for (
int dy = 0; dy < selection.height; ++dy) {
1722 for (
int dx = 0; dx < selection.width; ++dx) {
1723 const uint8_t pixel = selection.pixel_data[dy * selection.width + dx];
1727 const auto& rgb = colors[pixel & 7];
1728 const ImVec2 p0 =
PixelToScreen(selection.x + dx, selection.y + dy);
1731 draw_list->AddRectFilled(
1732 p0, p1, ImGui::GetColorU32(IM_COL32(rgb[0], rgb[1], rgb[2], 230)));
1737 selection.y + selection.height),
1744 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 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.
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::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)
RAII guard for ImGui style colors.
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)
bool PixelIconButton(const char *label, float button_size)
zelda3::GraphicsSheetStore * ActiveSheetStore(const GraphicsEditorState &state)
absl::StatusOr< ClipboardPaste > MapClipboardPng(const std::vector< uint8_t > &png_bytes, const SheetColors &colors, int max_width, int max_height)
std::array< std::array< uint8_t, 3 >, 8 > SheetColors
The 8 colors a 3bpp sheet shows (index 0 is transparent in game).
void DrawGraphicsPreviewPaletteSelector(GraphicsEditorState &state, GraphicsPaletteSelectorLayout layout)
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.
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.
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.