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:
@@ -685,6 +685,10 @@ console)**. Each is GUI → `dev` + eye-check.
|
|||||||
auto-assign it to the component, without leaving the editor — today scripts
|
auto-assign it to the component, without leaving the editor — today scripts
|
||||||
must be authored as files outside the editor. The `.rhai` loader +
|
must be authored as files outside the editor. The `.rhai` loader +
|
||||||
`AssetKind::Script` already exist; this writes a template file + registers it.
|
`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 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
|
open a `.rhai` in an in-editor text view or launch `$EDITOR` / a configured
|
||||||
external editor; edits flow back through the existing live reload.
|
external editor; edits flow back through the existing live reload.
|
||||||
|
|||||||
@@ -207,6 +207,13 @@ Scripting plugs into the editor the same way physics does:
|
|||||||
rendered generically from its fields. The `source` field is an
|
rendered generically from its fields. The `source` field is an
|
||||||
`AssetRef<ScriptAsset>`, which the inspector shows as an asset picker filtered
|
`AssetRef<ScriptAsset>`, which the inspector shows as an asset picker filtered
|
||||||
to the project's `scripts/` folder (`AssetKind::Script`).
|
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`
|
- **Play loop.** When Play starts, the editor's play `App` adds `ScriptModule`
|
||||||
alongside `PhysicsModule`, **shares the editor's `AssetServer`**, and is handed
|
alongside `PhysicsModule`, **shares the editor's `AssetServer`**, and is handed
|
||||||
a snapshot of the project `AssetDatabase`. Sharing the server is what lets the
|
a snapshot of the project `AssetDatabase`. Sharing the server is what lets the
|
||||||
|
|||||||
@@ -525,6 +525,8 @@ pub struct Shell {
|
|||||||
open_project_path: String,
|
open_project_path: String,
|
||||||
/// Text entered in the "Groups" editor's "add group" field.
|
/// Text entered in the "Groups" editor's "add group" field.
|
||||||
new_group_name: String,
|
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).
|
/// The Console panel's command-input buffer (the terminal prompt).
|
||||||
terminal_input: String,
|
terminal_input: String,
|
||||||
|
|
||||||
@@ -624,6 +626,7 @@ impl Shell {
|
|||||||
new_project_name: String::new(),
|
new_project_name: String::new(),
|
||||||
open_project_path: String::new(),
|
open_project_path: String::new(),
|
||||||
new_group_name: String::new(),
|
new_group_name: String::new(),
|
||||||
|
new_script_name: String::new(),
|
||||||
terminal_input: String::new(),
|
terminal_input: String::new(),
|
||||||
quit_requested: false,
|
quit_requested: false,
|
||||||
viewport_rect_px: None,
|
viewport_rect_px: None,
|
||||||
@@ -1125,6 +1128,7 @@ impl Shell {
|
|||||||
extensions: &mut self.extensions,
|
extensions: &mut self.extensions,
|
||||||
pending: &mut self.pending,
|
pending: &mut self.pending,
|
||||||
rename_buf: &mut self.rename_buf,
|
rename_buf: &mut self.rename_buf,
|
||||||
|
new_script_name: &mut self.new_script_name,
|
||||||
terminal_input: &mut self.terminal_input,
|
terminal_input: &mut self.terminal_input,
|
||||||
rot_euler: &mut self.rot_euler,
|
rot_euler: &mut self.rot_euler,
|
||||||
euler_for: &mut self.euler_for,
|
euler_for: &mut self.euler_for,
|
||||||
@@ -2154,6 +2158,9 @@ struct ShellTabViewer<'a> {
|
|||||||
extensions: &'a mut EditorExtensions,
|
extensions: &'a mut EditorExtensions,
|
||||||
pending: &'a mut Vec<PendingAction>,
|
pending: &'a mut Vec<PendingAction>,
|
||||||
rename_buf: &'a mut String,
|
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).
|
/// The Console command-input buffer (the terminal prompt).
|
||||||
terminal_input: &'a mut String,
|
terminal_input: &'a mut String,
|
||||||
rot_euler: &'a mut Vec3,
|
rot_euler: &'a mut Vec3,
|
||||||
@@ -3364,6 +3371,14 @@ impl<'a> ShellTabViewer<'a> {
|
|||||||
edits.push((comp.name, row.info.name, new_ron));
|
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
|
/// Renders one reflected field as a typed widget chosen from its
|
||||||
/// `type_name`, returning the field's new RON if the user changed it.
|
/// `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
|
/// Unknown types fall back to an editable RON text box, so the inspector is
|
||||||
|
|||||||
Reference in New Issue
Block a user