mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-13 16:34:55 +00:00
Merge pull request #2245 from TrevorBrown-dev/fix/grid-fill-tracks-ignore-fixed-children
M2.1: grid Fill tracks share the space and let a fixed child overflow, as measured in Figma
This commit is contained in:
@@ -674,27 +674,48 @@ fn grid_rules() {
|
||||
rects: &[(0.0, 0.0, 60.0, 20.0), (60.0, 0.0, 240.0, 20.0)],
|
||||
},
|
||||
Case {
|
||||
rule: "fr tracks behave like the CSS fr unit: a fixed child taller than its share keeps its row",
|
||||
// Share 50 < 80: row 1 keeps 80, row 2 gets the remaining 20.
|
||||
rule: "fr tracks as measured in Figma: a fixed child taller than its share keeps its row's share and overflows",
|
||||
// Share 50 < 80: rows stay 50 / 50, child 1 overflows into row 2.
|
||||
frame: Frame { mode: Grid, size: (100.0, 100.0), columns: &[fr(1.0)], rows: &[fr(1.0), fr(1.0)], ..FRAME },
|
||||
kids: &[kid(20.0, 80.0), Kid { sizing: (Fixed, Fill), ..KID }],
|
||||
want: (100.0, 100.0),
|
||||
rects: &[(0.0, 0.0, 20.0, 80.0), (0.0, 80.0, 20.0, 20.0)],
|
||||
rects: &[(0.0, 0.0, 20.0, 80.0), (0.0, 50.0, 20.0, 50.0)],
|
||||
},
|
||||
Case {
|
||||
rule: "fr tracks: children bigger than every share keep their sizes (the grid overflows)",
|
||||
rule: "fr tracks as measured in Figma: children bigger than every share keep their sizes (the grid overflows)",
|
||||
frame: Frame { mode: Grid, size: (100.0, 100.0), columns: &[fr(1.0)], rows: &[fr(1.0), fr(1.0)], ..FRAME },
|
||||
kids: &[kid(20.0, 80.0), kid(20.0, 70.0)],
|
||||
want: (100.0, 100.0),
|
||||
rects: &[(0.0, 0.0, 20.0, 80.0), (0.0, 80.0, 20.0, 70.0)],
|
||||
rects: &[(0.0, 0.0, 20.0, 80.0), (0.0, 50.0, 20.0, 70.0)],
|
||||
},
|
||||
Case {
|
||||
rule: "fr columns: a fixed child wider than its share keeps its column",
|
||||
// Share 100 < 150: column 1 keeps 150; column 2 (2fr alone) gets the other 150.
|
||||
rule: "fr columns as measured in Figma: a fixed child wider than its share overflows its column",
|
||||
// Share 100 < 150: columns stay 100 / 200.
|
||||
frame: Frame { mode: Grid, size: (300.0, 20.0), columns: &[fr(1.0), fr(2.0)], ..FRAME },
|
||||
kids: &[kid(150.0, 20.0), Kid { sizing: (Fill, Fixed), ..KID }],
|
||||
want: (300.0, 20.0),
|
||||
rects: &[(0.0, 0.0, 150.0, 20.0), (150.0, 0.0, 150.0, 20.0)],
|
||||
rects: &[(0.0, 0.0, 150.0, 20.0), (100.0, 0.0, 200.0, 20.0)],
|
||||
},
|
||||
Case {
|
||||
rule: "fr rows as measured in Figma: 300 x 200, 1fr / 1fr, a 150-tall child does not widen its row",
|
||||
frame: Frame { mode: Grid, size: (300.0, 200.0), columns: &[fr(1.0)], rows: &[fr(1.0), fr(1.0)], ..FRAME },
|
||||
kids: &[kid(100.0, 150.0), kid(100.0, 20.0)],
|
||||
want: (300.0, 200.0),
|
||||
rects: &[(0.0, 0.0, 100.0, 150.0), (0.0, 100.0, 100.0, 20.0)],
|
||||
},
|
||||
Case {
|
||||
rule: "fr rows as measured in Figma: the row gap comes off first (95 / 95, second child at 105)",
|
||||
frame: Frame { mode: Grid, size: (300.0, 200.0), cross_gap: 10.0, columns: &[fr(1.0)], rows: &[fr(1.0), fr(1.0)], ..FRAME },
|
||||
kids: &[kid(100.0, 150.0), kid(100.0, 20.0)],
|
||||
want: (300.0, 200.0),
|
||||
rects: &[(0.0, 0.0, 100.0, 150.0), (0.0, 105.0, 100.0, 20.0)],
|
||||
},
|
||||
Case {
|
||||
rule: "fr rows as measured in Figma: with the frame on Hug the rows size to content (150 / 20, frame 170)",
|
||||
frame: Frame { mode: Grid, size: (300.0, 200.0), sizing: (Fixed, Hug), columns: &[fr(1.0)], rows: &[fr(1.0), fr(1.0)], ..FRAME },
|
||||
kids: &[kid(100.0, 150.0), kid(100.0, 20.0)],
|
||||
want: (300.0, 170.0),
|
||||
rects: &[(0.0, 0.0, 100.0, 150.0), (0.0, 150.0, 100.0, 20.0)],
|
||||
},
|
||||
Case {
|
||||
rule: "placement: left to right, top to bottom; rows grow to fit the children",
|
||||
|
||||
@@ -794,9 +794,9 @@ fn track_spec(specs: &[figcraft_doc::GridTrack], i: usize) -> (Sizing, f64) {
|
||||
}
|
||||
|
||||
/// Resolve track sizes: fixed tracks keep their size, hug tracks fit their content, fill tracks
|
||||
/// share what is left by their fractional units like the CSS `fr` unit (a track never gets less
|
||||
/// than its content: one whose share is too small keeps its content size and the others share the
|
||||
/// rest). `avail` is None when the frame hugs that axis (fill tracks then hug their content too).
|
||||
/// share what is left by their fractional units (a track whose share is below its content, which
|
||||
/// only a Fill child's minimum or a spanning child asks for, keeps that size and the others share
|
||||
/// the rest). `avail` is None when the frame hugs that axis (fill tracks then hug their content too).
|
||||
fn resolve_tracks(specs: &[figcraft_doc::GridTrack], content: &[f64], avail: Option<f64>) -> Vec<f64> {
|
||||
let mut out: Vec<f64> = (0..content.len())
|
||||
.map(|i| match track_spec(specs, i) {
|
||||
@@ -891,13 +891,29 @@ pub fn grid_geometry(doc: &Document, frame: NodeId) -> Option<GridGeometry> {
|
||||
let gy = finite_or_zero(al.cross_gap).max(0.0);
|
||||
let hug_w = f.child_layout.sizing_h == Sizing::Hug;
|
||||
let hug_h = f.child_layout.sizing_v == Sizing::Hug;
|
||||
// What each child asks of its tracks: its size, or (filling a non-hugging axis) its minimum.
|
||||
// What each child asks of its tracks: its size, its minimum when it fills a non-hugging axis, or
|
||||
// nothing when it sits alone in a Fill track of a non-hugging axis.
|
||||
let (mut ws, mut hs) = (Vec::with_capacity(cells.len()), Vec::with_capacity(cells.len()));
|
||||
for c in &cells {
|
||||
let (w, h) = outer_size(doc, c.id);
|
||||
let cl = doc.node(c.id).map(|n| n.child_layout.clone()).unwrap_or_default();
|
||||
ws.push(if cl.sizing_h == Sizing::Fill && !hug_w { cl.min_w.unwrap_or(0.0) } else { w });
|
||||
hs.push(if cl.sizing_v == Sizing::Fill && !hug_h { cl.min_h.unwrap_or(0.0) } else { h });
|
||||
// Measured in Figma: a Fill track of a fixed-size axis shares the space by its fraction and
|
||||
// ignores a fixed-size (or hug-size) child in it, which overflows into the next track.
|
||||
let in_fill = |tracks: &[figcraft_doc::GridTrack], start: usize, span: usize| span == 1 && track_spec(tracks, start).0 == Sizing::Fill;
|
||||
ws.push(if cl.sizing_h == Sizing::Fill && !hug_w {
|
||||
cl.min_w.unwrap_or(0.0)
|
||||
} else if !hug_w && in_fill(&al.column_tracks, c.column, c.column_span) {
|
||||
0.0
|
||||
} else {
|
||||
w
|
||||
});
|
||||
hs.push(if cl.sizing_v == Sizing::Fill && !hug_h {
|
||||
cl.min_h.unwrap_or(0.0)
|
||||
} else if !hug_h && in_fill(&al.row_tracks, c.row, c.row_span) {
|
||||
0.0
|
||||
} else {
|
||||
h
|
||||
});
|
||||
}
|
||||
let col_content = track_content(&cells, &ws, &al.column_tracks, cols, gx, true, hug_w);
|
||||
let row_content = track_content(&cells, &hs, &al.row_tracks, rows, gy, false, hug_h);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Auto layout and constraints parity
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-09 · **Change:** patch (a child dragged within its own auto layout row or column passes a sibling when its leading edge reaches the sibling's middle, as measured in Figma's web app, wherever it is grabbed; a Cmd/Ctrl-click on a padding field sets all four sides with the value typed, as the Guide to auto layout's shortcut guide says; Suggest auto layout is its own command, as the help center says: it adds as many auto layout frames as needed, splitting a frame's layers into rows or columns by the gaps between them, and keeps the placement of the design where the gaps are even (#1823); baseline alignment puts a layer without text with its bottom on the line, as the help center's figure shows an icon next to text (#1825); fill children share the free space by their content area, border-box fashion, as the Flexbox article says (#1824); double-click an edge of an auto layout frame's box for Hug contents, Alt+double-click an edge of a child's box for Fill container, as the Guide to auto layout says (#1838); a child resized to the full available space of its auto layout frame becomes Fill container on that axis, as the Guide to auto layout says; a text keeps a max height or a number of max lines, not both, as the Guide to auto layout says: a max height sets max lines to Auto and max lines remove the max height; Remove all auto layout, as the help center's More layout options has it: `layout.removeAll` takes the auto layout off a frame and every frame nested in it in one undo step; grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); Layout sidebar: Add auto layout (+) is available for groups and converts a single group to a frame in place, with mask order, flow-only inference and Fill sizing preserved; adding auto layout to an empty frame keeps its size (#369); `node.set` applies auto layout keys only to frames, with warnings (#603); core editor leftovers: auto gap handles, wrapped-line alignment; behaviour suite written from the public documentation, every rule's status; fixes and the missing settings it found) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-09 · **Change:** patch (grid Fill (`fr`) tracks share the space of a fixed-size axis by their fractions and let a fixed-size child overflow, as measured in Figma (300 x 200, two rows, 100 x 150 and 100 x 20 children: rows 100 / 100, second child at y 100; with a row gap of 10, rows 95 / 95 and y 105; on Hug the rows fit their content, 150 / 20) -- this replaces the 2026-10-11 rule from the documentation; a child dragged within its own auto layout row or column passes a sibling when its leading edge reaches the sibling's middle, as measured in Figma's web app, wherever it is grabbed; a Cmd/Ctrl-click on a padding field sets all four sides with the value typed, as the Guide to auto layout's shortcut guide says; Suggest auto layout is its own command, as the help center says: it adds as many auto layout frames as needed, splitting a frame's layers into rows or columns by the gaps between them, and keeps the placement of the design where the gaps are even (#1823); baseline alignment puts a layer without text with its bottom on the line, as the help center's figure shows an icon next to text (#1825); fill children share the free space by their content area, border-box fashion, as the Flexbox article says (#1824); double-click an edge of an auto layout frame's box for Hug contents, Alt+double-click an edge of a child's box for Fill container, as the Guide to auto layout says (#1838); a child resized to the full available space of its auto layout frame becomes Fill container on that axis, as the Guide to auto layout says; a text keeps a max height or a number of max lines, not both, as the Guide to auto layout says: a max height sets max lines to Auto and max lines remove the max height; Remove all auto layout, as the help center's More layout options has it: `layout.removeAll` takes the auto layout off a frame and every frame nested in it in one undo step; grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); Layout sidebar: Add auto layout (+) is available for groups and converts a single group to a frame in place, with mask order, flow-only inference and Fill sizing preserved; adding auto layout to an empty frame keeps its size (#369); `node.set` applies auto layout keys only to frames, with warnings (#603); core editor leftovers: auto gap handles, wrapped-line alignment; behaviour suite written from the public documentation, every rule's status; fixes and the missing settings it found) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
|
||||
Auto layout is the most used layout feature in Figma and the one where small differences (a
|
||||
hugging frame one pixel off, text wrapping at another width) break real files. **Breadth ~98%,
|
||||
ready ~74% (estimated)**: every rule of the public documentation is a test case with the sizes and
|
||||
positions it implies (`crates/layout/src/behaviour_tests.rs`, 64 cases, and
|
||||
positions it implies (`crates/layout/src/behaviour_tests.rs`, 68 cases, and
|
||||
`crates/engine/src/al_behaviour_tests.rs` through commands), and all pass; nothing yet against real
|
||||
Figma files. Remaining: 3–6 h.
|
||||
|
||||
@@ -67,7 +67,7 @@ Status: **pass** (a case in the suite holds), **fixed** (the suite found a diver
|
||||
| Ignore auto layout: out of the flow, kept in the frame; constraints apply only then | pass | `absolute_position_rules` |
|
||||
| Canvas stacking: last on top by default, first on top on request; the layer order stays | **fixed** + **added** | `reverseZ` was stored but never drawn: now the canvas, PNG, SVG and PDF exports and hit testing paint in that order; Design panel control |
|
||||
| Grid: `fr` tracks share by fraction; fixed tracks keep their size; hug tracks fit their content | pass | `grid_rules` |
|
||||
| Grid `fr` tracks behave like the CSS `fr` unit: a track never gets less than its content | **fixed** | was `minmax(0, 1fr)`: a fixed-size child taller than its share overflowed into the next row |
|
||||
| Grid `fr` tracks as measured in Figma: on a fixed-size axis they share the space left after gaps and padding by their fractions and a fixed-size child bigger than its share overflows into the next track (`minmax(0, 1fr)` for fixed-size and hug-size children); a Fill child's own minimum width or height still keeps its track at least that big (not measured, kept from before: two 1fr rows in 200 with a 150 minimum resolve to 150 / 50); on a Hug axis they size to their content | **fixed** | 300 x 200 frame, rows 1fr / 1fr, children 100 x 150 and 100 x 20: rows 100 / 100, second child at y 100; row gap 10: rows 95 / 95, y 105; frame height Hug: rows 150 / 20, frame 170; columns the same. Measured in Figma 2026-11-11; was the CSS rule from the documentation (2026-10-11) |
|
||||
| Grid placement left to right, top to bottom; rows grow; anchors; spans; gaps; padding | pass | `grid_rules` |
|
||||
| Grid children align independently in their cells | **added** | `gridAlignH` / `gridAlignV` (Auto follows the frame), Design panel cell alignment |
|
||||
| A hugging grid with `fr` tracks | silent | the file format calls it invalid; `fr` tracks hug their content |
|
||||
@@ -79,7 +79,7 @@ Status: **pass** (a case in the suite holds), **fixed** (the suite found a diver
|
||||
| Feature | Status | Evidence / notes |
|
||||
|---|---|---|
|
||||
| Horizontal, vertical, wrap (both directions) | done | `crates/layout` |
|
||||
| Grid auto layout: fixed / hug / `fr` tracks, cells, spans, gaps, cell alignment | done | `fr` like CSS `fr` |
|
||||
| Grid auto layout: fixed / hug / `fr` tracks, cells, spans, gaps, cell alignment | done | `fr` as measured |
|
||||
| Gap, auto gap (between / evenly / around), padding (per side), alignment 3×3 | done | a Cmd/Ctrl-click on a padding field (the pair or a side) makes the value typed go to all four sides in one undo step (Guide to auto layout, Keyboard shortcut guide; `padding_individual_tests`) |
|
||||
| Text baseline alignment | done | |
|
||||
| Hug / Fill / Fixed sizing with coherent rules (Fill turns a hugging parent Fixed) | done | `al_nesting_tests.rs`, `al_behaviour_tests.rs` |
|
||||
@@ -101,6 +101,7 @@ Status: **pass** (a case in the suite holds), **fixed** (the suite found a diver
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-11 | patch | M2.1: grid Fill (`fr`) tracks share the space of a fixed-size axis by their fractions and let a fixed-size child overflow, as measured in Figma (300 x 200, two rows, 100 x 150 and 100 x 20 children: rows 100 / 100, second child at y 100; with a row gap of 10, rows 95 / 95 and y 105; on Hug the rows fit their content, 150 / 20) (this replaces the 2026-10-11 rule from the documentation). Layout behaviour suite: three cases flipped, three added |
|
||||
| 2026-11-05 | patch | M2, #1823: Suggest auto layout is no longer Add auto layout under another name: `layout.suggest` splits a frame's layers into rows (Vertical) or else columns (Horizontal) by the gaps between their boxes, wraps each band of several layers in a new frame without fill or clipping that hugs it, and repeats inside it (16 levels, 1,000 layers per frame at most). The frame keeps its size; gap and padding come from the bands, so layers keep their boxes where the gaps are even and the bands start at the same offset; instances and layers inside them are refused; uneven gaps (smallest used), bands that start at different offsets across the flow (lined up at the smallest) and layers that overlap along both axes come back as `warnings`. A band frame hugs only when its own split succeeds: a band whose layers overlap along both axes gets no auto layout and stays Fixed, so it cannot reflow those children. `direction` sets the top level only; one undo step; `layout.add` is unchanged. Tests in `layout_suggest_tests.rs` |
|
||||
| 2026-11-02 | patch | M2: a Cmd/Ctrl-click on a padding field (the horizontal or vertical pair, or a side with Individual padding open) makes the value typed go to all four sides in one undo step (Guide to auto layout, Keyboard shortcut guide); a plain click still edits that axis or side alone. The modifier was ignored before. Test: `padding_individual_tests` |
|
||||
| 2026-11-03 | patch | M2, #1825: baseline alignment of a layer without text (an icon next to a text) puts its bottom on the line's baseline, as the help center's figure shows ("the bottoms of the icon and the word home are aligned on the red line"); it kept to the top of the line. A layer reaching below the text's baseline moves the text down, and a line with no text lines the layers' bottoms up (the documentation is silent; CSS baseline alignment does the same). Tests: `alignment_rules` (three cases), engine `baseline_alignment_puts_an_icons_bottom_on_the_text_baseline`. Numbers unchanged. |
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user