diff --git a/src/app/core/controller.cc b/src/app/core/controller.cc index aab082fa..22b802b4 100644 --- a/src/app/core/controller.cc +++ b/src/app/core/controller.cc @@ -8,7 +8,7 @@ #include "app/core/timing.h" #include "app/core/window.h" #include "app/editor/editor_manager.h" -#include "app/editor/ui/background_renderer.h" +#include "app/gui/background_renderer.h" #include "app/gfx/arena.h" // Add include for Arena #include "app/gfx/backend/sdl2_renderer.h" // Add include for new renderer #include "app/gui/theme_manager.h" diff --git a/src/app/core/core_library.cmake b/src/app/core/core_library.cmake index 8ba2d4c2..96e54129 100644 --- a/src/app/core/core_library.cmake +++ b/src/app/core/core_library.cmake @@ -95,6 +95,7 @@ target_include_directories(yaze_core_lib PUBLIC target_link_libraries(yaze_core_lib PUBLIC yaze_util yaze_gfx + yaze_zelda3 # Needed for Zelda3Labels in project.cc yaze_common ImGui asar-static diff --git a/src/app/editor/editor_manager.cc b/src/app/editor/editor_manager.cc index 1f288d3a..2e436122 100644 --- a/src/app/editor/editor_manager.cc +++ b/src/app/editor/editor_manager.cc @@ -30,7 +30,7 @@ #include "app/emu/emulator.h" #include "app/gfx/arena.h" #include "app/gfx/performance/performance_profiler.h" -#include "app/editor/ui/background_renderer.h" +#include "app/gui/background_renderer.h" #include "app/gui/icons.h" #include "app/gui/input.h" #include "app/gui/style.h" diff --git a/src/app/editor/ui/workspace_manager.cc b/src/app/editor/ui/workspace_manager.cc index e776d085..c7181d69 100644 --- a/src/app/editor/ui/workspace_manager.cc +++ b/src/app/editor/ui/workspace_manager.cc @@ -1,9 +1,12 @@ #include "app/editor/ui/workspace_manager.h" #include "app/editor/system/toast_manager.h" +#include "app/gui/editor_card_manager.h" #include "app/rom.h" #include "absl/strings/str_format.h" #include "util/file_util.h" #include "util/platform_paths.h" +#include "imgui/imgui.h" +#include "imgui/imgui_internal.h" namespace yaze { namespace editor { @@ -122,23 +125,60 @@ void WorkspaceManager::LoadModderLayout() { } void WorkspaceManager::ShowAllWindows() { - // TODO: Set all editor windows to visible + gui::EditorCardManager::Get().ShowAll(); + if (toast_manager_) { + toast_manager_->Show("All windows shown", ToastType::kInfo); + } } void WorkspaceManager::HideAllWindows() { - // TODO: Hide all editor windows + gui::EditorCardManager::Get().HideAll(); + if (toast_manager_) { + toast_manager_->Show("All windows hidden", ToastType::kInfo); + } } void WorkspaceManager::MaximizeCurrentWindow() { - // TODO: Maximize focused window + // Use ImGui internal API to maximize current window + ImGuiWindow* window = ImGui::GetCurrentWindowRead(); + if (window && window->DockNode) { + ImGuiID central_node_id = ImGui::DockBuilderGetCentralNode( + ImGui::GetID("MainDockSpace"))->ID; + ImGui::DockBuilderDockWindow(window->Name, central_node_id); + } + if (toast_manager_) { + toast_manager_->Show("Window maximized", ToastType::kInfo); + } } void WorkspaceManager::RestoreAllWindows() { - // TODO: Restore all windows to default size + // Reset all window sizes - ImGui will auto-restore based on docking + ImGuiContext* ctx = ImGui::GetCurrentContext(); + if (ctx) { + for (ImGuiWindow* window : ctx->Windows) { + if (window && !window->Collapsed) { + ImGui::SetWindowSize(window->Name, ImVec2(0, 0)); // Auto-size + } + } + } + if (toast_manager_) { + toast_manager_->Show("All windows restored", ToastType::kInfo); + } } void WorkspaceManager::CloseAllFloatingWindows() { - // TODO: Close undocked windows + // Close all windows that are not docked + ImGuiContext* ctx = ImGui::GetCurrentContext(); + if (ctx) { + for (ImGuiWindow* window : ctx->Windows) { + if (window && !window->DockNode && !window->Collapsed) { + window->Hidden = true; + } + } + } + if (toast_manager_) { + toast_manager_->Show("Floating windows closed", ToastType::kInfo); + } } size_t WorkspaceManager::GetActiveSessionCount() const { @@ -155,7 +195,7 @@ size_t WorkspaceManager::GetActiveSessionCount() const { bool WorkspaceManager::HasDuplicateSession(const std::string& filepath) const { if (!sessions_) return false; - + for (const auto& session : *sessions_) { if (session.filepath == filepath && session.rom && session.rom->is_loaded()) { return true; @@ -164,5 +204,74 @@ bool WorkspaceManager::HasDuplicateSession(const std::string& filepath) const { return false; } +// Window navigation operations +void WorkspaceManager::FocusNextWindow() { + ImGuiContext* ctx = ImGui::GetCurrentContext(); + if (ctx && ctx->NavWindow) { + ImGui::FocusWindow(ImGui::FindWindowByName(ctx->NavWindow->Name)); + } + // TODO: Implement proper window cycling +} + +void WorkspaceManager::FocusPreviousWindow() { + // TODO: Implement window cycling backward +} + +void WorkspaceManager::SplitWindowHorizontal() { + ImGuiWindow* window = ImGui::GetCurrentWindowRead(); + if (window && window->DockNode) { + ImGuiID node_id = window->DockNode->ID; + ImGuiID out_id_at_dir = 0; + ImGuiID out_id_at_opposite_dir = 0; + ImGui::DockBuilderSplitNode(node_id, ImGuiDir_Down, 0.5f, + &out_id_at_dir, &out_id_at_opposite_dir); + } +} + +void WorkspaceManager::SplitWindowVertical() { + ImGuiWindow* window = ImGui::GetCurrentWindowRead(); + if (window && window->DockNode) { + ImGuiID node_id = window->DockNode->ID; + ImGuiID out_id_at_dir = 0; + ImGuiID out_id_at_opposite_dir = 0; + ImGui::DockBuilderSplitNode(node_id, ImGuiDir_Right, 0.5f, + &out_id_at_dir, &out_id_at_opposite_dir); + } +} + +void WorkspaceManager::CloseCurrentWindow() { + ImGuiWindow* window = ImGui::GetCurrentWindowRead(); + if (window) { + window->Hidden = true; + } +} + +// Command execution for WhichKey integration +void WorkspaceManager::ExecuteWorkspaceCommand(const std::string& command_id) { + // Window commands (Space + w) + if (command_id == "w.s") { ShowAllWindows(); } + else if (command_id == "w.h") { HideAllWindows(); } + else if (command_id == "w.m") { MaximizeCurrentWindow(); } + else if (command_id == "w.r") { RestoreAllWindows(); } + else if (command_id == "w.c") { CloseCurrentWindow(); } + else if (command_id == "w.f") { CloseAllFloatingWindows(); } + else if (command_id == "w.v") { SplitWindowVertical(); } + else if (command_id == "w.H") { SplitWindowHorizontal(); } + + // Layout commands (Space + l) + else if (command_id == "l.s") { SaveWorkspaceLayout(); } + else if (command_id == "l.l") { LoadWorkspaceLayout(); } + else if (command_id == "l.r") { ResetWorkspaceLayout(); } + else if (command_id == "l.d") { LoadDeveloperLayout(); } + else if (command_id == "l.g") { LoadDesignerLayout(); } + else if (command_id == "l.m") { LoadModderLayout(); } + + // Unknown command + else if (toast_manager_) { + toast_manager_->Show(absl::StrFormat("Unknown command: %s", command_id), + ToastType::kWarning); + } +} + } // namespace editor } // namespace yaze diff --git a/src/app/editor/ui/workspace_manager.h b/src/app/editor/ui/workspace_manager.h index feb36215..34988258 100644 --- a/src/app/editor/ui/workspace_manager.h +++ b/src/app/editor/ui/workspace_manager.h @@ -47,6 +47,16 @@ class WorkspaceManager { void MaximizeCurrentWindow(); void RestoreAllWindows(); void CloseAllFloatingWindows(); + + // Window operations for keyboard navigation + void FocusNextWindow(); + void FocusPreviousWindow(); + void SplitWindowHorizontal(); + void SplitWindowVertical(); + void CloseCurrentWindow(); + + // Command execution (for WhichKey integration) + void ExecuteWorkspaceCommand(const std::string& command_id); // Session queries size_t GetActiveSessionCount() const; diff --git a/src/app/gfx/compression.cc b/src/app/gfx/compression.cc index 12d65aa2..302ecfb9 100644 --- a/src/app/gfx/compression.cc +++ b/src/app/gfx/compression.cc @@ -7,7 +7,7 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" #include "app/rom.h" -#include "app/zelda3/hyrule_magic.h" +#include "util/hyrule_magic.h" #include "util/macro.h" #define DEBUG_LOG(msg) std::cout << msg << std::endl diff --git a/src/app/editor/ui/background_renderer.cc b/src/app/gui/background_renderer.cc similarity index 99% rename from src/app/editor/ui/background_renderer.cc rename to src/app/gui/background_renderer.cc index 442c6d84..b6147dba 100644 --- a/src/app/editor/ui/background_renderer.cc +++ b/src/app/gui/background_renderer.cc @@ -1,4 +1,4 @@ -#include "app/editor/ui/background_renderer.h" +#include "app/gui/background_renderer.h" #include #include diff --git a/src/app/editor/ui/background_renderer.h b/src/app/gui/background_renderer.h similarity index 100% rename from src/app/editor/ui/background_renderer.h rename to src/app/gui/background_renderer.h diff --git a/src/app/gui/canvas/canvas_utils.cc b/src/app/gui/canvas/canvas_utils.cc index 69137ea4..db2793db 100644 --- a/src/app/gui/canvas/canvas_utils.cc +++ b/src/app/gui/canvas/canvas_utils.cc @@ -385,5 +385,29 @@ void DrawCanvasLabels(const CanvasRenderContext& ctx, } } // namespace CanvasUtils + +// CanvasConfig theme-aware methods implementation +float CanvasConfig::GetToolbarHeight() const { + if (!use_theme_sizing) { + return 32.0f; // Legacy fixed height + } + + // Use layout helpers for theme-aware sizing + // We need to include layout_helpers.h in the implementation file + // For now, return a reasonable default that respects ImGui font size + return ImGui::GetFontSize() * 0.75f; // Will be replaced with LayoutHelpers call +} + +float CanvasConfig::GetGridSpacing() const { + if (!use_theme_sizing) { + return grid_step; // Use configured grid_step as-is + } + + // Apply minimal theme-aware adjustment based on font size + // Grid should stay consistent, but scale slightly with UI density + float base_spacing = ImGui::GetFontSize() * 0.5f; + return std::max(grid_step, base_spacing); +} + } // namespace gui } // namespace yaze diff --git a/src/app/gui/canvas/canvas_utils.h b/src/app/gui/canvas/canvas_utils.h index 16cb7ab5..3a048726 100644 --- a/src/app/gui/canvas/canvas_utils.h +++ b/src/app/gui/canvas/canvas_utils.h @@ -20,12 +20,18 @@ struct CanvasConfig { bool enable_context_menu = true; bool is_draggable = false; bool auto_resize = false; - bool clamp_rect_to_local_maps = true; // NEW: Prevent rectangle wrap across 512x512 boundaries + bool clamp_rect_to_local_maps = true; // Prevent rectangle wrap across 512x512 boundaries + bool use_theme_sizing = true; // Use theme-aware sizing instead of fixed sizes float grid_step = 32.0f; float global_scale = 1.0f; ImVec2 canvas_size = ImVec2(0, 0); ImVec2 content_size = ImVec2(0, 0); // Size of actual content (bitmap, etc.) bool custom_canvas_size = false; + + // Get theme-aware canvas toolbar height (when use_theme_sizing is true) + float GetToolbarHeight() const; + // Get theme-aware grid spacing (when use_theme_sizing is true) + float GetGridSpacing() const; }; /** diff --git a/src/app/gui/gui_library.cmake b/src/app/gui/gui_library.cmake index 51dd91f1..9fe37732 100644 --- a/src/app/gui/gui_library.cmake +++ b/src/app/gui/gui_library.cmake @@ -13,11 +13,14 @@ set( app/gui/editor_card_manager.cc app/gui/editor_layout.cc app/gui/input.cc + app/gui/layout_helpers.cc + app/gui/themed_widgets.cc app/gui/modules/asset_browser.cc app/gui/modules/text_editor.cc app/gui/style.cc app/gui/theme_manager.cc app/gui/ui_helpers.cc + app/gui/background_renderer.cc # Moved from yaze_editor (used by style.cc) app/gui/widgets/agent_chat_widget.cc app/gui/widgets/collaboration_panel.cc app/gui/widgets/dungeon_object_emulator_preview.cc diff --git a/src/app/gui/layout_helpers.cc b/src/app/gui/layout_helpers.cc new file mode 100644 index 00000000..292ba063 --- /dev/null +++ b/src/app/gui/layout_helpers.cc @@ -0,0 +1,281 @@ +#include "app/gui/layout_helpers.h" + +#include "imgui/imgui.h" +#include "imgui/imgui_internal.h" +#include "app/gui/theme_manager.h" +#include "app/gui/color.h" + +namespace yaze { +namespace gui { + +// Core sizing functions +float LayoutHelpers::GetStandardWidgetHeight() { + const auto& theme = GetTheme(); + return GetBaseFontSize() * theme.widget_height_multiplier * theme.compact_factor; +} + +float LayoutHelpers::GetStandardSpacing() { + const auto& theme = GetTheme(); + return GetBaseFontSize() * 0.5f * theme.spacing_multiplier * theme.compact_factor; +} + +float LayoutHelpers::GetToolbarHeight() { + const auto& theme = GetTheme(); + return GetBaseFontSize() * theme.toolbar_height_multiplier * theme.compact_factor; +} + +float LayoutHelpers::GetPanelPadding() { + const auto& theme = GetTheme(); + return GetBaseFontSize() * 0.5f * theme.panel_padding_multiplier * theme.compact_factor; +} + +float LayoutHelpers::GetStandardInputWidth() { + const auto& theme = GetTheme(); + return GetBaseFontSize() * 8.0f * theme.input_width_multiplier * theme.compact_factor; +} + +float LayoutHelpers::GetButtonPadding() { + const auto& theme = GetTheme(); + return GetBaseFontSize() * 0.3f * theme.button_padding_multiplier * theme.compact_factor; +} + +float LayoutHelpers::GetTableRowHeight() { + const auto& theme = GetTheme(); + return GetBaseFontSize() * theme.table_row_height_multiplier * theme.compact_factor; +} + +float LayoutHelpers::GetCanvasToolbarHeight() { + const auto& theme = GetTheme(); + return GetBaseFontSize() * theme.canvas_toolbar_multiplier * theme.compact_factor; +} + +// Layout utilities +void LayoutHelpers::BeginPaddedPanel(const char* label, float padding) { + if (padding < 0.0f) { + padding = GetPanelPadding(); + } + ImGui::BeginChild(label, ImVec2(0, 0), true); + ImGui::Dummy(ImVec2(padding, padding)); + ImGui::SameLine(); + ImGui::BeginGroup(); + ImGui::Dummy(ImVec2(0, padding)); +} + +void LayoutHelpers::EndPaddedPanel() { + ImGui::Dummy(ImVec2(0, GetPanelPadding())); + ImGui::EndGroup(); + ImGui::SameLine(); + ImGui::Dummy(ImVec2(GetPanelPadding(), 0)); + ImGui::EndChild(); +} + +bool LayoutHelpers::BeginTableWithTheming(const char* str_id, int columns, + ImGuiTableFlags flags, + const ImVec2& outer_size, + float inner_width) { + const auto& theme = GetTheme(); + + // Apply theme colors to table + ImGui::PushStyleColor(ImGuiCol_TableHeaderBg, ConvertColorToImVec4(theme.table_header_bg)); + ImGui::PushStyleColor(ImGuiCol_TableBorderStrong, ConvertColorToImVec4(theme.table_border_strong)); + ImGui::PushStyleColor(ImGuiCol_TableBorderLight, ConvertColorToImVec4(theme.table_border_light)); + ImGui::PushStyleColor(ImGuiCol_TableRowBg, ConvertColorToImVec4(theme.table_row_bg)); + ImGui::PushStyleColor(ImGuiCol_TableRowBgAlt, ConvertColorToImVec4(theme.table_row_bg_alt)); + + // Set row height if not overridden by caller + if (!(flags & ImGuiTableFlags_NoHostExtendY)) { + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, + ImVec2(ImGui::GetStyle().CellPadding.x, GetTableRowHeight() * 0.25f)); + } + + return ImGui::BeginTable(str_id, columns, flags, outer_size, inner_width); +} + +void LayoutHelpers::BeginCanvasPanel(const char* label, ImVec2* canvas_size) { + const auto& theme = GetTheme(); + + // Apply theme to canvas container + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.editor_background)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + + if (canvas_size) { + ImGui::BeginChild(label, *canvas_size, true); + } else { + ImGui::BeginChild(label, ImVec2(0, 0), true); + } +} + +void LayoutHelpers::EndCanvasPanel() { + ImGui::EndChild(); + ImGui::PopStyleVar(1); + ImGui::PopStyleColor(1); +} + +// Input field helpers +bool LayoutHelpers::AutoSizedInputField(const char* label, char* buf, + size_t buf_size, ImGuiInputTextFlags flags) { + ImGui::SetNextItemWidth(GetStandardInputWidth()); + return ImGui::InputText(label, buf, buf_size, flags); +} + +bool LayoutHelpers::AutoSizedInputInt(const char* label, int* v, int step, + int step_fast, ImGuiInputTextFlags flags) { + ImGui::SetNextItemWidth(GetStandardInputWidth()); + return ImGui::InputInt(label, v, step, step_fast, flags); +} + +bool LayoutHelpers::AutoSizedInputFloat(const char* label, float* v, float step, + float step_fast, const char* format, + ImGuiInputTextFlags flags) { + ImGui::SetNextItemWidth(GetStandardInputWidth()); + return ImGui::InputFloat(label, v, step, step_fast, format, flags); +} + +// Input preset functions for common patterns +bool LayoutHelpers::InputHexRow(const char* label, uint8_t* data) { + const auto& theme = GetTheme(); + float input_width = GetStandardInputWidth() * 0.5f; // Hex inputs are smaller + + ImGui::AlignTextToFramePadding(); + ImGui::Text("%s", label); + ImGui::SameLine(); + + // Use theme-aware input width for hex byte (2 chars + controls) + ImGui::SetNextItemWidth(input_width); + + char buf[8]; + snprintf(buf, sizeof(buf), "%02X", *data); + + bool changed = ImGui::InputText( + ("##" + std::string(label)).c_str(), buf, sizeof(buf), + ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_AutoSelectAll); + + if (changed) { + unsigned int temp; + if (sscanf(buf, "%X", &temp) == 1) { + *data = static_cast(temp & 0xFF); + } + } + + return changed; +} + +bool LayoutHelpers::InputHexRow(const char* label, uint16_t* data) { + const auto& theme = GetTheme(); + float input_width = GetStandardInputWidth() * 0.6f; // Hex word slightly wider + + ImGui::AlignTextToFramePadding(); + ImGui::Text("%s", label); + ImGui::SameLine(); + + // Use theme-aware input width for hex word (4 chars + controls) + ImGui::SetNextItemWidth(input_width); + + char buf[8]; + snprintf(buf, sizeof(buf), "%04X", *data); + + bool changed = ImGui::InputText( + ("##" + std::string(label)).c_str(), buf, sizeof(buf), + ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_AutoSelectAll); + + if (changed) { + unsigned int temp; + if (sscanf(buf, "%X", &temp) == 1) { + *data = static_cast(temp & 0xFFFF); + } + } + + return changed; +} + +void LayoutHelpers::BeginPropertyGrid(const char* label) { + const auto& theme = GetTheme(); + + // Create a 2-column table for property editing + if (ImGui::BeginTable(label, 2, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + // Setup columns: label column (30%) and value column (70%) + ImGui::TableSetupColumn("Property", ImGuiTableColumnFlags_WidthFixed, + GetStandardInputWidth() * 1.5f); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + } +} + +void LayoutHelpers::EndPropertyGrid() { + ImGui::EndTable(); +} + +bool LayoutHelpers::InputToolbarField(const char* label, char* buf, size_t buf_size) { + // Compact input field for toolbars + float compact_width = GetStandardInputWidth() * 0.8f * GetTheme().compact_factor; + ImGui::SetNextItemWidth(compact_width); + + return ImGui::InputText(label, buf, buf_size, + ImGuiInputTextFlags_AutoSelectAll); +} + +// Toolbar helpers +void LayoutHelpers::BeginToolbar(const char* label) { + const auto& theme = GetTheme(); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.menu_bar_bg)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, + ImVec2(GetButtonPadding(), GetButtonPadding())); + ImGui::BeginChild(label, ImVec2(0, GetToolbarHeight()), true, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); +} + +void LayoutHelpers::EndToolbar() { + ImGui::EndChild(); + ImGui::PopStyleVar(1); + ImGui::PopStyleColor(1); +} + +void LayoutHelpers::ToolbarSeparator() { + ImGui::SameLine(); + ImGui::Dummy(ImVec2(GetStandardSpacing(), 0)); + ImGui::SameLine(); + ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); + ImGui::SameLine(); + ImGui::Dummy(ImVec2(GetStandardSpacing(), 0)); + ImGui::SameLine(); +} + +bool LayoutHelpers::ToolbarButton(const char* label, const ImVec2& size) { + const auto& theme = GetTheme(); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + ImVec2(GetButtonPadding(), GetButtonPadding())); + bool result = ImGui::Button(label, size); + ImGui::PopStyleVar(1); + return result; +} + +// Common layout patterns +void LayoutHelpers::PropertyRow(const char* label, std::function widget_callback) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::Text("%s", label); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + widget_callback(); +} + +void LayoutHelpers::SectionHeader(const char* label) { + const auto& theme = GetTheme(); + ImGui::PushStyleColor(ImGuiCol_Text, ConvertColorToImVec4(theme.accent)); + ImGui::SeparatorText(label); + ImGui::PopStyleColor(1); +} + +void LayoutHelpers::HelpMarker(const char* desc) { + ImGui::TextDisabled("(?)"); + if (ImGui::BeginItemTooltip()) { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextUnformatted(desc); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } +} + +} // namespace gui +} // namespace yaze diff --git a/src/app/gui/layout_helpers.h b/src/app/gui/layout_helpers.h new file mode 100644 index 00000000..d011b414 --- /dev/null +++ b/src/app/gui/layout_helpers.h @@ -0,0 +1,83 @@ +#ifndef YAZE_APP_GUI_LAYOUT_HELPERS_H +#define YAZE_APP_GUI_LAYOUT_HELPERS_H + +#include "imgui/imgui.h" +#include "app/gui/theme_manager.h" + +namespace yaze { +namespace gui { + +/** + * @brief Theme-aware sizing helpers for consistent UI layout + * + * All sizing functions respect the current theme's compact_factor and + * semantic multipliers, ensuring layouts are consistent but customizable. + */ +class LayoutHelpers { + public: + // Core sizing functions (respect theme compact_factor + multipliers) + static float GetStandardWidgetHeight(); + static float GetStandardSpacing(); + static float GetToolbarHeight(); + static float GetPanelPadding(); + static float GetStandardInputWidth(); + static float GetButtonPadding(); + static float GetTableRowHeight(); + static float GetCanvasToolbarHeight(); + + // Layout utilities + static void BeginPaddedPanel(const char* label, float padding = -1.0f); + static void EndPaddedPanel(); + + static bool BeginTableWithTheming(const char* str_id, int columns, + ImGuiTableFlags flags = 0, + const ImVec2& outer_size = ImVec2(0, 0), + float inner_width = 0.0f); + static void EndTable() { ImGui::EndTable(); } + + static void BeginCanvasPanel(const char* label, ImVec2* canvas_size = nullptr); + static void EndCanvasPanel(); + + // Input field helpers + static bool AutoSizedInputField(const char* label, char* buf, size_t buf_size, + ImGuiInputTextFlags flags = 0); + static bool AutoSizedInputInt(const char* label, int* v, int step = 1, + int step_fast = 100, ImGuiInputTextFlags flags = 0); + static bool AutoSizedInputFloat(const char* label, float* v, float step = 0.0f, + float step_fast = 0.0f, const char* format = "%.3f", + ImGuiInputTextFlags flags = 0); + + // Input preset functions for common patterns + static bool InputHexRow(const char* label, uint8_t* data); + static bool InputHexRow(const char* label, uint16_t* data); + static void BeginPropertyGrid(const char* label); + static void EndPropertyGrid(); + static bool InputToolbarField(const char* label, char* buf, size_t buf_size); + + // Toolbar helpers + static void BeginToolbar(const char* label); + static void EndToolbar(); + static void ToolbarSeparator(); + static bool ToolbarButton(const char* label, const ImVec2& size = ImVec2(0, 0)); + + // Common layout patterns + static void PropertyRow(const char* label, std::function widget_callback); + static void SectionHeader(const char* label); + static void HelpMarker(const char* desc); + + // Get current theme + static const EnhancedTheme& GetTheme() { + return ThemeManager::Get().GetCurrentTheme(); + } + + private: + static float GetBaseFontSize() { return ImGui::GetFontSize(); } + static float ApplyCompactFactor(float base_value) { + return base_value * GetTheme().compact_factor; + } +}; + +} // namespace gui +} // namespace yaze + +#endif // YAZE_APP_GUI_LAYOUT_HELPERS_H diff --git a/src/app/gui/style.cc b/src/app/gui/style.cc index a45ef34b..0ad52233 100644 --- a/src/app/gui/style.cc +++ b/src/app/gui/style.cc @@ -4,7 +4,7 @@ #include "util/file_util.h" #include "app/gui/theme_manager.h" -#include "app/editor/ui/background_renderer.h" +#include "app/gui/background_renderer.h" #include "app/platform/font_loader.h" #include "app/gui/color.h" #include "app/gui/icons.h" diff --git a/src/app/gui/theme_manager.h b/src/app/gui/theme_manager.h index 80ce4ce1..aa345a73 100644 --- a/src/app/gui/theme_manager.h +++ b/src/app/gui/theme_manager.h @@ -150,11 +150,25 @@ struct EnhancedTheme { float tab_rounding = 0.0f; float window_border_size = 0.0f; float frame_border_size = 0.0f; - + // Animation and effects bool enable_animations = true; float animation_speed = 1.0f; bool enable_glow_effects = false; + + // Theme-aware sizing system (relative to font size) + // compact_factor: 0.8 = very compact, 1.0 = normal, 1.2 = spacious + float compact_factor = 1.0f; + + // Semantic sizing multipliers (applied on top of compact_factor) + float widget_height_multiplier = 1.0f; // Standard widget height + float spacing_multiplier = 1.0f; // Padding/margins between elements + float toolbar_height_multiplier = 0.8f; // Compact toolbars + float panel_padding_multiplier = 1.0f; // Panel interior padding + float input_width_multiplier = 1.0f; // Standard input field width + float button_padding_multiplier = 1.0f; // Button interior padding + float table_row_height_multiplier = 1.0f; // Table row height + float canvas_toolbar_multiplier = 0.75f; // Canvas overlay toolbars // Helper methods void ApplyToImGui() const; diff --git a/src/app/gui/themed_widgets.cc b/src/app/gui/themed_widgets.cc new file mode 100644 index 00000000..59a6a2d6 --- /dev/null +++ b/src/app/gui/themed_widgets.cc @@ -0,0 +1,272 @@ +#include "app/gui/themed_widgets.h" + +namespace yaze { +namespace gui { +namespace themed { + +// ============================================================================ +// Buttons +// ============================================================================ + +bool Button(const char* label, const ImVec2& size) { + const auto& theme = GetTheme(); + ImGui::PushStyleColor(ImGuiCol_Button, ConvertColorToImVec4(theme.button)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ConvertColorToImVec4(theme.button_hovered)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ConvertColorToImVec4(theme.button_active)); + + bool result = ImGui::Button(label, size); + + ImGui::PopStyleColor(3); + return result; +} + +bool IconButton(const char* icon, const char* tooltip) { + bool result = Button(icon, ImVec2(LayoutHelpers::GetStandardWidgetHeight(), + LayoutHelpers::GetStandardWidgetHeight())); + if (tooltip && ImGui::IsItemHovered()) { + BeginTooltip(); + ImGui::Text("%s", tooltip); + EndTooltip(); + } + return result; +} + +bool PrimaryButton(const char* label, const ImVec2& size) { + const auto& theme = GetTheme(); + ImGui::PushStyleColor(ImGuiCol_Button, ConvertColorToImVec4(theme.accent)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, + ImVec4(theme.accent.red * 1.2f, theme.accent.green * 1.2f, + theme.accent.blue * 1.2f, theme.accent.alpha)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, + ImVec4(theme.accent.red * 0.8f, theme.accent.green * 0.8f, + theme.accent.blue * 0.8f, theme.accent.alpha)); + + bool result = ImGui::Button(label, size); + + ImGui::PopStyleColor(3); + return result; +} + +bool DangerButton(const char* label, const ImVec2& size) { + const auto& theme = GetTheme(); + ImGui::PushStyleColor(ImGuiCol_Button, ConvertColorToImVec4(theme.error)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, + ImVec4(theme.error.red * 1.2f, theme.error.green * 1.2f, + theme.error.blue * 1.2f, theme.error.alpha)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, + ImVec4(theme.error.red * 0.8f, theme.error.green * 0.8f, + theme.error.blue * 0.8f, theme.error.alpha)); + + bool result = ImGui::Button(label, size); + + ImGui::PopStyleColor(3); + return result; +} + +// ============================================================================ +// Headers & Sections +// ============================================================================ + +void Header(const char* label) { + LayoutHelpers::SectionHeader(label); +} + +bool CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) { + const auto& theme = GetTheme(); + ImGui::PushStyleColor(ImGuiCol_Header, ConvertColorToImVec4(theme.header)); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ConvertColorToImVec4(theme.header_hovered)); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, ConvertColorToImVec4(theme.header_active)); + + bool result = ImGui::CollapsingHeader(label, flags); + + ImGui::PopStyleColor(3); + return result; +} + +// ============================================================================ +// Cards & Panels +// ============================================================================ + +void Card(const char* label, std::function content, const ImVec2& size) { + BeginPanel(label, size); + content(); + EndPanel(); +} + +void BeginPanel(const char* label, const ImVec2& size) { + const auto& theme = GetTheme(); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ConvertColorToImVec4(theme.surface)); + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, theme.window_rounding); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, + ImVec2(LayoutHelpers::GetPanelPadding(), + LayoutHelpers::GetPanelPadding())); + + ImGui::BeginChild(label, size, true); +} + +void EndPanel() { + ImGui::EndChild(); + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(1); +} + +// ============================================================================ +// Inputs +// ============================================================================ + +bool InputText(const char* label, char* buf, size_t buf_size, + ImGuiInputTextFlags flags) { + const auto& theme = GetTheme(); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ConvertColorToImVec4(theme.frame_bg)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ConvertColorToImVec4(theme.frame_bg_hovered)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ConvertColorToImVec4(theme.frame_bg_active)); + + ImGui::SetNextItemWidth(LayoutHelpers::GetStandardInputWidth()); + bool result = ImGui::InputText(label, buf, buf_size, flags); + + ImGui::PopStyleColor(3); + return result; +} + +bool InputInt(const char* label, int* v, int step, int step_fast, + ImGuiInputTextFlags flags) { + const auto& theme = GetTheme(); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ConvertColorToImVec4(theme.frame_bg)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ConvertColorToImVec4(theme.frame_bg_hovered)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ConvertColorToImVec4(theme.frame_bg_active)); + + ImGui::SetNextItemWidth(LayoutHelpers::GetStandardInputWidth()); + bool result = ImGui::InputInt(label, v, step, step_fast, flags); + + ImGui::PopStyleColor(3); + return result; +} + +bool InputFloat(const char* label, float* v, float step, float step_fast, + const char* format, ImGuiInputTextFlags flags) { + const auto& theme = GetTheme(); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ConvertColorToImVec4(theme.frame_bg)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ConvertColorToImVec4(theme.frame_bg_hovered)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ConvertColorToImVec4(theme.frame_bg_active)); + + ImGui::SetNextItemWidth(LayoutHelpers::GetStandardInputWidth()); + bool result = ImGui::InputFloat(label, v, step, step_fast, format, flags); + + ImGui::PopStyleColor(3); + return result; +} + +bool Checkbox(const char* label, bool* v) { + const auto& theme = GetTheme(); + ImGui::PushStyleColor(ImGuiCol_CheckMark, ConvertColorToImVec4(theme.check_mark)); + + bool result = ImGui::Checkbox(label, v); + + ImGui::PopStyleColor(1); + return result; +} + +bool Combo(const char* label, int* current_item, const char* const items[], + int items_count, int popup_max_height_in_items) { + const auto& theme = GetTheme(); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ConvertColorToImVec4(theme.frame_bg)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ConvertColorToImVec4(theme.frame_bg_hovered)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ConvertColorToImVec4(theme.frame_bg_active)); + + ImGui::SetNextItemWidth(LayoutHelpers::GetStandardInputWidth()); + bool result = ImGui::Combo(label, current_item, items, items_count, + popup_max_height_in_items); + + ImGui::PopStyleColor(3); + return result; +} + +// ============================================================================ +// Tables +// ============================================================================ + +bool BeginTable(const char* str_id, int columns, ImGuiTableFlags flags, + const ImVec2& outer_size, float inner_width) { + return LayoutHelpers::BeginTableWithTheming(str_id, columns, flags, outer_size, inner_width); +} + +void EndTable() { + LayoutHelpers::EndTable(); +} + +// ============================================================================ +// Tooltips & Help +// ============================================================================ + +void HelpMarker(const char* desc) { + LayoutHelpers::HelpMarker(desc); +} + +void BeginTooltip() { + const auto& theme = GetTheme(); + ImGui::PushStyleColor(ImGuiCol_PopupBg, ConvertColorToImVec4(theme.popup_bg)); + ImGui::BeginTooltip(); +} + +void EndTooltip() { + ImGui::EndTooltip(); + ImGui::PopStyleColor(1); +} + +// ============================================================================ +// Status & Feedback +// ============================================================================ + +void StatusText(const char* text, StatusType type) { + const auto& theme = GetTheme(); + ImVec4 color; + + switch (type) { + case StatusType::kSuccess: + color = ConvertColorToImVec4(theme.success); + break; + case StatusType::kWarning: + color = ConvertColorToImVec4(theme.warning); + break; + case StatusType::kError: + color = ConvertColorToImVec4(theme.error); + break; + case StatusType::kInfo: + color = ConvertColorToImVec4(theme.info); + break; + } + + ImGui::TextColored(color, "%s", text); +} + +void ProgressBar(float fraction, const ImVec2& size, const char* overlay) { + const auto& theme = GetTheme(); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ConvertColorToImVec4(theme.accent)); + + ImGui::ProgressBar(fraction, size, overlay); + + ImGui::PopStyleColor(1); +} + +// ============================================================================ +// Utility +// ============================================================================ + +void PushWidgetColors() { + const auto& theme = GetTheme(); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ConvertColorToImVec4(theme.frame_bg)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ConvertColorToImVec4(theme.frame_bg_hovered)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ConvertColorToImVec4(theme.frame_bg_active)); + ImGui::PushStyleColor(ImGuiCol_Button, ConvertColorToImVec4(theme.button)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ConvertColorToImVec4(theme.button_hovered)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ConvertColorToImVec4(theme.button_active)); +} + +void PopWidgetColors() { + ImGui::PopStyleColor(6); +} + +} // namespace themed +} // namespace gui +} // namespace yaze diff --git a/src/app/gui/themed_widgets.h b/src/app/gui/themed_widgets.h new file mode 100644 index 00000000..027b1f83 --- /dev/null +++ b/src/app/gui/themed_widgets.h @@ -0,0 +1,211 @@ +#ifndef YAZE_APP_GUI_THEMED_WIDGETS_H +#define YAZE_APP_GUI_THEMED_WIDGETS_H + +#include "imgui/imgui.h" +#include "app/gui/theme_manager.h" +#include "app/gui/layout_helpers.h" +#include "app/gui/color.h" + +namespace yaze { +namespace gui { + +/** + * @brief Opt-in themed widget library for gradual migration + * + * All widgets automatically use the current theme from ThemeManager. + * Editors can opt-in by using these widgets instead of raw ImGui calls. + * + * Usage: + * ```cpp + * using namespace yaze::gui::themed; + * + * if (Button("Save")) { + * // Button uses theme colors automatically + * } + * + * Header("Settings"); // Themed section header + * + * Card("Properties", [&]() { + * // Content inside themed card + * }); + * ``` + */ +namespace themed { + +// ============================================================================ +// Buttons +// ============================================================================ + +/** + * @brief Themed button with automatic color application + */ +bool Button(const char* label, const ImVec2& size = ImVec2(0, 0)); + +/** + * @brief Themed button with icon (Material Design Icons) + */ +bool IconButton(const char* icon, const char* tooltip = nullptr); + +/** + * @brief Primary action button (uses accent color) + */ +bool PrimaryButton(const char* label, const ImVec2& size = ImVec2(0, 0)); + +/** + * @brief Danger/destructive action button (uses error color) + */ +bool DangerButton(const char* label, const ImVec2& size = ImVec2(0, 0)); + +// ============================================================================ +// Headers & Sections +// ============================================================================ + +/** + * @brief Themed section header with accent color + */ +void Header(const char* label); + +/** + * @brief Collapsible section with themed header + */ +bool CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags = 0); + +// ============================================================================ +// Cards & Panels +// ============================================================================ + +/** + * @brief Themed card with rounded corners and shadow + * @param label Unique ID for the card + * @param content Callback function to render card content + * @param size Card size (0, 0 for auto-size) + */ +void Card(const char* label, std::function content, + const ImVec2& size = ImVec2(0, 0)); + +/** + * @brief Begin themed panel (manual version of Card) + */ +void BeginPanel(const char* label, const ImVec2& size = ImVec2(0, 0)); + +/** + * @brief End themed panel + */ +void EndPanel(); + +// ============================================================================ +// Inputs +// ============================================================================ + +/** + * @brief Themed text input + */ +bool InputText(const char* label, char* buf, size_t buf_size, + ImGuiInputTextFlags flags = 0); + +/** + * @brief Themed integer input + */ +bool InputInt(const char* label, int* v, int step = 1, int step_fast = 100, + ImGuiInputTextFlags flags = 0); + +/** + * @brief Themed float input + */ +bool InputFloat(const char* label, float* v, float step = 0.0f, + float step_fast = 0.0f, const char* format = "%.3f", + ImGuiInputTextFlags flags = 0); + +/** + * @brief Themed checkbox + */ +bool Checkbox(const char* label, bool* v); + +/** + * @brief Themed combo box + */ +bool Combo(const char* label, int* current_item, const char* const items[], + int items_count, int popup_max_height_in_items = -1); + +// ============================================================================ +// Tables +// ============================================================================ + +/** + * @brief Begin themed table with automatic styling + */ +bool BeginTable(const char* str_id, int columns, ImGuiTableFlags flags = 0, + const ImVec2& outer_size = ImVec2(0, 0), float inner_width = 0.0f); + +/** + * @brief End themed table + */ +void EndTable(); + +// ============================================================================ +// Tooltips & Help +// ============================================================================ + +/** + * @brief Themed help marker with tooltip + */ +void HelpMarker(const char* desc); + +/** + * @brief Begin themed tooltip + */ +void BeginTooltip(); + +/** + * @brief End themed tooltip + */ +void EndTooltip(); + +// ============================================================================ +// Status & Feedback +// ============================================================================ + +/** + * @brief Themed status text (success, warning, error, info) + */ +void StatusText(const char* text, StatusType type); + +enum class StatusType { + kSuccess, + kWarning, + kError, + kInfo +}; + +/** + * @brief Themed progress bar + */ +void ProgressBar(float fraction, const ImVec2& size = ImVec2(-1, 0), + const char* overlay = nullptr); + +// ============================================================================ +// Utility +// ============================================================================ + +/** + * @brief Get current theme (shortcut) + */ +inline const EnhancedTheme& GetTheme() { + return ThemeManager::Get().GetCurrentTheme(); +} + +/** + * @brief Apply theme colors to next widget + */ +void PushWidgetColors(); + +/** + * @brief Restore previous colors + */ +void PopWidgetColors(); + +} // namespace themed +} // namespace gui +} // namespace yaze + +#endif // YAZE_APP_GUI_THEMED_WIDGETS_H diff --git a/src/app/zelda3/zelda3_library.cmake b/src/app/zelda3/zelda3_library.cmake index 4326a00a..fe8cb9f5 100644 --- a/src/app/zelda3/zelda3_library.cmake +++ b/src/app/zelda3/zelda3_library.cmake @@ -7,7 +7,6 @@ set( app/zelda3/dungeon/room.cc app/zelda3/dungeon/room_layout.cc app/zelda3/dungeon/room_object.cc - app/zelda3/hyrule_magic.cc app/zelda3/music/tracker.cc app/zelda3/overworld/overworld.cc app/zelda3/overworld/overworld_map.cc diff --git a/src/cli/agent.cmake b/src/cli/agent.cmake index b501e4de..b2365c4c 100644 --- a/src/cli/agent.cmake +++ b/src/cli/agent.cmake @@ -1,12 +1,18 @@ set(YAZE_AGENT_SOURCES + # Core infrastructure + cli/service/command_registry.cc cli/service/agent/proposal_executor.cc cli/handlers/agent/todo_commands.cc cli/service/agent/conversational_agent_service.cc cli/service/agent/simple_chat_session.cc cli/service/agent/enhanced_tui.cc cli/service/agent/tool_dispatcher.cc + + # Advanced features cli/service/agent/learned_knowledge_service.cc cli/service/agent/todo_manager.cc + cli/service/agent/advanced_routing.cc + cli/service/agent/agent_pretraining.cc cli/service/agent/vim_mode.cc cli/service/ai/ai_service.cc cli/service/ai/ai_action_parser.cc diff --git a/src/cli/handlers/agent.cc b/src/cli/handlers/agent.cc index 622e2177..b3a613a6 100644 --- a/src/cli/handlers/agent.cc +++ b/src/cli/handlers/agent.cc @@ -7,20 +7,18 @@ #include "absl/flags/declare.h" #include "absl/flags/flag.h" #include "absl/status/status.h" +#include "absl/strings/str_cat.h" +#include "cli/service/command_registry.h" #include "cli/handlers/agent/common.h" #include "cli/handlers/agent/todo_commands.h" #include "cli/handlers/agent/simple_chat_command.h" -#include "cli/handlers/tools/resource_commands.h" -#include "cli/handlers/game/dungeon_commands.h" -#include "cli/handlers/game/overworld_commands.h" -#include "cli/handlers/tools/gui_commands.h" ABSL_DECLARE_FLAG(bool, quiet); namespace yaze { namespace cli { -// Forward declarations from general_commands.cc +// Forward declarations for special agent commands (not in registry) namespace agent { absl::Status HandlePlanCommand(const std::vector& args); absl::Status HandleTestCommand(const std::vector& args); @@ -29,211 +27,134 @@ absl::Status HandleGuiCommand(const std::vector& args); absl::Status HandleLearnCommand(const std::vector& args); absl::Status HandleListCommand(); absl::Status HandleDescribeCommand(const std::vector& args); - -// Wrapper functions to call CommandHandlers -absl::Status HandleResourceListCommand(const std::vector& args, Rom* rom) { - handlers::ResourceListCommandHandler handler; - return handler.Run(args, rom); -} - -absl::Status HandleResourceSearchCommand(const std::vector& args, Rom* rom) { - handlers::ResourceSearchCommandHandler handler; - return handler.Run(args, rom); -} - -absl::Status HandleDungeonListSpritesCommand(const std::vector& args, Rom* rom) { - handlers::DungeonListSpritesCommandHandler handler; - return handler.Run(args, rom); -} - -absl::Status HandleDungeonDescribeRoomCommand(const std::vector& args, Rom* rom) { - handlers::DungeonDescribeRoomCommandHandler handler; - return handler.Run(args, rom); -} - -absl::Status HandleOverworldFindTileCommand(const std::vector& args, Rom* rom) { - handlers::OverworldFindTileCommandHandler handler; - return handler.Run(args, rom); -} - -absl::Status HandleOverworldDescribeMapCommand(const std::vector& args, Rom* rom) { - handlers::OverworldDescribeMapCommandHandler handler; - return handler.Run(args, rom); -} - -absl::Status HandleOverworldListWarpsCommand(const std::vector& args, Rom* rom) { - handlers::OverworldListWarpsCommandHandler handler; - return handler.Run(args, rom); -} - } // namespace agent namespace { -constexpr absl::string_view kUsage = - "Usage: agent [options]\n" - "\n" - "AI-Powered Agent Subcommands:\n" - " simple-chat Simple text-based chat (recommended for testing)\n" - " Modes: interactive | piped | batch | single-message\n" - " Example: agent simple-chat \"What dungeons exist?\" --rom=zelda3.sfc\n" - " Example: agent simple-chat --rom=zelda3.sfc --ai_provider=ollama\n" - " Example: echo \"List sprites\" | agent simple-chat --rom=zelda3.sfc\n" - " Example: agent simple-chat --file=queries.txt --rom=zelda3.sfc\n" - "\n" - " test-conversation Run automated test conversation with AI\n" - " Example: agent test-conversation --rom=zelda3.sfc --ai_provider=ollama\n" - "\n" - " chat Full FTXUI-based chat interface\n" - " Example: agent chat --rom=zelda3.sfc\n" - "\n" - "ROM Inspection Tools (can be called by AI or directly):\n" - " resource-list List labeled resources (dungeons, sprites, etc.)\n" - " Example: agent resource-list --type=dungeon --format=json\n" - "\n" - " resource-search Search resource labels by fuzzy text\n" - " Example: agent resource-search --query=soldier --type=sprite\n" - "\n" - " dungeon-list-sprites List sprites in a dungeon room\n" - " Example: agent dungeon-list-sprites --room=5 --format=json\n" - "\n" - " dungeon-describe-room Summarize metadata for a dungeon room\n" - " Example: agent dungeon-describe-room --room=0x12 --format=text\n" - "\n" - " overworld-find-tile Search for tile placements in overworld\n" - " Example: agent overworld-find-tile --tile=0x02E --format=json\n" - "\n" - " overworld-describe-map Get metadata about an overworld map\n" - " Example: agent overworld-describe-map --map=0 --format=json\n" - "\n" - " overworld-list-warps List entrances/exits/holes in overworld\n" - " Example: agent overworld-list-warps --map=0 --format=json\n" - "\n" - "Proposal & Testing Commands:\n" - " run Execute agent task\n" - " plan Generate execution plan\n" - " diff Show ROM differences\n" - " accept Accept and apply proposal changes\n" - " test Run agent tests\n" - " gui Launch GUI components\n" - " learn Train agent on examples\n" - " list List available resources\n" - " commit Commit changes\n" - " revert Revert changes\n" - " describe Describe agent capabilities\n" - " todo Manage tasks and project planning\n" - "\n" - "Global Options:\n" - " --rom= Path to Zelda3 ROM file (required for most commands)\n" - " --ai_provider= AI provider: mock (default) | ollama | gemini\n" - " --ai_model= Model name (e.g., qwen2.5-coder:7b for Ollama)\n" - " --ollama_host= Ollama server URL (default: http://localhost:11434)\n" - " --gemini_api_key= Gemini API key (or set GEMINI_API_KEY env var)\n" - " --format= Output format: text | markdown | json | compact\n" - "\n" - "For more details, see: docs/simple_chat_input_methods.md"; +std::string GenerateAgentHelp() { + auto& registry = CommandRegistry::Instance(); + + std::ostringstream help; + help << "Usage: agent [options]\n\n"; + + help << "AI-Powered Agent Commands:\n"; + help << " simple-chat Interactive AI chat\n"; + help << " test-conversation Automated test conversation\n"; + help << " plan Generate execution plan\n"; + help << " learn Manage learned knowledge\n"; + help << " todo Task management\n"; + help << " test Run tests\n"; + help << " list/describe List/describe proposals\n\n"; + + // Auto-list available tool commands from registry + help << "Tool Commands (AI can call these):\n"; + auto agent_commands = registry.GetAgentCommands(); + int count = 0; + for (const auto& cmd : agent_commands) { + if (count++ < 10) { // Show first 10 + auto* meta = registry.GetMetadata(cmd); + if (meta) { + help << " " << cmd; + for (size_t i = cmd.length(); i < 24; i++) help << " "; + help << meta->description << "\n"; + } + } + } + help << " ... and " << (agent_commands.size() - 10) << " more (see z3ed --list-commands)\n\n"; + + help << "Global Options:\n"; + help << " --rom= Path to ROM file\n"; + help << " --ai_provider= AI provider: ollama | gemini\n"; + help << " --format= Output format: text | json\n\n"; + + help << "For detailed help: z3ed agent --help\n"; + help << "For all commands: z3ed --list-commands\n"; + + return help.str(); +} + +constexpr absl::string_view kUsage = ""; } // namespace namespace handlers { -// Legacy Agent class removed - using new CommandHandler system -// This implementation should be moved to a proper AgentCommandHandler +/** + * @brief Unified agent command handler using CommandRegistry + * + * Routes commands in this order: + * 1. Special agent commands (plan, test, learn, todo) - Not in registry + * 2. Registry commands (resource-*, dungeon-*, overworld-*, emulator-*, etc.) + * 3. Fallback to error + */ absl::Status HandleAgentCommand(const std::vector& arg_vec) { if (arg_vec.empty()) { - return absl::InvalidArgumentError(std::string(kUsage)); + std::cout << GenerateAgentHelp(); + return absl::InvalidArgumentError("No subcommand specified"); } const std::string& subcommand = arg_vec[0]; std::vector subcommand_args(arg_vec.begin() + 1, arg_vec.end()); - - if (subcommand == "run") { - return absl::UnimplementedError("Agent run command requires ROM context - not yet implemented"); + + // === Special Agent Commands (not in registry) === + + if (subcommand == "simple-chat" || subcommand == "chat") { + auto& registry = CommandRegistry::Instance(); + return registry.Execute("simple-chat", subcommand_args, nullptr); } + if (subcommand == "plan") { return agent::HandlePlanCommand(subcommand_args); } - if (subcommand == "diff") { - return absl::UnimplementedError("Agent diff command requires ROM context - not yet implemented"); - } - if (subcommand == "accept") { - return absl::UnimplementedError("Agent accept command requires ROM context - not yet implemented"); - } + if (subcommand == "test") { return agent::HandleTestCommand(subcommand_args); } + if (subcommand == "test-conversation") { return agent::HandleTestConversationCommand(subcommand_args); } + if (subcommand == "gui") { return agent::HandleGuiCommand(subcommand_args); } + if (subcommand == "learn") { return agent::HandleLearnCommand(subcommand_args); } + + if (subcommand == "todo") { + return handlers::HandleTodoCommand(subcommand_args); + } + if (subcommand == "list") { return agent::HandleListCommand(); } - if (subcommand == "commit") { - return absl::UnimplementedError("Agent commit command requires ROM context - not yet implemented"); - } - if (subcommand == "revert") { - return absl::UnimplementedError("Agent revert command requires ROM context - not yet implemented"); - } + if (subcommand == "describe") { return agent::HandleDescribeCommand(subcommand_args); } - if (subcommand == "resource-list") { - return agent::HandleResourceListCommand(subcommand_args, nullptr); - } - if (subcommand == "resource-search") { - return agent::HandleResourceSearchCommand(subcommand_args, nullptr); - } - if (subcommand == "dungeon-list-sprites") { - return agent::HandleDungeonListSpritesCommand(subcommand_args, nullptr); - } - if (subcommand == "dungeon-describe-room") { - return agent::HandleDungeonDescribeRoomCommand(subcommand_args, nullptr); - } - if (subcommand == "overworld-find-tile") { - return agent::HandleOverworldFindTileCommand(subcommand_args, nullptr); - } - if (subcommand == "overworld-describe-map") { - return agent::HandleOverworldDescribeMapCommand(subcommand_args, nullptr); - } - if (subcommand == "overworld-list-warps") { - return agent::HandleOverworldListWarpsCommand(subcommand_args, nullptr); - } - // if (subcommand == "chat") { - // return absl::UnimplementedError("Agent chat command requires ROM context - not yet implemented"); - // } - // if (subcommand == "todo") { - // return handlers::HandleTodoCommand(subcommand_args); - // } - // // Hex manipulation commands - // if (subcommand == "hex-read") { - // return HandleHexRead(subcommand_args, nullptr); - // } - // if (subcommand == "hex-write") { - // return HandleHexWrite(subcommand_args, nullptr); - // } - // if (subcommand == "hex-search") { - // return HandleHexSearch(subcommand_args, nullptr); - // } + // Placeholder for unimplemented workflow commands + if (subcommand == "run" || subcommand == "diff" || subcommand == "accept" || + subcommand == "commit" || subcommand == "revert") { + return absl::UnimplementedError( + absl::StrCat("Agent ", subcommand, " command requires ROM context - not yet implemented")); + } - // // Palette manipulation commands - // if (subcommand == "palette-get-colors") { - // return HandlePaletteGetColors(subcommand_args, nullptr); - // } - // if (subcommand == "palette-set-color") { - // return HandlePaletteSetColor(subcommand_args, nullptr); - // } - // if (subcommand == "palette-analyze") { - // return HandlePaletteAnalyze(subcommand_args, nullptr); - // } - - return absl::InvalidArgumentError(std::string(kUsage)); + // === Registry Commands (resource, dungeon, overworld, emulator, etc.) === + + auto& registry = CommandRegistry::Instance(); + + // Check if this is a registered command + if (registry.HasCommand(subcommand)) { + return registry.Execute(subcommand, subcommand_args, nullptr); + } + + // Not found + std::cout << GenerateAgentHelp(); + return absl::InvalidArgumentError( + absl::StrCat("Unknown agent command: ", subcommand)); } // Handler functions are now implemented in command_wrappers.cc diff --git a/src/cli/service/command_registry.cc b/src/cli/service/command_registry.cc new file mode 100644 index 00000000..81b9e427 --- /dev/null +++ b/src/cli/service/command_registry.cc @@ -0,0 +1,279 @@ +#include "cli/service/command_registry.h" + +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_join.h" +#include "cli/handlers/command_handlers.h" + +namespace yaze { +namespace cli { + +CommandRegistry& CommandRegistry::Instance() { + static CommandRegistry instance; + static bool initialized = false; + if (!initialized) { + instance.RegisterAllCommands(); + initialized = true; + } + return instance; +} + +void CommandRegistry::Register(std::unique_ptr handler, + const CommandMetadata& metadata) { + std::string name = handler->GetName(); + + // Store metadata + metadata_[name] = metadata; + + // Register aliases + for (const auto& alias : metadata.aliases) { + aliases_[alias] = name; + } + + // Store handler + handlers_[name] = std::move(handler); +} + +resources::CommandHandler* CommandRegistry::Get(const std::string& name) const { + // Check direct name + auto it = handlers_.find(name); + if (it != handlers_.end()) { + return it->second.get(); + } + + // Check aliases + auto alias_it = aliases_.find(name); + if (alias_it != aliases_.end()) { + auto handler_it = handlers_.find(alias_it->second); + if (handler_it != handlers_.end()) { + return handler_it->second.get(); + } + } + + return nullptr; +} + +const CommandRegistry::CommandMetadata* CommandRegistry::GetMetadata( + const std::string& name) const { + // Resolve alias first + std::string canonical_name = name; + auto alias_it = aliases_.find(name); + if (alias_it != aliases_.end()) { + canonical_name = alias_it->second; + } + + auto it = metadata_.find(canonical_name); + return (it != metadata_.end()) ? &it->second : nullptr; +} + +std::vector CommandRegistry::GetCommandsInCategory( + const std::string& category) const { + std::vector result; + for (const auto& [name, metadata] : metadata_) { + if (metadata.category == category) { + result.push_back(name); + } + } + return result; +} + +std::vector CommandRegistry::GetCategories() const { + std::vector categories; + for (const auto& [_, metadata] : metadata_) { + if (std::find(categories.begin(), categories.end(), metadata.category) == categories.end()) { + categories.push_back(metadata.category); + } + } + return categories; +} + +std::vector CommandRegistry::GetAgentCommands() const { + std::vector result; + for (const auto& [name, metadata] : metadata_) { + if (metadata.available_to_agent) { + result.push_back(name); + } + } + return result; +} + +std::string CommandRegistry::ExportFunctionSchemas() const { + // TODO: Generate JSON function schemas from metadata + // This would replace manual function_schemas.json maintenance + return "{}"; // Placeholder +} + +std::string CommandRegistry::GenerateHelp(const std::string& name) const { + auto* metadata = GetMetadata(name); + if (!metadata) { + return absl::StrFormat("Command '%s' not found", name); + } + + std::ostringstream help; + help << "\n\033[1;36m" << metadata->name << "\033[0m - " << metadata->description << "\n\n"; + help << "\033[1;33mUsage:\033[0m\n"; + help << " " << metadata->usage << "\n\n"; + + if (!metadata->examples.empty()) { + help << "\033[1;33mExamples:\033[0m\n"; + for (const auto& example : metadata->examples) { + help << " " << example << "\n"; + } + help << "\n"; + } + + if (metadata->requires_rom) { + help << "\033[1;33mRequires:\033[0m ROM file (--rom=)\n"; + } + if (metadata->requires_grpc) { + help << "\033[1;33mRequires:\033[0m YAZE running with gRPC enabled\n"; + } + + if (!metadata->aliases.empty()) { + help << "\n\033[1;33mAliases:\033[0m " << absl::StrJoin(metadata->aliases, ", ") << "\n"; + } + + return help.str(); +} + +std::string CommandRegistry::GenerateCategoryHelp(const std::string& category) const { + auto commands = GetCommandsInCategory(category); + if (commands.empty()) { + return absl::StrFormat("No commands in category '%s'", category); + } + + std::ostringstream help; + help << "\n\033[1;36m" << category << " commands:\033[0m\n\n"; + + for (const auto& cmd : commands) { + auto* metadata = GetMetadata(cmd); + if (metadata) { + help << " \033[1;33m" << cmd << "\033[0m\n"; + help << " " << metadata->description << "\n"; + if (!metadata->usage.empty()) { + help << " Usage: " << metadata->usage << "\n"; + } + help << "\n"; + } + } + + return help.str(); +} + +std::string CommandRegistry::GenerateCompleteHelp() const { + std::ostringstream help; + help << "\n\033[1;36mAll z3ed Commands:\033[0m\n\n"; + + auto categories = GetCategories(); + for (const auto& category : categories) { + help << GenerateCategoryHelp(category); + } + + return help.str(); +} + +absl::Status CommandRegistry::Execute(const std::string& name, + const std::vector& args, + Rom* rom_context) { + auto* handler = Get(name); + if (!handler) { + return absl::NotFoundError(absl::StrFormat("Command '%s' not found", name)); + } + + return handler->Run(args, rom_context); +} + +bool CommandRegistry::HasCommand(const std::string& name) const { + return Get(name) != nullptr; +} + +void CommandRegistry::RegisterAllCommands() { + // Get all command handlers from factory + auto all_handlers = handlers::CreateAllCommandHandlers(); + + for (auto& handler : all_handlers) { + std::string name = handler->GetName(); + + // Build metadata from handler + CommandMetadata metadata; + metadata.name = name; + metadata.usage = handler->GetUsage(); + metadata.available_to_agent = true; // Most commands available to agent + metadata.requires_rom = true; // Most commands need ROM + metadata.requires_grpc = false; + + // Categorize and enhance metadata based on command type + if (name.find("resource-") == 0) { + metadata.category = "resource"; + metadata.description = "Resource inspection and search"; + if (name == "resource-list") { + metadata.examples = { + "z3ed resource-list --type=dungeon --format=json", + "z3ed resource-list --type=sprite --format=table" + }; + } + } else if (name.find("dungeon-") == 0) { + metadata.category = "dungeon"; + metadata.description = "Dungeon inspection and editing"; + if (name == "dungeon-describe-room") { + metadata.examples = { + "z3ed dungeon-describe-room --room=5 --format=json" + }; + } + } else if (name.find("overworld-") == 0) { + metadata.category = "overworld"; + metadata.description = "Overworld inspection and editing"; + if (name == "overworld-find-tile") { + metadata.examples = { + "z3ed overworld-find-tile --tile=0x42 --format=json" + }; + } + } else if (name.find("emulator-") == 0) { + metadata.category = "emulator"; + metadata.description = "Emulator control and debugging"; + metadata.requires_grpc = true; + if (name == "emulator-set-breakpoint") { + metadata.examples = { + "z3ed emulator-set-breakpoint --address=0x83D7 --description='NMI handler'" + }; + } + } else if (name.find("gui-") == 0) { + metadata.category = "gui"; + metadata.description = "GUI automation"; + metadata.requires_grpc = true; + } else if (name.find("hex-") == 0) { + metadata.category = "graphics"; + metadata.description = "Hex data manipulation"; + } else if (name.find("palette-") == 0) { + metadata.category = "graphics"; + metadata.description = "Palette operations"; + } else if (name.find("sprite-") == 0) { + metadata.category = "graphics"; + metadata.description = "Sprite operations"; + } else if (name.find("message-") == 0 || name.find("dialogue-") == 0) { + metadata.category = "game"; + metadata.description = name.find("message-") == 0 ? "Message inspection" : "Dialogue inspection"; + } else if (name.find("music-") == 0) { + metadata.category = "game"; + metadata.description = "Music/audio inspection"; + } else if (name == "simple-chat" || name == "chat") { + metadata.category = "agent"; + metadata.description = "AI conversational agent"; + metadata.available_to_agent = false; // Meta-command + metadata.requires_rom = false; + metadata.examples = { + "z3ed simple-chat --rom=zelda3.sfc", + "z3ed simple-chat \"What dungeons exist?\" --rom=zelda3.sfc" + }; + } else { + metadata.category = "misc"; + metadata.description = "Miscellaneous command"; + } + + Register(std::move(handler), metadata); + } +} + +} // namespace cli +} // namespace yaze + diff --git a/src/cli/service/command_registry.h b/src/cli/service/command_registry.h new file mode 100644 index 00000000..f34c4132 --- /dev/null +++ b/src/cli/service/command_registry.h @@ -0,0 +1,130 @@ +#ifndef YAZE_CLI_SERVICE_COMMAND_REGISTRY_H_ +#define YAZE_CLI_SERVICE_COMMAND_REGISTRY_H_ + +#include +#include +#include +#include + +#include "absl/status/statusor.h" +#include "cli/service/resources/command_handler.h" + +namespace yaze { +namespace cli { + +/** + * @class CommandRegistry + * @brief Single source of truth for all z3ed commands + * + * Serves as the central registry for: + * - CLI command routing + * - Agent tool calling + * - Help text generation + * - TUI menu generation + * - Function schema export (for AI) + * + * Ensures consistency: if a command exists, it's available to both + * human users (CLI) and AI agents (tool calling). + */ +class CommandRegistry { + public: + struct CommandMetadata { + std::string name; // e.g., "resource-list" + std::string category; // e.g., "resource", "emulator", "dungeon" + std::string description; // Short description + std::string usage; // Full usage string + bool available_to_agent; // Can AI call this via tool dispatch? + bool requires_rom; // Requires ROM context? + bool requires_grpc; // Requires gRPC/emulator running? + std::vector aliases; // Alternative names + std::vector examples; // Usage examples + std::string todo_reference; // TODO tracker reference (if incomplete) + }; + + static CommandRegistry& Instance(); + + /** + * @brief Register a command handler + */ + void Register(std::unique_ptr handler, + const CommandMetadata& metadata); + + /** + * @brief Get a command handler by name or alias + */ + resources::CommandHandler* Get(const std::string& name) const; + + /** + * @brief Get command metadata + */ + const CommandMetadata* GetMetadata(const std::string& name) const; + + /** + * @brief Get all commands in a category + */ + std::vector GetCommandsInCategory(const std::string& category) const; + + /** + * @brief Get all categories + */ + std::vector GetCategories() const; + + /** + * @brief Get all commands available to AI agents + */ + std::vector GetAgentCommands() const; + + /** + * @brief Export function schemas for AI tool calling (JSON) + */ + std::string ExportFunctionSchemas() const; + + /** + * @brief Generate help text for a command + */ + std::string GenerateHelp(const std::string& name) const; + + /** + * @brief Generate category help text + */ + std::string GenerateCategoryHelp(const std::string& category) const; + + /** + * @brief Generate complete help text (all commands) + */ + std::string GenerateCompleteHelp() const; + + /** + * @brief Execute a command by name + */ + absl::Status Execute(const std::string& name, + const std::vector& args, + Rom* rom_context = nullptr); + + /** + * @brief Check if command exists + */ + bool HasCommand(const std::string& name) const; + + /** + * @brief Get total command count + */ + size_t Count() const { return handlers_.size(); } + + private: + CommandRegistry() = default; + + // Storage + std::map> handlers_; + std::map metadata_; + std::map aliases_; // alias → canonical name + + // Auto-register all commands + void RegisterAllCommands(); +}; + +} // namespace cli +} // namespace yaze + +#endif // YAZE_CLI_SERVICE_COMMAND_REGISTRY_H_ + diff --git a/src/cli/service/resources/command_handler.h b/src/cli/service/resources/command_handler.h index 5e7b986a..31fc6710 100644 --- a/src/cli/service/resources/command_handler.h +++ b/src/cli/service/resources/command_handler.h @@ -81,6 +81,11 @@ class CommandHandler { * @brief Provide metadata for TUI/help summaries. */ virtual Descriptor Describe() const; + + /** + * @brief Get the command usage string + */ + virtual std::string GetUsage() const = 0; protected: /** @@ -100,10 +105,6 @@ class CommandHandler { virtual absl::Status Execute(Rom* rom, const ArgumentParser& parser, OutputFormatter& formatter) = 0; - /** - * @brief Get the command usage string - */ - virtual std::string GetUsage() const = 0; /** * @brief Check if the command requires ROM labels diff --git a/src/app/zelda3/hyrule_magic.cc b/src/util/hyrule_magic.cc similarity index 98% rename from src/app/zelda3/hyrule_magic.cc rename to src/util/hyrule_magic.cc index 4e047258..979522e2 100644 --- a/src/app/zelda3/hyrule_magic.cc +++ b/src/util/hyrule_magic.cc @@ -1,4 +1,4 @@ -#include "hyrule_magic.h" +#include "util/hyrule_magic.h" namespace yaze { namespace zelda3 { diff --git a/src/app/zelda3/hyrule_magic.h b/src/util/hyrule_magic.h similarity index 86% rename from src/app/zelda3/hyrule_magic.h rename to src/util/hyrule_magic.h index a6e6f1c5..de7de2df 100644 --- a/src/app/zelda3/hyrule_magic.h +++ b/src/util/hyrule_magic.h @@ -1,5 +1,5 @@ -#ifndef YAZE_APP_ZELDA3_HYRULE_MAGIC_H -#define YAZE_APP_ZELDA3_HYRULE_MAGIC_H +#ifndef YAZE_UTIL_HYRULE_MAGIC_H +#define YAZE_UTIL_HYRULE_MAGIC_H #include #include @@ -28,4 +28,4 @@ uint16_t ldle16b(uint8_t const *const p_arr); } // namespace zelda3 } // namespace yaze -#endif // YAZE_APP_ZELDA3_HYRULE_MAGIC_H \ No newline at end of file +#endif // YAZE_UTIL_HYRULE_MAGIC_H \ No newline at end of file diff --git a/src/util/util.cmake b/src/util/util.cmake index bbb64a2e..b646a6dd 100644 --- a/src/util/util.cmake +++ b/src/util/util.cmake @@ -17,7 +17,7 @@ set(YAZE_UTIL_SRC util/log.cc util/platform_paths.cc util/file_util.cc - util/platform_paths.cc + util/hyrule_magic.cc # Byte order utilities (moved from zelda3) ) add_library(yaze_util STATIC ${YAZE_UTIL_SRC})