Files
yaze/docs/internal/plans/editor-ui-refactor.md

63 KiB

Editor UI Architecture Refactor Proposal

Executive Summary

This document proposes a refactoring of yaze's editor panel system to eliminate naming confusion, centralize lifecycle management, and reduce boilerplate. The key change is renaming "Card" terminology to "Panel"—a more precise term used by professional IDEs like VSCode, JetBrains, and Xcode.


Part 1: Terminology Decision

Why "Panel" Instead of "Card"?

Term Associations Precision
Card Material Design cards, Kanban boards, mobile UI Vague—cards are typically static content containers
Panel VSCode Side Panel, JetBrains Tool Windows, Xcode Inspectors Precise—panels are dockable, resizable tool windows
Pane Split views, editor areas Typically refers to divisions within a window
Tool Window JetBrains, Visual Studio Verbose, but very precise

Decision: Use "Panel" as the primary term:

  • gui::EditorCardgui::PanelWindow
  • CardInfoPanelDescriptor
  • EditorCardRegistryPanelManager
  • *Card classes → *Panel classes

Part 2: Current Architecture Analysis

2.1 Key Components (As-Is)

Component Location Current Name Proposed Name
ImGui window wrapper editor_layout.h gui::EditorCard gui::PanelWindow
Metadata struct editor_card_registry.h CardInfo PanelDescriptor
Central registry editor_card_registry.h EditorCardRegistry PanelManager
Sidebar UI activity_bar.h ActivityBar ActivityBar (unchanged)
Layout builder layout_manager.h LayoutManager LayoutManager (unchanged)
Presets layout_presets.h LayoutPresets LayoutPresets (unchanged)

2.2 Current Workflow (Dual Registration Problem)

┌─────────────────────────────────────────────────────────────────────────────┐
│                           CURRENT WORKFLOW                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  Editor::Initialize()                    Editor::Update()                    │
│  ───────────────────                     ─────────────────                   │
│                                                                              │
│  1. Register CardInfo metadata           1. Create gui::EditorCard instance  │
│     with registry                           (per-frame or static)            │
│                                                                              │
│  card_registry->RegisterCard({           gui::EditorCard my_panel(           │
│    .card_id = MakeCardId("x.foo"),         MakeCardTitle("Foo"),             │
│    .display_name = "Foo",                  ICON_MD_FOO);                     │
│    .window_title = " Foo",                                                   │
│    .visibility_flag = &show_foo_,        if (my_panel.Begin(&show_foo_)) {   │
│    ...                                     DrawFooContent();                 │
│  });                                     }                                   │
│                                          my_panel.End();                     │
│                                                                              │
│  PROBLEM: Two separate steps, titles can diverge, no centralized drawing    │
└─────────────────────────────────────────────────────────────────────────────┘

2.3 Identified Issues

  1. Naming Collision: gui::EditorCard vs conceptual "editor card" vs *Card classes
  2. Dual Registration: Editors register metadata AND manually draw—no central control
  3. Title Mismatch Risk: CardInfo.window_title vs gui::EditorCard constructor title
  4. No Cross-Editor Panel Support: Panels are tied to their parent editor
  5. Inconsistent Instantiation: Static vs per-frame vs unique_ptr member patterns

Part 3: Complete Editor Inventory

3.1 Overworld Editor

Tool Panels (Static)

Panel ID Display Name Icon Purpose Default Visible
overworld.canvas Overworld Canvas MAP Main map editing canvas with toolset
overworld.tile16_selector Tile16 Selector GRID_ON Tile palette for painting
overworld.tile8_selector Tile8 Selector GRID_3X3 Low-level 8x8 tile editing
overworld.area_graphics Area Graphics IMAGE GFX sheet preview for current area
overworld.scratch Scratch Workspace DRAW Layout planning/clipboard area
overworld.gfx_groups GFX Groups FOLDER Graphics group configuration
overworld.usage_stats Usage Statistics ANALYTICS Tile usage analysis across all maps
overworld.v3_settings v3 Settings SETTINGS ZSCustomOverworld configuration
overworld.properties Map Properties TUNE Per-map settings (palette, GFX, etc.)
overworld.debug Debug Window BUG_REPORT Internal debug information

Resource Panels (Dynamic per-map)

Panel ID Pattern Display Name Purpose
overworld.map_{id} Map {id} ({world}) Focused view of specific overworld map
overworld.map_{id}.entities Map {id} Entities Entity list (entrances, exits, items, sprites)

Note: overworld.usage_stats is useful cross-editor—see Section 5.

3.2 Dungeon Editor (V2)

Tool Panels (Static)

Panel ID Display Name Icon Purpose Default Visible
dungeon.control_panel Dungeon Controls CASTLE Mode and tool selection
dungeon.room_selector Room Selector LIST Room list with search/filter (296 rooms)
dungeon.entrance_list Entrance List DOOR_FRONT Entrance list with search/filter
dungeon.room_matrix Room Matrix GRID_VIEW Visual 16x16 room layout
dungeon.entrances Entrance Properties DOOR_SLIDING Selected entrance property editor
dungeon.room_graphics Room Graphics IMAGE Room GFX sheet preview
dungeon.object_editor Object Editor CONSTRUCTION Object placement/editing
dungeon.palette_editor Palette Editor PALETTE Room palette selection
dungeon.debug_controls Debug Controls BUG_REPORT Debug tools and state inspection
dungeon.emulator_preview SNES Object Preview MONITOR Live emulator object preview

Resource Panels (Dynamic per-room)

Panel ID Pattern Display Name Purpose
dungeon.room_{id} Room {id} Canvas for editing specific room (0-295)
dungeon.room_{id}.objects Room {id} Objects Object list for specific room

Architecture: Dungeon Editor is pure panel-based—no "main canvas" concept. All panels are peers. Resource panels (room-specific) are created on-demand when a room is opened.

3.3 Graphics Editor

Tool Panels (Static)

Panel ID Display Name Icon Purpose Default Visible
graphics.sheet_browser_v2 Sheet Browser VIEW_LIST Navigate all 223 graphics sheets
graphics.pixel_editor Pixel Editor DRAW 8x8/16x16 tile pixel editing
graphics.palette_controls Palette Controls PALETTE Palette selection for editing
graphics.link_sprite_editor Link Sprite Editor PERSON Edit Link's sprite frames
graphics.polyhedral_editor 3D Objects VIEW_IN_AR Edit rupees, crystals, triforce
graphics.sheet_editor Sheet Editor (Legacy) EDIT Older sheet editing interface
graphics.sheet_browser Asset Browser (Legacy) VIEW_LIST Older asset browsing
graphics.player_animations Player Animations PERSON View Link animation sequences
graphics.prototype_viewer Prototype Viewer CONSTRUCTION Experimental feature viewer

