Skip to main content

frus_layout/
style.rs

1//! Layout style — a thin frus API, translated to taffy internally.
2
3use frus_core::Insets;
4
5/// The dimension of one axis, width or height.
6#[derive(Clone, Copy, Debug, PartialEq)]
7pub enum Dimension {
8    /// A size decided by the content and the layout.
9    Auto,
10    /// A fixed size, in logical pixels.
11    Length(f32),
12    /// A percentage of the parent's size (`0.0..=1.0`).
13    Percent(f32),
14}
15
16impl Dimension {
17    fn to_taffy(self) -> taffy::Dimension {
18        match self {
19            Dimension::Auto => taffy::Dimension::Auto,
20            Dimension::Length(v) => taffy::Dimension::Length(v),
21            Dimension::Percent(p) => taffy::Dimension::Percent(p),
22        }
23    }
24}
25
26/// The main-axis direction of a flex container.
27#[derive(Clone, Copy, Debug, PartialEq)]
28pub enum FlexDirection {
29    /// Children laid out horizontally, first one at the start.
30    Row,
31    /// Children laid out vertically, first one at the top.
32    Column,
33    /// Horizontal, **last child first**. It is what a row laid out against the reading
34    /// direction amounts to, and the only way to say so per-container: the reading
35    /// direction itself is ambient and mirrors the whole frame.
36    RowReverse,
37    /// Vertical, **last child first** — a column that grows upwards, which is what a
38    /// chat transcript or a stack of toasts anchored to the bottom wants.
39    ColumnReverse,
40}
41
42impl FlexDirection {
43    /// Whether the main axis is the horizontal one, whichever way it runs.
44    pub fn is_horizontal(self) -> bool {
45        matches!(self, FlexDirection::Row | FlexDirection::RowReverse)
46    }
47
48    /// Whether the children run backwards along the main axis.
49    pub fn is_reversed(self) -> bool {
50        matches!(
51            self,
52            FlexDirection::RowReverse | FlexDirection::ColumnReverse
53        )
54    }
55
56    /// The same axis, run backwards (or forwards again).
57    pub fn reversed(self) -> Self {
58        match self {
59            FlexDirection::Row => FlexDirection::RowReverse,
60            FlexDirection::RowReverse => FlexDirection::Row,
61            FlexDirection::Column => FlexDirection::ColumnReverse,
62            FlexDirection::ColumnReverse => FlexDirection::Column,
63        }
64    }
65
66    fn to_taffy(self) -> taffy::FlexDirection {
67        match self {
68            FlexDirection::Row => taffy::FlexDirection::Row,
69            FlexDirection::Column => taffy::FlexDirection::Column,
70            FlexDirection::RowReverse => taffy::FlexDirection::RowReverse,
71            FlexDirection::ColumnReverse => taffy::FlexDirection::ColumnReverse,
72        }
73    }
74}
75
76/// How children are distributed along the **main** axis.
77#[derive(Clone, Copy, Debug, PartialEq)]
78pub enum Justify {
79    /// Packed at the start of the main axis.
80    Start,
81    /// Packed at the middle.
82    Center,
83    /// Packed at the end of the main axis.
84    End,
85    /// The first and last children touch the edges, the room shared between them.
86    SpaceBetween,
87    /// Equal space around each child: the outer gaps are half the inner ones.
88    SpaceAround,
89    /// Equal space **everywhere**, the outer gaps included: with `SpaceAround` the end
90    /// gaps are half the inner ones, which reads as a mistake when the row is the only
91    /// thing on the line.
92    SpaceEvenly,
93}
94
95impl Justify {
96    fn to_taffy(self) -> taffy::JustifyContent {
97        match self {
98            Justify::Start => taffy::JustifyContent::FlexStart,
99            Justify::Center => taffy::JustifyContent::Center,
100            Justify::End => taffy::JustifyContent::FlexEnd,
101            Justify::SpaceBetween => taffy::JustifyContent::SpaceBetween,
102            Justify::SpaceAround => taffy::JustifyContent::SpaceAround,
103            Justify::SpaceEvenly => taffy::JustifyContent::SpaceEvenly,
104        }
105    }
106}
107
108/// How a wrapping container's **lines** are distributed on the cross axis.
109///
110/// Not the same question as [`Align`], which places each child *within* its line. This
111/// one places the lines themselves, and it only has an answer when there is more than
112/// one — that is, when the container wraps and has cross-axis room to spare.
113#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
114pub enum AlignContent {
115    /// The lines fill the cross axis between them. Flexbox's default, and ours.
116    #[default]
117    Stretch,
118    /// Packed at the near edge, keeping their own heights.
119    Start,
120    /// Packed at the middle.
121    Center,
122    /// Packed at the far edge.
123    End,
124    /// The first and last lines touch the edges, the room shared between them.
125    SpaceBetween,
126    /// Equal space around each line: the outer gaps are half the inner ones.
127    SpaceAround,
128    /// Equal space everywhere, the outer gaps included.
129    SpaceEvenly,
130}
131
132impl AlignContent {
133    fn to_taffy(self) -> taffy::AlignContent {
134        match self {
135            AlignContent::Stretch => taffy::AlignContent::Stretch,
136            AlignContent::Start => taffy::AlignContent::FlexStart,
137            AlignContent::Center => taffy::AlignContent::Center,
138            AlignContent::End => taffy::AlignContent::FlexEnd,
139            AlignContent::SpaceBetween => taffy::AlignContent::SpaceBetween,
140            AlignContent::SpaceAround => taffy::AlignContent::SpaceAround,
141            AlignContent::SpaceEvenly => taffy::AlignContent::SpaceEvenly,
142        }
143    }
144}
145
146/// How children are aligned on the **cross** axis.
147#[derive(Clone, Copy, Debug, PartialEq)]
148pub enum Align {
149    /// Packed at the near edge of the cross axis.
150    Start,
151    /// Packed at the middle.
152    Center,
153    /// Packed at the far edge of the cross axis.
154    End,
155    /// Children stretch to fill the cross axis. This is the default.
156    Stretch,
157    /// Children are aligned on their **text baselines**: the line their letters sit
158    /// on, rather than their tops, their middles or their bottoms.
159    ///
160    /// It is the only alignment that makes two runs of different sizes look like one
161    /// line — a price beside its currency, a heading beside a note. Nothing here
162    /// resolves it: the layer above measures each child's baseline and turns the
163    /// difference into a top margin, so by the time taffy sees it the children are
164    /// already where they belong and it aligns them to the start.
165    Baseline,
166}
167
168impl Align {
169    fn to_taffy(self) -> taffy::AlignItems {
170        match self {
171            Align::Start => taffy::AlignItems::FlexStart,
172            Align::Center => taffy::AlignItems::Center,
173            Align::End => taffy::AlignItems::FlexEnd,
174            Align::Stretch => taffy::AlignItems::Stretch,
175            // Already resolved into per-child margins by then; from here it is a
176            // start alignment, and it must be one — stretching would give every child
177            // the row's height and there would be no baseline left to align.
178            Align::Baseline => taffy::AlignItems::FlexStart,
179        }
180    }
181}
182
183/// A layout node's style.
184#[derive(Clone, Copy, Debug, PartialEq)]
185pub struct Style {
186    /// Width.
187    pub width: Dimension,
188    /// Height.
189    pub height: Dimension,
190    /// **Minimum** width: a floor the layout never goes below. Useful for a box that
191    /// grows with its content and never squashes (`Auto` = no floor).
192    pub min_width: Dimension,
193    /// **Minimum** height: a floor the layout never goes below — for instance a row
194    /// that grows with tall content and never shrinks (`Auto` = no floor).
195    pub min_height: Dimension,
196    /// **Maximum** width: a ceiling the layout never goes above, whatever the content
197    /// or the space on offer asks for (`Auto` = no ceiling).
198    pub max_width: Dimension,
199    /// **Maximum** height: the same ceiling on the vertical axis (`Auto` = none).
200    pub max_height: Dimension,
201    /// The main-axis grow factor (flexbox): the share of **spare** room this item takes.
202    pub flex_grow: f32,
203    /// The main-axis **shrink** factor: the share of a *deficit* this item absorbs when
204    /// the children do not fit. `1.0` — the flexbox default — means everything squashes
205    /// together, in proportion to its basis; `0.0` means *not me*, and is what fixed
206    /// chrome wants: an icon button at the end of a row keeps its 40 px however long the
207    /// label beside it is. See [`Style::flex_basis`] for the other half.
208    pub flex_shrink: f32,
209    /// The main-axis **basis**: the size an item starts from, before growing or
210    /// shrinking. `Auto` — the default — means *use the content*, so a long label starts
211    /// long and drags the whole row with it. `Length(0.0)` means *start from nothing and
212    /// take what is left*, which is the shape of the reference's expanding child.
213    pub flex_basis: Dimension,
214    /// The main-axis direction, for a container.
215    pub flex_direction: FlexDirection,
216    /// Distribution along the main axis.
217    pub justify: Justify,
218    /// Alignment on the cross axis, for a container: what it does to its children.
219    pub align: Align,
220    /// How a **wrapping** container's lines are distributed on the cross axis.
221    ///
222    /// Silent unless the container wraps *and* has cross-axis room to spare, since one
223    /// line has nowhere to go.
224    pub align_content: AlignContent,
225    /// This item's **own** cross-axis alignment, overriding whatever its parent asked
226    /// for. `None` — the default — means "do as the others do".
227    ///
228    /// One box out of step with its row is a common enough shape that the alternative is
229    /// a wrapper widget per exception, and flexbox already has the field.
230    pub align_self: Option<Align>,
231    /// Padding, per side, in logical pixels.
232    pub padding: Insets,
233    /// **Margin**, per side, in logical pixels: space reserved **around** the box,
234    /// outside its decoration, which pushes siblings away.
235    pub margin: Insets,
236    /// When `Some(r)`, the box **keeps an aspect ratio** of `r` (`width / height`):
237    /// the free dimension is derived from the constrained one. `None` means no
238    /// ratio constraint.
239    pub aspect_ratio: Option<f32>,
240    /// Spacing between children, in logical pixels.
241    pub gap: f32,
242    /// Spacing **between rows**, when it differs from `gap`. `None` means `gap`.
243    ///
244    /// A grid's two spacings are separate in the reference — one along the axis it
245    /// scrolls, one across it — and a tile grid usually wants them different.
246    pub row_gap: Option<f32>,
247    /// Spacing **between columns**, when it differs from `gap`. `None` means `gap`.
248    pub column_gap: Option<f32>,
249    /// When `true`, children **wrap** onto the next line (flex-wrap) once they
250    /// overflow the main axis — automatic responsive reflow.
251    pub flex_wrap: bool,
252    /// When `Some(n)`, the container is a **grid** of `n` equal columns, with
253    /// children placed automatically, row by row. `None` means flex.
254    pub grid_columns: Option<usize>,
255    /// When `Some(h)`, every grid row is exactly `h` logical pixels tall instead of
256    /// following its content — the reference's per-tile main-axis extent. Ignored
257    /// outside a grid.
258    pub grid_row_height: Option<f32>,
259    /// When `true`, the children **overlap**: all of them share one cell, which is as big
260    /// as the largest, and each is placed in it by [`Style::align`] on both axes.
261    ///
262    /// It is how a box sizes itself to the biggest of several things drawn on top of one
263    /// another — a picture on its way out under the one coming in — which a flex line
264    /// cannot say (it adds its children up) and a stack here does not (its layers are laid
265    /// out apart from it, so an unsized stack is nothing). Handed a box by its parent, the
266    /// cell fills it, as the reference's stack fills a tight constraint.
267    pub overlap: bool,
268}
269
270impl Default for Style {
271    fn default() -> Self {
272        Self {
273            width: Dimension::Auto,
274            height: Dimension::Auto,
275            min_width: Dimension::Auto,
276            min_height: Dimension::Auto,
277            max_width: Dimension::Auto,
278            max_height: Dimension::Auto,
279            flex_grow: 0.0,
280            // **Not** flexbox's default of 1. The reference hands an inflexible child of
281            // a row or a column an unbounded main axis: it is never squeezed, and a line
282            // that does not fit overflows and says so. Flexbox would quietly take the
283            // deficit out of every child in proportion, which is how a 40 px button came
284            // to be laid out at 13 and drawn off the card (milestone 333) — silently,
285            // for three milestones. A box gives way when it says it will, and the walk
286            // grants it to a **lone** child, which is not dividing a line up but being
287            // handed its parent's constraints.
288            flex_shrink: 0.0,
289            flex_basis: Dimension::Auto,
290            flex_direction: FlexDirection::Row,
291            justify: Justify::Start,
292            align_content: AlignContent::default(),
293            align: Align::Stretch,
294            align_self: None,
295            padding: Insets::ZERO,
296            margin: Insets::ZERO,
297            aspect_ratio: None,
298            gap: 0.0,
299            row_gap: None,
300            column_gap: None,
301            flex_wrap: false,
302            grid_columns: None,
303            grid_row_height: None,
304            overlap: false,
305        }
306    }
307}
308
309impl Style {
310    /// Mixes into `hasher` **every field that affects layout geometry**. Two styles
311    /// with the same fingerprint produce the same arrangement — which is what makes
312    /// a relayout cache possible, skipping taffy when nothing relevant has changed.
313    /// The `f32`s are hashed by bit pattern, so equality is exact; colour and text do
314    /// not enter into it, since they only affect painting.
315    pub fn layout_hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
316        use std::hash::Hash;
317        fn dim<H: std::hash::Hasher>(d: Dimension, h: &mut H) {
318            match d {
319                Dimension::Auto => 0u8.hash(h),
320                Dimension::Length(v) => {
321                    1u8.hash(h);
322                    v.to_bits().hash(h);
323                }
324                Dimension::Percent(v) => {
325                    2u8.hash(h);
326                    v.to_bits().hash(h);
327                }
328            }
329        }
330        dim(self.width, hasher);
331        dim(self.height, hasher);
332        dim(self.min_width, hasher);
333        dim(self.min_height, hasher);
334        dim(self.max_width, hasher);
335        dim(self.max_height, hasher);
336        self.flex_grow.to_bits().hash(hasher);
337        self.flex_shrink.to_bits().hash(hasher);
338        dim(self.flex_basis, hasher);
339        (self.flex_direction as u8).hash(hasher);
340        (self.justify as u8).hash(hasher);
341        (self.align_content as u8).hash(hasher);
342        (self.align as u8).hash(hasher);
343        match self.align_self {
344            None => 0u8.hash(hasher),
345            Some(a) => {
346                1u8.hash(hasher);
347                (a as u8).hash(hasher);
348            }
349        }
350        self.padding.top.to_bits().hash(hasher);
351        self.padding.right.to_bits().hash(hasher);
352        self.padding.bottom.to_bits().hash(hasher);
353        self.padding.left.to_bits().hash(hasher);
354        self.margin.top.to_bits().hash(hasher);
355        self.margin.right.to_bits().hash(hasher);
356        self.margin.bottom.to_bits().hash(hasher);
357        self.margin.left.to_bits().hash(hasher);
358        match self.aspect_ratio {
359            None => 0u8.hash(hasher),
360            Some(r) => {
361                1u8.hash(hasher);
362                r.to_bits().hash(hasher);
363            }
364        }
365        self.gap.to_bits().hash(hasher);
366        self.row_gap.map(f32::to_bits).hash(hasher);
367        self.column_gap.map(f32::to_bits).hash(hasher);
368        self.flex_wrap.hash(hasher);
369        self.grid_columns.hash(hasher);
370        self.grid_row_height.map(f32::to_bits).hash(hasher);
371        self.overlap.hash(hasher);
372    }
373
374    pub(crate) fn to_taffy(self) -> taffy::Style {
375        let mut style = taffy::Style {
376            size: taffy::Size {
377                width: self.width.to_taffy(),
378                height: self.height.to_taffy(),
379            },
380            min_size: taffy::Size {
381                width: self.min_width.to_taffy(),
382                height: self.min_height.to_taffy(),
383            },
384            max_size: taffy::Size {
385                width: self.max_width.to_taffy(),
386                height: self.max_height.to_taffy(),
387            },
388            flex_grow: self.flex_grow,
389            flex_shrink: self.flex_shrink,
390            flex_basis: self.flex_basis.to_taffy(),
391            flex_direction: self.flex_direction.to_taffy(),
392            flex_wrap: if self.flex_wrap {
393                taffy::FlexWrap::Wrap
394            } else {
395                taffy::FlexWrap::NoWrap
396            },
397            justify_content: Some(self.justify.to_taffy()),
398            align_content: Some(self.align_content.to_taffy()),
399            align_items: Some(self.align.to_taffy()),
400            align_self: self.align_self.map(Align::to_taffy),
401            padding: taffy::Rect {
402                left: taffy::LengthPercentage::Length(self.padding.left),
403                right: taffy::LengthPercentage::Length(self.padding.right),
404                top: taffy::LengthPercentage::Length(self.padding.top),
405                bottom: taffy::LengthPercentage::Length(self.padding.bottom),
406            },
407            margin: taffy::Rect {
408                left: taffy::LengthPercentageAuto::Length(self.margin.left),
409                right: taffy::LengthPercentageAuto::Length(self.margin.right),
410                top: taffy::LengthPercentageAuto::Length(self.margin.top),
411                bottom: taffy::LengthPercentageAuto::Length(self.margin.bottom),
412            },
413            gap: taffy::Size {
414                width: taffy::LengthPercentage::Length(self.column_gap.unwrap_or(self.gap)),
415                height: taffy::LengthPercentage::Length(self.row_gap.unwrap_or(self.gap)),
416            },
417            aspect_ratio: self.aspect_ratio,
418            ..Default::default()
419        };
420
421        // Grid: `n` equal columns of 1fr each; children are placed automatically,
422        // row by row (auto-flow), with rows sized to their content.
423        if let Some(columns) = self.grid_columns {
424            use taffy::style_helpers::fr;
425            style.display = taffy::Display::Grid;
426            // The explicit `1.0_f32`: `fr` is generic, and the silent fallback from
427            // `1.0` to `f32` is being withdrawn (future_incompatible, rust#154024).
428            // Without the suffix this becomes a hard error.
429            style.grid_template_columns = (0..columns).map(|_| fr(1.0_f32)).collect();
430            // A fixed **tile height**: the rows the auto-flow creates are all this tall
431            // instead of following their content, which is what makes a grid of tiles
432            // read as a grid rather than as ragged bands.
433            if let Some(height) = self.grid_row_height {
434                style.grid_auto_rows = vec![taffy::style_helpers::length(height)];
435            }
436        }
437
438        // Overlap: a grid of one cell. The children are put in it by `Layout::container`,
439        // the one place that sees a parent and its children together; this is the cell.
440        // Its track is `auto`, so it is as big as the largest child, and **stretched** to a
441        // box the parent hands over — without that, a switcher in a stretching column would
442        // centre its children on the widest of them and sit at the start of its own box.
443        if self.overlap {
444            style.display = taffy::Display::Grid;
445            style.justify_content = Some(taffy::JustifyContent::Stretch);
446            style.align_content = Some(taffy::AlignContent::Stretch);
447            // `align_items` already carries `align`; the other axis gets the same.
448            style.justify_items = Some(self.align.to_taffy());
449        }
450
451        style
452    }
453}