Timeline: a Shift-drag on an unselected bar moves the whole selection (#1828)

A Shift-press on an unselected bar adds it to the selection, so the drag now moves that
selection and its layers are no snap targets. Tests cover the selection drag and that a
moved layer's own marker is no target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
arnav-hiwarkar
2026-11-10 18:44:24 +05:30
co-authored by Claude Opus 5.5
parent 3f3031d941
commit 3c6abe3606
3 changed files with 36 additions and 9 deletions
+8 -5
View File
@@ -3526,11 +3526,15 @@ fn layer_bar_drag(g: &mut Rw, rc: &RowCtx<'_>, bars: &BarResps, quick: bool) {
}
let id = egui::Id::unique(("bar-drag", layer.id.0, kind));
if resp.dragged() {
// Moving or trimming a selected layer acts on the whole selection as it was when the
// drag started (a Shift-drag adds the layer to it); each layer trims from its own
// in/out point.
let shift = ctx.input(|i| i.modifiers.shift);
// Moving or trimming a selected layer acts on the whole selection as it is when the
// drag starts; each layer trims from its own in/out point. A Shift-drag on an
// unselected bar adds it to the selection (`layer_bar_hits`) and moves them all.
let start = || {
let edited = edited_layers(layer.id, &g.selected);
let mut edited = edited_layers(layer.id, &g.selected);
if kind == "move" && shift && !g.selected.contains(&layer.id) {
edited.extend(g.selected.iter().map(|l| l.0));
}
let (i, o) = (layer.in_point.seconds(), layer.out_point.seconds());
let ends = match kind {
"move" => vec![i, o],
@@ -3543,7 +3547,6 @@ fn layer_bar_drag(g: &mut Rw, rc: &RowCtx<'_>, bars: &BarResps, quick: bool) {
};
let mut drag = ctx.data(|d| d.get_temp::<BarDrag>(id)).filter(|_| !resp.drag_started()).unwrap_or_else(start);
drag.px += f64::from(resp.drag_delta().x);
let shift = ctx.input(|i| i.modifiers.shift);
let frames = bar_drag_frames(drag.px, g.tl.pps, g.tl.fd, &drag.ends, &drag.targets, shift);
if frames != drag.sent {
let d = frames.saturating_sub(drag.sent) as f64 * g.tl.fd;
+15 -4
View File
@@ -432,11 +432,22 @@ fn cti_snaps_to_the_nearest_candidate() {
let comp = app.session.active_comp().unwrap().clone();
let c = snap_candidates(&comp, &build_rows(&app, &comp));
assert!(c.contains(&comp.duration.seconds()) && c.contains(&0.0));
// A dragged layer's own ends are no targets; moved, neither are its markers and keys (#1828).
}
/// A dragged layer's own In / Out points are no snap targets; moved, neither are its markers
/// (#1828).
#[test]
fn bar_drags_dont_snap_to_their_own_layer() {
let mut app = app();
let bx = app.session.active_comp().unwrap().layers.iter().find(|l| l.name == "Box").unwrap().id.0;
app.session.execute("layer.addMarker", json!({"layers": [bx], "time": 1.0})).unwrap();
let comp = app.session.active_comp().unwrap().clone();
let rows = build_rows(&app, &comp);
let l = comp.layers.first().unwrap().id.0;
assert_eq!(snap_targets(&comp, &rows, &[l], false).len(), c.len() - 2);
assert!(snap_targets(&comp, &rows, &[l], true).len() <= c.len() - 2);
let all = snap_targets(&comp, &rows, &[], false);
let trimmed = snap_targets(&comp, &rows, &[bx], false);
let moved = snap_targets(&comp, &rows, &[bx], true);
assert_eq!(trimmed.len(), all.len() - 2, "trimming drops only the layer's In and Out");
assert!(trimmed.contains(&1.0) && !moved.contains(&1.0), "moving drops the layer's marker: {moved:?}");
}
/// Shift snaps a bar drag's nearest end to a target within 8 px of the pointer; farther away, or
@@ -148,6 +148,19 @@ fn dragging_bar_ends_trims_in_and_out() {
assert!((i - 3.0).abs() < 1e-6 && (o - 8.0).abs() < 1.5 / 30.0 && s.abs() < 1e-6, "out extended to ~8 s, not slipped: in {i} out {o} start {s}");
}
/// A Shift-drag on an unselected bar adds it to the selection and moves the whole selection
/// together, so the other selected layers keep their offset (#1828).
#[test]
fn shift_dragging_an_unselected_bar_moves_the_selection() {
let (mut h, solid, pre) = harness();
h.state_mut().session.execute("layer.select", json!({"layers": [pre]})).unwrap();
h.run_steps(2);
let b = bar(&h, solid);
drag_with(&mut h, b.center(), b.center() + vec2(b.width() / 4.0 * 2.5, 0.0), Modifiers::SHIFT);
let ((si, ..), (pi, ..)) = (timing(&h, solid), timing(&h, pre));
assert!(si > 4.0 && (pi - si - 1.0).abs() < 1e-6, "both moved by the same amount: solid in {si}, precomp in {pi}");
}
/// With Frames and Frame Count Start at 1, In moves to 00061 but the Duration column still counts
/// the layer's 120 frames (#1807).
#[test]