diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index be33e0e5a..f9804f995 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -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::(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; diff --git a/crates/ui-egui/src/panels/timeline/tests.rs b/crates/ui-egui/src/panels/timeline/tests.rs index 9185c614b..1cd5e3423 100644 --- a/crates/ui-egui/src/panels/timeline/tests.rs +++ b/crates/ui-egui/src/panels/timeline/tests.rs @@ -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 diff --git a/crates/ui-egui/tests/it/ui_timeline_trim.rs b/crates/ui-egui/tests/it/ui_timeline_trim.rs index 0ba1c73a6..9aaf1f500 100644 --- a/crates/ui-egui/tests/it/ui_timeline_trim.rs +++ b/crates/ui-egui/tests/it/ui_timeline_trim.rs @@ -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]