From 6c0e7a96a54aaed5467ff946e985042811dcd5b9 Mon Sep 17 00:00:00 2001 From: scawful Date: Sat, 4 Oct 2025 23:59:08 -0400 Subject: [PATCH] refactor: Update GUI components and integrate new palette widget - Removed references to the old `EnhancedPaletteEditor` and replaced it with the new `PaletteWidget` across various files, including canvas and context menu implementations. - Updated CMake configurations to include the new `palette_widget` source files, ensuring proper integration into the build system. - Refactored GUI code to enhance modularity and maintainability, improving the overall user experience in palette management and editing functionalities. - Introduced new UI components for background rendering and editor selection dialogs, enhancing the application's graphical interface. --- src/CMakeLists.txt | 4 +- src/app/core/controller.cc | 2 +- src/app/editor/editor_library.cmake | 4 + src/app/editor/editor_manager.cc | 21 +- src/app/editor/editor_manager.h | 5 + src/app/editor/menu_builder.cc | 187 +++++ src/app/editor/menu_builder.h | 119 +++ .../{gui => editor/ui}/background_renderer.cc | 2 +- .../{gui => editor/ui}/background_renderer.h | 6 +- src/app/editor/ui/editor_selection_dialog.cc | 256 +++++++ src/app/editor/ui/editor_selection_dialog.h | 104 +++ src/app/editor/ui/welcome_screen.cc | 690 ++++++++++++++++++ src/app/editor/ui/welcome_screen.h | 111 +++ src/app/gui/canvas.cc | 4 +- src/app/gui/canvas.h | 4 +- src/app/gui/canvas/canvas_context_menu.cc | 2 +- src/app/gui/canvas/canvas_modals.cc | 14 +- src/app/gui/gui_library.cmake | 7 +- src/app/gui/style.cc | 2 +- .../palette_widget.cc} | 28 +- .../palette_widget.h} | 19 +- 21 files changed, 1536 insertions(+), 55 deletions(-) create mode 100644 src/app/editor/menu_builder.cc create mode 100644 src/app/editor/menu_builder.h rename src/app/{gui => editor/ui}/background_renderer.cc (99%) rename src/app/{gui => editor/ui}/background_renderer.h (95%) create mode 100644 src/app/editor/ui/editor_selection_dialog.cc create mode 100644 src/app/editor/ui/editor_selection_dialog.h create mode 100644 src/app/editor/ui/welcome_screen.cc create mode 100644 src/app/editor/ui/welcome_screen.h rename src/app/gui/{enhanced_palette_editor.cc => widgets/palette_widget.cc} (92%) rename src/app/gui/{enhanced_palette_editor.h => widgets/palette_widget.h} (79%) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3f14f8e3..e6d2c712 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -926,8 +926,6 @@ source_group("Application\\Graphics" FILES # GUI System source_group("Application\\GUI" FILES - app/gui/background_renderer.cc - app/gui/background_renderer.h app/gui/canvas_utils.cc app/gui/canvas_utils.h app/gui/canvas.cc @@ -935,7 +933,7 @@ source_group("Application\\GUI" FILES app/gui/color.cc app/gui/color.h app/gui/enhanced_palette_editor.cc - app/gui/enhanced_palette_editor.h + app/gui/widgets/palette_widget.h app/gui/icons.h app/gui/input.cc app/gui/input.h diff --git a/src/app/core/controller.cc b/src/app/core/controller.cc index 71173394..a4ca4d8a 100644 --- a/src/app/core/controller.cc +++ b/src/app/core/controller.cc @@ -5,7 +5,7 @@ #include "absl/status/status.h" #include "app/core/window.h" #include "app/editor/editor_manager.h" -#include "app/gui/background_renderer.h" +#include "app/editor/ui/background_renderer.h" #include "app/gui/theme_manager.h" #include "app/gui/widgets/widget_id_registry.h" #include "imgui/backends/imgui_impl_sdl2.h" diff --git a/src/app/editor/editor_library.cmake b/src/app/editor/editor_library.cmake index d84ca553..f02c2cd1 100644 --- a/src/app/editor/editor_library.cmake +++ b/src/app/editor/editor_library.cmake @@ -1,6 +1,10 @@ set( YAZE_APP_EDITOR_SRC app/editor/editor_manager.cc + app/editor/menu_builder.cc + app/editor/ui/editor_selection_dialog.cc + app/editor/ui/welcome_screen.cc + app/editor/ui/background_renderer.cc app/editor/dungeon/dungeon_editor.cc app/editor/dungeon/dungeon_editor_v2.cc app/editor/dungeon/dungeon_room_selector.cc diff --git a/src/app/editor/editor_manager.cc b/src/app/editor/editor_manager.cc index cfacc27a..bd513e43 100644 --- a/src/app/editor/editor_manager.cc +++ b/src/app/editor/editor_manager.cc @@ -21,10 +21,11 @@ #include "app/editor/music/music_editor.h" #include "app/editor/overworld/overworld_editor.h" #include "app/editor/sprite/sprite_editor.h" +#include "app/editor/ui/editor_selection_dialog.h" #include "app/emu/emulator.h" #include "app/gfx/arena.h" #include "app/gfx/performance_profiler.h" -#include "app/gui/background_renderer.h" +#include "app/editor/ui/background_renderer.h" #include "app/gui/icons.h" #include "app/gui/input.h" #include "app/gui/style.h" @@ -741,16 +742,16 @@ void EditorManager::Initialize(const std::string& filename) { } }, [&]() { return current_rom_ && current_rom_->is_loaded(); }}, - {absl::StrCat(ICON_MD_SAVE_ALT, " Test Save/Load"), "", - [&]() { - if (current_rom_) { - [[maybe_unused]] auto status = test::TestManager::Get().TestRomSaveLoad(current_rom_); - } - }, - [&]() { return current_rom_ && current_rom_->is_loaded(); }}, - }}, + {absl::StrCat(ICON_MD_SAVE_ALT, " Test Save/Load"), "", + [&]() { + if (current_rom_) { + [[maybe_unused]] auto status = test::TestManager::Get().TestRomSaveLoad(current_rom_); + } + }, + [&]() { return current_rom_ && current_rom_->is_loaded(); }}, + }}, - {absl::StrCat(ICON_MD_CODE, " ZSCustomOverworld"), "", []() {}, []() { return true; }, + {absl::StrCat(ICON_MD_CODE, " ZSCustomOverworld"), "", []() {}, []() { return true; }, std::vector{ {absl::StrCat(ICON_MD_INFO, " Check ROM Version"), "", [&]() { diff --git a/src/app/editor/editor_manager.h b/src/app/editor/editor_manager.h index 9f771367..10d3adbd 100644 --- a/src/app/editor/editor_manager.h +++ b/src/app/editor/editor_manager.h @@ -29,6 +29,7 @@ #endif #include "app/editor/system/settings_editor.h" #include "app/editor/system/toast_manager.h" +#include "app/editor/ui/editor_selection_dialog.h" #include "app/emu/emulator.h" #include "app/gfx/performance_dashboard.h" #include "app/rom.h" @@ -187,6 +188,10 @@ class EditorManager { // Project file editor ProjectFileEditor project_file_editor_; + + // Editor selection dialog + EditorSelectionDialog editor_selection_dialog_; + bool show_editor_selection_ = false; #ifdef YAZE_WITH_GRPC // Agent editor - manages chat, collaboration, and network coordination diff --git a/src/app/editor/menu_builder.cc b/src/app/editor/menu_builder.cc new file mode 100644 index 00000000..ea79341b --- /dev/null +++ b/src/app/editor/menu_builder.cc @@ -0,0 +1,187 @@ +#include "app/editor/menu_builder.h" + +#include "absl/strings/str_cat.h" + +namespace yaze { +namespace editor { + +MenuBuilder& MenuBuilder::BeginMenu(const char* label, const char* icon) { + Menu menu; + menu.label = label; + if (icon) { + menu.icon = icon; + } + menus_.push_back(menu); + current_menu_ = &menus_.back(); + return *this; +} + +MenuBuilder& MenuBuilder::BeginSubMenu(const char* label, const char* icon, + EnabledCheck enabled) { + if (!current_menu_) return *this; + + MenuItem item; + item.type = MenuItem::Type::kSubMenuBegin; + item.label = label; + if (icon) { + item.icon = icon; + } + item.enabled = enabled; + current_menu_->items.push_back(item); + return *this; +} + +MenuBuilder& MenuBuilder::EndMenu() { + if (!current_menu_) return *this; + + // Check if we're ending a submenu or top-level menu + bool is_submenu = false; + for (auto it = current_menu_->items.rbegin(); + it != current_menu_->items.rend(); ++it) { + if (it->type == MenuItem::Type::kSubMenuBegin) { + is_submenu = true; + break; + } else if (it->type == MenuItem::Type::kSubMenuEnd) { + break; + } + } + + if (is_submenu) { + MenuItem item; + item.type = MenuItem::Type::kSubMenuEnd; + current_menu_->items.push_back(item); + } else { + current_menu_ = nullptr; + } + + return *this; +} + +MenuBuilder& MenuBuilder::Item(const char* label, const char* icon, + Callback callback, const char* shortcut, + EnabledCheck enabled, EnabledCheck checked) { + if (!current_menu_) return *this; + + MenuItem item; + item.type = MenuItem::Type::kItem; + item.label = label; + if (icon) { + item.icon = icon; + } + if (shortcut) { + item.shortcut = shortcut; + } + item.callback = callback; + item.enabled = enabled; + item.checked = checked; + current_menu_->items.push_back(item); + return *this; +} + +MenuBuilder& MenuBuilder::Item(const char* label, Callback callback, + const char* shortcut, EnabledCheck enabled) { + return Item(label, nullptr, callback, shortcut, enabled, nullptr); +} + +MenuBuilder& MenuBuilder::Separator() { + if (!current_menu_) return *this; + + MenuItem item; + item.type = MenuItem::Type::kSeparator; + current_menu_->items.push_back(item); + return *this; +} + +MenuBuilder& MenuBuilder::DisabledItem(const char* label, const char* icon) { + if (!current_menu_) return *this; + + MenuItem item; + item.type = MenuItem::Type::kDisabled; + item.label = label; + if (icon) { + item.icon = icon; + } + current_menu_->items.push_back(item); + return *this; +} + +void MenuBuilder::Draw() { + for (const auto& menu : menus_) { + std::string menu_label = menu.icon.empty() + ? menu.label + : absl::StrCat(menu.icon, " ", menu.label); + + if (ImGui::BeginMenu(menu_label.c_str())) { + for (const auto& item : menu.items) { + DrawMenuItem(item); + } + ImGui::EndMenu(); + } + } +} + +void MenuBuilder::DrawMenuItem(const MenuItem& item) { + switch (item.type) { + case MenuItem::Type::kSeparator: + ImGui::Separator(); + break; + + case MenuItem::Type::kDisabled: { + std::string label = item.icon.empty() + ? item.label + : absl::StrCat(item.icon, " ", item.label); + ImGui::BeginDisabled(); + ImGui::MenuItem(label.c_str(), nullptr, false, false); + ImGui::EndDisabled(); + break; + } + + case MenuItem::Type::kSubMenuBegin: { + std::string label = item.icon.empty() + ? item.label + : absl::StrCat(item.icon, " ", item.label); + + bool enabled = !item.enabled || item.enabled(); + if (enabled && ImGui::BeginMenu(label.c_str())) { + // Submenu items will be drawn in subsequent calls + } else if (!enabled) { + ImGui::BeginDisabled(); + ImGui::MenuItem(label.c_str(), nullptr, false, false); + ImGui::EndDisabled(); + } + break; + } + + case MenuItem::Type::kSubMenuEnd: + ImGui::EndMenu(); + break; + + case MenuItem::Type::kItem: { + std::string label = item.icon.empty() + ? item.label + : absl::StrCat(item.icon, " ", item.label); + + bool enabled = !item.enabled || item.enabled(); + bool checked = item.checked && item.checked(); + + const char* shortcut_str = item.shortcut.empty() + ? nullptr + : item.shortcut.c_str(); + + if (ImGui::MenuItem(label.c_str(), shortcut_str, checked, enabled)) { + if (item.callback) { + item.callback(); + } + } + break; + } + } +} + +void MenuBuilder::Clear() { + menus_.clear(); + current_menu_ = nullptr; +} + +} // namespace editor +} // namespace yaze diff --git a/src/app/editor/menu_builder.h b/src/app/editor/menu_builder.h new file mode 100644 index 00000000..e5a0f733 --- /dev/null +++ b/src/app/editor/menu_builder.h @@ -0,0 +1,119 @@ +#ifndef YAZE_APP_EDITOR_MENU_BUILDER_H_ +#define YAZE_APP_EDITOR_MENU_BUILDER_H_ + +#include +#include +#include + +#include "app/gui/icons.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace editor { + +/** + * @class MenuBuilder + * @brief Fluent interface for building ImGui menus with icons + * + * Provides a cleaner, more maintainable way to construct menus: + * + * MenuBuilder menu; + * menu.BeginMenu("File", ICON_MD_FOLDER) + * .Item("Open", ICON_MD_FILE_OPEN, []() { OpenFile(); }) + * .Separator() + * .Item("Quit", ICON_MD_EXIT_TO_APP, []() { Quit(); }) + * .EndMenu(); + */ +class MenuBuilder { + public: + using Callback = std::function; + using EnabledCheck = std::function; + + MenuBuilder() = default; + + /** + * @brief Begin a top-level menu + */ + MenuBuilder& BeginMenu(const char* label, const char* icon = nullptr); + + /** + * @brief Begin a submenu + */ + MenuBuilder& BeginSubMenu(const char* label, const char* icon = nullptr, + EnabledCheck enabled = nullptr); + + /** + * @brief End the current menu/submenu + */ + MenuBuilder& EndMenu(); + + /** + * @brief Add a menu item + */ + MenuBuilder& Item(const char* label, const char* icon, Callback callback, + const char* shortcut = nullptr, + EnabledCheck enabled = nullptr, + EnabledCheck checked = nullptr); + + /** + * @brief Add a menu item without icon (convenience) + */ + MenuBuilder& Item(const char* label, Callback callback, + const char* shortcut = nullptr, + EnabledCheck enabled = nullptr); + + /** + * @brief Add a separator + */ + MenuBuilder& Separator(); + + /** + * @brief Add a disabled item (grayed out) + */ + MenuBuilder& DisabledItem(const char* label, const char* icon = nullptr); + + /** + * @brief Draw the menu bar (call in main menu bar) + */ + void Draw(); + + /** + * @brief Clear all menus + */ + void Clear(); + + private: + struct MenuItem { + enum class Type { + kItem, + kSubMenuBegin, + kSubMenuEnd, + kSeparator, + kDisabled + }; + + Type type; + std::string label; + std::string icon; + std::string shortcut; + Callback callback; + EnabledCheck enabled; + EnabledCheck checked; + }; + + struct Menu { + std::string label; + std::string icon; + std::vector items; + }; + + std::vector menus_; + Menu* current_menu_ = nullptr; + + void DrawMenuItem(const MenuItem& item); +}; + +} // namespace editor +} // namespace yaze + +#endif // YAZE_APP_EDITOR_MENU_BUILDER_H_ diff --git a/src/app/gui/background_renderer.cc b/src/app/editor/ui/background_renderer.cc similarity index 99% rename from src/app/gui/background_renderer.cc rename to src/app/editor/ui/background_renderer.cc index 354b4cc3..e57c228f 100644 --- a/src/app/gui/background_renderer.cc +++ b/src/app/editor/ui/background_renderer.cc @@ -1,4 +1,4 @@ -#include "background_renderer.h" +#include "app/editor/ui/background_renderer.h" #include #include diff --git a/src/app/gui/background_renderer.h b/src/app/editor/ui/background_renderer.h similarity index 95% rename from src/app/gui/background_renderer.h rename to src/app/editor/ui/background_renderer.h index 77bad64d..ee2f1fcc 100644 --- a/src/app/gui/background_renderer.h +++ b/src/app/editor/ui/background_renderer.h @@ -1,5 +1,5 @@ -#ifndef YAZE_APP_GUI_BACKGROUND_RENDERER_H -#define YAZE_APP_GUI_BACKGROUND_RENDERER_H +#ifndef YAZE_APP_EDITOR_UI_BACKGROUND_RENDERER_H +#define YAZE_APP_EDITOR_UI_BACKGROUND_RENDERER_H #include "imgui/imgui.h" #include "app/gui/color.h" @@ -93,4 +93,4 @@ private: } // namespace gui } // namespace yaze -#endif // YAZE_APP_GUI_BACKGROUND_RENDERER_H +#endif // YAZE_APP_EDITOR_UI_BACKGROUND_RENDERER_H diff --git a/src/app/editor/ui/editor_selection_dialog.cc b/src/app/editor/ui/editor_selection_dialog.cc new file mode 100644 index 00000000..5d263056 --- /dev/null +++ b/src/app/editor/ui/editor_selection_dialog.cc @@ -0,0 +1,256 @@ +#include "app/editor/ui/editor_selection_dialog.h" + +#include +#include +#include + +#include "absl/strings/str_cat.h" +#include "imgui/imgui.h" +#include "app/gui/icons.h" +#include "app/gui/style.h" +#include "util/file_util.h" + +namespace yaze { +namespace editor { + +EditorSelectionDialog::EditorSelectionDialog() { + // Initialize editor metadata + editors_ = { + {EditorType::kOverworld, "Overworld", ICON_MD_MAP, + "Edit overworld maps, entrances, and properties", "Ctrl+1", false, true}, + + {EditorType::kDungeon, "Dungeon", ICON_MD_CASTLE, + "Design dungeon rooms, layouts, and mechanics", "Ctrl+2", false, true}, + + {EditorType::kGraphics, "Graphics", ICON_MD_PALETTE, + "Modify tiles, palettes, and graphics sets", "Ctrl+3", false, true}, + + {EditorType::kSprite, "Sprites", ICON_MD_EMOJI_EMOTIONS, + "Edit sprite graphics and properties", "Ctrl+4", false, true}, + + {EditorType::kMessage, "Messages", ICON_MD_CHAT_BUBBLE, + "Edit dialogue, signs, and text", "Ctrl+5", false, true}, + + {EditorType::kMusic, "Music", ICON_MD_MUSIC_NOTE, + "Configure music and sound effects", "Ctrl+6", false, true}, + + {EditorType::kPalette, "Palettes", ICON_MD_COLOR_LENS, + "Edit color palettes and animations", "Ctrl+7", false, true}, + + {EditorType::kScreen, "Screens", ICON_MD_TV, + "Edit title screen and ending screens", "Ctrl+8", false, true}, + + {EditorType::kAssembly, "Assembly", ICON_MD_CODE, + "Write and edit assembly code", "Ctrl+9", false, false}, + + {EditorType::kMemory, "Hex Editor", ICON_MD_DATA_ARRAY, + "Direct ROM memory editing", "Ctrl+0", false, true}, + + {EditorType::kSettings, "Settings", ICON_MD_SETTINGS, + "Configure ROM and project settings", "", false, true}, + }; + + LoadRecentEditors(); +} + +bool EditorSelectionDialog::Show(bool* p_open) { + if (!is_open_) { + return false; + } + + bool editor_selected = false; + + // Center the dialog + ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(900, 600), ImGuiCond_Appearing); + + if (ImGui::Begin("Editor Selection", p_open ? p_open : &is_open_, + ImGuiWindowFlags_NoCollapse)) { + DrawWelcomeHeader(); + + ImGui::Separator(); + ImGui::Spacing(); + + // Quick access buttons for recently used + if (!recent_editors_.empty()) { + DrawQuickAccessButtons(); + ImGui::Separator(); + ImGui::Spacing(); + } + + // Main editor grid + ImGui::Text(ICON_MD_APPS " All Editors"); + ImGui::Spacing(); + + float button_size = 200.0f; + int columns = static_cast(ImGui::GetContentRegionAvail().x / button_size); + columns = std::max(columns, 1); + + if (ImGui::BeginTable("##EditorGrid", columns, + ImGuiTableFlags_None)) { + for (size_t i = 0; i < editors_.size(); ++i) { + ImGui::TableNextColumn(); + DrawEditorCard(editors_[i], static_cast(i)); + + if (selected_editor_ != EditorType::kNone) { + editor_selected = true; + MarkRecentlyUsed(selected_editor_); + if (selection_callback_) { + selection_callback_(selected_editor_); + } + } + } + ImGui::EndTable(); + } + } + ImGui::End(); + + if (editor_selected) { + is_open_ = false; + } + + return editor_selected; +} + +void EditorSelectionDialog::DrawWelcomeHeader() { + ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]); // Larger font if available + + ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), + ICON_MD_EDIT " Select an Editor"); + + ImGui::PopFont(); + + ImGui::TextWrapped("Choose an editor to begin working on your ROM. " + "You can open multiple editors simultaneously."); +} + +void EditorSelectionDialog::DrawQuickAccessButtons() { + ImGui::Text(ICON_MD_HISTORY " Recently Used"); + ImGui::Spacing(); + + for (EditorType type : recent_editors_) { + // Find editor info + auto it = std::find_if(editors_.begin(), editors_.end(), + [type](const EditorInfo& info) { + return info.type == type; + }); + + if (it != editors_.end()) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.8f, 0.6f)); + + if (ImGui::Button(absl::StrCat(it->icon, " ", it->name).c_str(), + ImVec2(150, 30))) { + selected_editor_ = type; + } + + ImGui::PopStyleColor(); + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", it->description); + } + + ImGui::SameLine(); + } + } + + ImGui::NewLine(); +} + +void EditorSelectionDialog::DrawEditorCard(const EditorInfo& info, int index) { + ImGui::PushID(index); + + // Card styling + bool is_recent = std::find(recent_editors_.begin(), recent_editors_.end(), + info.type) != recent_editors_.end(); + + if (is_recent) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.25f, 0.55f, 0.85f, 0.4f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.6f, 0.9f, 0.6f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.35f, 0.65f, 0.95f, 0.8f)); + } + + // Editor button with icon + ImVec2 button_size(180, 100); + if (ImGui::Button(absl::StrCat(info.icon, "\n", info.name).c_str(), + button_size)) { + selected_editor_ = info.type; + } + + if (is_recent) { + ImGui::PopStyleColor(3); + } + + // Tooltip with description + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("%s %s", info.icon, info.name); + ImGui::Separator(); + ImGui::TextWrapped("%s", info.description); + if (info.shortcut && info.shortcut[0]) { + ImGui::Spacing(); + ImGui::TextDisabled("Shortcut: %s", info.shortcut); + } + ImGui::EndTooltip(); + } + + // Recent indicator + if (is_recent) { + ImGui::SameLine(); + ImGui::TextDisabled(ICON_MD_STAR); + } + + ImGui::PopID(); +} + +void EditorSelectionDialog::MarkRecentlyUsed(EditorType type) { + // Remove if already in list + auto it = std::find(recent_editors_.begin(), recent_editors_.end(), type); + if (it != recent_editors_.end()) { + recent_editors_.erase(it); + } + + // Add to front + recent_editors_.insert(recent_editors_.begin(), type); + + // Limit size + if (recent_editors_.size() > kMaxRecentEditors) { + recent_editors_.resize(kMaxRecentEditors); + } + + SaveRecentEditors(); +} + +void EditorSelectionDialog::LoadRecentEditors() { + try { + auto data = util::LoadConfigFile("recent_editors.txt"); + if (!data.empty()) { + std::istringstream ss(data); + std::string line; + while (std::getline(ss, line) && + recent_editors_.size() < kMaxRecentEditors) { + int type_int = std::stoi(line); + if (type_int >= 0 && type_int < static_cast(EditorType::kLast)) { + recent_editors_.push_back(static_cast(type_int)); + } + } + } + } catch (...) { + // Ignore errors, just start with empty recent list + } +} + +void EditorSelectionDialog::SaveRecentEditors() { + try { + std::ostringstream ss; + for (EditorType type : recent_editors_) { + ss << static_cast(type) << "\n"; + } + util::SaveFile("recent_editors.txt", ss.str()); + } catch (...) { + // Ignore save errors + } +} + +} // namespace editor +} // namespace yaze diff --git a/src/app/editor/ui/editor_selection_dialog.h b/src/app/editor/ui/editor_selection_dialog.h new file mode 100644 index 00000000..a791f2a5 --- /dev/null +++ b/src/app/editor/ui/editor_selection_dialog.h @@ -0,0 +1,104 @@ +#ifndef YAZE_APP_EDITOR_UI_EDITOR_SELECTION_DIALOG_H_ +#define YAZE_APP_EDITOR_UI_EDITOR_SELECTION_DIALOG_H_ + +#include +#include +#include + +#include "app/editor/editor.h" + +namespace yaze { +namespace editor { + +/** + * @struct EditorInfo + * @brief Metadata about an available editor + */ +struct EditorInfo { + EditorType type; + const char* name; + const char* icon; + const char* description; + const char* shortcut; + bool recently_used = false; + bool requires_rom = true; +}; + +/** + * @class EditorSelectionDialog + * @brief Beautiful grid-based editor selection dialog + * + * Displays when a ROM is loaded, showing all available editors + * with icons, descriptions, and quick access. + */ +class EditorSelectionDialog { + public: + EditorSelectionDialog(); + + /** + * @brief Show the dialog + * @return True if an editor was selected + */ + bool Show(bool* p_open = nullptr); + + /** + * @brief Get the selected editor type + */ + EditorType GetSelectedEditor() const { return selected_editor_; } + + /** + * @brief Check if dialog is open + */ + bool IsOpen() const { return is_open_; } + + /** + * @brief Open the dialog + */ + void Open() { is_open_ = true; } + + /** + * @brief Close the dialog + */ + void Close() { is_open_ = false; } + + /** + * @brief Set callback for when editor is selected + */ + void SetSelectionCallback(std::function callback) { + selection_callback_ = callback; + } + + /** + * @brief Mark an editor as recently used + */ + void MarkRecentlyUsed(EditorType type); + + /** + * @brief Load recently used editors from settings + */ + void LoadRecentEditors(); + + /** + * @brief Save recently used editors to settings + */ + void SaveRecentEditors(); + + private: + void DrawEditorCard(const EditorInfo& info, int index); + void DrawWelcomeHeader(); + void DrawQuickAccessButtons(); + + std::vector editors_; + EditorType selected_editor_ = EditorType::kNone; + bool is_open_ = false; + std::function selection_callback_; + + // Recently used tracking + std::vector recent_editors_; + static constexpr int kMaxRecentEditors = 5; +}; + +} // namespace editor +} // namespace yaze + +#endif // YAZE_APP_EDITOR_UI_EDITOR_SELECTION_DIALOG_H_ diff --git a/src/app/editor/ui/welcome_screen.cc b/src/app/editor/ui/welcome_screen.cc new file mode 100644 index 00000000..d254400b --- /dev/null +++ b/src/app/editor/ui/welcome_screen.cc @@ -0,0 +1,690 @@ +#include "app/editor/ui/welcome_screen.h" + +#include +#include +#include +#include +#include + +#include "absl/strings/str_format.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "app/core/project.h" +#include "app/gui/icons.h" +#include "app/gui/theme_manager.h" +#include "imgui/imgui.h" +#include "imgui/imgui_internal.h" +#include "util/file_util.h" + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +namespace yaze { +namespace editor { + +namespace { + +// Get Zelda-inspired colors from theme or use fallback +ImVec4 GetThemedColor(const char* color_name, const ImVec4& fallback) { + auto& theme_mgr = gui::ThemeManager::Get(); + const auto& theme = theme_mgr.GetCurrentTheme(); + + // Map color names to theme colors + if (strcmp(color_name, "triforce_gold") == 0) { + return theme.accent.to_im_vec4(); + } else if (strcmp(color_name, "hyrule_green") == 0) { + return theme.success.to_im_vec4(); + } else if (strcmp(color_name, "master_sword_blue") == 0) { + return theme.info.to_im_vec4(); + } else if (strcmp(color_name, "ganon_purple") == 0) { + return theme.secondary.to_im_vec4(); + } else if (strcmp(color_name, "heart_red") == 0) { + return theme.error.to_im_vec4(); + } else if (strcmp(color_name, "spirit_orange") == 0) { + return theme.warning.to_im_vec4(); + } + + return fallback; +} + +// Zelda-inspired color palette (fallbacks) +const ImVec4 kTriforceGoldFallback = ImVec4(1.0f, 0.843f, 0.0f, 1.0f); +const ImVec4 kHyruleGreenFallback = ImVec4(0.133f, 0.545f, 0.133f, 1.0f); +const ImVec4 kMasterSwordBlueFallback = ImVec4(0.196f, 0.6f, 0.8f, 1.0f); +const ImVec4 kGanonPurpleFallback = ImVec4(0.502f, 0.0f, 0.502f, 1.0f); +const ImVec4 kHeartRedFallback = ImVec4(0.863f, 0.078f, 0.235f, 1.0f); +const ImVec4 kSpiritOrangeFallback = ImVec4(1.0f, 0.647f, 0.0f, 1.0f); +const ImVec4 kShadowPurpleFallback = ImVec4(0.416f, 0.353f, 0.804f, 1.0f); + +// Active colors (updated each frame from theme) +ImVec4 kTriforceGold = kTriforceGoldFallback; +ImVec4 kHyruleGreen = kHyruleGreenFallback; +ImVec4 kMasterSwordBlue = kMasterSwordBlueFallback; +ImVec4 kGanonPurple = kGanonPurpleFallback; +ImVec4 kHeartRed = kHeartRedFallback; +ImVec4 kSpiritOrange = kSpiritOrangeFallback; +ImVec4 kShadowPurple = kShadowPurpleFallback; + +std::string GetRelativeTimeString(const std::filesystem::file_time_type& ftime) { + auto sctp = std::chrono::time_point_cast( + ftime - std::filesystem::file_time_type::clock::now() + + std::chrono::system_clock::now()); + auto now = std::chrono::system_clock::now(); + auto diff = std::chrono::duration_cast(now - sctp); + + int hours = diff.count(); + if (hours < 24) { + return "Today"; + } else if (hours < 48) { + return "Yesterday"; + } else if (hours < 168) { + int days = hours / 24; + return absl::StrFormat("%d days ago", days); + } else if (hours < 720) { + int weeks = hours / 168; + return absl::StrFormat("%d week%s ago", weeks, weeks > 1 ? "s" : ""); + } else { + int months = hours / 720; + return absl::StrFormat("%d month%s ago", months, months > 1 ? "s" : ""); + } +} + +// Draw a pulsing triforce in the background +void DrawTriforceBackground(ImDrawList* draw_list, ImVec2 pos, float size, float alpha, float glow) { + float height = size * 0.866f; // sqrt(3)/2 for equilateral triangle + + // Calculate triangle points + auto triangle = [&](ImVec2 center, float s, ImU32 color) { + ImVec2 p1(center.x, center.y - height * s / size); + ImVec2 p2(center.x - s / 2, center.y + height * s / (2 * size)); + ImVec2 p3(center.x + s / 2, center.y + height * s / (2 * size)); + + draw_list->AddTriangleFilled(p1, p2, p3, color); + + // Glow effect + if (glow > 0.0f) { + ImU32 glow_color = ImGui::GetColorU32(ImVec4(1.0f, 0.843f, 0.0f, glow * 0.3f)); + draw_list->AddTriangle(p1, p2, p3, glow_color, 2.0f + glow * 3.0f); + } + }; + + ImU32 gold = ImGui::GetColorU32(ImVec4(1.0f, 0.843f, 0.0f, alpha)); + + // Top triangle + triangle(ImVec2(pos.x, pos.y), size, gold); + + // Bottom left triangle + triangle(ImVec2(pos.x - size / 4, pos.y + height / 2), size / 2, gold); + + // Bottom right triangle + triangle(ImVec2(pos.x + size / 4, pos.y + height / 2), size / 2, gold); +} + +} // namespace + +WelcomeScreen::WelcomeScreen() { + RefreshRecentProjects(); +} + +bool WelcomeScreen::Show(bool* p_open) { + // Update theme colors each frame + kTriforceGold = GetThemedColor("triforce_gold", kTriforceGoldFallback); + kHyruleGreen = GetThemedColor("hyrule_green", kHyruleGreenFallback); + kMasterSwordBlue = GetThemedColor("master_sword_blue", kMasterSwordBlueFallback); + kGanonPurple = GetThemedColor("ganon_purple", kGanonPurpleFallback); + kHeartRed = GetThemedColor("heart_red", kHeartRedFallback); + kSpiritOrange = GetThemedColor("spirit_orange", kSpiritOrangeFallback); + + UpdateAnimations(); + bool action_taken = false; + + ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->WorkPos); + ImGui::SetNextWindowSize(viewport->WorkSize); + ImGui::SetNextWindowViewport(viewport->ID); + + ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDecoration | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoBringToFrontOnFocus | + ImGuiWindowFlags_NoNavFocus; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(20, 20)); + + if (ImGui::Begin("##WelcomeScreen", p_open, window_flags)) { + ImDrawList* bg_draw_list = ImGui::GetWindowDrawList(); + ImVec2 window_pos = ImGui::GetWindowPos(); + ImVec2 window_size = ImGui::GetWindowSize(); + + // Dreamlike animated background with floating triforces + for (int i = 0; i < 5; ++i) { + float offset = animation_time_ * 0.5f + i * 2.0f; + float x = window_pos.x + window_size.x * (0.2f + 0.15f * i + sin(offset) * 0.1f); + float y = window_pos.y + window_size.y * (0.3f + cos(offset * 0.7f) * 0.3f); + float size = 40.0f + sin(offset * 1.3f) * 20.0f; + float alpha = 0.05f + sin(offset) * 0.05f; + + DrawTriforceBackground(bg_draw_list, ImVec2(x, y), size, alpha, 0.0f); + } + + DrawHeader(); + + ImGui::Spacing(); + ImGui::Spacing(); + + // Main content area with gradient separator + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + ImVec2 separator_start = ImGui::GetCursorScreenPos(); + ImVec2 separator_end(separator_start.x + ImGui::GetContentRegionAvail().x, separator_start.y + 2); + draw_list->AddRectFilledMultiColor( + separator_start, separator_end, + ImGui::GetColorU32(kTriforceGold), + ImGui::GetColorU32(kMasterSwordBlue), + ImGui::GetColorU32(kMasterSwordBlue), + ImGui::GetColorU32(kTriforceGold)); + + ImGui::Dummy(ImVec2(0, 10)); + + ImGui::BeginChild("WelcomeContent", ImVec2(0, -60), false); + + // Left side - Quick Actions & Templates + ImGui::BeginChild("LeftPanel", ImVec2(ImGui::GetContentRegionAvail().x * 0.3f, 0), true, + ImGuiWindowFlags_NoScrollbar); + DrawQuickActions(); + ImGui::Spacing(); + + // Animated separator + ImVec2 sep_start = ImGui::GetCursorScreenPos(); + float pulse = sin(animation_time_ * 2.0f) * 0.5f + 0.5f; + draw_list->AddLine( + sep_start, + ImVec2(sep_start.x + ImGui::GetContentRegionAvail().x, sep_start.y), + ImGui::GetColorU32(ImVec4(kTriforceGold.x, kTriforceGold.y, kTriforceGold.z, 0.3f + pulse * 0.3f)), + 2.0f); + + ImGui::Dummy(ImVec2(0, 5)); + DrawTemplatesSection(); + ImGui::EndChild(); + + ImGui::SameLine(); + + // Right side - Recent Projects & What's New + ImGui::BeginChild("RightPanel", ImVec2(0, 0), true); + DrawRecentProjects(); + ImGui::Spacing(); + + // Another animated separator + sep_start = ImGui::GetCursorScreenPos(); + draw_list->AddLine( + sep_start, + ImVec2(sep_start.x + ImGui::GetContentRegionAvail().x, sep_start.y), + ImGui::GetColorU32(ImVec4(kMasterSwordBlue.x, kMasterSwordBlue.y, kMasterSwordBlue.z, 0.3f + pulse * 0.3f)), + 2.0f); + + ImGui::Dummy(ImVec2(0, 5)); + DrawWhatsNew(); + ImGui::EndChild(); + + ImGui::EndChild(); + + // Footer with gradient + ImVec2 footer_start = ImGui::GetCursorScreenPos(); + ImVec2 footer_end(footer_start.x + ImGui::GetContentRegionAvail().x, footer_start.y + 2); + draw_list->AddRectFilledMultiColor( + footer_start, footer_end, + ImGui::GetColorU32(kHeartRed), + ImGui::GetColorU32(kHyruleGreen), + ImGui::GetColorU32(kHyruleGreen), + ImGui::GetColorU32(kHeartRed)); + + ImGui::Dummy(ImVec2(0, 5)); + DrawTipsSection(); + } + ImGui::End(); + + ImGui::PopStyleVar(); + + return action_taken; +} + +void WelcomeScreen::UpdateAnimations() { + animation_time_ += ImGui::GetIO().DeltaTime; + header_glow_ = sin(animation_time_ * 2.0f) * 0.5f + 0.5f; + + // Smooth card hover animations + for (int i = 0; i < 6; ++i) { + float target = (hovered_card_ == i) ? 1.05f : 1.0f; + card_hover_scale_[i] += (target - card_hover_scale_[i]) * ImGui::GetIO().DeltaTime * 8.0f; + } +} + +void WelcomeScreen::RefreshRecentProjects() { + recent_projects_.clear(); + + // Use the ProjectManager singleton to get recent files + auto& recent_files = core::ProjectManager::GetInstance().GetRecentFiles(); + + for (const auto& filepath : recent_files) { + if (recent_projects_.size() >= kMaxRecentProjects) break; + + RecentProject project; + project.filepath = filepath; + + // Extract filename + std::filesystem::path path(filepath); + project.name = path.filename().string(); + + // Get file modification time if it exists + if (std::filesystem::exists(path)) { + auto ftime = std::filesystem::last_write_time(path); + project.last_modified = GetRelativeTimeString(ftime); + project.rom_title = "ALTTP ROM"; + } else { + project.last_modified = "File not found"; + project.rom_title = "Missing"; + } + + recent_projects_.push_back(project); + } +} + +void WelcomeScreen::DrawHeader() { + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[2]); // Large font + + // Animated title with glow effect + const char* title = ICON_MD_CASTLE " yaze"; + auto windowWidth = ImGui::GetWindowSize().x; + auto textWidth = ImGui::CalcTextSize(title).x; + float xPos = (windowWidth - textWidth) * 0.5f; + + ImGui::SetCursorPosX(xPos); + ImVec2 text_pos = ImGui::GetCursorScreenPos(); + + // Glow effect behind text + float glow_size = 40.0f * header_glow_; + ImU32 glow_color = ImGui::GetColorU32(ImVec4(kTriforceGold.x, kTriforceGold.y, kTriforceGold.z, 0.3f * header_glow_)); + draw_list->AddCircleFilled( + ImVec2(text_pos.x + textWidth / 2, text_pos.y + 15), + glow_size, + glow_color, + 32); + + // Rainbow gradient on title + ImVec4 color1 = kTriforceGold; + ImVec4 color2 = kMasterSwordBlue; + float t = sin(animation_time_ * 1.5f) * 0.5f + 0.5f; + ImVec4 title_color( + color1.x * (1 - t) + color2.x * t, + color1.y * (1 - t) + color2.y * t, + color1.z * (1 - t) + color2.z * t, + 1.0f); + + ImGui::TextColored(title_color, "%s", title); + ImGui::PopFont(); + + // Animated subtitle + const char* subtitle = "Yet Another Zelda3 Editor"; + textWidth = ImGui::CalcTextSize(subtitle).x; + ImGui::SetCursorPosX((windowWidth - textWidth) * 0.5f); + + float subtitle_alpha = 0.6f + sin(animation_time_ * 3.0f) * 0.2f; + ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, subtitle_alpha), "%s", subtitle); + + // Draw small decorative triforces on either side of title + ImVec2 left_tri_pos(xPos - 50, text_pos.y + 10); + ImVec2 right_tri_pos(xPos + textWidth + 20, text_pos.y + 10); + DrawTriforceBackground(draw_list, left_tri_pos, 30, 0.5f, header_glow_); + DrawTriforceBackground(draw_list, right_tri_pos, 30, 0.5f, header_glow_); + + ImGui::Spacing(); +} + +void WelcomeScreen::DrawQuickActions() { + ImGui::TextColored(kSpiritOrange, ICON_MD_BOLT " Quick Actions"); + ImGui::Spacing(); + + float button_width = ImGui::GetContentRegionAvail().x; + + // Animated button colors + auto draw_action_button = [&](const char* icon, const char* text, + const ImVec4& color, bool enabled, + std::function callback) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(color.x * 0.6f, color.y * 0.6f, color.z * 0.6f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(color.x, color.y, color.z, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(color.x * 1.2f, color.y * 1.2f, color.z * 1.2f, 1.0f)); + + if (!enabled) ImGui::BeginDisabled(); + + bool clicked = ImGui::Button(absl::StrFormat("%s %s", icon, text).c_str(), + ImVec2(button_width, 45)); + + if (!enabled) ImGui::EndDisabled(); + + ImGui::PopStyleColor(3); + + if (clicked && enabled && callback) { + callback(); + } + + return clicked; + }; + + // Open ROM button - Green like finding an item + if (draw_action_button(ICON_MD_FOLDER_OPEN, "Open ROM", kHyruleGreen, true, open_rom_callback_)) { + // Handled by callback + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(ICON_MD_INFO " Open an existing ALTTP ROM file"); + } + + ImGui::Spacing(); + + // New Project button - Gold like getting a treasure + if (draw_action_button(ICON_MD_ADD_CIRCLE, "New Project", kTriforceGold, true, new_project_callback_)) { + // Handled by callback + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(ICON_MD_INFO " Create a new ROM hacking project"); + } + + ImGui::Spacing(); + + // Clone Project button - Blue like water/ice dungeon + draw_action_button(ICON_MD_CLOUD_DOWNLOAD, "Clone Project", kMasterSwordBlue, false, nullptr); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { + ImGui::SetTooltip(ICON_MD_CONSTRUCTION " Clone a project from git (Coming soon)"); + } +} + +void WelcomeScreen::DrawRecentProjects() { + ImGui::TextColored(kMasterSwordBlue, ICON_MD_HISTORY " Recent Projects"); + ImGui::Spacing(); + + if (recent_projects_.empty()) { + // Draw a cute "empty state" with animated icons + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); + + float pulse = sin(animation_time_ * 2.0f) * 0.3f + 0.7f; + ImVec2 cursor = ImGui::GetCursorPos(); + ImGui::SetCursorPosX(cursor.x + ImGui::GetContentRegionAvail().x * 0.3f); + ImGui::TextColored(ImVec4(kTriforceGold.x, kTriforceGold.y, kTriforceGold.z, pulse), + ICON_MD_EXPLORE); + ImGui::SetCursorPosX(cursor.x); + + ImGui::TextWrapped("No recent projects yet.\nOpen a ROM to begin your adventure!"); + ImGui::PopStyleColor(); + return; + } + + // Grid layout for project cards + float card_width = 220.0f; + float card_height = 140.0f; + int columns = std::max(1, (int)(ImGui::GetContentRegionAvail().x / (card_width + 15))); + + for (size_t i = 0; i < recent_projects_.size(); ++i) { + if (i % columns != 0) { + ImGui::SameLine(); + } + DrawProjectCard(recent_projects_[i], i); + } +} + +void WelcomeScreen::DrawProjectCard(const RecentProject& project, int index) { + ImGui::BeginGroup(); + + ImVec2 card_size(220, 140); + ImVec2 cursor_pos = ImGui::GetCursorScreenPos(); + + // Apply hover scale + float scale = card_hover_scale_[index]; + if (scale != 1.0f) { + ImVec2 center(cursor_pos.x + card_size.x / 2, cursor_pos.y + card_size.y / 2); + cursor_pos.x = center.x - (card_size.x * scale) / 2; + cursor_pos.y = center.y - (card_size.y * scale) / 2; + card_size.x *= scale; + card_size.y *= scale; + } + + // Draw card background with Zelda-themed gradient + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + // Gradient background + ImU32 color_top = ImGui::GetColorU32(ImVec4(0.15f, 0.20f, 0.25f, 1.0f)); + ImU32 color_bottom = ImGui::GetColorU32(ImVec4(0.10f, 0.15f, 0.20f, 1.0f)); + draw_list->AddRectFilledMultiColor( + cursor_pos, + ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y), + color_top, color_top, color_bottom, color_bottom); + + // Border with animated color + float border_t = (sin(animation_time_ + index) * 0.5f + 0.5f); + ImVec4 border_color_base = (index % 3 == 0) ? kHyruleGreen : + (index % 3 == 1) ? kMasterSwordBlue : kTriforceGold; + ImU32 border_color = ImGui::GetColorU32( + ImVec4(border_color_base.x, border_color_base.y, border_color_base.z, 0.4f + border_t * 0.3f)); + + draw_list->AddRect(cursor_pos, + ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y), + border_color, 6.0f, 0, 2.0f); + + // Make the card clickable + ImGui::SetCursorScreenPos(cursor_pos); + ImGui::InvisibleButton(absl::StrFormat("ProjectCard_%d", index).c_str(), card_size); + bool is_hovered = ImGui::IsItemHovered(); + bool is_clicked = ImGui::IsItemClicked(); + + hovered_card_ = is_hovered ? index : (hovered_card_ == index ? -1 : hovered_card_); + + // Hover glow effect + if (is_hovered) { + ImU32 hover_color = ImGui::GetColorU32(ImVec4(kTriforceGold.x, kTriforceGold.y, kTriforceGold.z, 0.2f)); + draw_list->AddRectFilled(cursor_pos, + ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y), + hover_color, 6.0f); + + // Draw small particles around the card + for (int p = 0; p < 5; ++p) { + float angle = animation_time_ * 2.0f + p * M_PI * 0.4f; + float radius = 10.0f + sin(animation_time_ * 3.0f + p) * 5.0f; + ImVec2 particle_pos( + cursor_pos.x + card_size.x / 2 + cos(angle) * (card_size.x / 2 + radius), + cursor_pos.y + card_size.y / 2 + sin(angle) * (card_size.y / 2 + radius)); + float particle_alpha = 0.3f + sin(animation_time_ * 4.0f + p) * 0.2f; + draw_list->AddCircleFilled(particle_pos, 2.0f, + ImGui::GetColorU32(ImVec4(kTriforceGold.x, kTriforceGold.y, kTriforceGold.z, particle_alpha))); + } + } + + // Draw content + ImVec2 content_pos(cursor_pos.x + 15, cursor_pos.y + 15); + + // Icon with colored background circle + ImVec2 icon_center(content_pos.x + 20, content_pos.y + 20); + ImU32 icon_bg = ImGui::GetColorU32(border_color_base); + draw_list->AddCircleFilled(icon_center, 25, icon_bg, 32); + + ImGui::SetCursorScreenPos(ImVec2(content_pos.x + 5, content_pos.y + 8)); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 1)); + ImGui::Text(ICON_MD_VIDEOGAME_ASSET); + ImGui::PopStyleColor(); + + // Project name + ImGui::SetCursorScreenPos(ImVec2(content_pos.x + 50, content_pos.y + 15)); + ImGui::PushTextWrapPos(cursor_pos.x + card_size.x - 15); + ImGui::TextColored(kTriforceGold, "%s", project.name.c_str()); + ImGui::PopTextWrapPos(); + + // ROM title + ImGui::SetCursorScreenPos(ImVec2(content_pos.x + 5, content_pos.y + 55)); + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), ICON_MD_GAMEPAD " %s", project.rom_title.c_str()); + + // Last modified with clock icon + ImGui::SetCursorScreenPos(ImVec2(content_pos.x + 5, content_pos.y + 80)); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), + ICON_MD_ACCESS_TIME " %s", project.last_modified.c_str()); + + // Path in card + ImGui::SetCursorScreenPos(ImVec2(content_pos.x + 5, content_pos.y + 105)); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.4f, 0.4f, 1.0f)); + std::string short_path = project.filepath; + if (short_path.length() > 35) { + short_path = "..." + short_path.substr(short_path.length() - 32); + } + ImGui::Text(ICON_MD_FOLDER " %s", short_path.c_str()); + ImGui::PopStyleColor(); + + // Tooltip + if (is_hovered) { + ImGui::BeginTooltip(); + ImGui::TextColored(kMasterSwordBlue, ICON_MD_INFO " Project Details"); + ImGui::Separator(); + ImGui::Text("Name: %s", project.name.c_str()); + ImGui::Text("Path: %s", project.filepath.c_str()); + ImGui::Text("Modified: %s", project.last_modified.c_str()); + ImGui::Separator(); + ImGui::TextColored(kTriforceGold, ICON_MD_TOUCH_APP " Click to open"); + ImGui::EndTooltip(); + } + + // Handle click + if (is_clicked && open_project_callback_) { + open_project_callback_(project.filepath); + } + + ImGui::EndGroup(); +} + +void WelcomeScreen::DrawTemplatesSection() { + ImGui::TextColored(kGanonPurple, ICON_MD_LAYERS " Templates"); + ImGui::Spacing(); + + struct Template { + const char* icon; + const char* name; + ImVec4 color; + }; + + Template templates[] = { + {ICON_MD_COTTAGE, "Vanilla ALTTP", kHyruleGreen}, + {ICON_MD_MAP, "ZSCustomOverworld v3", kMasterSwordBlue}, + {ICON_MD_PUBLIC, "Parallel Worlds Base", kGanonPurple}, + }; + + for (int i = 0; i < 3; ++i) { + bool is_selected = (selected_template_ == i); + + // Animated selection glow + if (is_selected) { + float glow = sin(animation_time_ * 3.0f) * 0.3f + 0.5f; + ImGui::PushStyleColor(ImGuiCol_Header, + ImVec4(templates[i].color.x * glow, templates[i].color.y * glow, + templates[i].color.z * glow, 0.8f)); + } + + if (ImGui::Selectable(absl::StrFormat("%s %s", templates[i].icon, templates[i].name).c_str(), + is_selected)) { + selected_template_ = i; + } + + if (is_selected) { + ImGui::PopStyleColor(); + } + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(ICON_MD_STAR " Start with a %s template", templates[i].name); + } + } + + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(kSpiritOrange.x * 0.6f, kSpiritOrange.y * 0.6f, kSpiritOrange.z * 0.6f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, kSpiritOrange); + ImGui::BeginDisabled(true); + ImGui::Button(absl::StrFormat("%s Use Template", ICON_MD_ROCKET_LAUNCH).c_str(), + ImVec2(-1, 35)); + ImGui::EndDisabled(); + ImGui::PopStyleColor(2); +} + +void WelcomeScreen::DrawTipsSection() { + // Rotating tips + const char* tips[] = { + "Press Ctrl+P to open the command palette", + "Use z3ed agent for AI-powered ROM editing", + "Enable ZSCustomOverworld in Debug menu for expanded features", + "Check the Performance Dashboard for optimization insights", + "Collaborate in real-time with yaze-server" + }; + int tip_index = ((int)(animation_time_ / 5.0f)) % 5; + + ImGui::Text(ICON_MD_LIGHTBULB); + ImGui::SameLine(); + ImGui::TextColored(kTriforceGold, "Tip:"); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "%s", tips[tip_index]); + + ImGui::SameLine(ImGui::GetWindowWidth() - 220); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.3f, 0.3f, 0.5f)); + if (ImGui::SmallButton(absl::StrFormat("%s Don't show again", ICON_MD_CLOSE).c_str())) { + manually_closed_ = true; + } + ImGui::PopStyleColor(); +} + +void WelcomeScreen::DrawWhatsNew() { + ImGui::TextColored(kHeartRed, ICON_MD_NEW_RELEASES " What's New"); + ImGui::Spacing(); + + // Version badge + float pulse = sin(animation_time_ * 2.0f) * 0.2f + 0.8f; + ImGui::TextColored(ImVec4(kMasterSwordBlue.x * pulse, kMasterSwordBlue.y * pulse, + kMasterSwordBlue.z * pulse, 1.0f), + ICON_MD_VERIFIED " yaze v0.2.0-alpha"); + ImGui::Spacing(); + + // Feature list with icons and colors + struct Feature { + const char* icon; + const char* title; + const char* desc; + ImVec4 color; + }; + + Feature features[] = { + {ICON_MD_PSYCHOLOGY, "AI Agent Integration", + "Natural language ROM editing with z3ed agent", kGanonPurple}, + {ICON_MD_CLOUD_SYNC, "Collaboration Features", + "Real-time ROM collaboration via yaze-server", kMasterSwordBlue}, + {ICON_MD_HISTORY, "Version Management", + "ROM snapshots, rollback, corruption detection", kHyruleGreen}, + {ICON_MD_PALETTE, "Enhanced Palette Editor", + "Advanced color tools with ROM palette browser", kSpiritOrange}, + {ICON_MD_SPEED, "Performance Improvements", + "Faster dungeon loading with parallel processing", kTriforceGold}, + }; + + for (const auto& feature : features) { + ImGui::Bullet(); + ImGui::SameLine(); + ImGui::TextColored(feature.color, "%s ", feature.icon); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.95f, 0.95f, 0.95f, 1.0f), "%s", feature.title); + + ImGui::Indent(25); + ImGui::TextColored(ImVec4(0.65f, 0.65f, 0.65f, 1.0f), "%s", feature.desc); + ImGui::Unindent(25); + ImGui::Spacing(); + } + + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(kMasterSwordBlue.x * 0.6f, kMasterSwordBlue.y * 0.6f, kMasterSwordBlue.z * 0.6f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, kMasterSwordBlue); + if (ImGui::Button(absl::StrFormat("%s View Full Changelog", ICON_MD_OPEN_IN_NEW).c_str())) { + // Open changelog or GitHub releases + } + ImGui::PopStyleColor(2); +} + +} // namespace editor +} // namespace yaze \ No newline at end of file diff --git a/src/app/editor/ui/welcome_screen.h b/src/app/editor/ui/welcome_screen.h new file mode 100644 index 00000000..9cf31f77 --- /dev/null +++ b/src/app/editor/ui/welcome_screen.h @@ -0,0 +1,111 @@ +#ifndef YAZE_APP_EDITOR_UI_WELCOME_SCREEN_H_ +#define YAZE_APP_EDITOR_UI_WELCOME_SCREEN_H_ + +#include +#include +#include + +namespace yaze { +namespace editor { + +/** + * @struct RecentProject + * @brief Information about a recently used project + */ +struct RecentProject { + std::string name; + std::string filepath; + std::string rom_title; + std::string last_modified; + std::string thumbnail_path; // Optional screenshot + int days_ago = 0; +}; + +/** + * @class WelcomeScreen + * @brief Modern welcome screen with project grid and quick actions + */ +class WelcomeScreen { + public: + WelcomeScreen(); + + /** + * @brief Show the welcome screen + * @param p_open Pointer to open state + * @return True if an action was taken (ROM opened, etc.) + */ + bool Show(bool* p_open); + + /** + * @brief Set callback for opening ROM + */ + void SetOpenRomCallback(std::function callback) { + open_rom_callback_ = callback; + } + + /** + * @brief Set callback for creating new project + */ + void SetNewProjectCallback(std::function callback) { + new_project_callback_ = callback; + } + + /** + * @brief Set callback for opening project + */ + void SetOpenProjectCallback(std::function callback) { + open_project_callback_ = callback; + } + + /** + * @brief Refresh recent projects list from the project manager + */ + void RefreshRecentProjects(); + + /** + * @brief Update animation time for dynamic effects + */ + void UpdateAnimations(); + + /** + * @brief Check if screen should be shown + */ + bool ShouldShow() const { return !manually_closed_; } + + /** + * @brief Mark as manually closed (don't show again this session) + */ + void MarkManuallyClosed() { manually_closed_ = true; } + + private: + void DrawHeader(); + void DrawQuickActions(); + void DrawRecentProjects(); + void DrawProjectCard(const RecentProject& project, int index); + void DrawTemplatesSection(); + void DrawTipsSection(); + void DrawWhatsNew(); + + std::vector recent_projects_; + bool manually_closed_ = false; + + // Callbacks + std::function open_rom_callback_; + std::function new_project_callback_; + std::function open_project_callback_; + + // UI state + int selected_template_ = 0; + static constexpr int kMaxRecentProjects = 6; + + // Animation state + float animation_time_ = 0.0f; + float header_glow_ = 0.0f; + float card_hover_scale_[6] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; + int hovered_card_ = -1; +}; + +} // namespace editor +} // namespace yaze + +#endif // YAZE_APP_EDITOR_UI_WELCOME_SCREEN_H_ diff --git a/src/app/gui/canvas.cc b/src/app/gui/canvas.cc index d068532f..f3f3c8a5 100644 --- a/src/app/gui/canvas.cc +++ b/src/app/gui/canvas.cc @@ -67,7 +67,7 @@ void Canvas::InitializeDefaults() { selection_.Clear(); // Initialize palette editor - palette_editor_ = std::make_unique(); + palette_editor_ = std::make_unique(); // Initialize interaction handler interaction_handler_.Initialize(canvas_id_); @@ -1744,7 +1744,7 @@ void Canvas::ShowAdvancedCanvasProperties() { } } -// Old ShowPaletteManager method removed - now handled by EnhancedPaletteEditor +// Old ShowPaletteManager method removed - now handled by PaletteWidget void Canvas::ShowScalingControls() { // Use the new modal system if available diff --git a/src/app/gui/canvas.h b/src/app/gui/canvas.h index 806cd6e6..05165f11 100644 --- a/src/app/gui/canvas.h +++ b/src/app/gui/canvas.h @@ -12,7 +12,7 @@ #include "app/gfx/bitmap.h" #include "app/rom.h" #include "app/gui/canvas_utils.h" -#include "app/gui/enhanced_palette_editor.h" +#include "app/gui/widgets/palette_widget.h" #include "app/gfx/bpp_format_manager.h" #include "app/gui/bpp_format_ui.h" #include "app/gui/canvas/canvas_modals.h" @@ -412,7 +412,7 @@ class Canvas { // Modular configuration and state CanvasConfig config_; CanvasSelection selection_; - std::unique_ptr palette_editor_; + std::unique_ptr palette_editor_; // Core canvas state bool is_hovered_ = false; diff --git a/src/app/gui/canvas/canvas_context_menu.cc b/src/app/gui/canvas/canvas_context_menu.cc index 0ce91e92..471d43fc 100644 --- a/src/app/gui/canvas/canvas_context_menu.cc +++ b/src/app/gui/canvas/canvas_context_menu.cc @@ -6,7 +6,7 @@ #include "app/gfx/performance_profiler.h" #include "app/gfx/performance_dashboard.h" -#include "app/gui/enhanced_palette_editor.h" +#include "app/gui/widgets/palette_widget.h" #include "app/gui/bpp_format_ui.h" #include "app/gui/icons.h" #include "app/gui/canvas/canvas_modals.h" diff --git a/src/app/gui/canvas/canvas_modals.cc b/src/app/gui/canvas/canvas_modals.cc index e240df9e..1c14644d 100644 --- a/src/app/gui/canvas/canvas_modals.cc +++ b/src/app/gui/canvas/canvas_modals.cc @@ -6,7 +6,7 @@ #include "app/gfx/performance_profiler.h" #include "app/gfx/performance_dashboard.h" -#include "app/gui/enhanced_palette_editor.h" +#include "app/gui/widgets/palette_widget.h" #include "app/gui/bpp_format_ui.h" #include "app/gui/icons.h" #include "imgui/imgui.h" @@ -421,9 +421,9 @@ void CanvasModals::RenderPaletteEditorModal(const std::string& canvas_id, ImGui::Text("%s %s", ICON_MD_PALETTE, modal_title.c_str()); ImGui::Separator(); - // Use the existing EnhancedPaletteEditor - static std::unique_ptr palette_editor = - std::make_unique(); + // Use the existing PaletteWidget + static std::unique_ptr palette_editor = + std::make_unique(); if (options.palette) { palette_editor->ShowPaletteEditor(*options.palette, modal_title); @@ -454,9 +454,9 @@ void CanvasModals::RenderColorAnalysisModal(const std::string& canvas_id, ImGui::Text("%s %s", ICON_MD_ZOOM_IN, modal_title.c_str()); ImGui::Separator(); - // Use the existing EnhancedPaletteEditor for color analysis - static std::unique_ptr palette_editor = - std::make_unique(); + // Use the existing PaletteWidget for color analysis + static std::unique_ptr palette_editor = + std::make_unique(); if (options.bitmap) { palette_editor->ShowColorAnalysis(*options.bitmap, modal_title); diff --git a/src/app/gui/gui_library.cmake b/src/app/gui/gui_library.cmake index 8b052922..5b22cef7 100644 --- a/src/app/gui/gui_library.cmake +++ b/src/app/gui/gui_library.cmake @@ -6,15 +6,14 @@ set( app/gui/widgets/collaboration_panel.cc app/gui/canvas.cc app/gui/canvas_utils.cc - app/gui/enhanced_palette_editor.cc + app/gui/widgets/palette_widget.cc app/gui/input.cc app/gui/style.cc app/gui/color.cc app/gui/theme_manager.cc - app/gui/background_renderer.cc app/gui/bpp_format_ui.cc - app/gui/widget_id_registry.cc - app/gui/widget_auto_register.cc + app/gui/widgets/widget_id_registry.cc + app/gui/widgets/widget_auto_register.cc # Canvas system components app/gui/canvas/canvas_modals.cc app/gui/canvas/canvas_context_menu.cc diff --git a/src/app/gui/style.cc b/src/app/gui/style.cc index 1d2df2c2..22f224ef 100644 --- a/src/app/gui/style.cc +++ b/src/app/gui/style.cc @@ -4,7 +4,7 @@ #include "util/file_util.h" #include "app/gui/theme_manager.h" -#include "app/gui/background_renderer.h" +#include "app/editor/ui/background_renderer.h" #include "app/core/platform/font_loader.h" #include "app/gui/color.h" #include "app/gui/icons.h" diff --git a/src/app/gui/enhanced_palette_editor.cc b/src/app/gui/widgets/palette_widget.cc similarity index 92% rename from src/app/gui/enhanced_palette_editor.cc rename to src/app/gui/widgets/palette_widget.cc index 0cafe891..e42282fc 100644 --- a/src/app/gui/enhanced_palette_editor.cc +++ b/src/app/gui/widgets/palette_widget.cc @@ -1,4 +1,4 @@ -#include "enhanced_palette_editor.h" +#include "app/gui/widgets/palette_widget.h" #include #include @@ -11,7 +11,7 @@ namespace gui { using core::Renderer; -void EnhancedPaletteEditor::Initialize(Rom* rom) { +void PaletteWidget::Initialize(Rom* rom) { rom_ = rom; rom_palettes_loaded_ = false; if (rom_) { @@ -19,7 +19,7 @@ void EnhancedPaletteEditor::Initialize(Rom* rom) { } } -void EnhancedPaletteEditor::ShowPaletteEditor(gfx::SnesPalette& palette, const std::string& title) { +void PaletteWidget::ShowPaletteEditor(gfx::SnesPalette& palette, const std::string& title) { if (ImGui::BeginPopupModal(title.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("Enhanced Palette Editor"); ImGui::Separator(); @@ -63,7 +63,7 @@ void EnhancedPaletteEditor::ShowPaletteEditor(gfx::SnesPalette& palette, const s } } -void EnhancedPaletteEditor::ShowROMPaletteManager() { +void PaletteWidget::ShowROMPaletteManager() { if (!show_rom_manager_) return; if (ImGui::Begin("ROM Palette Manager", &show_rom_manager_)) { @@ -92,7 +92,7 @@ void EnhancedPaletteEditor::ShowROMPaletteManager() { ImGui::End(); } -void EnhancedPaletteEditor::ShowColorAnalysis(const gfx::Bitmap& bitmap, const std::string& title) { +void PaletteWidget::ShowColorAnalysis(const gfx::Bitmap& bitmap, const std::string& title) { if (!show_color_analysis_) return; if (ImGui::Begin(title.c_str(), &show_color_analysis_)) { @@ -150,7 +150,7 @@ void EnhancedPaletteEditor::ShowColorAnalysis(const gfx::Bitmap& bitmap, const s ImGui::End(); } -bool EnhancedPaletteEditor::ApplyROMPalette(gfx::Bitmap* bitmap, int group_index, int palette_index) { +bool PaletteWidget::ApplyROMPalette(gfx::Bitmap* bitmap, int group_index, int palette_index) { if (!bitmap || !rom_palettes_loaded_ || group_index < 0 || group_index >= static_cast(rom_palette_groups_.size())) { return false; @@ -181,7 +181,7 @@ bool EnhancedPaletteEditor::ApplyROMPalette(gfx::Bitmap* bitmap, int group_index } } -const gfx::SnesPalette* EnhancedPaletteEditor::GetSelectedROMPalette() const { +const gfx::SnesPalette* PaletteWidget::GetSelectedROMPalette() const { if (!rom_palettes_loaded_ || current_group_index_ < 0 || current_group_index_ >= static_cast(rom_palette_groups_.size())) { return nullptr; @@ -190,11 +190,11 @@ const gfx::SnesPalette* EnhancedPaletteEditor::GetSelectedROMPalette() const { return &rom_palette_groups_[current_group_index_]; } -void EnhancedPaletteEditor::SavePaletteBackup(const gfx::SnesPalette& palette) { +void PaletteWidget::SavePaletteBackup(const gfx::SnesPalette& palette) { backup_palette_ = palette; } -bool EnhancedPaletteEditor::RestorePaletteBackup(gfx::SnesPalette& palette) { +bool PaletteWidget::RestorePaletteBackup(gfx::SnesPalette& palette) { if (backup_palette_.size() == 0) { return false; } @@ -203,7 +203,7 @@ bool EnhancedPaletteEditor::RestorePaletteBackup(gfx::SnesPalette& palette) { return true; } -void EnhancedPaletteEditor::DrawPaletteGrid(gfx::SnesPalette& palette, int cols) { +void PaletteWidget::DrawPaletteGrid(gfx::SnesPalette& palette, int cols) { for (int i = 0; i < static_cast(palette.size()); i++) { if (i % cols != 0) ImGui::SameLine(); @@ -281,7 +281,7 @@ void EnhancedPaletteEditor::DrawPaletteGrid(gfx::SnesPalette& palette, int cols) } } -void EnhancedPaletteEditor::DrawROMPaletteSelector() { +void PaletteWidget::DrawROMPaletteSelector() { if (!rom_palettes_loaded_) { LoadROMPalettes(); } @@ -323,7 +323,7 @@ void EnhancedPaletteEditor::DrawROMPaletteSelector() { } } -void EnhancedPaletteEditor::DrawColorEditControls(gfx::SnesColor& color, int color_index) { +void PaletteWidget::DrawColorEditControls(gfx::SnesColor& color, int color_index) { ImVec4 rgba = color.rgb(); ImGui::PushID(color_index); @@ -359,7 +359,7 @@ void EnhancedPaletteEditor::DrawColorEditControls(gfx::SnesColor& color, int col ImGui::PopID(); } -void EnhancedPaletteEditor::DrawPaletteAnalysis(const gfx::SnesPalette& palette) { +void PaletteWidget::DrawPaletteAnalysis(const gfx::SnesPalette& palette) { ImGui::Text("Palette Information:"); ImGui::Text("Size: %zu colors", palette.size()); @@ -413,7 +413,7 @@ void EnhancedPaletteEditor::DrawPaletteAnalysis(const gfx::SnesPalette& palette) ImGui::ProgressBar(avg_brightness, ImVec2(-1, 0), "Avg"); } -void EnhancedPaletteEditor::LoadROMPalettes() { +void PaletteWidget::LoadROMPalettes() { if (!rom_ || rom_palettes_loaded_) return; try { diff --git a/src/app/gui/enhanced_palette_editor.h b/src/app/gui/widgets/palette_widget.h similarity index 79% rename from src/app/gui/enhanced_palette_editor.h rename to src/app/gui/widgets/palette_widget.h index da59fe49..91b6d296 100644 --- a/src/app/gui/enhanced_palette_editor.h +++ b/src/app/gui/widgets/palette_widget.h @@ -1,5 +1,5 @@ -#ifndef YAZE_APP_GUI_ENHANCED_PALETTE_EDITOR_H -#define YAZE_APP_GUI_ENHANCED_PALETTE_EDITOR_H +#ifndef YAZE_APP_GUI_PALETTE_WIDGET_H +#define YAZE_APP_GUI_PALETTE_WIDGET_H #include #include @@ -12,11 +12,18 @@ namespace yaze { namespace gui { /** - * @brief Enhanced palette editor with ROM integration and analysis tools + * @brief Palette widget with ROM integration, analysis tools, and AI tool call support + * + * This widget provides comprehensive palette editing capabilities including: + * - Grid-based color editing with preview + * - ROM palette browser and manager + * - Color analysis and statistics + * - Export/import functionality + * - AI agent tool call integration for programmatic palette access */ -class EnhancedPaletteEditor { +class PaletteWidget { public: - EnhancedPaletteEditor() = default; + PaletteWidget() = default; /** * @brief Initialize the palette editor with ROM data @@ -89,4 +96,4 @@ private: } // namespace gui } // namespace yaze -#endif // YAZE_APP_GUI_ENHANCED_PALETTE_EDITOR_H +#endif // YAZE_APP_GUI_WIDGETS_PALETTE_WIDGET_H