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 <noreply@anthropic.com>
This commit is contained in:
Trevor BrownandClaude Fable 5.1 committed 2026-11-06 12:51:52 -05:00
1 parent 0b97f3f180
commit 5d21ca8791
2 files changed
+107

No files matched your search

+11
View File
@@ -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();
}
+96
View File
@@ -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<figcraft_doc::NodeId>) {
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);
}