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.
This commit is contained in:
scawful
2025-10-04 23:59:08 -04:00
parent bcc8f8e8f9
commit 6c0e7a96a5
21 changed files with 1536 additions and 55 deletions

View File

@@ -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

View File

@@ -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<gui::MenuItem>{
{absl::StrCat(ICON_MD_INFO, " Check ROM Version"), "",
[&]() {

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,119 @@
#ifndef YAZE_APP_EDITOR_MENU_BUILDER_H_
#define YAZE_APP_EDITOR_MENU_BUILDER_H_
#include <functional>
#include <string>
#include <vector>
#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<void()>;
using EnabledCheck = std::function<bool()>;
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<MenuItem> items;
};
std::vector<Menu> menus_;
Menu* current_menu_ = nullptr;
void DrawMenuItem(const MenuItem& item);
};
} // namespace editor
} // namespace yaze
#endif // YAZE_APP_EDITOR_MENU_BUILDER_H_

View File

@@ -0,0 +1,380 @@
#include "app/editor/ui/background_renderer.h"
#include <algorithm>
#include <cmath>
#include "app/gui/theme_manager.h"
#include "imgui/imgui.h"
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
namespace yaze {
namespace gui {
// BackgroundRenderer Implementation
BackgroundRenderer& BackgroundRenderer::Get() {
static BackgroundRenderer instance;
return instance;
}
void BackgroundRenderer::RenderDockingBackground(ImDrawList* draw_list, const ImVec2& window_pos,
const ImVec2& window_size, const Color& theme_color) {
if (!draw_list) return;
UpdateAnimation(ImGui::GetIO().DeltaTime);
// Get current theme colors
auto& theme_manager = ThemeManager::Get();
auto current_theme = theme_manager.GetCurrentTheme();
// Create a subtle tinted background
Color bg_tint = {
current_theme.background.red * 1.1f,
current_theme.background.green * 1.1f,
current_theme.background.blue * 1.1f,
0.3f
};
ImU32 bg_color = ImGui::ColorConvertFloat4ToU32(ConvertColorToImVec4(bg_tint));
draw_list->AddRectFilled(window_pos,
ImVec2(window_pos.x + window_size.x, window_pos.y + window_size.y),
bg_color);
// Render the grid if enabled
if (grid_settings_.grid_size > 0) {
RenderGridBackground(draw_list, window_pos, window_size, theme_color);
}
// Add subtle corner accents
if (current_theme.enable_glow_effects) {
float corner_size = 60.0f;
Color accent_faded = current_theme.accent;
accent_faded.alpha = 0.1f + 0.05f * sinf(animation_time_ * 2.0f);
ImU32 corner_color = ImGui::ColorConvertFloat4ToU32(ConvertColorToImVec4(accent_faded));
// Top-left corner
draw_list->AddRectFilledMultiColor(
window_pos,
ImVec2(window_pos.x + corner_size, window_pos.y + corner_size),
corner_color, IM_COL32(0,0,0,0), IM_COL32(0,0,0,0), corner_color);
// Bottom-right corner
draw_list->AddRectFilledMultiColor(
ImVec2(window_pos.x + window_size.x - corner_size, window_pos.y + window_size.y - corner_size),
ImVec2(window_pos.x + window_size.x, window_pos.y + window_size.y),
IM_COL32(0,0,0,0), corner_color, corner_color, IM_COL32(0,0,0,0));
}
}
void BackgroundRenderer::RenderGridBackground(ImDrawList* draw_list, const ImVec2& window_pos,
const ImVec2& window_size, const Color& grid_color) {
if (!draw_list || grid_settings_.grid_size <= 0) return;
// Grid parameters with optional animation
float grid_size = grid_settings_.grid_size;
float offset_x = 0.0f;
float offset_y = 0.0f;
// Apply animation if enabled
if (grid_settings_.enable_animation) {
float animation_offset = animation_time_ * grid_settings_.animation_speed * 10.0f;
offset_x = fmodf(animation_offset, grid_size);
offset_y = fmodf(animation_offset * 0.7f, grid_size); // Different speed for interesting effect
}
// Window center for radial calculations
ImVec2 center = ImVec2(window_pos.x + window_size.x * 0.5f,
window_pos.y + window_size.y * 0.5f);
float max_distance = sqrtf(window_size.x * window_size.x + window_size.y * window_size.y) * 0.5f;
// Apply breathing effect to color if enabled
Color themed_grid_color = grid_color;
themed_grid_color.alpha = grid_settings_.opacity;
if (grid_settings_.enable_breathing) {
float breathing_factor = 1.0f + grid_settings_.breathing_intensity *
sinf(animation_time_ * grid_settings_.breathing_speed);
themed_grid_color.red = std::min(1.0f, themed_grid_color.red * breathing_factor);
themed_grid_color.green = std::min(1.0f, themed_grid_color.green * breathing_factor);
themed_grid_color.blue = std::min(1.0f, themed_grid_color.blue * breathing_factor);
}
if (grid_settings_.enable_dots) {
// Render grid as dots
for (float x = window_pos.x - offset_x; x < window_pos.x + window_size.x + grid_size; x += grid_size) {
for (float y = window_pos.y - offset_y; y < window_pos.y + window_size.y + grid_size; y += grid_size) {
ImVec2 dot_pos(x, y);
// Calculate radial fade
float fade_factor = 1.0f;
if (grid_settings_.radial_fade) {
float distance = sqrtf((dot_pos.x - center.x) * (dot_pos.x - center.x) +
(dot_pos.y - center.y) * (dot_pos.y - center.y));
fade_factor = 1.0f - std::min(distance / grid_settings_.fade_distance, 1.0f);
fade_factor = fade_factor * fade_factor; // Square for smoother falloff
}
if (fade_factor > 0.01f) {
ImU32 dot_color = BlendColorWithFade(themed_grid_color, fade_factor);
DrawGridDot(draw_list, dot_pos, dot_color, grid_settings_.dot_size);
}
}
}
} else {
// Render grid as lines
// Vertical lines
for (float x = window_pos.x - offset_x; x < window_pos.x + window_size.x + grid_size; x += grid_size) {
ImVec2 line_start(x, window_pos.y);
ImVec2 line_end(x, window_pos.y + window_size.y);
// Calculate average fade for this line
float avg_fade = 0.0f;
if (grid_settings_.radial_fade) {
for (float y = window_pos.y; y < window_pos.y + window_size.y; y += grid_size * 0.5f) {
float distance = sqrtf((x - center.x) * (x - center.x) + (y - center.y) * (y - center.y));
float fade = 1.0f - std::min(distance / grid_settings_.fade_distance, 1.0f);
avg_fade += fade * fade;
}
avg_fade /= (window_size.y / (grid_size * 0.5f));
} else {
avg_fade = 1.0f;
}
if (avg_fade > 0.01f) {
ImU32 line_color = BlendColorWithFade(themed_grid_color, avg_fade);
DrawGridLine(draw_list, line_start, line_end, line_color, grid_settings_.line_thickness);
}
}
// Horizontal lines
for (float y = window_pos.y - offset_y; y < window_pos.y + window_size.y + grid_size; y += grid_size) {
ImVec2 line_start(window_pos.x, y);
ImVec2 line_end(window_pos.x + window_size.x, y);
// Calculate average fade for this line
float avg_fade = 0.0f;
if (grid_settings_.radial_fade) {
for (float x = window_pos.x; x < window_pos.x + window_size.x; x += grid_size * 0.5f) {
float distance = sqrtf((x - center.x) * (x - center.x) + (y - center.y) * (y - center.y));
float fade = 1.0f - std::min(distance / grid_settings_.fade_distance, 1.0f);
avg_fade += fade * fade;
}
avg_fade /= (window_size.x / (grid_size * 0.5f));
} else {
avg_fade = 1.0f;
}
if (avg_fade > 0.01f) {
ImU32 line_color = BlendColorWithFade(themed_grid_color, avg_fade);
DrawGridLine(draw_list, line_start, line_end, line_color, grid_settings_.line_thickness);
}
}
}
}
void BackgroundRenderer::RenderRadialGradient(ImDrawList* draw_list, const ImVec2& center,
float radius, const Color& inner_color, const Color& outer_color) {
if (!draw_list) return;
const int segments = 32;
const int rings = 8;
for (int ring = 0; ring < rings; ++ring) {
float ring_radius = radius * (ring + 1) / rings;
float inner_ring_radius = radius * ring / rings;
// Interpolate colors for this ring
float t = static_cast<float>(ring) / rings;
Color ring_color = {
inner_color.red * (1.0f - t) + outer_color.red * t,
inner_color.green * (1.0f - t) + outer_color.green * t,
inner_color.blue * (1.0f - t) + outer_color.blue * t,
inner_color.alpha * (1.0f - t) + outer_color.alpha * t
};
ImU32 color = ImGui::ColorConvertFloat4ToU32(ConvertColorToImVec4(ring_color));
if (ring == 0) {
// Center circle
draw_list->AddCircleFilled(center, ring_radius, color, segments);
} else {
// Ring
for (int i = 0; i < segments; ++i) {
float angle1 = (2.0f * M_PI * i) / segments;
float angle2 = (2.0f * M_PI * (i + 1)) / segments;
ImVec2 p1_inner = ImVec2(center.x + cosf(angle1) * inner_ring_radius,
center.y + sinf(angle1) * inner_ring_radius);
ImVec2 p2_inner = ImVec2(center.x + cosf(angle2) * inner_ring_radius,
center.y + sinf(angle2) * inner_ring_radius);
ImVec2 p1_outer = ImVec2(center.x + cosf(angle1) * ring_radius,
center.y + sinf(angle1) * ring_radius);
ImVec2 p2_outer = ImVec2(center.x + cosf(angle2) * ring_radius,
center.y + sinf(angle2) * ring_radius);
draw_list->AddQuadFilled(p1_inner, p2_inner, p2_outer, p1_outer, color);
}
}
}
}
void BackgroundRenderer::UpdateAnimation(float delta_time) {
if (grid_settings_.enable_animation) {
animation_time_ += delta_time;
}
}
void BackgroundRenderer::UpdateForTheme(const Color& primary_color, const Color& background_color) {
// Create a grid color that's a subtle blend of the theme's primary and background
cached_grid_color_ = {
(primary_color.red * 0.3f + background_color.red * 0.7f),
(primary_color.green * 0.3f + background_color.green * 0.7f),
(primary_color.blue * 0.3f + background_color.blue * 0.7f),
grid_settings_.opacity
};
}
void BackgroundRenderer::DrawSettingsUI() {
if (ImGui::CollapsingHeader("Background Grid Settings")) {
ImGui::Indent();
ImGui::SliderFloat("Grid Size", &grid_settings_.grid_size, 8.0f, 128.0f, "%.0f px");
ImGui::SliderFloat("Line Thickness", &grid_settings_.line_thickness, 0.5f, 3.0f, "%.1f px");
ImGui::SliderFloat("Opacity", &grid_settings_.opacity, 0.01f, 0.3f, "%.3f");
ImGui::SliderFloat("Fade Distance", &grid_settings_.fade_distance, 50.0f, 500.0f, "%.0f px");
ImGui::Separator();
ImGui::Text("Visual Effects:");
ImGui::Checkbox("Enable Animation", &grid_settings_.enable_animation);
ImGui::SameLine();
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Makes the grid move slowly across the screen");
}
ImGui::Checkbox("Color Breathing", &grid_settings_.enable_breathing);
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Grid color pulses with a breathing effect");
}
ImGui::Checkbox("Radial Fade", &grid_settings_.radial_fade);
ImGui::Checkbox("Use Dots Instead of Lines", &grid_settings_.enable_dots);
// Animation settings (only show if animation is enabled)
if (grid_settings_.enable_animation) {
ImGui::Indent();
ImGui::SliderFloat("Animation Speed", &grid_settings_.animation_speed, 0.1f, 3.0f, "%.1fx");
ImGui::Unindent();
}
// Breathing settings (only show if breathing is enabled)
if (grid_settings_.enable_breathing) {
ImGui::Indent();
ImGui::SliderFloat("Breathing Speed", &grid_settings_.breathing_speed, 0.5f, 3.0f, "%.1fx");
ImGui::SliderFloat("Breathing Intensity", &grid_settings_.breathing_intensity, 0.1f, 0.8f, "%.1f");
ImGui::Unindent();
}
if (grid_settings_.enable_dots) {
ImGui::SliderFloat("Dot Size", &grid_settings_.dot_size, 1.0f, 8.0f, "%.1f px");
}
// Preview
ImGui::Spacing();
ImGui::Text("Preview:");
ImVec2 preview_size(200, 100);
ImVec2 preview_pos = ImGui::GetCursorScreenPos();
ImDrawList* preview_draw_list = ImGui::GetWindowDrawList();
auto& theme_manager = ThemeManager::Get();
auto theme_color = theme_manager.GetCurrentTheme().primary;
// Draw preview background
preview_draw_list->AddRectFilled(preview_pos,
ImVec2(preview_pos.x + preview_size.x, preview_pos.y + preview_size.y),
IM_COL32(30, 30, 30, 255));
// Draw preview grid
RenderGridBackground(preview_draw_list, preview_pos, preview_size, theme_color);
// Advance cursor
ImGui::Dummy(preview_size);
ImGui::Unindent();
}
}
float BackgroundRenderer::CalculateRadialFade(const ImVec2& pos, const ImVec2& center, float max_distance) const {
float distance = sqrtf((pos.x - center.x) * (pos.x - center.x) +
(pos.y - center.y) * (pos.y - center.y));
float fade = 1.0f - std::min(distance / max_distance, 1.0f);
return fade * fade; // Square for smoother falloff
}
ImU32 BackgroundRenderer::BlendColorWithFade(const Color& base_color, float fade_factor) const {
Color faded_color = {
base_color.red,
base_color.green,
base_color.blue,
base_color.alpha * fade_factor
};
return ImGui::ColorConvertFloat4ToU32(ConvertColorToImVec4(faded_color));
}
void BackgroundRenderer::DrawGridLine(ImDrawList* draw_list, const ImVec2& start, const ImVec2& end,
ImU32 color, float thickness) const {
draw_list->AddLine(start, end, color, thickness);
}
void BackgroundRenderer::DrawGridDot(ImDrawList* draw_list, const ImVec2& pos, ImU32 color, float size) const {
draw_list->AddCircleFilled(pos, size, color);
}
// DockSpaceRenderer Implementation
bool DockSpaceRenderer::background_enabled_ = true;
bool DockSpaceRenderer::grid_enabled_ = true;
bool DockSpaceRenderer::effects_enabled_ = true;
ImVec2 DockSpaceRenderer::last_dockspace_pos_{};
ImVec2 DockSpaceRenderer::last_dockspace_size_{};
void DockSpaceRenderer::BeginEnhancedDockSpace(ImGuiID dockspace_id, const ImVec2& size,
ImGuiDockNodeFlags flags) {
// Store window info
last_dockspace_pos_ = ImGui::GetWindowPos();
last_dockspace_size_ = ImGui::GetWindowSize();
// Create the actual dockspace first
ImGui::DockSpace(dockspace_id, size, flags);
// NOW draw the background effects on the foreground draw list so they're visible
if (background_enabled_) {
ImDrawList* fg_draw_list = ImGui::GetForegroundDrawList();
auto& theme_manager = ThemeManager::Get();
auto current_theme = theme_manager.GetCurrentTheme();
if (grid_enabled_) {
auto& bg_renderer = BackgroundRenderer::Get();
// Use the main viewport for full-screen grid
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImVec2 grid_pos = viewport->WorkPos;
ImVec2 grid_size = viewport->WorkSize;
// Use subtle grid color that doesn't distract
Color subtle_grid_color = current_theme.primary;
// Use the grid settings opacity for consistency
subtle_grid_color.alpha = bg_renderer.GetGridSettings().opacity;
bg_renderer.RenderGridBackground(fg_draw_list, grid_pos, grid_size, subtle_grid_color);
}
}
}
void DockSpaceRenderer::EndEnhancedDockSpace() {
// Additional post-processing effects could go here
// For now, this is just for API consistency
}
} // namespace gui
} // namespace yaze

