diff --git a/PLAN.md b/PLAN.md index c37b8d1..fc81f2b 100644 --- a/PLAN.md +++ b/PLAN.md @@ -685,6 +685,10 @@ console)**. Each is GUI → `dev` + eye-check. auto-assign it to the component, without leaving the editor — today scripts must be authored as files outside the editor. The `.rhai` loader + `AssetKind::Script` already exist; this writes a template file + registers it. + **🚧 On `dev` (2026-07-10), awaiting eye-check**: name field + ➕ button at the + bottom of the Script section; template/sanitize/collision helpers are + unit-tested on `main` (`editor/src/assets.rs`), assignment goes through + `SetFieldCmd` (undoable). - **Open a script in an editor** (Stage 10 follow-up). Double-click / button to open a `.rhai` in an in-editor text view or launch `$EDITOR` / a configured external editor; edits flow back through the existing live reload. diff --git a/docs/scripting.md b/docs/scripting.md index e136b42..78b7c8a 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -207,6 +207,13 @@ Scripting plugs into the editor the same way physics does: rendered generically from its fields. The `source` field is an `AssetRef`, which the inspector shows as an asset picker filtered to the project's `scripts/` folder (`AssetKind::Script`). +- **New Script button.** The `Script` section of the inspector ends with a name + field + **➕ New Script**: it writes a fresh `.rhai` from the built-in template + into `assets/scripts/` (name sanitized, collisions suffixed `_2`, `_3`, …), + registers it in the asset database, and assigns it to the component through + the undo stack — no round-trip through an external file manager. The template + is `oxide_editor::assets::script_template` (a unit test compiles it under the + sandboxed engine so it can never ship a syntax error). - **Play loop.** When Play starts, the editor's play `App` adds `ScriptModule` alongside `PhysicsModule`, **shares the editor's `AssetServer`**, and is handed a snapshot of the project `AssetDatabase`. Sharing the server is what lets the diff --git a/editor/src/shell.rs b/editor/src/shell.rs index 453f11e..6528bae 100644 --- a/editor/src/shell.rs +++ b/editor/src/shell.rs @@ -525,6 +525,8 @@ pub struct Shell { open_project_path: String, /// Text entered in the "Groups" editor's "add group" field. new_group_name: String, + /// Text entered in the Script inspector's "New Script" name field. + new_script_name: String, /// The Console panel's command-input buffer (the terminal prompt). terminal_input: String, @@ -624,6 +626,7 @@ impl Shell { new_project_name: String::new(), open_project_path: String::new(), new_group_name: String::new(), + new_script_name: String::new(), terminal_input: String::new(), quit_requested: false, viewport_rect_px: None, @@ -1125,6 +1128,7 @@ impl Shell { extensions: &mut self.extensions, pending: &mut self.pending, rename_buf: &mut self.rename_buf, + new_script_name: &mut self.new_script_name, terminal_input: &mut self.terminal_input, rot_euler: &mut self.rot_euler, euler_for: &mut self.euler_for, @@ -2154,6 +2158,9 @@ struct ShellTabViewer<'a> { extensions: &'a mut EditorExtensions, pending: &'a mut Vec, rename_buf: &'a mut String, + /// The Script inspector's "New Script" name field. Borrowed from + /// [`Shell::new_script_name`]. + new_script_name: &'a mut String, /// The Console command-input buffer (the terminal prompt). terminal_input: &'a mut String, rot_euler: &'a mut Vec3, @@ -3364,6 +3371,14 @@ impl<'a> ShellTabViewer<'a> { edits.push((comp.name, row.info.name, new_ron)); } } + // Stage-10 UX: author a script without leaving the editor. The + // assignment goes through `edits` → SetFieldCmd like any field + // change, so it is undoable (the file itself stays — harmless). + if comp.name == "Script" { + if let Some(ron) = self.new_script_row(ui) { + edits.push((comp.name, "source", ron)); + } + } }); } @@ -3409,6 +3424,59 @@ impl<'a> ShellTabViewer<'a> { } } + /// The "New Script" row at the bottom of a `Script` component's section: + /// a name field + create button that writes a `.rhai` template into + /// `assets/scripts/`, registers it in the asset database, and returns the + /// RON for the component's `source` field — the caller routes it through + /// the normal edit path so the assignment is undoable (undo detaches the + /// script; the created file stays, which is harmless). Disabled until a + /// project is open. Errors go to the log, i.e. the Console panel. + fn new_script_row(&mut self, ui: &mut egui::Ui) -> Option { + let mut created = None; + ui.horizontal(|ui| { + let open = self.state.asset_db.is_some(); + ui.add_enabled_ui(open, |ui| { + let name = ui.add( + egui::TextEdit::singleline(self.new_script_name) + .hint_text("new_script") + .desired_width(120.0), + ); + let submitted = name.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)); + let clicked = ui + .small_button("➕ New Script") + .on_hover_text( + "Create a .rhai file from the template in assets/scripts/ \ + and assign it to this component", + ) + .clicked(); + if clicked || submitted { + let db = self + .state + .asset_db + .as_mut() + .expect("row is enabled only with a project open"); + match crate::assets::create_script_file(&db.assets_dir(), self.new_script_name) + { + Ok(rel) => { + let uid = db.register(&rel); + if let Err(err) = db.save() { + log::warn!("could not write asset manifest: {err}"); + } + log::info!("created {rel}"); + self.new_script_name.clear(); + created = ron::to_string(&Some(uid)).ok(); + } + Err(err) => log::warn!("could not create script: {err}"), + } + } + }); + if !open { + ui.label(egui::RichText::new("(open a project to create scripts)").weak()); + } + }); + created + } + /// Renders one reflected field as a typed widget chosen from its /// `type_name`, returning the field's new RON if the user changed it. /// Unknown types fall back to an editable RON text box, so the inspector is