mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
Merge main into #1897; EvalCtx in convert_to_bezier built with ..EvalCtx::new
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -176,11 +176,9 @@ fn convert_to_bezier(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// Read the geometry before editing, with the same expression-aware context the renderer
|
||||
// uses: an expression-driven corner radius, size or position must convert to what is on
|
||||
// screen, not to the property's underlying value.
|
||||
let expr = s.expr.clone();
|
||||
let prepared = {
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||||
let ctx =
|
||||
EvalCtx { project: &s.project, comp_id: cid, comp, time: t, expr: expr.as_deref(), footage: None, property_overrides: &[], media_overrides: &[] };
|
||||
let ctx = EvalCtx { expr: s.expr.as_deref(), ..EvalCtx::new(&s.project, cid, comp, t) };
|
||||
let mut prepared = Vec::new();
|
||||
for (lid, uid) in &shapes {
|
||||
let Some(l) = comp.layer(*lid) else { continue };
|
||||
|
||||
@@ -577,7 +577,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Mouse Event on Script Window Control",
|
||||
[],
|
||||
None,
|
||||
"{window?, widget, type: mousedown | mouseup | mousemove | mouseover | mouseout | click (default), x?, y? (relative to the control; default its centre), button? (0 left, 1 middle, 2 right), detail? (click count), screenX?, screenY?, altKey?, ctrlKey?, metaKey?, shiftKey?} → the script's addEventListener listeners on the control and its containers receive a MouseEvent",
|
||||
"{window?, widget, type: mousedown | mouseup | mousemove | mouseover | mouseout | click (default), x?, y? (relative to the control; default its centre), button? (0 left, 1 middle, 2 right), buttons? (held: 1 left, 2 right, 4 middle), detail? (click count), screenX?, screenY?, altKey?, ctrlKey?, metaKey?, shiftKey?} → the script's addEventListener listeners on the control and its containers receive a MouseEvent",
|
||||
always,
|
||||
scriptui::mouse
|
||||
),
|
||||
|
||||
@@ -759,6 +759,7 @@ pub(crate) fn mouse(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"screenX": num("screenX", -FAR, FAR, 0.0),
|
||||
"screenY": num("screenY", -FAR, FAR, 0.0),
|
||||
"button": num("button", 0.0, 2.0, 0.0).round(),
|
||||
"buttons": num("buttons", 0.0, 7.0, 0.0).round(),
|
||||
"detail": num("detail", 0.0, 1000.0, clicks).round(),
|
||||
"altKey": flag("altKey"),
|
||||
"ctrlKey": flag("ctrlKey"),
|
||||
|
||||
@@ -745,7 +745,8 @@ function __uiMouseWanted(c, inherited) {
|
||||
}
|
||||
|
||||
// A ScriptUI MouseEvent. `v`: {x, y (relative to the target control), button (0 left, 1 middle,
|
||||
// 2 right), detail (click count), screenX, screenY, altKey, ctrlKey, metaKey, shiftKey}.
|
||||
// 2 right), buttons (held: 1 left, 2 right, 4 middle), detail (click count), screenX, screenY,
|
||||
// altKey, ctrlKey, metaKey, shiftKey}.
|
||||
function __uiMouseEvent(type, target, v) {
|
||||
var num = function (x, d) {
|
||||
x = Number(x);
|
||||
@@ -765,6 +766,7 @@ function __uiMouseEvent(type, target, v) {
|
||||
timeStamp: new Date(),
|
||||
detail: num(v.detail, type === "mousemove" || type === "mouseover" || type === "mouseout" ? 0 : 1),
|
||||
button: num(v.button, 0),
|
||||
buttons: num(v.buttons, 0),
|
||||
clientX: num(v.x, 0),
|
||||
clientY: num(v.y, 0),
|
||||
screenX: num(v.screenX, 0),
|
||||
|
||||
@@ -395,7 +395,11 @@ fn mouse_events(ui: &egui::Ui, origin: egui::Pos2, area: Rect, win: u32, root: &
|
||||
return;
|
||||
}
|
||||
let mut st: MouseTrack = ui.data(|d| d.get_temp(id)).unwrap_or_default();
|
||||
let (latest, events, mods, time) = ui.input(|i| (i.pointer.latest_pos(), i.events.clone(), i.modifiers, i.time));
|
||||
let (latest, events, mods, time, held) = ui.input(|i| {
|
||||
let held = |b: egui::PointerButton, bit: u8| if i.pointer.button_down(b) { bit } else { 0 };
|
||||
let held = held(egui::PointerButton::Primary, 1) | held(egui::PointerButton::Secondary, 2) | held(egui::PointerButton::Middle, 4);
|
||||
(i.pointer.latest_pos(), i.events.clone(), i.modifiers, i.time, held)
|
||||
});
|
||||
let ctx = ui.ctx();
|
||||
let layer = ui.layer_id();
|
||||
let target = |p: egui::Pos2| -> Option<&Widget> {
|
||||
@@ -414,7 +418,7 @@ fn mouse_events(ui: &egui::Ui, origin: egui::Pos2, area: Rect, win: u32, root: &
|
||||
w.id,
|
||||
json!({
|
||||
"type": ty, "x": p.x - at.x, "y": p.y - at.y, "screenX": p.x, "screenY": p.y,
|
||||
"button": button, "detail": detail,
|
||||
"button": button, "buttons": held, "detail": detail,
|
||||
"altKey": mods.alt, "ctrlKey": mods.ctrl, "shiftKey": mods.shift, "metaKey": mods.mac_cmd,
|
||||
}),
|
||||
));
|
||||
@@ -431,10 +435,15 @@ fn mouse_events(ui: &egui::Ui, origin: egui::Pos2, area: Rect, win: u32, root: &
|
||||
}
|
||||
st.hover = now_id;
|
||||
}
|
||||
if let Some((w, p)) = now
|
||||
&& st.at != Some(p)
|
||||
{
|
||||
send(w, "mousemove", p, 0, 0, acts);
|
||||
// While a button is held, the control it was pressed on keeps getting the moves, inside or
|
||||
// outside the window, as in After Effects (#2584).
|
||||
let captured = st.down.and_then(|(c, _)| root.find(c));
|
||||
if let Some(p) = latest.filter(|p| st.at != Some(*p)) {
|
||||
match (captured, now) {
|
||||
(Some(c), _) => send(c, "mousemove", p, 0, 0, acts),
|
||||
(None, Some((w, _))) => send(w, "mousemove", p, 0, 0, acts),
|
||||
(None, None) => {}
|
||||
}
|
||||
}
|
||||
st.at = latest;
|
||||
// Presses and releases.
|
||||
@@ -446,7 +455,9 @@ fn mouse_events(ui: &egui::Ui, origin: egui::Pos2, area: Rect, win: u32, root: &
|
||||
egui::PointerButton::Secondary => 2,
|
||||
_ => continue,
|
||||
};
|
||||
let Some(w) = target(*pos) else {
|
||||
// A release goes to the control the button was pressed on, wherever the pointer is.
|
||||
let pressed_on = st.down.filter(|(_, db)| !*pressed && *db == b).and_then(|(c, _)| root.find(c));
|
||||
let Some(w) = pressed_on.or_else(|| target(*pos)) else {
|
||||
if !pressed {
|
||||
st.down = None;
|
||||
}
|
||||
@@ -460,7 +471,8 @@ fn mouse_events(ui: &egui::Ui, origin: egui::Pos2, area: Rect, win: u32, root: &
|
||||
send(w, "mousedown", *pos, b, st.clicks, acts);
|
||||
} else {
|
||||
send(w, "mouseup", *pos, b, st.clicks, acts);
|
||||
if st.down == Some((w.id, b)) && b == 0 && w.enabled && w.mouse.iter().any(|m| m == "click") {
|
||||
let over = target(*pos).is_some_and(|t| t.id == w.id);
|
||||
if over && st.down == Some((w.id, b)) && b == 0 && w.enabled && w.mouse.iter().any(|m| m == "click") {
|
||||
// The click with the pointer's details, after mousedown / mouseup, in place of
|
||||
// the plain click the control reported.
|
||||
acts.retain(|a| !matches!(a, Act::Click(aw, ac) if *aw == win && *ac == w.id));
|
||||
|
||||
@@ -3591,7 +3591,15 @@ fn group_row_click(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: &mut Rw, rc:
|
||||
}
|
||||
app.auto.add(&format!("timeline.group.{uid}.name"), gr_rect, name);
|
||||
if gr.clicked() && uid & (WAVE_BIT | GEOM_BIT) == 0 {
|
||||
actions.push(("prop.select".into(), json!({"layer": layer.id.0, "prop": uid, "selectKeys": false})));
|
||||
// Shift or Cmd/Ctrl adds the group to the selection, or takes a selected one out, as
|
||||
// property rows and Effect Controls headers do (#2552: several effects and styles for
|
||||
// one animation preset).
|
||||
let add = ui.input(|i| i.modifiers.shift || i.modifiers.command);
|
||||
if add && app.session.state.selected_props.contains(&(layer.id, *uid)) {
|
||||
actions.push(("prop.deselect".into(), json!({"layer": layer.id.0, "prop": uid})));
|
||||
} else {
|
||||
actions.push(("prop.select".into(), json!({"layer": layer.id.0, "prop": uid, "selectKeys": false, "add": add})));
|
||||
}
|
||||
}
|
||||
if gr.double_clicked() {
|
||||
if fx.is_some() {
|
||||
|
||||
@@ -300,6 +300,47 @@ fn script_windows_send_mouse_events_to_listeners() {
|
||||
h.state_mut().session.execute("scriptui.close", json!({})).unwrap();
|
||||
}
|
||||
|
||||
/// #2584: a drag that starts on a control keeps reaching it outside the script window (moves and
|
||||
/// the release), and `MouseEvent.buttons` tells a drag from a hover.
|
||||
#[test]
|
||||
fn a_drag_keeps_its_control_past_the_window_edge() {
|
||||
let mut h = harness();
|
||||
let code = r#"
|
||||
var w = new Window("palette", "Drag");
|
||||
var pad = w.add("group", undefined, undefined, { name: "pad" });
|
||||
pad.preferredSize = [120, 80];
|
||||
var log = w.add("statictext", undefined, "", { name: "log" });
|
||||
log.preferredSize = [400, 20];
|
||||
var seen = [];
|
||||
var rec = function (e) { seen.push(e.type + (e.clientX < 0 ? " out" : " in") + " B" + e.buttons); log.text = seen.join("|"); };
|
||||
pad.addEventListener("mousedown", rec);
|
||||
pad.addEventListener("mousemove", rec);
|
||||
pad.addEventListener("mouseup", rec);
|
||||
w.show();
|
||||
"#;
|
||||
let r = h.state_mut().session.execute("script.run", json!({"code": code, "name": "drag.jsx"})).unwrap();
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.run_steps(3);
|
||||
let win = h.state().session.script_ui.windows[0].id;
|
||||
let pad = rect(&h, &format!("scriptui.{win}.pad"));
|
||||
let (from, to) = (pad.min + vec2(10.0, 20.0), pad.min + vec2(-300.0, 20.0));
|
||||
h.input_mut().events.push(Event::PointerMoved(from));
|
||||
h.run_steps(1);
|
||||
h.input_mut().events.push(Event::PointerButton { pos: from, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
|
||||
h.run_steps(1);
|
||||
for k in 1..=4 {
|
||||
h.input_mut().events.push(Event::PointerMoved(from + (to - from) * (k as f32 / 4.0)));
|
||||
h.run_steps(1);
|
||||
}
|
||||
h.input_mut().events.push(Event::PointerButton { pos: to, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });
|
||||
h.run_steps(3);
|
||||
let log = h.state().session.script_ui.windows[0].root.lookup(&json!("log")).cloned().unwrap().text;
|
||||
assert!(log.starts_with("mousemove in B0|mousedown in B1|"), "a hover move has no button held: {log}");
|
||||
assert!(log.contains("mousedown in B1|mousemove out B1"), "moves outside the window still arrive, with the button held: {log}");
|
||||
assert!(log.ends_with("|mouseup out B0"), "the release outside reaches the control: {log}");
|
||||
h.state_mut().session.execute("scriptui.close", json!({})).unwrap();
|
||||
}
|
||||
|
||||
fn click_at(h: &mut Harness<'_, EffectcraftApp>, c: egui::Pos2) {
|
||||
h.input_mut().events.push(Event::PointerMoved(c));
|
||||
h.run_steps(1);
|
||||
|
||||
@@ -453,3 +453,42 @@ fn effects_drag_to_a_new_place_in_the_timeline() {
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(names(&h), ["Tint", "Gaussian Blur", "Invert"], "one undo step per drop");
|
||||
}
|
||||
|
||||
/// #2552: Shift-clicking effect rows in the Timeline adds them to the selection (and a second
|
||||
/// Shift-click takes one out), so one animation preset can hold several effects.
|
||||
#[test]
|
||||
fn shift_click_selects_several_effects_for_a_preset() {
|
||||
let mut s = session();
|
||||
let l = s.execute("layer.newSolid", json!({"name": "Plate", "color": "#406080"})).unwrap()["layer"].as_u64().unwrap();
|
||||
for fx in ["Gaussian Blur", "Invert"] {
|
||||
s.execute("effect.apply", json!({"layers": [l], "effect": fx})).unwrap();
|
||||
}
|
||||
let effects = s.active_comp().unwrap().layer(LayerId(l)).unwrap().props.sub("effects").unwrap().clone();
|
||||
let uids: Vec<u64> = effects.groups().map(|g| g.uid).collect();
|
||||
let mut h = harness(s);
|
||||
h.state_mut().ui.timeline.open_groups.insert(effects.uid);
|
||||
open_layer(&mut h, l);
|
||||
let shift_click = |h: &mut Harness<'_, EffectcraftApp>, uid: u64| {
|
||||
let p = center(h, &format!("timeline.group.{uid}.name"));
|
||||
h.input_mut().events.push(Event::ModifiersChanged(Modifiers::SHIFT));
|
||||
h.input_mut().events.push(Event::PointerMoved(p));
|
||||
h.step();
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Modifiers::SHIFT });
|
||||
h.step();
|
||||
}
|
||||
h.input_mut().events.push(Event::ModifiersChanged(Modifiers::NONE));
|
||||
h.run_steps(2);
|
||||
};
|
||||
let selected = |h: &Harness<'_, EffectcraftApp>| h.state().session.state.selected_props.iter().map(|(_, u)| *u).collect::<Vec<_>>();
|
||||
click(&mut h, &format!("timeline.group.{}.name", uids[0]));
|
||||
assert_eq!(selected(&h), [uids[0]]);
|
||||
shift_click(&mut h, uids[1]);
|
||||
assert_eq!(selected(&h), [uids[0], uids[1]], "Shift adds the second effect");
|
||||
let path = std::env::temp_dir().join(format!("ec-2552-{}.ffx", std::process::id()));
|
||||
let r = h.state_mut().session.execute("anim.savePreset", json!({"path": path.to_string_lossy()})).unwrap();
|
||||
assert_eq!(r["effects"], 2, "{r}");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
shift_click(&mut h, uids[1]);
|
||||
assert_eq!(selected(&h), [uids[0]], "a second Shift-click takes it out");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user