View File

@@ -0,0 +1,96 @@
#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"
namespace yaze {
namespace gui {
/**
* @class BackgroundRenderer
* @brief Renders themed background effects for docking windows
*/
class BackgroundRenderer {
public:
struct GridSettings {
float grid_size = 32.0f; // Size of grid cells
float line_thickness = 1.0f; // Thickness of grid lines
float opacity = 0.12f; // Subtle but visible opacity
float fade_distance = 400.0f; // Distance over which grid fades
bool enable_animation = false; // Animation toggle (default off)
bool enable_breathing = false; // Color breathing effect toggle (default off)
bool radial_fade = true; // Re-enable subtle radial fade
bool enable_dots = false; // Use dots instead of lines
float dot_size = 2.0f; // Size of grid dots
float animation_speed = 1.0f; // Animation speed multiplier
float breathing_speed = 1.5f; // Breathing effect speed
float breathing_intensity = 0.3f; // How much color changes during breathing
};
static BackgroundRenderer& Get();
// Main rendering functions
void RenderDockingBackground(ImDrawList* draw_list, const ImVec2& window_pos,
const ImVec2& window_size, const Color& theme_color);
void RenderGridBackground(ImDrawList* draw_list, const ImVec2& window_pos,
const ImVec2& window_size, const Color& grid_color);
void RenderRadialGradient(ImDrawList* draw_list, const ImVec2& center,
float radius, const Color& inner_color, const Color& outer_color);
// Configuration
void SetGridSettings(const GridSettings& settings) { grid_settings_ = settings; }
const GridSettings& GetGridSettings() const { return grid_settings_; }
// Animation
void UpdateAnimation(float delta_time);
void SetAnimationEnabled(bool enabled) { grid_settings_.enable_animation = enabled; }
// Theme integration
void UpdateForTheme(const Color& primary_color, const Color& background_color);
// UI for settings
void DrawSettingsUI();
private:
BackgroundRenderer() = default;
GridSettings grid_settings_;
float animation_time_ = 0.0f;
Color cached_grid_color_{0.5f, 0.5f, 0.5f, 0.1f};
// Helper functions
float CalculateRadialFade(const ImVec2& pos, const ImVec2& center, float max_distance) const;
ImU32 BlendColorWithFade(const Color& base_color, float fade_factor) const;
void DrawGridLine(ImDrawList* draw_list, const ImVec2& start, const ImVec2& end,
ImU32 color, float thickness) const;
void DrawGridDot(ImDrawList* draw_list, const ImVec2& pos, ImU32 color, float size) const;
};
/**
* @class DockSpaceRenderer
* @brief Enhanced docking space with themed background effects
*/
class DockSpaceRenderer {
public:
static void BeginEnhancedDockSpace(ImGuiID dockspace_id, const ImVec2& size = ImVec2(0, 0),
ImGuiDockNodeFlags flags = 0);
static void EndEnhancedDockSpace();
// Configuration
static void SetBackgroundEnabled(bool enabled) { background_enabled_ = enabled; }
static void SetGridEnabled(bool enabled) { grid_enabled_ = enabled; }
static void SetEffectsEnabled(bool enabled) { effects_enabled_ = enabled; }
private:
static bool background_enabled_;
static bool grid_enabled_;
static bool effects_enabled_;
static ImVec2 last_dockspace_pos_;
static ImVec2 last_dockspace_size_;
};
} // namespace gui
} // namespace yaze
#endif // YAZE_APP_EDITOR_UI_BACKGROUND_RENDERER_H