Resource Panels (Dynamic per-sheet)

Panel ID Pattern Display Name Purpose
graphics.sheet_{id} Sheet {id} Dedicated editor for specific GFX sheet

3.4 Palette Editor

TODO: Remove control panel and fold into activity bar for useful actions? Resource management

Panel ID Display Name Icon Purpose Default Visible
palette.control_panel Palette Controls PALETTE Group manager and quick actions
palette.ow_main Overworld Main LANDSCAPE 6 overworld area palettes
palette.ow_animated Overworld Animated WATER Water, lava animation palettes
palette.dungeon_main Dungeon Main CASTLE 20 dungeon room palettes
palette.sprites Global Sprite Palettes PETS Main sprite color sets
palette.sprites_aux1 Sprites Aux 1 FILTER_1 Auxiliary sprite colors 1
palette.sprites_aux2 Sprites Aux 2 FILTER_2 Auxiliary sprite colors 2
palette.sprites_aux3 Sprites Aux 3 FILTER_3 Auxiliary sprite colors 3
palette.equipment Equipment Palettes SHIELD Link's tunic/equipment colors
palette.quick_access Quick Access COLOR_LENS Color harmony tools
palette.custom Custom Palette BRUSH Custom palette editing

3.5 Music Editor

Tool Panels (Static)

Panel ID Display Name Icon Purpose Default Visible
music.song_browser Song Browser LIBRARY_MUSIC Navigate all game songs
music.tracker Playback Control PLAY_CIRCLE Transport controls and BPM
music.piano_roll Piano Roll PIANO Visual note editing
music.instrument_editor Instrument Editor SPEAKER Edit instrument samples
music.sample_editor Sample Editor WAVES Edit BRR audio samples
music.assembly Assembly View CODE View music as 65816 assembly
music.help Help HELP Music editor documentation

Resource Panels (Dynamic per-song)

Panel ID Pattern Display Name Purpose
music.song_{index} Song: {name} Full song editor for a specific track
music.song_{index}.piano_roll {name} Piano Roll Piano roll for specific song
music.song_{index}.channels {name} Channels Channel mixer for specific song

Dynamic Panels: Music Editor creates per-song panels on demand. Multiple songs can be open simultaneously for comparison/copying.

3.6 Screen Editor

Panel ID Display Name Icon Purpose Default Visible
screen.dungeon_maps Dungeon Maps MAP Edit dungeon map screens
screen.inventory_menu Inventory Menu INVENTORY Edit inventory/pause menu
screen.overworld_map Overworld Map PUBLIC Edit overworld map screen
screen.title_screen Title Screen TITLE Edit title screen graphics
screen.naming_screen Naming Screen EDIT Edit save file naming screen

3.7 Sprite Editor

Panel ID Display Name Icon Purpose Default Visible
sprite.vanilla_editor Vanilla Sprites SMART_TOY View/edit built-in sprites
sprite.custom_editor Custom Sprites ADD_CIRCLE Import/edit custom ZSM sprites

3.8 Message Editor

Panel ID Display Name Icon Purpose Default Visible
message.message_list Message List LIST Navigate all game messages
message.message_editor Message Editor EDIT Edit message text and formatting
message.font_atlas Font Atlas FONT_DOWNLOAD View/edit font graphics
message.dictionary Dictionary BOOK Edit compression dictionary

3.9 Assembly Editor

Panel ID Display Name Icon Purpose Default Visible
assembly.editor Assembly Editor CODE 65816 assembly code editor
assembly.file_browser File Browser FOLDER_OPEN Navigate ASM project files

Note: Assembly Editor uses file browser integration for project navigation.

3.10 Agent Editor

Panel ID Display Name Icon Purpose Default Visible
agent.chat Agent Chat CHAT Main AI chat interface
agent.configuration AI Configuration SETTINGS API keys, model selection
agent.status Agent Status INFO Connection status, context info
agent.prompt_editor Prompt Editor EDIT Edit system prompts
agent.profiles Bot Profiles FOLDER Manage bot personalities
agent.history Chat History HISTORY View past conversations
agent.metrics Metrics Dashboard ANALYTICS Token usage, response times
agent.builder Agent Builder AUTO_FIX_HIGH Create custom agents

3.11 Emulator (Registered by EditorManager)

TODO: Consolidate some panels into PpuViewer nav as a canvas of sorts

Panel ID Display Name Icon Purpose Default Visible
emulator.cpu_debugger CPU Debugger BUG_REPORT CPU registers, stepping
emulator.ppu_viewer PPU Viewer VIDEOGAME_ASSET Graphics layer debugging
emulator.memory_viewer Memory Viewer MEMORY RAM/VRAM inspection
emulator.breakpoints Breakpoints STOP Breakpoint management
emulator.performance Performance SPEED Frame timing, FPS
emulator.ai_agent AI Agent SMART_TOY AI-assisted debugging
emulator.save_states Save States SAVE Save/load state management
emulator.keyboard_config Keyboard Config KEYBOARD Input configuration
emulator.virtual_controller Virtual Controller SPORTS_ESPORTS On-screen gamepad
emulator.apu_debugger APU Debugger AUDIOTRACK Audio processor debugging
emulator.audio_mixer Audio Mixer AUDIO_FILE Channel volume control

3.12 Memory (Registered by EditorManager)

TODO: Add more panels to help with hex editing operations?

Panel ID Display Name Icon Purpose Default Visible
memory.hex_editor Hex Editor MEMORY Raw ROM hex editing

Part 4: Cross-Editor Panel Visibility

4.1 The Problem

Currently, panels are tightly coupled to their parent editor. When switching editors, panels are hidden. But some panels are useful across editors:

Panel Useful When...
overworld.usage_stats Editing dungeons to check tile availability
palette.ow_main Editing overworld tiles to preview palette effects
emulator.cpu_debugger Any editing to test changes in real-time
graphics.pixel_editor Editing sprites, dungeons, or messages

4.2 Panel Categories

We propose three panel categories based on lifecycle behavior:

Category Behavior Examples
Editor-Bound Hidden when switching away from parent editor dungeon.room_selector, music.piano_roll
Persistent Remains visible across editor switches emulator.cpu_debugger, palette.quick_access
Cross-Editor Can be pinned to stay visible (user choice) overworld.usage_stats, graphics.pixel_editor

4.3 Implementation: Pin-to-Persist

Add a "pin" button to panel headers:

class PanelWindow {
 public:
  // ... existing methods ...
  
  /// If pinned, panel stays visible when switching editors
  void SetPinned(bool pinned) { pinned_ = pinned; }
  bool IsPinned() const { return pinned_; }
  
