From 0eda17470685e1158fad626f89db7c482ec704cd Mon Sep 17 00:00:00 2001 From: pravdomil <2387356+pravdomil@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:49:55 +0200 Subject: [PATCH] Shift+; again zooms the Timeline back in Zoom to Fit Comp remembers the span it left and goes back to it when pressed again from the whole comp. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/menus.rs | 2 +- crates/ui-egui/src/panels/timeline.rs | 15 +++++++++++++++ crates/ui-egui/src/state.rs | 4 ++++ crates/ui-egui/tests/ui_timeline_zoom.rs | 23 +++++++++++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index a60e524a..51921e62 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -592,7 +592,7 @@ pub fn invoke(app: &mut EffectcraftApp, ctx: &egui::Context, id: &str, params: V crate::panels::timeline::zoom(app, ctx, k); return Ok(Value::Null); } - "timeline.zoomFit" => app.ui.timeline.pps = None, + "timeline.zoomFit" => crate::panels::timeline::toggle_fit_zoom(app, ctx), "timeline.zoomFrameToggle" => crate::panels::timeline::toggle_frame_zoom(app, ctx), "timeline.column" => { let col = params.get("column").and_then(Value::as_str).ok_or("timeline.column: need `column`")?; diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index 72f10429..69f7eabd 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -545,6 +545,21 @@ pub fn toggle_frame_zoom(app: &mut EffectcraftApp, ctx: &egui::Context) { set_zoom(&mut app.ui.timeline, &comp, MAX_PPS, fit, cti, w as f64 / 2.0); } +/// `Shift+;`: show the whole comp; from there, go back to the span it was pressed in. +pub fn toggle_fit_zoom(app: &mut EffectcraftApp, ctx: &egui::Context) { + let tl = &mut app.ui.timeline; + if let Some(pps) = tl.pps { + tl.before_fit = Some((tl.start, pps)); + tl.pps = None; + tl.start = 0.0; + return; + } + let Some((start, pps)) = tl.before_fit.take() else { return }; + let Some((_, w)) = ctx.data(|d| d.get_temp::<(f32, f32)>(egui::Id::new("timeline-graph-area"))) else { return }; + let Some(comp) = app.session.active_comp_arc() else { return }; + set_zoom(&mut app.ui.timeline, &comp, pps, fit_pps(w, &comp), start, 0.0); +} + /// Zoom the time ruler around the CTI. pub fn zoom(app: &mut EffectcraftApp, ctx: &egui::Context, k: f64) { let Some((_, w)) = ctx.data(|d| d.get_temp::<(f32, f32)>(egui::Id::new("timeline-graph-area"))) else { return }; diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index d0a23279..56b5e2db 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -322,6 +322,9 @@ pub struct TimelineState { /// Visible span start (seconds) and pixels per second; None = fit comp. pub start: f64, pub pps: Option, + /// The span (`start`, `pps`) Zoom to Fit Comp left, which it goes back to when pressed again. + #[serde(skip)] + pub before_fit: Option<(f64, f64)>, pub scroll_y: f32, /// Left column area width. pub columns_w: f32, @@ -403,6 +406,7 @@ impl Default for TimelineState { TimelineState { start: 0.0, pps: None, + before_fit: None, scroll_y: 0.0, columns_w: 560.0, outline_scroll: 0.0, diff --git a/crates/ui-egui/tests/ui_timeline_zoom.rs b/crates/ui-egui/tests/ui_timeline_zoom.rs index 2c802600..a68ee0e0 100644 --- a/crates/ui-egui/tests/ui_timeline_zoom.rs +++ b/crates/ui-egui/tests/ui_timeline_zoom.rs @@ -163,6 +163,29 @@ fn semicolon_toggles_frame_level_and_the_whole_comp() { assert_eq!(h.state().ui.timeline.pps, None); } +/// `Shift+;` zooms out to the whole comp, and again back in to the span it left. +#[test] +fn shift_semicolon_toggles_the_whole_comp_and_the_span_before() { + let (mut h, _) = harness(); + h.state_mut().session.execute("time.set", json!({"time": 120.0})).unwrap(); + h.run_steps(3); + let ctx = h.ctx.clone(); + effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "timeline.zoomFrameToggle", json!({})).unwrap(); + h.run_steps(2); + let before = (h.state().ui.timeline.start, h.state().ui.timeline.pps); + assert!(before.1.is_some()); + effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "timeline.zoomFit", json!({})).unwrap(); + assert_eq!(h.state().ui.timeline.pps, None); + h.run_steps(2); + effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "timeline.zoomFit", json!({})).unwrap(); + assert_eq!((h.state().ui.timeline.start, h.state().ui.timeline.pps), before); + // With nothing to go back to, the whole comp stays. + h.state_mut().ui.timeline.pps = None; + h.state_mut().ui.timeline.before_fit = None; + effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "timeline.zoomFit", json!({})).unwrap(); + assert_eq!(h.state().ui.timeline.pps, None); +} + fn space(h: &mut Harness<'_, EffectcraftApp>, pressed: bool) { h.event(Event::Key { key: egui::Key::Space, physical_key: None, pressed, repeat: false, modifiers: Modifiers::NONE }); h.step();