From 3bde85dfdb738dc1066c92d84b0c617e84b10919 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 19:16:48 +0000 Subject: [PATCH] Timeline: Ctrl/Cmd+wheel and a trackpad pinch zoom time (#428) egui turns Ctrl/Cmd+wheel and a pinch into a zoom factor (zoom_delta) rather than a scroll, and the Timeline only zoomed on Alt+wheel, so both did nothing over the layers. They now zoom time around the pointer, like Alt+wheel. (B stays Set Work Area Start as in After Effects; Split Layer is Ctrl/Cmd+Shift+D.) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f --- crates/ui-egui/src/panels/timeline.rs | 9 ++++++-- crates/ui-egui/tests/ui_timeline_zoom.rs | 28 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index 15149560..ef2d40fe 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -1614,9 +1614,14 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let max_scroll = (total_h - rows_rect.height() + rh).max(0.0); if ui.rect_contains_pointer(rows_rect) { let (dy, dx, zoom) = ui.input(|i| (i.smooth_scroll_delta.y, i.smooth_scroll_delta.x, i.modifiers.alt)); - if zoom && dy.abs() > 0.0 { + // Ctrl/Cmd+wheel and a trackpad pinch reach egui as a zoom factor rather than a scroll + // (#428); like Alt+wheel they zoom time. + let pinch = ui.input(|i| i.zoom_delta()) as f64; + let at_x = ui.input(|i| i.pointer.hover_pos()).map(|p| p.x).unwrap_or(graph_x0); + if (pinch - 1.0).abs() > 1e-4 { + set_zoom(&mut app.ui.timeline, &comp, tm.pps * pinch, fit_pps, tm.t(at_x), (at_x - tm.x0) as f64); + } else if zoom && dy.abs() > 0.0 { // Alt+wheel zooms around the pointer, out until the whole comp shows. - let at_x = ui.input(|i| i.pointer.hover_pos()).map(|p| p.x).unwrap_or(graph_x0); set_zoom(&mut app.ui.timeline, &comp, tm.pps * (dy as f64 / 200.0).exp(), fit_pps, tm.t(at_x), (at_x - tm.x0) as f64); } else { let over_outline = ui.input(|i| i.pointer.hover_pos()).is_some_and(|p| p.x < graph_x0); diff --git a/crates/ui-egui/tests/ui_timeline_zoom.rs b/crates/ui-egui/tests/ui_timeline_zoom.rs index 48b19cb8..5fb16943 100644 --- a/crates/ui-egui/tests/ui_timeline_zoom.rs +++ b/crates/ui-egui/tests/ui_timeline_zoom.rs @@ -59,6 +59,34 @@ fn alt_wheel_zooms_out_until_the_whole_comp_shows() { assert!(h.state().ui.timeline.pps.is_some()); } +/// #428: Ctrl/Cmd+wheel and a trackpad pinch zoom time over the layers, as Alt+wheel does (egui +/// hands them over as a zoom factor, which the Timeline ignored). +#[test] +fn ctrl_wheel_and_pinch_zoom_time() { + let (mut h, layer) = harness(); + h.state_mut().ui.timeline.pps = Some(200.0); + h.run_steps(2); + let pps = |h: &Harness<'_, EffectcraftApp>| h.state().ui.timeline.pps.unwrap_or(0.0); + let start = pps(&h); + assert!(start > 0.0); + let at = over_bar(&h, layer); + h.event(Event::PointerMoved(at)); + h.event(Event::Zoom(0.5)); + h.step(); + let pinched = pps(&h); + assert!(pinched > 0.0 && pinched < start * 0.75, "a pinch in zooms out: {start} -> {pinched}"); + let at = over_bar(&h, layer); + h.event(Event::PointerMoved(at)); + h.event(Event::ModifiersChanged(Modifiers::COMMAND)); + h.event(Event::MouseWheel { unit: MouseWheelUnit::Point, delta: vec2(0.0, 240.0), modifiers: Modifiers::COMMAND, phase: egui::TouchPhase::Move }); + h.step(); + h.event(Event::ModifiersChanged(Modifiers::NONE)); + h.step(); + assert!(pps(&h) > pinched, "Ctrl/Cmd+wheel up zooms in: {pinched} -> {}", pps(&h)); + // Over the layers, so the rows did not scroll. + assert_eq!(h.state().ui.timeline.scroll_y, 0.0); +} + fn rect(h: &Harness<'_, EffectcraftApp>, id: &str) -> egui::Rect { let e = h.state().auto.find(id).unwrap_or_else(|| panic!("no {id}")).clone(); egui::Rect::from_min_size(pos2(e.rect[0], e.rect[1]), vec2(e.rect[2], e.rect[3]))