Files
figcraft/crates/ui-egui/src/panel_width_tests.rs
T
Trevor BrownandClaude Sonnet 5.5 ad7a886e3c Panels: the sidebars' width limits are Figma's (left 240 to 450, right 240 to 500)
Measured in Figma's web app: left sidebar 240 default, 240 min, 450 max; right
240 default, 240 min, 500 max; a double-click on the edge resets. FigCraft had
left 200 to 480 and right 240 to 480. Widths saved under the old limits are now
clamped when the app starts, not only when drawn. The narrow-window give-way
(canvas keeps 160 px) is unchanged, with the new 240 floor.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at medium
2026-11-07 23:17:00 -05:00

498 lines
24 KiB
Rust

//! The side panels' widths, driven through egui headless: dragging a panel's edge sizes it within
//! its limits, a double-click puts back the default, the widths are kept in the preferences (when
//! the drag ends) and come back with them, the view's centre stays where it was, Minimize UI
//! hides the panels without losing their widths, and a window at its smallest size still has a
//! canvas.
use std::sync::{Arc, Mutex};
use egui::{Event, Pos2, RawInput, Rect, pos2, vec2};
use figcraft_engine::{Prefs, Session};
use serde_json::json;
use crate::layers_drag_tests::Ui;
use crate::{CANVAS_MIN, FigApp, LEFT_PANEL, NoServices, PANEL_DEFAULT, RIGHT_PANEL, Services, fit_panels};
/// The panel widths in the preferences each time they were kept.
type Kept = Arc<Mutex<Vec<(f32, f32)>>>;
/// Services that note the preferences each time they are kept.
struct Keeps(Kept);
impl Services for Keeps {
fn save_prefs(&mut self, prefs: &Prefs) {
self.0.lock().unwrap().push(prefs.panel_widths);
}
}
fn ui() -> (Ui, Kept) {
let mut s = Session::new();
s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 300, "height": 200})).unwrap();
let mut u = Ui::new(s);
let kept = Kept::default();
u.app.services = Box::new(Keeps(kept.clone()));
u.frame(vec![]);
(u, kept)
}
/// The x of the left panel's edge (the canvas's left) and of the right panel's (its right).
fn edges(u: &Ui) -> (f32, f32) {
(u.app.canvas.rect.min.x, u.app.canvas.rect.max.x)
}
fn widths(u: &Ui) -> (f32, f32) {
(u.app.ui.left_width, u.app.ui.right_width)
}
/// Press on `from`, move to `to` in steps and let go.
fn drag(u: &mut Ui, from: Pos2, to: Pos2) {
u.drag_to(from, to);
u.button(to, false);
u.frame(vec![]);
}
fn double_click(u: &mut Ui, p: Pos2) {
// Well after any earlier click (a third quick click, within twice the platform's 0.5 s
// double-click window, isn't a double-click).
for _ in 0..70 {
u.frame(vec![]);
}
for _ in 0..2 {
u.button(p, true);
u.button(p, false);
}
u.frame(vec![]);
}
#[test]
fn panels_fit_the_window() {
let (l0, l1, r0, r1) = (*LEFT_PANEL.start(), *LEFT_PANEL.end(), *RIGHT_PANEL.start(), *RIGHT_PANEL.end());
// (room beside the icon rail, left asked (none: closed), right asked) → (left, right) drawn
let rows = [
((1384.0, Some(240.0), Some(240.0)), (240.0, 240.0), "as asked in a wide window"),
((1384.0, Some(100.0), Some(100.0)), (l0, r0), "never narrower than their least"),
((1384.0, Some(900.0), Some(900.0)), (l1, r1), "never wider than their most"),
((1384.0, None, Some(300.0)), (0.0, 300.0), "the left panel closed"),
((744.0, Some(240.0), Some(240.0)), (240.0, 240.0), "the smallest window with the default widths"),
((744.0, Some(480.0), Some(480.0)), (l0, 744.0 - l0 - CANVAS_MIN), "the smallest window: the left panel gives way first, then the right"),
((744.0, Some(480.0), Some(240.0)), (744.0 - 240.0 - CANVAS_MIN, 240.0), "the left panel gives the canvas its room"),
((744.0, None, Some(480.0)), (0.0, 480.0), "with the left panel closed the right one has room"),
((300.0, Some(240.0), Some(240.0)), (l0, r0), "a window smaller than any layout: the least widths"),
((1384.0, Some(f32::NAN), Some(f32::INFINITY)), (PANEL_DEFAULT, PANEL_DEFAULT), "junk widths: the default"),
((744.0, Some(480.0), None), (l1, 0.0), "the right panel closed (a board's): the left one keeps as much of its width as it may"),
((744.0, Some(450.0), None), (450.0, 0.0), "the right panel closed: the left one at its most"),
((1384.0, Some(200.0), Some(480.0)), (l0, 480.0), "a stored left width under 240 (the old limit was 200) is brought up to 240"),
((1384.0, Some(470.0), Some(520.0)), (l1, r1), "stored widths over the limits (the old ones were 480) are brought down to 450 and 500"),
(
(744.0, Some(300.0), Some(500.0)),
(240.0, 344.0),
"the smallest window with the right panel at its most: the right gives way last, the left to its 240 floor",
),
];
for ((avail, left, right), want, why) in rows {
assert_eq!(fit_panels(avail, left, right), want, "{why}");
}
// Whatever is asked, a window of the smallest size or more keeps its canvas.
for avail in [744.0, 900.0, 1384.0] {
for left in [None, Some(l0), Some(300.0), Some(l1)] {
for right in [r0, 333.0, r1] {
let (l, r) = fit_panels(avail, left, Some(right));
assert!(avail - l - r >= CANVAS_MIN, "{avail} {left:?} {right}: canvas {}", avail - l - r);
}
}
}
}
#[test]
fn dragging_an_edge_sizes_its_panel_within_its_limits_and_keeps_the_views_centre() {
let (mut u, kept) = ui();
assert_eq!(widths(&u), (PANEL_DEFAULT, PANEL_DEFAULT));
let (left, right) = edges(&u);
let centre = {
let v = &u.app.session.ds().unwrap().view;
(v.cx, v.cy, v.zoom)
};
// The left panel's edge, 60 to the right: 60 wider, and the canvas starts 60 further on.
drag(&mut u, pos2(left, 400.0), pos2(left + 60.0, 400.0));
assert_eq!(widths(&u), (300.0, 240.0));
assert_eq!(edges(&u), (left + 60.0, right));
// The right panel's edge, 40 to the left: 40 wider.
drag(&mut u, pos2(right, 400.0), pos2(right - 40.0, 400.0));
assert_eq!(widths(&u), (300.0, 280.0));
assert_eq!(edges(&u), (left + 60.0, right - 40.0));
// The view keeps its centre and zoom: the page doesn't jump, the canvas is just narrower.
let v = u.app.session.ds().unwrap().view.clone();
assert_eq!((v.cx, v.cy, v.zoom), centre);
assert_eq!(v.vw, f64::from(right - left - 100.0));
// Kept in the preferences, once per drag (not on every move).
assert_eq!(u.app.session.prefs.panel_widths, (300.0, 280.0));
assert_eq!(*kept.lock().unwrap(), [(300.0, 240.0), (300.0, 280.0)]);
// Past the limits the edge stops, and comes back with the pointer.
let (left, right) = edges(&u);
drag(&mut u, pos2(left, 300.0), pos2(left + 700.0, 300.0));
assert_eq!(widths(&u).0, *LEFT_PANEL.end());
assert_eq!(widths(&u).0, 450.0, "Figma's left sidebar stops at 450");
let (left, _) = edges(&u);
drag(&mut u, pos2(left, 300.0), pos2(20.0, 300.0));
assert_eq!(widths(&u).0, *LEFT_PANEL.start());
assert_eq!(widths(&u).0, 240.0, "and at 240 the other way");
drag(&mut u, pos2(right, 300.0), pos2(right + 500.0, 300.0));
assert_eq!(widths(&u).1, *RIGHT_PANEL.start());
let (_, right) = edges(&u);
u.drag_to(pos2(right, 300.0), pos2(right - 900.0, 300.0));
assert_eq!(widths(&u).1, *RIGHT_PANEL.end(), "at its most while the pointer is past it");
assert_eq!(widths(&u).1, 500.0, "Figma's right sidebar stops at 500");
u.frame(vec![Event::PointerMoved(pos2(right - 100.0, 300.0))]);
assert_eq!(widths(&u).1, *RIGHT_PANEL.start() + 100.0, "back with the pointer");
u.button(pos2(right - 100.0, 300.0), false);
}
#[test]
fn a_double_click_on_an_edge_puts_back_the_default_width() {
let (mut u, kept) = ui();
let (left, right) = edges(&u);
drag(&mut u, pos2(left, 400.0), pos2(left + 90.0, 400.0));
drag(&mut u, pos2(right, 400.0), pos2(right - 90.0, 400.0));
assert_eq!(widths(&u), (330.0, 330.0));
let (left, right) = edges(&u);
double_click(&mut u, pos2(left, 500.0));
assert_eq!(widths(&u), (PANEL_DEFAULT, 330.0), "the left panel alone");
double_click(&mut u, pos2(right, 500.0));
assert_eq!(widths(&u), (PANEL_DEFAULT, PANEL_DEFAULT));
assert_eq!(kept.lock().unwrap().last(), Some(&(PANEL_DEFAULT, PANEL_DEFAULT)), "kept");
// A click on the canvas beside the edge is the canvas's.
let (left, _) = edges(&u);
drag(&mut u, pos2(left + 40.0, 500.0), pos2(left + 90.0, 520.0));
assert_eq!(widths(&u), (PANEL_DEFAULT, PANEL_DEFAULT));
}
#[test]
fn the_command_sets_and_resets_the_widths() {
let (mut u, kept) = ui();
u.app.run("view.panelWidths", json!({"left": 320, "right": 400}));
u.frame(vec![]);
assert_eq!(widths(&u), (320.0, 400.0));
assert_eq!(u.app.inspect()["panels"], json!({"left": 320.0, "right": 400.0, "leftCollapsed": false, "minimized": false}));
u.app.run("view.panelWidths", json!({"left": 5000}));
assert_eq!(widths(&u), (*LEFT_PANEL.end(), 400.0), "within its limits; the other panel as it was");
for junk in [json!({"left": "wide"}), json!({"left": null, "right": null}), json!({})] {
u.app.run("view.panelWidths", junk.clone());
assert_eq!(widths(&u), (PANEL_DEFAULT, PANEL_DEFAULT), "{junk}: both back to the default");
u.app.run("view.panelWidths", json!({"left": 300, "right": 300}));
}
assert_eq!(kept.lock().unwrap().last(), Some(&(300.0, 300.0)));
}
#[test]
fn the_widths_come_back_with_the_preferences() {
let mut s = Session::new();
s.prefs.panel_widths = (360.0, 300.0);
let mut u = Ui::new(s);
u.frame(vec![]);
assert_eq!(widths(&u), (360.0, 300.0));
let (left, right) = edges(&u);
assert_eq!(right - left, 1440.0 - 56.0 - 360.0 - 300.0, "the canvas between them (the icon rail is 56 wide)");
// Widths from a damaged file are brought within the limits when drawn.
let mut s = Session::new();
s.prefs.panel_widths = (-5.0, f32::NAN);
let mut u = Ui::new(s);
u.frame(vec![]);
let (left, right) = edges(&u);
assert_eq!((left, 1440.0 - right), (56.0 + *LEFT_PANEL.start(), PANEL_DEFAULT));
// Widths saved under the old limits (left 200 to 480, right up to 480) or over the new ones
// are clamped when loaded, not rejected.
for (stored, want) in
[((200.0, 240.0), (240.0, 240.0)), ((239.0, 500.0), (240.0, 500.0)), ((451.0, 480.0), (450.0, 480.0)), ((480.0, 600.0), (450.0, 500.0))]
{
let mut s = Session::new();
s.prefs.panel_widths = stored;
let mut u = Ui::new(s);
u.frame(vec![]);
assert_eq!(widths(&u), want, "stored {stored:?}");
}
}
#[test]
fn minimize_ui_hides_the_panels_and_keeps_their_widths() {
let (mut u, _) = ui();
u.app.run("view.panelWidths", json!({"left": 320, "right": 400}));
u.frame(vec![]);
let shown = edges(&u);
u.app.run("view.toggleUi", json!({}));
u.frame(vec![]);
assert_eq!(edges(&u), (0.0, 1440.0), "the canvas has the window");
// No edge to drag where the panels were.
drag(&mut u, pos2(shown.0, 400.0), pos2(shown.0 + 80.0, 400.0));
assert_eq!(widths(&u), (320.0, 400.0));
u.app.run("view.toggleUi", json!({}));
u.frame(vec![]);
assert_eq!((edges(&u), widths(&u)), (shown, (320.0, 400.0)), "back as they were");
// The left sidebar alone: its edge goes with it, the right panel stays.
u.app.run("view.leftSidebar", json!({}));
u.frame(vec![]);
assert_eq!(edges(&u), (56.0, shown.1));
}
/// The middle of the accessible control with `role` and `label`, if one shows.
pub(crate) fn control(u: &mut Ui, role: egui::accesskit::Role, label: &str) -> Option<Pos2> {
u.ctx.enable_accesskit();
u.frame(vec![]);
let out = u.frame_output(vec![]);
let nodes = out.platform_output.accesskit_update.expect("an AccessKit update").nodes;
let n = nodes.iter().map(|(_, n)| n).find(|n| n.role() == role && n.label() == Some(label))?;
let b = n.bounds()?;
Some(pos2(((b.x0 + b.x1) / 2.0) as f32, ((b.y0 + b.y1) / 2.0) as f32))
}
/// "You can click Minimize next to the file name to collapse the navigation bar, left sidebar,
/// and right sidebar" (Explore the navigation bar and left sidebar; Shift+Cmd/Ctrl+\ too): the
/// rail and both sidebars go, the toolbar and the widths stay. "If you select an object while
/// the panels are minimized, the left sidebar remains minimized while the right sidebar expands
/// [...] Once all objects are deselected, the right panel minimizes again." The Expand button at
/// the top left puts everything back (#1835).
#[test]
fn minimize_collapses_the_rail_and_both_sidebars_and_the_right_one_follows_the_selection() {
use egui::accesskit::Role;
let (mut u, _) = ui();
u.app.run("view.panelWidths", json!({"left": 320, "right": 400}));
u.app.run("select.set", json!({"ids": []}));
u.frame(vec![]);
let shown = edges(&u);
let minimize = control(&mut u, Role::Button, "Minimize").expect("the Minimize button by the file name");
u.button(minimize, true);
u.button(minimize, false);
u.frame(vec![]);
assert_eq!(edges(&u), (0.0, 1440.0), "no rail and no sidebars: the canvas has the window");
assert!(control(&mut u, Role::Toolbar, "Tools").is_some(), "the toolbar stays");
assert_eq!(widths(&u), (320.0, 400.0), "the widths are kept");
// A selection brings the right sidebar, the left stays away; deselecting takes it again.
u.app.run("select.all", json!({}));
u.frame(vec![]);
assert!(edges(&u).0 == 0.0 && edges(&u).1 < 1440.0, "the right sidebar shows for the selection: {:?}", edges(&u));
u.app.run("select.set", json!({"ids": []}));
u.frame(vec![]);
assert_eq!(edges(&u), (0.0, 1440.0), "and goes once nothing is selected");
// Expand puts the interface back as it was.
let expand = control(&mut u, Role::Button, "Expand the interface").expect("the Expand button at the top left");
u.button(expand, true);
u.button(expand, false);
u.frame(vec![]);
assert_eq!((edges(&u), widths(&u)), (shown, (320.0, 400.0)), "back as it was");
assert!(control(&mut u, Role::Button, "Expand the interface").is_none());
// Hide UI hides the toolbar as well, as before.
u.app.run("view.toggleUi", json!({}));
assert!(control(&mut u, Role::Toolbar, "Tools").is_none(), "Hide UI takes the toolbar too");
u.app.run("view.toggleUi", json!({}));
}
#[test]
fn the_smallest_window_keeps_its_canvas_whatever_the_widths() {
for asked in [(240.0, 240.0), (450.0, 500.0), (450.0, 240.0), (240.0, 500.0)] {
let mut s = Session::new();
s.prefs.panel_widths = asked;
let ctx = egui::Context::default();
let mut app = FigApp::new(s, Box::new(NoServices));
// The desktop window's minimum inner size.
let screen = Rect::from_min_size(Pos2::ZERO, vec2(800.0, 500.0));
for k in 0..5 {
let input = RawInput { screen_rect: Some(screen), time: Some(f64::from(k) / 60.0), ..Default::default() };
ctx.run_ui(input, |ui| app.ui(ui)).textures_delta.clear();
}
let canvas = app.canvas.rect;
assert!(canvas.width() >= CANVAS_MIN, "{asked:?}: the canvas is {} wide", canvas.width());
assert!(screen.contains_rect(canvas), "{asked:?}: the canvas {canvas:?} is in the window");
assert!(canvas.height() > 300.0, "{asked:?}: and has its height");
assert_eq!((app.ui.left_width, app.ui.right_width), asked, "the widths asked for come back in a wider window");
}
}
/// Minimize takes nothing from a phone-sized window, which has no sidebars beside the canvas: its
/// panels open from their buttons as before, and the interface is minimized again once the window
/// is wide.
#[test]
fn a_narrow_window_keeps_its_panel_buttons_while_minimized() {
use crate::MobilePanel;
let ctx = egui::Context::default();
let mut app = FigApp::new(Session::new(), Box::new(NoServices));
let mut k = 0.0;
let mut canvas_width = |app: &mut FigApp, size: egui::Vec2| {
for _ in 0..4 {
k += 1.0 / 60.0;
let input = RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, size)), time: Some(k), ..Default::default() };
ctx.run_ui(input, |ui| app.ui(ui)).textures_delta.clear();
}
app.canvas.rect.width()
};
let phone = vec2(390.0, 844.0);
app.run("view.minimize", json!({"on": true}));
assert!(canvas_width(&mut app, phone) >= 380.0, "closed panels leave the whole width");
app.ui.mobile_panel = Some(MobilePanel::Left);
let w = canvas_width(&mut app, phone);
assert!((1.0..=60.0).contains(&w), "the Layers button opens the left panel while minimized: {w}");
app.ui.mobile_panel = None;
let wide = canvas_width(&mut app, vec2(1440.0, 900.0));
assert!(wide >= 1439.0, "wide again, the interface is minimized: the canvas has the window ({wide})");
app.run("view.minimize", json!({"on": false}));
let wide = canvas_width(&mut app, vec2(1440.0, 900.0));
assert!(wide > 600.0 && wide < 1100.0, "expanded, both panels are beside the canvas again ({wide})");
}
/// A phone-sized window: the canvas has the whole width, and each side panel opens alone over it,
/// leaving the strip with the panel buttons. A wide window ignores the choice.
#[test]
fn a_narrow_window_shows_one_side_panel_at_a_time() {
use crate::MobilePanel;
let ctx = egui::Context::default();
let mut app = FigApp::new(Session::new(), Box::new(NoServices));
let mut k = 0.0;
let mut canvas_width = |app: &mut FigApp, size: egui::Vec2| {
for _ in 0..4 {
k += 1.0 / 60.0;
let input = RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, size)), time: Some(k), ..Default::default() };
ctx.run_ui(input, |ui| app.ui(ui)).textures_delta.clear();
}
app.canvas.rect.width()
};
let phone = vec2(390.0, 844.0);
assert!(app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 300, "height": 200})).is_ok());
assert!(canvas_width(&mut app, phone) >= 380.0, "closed panels leave the whole width");
for side in [MobilePanel::Left, MobilePanel::Right] {
app.ui.mobile_panel = Some(side);
let w = canvas_width(&mut app, phone);
assert!((1.0..=60.0).contains(&w), "{side:?}: the open panel leaves {w}");
}
// A command that opens the left panel opens it on a phone too, in place of the right one.
app.run("view.assets", json!({}));
let w = canvas_width(&mut app, phone);
assert!((1.0..=60.0).contains(&w) && app.ui.mobile_panel == Some(MobilePanel::Left), "view.assets leaves {w}");
app.ui.mobile_panel = Some(MobilePanel::Right);
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);
}
/// The left sidebar and rail appearing while a press is held (Minimize toggled off mid-press)
/// moves the canvas's left edge: a still pointer keeps its page point, so the release moves nothing.
#[test]
fn a_held_press_keeps_its_page_point_when_the_canvas_moves() {
let (mut u, ids) = minimized_row();
u.app.run("select.set", json!({"ids": [ids[3]]}));
for _ in 0..3 {
u.frame(vec![]);
}
let steps = u.app.session.ds().unwrap().history.undo.len();
let left = u.app.canvas.rect.min.x;
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![]);
let before = u.app.to_page(p);
u.app.run("view.minimize", json!({"on": false}));
u.frame(vec![]);
u.frame(vec![]);
assert!(u.app.canvas.rect.min.x > left + 100.0, "the left sidebar and rail moved the canvas's edge");
let after = u.app.to_page(p);
assert!((after.x - before.x).abs() < 0.5 && (after.y - before.y).abs() < 0.5, "the pointer's page point moved: {before:?} to {after:?}");
u.frame(vec![Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() }]);
for _ in 0..3 {
u.frame(vec![]);
}
assert_eq!(frame_x(&u, ids[4]), 1880.0, "the frame did not move");
assert_eq!(frame_x(&u, ids[3]), 1410.0);
assert_eq!(u.app.session.ds().unwrap().history.undo.len(), steps, "no history step");
}