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}