Files
yaze/docs/public/developer/gui-consistency-guide.md
2025-11-21 21:35:50 -05:00

1333 lines
36 KiB
Markdown

# G5 - GUI Consistency and Card-Based Architecture Guide
This guide establishes standards for GUI consistency across all yaze editors, focusing on the modern card-based architecture, theming system, and layout patterns.
## Table of Contents
1. [Introduction](#1-introduction)
2. [Card-Based Architecture](#2-card-based-architecture)
3. [VSCode-Style Sidebar System](#3-vscode-style-sidebar-system)
4. [Toolset System](#4-toolset-system)
5. [GUI Library Architecture](#5-gui-library-architecture)
6. [Themed Widget System](#6-themed-widget-system)
7. [Begin/End Patterns](#7-beginend-patterns)
8. [Currently Integrated Editors](#8-currently-integrated-editors)
9. [Layout Helpers](#9-layout-helpers)
10. [Workspace Management](#10-workspace-management)
11. [Future Editor Improvements](#11-future-editor-improvements)
12. [Migration Checklist](#12-migration-checklist)
13. [Code Examples](#13-code-examples)
14. [Common Pitfalls](#14-common-pitfalls)
## 1. Introduction
### Purpose
This guide establishes GUI consistency standards to ensure all editors in yaze provide a unified, modern user experience with maintainable code. The card-based architecture allows editors to present multiple independent windows that can be opened, closed, minimized, and managed independently.
### Benefits
- **User Experience**: Consistent keyboard shortcuts, visual styling, and interaction patterns
- **Maintainability**: Reusable components reduce duplication and bugs
- **Modularity**: Independent cards can be developed and tested separately
- **Flexibility**: Users can arrange their workspace as needed
- **Discoverability**: Central EditorCardManager makes all features accessible
### Target Audience
Contributors working on:
- New editor implementations
- Refactoring existing editors
- Adding new UI features to editors
- Improving user experience consistency
## 2. Card-Based Architecture
### Philosophy
Modern yaze editors use **independent, modular windows** called "cards" rather than traditional tab-based or fixed-layout UIs. Each card:
- Is a top-level ImGui window (not a child window)
- Can be opened/closed independently via keyboard shortcuts
- Can be minimized to a floating icon
- Registers with `EditorCardManager` for centralized control
- Has its own visibility flag synchronized with the manager
### Core Components
#### EditorCardManager
Central singleton registry for all editor cards across the application.
**Key Features:**
- Global card registration with metadata
- Keyboard shortcut management
- View menu integration
- Workspace preset system
- Programmatic card control
**Registration Example:**
```cpp
void MyEditor::Initialize() {
auto& card_manager = gui::EditorCardManager::Get();
card_manager.RegisterCard({
.card_id = "myeditor.control_panel",
.display_name = "My Editor Controls",
.icon = ICON_MD_SETTINGS,
.category = "MyEditor",
.shortcut_hint = "Ctrl+Shift+M",
.visibility_flag = &show_control_panel_,
.priority = 10
});
// Register more cards...
}
```
#### EditorCard
Wrapper class for individual card windows with Begin/End pattern.
**Key Features:**
- Automatic positioning (Right, Left, Bottom, Floating, Free)
- Default size management
- Minimize/maximize support
- Focus management
- Docking control
**Usage Pattern:**
```cpp
void MyEditor::DrawMyCard() {
gui::EditorCard card("Card Title", ICON_MD_ICON, &show_card_);
card.SetDefaultSize(400, 300);
card.SetPosition(gui::EditorCard::Position::Right);
if (card.Begin(&show_card_)) {
// Draw card content here
ImGui::Text("Card content");
}
card.End();
}
```
### Centralized Visibility Pattern
All editors now use the centralized visibility system where EditorCardManager owns and manages all visibility bools:
```cpp
// Registration (in Initialize()):
card_manager.RegisterCard({
.card_id = "music.tracker",
.display_name = "Music Tracker",
.icon = ICON_MD_MUSIC_NOTE,
.category = "Music"
});
// Usage (in Update()):
static gui::EditorCard card("Music Tracker", ICON_MD_MUSIC_NOTE);
if (card.Begin(card_manager.GetVisibilityFlag("music.tracker"))) {
DrawContent();
card.End();
}
```
**Benefits:**
- Single source of truth for all card visibility
- No scattered bool members in editor classes
- Automatic X button close functionality
- Consistent behavior across all cards
- Easy to query/modify from anywhere
### Reference Implementations
**Best Examples:**
- `src/app/editor/dungeon/dungeon_editor_v2.cc` - Gold standard implementation
- `src/app/editor/palette/palette_editor.cc` - Recently refactored, clean patterns
**Key Patterns from Dungeon Editor v2:**
- Independent top-level cards (no parent wrapper)
- Control panel with minimize-to-icon
- Toolset integration
- Proper card registration with shortcuts
- Room cards in separate docking class
## 3. VSCode-Style Sidebar System
### Overview
The VSCode-style sidebar provides a unified interface for managing editor cards. It's a fixed 48px sidebar on the left edge with icon-based card toggles.
**Key Features:**
- Fixed position on left edge (48px width)
- Icon-based card toggles
- Category switcher for multi-editor sessions
- Card browser button (Ctrl+Shift+B)
- Collapse button (Ctrl+B)
- Theme-aware styling
- Recent categories stack (last 5 used)
### Usage
Each card-based editor simply calls:
```cpp
void MyEditor::DrawToolset() {
auto& card_manager = gui::EditorCardManager::Get();
card_manager.DrawSidebar("MyEditor");
}
```
The sidebar automatically reads from the existing card registry - no per-editor configuration needed.
### Card Browser
Press `Ctrl+Shift+B` to open the card browser:
- Search/filter cards by name
- Category tabs
- Visibility toggle for all cards
- Statistics (total/visible cards)
- Preset management
- Batch operations (Show All, Hide All per category)
## 4. Toolset System
### Overview
`gui::Toolset` provides an ultra-compact toolbar that merges mode buttons with inline settings. It's designed for minimal vertical space usage while maximizing functionality.
**Design Philosophy:**
- Single horizontal bar with everything inline
- Small icon-only buttons for modes
- Inline property editing (InputHex with scroll)
- Vertical separators for visual grouping
- No wasted space
### Basic Usage
```cpp
void MyEditor::DrawToolset() {
static gui::Toolset toolbar;
toolbar.Begin();
// Add toggle buttons for cards
if (toolbar.AddToggle(ICON_MD_LIST, &show_list_, "Show List (Ctrl+1)")) {
// Optional: React to toggle
}
if (toolbar.AddToggle(ICON_MD_GRID_VIEW, &show_grid_, "Show Grid (Ctrl+2)")) {
// Toggled
}
toolbar.AddSeparator();
// Add action buttons
if (toolbar.AddAction(ICON_MD_SAVE, "Save All")) {
SaveAllChanges();
}
if (toolbar.AddAction(ICON_MD_REFRESH, "Reload")) {
ReloadData();
}
toolbar.End();
}
```
### Advanced Features
**Inline Property Editing:**
```cpp
// Hex properties with scroll wheel support
toolbar.AddProperty(ICON_MD_PALETTE, "Palette", &palette_id_,
[]() { OnPaletteChanged(); });
toolbar.AddProperty(ICON_MD_IMAGE, "GFX", &gfx_id_,
[]() { OnGfxChanged(); });
```
**Mode Button Groups:**
```cpp
toolbar.BeginModeGroup();
bool draw_mode = toolbar.ModeButton(ICON_MD_BRUSH, mode_ == Mode::Draw, "Draw");
bool erase_mode = toolbar.ModeButton(ICON_MD_DELETE, mode_ == Mode::Erase, "Erase");
bool select_mode = toolbar.ModeButton(ICON_MD_SELECT, mode_ == Mode::Select, "Select");
toolbar.EndModeGroup();
if (draw_mode) mode_ = Mode::Draw;
if (erase_mode) mode_ = Mode::Erase;
if (select_mode) mode_ = Mode::Select;
```
**Version Badges:**
```cpp
// For ROM version indicators
toolbar.AddRomBadge(rom_->asm_version(), []() {
ShowUpgradeDialog();
});
toolbar.AddV3StatusBadge(rom_->asm_version(), []() {
ShowV3Settings();
});
```
### Best Practices
1. **Keep it compact**: Only essential controls belong in the Toolset
2. **Use icons**: Prefer icon-only buttons with tooltips
3. **Group logically**: Use separators to group related controls
4. **Provide shortcuts**: Include keyboard shortcuts in tooltips
5. **Consistent ordering**: Toggles first, properties second, actions third
## 5. GUI Library Architecture
### Modular Library Structure
The yaze GUI is organized into focused, layered libraries for improved build times and maintainability:
**gui_core (Foundation)**
- Theme management, colors, styling
- Icons, input handling, layout helpers
- Dependencies: yaze_util, ImGui, SDL2
**canvas (Core Widget)**
- Canvas widget system
- Canvas utilities, modals, context menus
- Dependencies: gui_core, yaze_gfx
**gui_widgets (Reusable Components)**
- Themed widgets, palette widgets
- Asset browser, text editor, tile selector
- Dependencies: gui_core, yaze_gfx
**gui_automation (Testing & AI)**
- Widget ID registry, auto-registration
- Widget state capture and measurement
- Dependencies: gui_core
**gui_app (Application-Specific UI)**
- EditorCardManager, EditorLayout
- Background renderer, collaboration panel
- Dependencies: gui_core, gui_widgets, gui_automation
**yaze_gui (Interface Library)**
- Aggregates all sub-libraries
- Single link target for executables
### Theme-Aware Sizing System
All UI sizing respects the theme's `compact_factor` (0.8-1.2) for global density control:
```cpp
#include "app/gui/layout_helpers.h"
using gui::LayoutHelpers;
// Standard widget sizing
float widget_height = LayoutHelpers::GetStandardWidgetHeight();
float spacing = LayoutHelpers::GetStandardSpacing();
float toolbar_height = LayoutHelpers::GetToolbarHeight();
// All sizing respects: theme.compact_factor * multiplier * ImGui::GetFontSize()
```
**Layout Helpers API:**
- `BeginTableWithTheming()` - Tables with automatic theme colors
- `BeginCanvasPanel() / EndCanvasPanel()` - Canvas containers
- `BeginPaddedPanel() / EndPaddedPanel()` - Consistent padding
- `InputHexRow()` - Labeled hex inputs
- `BeginPropertyGrid() / EndPropertyGrid()` - 2-column property tables
- `PropertyRow()` - Label + widget in table row
- `SectionHeader()` - Colored section headers
- `HelpMarker()` - Tooltip help icons
## 6. Themed Widget System
### Philosophy
**Never use hardcoded colors.** All UI elements must derive colors from the central theme system to ensure consistency and support for future dark/light theme switching.
### Themed Widget Prefixes
All theme-aware widgets are prefixed with `Themed*`:
**Available Widgets:**
- `ThemedButton()` - Standard button with theme colors
- `ThemedIconButton()` - Icon-only button
- `PrimaryButton()` - Emphasized primary action (e.g., Save)
- `DangerButton()` - Dangerous action (e.g., Delete, Discard)
- `SectionHeader()` - Visual section divider with text
### Usage Examples
```cpp
#include "app/gui/core/themed_widgets.h"
using gui::ThemedButton;
using gui::ThemedIconButton;
using gui::PrimaryButton;
using gui::DangerButton;
using gui::SectionHeader;
void MyCard::DrawContent() {
SectionHeader("Settings");
if (PrimaryButton("Save Changes", ImVec2(-1, 0))) {
SaveToRom();
}
if (DangerButton("Discard All", ImVec2(-1, 0))) {
DiscardChanges();
}
ImGui::Separator();
SectionHeader("Quick Actions");
if (ThemedIconButton(ICON_MD_REFRESH, "Reload")) {
Reload();
}
ImGui::SameLine();
if (ThemedIconButton(ICON_MD_COPY, "Duplicate")) {
Duplicate();
}
}
```
### Theme Colors
Access theme colors via `AgentUITheme` (despite the name, it's used project-wide):
```cpp
#include "app/editor/agent/agent_ui_theme.h"
void DrawCustomUI() {
const auto& theme = AgentUI::GetTheme();
// Use semantic colors
ImGui::PushStyleColor(ImGuiCol_ChildBg, theme.panel_bg_color);
ImGui::PushStyleColor(ImGuiCol_Text, theme.text_color);
// Draw content
ImGui::BeginChild("MyPanel");
ImGui::Text("Themed panel content");
ImGui::EndChild();
ImGui::PopStyleColor(2);
}
```
**Common Theme Colors:**
- `panel_bg_color` - Background for panels
- `text_color` - Primary text
- `text_dim_color` - Secondary/disabled text
- `accent_color` - Highlights and accents
- `status_success` - Success indicators (green)
- `status_warning` - Warning indicators (yellow)
- `status_error` - Error indicators (red)
### Migration from Hardcoded Colors
**Before (Bad):**
```cpp
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.2f, 0.2f, 0.25f, 1.0f));
ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Error!");
```
**After (Good):**
```cpp
const auto& theme = AgentUI::GetTheme();
ImGui::PushStyleColor(ImGuiCol_ChildBg, theme.panel_bg_color);
ImGui::TextColored(theme.status_error, "Error!");
```
### WhichKey Command System
Spacemacs-style hierarchical command navigation:
```cpp
// Press Space → Shows root menu with colored categories
// Press category key (e.g., w) → Shows window submenu
// Press command key → Executes command and closes
// Press ESC → Go back or close
```
**Features:**
- Fixed bottom bar (150px height)
- Color-coded categories
- Breadcrumb navigation ("Space > w")
- Auto-close after 5 seconds of inactivity
**Integration:**
```cpp
command_manager_.RegisterPrefix("w", 'w', "Window", "Window management");
command_manager_.RegisterSubcommand("w", "w.s", 's', "Show All", "Show all windows",
[this]() { workspace_manager_.ShowAllWindows(); });
```
## 7. Begin/End Patterns
### Philosophy
All resource-managing UI elements use the Begin/End pattern for RAII-style cleanup. This prevents resource leaks and ensures proper ImGui state management.
### EditorCard Begin/End
**Pattern:**
```cpp
void DrawMyCard() {
gui::EditorCard card("Title", ICON_MD_ICON, &show_flag_);
card.SetDefaultSize(400, 300);
// Begin returns false if window is collapsed/hidden
if (card.Begin(&show_flag_)) {
// Draw content only when visible
DrawCardContent();
}
// End MUST be called regardless of Begin result
card.End();
}
```
**Critical Rules:**
1. Always call `End()` even if `Begin()` returns false
2. Put Begin/End calls in same scope for exception safety
3. Check Begin() return value before expensive drawing
4. Pass `p_open` to both constructor and Begin() for proper close button handling
### ImGui Native Begin/End
**Window Pattern:**
```cpp
void DrawWindow() {
ImGui::SetNextWindowSize(ImVec2(400, 300), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Window Title", &show_window_)) {
// Draw window content
ImGui::Text("Content");
}
ImGui::End(); // ALWAYS call, even if Begin returns false
}
```
**Table Pattern:**
```cpp
if (ImGui::BeginTable("##MyTable", 3, ImGuiTableFlags_Borders)) {
ImGui::TableSetupColumn("Column 1");
ImGui::TableSetupColumn("Column 2");
ImGui::TableSetupColumn("Column 3");
ImGui::TableHeadersRow();
for (int row = 0; row < 10; row++) {
ImGui::TableNextRow();
for (int col = 0; col < 3; col++) {
ImGui::TableNextColumn();
ImGui::Text("Cell %d,%d", row, col);
}
}
ImGui::EndTable();
}
```
**Child Window Pattern:**
```cpp
if (ImGui::BeginChild("##ScrollRegion", ImVec2(0, 200), true)) {
// Scrollable content
for (int i = 0; i < 100; i++) {
ImGui::Text("Item %d", i);
}
}
ImGui::EndChild();
```
### Toolset Begin/End
```cpp
void DrawToolbar() {
static gui::Toolset toolbar;
toolbar.Begin();
// Add toolbar items
toolbar.AddToggle(ICON_MD_ICON, &flag_, "Tooltip");
toolbar.End(); // Finalizes layout
}
```
### Error Handling
**With Status Returns:**
```cpp
absl::Status DrawEditor() {
gui::EditorCard card("Editor", ICON_MD_EDIT);
if (card.Begin()) {
RETURN_IF_ERROR(DrawContent()); // Can early return
}
card.End(); // Still called via normal flow
return absl::OkStatus();
}
```
**Exception Safety:**
```cpp
// If exceptions are enabled, use RAII wrappers
struct ScopedCard {
gui::EditorCard& card;
explicit ScopedCard(gui::EditorCard& c) : card(c) { card.Begin(); }
~ScopedCard() { card.End(); }
};
```
## 8. Currently Integrated Editors
The card system is integrated across 11 of 13 editors:
| Editor | Cards | Status |
|--------|-------|--------|
| **DungeonEditorV2** | Room selector, canvas, object selector, object editor, entrance editor, tile painter, sprite placer | Complete |
| **PaletteEditor** | Group editor, animation editor, color picker | Complete |
| **GraphicsEditor** | Sheet editor, browser, player animations, prototype viewer | Complete |
| **ScreenEditor** | Dungeon maps, inventory, overworld map, title screen, naming screen | Complete |
| **SpriteEditor** | Vanilla sprites, custom sprites | Complete |
| **OverworldEditor** | Canvas, tile16/tile8 selectors, area graphics, scratch workspace, GFX groups, usage stats, properties, exits, items, sprites, settings | Complete |
| **MessageEditor** | Message list, editor, font atlas, dictionary | Complete |
| **HexEditor** | Hex editor with comparison | Complete |
| **AssemblyEditor** | Assembly editor, file browser | Complete |
| **MusicEditor** | Music tracker, instrument editor, assembly view | Complete |
| **Emulator** | CPU debugger, PPU viewer, memory viewer, breakpoints, performance, AI agent, save states, keyboard config, APU debugger, audio mixer | Complete |
**Not Yet Ported:**
- **SettingsEditor** - Monolithic settings window, low usage frequency
- **AgentEditor** - Complex AI agent UI, under active development
## 9. Layout Helpers
### Overview
`app/gui/layout_helpers.h` provides utilities for consistent spacing, sizing, and layout across all editors.
### Standard Input Widths
```cpp
#include "app/gui/core/layout_helpers.h"
using gui::LayoutHelpers;
void DrawSettings() {
ImGui::Text("Property:");
ImGui::SameLine();
// Standard width for input fields (120px)
ImGui::SetNextItemWidth(LayoutHelpers::GetStandardInputWidth());
ImGui::InputInt("##value", &my_value_);
}
```
### Help Markers
```cpp
ImGui::Text("Complex Setting");
ImGui::SameLine();
LayoutHelpers::HelpMarker(
"This is a detailed explanation of what this setting does. "
"It appears as a tooltip when hovering the (?) icon."
);
```
### Spacing Utilities
```cpp
// Vertical spacing
LayoutHelpers::VerticalSpacing(10.0f); // 10px vertical space
// Horizontal spacing
ImGui::SameLine();
LayoutHelpers::HorizontalSpacing(20.0f); // 20px horizontal space
// Separator with text
LayoutHelpers::SeparatorText("Section Name");
```
### Responsive Layout
```cpp
// Get available width
float available_width = ImGui::GetContentRegionAvail().x;
// Calculate dynamic widths
float button_width = available_width * 0.5f; // 50% of available
if (ImGui::Button("Full Width", ImVec2(-1, 0))) {
// -1 = fill available width
}
if (ImGui::Button("Half Width", ImVec2(button_width, 0))) {
// Fixed to 50%
}
```
### Grid Layouts
```cpp
// Two-column grid
if (ImGui::BeginTable("##Grid", 2, ImGuiTableFlags_SizingStretchSame)) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("Label 1:");
ImGui::TableNextColumn();
ImGui::InputText("##input1", buffer1, sizeof(buffer1));
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("Label 2:");
ImGui::TableNextColumn();
ImGui::InputText("##input2", buffer2, sizeof(buffer2));
ImGui::EndTable();
}
```
## 10. Workspace Management
The workspace manager provides comprehensive window and layout operations:
**Window Management:**
- `ShowAllWindows() / HideAllWindows()` - via EditorCardManager
- `MaximizeCurrentWindow()` - Dock to central node
- `RestoreAllWindows()` - Reset window sizes
- `CloseAllFloatingWindows()` - Close undocked windows
**Window Navigation:**
- `FocusNextWindow() / FocusPreviousWindow()` - Window cycling
- `SplitWindowHorizontal() / SplitWindowVertical()` - Split current window
- `CloseCurrentWindow()` - Close focused window
**Command Integration:**
```cpp
workspace_manager_.ExecuteWorkspaceCommand(command_id);
// Supports: w.s (show all), w.h (hide all), l.s (save layout), etc.
```
## 11. Future Editor Improvements
This section outlines remaining improvements for editors not yet fully integrated.
### SettingsEditor
**Current State:** Monolithic settings window
**Potential Improvements:**
1. Split into categorized cards
2. Register with EditorCardManager
3. Add search/filter functionality
### AgentEditor
**Current State:** Complex AI agent UI, under active development
**Potential Improvements:**
1. Consider card-based refactoring when stable
2. Integrate with EditorCardManager
3. Add keyboard shortcuts for common operations
## 12. Migration Checklist
Use this checklist when converting an editor to the card-based architecture:
### Planning Phase
- [ ] Identify all major UI components that should become cards
- [ ] Design keyboard shortcut scheme (Ctrl+Alt+[1-9] for cards)
- [ ] Plan `Toolset` contents (toggles, actions, properties)
- [ ] List all hardcoded colors to be replaced
### Implementation Phase - Core Structure
- [ ] Add visibility flags for all cards (e.g., `bool show_my_card_ = false;`)
- [ ] Create `Initialize()` method if not present
- [ ] Register all cards with `EditorCardManager` in `Initialize()`
- [ ] Add card priority values (10, 20, 30, etc.)
- [ ] Include shortcut hints in registration
### Implementation Phase - Toolset
- [ ] Create `DrawToolset()` method
- [ ] Add toggle buttons for each card
- [ ] Include keyboard shortcut hints in tooltips
- [ ] Add separators between logical groups
- [ ] Add action buttons for common operations
### Implementation Phase - Control Panel
- [ ] Create `DrawControlPanel()` method
- [ ] Call `DrawToolset()` at top of control panel
- [ ] Add checkbox grid for quick toggles
- [ ] Add minimize-to-icon button at bottom
- [ ] Include modified status indicators if applicable
- [ ] Add "Save All" / "Discard All" buttons if applicable
### Implementation Phase - Cards
- [ ] Create card classes or Draw methods
- [ ] Use `gui::EditorCard` wrapper with Begin/End
- [ ] Set default size and position for each card
- [ ] Pass visibility flag to both constructor and Begin()
- [ ] Implement proper card content
### Implementation Phase - Update Method
- [ ] Update `Update()` to draw control panel (if visible)
- [ ] Update `Update()` to draw minimize-to-icon (if minimized)
- [ ] Add visibility flag synchronization for each card:
```cpp
if (show_card_ && card_instance_) {
if (!card_instance_->IsVisible()) card_instance_->Show();
card_instance_->Draw();
if (!card_instance_->IsVisible()) show_card_ = false;
}
```
### Implementation Phase - Theming
- [ ] Replace all `ImVec4` color literals with theme colors
- [ ] Use `ThemedButton()` instead of `ImGui::Button()` where appropriate
- [ ] Use `PrimaryButton()` for save/apply actions
- [ ] Use `DangerButton()` for delete/discard actions
- [ ] Use `SectionHeader()` for visual hierarchy
- [ ] Use `ThemedIconButton()` for icon-only buttons
### Testing Phase
- [ ] Test opening each card via control panel checkbox
- [ ] Test opening each card via keyboard shortcut
- [ ] Test closing cards with X button
- [ ] Test minimize-to-icon on control panel
- [ ] Test reopening from icon
- [ ] Verify EditorCardManager shows all cards in View menu
- [ ] Test that closing control panel doesn't affect other cards
- [ ] Verify visibility flags sync properly
- [ ] Test docking behavior (if enabled)
- [ ] Verify all themed widgets render correctly
### Documentation Phase
- [ ] Document keyboard shortcuts in header comment
- [ ] Update `architecture.md` editor status if applicable
- [ ] Add example to this guide if pattern is novel
- [ ] Update CLAUDE.md if editor behavior changed significantly
## 13. Code Examples
### Complete Editor Implementation
This example shows a minimal but complete editor implementation using all the patterns:
```cpp
// my_editor.h
#ifndef YAZE_APP_EDITOR_MY_EDITOR_H
#define YAZE_APP_EDITOR_MY_EDITOR_H
#include "app/editor/editor.h"
#include "app/rom.h"
namespace yaze {
namespace editor {
class MyEditor : public Editor {
public:
explicit MyEditor(Rom* rom = nullptr) : rom_(rom) {
type_ = EditorType::kMyEditor;
}
void Initialize() override;
absl::Status Load() override;
absl::Status Update() override;
void set_rom(Rom* rom) { rom_ = rom; }
Rom* rom() const { return rom_; }
private:
void DrawToolset();
void DrawControlPanel();
void DrawListCard();
void DrawPropertiesCard();
Rom* rom_;
// Card visibility flags
bool show_control_panel_ = true;
bool show_list_card_ = false;
bool show_properties_card_ = false;
bool control_panel_minimized_ = false;
// Data
int selected_item_ = -1;
};
} // namespace editor
} // namespace yaze
#endif // YAZE_APP_EDITOR_MY_EDITOR_H
```
```cpp
// my_editor.cc
#include "my_editor.h"
#include "app/gui/app/editor_card_manager.h"
#include "app/gui/app/editor_layout.h"
#include "app/gui/core/icons.h"
#include "app/gui/core/themed_widgets.h"
#include "imgui/imgui.h"
namespace yaze {
namespace editor {
void MyEditor::Initialize() {
auto& card_manager = gui::EditorCardManager::Get();
card_manager.RegisterCard({
.card_id = "myeditor.control_panel",
.display_name = "My Editor Controls",
.icon = ICON_MD_SETTINGS,
.category = "MyEditor",
.shortcut_hint = "Ctrl+Shift+M",
.visibility_flag = &show_control_panel_,
.priority = 10
});
card_manager.RegisterCard({
.card_id = "myeditor.list",
.display_name = "Item List",
.icon = ICON_MD_LIST,
.category = "MyEditor",
.shortcut_hint = "Ctrl+Alt+1",
.visibility_flag = &show_list_card_,
.priority = 20
});
card_manager.RegisterCard({
.card_id = "myeditor.properties",
.display_name = "Properties",
.icon = ICON_MD_TUNE,
.category = "MyEditor",
.shortcut_hint = "Ctrl+Alt+2",
.visibility_flag = &show_properties_card_,
.priority = 30
});
}
absl::Status MyEditor::Load() {
if (!rom_ || !rom_->is_loaded()) {
return absl::NotFoundError("ROM not loaded");
}
// Load data from ROM
// ...
return absl::OkStatus();
}
absl::Status MyEditor::Update() {
if (!rom_ || !rom_->is_loaded()) {
gui::EditorCard loading_card("My Editor Loading", ICON_MD_SETTINGS);
loading_card.SetDefaultSize(400, 200);
if (loading_card.Begin()) {
ImGui::Text("Waiting for ROM to load...");
}
loading_card.End();
return absl::OkStatus();
}
// Control panel (can be hidden/minimized)
if (show_control_panel_) {
DrawControlPanel();
} else if (control_panel_minimized_) {
// Minimize-to-icon
ImGui::SetNextWindowPos(ImVec2(10, 100));
ImGui::SetNextWindowSize(ImVec2(50, 50));
ImGuiWindowFlags icon_flags = ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoDocking;
if (ImGui::Begin("##MyEditorControlIcon", nullptr, icon_flags)) {
if (ImGui::Button(ICON_MD_SETTINGS, ImVec2(40, 40))) {
show_control_panel_ = true;
control_panel_minimized_ = false;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Open My Editor Controls");
}
}
ImGui::End();
}
// Independent cards
if (show_list_card_) {
DrawListCard();
}
if (show_properties_card_) {
DrawPropertiesCard();
}
return absl::OkStatus();
}
void MyEditor::DrawToolset() {
static gui::Toolset toolbar;
toolbar.Begin();
if (toolbar.AddToggle(ICON_MD_LIST, &show_list_card_,
"Item List (Ctrl+Alt+1)")) {
// Toggled
}
if (toolbar.AddToggle(ICON_MD_TUNE, &show_properties_card_,
"Properties (Ctrl+Alt+2)")) {
// Toggled
}
toolbar.AddSeparator();
if (toolbar.AddAction(ICON_MD_REFRESH, "Reload")) {
Load();
}
toolbar.End();
}
void MyEditor::DrawControlPanel() {
using gui::PrimaryButton;
using gui::DangerButton;
ImGui::SetNextWindowSize(ImVec2(280, 220), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImVec2(10, 100), ImGuiCond_FirstUseEver);
if (ImGui::Begin(ICON_MD_SETTINGS " My Editor Controls",
&show_control_panel_)) {
DrawToolset();
ImGui::Separator();
ImGui::Text("Quick Toggles:");
if (ImGui::BeginTable("##QuickToggles", 2,
ImGuiTableFlags_SizingStretchSame)) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Checkbox("List", &show_list_card_);
ImGui::TableNextColumn();
ImGui::Checkbox("Properties", &show_properties_card_);
ImGui::EndTable();
}
ImGui::Separator();
if (ImGui::SmallButton(ICON_MD_MINIMIZE " Minimize to Icon")) {
control_panel_minimized_ = true;
show_control_panel_ = false;
}
}
ImGui::End();
}
void MyEditor::DrawListCard() {
gui::EditorCard card("Item List", ICON_MD_LIST, &show_list_card_);
card.SetDefaultSize(300, 500);
card.SetPosition(gui::EditorCard::Position::Left);
if (card.Begin(&show_list_card_)) {
ImGui::Text("Item List Content");
if (ImGui::BeginChild("##ItemListScroll", ImVec2(0, 0), true)) {
for (int i = 0; i < 50; i++) {
bool is_selected = (selected_item_ == i);
if (ImGui::Selectable(absl::StrFormat("Item %d", i).c_str(),
is_selected)) {
selected_item_ = i;
}
}
}
ImGui::EndChild();
}
card.End();
}
void MyEditor::DrawPropertiesCard() {
using gui::ThemedIconButton;
using gui::SectionHeader;
using gui::PrimaryButton;
gui::EditorCard card("Properties", ICON_MD_TUNE, &show_properties_card_);
card.SetDefaultSize(350, 400);
card.SetPosition(gui::EditorCard::Position::Right);
if (card.Begin(&show_properties_card_)) {
if (selected_item_ < 0) {
ImGui::TextDisabled("No item selected");
} else {
SectionHeader("Item Properties");
ImGui::Text("Item: %d", selected_item_);
static char name_buffer[64] = "Item Name";
ImGui::InputText("Name", name_buffer, sizeof(name_buffer));
static int value = 100;
ImGui::InputInt("Value", &value);
ImGui::Separator();
if (PrimaryButton("Save Changes", ImVec2(-1, 0))) {
// Save to ROM
}
if (ThemedIconButton(ICON_MD_REFRESH, "Reset to defaults")) {
// Reset
}
}
}
card.End();
}
} // namespace editor
} // namespace yaze
```
## 14. Common Pitfalls
### 1. Forgetting Bidirectional Visibility Sync
**Problem:** Cards don't reopen after being closed with X button.
**Cause:** Not syncing the visibility flag back when the card is closed.
**Solution:**
```cpp
// WRONG
if (show_my_card_) {
my_card_->Draw();
}
// CORRECT
if (show_my_card_ && my_card_) {
if (!my_card_->IsVisible()) my_card_->Show();
my_card_->Draw();
if (!my_card_->IsVisible()) show_my_card_ = false; // Sync back!
}
```
### 2. Using Hardcoded Colors
**Problem:** UI looks inconsistent, doesn't respect theme.
**Cause:** Using `ImVec4` literals instead of theme colors.
**Solution:**
```cpp
// WRONG
ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Error!");
// CORRECT
const auto& theme = AgentUI::GetTheme();
ImGui::TextColored(theme.status_error, "Error!");
```
### 3. Not Calling Show() Before Draw()
**Problem:** Newly opened cards don't appear.
**Cause:** The card's internal `show_` flag isn't set when visibility flag changes.
**Solution:**
```cpp
// WRONG
if (show_my_card_) {
my_card_->Draw(); // Card won't appear if internally hidden
}
// CORRECT
if (show_my_card_) {
if (!my_card_->IsVisible()) my_card_->Show(); // Ensure visible
my_card_->Draw();
}
```
### 4. Missing EditorCardManager Registration
**Problem:** Cards don't appear in View menu, shortcuts don't work.
**Cause:** Forgot to register cards in `Initialize()`.
**Solution:**
```cpp
void MyEditor::Initialize() {
auto& card_manager = gui::EditorCardManager::Get();
// Register ALL cards
card_manager.RegisterCard({
.card_id = "myeditor.my_card",
.display_name = "My Card",
.icon = ICON_MD_ICON,
.category = "MyEditor",
.shortcut_hint = "Ctrl+Alt+1",
.visibility_flag = &show_my_card_,
.priority = 20
});
}
```
### 5. Improper Begin/End Pairing
**Problem:** ImGui asserts, UI state corruption.
**Cause:** Not calling `End()` when `Begin()` returns false, or early returns.
**Solution:**
```cpp
// WRONG
if (card.Begin()) {
DrawContent();
card.End(); // Only called if Begin succeeded
}
// CORRECT
if (card.Begin()) {
DrawContent();
}
card.End(); // ALWAYS called
```
### 6. Not Testing Minimize-to-Icon
**Problem:** Control panel can't be reopened after minimizing.
**Cause:** Forgot to implement the minimize-to-icon floating button.
**Solution:**
```cpp
if (show_control_panel_) {
DrawControlPanel();
} else if (control_panel_minimized_) {
// Draw floating icon button
ImGui::SetNextWindowPos(ImVec2(10, 100));
ImGui::SetNextWindowSize(ImVec2(50, 50));
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoDocking;
if (ImGui::Begin("##ControlIcon", nullptr, flags)) {
if (ImGui::Button(ICON_MD_SETTINGS, ImVec2(40, 40))) {
show_control_panel_ = true;
control_panel_minimized_ = false;
}
}
ImGui::End();
}
```
### 7. Wrong Card Position Enum
**Problem:** Card appears in unexpected location.
**Cause:** Using wrong `Position` enum value.
**Solution:**
```cpp
// Card positions
card.SetPosition(gui::EditorCard::Position::Right); // Dock to right side
card.SetPosition(gui::EditorCard::Position::Left); // Dock to left side
card.SetPosition(gui::EditorCard::Position::Bottom); // Dock to bottom
card.SetPosition(gui::EditorCard::Position::Floating);// Save position
card.SetPosition(gui::EditorCard::Position::Free); // No positioning
```
### 8. Not Handling Null Rom
**Problem:** Editor crashes when ROM isn't loaded.
**Cause:** Not checking `rom_` before access.
**Solution:**
```cpp
absl::Status MyEditor::Update() {
if (!rom_ || !rom_->is_loaded()) {
// Show loading card
gui::EditorCard loading_card("Editor Loading", ICON_MD_ICON);
loading_card.SetDefaultSize(400, 200);
if (loading_card.Begin()) {
ImGui::Text("Waiting for ROM...");
}
loading_card.End();
return absl::OkStatus();
}
// Safe to use rom_ now
DrawEditor();
return absl::OkStatus();
}
```
### 9. Forgetting Toolset Begin/End
**Problem:** Toolset items don't render or layout is broken.
**Cause:** Missing `Begin()` or `End()` calls.
**Solution:**
```cpp
void DrawToolset() {
static gui::Toolset toolbar;
toolbar.Begin(); // REQUIRED
toolbar.AddToggle(ICON_MD_ICON, &flag_, "Tooltip");
toolbar.AddSeparator();
toolbar.AddAction(ICON_MD_SAVE, "Save");
toolbar.End(); // REQUIRED
}
```
### 10. Hardcoded Shortcuts in Tooltips
**Problem:** Shortcuts shown in tooltips don't match actual keybinds.
**Cause:** Tooltip string doesn't match `shortcut_hint` in registration.
**Solution:**
```cpp
// In Registration
card_manager.RegisterCard({
.shortcut_hint = "Ctrl+Alt+1", // Define once
// ...
});
// In Toolset
toolbar.AddToggle(ICON_MD_LIST, &show_list_,
"Item List (Ctrl+Alt+1)"); // Match exactly
```
---
## Summary
Following this guide ensures:
- **Consistency**: All editors use the same patterns and components
- **Maintainability**: Reusable components reduce code duplication
- **User Experience**: Predictable keyboard shortcuts and visual styling
- **Flexibility**: Independent cards allow custom workspace arrangements
- **Discoverability**: EditorCardManager makes all features accessible
When adding new editors or refactoring existing ones, refer to:
1. **Dungeon Editor v2** (`dungeon_editor_v2.cc`) - Gold standard implementation
2. **Palette Editor** (`palette_editor.cc`) - Recently refactored, clean patterns
3. **This Guide** - Comprehensive reference for all patterns
For questions or suggestions about GUI consistency, please open an issue on GitHub or discuss in the development chat.
---
**Last Updated**: October 13, 2025