 private:
  bool pinned_ = false;
};

The PanelManager respects pinned state:

void PanelManager::OnEditorSwitch(EditorType from, EditorType to) {
  // Hide non-pinned panels from the previous editor
  for (auto& [id, panel] : panels_) {
    if (panel->GetCategory() == GetCategoryForEditor(from) && !IsPinned(id)) {
      HidePanel(id);
    }
  }
  
  // Show default panels for new editor
  auto defaults = LayoutPresets::GetDefaultPanels(to);
  for (const auto& id : defaults) {
    ShowPanel(id);
  }
}

For Editor-Bound panels only, we can define parent-child relationships:

struct PanelDescriptor {
  // ... existing fields ...
  
  /// If set, this panel closes when parent panel closes
  std::string parent_panel_id;
  
  /// If true, closing this panel also closes children
  bool cascade_close = false;
};

Example: Closing dungeon.control_panel could cascade-close dungeon.object_editor if they're defined as related.

Documentation Requirement: Any cascade behavior MUST be documented in LayoutPresets comments.


Part 5: Resource Panels & Multi-Session Support

5.1 The Resource Panel Concept

Some panels represent specific resources within the ROM—not generic tools, but windows for editing a particular piece of data:

Editor Resource Type Example Panel IDs
Dungeon Rooms (0-295) dungeon.room_42, dungeon.room_128
Music Songs (0-63) music.song_5, music.song_12
Overworld Maps (0-159) overworld.map_0, overworld.map_64
Graphics Sheets (0-222) graphics.sheet_100, graphics.sheet_212
Sprite Sprites (0-255) sprite.vanilla_42, sprite.custom_3
Message Messages (0-395) message.text_42, message.text_100

5.2 Resource Panel Lifecycle

/// Base class for resource-specific panels
class ResourcePanel : public EditorPanel {
 public:
  /// The resource ID this panel edits (room_id, song_index, etc.)
  virtual int GetResourceId() const = 0;
  
  /// Resource type for grouping
  virtual std::string GetResourceType() const = 0;
  
  /// Can have multiple instances open simultaneously
  virtual bool AllowMultipleInstances() const { return true; }
  
  // Resource panels are always EditorBound by default
  PanelCategory GetPanelCategory() const override { 
    return PanelCategory::EditorBound; 
  }
};

5.3 Resource Panel ID Format

{session}.{category}.{resource_type}_{resource_id}[.{subpanel}]

Examples:
  s0.dungeon.room_42           -- Room 42 in session 0
  s1.dungeon.room_42           -- Room 42 in session 1 (different ROM)
  s0.music.song_5              -- Song 5 in session 0
  s0.music.song_5.piano_roll   -- Piano roll for song 5
  s0.overworld.map_64.entities -- Entity list for map 64

5.4 Multi-Session Awareness

When multiple ROMs are loaded (multi-session editing), resource panels must be uniquely identified:

class PanelManager {
 public:
  /// Create a resource panel for the current session
  std::string CreateResourcePanel(const std::string& category,
                                  const std::string& resource_type,
                                  int resource_id) {
    std::string panel_id = MakeResourcePanelId(
        active_session_, category, resource_type, resource_id);
    
    // Check if already exists
    if (panels_.contains(panel_id)) {
      ShowPanel(panel_id);  // Just bring to front
      return panel_id;
    }
    
    // Create new resource panel
    auto panel = CreateResourcePanelImpl(category, resource_type, resource_id);
    RegisterPanel(std::move(panel));
    ShowPanel(panel_id);
    
    return panel_id;
  }
  
  /// Generate session-aware resource panel ID
  std::string MakeResourcePanelId(size_t session_id,
                                  const std::string& category,
                                  const std::string& resource_type,
                                  int resource_id) const {
    if (session_count_ > 1) {
      return absl::StrFormat("s%zu.%s.%s_%d", 
                             session_id, category, resource_type, resource_id);
    }
    return absl::StrFormat("%s.%s_%d", category, resource_type, resource_id);
  }
  
  /// Get all resource panels for a session
  std::vector<EditorPanel*> GetResourcePanelsInSession(size_t session_id);
  
  /// Close all resource panels when a session closes
  void CloseSessionResourcePanels(size_t session_id);
};

5.5 Multi-ROM Side-by-Side Editing

With session-aware IDs, users can:

  1. Open two ROMs (vanilla + hack)
  2. View same room side-by-side:
    • s0.dungeon.room_42 (vanilla)
    • s1.dungeon.room_42 (hack)
  3. Compare and copy between them
┌─────────────────────────────────────────────────────────────────────────────┐
│                      MULTI-SESSION RESOURCE PANELS                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  Session 0 (vanilla.sfc)              Session 1 (myhack.sfc)                │
│  ┌─────────────────────┐              ┌─────────────────────┐               │
│  │ s0.dungeon.room_42  │              │ s1.dungeon.room_42  │               │
│  │ Room 42             │              │ Room 42             │               │
│  │ [Session 0]         │   ◄─────►    │ [Session 1]         │               │
│  │                     │   Compare    │                     │               │
│  │ ┌───────────────┐   │              │ ┌───────────────┐   │               │
│  │ │ Canvas        │   │              │ │ Canvas        │   │               │
│  │ │ (vanilla)     │   │              │ │ (modified)    │   │               │
│  │ └───────────────┘   │              │ └───────────────┘   │               │
│  └─────────────────────┘              └─────────────────────┘               │
│                                                                              │
│  Window Title Format: "Room 42 [S0]"  "Room 42 [S1]"                        │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

5.6 Resource Panel Window Titles

std::string PanelManager::GetResourcePanelTitle(const std::string& panel_id) const {
  auto* panel = GetPanel(panel_id);
  if (!panel) return "";
  
  auto* resource_panel = dynamic_cast<ResourcePanel*>(panel);
  if (!resource_panel) {
    return GetWindowTitle(panel_id);  // Fallback to normal
  }
  
  std::string title = absl::StrFormat("%s %s %d",
      resource_panel->GetIcon(),
      resource_panel->GetResourceType(),
      resource_panel->GetResourceId());
  
  // Add session suffix for multi-session
  if (session_count_ > 1) {
    size_t session_id = GetSessionFromPanelId(panel_id);
    title += absl::StrFormat(" [S%zu]", session_id);
  }
  
  return title;
}

5.7 Resource Panel Limits

To prevent memory bloat, enforce limits:

struct ResourcePanelLimits {
  static constexpr size_t kMaxRoomPanels = 8;      // Max open rooms
  static constexpr size_t kMaxSongPanels = 4;      // Max open songs
  static constexpr size_t kMaxSheetPanels = 6;     // Max open GFX sheets
  static constexpr size_t kMaxTotalResourcePanels = 20;
};

