From 5d21ca879126e8c27d5f757d6b834b67b17cbb5c Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Fri, 6 Nov 2026 12:51:52 -0500 Subject: [PATCH] Minimize: a click that shows the right sidebar no longer moves the frame it selects (#2051) The view keeps its centre when the canvas is resized, so the right panel appearing (minimized UI, on the press that selects) shifted the pointer's page point by half the width over the zoom and the move handling took it as a drag. While a button that went down on the canvas is held, a resize now keeps the top-left instead. Co-Authored-By: Claude Fable 5.1 --- crates/ui-egui/src/canvas.rs | 11 +++ crates/ui-egui/src/panel_width_tests.rs | 96 +++++++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 5ccfe5d46..e1e78b53d 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -206,8 +206,19 @@ pub fn show(app: &mut FigApp, ui: &mut Ui, worker: &mut Worker) { { // Keep the centre; the first sizing after opening re-fits. let first = ds.view.vw == 1200.0 && ds.view.vh == 800.0; + // While a button is held that went down on the canvas (a click that selects shows the right panel, minimized, and a + // click that deselects hides it) the page point under the pointer stays where it is: the + // gesture's own pointer events would otherwise move by half the panel's width over the + // zoom, and the move handling would take that for a drag. Keep the top-left instead. + let held = resp.is_pointer_button_down_on() && !first; + let z = ds.view.zoom.max(1e-9); + let (dw, dh) = (f64::from(rect.width()) - ds.view.vw, f64::from(rect.height()) - ds.view.vh); ds.view.vw = f64::from(rect.width()); ds.view.vh = f64::from(rect.height()); + if held { + ds.view.cx += dw / 2.0 / z; + ds.view.cy += dh / 2.0 / z; + } if first { ds.zoom_to_fit(); } diff --git a/crates/ui-egui/src/panel_width_tests.rs b/crates/ui-egui/src/panel_width_tests.rs index 357b13669..c7453be24 100644 --- a/crates/ui-egui/src/panel_width_tests.rs +++ b/crates/ui-egui/src/panel_width_tests.rs @@ -346,3 +346,99 @@ fn a_narrow_window_shows_one_side_panel_at_a_time() { let wide = canvas_width(&mut app, vec2(1440.0, 900.0)); assert!(wide > 600.0 && wide < 1100.0, "a wide window shows both panels beside the canvas ({wide})"); } + +/// Five frames side by side at 46% zoom with the interface minimized and nothing selected. +fn minimized_row() -> (Ui, Vec) { + let mut s = Session::new(); + let mut ids = Vec::new(); + for k in 0..5 { + let r = s.execute("frame.create", &json!({"x": k * 470, "y": 0, "width": 400, "height": 600})).unwrap(); + ids.push(figcraft_engine::params::id_of(&r["id"]).unwrap()); + } + s.execute("select.set", &json!({"ids": []})).unwrap(); + let mut u = Ui::new(s); + u.app.run("view.minimize", json!({"on": true})); + u.app.run("view.set", json!({"zoom": 0.46, "cx": 1500, "cy": 300})); + for _ in 0..4 { + u.frame(vec![]); + } + (u, ids) +} + +fn frame_x(u: &Ui, id: figcraft_doc::NodeId) -> f64 { + u.app.session.doc().unwrap().world_bounds(id).unwrap().x0 +} + +/// A click on a frame while minimized selects it and shows the right panel (the canvas gets +/// narrower): the frame stays where it was, and no step is added to the history. +#[test] +fn a_click_that_shows_the_right_panel_does_not_move_the_frame() { + let (mut u, ids) = minimized_row(); + assert!(u.app.ui.right_panel_rect.is_none(), "nothing selected: no right panel"); + let wide = u.app.canvas.rect.width(); + let steps = u.app.session.ds().unwrap().history.undo.len(); + let p = u.app.to_screen(1880.0 + 200.0, 300.0); + u.frame(vec![Event::PointerMoved(p)]); + u.frame(vec![Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() }]); + u.frame(vec![]); + u.frame(vec![]); + u.frame(vec![Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() }]); + for _ in 0..4 { + u.frame(vec![]); + } + assert_eq!(u.app.session.selection(), vec![ids[4]], "the click selects the frame"); + assert!(u.app.ui.right_panel_rect.is_some(), "the right panel shows"); + assert!(u.app.canvas.rect.width() < wide - 100.0, "the canvas is narrower"); + assert_eq!(frame_x(&u, ids[4]), 1880.0, "a click did not move the frame"); + assert_eq!(u.app.session.ds().unwrap().history.undo.len(), steps, "no history step"); +} + +/// A real drag still moves the frame, by the pointer's screen movement over the zoom, in one step. +#[test] +fn a_drag_after_the_right_panel_shows_moves_the_frame_by_the_pointer() { + let (mut u, ids) = minimized_row(); + let steps = u.app.session.ds().unwrap().history.undo.len(); + let p = u.app.to_screen(1880.0 + 200.0, 300.0); + u.frame(vec![Event::PointerMoved(p)]); + u.frame(vec![Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() }]); + u.frame(vec![]); + u.frame(vec![Event::PointerMoved(p + vec2(50.0, 0.0))]); + u.frame(vec![Event::PointerMoved(p + vec2(100.0, 0.0))]); + u.frame(vec![Event::PointerButton { + pos: p + vec2(100.0, 0.0), + button: egui::PointerButton::Primary, + pressed: false, + modifiers: Default::default(), + }]); + for _ in 0..3 { + u.frame(vec![]); + } + assert!(u.app.ui.right_panel_rect.is_some(), "the right panel shows"); + let moved = frame_x(&u, ids[4]) - 1880.0; + assert!((moved - 100.0 / 0.46).abs() < 6.0, "moved by 100 points over the zoom, got {moved}"); + assert_eq!(u.app.session.ds().unwrap().history.undo.len(), steps + 1, "one step"); +} + +/// Clicking empty canvas deselects and hides the right panel again; the marquee that starts there +/// has no offset: nothing moves and nothing is selected. +#[test] +fn a_click_that_hides_the_right_panel_does_not_move_the_frame() { + let (mut u, ids) = minimized_row(); + u.app.run("select.set", json!({"ids": [ids[4]]})); + for _ in 0..3 { + u.frame(vec![]); + } + assert!(u.app.ui.right_panel_rect.is_some()); + let p = u.app.to_screen(1880.0 + 200.0, 1000.0); + let steps = u.app.session.ds().unwrap().history.undo.len(); + u.frame(vec![Event::PointerMoved(p)]); + u.frame(vec![Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() }]); + u.frame(vec![]); + u.frame(vec![Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() }]); + for _ in 0..3 { + u.frame(vec![]); + } + assert!(u.app.ui.right_panel_rect.is_none(), "deselected: no right panel"); + assert_eq!(frame_x(&u, ids[4]), 1880.0); + assert_eq!(u.app.session.ds().unwrap().history.undo.len(), steps); +}