From 5bb66bad745fcec1f68e0f6a28a7697ba596aec1 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 30 Oct 2026 07:37:19 +0700 Subject: [PATCH] ScriptUI: a docked panel gets its dock's size and onResize (#2585) A docked ScriptUI panel only ever saw its preferredSize: the host drew it in a scroll area at max(dock, preferred) and the script's window never learned the dock. scriptui.resize now gives a window a size: the script's window takes it, lays itself out again and runs onResizing / onResize. The dock host sends it whenever the panel's dock size changes, so window.size is the dock's, as in After Effects. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/scripts.rs | 9 ++++++ crates/engine/src/scriptui.rs | 11 ++++++++ crates/script/src/scriptui.js | 12 ++++++++ crates/ui-egui/src/panels/scriptui_view.rs | 11 ++++++++ crates/ui-egui/tests/it/ui_scriptui.rs | 32 ++++++++++++++++++++++ 5 files changed, 75 insertions(+) diff --git a/crates/engine/src/cmd/scripts.rs b/crates/engine/src/cmd/scripts.rs index a523770e3..a92d484a7 100644 --- a/crates/engine/src/cmd/scripts.rs +++ b/crates/engine/src/cmd/scripts.rs @@ -467,6 +467,15 @@ pub fn specs() -> Vec { always, scriptui::mouse ), + cmd!( + "scriptui.resize", + "Resize Script Window", + [], + None, + "{window?, size: [width, height] (pixels)} → the window takes the size, lays itself out again and its onResizing / onResize run (a docked ScriptUI panel gets its dock's size)", + always, + scriptui::resize + ), cmd!( "scriptui.close", "Close Script Window", diff --git a/crates/engine/src/scriptui.rs b/crates/engine/src/scriptui.rs index af89932fa..d8f9f1d1e 100644 --- a/crates/engine/src/scriptui.rs +++ b/crates/engine/src/scriptui.rs @@ -765,6 +765,17 @@ pub(crate) fn mouse(s: &mut Session, p: &Value) -> Result { dispatch(s, ScriptUiEvent { window, widget, kind: (*ty).to_string(), value }) } +/// `scriptui.resize`: the host gave the window a new size (a docked panel's dock, #2585): the +/// script's window takes it, lays itself out again and gets `onResizing` / `onResize`. +pub(crate) fn resize(s: &mut Session, p: &Value) -> Result { + let c = "scriptui.resize"; + let window = window_p(s, p, c)?; + let size = p.get("size").and_then(Value::as_array).ok_or_else(|| crate::commands::bad(c, "missing `size` [width, height]"))?; + let side = |i: usize| size.get(i).and_then(Value::as_f64).filter(|v| v.is_finite()).map(|v| v.round().clamp(1.0, 16384.0)); + let (Some(w), Some(h)) = (side(0), side(1)) else { return Err(crate::commands::bad(c, "`size` must be [width, height] in pixels")) }; + dispatch(s, ScriptUiEvent { window, widget: 0, kind: "resize".into(), value: json!([w, h]) }) +} + /// `scriptui.close`: close a window (a dialog's `show()` returns `result`, default 2 = Cancel). pub(crate) fn close(s: &mut Session, p: &Value) -> Result { let window = window_p(s, p, "scriptui.close")?; diff --git a/crates/script/src/scriptui.js b/crates/script/src/scriptui.js index 178bd8be1..ddfbc1f3e 100644 --- a/crates/script/src/scriptui.js +++ b/crates/script/src/scriptui.js @@ -912,6 +912,18 @@ function __uiDispatch(winId, id, kind, valueJson) { w.close(v === null ? 2 : v); return; } + // The host gave the window a new size (a docked panel's dock): take it, lay out, tell the script. + if (kind === "resize") { + var nw = Number(v && v[0]), nh = Number(v && v[1]); + var b = w.__ub || w.__lb; + if (nw > 0 && nh > 0 && (b[2] - b[0] !== nw || b[3] - b[1] !== nh)) { + w.__ub = [b[0], b[1], b[0] + nw, b[1] + nh]; + __uiLayout(w); + w.__fire("onResizing"); + w.__fire("onResize"); + } + return; + } var c = __uiFind(w, id); if (!c || c.enabled === false) return; var radio = function () { diff --git a/crates/ui-egui/src/panels/scriptui_view.rs b/crates/ui-egui/src/panels/scriptui_view.rs index 14460ed28..753b2884e 100644 --- a/crates/ui-egui/src/panels/scriptui_view.rs +++ b/crates/ui-egui/src/panels/scriptui_view.rs @@ -26,6 +26,8 @@ enum Act { Close(u32), /// A mouse event (`scriptui.mouse` parameters without window and widget). Mouse(u32, u32, Value), + /// A docked panel's new size (`scriptui.resize`). + Resize(u32, [f32; 2]), } fn rect_of(origin: egui::Pos2, b: [f64; 4]) -> Rect { @@ -500,6 +502,7 @@ fn perform(app: &mut EffectcraftApp, ctx: &egui::Context, acts: Vec) { Act::Set(w, id, v) => ("scriptui.set", json!({"window": w, "widget": id, "value": v})), Act::Changing(w, id, v) => ("scriptui.set", json!({"window": w, "widget": id, "value": v, "changing": true})), Act::Close(w) => ("scriptui.close", json!({"window": w})), + Act::Resize(w, [x, y]) => ("scriptui.resize", json!({"window": w, "size": [x, y]})), Act::Mouse(w, id, mut v) => { if let Some(o) = v.as_object_mut() { o.insert("window".into(), json!(w)); @@ -582,6 +585,14 @@ pub fn panel(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect, id: u32) { // A modal script dialog blocks the panels too. let modal = app.session.script_ui.windows.iter().any(|w| w.modal); let mut acts = vec![]; + // The script learns its dock's size when it changes (#2585), as After Effects tells a + // docked panel: its window takes the size and onResize runs. + let dock = [rect.width().round(), rect.height().round()]; + let told = egui::Id::unique(("scriptui-dock-size", id)); + if ui.data(|d| d.get_temp::<[f32; 2]>(told)) != Some(dock) && dock[0] >= 1.0 && dock[1] >= 1.0 { + ui.data_mut(|d| d.insert_temp(told, dock)); + acts.push(Act::Resize(id, dock)); + } let content = rect; egui::ScrollArea::both().id_salt(("scriptui-panel", id)).max_width(content.width()).max_height(content.height()).show(ui, |ui| { let (r, _) = ui.allocate_exact_size(vec2(w.root.bounds[2] as f32, w.root.bounds[3] as f32), Sense::hover()); diff --git a/crates/ui-egui/tests/it/ui_scriptui.rs b/crates/ui-egui/tests/it/ui_scriptui.rs index 2669a2631..8a248155b 100644 --- a/crates/ui-egui/tests/it/ui_scriptui.rs +++ b/crates/ui-egui/tests/it/ui_scriptui.rs @@ -305,6 +305,38 @@ fn a_drag_keeps_its_control_past_the_window_edge() { h.state_mut().session.execute("scriptui.close", json!({})).unwrap(); } +/// #2585: a window the host resizes takes the size, lays out again and runs onResize; a docked +/// ScriptUI panel gets its dock's size that way. +#[test] +fn script_windows_learn_the_size_they_are_given() { + let mut h = harness(); + let code = r#" + var w = new Window("palette", "Sized"); + var log = w.add("statictext", undefined, "", { name: "log" }); + log.preferredSize = [200, 20]; + w.onResize = function () { log.text = "resized " + w.size.width + "x" + w.size.height; }; + w.show(); + "#; + let r = h.state_mut().session.execute("script.run", json!({"code": code, "name": "sized.jsx"})).unwrap(); + assert_eq!(r["ok"], true, "{r}"); + h.run_steps(2); + h.state_mut().session.execute("scriptui.resize", json!({"window": "Sized", "size": [300, 180]})).unwrap(); + let win = h.state().session.script_ui.windows[0].clone(); + assert_eq!(win.root.lookup(&json!("log")).unwrap().text, "resized 300x180"); + let list = h.state_mut().session.execute("scriptui.list", json!({})).unwrap(); + assert_eq!(list[0]["size"], json!([300.0, 180.0]), "{list}"); + assert!(h.state_mut().session.execute("scriptui.resize", json!({"window": "Sized", "size": ["wide", 1]})).is_err()); + h.state_mut().session.execute("scriptui.close", json!({})).unwrap(); + // A docked panel: its window is its dock's size, not its preferred size. + h.state_mut().session.execute("window.scriptPanel", json!({"name": "Layer Tools.jsx"})).unwrap(); + h.run_steps(4); + let panel = h.state().session.script_ui.windows.iter().find(|w| w.script == "Layer Tools.jsx").unwrap().clone(); + let dock = rect(&h, &format!("scriptui.{}", panel.id)); + assert_eq!([panel.root.bounds[2].round(), panel.root.bounds[3].round()], [dock.width().round() as f64, dock.height().round() as f64]); + h.state_mut().close_panel(PanelKind::ScriptPanel(panel.id)); + h.run_steps(2); +} + fn click_at(h: &mut Harness<'_, EffectcraftApp>, c: egui::Pos2) { h.input_mut().events.push(Event::PointerMoved(c)); h.run_steps(1);