From 14a3084c7f0ebab7c06f067ea6c77c654c22f33b Mon Sep 17 00:00:00 2001 From: scawful Date: Sun, 28 Dec 2025 10:51:47 -0600 Subject: [PATCH] imgui-frontend-engineer: refresh editor UI layout --- src/app/editor/layout/layout_coordinator.cc | 7 +- src/app/editor/menu/activity_bar.cc | 5 +- src/app/editor/menu/right_panel_manager.cc | 67 ++- src/app/editor/system/panel_manager.h | 8 + src/app/editor/ui/dashboard_panel.cc | 434 ++++++++++++++----- src/app/editor/ui/dashboard_panel.h | 4 +- src/app/editor/ui/editor_selection_dialog.cc | 393 ++++++++++++----- src/app/editor/ui/editor_selection_dialog.h | 4 +- src/app/editor/ui/popup_manager.cc | 4 +- src/app/editor/ui/ui_coordinator.cc | 152 ++++++- src/app/editor/ui/ui_coordinator.h | 4 + src/app/editor/ui/welcome_screen.cc | 340 +++++++++++---- src/app/editor/ui/welcome_screen.h | 3 +- 13 files changed, 1097 insertions(+), 328 deletions(-) diff --git a/src/app/editor/layout/layout_coordinator.cc b/src/app/editor/layout/layout_coordinator.cc index 1766f527..dd25d3bc 100644 --- a/src/app/editor/layout/layout_coordinator.cc +++ b/src/app/editor/layout/layout_coordinator.cc @@ -47,7 +47,12 @@ float LayoutCoordinator::GetLeftLayoutOffset() const { // Add Side Panel width if expanded if (panel_manager_->IsPanelExpanded()) { - width += PanelManager::GetSidePanelWidth(); + float viewport_width = 0.0f; + if (ImGui::GetCurrentContext()) { + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + viewport_width = viewport ? viewport->WorkSize.x : 0.0f; + } + width += PanelManager::GetSidePanelWidthForViewport(viewport_width); } return width; diff --git a/src/app/editor/menu/activity_bar.cc b/src/app/editor/menu/activity_bar.cc index 0611eae5..c523f1c0 100644 --- a/src/app/editor/menu/activity_bar.cc +++ b/src/app/editor/menu/activity_bar.cc @@ -210,7 +210,8 @@ void ActivityBar::DrawSidePanel(size_t session_id, const std::string& category, const auto& theme = gui::ThemeManager::Get().GetCurrentTheme(); const ImGuiViewport* viewport = ImGui::GetMainViewport(); const float bar_width = PanelManager::GetSidebarWidth(); - const float panel_width = PanelManager::GetSidePanelWidth(); + const float panel_width = + PanelManager::GetSidePanelWidthForViewport(viewport->WorkSize.x); ImGui::SetNextWindowPos( ImVec2(viewport->WorkPos.x + bar_width, viewport->WorkPos.y)); @@ -661,4 +662,4 @@ void ActivityBar::DrawPanelBrowser(size_t session_id, bool* p_open) { } } // namespace editor -} // namespace yaze \ No newline at end of file +} // namespace yaze diff --git a/src/app/editor/menu/right_panel_manager.cc b/src/app/editor/menu/right_panel_manager.cc index fc794e58..cb657ee7 100644 --- a/src/app/editor/menu/right_panel_manager.cc +++ b/src/app/editor/menu/right_panel_manager.cc @@ -1,6 +1,7 @@ #include "app/editor/menu/right_panel_manager.h" #include +#include #include "absl/strings/str_format.h" #include "app/editor/agent/agent_chat.h" @@ -14,10 +15,32 @@ #include "app/gui/core/style.h" #include "app/gui/core/theme_manager.h" #include "imgui/imgui.h" +#include "util/platform_paths.h" namespace yaze { namespace editor { +namespace { + +std::string ResolveAgentChatHistoryPath() { + auto agent_dir = util::PlatformPaths::GetAppDataSubdirectory("agent"); + if (agent_dir.ok()) { + return (*agent_dir / "agent_chat_history.json").string(); + } + auto docs_dir = util::PlatformPaths::GetUserDocumentsSubdirectory("agent"); + if (docs_dir.ok()) { + return (*docs_dir / "agent_chat_history.json").string(); + } + auto temp_dir = util::PlatformPaths::GetTempDirectory(); + if (temp_dir.ok()) { + return (*temp_dir / "agent_chat_history.json").string(); + } + return (std::filesystem::current_path() / "agent_chat_history.json") + .string(); +} + +} // namespace + const char* GetPanelTypeName(RightPanelManager::PanelType type) { switch (type) { case RightPanelManager::PanelType::kNone: @@ -89,24 +112,50 @@ float RightPanelManager::GetPanelWidth() const { return 0.0f; } + float width = 0.0f; switch (active_panel_) { case PanelType::kAgentChat: - return agent_chat_width_; + width = agent_chat_width_; + break; case PanelType::kProposals: - return proposals_width_; + width = proposals_width_; + break; case PanelType::kSettings: - return settings_width_; + width = settings_width_; + break; case PanelType::kHelp: - return help_width_; + width = help_width_; + break; case PanelType::kNotifications: - return notifications_width_; + width = notifications_width_; + break; case PanelType::kProperties: - return properties_width_; + width = properties_width_; + break; case PanelType::kProject: - return project_width_; + width = project_width_; + break; default: - return 0.0f; + width = 0.0f; + break; } + + ImGuiContext* context = ImGui::GetCurrentContext(); + if (!context) { + return width; + } + + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + if (!viewport) { + return width; + } + + const float max_width = viewport->WorkSize.x * 0.35f; + if (max_width > 0.0f && width > max_width) { + width = max_width; + } + + return width; } void RightPanelManager::SetPanelWidth(PanelType type, float width) { @@ -503,7 +552,7 @@ void RightPanelManager::DrawAgentChatPanel() { } ImGui::SameLine(); if (ImGui::Button(ICON_MD_FILE_DOWNLOAD " Save", half_width)) { - agent_chat_->SaveHistory(".yaze/agent_chat_history.json"); + agent_chat_->SaveHistory(ResolveAgentChatHistoryPath()); } ImGui::PopStyleColor(3); ImGui::PopStyleVar(); diff --git a/src/app/editor/system/panel_manager.h b/src/app/editor/system/panel_manager.h index 8fc76e88..b4d531c9 100644 --- a/src/app/editor/system/panel_manager.h +++ b/src/app/editor/system/panel_manager.h @@ -214,6 +214,14 @@ class PanelManager { static constexpr float GetSidebarWidth() { return 48.0f; } static constexpr float GetSidePanelWidth() { return 250.0f; } + static float GetSidePanelWidthForViewport(float viewport_width) { + float width = GetSidePanelWidth(); + const float max_width = viewport_width * 0.28f; + if (viewport_width > 0.0f && max_width > 0.0f && width > max_width) { + width = max_width; + } + return width; + } static constexpr float GetCollapsedSidebarWidth() { return 16.0f; } static std::string GetCategoryIcon(const std::string& category); diff --git a/src/app/editor/ui/dashboard_panel.cc b/src/app/editor/ui/dashboard_panel.cc index a47283a7..adf79b46 100644 --- a/src/app/editor/ui/dashboard_panel.cc +++ b/src/app/editor/ui/dashboard_panel.cc @@ -10,6 +10,8 @@ #include "app/gui/core/icons.h" #include "app/gui/core/platform_keys.h" #include "app/gui/core/style.h" +#include "app/gui/core/theme_manager.h" +#include "app/gui/core/color.h" #include "app/gui/widgets/themed_widgets.h" #include "imgui/imgui.h" #include "imgui/imgui_internal.h" @@ -18,52 +20,163 @@ namespace yaze { namespace editor { +namespace { + +constexpr float kDashboardCardBaseWidth = 180.0f; +constexpr float kDashboardCardBaseHeight = 120.0f; +constexpr float kDashboardCardWidthMaxFactor = 1.35f; +constexpr float kDashboardCardHeightMaxFactor = 1.35f; +constexpr float kDashboardCardMinWidthFactor = 0.9f; +constexpr float kDashboardCardMinHeightFactor = 0.9f; +constexpr int kDashboardMaxColumns = 5; +constexpr float kDashboardRecentBaseWidth = 150.0f; +constexpr float kDashboardRecentBaseHeight = 35.0f; +constexpr float kDashboardRecentWidthMaxFactor = 1.3f; +constexpr int kDashboardMaxRecentColumns = 4; + +struct FlowLayout { + int columns = 1; + float item_width = 0.0f; + float item_height = 0.0f; + float spacing = 0.0f; +}; + +FlowLayout ComputeFlowLayout(float avail_width, float min_width, + float max_width, float min_height, + float max_height, float aspect_ratio, + float spacing, int max_columns, + int item_count) { + FlowLayout layout; + layout.spacing = spacing; + if (avail_width <= 0.0f) { + layout.columns = 1; + layout.item_width = min_width; + layout.item_height = min_height; + return layout; + } + + const int clamped_max = + std::max(1, max_columns > 0 ? max_columns : item_count); + const auto width_for_columns = [avail_width, spacing](int columns) { + return (avail_width - spacing * static_cast(columns - 1)) / + static_cast(columns); + }; + + int columns = + std::max(1, static_cast((avail_width + spacing) / + (min_width + spacing))); + columns = std::min(columns, clamped_max); + if (item_count > 0) { + columns = std::min(columns, item_count); + } + columns = std::max(columns, 1); + + float width = width_for_columns(columns); + while (columns < clamped_max && width > max_width) { + columns += 1; + width = width_for_columns(columns); + } + + const float clamped_max_width = std::min(max_width, avail_width); + const float clamped_min_width = std::min(min_width, clamped_max_width); + layout.item_width = std::clamp(width, clamped_min_width, clamped_max_width); + layout.item_height = + std::clamp(layout.item_width * aspect_ratio, min_height, max_height); + + layout.columns = columns; + + return layout; +} + +ImVec4 ScaleColor(const ImVec4& color, float scale, float alpha) { + return ImVec4(color.x * scale, color.y * scale, color.z * scale, alpha); +} + +ImVec4 ScaleColor(const ImVec4& color, float scale) { + return ScaleColor(color, scale, color.w); +} + +ImVec4 WithAlpha(ImVec4 color, float alpha) { + color.w = alpha; + return color; +} + +ImVec4 GetEditorAccentColor(EditorType type, const gui::Theme& theme) { + switch (type) { + case EditorType::kOverworld: + return gui::ConvertColorToImVec4(theme.success); + case EditorType::kDungeon: + return gui::ConvertColorToImVec4(theme.secondary); + case EditorType::kGraphics: + return gui::ConvertColorToImVec4(theme.warning); + case EditorType::kSprite: + return gui::ConvertColorToImVec4(theme.info); + case EditorType::kMessage: + return gui::ConvertColorToImVec4(theme.primary); + case EditorType::kMusic: + return gui::ConvertColorToImVec4(theme.accent); + case EditorType::kPalette: + return gui::ConvertColorToImVec4(theme.error); + case EditorType::kScreen: + return gui::ConvertColorToImVec4(theme.info); + case EditorType::kAssembly: + return gui::ConvertColorToImVec4(theme.text_secondary); + case EditorType::kHex: + return gui::ConvertColorToImVec4(theme.success); + case EditorType::kEmulator: + return gui::ConvertColorToImVec4(theme.info); + case EditorType::kAgent: + return gui::ConvertColorToImVec4(theme.accent); + case EditorType::kSettings: + case EditorType::kUnknown: + default: + return gui::ConvertColorToImVec4(theme.text_primary); + } +} + +} // namespace + DashboardPanel::DashboardPanel(EditorManager* editor_manager) : editor_manager_(editor_manager), window_("Dashboard", ICON_MD_DASHBOARD) { window_.SetDefaultSize(950, 650); window_.SetPosition(gui::PanelWindow::Position::Center); - // Initialize editor list with colors matching EditorSelectionDialog // Use platform-aware shortcut strings (Cmd on macOS, Ctrl elsewhere) const char* ctrl = gui::GetCtrlDisplayName(); editors_ = { - {"Overworld", ICON_MD_MAP, "Edit overworld maps, entrances, and properties", - absl::StrFormat("%s+1", ctrl), EditorType::kOverworld, - ImVec4(0.133f, 0.545f, 0.133f, 1.0f)}, // Hyrule green - {"Dungeon", ICON_MD_CASTLE, "Design dungeon rooms, layouts, and mechanics", - absl::StrFormat("%s+2", ctrl), EditorType::kDungeon, - ImVec4(0.502f, 0.0f, 0.502f, 1.0f)}, // Ganon purple - {"Graphics", ICON_MD_PALETTE, "Modify tiles, palettes, and graphics sets", - absl::StrFormat("%s+3", ctrl), EditorType::kGraphics, - ImVec4(1.0f, 0.843f, 0.0f, 1.0f)}, // Triforce gold - {"Sprites", ICON_MD_EMOJI_EMOTIONS, "Edit sprite graphics and properties", - absl::StrFormat("%s+4", ctrl), EditorType::kSprite, - ImVec4(1.0f, 0.647f, 0.0f, 1.0f)}, // Spirit orange + {"Overworld", ICON_MD_MAP, + "Edit overworld maps, entrances, and properties", + absl::StrFormat("%s+1", ctrl), EditorType::kOverworld}, + {"Dungeon", ICON_MD_CASTLE, + "Design dungeon rooms, layouts, and mechanics", + absl::StrFormat("%s+2", ctrl), EditorType::kDungeon}, + {"Graphics", ICON_MD_PALETTE, + "Modify tiles, palettes, and graphics sets", + absl::StrFormat("%s+3", ctrl), EditorType::kGraphics}, + {"Sprites", ICON_MD_EMOJI_EMOTIONS, + "Edit sprite graphics and properties", + absl::StrFormat("%s+4", ctrl), EditorType::kSprite}, {"Messages", ICON_MD_CHAT_BUBBLE, "Edit dialogue, signs, and text", - absl::StrFormat("%s+5", ctrl), EditorType::kMessage, - ImVec4(0.196f, 0.6f, 0.8f, 1.0f)}, // Master sword blue + absl::StrFormat("%s+5", ctrl), EditorType::kMessage}, {"Music", ICON_MD_MUSIC_NOTE, "Configure music and sound effects", - absl::StrFormat("%s+6", ctrl), EditorType::kMusic, - ImVec4(0.416f, 0.353f, 0.804f, 1.0f)}, // Shadow purple - {"Palettes", ICON_MD_COLOR_LENS, "Edit color palettes and animations", - absl::StrFormat("%s+7", ctrl), EditorType::kPalette, - ImVec4(0.863f, 0.078f, 0.235f, 1.0f)}, // Heart red + absl::StrFormat("%s+6", ctrl), EditorType::kMusic}, + {"Palettes", ICON_MD_COLOR_LENS, + "Edit color palettes and animations", + absl::StrFormat("%s+7", ctrl), EditorType::kPalette}, {"Screens", ICON_MD_TV, "Edit title screen and ending screens", - absl::StrFormat("%s+8", ctrl), EditorType::kScreen, - ImVec4(0.4f, 0.8f, 1.0f, 1.0f)}, // Sky blue + absl::StrFormat("%s+8", ctrl), EditorType::kScreen}, {"Assembly", ICON_MD_CODE, "Write and edit assembly code", - absl::StrFormat("%s+9", ctrl), EditorType::kAssembly, - ImVec4(0.8f, 0.8f, 0.8f, 1.0f)}, // Silver - {"Hex Editor", ICON_MD_DATA_ARRAY, "Direct ROM memory editing and comparison", - absl::StrFormat("%s+0", ctrl), EditorType::kHex, - ImVec4(0.2f, 0.8f, 0.4f, 1.0f)}, // Matrix green - {"Emulator", ICON_MD_VIDEOGAME_ASSET, "Test and debug your ROM in real-time", - absl::StrFormat("%s+Shift+E", ctrl), EditorType::kEmulator, - ImVec4(0.2f, 0.6f, 1.0f, 1.0f)}, // Emulator blue - {"AI Agent", ICON_MD_SMART_TOY, "Configure AI agent, collaboration, and automation", - absl::StrFormat("%s+Shift+A", ctrl), EditorType::kAgent, - ImVec4(0.8f, 0.4f, 1.0f, 1.0f)}, // Purple/magenta + absl::StrFormat("%s+9", ctrl), EditorType::kAssembly}, + {"Hex Editor", ICON_MD_DATA_ARRAY, + "Direct ROM memory editing and comparison", + absl::StrFormat("%s+0", ctrl), EditorType::kHex}, + {"Emulator", ICON_MD_VIDEOGAME_ASSET, + "Test and debug your ROM in real-time", + absl::StrFormat("%s+Shift+E", ctrl), EditorType::kEmulator}, + {"AI Agent", ICON_MD_SMART_TOY, + "Configure AI agent, collaboration, and automation", + absl::StrFormat("%s+Shift+A", ctrl), EditorType::kAgent}, }; LoadRecentEditors(); @@ -81,25 +194,27 @@ void DashboardPanel::Draw() { DrawWelcomeHeader(); ImGui::Separator(); ImGui::Spacing(); - + DrawRecentEditors(); if (!recent_editors_.empty()) { ImGui::Separator(); ImGui::Spacing(); } - DrawEditorGrid(); } window_.End(); } void DashboardPanel::DrawWelcomeHeader() { + const auto& theme = gui::ThemeManager::Get().GetCurrentTheme(); + const ImVec4 accent = gui::ConvertColorToImVec4(theme.accent); + const ImVec4 text_secondary = gui::ConvertColorToImVec4(theme.text_secondary); + ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[2]); // Large font - ImVec4 title_color = ImVec4(1.0f, 0.843f, 0.0f, 1.0f); // Triforce gold - ImGui::TextColored(title_color, ICON_MD_EDIT " Select an Editor"); + ImGui::TextColored(accent, ICON_MD_EDIT " Select an Editor"); ImGui::PopFont(); - ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), + ImGui::TextColored(text_secondary, "Choose an editor to begin working on your ROM. " "You can open multiple editors simultaneously."); } @@ -107,29 +222,65 @@ void DashboardPanel::DrawWelcomeHeader() { void DashboardPanel::DrawRecentEditors() { if (recent_editors_.empty()) return; - ImGui::TextColored(ImVec4(1.0f, 0.843f, 0.0f, 1.0f), - ICON_MD_HISTORY " Recently Used"); + const auto& theme = gui::ThemeManager::Get().GetCurrentTheme(); + const ImVec4 accent = gui::ConvertColorToImVec4(theme.accent); + ImGui::TextColored(accent, 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; }); + const ImGuiStyle& style = ImGui::GetStyle(); + const float avail_width = ImGui::GetContentRegionAvail().x; + const float min_width = kDashboardRecentBaseWidth; + const float max_width = kDashboardRecentBaseWidth * + kDashboardRecentWidthMaxFactor; + const float height = std::max(kDashboardRecentBaseHeight, + ImGui::GetFrameHeight()); + const float spacing = style.ItemSpacing.x; + const bool stack_items = avail_width < min_width * 1.6f; + FlowLayout row_layout{}; + if (stack_items) { + row_layout.columns = 1; + row_layout.item_width = avail_width; + row_layout.item_height = height; + row_layout.spacing = spacing; + } else { + row_layout = ComputeFlowLayout( + avail_width, min_width, max_width, height, height, + height / std::max(min_width, 1.0f), spacing, kDashboardMaxRecentColumns, + static_cast(recent_editors_.size())); + } - if (it != editors_.end()) { - // Use editor's theme color for button - ImVec4 color = it->color; - ImGui::PushStyleColor( - ImGuiCol_Button, - ImVec4(color.x * 0.5f, color.y * 0.5f, color.z * 0.5f, 0.7f)); - ImGui::PushStyleColor( - ImGuiCol_ButtonHovered, - ImVec4(color.x * 0.7f, color.y * 0.7f, color.z * 0.7f, 0.9f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, color); + ImGuiTableFlags table_flags = ImGuiTableFlags_SizingFixedFit | + ImGuiTableFlags_NoPadOuterX; + const ImVec2 cell_padding(row_layout.spacing * 0.5f, + style.ItemSpacing.y * 0.4f); + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, cell_padding); + if (ImGui::BeginTable("DashboardRecentGrid", row_layout.columns, + table_flags)) { + 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()) { + continue; + } + + ImGui::TableNextColumn(); + + const ImVec4 base_color = GetEditorAccentColor(it->type, theme); + ImGui::PushStyleColor(ImGuiCol_Button, + ScaleColor(base_color, 0.5f, 0.7f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, + ScaleColor(base_color, 0.7f, 0.9f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, + WithAlpha(base_color, 1.0f)); + + ImVec2 button_size(stack_items ? avail_width : row_layout.item_width, + row_layout.item_height > 0.0f ? row_layout.item_height + : height); if (ImGui::Button(absl::StrCat(it->icon, " ", it->name).c_str(), - ImVec2(150, 35))) { + button_size)) { if (editor_manager_) { MarkRecentlyUsed(type); editor_manager_->SwitchToEditor(type); @@ -142,151 +293,212 @@ void DashboardPanel::DrawRecentEditors() { if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s", it->description.c_str()); } - - ImGui::SameLine(); } + ImGui::EndTable(); } - - ImGui::NewLine(); + ImGui::PopStyleVar(); } void DashboardPanel::DrawEditorGrid() { ImGui::Text(ICON_MD_APPS " All Editors"); ImGui::Spacing(); - const float card_width = 180.0f; - const float spacing = ImGui::GetStyle().ItemSpacing.x; - const float window_width = ImGui::GetContentRegionAvail().x; - int columns = static_cast(window_width / (card_width + spacing)); - columns = std::max(columns, 1); + const ImGuiStyle& style = ImGui::GetStyle(); + const float avail_width = ImGui::GetContentRegionAvail().x; + const float min_width = kDashboardCardBaseWidth * kDashboardCardMinWidthFactor; + const float max_width = + kDashboardCardBaseWidth * kDashboardCardWidthMaxFactor; + const float min_height = + std::max(kDashboardCardBaseHeight * kDashboardCardMinHeightFactor, + ImGui::GetFrameHeight() * 3.2f); + const float max_height = + kDashboardCardBaseHeight * kDashboardCardHeightMaxFactor; + const float aspect_ratio = + kDashboardCardBaseHeight / std::max(kDashboardCardBaseWidth, 1.0f); + const float spacing = style.ItemSpacing.x; - if (ImGui::BeginTable("EditorGrid", columns)) { + FlowLayout layout = ComputeFlowLayout( + avail_width, min_width, max_width, min_height, max_height, + aspect_ratio, spacing, kDashboardMaxColumns, + static_cast(editors_.size())); + + ImGuiTableFlags table_flags = ImGuiTableFlags_SizingFixedFit | + ImGuiTableFlags_NoPadOuterX; + const ImVec2 cell_padding(layout.spacing * 0.5f, + style.ItemSpacing.y * 0.5f); + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, cell_padding); + if (ImGui::BeginTable("DashboardEditorGrid", layout.columns, table_flags)) { for (size_t i = 0; i < editors_.size(); ++i) { ImGui::TableNextColumn(); - DrawEditorPanel(editors_[i], static_cast(i)); + DrawEditorPanel(editors_[i], static_cast(i), + ImVec2(layout.item_width, layout.item_height)); } ImGui::EndTable(); } + ImGui::PopStyleVar(); } -void DashboardPanel::DrawEditorPanel(const EditorInfo& info, int index) { +void DashboardPanel::DrawEditorPanel(const EditorInfo& info, int index, + const ImVec2& card_size) { ImGui::PushID(index); - ImVec2 button_size(180, 120); - ImVec2 cursor_pos = ImGui::GetCursorScreenPos(); + const auto& theme = gui::ThemeManager::Get().GetCurrentTheme(); + const ImVec4 base_color = GetEditorAccentColor(info.type, theme); + const ImVec4 text_primary = gui::ConvertColorToImVec4(theme.text_primary); + const ImVec4 text_secondary = gui::ConvertColorToImVec4(theme.text_secondary); + const ImVec4 accent = gui::ConvertColorToImVec4(theme.accent); + + const ImGuiStyle& style = ImGui::GetStyle(); + const float line_height = ImGui::GetTextLineHeight(); + const float padding_x = std::max(style.FramePadding.x, card_size.x * 0.06f); + const float padding_y = std::max(style.FramePadding.y, card_size.y * 0.08f); + + const float footer_height = info.shortcut.empty() ? 0.0f : line_height; + const float footer_spacing = info.shortcut.empty() ? 0.0f : style.ItemSpacing.y; + const float available_icon_height = + card_size.y - padding_y * 2.0f - line_height - footer_height - footer_spacing; + const float min_icon_radius = line_height * 0.9f; + float max_icon_radius = card_size.y * 0.24f; + max_icon_radius = std::max(max_icon_radius, min_icon_radius); + const float icon_radius = + std::clamp(available_icon_height * 0.5f, min_icon_radius, max_icon_radius); + + const ImVec2 cursor_pos = ImGui::GetCursorScreenPos(); ImDrawList* draw_list = ImGui::GetWindowDrawList(); + const ImVec2 icon_center(cursor_pos.x + card_size.x * 0.5f, + cursor_pos.y + padding_y + icon_radius); + float title_y = icon_center.y + icon_radius + style.ItemSpacing.y; + const float footer_y = cursor_pos.y + card_size.y - padding_y - footer_height; + if (title_y + line_height > footer_y - style.ItemSpacing.y) { + title_y = footer_y - line_height - style.ItemSpacing.y; + } bool is_recent = std::find(recent_editors_.begin(), recent_editors_.end(), info.type) != recent_editors_.end(); // Create gradient background - ImVec4 base_color = info.color; - ImU32 color_top = ImGui::GetColorU32(ImVec4( - base_color.x * 0.4f, base_color.y * 0.4f, base_color.z * 0.4f, 0.8f)); - ImU32 color_bottom = ImGui::GetColorU32(ImVec4( - base_color.x * 0.2f, base_color.y * 0.2f, base_color.z * 0.2f, 0.9f)); + ImU32 color_top = ImGui::GetColorU32(ScaleColor(base_color, 0.4f, 0.85f)); + ImU32 color_bottom = + ImGui::GetColorU32(ScaleColor(base_color, 0.2f, 0.9f)); // Draw gradient card background draw_list->AddRectFilledMultiColor( cursor_pos, - ImVec2(cursor_pos.x + button_size.x, cursor_pos.y + button_size.y), + ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y), color_top, color_top, color_bottom, color_bottom); // Colored border ImU32 border_color = is_recent ? ImGui::GetColorU32( - ImVec4(base_color.x, base_color.y, base_color.z, 1.0f)) - : ImGui::GetColorU32(ImVec4(base_color.x * 0.6f, base_color.y * 0.6f, - base_color.z * 0.6f, 0.7f)); + WithAlpha(base_color, 1.0f)) + : ImGui::GetColorU32(ScaleColor(base_color, 0.6f, 0.7f)); + const float rounding = std::max(style.FrameRounding, card_size.y * 0.05f); + const float border_thickness = + is_recent ? std::max(2.0f, style.FrameBorderSize + 1.0f) + : std::max(1.0f, style.FrameBorderSize); draw_list->AddRect( cursor_pos, - ImVec2(cursor_pos.x + button_size.x, cursor_pos.y + button_size.y), - border_color, 4.0f, 0, is_recent ? 3.0f : 2.0f); + ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y), + border_color, rounding, 0, border_thickness); // Recent indicator badge if (is_recent) { - ImVec2 badge_pos(cursor_pos.x + button_size.x - 25, cursor_pos.y + 5); - draw_list->AddCircleFilled(badge_pos, 12, ImGui::GetColorU32(base_color), - 16); - ImGui::SetCursorScreenPos(ImVec2(badge_pos.x - 6, badge_pos.y - 8)); - ImGui::TextColored(ImVec4(1, 1, 1, 1), ICON_MD_STAR); + const float badge_radius = + std::clamp(line_height * 0.6f, line_height * 0.4f, line_height); + ImVec2 badge_pos(cursor_pos.x + card_size.x - padding_x - badge_radius, + cursor_pos.y + padding_y + badge_radius); + draw_list->AddCircleFilled(badge_pos, badge_radius, + ImGui::GetColorU32(base_color), 16); + ImVec2 star_size = ImGui::CalcTextSize(ICON_MD_STAR); + ImGui::SetCursorScreenPos( + ImVec2(badge_pos.x - star_size.x * 0.5f, + badge_pos.y - star_size.y * 0.5f)); + ImGui::TextColored(text_primary, ICON_MD_STAR); } // Make button transparent (we draw our own background) - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + ImVec4 button_bg = ImGui::GetStyleColorVec4(ImGuiCol_Button); + button_bg.w = 0.0f; + ImGui::PushStyleColor(ImGuiCol_Button, button_bg); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, - ImVec4(base_color.x * 0.3f, base_color.y * 0.3f, - base_color.z * 0.3f, 0.5f)); + ScaleColor(base_color, 0.3f, 0.5f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, - ImVec4(base_color.x * 0.5f, base_color.y * 0.5f, - base_color.z * 0.5f, 0.7f)); + ScaleColor(base_color, 0.5f, 0.7f)); ImGui::SetCursorScreenPos(cursor_pos); bool clicked = - ImGui::Button(absl::StrCat("##", info.name).c_str(), button_size); + ImGui::Button(absl::StrCat("##", info.name).c_str(), card_size); bool is_hovered = ImGui::IsItemHovered(); ImGui::PopStyleColor(3); // Draw icon with colored background circle - ImVec2 icon_center(cursor_pos.x + button_size.x / 2, cursor_pos.y + 30); ImU32 icon_bg = ImGui::GetColorU32(base_color); - draw_list->AddCircleFilled(icon_center, 22, icon_bg, 32); + draw_list->AddCircleFilled(icon_center, icon_radius, icon_bg, 32); // Draw icon - ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[2]); // Larger font for icon + ImFont* icon_font = ImGui::GetFont(); + if (ImGui::GetIO().Fonts->Fonts.size() > 2 && + card_size.y >= kDashboardCardBaseHeight) { + icon_font = ImGui::GetIO().Fonts->Fonts[2]; + } else if (ImGui::GetIO().Fonts->Fonts.size() > 1) { + icon_font = ImGui::GetIO().Fonts->Fonts[1]; + } + ImGui::PushFont(icon_font); ImVec2 icon_size = ImGui::CalcTextSize(info.icon.c_str()); ImGui::SetCursorScreenPos( ImVec2(icon_center.x - icon_size.x / 2, icon_center.y - icon_size.y / 2)); - ImGui::TextColored(ImVec4(1, 1, 1, 1), "%s", info.icon.c_str()); + ImGui::TextColored(text_primary, "%s", info.icon.c_str()); ImGui::PopFont(); // Draw name - ImGui::SetCursorScreenPos(ImVec2(cursor_pos.x + 10, cursor_pos.y + 65)); - ImGui::PushTextWrapPos(cursor_pos.x + button_size.x - 10); - ImVec2 name_size = ImGui::CalcTextSize(info.name.c_str()); + const float name_wrap_width = card_size.x - padding_x * 2.0f; + ImGui::PushTextWrapPos(cursor_pos.x + card_size.x - padding_x); + ImVec2 name_size = + ImGui::CalcTextSize(info.name.c_str(), nullptr, false, name_wrap_width); ImGui::SetCursorScreenPos(ImVec2( - cursor_pos.x + (button_size.x - name_size.x) / 2, cursor_pos.y + 65)); + cursor_pos.x + (card_size.x - name_size.x) / 2.0f, title_y)); ImGui::TextColored(base_color, "%s", info.name.c_str()); ImGui::PopTextWrapPos(); // Draw shortcut hint if available if (!info.shortcut.empty()) { ImGui::SetCursorScreenPos( - ImVec2(cursor_pos.x + 10, cursor_pos.y + button_size.y - 20)); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "%s", info.shortcut.c_str()); + ImVec2(cursor_pos.x + padding_x, footer_y)); + ImGui::TextColored(text_secondary, "%s", info.shortcut.c_str()); } // Hover glow effect if (is_hovered) { - ImU32 glow_color = ImGui::GetColorU32( - ImVec4(base_color.x, base_color.y, base_color.z, 0.2f)); + ImU32 glow_color = + ImGui::GetColorU32(ScaleColor(base_color, 1.0f, 0.18f)); draw_list->AddRectFilled( cursor_pos, - ImVec2(cursor_pos.x + button_size.x, cursor_pos.y + button_size.y), - glow_color, 4.0f); + ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y), + glow_color, rounding); } // Enhanced tooltip if (is_hovered) { - ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always); + const float tooltip_width = std::clamp(card_size.x * 1.4f, 240.0f, 340.0f); + ImGui::SetNextWindowSize(ImVec2(tooltip_width, 0), ImGuiCond_Always); ImGui::BeginTooltip(); ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[1]); // Medium font ImGui::TextColored(base_color, "%s %s", info.icon.c_str(), info.name.c_str()); ImGui::PopFont(); ImGui::Separator(); - ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + 280); + ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + tooltip_width - 20.0f); ImGui::TextWrapped("%s", info.description.c_str()); ImGui::PopTextWrapPos(); if (!info.shortcut.empty()) { ImGui::Spacing(); - ImGui::TextColored(base_color, ICON_MD_KEYBOARD " %s", info.shortcut.c_str()); + ImGui::TextColored(base_color, ICON_MD_KEYBOARD " %s", + info.shortcut.c_str()); } if (is_recent) { ImGui::Spacing(); - ImGui::TextColored(ImVec4(1.0f, 0.843f, 0.0f, 1.0f), - ICON_MD_STAR " Recently used"); + ImGui::TextColored(accent, ICON_MD_STAR " Recently used"); } ImGui::EndTooltip(); } diff --git a/src/app/editor/ui/dashboard_panel.h b/src/app/editor/ui/dashboard_panel.h index a0bb8b22..de2d9431 100644 --- a/src/app/editor/ui/dashboard_panel.h +++ b/src/app/editor/ui/dashboard_panel.h @@ -38,14 +38,14 @@ class DashboardPanel { std::string description; std::string shortcut; EditorType type; - ImVec4 color; bool recently_used = false; }; void DrawWelcomeHeader(); void DrawRecentEditors(); void DrawEditorGrid(); - void DrawEditorPanel(const EditorInfo& info, int index); + void DrawEditorPanel(const EditorInfo& info, int index, + const ImVec2& card_size); EditorManager* editor_manager_; gui::PanelWindow window_; diff --git a/src/app/editor/ui/editor_selection_dialog.cc b/src/app/editor/ui/editor_selection_dialog.cc index 17547bcc..1a71ab21 100644 --- a/src/app/editor/ui/editor_selection_dialog.cc +++ b/src/app/editor/ui/editor_selection_dialog.cc @@ -9,76 +9,177 @@ #include "app/gui/core/icons.h" #include "app/gui/core/platform_keys.h" #include "app/gui/core/style.h" +#include "app/gui/core/theme_manager.h" +#include "app/gui/core/color.h" #include "imgui/imgui.h" #include "util/file_util.h" namespace yaze { namespace editor { +namespace { + +constexpr float kEditorSelectBaseFontSize = 16.0f; +constexpr float kEditorSelectCardBaseWidth = 180.0f; +constexpr float kEditorSelectCardBaseHeight = 120.0f; +constexpr float kEditorSelectCardWidthMaxFactor = 1.35f; +constexpr float kEditorSelectCardHeightMaxFactor = 1.35f; +constexpr float kEditorSelectRecentBaseWidth = 150.0f; +constexpr float kEditorSelectRecentBaseHeight = 35.0f; +constexpr float kEditorSelectRecentWidthMaxFactor = 1.3f; + +struct GridLayout { + int columns = 1; + float item_width = 0.0f; + float item_height = 0.0f; + float spacing = 0.0f; + float row_start_x = 0.0f; +}; + +float GetEditorSelectScale() { + const float font_size = ImGui::GetFontSize(); + if (font_size <= 0.0f) { + return 1.0f; + } + return font_size / kEditorSelectBaseFontSize; +} + +GridLayout ComputeGridLayout(float avail_width, float min_width, + float max_width, float min_height, + float max_height, float preferred_width, + float aspect_ratio, float spacing) { + GridLayout layout; + layout.spacing = spacing; + const auto width_for_columns = [avail_width, spacing](int columns) { + return (avail_width - spacing * static_cast(columns - 1)) / + static_cast(columns); + }; + + layout.columns = + std::max(1, static_cast((avail_width + spacing) / + (preferred_width + spacing))); + + layout.item_width = width_for_columns(layout.columns); + while (layout.columns > 1 && layout.item_width < min_width) { + layout.columns -= 1; + layout.item_width = width_for_columns(layout.columns); + } + + layout.item_width = std::min(layout.item_width, max_width); + layout.item_width = std::min(layout.item_width, avail_width); + layout.item_height = + std::clamp(layout.item_width * aspect_ratio, min_height, max_height); + + const float row_width = + layout.item_width * static_cast(layout.columns) + + spacing * static_cast(layout.columns - 1); + layout.row_start_x = ImGui::GetCursorPosX(); + if (row_width < avail_width) { + layout.row_start_x += (avail_width - row_width) * 0.5f; + } + + return layout; +} + +ImVec4 ScaleColor(const ImVec4& color, float scale, float alpha) { + return ImVec4(color.x * scale, color.y * scale, color.z * scale, alpha); +} + +ImVec4 ScaleColor(const ImVec4& color, float scale) { + return ScaleColor(color, scale, color.w); +} + +ImVec4 WithAlpha(ImVec4 color, float alpha) { + color.w = alpha; + return color; +} + +ImVec4 GetEditorAccentColor(EditorType type, const gui::Theme& theme) { + switch (type) { + case EditorType::kOverworld: + return gui::ConvertColorToImVec4(theme.success); + case EditorType::kDungeon: + return gui::ConvertColorToImVec4(theme.secondary); + case EditorType::kGraphics: + return gui::ConvertColorToImVec4(theme.warning); + case EditorType::kSprite: + return gui::ConvertColorToImVec4(theme.info); + case EditorType::kMessage: + return gui::ConvertColorToImVec4(theme.primary); + case EditorType::kMusic: + return gui::ConvertColorToImVec4(theme.accent); + case EditorType::kPalette: + return gui::ConvertColorToImVec4(theme.error); + case EditorType::kScreen: + return gui::ConvertColorToImVec4(theme.info); + case EditorType::kAssembly: + return gui::ConvertColorToImVec4(theme.text_secondary); + case EditorType::kHex: + return gui::ConvertColorToImVec4(theme.success); + case EditorType::kEmulator: + return gui::ConvertColorToImVec4(theme.info); + case EditorType::kAgent: + return gui::ConvertColorToImVec4(theme.accent); + case EditorType::kSettings: + case EditorType::kUnknown: + default: + return gui::ConvertColorToImVec4(theme.text_primary); + } +} + +} // namespace + EditorSelectionDialog::EditorSelectionDialog() { - // Initialize editor metadata with distinct colors // Use platform-aware shortcut strings (Cmd on macOS, Ctrl elsewhere) const char* ctrl = gui::GetCtrlDisplayName(); editors_ = { {EditorType::kOverworld, "Overworld", ICON_MD_MAP, "Edit overworld maps, entrances, and properties", - absl::StrFormat("%s+1", ctrl), false, true, - ImVec4(0.133f, 0.545f, 0.133f, 1.0f)}, // Hyrule green + absl::StrFormat("%s+1", ctrl), false, true}, {EditorType::kDungeon, "Dungeon", ICON_MD_CASTLE, "Design dungeon rooms, layouts, and mechanics", - absl::StrFormat("%s+2", ctrl), false, true, - ImVec4(0.502f, 0.0f, 0.502f, 1.0f)}, // Ganon purple + absl::StrFormat("%s+2", ctrl), false, true}, {EditorType::kGraphics, "Graphics", ICON_MD_PALETTE, "Modify tiles, palettes, and graphics sets", - absl::StrFormat("%s+3", ctrl), false, true, - ImVec4(1.0f, 0.843f, 0.0f, 1.0f)}, // Triforce gold + absl::StrFormat("%s+3", ctrl), false, true}, {EditorType::kSprite, "Sprites", ICON_MD_EMOJI_EMOTIONS, "Edit sprite graphics and properties", - absl::StrFormat("%s+4", ctrl), false, true, - ImVec4(1.0f, 0.647f, 0.0f, 1.0f)}, // Spirit orange + absl::StrFormat("%s+4", ctrl), false, true}, {EditorType::kMessage, "Messages", ICON_MD_CHAT_BUBBLE, "Edit dialogue, signs, and text", - absl::StrFormat("%s+5", ctrl), false, true, - ImVec4(0.196f, 0.6f, 0.8f, 1.0f)}, // Master sword blue + absl::StrFormat("%s+5", ctrl), false, true}, {EditorType::kMusic, "Music", ICON_MD_MUSIC_NOTE, "Configure music and sound effects", - absl::StrFormat("%s+6", ctrl), false, true, - ImVec4(0.416f, 0.353f, 0.804f, 1.0f)}, // Shadow purple + absl::StrFormat("%s+6", ctrl), false, true}, {EditorType::kPalette, "Palettes", ICON_MD_COLOR_LENS, "Edit color palettes and animations", - absl::StrFormat("%s+7", ctrl), false, true, - ImVec4(0.863f, 0.078f, 0.235f, 1.0f)}, // Heart red + absl::StrFormat("%s+7", ctrl), false, true}, {EditorType::kScreen, "Screens", ICON_MD_TV, "Edit title screen and ending screens", - absl::StrFormat("%s+8", ctrl), false, true, - ImVec4(0.4f, 0.8f, 1.0f, 1.0f)}, // Sky blue + absl::StrFormat("%s+8", ctrl), false, true}, {EditorType::kAssembly, "Assembly", ICON_MD_CODE, "Write and edit assembly code", - absl::StrFormat("%s+9", ctrl), false, false, - ImVec4(0.8f, 0.8f, 0.8f, 1.0f)}, // Silver + absl::StrFormat("%s+9", ctrl), false, false}, {EditorType::kHex, "Hex Editor", ICON_MD_DATA_ARRAY, "Direct ROM memory editing and comparison", - absl::StrFormat("%s+0", ctrl), false, true, - ImVec4(0.2f, 0.8f, 0.4f, 1.0f)}, // Matrix green + absl::StrFormat("%s+0", ctrl), false, true}, {EditorType::kEmulator, "Emulator", ICON_MD_VIDEOGAME_ASSET, "Test and debug your ROM in real-time with live debugging", - absl::StrFormat("%s+Shift+E", ctrl), false, true, - ImVec4(0.2f, 0.6f, 1.0f, 1.0f)}, // Emulator blue + absl::StrFormat("%s+Shift+E", ctrl), false, true}, {EditorType::kAgent, "AI Agent", ICON_MD_SMART_TOY, "Configure AI agent, collaboration, and automation", - absl::StrFormat("%s+Shift+A", ctrl), false, false, - ImVec4(0.8f, 0.4f, 1.0f, 1.0f)}, // Purple/magenta + absl::StrFormat("%s+Shift+A", ctrl), false, false}, }; LoadRecentEditors(); @@ -125,33 +226,55 @@ bool EditorSelectionDialog::Show(bool* p_open) { 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); + const float scale = GetEditorSelectScale(); + const float min_width = kEditorSelectCardBaseWidth * scale; + const float max_width = + kEditorSelectCardBaseWidth * kEditorSelectCardWidthMaxFactor * scale; + const float min_height = kEditorSelectCardBaseHeight * scale; + const float max_height = + kEditorSelectCardBaseHeight * kEditorSelectCardHeightMaxFactor * scale; + const float spacing = ImGui::GetStyle().ItemSpacing.x; + const float aspect_ratio = min_height / std::max(min_width, 1.0f); + GridLayout layout = ComputeGridLayout(ImGui::GetContentRegionAvail().x, + min_width, max_width, min_height, + max_height, min_width, aspect_ratio, + spacing); - if (ImGui::BeginTable("##EditorGrid", columns, ImGuiTableFlags_None)) { - for (size_t i = 0; i < editors_.size(); ++i) { - ImGui::TableNextColumn(); + int column = 0; + for (size_t i = 0; i < editors_.size(); ++i) { + if (column == 0) { + ImGui::SetCursorPosX(layout.row_start_x); + } - EditorType prev_selection = selected_editor_; - DrawEditorPanel(editors_[i], static_cast(i)); + EditorType prev_selection = selected_editor_; + DrawEditorPanel(editors_[i], static_cast(i), + ImVec2(layout.item_width, layout.item_height)); - // Check if an editor was just selected - if (selected_editor_ != prev_selection) { - editor_selected = true; - MarkRecentlyUsed(selected_editor_); - if (selection_callback_) { - selection_callback_(selected_editor_); - } - // Auto-dismiss after selection - is_open_ = false; - if (p_open) { - *p_open = false; - } + // Check if an editor was just selected + if (selected_editor_ != prev_selection) { + editor_selected = true; + MarkRecentlyUsed(selected_editor_); + if (selection_callback_) { + selection_callback_(selected_editor_); + } + // Auto-dismiss after selection + is_open_ = false; + if (p_open) { + *p_open = false; } } - ImGui::EndTable(); + + column += 1; + if (column < layout.columns) { + ImGui::SameLine(0.0f, layout.spacing); + } else { + column = 0; + ImGui::Spacing(); + } + } + + if (column != 0) { + ImGui::NewLine(); } } ImGui::End(); @@ -168,29 +291,38 @@ bool EditorSelectionDialog::Show(bool* p_open) { } void EditorSelectionDialog::DrawWelcomeHeader() { - ImDrawList* draw_list = ImGui::GetWindowDrawList(); - ImVec2 header_start = ImGui::GetCursorScreenPos(); + const auto& theme = gui::ThemeManager::Get().GetCurrentTheme(); + const ImVec4 accent = gui::ConvertColorToImVec4(theme.accent); + const ImVec4 text_secondary = gui::ConvertColorToImVec4(theme.text_secondary); ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[2]); // Large font - // Colorful gradient title - ImVec4 title_color = ImVec4(1.0f, 0.843f, 0.0f, 1.0f); // Triforce gold - ImGui::TextColored(title_color, ICON_MD_EDIT " Select an Editor"); + ImGui::TextColored(accent, ICON_MD_EDIT " Select an Editor"); ImGui::PopFont(); - // Subtitle with gradient separator - ImVec2 subtitle_pos = ImGui::GetCursorScreenPos(); - ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), + ImGui::TextColored(text_secondary, "Choose an editor to begin working on your ROM. " "You can open multiple editors simultaneously."); } void EditorSelectionDialog::DrawQuickAccessButtons() { - ImGui::TextColored(ImVec4(1.0f, 0.843f, 0.0f, 1.0f), - ICON_MD_HISTORY " Recently Used"); + const auto& theme = gui::ThemeManager::Get().GetCurrentTheme(); + const ImVec4 accent = gui::ConvertColorToImVec4(theme.accent); + ImGui::TextColored(accent, ICON_MD_HISTORY " Recently Used"); ImGui::Spacing(); + const float scale = GetEditorSelectScale(); + const float min_width = kEditorSelectRecentBaseWidth * scale; + const float max_width = + kEditorSelectRecentBaseWidth * kEditorSelectRecentWidthMaxFactor * scale; + const float height = kEditorSelectRecentBaseHeight * scale; + const float spacing = ImGui::GetStyle().ItemSpacing.x; + GridLayout layout = ComputeGridLayout( + ImGui::GetContentRegionAvail().x, min_width, max_width, height, height, + min_width, height / std::max(min_width, 1.0f), spacing); + + int column = 0; for (EditorType type : recent_editors_) { // Find editor info auto it = std::find_if( @@ -198,18 +330,21 @@ void EditorSelectionDialog::DrawQuickAccessButtons() { [type](const EditorInfo& info) { return info.type == type; }); if (it != editors_.end()) { - // Use editor's theme color for button - ImVec4 color = it->color; + if (column == 0) { + ImGui::SetCursorPosX(layout.row_start_x); + } + + const ImVec4 base_color = GetEditorAccentColor(it->type, theme); ImGui::PushStyleColor( ImGuiCol_Button, - ImVec4(color.x * 0.5f, color.y * 0.5f, color.z * 0.5f, 0.7f)); + ScaleColor(base_color, 0.5f, 0.7f)); ImGui::PushStyleColor( ImGuiCol_ButtonHovered, - ImVec4(color.x * 0.7f, color.y * 0.7f, color.z * 0.7f, 0.9f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, color); + ScaleColor(base_color, 0.7f, 0.9f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, WithAlpha(base_color, 1.0f)); if (ImGui::Button(absl::StrCat(it->icon, " ", it->name).c_str(), - ImVec2(150, 35))) { + ImVec2(layout.item_width, layout.item_height))) { selected_editor_ = type; } @@ -219,123 +354,166 @@ void EditorSelectionDialog::DrawQuickAccessButtons() { ImGui::SetTooltip("%s", it->description); } - ImGui::SameLine(); + column += 1; + if (column < layout.columns) { + ImGui::SameLine(0.0f, layout.spacing); + } else { + column = 0; + ImGui::Spacing(); + } } } - ImGui::NewLine(); + if (column != 0) { + ImGui::NewLine(); + } } -void EditorSelectionDialog::DrawEditorPanel(const EditorInfo& info, int index) { +void EditorSelectionDialog::DrawEditorPanel(const EditorInfo& info, int index, + const ImVec2& card_size) { ImGui::PushID(index); - ImVec2 button_size(180, 120); - ImVec2 cursor_pos = ImGui::GetCursorScreenPos(); + const auto& theme = gui::ThemeManager::Get().GetCurrentTheme(); + const ImVec4 base_color = GetEditorAccentColor(info.type, theme); + const ImVec4 text_primary = gui::ConvertColorToImVec4(theme.text_primary); + const ImVec4 text_secondary = gui::ConvertColorToImVec4(theme.text_secondary); + const ImVec4 accent = gui::ConvertColorToImVec4(theme.accent); + + const ImGuiStyle& style = ImGui::GetStyle(); + const float line_height = ImGui::GetTextLineHeight(); + const float padding_x = std::max(style.FramePadding.x, card_size.x * 0.06f); + const float padding_y = std::max(style.FramePadding.y, card_size.y * 0.08f); + + const float footer_height = info.shortcut.empty() ? 0.0f : line_height; + const float footer_spacing = info.shortcut.empty() ? 0.0f : style.ItemSpacing.y; + const float available_icon_height = + card_size.y - padding_y * 2.0f - line_height - footer_height - footer_spacing; + const float min_icon_radius = line_height * 0.9f; + float max_icon_radius = card_size.y * 0.24f; + max_icon_radius = std::max(max_icon_radius, min_icon_radius); + const float icon_radius = + std::clamp(available_icon_height * 0.5f, min_icon_radius, max_icon_radius); + + const ImVec2 cursor_pos = ImGui::GetCursorScreenPos(); ImDrawList* draw_list = ImGui::GetWindowDrawList(); + const ImVec2 icon_center(cursor_pos.x + card_size.x * 0.5f, + cursor_pos.y + padding_y + icon_radius); + float title_y = icon_center.y + icon_radius + style.ItemSpacing.y; + const float footer_y = cursor_pos.y + card_size.y - padding_y - footer_height; + if (title_y + line_height > footer_y - style.ItemSpacing.y) { + title_y = footer_y - line_height - style.ItemSpacing.y; + } // Panel styling with gradients bool is_recent = std::find(recent_editors_.begin(), recent_editors_.end(), info.type) != recent_editors_.end(); // Create gradient background - ImVec4 base_color = info.color; - ImU32 color_top = ImGui::GetColorU32(ImVec4( - base_color.x * 0.4f, base_color.y * 0.4f, base_color.z * 0.4f, 0.8f)); - ImU32 color_bottom = ImGui::GetColorU32(ImVec4( - base_color.x * 0.2f, base_color.y * 0.2f, base_color.z * 0.2f, 0.9f)); + ImU32 color_top = ImGui::GetColorU32(ScaleColor(base_color, 0.4f, 0.85f)); + ImU32 color_bottom = + ImGui::GetColorU32(ScaleColor(base_color, 0.2f, 0.9f)); // Draw gradient card background draw_list->AddRectFilledMultiColor( cursor_pos, - ImVec2(cursor_pos.x + button_size.x, cursor_pos.y + button_size.y), + ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y), color_top, color_top, color_bottom, color_bottom); // Colored border ImU32 border_color = is_recent ? ImGui::GetColorU32( - ImVec4(base_color.x, base_color.y, base_color.z, 1.0f)) - : ImGui::GetColorU32(ImVec4(base_color.x * 0.6f, base_color.y * 0.6f, - base_color.z * 0.6f, 0.7f)); + WithAlpha(base_color, 1.0f)) + : ImGui::GetColorU32(ScaleColor(base_color, 0.6f, 0.7f)); + const float rounding = std::max(style.FrameRounding, card_size.y * 0.05f); + const float border_thickness = + is_recent ? std::max(2.0f, style.FrameBorderSize + 1.0f) + : std::max(1.0f, style.FrameBorderSize); draw_list->AddRect( cursor_pos, - ImVec2(cursor_pos.x + button_size.x, cursor_pos.y + button_size.y), - border_color, 4.0f, 0, is_recent ? 3.0f : 2.0f); + ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y), + border_color, rounding, 0, border_thickness); // Recent indicator badge if (is_recent) { - ImVec2 badge_pos(cursor_pos.x + button_size.x - 25, cursor_pos.y + 5); - draw_list->AddCircleFilled(badge_pos, 12, ImGui::GetColorU32(base_color), - 16); - ImGui::SetCursorScreenPos(ImVec2(badge_pos.x - 6, badge_pos.y - 8)); - ImGui::TextColored(ImVec4(1, 1, 1, 1), ICON_MD_STAR); + const float badge_radius = + std::clamp(line_height * 0.6f, line_height * 0.4f, line_height); + ImVec2 badge_pos(cursor_pos.x + card_size.x - padding_x - badge_radius, + cursor_pos.y + padding_y + badge_radius); + draw_list->AddCircleFilled(badge_pos, badge_radius, + ImGui::GetColorU32(base_color), 16); + ImVec2 star_size = ImGui::CalcTextSize(ICON_MD_STAR); + ImGui::SetCursorScreenPos( + ImVec2(badge_pos.x - star_size.x * 0.5f, + badge_pos.y - star_size.y * 0.5f)); + ImGui::TextColored(text_primary, ICON_MD_STAR); } // Make button transparent (we draw our own background) - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + ImVec4 button_bg = ImGui::GetStyleColorVec4(ImGuiCol_Button); + button_bg.w = 0.0f; + ImGui::PushStyleColor(ImGuiCol_Button, button_bg); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, - ImVec4(base_color.x * 0.3f, base_color.y * 0.3f, - base_color.z * 0.3f, 0.5f)); + ScaleColor(base_color, 0.3f, 0.5f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, - ImVec4(base_color.x * 0.5f, base_color.y * 0.5f, - base_color.z * 0.5f, 0.7f)); + ScaleColor(base_color, 0.5f, 0.7f)); ImGui::SetCursorScreenPos(cursor_pos); bool clicked = - ImGui::Button(absl::StrCat("##", info.name).c_str(), button_size); + ImGui::Button(absl::StrCat("##", info.name).c_str(), card_size); bool is_hovered = ImGui::IsItemHovered(); ImGui::PopStyleColor(3); // Draw icon with colored background circle - ImVec2 icon_center(cursor_pos.x + button_size.x / 2, cursor_pos.y + 30); ImU32 icon_bg = ImGui::GetColorU32(base_color); - draw_list->AddCircleFilled(icon_center, 22, icon_bg, 32); + draw_list->AddCircleFilled(icon_center, icon_radius, icon_bg, 32); // Draw icon ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[2]); // Larger font for icon ImVec2 icon_size = ImGui::CalcTextSize(info.icon); ImGui::SetCursorScreenPos( ImVec2(icon_center.x - icon_size.x / 2, icon_center.y - icon_size.y / 2)); - ImGui::TextColored(ImVec4(1, 1, 1, 1), "%s", info.icon); + ImGui::TextColored(text_primary, "%s", info.icon); ImGui::PopFont(); // Draw name - ImGui::SetCursorScreenPos(ImVec2(cursor_pos.x + 10, cursor_pos.y + 65)); - ImGui::PushTextWrapPos(cursor_pos.x + button_size.x - 10); - ImVec2 name_size = ImGui::CalcTextSize(info.name); + const float name_wrap_width = card_size.x - padding_x * 2.0f; + ImGui::PushTextWrapPos(cursor_pos.x + card_size.x - padding_x); + ImVec2 name_size = + ImGui::CalcTextSize(info.name, nullptr, false, name_wrap_width); ImGui::SetCursorScreenPos(ImVec2( - cursor_pos.x + (button_size.x - name_size.x) / 2, cursor_pos.y + 65)); + cursor_pos.x + (card_size.x - name_size.x) / 2.0f, title_y)); ImGui::TextColored(base_color, "%s", info.name); ImGui::PopTextWrapPos(); // Draw shortcut hint if available if (!info.shortcut.empty()) { ImGui::SetCursorScreenPos( - ImVec2(cursor_pos.x + 10, cursor_pos.y + button_size.y - 20)); - ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "%s", info.shortcut.c_str()); + ImVec2(cursor_pos.x + padding_x, footer_y)); + ImGui::TextColored(text_secondary, "%s", info.shortcut.c_str()); } // Hover glow effect if (is_hovered) { - ImU32 glow_color = ImGui::GetColorU32( - ImVec4(base_color.x, base_color.y, base_color.z, 0.2f)); + ImU32 glow_color = + ImGui::GetColorU32(ScaleColor(base_color, 1.0f, 0.18f)); draw_list->AddRectFilled( cursor_pos, - ImVec2(cursor_pos.x + button_size.x, cursor_pos.y + button_size.y), - glow_color, 4.0f); + ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y), + glow_color, rounding); } // Enhanced tooltip with fixed sizing if (is_hovered) { - // Force tooltip to have a fixed max width to prevent flickering - ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always); + const float tooltip_width = std::clamp(card_size.x * 1.4f, 240.0f, 340.0f); + ImGui::SetNextWindowSize(ImVec2(tooltip_width, 0), ImGuiCond_Always); ImGui::BeginTooltip(); ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[1]); // Medium font ImGui::TextColored(base_color, "%s %s", info.icon, info.name); ImGui::PopFont(); ImGui::Separator(); - ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + 280); + ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + tooltip_width - 20.0f); ImGui::TextWrapped("%s", info.description); ImGui::PopTextWrapPos(); if (!info.shortcut.empty()) { @@ -344,8 +522,7 @@ void EditorSelectionDialog::DrawEditorPanel(const EditorInfo& info, int index) { } if (is_recent) { ImGui::Spacing(); - ImGui::TextColored(ImVec4(1.0f, 0.843f, 0.0f, 1.0f), - ICON_MD_STAR " Recently used"); + ImGui::TextColored(accent, ICON_MD_STAR " Recently used"); } ImGui::EndTooltip(); } diff --git a/src/app/editor/ui/editor_selection_dialog.h b/src/app/editor/ui/editor_selection_dialog.h index 0a0e5648..3badfd49 100644 --- a/src/app/editor/ui/editor_selection_dialog.h +++ b/src/app/editor/ui/editor_selection_dialog.h @@ -23,7 +23,6 @@ struct EditorInfo { std::string shortcut; // Platform-aware shortcut string bool recently_used = false; bool requires_rom = true; - ImVec4 color = ImVec4(0.5f, 0.5f, 0.5f, 1.0f); // Theme color for this editor }; /** @@ -94,7 +93,8 @@ class EditorSelectionDialog { } private: - void DrawEditorPanel(const EditorInfo& info, int index); + void DrawEditorPanel(const EditorInfo& info, int index, + const ImVec2& card_size); void DrawWelcomeHeader(); void DrawQuickAccessButtons(); diff --git a/src/app/editor/ui/popup_manager.cc b/src/app/editor/ui/popup_manager.cc index c34fff47..66cb043e 100644 --- a/src/app/editor/ui/popup_manager.cc +++ b/src/app/editor/ui/popup_manager.cc @@ -9,6 +9,7 @@ #include "app/gui/core/icons.h" #include "app/gui/core/style.h" #include "imgui/misc/cpp/imgui_stdlib.h" +#include "util/file_util.h" #include "util/hex.h" namespace yaze { @@ -351,7 +352,8 @@ void PopupManager::DrawNewProjectPopup() { if (Button(absl::StrFormat("%s ROM File", ICON_MD_VIDEOGAME_ASSET).c_str(), gui::kDefaultModalSize)) { - rom_filename = util::FileDialogWrapper::ShowOpenFileDialog(); + rom_filename = util::FileDialogWrapper::ShowOpenFileDialog( + util::MakeRomFileDialogOptions(false)); } SameLine(); Text("%s", rom_filename.empty() ? "(Not set)" : rom_filename.c_str()); diff --git a/src/app/editor/ui/ui_coordinator.cc b/src/app/editor/ui/ui_coordinator.cc index e6d10c40..751acddf 100644 --- a/src/app/editor/ui/ui_coordinator.cc +++ b/src/app/editor/ui/ui_coordinator.cc @@ -11,6 +11,12 @@ #ifdef __EMSCRIPTEN__ #include #endif +#ifdef __APPLE__ +#include +#endif +#if defined(__APPLE__) && TARGET_OS_IOS == 1 +#include "app/platform/ios/ios_platform_state.h" +#endif #include "app/editor/editor.h" #include "app/editor/editor_manager.h" #include "app/editor/system/editor_registry.h" @@ -74,10 +80,13 @@ UICoordinator::UICoordinator( toast_manager_.Show( absl::StrFormat("Failed to load ROM: %s", status.message()), ToastType::kError); - } else { + } +#if !(defined(__APPLE__) && TARGET_OS_IOS == 1) + else { // Transition to Dashboard on successful ROM load SetStartupSurface(StartupSurface::kDashboard); } +#endif } #endif }); @@ -188,6 +197,147 @@ void UICoordinator::DrawAllUI() { // Draw popups and toasts DrawAllPopups(); toast_manager_.Draw(); + DrawMobileNavigation(); +} + +bool UICoordinator::IsCompactLayout() const { + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + if (!viewport) { + return false; + } + const float width = viewport->WorkSize.x; +#if defined(__APPLE__) && TARGET_OS_IOS == 1 + return true; +#else + return width < 900.0f; +#endif +} + +void UICoordinator::DrawMobileNavigation() { + if (!IsCompactLayout()) { + return; + } + + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + if (!viewport) { + return; + } + + const ImGuiStyle& style = ImGui::GetStyle(); + ImVec2 safe = style.DisplaySafeAreaPadding; +#if defined(__APPLE__) && TARGET_OS_IOS == 1 + const auto safe_area = ::yaze::platform::ios::GetSafeAreaInsets(); + if (safe_area.left != 0.0f || safe_area.right != 0.0f || + safe_area.top != 0.0f || safe_area.bottom != 0.0f) { + safe = ImVec2(safe_area.right, safe_area.bottom); + } +#endif + const float button_size = std::max(44.0f, ImGui::GetFontSize() * 2.1f); + const float padding = style.WindowPadding.x; + const ImVec2 pos(viewport->WorkPos.x + viewport->WorkSize.x - safe.x - padding - + button_size, + viewport->WorkPos.y + viewport->WorkSize.y - safe.y - padding - + button_size); + + const auto& theme = gui::ThemeManager::Get().GetCurrentTheme(); + const ImVec4 button_color = gui::ConvertColorToImVec4(theme.button); + const ImVec4 button_hovered = gui::ConvertColorToImVec4(theme.button_hovered); + const ImVec4 button_active = gui::ConvertColorToImVec4(theme.button_active); + const ImVec4 button_text = gui::ConvertColorToImVec4(theme.accent); + + ImGui::SetNextWindowPos(pos, ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(button_size, button_size), ImGuiCond_Always); + + ImGuiWindowFlags flags = + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoBackground; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + if (ImGui::Begin("##MobileNavButton", nullptr, flags)) { + ImGui::PushStyleColor(ImGuiCol_Button, button_color); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, button_hovered); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, button_active); + ImGui::PushStyleColor(ImGuiCol_Text, button_text); + + if (ImGui::Button(ICON_MD_APPS, ImVec2(button_size, button_size))) { + ImGui::OpenPopup("##MobileNavPopup"); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Navigation"); + } + + ImGui::PopStyleColor(4); + } + ImGui::End(); + ImGui::PopStyleVar(); + + ImGui::PushStyleColor(ImGuiCol_PopupBg, + gui::ConvertColorToImVec4(theme.surface)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(12.0f, 10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8.0f, 6.0f)); + + if (ImGui::BeginPopup("##MobileNavPopup")) { + bool has_rom = false; + if (editor_manager_) { + auto* current_rom = editor_manager_->GetCurrentRom(); + has_rom = current_rom && current_rom->is_loaded(); + } + + if (ImGui::MenuItem(ICON_MD_FOLDER_OPEN " Open ROM")) { + if (editor_manager_) { + auto status = editor_manager_->LoadRom(); + if (!status.ok()) { + toast_manager_.Show( + absl::StrFormat("Failed to load ROM: %s", status.message()), + ToastType::kError); + } +#if !(defined(__APPLE__) && TARGET_OS_IOS == 1) + else { + SetStartupSurface(StartupSurface::kDashboard); + } +#endif + } + } + + if (has_rom) { + if (ImGui::MenuItem(ICON_MD_DASHBOARD " Dashboard", nullptr, + current_startup_surface_ == + StartupSurface::kDashboard)) { + SetStartupSurface(StartupSurface::kDashboard); + } + if (ImGui::MenuItem( + ICON_MD_EDIT " Editor", nullptr, + current_startup_surface_ == StartupSurface::kEditor)) { + SetStartupSurface(StartupSurface::kEditor); + } + } + + const bool sidebar_visible = panel_manager_.IsSidebarVisible(); + if (ImGui::MenuItem(ICON_MD_VIEW_SIDEBAR " Toggle Sidebar", nullptr, + sidebar_visible)) { + TogglePanelSidebar(); + } + + if (editor_manager_) { + auto* right_panel = editor_manager_->right_panel_manager(); + if (right_panel) { + const bool settings_active = right_panel->IsPanelActive( + RightPanelManager::PanelType::kSettings); + if (ImGui::MenuItem(ICON_MD_SETTINGS " Settings", nullptr, + settings_active)) { + right_panel->TogglePanel(RightPanelManager::PanelType::kSettings); + } + } + } + + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(); } // ============================================================================= diff --git a/src/app/editor/ui/ui_coordinator.h b/src/app/editor/ui/ui_coordinator.h index 7648b668..ba212b77 100644 --- a/src/app/editor/ui/ui_coordinator.h +++ b/src/app/editor/ui/ui_coordinator.h @@ -273,6 +273,10 @@ class UICoordinator { const ImVec4& color, std::function callback, bool enabled = true); + // Mobile helpers + bool IsCompactLayout() const; + void DrawMobileNavigation(); + // Layout and positioning helpers void CenterWindow(const std::string& window_name); void PositionWindow(const std::string& window_name, float x, float y); diff --git a/src/app/editor/ui/welcome_screen.cc b/src/app/editor/ui/welcome_screen.cc index 278a61f4..8a2a3076 100644 --- a/src/app/editor/ui/welcome_screen.cc +++ b/src/app/editor/ui/welcome_screen.cc @@ -5,6 +5,7 @@ #include #include #include +#include #include "absl/strings/str_format.h" #include "absl/time/clock.h" @@ -31,21 +32,33 @@ ImVec4 GetThemedColor(const char* color_name, const ImVec4& fallback) { auto& theme_mgr = gui::ThemeManager::Get(); const auto& theme = theme_mgr.GetCurrentTheme(); - // TODO: Fix this - // 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(); - // } + if (!color_name) { + return fallback; + } + + const std::string_view name(color_name); + if (name == "triforce_gold") { + return gui::ConvertColorToImVec4(theme.accent); + } + if (name == "hyrule_green") { + return gui::ConvertColorToImVec4(theme.success); + } + if (name == "master_sword_blue") { + return gui::ConvertColorToImVec4(theme.info); + } + if (name == "ganon_purple") { + return gui::ConvertColorToImVec4(theme.secondary); + } + if (name == "heart_red") { + return gui::ConvertColorToImVec4(theme.error); + } + if (name == "spirit_orange") { + return gui::ConvertColorToImVec4(theme.warning); + } + if (name == "shadow_purple") { + return ImLerp(gui::ConvertColorToImVec4(theme.secondary), + gui::GetSurfaceVec4(), 0.4f); + } return fallback; } @@ -59,6 +72,11 @@ 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); +constexpr float kRecentCardBaseWidth = 220.0f; +constexpr float kRecentCardBaseHeight = 95.0f; +constexpr float kRecentCardWidthMaxFactor = 1.25f; +constexpr float kRecentCardHeightMaxFactor = 1.25f; + // Active colors (updated each frame from theme) ImVec4 kTriforceGold = kTriforceGoldFallback; ImVec4 kHyruleGreen = kHyruleGreenFallback; @@ -116,7 +134,9 @@ void DrawTriforceBackground(ImDrawList* draw_list, ImVec2 pos, float size, draw_list->AddTriangleFilled(p1, p2, p3, color); }; - ImU32 gold = ImGui::GetColorU32(ImVec4(1.0f, 0.843f, 0.0f, alpha)); + ImVec4 gold_color = kTriforceGold; + gold_color.w = alpha; + ImU32 gold = ImGui::GetColorU32(gold_color); // Proper triforce layout with three triangles float small_size = size / 2.0f; @@ -134,6 +154,51 @@ void DrawTriforceBackground(ImDrawList* draw_list, ImVec2 pos, float size, gold); } +struct GridLayout { + int columns = 1; + float item_width = 0.0f; + float item_height = 0.0f; + float spacing = 0.0f; + float row_start_x = 0.0f; +}; + +GridLayout ComputeGridLayout(float avail_width, float min_width, + float max_width, float min_height, + float max_height, float preferred_width, + float aspect_ratio, float spacing) { + GridLayout layout; + layout.spacing = spacing; + const auto width_for_columns = [avail_width, spacing](int columns) { + return (avail_width - spacing * static_cast(columns - 1)) / + static_cast(columns); + }; + + layout.columns = + std::max(1, static_cast((avail_width + spacing) / + (preferred_width + spacing))); + + layout.item_width = width_for_columns(layout.columns); + while (layout.columns > 1 && layout.item_width < min_width) { + layout.columns -= 1; + layout.item_width = width_for_columns(layout.columns); + } + + layout.item_width = std::min(layout.item_width, max_width); + layout.item_width = std::min(layout.item_width, avail_width); + layout.item_height = + std::clamp(layout.item_width * aspect_ratio, min_height, max_height); + + const float row_width = + layout.item_width * static_cast(layout.columns) + + spacing * static_cast(layout.columns - 1); + layout.row_start_x = ImGui::GetCursorPosX(); + if (row_width < avail_width) { + layout.row_start_x += (avail_width - row_width) * 0.5f; + } + + return layout; +} + } // namespace WelcomeScreen::WelcomeScreen() { @@ -380,46 +445,83 @@ bool WelcomeScreen::Show(bool* p_open) { ImGui::Dummy(ImVec2(0, 10)); ImGui::BeginChild("WelcomeContent", ImVec2(0, -60), false); + const float content_width = ImGui::GetContentRegionAvail().x; + const float content_height = ImGui::GetContentRegionAvail().y; + const bool narrow_layout = content_width < 900.0f; - // Left side - Quick Actions & Templates - ImGui::BeginChild("LeftPanel", - ImVec2(ImGui::GetContentRegionAvail().x * 0.3f, 0), true, - ImGuiWindowFlags_NoScrollbar); - DrawQuickActions(); - ImGui::Spacing(); + if (narrow_layout) { + float left_height = std::clamp(content_height * 0.45f, 260.0f, content_height); + ImGui::BeginChild("LeftPanel", ImVec2(0, left_height), true, + ImGuiWindowFlags_NoScrollbar); + DrawQuickActions(); + ImGui::Spacing(); - // Subtle separator - ImVec2 sep_start = ImGui::GetCursorScreenPos(); - 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.2f)), - 1.0f); + ImVec2 sep_start = ImGui::GetCursorScreenPos(); + 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.2f)), + 1.0f); - ImGui::Dummy(ImVec2(0, 5)); - DrawTemplatesSection(); - ImGui::EndChild(); + ImGui::Dummy(ImVec2(0, 5)); + DrawTemplatesSection(); + ImGui::EndChild(); - ImGui::SameLine(); + ImGui::Spacing(); - // Right side - Recent Projects & What's New - ImGui::BeginChild("RightPanel", ImVec2(0, 0), true); - DrawRecentProjects(); - ImGui::Spacing(); + ImGui::BeginChild("RightPanel", ImVec2(0, 0), true); + DrawRecentProjects(); + ImGui::Spacing(); - // Subtle 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.2f)), - 1.0f); + 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.2f)), + 1.0f); - ImGui::Dummy(ImVec2(0, 5)); - DrawWhatsNew(); - ImGui::EndChild(); + ImGui::Dummy(ImVec2(0, 5)); + DrawWhatsNew(); + ImGui::EndChild(); + } else { + ImGui::BeginChild("LeftPanel", + ImVec2(ImGui::GetContentRegionAvail().x * 0.3f, 0), true, + ImGuiWindowFlags_NoScrollbar); + DrawQuickActions(); + ImGui::Spacing(); + + ImVec2 sep_start = ImGui::GetCursorScreenPos(); + 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.2f)), + 1.0f); + + ImGui::Dummy(ImVec2(0, 5)); + DrawTemplatesSection(); + ImGui::EndChild(); + + ImGui::SameLine(); + + ImGui::BeginChild("RightPanel", ImVec2(0, 0), true); + DrawRecentProjects(); + ImGui::Spacing(); + + 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.2f)), + 1.0f); + + ImGui::Dummy(ImVec2(0, 5)); + DrawWhatsNew(); + ImGui::EndChild(); + } ImGui::EndChild(); @@ -629,7 +731,8 @@ void WelcomeScreen::DrawRecentProjects() { if (recent_projects_.empty()) { // Simple empty state - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); + const ImVec4 text_secondary = gui::GetTextSecondaryVec4(); + ImGui::PushStyleColor(ImGuiCol_Text, text_secondary); ImVec2 cursor = ImGui::GetCursorPos(); ImGui::SetCursorPosX(cursor.x + ImGui::GetContentRegionAvail().x * 0.3f); @@ -644,47 +747,89 @@ void WelcomeScreen::DrawRecentProjects() { return; } - // Grid layout for project cards (compact) - float card_width = 220.0f; // Reduced for compactness - float card_height = 95.0f; // Reduced for less scrolling - int columns = - std::max(1, (int)(ImGui::GetContentRegionAvail().x / (card_width + 12))); + const float scale = ImGui::GetFontSize() / 16.0f; + const float min_width = kRecentCardBaseWidth * scale; + const float max_width = kRecentCardBaseWidth * kRecentCardWidthMaxFactor * scale; + const float min_height = kRecentCardBaseHeight * scale; + const float max_height = kRecentCardBaseHeight * kRecentCardHeightMaxFactor * scale; + const float spacing = ImGui::GetStyle().ItemSpacing.x; + const float aspect_ratio = min_height / std::max(min_width, 1.0f); + GridLayout layout = ComputeGridLayout(ImGui::GetContentRegionAvail().x, + min_width, max_width, min_height, + max_height, min_width, aspect_ratio, + spacing); + + int column = 0; for (size_t i = 0; i < recent_projects_.size(); ++i) { - if (i % columns != 0) { - ImGui::SameLine(); + if (column == 0) { + ImGui::SetCursorPosX(layout.row_start_x); } - DrawProjectPanel(recent_projects_[i], i); + + DrawProjectPanel(recent_projects_[i], static_cast(i), + ImVec2(layout.item_width, layout.item_height)); + + column += 1; + if (column < layout.columns) { + ImGui::SameLine(0.0f, layout.spacing); + } else { + column = 0; + ImGui::Spacing(); + } + } + + if (column != 0) { + ImGui::NewLine(); } } -void WelcomeScreen::DrawProjectPanel(const RecentProject& project, int index) { +void WelcomeScreen::DrawProjectPanel(const RecentProject& project, int index, + const ImVec2& card_size) { ImGui::BeginGroup(); - ImVec2 card_size(200, 95); // Compact size + const ImVec4 surface = gui::GetSurfaceVec4(); + const ImVec4 surface_variant = gui::GetSurfaceVariantVec4(); + const ImVec4 text_primary = gui::GetOnSurfaceVec4(); + const ImVec4 text_secondary = gui::GetTextSecondaryVec4(); + const ImVec4 text_disabled = gui::GetTextDisabledVec4(); + + ImVec2 resolved_card_size = card_size; ImVec2 cursor_pos = ImGui::GetCursorScreenPos(); // Subtle hover scale (only on actual hover, no animation) - 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; + float hover_scale = card_hover_scale_[index]; + if (hover_scale != 1.0f) { + ImVec2 center(cursor_pos.x + resolved_card_size.x / 2, + cursor_pos.y + resolved_card_size.y / 2); + cursor_pos.x = center.x - (resolved_card_size.x * hover_scale) / 2; + cursor_pos.y = center.y - (resolved_card_size.y * hover_scale) / 2; + resolved_card_size.x *= hover_scale; + resolved_card_size.y *= hover_scale; } + const float layout_scale = resolved_card_size.y / kRecentCardBaseHeight; + const float padding = 8.0f * layout_scale; + const float icon_radius = 15.0f * layout_scale; + const float icon_offset = 13.0f * layout_scale; + const float text_offset = 32.0f * layout_scale; + const float name_offset_y = 8.0f * layout_scale; + const float rom_offset_y = 35.0f * layout_scale; + const float path_offset_y = 58.0f * layout_scale; + // Draw card background with subtle 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)); + ImVec4 color_top = ImLerp(surface_variant, surface, 0.7f); + ImVec4 color_bottom = ImLerp(surface_variant, surface, 0.3f); + ImU32 color_top_u32 = ImGui::GetColorU32(color_top); + ImU32 color_bottom_u32 = ImGui::GetColorU32(color_bottom); 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); + ImVec2(cursor_pos.x + resolved_card_size.x, + cursor_pos.y + resolved_card_size.y), + color_top_u32, + color_top_u32, color_bottom_u32, color_bottom_u32); // Static themed border ImVec4 border_color_base = (index % 3 == 0) ? kHyruleGreen @@ -695,13 +840,14 @@ void WelcomeScreen::DrawProjectPanel(const RecentProject& project, int index) { draw_list->AddRect( cursor_pos, - ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y), + ImVec2(cursor_pos.x + resolved_card_size.x, + cursor_pos.y + resolved_card_size.y), border_color, 6.0f, 0, 2.0f); // Make the card clickable ImGui::SetCursorScreenPos(cursor_pos); ImGui::InvisibleButton(absl::StrFormat("ProjectPanel_%d", index).c_str(), - card_size); + resolved_card_size); bool is_hovered = ImGui::IsItemHovered(); bool is_clicked = ImGui::IsItemClicked(); @@ -714,50 +860,64 @@ void WelcomeScreen::DrawProjectPanel(const RecentProject& project, int index) { ImVec4(kTriforceGold.x, kTriforceGold.y, kTriforceGold.z, 0.15f)); draw_list->AddRectFilled( cursor_pos, - ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y), + ImVec2(cursor_pos.x + resolved_card_size.x, + cursor_pos.y + resolved_card_size.y), hover_color, 6.0f); } // Draw content (tighter layout) - ImVec2 content_pos(cursor_pos.x + 8, cursor_pos.y + 8); + ImVec2 content_pos(cursor_pos.x + padding, cursor_pos.y + padding); // Icon with colored background circle (compact) - ImVec2 icon_center(content_pos.x + 13, content_pos.y + 13); + ImVec2 icon_center(content_pos.x + icon_offset, content_pos.y + icon_offset); ImU32 icon_bg = ImGui::GetColorU32(border_color_base); - draw_list->AddCircleFilled(icon_center, 15, icon_bg, 24); + draw_list->AddCircleFilled(icon_center, icon_radius, icon_bg, 24); // Center the icon properly ImVec2 icon_size = ImGui::CalcTextSize(ICON_MD_VIDEOGAME_ASSET); ImGui::SetCursorScreenPos( ImVec2(icon_center.x - icon_size.x / 2, icon_center.y - icon_size.y / 2)); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 1)); + ImGui::PushStyleColor(ImGuiCol_Text, text_primary); ImGui::Text(ICON_MD_VIDEOGAME_ASSET); ImGui::PopStyleColor(); // Project name (compact, shorten if too long) - ImGui::SetCursorScreenPos(ImVec2(content_pos.x + 32, content_pos.y + 8)); - ImGui::PushTextWrapPos(cursor_pos.x + card_size.x - 8); + ImGui::SetCursorScreenPos( + ImVec2(content_pos.x + text_offset, content_pos.y + name_offset_y)); + ImGui::PushTextWrapPos(cursor_pos.x + resolved_card_size.x - padding); ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[0]); // Default font std::string short_name = project.name; - if (short_name.length() > 22) { - short_name = short_name.substr(0, 19) + "..."; + const float approx_char_width = ImGui::CalcTextSize("A").x; + const float name_max_width = + std::max(120.0f, resolved_card_size.x - text_offset - padding); + size_t max_chars = static_cast( + std::max(12.0f, name_max_width / std::max(approx_char_width, 1.0f))); + if (short_name.length() > max_chars) { + short_name = short_name.substr(0, max_chars - 3) + "..."; } ImGui::TextColored(kTriforceGold, "%s", short_name.c_str()); ImGui::PopFont(); ImGui::PopTextWrapPos(); // ROM title (compact) - ImGui::SetCursorScreenPos(ImVec2(content_pos.x + 4, content_pos.y + 35)); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.65f, 0.65f, 0.65f, 1.0f)); + ImGui::SetCursorScreenPos( + ImVec2(content_pos.x + padding * 0.5f, content_pos.y + rom_offset_y)); + ImGui::PushStyleColor(ImGuiCol_Text, text_secondary); ImGui::Text(ICON_MD_GAMEPAD " %s", project.rom_title.c_str()); ImGui::PopStyleColor(); // Path in card (compact) - ImGui::SetCursorScreenPos(ImVec2(content_pos.x + 4, content_pos.y + 58)); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.4f, 0.4f, 1.0f)); + ImGui::SetCursorScreenPos( + ImVec2(content_pos.x + padding * 0.5f, content_pos.y + path_offset_y)); + ImGui::PushStyleColor(ImGuiCol_Text, text_disabled); std::string short_path = project.filepath; - if (short_path.length() > 26) { - short_path = "..." + short_path.substr(short_path.length() - 23); + const float path_max_width = + std::max(120.0f, resolved_card_size.x - padding * 2.0f); + size_t max_path_chars = static_cast( + std::max(18.0f, path_max_width / std::max(approx_char_width, 1.0f))); + if (short_path.length() > max_path_chars) { + short_path = + "..." + short_path.substr(short_path.length() - (max_path_chars - 3)); } ImGui::Text(ICON_MD_FOLDER " %s", short_path.c_str()); ImGui::PopStyleColor(); @@ -1003,4 +1163,4 @@ void WelcomeScreen::DrawWhatsNew() { } } // namespace editor -} // namespace yaze \ No newline at end of file +} // namespace yaze diff --git a/src/app/editor/ui/welcome_screen.h b/src/app/editor/ui/welcome_screen.h index 8b48914c..67edc6c7 100644 --- a/src/app/editor/ui/welcome_screen.h +++ b/src/app/editor/ui/welcome_screen.h @@ -114,7 +114,8 @@ class WelcomeScreen { void DrawHeader(); void DrawQuickActions(); void DrawRecentProjects(); - void DrawProjectPanel(const RecentProject& project, int index); + void DrawProjectPanel(const RecentProject& project, int index, + const ImVec2& card_size); void DrawTemplatesSection(); void DrawTipsSection(); void DrawWhatsNew();