Merge pull request #6196 from KaboomFox/fix/opening-fit-settles

Canvas: a document opens at the zoom of the settled window
This commit is contained in:
BFlat
2026-11-09 03:20:38 +08:00
committed by GitHub
4 changed files with 96 additions and 1 deletions
+1
View File
@@ -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),
},
+4
View File
@@ -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<egui::Rect>,
/// 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<canvas::ViewKey>,
/// 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.
+37 -1
View File
@@ -186,6 +186,36 @@ 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 { .. }
)
})
}
/// 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());
@@ -225,13 +255,19 @@ 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 (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;
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 = 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);
+54
View File
@@ -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<egui::Event>) -> 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]