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>
9.5 KiB
Editor Shell
The Stage-6 docking shell is the editor's host frame: the top menu bar, the bottom status bar, the dockable panel area, the Preferences window, and the wiring between every Stage-6 framework piece — command stack, project system, settings, file watcher, and the module → editor extension API.
Lives in oxide_editor::shell (the library) with
oxide-editor (the binary) acting as glue: open a
window, run the egui paint pump, run the 3D viewport, hand events to the
shell. Splitting the shell into the library lets it be unit-tested without
spinning up a window.
Layout
┌──────────────────────────────────────────────────────────────┐
│ File Edit View Project Modules Help ⚙ Prefs │ ← menu bar
├────────────┬───────────────────────────┬────────────────────┤
│ │ │ │
│ Hierarchy │ Viewport │ Inspector │
│ │ │ │
│ ├───────────────────────────┤ │
│ │ Project │ Console │ │
│ │ │ │
├────────────┴───────────────────────────┴────────────────────┤
│ Reloaded 3 asset(s) modules: 0 undo: 2 │ ← status bar
└──────────────────────────────────────────────────────────────┘
Panels are dockable: drag a tab to re-dock, resize splits, or pop it out as a
floating window — provided by egui_dock. The
default layout is built once in Shell::default_dock; persisting the user's
layout across restarts is a later refinement.
What's wired to what
| Shell surface | Backing system |
|---|---|
| File menu → New / Open / Save / Recent / Quit | Project + RecentProjects + Shell::take_quit_request |
| Edit menu → Undo / Redo (Ctrl+Z / Ctrl+Y) | CommandStack |
| View menu → Show/Hide panel toggles | dock state |
| Project menu → Open project info | EditorState::project |
| Modules menu → module-contributed items | EditorExtensions |
| Help → About | static info |
| Status bar → live hint (last action, errors) | StatusLine (timed TTL) |
| Status bar → module / undo counters | EditorExtensions + CommandStack |
| Preferences window → settings sections + module on/off | Settings + EditorExtensions |
File watcher (on project open) → AssetServer::reload_path |
FileWatcher |
Commands and undo
Edit menu shows the labels of the next undo / redo entry; Ctrl+Z /
Ctrl+Y (also Ctrl+Shift+Z) drive them. The shortcut router is
Shell::try_consume_shortcut — same path the menu uses, so the unit tests
exercise the real flow.
The first wired commands (SetTransformCmd, RenameCmd) live in
oxide_editor::commands.
SetTransformCmd::merge coalesces consecutive edits to the same entity, so
a slider drag (or a future gizmo drag) is one undo entry instead of one
per frame.
Structural edits (spawn / despawn / reparent / change mesh) still bypass the stack today — round-tripping a despawn through undo needs stable entity ids, a Stage-7 design step alongside the gizmos.
File watcher
Shell::open_project (or create_project) attaches a
FileWatcher over the project's assets/, scenes/, and
scripts/ directories (~150 ms debounce window). The shell's
frame_tick pumps events through
reload_changed_assets each frame, so editing a file
externally hot-reloads any handle that was already loaded. Closing the
project tears the watcher down.
Backends that don't deliver events (some sandboxed CI environments) log a warning and let the editor keep running — the watcher is best-effort.
Module integration
Anything a module registers via the extension API is hosted by the shell with no editor-source edits:
- Menu items appear under the
Modulestop menu (shown only when at least one item is registered). - Panels appear in the dock as
PanelKind::Custom(name)tabs, rendered through the module'sFnMut(&mut egui::Ui)closure. - Settings pages + module on/off checkboxes are surfaced in the Preferences window's sidebar.
- Component inspectors (Stage 7+) will be looked up by reflection name when the Inspector encounters a selection holding that component.
Disabling a module in Preferences hides every contribution at once but keeps it registered — re-enabling restores it instantly, no shell rebuild.
Why a Shell library
A few reasons it lives in editor/src/shell.rs rather than main.rs:
- Unit-testable behavior. Shortcut routing, project open/close, recent list updates, command stack lifecycle — all exercised without a window. The maintainer's manual pass focuses on what tests can't prove: how the UI looks and feels.
- Reusable in tests and future hosts. A headless reproducer for a UI bug can drive the shell directly; an alternate front-end (web, embedded) could reuse it.
- Separation of concerns. The binary stays a thin runner — window event loop, 3D viewport, egui paint pump — while the shell owns editor state, layout, and the framework wiring.
Viewport camera modes (Stage 7)
The viewport has two camera schemes; the active one is toggled with
F (the default binding for the editor.camera.toggle_flythrough
action) while the cursor is over the Viewport tab. Bindings live in
EditorState::actions (ActionMap) registered with editor
defaults at startup; the Input Bindings
preferences page exposes them for remapping.
| Mode | Controls |
|---|---|
| Orbit (default) | L-drag = orbit · R-drag = pan · scroll = zoom · click = pick |
| Flythrough | WASD = forward/back + strafe · QE = down/up · Shift = sprint · R-drag = look · scroll = adjust move speed · click = pick |
Toggling preserves pose: the new camera lands looking at the same view the previous one was showing, so the scene doesn't snap.
Input Bindings preferences page (Stage 7 piece 5)
The Preferences window's input.bindings section renders a rich page
listing every registered editor action — buttons, 1D axes, 2D axes — with
its current bindings. Each binding cell:
- Clicking it arms a capture for that slot. The page shows
"Press a key…"; the next non-
Escapekey or mouse button press becomes the binding.Escapecancels. ✕removes that binding.+(per direction or per action) starts an append capture so the user can add a binding without replacing one.↺(per action) restores that action to its code-defined defaults.- A global Restore all defaults button at the top resets every action.
Edits flow through Shell's
try_complete_capture, which mutates EditorState::actions, syncs the
new bindings into the input.bindings settings section via
sync_action_overrides_to_settings, and flips a bindings_dirty flag.
The host runner reads-and-clears the flag each frame and writes the
preferences file to $XDG_CONFIG_HOME/oxide/editor.ron (or
$HOME/.config/oxide/editor.ron). On startup the editor reads that
file, calls Settings::import, and apply_action_overrides_from_settings
layers the user's remap on top of the defaults — so a remap survives a
restart, and removing an action from code never breaks an old file
(unknown sections are silently skipped).
The page is intentionally a built-in Shell feature rather than going
through EditorExtensions::add_settings_page: it needs to mutate
EditorState::actions while a capture is in flight, which is more
direct from the Shell than through the extension API's FnMut(&mut Ui)
contract.
What's not yet here
| Feature | Where it lands |
|---|---|
Native New/Open dialogs (rfd or similar) |
Polish; the in-app text-path modals fill the gap today |
✅ Landed in Stage 7 piece 6b: FrameContext::viewport_rect restricts the wgpu viewport and drives the projection aspect; Viewport::pick rebases the cursor to tab-local NDC. |
|
| Layout persistence across restarts | After settings sections are richer (Preferences-driven) |
| Inspector via reflection-keyed component editors | Stage 7 alongside the gizmos |
| Undo/redo for spawn/despawn/reparent | Stage 7 — needs stable entity ids |
| Console wired to a real log feed / Stage-10 terminal | Stage 10 |