From 4fe23b9af23444f8c291068d022b273789339f6b Mon Sep 17 00:00:00 2001 From: scawful Date: Sun, 12 Oct 2025 08:05:56 -0400 Subject: [PATCH] feat(editor): reorganize palette editor structure and enhance linking - Moved palette_editor and palette_group_card files to a dedicated palette directory for better organization. - Updated CMake configuration to link the new palette editor files and added support for the yaze_agent library when not in minimal build. - Refactored include paths in various editor files to reflect the new structure, ensuring proper linkage and modularity. Benefits: - Improved code organization and maintainability by grouping related files. - Enhanced functionality with the integration of AI features through the yaze_agent library. --- src/app/editor/editor_library.cmake | 12 +- src/app/editor/editor_manager.cc | 2 +- src/app/editor/editor_manager.h | 2 +- src/app/editor/graphics/graphics_editor.h | 2 +- src/app/editor/overworld/overworld_editor.h | 2 +- src/app/editor/overworld/tile16_editor.h | 2 +- .../{graphics => palette}/palette_editor.cc | 192 +++- .../{graphics => palette}/palette_editor.h | 10 + src/app/editor/palette/palette_group_card.cc | 1014 +++++++++++++++++ src/app/editor/palette/palette_group_card.h | 403 +++++++ src/app/gui/themed_widgets.cc | 175 ++- src/app/gui/themed_widgets.h | 112 +- 12 files changed, 1809 insertions(+), 119 deletions(-) rename src/app/editor/{graphics => palette}/palette_editor.cc (75%) rename src/app/editor/{graphics => palette}/palette_editor.h (87%) create mode 100644 src/app/editor/palette/palette_group_card.cc create mode 100644 src/app/editor/palette/palette_group_card.h diff --git a/src/app/editor/editor_library.cmake b/src/app/editor/editor_library.cmake index 0485bb22..1f8bf0f2 100644 --- a/src/app/editor/editor_library.cmake +++ b/src/app/editor/editor_library.cmake @@ -23,8 +23,9 @@ set( app/editor/editor_manager.cc app/editor/graphics/gfx_group_editor.cc app/editor/graphics/graphics_editor.cc - app/editor/graphics/palette_editor.cc app/editor/graphics/screen_editor.cc + app/editor/palette/palette_editor.cc + app/editor/palette/palette_group_card.cc app/editor/message/message_data.cc app/editor/message/message_editor.cc app/editor/message/message_preview.cc @@ -101,11 +102,20 @@ target_link_libraries(yaze_editor PUBLIC yaze_gfx yaze_gui yaze_zelda3 + yaze_emulator # Needed for emulator integration (APU, PPU, SNES) yaze_util yaze_common ImGui ) +# Link agent library for AI features (always available when not in minimal build) +if(NOT YAZE_MINIMAL_BUILD) + if(TARGET yaze_agent) + target_link_libraries(yaze_editor PUBLIC yaze_agent) + message(STATUS "✓ yaze_editor linked to yaze_agent") + endif() +endif() + # Note: yaze_test_support linking is deferred to test.cmake to ensure proper ordering if(YAZE_WITH_JSON) diff --git a/src/app/editor/editor_manager.cc b/src/app/editor/editor_manager.cc index 2e436122..07ab8eee 100644 --- a/src/app/editor/editor_manager.cc +++ b/src/app/editor/editor_manager.cc @@ -21,7 +21,7 @@ #include "app/editor/code/assembly_editor.h" #include "app/editor/dungeon/dungeon_editor_v2.h" #include "app/editor/graphics/graphics_editor.h" -#include "app/editor/graphics/palette_editor.h" +#include "app/editor/palette/palette_editor.h" #include "app/editor/graphics/screen_editor.h" #include "app/editor/music/music_editor.h" #include "app/editor/overworld/overworld_editor.h" diff --git a/src/app/editor/editor_manager.h b/src/app/editor/editor_manager.h index f0445324..12ea03da 100644 --- a/src/app/editor/editor_manager.h +++ b/src/app/editor/editor_manager.h @@ -20,7 +20,7 @@ #include "app/editor/code/project_file_editor.h" #include "app/editor/dungeon/dungeon_editor_v2.h" #include "app/editor/graphics/graphics_editor.h" -#include "app/editor/graphics/palette_editor.h" +#include "app/editor/palette/palette_editor.h" #include "app/editor/graphics/screen_editor.h" #include "app/editor/message/message_editor.h" #include "app/editor/music/music_editor.h" diff --git a/src/app/editor/graphics/graphics_editor.h b/src/app/editor/graphics/graphics_editor.h index fbb50db2..324b0507 100644 --- a/src/app/editor/graphics/graphics_editor.h +++ b/src/app/editor/graphics/graphics_editor.h @@ -5,7 +5,7 @@ #include "absl/status/status.h" #include "app/editor/editor.h" -#include "app/editor/graphics/palette_editor.h" +#include "app/editor/palette/palette_editor.h" #include "app/gfx/bitmap.h" #include "app/gui/editor_card_manager.h" #include "app/gfx/snes_tile.h" diff --git a/src/app/editor/overworld/overworld_editor.h b/src/app/editor/overworld/overworld_editor.h index 01429a2f..7006cc68 100644 --- a/src/app/editor/overworld/overworld_editor.h +++ b/src/app/editor/overworld/overworld_editor.h @@ -4,7 +4,7 @@ #include "absl/status/status.h" #include "app/editor/editor.h" #include "app/editor/graphics/gfx_group_editor.h" -#include "app/editor/graphics/palette_editor.h" +#include "app/editor/palette/palette_editor.h" #include "app/gui/editor_card_manager.h" #include "app/editor/overworld/tile16_editor.h" #include "app/editor/overworld/map_properties.h" diff --git a/src/app/editor/overworld/tile16_editor.h b/src/app/editor/overworld/tile16_editor.h index 2defad0c..c9c242bd 100644 --- a/src/app/editor/overworld/tile16_editor.h +++ b/src/app/editor/overworld/tile16_editor.h @@ -7,7 +7,7 @@ #include #include "absl/status/status.h" -#include "app/editor/graphics/palette_editor.h" +#include "app/editor/palette/palette_editor.h" #include "app/gfx/bitmap.h" #include "app/gfx/snes_palette.h" #include "app/gfx/snes_tile.h" diff --git a/src/app/editor/graphics/palette_editor.cc b/src/app/editor/palette/palette_editor.cc similarity index 75% rename from src/app/editor/graphics/palette_editor.cc rename to src/app/editor/palette/palette_editor.cc index 5ceeea9e..1159f501 100644 --- a/src/app/editor/graphics/palette_editor.cc +++ b/src/app/editor/palette/palette_editor.cc @@ -187,6 +187,14 @@ absl::Status DisplayPalette(gfx::SnesPalette& palette, bool loaded) { } void PaletteEditor::Initialize() { + // Initialize palette cards + if (rom_ && rom_->is_loaded()) { + ow_main_card_ = std::make_unique(rom_); + ow_animated_card_ = std::make_unique(rom_); + dungeon_main_card_ = std::make_unique(rom_); + sprite_card_ = std::make_unique(rom_); + equipment_card_ = std::make_unique(rom_); + } } absl::Status PaletteEditor::Load() { @@ -206,48 +214,88 @@ absl::Status PaletteEditor::Load() { } absl::Status PaletteEditor::Update() { - static int current_palette_group = 0; - if (BeginTable("paletteGroupsTable", 3, kPaletteTableFlags)) { - TableSetupColumn("Categories", ImGuiTableColumnFlags_WidthFixed, 200); - TableSetupColumn("Palette Editor", ImGuiTableColumnFlags_WidthStretch); - TableSetupColumn("Quick Access", ImGuiTableColumnFlags_WidthStretch); - TableHeadersRow(); + static bool use_legacy_view = false; - TableNextRow(); - TableNextColumn(); - - static int selected_category = 0; - BeginChild("CategoryList", ImVec2(0, GetContentRegionAvail().y), true); - - for (int i = 0; i < kNumPalettes; i++) { - const bool is_selected = (selected_category == i); - if (Selectable(std::string(kPaletteCategoryNames[i]).c_str(), - is_selected)) { - selected_category = i; - } - } - - EndChild(); - - TableNextColumn(); - BeginChild("PaletteEditor", ImVec2(0, 0), true); - - Text("%s", std::string(kPaletteCategoryNames[selected_category]).c_str()); - - Separator(); - - if (rom()->is_loaded()) { - status_ = DrawPaletteGroup(selected_category, true); - } - - EndChild(); - - TableNextColumn(); - DrawQuickAccessTab(); - - EndTable(); + // Toolbar with view selector + if (ImGui::Button(use_legacy_view ? "Switch to Card View" : "Switch to Legacy View")) { + use_legacy_view = !use_legacy_view; } + ImGui::SameLine(); + ImGui::TextDisabled("|"); + ImGui::SameLine(); + + if (use_legacy_view) { + // Original table-based view + static int current_palette_group = 0; + if (BeginTable("paletteGroupsTable", 3, kPaletteTableFlags)) { + TableSetupColumn("Categories", ImGuiTableColumnFlags_WidthFixed, 200); + TableSetupColumn("Palette Editor", ImGuiTableColumnFlags_WidthStretch); + TableSetupColumn("Quick Access", ImGuiTableColumnFlags_WidthStretch); + TableHeadersRow(); + + TableNextRow(); + TableNextColumn(); + + static int selected_category = 0; + BeginChild("CategoryList", ImVec2(0, GetContentRegionAvail().y), true); + + for (int i = 0; i < kNumPalettes; i++) { + const bool is_selected = (selected_category == i); + if (Selectable(std::string(kPaletteCategoryNames[i]).c_str(), + is_selected)) { + selected_category = i; + } + } + + EndChild(); + + TableNextColumn(); + BeginChild("PaletteEditor", ImVec2(0, 0), true); + + Text("%s", std::string(kPaletteCategoryNames[selected_category]).c_str()); + + Separator(); + + if (rom()->is_loaded()) { + status_ = DrawPaletteGroup(selected_category, true); + } + + EndChild(); + + TableNextColumn(); + DrawQuickAccessTab(); + + EndTable(); + } + } else { + // New card-based view with quick access sidebar + if (BeginTable("paletteCardsTable", 2, kPaletteTableFlags)) { + TableSetupColumn("Palette Cards", ImGuiTableColumnFlags_WidthStretch); + TableSetupColumn("Quick Access", ImGuiTableColumnFlags_WidthFixed, 300); + TableHeadersRow(); + + TableNextRow(); + TableNextColumn(); + + BeginChild("PaletteCardsView", ImVec2(0, 0), true); + DrawPaletteCards(); + EndChild(); + + TableNextColumn(); + DrawQuickAccessTab(); + + EndTable(); + } + } + + // Draw palette card windows (dockable/floating) + if (ow_main_card_) ow_main_card_->Draw(); + if (ow_animated_card_) ow_animated_card_->Draw(); + if (dungeon_main_card_) dungeon_main_card_->Draw(); + if (sprite_card_) sprite_card_->Draw(); + if (equipment_card_) equipment_card_->Draw(); + return absl::OkStatus(); } @@ -559,5 +607,71 @@ absl::Status PaletteEditor::ResetColorToOriginal( return absl::OkStatus(); } +void PaletteEditor::DrawPaletteCards() { + ImGui::TextWrapped( + "Click a palette card below to open it as a dockable/floating window. " + "Each card provides full editing capabilities with undo/redo, " + "save/discard workflow, and detailed metadata."); + + ImGui::Separator(); + + // Draw card launcher buttons + ImGui::Text("Overworld Palettes"); + if (ImGui::Button("Open Overworld Main", ImVec2(-1, 0))) { + if (ow_main_card_) ow_main_card_->Show(); + } + if (ImGui::Button("Open Overworld Animated", ImVec2(-1, 0))) { + if (ow_animated_card_) ow_animated_card_->Show(); + } + + ImGui::Separator(); + + ImGui::Text("Dungeon Palettes"); + if (ImGui::Button("Open Dungeon Main", ImVec2(-1, 0))) { + if (dungeon_main_card_) dungeon_main_card_->Show(); + } + + ImGui::Separator(); + + ImGui::Text("Sprite & Equipment Palettes"); + if (ImGui::Button("Open Sprite Palettes", ImVec2(-1, 0))) { + if (sprite_card_) sprite_card_->Show(); + } + if (ImGui::Button("Open Equipment Palettes", ImVec2(-1, 0))) { + if (equipment_card_) equipment_card_->Show(); + } + + ImGui::Separator(); + + // Show modified status for each card + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Modified Cards:"); + bool any_modified = false; + + if (ow_main_card_ && ow_main_card_->HasUnsavedChanges()) { + ImGui::BulletText("Overworld Main"); + any_modified = true; + } + if (ow_animated_card_ && ow_animated_card_->HasUnsavedChanges()) { + ImGui::BulletText("Overworld Animated"); + any_modified = true; + } + if (dungeon_main_card_ && dungeon_main_card_->HasUnsavedChanges()) { + ImGui::BulletText("Dungeon Main"); + any_modified = true; + } + if (sprite_card_ && sprite_card_->HasUnsavedChanges()) { + ImGui::BulletText("Sprite Palettes"); + any_modified = true; + } + if (equipment_card_ && equipment_card_->HasUnsavedChanges()) { + ImGui::BulletText("Equipment Palettes"); + any_modified = true; + } + + if (!any_modified) { + ImGui::TextDisabled("No unsaved changes"); + } +} + } // namespace editor } // namespace yaze diff --git a/src/app/editor/graphics/palette_editor.h b/src/app/editor/palette/palette_editor.h similarity index 87% rename from src/app/editor/graphics/palette_editor.h rename to src/app/editor/palette/palette_editor.h index fc067bfa..5cbe577b 100644 --- a/src/app/editor/graphics/palette_editor.h +++ b/src/app/editor/palette/palette_editor.h @@ -8,6 +8,7 @@ #include "absl/status/status.h" #include "app/editor/editor.h" #include "app/editor/graphics/gfx_group_editor.h" +#include "app/editor/palette/palette_group_card.h" #include "app/gfx/snes_color.h" #include "app/gui/editor_card_manager.h" #include "app/gfx/snes_palette.h" @@ -112,6 +113,8 @@ class PaletteEditor : public Editor { private: absl::Status HandleColorPopup(gfx::SnesPalette& palette, int i, int j, int n); + void DrawPaletteCards(); + absl::Status status_; gfx::SnesColor current_color_; @@ -127,6 +130,13 @@ class PaletteEditor : public Editor { palette_internal::PaletteEditorHistory history_; Rom* rom_; + + // Palette card instances + std::unique_ptr ow_main_card_; + std::unique_ptr ow_animated_card_; + std::unique_ptr dungeon_main_card_; + std::unique_ptr sprite_card_; + std::unique_ptr equipment_card_; }; } // namespace editor diff --git a/src/app/editor/palette/palette_group_card.cc b/src/app/editor/palette/palette_group_card.cc new file mode 100644 index 00000000..f707f8c8 --- /dev/null +++ b/src/app/editor/palette/palette_group_card.cc @@ -0,0 +1,1014 @@ +#include "palette_group_card.h" + +#include + +#include "absl/strings/str_format.h" +#include "app/gfx/snes_palette.h" +#include "app/gui/color.h" +#include "app/gui/icons.h" +#include "app/gui/layout_helpers.h" +#include "app/gui/themed_widgets.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace editor { + +using namespace gui; +using gui::ThemedButton; +using gui::ThemedIconButton; +using gui::PrimaryButton; +using gui::DangerButton; +using gui::SectionHeader; + +PaletteGroupCard::PaletteGroupCard(const std::string& group_name, + const std::string& display_name, + Rom* rom) + : group_name_(group_name), + display_name_(display_name), + rom_(rom) { + // Load original palettes from ROM for reset/comparison + if (rom_ && rom_->is_loaded()) { + auto* palette_group = GetPaletteGroup(); + if (palette_group) { + for (size_t i = 0; i < palette_group->size(); i++) { + original_palettes_.push_back(palette_group->palette(i)); + } + } + } +} + +void PaletteGroupCard::Draw() { + if (!show_ || !rom_ || !rom_->is_loaded()) { + return; + } + + // Main card window + if (ImGui::Begin(display_name_.c_str(), &show_)) { + DrawToolbar(); + ImGui::Separator(); + + // Two-column layout: Grid on left, picker on right + if (ImGui::BeginTable("##PaletteCardLayout", 2, + ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV)) { + ImGui::TableSetupColumn("Grid", ImGuiTableColumnFlags_WidthStretch, 0.6f); + ImGui::TableSetupColumn("Editor", ImGuiTableColumnFlags_WidthStretch, 0.4f); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + // Left: Palette selector + grid + DrawPaletteSelector(); + ImGui::Separator(); + DrawPaletteGrid(); + + ImGui::TableNextColumn(); + + // Right: Color picker + info + if (selected_color_ >= 0) { + DrawColorPicker(); + ImGui::Separator(); + DrawColorInfo(); + ImGui::Separator(); + DrawMetadataInfo(); + } else { + ImGui::TextDisabled("Select a color to edit"); + ImGui::Separator(); + DrawMetadataInfo(); + } + + // Custom panels from derived classes + DrawCustomPanels(); + + ImGui::EndTable(); + } + } + ImGui::End(); + + // Batch operations popup + DrawBatchOperationsPopup(); +} + +void PaletteGroupCard::DrawToolbar() { + bool has_changes = HasUnsavedChanges(); + + // Save button (primary action) + ImGui::BeginDisabled(!has_changes); + if (PrimaryButton(absl::StrFormat("%s Save to ROM", ICON_MD_SAVE).c_str())) { + auto status = SaveToRom(); + if (!status.ok()) { + // TODO: Show error toast + } + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + + // Discard button (danger action) + ImGui::BeginDisabled(!has_changes); + if (DangerButton(absl::StrFormat("%s Discard", ICON_MD_UNDO).c_str())) { + DiscardChanges(); + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + + // Modified indicator badge + if (has_changes) { + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), + "%s %zu modified", + ICON_MD_EDIT, + modified_palettes_.size()); + } + + ImGui::SameLine(); + ImGui::Dummy(ImVec2(20, 0)); // Spacer + ImGui::SameLine(); + + // Undo/Redo + ImGui::BeginDisabled(!CanUndo()); + if (ThemedIconButton(ICON_MD_UNDO, "Undo")) { + Undo(); + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + ImGui::BeginDisabled(!CanRedo()); + if (ThemedIconButton(ICON_MD_REDO, "Redo")) { + Redo(); + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + ImGui::Dummy(ImVec2(20, 0)); // Spacer + ImGui::SameLine(); + + // Export/Import + if (ThemedIconButton(ICON_MD_FILE_DOWNLOAD, "Export to clipboard")) { + ExportToClipboard(); + } + + ImGui::SameLine(); + if (ThemedIconButton(ICON_MD_FILE_UPLOAD, "Import from clipboard")) { + ImportFromClipboard(); + } + + ImGui::SameLine(); + if (ThemedIconButton(ICON_MD_MORE_VERT, "Batch operations")) { + ImGui::OpenPopup("BatchOperations"); + } + + // Custom toolbar buttons from derived classes + DrawCustomToolbarButtons(); +} + +void PaletteGroupCard::DrawPaletteSelector() { + auto* palette_group = GetPaletteGroup(); + if (!palette_group) return; + + int num_palettes = palette_group->size(); + + ImGui::Text("Palette:"); + ImGui::SameLine(); + + ImGui::SetNextItemWidth(LayoutHelpers::GetStandardInputWidth()); + if (ImGui::BeginCombo("##PaletteSelect", + absl::StrFormat("Palette %d", selected_palette_).c_str())) { + for (int i = 0; i < num_palettes; i++) { + bool is_selected = (selected_palette_ == i); + bool is_modified = IsPaletteModified(i); + + std::string label = absl::StrFormat("Palette %d", i); + if (is_modified) { + label += " *"; + } + + if (ImGui::Selectable(label.c_str(), is_selected)) { + selected_palette_ = i; + selected_color_ = -1; // Reset color selection + } + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + // Show reset button for current palette + ImGui::SameLine(); + ImGui::BeginDisabled(!IsPaletteModified(selected_palette_)); + if (ThemedIconButton(ICON_MD_RESTORE, "Reset palette to original")) { + ResetPalette(selected_palette_); + } + ImGui::EndDisabled(); +} + +void PaletteGroupCard::DrawColorPicker() { + if (selected_color_ < 0) return; + + auto* palette = GetMutablePalette(selected_palette_); + if (!palette) return; + + SectionHeader("Color Editor"); + + auto& color = (*palette)[selected_color_]; + auto original = GetOriginalColor(selected_palette_, selected_color_); + + // Color picker with hue wheel + ImVec4 col = ConvertSnesColorToImVec4(editing_color_); + if (ImGui::ColorPicker4("##picker", &col.x, + ImGuiColorEditFlags_NoAlpha | + ImGuiColorEditFlags_PickerHueWheel | + ImGuiColorEditFlags_DisplayRGB | + ImGuiColorEditFlags_DisplayHSV)) { + editing_color_ = ConvertImVec4ToSnesColor(col); + SetColor(selected_palette_, selected_color_, editing_color_); + } + + // Current vs Original comparison + ImGui::Separator(); + ImGui::Text("Current vs Original"); + + ImGui::ColorButton("##current", col, + ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoPicker, + ImVec2(60, 40)); + + LayoutHelpers::HelpMarker("Current color being edited"); + + ImGui::SameLine(); + + ImVec4 orig_col = ConvertSnesColorToImVec4(original); + if (ImGui::ColorButton("##original", orig_col, + ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoPicker, + ImVec2(60, 40))) { + // Click to restore original + editing_color_ = original; + SetColor(selected_palette_, selected_color_, original); + } + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Click to restore original color"); + } + + // Reset button + ImGui::BeginDisabled(!IsColorModified(selected_palette_, selected_color_)); + if (ThemedButton(absl::StrFormat("%s Reset", ICON_MD_RESTORE).c_str(), + ImVec2(-1, 0))) { + ResetColor(selected_palette_, selected_color_); + } + ImGui::EndDisabled(); +} + +void PaletteGroupCard::DrawColorInfo() { + if (selected_color_ < 0) return; + + SectionHeader("Color Information"); + + auto col = editing_color_.rgb(); + int r = static_cast(col.x); + int g = static_cast(col.y); + int b = static_cast(col.z); + + // RGB values + ImGui::Text("RGB (0-255): (%d, %d, %d)", r, g, b); + if (ImGui::IsItemClicked()) { + ImGui::SetClipboardText(absl::StrFormat("(%d, %d, %d)", r, g, b).c_str()); + } + + // SNES BGR555 value + if (show_snes_format_) { + ImGui::Text("SNES BGR555: $%04X", editing_color_.snes()); + if (ImGui::IsItemClicked()) { + ImGui::SetClipboardText(absl::StrFormat("$%04X", editing_color_.snes()).c_str()); + } + } + + // Hex value + if (show_hex_format_) { + ImGui::Text("Hex: #%02X%02X%02X", r, g, b); + if (ImGui::IsItemClicked()) { + ImGui::SetClipboardText(absl::StrFormat("#%02X%02X%02X", r, g, b).c_str()); + } + } + + ImGui::TextDisabled("Click any value to copy"); +} + +void PaletteGroupCard::DrawMetadataInfo() { + const auto& metadata = GetMetadata(); + if (selected_palette_ >= metadata.palettes.size()) return; + + const auto& pal_meta = metadata.palettes[selected_palette_]; + + SectionHeader("Palette Metadata"); + + // Palette ID + ImGui::Text("Palette ID: %d", pal_meta.palette_id); + + // Name + if (!pal_meta.name.empty()) { + ImGui::Text("Name: %s", pal_meta.name.c_str()); + } + + // Description + if (!pal_meta.description.empty()) { + ImGui::TextWrapped("%s", pal_meta.description.c_str()); + } + + // ROM Address + ImGui::Text("ROM Address: $%06X", pal_meta.rom_address); + if (ImGui::IsItemClicked()) { + ImGui::SetClipboardText(absl::StrFormat("$%06X", pal_meta.rom_address).c_str()); + } + + // VRAM Address (if applicable) + if (pal_meta.vram_address > 0) { + ImGui::Text("VRAM Address: $%04X", pal_meta.vram_address); + if (ImGui::IsItemClicked()) { + ImGui::SetClipboardText(absl::StrFormat("$%04X", pal_meta.vram_address).c_str()); + } + } + + // Usage notes + if (!pal_meta.usage_notes.empty()) { + ImGui::Separator(); + ImGui::TextDisabled("Usage Notes:"); + ImGui::TextWrapped("%s", pal_meta.usage_notes.c_str()); + } +} + +void PaletteGroupCard::DrawBatchOperationsPopup() { + if (ImGui::BeginPopup("BatchOperations")) { + SectionHeader("Batch Operations"); + + if (ThemedButton("Copy Current Palette", ImVec2(-1, 0))) { + ExportToClipboard(); + ImGui::CloseCurrentPopup(); + } + + if (ThemedButton("Paste to Current Palette", ImVec2(-1, 0))) { + ImportFromClipboard(); + ImGui::CloseCurrentPopup(); + } + + ImGui::Separator(); + + if (ThemedButton("Reset All Palettes", ImVec2(-1, 0))) { + DiscardChanges(); + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } +} + +// ========== Palette Operations ========== + +void PaletteGroupCard::SetColor(int palette_index, int color_index, + const gfx::SnesColor& new_color) { + auto* palette = GetMutablePalette(palette_index); + if (!palette) return; + + auto original_color = (*palette)[color_index]; + + // Update in-memory palette + (*palette)[color_index] = new_color; + + // Track modification + MarkModified(palette_index, color_index); + + // Record for undo + auto now = std::chrono::system_clock::now(); + auto timestamp = std::chrono::duration_cast( + now.time_since_epoch()) + .count(); + + undo_stack_.push_back( + {palette_index, color_index, original_color, new_color, static_cast(timestamp)}); + + // Limit undo history + if (undo_stack_.size() > kMaxUndoHistory) { + undo_stack_.erase(undo_stack_.begin()); + } + + redo_stack_.clear(); + + // Auto-save if enabled + if (auto_save_enabled_) { + WriteColorToRom(palette_index, color_index, new_color); + } +} + +absl::Status PaletteGroupCard::SaveToRom() { + auto* palette_group = GetPaletteGroup(); + if (!palette_group) { + return absl::NotFoundError("Palette group not found"); + } + + // Save each modified palette + for (int palette_idx : modified_palettes_) { + auto* palette = palette_group->mutable_palette(palette_idx); + + // Write each modified color in this palette + for (int color_idx : modified_colors_[palette_idx]) { + RETURN_IF_ERROR(WriteColorToRom(palette_idx, color_idx, (*palette)[color_idx])); + } + } + + // Clear modified flags after successful save + modified_palettes_.clear(); + modified_colors_.clear(); + + // Update original palettes + original_palettes_.clear(); + for (size_t i = 0; i < palette_group->size(); i++) { + original_palettes_.push_back(palette_group->palette(i)); + } + + rom_->set_dirty(true); + return absl::OkStatus(); +} + +void PaletteGroupCard::DiscardChanges() { + auto* palette_group = GetPaletteGroup(); + if (!palette_group) return; + + // Restore all palettes from original + for (int palette_idx : modified_palettes_) { + if (palette_idx < original_palettes_.size()) { + *palette_group->mutable_palette(palette_idx) = original_palettes_[palette_idx]; + } + } + + // Clear modified tracking + modified_palettes_.clear(); + modified_colors_.clear(); + + // Clear undo/redo + ClearHistory(); + + // Reset selection + selected_color_ = -1; +} + +void PaletteGroupCard::ResetPalette(int palette_index) { + auto* palette_group = GetPaletteGroup(); + if (!palette_group || palette_index >= original_palettes_.size()) { + return; + } + + // Restore from original + *palette_group->mutable_palette(palette_index) = original_palettes_[palette_index]; + + // Clear modified flags for this palette + ClearModified(palette_index); +} + +void PaletteGroupCard::ResetColor(int palette_index, int color_index) { + auto original = GetOriginalColor(palette_index, color_index); + SetColor(palette_index, color_index, original); + + // Remove from modified tracking + if (modified_colors_.contains(palette_index)) { + modified_colors_[palette_index].erase(color_index); + if (modified_colors_[palette_index].empty()) { + modified_palettes_.erase(palette_index); + } + } +} + +// ========== History Management ========== + +void PaletteGroupCard::Undo() { + if (!CanUndo()) return; + + auto change = undo_stack_.back(); + undo_stack_.pop_back(); + + // Restore original color + auto* palette = GetMutablePalette(change.palette_index); + if (palette) { + (*palette)[change.color_index] = change.original_color; + } + + // Update ROM if auto-save enabled + if (auto_save_enabled_) { + WriteColorToRom(change.palette_index, change.color_index, change.original_color); + } + + // Move to redo stack + redo_stack_.push_back(change); +} + +void PaletteGroupCard::Redo() { + if (!CanRedo()) return; + + auto change = redo_stack_.back(); + redo_stack_.pop_back(); + + // Reapply new color + auto* palette = GetMutablePalette(change.palette_index); + if (palette) { + (*palette)[change.color_index] = change.new_color; + } + + // Update ROM if auto-save enabled + if (auto_save_enabled_) { + WriteColorToRom(change.palette_index, change.color_index, change.new_color); + } + + // Move back to undo stack + undo_stack_.push_back(change); +} + +void PaletteGroupCard::ClearHistory() { + undo_stack_.clear(); + redo_stack_.clear(); +} + +// ========== State Queries ========== + +bool PaletteGroupCard::IsPaletteModified(int palette_index) const { + return modified_palettes_.contains(palette_index); +} + +bool PaletteGroupCard::IsColorModified(int palette_index, int color_index) const { + auto it = modified_colors_.find(palette_index); + if (it == modified_colors_.end()) { + return false; + } + return it->second.contains(color_index); +} + +// ========== Helper Methods ========== + +gfx::SnesPalette* PaletteGroupCard::GetMutablePalette(int index) { + auto* palette_group = GetPaletteGroup(); + if (!palette_group || index < 0 || index >= palette_group->size()) { + return nullptr; + } + return palette_group->mutable_palette(index); +} + +gfx::SnesColor PaletteGroupCard::GetOriginalColor(int palette_index, + int color_index) const { + if (palette_index >= original_palettes_.size()) { + return gfx::SnesColor(); + } + return original_palettes_[palette_index][color_index]; +} + +absl::Status PaletteGroupCard::WriteColorToRom(int palette_index, int color_index, + const gfx::SnesColor& color) { + uint32_t address = gfx::GetPaletteAddress(group_name_, palette_index, color_index); + return rom_->WriteColor(address, color); +} + +void PaletteGroupCard::MarkModified(int palette_index, int color_index) { + modified_palettes_.insert(palette_index); + modified_colors_[palette_index].insert(color_index); +} + +void PaletteGroupCard::ClearModified(int palette_index) { + modified_palettes_.erase(palette_index); + modified_colors_.erase(palette_index); +} + +// ========== Export/Import ========== + +std::string PaletteGroupCard::ExportToJson() const { + // TODO: Implement JSON export + return "{}"; +} + +absl::Status PaletteGroupCard::ImportFromJson(const std::string& json) { + // TODO: Implement JSON import + return absl::UnimplementedError("Import from JSON not yet implemented"); +} + +std::string PaletteGroupCard::ExportToClipboard() const { + auto* palette_group = GetPaletteGroup(); + if (!palette_group || selected_palette_ >= palette_group->size()) { + return ""; + } + + auto palette = palette_group->palette(selected_palette_); + std::string result; + + for (size_t i = 0; i < palette.size(); i++) { + result += absl::StrFormat("$%04X", palette[i].snes()); + if (i < palette.size() - 1) { + result += ","; + } + } + + ImGui::SetClipboardText(result.c_str()); + return result; +} + +absl::Status PaletteGroupCard::ImportFromClipboard() { + // TODO: Implement clipboard import + return absl::UnimplementedError("Import from clipboard not yet implemented"); +} + +// ============================================================================ +// Concrete Palette Card Implementations +// ============================================================================ + +// ========== Overworld Main Palette Card ========== + +const PaletteGroupMetadata OverworldMainPaletteCard::metadata_ = + OverworldMainPaletteCard::InitializeMetadata(); + +OverworldMainPaletteCard::OverworldMainPaletteCard(Rom* rom) + : PaletteGroupCard("ow_main", "Overworld Main Palettes", rom) {} + +PaletteGroupMetadata OverworldMainPaletteCard::InitializeMetadata() { + PaletteGroupMetadata metadata; + metadata.group_name = "ow_main"; + metadata.display_name = "Overworld Main Palettes"; + metadata.colors_per_palette = 8; + metadata.colors_per_row = 8; + + // Light World palettes (0-19) + for (int i = 0; i < 20; i++) { + PaletteMetadata pal; + pal.palette_id = i; + pal.name = absl::StrFormat("Light World %d", i); + pal.description = "Used for Light World overworld graphics"; + pal.rom_address = 0xDE6C8 + (i * 16); // Base address + offset + pal.vram_address = 0; + pal.usage_notes = "Modifying these colors affects Light World appearance"; + metadata.palettes.push_back(pal); + } + + // Dark World palettes (20-39) + for (int i = 20; i < 40; i++) { + PaletteMetadata pal; + pal.palette_id = i; + pal.name = absl::StrFormat("Dark World %d", i - 20); + pal.description = "Used for Dark World overworld graphics"; + pal.rom_address = 0xDE6C8 + (i * 16); + pal.vram_address = 0; + pal.usage_notes = "Modifying these colors affects Dark World appearance"; + metadata.palettes.push_back(pal); + } + + // Special World palettes (40-59) + for (int i = 40; i < 60; i++) { + PaletteMetadata pal; + pal.palette_id = i; + pal.name = absl::StrFormat("Special %d", i - 40); + pal.description = "Used for Special World and triforce room"; + pal.rom_address = 0xDE6C8 + (i * 16); + pal.vram_address = 0; + pal.usage_notes = "Modifying these colors affects Special World areas"; + metadata.palettes.push_back(pal); + } + + return metadata; +} + +gfx::PaletteGroup* OverworldMainPaletteCard::GetPaletteGroup() { + return rom_->mutable_palette_group()->get_group("ow_main"); +} + +const gfx::PaletteGroup* OverworldMainPaletteCard::GetPaletteGroup() const { + // Note: rom_->palette_group() returns by value, so we need to use the mutable version + return const_cast(rom_)->mutable_palette_group()->get_group("ow_main"); +} + +void OverworldMainPaletteCard::DrawPaletteGrid() { + auto* palette = GetMutablePalette(selected_palette_); + if (!palette) return; + + const float button_size = 32.0f; + const int colors_per_row = GetColorsPerRow(); + + for (int i = 0; i < palette->size(); i++) { + bool is_selected = (i == selected_color_); + bool is_modified = IsColorModified(selected_palette_, i); + + ImGui::PushID(i); + + if (PaletteColorButton(absl::StrFormat("##color%d", i).c_str(), + (*palette)[i], is_selected, is_modified, + ImVec2(button_size, button_size))) { + selected_color_ = i; + editing_color_ = (*palette)[i]; + } + + ImGui::PopID(); + + // Wrap to next row + if ((i + 1) % colors_per_row != 0 && i + 1 < palette->size()) { + ImGui::SameLine(); + } + } +} + +// ========== Overworld Animated Palette Card ========== + +const PaletteGroupMetadata OverworldAnimatedPaletteCard::metadata_ = + OverworldAnimatedPaletteCard::InitializeMetadata(); + +OverworldAnimatedPaletteCard::OverworldAnimatedPaletteCard(Rom* rom) + : PaletteGroupCard("ow_animated", "Overworld Animated Palettes", rom) {} + +PaletteGroupMetadata OverworldAnimatedPaletteCard::InitializeMetadata() { + PaletteGroupMetadata metadata; + metadata.group_name = "ow_animated"; + metadata.display_name = "Overworld Animated Palettes"; + metadata.colors_per_palette = 8; + metadata.colors_per_row = 8; + + // Animated palettes + const char* anim_names[] = {"Water", "Lava", "Poison Water", "Ice"}; + for (int i = 0; i < 4; i++) { + PaletteMetadata pal; + pal.palette_id = i; + pal.name = anim_names[i]; + pal.description = absl::StrFormat("%s animated palette cycle", anim_names[i]); + pal.rom_address = 0xDE86C + (i * 16); + pal.vram_address = 0; + pal.usage_notes = "These palettes cycle through multiple frames for animation"; + metadata.palettes.push_back(pal); + } + + return metadata; +} + +gfx::PaletteGroup* OverworldAnimatedPaletteCard::GetPaletteGroup() { + return rom_->mutable_palette_group()->get_group("ow_animated"); +} + +const gfx::PaletteGroup* OverworldAnimatedPaletteCard::GetPaletteGroup() const { + return const_cast(rom_)->mutable_palette_group()->get_group("ow_animated"); +} + +void OverworldAnimatedPaletteCard::DrawPaletteGrid() { + auto* palette = GetMutablePalette(selected_palette_); + if (!palette) return; + + const float button_size = 32.0f; + const int colors_per_row = GetColorsPerRow(); + + for (int i = 0; i < palette->size(); i++) { + bool is_selected = (i == selected_color_); + bool is_modified = IsColorModified(selected_palette_, i); + + ImGui::PushID(i); + + if (PaletteColorButton(absl::StrFormat("##color%d", i).c_str(), + (*palette)[i], is_selected, is_modified, + ImVec2(button_size, button_size))) { + selected_color_ = i; + editing_color_ = (*palette)[i]; + } + + ImGui::PopID(); + + if ((i + 1) % colors_per_row != 0 && i + 1 < palette->size()) { + ImGui::SameLine(); + } + } +} + +// ========== Dungeon Main Palette Card ========== + +const PaletteGroupMetadata DungeonMainPaletteCard::metadata_ = + DungeonMainPaletteCard::InitializeMetadata(); + +DungeonMainPaletteCard::DungeonMainPaletteCard(Rom* rom) + : PaletteGroupCard("dungeon_main", "Dungeon Main Palettes", rom) {} + +PaletteGroupMetadata DungeonMainPaletteCard::InitializeMetadata() { + PaletteGroupMetadata metadata; + metadata.group_name = "dungeon_main"; + metadata.display_name = "Dungeon Main Palettes"; + metadata.colors_per_palette = 16; + metadata.colors_per_row = 16; + + // Dungeon palettes (0-19) + const char* dungeon_names[] = { + "Sewers", "Hyrule Castle", "Eastern Palace", "Desert Palace", + "Agahnim's Tower", "Swamp Palace", "Palace of Darkness", "Misery Mire", + "Skull Woods", "Ice Palace", "Tower of Hera", "Thieves' Town", + "Turtle Rock", "Ganon's Tower", "Generic 1", "Generic 2", + "Generic 3", "Generic 4", "Generic 5", "Generic 6" + }; + + for (int i = 0; i < 20; i++) { + PaletteMetadata pal; + pal.palette_id = i; + pal.name = dungeon_names[i]; + pal.description = absl::StrFormat("Dungeon palette %d", i); + pal.rom_address = 0xDE604 + (i * 32); + pal.vram_address = 0; + pal.usage_notes = "16 colors per dungeon palette"; + metadata.palettes.push_back(pal); + } + + return metadata; +} + +gfx::PaletteGroup* DungeonMainPaletteCard::GetPaletteGroup() { + return rom_->mutable_palette_group()->get_group("dungeon_main"); +} + +const gfx::PaletteGroup* DungeonMainPaletteCard::GetPaletteGroup() const { + return const_cast(rom_)->mutable_palette_group()->get_group("dungeon_main"); +} + +void DungeonMainPaletteCard::DrawPaletteGrid() { + auto* palette = GetMutablePalette(selected_palette_); + if (!palette) return; + + const float button_size = 28.0f; + const int colors_per_row = GetColorsPerRow(); + + for (int i = 0; i < palette->size(); i++) { + bool is_selected = (i == selected_color_); + bool is_modified = IsColorModified(selected_palette_, i); + + ImGui::PushID(i); + + if (PaletteColorButton(absl::StrFormat("##color%d", i).c_str(), + (*palette)[i], is_selected, is_modified, + ImVec2(button_size, button_size))) { + selected_color_ = i; + editing_color_ = (*palette)[i]; + } + + ImGui::PopID(); + + if ((i + 1) % colors_per_row != 0 && i + 1 < palette->size()) { + ImGui::SameLine(); + } + } +} + +// ========== Sprite Palette Card ========== + +const PaletteGroupMetadata SpritePaletteCard::metadata_ = + SpritePaletteCard::InitializeMetadata(); + +SpritePaletteCard::SpritePaletteCard(Rom* rom) + : PaletteGroupCard("sprites", "Sprite Palettes", rom) {} + +PaletteGroupMetadata SpritePaletteCard::InitializeMetadata() { + PaletteGroupMetadata metadata; + metadata.group_name = "sprites"; + metadata.display_name = "Sprite Palettes"; + metadata.colors_per_palette = 8; + metadata.colors_per_row = 8; + + // Global sprite palettes (0-3) + const char* sprite_names[] = { + "Green Mail Sprite", "Blue Mail Sprite", "Red Mail Sprite", "Gold Armor" + }; + + for (int i = 0; i < 4; i++) { + PaletteMetadata pal; + pal.palette_id = i; + pal.name = sprite_names[i]; + pal.description = "Global sprite palette"; + pal.rom_address = 0xDD218 + (i * 16); + pal.vram_address = 0x8D00 + (i * 16); // VRAM sprite palette area + pal.usage_notes = "Used by sprites throughout the game"; + metadata.palettes.push_back(pal); + } + + // Auxiliary sprite palettes (4-5) + for (int i = 4; i < 6; i++) { + PaletteMetadata pal; + pal.palette_id = i; + pal.name = absl::StrFormat("Auxiliary %d", i - 4); + pal.description = "Auxiliary sprite palette"; + pal.rom_address = 0xDD218 + (i * 16); + pal.vram_address = 0x8D00 + (i * 16); + pal.usage_notes = "Used by specific sprites"; + metadata.palettes.push_back(pal); + } + + return metadata; +} + +gfx::PaletteGroup* SpritePaletteCard::GetPaletteGroup() { + return rom_->mutable_palette_group()->get_group("sprites"); +} + +const gfx::PaletteGroup* SpritePaletteCard::GetPaletteGroup() const { + return const_cast(rom_)->mutable_palette_group()->get_group("sprites"); +} + +void SpritePaletteCard::DrawPaletteGrid() { + auto* palette = GetMutablePalette(selected_palette_); + if (!palette) return; + + const float button_size = 32.0f; + const int colors_per_row = GetColorsPerRow(); + + for (int i = 0; i < palette->size(); i++) { + bool is_selected = (i == selected_color_); + bool is_modified = IsColorModified(selected_palette_, i); + + ImGui::PushID(i); + + if (PaletteColorButton(absl::StrFormat("##color%d", i).c_str(), + (*palette)[i], is_selected, is_modified, + ImVec2(button_size, button_size))) { + selected_color_ = i; + editing_color_ = (*palette)[i]; + } + + ImGui::PopID(); + + if ((i + 1) % colors_per_row != 0 && i + 1 < palette->size()) { + ImGui::SameLine(); + } + } +} + +void SpritePaletteCard::DrawCustomPanels() { + // Show VRAM info panel + SectionHeader("VRAM Information"); + + const auto& metadata = GetMetadata(); + if (selected_palette_ < metadata.palettes.size()) { + const auto& pal_meta = metadata.palettes[selected_palette_]; + + ImGui::TextWrapped("This sprite palette is loaded to VRAM address $%04X", + pal_meta.vram_address); + ImGui::TextDisabled("VRAM palettes are used by the SNES PPU for sprite rendering"); + } +} + +// ========== Equipment Palette Card ========== + +const PaletteGroupMetadata EquipmentPaletteCard::metadata_ = + EquipmentPaletteCard::InitializeMetadata(); + +EquipmentPaletteCard::EquipmentPaletteCard(Rom* rom) + : PaletteGroupCard("armor", "Equipment Palettes", rom) {} + +PaletteGroupMetadata EquipmentPaletteCard::InitializeMetadata() { + PaletteGroupMetadata metadata; + metadata.group_name = "armor"; + metadata.display_name = "Equipment Palettes"; + metadata.colors_per_palette = 8; + metadata.colors_per_row = 8; + + const char* armor_names[] = {"Green Mail", "Blue Mail", "Red Mail"}; + + for (int i = 0; i < 3; i++) { + PaletteMetadata pal; + pal.palette_id = i; + pal.name = armor_names[i]; + pal.description = absl::StrFormat("Link's %s colors", armor_names[i]); + pal.rom_address = 0xDD308 + (i * 16); + pal.vram_address = 0; + pal.usage_notes = "Changes Link's tunic appearance"; + metadata.palettes.push_back(pal); + } + + return metadata; +} + +gfx::PaletteGroup* EquipmentPaletteCard::GetPaletteGroup() { + return rom_->mutable_palette_group()->get_group("armor"); +} + +const gfx::PaletteGroup* EquipmentPaletteCard::GetPaletteGroup() const { + return const_cast(rom_)->mutable_palette_group()->get_group("armor"); +} + +void EquipmentPaletteCard::DrawPaletteGrid() { + auto* palette = GetMutablePalette(selected_palette_); + if (!palette) return; + + const float button_size = 32.0f; + const int colors_per_row = GetColorsPerRow(); + + for (int i = 0; i < palette->size(); i++) { + bool is_selected = (i == selected_color_); + bool is_modified = IsColorModified(selected_palette_, i); + + ImGui::PushID(i); + + if (PaletteColorButton(absl::StrFormat("##color%d", i).c_str(), + (*palette)[i], is_selected, is_modified, + ImVec2(button_size, button_size))) { + selected_color_ = i; + editing_color_ = (*palette)[i]; + } + + ImGui::PopID(); + + if ((i + 1) % colors_per_row != 0 && i + 1 < palette->size()) { + ImGui::SameLine(); + } + } +} + +} // namespace editor +} // namespace yaze diff --git a/src/app/editor/palette/palette_group_card.h b/src/app/editor/palette/palette_group_card.h new file mode 100644 index 00000000..d1f9d551 --- /dev/null +++ b/src/app/editor/palette/palette_group_card.h @@ -0,0 +1,403 @@ +#ifndef YAZE_APP_EDITOR_GRAPHICS_PALETTE_GROUP_CARD_H +#define YAZE_APP_EDITOR_GRAPHICS_PALETTE_GROUP_CARD_H + +#include +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "app/gfx/snes_color.h" +#include "app/gfx/snes_palette.h" +#include "app/gui/editor_card_manager.h" +#include "app/rom.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace editor { + +/** + * @brief Represents a single color change for undo/redo + */ +struct ColorChange { + int palette_index; + int color_index; + gfx::SnesColor original_color; + gfx::SnesColor new_color; + uint64_t timestamp; // For history ordering +}; + +/** + * @brief Metadata for a single palette in a group + */ +struct PaletteMetadata { + int palette_id; // Palette ID in ROM + std::string name; // Display name (e.g., "Light World Main") + std::string description; // Usage description + uint32_t rom_address; // Base ROM address for this palette + uint32_t vram_address; // VRAM address (for sprite palettes, 0 if N/A) + std::string usage_notes; // Additional usage information +}; + +/** + * @brief Metadata for an entire palette group + */ +struct PaletteGroupMetadata { + std::string group_name; // Internal group name + std::string display_name; // Display name for UI + std::vector palettes; // Metadata for each palette + int colors_per_palette; // Number of colors per palette (usually 8 or 16) + int colors_per_row; // Colors per row for grid layout +}; + +/** + * @brief Base class for palette group editing cards + * + * Provides common functionality for all palette group editors: + * - ROM persistence with transaction-based writes + * - Undo/redo history management + * - Modified state tracking with visual indicators + * - Save/discard workflow + * - Common toolbar and color picker UI + * - EditorCardManager integration + * + * Derived classes implement specific grid layouts and palette access. + */ +class PaletteGroupCard { + public: + /** + * @brief Construct a new Palette Group Card + * @param group_name Internal palette group name (e.g., "ow_main", "dungeon_main") + * @param display_name Human-readable name for UI + * @param rom ROM instance for reading/writing palettes + */ + PaletteGroupCard(const std::string& group_name, + const std::string& display_name, + Rom* rom); + + virtual ~PaletteGroupCard() = default; + + // ========== Main Rendering ========== + + /** + * @brief Draw the card's ImGui UI + */ + void Draw(); + + // ========== Card Control ========== + + void Show() { show_ = true; } + void Hide() { show_ = false; } + bool IsVisible() const { return show_; } + bool* visibility_flag() { return &show_; } + + // ========== Palette Operations ========== + + /** + * @brief Save all modified palettes to ROM + */ + absl::Status SaveToRom(); + + /** + * @brief Discard all unsaved changes + */ + void DiscardChanges(); + + /** + * @brief Reset a specific palette to original ROM values + */ + void ResetPalette(int palette_index); + + /** + * @brief Reset a specific color to original ROM value + */ + void ResetColor(int palette_index, int color_index); + + /** + * @brief Set a color value (records change for undo) + */ + void SetColor(int palette_index, int color_index, const gfx::SnesColor& new_color); + + // ========== History Management ========== + + void Undo(); + void Redo(); + bool CanUndo() const { return !undo_stack_.empty(); } + bool CanRedo() const { return !redo_stack_.empty(); } + void ClearHistory(); + + // ========== State Queries ========== + + bool HasUnsavedChanges() const { return !modified_palettes_.empty(); } + bool IsPaletteModified(int palette_index) const; + bool IsColorModified(int palette_index, int color_index) const; + + int GetSelectedPaletteIndex() const { return selected_palette_; } + void SetSelectedPaletteIndex(int index) { selected_palette_ = index; } + + int GetSelectedColorIndex() const { return selected_color_; } + void SetSelectedColorIndex(int index) { selected_color_ = index; } + + // ========== Export/Import ========== + + std::string ExportToJson() const; + absl::Status ImportFromJson(const std::string& json); + + std::string ExportToClipboard() const; + absl::Status ImportFromClipboard(); + + protected: + // ========== Pure Virtual Methods (Implemented by Derived Classes) ========== + + /** + * @brief Get the palette group for this card + */ + virtual gfx::PaletteGroup* GetPaletteGroup() = 0; + virtual const gfx::PaletteGroup* GetPaletteGroup() const = 0; + + /** + * @brief Get metadata for this palette group + */ + virtual const PaletteGroupMetadata& GetMetadata() const = 0; + + /** + * @brief Draw the palette grid specific to this palette type + */ + virtual void DrawPaletteGrid() = 0; + + /** + * @brief Get the number of colors per row for grid layout + */ + virtual int GetColorsPerRow() const = 0; + + // ========== Optional Overrides ========== + + /** + * @brief Draw additional toolbar buttons (called after standard buttons) + */ + virtual void DrawCustomToolbarButtons() {} + + /** + * @brief Draw additional panels (called after main content) + */ + virtual void DrawCustomPanels() {} + + // ========== Common UI Components ========== + + /** + * @brief Draw standard toolbar with save/discard/undo/redo + */ + void DrawToolbar(); + + /** + * @brief Draw palette selector dropdown + */ + void DrawPaletteSelector(); + + /** + * @brief Draw color picker for selected color + */ + void DrawColorPicker(); + + /** + * @brief Draw color info panel with RGB/SNES/Hex values + */ + void DrawColorInfo(); + + /** + * @brief Draw palette metadata info panel + */ + void DrawMetadataInfo(); + + /** + * @brief Draw batch operations popup + */ + void DrawBatchOperationsPopup(); + + // ========== Helper Methods ========== + + /** + * @brief Get mutable palette by index + */ + gfx::SnesPalette* GetMutablePalette(int index); + + /** + * @brief Get original color from ROM (for reset/comparison) + */ + gfx::SnesColor GetOriginalColor(int palette_index, int color_index) const; + + /** + * @brief Write a single color to ROM + */ + absl::Status WriteColorToRom(int palette_index, int color_index, + const gfx::SnesColor& color); + + /** + * @brief Mark palette as modified + */ + void MarkModified(int palette_index, int color_index); + + /** + * @brief Clear modified flags for palette + */ + void ClearModified(int palette_index); + + // ========== Member Variables ========== + + std::string group_name_; // Internal name (e.g., "ow_main") + std::string display_name_; // Display name (e.g., "Overworld Main") + Rom* rom_; // ROM instance + bool show_ = false; // Visibility flag + + // Selection state + int selected_palette_ = 0; // Currently selected palette index + int selected_color_ = -1; // Currently selected color (-1 = none) + gfx::SnesColor editing_color_; // Color being edited in picker + + // Modified tracking + std::unordered_set modified_palettes_; + std::unordered_map> modified_colors_; + + // Undo/Redo + std::vector undo_stack_; + std::vector redo_stack_; + static constexpr size_t kMaxUndoHistory = 100; + + // Settings + bool auto_save_enabled_ = false; // Auto-save to ROM on every change + bool show_snes_format_ = true; // Show SNES $xxxx format in info + bool show_hex_format_ = true; // Show #xxxxxx hex in info + + // Original palettes (loaded from ROM for reset/comparison) + std::vector original_palettes_; + + // Card registration + gui::CardRegistration card_registration_; +}; + +// ============================================================================ +// Concrete Palette Card Implementations +// ============================================================================ + +/** + * @brief Overworld Main palette group card + * + * Manages palettes used for overworld rendering: + * - Light World palettes (0-19) + * - Dark World palettes (20-39) + * - Special World palettes (40-59) + */ +class OverworldMainPaletteCard : public PaletteGroupCard { + public: + explicit OverworldMainPaletteCard(Rom* rom); + ~OverworldMainPaletteCard() override = default; + + protected: + gfx::PaletteGroup* GetPaletteGroup() override; + const gfx::PaletteGroup* GetPaletteGroup() const override; + const PaletteGroupMetadata& GetMetadata() const override { return metadata_; } + void DrawPaletteGrid() override; + int GetColorsPerRow() const override { return 8; } + + private: + static PaletteGroupMetadata InitializeMetadata(); + static const PaletteGroupMetadata metadata_; +}; + +/** + * @brief Overworld Animated palette group card + * + * Manages animated palettes for water, lava, and other effects + */ +class OverworldAnimatedPaletteCard : public PaletteGroupCard { + public: + explicit OverworldAnimatedPaletteCard(Rom* rom); + ~OverworldAnimatedPaletteCard() override = default; + + protected: + gfx::PaletteGroup* GetPaletteGroup() override; + const gfx::PaletteGroup* GetPaletteGroup() const override; + const PaletteGroupMetadata& GetMetadata() const override { return metadata_; } + void DrawPaletteGrid() override; + int GetColorsPerRow() const override { return 8; } + + private: + static PaletteGroupMetadata InitializeMetadata(); + static const PaletteGroupMetadata metadata_; +}; + +/** + * @brief Dungeon Main palette group card + * + * Manages palettes for dungeon rooms (0-19) + */ +class DungeonMainPaletteCard : public PaletteGroupCard { + public: + explicit DungeonMainPaletteCard(Rom* rom); + ~DungeonMainPaletteCard() override = default; + + protected: + gfx::PaletteGroup* GetPaletteGroup() override; + const gfx::PaletteGroup* GetPaletteGroup() const override; + const PaletteGroupMetadata& GetMetadata() const override { return metadata_; } + void DrawPaletteGrid() override; + int GetColorsPerRow() const override { return 16; } + + private: + static PaletteGroupMetadata InitializeMetadata(); + static const PaletteGroupMetadata metadata_; +}; + +/** + * @brief Sprite palette group card + * + * Manages sprite palettes with VRAM locations + * - Global sprites (palettes 0-3) + * - Auxiliary sprites (palettes 4-5) + */ +class SpritePaletteCard : public PaletteGroupCard { + public: + explicit SpritePaletteCard(Rom* rom); + ~SpritePaletteCard() override = default; + + protected: + gfx::PaletteGroup* GetPaletteGroup() override; + const gfx::PaletteGroup* GetPaletteGroup() const override; + const PaletteGroupMetadata& GetMetadata() const override { return metadata_; } + void DrawPaletteGrid() override; + int GetColorsPerRow() const override { return 8; } + void DrawCustomPanels() override; // Show VRAM info + + private: + static PaletteGroupMetadata InitializeMetadata(); + static const PaletteGroupMetadata metadata_; +}; + +/** + * @brief Equipment/Armor palette group card + * + * Manages Link's equipment color palettes (green, blue, red tunics) + */ +class EquipmentPaletteCard : public PaletteGroupCard { + public: + explicit EquipmentPaletteCard(Rom* rom); + ~EquipmentPaletteCard() override = default; + + protected: + gfx::PaletteGroup* GetPaletteGroup() override; + const gfx::PaletteGroup* GetPaletteGroup() const override; + const PaletteGroupMetadata& GetMetadata() const override { return metadata_; } + void DrawPaletteGrid() override; + int GetColorsPerRow() const override { return 8; } + + private: + static PaletteGroupMetadata InitializeMetadata(); + static const PaletteGroupMetadata metadata_; +}; + +} // namespace editor +} // namespace yaze + +#endif // YAZE_APP_EDITOR_GRAPHICS_PALETTE_GROUP_CARD_H diff --git a/src/app/gui/themed_widgets.cc b/src/app/gui/themed_widgets.cc index 59a6a2d6..2954bb7a 100644 --- a/src/app/gui/themed_widgets.cc +++ b/src/app/gui/themed_widgets.cc @@ -1,14 +1,16 @@ #include "app/gui/themed_widgets.h" +#include "app/gui/color.h" +#include "app/gfx/snes_color.h" + namespace yaze { namespace gui { -namespace themed { // ============================================================================ // Buttons // ============================================================================ -bool Button(const char* label, const ImVec2& size) { +bool ThemedButton(const char* label, const ImVec2& size) { const auto& theme = GetTheme(); ImGui::PushStyleColor(ImGuiCol_Button, ConvertColorToImVec4(theme.button)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ConvertColorToImVec4(theme.button_hovered)); @@ -20,13 +22,13 @@ bool Button(const char* label, const ImVec2& size) { return result; } -bool IconButton(const char* icon, const char* tooltip) { - bool result = Button(icon, ImVec2(LayoutHelpers::GetStandardWidgetHeight(), - LayoutHelpers::GetStandardWidgetHeight())); +bool ThemedIconButton(const char* icon, const char* tooltip) { + bool result = ThemedButton(icon, ImVec2(LayoutHelpers::GetStandardWidgetHeight(), + LayoutHelpers::GetStandardWidgetHeight())); if (tooltip && ImGui::IsItemHovered()) { - BeginTooltip(); + BeginThemedTooltip(); ImGui::Text("%s", tooltip); - EndTooltip(); + EndThemedTooltip(); } return result; } @@ -67,11 +69,11 @@ bool DangerButton(const char* label, const ImVec2& size) { // Headers & Sections // ============================================================================ -void Header(const char* label) { +void SectionHeader(const char* label) { LayoutHelpers::SectionHeader(label); } -bool CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) { +bool ThemedCollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) { const auto& theme = GetTheme(); ImGui::PushStyleColor(ImGuiCol_Header, ConvertColorToImVec4(theme.header)); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ConvertColorToImVec4(theme.header_hovered)); @@ -87,13 +89,13 @@ bool CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) { // Cards & Panels // ============================================================================ -void Card(const char* label, std::function content, const ImVec2& size) { - BeginPanel(label, size); +void ThemedCard(const char* label, std::function content, const ImVec2& size) { + BeginThemedPanel(label, size); content(); - EndPanel(); + EndThemedPanel(); } -void BeginPanel(const char* label, const ImVec2& size) { +void BeginThemedPanel(const char* label, const ImVec2& size) { const auto& theme = GetTheme(); ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.surface)); @@ -105,7 +107,7 @@ void BeginPanel(const char* label, const ImVec2& size) { ImGui::BeginChild(label, size, true); } -void EndPanel() { +void EndThemedPanel() { ImGui::EndChild(); ImGui::PopStyleVar(2); ImGui::PopStyleColor(1); @@ -115,8 +117,8 @@ void EndPanel() { // Inputs // ============================================================================ -bool InputText(const char* label, char* buf, size_t buf_size, - ImGuiInputTextFlags flags) { +bool ThemedInputText(const char* label, char* buf, size_t buf_size, + ImGuiInputTextFlags flags) { const auto& theme = GetTheme(); ImGui::PushStyleColor(ImGuiCol_FrameBg, ConvertColorToImVec4(theme.frame_bg)); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ConvertColorToImVec4(theme.frame_bg_hovered)); @@ -129,8 +131,8 @@ bool InputText(const char* label, char* buf, size_t buf_size, return result; } -bool InputInt(const char* label, int* v, int step, int step_fast, - ImGuiInputTextFlags flags) { +bool ThemedInputInt(const char* label, int* v, int step, int step_fast, + ImGuiInputTextFlags flags) { const auto& theme = GetTheme(); ImGui::PushStyleColor(ImGuiCol_FrameBg, ConvertColorToImVec4(theme.frame_bg)); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ConvertColorToImVec4(theme.frame_bg_hovered)); @@ -143,8 +145,8 @@ bool InputInt(const char* label, int* v, int step, int step_fast, return result; } -bool InputFloat(const char* label, float* v, float step, float step_fast, - const char* format, ImGuiInputTextFlags flags) { +bool ThemedInputFloat(const char* label, float* v, float step, float step_fast, + const char* format, ImGuiInputTextFlags flags) { const auto& theme = GetTheme(); ImGui::PushStyleColor(ImGuiCol_FrameBg, ConvertColorToImVec4(theme.frame_bg)); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ConvertColorToImVec4(theme.frame_bg_hovered)); @@ -157,7 +159,7 @@ bool InputFloat(const char* label, float* v, float step, float step_fast, return result; } -bool Checkbox(const char* label, bool* v) { +bool ThemedCheckbox(const char* label, bool* v) { const auto& theme = GetTheme(); ImGui::PushStyleColor(ImGuiCol_CheckMark, ConvertColorToImVec4(theme.check_mark)); @@ -167,8 +169,8 @@ bool Checkbox(const char* label, bool* v) { return result; } -bool Combo(const char* label, int* current_item, const char* const items[], - int items_count, int popup_max_height_in_items) { +bool ThemedCombo(const char* label, int* current_item, const char* const items[], + int items_count, int popup_max_height_in_items) { const auto& theme = GetTheme(); ImGui::PushStyleColor(ImGuiCol_FrameBg, ConvertColorToImVec4(theme.frame_bg)); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ConvertColorToImVec4(theme.frame_bg_hovered)); @@ -186,12 +188,12 @@ bool Combo(const char* label, int* current_item, const char* const items[], // Tables // ============================================================================ -bool BeginTable(const char* str_id, int columns, ImGuiTableFlags flags, - const ImVec2& outer_size, float inner_width) { +bool BeginThemedTable(const char* str_id, int columns, ImGuiTableFlags flags, + const ImVec2& outer_size, float inner_width) { return LayoutHelpers::BeginTableWithTheming(str_id, columns, flags, outer_size, inner_width); } -void EndTable() { +void EndThemedTable() { LayoutHelpers::EndTable(); } @@ -199,17 +201,17 @@ void EndTable() { // Tooltips & Help // ============================================================================ -void HelpMarker(const char* desc) { +void ThemedHelpMarker(const char* desc) { LayoutHelpers::HelpMarker(desc); } -void BeginTooltip() { +void BeginThemedTooltip() { const auto& theme = GetTheme(); ImGui::PushStyleColor(ImGuiCol_PopupBg, ConvertColorToImVec4(theme.popup_bg)); ImGui::BeginTooltip(); } -void EndTooltip() { +void EndThemedTooltip() { ImGui::EndTooltip(); ImGui::PopStyleColor(1); } @@ -218,7 +220,7 @@ void EndTooltip() { // Status & Feedback // ============================================================================ -void StatusText(const char* text, StatusType type) { +void ThemedStatusText(const char* text, StatusType type) { const auto& theme = GetTheme(); ImVec4 color; @@ -240,7 +242,7 @@ void StatusText(const char* text, StatusType type) { ImGui::TextColored(color, "%s", text); } -void ProgressBar(float fraction, const ImVec2& size, const char* overlay) { +void ThemedProgressBar(float fraction, const ImVec2& size, const char* overlay) { const auto& theme = GetTheme(); ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ConvertColorToImVec4(theme.accent)); @@ -249,11 +251,117 @@ void ProgressBar(float fraction, const ImVec2& size, const char* overlay) { ImGui::PopStyleColor(1); } +// ============================================================================ +// Palette Editor Widgets +// ============================================================================ + +bool PaletteColorButton(const char* label, const yaze::gfx::SnesColor& color, + bool is_selected, bool is_modified, + const ImVec2& size) { + const auto& theme = GetTheme(); + + int style_count = 0; + + // Draw modified indicator with warning border + if (is_modified) { + ImGui::PushStyleColor(ImGuiCol_Border, ConvertColorToImVec4(theme.warning)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f); + style_count++; + } + + // Draw selection border (overrides modified if both) + if (is_selected) { + ImGui::PushStyleColor(ImGuiCol_Border, ConvertColorToImVec4(theme.accent)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 3.0f); + if (is_modified) { + ImGui::PopStyleVar(); // Remove modified border style + ImGui::PopStyleColor(); // Remove modified border color + } + style_count = 1; // Override count + } + + // Convert SNES color to ImGui format + ImVec4 col = ConvertSnesColorToImVec4(color); + + // Draw color button + bool clicked = ImGui::ColorButton(label, col, + ImGuiColorEditFlags_NoAlpha | + ImGuiColorEditFlags_NoPicker | + ImGuiColorEditFlags_NoTooltip, + size); + + // Cleanup styles + if (style_count > 0) { + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + } + + return clicked; +} + +void ColorInfoPanel(const yaze::gfx::SnesColor& color, + bool show_snes_format, + bool show_hex_format) { + auto col = color.rgb(); + int r = static_cast(col.x); + int g = static_cast(col.y); + int b = static_cast(col.z); + + // RGB values + ImGui::Text("RGB (0-255):"); + ImGui::SameLine(); + ImGui::Text("(%d, %d, %d)", r, g, b); + if (ImGui::IsItemClicked()) { + char buf[64]; + snprintf(buf, sizeof(buf), "(%d, %d, %d)", r, g, b); + ImGui::SetClipboardText(buf); + } + + // SNES BGR555 value + if (show_snes_format) { + ImGui::Text("SNES BGR555:"); + ImGui::SameLine(); + ImGui::Text("$%04X", color.snes()); + if (ImGui::IsItemClicked()) { + char buf[16]; + snprintf(buf, sizeof(buf), "$%04X", color.snes()); + ImGui::SetClipboardText(buf); + } + } + + // Hex value + if (show_hex_format) { + ImGui::Text("Hex:"); + ImGui::SameLine(); + ImGui::Text("#%02X%02X%02X", r, g, b); + if (ImGui::IsItemClicked()) { + char buf[16]; + snprintf(buf, sizeof(buf), "#%02X%02X%02X", r, g, b); + ImGui::SetClipboardText(buf); + } + } + + ImGui::TextDisabled("(Click any value to copy)"); +} + +void ModifiedBadge(bool is_modified, const char* text) { + if (!is_modified) return; + + const auto& theme = GetTheme(); + ImVec4 color = ConvertColorToImVec4(theme.warning); + + if (text) { + ImGui::TextColored(color, "%s", text); + } else { + ImGui::TextColored(color, "Modified"); + } +} + // ============================================================================ // Utility // ============================================================================ -void PushWidgetColors() { +void PushThemedWidgetColors() { const auto& theme = GetTheme(); ImGui::PushStyleColor(ImGuiCol_FrameBg, ConvertColorToImVec4(theme.frame_bg)); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ConvertColorToImVec4(theme.frame_bg_hovered)); @@ -263,10 +371,9 @@ void PushWidgetColors() { ImGui::PushStyleColor(ImGuiCol_ButtonActive, ConvertColorToImVec4(theme.button_active)); } -void PopWidgetColors() { +void PopThemedWidgetColors() { ImGui::PopStyleColor(6); } -} // namespace themed } // namespace gui } // namespace yaze diff --git a/src/app/gui/themed_widgets.h b/src/app/gui/themed_widgets.h index dc38debf..3ac71241 100644 --- a/src/app/gui/themed_widgets.h +++ b/src/app/gui/themed_widgets.h @@ -10,27 +10,26 @@ namespace yaze { namespace gui { /** - * @brief Opt-in themed widget library for gradual migration + * @brief Theme-aware widget library * - * All widgets automatically use the current theme from ThemeManager. - * Editors can opt-in by using these widgets instead of raw ImGui calls. + * All widgets in this file automatically use the current theme from ThemeManager. + * These are drop-in replacements for standard ImGui widgets with automatic theming. * * Usage: * ```cpp - * using namespace yaze::gui::themed; + * using namespace yaze::gui; * - * if (Button("Save")) { + * if (ThemedButton("Save")) { * // Button uses theme colors automatically * } * - * Header("Settings"); // Themed section header + * SectionHeader("Settings"); // Themed section header * - * Card("Properties", [&]() { + * ThemedCard("Properties", [&]() { * // Content inside themed card * }); * ``` */ -namespace themed { // ============================================================================ // Buttons @@ -39,12 +38,12 @@ namespace themed { /** * @brief Themed button with automatic color application */ -bool Button(const char* label, const ImVec2& size = ImVec2(0, 0)); +bool ThemedButton(const char* label, const ImVec2& size = ImVec2(0, 0)); /** * @brief Themed button with icon (Material Design Icons) */ -bool IconButton(const char* icon, const char* tooltip = nullptr); +bool ThemedIconButton(const char* icon, const char* tooltip = nullptr); /** * @brief Primary action button (uses accent color) @@ -63,12 +62,12 @@ bool DangerButton(const char* label, const ImVec2& size = ImVec2(0, 0)); /** * @brief Themed section header with accent color */ -void Header(const char* label); +void SectionHeader(const char* label); /** * @brief Collapsible section with themed header */ -bool CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags = 0); +bool ThemedCollapsingHeader(const char* label, ImGuiTreeNodeFlags flags = 0); // ============================================================================ // Cards & Panels @@ -80,18 +79,18 @@ bool CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags = 0); * @param content Callback function to render card content * @param size Card size (0, 0 for auto-size) */ -void Card(const char* label, std::function content, - const ImVec2& size = ImVec2(0, 0)); +void ThemedCard(const char* label, std::function content, + const ImVec2& size = ImVec2(0, 0)); /** - * @brief Begin themed panel (manual version of Card) + * @brief Begin themed panel (manual version of ThemedCard) */ -void BeginPanel(const char* label, const ImVec2& size = ImVec2(0, 0)); +void BeginThemedPanel(const char* label, const ImVec2& size = ImVec2(0, 0)); /** * @brief End themed panel */ -void EndPanel(); +void EndThemedPanel(); // ============================================================================ // Inputs @@ -100,32 +99,32 @@ void EndPanel(); /** * @brief Themed text input */ -bool InputText(const char* label, char* buf, size_t buf_size, - ImGuiInputTextFlags flags = 0); +bool ThemedInputText(const char* label, char* buf, size_t buf_size, + ImGuiInputTextFlags flags = 0); /** * @brief Themed integer input */ -bool InputInt(const char* label, int* v, int step = 1, int step_fast = 100, - ImGuiInputTextFlags flags = 0); +bool ThemedInputInt(const char* label, int* v, int step = 1, int step_fast = 100, + ImGuiInputTextFlags flags = 0); /** * @brief Themed float input */ -bool InputFloat(const char* label, float* v, float step = 0.0f, - float step_fast = 0.0f, const char* format = "%.3f", - ImGuiInputTextFlags flags = 0); +bool ThemedInputFloat(const char* label, float* v, float step = 0.0f, + float step_fast = 0.0f, const char* format = "%.3f", + ImGuiInputTextFlags flags = 0); /** * @brief Themed checkbox */ -bool Checkbox(const char* label, bool* v); +bool ThemedCheckbox(const char* label, bool* v); /** * @brief Themed combo box */ -bool Combo(const char* label, int* current_item, const char* const items[], - int items_count, int popup_max_height_in_items = -1); +bool ThemedCombo(const char* label, int* current_item, const char* const items[], + int items_count, int popup_max_height_in_items = -1); // ============================================================================ // Tables @@ -134,14 +133,14 @@ bool Combo(const char* label, int* current_item, const char* const items[], /** * @brief Begin themed table with automatic styling */ -bool BeginTable(const char* str_id, int columns, ImGuiTableFlags flags = 0, - const ImVec2& outer_size = ImVec2(0, 0), - float inner_width = 0.0f); +bool BeginThemedTable(const char* str_id, int columns, ImGuiTableFlags flags = 0, + const ImVec2& outer_size = ImVec2(0, 0), + float inner_width = 0.0f); /** * @brief End themed table */ -void EndTable(); +void EndThemedTable(); // ============================================================================ // Tooltips & Help @@ -150,17 +149,17 @@ void EndTable(); /** * @brief Themed help marker with tooltip */ -void HelpMarker(const char* desc); +void ThemedHelpMarker(const char* desc); /** * @brief Begin themed tooltip */ -void BeginTooltip(); +void BeginThemedTooltip(); /** * @brief End themed tooltip */ -void EndTooltip(); +void EndThemedTooltip(); // ============================================================================ // Status & Feedback @@ -170,13 +169,47 @@ enum class StatusType { kSuccess, kWarning, kError, kInfo }; /** * @brief Themed status text (success, warning, error, info) */ -void StatusText(const char* text, StatusType type); +void ThemedStatusText(const char* text, StatusType type); /** * @brief Themed progress bar */ -void ProgressBar(float fraction, const ImVec2& size = ImVec2(-1, 0), - const char* overlay = nullptr); +void ThemedProgressBar(float fraction, const ImVec2& size = ImVec2(-1, 0), + const char* overlay = nullptr); + +// ============================================================================ +// Palette Editor Widgets +// ============================================================================ + +/** + * @brief Palette color button with modified and selection indicators + * @param label Widget ID + * @param color SNES color to display + * @param is_selected Whether this color is currently selected + * @param is_modified Whether this color has unsaved changes + * @param size Button size (default 24x24) + * @return true if clicked + */ +bool PaletteColorButton(const char* label, const yaze::gfx::SnesColor& color, + bool is_selected, bool is_modified, + const ImVec2& size = ImVec2(24, 24)); + +/** + * @brief Display color information with copy-to-clipboard functionality + * @param color SNES color to display info for + * @param show_snes_format Show SNES $xxxx format + * @param show_hex_format Show #xxxxxx hex format + */ +void ColorInfoPanel(const yaze::gfx::SnesColor& color, + bool show_snes_format = true, + bool show_hex_format = true); + +/** + * @brief Modified indicator badge (displayed as text with icon) + * @param is_modified Whether to show the badge + * @param text Optional text to display after badge + */ +void ModifiedBadge(bool is_modified, const char* text = nullptr); // ============================================================================ // Utility @@ -192,14 +225,13 @@ inline const EnhancedTheme& GetTheme() { /** * @brief Apply theme colors to next widget */ -void PushWidgetColors(); +void PushThemedWidgetColors(); /** * @brief Restore previous colors */ -void PopWidgetColors(); +void PopThemedWidgetColors(); -} // namespace themed } // namespace gui } // namespace yaze