When limit is reached:

  1. Option A: Auto-close oldest panel (LRU)
  2. Option B: Warn user and prevent new panel
  3. Option C: Prompt to close one (user choice)

Recommendation: Option A with undo (can reopen from sidebar).


Part 6: Proposed Architecture

6.1 New Terminology Mapping

Current Proposed Description
gui::EditorCard gui::PanelWindow Pure ImGui window wrapper
CardInfo PanelDescriptor Panel metadata (immutable after registration)
EditorCardRegistry PanelManager Owns panels, manages lifecycle, draws all
*Card classes *Panel classes Logical component implementations

6.2 Interface Definitions

gui::PanelWindow

namespace yaze::gui {

/// Pure ImGui window wrapper for dockable panels
class PanelWindow {
 public:
  enum class Position { Free, Left, Right, Bottom, Top, Floating };

  PanelWindow(const char* title, const char* icon = nullptr);

  // Configuration
  void SetDefaultSize(float width, float height);
  void SetPosition(Position pos);
  void SetMinimizable(bool minimizable);
  void SetClosable(bool closable);
  void SetDockingAllowed(bool allowed);
  void SetPinnable(bool pinnable);  // NEW: Allow pin-to-persist

  // Header buttons (drawn in title bar)
  void AddHeaderButton(const char* icon, const char* tooltip,
                       std::function<void()> callback);

  // ImGui lifecycle
  bool Begin(bool* p_open = nullptr);
  void End();

  // State
  void SetPinned(bool pinned);
  bool IsPinned() const;
  void Focus();
  bool IsFocused() const;
  const char* GetWindowName() const;
};

}  // namespace yaze::gui

editor::EditorPanel (New Interface)

namespace yaze::editor {

/// Category for panel lifecycle behavior
enum class PanelCategory {
  EditorBound,  // Hidden on editor switch (default)
  Persistent,   // Always visible once shown
  CrossEditor   // User can pin to persist
};

/// Base interface for all logical panel components
class EditorPanel {
 public:
  virtual ~EditorPanel() = default;

  // ========== Identity ==========
  virtual std::string GetId() const = 0;           // e.g., "dungeon.room_selector"
  virtual std::string GetDisplayName() const = 0;  // e.g., "Room Selector"
  virtual std::string GetIcon() const = 0;         // e.g., ICON_MD_LIST
  virtual std::string GetEditorCategory() const = 0;  // e.g., "Dungeon"

  // ========== Drawing ==========
  virtual void Draw(bool* p_open) = 0;  // Called when visible

  // ========== Lifecycle ==========
  virtual void OnOpen() {}   // Panel becomes visible
  virtual void OnClose() {}  // Panel becomes hidden
  virtual void OnFocus() {}  // Panel receives focus

  // ========== Behavior ==========
  virtual PanelCategory GetPanelCategory() const { 
    return PanelCategory::EditorBound; 
  }
  virtual bool IsEnabled() const { return true; }
  virtual std::string GetDisabledTooltip() const { return ""; }
  virtual std::string GetShortcutHint() const { return ""; }
  virtual int GetPriority() const { return 50; }
  
  // ========== Relationships ==========
  virtual std::string GetParentPanelId() const { return ""; }
  virtual bool CascadeCloseChildren() const { return false; }
};

}  // namespace yaze::editor

editor::PanelManager

namespace yaze::editor {

/// Central manager for all EditorPanel instances
class PanelManager {
 public:
  // ========== Registration ==========
  void RegisterPanel(std::unique_ptr<EditorPanel> panel);
  
  template <typename T, typename... Args>
  T* EmplacePanel(Args&&... args);
  
  void UnregisterPanel(const std::string& panel_id);

  // ========== Visibility ==========
  void ShowPanel(const std::string& panel_id);
  void HidePanel(const std::string& panel_id);
  void TogglePanel(const std::string& panel_id);
  bool IsPanelVisible(const std::string& panel_id) const;
  
  void ShowAllInCategory(const std::string& category);
  void HideAllInCategory(const std::string& category);

  // ========== Central Drawing ==========
  void DrawAllVisiblePanels();  // Call once per frame

  // ========== Editor Switching ==========
  void OnEditorSwitch(EditorType from, EditorType to);
  void SetActiveEditor(EditorType type);

  // ========== Pin Management ==========
  void SetPanelPinned(const std::string& panel_id, bool pinned);
  bool IsPanelPinned(const std::string& panel_id) const;
  std::vector<std::string> GetPinnedPanels() const;

  // ========== Query ==========
  EditorPanel* GetPanel(const std::string& panel_id);
  std::vector<EditorPanel*> GetPanelsInCategory(const std::string& category);
  std::vector<std::string> GetAllCategories() const;

  // ========== Session Support ==========
  void SetActiveSession(size_t session_id);
  size_t GetActiveSession() const;

  // ========== Persistence ==========
  void SaveVisibilityState();
  void LoadVisibilityState();
  void SavePinnedState();
  void LoadPinnedState();

 private:
  std::unordered_map<std::string, std::unique_ptr<EditorPanel>> panels_;
  std::unordered_map<std::string, bool> visibility_;
  std::unordered_map<std::string, bool> pinned_;
  EditorType active_editor_ = EditorType::kUnknown;
  size_t active_session_ = 0;
};

}  // namespace yaze::editor

6.3 Architecture Diagram

┌─────────────────────────────────────────────────────────────────────────────┐
│                        PROPOSED ARCHITECTURE                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────────┐ │
│  │                          PanelManager                                    │ │
│  │  ┌─────────────────────────────────────────────────────────────────────┐ │ │
│  │  │ panels_: map<string, unique_ptr<EditorPanel>>                        │ │ │
│  │  │                                                                      │ │ │
│  │  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐               │ │ │
│  │  │  │ RoomList     │  │ ObjectEditor │  │  UsageStats  │  ...          │ │ │
│  │  │  │    Panel     │  │    Panel     │  │    Panel     │               │ │ │
│  │  │  │ [EditorBound]│  │ [EditorBound]│  │ [CrossEditor]│               │ │ │
│  │  │  └──────────────┘  └──────────────┘  └──────────────┘               │ │ │
│  │  └─────────────────────────────────────────────────────────────────────┘ │ │
│  │                                                                          │ │
│  │  DrawAllVisiblePanels() {                                                │ │
│  │    for (auto& [id, panel] : panels_) {                                   │ │
│  │      if (!IsVisible(id)) continue;                                       │ │
│  │                                                                          │ │
│  │      gui::PanelWindow window(GetWindowTitle(id), panel->GetIcon());      │ │
│  │      if (IsPinnable(id)) window.SetPinnable(true);                       │ │
│  │                                                                          │ │
│  │      if (window.Begin(&visibility_[id])) {                               │ │
│  │        panel->Draw(&visibility_[id]);                                    │ │
│  │      }                                                                   │ │
│  │      window.End();                                                       │ │
│  │    }                                                                     │ │
│  │  }                                                                       │ │
│  └─────────────────────────────────────────────────────────────────────────┘ │
│                                                                              │
│  ┌──────────────┐     ┌───────────────┐     ┌───────────────────┐           │
│  │ ActivityBar  │────▶│  PanelManager │◀────│   LayoutManager   │           │
│  │ (Sidebar UI) │     │  (Ownership)  │     │ (DockBuilder)     │           │
│  └──────────────┘     └───────────────┘     └───────────────────┘           │
│                              │                                               │
│                              ▼                                               │
│                       ┌──────────────┐                                       │
│                       │LayoutPresets │                                       │
│                       │(Default vis) │                                       │
│                       └──────────────┘                                       │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Part 7: Migration Plan & Current Status

