Editor: "New Script" button on the Script inspector

Stage-10 editor-UX follow-up. The Script component's inspector section
now ends with a name field + " New Script" button (Enter in the field
also submits): it writes a fresh .rhai from the tested template into
assets/scripts/ via create_script_file (sanitized stem, collisions
suffixed), registers it in the asset database, saves the manifest, and
assigns it to the component's `source` through the normal edit path —
so the assignment is one undoable SetFieldCmd. Disabled with a hint
while no project is open; failures land in the Console via the log.

GUI piece — needs an eye-check before promotion to main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Homer
2026-07-10 20:33:23 +02:00
parent d6cb9947b2
commit 4f1e9a48d7
3 changed files with 79 additions and 0 deletions
+4
View File
@@ -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.
+7
View File
@@ -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<ScriptAsset>`, 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
+68
View File
@@ -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<PendingAction>,
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<String> {
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