mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-13 13:52:34 +00:00
Merge pull request #2103 from TrevorBrown-dev/fix/panel-widths-measured
Panels: the sidebars' width limits are Figma's (left 240 to 450, right 240 to 500)
This commit is contained in:
4 files changed
+34
-6
No files matched your search
@@ -22,7 +22,11 @@ impl FigApp {
|
||||
/// state.
|
||||
pub fn new(session: Session, services: Box<dyn Services>) -> Self {
|
||||
// The main window's panels open as wide as they were left.
|
||||
// Widths saved under other limits are clamped to the current ones, not rejected; one that
|
||||
// isn't finite falls back to the default.
|
||||
let (left_width, right_width) = session.prefs.panel_widths;
|
||||
let within = |w: f32, range: std::ops::RangeInclusive<f32>| if w.is_finite() { w.clamp(*range.start(), *range.end()) } else { PANEL_DEFAULT };
|
||||
let (left_width, right_width) = (within(left_width, LEFT_PANEL), within(right_width, RIGHT_PANEL));
|
||||
let pages_height = session.prefs.pages_height;
|
||||
Self {
|
||||
ui: UiState { left_width, right_width, pages_height, ..UiState::default() },
|
||||
|
||||
@@ -78,7 +78,15 @@ fn panels_fit_the_window() {
|
||||
((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), (480.0, 0.0), "the right panel closed (a board's): the left one keeps its width"),
|
||||
((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}");
|
||||
@@ -122,14 +130,17 @@ fn dragging_an_edge_sizes_its_panel_within_its_limits_and_keeps_the_views_centre
|
||||
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);
|
||||
@@ -187,6 +198,17 @@ fn the_widths_come_back_with_the_preferences() {
|
||||
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]
|
||||
@@ -264,7 +286,7 @@ fn minimize_collapses_the_rail_and_both_sidebars_and_the_right_one_follows_the_s
|
||||
|
||||
#[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)] {
|
||||
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();
|
||||
|
||||
@@ -27,10 +27,11 @@ pub enum TitleBar {
|
||||
pub type AutoSaveEncoding = std::thread::JoinHandle<(figcraft_engine::cmd::file::FileSnapshot, figcraft_engine::Result<Vec<u8>>)>;
|
||||
|
||||
/// The side panels' widths: the least and most a drag of their edge gives, what a double-click on
|
||||
/// the edge puts back, and how much of the window they leave the canvas.
|
||||
pub const LEFT_PANEL: std::ops::RangeInclusive<f32> = 200.0..=480.0;
|
||||
/// the edge puts back, and how much of the window they leave the canvas. The limits are Figma's,
|
||||
/// measured in the web app: left 240 to 450, right 240 to 500, both 240 by default.
|
||||
pub const LEFT_PANEL: std::ops::RangeInclusive<f32> = 240.0..=450.0;
|
||||
|
||||
pub const RIGHT_PANEL: std::ops::RangeInclusive<f32> = 240.0..=480.0;
|
||||
pub const RIGHT_PANEL: std::ops::RangeInclusive<f32> = 240.0..=500.0;
|
||||
|
||||
pub const PANEL_DEFAULT: f32 = 240.0;
|
||||
|
||||
|
||||
+2
-1
@@ -20,7 +20,7 @@ interaction feel not yet compared.
|
||||
| UI3 layout: tab strip as title bar, icon rail, file header, pages + layers, floating bottom toolbar with flyouts, Design / Prototype / Inspect panels | done | rail and tab strip icons 20 pt (#553); frameless window on Windows/Linux with drawn buttons; on a Wayland compositor that offers no server-side decorations (GNOME) the window gets winit's Adwaita controls instead (#592; checked by its author on GNOME, KDE not run) |
|
||||
| Light and dark themes | done | colours from `theme::Tokens`; a file or a page made while the dark theme shows gets a `#1E1E1E` page (`#F5F5F5` in the light theme), its own from then on: changing the theme repaints no page and opened files keep theirs; the file the app opens with takes the theme once it is known; the CLI, MCP and exports have no theme and keep `#F5F5F5`; text drawn with the Text tool is white on a dark page and black on a light one (so is text a command makes in the app without naming a fill; headless sessions keep black) (`theme_pages_tests`, `dark_page_text_tests`) |
|
||||
| Minimize UI (Shift+Cmd/Ctrl+\\, the Minimize button by the file name) and Show/Hide UI (Cmd/Ctrl+\\) | done | Minimize collapses the navigation bar and both sidebars and keeps the toolbar and the panel widths; "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" (Explore the navigation bar and left sidebar); a small Expand button at the top left of the canvas puts them back (FigCraft's own, the article names no control for it). Hide UI takes the toolbar too (`panel_width_tests`, `shortcuts_tests`; #1835) |
|
||||
| Resizable panels | done | each panel's edge drags its width (left 200–480, right 240–480, 240 by default; a double-click on the edge or View ▸ Panels ▸ Reset panel widths puts the default back); the widths are each window's own, the main window's are kept in the preferences when a drag ends and come back at the next start, a new window opens with those of the window it comes from; Minimize UI hides the panels and brings them back as they were; the view keeps its centre and zoom while a panel is resized; in a window too small for them the panels give way so the canvas keeps 160 px (the left one first), down to the smallest window (800×500) (`panel_width_tests`). Not compared with Figma's widths; no snap points |
|
||||
| Resizable panels | done | each panel's edge drags its width (left 240–450, right 240–500, 240 by default, as measured in Figma's web app on 2026-11-08; a double-click on the edge or View ▸ Panels ▸ Reset panel widths puts the default back); the widths are each window's own, the main window's are kept in the preferences when a drag ends and come back at the next start (a width saved under other limits is clamped to the current ones on load, and one that isn't a number, NaN or infinite, falls back to 240), a new window opens with those of the window it comes from; Minimize UI hides the panels and brings them back as they were; the view keeps its centre and zoom while a panel is resized; in a window too small for them the panels give way so the canvas keeps 160 px (the left one first), down to the smallest window (800×500) (`panel_width_tests`). The double-click reset to 240 matches Figma's; no snap points |
|
||||
| Design panel's sections on one grid | done | every section lays its rows out on the same grid: the panel is 240 pt, its content 16 pt in from the left and 8 pt from the right, two 88 pt columns at that width with 8 pt gutters and a 24 pt column for the buttons at a row's end (the columns are flex, so they share the panel when it is dragged wider); rows 24 pt in a 32 pt track, 10 pt captions over them; section titles 11 pt semibold and a row's values 11 pt, their buttons 24 pt and 4 pt apart. The panel's edge is drawn inside it: a frame stroke counts as margin and took 1 pt off each side, putting every right margin 1–2 pt out (checked in the app on Windows over the control channel) |
|
||||
| Actions palette (Cmd+K, Cmd+/) | done | tabs: All (every command, grouped by menu, or Other; a command that needs parameters only through its menu item, and none twice under one label, #1574), Assets (components and styles) and Plugins & widgets; results ranked prefix first, then shortest, at most 60 rows; Up/Down move a highlight and Enter runs it; Recents list what was run from the palette (matching translated labels); one open/toggle path from every entry point (`palette_tests`). Ranking and the Assets results not compared with Figma's |
|
||||
| Context menus | done | canvas and layers; submenus as in the main menu below, and a right-click in a new spot starts a fresh menu (Select layer closed); Shift+F10 or the keyboard's Menu key opens them from the keyboard (at the focused layer row or below the canvas selection) and the menu takes the keyboard (`layers_keyboard_tests`) |
|
||||
@@ -218,6 +218,7 @@ This audit records candidates for subsequent fixes; their behaviour is unchanged
|
||||
| 2026-11-11 | patch | M5, gap 4: marquee selection as measured in Figma's web app (Select layers and objects, Selection marquee): a partly covered top-level frame or section gives the layers in it the marquee touches (it was selected only when enclosed, so nothing came), an empty frame is selected on touch, a rotated shape is touched by its outline and not its box, a drag from inside a frame selects that frame only when the marquee also takes another top-level frame (then whole), and also takes the top-level layers it touches outside it, and selecting a frame whole keeps the marquee at the top level (a partly covered frame then comes whole, not its layers). Flipped: `marquee_selects_what_it_touches`, `cmd_marquee_selects_nested_layers`, `a_box_selects_like_a_marquee`. Tests: `marquee_behaviour_tests`. Counts unchanged |
|
||||
| 2026-11-08 | minor | Vector edit mode, #1875, measured in Figma (web, real keys): Copy with segments selected takes only those (with just a point selected, the segments touching it) and a paste outside edit mode is a new vector layer in place; Cut removes them, the path splitting into subpaths in the same layer with edit mode staying on, and a paste outside gives a new layer with the cut segments; Paste while editing a vector adds the clipboard's segments, or a copied layer's outline, to the same network in place, selecting the pasted points, with no new layer and the bounds growing. FigCraft copied the whole layer, Cut left nothing to paste and Paste did nothing. `edit.copy`, `edit.cut` and `edit.paste` act so with no `ids`/`parent` given (Ctrl+X is the real Cut again; Shift+Delete still heals); the copy carries the layer's look (fills, strokes, effects, opacity, blend; the width profile only when every segment is copied); layers with no outline are skipped. Tests: `vector_clipboard_tests` |
|
||||
| 2026-11-08 | patch | Selection: Escape with layers selected clears the selection instead of stepping out to the parent first. Measured in the Figma web app with a rectangle in a group in a frame, selected from the canvas, from the Layers panel and after Tab: one Escape selects nothing. Editing, gestures and the pen still end first |
|
||||
| 2026-11-08 | patch | Panels: the sidebars' width limits are Figma's, measured in its web app (left 240 to 450, right 240 to 500, both 240 by default, a double-click on the edge resets), where FigCraft had left 200 to 480 and right 240 to 480; widths saved under the old limits are clamped on load |
|
||||
| 2026-11-07 | patch | Main menu: a submenu opens on the first frame its row is hovered instead of after 0.15 s (Figma's main menu measured with synthetic pointer events and 1 ms polling: median 11 ms to open, 13 ms to switch rows, no hover delay); the safe-triangle aim is unchanged, though the same measurement saw no aim logic in Figma, left for the maintainer to decide |
|
||||
| 2026-11-10 | patch | M5, #1560: locked layers follow Figma's rules (Lock and unlock layers; measured in Figma's web app): a locked layer selected in the Layers panel ignores the arrow keys and the nudge commands while its properties still edit, and a child of a locked layer keeps its padlock inert (the Layers panel padlock, Lock/Unlock and `object.toggleLock` without a value no longer lock the child on its own) |
|
||||
| 2026-11-10 | patch | #2217: the main menu's submenus (Object, say) ran past the window's bottom, behind the taskbar, with the lowest commands out of reach, because submenus were not constrained to the window (to keep them beside their row). Every menu panel now scrolls when taller than the window less 8 pt margins, and a submenu is held within the window's height (moved up, never above the top) while keeping its place beside its row, so menu-aim is unchanged. Tests: `menu_overflow_tests.rs`. Numbers unchanged. |
|
||||
|
||||
Reference in new issue
Block a user