Current Status (December 2024)

PHASES 1-8 COMPLETE - Full panel system migration complete including resource panels and cross-editor features.

Phase Status Description
Phase 1 Complete UI widget renamed (EditorCardPanelWindow)
Phase 2 Complete Registry renamed (EditorCardRegistry deleted, PanelManager created)
Phase 3 Complete EditorPanel interface defined with central drawing
Phase 4 Complete Dungeon Editor migrated (9 panels)
Phase 5 Complete All remaining editors migrated (40 panels)
Phase 6 Complete Resource panels with LRU logic
Phase 7 Complete Cross-editor features (Pin-to-Persist, OnEditorSwitch)
Phase 8 Complete Multi-session verification and testing

Phase 5 Completion Details

All editors now use the EditorPanel architecture:

Editor Static Panels Resource Panels Status Pattern Used
Dungeon Editor 10 dungeon.room_{id} Complete Callback + Resource
Graphics Editor 5 Complete Direct interface
Music Editor 7 music.song_{id}, music.piano_roll_{id} Complete Callback + Resource
Palette Editor 11 Complete Callback pattern
Agent Editor 8 Complete Callback pattern
Sprite Editor 2 Complete Callback pattern
Screen Editor 5 Complete Callback pattern
Message Editor 4 Complete Callback pattern
Overworld Editor 9 Complete Direct pointer pattern

Total: 61 static EditorPanel implementations + dynamic resource panels

Overworld Editor Migration Details

The Overworld Editor migration was the most complex, using a direct pointer pattern with separate .cc files for maintainability:

Panels Created (9 total)

High-Priority Panels:

Panel ID Purpose File
AreaGraphicsPanel overworld.area_graphics GFX sheet preview for current area area_graphics_panel.h/.cc
Tile16SelectorPanel overworld.tile16_selector Tile palette for painting tile16_selector_panel.h/.cc
MapPropertiesPanel overworld.properties Per-map settings editor map_properties_panel.h/.cc

Medium-Priority Panels:

Panel ID Purpose File
ScratchSpacePanel overworld.scratch Layout planning workspace scratch_space_panel.h/.cc
UsageStatisticsPanel overworld.usage_stats Tile usage analysis usage_statistics_panel.h/.cc

Low-Priority Panels:

Panel ID Purpose File
Tile8SelectorPanel overworld.tile8_selector 8x8 tile editing tile8_selector_panel.h/.cc
DebugWindowPanel overworld.debug Debug information debug_window_panel.h/.cc
GfxGroupsPanel overworld.gfx_groups Graphics group configuration gfx_groups_panel.h/.cc
V3SettingsPanel overworld.v3_settings ZSCustomOverworld settings v3_settings_panel.h/.cc

Architecture Pattern

// Panel header (area_graphics_panel.h)
class AreaGraphicsPanel : public EditorPanel {
 public:
  explicit AreaGraphicsPanel(OverworldEditor* editor);

  std::string GetId() const override { return "overworld.area_graphics"; }
  std::string GetDisplayName() const override { return "Area Graphics"; }
  std::string GetIcon() const override { return ICON_MD_IMAGE; }
  std::string GetEditorCategory() const override { return "Overworld"; }
  void Draw(bool* p_open) override;

 private:
  OverworldEditor* editor_;
};

// Registration in overworld_editor.cc
void OverworldEditor::Initialize() {
  panel_manager->RegisterEditorPanel(
      std::make_unique<AreaGraphicsPanel>(this));
  // ... all 9 panels registered once
}

Git Commits (16 atomic commits)

1cd667a93f docs: update architecture documentation for EditorPanel system
a436321bcc refactor(wasm): update control API for panel system compatibility
28ca469cb5 fix(emu): improve audio timing and SPC700 cycle accuracy
604667e47f feat(editor): integrate EditorManager and Activity Bar with panel system
060857179c refactor(ui): update settings and coordinator systems for panel architecture
e7c65a1c46 feat(overworld-editor): migrate to EditorPanel system
7df3980222 feat(message-editor): migrate to EditorPanel system
a91f08928a feat(screen-editor): migrate to EditorPanel system
081af10c80 feat(sprite-editor): migrate to EditorPanel system
36c8bee040 feat(agent-editor): migrate to EditorPanel system
862a69965d feat(palette-editor): migrate to EditorPanel system
d4f0dccf10 feat(music-editor): migrate to EditorPanel system
195cf8ac5e feat(graphics-editor): migrate existing panels to EditorPanel interface
a5daccce8e feat(dungeon-editor): migrate to EditorPanel system
e9e1123a57 refactor(layout): update layout system to use PanelManager
b652636618 refactor(editor): remove deprecated EditorCardRegistry
6007a1086d feat(editor): add EditorPanel system for unified panel management

Benefits Achieved

  • Centralized Management - All panels managed by PanelManager
  • Activity Bar Integration - Panels automatically appear in sidebar
  • Efficient Rendering - Panels created once in Initialize(), not per-frame
  • Layout Persistence - Panel arrangements can be saved/restored
  • Session Support - Proper scoping for multi-ROM editing
  • Consistent Architecture - All editors follow same pattern

Phase 6-8 Completion Details

Phase 6: Resource Panels Complete

Implemented Features:

  • ResourcePanel base class in resource_panel.h
  • Dynamic room panels (DungeonRoomPanel) created on-demand when rooms selected
  • Dynamic song panels (MusicSongPanel) and piano roll panels for music editor
  • Session-aware ID generation via MakePanelId()
  • LRU eviction in EnforceResourceLimits() respects pinned panels
  • Resource panels appear in Activity Bar under their editor category
  • Room selector properly limited to 296 rooms (kNumberOfRooms)