View File

@@ -0,0 +1,256 @@
#include "app/editor/ui/editor_selection_dialog.h"
#include <sstream>
#include <fstream>
#include <algorithm>
#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<int>(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<int>(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<int>(EditorType::kLast)) {
recent_editors_.push_back(static_cast<EditorType>(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<int>(type) << "\n";
}
util::SaveFile("recent_editors.txt", ss.str());
} catch (...) {
// Ignore save errors
}
}
} // namespace editor
} // namespace yaze

View File

@@ -0,0 +1,104 @@
#ifndef YAZE_APP_EDITOR_UI_EDITOR_SELECTION_DIALOG_H_
#define YAZE_APP_EDITOR_UI_EDITOR_SELECTION_DIALOG_H_
#include <string>
#include <vector>
#include <functional>
#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<void(EditorType)> 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<EditorInfo> editors_;
EditorType selected_editor_ = EditorType::kNone;
bool is_open_ = false;
std::function<void(EditorType)> selection_callback_;
// Recently used tracking
std::vector<EditorType> recent_editors_;
static constexpr int kMaxRecentEditors = 5;
};
} // namespace editor
} // namespace yaze
#endif // YAZE_APP_EDITOR_UI_EDITOR_SELECTION_DIALOG_H_

View File

@@ -0,0 +1,690 @@
#include "app/editor/ui/welcome_screen.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <filesystem>
#include <fstream>
#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<std::chrono::system_clock::duration>(
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<std::chrono::hours>(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<void()> 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

View File

@@ -0,0 +1,111 @@
#ifndef YAZE_APP_EDITOR_UI_WELCOME_SCREEN_H_
#define YAZE_APP_EDITOR_UI_WELCOME_SCREEN_H_
#include <functional>
#include <string>
#include <vector>
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<void()> callback) {
open_rom_callback_ = callback;
}
/**
* @brief Set callback for creating new project
*/
void SetNewProjectCallback(std::function<void()> callback) {
new_project_callback_ = callback;
}
/**
* @brief Set callback for opening project
*/
void SetOpenProjectCallback(std::function<void(const std::string&)> 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<RecentProject> recent_projects_;
bool manually_closed_ = false;
// Callbacks
std::function<void()> open_rom_callback_;
std::function<void()> new_project_callback_;
std::function<void(const std::string&)> 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_