From 80158bb69bafa94f7b63c27cb79d4b9cbe8c0a9e Mon Sep 17 00:00:00 2001 From: dorfee Date: Mon, 19 Oct 2026 16:45:39 +0300 Subject: [PATCH] ScriptUI: install scheduled task runner in host sessions --- crates/host/src/lib.rs | 22 ++++++++++++++++++---- crates/ui-egui/src/lib.rs | 4 ++++ crates/ui-egui/tests/ui_scriptui.rs | 15 +++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/crates/host/src/lib.rs b/crates/host/src/lib.rs index bdb4e2bc..577492a5 100644 --- a/crates/host/src/lib.rs +++ b/crates/host/src/lib.rs @@ -82,7 +82,7 @@ impl Exporter for FileExporter { /// A new session with media, import, expressions, scripting and export enabled. pub fn session() -> Session { - Session { + let mut s = Session { // The desktop app, the CLI and the MCP server share installed Roto Brush models. models_dir: config_dir().map(|d| d.join("models")), exporter: Some(Arc::new(FileExporter::default())), @@ -90,11 +90,11 @@ pub fn session() -> Session { importer: Some(Arc::new(MediaImporter)), expr: Some(Arc::new(effectcraft_expr::Expressions)), expr_check: Some(effectcraft_expr::check_syntax), - script: Some(effectcraft_script::runner), - script_ui: effectcraft_engine::scriptui::ScriptUi { dispatch: Some(effectcraft_script::dispatch_ui), ..Default::default() }, plugin_loader: effectcraft_plugin::wasm_available().then_some(effectcraft_plugin::loader as effectcraft_engine::PluginLoader), ..Default::default() - } + }; + effectcraft_script::install(&mut s); + s } /// The platform config directory for EffectCraft (`EFFECTCRAFT_CONFIG_DIR` overrides): @@ -109,6 +109,20 @@ pub fn config_dir() -> Option { mod tests { use serde_json::json; + #[test] + fn host_runs_tasks_scheduled_by_panel_handlers() { + let mut s = super::session(); + let result = s.execute("script.run", json!({"code": "var w=new Window('palette','Timer regression'); var b=w.add('button',undefined,'Import'); b.onClick=function(){app.scheduleTask(\"app.project.items.addComp('Scheduled',64,64,1,1,25)\",0,false);};w.show();"})).unwrap(); + assert_eq!(result["ok"], true, "{result}"); + s.execute("scriptui.click", json!({"window":"Timer regression","widget":"Import"})).unwrap(); + assert!(s.project.items.is_empty()); + let tick = s.execute("script.runTasks", json!({})).unwrap(); + assert_eq!(tick["ran"], 1, "{tick}"); + assert_eq!(tick["errors"], json!([]), "{tick}"); + assert!(s.project.items.values().any(|item| item.name == "Scheduled")); + s.execute("scriptui.close", json!({"window":"Timer regression"})).unwrap(); + } + /// Layer ▸ Camera ▸ Link Focus Distance to Point of Interest / to Layer: the expressions /// evaluate to the camera's focus on the target. #[test] diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 72016cc5..c330bcc3 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1775,7 +1775,11 @@ impl EffectcraftApp { if self.session.script_ui.task_hosts.is_empty() { return; } + let previous_comp = self.session.active_comp_id(); let Some(r) = effectcraft_engine::scriptui::ScriptUi::run_tasks(&mut self.session) else { return }; + if self.session.active_comp_id().is_some() && self.session.active_comp_id() != previous_comp { + self.ui.start_screen = false; + } if let Some(msg) = r["errors"].as_array().and_then(|e| e.first()).and_then(|e| e["message"].as_str()) { self.toast = Some((format!("Scheduled script task: {msg}"), ctx.input(|i| i.time))); } else if let Some(line) = r["output"].as_array().and_then(|o| o.last()).and_then(|l| l.as_str()) { diff --git a/crates/ui-egui/tests/ui_scriptui.rs b/crates/ui-egui/tests/ui_scriptui.rs index 6cfc2db7..31fb555d 100644 --- a/crates/ui-egui/tests/ui_scriptui.rs +++ b/crates/ui-egui/tests/ui_scriptui.rs @@ -271,3 +271,18 @@ fn click_at(h: &mut Harness<'_, EffectcraftApp>, c: egui::Pos2) { } h.run_steps(3); } +#[test] +fn scheduled_import_reveals_its_composition_without_manual_clicks() { + let mut s = effectcraft_host::session(); + let result = s.execute("script.run", json!({"code": "var w=new Window('palette','Automatic import');var b=w.add('button',undefined,'Open');b.onClick=function(){app.scheduleTask(\"var c=app.project.items.addComp('Imported',64,64,1,1,25);c.openInViewer();\",0,false);};w.show();"})).unwrap(); + assert_eq!(result["ok"], true, "{result}"); + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_| EffectcraftApp::new(s)); + h.state_mut().ui.start_screen = true; + h.run_steps(2); + h.state_mut().session.execute("scriptui.click", json!({"window":"Automatic import","widget":"Open"})).unwrap(); + h.run_steps(4); + let active = h.state().session.active_comp_id().unwrap(); + assert_eq!(h.state().session.project.items.get(&active).unwrap().name, "Imported"); + assert!(!h.state().ui.start_screen, "Home must not hide a composition opened by the scheduled import"); + h.state_mut().session.execute("scriptui.close", json!({"window":"Automatic import"})).unwrap(); +}