Key Implementation Details:

// Resource panels use BASE IDs - PanelManager handles session prefixing
std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
panel_manager->RegisterPanel({.card_id = card_id, ...});
panel_manager->ShowPanel(card_id);  // Sets visibility immediately

Important: Do NOT use MakeCardId() for resource panels - it causes double-prefixing since RegisterPanel() already calls MakePanelId() internally.

Phase 7: Cross-Editor Features Complete

Implemented Features:

  1. Pin-to-Persist UI - Pin button in Activity Bar sidebar (not title bar due to ImGui limitations)
  2. OnEditorSwitch() - Hides non-pinned panels from previous category, shows defaults for new
  3. Dashboard Category - kDashboardCategory suppresses all panels until editor selected
  4. Category Filtering - Resource panels only visible when their category is active OR pinned

Key Implementation Details:

// In PanelManager::DrawAllVisiblePanels()
if (active_category_.empty() || active_category_ == kDashboardCategory) {
  return;  // Suppress panels when no editor selected
}

// Resource panels check category + pin status
if (panel->GetEditorCategory() == active_category_ || 
    IsPanelPinned(panel_id) ||
    panel->GetPanelCategory() == PanelCategory::Persistent) {
  // Draw panel
}

Phase 8: Multi-Session Support Complete

Verified Behaviors:

  • Session-aware panel IDs: s0.dungeon.room_42 vs s1.dungeon.room_42
  • Resource panels properly scoped to their session
  • Fixed double-prefix bug (resource panels use base IDs, not MakeCardId())
  • Window titles include session suffix when multiple ROMs open

Dungeon Editor Panels (11 total):

Panel ID Type
Control Panel dungeon.control_panel Static
Room Selector dungeon.room_selector Static
Entrance List dungeon.entrance_list Static
Room Matrix dungeon.room_matrix Static
Entrances Properties dungeon.entrances Static
Room Graphics dungeon.room_graphics Static
Object Editor dungeon.object_editor Static
Debug Controls dungeon.debug_controls Static
Room {id} dungeon.room_{id} Resource (dynamic)

Music Editor Resource Panels:

Panel ID Pattern Type
Song Tracker music.song_{id} Resource (dynamic)
Piano Roll music.piano_roll_{id} Resource (dynamic)

Remaining Work / Future Enhancements

The core panel system refactor is complete. The following are optional enhancements:

Enhancement Priority Effort Description
Overworld Resource Panels Medium 4-6h Create overworld.map_{id} panels for per-map editing
Graphics Resource Panels Medium 4-6h Create graphics.sheet_{id} panels for per-sheet editing
Sprite Resource Panels Low 2-4h Create sprite.vanilla_{id} panels
Cascade Close Low 2-3h Implement parent-child panel relationships
Panel State Persistence Low 2-3h Save/restore pinned state and visibility to config
Keyboard Shortcuts Low 1-2h Add configurable shortcuts for common panels

Known Limitations:

  • Pin button is in Activity Bar sidebar, not panel title bar (ImGui docking limitation)
  • Resource panel limits are enforced but may need tuning based on user feedback
  • Some editors (Assembly, Agent) have minimal panel integration

Troubleshooting: Common Panel Visibility Issues

When panels don't respect visibility or appear duplicated, check for these common issues:

Issue 1: Duplicate Panel Drawing (DUPLICATE DETECTED warnings)

Symptom: Console shows [PanelWindow] DUPLICATE DETECTED: 'Panel Name' Begin() called twice

Cause: Panel is being drawn by BOTH:

  • Central PanelManager::DrawAllVisiblePanels() (via EditorPanel)
  • Local gui::PanelWindow code in the editor's Update() method

Fix: Remove the local drawing code. When using RegisterEditorPanel(), the central drawing handles everything:

// WRONG - draws twice
void MyEditor::Initialize() {
  panel_manager->RegisterEditorPanel(std::make_unique<MyPanel>(...));
}
void MyEditor::Update() {
  gui::PanelWindow panel("My Panel", ICON);
  if (panel.Begin(&show_panel_)) { DrawContent(); }
  panel.End();
}

// CORRECT - draws once via central system
void MyEditor::Initialize() {
  panel_manager->RegisterEditorPanel(std::make_unique<MyPanel>([this]() {
    DrawContent();
  }));
}
void MyEditor::Update() {
  // No local drawing - handled by PanelManager::DrawAllVisiblePanels()
  return absl::OkStatus();
}

Issue 2: Duplicate Registration (RegisterPanel + RegisterEditorPanel)

Symptom: Panel appears twice in Activity Bar, or metadata conflicts

Cause: Both RegisterPanel() AND RegisterEditorPanel() called for same panel

Fix: Use only RegisterEditorPanel() - the EditorPanel class provides all metadata:

// WRONG - duplicate registration
panel_manager->RegisterPanel({.card_id = "editor.my_panel", ...});
panel_manager->RegisterEditorPanel(std::make_unique<MyPanel>(...));

// CORRECT - EditorPanel provides metadata via GetId(), GetDisplayName(), etc.
panel_manager->RegisterEditorPanel(std::make_unique<MyPanel>(...));

Issue 3: Resource Panel Double-Prefixing

Symptom: Resource panels (rooms, songs) don't appear or have wrong IDs like s0.s0.dungeon.room_42

Cause: Using MakeCardId() for resource panels when RegisterPanel() already adds session prefix

Fix: Use base IDs for resource panels - RegisterPanel() handles prefixing:

// WRONG - double prefix
std::string card_id = MakeCardId(absl::StrFormat("dungeon.room_%d", room_id));
panel_manager->RegisterPanel({.card_id = card_id, ...});

// CORRECT - let RegisterPanel handle prefixing
std::string card_id = absl::StrFormat("dungeon.room_%d", room_id);
panel_manager->RegisterPanel({.card_id = card_id, ...});
panel_manager->ShowPanel(card_id);  // Uses same base ID

Issue 4: Context Menu Popups Don't Open

Symptom: Clicking context menu items doesn't open the expected popup (e.g., entity editor)

Cause: ImGui::OpenPopup() called from within another popup's callback doesn't work

Fix: Use deferred popup pattern - store request and process outside popup context:

// WRONG - OpenPopup inside context menu callback fails
entity_menu.callback = [this]() {
  InsertEntity();
  ImGui::OpenPopup("Entity Editor");  // Won't work!
};

// CORRECT - defer popup opening
void MyEditor::HandleEntityInsert(const std::string& type) {
  pending_insert_type_ = type;  // Store for later
}

void MyEditor::Update() {
  ProcessPendingInsert();  // Called outside popup context
  // ... draw popups here, OpenPopup() works now
}

