From e2d50ec926c1ac55ac859e3b2587aac5b3d60ef9 Mon Sep 17 00:00:00 2001 From: tylerjenningsw Date: Mon, 12 Oct 2026 17:45:30 -0400 Subject: [PATCH] Timeline: Shift ranges and key boxes stagger in comp order (#1012) In After Effects a marquee selection staggers in the comp's layer order, not in the direction the box was drawn. A Shift-click range of layer bars and a box of keys already come out top layer first; this test makes both with the pointer (the range from the bottom layer up, the box from the bottom right to the top left) and checks the Quick Offset that follows: the top layer stays and the bottom one moves the whole delta. --- crates/ui-egui/tests/ui_quick_offset.rs | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/crates/ui-egui/tests/ui_quick_offset.rs b/crates/ui-egui/tests/ui_quick_offset.rs index d8e16433..31161517 100644 --- a/crates/ui-egui/tests/ui_quick_offset.rs +++ b/crates/ui-egui/tests/ui_quick_offset.rs @@ -160,3 +160,42 @@ fn a_quick_offset_drag_shows_the_total_and_per_layer_offset() { .collect(); assert!(text.iter().any(|t| t.starts_with("Total Offset: +") && t.contains("Per Layer: +")), "{text:?}"); } + +fn click(h: &mut Harness<'_, EffectcraftApp>, at: Pos2, modifiers: Modifiers) { + h.input_mut().events.push(Event::PointerMoved(at)); + h.step(); + h.input_mut().events.push(Event::ModifiersChanged(modifiers)); + h.input_mut().events.push(Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: true, modifiers }); + h.step(); + h.input_mut().events.push(Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: false, modifiers }); + h.step(); + h.input_mut().events.push(Event::ModifiersChanged(Modifiers::default())); + h.run_steps(2); +} + +/// A Shift-click range of layers and a box of keys, drawn from the bottom right up to the top left, +/// stagger in the comp's layer order (top first), whichever way they were made (#1012). +#[test] +fn shift_ranges_and_key_boxes_stagger_in_comp_order() { + let (mut h, ids) = harness(); + // ids[2] is the top layer, ids[0] the bottom one. Click the bottom bar, Shift-click the top. + let bar = |h: &Harness<'_, EffectcraftApp>, id: u64| center(h, &format!("timeline.layer.{id}.bar")); + let (bottom, top) = (bar(&h, ids[0]), bar(&h, ids[2])); + click(&mut h, bottom, Modifiers::default()); + click(&mut h, top, Modifiers::SHIFT); + let selected: Vec = h.state().session.state.selected_layers.iter().map(|l| l.0).collect(); + assert_eq!(selected, vec![ids[2], ids[1], ids[0]], "a Shift range is in comp order"); + h.state_mut().session.execute("layer.quickOffset", json!({"delta": 2.0 / 30.0})).unwrap(); + assert_eq!([start(&h, ids[2]), start(&h, ids[1]), start(&h, ids[0])], [0, F, 2 * F]); + h.state_mut().session.execute("edit.undo", json!({})).unwrap(); + h.run_steps(2); + // A box around the 1 s keys, drawn upwards and to the left. + let one = Tick::from_seconds_f64(1.0).0; + let key = |h: &Harness<'_, EffectcraftApp>, id: u64| center(h, &format!("timeline.keyAt.{}.{one}", opacity_uid(h, id))); + let (low, high) = (key(&h, ids[0]), key(&h, ids[2])); + drag(&mut h, low + vec2(30.0, 5.0), high - vec2(30.0, 5.0), Modifiers::default()); + let order: Vec = h.state().session.state.selected_keys.iter().map(|k| k.layer.0).collect(); + assert_eq!(order, vec![ids[2], ids[1], ids[0]], "a key box is in comp order"); + h.state_mut().session.execute("keys.quickOffset", json!({"delta": 2.0 / 30.0})).unwrap(); + assert_eq!([key_times(&h, ids[2])[1], key_times(&h, ids[1])[1], key_times(&h, ids[0])[1]], [one, one + F, one + 2 * F]); +}