From 312522d7092ccb344e1e0ee42a1469d8fa33b4e0 Mon Sep 17 00:00:00 2001 From: scawful Date: Sun, 12 Oct 2025 11:57:39 -0400 Subject: [PATCH] feat(editor): enhance EditorManager with card-based editor functionality - Introduced methods to determine if an editor is card-based and to retrieve its category. - Implemented a sidebar toggle shortcut (Ctrl+B) for managing card visibility. - Added functionality to hide current editor cards when switching editors. - Updated editor initialization to register global shortcuts for activating specific editors. - Enhanced the Update method to draw a sidebar for the current category editor. Benefits: - Improved user experience by organizing editor cards and providing quick access to editor categories. - Streamlined editor management, making it easier to switch between different editing contexts. --- src/app/editor/dungeon/dungeon_editor_v2.cc | 39 +- src/app/editor/editor.h | 1 + src/app/editor/editor_manager.cc | 111 ++- src/app/editor/editor_manager.h | 10 + src/app/editor/palette/palette_editor.cc | 787 +++++++++++++++---- src/app/editor/palette/palette_editor.h | 44 +- src/app/editor/palette/palette_group_card.cc | 381 +++++++-- src/app/editor/palette/palette_group_card.h | 80 +- src/app/editor/palette/palette_utility.cc | 147 ++++ src/app/editor/palette/palette_utility.h | 78 ++ src/app/gfx/snes_palette.h | 65 +- src/app/gui/color.cc | 49 ++ src/app/gui/color.h | 6 + src/app/gui/editor_card_manager.cc | 149 +++- src/app/gui/editor_card_manager.h | 23 + src/app/gui/themed_widgets.cc | 45 +- src/app/gui/themed_widgets.h | 13 +- src/zelda3/palette_constants.cc | 76 ++ src/zelda3/palette_constants.h | 310 ++++++++ src/zelda3/palette_structure.md | 142 ++++ 20 files changed, 2213 insertions(+), 343 deletions(-) create mode 100644 src/app/editor/palette/palette_utility.cc create mode 100644 src/app/editor/palette/palette_utility.h create mode 100644 src/zelda3/palette_constants.cc create mode 100644 src/zelda3/palette_constants.h create mode 100644 src/zelda3/palette_structure.md diff --git a/src/app/editor/dungeon/dungeon_editor_v2.cc b/src/app/editor/dungeon/dungeon_editor_v2.cc index fb9a9f7a..b8126073 100644 --- a/src/app/editor/dungeon/dungeon_editor_v2.cc +++ b/src/app/editor/dungeon/dungeon_editor_v2.cc @@ -236,42 +236,9 @@ absl::Status DungeonEditorV2::Save() { } void DungeonEditorV2::DrawToolset() { - static gui::Toolset toolbar; - toolbar.Begin(); - - if (toolbar.AddAction(ICON_MD_ADD, "Open Room")) { - OnRoomSelected(room_selector_.current_room_id()); - } - - toolbar.AddSeparator(); - - if (toolbar.AddToggle(ICON_MD_LIST, &show_room_selector_, "Toggle Room Selector")) { - // Toggled - } - - if (toolbar.AddToggle(ICON_MD_GRID_VIEW, &show_room_matrix_, "Toggle Room Matrix")) { - // Toggled - } - - if (toolbar.AddToggle(ICON_MD_DOOR_FRONT, &show_entrances_list_, "Toggle Entrances List")) { - // Toggled - } - - if (toolbar.AddToggle(ICON_MD_IMAGE, &show_room_graphics_, "Toggle Room Graphics")) { - // Toggled - } - - toolbar.AddSeparator(); - - if (toolbar.AddToggle(ICON_MD_CONSTRUCTION, &show_object_editor_, "Toggle Object Editor")) { - // Toggled - } - - if (toolbar.AddToggle(ICON_MD_PALETTE, &show_palette_editor_, "Toggle Palette Editor")) { - // Toggled - } - - toolbar.End(); + // Draw VSCode-style sidebar using EditorCardManager + // auto& card_manager = gui::EditorCardManager::Get(); + // card_manager.DrawSidebar("Dungeon"); } void DungeonEditorV2::DrawControlPanel() { diff --git a/src/app/editor/editor.h b/src/app/editor/editor.h index 389d27d9..0c1e91fe 100644 --- a/src/app/editor/editor.h +++ b/src/app/editor/editor.h @@ -113,6 +113,7 @@ class Editor { bool* active() { return &active_; } void set_active(bool active) { active_ = active; } + void toggle_active() { active_ = !active_; } // ROM loading state helpers (default implementations) virtual bool IsRomLoaded() const { return false; } diff --git a/src/app/editor/editor_manager.cc b/src/app/editor/editor_manager.cc index 07ab8eee..7b4c55be 100644 --- a/src/app/editor/editor_manager.cc +++ b/src/app/editor/editor_manager.cc @@ -90,6 +90,59 @@ std::string GetEditorName(EditorType type) { } // namespace +// Static registry of editors that use the card-based layout system +bool EditorManager::IsCardBasedEditor(EditorType type) { + switch (type) { + case EditorType::kDungeon: + case EditorType::kPalette: + case EditorType::kGraphics: + case EditorType::kScreen: + case EditorType::kSprite: + case EditorType::kMessage: + case EditorType::kMusic: + case EditorType::kEmulator: + return true; + default: + return false; + } +} + +std::string EditorManager::GetEditorCategory(EditorType type) { + switch (type) { + case EditorType::kDungeon: + return "Dungeon"; + case EditorType::kPalette: + return "Palette"; + case EditorType::kGraphics: + return "Graphics"; + case EditorType::kOverworld: + return "Overworld"; + case EditorType::kSprite: + return "Sprite"; + case EditorType::kMessage: + return "Message"; + case EditorType::kMusic: + return "Music"; + case EditorType::kScreen: + return "Screen"; + case EditorType::kEmulator: + return "Emulator"; + default: + return "Unknown"; + } +} + +void EditorManager::HideCurrentEditorCards() { + if (!current_editor_) { + return; + } + + auto& card_manager = gui::EditorCardManager::Get(); + std::string category = GetEditorCategory(current_editor_->type()); + card_manager.HideAllCardsInCategory(category); + printf("[EditorManager] Closed all cards for category: %s\n", category.c_str()); +} + EditorManager::EditorManager() : blank_editor_set_(nullptr, &user_settings_) { std::stringstream ss; ss << YAZE_VERSION_MAJOR << "." << YAZE_VERSION_MINOR << "." @@ -163,6 +216,12 @@ void EditorManager::Initialize(gfx::IRenderer* renderer, const std::string& file // Set the popup manager in the context context_.popup_manager = popup_manager_.get(); + // Register global sidebar toggle shortcut (Ctrl+B) + context_.shortcut_manager.RegisterShortcut( + "global.toggle_sidebar", + {ImGuiKey_LeftCtrl, ImGuiKey_B}, + [this]() { show_card_sidebar_ = !show_card_sidebar_; }); + // Initialize project file editor project_file_editor_.SetToastManager(&toast_manager_); @@ -356,7 +415,7 @@ void EditorManager::Initialize(gfx::IRenderer* renderer, const std::string& file // Handle agent editor separately (doesn't require ROM) if (type == EditorType::kAgent) { #ifdef YAZE_WITH_GRPC - agent_editor_.set_active(true); + agent_editor_.toggle_active(); #endif return; } @@ -365,28 +424,28 @@ void EditorManager::Initialize(gfx::IRenderer* renderer, const std::string& file switch (type) { case EditorType::kOverworld: - current_editor_set_->overworld_editor_.set_active(true); + current_editor_set_->overworld_editor_.toggle_active(); break; case EditorType::kDungeon: - current_editor_set_->dungeon_editor_.set_active(true); + current_editor_set_->dungeon_editor_.toggle_active(); break; case EditorType::kGraphics: - current_editor_set_->graphics_editor_.set_active(true); + current_editor_set_->graphics_editor_.toggle_active(); break; case EditorType::kSprite: - current_editor_set_->sprite_editor_.set_active(true); + current_editor_set_->sprite_editor_.toggle_active(); break; case EditorType::kMessage: - current_editor_set_->message_editor_.set_active(true); + current_editor_set_->message_editor_.toggle_active(); break; case EditorType::kMusic: - current_editor_set_->music_editor_.set_active(true); + current_editor_set_->music_editor_.toggle_active(); break; case EditorType::kPalette: - current_editor_set_->palette_editor_.set_active(true); + current_editor_set_->palette_editor_.toggle_active(); break; case EditorType::kScreen: - current_editor_set_->screen_editor_.set_active(true); + current_editor_set_->screen_editor_.toggle_active(); break; case EditorType::kAssembly: show_asm_editor_ = true; @@ -395,7 +454,7 @@ void EditorManager::Initialize(gfx::IRenderer* renderer, const std::string& file show_emulator_ = true; break; case EditorType::kSettings: - current_editor_set_->settings_editor_.set_active(true); + current_editor_set_->settings_editor_.toggle_active(); break; default: break; @@ -481,34 +540,34 @@ void EditorManager::Initialize(gfx::IRenderer* renderer, const std::string& file // Editor shortcuts (Ctrl+1-9, Ctrl+0) context_.shortcut_manager.RegisterShortcut( "Overworld Editor", {ImGuiKey_1, ImGuiMod_Ctrl}, - [this]() { if (current_editor_set_) current_editor_set_->overworld_editor_.set_active(true); }); + [this]() { if (current_editor_set_) current_editor_set_->overworld_editor_.toggle_active(); }); context_.shortcut_manager.RegisterShortcut( "Dungeon Editor", {ImGuiKey_2, ImGuiMod_Ctrl}, - [this]() { if (current_editor_set_) current_editor_set_->dungeon_editor_.set_active(true); }); + [this]() { if (current_editor_set_) current_editor_set_->dungeon_editor_.toggle_active(); }); context_.shortcut_manager.RegisterShortcut( "Graphics Editor", {ImGuiKey_3, ImGuiMod_Ctrl}, - [this]() { if (current_editor_set_) current_editor_set_->graphics_editor_.set_active(true); }); + [this]() { if (current_editor_set_) current_editor_set_->graphics_editor_.toggle_active(); }); context_.shortcut_manager.RegisterShortcut( "Sprite Editor", {ImGuiKey_4, ImGuiMod_Ctrl}, - [this]() { if (current_editor_set_) current_editor_set_->sprite_editor_.set_active(true); }); + [this]() { if (current_editor_set_) current_editor_set_->sprite_editor_.toggle_active(); }); context_.shortcut_manager.RegisterShortcut( "Message Editor", {ImGuiKey_5, ImGuiMod_Ctrl}, - [this]() { if (current_editor_set_) current_editor_set_->message_editor_.set_active(true); }); + [this]() { if (current_editor_set_) current_editor_set_->message_editor_.toggle_active(); }); context_.shortcut_manager.RegisterShortcut( "Music Editor", {ImGuiKey_6, ImGuiMod_Ctrl}, - [this]() { if (current_editor_set_) current_editor_set_->music_editor_.set_active(true); }); + [this]() { if (current_editor_set_) current_editor_set_->music_editor_.toggle_active(); }); context_.shortcut_manager.RegisterShortcut( "Palette Editor", {ImGuiKey_7, ImGuiMod_Ctrl}, - [this]() { if (current_editor_set_) current_editor_set_->palette_editor_.set_active(true); }); + [this]() { if (current_editor_set_) current_editor_set_->palette_editor_.toggle_active(); }); context_.shortcut_manager.RegisterShortcut( "Screen Editor", {ImGuiKey_8, ImGuiMod_Ctrl}, - [this]() { if (current_editor_set_) current_editor_set_->screen_editor_.set_active(true); }); + [this]() { if (current_editor_set_) current_editor_set_->screen_editor_.toggle_active(); }); context_.shortcut_manager.RegisterShortcut( "Assembly Editor", {ImGuiKey_9, ImGuiMod_Ctrl}, [this]() { show_asm_editor_ = true; }); context_.shortcut_manager.RegisterShortcut( "Settings Editor", {ImGuiKey_0, ImGuiMod_Ctrl}, - [this]() { if (current_editor_set_) current_editor_set_->settings_editor_.set_active(true); }); + [this]() { if (current_editor_set_) current_editor_set_->settings_editor_.toggle_active(); }); // Editor Selection Dialog shortcut context_.shortcut_manager.RegisterShortcut( @@ -782,8 +841,7 @@ absl::Status EditorManager::Update() { } // CARD-BASED EDITORS: Don't wrap in Begin/End, they manage own windows - bool is_card_based_editor = (editor->type() == EditorType::kDungeon); - // TODO: Add EditorType::kGraphics, EditorType::kPalette when converted + bool is_card_based_editor = IsCardBasedEditor(editor->type()); if (is_card_based_editor) { // Card-based editors create their own top-level windows @@ -854,6 +912,13 @@ absl::Status EditorManager::Update() { } } + // Draw sidebar for current category editor + if (current_editor_) { + std::string category = GetEditorCategory(current_editor_->type()); + auto& card_manager = gui::EditorCardManager::Get(); + card_manager.DrawSidebar(category); + } + if (show_performance_dashboard_) { gfx::PerformanceDashboard::Get().Render(); } @@ -929,8 +994,8 @@ void EditorManager::DrawContextSensitiveCardControl() { category = "Message"; break; case EditorType::kPalette: - // Palette editor doesn't use cards (uses internal tabs) - return; + category = "Palette"; + break; case EditorType::kAssembly: // Assembly editor uses dynamic file tabs return; diff --git a/src/app/editor/editor_manager.h b/src/app/editor/editor_manager.h index 12ea03da..e604b763 100644 --- a/src/app/editor/editor_manager.h +++ b/src/app/editor/editor_manager.h @@ -168,6 +168,15 @@ class EditorManager { void JumpToOverworldMap(int map_id); void SwitchToEditor(EditorType editor_type); + // Card-based editor registry + static bool IsCardBasedEditor(EditorType type); + static std::string GetEditorCategory(EditorType type); + bool IsSidebarVisible() const { return show_card_sidebar_; } + void SetSidebarVisible(bool visible) { show_card_sidebar_ = visible; } + + // Clean up cards when switching editors + void HideCurrentEditorCards(); + // Session management void CreateNewSession(); void DuplicateCurrentSession(); @@ -247,6 +256,7 @@ class EditorManager { bool show_welcome_screen_ = false; bool welcome_screen_manually_closed_ = false; bool show_card_browser_ = false; + bool show_card_sidebar_ = true; // VSCode-style sidebar for editor cards (toggle with Ctrl+B) size_t session_to_rename_ = 0; char session_rename_buffer_[256] = {}; diff --git a/src/app/editor/palette/palette_editor.cc b/src/app/editor/palette/palette_editor.cc index 1159f501..8109ae01 100644 --- a/src/app/editor/palette/palette_editor.cc +++ b/src/app/editor/palette/palette_editor.cc @@ -5,6 +5,9 @@ #include "app/gfx/performance/performance_profiler.h" #include "app/gfx/snes_palette.h" #include "app/gui/color.h" +#include "app/gui/editor_card_manager.h" +#include "app/gui/editor_layout.h" +#include "app/gui/icons.h" #include "imgui/imgui.h" namespace yaze { @@ -16,7 +19,6 @@ using ImGui::BeginDragDropTarget; using ImGui::BeginGroup; using ImGui::BeginPopup; using ImGui::BeginPopupContextItem; -using ImGui::BeginTable; using ImGui::Button; using ImGui::ColorButton; using ImGui::ColorPicker4; @@ -24,8 +26,6 @@ using ImGui::EndChild; using ImGui::EndDragDropTarget; using ImGui::EndGroup; using ImGui::EndPopup; -using ImGui::EndTable; -using ImGui::GetContentRegionAvail; using ImGui::GetStyle; using ImGui::OpenPopup; using ImGui::PopID; @@ -34,10 +34,6 @@ using ImGui::SameLine; using ImGui::Selectable; using ImGui::Separator; using ImGui::SetClipboardText; -using ImGui::TableHeadersRow; -using ImGui::TableNextColumn; -using ImGui::TableNextRow; -using ImGui::TableSetupColumn; using ImGui::Text; using namespace gfx; @@ -187,114 +183,247 @@ 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_); - } + // Register all cards with EditorCardManager (done once during initialization) + auto& card_manager = gui::EditorCardManager::Get(); + + card_manager.RegisterCard({ + .card_id = "palette.control_panel", + .display_name = "Palette Controls", + .icon = ICON_MD_PALETTE, + .category = "Palette", + .shortcut_hint = "Ctrl+Shift+P", + .visibility_flag = &show_control_panel_, + .priority = 10 + }); + + card_manager.RegisterCard({ + .card_id = "palette.ow_main", + .display_name = "Overworld Main", + .icon = ICON_MD_LANDSCAPE, + .category = "Palette", + .shortcut_hint = "Ctrl+Alt+1", + .visibility_flag = &show_ow_main_card_, + .priority = 20 + }); + + card_manager.RegisterCard({ + .card_id = "palette.ow_animated", + .display_name = "Overworld Animated", + .icon = ICON_MD_WATER, + .category = "Palette", + .shortcut_hint = "Ctrl+Alt+2", + .visibility_flag = &show_ow_animated_card_, + .priority = 30 + }); + + card_manager.RegisterCard({ + .card_id = "palette.dungeon_main", + .display_name = "Dungeon Main", + .icon = ICON_MD_CASTLE, + .category = "Palette", + .shortcut_hint = "Ctrl+Alt+3", + .visibility_flag = &show_dungeon_main_card_, + .priority = 40 + }); + + card_manager.RegisterCard({ + .card_id = "palette.sprites", + .display_name = "Global Sprite Palettes", + .icon = ICON_MD_PETS, + .category = "Palette", + .shortcut_hint = "Ctrl+Alt+4", + .visibility_flag = &show_sprite_card_, + .priority = 50 + }); + + card_manager.RegisterCard({ + .card_id = "palette.sprites_aux1", + .display_name = "Sprites Aux 1", + .icon = ICON_MD_FILTER_1, + .category = "Palette", + .shortcut_hint = "Ctrl+Alt+7", + .visibility_flag = &show_sprites_aux1_card_, + .priority = 51 + }); + + card_manager.RegisterCard({ + .card_id = "palette.sprites_aux2", + .display_name = "Sprites Aux 2", + .icon = ICON_MD_FILTER_2, + .category = "Palette", + .shortcut_hint = "Ctrl+Alt+8", + .visibility_flag = &show_sprites_aux2_card_, + .priority = 52 + }); + + card_manager.RegisterCard({ + .card_id = "palette.sprites_aux3", + .display_name = "Sprites Aux 3", + .icon = ICON_MD_FILTER_3, + .category = "Palette", + .shortcut_hint = "Ctrl+Alt+9", + .visibility_flag = &show_sprites_aux3_card_, + .priority = 53 + }); + + card_manager.RegisterCard({ + .card_id = "palette.equipment", + .display_name = "Equipment Palettes", + .icon = ICON_MD_SHIELD, + .category = "Palette", + .shortcut_hint = "Ctrl+Alt+5", + .visibility_flag = &show_equipment_card_, + .priority = 60 + }); + + card_manager.RegisterCard({ + .card_id = "palette.quick_access", + .display_name = "Quick Access", + .icon = ICON_MD_COLOR_LENS, + .category = "Palette", + .shortcut_hint = "Ctrl+Alt+Q", + .visibility_flag = &show_quick_access_, + .priority = 70 + }); + + card_manager.RegisterCard({ + .card_id = "palette.custom", + .display_name = "Custom Palette", + .icon = ICON_MD_BRUSH, + .category = "Palette", + .shortcut_hint = "Ctrl+Alt+C", + .visibility_flag = &show_custom_palette_, + .priority = 80 + }); } absl::Status PaletteEditor::Load() { gfx::ScopedTimer timer("PaletteEditor::Load"); - - if (rom()->is_loaded()) { - // Initialize the labels - for (int i = 0; i < kNumPalettes; i++) { - rom()->resource_label()->CreateOrGetLabel( - "Palette Group Name", std::to_string(i), - std::string(kPaletteGroupNames[i])); - } - } else { + + if (!rom() || !rom()->is_loaded()) { return absl::NotFoundError("ROM not open, no palettes to display"); } + + // Initialize the labels + for (int i = 0; i < kNumPalettes; i++) { + rom()->resource_label()->CreateOrGetLabel( + "Palette Group Name", std::to_string(i), + std::string(kPaletteGroupNames[i])); + } + + // Initialize palette card instances NOW (after 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_); + sprites_aux1_card_ = std::make_unique(rom_); + sprites_aux2_card_ = std::make_unique(rom_); + sprites_aux3_card_ = std::make_unique(rom_); + equipment_card_ = std::make_unique(rom_); + return absl::OkStatus(); } absl::Status PaletteEditor::Update() { - static bool use_legacy_view = false; - - // Toolbar with view selector - if (ImGui::Button(use_legacy_view ? "Switch to Card View" : "Switch to Legacy View")) { - use_legacy_view = !use_legacy_view; + if (!rom_ || !rom_->is_loaded()) { + // Create a minimal loading card + gui::EditorCard loading_card("Palette Editor Loading", ICON_MD_PALETTE); + loading_card.SetDefaultSize(400, 200); + if (loading_card.Begin()) { + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Loading palette data..."); + ImGui::TextWrapped("Palette cards will appear once ROM data is loaded."); + } + loading_card.End(); + return absl::OkStatus(); } - ImGui::SameLine(); - ImGui::TextDisabled("|"); - ImGui::SameLine(); + // CARD-BASED EDITOR: All windows are independent top-level cards + // No parent wrapper - this allows closing control panel without affecting palettes - 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(); + // Optional control panel (can be hidden/minimized) + if (show_control_panel_) { + DrawControlPanel(); + } else if (control_panel_minimized_) { + // Draw floating icon button to reopen + ImGui::SetNextWindowPos(ImVec2(10, 100)); + ImGui::SetNextWindowSize(ImVec2(50, 50)); + ImGuiWindowFlags icon_flags = ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoDocking; - 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; - } + if (ImGui::Begin("##PaletteControlIcon", nullptr, icon_flags)) { + if (ImGui::Button(ICON_MD_PALETTE, ImVec2(40, 40))) { + show_control_panel_ = true; + control_panel_minimized_ = false; } - - 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); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Open Palette Controls"); } - - 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(); } + ImGui::End(); } - // 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(); + // Draw all independent palette cards + // Each card has its own show_ flag that needs to be synced with our visibility flags + if (show_ow_main_card_ && ow_main_card_) { + if (!ow_main_card_->IsVisible()) ow_main_card_->Show(); + ow_main_card_->Draw(); + // Sync back if user closed the card with X button + if (!ow_main_card_->IsVisible()) show_ow_main_card_ = false; + } + + if (show_ow_animated_card_ && ow_animated_card_) { + if (!ow_animated_card_->IsVisible()) ow_animated_card_->Show(); + ow_animated_card_->Draw(); + if (!ow_animated_card_->IsVisible()) show_ow_animated_card_ = false; + } + + if (show_dungeon_main_card_ && dungeon_main_card_) { + if (!dungeon_main_card_->IsVisible()) dungeon_main_card_->Show(); + dungeon_main_card_->Draw(); + if (!dungeon_main_card_->IsVisible()) show_dungeon_main_card_ = false; + } + + if (show_sprite_card_ && sprite_card_) { + if (!sprite_card_->IsVisible()) sprite_card_->Show(); + sprite_card_->Draw(); + if (!sprite_card_->IsVisible()) show_sprite_card_ = false; + } + + if (show_sprites_aux1_card_ && sprites_aux1_card_) { + if (!sprites_aux1_card_->IsVisible()) sprites_aux1_card_->Show(); + sprites_aux1_card_->Draw(); + if (!sprites_aux1_card_->IsVisible()) show_sprites_aux1_card_ = false; + } + + if (show_sprites_aux2_card_ && sprites_aux2_card_) { + if (!sprites_aux2_card_->IsVisible()) sprites_aux2_card_->Show(); + sprites_aux2_card_->Draw(); + if (!sprites_aux2_card_->IsVisible()) show_sprites_aux2_card_ = false; + } + + if (show_sprites_aux3_card_ && sprites_aux3_card_) { + if (!sprites_aux3_card_->IsVisible()) sprites_aux3_card_->Show(); + sprites_aux3_card_->Draw(); + if (!sprites_aux3_card_->IsVisible()) show_sprites_aux3_card_ = false; + } + + if (show_equipment_card_ && equipment_card_) { + if (!equipment_card_->IsVisible()) equipment_card_->Show(); + equipment_card_->Draw(); + if (!equipment_card_->IsVisible()) show_equipment_card_ = false; + } + + // Draw quick access and custom palette cards + if (show_quick_access_) { + DrawQuickAccessCard(); + } + + if (show_custom_palette_) { + DrawCustomPaletteCard(); + } return absl::OkStatus(); } @@ -450,7 +579,7 @@ void PaletteEditor::DrawCustomPalette() { } } -absl::Status PaletteEditor::DrawPaletteGroup(int category, bool right_side) { +absl::Status PaletteEditor::DrawPaletteGroup(int category, bool /*right_side*/) { if (!rom()->is_loaded()) { return absl::NotFoundError("ROM not open, no palettes to display"); } @@ -607,70 +736,416 @@ 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."); +// ============================================================================ +// Card-Based UI Methods +// ============================================================================ - ImGui::Separator(); +void PaletteEditor::DrawToolset() { + // Draw VSCode-style sidebar using EditorCardManager + // auto& card_manager = gui::EditorCardManager::Get(); + // card_manager.DrawSidebar("Palette"); +} - // 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(); +void PaletteEditor::DrawControlPanel() { + ImGui::SetNextWindowSize(ImVec2(320, 420), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(10, 100), ImGuiCond_FirstUseEver); + + ImGuiWindowFlags flags = ImGuiWindowFlags_None; + + if (ImGui::Begin(ICON_MD_PALETTE " Palette Controls", &show_control_panel_, flags)) { + // Toolbar with quick toggles + DrawToolset(); + + ImGui::Separator(); + + // Quick toggle checkboxes in a table + ImGui::Text("Palette Groups:"); + if (ImGui::BeginTable("##PaletteToggles", 2, + ImGuiTableFlags_SizingStretchSame)) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Checkbox("OW Main", &show_ow_main_card_); + ImGui::TableNextColumn(); + ImGui::Checkbox("OW Animated", &show_ow_animated_card_); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Checkbox("Dungeon", &show_dungeon_main_card_); + ImGui::TableNextColumn(); + ImGui::Checkbox("Sprites", &show_sprite_card_); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Checkbox("Equipment", &show_equipment_card_); + ImGui::TableNextColumn(); + // Empty cell + + ImGui::EndTable(); + } + + ImGui::Separator(); + + ImGui::Text("Utilities:"); + if (ImGui::BeginTable("##UtilityToggles", 2, + ImGuiTableFlags_SizingStretchSame)) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Checkbox("Quick Access", &show_quick_access_); + ImGui::TableNextColumn(); + ImGui::Checkbox("Custom", &show_custom_palette_); + + ImGui::EndTable(); + } + + ImGui::Separator(); + + // Modified status indicator + 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("Global Sprite Palettes"); + any_modified = true; + } + if (sprites_aux1_card_ && sprites_aux1_card_->HasUnsavedChanges()) { + ImGui::BulletText("Sprites Aux 1"); + any_modified = true; + } + if (sprites_aux2_card_ && sprites_aux2_card_->HasUnsavedChanges()) { + ImGui::BulletText("Sprites Aux 2"); + any_modified = true; + } + if (sprites_aux3_card_ && sprites_aux3_card_->HasUnsavedChanges()) { + ImGui::BulletText("Sprites Aux 3"); + any_modified = true; + } + if (equipment_card_ && equipment_card_->HasUnsavedChanges()) { + ImGui::BulletText("Equipment Palettes"); + any_modified = true; + } + + if (!any_modified) { + ImGui::TextDisabled("No unsaved changes"); + } + + ImGui::Separator(); + + // Quick actions + ImGui::Text("Quick Actions:"); + if (ImGui::Button("Save All Modified", ImVec2(-1, 0))) { + if (ow_main_card_ && ow_main_card_->HasUnsavedChanges()) { + ow_main_card_->SaveToRom(); + } + if (ow_animated_card_ && ow_animated_card_->HasUnsavedChanges()) { + ow_animated_card_->SaveToRom(); + } + if (dungeon_main_card_ && dungeon_main_card_->HasUnsavedChanges()) { + dungeon_main_card_->SaveToRom(); + } + if (sprite_card_ && sprite_card_->HasUnsavedChanges()) { + sprite_card_->SaveToRom(); + } + if (sprites_aux1_card_ && sprites_aux1_card_->HasUnsavedChanges()) { + sprites_aux1_card_->SaveToRom(); + } + if (sprites_aux2_card_ && sprites_aux2_card_->HasUnsavedChanges()) { + sprites_aux2_card_->SaveToRom(); + } + if (sprites_aux3_card_ && sprites_aux3_card_->HasUnsavedChanges()) { + sprites_aux3_card_->SaveToRom(); + } + if (equipment_card_ && equipment_card_->HasUnsavedChanges()) { + equipment_card_->SaveToRom(); + } + } + + if (ImGui::Button("Discard All Changes", ImVec2(-1, 0))) { + if (ow_main_card_) ow_main_card_->DiscardChanges(); + if (ow_animated_card_) ow_animated_card_->DiscardChanges(); + if (dungeon_main_card_) dungeon_main_card_->DiscardChanges(); + if (sprite_card_) sprite_card_->DiscardChanges(); + if (sprites_aux1_card_) sprites_aux1_card_->DiscardChanges(); + if (sprites_aux2_card_) sprites_aux2_card_->DiscardChanges(); + if (sprites_aux3_card_) sprites_aux3_card_->DiscardChanges(); + if (equipment_card_) equipment_card_->DiscardChanges(); + } + + ImGui::Separator(); + + // Editor Manager Menu Button + if (ImGui::Button(ICON_MD_DASHBOARD " Card Manager", ImVec2(-1, 0))) { + ImGui::OpenPopup("PaletteCardManager"); + } + + if (ImGui::BeginPopup("PaletteCardManager")) { + ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), + "%s Palette Card Manager", ICON_MD_PALETTE); + ImGui::Separator(); + + // Use EditorCardManager to draw the menu + auto& card_manager = gui::EditorCardManager::Get(); + card_manager.DrawViewMenuSection("Palette"); + + ImGui::EndPopup(); + } + + ImGui::Separator(); + + // Minimize button + if (ImGui::SmallButton(ICON_MD_MINIMIZE " Minimize to Icon")) { + control_panel_minimized_ = true; + show_control_panel_ = false; + } } - if (ImGui::Button("Open Overworld Animated", ImVec2(-1, 0))) { - if (ow_animated_card_) ow_animated_card_->Show(); + ImGui::End(); +} + +void PaletteEditor::DrawQuickAccessCard() { + gui::EditorCard card("Quick Access Palette", ICON_MD_COLOR_LENS, + &show_quick_access_); + card.SetDefaultSize(340, 300); + card.SetPosition(gui::EditorCard::Position::Right); + + if (card.Begin(&show_quick_access_)) { + // Current color picker with more options + ImGui::BeginGroup(); + ImGui::Text("Current Color"); + gui::SnesColorEdit4("##CurrentColorPicker", ¤t_color_, + kColorPopupFlags); + + char buf[64]; + auto col = current_color_.rgb(); + int cr = F32_TO_INT8_SAT(col.x / 255.0f); + int cg = F32_TO_INT8_SAT(col.y / 255.0f); + int cb = F32_TO_INT8_SAT(col.z / 255.0f); + + CustomFormatString(buf, IM_ARRAYSIZE(buf), "RGB: %d, %d, %d", cr, cg, cb); + ImGui::Text("%s", buf); + + CustomFormatString(buf, IM_ARRAYSIZE(buf), "SNES: $%04X", + current_color_.snes()); + ImGui::Text("%s", buf); + + if (ImGui::Button("Copy to Clipboard", ImVec2(-1, 0))) { + SetClipboardText(buf); + } + ImGui::EndGroup(); + + ImGui::Separator(); + + // Recently used colors + ImGui::Text("Recently Used Colors"); + if (recently_used_colors_.empty()) { + ImGui::TextDisabled("No recently used colors yet"); + } else { + for (int i = 0; i < recently_used_colors_.size(); i++) { + PushID(i); + if (i % 8 != 0) SameLine(); + ImVec4 displayColor = + gui::ConvertSnesColorToImVec4(recently_used_colors_[i]); + if (ImGui::ColorButton("##recent", displayColor, kPalButtonFlags, + ImVec2(28, 28))) { + // Set as current color + current_color_ = recently_used_colors_[i]; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("SNES: $%04X", recently_used_colors_[i].snes()); + } + PopID(); + } + } + } + card.End(); +} + +void PaletteEditor::DrawCustomPaletteCard() { + gui::EditorCard card("Custom Palette", ICON_MD_BRUSH, + &show_custom_palette_); + card.SetDefaultSize(420, 200); + card.SetPosition(gui::EditorCard::Position::Bottom); + + if (card.Begin(&show_custom_palette_)) { + ImGui::TextWrapped( + "Create your own custom color palette for reference. " + "Colors can be added from any palette group or created from scratch."); + + ImGui::Separator(); + + // Custom palette color grid + if (custom_palette_.empty()) { + ImGui::TextDisabled("Your custom palette is empty."); + ImGui::Text("Click + to add colors or drag colors from any palette."); + } else { + for (int i = 0; i < custom_palette_.size(); i++) { + PushID(i); + if (i > 0 && i % 16 != 0) SameLine(0.0f, 2.0f); + + // Enhanced color button with context menu and drag-drop support + ImVec4 displayColor = gui::ConvertSnesColorToImVec4(custom_palette_[i]); + bool open_color_picker = ImGui::ColorButton( + absl::StrFormat("##customPal%d", i).c_str(), displayColor, + kPalButtonFlags, ImVec2(28, 28)); + + if (open_color_picker) { + current_color_ = custom_palette_[i]; + edit_palette_index_ = i; + ImGui::OpenPopup("CustomPaletteColorEdit"); + } + + if (BeginPopupContextItem()) { + // Edit color directly in the popup + SnesColor original_color = custom_palette_[i]; + if (gui::SnesColorEdit4("Edit Color", &custom_palette_[i], + kColorPopupFlags)) { + // Color was changed, add to recently used + AddRecentlyUsedColor(custom_palette_[i]); + } + + if (ImGui::Button("Delete", ImVec2(-1, 0))) { + custom_palette_.erase(custom_palette_.begin() + i); + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + // Handle drag/drop for palette rearrangement + if (BeginDragDropTarget()) { + if (const ImGuiPayload* payload = + AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F)) { + ImVec4 color; + memcpy((float*)&color, payload->Data, sizeof(float) * 3); + color.w = 1.0f; // Set alpha to 1.0 + custom_palette_[i] = SnesColor(color); + AddRecentlyUsedColor(custom_palette_[i]); + } + EndDragDropTarget(); + } + + PopID(); + } + } + + ImGui::Separator(); + + // Buttons for palette management + if (ImGui::Button(ICON_MD_ADD " Add Color")) { + custom_palette_.push_back(SnesColor(0x7FFF)); + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_MD_DELETE " Clear All")) { + custom_palette_.clear(); + } + + ImGui::SameLine(); + if (ImGui::Button(ICON_MD_CONTENT_COPY " Export")) { + std::string clipboard; + for (const auto& color : custom_palette_) { + clipboard += absl::StrFormat("$%04X,", color.snes()); + } + if (!clipboard.empty()) { + clipboard.pop_back(); // Remove trailing comma + } + SetClipboardText(clipboard.c_str()); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Copy palette as comma-separated SNES values"); + } + } + card.End(); + + // Color picker popup for custom palette editing + if (ImGui::BeginPopup("CustomPaletteColorEdit")) { + if (edit_palette_index_ >= 0 && + edit_palette_index_ < custom_palette_.size()) { + SnesColor original_color = custom_palette_[edit_palette_index_]; + if (gui::SnesColorEdit4( + "Edit Color", &custom_palette_[edit_palette_index_], + kColorPopupFlags | ImGuiColorEditFlags_PickerHueWheel)) { + // Color was changed, add to recently used + AddRecentlyUsedColor(custom_palette_[edit_palette_index_]); + } + } + ImGui::EndPopup(); + } +} + +void PaletteEditor::JumpToPalette(const std::string& group_name, int palette_index) { + // Hide all cards first + show_ow_main_card_ = false; + show_ow_animated_card_ = false; + show_dungeon_main_card_ = false; + show_sprite_card_ = false; + show_sprites_aux1_card_ = false; + show_sprites_aux2_card_ = false; + show_sprites_aux3_card_ = false; + show_equipment_card_ = false; + + // Show and focus the appropriate card + if (group_name == "ow_main") { + show_ow_main_card_ = true; + if (ow_main_card_) { + ow_main_card_->Show(); + ow_main_card_->SetSelectedPaletteIndex(palette_index); + } + } else if (group_name == "ow_animated") { + show_ow_animated_card_ = true; + if (ow_animated_card_) { + ow_animated_card_->Show(); + ow_animated_card_->SetSelectedPaletteIndex(palette_index); + } + } else if (group_name == "dungeon_main") { + show_dungeon_main_card_ = true; + if (dungeon_main_card_) { + dungeon_main_card_->Show(); + dungeon_main_card_->SetSelectedPaletteIndex(palette_index); + } + } else if (group_name == "global_sprites") { + show_sprite_card_ = true; + if (sprite_card_) { + sprite_card_->Show(); + sprite_card_->SetSelectedPaletteIndex(palette_index); + } + } else if (group_name == "sprites_aux1") { + show_sprites_aux1_card_ = true; + if (sprites_aux1_card_) { + sprites_aux1_card_->Show(); + sprites_aux1_card_->SetSelectedPaletteIndex(palette_index); + } + } else if (group_name == "sprites_aux2") { + show_sprites_aux2_card_ = true; + if (sprites_aux2_card_) { + sprites_aux2_card_->Show(); + sprites_aux2_card_->SetSelectedPaletteIndex(palette_index); + } + } else if (group_name == "sprites_aux3") { + show_sprites_aux3_card_ = true; + if (sprites_aux3_card_) { + sprites_aux3_card_->Show(); + sprites_aux3_card_->SetSelectedPaletteIndex(palette_index); + } + } else if (group_name == "armors") { + show_equipment_card_ = true; + if (equipment_card_) { + equipment_card_->Show(); + equipment_card_->SetSelectedPaletteIndex(palette_index); + } } - 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"); - } + // Show control panel too for easy navigation + show_control_panel_ = true; } } // namespace editor diff --git a/src/app/editor/palette/palette_editor.h b/src/app/editor/palette/palette_editor.h index 5cbe577b..b56c2eb0 100644 --- a/src/app/editor/palette/palette_editor.h +++ b/src/app/editor/palette/palette_editor.h @@ -10,7 +10,6 @@ #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" #include "app/rom.h" #include "imgui/imgui.h" @@ -97,24 +96,32 @@ class PaletteEditor : public Editor { absl::Status Find() override { return absl::OkStatus(); } absl::Status Save() override { return absl::UnimplementedError("Save"); } - void DrawQuickAccessTab(); + void set_rom(Rom* rom) { rom_ = rom; } + Rom* rom() const { return rom_; } + /** + * @brief Jump to a specific palette by group and index + * @param group_name The palette group name (e.g., "ow_main", "dungeon_main") + * @param palette_index The palette index within the group + */ + void JumpToPalette(const std::string& group_name, int palette_index); + + private: + void DrawToolset(); + void DrawControlPanel(); + void DrawQuickAccessCard(); + void DrawCustomPaletteCard(); + + // Legacy methods (for backward compatibility if needed) + void DrawQuickAccessTab(); void DrawCustomPalette(); absl::Status DrawPaletteGroup(int category, bool right_side = false); absl::Status EditColorInPalette(gfx::SnesPalette& palette, int index); absl::Status ResetColorToOriginal(gfx::SnesPalette& palette, int index, const gfx::SnesPalette& originalPalette); - void AddRecentlyUsedColor(const gfx::SnesColor& color); - - void set_rom(Rom* rom) { rom_ = rom; } - Rom* rom() const { return rom_; } - - private: absl::Status HandleColorPopup(gfx::SnesPalette& palette, int i, int j, int n); - void DrawPaletteCards(); - absl::Status status_; gfx::SnesColor current_color_; @@ -131,11 +138,28 @@ class PaletteEditor : public Editor { Rom* rom_; + // Card visibility flags (registered with EditorCardManager) + bool show_control_panel_ = true; + bool show_ow_main_card_ = false; + bool show_ow_animated_card_ = false; + bool show_dungeon_main_card_ = false; + bool show_sprite_card_ = false; + bool show_sprites_aux1_card_ = false; + bool show_sprites_aux2_card_ = false; + bool show_sprites_aux3_card_ = false; + bool show_equipment_card_ = false; + bool show_quick_access_ = false; + bool show_custom_palette_ = false; + bool control_panel_minimized_ = false; + // 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 sprites_aux1_card_; + std::unique_ptr sprites_aux2_card_; + std::unique_ptr sprites_aux3_card_; std::unique_ptr equipment_card_; }; diff --git a/src/app/editor/palette/palette_group_card.cc b/src/app/editor/palette/palette_group_card.cc index f707f8c8..adfc11a7 100644 --- a/src/app/editor/palette/palette_group_card.cc +++ b/src/app/editor/palette/palette_group_card.cc @@ -13,7 +13,7 @@ namespace yaze { namespace editor { -using namespace gui; +using namespace yaze::gui; using gui::ThemedButton; using gui::ThemedIconButton; using gui::PrimaryButton; @@ -26,8 +26,18 @@ PaletteGroupCard::PaletteGroupCard(const std::string& group_name, : group_name_(group_name), display_name_(display_name), rom_(rom) { - // Load original palettes from ROM for reset/comparison - if (rom_ && rom_->is_loaded()) { + // Note: We can't call GetPaletteGroup() here because it's a pure virtual function + // and the derived class isn't fully constructed yet. Original palettes will be + // loaded on first Draw() call instead. +} + +void PaletteGroupCard::Draw() { + if (!show_ || !rom_ || !rom_->is_loaded()) { + return; + } + + // Lazy load original palettes on first draw (after derived class is fully constructed) + if (original_palettes_.empty()) { auto* palette_group = GetPaletteGroup(); if (palette_group) { for (size_t i = 0; i < palette_group->size(); i++) { @@ -35,12 +45,6 @@ PaletteGroupCard::PaletteGroupCard(const std::string& group_name, } } } -} - -void PaletteGroupCard::Draw() { - if (!show_ || !rom_ || !rom_->is_loaded()) { - return; - } // Main card window if (ImGui::Begin(display_name_.c_str(), &show_)) { @@ -314,11 +318,27 @@ void PaletteGroupCard::DrawMetadataInfo() { ImGui::TextWrapped("%s", pal_meta.description.c_str()); } + ImGui::Separator(); + + // Palette dimensions and color depth + ImGui::Text("Dimensions: %d colors (%dx%d)", + metadata.colors_per_palette, + metadata.colors_per_row, + (metadata.colors_per_palette + metadata.colors_per_row - 1) / metadata.colors_per_row); + + ImGui::Text("Color Depth: %d BPP (4-bit SNES)", 4); + ImGui::TextDisabled("(16 colors per palette possible)"); + + ImGui::Separator(); + // 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()); } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Click to copy address"); + } // VRAM Address (if applicable) if (pal_meta.vram_address > 0) { @@ -326,6 +346,9 @@ void PaletteGroupCard::DrawMetadataInfo() { if (ImGui::IsItemClicked()) { ImGui::SetClipboardText(absl::StrFormat("$%04X", pal_meta.vram_address).c_str()); } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Click to copy VRAM address"); + } } // Usage notes @@ -580,7 +603,7 @@ std::string PaletteGroupCard::ExportToJson() const { return "{}"; } -absl::Status PaletteGroupCard::ImportFromJson(const std::string& json) { +absl::Status PaletteGroupCard::ImportFromJson(const std::string& /*json*/) { // TODO: Implement JSON import return absl::UnimplementedError("Import from JSON not yet implemented"); } @@ -690,7 +713,7 @@ void OverworldMainPaletteCard::DrawPaletteGrid() { ImGui::PushID(i); - if (PaletteColorButton(absl::StrFormat("##color%d", i).c_str(), + if (yaze::gui::PaletteColorButton(absl::StrFormat("##color%d", i).c_str(), (*palette)[i], is_selected, is_modified, ImVec2(button_size, button_size))) { selected_color_ = i; @@ -758,7 +781,7 @@ void OverworldAnimatedPaletteCard::DrawPaletteGrid() { ImGui::PushID(i); - if (PaletteColorButton(absl::StrFormat("##color%d", i).c_str(), + if (yaze::gui::PaletteColorButton(absl::StrFormat("##color%d", i).c_str(), (*palette)[i], is_selected, is_modified, ImVec2(button_size, button_size))) { selected_color_ = i; @@ -832,7 +855,7 @@ void DungeonMainPaletteCard::DrawPaletteGrid() { ImGui::PushID(i); - if (PaletteColorButton(absl::StrFormat("##color%d", i).c_str(), + if (yaze::gui::PaletteColorButton(absl::StrFormat("##color%d", i).c_str(), (*palette)[i], is_selected, is_modified, ImVec2(button_size, button_size))) { selected_color_ = i; @@ -853,40 +876,30 @@ const PaletteGroupMetadata SpritePaletteCard::metadata_ = SpritePaletteCard::InitializeMetadata(); SpritePaletteCard::SpritePaletteCard(Rom* rom) - : PaletteGroupCard("sprites", "Sprite Palettes", rom) {} + : PaletteGroupCard("global_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; + metadata.group_name = "global_sprites"; + metadata.display_name = "Global Sprite Palettes"; + metadata.colors_per_palette = 60; // 60 colors: 4 rows of 16 colors (with transparent at 0, 16, 32, 48) + metadata.colors_per_row = 16; // Display in 16-color rows - // Global sprite palettes (0-3) + // 2 palette sets: Light World and Dark World const char* sprite_names[] = { - "Green Mail Sprite", "Blue Mail Sprite", "Red Mail Sprite", "Gold Armor" + "Global Sprites (Light World)", + "Global Sprites (Dark World)" }; - for (int i = 0; i < 4; i++) { + for (int i = 0; i < 2; 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"; + pal.description = "60 colors = 4 sprite sub-palettes (rows) with transparent at 0, 16, 32, 48"; + pal.rom_address = (i == 0) ? 0xDD218 : 0xDD290; // LW or DW address + pal.vram_address = 0; // Loaded dynamically + pal.usage_notes = "4 sprite sub-palettes of 15 colors + transparent each. " + "Row 0: colors 0-15, Row 1: 16-31, Row 2: 32-47, Row 3: 48-59"; metadata.palettes.push_back(pal); } @@ -894,18 +907,18 @@ PaletteGroupMetadata SpritePaletteCard::InitializeMetadata() { } gfx::PaletteGroup* SpritePaletteCard::GetPaletteGroup() { - return rom_->mutable_palette_group()->get_group("sprites"); + return rom_->mutable_palette_group()->get_group("global_sprites"); } const gfx::PaletteGroup* SpritePaletteCard::GetPaletteGroup() const { - return const_cast(rom_)->mutable_palette_group()->get_group("sprites"); + return const_cast(rom_)->mutable_palette_group()->get_group("global_sprites"); } void SpritePaletteCard::DrawPaletteGrid() { auto* palette = GetMutablePalette(selected_palette_); if (!palette) return; - const float button_size = 32.0f; + const float button_size = 28.0f; const int colors_per_row = GetColorsPerRow(); for (int i = 0; i < palette->size(); i++) { @@ -914,11 +927,33 @@ void SpritePaletteCard::DrawPaletteGrid() { 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]; + // Draw transparent color indicator at start of each 16-color row (0, 16, 32, 48, ...) + bool is_transparent_slot = (i % 16 == 0); + if (is_transparent_slot) { + ImGui::BeginGroup(); + if (yaze::gui::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]; + } + // Draw "T" for transparent + ImVec2 pos = ImGui::GetItemRectMin(); + ImGui::GetWindowDrawList()->AddText( + ImVec2(pos.x + button_size / 2 - 4, pos.y + button_size / 2 - 8), + IM_COL32(255, 255, 255, 200), "T"); + ImGui::EndGroup(); + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Transparent color slot for sprite sub-palette %d", i / 16); + } + } else { + if (yaze::gui::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(); @@ -949,11 +984,11 @@ const PaletteGroupMetadata EquipmentPaletteCard::metadata_ = EquipmentPaletteCard::InitializeMetadata(); EquipmentPaletteCard::EquipmentPaletteCard(Rom* rom) - : PaletteGroupCard("armor", "Equipment Palettes", rom) {} + : PaletteGroupCard("armors", "Equipment Palettes", rom) {} PaletteGroupMetadata EquipmentPaletteCard::InitializeMetadata() { PaletteGroupMetadata metadata; - metadata.group_name = "armor"; + metadata.group_name = "armors"; metadata.display_name = "Equipment Palettes"; metadata.colors_per_palette = 8; metadata.colors_per_row = 8; @@ -975,11 +1010,11 @@ PaletteGroupMetadata EquipmentPaletteCard::InitializeMetadata() { } gfx::PaletteGroup* EquipmentPaletteCard::GetPaletteGroup() { - return rom_->mutable_palette_group()->get_group("armor"); + return rom_->mutable_palette_group()->get_group("armors"); } const gfx::PaletteGroup* EquipmentPaletteCard::GetPaletteGroup() const { - return const_cast(rom_)->mutable_palette_group()->get_group("armor"); + return const_cast(rom_)->mutable_palette_group()->get_group("armors"); } void EquipmentPaletteCard::DrawPaletteGrid() { @@ -995,7 +1030,7 @@ void EquipmentPaletteCard::DrawPaletteGrid() { ImGui::PushID(i); - if (PaletteColorButton(absl::StrFormat("##color%d", i).c_str(), + if (yaze::gui::PaletteColorButton(absl::StrFormat("##color%d", i).c_str(), (*palette)[i], is_selected, is_modified, ImVec2(button_size, button_size))) { selected_color_ = i; @@ -1010,5 +1045,251 @@ void EquipmentPaletteCard::DrawPaletteGrid() { } } +// ========== Sprites Aux1 Palette Card ========== + +const PaletteGroupMetadata SpritesAux1PaletteCard::metadata_ = + SpritesAux1PaletteCard::InitializeMetadata(); + +SpritesAux1PaletteCard::SpritesAux1PaletteCard(Rom* rom) + : PaletteGroupCard("sprites_aux1", "Sprites Aux 1", rom) {} + +PaletteGroupMetadata SpritesAux1PaletteCard::InitializeMetadata() { + PaletteGroupMetadata metadata; + metadata.group_name = "sprites_aux1"; + metadata.display_name = "Sprites Aux 1"; + metadata.colors_per_palette = 8; // 7 colors + transparent + metadata.colors_per_row = 8; + + for (int i = 0; i < 12; i++) { + PaletteMetadata pal; + pal.palette_id = i; + pal.name = absl::StrFormat("Sprites Aux1 %02d", i); + pal.description = "Auxiliary sprite palette (7 colors + transparent)"; + pal.rom_address = 0xDD39E + (i * 14); // 7 colors * 2 bytes + pal.vram_address = 0; + pal.usage_notes = "Used by specific sprites. Color 0 is transparent."; + metadata.palettes.push_back(pal); + } + + return metadata; +} + +gfx::PaletteGroup* SpritesAux1PaletteCard::GetPaletteGroup() { + return rom_->mutable_palette_group()->get_group("sprites_aux1"); +} + +const gfx::PaletteGroup* SpritesAux1PaletteCard::GetPaletteGroup() const { + return const_cast(rom_)->mutable_palette_group()->get_group("sprites_aux1"); +} + +void SpritesAux1PaletteCard::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); + + // Draw transparent color indicator for index 0 + if (i == 0) { + ImGui::BeginGroup(); + if (yaze::gui::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]; + } + // Draw "T" for transparent + ImVec2 pos = ImGui::GetItemRectMin(); + ImGui::GetWindowDrawList()->AddText( + ImVec2(pos.x + button_size / 2 - 4, pos.y + button_size / 2 - 8), + IM_COL32(255, 255, 255, 200), "T"); + ImGui::EndGroup(); + } else { + if (yaze::gui::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(); + } + } +} + +// ========== Sprites Aux2 Palette Card ========== + +const PaletteGroupMetadata SpritesAux2PaletteCard::metadata_ = + SpritesAux2PaletteCard::InitializeMetadata(); + +SpritesAux2PaletteCard::SpritesAux2PaletteCard(Rom* rom) + : PaletteGroupCard("sprites_aux2", "Sprites Aux 2", rom) {} + +PaletteGroupMetadata SpritesAux2PaletteCard::InitializeMetadata() { + PaletteGroupMetadata metadata; + metadata.group_name = "sprites_aux2"; + metadata.display_name = "Sprites Aux 2"; + metadata.colors_per_palette = 8; // 7 colors + transparent + metadata.colors_per_row = 8; + + for (int i = 0; i < 11; i++) { + PaletteMetadata pal; + pal.palette_id = i; + pal.name = absl::StrFormat("Sprites Aux2 %02d", i); + pal.description = "Auxiliary sprite palette (7 colors + transparent)"; + pal.rom_address = 0xDD446 + (i * 14); // 7 colors * 2 bytes + pal.vram_address = 0; + pal.usage_notes = "Used by specific sprites. Color 0 is transparent."; + metadata.palettes.push_back(pal); + } + + return metadata; +} + +gfx::PaletteGroup* SpritesAux2PaletteCard::GetPaletteGroup() { + return rom_->mutable_palette_group()->get_group("sprites_aux2"); +} + +const gfx::PaletteGroup* SpritesAux2PaletteCard::GetPaletteGroup() const { + return const_cast(rom_)->mutable_palette_group()->get_group("sprites_aux2"); +} + +void SpritesAux2PaletteCard::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); + + // Draw transparent color indicator for index 0 + if (i == 0) { + ImGui::BeginGroup(); + if (yaze::gui::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]; + } + // Draw "T" for transparent + ImVec2 pos = ImGui::GetItemRectMin(); + ImGui::GetWindowDrawList()->AddText( + ImVec2(pos.x + button_size / 2 - 4, pos.y + button_size / 2 - 8), + IM_COL32(255, 255, 255, 200), "T"); + ImGui::EndGroup(); + } else { + if (yaze::gui::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(); + } + } +} + +// ========== Sprites Aux3 Palette Card ========== + +const PaletteGroupMetadata SpritesAux3PaletteCard::metadata_ = + SpritesAux3PaletteCard::InitializeMetadata(); + +SpritesAux3PaletteCard::SpritesAux3PaletteCard(Rom* rom) + : PaletteGroupCard("sprites_aux3", "Sprites Aux 3", rom) {} + +PaletteGroupMetadata SpritesAux3PaletteCard::InitializeMetadata() { + PaletteGroupMetadata metadata; + metadata.group_name = "sprites_aux3"; + metadata.display_name = "Sprites Aux 3"; + metadata.colors_per_palette = 8; // 7 colors + transparent + metadata.colors_per_row = 8; + + for (int i = 0; i < 24; i++) { + PaletteMetadata pal; + pal.palette_id = i; + pal.name = absl::StrFormat("Sprites Aux3 %02d", i); + pal.description = "Auxiliary sprite palette (7 colors + transparent)"; + pal.rom_address = 0xDD4E0 + (i * 14); // 7 colors * 2 bytes + pal.vram_address = 0; + pal.usage_notes = "Used by specific sprites. Color 0 is transparent."; + metadata.palettes.push_back(pal); + } + + return metadata; +} + +gfx::PaletteGroup* SpritesAux3PaletteCard::GetPaletteGroup() { + return rom_->mutable_palette_group()->get_group("sprites_aux3"); +} + +const gfx::PaletteGroup* SpritesAux3PaletteCard::GetPaletteGroup() const { + return const_cast(rom_)->mutable_palette_group()->get_group("sprites_aux3"); +} + +void SpritesAux3PaletteCard::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); + + // Draw transparent color indicator for index 0 + if (i == 0) { + ImGui::BeginGroup(); + if (yaze::gui::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]; + } + // Draw "T" for transparent + ImVec2 pos = ImGui::GetItemRectMin(); + ImGui::GetWindowDrawList()->AddText( + ImVec2(pos.x + button_size / 2 - 4, pos.y + button_size / 2 - 8), + IM_COL32(255, 255, 255, 200), "T"); + ImGui::EndGroup(); + } else { + if (yaze::gui::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 index d1f9d551..3a140e1d 100644 --- a/src/app/editor/palette/palette_group_card.h +++ b/src/app/editor/palette/palette_group_card.h @@ -351,11 +351,12 @@ class DungeonMainPaletteCard : public PaletteGroupCard { }; /** - * @brief Sprite palette group card + * @brief Global Sprite palette group card * - * Manages sprite palettes with VRAM locations - * - Global sprites (palettes 0-3) - * - Auxiliary sprites (palettes 4-5) + * Manages global sprite palettes for Light World and Dark World + * - 2 palettes (LW and DW) + * - Each has 60 colors organized as 4 rows of 16 colors + * - Transparent colors at indices 0, 16, 32, 48 */ class SpritePaletteCard : public PaletteGroupCard { public: @@ -367,7 +368,7 @@ class SpritePaletteCard : public PaletteGroupCard { const gfx::PaletteGroup* GetPaletteGroup() const override; const PaletteGroupMetadata& GetMetadata() const override { return metadata_; } void DrawPaletteGrid() override; - int GetColorsPerRow() const override { return 8; } + int GetColorsPerRow() const override { return 16; } void DrawCustomPanels() override; // Show VRAM info private: @@ -375,6 +376,75 @@ class SpritePaletteCard : public PaletteGroupCard { static const PaletteGroupMetadata metadata_; }; +/** + * @brief Sprites Aux1 palette group card + * + * Manages auxiliary sprite palettes 1 + * - 12 palettes of 8 colors (7 colors + transparent) + */ +class SpritesAux1PaletteCard : public PaletteGroupCard { + public: + explicit SpritesAux1PaletteCard(Rom* rom); + ~SpritesAux1PaletteCard() 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 Sprites Aux2 palette group card + * + * Manages auxiliary sprite palettes 2 + * - 11 palettes of 8 colors (7 colors + transparent) + */ +class SpritesAux2PaletteCard : public PaletteGroupCard { + public: + explicit SpritesAux2PaletteCard(Rom* rom); + ~SpritesAux2PaletteCard() 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 Sprites Aux3 palette group card + * + * Manages auxiliary sprite palettes 3 + * - 24 palettes of 8 colors (7 colors + transparent) + */ +class SpritesAux3PaletteCard : public PaletteGroupCard { + public: + explicit SpritesAux3PaletteCard(Rom* rom); + ~SpritesAux3PaletteCard() 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 Equipment/Armor palette group card * diff --git a/src/app/editor/palette/palette_utility.cc b/src/app/editor/palette/palette_utility.cc new file mode 100644 index 00000000..2966bf6d --- /dev/null +++ b/src/app/editor/palette/palette_utility.cc @@ -0,0 +1,147 @@ +#include "palette_utility.h" + +#include "absl/strings/str_format.h" +#include "app/editor/palette/palette_editor.h" +#include "app/gfx/snes_palette.h" +#include "app/gui/icons.h" +#include "app/rom.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace editor { +namespace palette_utility { + +bool DrawPaletteJumpButton(const char* label, const std::string& group_name, + int palette_index, PaletteEditor* editor) { + bool clicked = ImGui::SmallButton( + absl::StrFormat("%s %s", ICON_MD_PALETTE, label).c_str()); + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Jump to palette editor:\n%s - Palette %d", + group_name.c_str(), palette_index); + } + + if (clicked && editor) { + editor->JumpToPalette(group_name, palette_index); + } + + return clicked; +} + +bool DrawInlineColorEdit(const char* label, gfx::SnesColor* color, + const std::string& group_name, int palette_index, + int color_index, PaletteEditor* editor) { + ImGui::PushID(label); + + // Draw color button + ImVec4 col = gui::ConvertSnesColorToImVec4(*color); + bool changed = ImGui::ColorEdit4(label, &col.x, + ImGuiColorEditFlags_NoInputs | + ImGuiColorEditFlags_NoLabel); + + if (changed) { + *color = gui::ConvertImVec4ToSnesColor(col); + } + + // Draw jump button + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); + if (ImGui::SmallButton(ICON_MD_OPEN_IN_NEW)) { + if (editor) { + editor->JumpToPalette(group_name, palette_index); + } + } + ImGui::PopStyleColor(); + + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("Jump to Palette Editor"); + ImGui::TextDisabled("%s - Palette %d, Color %d", + group_name.c_str(), palette_index, color_index); + DrawColorInfoTooltip(*color); + ImGui::EndTooltip(); + } + + ImGui::PopID(); + return changed; +} + +bool DrawPaletteIdSelector(const char* label, int* palette_id, + const std::string& group_name, + PaletteEditor* editor) { + ImGui::PushID(label); + + // Draw combo box + bool changed = ImGui::InputInt(label, palette_id); + + // Clamp to valid range (0-255 typically) + if (*palette_id < 0) *palette_id = 0; + if (*palette_id > 255) *palette_id = 255; + + // Draw jump button + ImGui::SameLine(); + if (DrawPaletteJumpButton("Jump", group_name, *palette_id, editor)) { + // Button clicked, editor will handle jump + } + + ImGui::PopID(); + return changed; +} + +void DrawColorInfoTooltip(const gfx::SnesColor& color) { + auto rgb = color.rgb(); + ImGui::Separator(); + ImGui::Text("RGB: (%d, %d, %d)", + static_cast(rgb.x), + static_cast(rgb.y), + static_cast(rgb.z)); + ImGui::Text("SNES: $%04X", color.snes()); + ImGui::Text("Hex: #%02X%02X%02X", + static_cast(rgb.x), + static_cast(rgb.y), + static_cast(rgb.z)); +} + +void DrawPalettePreview(const std::string& group_name, int palette_index, + Rom* rom) { + if (!rom || !rom->is_loaded()) { + ImGui::TextDisabled("(ROM not loaded)"); + return; + } + + auto* group = rom->mutable_palette_group()->get_group(group_name); + if (!group || palette_index >= group->size()) { + ImGui::TextDisabled("(Palette not found)"); + return; + } + + auto palette = group->palette(palette_index); + + // Draw colors in a row + int preview_size = std::min(8, static_cast(palette.size())); + for (int i = 0; i < preview_size; i++) { + if (i > 0) ImGui::SameLine(); + + ImGui::PushID(i); + ImVec4 col = gui::ConvertSnesColorToImVec4(palette[i]); + ImGui::ColorButton("##preview", col, + ImGuiColorEditFlags_NoAlpha | + ImGuiColorEditFlags_NoPicker | + ImGuiColorEditFlags_NoTooltip, + ImVec2(16, 16)); + + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("Color %d", i); + DrawColorInfoTooltip(palette[i]); + ImGui::EndTooltip(); + } + + ImGui::PopID(); + } +} + +} // namespace palette_utility +} // namespace editor +} // namespace yaze + diff --git a/src/app/editor/palette/palette_utility.h b/src/app/editor/palette/palette_utility.h new file mode 100644 index 00000000..eefcac38 --- /dev/null +++ b/src/app/editor/palette/palette_utility.h @@ -0,0 +1,78 @@ +#ifndef YAZE_APP_EDITOR_PALETTE_UTILITY_H +#define YAZE_APP_EDITOR_PALETTE_UTILITY_H + +#include + +#include "app/gfx/snes_color.h" +#include "app/gui/color.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace editor { + +class PaletteEditor; // Forward declaration + +/** + * @brief Utility functions for palette operations across editors + */ +namespace palette_utility { + +/** + * @brief Draw a palette selector button that opens palette editor + * @param label Button label + * @param group_name The palette group name + * @param palette_index The palette index within the group + * @param editor Pointer to palette editor (can be null) + * @return true if button was clicked + */ +bool DrawPaletteJumpButton(const char* label, const std::string& group_name, + int palette_index, PaletteEditor* editor); + +/** + * @brief Draw inline color edit with jump to palette + * @param label Label for the color widget + * @param color Color to edit + * @param group_name Palette group this color belongs to + * @param palette_index Palette index within group + * @param color_index Color index within palette + * @param editor Pointer to palette editor (can be null) + * @return true if color was changed + */ +bool DrawInlineColorEdit(const char* label, gfx::SnesColor* color, + const std::string& group_name, int palette_index, + int color_index, PaletteEditor* editor); + +/** + * @brief Draw a compact palette ID selector with preview + * @param label Label for the widget + * @param palette_id Current palette ID (in/out) + * @param group_name The palette group name + * @param editor Pointer to palette editor (can be null) + * @return true if palette ID changed + */ +bool DrawPaletteIdSelector(const char* label, int* palette_id, + const std::string& group_name, + PaletteEditor* editor); + +/** + * @brief Draw color info tooltip on hover + * @param color The color to show info for + */ +void DrawColorInfoTooltip(const gfx::SnesColor& color); + +/** + * @brief Draw a small palette preview (8 colors in a row) + * @param group_name Palette group name + * @param palette_index Palette index + * @param rom ROM instance to read palette from + */ +void DrawPalettePreview(const std::string& group_name, int palette_index, + class Rom* rom); + +} // namespace palette_utility + +} // namespace editor +} // namespace yaze + +#endif // YAZE_APP_EDITOR_PALETTE_UTILITY_H + diff --git a/src/app/gfx/snes_palette.h b/src/app/gfx/snes_palette.h index 57bc5c76..9e48e6db 100644 --- a/src/app/gfx/snes_palette.h +++ b/src/app/gfx/snes_palette.h @@ -211,6 +211,8 @@ struct PaletteGroup { PaletteGroup() = default; PaletteGroup(const std::string& name) : name_(name) {} + // ========== Basic Operations ========== + void AddPalette(SnesPalette pal) { palettes.emplace_back(pal); } void AddColor(SnesColor color) { @@ -222,23 +224,74 @@ struct PaletteGroup { void clear() { palettes.clear(); } void resize(size_t new_size) { palettes.resize(new_size); } + + // ========== Accessors ========== + auto name() const { return name_; } auto size() const { return palettes.size(); } + bool empty() const { return palettes.empty(); } + + // Const access auto palette(int i) const { return palettes[i]; } + const SnesPalette& palette_ref(int i) const { return palettes[i]; } + + // Mutable access auto mutable_palette(int i) { return &palettes[i]; } + SnesPalette& palette_ref(int i) { return palettes[i]; } + + // ========== Color Operations ========== + + /** + * @brief Get a specific color from a palette + * @param palette_index The palette index + * @param color_index The color index within the palette + * @return The color, or SnesColor() if indices are invalid + */ + SnesColor GetColor(int palette_index, int color_index) const { + if (palette_index >= 0 && palette_index < palettes.size()) { + const auto& pal = palettes[palette_index]; + if (color_index >= 0 && color_index < pal.size()) { + return pal[color_index]; + } + } + return SnesColor(); + } + + /** + * @brief Set a specific color in a palette + * @param palette_index The palette index + * @param color_index The color index within the palette + * @param color The new color value + * @return true if color was set successfully + */ + bool SetColor(int palette_index, int color_index, const SnesColor& color) { + if (palette_index >= 0 && palette_index < palettes.size()) { + auto& pal = palettes[palette_index]; + if (color_index >= 0 && color_index < pal.size()) { + pal[color_index] = color; + return true; + } + } + return false; + } + + // ========== Operator Overloads ========== SnesPalette operator[](int i) { - if (i > palettes.size()) { - std::cout << "PaletteGroup: Index out of bounds" << std::endl; - return palettes[0]; + if (i >= palettes.size()) { + std::cout << "PaletteGroup: Index " << i << " out of bounds (size: " + << palettes.size() << ")" << std::endl; + return SnesPalette(); } return palettes[i]; } const SnesPalette& operator[](int i) const { - if (i > palettes.size()) { - std::cout << "PaletteGroup: Index out of bounds" << std::endl; - return palettes[0]; + if (i >= palettes.size()) { + std::cout << "PaletteGroup: Index " << i << " out of bounds (size: " + << palettes.size() << ")" << std::endl; + static const SnesPalette empty_palette; + return empty_palette; } return palettes[i]; } diff --git a/src/app/gui/color.cc b/src/app/gui/color.cc index d20fde84..db2fd9e7 100644 --- a/src/app/gui/color.cc +++ b/src/app/gui/color.cc @@ -260,5 +260,54 @@ absl::Status DisplayEditablePalette(gfx::SnesPalette& palette, return absl::OkStatus(); } +IMGUI_API bool PaletteColorButton(const char* id, const gfx::SnesColor& color, + bool is_selected, bool is_modified, + const ImVec2& size, + ImGuiColorEditFlags flags) { + ImVec4 display_color = ConvertSnesColorToImVec4(color); + + // Add visual indicators for selection and modification + ImGui::PushID(id); + + // Selection border + if (is_selected) { + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 0.8f, 0.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f); + } + + bool clicked = ImGui::ColorButton(id, display_color, flags, size); + + if (is_selected) { + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + } + + // Modification indicator (small dot in corner) + if (is_modified) { + ImVec2 pos = ImGui::GetItemRectMin(); + ImVec2 dot_pos = ImVec2(pos.x + size.x - 6, pos.y + 2); + ImGui::GetWindowDrawList()->AddCircleFilled(dot_pos, 3.0f, + IM_COL32(255, 128, 0, 255)); + } + + // Tooltip with color info + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("SNES: $%04X", color.snes()); + auto rgb = color.rgb(); + ImGui::Text("RGB: (%d, %d, %d)", + static_cast(rgb.x), + static_cast(rgb.y), + static_cast(rgb.z)); + if (is_modified) { + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "Modified"); + } + ImGui::EndTooltip(); + } + + ImGui::PopID(); + return clicked; +} + } // namespace gui } // namespace yaze diff --git a/src/app/gui/color.h b/src/app/gui/color.h index c9d608a2..db2a2837 100644 --- a/src/app/gui/color.h +++ b/src/app/gui/color.h @@ -56,6 +56,12 @@ IMGUI_API absl::Status DisplayEditablePalette(gfx::SnesPalette &palette, void SelectablePalettePipeline(uint64_t &palette_id, bool &refresh_graphics, gfx::SnesPalette &palette); +// Palette color button with selection and modification indicators +IMGUI_API bool PaletteColorButton(const char* id, const gfx::SnesColor& color, + bool is_selected, bool is_modified, + const ImVec2& size = ImVec2(28, 28), + ImGuiColorEditFlags flags = 0); + } // namespace gui } // namespace yaze diff --git a/src/app/gui/editor_card_manager.cc b/src/app/gui/editor_card_manager.cc index 3981bca4..e30ef1c8 100644 --- a/src/app/gui/editor_card_manager.cc +++ b/src/app/gui/editor_card_manager.cc @@ -5,8 +5,8 @@ #include "absl/strings/str_format.h" #include "app/gui/icons.h" +#include "app/gui/theme_manager.h" #include "imgui/imgui.h" -#include "util/file_util.h" namespace yaze { namespace gui { @@ -34,11 +34,54 @@ void EditorCardManager::RegisterCard(const CardInfo& info) { info.card_id.c_str(), info.display_name.c_str()); } +void EditorCardManager::RegisterCard(const std::string& card_id, + const std::string& display_name, + const std::string& icon, + const std::string& category, + const std::string& shortcut_hint, + int priority, + std::function on_show, + std::function on_hide, + bool visible_by_default) { + if (card_id.empty()) { + printf("[EditorCardManager] Warning: Attempted to register card with empty ID\n"); + return; + } + + // Check if already registered + if (cards_.find(card_id) != cards_.end()) { + printf("[EditorCardManager] WARNING: Card '%s' already registered, skipping duplicate\n", + card_id.c_str()); + return; + } + + // Create centralized visibility flag + centralized_visibility_[card_id] = visible_by_default; + + // Register card with pointer to centralized flag + CardInfo info; + info.card_id = card_id; + info.display_name = display_name; + info.icon = icon; + info.category = category; + info.shortcut_hint = shortcut_hint; + info.priority = priority; + info.visibility_flag = ¢ralized_visibility_[card_id]; + info.on_show = on_show; + info.on_hide = on_hide; + + cards_[card_id] = info; + printf("[EditorCardManager] Registered card with centralized visibility: %s (%s) [default: %s]\n", + card_id.c_str(), display_name.c_str(), visible_by_default ? "visible" : "hidden"); +} + void EditorCardManager::UnregisterCard(const std::string& card_id) { auto it = cards_.find(card_id); if (it != cards_.end()) { printf("[EditorCardManager] Unregistered card: %s\n", card_id.c_str()); cards_.erase(it); + // Also remove centralized visibility if it exists + centralized_visibility_.erase(card_id); } } @@ -56,6 +99,9 @@ bool EditorCardManager::ShowCard(const std::string& card_id) { if (it->second.visibility_flag) { *it->second.visibility_flag = true; + // Set active category when showing a card + SetActiveCategory(it->second.category); + if (it->second.on_show) { it->second.on_show(); } @@ -669,6 +715,107 @@ void EditorCardManager::LoadPresetsFromFile() { printf("[EditorCardManager] Loading presets from file\n"); } +void EditorCardManager::SetActiveCategory(const std::string& category) { + if (active_category_ != category) { + active_category_ = category; + printf("[EditorCardManager] Active category changed to: %s\n", category.c_str()); + } +} + +void EditorCardManager::DrawSidebar(const std::string& category) { + // Set this category as active when sidebar is drawn + SetActiveCategory(category); + + // Use ThemeManager for consistent theming + const auto& theme = ThemeManager::Get().GetCurrentTheme(); + + const float sidebar_width = GetSidebarWidth(); + + // Fixed sidebar window on the left edge of screen + ImGui::SetNextWindowPos(ImVec2(0, ImGui::GetFrameHeight())); // Below menu bar + ImGui::SetNextWindowSize(ImVec2(sidebar_width, -1)); // Full height below menu + + ImGuiWindowFlags sidebar_flags = + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_NoBringToFrontOnFocus; + + ImGui::PushStyleColor(ImGuiCol_WindowBg, ConvertColorToImVec4(theme.child_bg)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(4.0f, 8.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 6.0f)); + + if (ImGui::Begin(absl::StrFormat("##%s_Sidebar", category).c_str(), nullptr, sidebar_flags)) { + // Get cards for this category + auto cards = GetCardsInCategory(category); + + // Close All button at top + ImVec4 error_color = ConvertColorToImVec4(theme.error); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4( + error_color.x * 0.6f, error_color.y * 0.6f, error_color.z * 0.6f, 0.7f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, error_color); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4( + error_color.x * 1.2f, error_color.y * 1.2f, error_color.z * 1.2f, 1.0f)); + + if (ImGui::Button(ICON_MD_CLOSE, ImVec2(40.0f, 40.0f))) { + HideAllCardsInCategory(category); + } + + ImGui::PopStyleColor(3); + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Close All %s Cards", category.c_str()); + } + + ImGui::Dummy(ImVec2(0, 4.0f)); + + // Draw card buttons + ImVec4 accent_color = ConvertColorToImVec4(theme.accent); + ImVec4 button_bg = ConvertColorToImVec4(theme.button); + + for (const auto& card : cards) { + ImGui::PushID(card.card_id.c_str()); + + bool is_active = card.visibility_flag && *card.visibility_flag; + + if (is_active) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4( + accent_color.x, accent_color.y, accent_color.z, 0.5f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4( + accent_color.x, accent_color.y, accent_color.z, 0.7f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, accent_color); + } else { + ImGui::PushStyleColor(ImGuiCol_Button, button_bg); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ConvertColorToImVec4(theme.button_hovered)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ConvertColorToImVec4(theme.button_active)); + } + + if (ImGui::Button(card.icon.c_str(), ImVec2(40.0f, 40.0f))) { + ToggleCard(card.card_id); + SetActiveCategory(category); + } + + ImGui::PopStyleColor(3); + + if (ImGui::IsItemHovered() || ImGui::IsItemActive()) { + SetActiveCategory(category); + + ImGui::SetTooltip("%s\n%s", card.display_name.c_str(), + card.shortcut_hint.empty() ? "" : card.shortcut_hint.c_str()); + } + + ImGui::PopID(); + } + } + ImGui::End(); + + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(); +} + } // namespace gui } // namespace yaze diff --git a/src/app/gui/editor_card_manager.h b/src/app/gui/editor_card_manager.h index 43cbe56a..fd0dab7a 100644 --- a/src/app/gui/editor_card_manager.h +++ b/src/app/gui/editor_card_manager.h @@ -75,6 +75,18 @@ class EditorCardManager { // Registration void RegisterCard(const CardInfo& info); + + // Register card with centralized visibility management (preferred method) + void RegisterCard(const std::string& card_id, + const std::string& display_name, + const std::string& icon, + const std::string& category, + const std::string& shortcut_hint = "", + int priority = 50, + std::function on_show = nullptr, + std::function on_hide = nullptr, + bool visible_by_default = false); + void UnregisterCard(const std::string& card_id); void ClearAllCards(); @@ -98,6 +110,15 @@ class EditorCardManager { void DrawViewMenuSection(const std::string& category); void DrawViewMenuAll(); // Draw all categories as submenus + // VSCode-style sidebar (replaces Toolset) + void DrawSidebar(const std::string& category); // Icon-only sidebar for category + static constexpr float GetSidebarWidth() { return 48.0f; } + + // Active editor tracking (based on most recently interacted card) + void SetActiveCategory(const std::string& category); + std::string GetActiveCategory() const { return active_category_; } + bool IsCategoryActive(const std::string& category) const { return active_category_ == category; } + // Compact inline card control for menu bar void DrawCompactCardControl(const std::string& category); // Shows only active editor's cards void DrawInlineCardToggles(const std::string& category); // Minimal inline checkboxes @@ -135,7 +156,9 @@ class EditorCardManager { EditorCardManager& operator=(const EditorCardManager&) = delete; std::unordered_map cards_; + std::unordered_map centralized_visibility_; // Centralized card visibility flags std::unordered_map presets_; + std::string active_category_; // Currently active editor category (based on last card interaction) // Helper methods void SavePresetsToFile(); diff --git a/src/app/gui/themed_widgets.cc b/src/app/gui/themed_widgets.cc index 2954bb7a..9ed10868 100644 --- a/src/app/gui/themed_widgets.cc +++ b/src/app/gui/themed_widgets.cc @@ -254,50 +254,7 @@ void ThemedProgressBar(float fraction, const ImVec2& size, const char* overlay) // ============================================================================ // 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; -} +// NOTE: PaletteColorButton moved to color.cc void ColorInfoPanel(const yaze::gfx::SnesColor& color, bool show_snes_format, diff --git a/src/app/gui/themed_widgets.h b/src/app/gui/themed_widgets.h index 3ac71241..e99ffafe 100644 --- a/src/app/gui/themed_widgets.h +++ b/src/app/gui/themed_widgets.h @@ -181,18 +181,7 @@ void ThemedProgressBar(float fraction, const ImVec2& size = ImVec2(-1, 0), // 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)); +// NOTE: PaletteColorButton moved to color.h for consistency with other color utilities /** * @brief Display color information with copy-to-clipboard functionality diff --git a/src/zelda3/palette_constants.cc b/src/zelda3/palette_constants.cc new file mode 100644 index 00000000..2504a1e7 --- /dev/null +++ b/src/zelda3/palette_constants.cc @@ -0,0 +1,76 @@ +#include "zelda3/palette_constants.h" + +namespace yaze { +namespace zelda3 { + +const PaletteGroupMetadata* GetPaletteGroupMetadata(const char* group_id) { + std::string id_str(group_id); + + if (id_str == PaletteGroupName::kOverworldMain) { + return &PaletteMetadata::kOverworldMain; + } else if (id_str == PaletteGroupName::kOverworldAux) { + return &PaletteMetadata::kOverworldAux; + } else if (id_str == PaletteGroupName::kOverworldAnimated) { + return &PaletteMetadata::kOverworldAnimated; + } else if (id_str == PaletteGroupName::kDungeonMain) { + return &PaletteMetadata::kDungeonMain; + } else if (id_str == PaletteGroupName::kGlobalSprites) { + return &PaletteMetadata::kGlobalSprites; + } else if (id_str == PaletteGroupName::kSpritesAux1) { + return &PaletteMetadata::kSpritesAux1; + } else if (id_str == PaletteGroupName::kSpritesAux2) { + return &PaletteMetadata::kSpritesAux2; + } else if (id_str == PaletteGroupName::kSpritesAux3) { + return &PaletteMetadata::kSpritesAux3; + } else if (id_str == PaletteGroupName::kArmor) { + return &PaletteMetadata::kArmor; + } else if (id_str == PaletteGroupName::kSwords) { + return &PaletteMetadata::kSwords; + } else if (id_str == PaletteGroupName::kShields) { + return &PaletteMetadata::kShields; + } else if (id_str == PaletteGroupName::kHud) { + return &PaletteMetadata::kHud; + } else if (id_str == PaletteGroupName::kGrass) { + return &PaletteMetadata::kGrass; + } else if (id_str == PaletteGroupName::k3DObject) { + return &PaletteMetadata::k3DObject; + } else if (id_str == PaletteGroupName::kOverworldMiniMap) { + return &PaletteMetadata::kOverworldMiniMap; + } + return nullptr; +} + +std::vector GetAllPaletteGroups() { + return { + // Overworld + &PaletteMetadata::kOverworldMain, + &PaletteMetadata::kOverworldAux, + &PaletteMetadata::kOverworldAnimated, + + // Dungeon + &PaletteMetadata::kDungeonMain, + + // Sprites + &PaletteMetadata::kGlobalSprites, + &PaletteMetadata::kSpritesAux1, + &PaletteMetadata::kSpritesAux2, + &PaletteMetadata::kSpritesAux3, + + // Equipment + &PaletteMetadata::kArmor, + &PaletteMetadata::kSwords, + &PaletteMetadata::kShields, + + // Interface + &PaletteMetadata::kHud, + &PaletteMetadata::kOverworldMiniMap, + + // Special + &PaletteMetadata::kGrass, + &PaletteMetadata::k3DObject + }; +} + +} // namespace zelda3 +} // namespace yaze + diff --git a/src/zelda3/palette_constants.h b/src/zelda3/palette_constants.h new file mode 100644 index 00000000..a90dc0c6 --- /dev/null +++ b/src/zelda3/palette_constants.h @@ -0,0 +1,310 @@ +#ifndef YAZE_ZELDA3_PALETTE_CONSTANTS_H +#define YAZE_ZELDA3_PALETTE_CONSTANTS_H + +#include +#include + +namespace yaze { +namespace zelda3 { + +// ============================================================================ +// Palette Group Names +// ============================================================================ +// These constants ensure consistent naming across the entire program + +namespace PaletteGroupName { +constexpr const char* kOverworldMain = "ow_main"; +constexpr const char* kOverworldAux = "ow_aux"; +constexpr const char* kOverworldAnimated = "ow_animated"; +constexpr const char* kHud = "hud"; +constexpr const char* kGlobalSprites = "global_sprites"; +constexpr const char* kArmor = "armor"; +constexpr const char* kSwords = "swords"; +constexpr const char* kShields = "shields"; +constexpr const char* kSpritesAux1 = "sprites_aux1"; +constexpr const char* kSpritesAux2 = "sprites_aux2"; +constexpr const char* kSpritesAux3 = "sprites_aux3"; +constexpr const char* kDungeonMain = "dungeon_main"; +constexpr const char* kGrass = "grass"; +constexpr const char* k3DObject = "3d_object"; +constexpr const char* kOverworldMiniMap = "ow_mini_map"; +} // namespace PaletteGroupName + +// ============================================================================ +// ROM Addresses +// ============================================================================ + +namespace PaletteAddress { +constexpr uint32_t kOverworldMain = 0xDE6C8; +constexpr uint32_t kOverworldAux = 0xDE86C; +constexpr uint32_t kOverworldAnimated = 0xDE604; +constexpr uint32_t kGlobalSpritesLW = 0xDD218; +constexpr uint32_t kGlobalSpritesDW = 0xDD290; +constexpr uint32_t kArmor = 0xDD308; +constexpr uint32_t kSpritesAux1 = 0xDD39E; +constexpr uint32_t kSpritesAux2 = 0xDD446; +constexpr uint32_t kSpritesAux3 = 0xDD4E0; +constexpr uint32_t kSwords = 0xDD630; +constexpr uint32_t kShields = 0xDD648; +constexpr uint32_t kHud = 0xDD660; +constexpr uint32_t kDungeonMap = 0xDD70A; +constexpr uint32_t kDungeonMain = 0xDD734; +constexpr uint32_t kDungeonMapBg = 0xDE544; +constexpr uint32_t kGrassLW = 0x5FEA9; +constexpr uint32_t kGrassDW = 0x05FEB3; +constexpr uint32_t kGrassSpecial = 0x75640; +constexpr uint32_t kOverworldMiniMap = 0x55B27; +constexpr uint32_t kTriforce = 0x64425; +constexpr uint32_t kCrystal = 0xF4CD3; +} // namespace PaletteAddress + +// ============================================================================ +// Palette Counts +// ============================================================================ + +namespace PaletteCount { +constexpr int kHud = 2; +constexpr int kOverworldMain = 60; // 20 LW, 20 DW, 20 Special +constexpr int kOverworldAux = 20; +constexpr int kOverworldAnimated = 14; +constexpr int kGlobalSprites = 6; +constexpr int kArmor = 5; +constexpr int kSwords = 4; +constexpr int kSpritesAux1 = 12; +constexpr int kSpritesAux2 = 11; +constexpr int kSpritesAux3 = 24; +constexpr int kShields = 3; +constexpr int kDungeonMain = 20; +constexpr int kGrass = 3; +constexpr int k3DObject = 2; +constexpr int kOverworldMiniMap = 2; +} // namespace PaletteCount + +// ============================================================================ +// Palette Metadata +// ============================================================================ + +struct PaletteGroupMetadata { + const char* group_id; // Unique identifier (e.g., "ow_main") + const char* display_name; // Human-readable name + const char* category; // Category (e.g., "Overworld", "Dungeon") + uint32_t base_address; // ROM address + int palette_count; // Number of palettes + int colors_per_palette; // Colors in each palette + int colors_per_row; // How many colors per row in UI + int bits_per_pixel; // Color depth (typically 4 for SNES) + const char* description; // Usage description + bool has_animations; // Whether palettes animate +}; + +// Predefined metadata for all palette groups +namespace PaletteMetadata { + +constexpr PaletteGroupMetadata kOverworldMain = { + .group_id = PaletteGroupName::kOverworldMain, + .display_name = "Overworld Main", + .category = "Overworld", + .base_address = PaletteAddress::kOverworldMain, + .palette_count = PaletteCount::kOverworldMain, + .colors_per_palette = 35, // 35 colors: 2 full rows (0-15, 16-31) + 3 colors (32-34) + .colors_per_row = 7, // Display in 16-color rows for proper SNES alignment + .bits_per_pixel = 4, + .description = "Main overworld palettes: 35 colors per set (2 full rows + 3 colors)", + .has_animations = false +}; + +constexpr PaletteGroupMetadata kOverworldAnimated = { + .group_id = PaletteGroupName::kOverworldAnimated, + .display_name = "Overworld Animated", + .category = "Overworld", + .base_address = PaletteAddress::kOverworldAnimated, + .palette_count = PaletteCount::kOverworldAnimated, + .colors_per_palette = 7, // 7 colors (overlay palette, no transparent) + .colors_per_row = 8, // Display in 8-color groups + .bits_per_pixel = 4, + .description = "Animated overlay palettes: 7 colors per set (water, lava, etc.)", + .has_animations = true +}; + +constexpr PaletteGroupMetadata kDungeonMain = { + .group_id = PaletteGroupName::kDungeonMain, + .display_name = "Dungeon Main", + .category = "Dungeon", + .base_address = PaletteAddress::kDungeonMain, + .palette_count = PaletteCount::kDungeonMain, + .colors_per_palette = 90, // 90 colors: 5 full rows (0-15, 16-31, 32-47, 48-63, 64-79) + 10 colors (80-89) + .colors_per_row = 16, // Display in 16-color rows for proper SNES alignment + .bits_per_pixel = 4, + .description = "Dungeon-specific palettes: 90 colors per set (5 full rows + 10 colors)", + .has_animations = false +}; + +constexpr PaletteGroupMetadata kGlobalSprites = { + .group_id = PaletteGroupName::kGlobalSprites, + .display_name = "Global Sprites", + .category = "Sprites", + .base_address = PaletteAddress::kGlobalSpritesLW, + .palette_count = 2, // 2 sets (LW and DW), each with 60 colors + .colors_per_palette = 60, // 60 colors: 4 rows (0-15, 16-31, 32-47, 48-59) with transparent at 0, 16, 32, 48 + .colors_per_row = 16, // Display in 16-color rows for proper SNES alignment + .bits_per_pixel = 4, + .description = "Global sprite palettes: 60 colors per set (4 sprite sub-palettes of 15+transparent each)", + .has_animations = false +}; + +constexpr PaletteGroupMetadata kSpritesAux1 = { + .group_id = PaletteGroupName::kSpritesAux1, + .display_name = "Sprites Aux 1", + .category = "Sprites", + .base_address = PaletteAddress::kSpritesAux1, + .palette_count = PaletteCount::kSpritesAux1, + .colors_per_palette = 7, // 7 colors (ROM stores 7, transparent added in memory) + .colors_per_row = 8, // Display as 8-color sub-palettes (with transparent) + .bits_per_pixel = 4, + .description = "Auxiliary sprite palettes 1: 7 colors per palette (transparent added at runtime)", + .has_animations = false +}; + +constexpr PaletteGroupMetadata kSpritesAux2 = { + .group_id = PaletteGroupName::kSpritesAux2, + .display_name = "Sprites Aux 2", + .category = "Sprites", + .base_address = PaletteAddress::kSpritesAux2, + .palette_count = PaletteCount::kSpritesAux2, + .colors_per_palette = 7, // 7 colors (ROM stores 7, transparent added in memory) + .colors_per_row = 8, // Display as 8-color sub-palettes (with transparent) + .bits_per_pixel = 4, + .description = "Auxiliary sprite palettes 2: 7 colors per palette (transparent added at runtime)", + .has_animations = false +}; + +constexpr PaletteGroupMetadata kSpritesAux3 = { + .group_id = PaletteGroupName::kSpritesAux3, + .display_name = "Sprites Aux 3", + .category = "Sprites", + .base_address = PaletteAddress::kSpritesAux3, + .palette_count = PaletteCount::kSpritesAux3, + .colors_per_palette = 7, // 7 colors (ROM stores 7, transparent added in memory) + .colors_per_row = 8, // Display as 8-color sub-palettes (with transparent) + .bits_per_pixel = 4, + .description = "Auxiliary sprite palettes 3: 7 colors per palette (transparent added at runtime)", + .has_animations = false +}; + +constexpr PaletteGroupMetadata kArmor = { + .group_id = PaletteGroupName::kArmor, + .display_name = "Armor / Link", + .category = "Equipment", + .base_address = PaletteAddress::kArmor, + .palette_count = PaletteCount::kArmor, + .colors_per_palette = 15, // 15 colors (ROM stores 15, transparent added in memory for full row) + .colors_per_row = 16, // Display as full 16-color rows (with transparent at index 0) + .bits_per_pixel = 4, + .description = "Link's tunic colors: 15 colors per palette (Green, Blue, Red, Bunny, Electrocuted)", + .has_animations = false +}; + +constexpr PaletteGroupMetadata kSwords = { + .group_id = PaletteGroupName::kSwords, + .display_name = "Swords", + .category = "Equipment", + .base_address = PaletteAddress::kSwords, + .palette_count = PaletteCount::kSwords, + .colors_per_palette = 3, // 3 colors (overlay palette, no transparent) + .colors_per_row = 4, // Display in compact groups + .bits_per_pixel = 4, + .description = "Sword blade colors: 3 colors per palette (Fighter, Master, Tempered, Golden)", + .has_animations = false +}; + +constexpr PaletteGroupMetadata kShields = { + .group_id = PaletteGroupName::kShields, + .display_name = "Shields", + .category = "Equipment", + .base_address = PaletteAddress::kShields, + .palette_count = PaletteCount::kShields, + .colors_per_palette = 4, // 4 colors (overlay palette, no transparent) + .colors_per_row = 4, // Display in compact groups + .bits_per_pixel = 4, + .description = "Shield colors: 4 colors per palette (Fighter, Fire, Mirror)", + .has_animations = false +}; + +constexpr PaletteGroupMetadata kHud = { + .group_id = PaletteGroupName::kHud, + .display_name = "HUD", + .category = "Interface", + .base_address = PaletteAddress::kHud, + .palette_count = PaletteCount::kHud, + .colors_per_palette = 32, // 32 colors: 2 full rows (0-15, 16-31) with transparent at 0, 16 + .colors_per_row = 16, // Display in 16-color rows + .bits_per_pixel = 2, // HUD palettes are 2bpp + .description = "HUD/Interface palettes: 32 colors per set (2 full rows)", + .has_animations = false +}; + +constexpr PaletteGroupMetadata kOverworldAux = { + .group_id = PaletteGroupName::kOverworldAux, + .display_name = "Overworld Auxiliary", + .category = "Overworld", + .base_address = PaletteAddress::kOverworldAux, + .palette_count = PaletteCount::kOverworldAux, + .colors_per_palette = 21, // 21 colors: 1 full row (0-15) + 5 colors (16-20) + .colors_per_row = 16, // Display in 16-color rows + .bits_per_pixel = 4, + .description = "Overworld auxiliary palettes: 21 colors per set (1 full row + 5 colors)", + .has_animations = false +}; + +constexpr PaletteGroupMetadata kGrass = { + .group_id = PaletteGroupName::kGrass, + .display_name = "Grass", + .category = "Overworld", + .base_address = PaletteAddress::kGrassLW, + .palette_count = PaletteCount::kGrass, + .colors_per_palette = 1, // Single color per entry + .colors_per_row = 3, // Display all 3 in one row + .bits_per_pixel = 4, + .description = "Hardcoded grass colors: 3 individual colors (LW, DW, Special)", + .has_animations = false +}; + +constexpr PaletteGroupMetadata k3DObject = { + .group_id = PaletteGroupName::k3DObject, + .display_name = "3D Objects", + .category = "Special", + .base_address = PaletteAddress::kTriforce, + .palette_count = PaletteCount::k3DObject, + .colors_per_palette = 8, // 8 colors per palette (7 + transparent) + .colors_per_row = 8, // Display in 8-color groups + .bits_per_pixel = 4, + .description = "3D object palettes: 8 colors per palette (Triforce, Crystal)", + .has_animations = false +}; + +constexpr PaletteGroupMetadata kOverworldMiniMap = { + .group_id = PaletteGroupName::kOverworldMiniMap, + .display_name = "Overworld Mini Map", + .category = "Interface", + .base_address = PaletteAddress::kOverworldMiniMap, + .palette_count = PaletteCount::kOverworldMiniMap, + .colors_per_palette = 128, // 128 colors: 8 full rows (0-127) with transparent at 0, 16, 32, 48, 64, 80, 96, 112 + .colors_per_row = 16, // Display in 16-color rows + .bits_per_pixel = 4, + .description = "Overworld mini-map palettes: 128 colors per set (8 full rows)", + .has_animations = false +}; + +} // namespace PaletteMetadata + +// Helper to get metadata by group name +const PaletteGroupMetadata* GetPaletteGroupMetadata(const char* group_id); + +// Get all available palette groups +std::vector GetAllPaletteGroups(); + +} // namespace zelda3 +} // namespace yaze + +#endif // YAZE_ZELDA3_PALETTE_CONSTANTS_H + diff --git a/src/zelda3/palette_structure.md b/src/zelda3/palette_structure.md new file mode 100644 index 00000000..b5db41f1 --- /dev/null +++ b/src/zelda3/palette_structure.md @@ -0,0 +1,142 @@ +# SNES Palette Structure for ALTTP + +## SNES Palette Memory Layout + +The SNES has 256 color palette entries organized as: +- **16 palette rows** of **16 colors each** +- Each row starts with color index 0, which is **transparent** +- Palettes must be aligned to 16-color boundaries + +### Example Layout +``` +Row 0: Colors 0-15 (Color 0 = transparent) +Row 1: Colors 16-31 (Color 16 = transparent) +Row 2: Colors 32-47 (Color 32 = transparent) +... +``` + +## ALTTP Palette Groups - Corrected Structure + +### Background Palettes (BG) + +#### Overworld Main (35 colors per set) +- **Structure**: 2 full rows + 3 colors + - Row 0: Colors 0-15 (transparent + 15 colors) + - Row 1: Colors 16-31 (transparent + 15 colors) + - Row 2: Colors 32-34 (3 colors) +- **ROM**: 0xDE6C8 +- **Sets**: 60 (20 LW, 20 DW, 20 Special) + +#### Overworld Auxiliary (21 colors per set) +- **Structure**: 1 full row + 5 colors + - Row 0: Colors 0-15 (transparent + 15 colors) + - Row 1: Colors 16-20 (5 colors) +- **ROM**: 0xDE86C +- **Sets**: 20 + +#### Overworld Animated (7 colors per set) +- **Structure**: Half-row without transparent + - Colors 0-6 (7 colors, no transparent marker as these overlay existing) +- **ROM**: 0xDE604 +- **Sets**: 14 + +#### Dungeon Main (90 colors per set) +- **Structure**: 5 full rows + 10 colors + - Row 0: Colors 0-15 (transparent + 15 colors) + - Row 1: Colors 16-31 (transparent + 15 colors) + - Row 2: Colors 32-47 (transparent + 15 colors) + - Row 3: Colors 48-63 (transparent + 15 colors) + - Row 4: Colors 64-79 (transparent + 15 colors) + - Row 5: Colors 80-89 (10 colors) +- **ROM**: 0xDD734 +- **Sets**: 20 (one per dungeon) + +### Sprite Palettes (OAM) + +Sprite palettes use rows 8-15 (colors 128-255). + +#### Global Sprites (60 colors total) +- **Structure**: 4 rows (each with 15 actual colors + transparent) + - Row 8: Colors 128-143 (Sprite Palette 0: transparent + 15 colors) + - Row 9: Colors 144-159 (Sprite Palette 1: transparent + 15 colors) + - Row 10: Colors 160-175 (Sprite Palette 2: transparent + 15 colors) + - Row 11: Colors 176-191 (Sprite Palette 3: transparent + 15 colors) +- **ROM LW**: 0xDD218 +- **ROM DW**: 0xDD290 +- **Total**: 2 sets (LW and DW) + +#### Sprites Auxiliary 1 (7 colors per palette) +- **Structure**: 12 palettes, each occupying half a row + - Palette 0: 7 colors (indices 1-7 of first half-row) + - Palette 1: 7 colors (indices 9-15 of second half-row) + - ...and so on +- **ROM**: 0xDD39E +- **Palettes**: 12 + +#### Sprites Auxiliary 2 (7 colors per palette) +- **Structure**: 11 palettes, each occupying half a row +- **ROM**: 0xDD446 +- **Palettes**: 11 + +#### Sprites Auxiliary 3 (7 colors per palette) +- **Structure**: 24 palettes, each occupying half a row +- **ROM**: 0xDD4E0 +- **Palettes**: 24 + +### Equipment/Link Palettes + +#### Armor/Link (15 colors per palette) +- **Structure**: Full row minus transparent + - Each palette: transparent + 15 colors +- **ROM**: 0xDD308 +- **Palettes**: 5 (Green Mail, Blue Mail, Red Mail, Bunny, Electrocuted) + +#### Swords (3 colors per palette) +- **Structure**: 3 colors within row (no transparent needed as overlay) +- **ROM**: 0xDD630 +- **Palettes**: 4 (Fighter, Master, Tempered, Golden) + +#### Shields (4 colors per palette) +- **Structure**: 4 colors within row (no transparent needed as overlay) +- **ROM**: 0xDD648 +- **Palettes**: 3 (Fighter, Fire, Mirror) + +### HUD Palettes + +#### HUD (32 colors per set) +- **Structure**: 2 full rows + - Row 0: Colors 0-15 (transparent + 15 colors) + - Row 1: Colors 16-31 (transparent + 15 colors) +- **ROM**: 0xDD660 +- **Sets**: 2 + +### Special Colors + +#### Grass (3 individual colors) +- LW: 0x5FEA9 +- DW: 0x5FEB3 +- Special: 0x75640 + +#### 3D Objects (8 colors per palette) +- **Triforce**: 0x64425 +- **Crystal**: 0xF4CD3 + +#### Overworld Mini Map (128 colors per set) +- **Structure**: 8 full rows +- **ROM**: 0x55B27 +- **Sets**: 2 + +## Key Principles + +1. **Transparent Color**: Always at indices 0, 16, 32, 48, 64, etc. (multiples of 16) +2. **Row Alignment**: Palettes should respect 16-color row boundaries +3. **Overlay Palettes**: Some palettes (like animated, swords, shields) overlay existing colors and don't have their own transparent +4. **Sub-Palettes**: Multiple small palettes can share a row if they're 8 colors or less + +## Implementation Notes + +When loading palettes, we must: +1. Mark color index 0 of each 16-color row as transparent +2. For palettes < 16 colors, understand if they're standalone (need transparent) or overlays (don't need transparent) +3. Display palettes in UI with proper row alignment for clarity +