Issue 5: Panels Visible Before Editor Selected

Symptom: Panels from other editors appear when on Dashboard

Cause: active_category_ not set to Dashboard, or missing category check

Fix: Ensure SetActiveCategory(kDashboardCategory) when ROM loaded but no editor selected:

void EditorManager::LoadRom() {
  // After loading...
  panel_manager_.SetActiveCategory(PanelManager::kDashboardCategory);
}

void EditorManager::SwitchToEditor(EditorType type) {
  panel_manager_.SetActiveCategory(EditorRegistry::GetEditorCategory(type));
}

Issue 6: Resource Panels Always Visible (Act Like Pinned)

Symptom: Resource panels (rooms, songs) don't hide when switching editors

Cause: Missing category filtering in the editor's drawing loop

Fix: Add category + pin check before drawing resource panels:

// In editor's DrawLayout() for dynamic resource panels:
for (auto& resource : active_resources_) {
  std::string card_id = absl::StrFormat("editor.resource_%d", resource.id);
  
  // Skip if not in category AND not pinned
  if (panel_manager->GetActiveCategory() != "MyEditor" &&
      !panel_manager->IsPanelPinned(card_id)) {
    continue;
  }
  
  // Draw the resource panel...
}

Checklist for Migrating an Editor to EditorPanel System

  1. Create EditorPanel classes in panels/ subdirectory

    • Implement GetId(), GetDisplayName(), GetIcon(), GetEditorCategory(), GetPriority()
    • Implement Draw(bool* p_open) with content drawing
  2. Update Initialize()

    • Call RegisterEditorPanel() for each panel
    • Remove any RegisterPanel() calls (EditorPanel provides metadata)
    • Call ShowPanel() for default-visible panels
  3. Update Update()

    • Remove ALL local gui::PanelWindow drawing code
    • Central PanelManager::DrawAllVisiblePanels() handles drawing
    • Keep only non-panel logic (popup modals, shortcuts, etc.)
  4. For Resource Panels (dynamic, per-resource)

    • Use base IDs (no MakeCardId())
    • Add category filtering before drawing
    • Register on-demand when resource opened
    • Unregister when resource closed
  5. Test

    • Verify no DUPLICATE DETECTED warnings
    • Verify panels appear/hide on editor switch
    • Verify Activity Bar shows correct panels
    • Verify pinning works across editor switches

Original Migration Plan (For Reference)

Phase 1: Rename UI Widget (2-3 hours) COMPLETE

  1. Rename gui::EditorCardgui::PanelWindow
  2. Update all 24 files that use gui::EditorCard
  3. Update documentation

Phase 2: Rename Registry (3-4 hours) COMPLETE

  1. Rename CardInfoPanelDescriptor
  2. Rename EditorCardRegistryPanelManager
  3. Update all registration calls
  4. Update LayoutPresets constants

Phase 3: Create EditorPanel Interface (4-6 hours) COMPLETE

  1. Define EditorPanel base class with PanelCategory
  2. Add pin support to PanelWindow
  3. Implement central drawing in PanelManager

Phase 4: Migrate Dungeon Editor (Proof of Concept, 8-12 hours) COMPLETE

  1. Convert all 9 Dungeon panels to EditorPanel implementations
  2. Register with PanelManager::EmplacePanel<>
  3. Remove manual drawing from DungeonEditorV2::Update()
  4. Verify LayoutManager docking works

Phase 5: Migrate Remaining Editors (2-4 hours each) COMPLETE

Priority order based on panel count:

  1. Palette Editor (11 panels) - Has PaletteGroupPanel hierarchy
  2. Overworld Editor (9 panels) - Most complex, has main canvas
  3. Graphics Editor (5 panels) - Mix of legacy and new
  4. Agent Editor (8 panels) - Pure panel-based
  5. Music Editor (7 panels + dynamic) - Has dynamic song panels
  6. Screen Editor (5 panels) - Straightforward
  7. Message Editor (4 panels) - Straightforward
  8. Sprite Editor (2 panels) - Simple
  9. Assembly Editor (2 panels) - Deferred (project-based workflow)

Part 8: Summary Statistics & Default Visibility

8.1 Panel Counts by Category

Category Static Panels Resource Panels Default Visible Notes
Overworld 10 per-map 5 Canvas, Tile16, Area GFX, Usage, Properties
Dungeon 9 per-room 5 Controls, Selector, Matrix, GFX, Object Editor
Graphics 9 per-sheet 4 Browser, Pixel, Palette, 3D Objects
Palette 11 5 Controls, OW Main, Dungeon, Sprites, Quick
Music 7 per-song 3 Browser, Tracker, Instruments
Screen 5 3 Dungeon Maps, Inventory, Title
Sprite 2 per-sprite 1 Vanilla Editor
Message 4 3 List, Editor, Font Atlas
Assembly 2 per-file 1 User-initiated only
Agent 8 1 User-initiated only
Emulator 11 6 CPU, PPU, Perf, States, Keys, Controller
Memory 1 1 User-initiated only
TOTAL ~80 dynamic 35 ~44% visible by default

8.2 Default Visibility Rationale

Philosophy: Show panels that provide immediate value without overwhelming the user.

Default ON Reason
Main canvas/selector Core editing workflow
Graphics preview Visual feedback while editing
Object/entity editors Primary editing task
Usage statistics Optimization guidance
Playback controls Audio editors need transport
Default OFF Reason
Debug panels Developer-focused
Legacy panels Replaced by newer versions
Advanced tools Power users enable as needed
Agent/AI panels Requires configuration first
Assembly panels Project-based workflow

8.3 First-Time User Experience

When a ROM is first loaded, show a balanced workspace:

┌─────────────────────────────────────────────────────────────────────────────┐
│                     RECOMMENDED DEFAULT LAYOUT                               │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  ┌──────────┐  ┌────────────────────────────────────┐  ┌──────────────────┐ │
│  │ Activity │  │                                    │  │ Properties       │ │
│  │ Bar      │  │         Main Canvas                │  │ Panel            │ │
│  │          │  │         (Overworld/Room)           │  │                  │ │
│  │ [OW]     │  │                                    │  │ - Map Info       │ │
│  │ [Dun]    │  │                                    │  │ - Palette        │ │
│  │ [Gfx]    │  │                                    │  │ - GFX Group      │ │
│  │ [Pal]    │  │                                    │  │                  │ │
│  │ [Mus]    │  ├────────────────────────────────────┤  ├──────────────────┤ │
│  │ [Scr]    │  │ Tile Selector    │ Usage Stats    │  │ Graphics         │ │
│  │ [Spr]    │  │                  │                │  │ Preview          │ │
│  │ [Msg]    │  │ [Tile16 Grid]    │ [Heat Map]     │  │                  │ │
│  │ [Asm]    │  │                  │                │  │ [Current Sheet]  │ │
│  │          │  └──────────────────┴────────────────┘  └──────────────────┘ │
│  │ ──────── │                                                               │
│  │ [Emu]    │  Status Bar: ROM Name | Session | Unsaved Changes            │
│  │ [Set]    │                                                               │
│  └──────────┘                                                               │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Appendix A: File Changes Summary

