Full project snapshot migrated to new Gitea remote without history: engine, editor, physics, script, examples, tests, docs, and assets. Relicensed from GPLv3 to MIT and updated repo URLs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.9 KiB
Editor Extension API
oxide_editor::extension is the editor-side companion to the engine's
Module trait. It is the mechanism through which a module
contributes the UI it needs the editor to host on its behalf — menu items,
dockable panels, viewport tools, component inspectors, and Preferences
pages — without editing the editor's source.
This is the extension surface for both first-party modules (Stage-7 input binding pages, Stage-9 physics inspectors, Stage-17 ray-traced-audio panels…) and any third-party or AI-authored module.
Why a separate trait
The engine doesn't depend on egui — putting the editor hook on
oxide_engine::app::Module would pull egui into the engine. Instead a module
that wants to participate in the editor implements two traits on the same
struct:
struct AudioPreviewModule;
impl oxide_engine::app::Module for AudioPreviewModule {
fn name(&self) -> &'static str { "audio_preview" }
fn build(&self, app: &mut oxide_engine::app::App) {
// … register systems, types, asset loaders
}
}
impl oxide_editor::extension::EditorModule for AudioPreviewModule {
fn name(&self) -> &'static str { "audio_preview" }
fn build_editor(&self, ext: &mut oxide_editor::extension::EditorExtensions) {
// … register menu items, panels, inspectors, settings pages
}
}
The shared name is how enable/disable in Preferences stays consistent across
the two halves: toggling "audio_preview" hides the editor contributions and
disables the engine systems together.
What a module can contribute
| Kind | Helper | Stage-6 criterion |
|---|---|---|
Menu items (File/New, Help/About, …) |
add_menu_item |
✔ required |
| Dockable panels | add_panel |
✔ required |
| Viewport tools (gizmos, brushes) | add_viewport_tool |
future stages |
| Component inspectors (by reflection name) | add_inspector |
future stages |
| Settings pages (by section name) | add_settings_page |
✔ required |
use oxide_editor::extension::{DockLocation, EditorExtensions, EditorModule};
struct DemoModule;
impl EditorModule for DemoModule {
fn name(&self) -> &'static str { "demo" }
fn build_editor(&self, ext: &mut EditorExtensions) {
ext.add_menu_item("File/Demo…", || { /* open the demo dialog */ });
ext.add_panel("Demo Panel", DockLocation::Right, |ui| {
ui.label("hello from a module-owned panel");
});
ext.add_inspector("DemoComponent", |ui| {
ui.label("custom editor for DemoComponent");
});
ext.add_settings_page("demo", "Demo", |ui| {
ui.label("module preferences here");
});
}
}
The render closures take only &mut egui::Ui in Piece 5 (registration). Piece
6 — the docking shell — refines the signatures to pass through the editor's
runtime context (scene, selection, asset server, settings). Modules that need
shared state today can capture it through interior mutability
(Rc<RefCell<...>>).
How the shell consumes the registry
# use oxide_editor::extension::{EditorExtensions, EditorModule, DockLocation};
# struct M; impl EditorModule for M {
# fn name(&self) -> &'static str { "m" }
# fn build_editor(&self, ext: &mut EditorExtensions) {
# ext.add_menu_item("File/Open", || {});
# ext.add_panel("Inspector", DockLocation::Right, |_| {});
# }
# }
let mut ext = EditorExtensions::new();
ext.add_module(M);
// What the shell will do in piece 6:
for item in ext.iter_menu_items() {
let _ = &item.path; // build the menu tree
}
for panel in ext.iter_panels() {
let _ = (&panel.name, panel.default_dock); // place in dock layout
}
Lookups by name are also provided (has_inspector_for("Transform"),
has_settings_page_for("audio")) so the Inspector and Preferences windows can
ask "is there a custom editor for this thing?" before rendering a fallback.
Attribution and lifecycle
Every contribution remembers its source module. That gives three lifecycle
operations the engine App already has and the editor needs to mirror:
| Operation | Effect |
|---|---|
add_module |
Runs build_editor, attributes every contribution to the module, marks enabled. Re-adding replaces the old registration cleanly. |
set_module_enabled(name, false) |
Contributions stay registered but vanish from every iter_* / has_* lookup — toggling Preferences is reversible without rebuilding state. |
remove_module(name) |
Drops every contribution attributed to the module in one shot. |
Adding contributions outside a module's build_editor panics: every entry
must be attributable to some module, otherwise removal would leave orphans.
Inspector / settings-page resolution
The Inspector panel renders custom editors for components whose type has a
registered inspector — keyed by the same name the component is registered
under in the reflection registry. Settings pages plug into the
Preferences framework by matching their section_name to the
section the module registered. Both lookups respect the enabled flag, so a
disabled module's inspector / page disappears even if the underlying section
or type is still registered.
Testing strategy
The whole API is purely about registration, so it's directly unit-testable
without bringing up egui — tests construct an EditorExtensions, add a
demo module, and assert via the lookup helpers. The actual rendering of the
contributed UI is exercised by the Piece-6 docking shell with a maintainer
manual pass; the Stage-6 criterion ("a trivial test module adds a menu item,
a panel, and a settings page through the API with no editor-core edits") is
covered by the integration test in tests/src/lib.rs::stage6.