ADR-012: Editor Plugin Extension Architecture
Status: Proposed Date: 2026-01-01
Context
The KeenEyes Editor is being built as a modular, plugin-based application. Currently, editor features like the Inspector, Hierarchy, and Viewport panels are implemented as static classes tightly coupled to EditorApplication. This creates several problems:
Limited Extensibility
Third-party developers cannot add custom panels, property drawers, gizmos, or menu items without modifying editor source code.
Monolithic Structure
All editor features are bundled together. Users cannot disable unused features or load only what they need.
Testing Difficulty
Editor components are hard to test in isolation because they depend on the full EditorApplication context.
Inconsistent Patterns
The runtime IWorldPlugin system provides a clean capability-based pattern (ADR-007), but the editor has no equivalent architecture.
Decision
Introduce IEditorPlugin and IEditorContext interfaces that mirror the runtime plugin architecture, extended with editor-specific capabilities.
Core Interfaces
public interface IEditorPlugin
{
string Name { get; }
string Version { get; }
string? Description { get; }
void Initialize(IEditorContext context);
void Shutdown();
}
public interface IEditorContext
{
// Core services (read-only access)
EditorProject Project { get; }
EditorWorldManager Worlds { get; }
SelectionManager Selection { get; }
UndoRedoManager UndoRedo { get; }
AssetDatabase Assets { get; }
IWorld EditorWorld { get; }
// Extension storage (mirrors IPluginContext)
void SetExtension<T>(T extension) where T : class;
T GetExtension<T>() where T : class;
bool TryGetExtension<T>(out T? extension) where T : class;
bool RemoveExtension<T>() where T : class;
// Capability access (mirrors ADR-007)
T GetCapability<T>() where T : class, IEditorCapability;
bool TryGetCapability<T>(out T? capability) where T : class, IEditorCapability;
bool HasCapability<T>() where T : class, IEditorCapability;
// Event subscriptions
EventSubscription OnSceneOpened(Action<World> handler);
EventSubscription OnSceneClosed(Action handler);
EventSubscription OnSelectionChanged(Action<IReadOnlyList<Entity>> handler);
EventSubscription OnPlayModeChanged(Action<EditorPlayState> handler);
}
Editor Capabilities
Following ADR-007's pattern, define editor features as capability interfaces:
| Capability | Purpose |
|---|---|
IInspectorCapability |
Register property drawers, component inspectors |
IViewportCapability |
Add gizmo renderers, overlays, pick handlers |
IMenuCapability |
Add menu items, context menus, toolbar buttons |
IPanelCapability |
Register dockable panels |
IAssetCapability |
Custom asset importers, thumbnails |
IShortcutCapability |
Register keyboard shortcuts |
IToolCapability |
Register viewport tools (select, move, etc.) |
Capability Interface Definitions
IInspectorCapability
public interface IInspectorCapability : IEditorCapability
{
void RegisterPropertyDrawer(Type fieldType, PropertyDrawer drawer);
void RegisterPropertyDrawer<T>(PropertyDrawer drawer);
void RegisterDrawerForAttribute<TAttribute>(PropertyDrawer drawer)
where TAttribute : Attribute;
void RegisterComponentInspector<TComponent>(IComponentInspector inspector);
void RegisterComponentActions<TComponent>(IComponentActionProvider provider);
}
IViewportCapability
public interface IViewportCapability : IEditorCapability
{
void AddGizmoRenderer(IGizmoRenderer renderer);
void RemoveGizmoRenderer(IGizmoRenderer renderer);
void AddOverlay(string id, IViewportOverlay overlay);
void SetOverlayVisible(string id, bool visible);
void AddPickHandler(IPickHandler handler);
void RegisterCameraMode(string id, ICameraMode mode);
}
public interface IGizmoRenderer
{
int Order { get; }
bool IsVisible { get; }
void Render(GizmoRenderContext context, IReadOnlyList<Entity> selection);
}
IMenuCapability
public interface IMenuCapability : IEditorCapability
{
void AddMenuItem(MenuPath path, EditorCommand command);
void AddContextMenuItem<T>(MenuPath path, EditorCommand<T> command);
void AddToolbarButton(ToolbarSection section, EditorCommand command);
void RemoveMenuItem(MenuPath path);
}
public record MenuPath(string Path)
{
public static MenuPath File(string item) => new($"File/{item}");
public static MenuPath Edit(string item) => new($"Edit/{item}");
public static MenuPath Entity(string item) => new($"Entity/{item}");
public static MenuPath Window(string item) => new($"Window/{item}");
}
IPanelCapability
public interface IPanelCapability : IEditorCapability
{
void RegisterPanel<T>(PanelDescriptor descriptor) where T : IEditorPanel, new();
void OpenPanel(string id);
void ClosePanel(string id);
bool IsPanelOpen(string id);
}
public interface IEditorPanel : IDisposable
{
string Title { get; }
Entity CreateUI(IWorld editorWorld, Entity parent, FontHandle font);
void Update(float deltaTime);
}
public record PanelDescriptor(
string Id,
string Title,
DockPosition DefaultPosition = DockPosition.Right,
bool ShowByDefault = false,
MenuPath? WindowMenuItem = null
);
Source-Generated Extensions
Following the PluginExtensionAttribute pattern, provide typed access to editor extensions:
[AttributeUsage(AttributeTargets.Class)]
public sealed class EditorExtensionAttribute(string propertyName) : Attribute
{
public string PropertyName { get; } = propertyName;
}
// Usage:
[EditorExtension("Physics")]
public sealed class PhysicsEditorExtension
{
public void ShowColliderBounds(bool visible) { ... }
}
// Generated extension member:
extension(IEditorContext context)
{
public PhysicsEditorExtension Physics
=> context.GetExtension<PhysicsEditorExtension>();
}
Plugin Lifecycle
EditorApplication starts
↓
Load plugin assemblies
↓
For each IEditorPlugin:
├── Create IEditorContext
├── Call plugin.Initialize(context)
└── Track registered resources
↓
Editor runs
↓
On shutdown:
├── For each plugin (reverse order):
│ └── Call plugin.Shutdown()
└── Dispose tracked resources
Built-in Plugins
Core editor features are refactored as internal plugins:
| Plugin | Provides |
|---|---|
CoreEditorPlugin |
Selection, undo/redo, basic commands |
InspectorPlugin |
Component inspector, built-in property drawers |
HierarchyPlugin |
Scene tree panel |
ViewportPlugin |
3D/2D viewport, transform gizmos, grid |
ConsolePlugin |
Log panel |
ProfilerPlugin |
System timing panel |
ProjectPlugin |
Asset browser panel |
This serves as reference implementations for third-party plugins.
Consequences
Positive
- Extensibility - Third parties can add panels, drawers, gizmos, menu items
- Modularity - Editor features are isolated and independently testable
- Consistency - Same capability pattern as runtime plugins (ADR-007)
- Discoverability - Capability interfaces document available extension points
- Clean shutdown - Plugin resources are tracked and disposed properly
Negative
- Migration effort - Existing editor code needs refactoring to plugin pattern
- Indirection - Accessing features requires capability lookup
- Learning curve - Plugin authors must understand capability system
Neutral
- Performance - Interface dispatch overhead is negligible for editor code
- Gradual adoption - Can migrate panels one at a time
Implementation Phases
Phase 1: Core Abstractions
- Create
IEditorPlugin,IEditorContextinterfaces - Create
IEditorCapabilitymarker interface - Create
EditorPluginManagerfor lifecycle management
Phase 2: Capability Interfaces
IInspectorCapabilitywith PropertyDrawer registrationIMenuCapabilitywith menu/toolbar registrationIPanelCapabilitywith panel registration
Phase 3: Viewport Capabilities
IViewportCapabilityfor gizmos and overlaysIToolCapabilityfor viewport toolsIShortcutCapabilityfor keybindings
Phase 4: Asset Capabilities
IAssetCapabilityfor importers- Thumbnail generators
- Drag-drop handlers
Phase 5: Built-in Plugin Refactoring
- Convert InspectorPanel to InspectorPlugin
- Convert HierarchyPanel to HierarchyPlugin
- Convert ViewportPanel to ViewportPlugin