Merge pull request #916 from pravdomil/claude/composition-maximize-zoom-fit-86627e

Viewer: Fit up to 100% follows the panel size

# Conflicts:
#	crates/ui-egui/src/panels/viewer_tools.rs
#	crates/ui-egui/tests/ui_viewer.rs
This commit is contained in:
Brandon Thomas 2026-10-12 04:22:53 -04:00
commit 2ea7220e49
6 changed files with 68 additions and 14 deletions

View File

@ -561,6 +561,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);
}

View File

@ -122,10 +122,11 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
// Bottom bar: magnification and View menus.
p.rect_filled(bar, 0.0, t.panel_bg);
let mr = Rect::from_min_size(pos2(bar.min.x + 8.0, bar.center().y - 10.0), vec2(80.0, 20.0));
if let Some(z) =
super::viewer_tools::magnification_menu(app, ui, mr, "layer-mag", "layerPanel.magnification", "layerMagnification", zoom as f32, app.ui.layer_zoom, fit)
if let Some((z, up)) =
super::viewer_tools::magnification_menu(app, ui, mr, "layer-mag", "layerPanel.magnification", "layerMagnification", zoom as f32, app.ui.layer_zoom, false)
{
app.ui.layer_zoom = z;
// The Layer panel keeps no fit mode: Fit up to 100% is the fit, at most 100 %.
app.ui.layer_zoom = if up { Some(fit.min(1.0 / ui.ctx().pixels_per_point())) } else { z };
app.ui.layer_pan = [0.0, 0.0];
}
let vx = mr.max.x + 10.0;

View File

@ -795,6 +795,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]);
@ -1952,7 +1954,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);
}

View File

@ -523,8 +523,8 @@ pub(crate) fn magnification_menu(
auto_items: &str,
zoom: f32,
current: Option<f32>,
fit: f32,
) -> Option<Option<f32>> {
fit_up: bool,
) -> Option<(Option<f32>, bool)> {
let t = app.tokens;
let ppp = ui.ctx().pixels_per_point();
let pop = format!("{id}-pop");
@ -540,17 +540,20 @@ pub(crate) fn magnification_menu(
.iter()
.map(|m| {
let on = match *m {
"Fit" => current.is_none(),
"Fit" => current.is_none() && !fit_up,
"Fit up to 100%" => current.is_none() && fit_up,
s => s.trim_end_matches('%').parse::<f32>().is_ok_and(|v| current.is_some() && (v - cur).abs() < 0.06),
};
(m.to_string(), on)
})
.collect();
let i = popup(app, ui, &pop, r, &items, auto_items)?;
// `(zoom, fit up to 100 %)`: Fit and Fit up to 100% leave the zoom unset (it follows the
// panel size).
match *MAGS.get(i)? {
"Fit" => Some(None),
"Fit up to 100%" => Some(Some(fit.min(1.0 / ppp))),
s => s.trim_end_matches('%').parse::<f32>().ok().map(|v| Some(v / 100.0 / ppp)),
"Fit" => Some((None, false)),
"Fit up to 100%" => Some((None, true)),
s => s.trim_end_matches('%').parse::<f32>().ok().map(|v| (Some(v / 100.0 / ppp), false)),
}
}
@ -583,7 +586,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);
@ -599,7 +602,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)))
@ -612,7 +615,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;
@ -621,8 +624,10 @@ fn bottom_bar_contents(app: &mut EffectcraftApp, ui: &mut egui::Ui, bar: Rect, z
// Magnification.
let r = Rect::from_min_size(pos2(x, cy - 10.0), vec2(80.0, 20.0));
if let Some(z) = magnification_menu(app, ui, r, "vw-mag", "viewer.magnification", "magnification", zoom, app.ui.viewer.zoom, fit) {
let fit_up = app.ui.viewer.fit_up_to_100;
if let Some((z, up)) = magnification_menu(app, ui, r, "vw-mag", "viewer.magnification", "magnification", zoom, app.ui.viewer.zoom, fit_up) {
app.ui.viewer.pan = [0.0, 0.0];
app.ui.viewer.fit_up_to_100 = up;
app.ui.viewer.zoom = z;
}
x = r.max.x + 4.0;

View File

@ -233,6 +233,8 @@ pub use effectcraft_engine::project::Resolution;
pub struct ViewerState {
/// Magnification (1 = 100%); None = fit.
pub zoom: Option<f32>,
/// 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 transparency_grid: bool,
@ -272,6 +274,7 @@ impl Default for ViewerState {
fn default() -> Self {
ViewerState {
zoom: None,
fit_up_to_100: false,
pan: [0.0, 0.0],
transparency_grid: false,
show_masks: true,

View File

@ -2046,3 +2046,45 @@ fn frame_sits_on_the_physical_pixel_grid() {
}
}
}
/// 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);
}