Import Oxide engine (Stages 0–10) under MIT license
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>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
# Editor Extension API
|
||||
|
||||
`oxide_editor::extension` is the editor-side companion to the engine's
|
||||
[`Module`](modules.md) 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:
|
||||
|
||||
```rust
|
||||
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 |
|
||||
|
||||
```rust,no_run
|
||||
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
|
||||
|
||||
```rust
|
||||
# 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](reflection.md). Settings pages plug into the
|
||||
[Preferences framework](settings.md) 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`.
|
||||
|
||||
[`extension`]: ../editor/src/extension.rs
|
||||
[`EditorExtensions`]: ../editor/src/extension.rs
|
||||
[`EditorModule`]: ../editor/src/extension.rs
|
||||
Reference in New Issue
Block a user