File Changes Required
src/app/gui/app/editor_layout.h Rename class, add pin support
src/app/gui/app/editor_layout.cc Update implementation
src/app/editor/system/editor_card_registry.h Rename to panel_manager.h
src/app/editor/system/editor_card_registry.cc Rename, add central drawing
src/app/editor/ui/layout_presets.h Rename Card → Panel constants
src/app/editor/menu/activity_bar.cc Update to use PanelManager
src/app/editor/*/[editor].cc (24 files) Update registrations

Appendix B: Naming Convention

Static Panel IDs

  • Format: {category}.{name} (lowercase, underscores)
  • Examples: dungeon.room_selector, palette.ow_main

Resource Panel IDs

  • Format: [s{session}.]{category}.{resource}_{id}[.{subpanel}]
  • Session prefix only when session_count_ > 1
Pattern Example Description
{cat}.{res}_{id} dungeon.room_42 Single session
s{n}.{cat}.{res}_{id} s0.dungeon.room_42 Multi-session
s{n}.{cat}.{res}_{id}.{sub} s1.music.song_5.piano_roll Subpanel

Window Titles

  • Static panels: {icon} {Display Name}
  • Resource panels: {icon} {Resource Type} {ID}
  • Session suffix: [S{n}] when multi-session
Panel Type Single Session Multi-Session
Static 🏰 Room Selector 🏰 Room Selector [S0]
Resource 🚪 Room 42 🚪 Room 42 [S1]
Subpanel 🎹 Song 5 Piano Roll 🎹 Song 5 Piano Roll [S0]

Categories (for ActivityBar grouping)

  • Match EditorType: Overworld, Dungeon, Graphics, Palette, Music, Screen, Sprite, Message, Assembly, Agent, Emulator, Memory

Resource Types (for resource panels)

Category Resource Types
Dungeon room
Music song, instrument, sample
Overworld map
Graphics sheet
Sprite vanilla, custom
Message text
Assembly file

Appendix C: ResourcePanel Interface

namespace yaze::editor {

/**
 * @class ResourcePanel
 * @brief Base class for panels that edit specific ROM resources
 *
 * A ResourcePanel represents a window for editing a specific piece of
 * data within a ROM, such as a dungeon room, a song, or a graphics sheet.
 *
 * Key Features:
 * - Session-aware: Can distinguish between same resource in different ROMs
 * - Multi-instance: Multiple resources can be open simultaneously
 * - LRU managed: Oldest panels auto-close when limit reached
 *
 * Subclasses:
 * - DungeonRoomPanel: Edits a specific room (0-295)
 * - MusicSongPanel: Edits a specific song with tracker/piano roll
 * - GraphicsSheetPanel: Edits a specific GFX sheet
 * - OverworldMapPanel: Edits a specific overworld map
 */
class ResourcePanel : public EditorPanel {
 public:
  // ========== Resource Identity ==========
  
  /// The numeric ID of the resource (room_id, song_index, sheet_id, etc.)
  virtual int GetResourceId() const = 0;
  
  /// The resource type name (e.g., "room", "song", "sheet")
  virtual std::string GetResourceType() const = 0;
  
  /// Human-readable resource name (e.g., "Hyrule Castle Entrance")
  virtual std::string GetResourceName() const {
    return absl::StrFormat("%s %d", GetResourceType(), GetResourceId());
  }
  
  // ========== Panel Identity (from EditorPanel) ==========
  
  std::string GetId() const override {
    // Generated from resource type and ID
    return absl::StrFormat("%s.%s_%d", 
        GetEditorCategory(), GetResourceType(), GetResourceId());
  }
  
  std::string GetDisplayName() const override {
    return GetResourceName();
  }
  
  // ========== Behavior ==========
  
  /// Resource panels are always editor-bound
  PanelCategory GetPanelCategory() const override { 
    return PanelCategory::EditorBound; 
  }
  
  /// Allow multiple resource panels of same type
  virtual bool AllowMultipleInstances() const { return true; }
  
  /// Get the session ID this resource belongs to
  virtual size_t GetSessionId() const { return session_id_; }
  
  // ========== Lifecycle ==========
  
  /// Called when resource data changes externally
  virtual void OnResourceModified() {}
  
  /// Called when resource is deleted from ROM
  virtual void OnResourceDeleted() { 
    // Default: close the panel
  }
  
 protected:
  size_t session_id_ = 0;
};

/**
 * @brief Example: Dungeon Room Panel
 */
class DungeonRoomPanel : public ResourcePanel {
 public:
  DungeonRoomPanel(size_t session_id, int room_id, 
                   zelda3::Room* room, DungeonCanvasViewer* viewer)
      : room_id_(room_id), room_(room), viewer_(viewer) {
    session_id_ = session_id;
  }
  
  int GetResourceId() const override { return room_id_; }
  std::string GetResourceType() const override { return "room"; }
  std::string GetIcon() const override { return ICON_MD_DOOR_FRONT; }
  std::string GetEditorCategory() const override { return "Dungeon"; }
  
  void Draw(bool* p_open) override {
    // Draw room canvas with objects, sprites, etc.
    viewer_->DrawRoom(room_id_, p_open);
  }
  
 private:
  int room_id_;
  zelda3::Room* room_;
  DungeonCanvasViewer* viewer_;
};

}  // namespace yaze::editor

Editor Panels Visible by Default Rationale
Overworld Canvas, Tile16, Area GFX, Usage Stats, Properties Full tile editing workflow
Dungeon Controls, Selector, Matrix, Room GFX, Object Editor Room editing + visual navigation
Graphics Sheet Browser, Pixel Editor, Palette Controls, 3D Objects Complete pixel art workflow
Palette Controls, OW Main, Dungeon Main, Sprites, Quick Access Color editing across contexts
Music Song Browser, Tracker, Instrument Editor Music composition workflow
Screen Dungeon Maps, Inventory, Title Screen Most commonly edited screens
Sprite Vanilla Editor View-only by default, editing opt-in
Message Message List, Message Editor, Font Atlas Text editing workflow
Assembly None Project-based, user-initiated
Agent None Requires API config first
Emulator CPU Debugger, PPU, Performance, Save States, Keys, Controller Debugging + playback
Memory None Power user feature