7#include "absl/strings/str_format.h"
37 int display_index,
int dungeon_palette_id) {
38 if (display_index < 0 || display_index >= 128) {
41 if (display_index < 32) {
46 const int local_index = display_index - 32;
47 const int row = local_index / 16;
48 const int col = local_index % 16;
49 if (row >= 6 || col == 0) {
54 dungeon_palette_id, row * 15 + (col - 1)};
71 return "HUD / floor-ceiling";
73 return absl::StrFormat(
"Dungeon row %d", target.
color_index / 15 + 2);
77 auto* names =
static_cast<std::vector<std::string>*
>(user_data);
78 if (!names || idx < 0 || idx >=
static_cast<int>(names->size())) {
81 return (*names)[idx].c_str();
84void DrawColorDropTargetOutline(ImVec2 min, ImVec2 max) {
86 ImGui::GetForegroundDrawList()->AddRect(
92 if (!color || !ImGui::BeginDragDropTarget()) {
97 constexpr ImGuiDragDropFlags kFlags =
98 ImGuiDragDropFlags_AcceptBeforeDelivery |
99 ImGuiDragDropFlags_AcceptNoDrawDefaultRect |
100 ImGuiDragDropFlags_AcceptNoPreviewTooltip;
101 auto accept = [&](
const char* type,
size_t components) {
105 if (
const ImGuiPayload* payload =
106 ImGui::AcceptDragDropPayload(type, kFlags)) {
107 DrawColorDropTargetOutline(min, max);
108 if (payload->IsDelivery()) {
109 ImVec4 dropped(0, 0, 0, 1);
110 std::memcpy(&dropped.x, payload->Data,
sizeof(
float) * components);
111 if (components == 3) {
120 accept(IMGUI_PAYLOAD_TYPE_COLOR_3F, 3);
121 accept(IMGUI_PAYLOAD_TYPE_COLOR_4F, 4);
122 ImGui::EndDragDropTarget();
129 float min_swatch_size,
130 float item_spacing) {
131 const float safe_width = std::max(available_width, 1.0f);
132 const float safe_swatch = std::max(min_swatch_size, 1.0f);
133 const float safe_spacing = std::max(item_spacing, 0.0f);
134 for (
const int columns : {16, 8, 4, 2, 1}) {
135 const float required_width =
136 safe_swatch * columns + safe_spacing * (columns - 1);
137 if (safe_width >= required_width) {
167 int display_index, std::optional<gfx::SnesColor> color) {
169 return absl::FailedPreconditionError(
"GameData not loaded");
174 if (!target.has_value()) {
175 return absl::InvalidArgumentError(absl::StrFormat(
176 "Dungeon render palette slot %d is reserved or out of range",
181 if (!palette_manager.IsManaging(
game_data_)) {
182 return absl::FailedPreconditionError(
183 "PaletteManager is bound to a different ROM session");
185 const char* group_name = PaletteManagerGroupName(target->source);
186 absl::Status status =
188 ? palette_manager.SetColor(group_name, target->palette_index,
189 target->color_index, *color)
190 : palette_manager.ResetColor(group_name, target->palette_index,
191 target->color_index);
199 return absl::OkStatus();
205 ImGui::TextColored(ImVec4(1, 0, 0, 1), tr(
"GameData not loaded"));
215 if (dungeon_pal_group.empty()) {
216 ImGui::TextDisabled(tr(
"Dungeon palettes unavailable"));
243 ImGui::TextDisabled(tr(
"Select a color to edit"));
253 int num_palettes = dungeon_pal_group.
size();
254 if (num_palettes <= 0) {
255 ImGui::TextDisabled(tr(
"No dungeon palettes"));
261 ImGui::Text(tr(
"Dungeon Palette:"));
263 ImGui::SetNextItemWidth(std::min(180.0f, ImGui::GetContentRegionAvail().x));
265 if (ImGui::BeginCombo(
268 for (
int i = 0; i < num_palettes; i++) {
270 if (ImGui::Selectable(absl::StrFormat(
"Palette %d", i).c_str(),
276 ImGui::SetItemDefaultFocus();
283 ImGui::TextUnformatted(tr(
"Palette ID"));
285 ImGui::SetNextItemWidth(72.0f);
287 "##DungeonPaletteId", ImGuiDataType_S32, &requested_palette_id,
"%d", 0,
290 "Dungeon main palette ID");
291 if (palette_id_changed) {
301 if (dungeon_pal_group.empty()) {
306 ImGui::SeparatorText(
320 ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_PickerHueWheel)) {
335 ImGui::Text(tr(
"RGB (0-255): (%d, %d, %d)"),
339 ImGui::Text(tr(
"SNES BGR555: 0x%04X"), original_color.snes());
354 ImGui::TextDisabled(tr(
"Slots 00-1F: HUD / floor / ceiling"));
355 ImGui::TextDisabled(tr(
"Slots 20-7F: Dungeon main"));
356 constexpr float kSwatchSpacing = 2.0f;
357 const ImVec2 previous_spacing = ImGui::GetStyle().ItemSpacing;
358 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing,
359 ImVec2(kSwatchSpacing, previous_spacing.y));
361 ImGui::GetContentRegionAvail().x, 14.0f,
365 for (
int i = 0; i < 128; ++i) {
366 if (i % columns != 0) {
371 bool editable =
false;
372 std::string tooltip_label =
"Reserved / transparent";
373 int source_index = -1;
376 if (target.has_value()) {
377 const auto& group = PaletteGroupForTarget(*
game_data_, *target);
378 if (target->palette_index >= 0 &&
379 target->palette_index <
static_cast<int>(group.size())) {
380 const auto& palette = group.palette_ref(target->palette_index);
381 if (target->color_index >= 0 &&
382 target->color_index <
static_cast<int>(palette.size())) {
383 color = palette[target->color_index];
385 tooltip_label = DungeonRenderColorSourceLabel(*target);
386 source_index = target->color_index;
394 ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.25f);
396 const bool clicked = ImGui::ColorButton(
"##render_color", display_color,
397 ImGuiColorEditFlags_NoTooltip,
398 ImVec2(swatch_size, swatch_size));
400 absl::StrFormat(
"palette_swatch_%03d", i),
401 "Dungeon render palette swatch");
402 const ImVec2 swatch_min = ImGui::GetItemRectMin();
403 const ImVec2 swatch_max = ImGui::GetItemRectMax();
404 const bool double_clicked =
405 editable && ImGui::IsItemHovered() &&
406 ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
408 ImGui::PopStyleVar();
411 if ((clicked || double_clicked) && editable) {
419 AcceptImGuiColorDrop(&dropped_color, swatch_min, swatch_max)) {
427 LOG_ERROR(
"PaletteEditorWidget",
"Failed to apply dropped color: %s",
428 status.message().data());
432 if (ImGui::IsItemHovered()) {
434 ImGui::SetTooltip(tr(
"%s\nSlot %d\nSNES: 0x%04X\nRGB: (%d, %d, %d)"),
435 tooltip_label.c_str(), source_index, color.
snes(),
436 static_cast<int>(display_color.x * 255),
437 static_cast<int>(display_color.y * 255),
438 static_cast<int>(display_color.z * 255));
440 ImGui::SetTooltip(
"%s", tooltip_label.c_str());
445 ImGui::PopStyleVar();
449 float max_size)
const {
450 const float available_width =
451 std::max(ImGui::GetContentRegionAvail().x, 1.0f);
452 const float spacing =
453 std::max(ImGui::GetStyle().ItemSpacing.x, 2.0f) * (columns - 1);
454 const float raw = (available_width - spacing) / std::max(columns, 1);
455 return std::clamp(raw, min_size, max_size);
467 const auto& group = PaletteGroupForTarget(*
game_data_, *target);
468 if (target->palette_index < 0 ||
469 target->palette_index >=
static_cast<int>(group.size())) {
474 const auto& palette = group.palette_ref(target->palette_index);
475 if (target->color_index < 0 ||
476 target->color_index >=
static_cast<int>(palette.size())) {
482 ImGui::SeparatorText(absl::StrFormat(
"%s Color %d",
483 DungeonRenderColorSourceLabel(*target),
487 const gfx::SnesColor current_color = palette[target->color_index];
490 "Color", &edited_color,
491 ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_PickerHueWheel)) {
492 const absl::Status status =
497 LOG_ERROR(
"PaletteEditorWidget",
"Failed to apply palette color: %s",
498 status.message().data());
502 uint16_t raw_color = current_color.
snes();
503 ImGui::AlignTextToFramePadding();
504 ImGui::TextUnformatted(tr(
"Raw BGR555"));
506 ImGui::SetNextItemWidth(96.0f);
508 "##SelectedDungeonPaletteBgr555", ImGuiDataType_U16, &raw_color,
"%04X",
509 ImGuiInputTextFlags_CharsHexadecimal,
511 static_cast<uint64_t
>(target->source),
512 static_cast<uint64_t
>(target->palette_index),
513 static_cast<uint64_t
>(target->color_index)});
515 "Selected dungeon palette raw BGR555 color");
519 const absl::Status status =
524 LOG_ERROR(
"PaletteEditorWidget",
"Failed to apply raw palette color: %s",
525 status.message().data());
529 ImGui::Text(tr(
"RGB (0-255): (%d, %d, %d)"),
533 ImGui::Text(tr(
"SNES BGR555: 0x%04X"), current_color.
snes());
536 const absl::Status status =
540 PaletteManagerGroupName(target->source), target->palette_index,
541 target->color_index);
544 LOG_ERROR(
"PaletteEditorWidget",
"Failed to reset palette color: %s",
545 status.message().data());
553 const std::string& title) {
554 if (ImGui::BeginPopupModal(title.c_str(),
nullptr,
555 ImGuiWindowFlags_AlwaysAutoResize)) {
556 ImGui::Text(tr(
"Enhanced Palette Editor"));
562 if (ImGui::CollapsingHeader(tr(
"Palette Analysis"))) {
566 if (ImGui::CollapsingHeader(tr(
"ROM Palette Manager")) &&
rom_) {
589 ImGui::CloseCurrentPopup();
602 ImGui::Text(tr(
"No ROM loaded"));
615 ImGui::Text(tr(
"Preview of %s:"),
627 const std::string& title) {
632 ImGui::Text(tr(
"Bitmap Color Analysis"));
636 ImGui::Text(tr(
"Bitmap is not active"));
641 std::map<uint8_t, int> pixel_counts;
642 const auto& data = bitmap.
vector();
644 for (uint8_t pixel : data) {
645 uint8_t palette_index = pixel & 0x0F;
646 pixel_counts[palette_index]++;
649 ImGui::Text(tr(
"Bitmap Size: %d x %d (%zu pixels)"), bitmap.
width(),
650 bitmap.
height(), data.size());
653 ImGui::Text(tr(
"Pixel Distribution:"));
655 int total_pixels =
static_cast<int>(data.size());
659 .x_label =
"Palette Index",
661 .flags = ImPlotFlags_NoBoxSelect,
662 .x_axis_flags = ImPlotAxisFlags_AutoFit,
663 .y_axis_flags = ImPlotAxisFlags_AutoFit};
664 std::vector<double> x;
665 std::vector<double> y;
666 x.reserve(pixel_counts.size());
667 y.reserve(pixel_counts.size());
668 for (
const auto& [index, count] : pixel_counts) {
669 x.push_back(
static_cast<double>(index));
670 y.push_back(
static_cast<double>(count));
673 if (plot && !x.empty()) {
674 ImPlot::PlotBars(
"Usage", x.data(), y.data(),
static_cast<int>(x.size()),
675 0.67, 0.0, ImPlotBarsFlags_None);
692 if (palette_index >= 0 && palette_index < 8) {
704 }
catch (
const std::exception& e) {
731 const float swatch_size =
733 for (
int i = 0; i < static_cast<int>(palette.
size()); i++) {
737 auto color = palette[i];
741 const bool clicked = ImGui::ColorButton(
"##color", display_color,
742 ImGuiColorEditFlags_NoTooltip,
743 ImVec2(swatch_size, swatch_size));
744 const ImVec2 swatch_min = ImGui::GetItemRectMin();
745 const ImVec2 swatch_max = ImGui::GetItemRectMax();
746 const bool double_clicked =
747 ImGui::IsItemHovered() &&
748 ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
749 if (clicked || double_clicked) {
755 if (AcceptImGuiColorDrop(&palette[i], swatch_min, swatch_max)) {
766 if (ImGui::BeginPopupContextItem()) {
767 ImGui::TextDisabled(tr(
"Color %d (0x%04X)"), i, color.snes());
769 std::vector<MenuItemSpec> items;
770 items.emplace_back(tr(
"Edit Color..."),
ICON_MD_EDIT, [&, i]() {
776 items.back().separator_after =
true;
793 if (ImGui::IsItemHovered()) {
794 ImGui::SetTooltip(tr(
"Color %d\nSNES: 0x%04X\nRGB: (%d, %d, %d)"), i,
795 color.snes(),
static_cast<int>(display_color.x * 255),
796 static_cast<int>(display_color.y * 255),
797 static_cast<int>(display_color.z * 255));
805 std::string popup_id =
808 ImGui::OpenPopup(popup_id.c_str());
809 if (ImGui::BeginPopupModal(popup_id.c_str(),
nullptr,
810 ImGuiWindowFlags_AlwaysAutoResize)) {
812 if (ImGui::ColorEdit4(
814 ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_DisplayRGB)) {
827 ImGui::CloseCurrentPopup();
832 ImGui::CloseCurrentPopup();
845 ImGui::Text(tr(
"No ROM palettes available"));
849 ImGui::Text(tr(
"Palette Group:"));
854 ImGui::Text(tr(
"Palette Index:"));
858 ImGui::Text(tr(
"Preview:"));
860 for (
int i = 0; i < 8 && i < static_cast<int>(preview_palette.size());
864 auto color = preview_palette[i];
866 ImGui::ColorButton((
"##preview" + std::to_string(i)).c_str(),
867 display_color, ImGuiColorEditFlags_NoTooltip,
877 ImGui::PushID(color_index);
879 if (ImGui::ColorEdit4(
880 "##color_edit", &rgba.x,
881 ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_DisplayRGB)) {
885 ImGui::Text(tr(
"SNES Color: 0x%04X"), color.
snes());
887 int r = (color.
snes() & 0x1F);
888 int g = (color.
snes() >> 5) & 0x1F;
889 int b = (color.
snes() >> 10) & 0x1F;
891 if (ImGui::SliderInt(tr(
"Red"), &r, 0, 31)) {
892 uint16_t new_color = (color.
snes() & 0xFFE0) | (r & 0x1F);
895 if (ImGui::SliderInt(tr(
"Green"), &g, 0, 31)) {
896 uint16_t new_color = (color.
snes() & 0xFC1F) | ((g & 0x1F) << 5);
899 if (ImGui::SliderInt(tr(
"Blue"), &b, 0, 31)) {
900 uint16_t new_color = (color.
snes() & 0x83FF) | ((b & 0x1F) << 10);
908 ImGui::Text(tr(
"Palette Information:"));
909 ImGui::Text(tr(
"Size: %zu colors"), palette.
size());
911 std::map<uint16_t, int> color_frequency;
912 for (
int i = 0; i < static_cast<int>(palette.
size()); i++) {
913 color_frequency[palette[i].snes()]++;
916 ImGui::Text(tr(
"Unique Colors: %zu"), color_frequency.size());
918 if (color_frequency.size() < palette.
size()) {
919 ImGui::TextColored(ImVec4(1, 1, 0, 1),
920 tr(
"Warning: Duplicate colors detected!"));
921 if (ImGui::TreeNode(
"Duplicate Colors")) {
922 for (
const auto& [
snes_color, count] : color_frequency) {
924 ImVec4 display_color =
926 ImGui::ColorButton((
"##dup" + std::to_string(
snes_color)).c_str(),
927 display_color, ImGuiColorEditFlags_NoTooltip,
930 ImGui::Text(tr(
"0x%04X appears %d times"),
snes_color, count);
942 .x_label =
"Color Index",
944 .flags = ImPlotFlags_NoBoxSelect,
945 .x_axis_flags = ImPlotAxisFlags_AutoFit,
946 .y_axis_flags = ImPlotAxisFlags_AutoFit};
947 std::vector<double> x;
948 std::vector<double> y;
949 x.reserve(color_frequency.size());
950 y.reserve(color_frequency.size());
951 for (
const auto& [
snes_color, count] : color_frequency) {
953 y.push_back(
static_cast<double>(count));
956 if (plot && !x.empty()) {
957 ImPlot::PlotBars(
"Count", x.data(), y.data(),
static_cast<int>(x.size()),
958 0.5, 0.0, ImPlotBarsFlags_None);
962 float total_brightness = 0.0f;
963 float min_brightness = 1.0f;
964 float max_brightness = 0.0f;
966 for (
int i = 0; i < static_cast<int>(palette.
size()); i++) {
968 float brightness = (color.x + color.y + color.z) / 3.0f;
969 total_brightness += brightness;
970 min_brightness = std::min(min_brightness, brightness);
971 max_brightness = std::max(max_brightness, brightness);
974 float avg_brightness = total_brightness / palette.
size();
977 ImGui::Text(tr(
"Brightness Analysis:"));
978 ImGui::Text(tr(
"Average: %.2f"), avg_brightness);
979 ImGui::Text(tr(
"Range: %.2f - %.2f"), min_brightness, max_brightness);
981 ImGui::Text(tr(
"Brightness Distribution:"));
982 ImGui::ProgressBar(avg_brightness, ImVec2(-1, 0),
"Avg");
994 if (palette_groups.overworld_main.size() > 0) {
998 if (palette_groups.overworld_aux.size() > 0) {
1002 if (palette_groups.overworld_animated.size() > 0) {
1006 if (palette_groups.dungeon_main.size() > 0) {
1010 if (palette_groups.global_sprites.size() > 0) {
1014 if (palette_groups.armors.size() > 0) {
1018 if (palette_groups.swords.size() > 0) {
1024 }
catch (
const std::exception& e) {
1025 LOG_ERROR(
"Enhanced Palette Editor",
"Failed to load ROM palettes");
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Represents a bitmap image optimized for SNES ROM hacking.
const SnesPalette & palette() const
const std::vector< uint8_t > & vector() const
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
void SetPaletteWithTransparent(const SnesPalette &palette, size_t index, int length=7)
Set the palette with a transparent color.
static PaletteManager & Get()
Get the singleton instance.
SnesColor GetColor(const std::string &group_name, int palette_index, int color_index) const
Get a color from a palette.
constexpr uint16_t snes() const
Get SNES 15-bit color.
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
#define ICON_MD_FORMAT_COLOR_RESET
#define LOG_ERROR(category, format,...)
ImVec4 ConvertColorToImVec4(const Color &color)
bool PrimaryButton(const char *label, const ImVec2 &size, const char *panel_id, const char *anim_id)
Draw a primary action button (accented color).
DungeonRenderPaletteSource
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
std::string MakePopupIdWithInstance(const std::string &editor_name, const std::string &popup_name, const void *instance)
Generate popup ID with instance pointer for guaranteed uniqueness.
void SnesColorCopyMenuItems(const gfx::SnesColor &color)
bool ThemedButton(const char *label, const ImVec2 &size, const char *panel_id, const char *anim_id)
Draw a standard text button with theme colors.
ImVec4 ConvertSnesColorToImVec4(const gfx::SnesColor &color)
Convert SnesColor to standard ImVec4 for display.
bool InputScalarDeferred(const char *label, ImGuiDataType data_type, void *data, const char *format, ImGuiInputTextFlags flags, InputScalarTargetIdentity target_identity)
int ResolveDungeonRenderPaletteColumns(float available_width, float min_swatch_size, float item_spacing)
IMGUI_API bool SnesColorEdit4(absl::string_view label, gfx::SnesColor *color, ImGuiColorEditFlags flags)
void RenderMenuItem(const CanvasMenuItem &item, std::function< void(const std::string &, std::function< void()>)> popup_opened_callback)
Render a single menu item.
void RenderMenuItems(const std::vector< MenuItemSpec > &items)
SNES color in 15-bit RGB format (BGR555)
PaletteGroup dungeon_main
Represents a group of palettes.
gfx::PaletteGroupMap palette_groups