mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-29 20:47:18 -06:00
#766: deliver ScriptUI mouse events to script listeners
Problem: ScriptUI controls never sent mouse events to scripts.
addEventListener("mousedown" | "mouseup" | "mousemove" | "mouseover" |
"mouseout" | "click") listeners never ran, so custom-drawn controls
(onDraw + mouse, as DuScriptUI builds them) could not react to the pointer.
docs/plugins.md listed this as missing.
Fix: scriptui.js builds a ScriptUI MouseEvent (clientX/clientY relative to
the target, button, detail = click count, screenX/Y, modifier keys,
getModifierState, stopPropagation, preventDefault) and delivers it in the
capture, target and bubble phases. addEventListener takes the capture flag.
The parent walk is bounded, so a script-made parent cycle cannot hang it.
Each published control lists the mouse events a listener wants
(Widget::mouse, inherited from containers). The new scriptui.mouse
command dispatches one event with clamped, finite values and rejects
unknown types and disabled controls. The egui ScriptUI view hit-tests the
pointer over script windows and docked panels, skipping covered layers and
windows blocked by a modal dialog. It sends mouseover/mouseout,
mousemove, mousedown/mouseup with click counts, and a click carrying the
pointer details in place of the plain click, so a checkbox still toggles
once. Handlers run under the existing script limits; a throwing listener
reports an error and the window keeps working.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9a7d3e7699
commit
1c06a793a5
@@ -110,6 +110,10 @@ pub struct Widget {
|
||||
pub read_only: bool,
|
||||
/// Event handlers the script attached (`onClick`, `onChange`, `onChanging`…).
|
||||
pub handlers: Vec<String>,
|
||||
/// The mouse events ([`MOUSE_EVENTS`]) a script listener receives when they happen on this
|
||||
/// control: its own `addEventListener` listeners and its containers' (events bubble).
|
||||
/// Frontends send only these.
|
||||
pub mouse: Vec<String>,
|
||||
/// The active tab of a tabbed panel (child index).
|
||||
pub active_tab: usize,
|
||||
/// What the control's `onDraw` handler painted (ScriptUIGraphics), in control coordinates.
|
||||
@@ -364,12 +368,17 @@ pub struct ScriptWindow {
|
||||
pub struct ScriptUiEvent {
|
||||
pub window: u32,
|
||||
pub widget: u32,
|
||||
/// `click`, `change`, `changing` or `close`.
|
||||
/// `click`, `change`, `changing`, `close`, or a mouse event ([`MOUSE_EVENTS`]).
|
||||
pub kind: String,
|
||||
/// The new value: text, number, bool, or selected index(es); for `close`, the result.
|
||||
/// The new value: text, number, bool, or selected index(es); for `close`, the result; for
|
||||
/// mouse events (and a mouse `click`), the pointer: `{x, y, button, detail, screenX, screenY,
|
||||
/// altKey, ctrlKey, metaKey, shiftKey}` with `x`/`y` relative to the control.
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
/// The ScriptUI mouse events scripts can listen for (`addEventListener`).
|
||||
pub const MOUSE_EVENTS: [&str; 6] = ["mousedown", "mouseup", "mousemove", "mouseover", "mouseout", "click"];
|
||||
|
||||
/// Hands an event to the script that owns the window; returns what the handler reported
|
||||
/// (`{ok, output, error}`) and updates [`Session::script_ui`].
|
||||
pub type ScriptUiDispatch = fn(&mut Session, &ScriptUiEvent) -> std::result::Result<Value, String>;
|
||||
@@ -687,6 +696,42 @@ pub(crate) fn set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
dispatch(s, ScriptUiEvent { window, widget, kind: kind.into(), value })
|
||||
}
|
||||
|
||||
/// `scriptui.mouse`: a mouse event on a control, as the pointer would make it: the script's
|
||||
/// `addEventListener` listeners on the control and its containers receive a MouseEvent
|
||||
/// (`clientX`/`clientY` relative to the control, `button`, `detail` = click count, modifier
|
||||
/// keys). A `click` also does what clicking the control does (toggle a checkbox, press a button).
|
||||
pub(crate) fn mouse(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let c = "scriptui.mouse";
|
||||
let window = window_p(s, p, c)?;
|
||||
let widget = widget_p(s, window, p, c)?;
|
||||
let ty = p.get("type").and_then(Value::as_str).unwrap_or("click").to_ascii_lowercase();
|
||||
let Some(ty) = MOUSE_EVENTS.iter().find(|t| **t == ty) else {
|
||||
return Err(crate::commands::bad(c, format!("unknown mouse event `{ty}` (one of {})", MOUSE_EVENTS.join(", "))));
|
||||
};
|
||||
let w = s.script_ui.window(window).and_then(|w| w.root.find(widget)).cloned().unwrap_or_default();
|
||||
if !w.enabled {
|
||||
return Err(crate::commands::bad(c, format!("control {widget} is disabled")));
|
||||
}
|
||||
// Hostile input stays finite and bounded.
|
||||
const FAR: f64 = 1.0e6;
|
||||
let num = |k: &str, lo: f64, hi: f64, d: f64| p.get(k).and_then(Value::as_f64).filter(|v| v.is_finite()).map_or(d, |v| v.clamp(lo, hi));
|
||||
let flag = |k: &str| p.get(k).and_then(Value::as_bool).unwrap_or(false);
|
||||
let clicks = if matches!(*ty, "mousemove" | "mouseover" | "mouseout") { 0.0 } else { 1.0 };
|
||||
let value = json!({
|
||||
"x": num("x", -FAR, FAR, w.bounds[2] / 2.0),
|
||||
"y": num("y", -FAR, FAR, w.bounds[3] / 2.0),
|
||||
"screenX": num("screenX", -FAR, FAR, 0.0),
|
||||
"screenY": num("screenY", -FAR, FAR, 0.0),
|
||||
"button": num("button", 0.0, 2.0, 0.0).round(),
|
||||
"detail": num("detail", 0.0, 1000.0, clicks).round(),
|
||||
"altKey": flag("altKey"),
|
||||
"ctrlKey": flag("ctrlKey"),
|
||||
"metaKey": flag("metaKey"),
|
||||
"shiftKey": flag("shiftKey"),
|
||||
});
|
||||
dispatch(s, ScriptUiEvent { window, widget, kind: (*ty).to_string(), value })
|
||||
}
|
||||
|
||||
/// `scriptui.close`: close a window (a dialog's `show()` returns `result`, default 2 = Cancel).
|
||||
pub(crate) fn close(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let window = window_p(s, p, "scriptui.close")?;
|
||||
|
||||
Reference in New Issue
Block a user