From f794f4665c3f8c41a438bd6b2e3e00010c9200e5 Mon Sep 17 00:00:00 2001 From: Louise Fox <2503492+KaboomFox@users.noreply.github.com> Date: Sun, 8 Nov 2026 04:46:38 +0000 Subject: [PATCH 1/4] Canvas: a test of the opening fit when the first frame's canvas is narrower than the settled one A document without a saved view is fitted to the canvas of the first frame that shows it. The test lays that frame's canvas out 2 points narrower than the next, and expects the zoom the settled canvas gives. It fails until the opening fit follows the canvas. --- crates/ui-egui/src/canvas/tests.rs | 54 ++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index 2c4ef5baa..9ef8fe597 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -87,6 +87,60 @@ fn subpixel_dock_layout_does_not_change_pointer_document_coordinates() { assert_ne!(observed[0], observed[2], "a real device-pixel resize must still change the mapping"); } +/// A document's opening view is fitted to the canvas of the first frame that shows it, and the +/// window's layout can still move the canvas's edges over the next frames. Two starts of the app +/// then showed one file at zooms a hair apart, on canvases alike once the window had settled. So +/// until the view is moved or the user presses, types or scrolls, the opening fit follows the +/// canvas. +#[test] +fn the_opening_fit_follows_the_canvas_until_the_view_moves_or_the_user_acts() { + // A frame that shows the canvas `width` points wide, which limits the fit of the 800 × 600 + // artboard, with `events`: the view after it. + fn frame(app: &mut VectorcraftApp, ctx: &egui::Context, width: f32, events: Vec) -> View { + let raw = egui::RawInput { + screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(1800.0, 900.0))), + max_texture_side: Some(8192), + events, + ..Default::default() + }; + let mut out = ctx.run_ui(raw, |ui| { + let area = egui::Rect::from_min_size(pos2(54.0, 72.0), vec2(width + SCROLL_BAR, 636.0 + SCROLL_BAR)); + let mut child = ui.new_child(egui::UiBuilder::new().max_rect(area)); + show(app, &mut child); + }); + out.textures_delta.clear(); + *app.view().unwrap() + } + fn opened() -> (VectorcraftApp, egui::Context) { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 800, "height": 600, "created": 0})).unwrap(); + (app, egui::Context::default()) + } + let first_shown_at = |width: f32| { + let (mut app, ctx) = opened(); + frame(&mut app, &ctx, width, vec![]).zoom + }; + let (narrow, wide) = (first_shown_at(700.0), first_shown_at(702.0)); + assert!(narrow < wide, "the canvas's width limits the fit: {narrow} and {wide}"); + let escape = || egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE }; + + // The first frame lays the canvas out 2 points narrower than the next: the document opens at + // the zoom of the settled canvas, as it does when the first frame has it already. + let (mut app, ctx) = opened(); + frame(&mut app, &ctx, 700.0, vec![]); + assert_eq!(frame(&mut app, &ctx, 702.0, vec![]).zoom, wide); + // Once the user has pressed a key, the canvas changing keeps the view, as resizing the window + // does. + assert_eq!(frame(&mut app, &ctx, 702.0, vec![escape()]).zoom, wide); + assert_eq!(frame(&mut app, &ctx, 700.0, vec![]).zoom, wide); + + // A view moved before the canvas changes stays where it was moved to. + let (mut app, ctx) = opened(); + frame(&mut app, &ctx, 700.0, vec![]); + app.view_mut().unwrap().zoom = 0.5; + assert_eq!(frame(&mut app, &ctx, 702.0, vec![]).zoom, 0.5); +} + /// A rendered raster becomes the image egui's own conversion makes of it, half-transparent /// edges and all; one whose bytes don't match its size becomes an empty image of that size. #[test] From 1b85e7fee2f594e2e42e1a8c09a76fb552490b77 Mon Sep 17 00:00:00 2001 From: Louise Fox <2503492+KaboomFox@users.noreply.github.com> Date: Sun, 8 Nov 2026 04:52:43 +0000 Subject: [PATCH 2/4] Canvas: a document opens at the zoom of the settled window A document without a saved view was fitted once, to the canvas of the first frame that shows it, and the window's layout can still move the canvas's edges over the next frames. Two starts of the app then opened one file at zooms a hair apart (4.9812 and 4.9883 in a CI probe of v0.21.0, the canvas and the view's centre alike). The opening fit now follows the canvas until the view is moved or the user presses a button or a key, types, scrolls or zooms (CanvasCache::opening_fit). After that a canvas that changes keeps the view, as before. --- crates/ui-egui/src/app.rs | 1 + crates/ui-egui/src/caches.rs | 4 ++++ crates/ui-egui/src/canvas.rs | 27 ++++++++++++++++++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index 8dd08c9b5..d139efe64 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -89,6 +89,7 @@ impl VectorcraftApp { view_moved: None, hover_cursor: None, presented: None, + opening_fit: None, panes: Default::default(), windows: crate::doc_windows::Windows::new(known), }, diff --git a/crates/ui-egui/src/caches.rs b/crates/ui-egui/src/caches.rs index 1bb731ca8..a9211eccf 100644 --- a/crates/ui-egui/src/caches.rs +++ b/crates/ui-egui/src/caches.rs @@ -65,6 +65,10 @@ pub struct CanvasCache { /// In Presentation Mode, the canvas rect the active artboard was last fitted to: it is fitted /// again when the rect changes (the window going full screen). pub presented: Option, + /// The canvas a document's opening view was fitted to, with the view the fit gave. The window's + /// layout can still move the canvas's edges over the next frames, so until the view is moved or + /// the user acts, the opening view is fitted again when the canvas changes ([`canvas::show`]). + pub opening_fit: Option, /// Window › Arrange › Tile: the renders the panes of the other documents draw. pub panes: canvas::Panes, /// Window › Arrange › Float in Window: what the floating document windows keep between frames. diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 0793ac9e0..80351ca81 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -186,6 +186,22 @@ fn view_rects(app: &VectorcraftApp, outer: egui::Rect, ppp: f32) -> (egui::Rect, (full, rect.round_to_pixels(ppp)) } +/// Whether the user pressed a button or a key, typed, scrolled or zoomed in this frame: the +/// opening fit stops following the canvas then ([`crate::caches::CanvasCache::opening_fit`]). +fn acted(i: &egui::InputState) -> bool { + i.pointer.any_pressed() + || i.events.iter().any(|e| { + matches!( + e, + egui::Event::Key { pressed: true, .. } + | egui::Event::Text(_) + | egui::Event::MouseWheel { .. } + | egui::Event::Zoom(_) + | egui::Event::Touch { .. } + ) + }) +} + /// Paint the document canvas, complete or cached artwork, navigation and tool overlays. pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); @@ -225,13 +241,22 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let (full, rect) = view_rects(app, outer, ui.ctx().pixels_per_point()); app.canvas_rect = Some(rect); let fitted = app.view().is_some_and(|v| v.fitted); + // A document's opening view is fitted to the canvas of the first frame that shows it, and the + // window's layout can still move the canvas's edges over the next frames. Until the view is + // moved or the user acts, the opening fit follows the canvas, so that the document opens at the + // zoom of the settled window. + let key = |v: &View| ViewKey { rect, zoom: v.zoom, center: v.center, rotation: v.rotation }; + let following = + !fitted || (!ui.input(acted) && app.canvas.opening_fit.zip(app.view()).is_some_and(|(was, v)| ViewKey { rect: was.rect, ..key(v) } == was)); + let settling = fitted && following && app.canvas.opening_fit.is_some_and(|was| was.rect != rect); // Presentation Mode shows the active artboard on the whole screen and nothing but the art: // no selection, guides, grids or artboard edges (#4365). let presenting = app.ui.screen_mode == 3; - if !fitted || (presenting && app.canvas.presented != Some(rect)) { + if !fitted || settling || (presenting && app.canvas.presented != Some(rect)) { fit(app, "view.fitArtboard"); } app.canvas.presented = presenting.then_some(rect); + app.canvas.opening_fit = if following { app.view().map(key) } else { None }; // Before the canvas's own interaction, so the view the canvas draws is the one they moved to. if bars { scroll_bars(app, ui, rect, outer, &t); From 2d945f92a12bc07a45968c036b15df6633e84793 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 9 Nov 2026 01:38:26 +0700 Subject: [PATCH 3/4] Canvas: the opening fit's conditions in a helper, keeping the canvas frame under the complexity limit --- crates/ui-egui/src/canvas.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 80351ca81..8081a40bf 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -202,6 +202,20 @@ fn acted(i: &egui::InputState) -> bool { }) } +/// The view `v` shows in the canvas `rect`, as the opening fit records it. +fn view_key(v: &View, rect: egui::Rect) -> ViewKey { + ViewKey { rect, zoom: v.zoom, center: v.center, rotation: v.rotation } +} + +/// Whether the opening fit still follows the canvas `rect` (the view is unfitted, or unmoved since +/// the fit and the user hasn't acted), and whether the canvas moved under it this frame, so the view +/// is fitted again. +fn opening_fit_follows(app: &VectorcraftApp, ui: &Ui, rect: egui::Rect, fitted: bool) -> (bool, bool) { + let following = !fitted || (!ui.input(acted) && app.canvas.opening_fit.zip(app.view()).is_some_and(|(was, v)| view_key(v, was.rect) == was)); + let settling = fitted && following && app.canvas.opening_fit.is_some_and(|was| was.rect != rect); + (following, settling) +} + /// Paint the document canvas, complete or cached artwork, navigation and tool overlays. pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); @@ -245,10 +259,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { // window's layout can still move the canvas's edges over the next frames. Until the view is // moved or the user acts, the opening fit follows the canvas, so that the document opens at the // zoom of the settled window. - let key = |v: &View| ViewKey { rect, zoom: v.zoom, center: v.center, rotation: v.rotation }; - let following = - !fitted || (!ui.input(acted) && app.canvas.opening_fit.zip(app.view()).is_some_and(|(was, v)| ViewKey { rect: was.rect, ..key(v) } == was)); - let settling = fitted && following && app.canvas.opening_fit.is_some_and(|was| was.rect != rect); + let (following, settling) = opening_fit_follows(app, ui, rect, fitted); // Presentation Mode shows the active artboard on the whole screen and nothing but the art: // no selection, guides, grids or artboard edges (#4365). let presenting = app.ui.screen_mode == 3; @@ -256,7 +267,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { fit(app, "view.fitArtboard"); } app.canvas.presented = presenting.then_some(rect); - app.canvas.opening_fit = if following { app.view().map(key) } else { None }; + app.canvas.opening_fit = if following { app.view().map(|v| view_key(v, rect)) } else { None }; // Before the canvas's own interaction, so the view the canvas draws is the one they moved to. if bars { scroll_bars(app, ui, rect, outer, &t); From ca7605fe20025fc053adaab8b686296c9df4180c Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 9 Nov 2026 01:41:03 +0700 Subject: [PATCH 4/4] Canvas: record the opening fit without a branch in the canvas frame --- crates/ui-egui/src/canvas.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 8081a40bf..19244247d 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -267,7 +267,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { fit(app, "view.fitArtboard"); } app.canvas.presented = presenting.then_some(rect); - app.canvas.opening_fit = if following { app.view().map(|v| view_key(v, rect)) } else { None }; + app.canvas.opening_fit = following.then(|| app.view().map(|v| view_key(v, rect))).flatten(); // Before the canvas's own interaction, so the view the canvas draws is the one they moved to. if bars { scroll_bars(app, ui, rect, outer, &t);