# 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>`). ## 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