From a78961a34663280faa1cadf83aa497b24819f397 Mon Sep 17 00:00:00 2001 From: pravdomil <2387356+pravdomil@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:21:52 +0200 Subject: [PATCH] Viewer: Fit up to 100% follows the panel size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Magnification ▸ Fit up to 100% stored the magnification of the moment it was picked, so the comp kept that size when the panel was maximized or resized. It is now a fit mode like Fit, capped at 100%. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/menus.rs | 1 + crates/ui-egui/src/panels/viewer.rs | 4 ++- crates/ui-egui/src/panels/viewer_tools.rs | 13 +++---- crates/ui-egui/src/state.rs | 3 ++ crates/ui-egui/tests/ui_viewer.rs | 42 +++++++++++++++++++++++ 5 files changed, 56 insertions(+), 7 deletions(-) diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 5c61b497..71f888fc 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -527,6 +527,7 @@ pub fn invoke(app: &mut EffectcraftApp, ctx: &egui::Context, id: &str, params: V } "view.fit" => { app.ui.viewer.zoom = None; + app.ui.viewer.fit_up_to_100 = false; app.ui.viewer.pan = [0.0, 0.0]; return Ok(Value::Null); } diff --git a/crates/ui-egui/src/panels/viewer.rs b/crates/ui-egui/src/panels/viewer.rs index 5fe7697c..c7d25fb8 100644 --- a/crates/ui-egui/src/panels/viewer.rs +++ b/crates/ui-egui/src/panels/viewer.rs @@ -731,6 +731,8 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let (cw, ch) = (comp.width as f32, comp.height as f32); let fit = ((area.width() - 40.0) / cw).min((area.height() - 40.0) / ch).max(0.01); + // Fit up to 100% follows the panel's size like Fit does, and stops at 100%. + let fit = if app.ui.viewer.fit_up_to_100 { fit.min(1.0 / ctx.pixels_per_point()) } else { fit }; ctx.data_mut(|d| d.insert_temp(egui::Id::new("viewer-fit"), fit)); let zoom = app.ui.viewer.zoom.unwrap_or(fit); let center = area.center() + vec2(app.ui.viewer.pan[0], app.ui.viewer.pan[1]); @@ -1828,7 +1830,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { vt::draw_snap(&ctx, &painter, &map, &ectx); vt::rulers(app, ui, &map, outer, area); - vt::bottom_bar(app, ui, bar, zoom, fit, time, &comp); + vt::bottom_bar(app, ui, bar, zoom, time, &comp); if err_h > 0.0 { super::expr_bar::draw(app, ui, err_bar, cid); } diff --git a/crates/ui-egui/src/panels/viewer_tools.rs b/crates/ui-egui/src/panels/viewer_tools.rs index 772d0769..3759c875 100644 --- a/crates/ui-egui/src/panels/viewer_tools.rs +++ b/crates/ui-egui/src/panels/viewer_tools.rs @@ -538,7 +538,7 @@ pub(crate) fn resolution_label(res: Resolution, scale: f64) -> String { /// transparency grid, mask and shape path visibility, region of interest, grid and guide /// options, show channel, reset exposure, exposure, take / show snapshot, Fast Previews, the 3D /// renderer and view, and the current time. -pub(crate) fn bottom_bar(app: &mut EffectcraftApp, ui: &mut egui::Ui, bar: Rect, zoom: f32, fit: f32, time: Tick, comp: &Comp) { +pub(crate) fn bottom_bar(app: &mut EffectcraftApp, ui: &mut egui::Ui, bar: Rect, zoom: f32, time: Tick, comp: &Comp) { let t = app.tokens; let p = ui.painter().clone(); p.rect_filled(bar, 0.0, t.panel_bg); @@ -554,7 +554,7 @@ pub(crate) fn bottom_bar(app: &mut EffectcraftApp, ui: &mut egui::Ui, bar: Rect, egui::ScrollArea::horizontal().id_salt("viewer-controls").auto_shrink([false, false]).show(ui, |ui| { let row = Rect::from_min_size(ui.max_rect().min, vec2(bar.width(), 30.0)); let first_control = app.auto.elements.len(); - let width = bottom_bar_contents(app, ui, row, zoom, fit, time, comp); + let width = bottom_bar_contents(app, ui, row, zoom, time, comp); ui.allocate_rect(Rect::from_min_size(row.min, vec2(width, row.height())), Sense::hover()); if let Some(focused) = ui.ctx().memory(|memory| memory.focused()) && app.auto.elements.get(first_control..).is_some_and(|controls| controls.iter().any(|control| control.response_id == Some(focused))) @@ -567,7 +567,7 @@ pub(crate) fn bottom_bar(app: &mut EffectcraftApp, ui: &mut egui::Ui, bar: Rect, }); } -fn bottom_bar_contents(app: &mut EffectcraftApp, ui: &mut egui::Ui, bar: Rect, zoom: f32, fit: f32, time: Tick, comp: &Comp) -> f32 { +fn bottom_bar_contents(app: &mut EffectcraftApp, ui: &mut egui::Ui, bar: Rect, zoom: f32, time: Tick, comp: &Comp) -> f32 { let t = app.tokens; let p = ui.painter().clone(); let cy = bar.center().y; @@ -588,7 +588,8 @@ fn bottom_bar_contents(app: &mut EffectcraftApp, ui: &mut egui::Ui, bar: Rect, z .iter() .map(|m| { let on = match *m { - "Fit" => app.ui.viewer.zoom.is_none(), + "Fit" => app.ui.viewer.zoom.is_none() && !app.ui.viewer.fit_up_to_100, + "Fit up to 100%" => app.ui.viewer.zoom.is_none() && app.ui.viewer.fit_up_to_100, s => s.trim_end_matches('%').parse::().is_ok_and(|v| app.ui.viewer.zoom.is_some() && (v - cur).abs() < 0.06), }; (m.to_string(), on) @@ -596,9 +597,9 @@ fn bottom_bar_contents(app: &mut EffectcraftApp, ui: &mut egui::Ui, bar: Rect, z .collect(); if let Some(i) = popup(app, ui, "vw-mag-pop", r, &items, "magnification") { app.ui.viewer.pan = [0.0, 0.0]; + app.ui.viewer.fit_up_to_100 = MAGS[i] == "Fit up to 100%"; app.ui.viewer.zoom = match MAGS[i] { - "Fit" => None, - "Fit up to 100%" => Some(fit.min(1.0 / ppp)), + "Fit" | "Fit up to 100%" => None, s => s.trim_end_matches('%').parse::().ok().map(|v| v / 100.0 / ppp), }; } diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 0cce2c11..04fe1e32 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -273,6 +273,8 @@ impl Resolution { pub struct ViewerState { /// Magnification (1 = 100%); None = fit. pub zoom: Option, + /// Fit up to 100%: while fitting (`zoom` is None), the comp is never magnified past 100%. + pub fit_up_to_100: bool, /// Pan offset in points from the centred position. pub pan: [f32; 2], pub res: Resolution, @@ -313,6 +315,7 @@ impl Default for ViewerState { fn default() -> Self { ViewerState { zoom: None, + fit_up_to_100: false, pan: [0.0, 0.0], res: Resolution::Auto, transparency_grid: false, diff --git a/crates/ui-egui/tests/ui_viewer.rs b/crates/ui-egui/tests/ui_viewer.rs index bdb4189d..a447d350 100644 --- a/crates/ui-egui/tests/ui_viewer.rs +++ b/crates/ui-egui/tests/ui_viewer.rs @@ -1948,3 +1948,45 @@ fn vertex_tools_edit_motion_path_keyframes() { click(&mut h, quarter); assert_eq!(keys(&h).len(), 3); } + +/// Magnification ▸ Fit up to 100% keeps fitting as the panel changes size (Maximize Panel), like +/// Fit, and never magnifies past 100%. +#[test] +fn fit_up_to_100_follows_the_maximized_panel() { + use effectcraft_ui_egui::dock::PanelKind; + let pick = |h: &mut Harness<'_, EffectcraftApp>| { + let at = rect(h, "viewer.magnification").center(); + click(h, at); + let at = rect(h, "viewer.magnification.1").center(); + click(h, at); + assert_eq!(h.state().ui.viewer.zoom, None, "a fit mode, not a fixed magnification"); + assert!(h.state().ui.viewer.fit_up_to_100); + h.run_steps(2); + }; + // A comp larger than the panel: it fills the docked panel, then the maximized one. + let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| { + let mut s = Session::default(); + s.execute("comp.new", json!({"name": "Big", "width": 3840, "height": 2160, "duration": 4})).unwrap(); + EffectcraftApp::new(s) + }); + h.run_steps(3); + pick(&mut h); + let docked = rect(&h, "viewer.comp").width(); + h.state_mut().toggle_maximize(PanelKind::Composition); + h.run_steps(3); + let area = rect(&h, "viewer.area"); + let maximized = rect(&h, "viewer.comp"); + assert!(maximized.width() > docked + 100.0, "{docked} -> {}", maximized.width()); + assert!(area.contains_rect(maximized), "{maximized:?} in {area:?}"); + h.state_mut().toggle_maximize(PanelKind::Composition); + h.run_steps(3); + assert_eq!(rect(&h, "viewer.comp").width(), docked, "and back when the panel is restored"); + // A comp smaller than the panel stops at 100%. + let mut h = harness(); + pick(&mut h); + let ppp = h.ctx.pixels_per_point(); + assert!(rect(&h, "viewer.comp").width() <= 640.0 / ppp + 0.01); + h.state_mut().toggle_maximize(PanelKind::Composition); + h.run_steps(3); + assert!((rect(&h, "viewer.comp").width() - 640.0 / ppp).abs() < 0.01); +}