diff --git a/src/app/gui/canvas/canvas.cc b/src/app/gui/canvas/canvas.cc index 2c8027d2..b37dea38 100644 --- a/src/app/gui/canvas/canvas.cc +++ b/src/app/gui/canvas/canvas.cc @@ -605,51 +605,64 @@ void Canvas::ClearContextMenuItems() { } void Canvas::OpenPersistentPopup(const std::string& popup_id, std::function render_callback) { - // Check if popup already exists + // Phase 3: Delegate to new popup registry + popup_registry_.Open(popup_id, render_callback); + + // Maintain backward compatibility with legacy active_popups_ vector + // TODO(Phase 4): Remove this synchronization once all editors migrate + bool found = false; for (auto& popup : active_popups_) { if (popup.popup_id == popup_id) { popup.is_open = true; - popup.render_callback = std::move(render_callback); - ImGui::OpenPopup(popup_id.c_str()); - return; + popup.render_callback = render_callback; + found = true; + break; } } - // Add new popup - PopupState new_popup; - new_popup.popup_id = popup_id; - new_popup.is_open = true; - new_popup.render_callback = std::move(render_callback); - active_popups_.push_back(new_popup); - ImGui::OpenPopup(popup_id.c_str()); + if (!found) { + PopupState new_popup; + new_popup.popup_id = popup_id; + new_popup.is_open = true; + new_popup.render_callback = render_callback; + active_popups_.push_back(new_popup); + } } void Canvas::ClosePersistentPopup(const std::string& popup_id) { + // Phase 3: Delegate to new popup registry + popup_registry_.Close(popup_id); + + // Maintain backward compatibility with legacy active_popups_ vector + // TODO(Phase 4): Remove this synchronization once all editors migrate for (auto& popup : active_popups_) { if (popup.popup_id == popup_id) { popup.is_open = false; - ImGui::CloseCurrentPopup(); return; } } } void Canvas::RenderPersistentPopups() { - // Render all active popups + // Phase 3: Delegate to new popup registry + popup_registry_.RenderAll(); + + // Maintain backward compatibility: sync legacy vector with registry state + // TODO(Phase 4): Remove this synchronization once all editors migrate + const auto& registry_popups = popup_registry_.GetPopups(); + + // Remove closed popups from legacy vector auto it = active_popups_.begin(); while (it != active_popups_.end()) { - if (it->is_open && it->render_callback) { - // Call the render callback which should handle BeginPopup/EndPopup - it->render_callback(); - - // If popup was closed by user, mark it for removal - if (!ImGui::IsPopupOpen(it->popup_id.c_str())) { - it->is_open = false; + bool found_in_registry = false; + for (const auto& reg_popup : registry_popups) { + if (reg_popup.popup_id == it->popup_id && reg_popup.is_open) { + found_in_registry = true; + break; } } - // Remove closed popups - if (!it->is_open) { + if (!found_in_registry) { it = active_popups_.erase(it); } else { ++it; @@ -1487,6 +1500,8 @@ void GraphicsBinCanvasPipeline(int width, int height, int tile_size, canvas.DrawTileSelector(tile_size); canvas.DrawGrid(tile_size); canvas.DrawOverlay(); + // Phase 3: Render persistent popups (previously only available via End()) + canvas.RenderPersistentPopups(); } ImGui::EndChild(); } @@ -1502,6 +1517,8 @@ void BitmapCanvasPipeline(gui::Canvas& canvas, gfx::Bitmap& bitmap, int width, canvas.DrawTileSelector(tile_size); canvas.DrawGrid(tile_size); canvas.DrawOverlay(); + // Phase 3: Render persistent popups (previously only available via End()) + canvas.RenderPersistentPopups(); }; if (scrollbar) { @@ -1542,6 +1559,8 @@ void TableCanvasPipeline(gui::Canvas& canvas, gfx::Bitmap& bitmap, canvas.DrawGrid(); canvas.DrawOverlay(); + // Phase 3: Render persistent popups (previously only available via End()) + canvas.RenderPersistentPopups(); } canvas.EndTableCanvas(); } diff --git a/src/app/gui/canvas/canvas.h b/src/app/gui/canvas/canvas.h index a04445c3..dcebeb80 100644 --- a/src/app/gui/canvas/canvas.h +++ b/src/app/gui/canvas/canvas.h @@ -23,6 +23,8 @@ #include "app/gui/canvas/canvas_usage_tracker.h" #include "app/gui/canvas/canvas_performance_integration.h" #include "app/gui/canvas/canvas_interaction_handler.h" +#include "app/gui/canvas/canvas_menu.h" +#include "app/gui/canvas/canvas_popup.h" #include "imgui/imgui.h" namespace yaze { @@ -205,6 +207,10 @@ class Canvas { void ClosePersistentPopup(const std::string& popup_id); void RenderPersistentPopups(); + // Popup registry access (Phase 3: for advanced users and testing) + PopupRegistry& GetPopupRegistry() { return popup_registry_; } + const PopupRegistry& GetPopupRegistry() const { return popup_registry_; } + // Enhanced view and edit operations void ShowAdvancedCanvasProperties(); void ShowScalingControls(); @@ -432,6 +438,11 @@ class Canvas { bool context_menu_enabled_ = true; // Persistent popup state for context menu actions + // Phase 3: New popup registry (preferred for new code) + PopupRegistry popup_registry_; + + // Legacy popup state (deprecated - kept for backward compatibility during migration) + // TODO(Phase 4): Remove once all editors use popup_registry_ directly struct PopupState { std::string popup_id; bool is_open = false; diff --git a/src/app/gui/canvas/canvas_menu.cc b/src/app/gui/canvas/canvas_menu.cc new file mode 100644 index 00000000..791f4197 --- /dev/null +++ b/src/app/gui/canvas/canvas_menu.cc @@ -0,0 +1,116 @@ +#include "canvas_menu.h" + +namespace yaze { +namespace gui { + +void RenderMenuItem(const CanvasMenuItem& item, + std::function)> + popup_opened_callback) { + // Check visibility + if (!item.visible_condition()) { + return; + } + + // Apply disabled state if needed + if (!item.enabled_condition()) { + ImGui::BeginDisabled(); + } + + // Build label with icon if present + std::string display_label = item.label; + if (!item.icon.empty()) { + display_label = item.icon + " " + item.label; + } + + // Render menu item based on type + if (item.subitems.empty()) { + // Simple menu item + bool selected = false; + if (item.color.x != 1.0f || item.color.y != 1.0f || + item.color.z != 1.0f || item.color.w != 1.0f) { + // Render with custom color + ImGui::PushStyleColor(ImGuiCol_Text, item.color); + selected = ImGui::MenuItem(display_label.c_str(), + item.shortcut.empty() ? nullptr : item.shortcut.c_str()); + ImGui::PopStyleColor(); + } else { + selected = ImGui::MenuItem(display_label.c_str(), + item.shortcut.empty() ? nullptr : item.shortcut.c_str()); + } + + if (selected) { + // Invoke callback + if (item.callback) { + item.callback(); + } + + // Handle popup if defined + if (item.popup.has_value() && + item.popup->auto_open_on_select && + popup_opened_callback) { + popup_opened_callback(item.popup->popup_id, item.popup->render_callback); + } + } + } else { + // Submenu + if (ImGui::BeginMenu(display_label.c_str())) { + for (const auto& subitem : item.subitems) { + RenderMenuItem(subitem, popup_opened_callback); + } + ImGui::EndMenu(); + } + } + + // Restore enabled state + if (!item.enabled_condition()) { + ImGui::EndDisabled(); + } + + // Render separator if requested + if (item.separator_after) { + ImGui::Separator(); + } +} + +void RenderMenuSection(const CanvasMenuSection& section, + std::function)> + popup_opened_callback) { + // Skip empty sections + if (section.items.empty()) { + return; + } + + // Render section title if present + if (!section.title.empty()) { + ImGui::TextColored(section.title_color, "%s", section.title.c_str()); + ImGui::Separator(); + } + + // Render all items in section + for (const auto& item : section.items) { + RenderMenuItem(item, popup_opened_callback); + } + + // Render separator after section if requested + if (section.separator_after) { + ImGui::Separator(); + } +} + +void RenderCanvasMenu(const CanvasMenuDefinition& menu, + std::function)> + popup_opened_callback) { + // Skip disabled menus + if (!menu.enabled) { + return; + } + + // Render all sections + for (const auto& section : menu.sections) { + RenderMenuSection(section, popup_opened_callback); + } +} + +} // namespace gui +} // namespace yaze + diff --git a/src/app/gui/canvas/canvas_menu.h b/src/app/gui/canvas/canvas_menu.h new file mode 100644 index 00000000..aadfc6e4 --- /dev/null +++ b/src/app/gui/canvas/canvas_menu.h @@ -0,0 +1,230 @@ +#ifndef YAZE_APP_GUI_CANVAS_CANVAS_MENU_H +#define YAZE_APP_GUI_CANVAS_CANVAS_MENU_H + +#include +#include +#include +#include + +#include "imgui/imgui.h" + +namespace yaze { +namespace gui { + +/** + * @brief Declarative popup definition for menu items + * + * Links a menu item to a persistent popup that should open when the menu + * item is selected. This separates popup definition from popup rendering. + */ +struct CanvasPopupDefinition { + // Unique popup identifier for ImGui + std::string popup_id; + + // Callback that renders the popup content (should call ImGui::BeginPopup/EndPopup) + std::function render_callback; + + // Whether to automatically open the popup when menu item is selected + bool auto_open_on_select = true; + + // Whether the popup should persist across frames until explicitly closed + bool persist_across_frames = true; + + // Default constructor + CanvasPopupDefinition() = default; + + // Constructor for simple popups + CanvasPopupDefinition(const std::string& id, std::function callback) + : popup_id(id), render_callback(std::move(callback)) {} +}; + +/** + * @brief Declarative menu item definition + * + * Pure data structure representing a menu item with optional popup linkage. + * Can be composed into hierarchical menus via subitems. + */ +struct CanvasMenuItem { + // Display label for the menu item + std::string label; + + // Optional icon (Material Design icon name or Unicode glyph) + std::string icon; + + // Optional keyboard shortcut display (e.g., "Ctrl+S") + std::string shortcut; + + // Callback invoked when menu item is selected + std::function callback; + + // Optional popup definition - if present, popup will be managed automatically + std::optional popup; + + // Condition to determine if menu item is enabled + std::function enabled_condition = []() { return true; }; + + // Condition to determine if menu item is visible + std::function visible_condition = []() { return true; }; + + // Nested submenu items + std::vector subitems; + + // Color for the menu item label + ImVec4 color = ImVec4(1, 1, 1, 1); + + // Whether to show a separator after this item + bool separator_after = false; + + // Default constructor + CanvasMenuItem() = default; + + // Simple menu item constructor + CanvasMenuItem(const std::string& lbl, std::function cb) + : label(lbl), callback(std::move(cb)) {} + + // Menu item with icon + CanvasMenuItem(const std::string& lbl, const std::string& ico, + std::function cb) + : label(lbl), icon(ico), callback(std::move(cb)) {} + + // Menu item with icon and shortcut + CanvasMenuItem(const std::string& lbl, const std::string& ico, + std::function cb, const std::string& sc) + : label(lbl), icon(ico), callback(std::move(cb)), shortcut(sc) {} + + // Helper to create a disabled menu item + static CanvasMenuItem Disabled(const std::string& lbl) { + CanvasMenuItem item; + item.label = lbl; + item.enabled_condition = []() { return false; }; + return item; + } + + // Helper to create a conditional menu item + static CanvasMenuItem Conditional(const std::string& lbl, + std::function cb, + std::function condition) { + CanvasMenuItem item; + item.label = lbl; + item.callback = std::move(cb); + item.enabled_condition = std::move(condition); + return item; + } + + // Helper to create a menu item with popup + static CanvasMenuItem WithPopup(const std::string& lbl, + const std::string& popup_id, + std::function render_callback) { + CanvasMenuItem item; + item.label = lbl; + item.popup = CanvasPopupDefinition(popup_id, std::move(render_callback)); + return item; + } +}; + +/** + * @brief Menu section grouping related menu items + * + * Provides visual organization of menu items with optional section titles. + */ +struct CanvasMenuSection { + // Optional section title (rendered as colored text) + std::string title; + + // Color for section title + ImVec4 title_color = ImVec4(0.7f, 0.7f, 0.7f, 1.0f); + + // Menu items in this section + std::vector items; + + // Whether to show a separator after this section + bool separator_after = true; + + // Default constructor + CanvasMenuSection() = default; + + // Constructor with title + explicit CanvasMenuSection(const std::string& t) : title(t) {} + + // Constructor with title and items + CanvasMenuSection(const std::string& t, const std::vector& its) + : title(t), items(its) {} +}; + +/** + * @brief Complete menu definition + * + * Aggregates menu sections for a complete context menu or popup menu. + */ +struct CanvasMenuDefinition { + // Menu sections (rendered in order) + std::vector sections; + + // Whether the menu is enabled + bool enabled = true; + + // Default constructor + CanvasMenuDefinition() = default; + + // Constructor with sections + explicit CanvasMenuDefinition(const std::vector& secs) + : sections(secs) {} + + // Add a section + void AddSection(const CanvasMenuSection& section) { + sections.push_back(section); + } + + // Add items without a section title + void AddItems(const std::vector& items) { + CanvasMenuSection section; + section.items = items; + section.separator_after = false; + sections.push_back(section); + } +}; + +// ==================== Free Function API ==================== + +/** + * @brief Render a single menu item + * + * Handles visibility, enabled state, subitems, and popup linkage. + * + * @param item Menu item to render + * @param popup_opened_callback Optional callback invoked when popup is opened + */ +void RenderMenuItem(const CanvasMenuItem& item, + std::function)> + popup_opened_callback = nullptr); + +/** + * @brief Render a menu section + * + * Renders section title (if present), all items, and separator. + * + * @param section Menu section to render + * @param popup_opened_callback Optional callback invoked when popup is opened + */ +void RenderMenuSection(const CanvasMenuSection& section, + std::function)> + popup_opened_callback = nullptr); + +/** + * @brief Render a complete menu definition + * + * Renders all sections in order. Does not handle ImGui::BeginPopup/EndPopup - + * caller is responsible for popup context. + * + * @param menu Menu definition to render + * @param popup_opened_callback Optional callback invoked when popup is opened + */ +void RenderCanvasMenu(const CanvasMenuDefinition& menu, + std::function)> + popup_opened_callback = nullptr); + +} // namespace gui +} // namespace yaze + +#endif // YAZE_APP_GUI_CANVAS_CANVAS_MENU_H + diff --git a/src/app/gui/canvas/canvas_popup.cc b/src/app/gui/canvas/canvas_popup.cc new file mode 100644 index 00000000..6d5ce8e9 --- /dev/null +++ b/src/app/gui/canvas/canvas_popup.cc @@ -0,0 +1,113 @@ +#include "canvas_popup.h" + +#include + +namespace yaze { +namespace gui { + +void PopupRegistry::Open(const std::string& popup_id, + std::function render_callback) { + // Check if popup already exists + auto it = FindPopup(popup_id); + + if (it != popups_.end()) { + // Update existing popup + it->is_open = true; + it->render_callback = std::move(render_callback); + ImGui::OpenPopup(popup_id.c_str()); + return; + } + + // Add new popup + PopupState new_popup; + new_popup.popup_id = popup_id; + new_popup.is_open = true; + new_popup.render_callback = std::move(render_callback); + new_popup.persist = true; + + popups_.push_back(new_popup); + + // Open the popup in ImGui + ImGui::OpenPopup(popup_id.c_str()); +} + +void PopupRegistry::Close(const std::string& popup_id) { + auto it = FindPopup(popup_id); + + if (it != popups_.end()) { + it->is_open = false; + + // Close in ImGui if it's the current popup + // Note: ImGui::CloseCurrentPopup() only works if this is the active popup + // In practice, the popup will be removed on next RenderAll() call + if (ImGui::IsPopupOpen(popup_id.c_str())) { + ImGui::CloseCurrentPopup(); + } + } +} + +bool PopupRegistry::IsOpen(const std::string& popup_id) const { + auto it = FindPopup(popup_id); + return it != popups_.end() && it->is_open; +} + +void PopupRegistry::RenderAll() { + // Render all active popups + auto it = popups_.begin(); + while (it != popups_.end()) { + if (it->is_open && it->render_callback) { + // Call the render callback which should handle BeginPopup/EndPopup + it->render_callback(); + + // Check if popup was closed by user (clicking outside, pressing Escape, etc.) + if (!ImGui::IsPopupOpen(it->popup_id.c_str())) { + it->is_open = false; + } + } + + // Remove closed popups from the registry + if (!it->is_open) { + it = popups_.erase(it); + } else { + ++it; + } + } +} + +size_t PopupRegistry::GetActiveCount() const { + return std::count_if(popups_.begin(), popups_.end(), + [](const PopupState& popup) { return popup.is_open; }); +} + +void PopupRegistry::Clear() { + // Close all popups + for (auto& popup : popups_) { + if (popup.is_open && ImGui::IsPopupOpen(popup.popup_id.c_str())) { + ImGui::CloseCurrentPopup(); + } + popup.is_open = false; + } + + // Clear the registry + popups_.clear(); +} + +std::vector::iterator PopupRegistry::FindPopup( + const std::string& popup_id) { + return std::find_if(popups_.begin(), popups_.end(), + [&popup_id](const PopupState& popup) { + return popup.popup_id == popup_id; + }); +} + +std::vector::const_iterator PopupRegistry::FindPopup( + const std::string& popup_id) const { + return std::find_if(popups_.begin(), popups_.end(), + [&popup_id](const PopupState& popup) { + return popup.popup_id == popup_id; + }); +} + +} // namespace gui +} // namespace yaze + diff --git a/src/app/gui/canvas/canvas_popup.h b/src/app/gui/canvas/canvas_popup.h new file mode 100644 index 00000000..61c9fda1 --- /dev/null +++ b/src/app/gui/canvas/canvas_popup.h @@ -0,0 +1,130 @@ +#ifndef YAZE_APP_GUI_CANVAS_CANVAS_POPUP_H +#define YAZE_APP_GUI_CANVAS_CANVAS_POPUP_H + +#include +#include +#include + +#include "imgui/imgui.h" + +namespace yaze { +namespace gui { + +/** + * @brief State for a single persistent popup + * + * POD struct representing the state of a popup that persists across frames. + * Popups remain open until explicitly closed or the user dismisses them. + */ +struct PopupState { + // Unique popup identifier (used with ImGui::OpenPopup/BeginPopup) + std::string popup_id; + + // Whether the popup is currently open + bool is_open = false; + + // Callback that renders the popup content + // Should call ImGui::BeginPopup(popup_id) / ImGui::EndPopup() + std::function render_callback; + + // Whether the popup should persist across frames + bool persist = true; + + // Default constructor + PopupState() = default; + + // Constructor with id and callback + PopupState(const std::string& id, std::function callback) + : popup_id(id), is_open(false), render_callback(std::move(callback)) {} +}; + +/** + * @brief Registry for managing persistent popups + * + * Maintains a collection of popups and their lifecycle. Handles opening, + * closing, and rendering popups across frames. + * + * This class is designed to be embedded in Canvas or used standalone for + * testing and custom UI components. + */ +class PopupRegistry { + public: + PopupRegistry() = default; + + /** + * @brief Open a persistent popup + * + * If the popup already exists, updates its callback and reopens it. + * If the popup is new, adds it to the registry and opens it. + * + * @param popup_id Unique identifier for the popup + * @param render_callback Function that renders the popup content + */ + void Open(const std::string& popup_id, std::function render_callback); + + /** + * @brief Close a persistent popup + * + * Marks the popup as closed. It will be removed from the registry on the + * next render pass. + * + * @param popup_id Identifier of the popup to close + */ + void Close(const std::string& popup_id); + + /** + * @brief Check if a popup is currently open + * + * @param popup_id Identifier of the popup to check + * @return true if popup is open, false otherwise + */ + bool IsOpen(const std::string& popup_id) const; + + /** + * @brief Render all active popups + * + * Iterates through all open popups and calls their render callbacks. + * Automatically removes popups that have been closed by the user. + * + * This should be called once per frame, typically at the end of the + * frame after all other rendering is complete. + */ + void RenderAll(); + + /** + * @brief Get the number of active popups + * + * @return Number of open popups in the registry + */ + size_t GetActiveCount() const; + + /** + * @brief Clear all popups from the registry + * + * Closes all popups and removes them from the registry. + * Useful for cleanup or resetting state. + */ + void Clear(); + + /** + * @brief Get direct access to the popup list (for migration/debugging) + * + * @return Reference to the internal popup vector + */ + std::vector& GetPopups() { return popups_; } + const std::vector& GetPopups() const { return popups_; } + + private: + // Internal storage for popup states + std::vector popups_; + + // Helper to find a popup by ID + std::vector::iterator FindPopup(const std::string& popup_id); + std::vector::const_iterator FindPopup(const std::string& popup_id) const; +}; + +} // namespace gui +} // namespace yaze + +#endif // YAZE_APP_GUI_CANVAS_CANVAS_POPUP_H + diff --git a/src/app/gui/gui_library.cmake b/src/app/gui/gui_library.cmake index d6afba52..91f7d5a0 100644 --- a/src/app/gui/gui_library.cmake +++ b/src/app/gui/gui_library.cmake @@ -32,8 +32,10 @@ set(CANVAS_SRC app/gui/canvas/canvas_geometry.cc app/gui/canvas/canvas_interaction.cc app/gui/canvas/canvas_interaction_handler.cc + app/gui/canvas/canvas_menu.cc app/gui/canvas/canvas_modals.cc app/gui/canvas/canvas_performance_integration.cc + app/gui/canvas/canvas_popup.cc app/gui/canvas/canvas_rendering.cc app/gui/canvas/canvas_usage_tracker.cc app/gui/canvas/canvas_utils.cc