diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index 802913c4..5b422652 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -1141,17 +1141,18 @@ fn key_drag_id() -> egui::Id { egui::Id::new("tl-key-drag") } -/// A Quick Offset drag of selected layer bars (Ctrl/Cmd+Alt, #1012): where the pointer started, -/// how many layers it staggers and how many frames the last one has moved so far. +/// A Quick Offset drag of selected layer bars (Ctrl/Cmd+Alt, #1012): the bar it started on, where +/// the pointer started, how many layers it staggers and how many frames the last one has moved. #[derive(Clone, Copy, Debug)] struct QuickBar { + layer: LayerId, origin_x: f32, layers: usize, applied: i64, } -fn quick_bar_id(layer: LayerId) -> egui::Id { - egui::Id::new(("tl-quick-bar", layer.0)) +fn quick_bar_id() -> egui::Id { + egui::Id::new("tl-quick-bar") } /// How many layers a Quick Offset of `layers` staggers (#1012): the unlocked ones, each once, if @@ -2170,7 +2171,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { layer_context_menu(&body, &comp, layer, &selected, vector_footage_layer(&app.session.project, layer), &mut actions); let drag_key = format!("bar-{}", layer.id.0); // Ctrl/Cmd+Alt dragging a selected bar: Quick Offset (#1012), chosen as the - // drag starts. The last selected layer moves by whole frames with the pointer. + // drag starts and followed below the rows until the button is released. let mods = ui.input(|i| i.modifiers); if body.drag_started() && is_sel @@ -2179,29 +2180,11 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { && let Some(n) = quick_offset_layers(&comp, selected.iter().copied()) && let Some(p) = ctx.input(|i| i.pointer.press_origin()) { - ctx.data_mut(|d| d.insert_temp(quick_bar_id(layer.id), QuickBar { origin_x: p.x, layers: n, applied: 0 })); - } - let quick = ctx.data(|d| d.get_temp::(quick_bar_id(layer.id))); - if let Some(mut q) = quick { - if body.dragged() - && let Some(ptr) = ctx.input(|i| i.pointer.latest_pos()) - { - let frames = (((ptr.x - q.origin_x) as f64 / pps) / fd).round() as i64; - if frames != q.applied { - q.applied = frames; - ctx.data_mut(|d| d.insert_temp(quick_bar_id(layer.id), q)); - let merge = format!("bar-quick-{}", layer.id.0); - actions.push(("layer.quickOffset".into(), json!({"delta": frames as f64 * fd, "merge": merge, "fromStart": true}))); - } - drag_tip(&ctx, &t, ptr, quick_offset_note(q.applied, q.layers)); - } - if body.drag_stopped() { - ctx.data_mut(|d| d.remove::(quick_bar_id(layer.id))); - ui_actions.push(UiAct::EndMerge); - } + ctx.data_mut(|d| d.insert_temp(quick_bar_id(), QuickBar { layer: layer.id, origin_x: p.x, layers: n, applied: 0 })); } + let quick = ctx.data(|d| d.get_temp::(quick_bar_id())).is_some_and(|q| q.layer == layer.id); for (resp, kind) in [(&body, "move"), (&lin, "in"), (&lout, "out")] { - if kind == "move" && quick.is_some() { + if kind == "move" && quick { continue; } if resp.dragged() { @@ -2937,6 +2920,27 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { ctx.data_mut(|d| d.remove::>(layer_drag_id())); } } + // Quick Offset of layer bars: the last selected layer moves by whole frames with the pointer + // until the button is released, wherever the bar's row has scrolled to. + if let Some(mut q) = ctx.data(|d| d.get_temp::(quick_bar_id())) { + let (down, ptr) = ctx.input(|i| (i.pointer.primary_down(), i.pointer.latest_pos())); + match ptr.filter(|_| down) { + Some(ptr) => { + let frames = (((ptr.x - q.origin_x) as f64 / pps) / fd).round() as i64; + if frames != q.applied { + q.applied = frames; + ctx.data_mut(|d| d.insert_temp(quick_bar_id(), q)); + let merge = format!("bar-quick-{}", q.layer.0); + actions.push(("layer.quickOffset".into(), json!({"delta": frames as f64 * fd, "merge": merge, "fromStart": true}))); + } + drag_tip(&ctx, &t, ptr, quick_offset_note(q.applied, q.layers)); + } + None => { + ctx.data_mut(|d| d.remove::(quick_bar_id())); + ui_actions.push(UiAct::EndMerge); + } + } + } // Keyframe drag: the selected keys move by whole frames with the pointer until it is // released (one undo step); Shift snaps the grabbed key to the current time, the work area // and composition markers. diff --git a/crates/ui-egui/tests/ui_quick_offset.rs b/crates/ui-egui/tests/ui_quick_offset.rs index 31161517..0587e85f 100644 --- a/crates/ui-egui/tests/ui_quick_offset.rs +++ b/crates/ui-egui/tests/ui_quick_offset.rs @@ -199,3 +199,45 @@ fn shift_ranges_and_key_boxes_stagger_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]); } + +/// Releasing a Quick Offset drag with its row scrolled out of view ends it: a later plain drag of +/// that bar moves the selection together, as its own undo step. +#[test] +fn a_quick_offset_released_off_screen_ends() { + let mut s = Session::default(); + s.execute("comp.new", json!({"name": "Many", "width": 320, "height": 180, "frameRate": 30, "duration": 4})).unwrap(); + let ids: Vec = + (0..40).map(|i| s.execute("layer.newSolid", json!({"name": format!("L{i}"), "color": "#e04020"})).unwrap()["layer"].as_u64().unwrap()).collect(); + let (top, next) = (ids[39], ids[38]); + s.execute("layer.select", json!({"layers": [top, next]})).unwrap(); + let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s)); + h.run_steps(3); + let from = center(&h, &format!("timeline.layer.{top}.bar")); + h.input_mut().events.push(Event::PointerMoved(from)); + h.step(); + h.input_mut().events.push(Event::ModifiersChanged(ctrl_alt())); + h.input_mut().events.push(Event::PointerButton { pos: from, button: egui::PointerButton::Primary, pressed: true, modifiers: ctrl_alt() }); + h.step(); + for i in 1..=12 { + h.input_mut().events.push(Event::PointerMoved(from + vec2(10.0 * i as f32, 0.0))); + h.step(); + } + // Let go of Ctrl+Alt, scroll the row away, then release the button. + h.input_mut().events.push(Event::ModifiersChanged(Modifiers::default())); + h.state_mut().ui.timeline.scroll_y = 10_000.0; + h.run_steps(2); + assert!(h.state().auto.find(&format!("timeline.layer.{top}.bar")).is_none(), "the row is out of view"); + let to = from + vec2(120.0, 0.0); + h.input_mut().events.push(Event::PointerButton { pos: to, button: egui::PointerButton::Primary, pressed: false, modifiers: Modifiers::default() }); + h.run_steps(2); + let staggered = (start(&h, next), start(&h, top)); + assert!(staggered.0 > 0 && staggered.1 == 0, "the Quick Offset applied: {staggered:?}"); + h.state_mut().ui.timeline.scroll_y = 0.0; + h.run_steps(3); + let from = center(&h, &format!("timeline.layer.{top}.bar")); + drag(&mut h, from, from + vec2(160.0, 0.0), Modifiers::default()); + let d = start(&h, top); + assert!(d > 0 && start(&h, next) == staggered.0 + d, "a plain drag moves both by the same amount"); + h.state_mut().session.execute("edit.undo", json!({})).unwrap(); + assert_eq!((start(&h, next), start(&h, top)), staggered, "as its own undo step"); +}