Timeline: Alt held as a pick whip drops refers by index (refs #1072)

A property or expression pick whip dropped with Alt held, onto the
Timeline or Effect Controls, now sends byIndex: an effect's property is
referred to by its index, as in After Effects.

Test: an_alt_pick_whip_refers_to_an_effect_property_by_index (Alt and
plain drops onto Effect Controls and the Timeline, into the expression
being edited with a dimension, one undo step each).
This commit is contained in:
tylerjenningsw committed 2026-10-13 21:57:19 -04:00
1 parent f2cdad9edb
commit 25fc51f3ed
2 files changed
+72

No files matched your search

+4
View File
@@ -3052,6 +3052,10 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
if let Some(d) = d {
params["target"]["dimension"] = json!(d);
}
// Alt held as it drops: an effect's properties by their index (#1072).
if ctx.input(|i| i.modifiers.alt) {
params["byIndex"] = json!(true);
}
if let Some((text, [a, b])) = into {
// Into the expression being edited, which goes on being edited.
params["expression"] = json!(text);
+68
View File
@@ -539,3 +539,71 @@ fn arrow_keys_move_the_caret_in_rename_fields() {
let layer_name = h.state().session.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(layer)).unwrap().name.clone();
assert_eq!(layer_name, "Pla-te");
}
/// A primary-button drag from `from` to `to` with `modifiers` held from the press to the release.
fn drag_with(h: &mut Harness<'_, EffectcraftApp>, from: Pos2, to: Pos2, modifiers: Modifiers) {
h.input_mut().events.push(Event::PointerMoved(from));
h.input_mut().events.push(Event::ModifiersChanged(modifiers));
h.step();
h.input_mut().events.push(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers });
h.step();
for k in 1..=8 {
h.input_mut().events.push(Event::PointerMoved(from + (to - from) * (k as f32 / 8.0)));
h.step();
}
h.input_mut().events.push(Event::PointerButton { pos: to, button: PointerButton::Primary, pressed: false, modifiers });
h.step();
h.input_mut().events.push(Event::ModifiersChanged(Modifiers::NONE));
h.run_steps(3);
}
/// #1072: with Alt held as it drops, a pick whip refers to an effect's property by its index,
/// `effect("Slider Control")(1)`, as in After Effects; without Alt, by its name. Onto Effect
/// Controls or the Timeline, and into the expression being edited (with a dimension).
#[test]
fn an_alt_pick_whip_refers_to_an_effect_property_by_index() {
let (mut h, _) = harness();
let (layer, opacity, transform, effects) = {
let s = &mut h.state_mut().session;
let l = s.execute("layer.newSolid", json!({"name": "Plate", "color": "#406080"})).unwrap()["layer"].as_u64().unwrap();
s.execute("layer.select", json!({"layers": [l]})).unwrap();
for fx in ["Slider Control", "Point Control"] {
s.execute("effect.apply", json!({"layers": [l], "effect": fx})).unwrap();
}
let props = s.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(l)).unwrap().props.clone();
let effects: Vec<u64> = std::iter::once(props.sub("effects").unwrap().uid).chain(props.sub("effects").unwrap().groups().map(|g| g.uid)).collect();
(l, props.sub("transform").unwrap().get("opacity").unwrap().uid, props.sub("transform").unwrap().uid, effects)
};
h.state_mut().ui.timeline.open_layers.insert(layer);
h.state_mut().ui.timeline.open_groups.insert(transform);
h.state_mut().ui.timeline.open_groups.extend(effects);
h.state_mut().show_panel(effectcraft_ui_egui::dock::PanelKind::EffectControls);
h.run_steps(3);
let props =
|h: &Harness<'_, EffectcraftApp>| h.state().session.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(layer)).unwrap().props.clone();
let (slider, point) = (props(&h).prop("effects/#1/slider").unwrap().uid, props(&h).prop("effects/#2/point").unwrap().uid);
let expr = |h: &Harness<'_, EffectcraftApp>| props(h).find(opacity).unwrap().expr.clone().map(|e| e.text);
let alt = Modifiers { alt: true, ..Modifiers::NONE };
let whip = |h: &mut Harness<'_, EffectcraftApp>, target: &str, modifiers: Modifiers| {
let (from, to) = (center(h, &format!("timeline.prop.{opacity}.propertyPickWhip")), center(h, target));
drag_with(h, from, to, modifiers);
};
whip(&mut h, &format!("effectControls.prop.{slider}.value"), alt);
assert_eq!(expr(&h).as_deref(), Some("effect(\"Slider Control\")(1)"), "Alt onto Effect Controls");
let undo = h.state().session.history.undo.len();
whip(&mut h, &format!("effectControls.prop.{slider}.value"), Modifiers::NONE);
assert_eq!(expr(&h).as_deref(), Some("effect(\"Slider Control\")(\"Slider\")"), "without Alt, by name");
assert_eq!(h.state().session.history.undo.len(), undo + 1, "one undo step");
whip(&mut h, &format!("timeline.prop.{slider}.name"), alt);
assert_eq!(expr(&h).as_deref(), Some("effect(\"Slider Control\")(1)"), "Alt onto the Timeline");
// Into the expression being edited, at the cursor, with the value it was dropped on.
let p = center(&h, &format!("timeline.prop.{opacity}.expression"));
click(&mut h, p, 1);
key(&mut h, Key::End);
type_text(&mut h, " + ");
let (from, to) = (center(&h, &format!("timeline.prop.{opacity}.pickWhip")), center(&h, &format!("effectControls.prop.{point}.value.1")));
drag_with(&mut h, from, to, alt);
assert_eq!(expr(&h).as_deref(), Some("effect(\"Slider Control\")(1) + effect(\"Point Control\")(1)[1]"));
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
assert_eq!(expr(&h).as_deref(), Some("effect(\"Slider Control\")(1)"), "undone in one step");
}