mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-29 21:06:50 -06:00
- Cursors: the engine names a cursor for every tool, modifier and hover target (rotate turned to its corner, pen states, eyedropper, comment, copy on Alt, not-allowed while an instance refuses the edit); the UI shows the system's or draws its own; guides and handles keep their cursors; ui.inspect reports it. - Panels: each edge drags its panel within limits, a double-click resets, widths are per window and kept in the preferences (view.panelWidths); a small window keeps its canvas. - Outline mode: Include hidden layers and Include object bounds (view.outlinesHiddenLayers, view.outlinesObjectBounds), drawn and clickable; light lines on a dark page; check marks in the View menu. - A file or page made in the dark theme has a #1E1E1E page (sessions without an interface keep #F5F5F5); text drawn on a dark page is white. - Tests for Enter and Escape on a selected text.
231 lines
10 KiB
Rust
231 lines
10 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 isn't a double-click).
|
|
for _ in 0..40 {
|
|
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), 240.0), (240.0, 240.0), "as asked in a wide window"),
|
|
((1384.0, Some(100.0), 100.0), (l0, r0), "never narrower than their least"),
|
|
((1384.0, Some(900.0), 900.0), (l1, r1), "never wider than their most"),
|
|
((1384.0, None, 300.0), (0.0, 300.0), "the left panel closed"),
|
|
((744.0, Some(240.0), 240.0), (240.0, 240.0), "the smallest window with the default widths"),
|
|
((744.0, Some(480.0), 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), 240.0), (744.0 - 240.0 - CANVAS_MIN, 240.0), "the left panel gives the canvas its room"),
|
|
((744.0, None, 480.0), (0.0, 480.0), "with the left panel closed the right one has room"),
|
|
((300.0, Some(240.0), 240.0), (l0, r0), "a window smaller than any layout: the least widths"),
|
|
((1384.0, Some(f32::NAN), f32::INFINITY), (PANEL_DEFAULT, PANEL_DEFAULT), "junk widths: the default"),
|
|
];
|
|
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, 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());
|
|
let (left, _) = edges(&u);
|
|
drag(&mut u, pos2(left, 300.0), pos2(20.0, 300.0));
|
|
assert_eq!(widths(&u).0, *LEFT_PANEL.start());
|
|
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");
|
|
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}));
|
|
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));
|
|
}
|
|
|
|
#[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));
|
|
}
|
|
|
|
#[test]
|
|
fn the_smallest_window_keeps_its_canvas_whatever_the_widths() {
|
|
for asked in [(240.0, 240.0), (480.0, 480.0), (480.0, 240.0), (200.0, 480.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");
|
|
}
|
|
}
|