Skip to main content

alpha2_model/
layout.rs

1//! The box tree of a layout generator: boxes laid out by CSS flexbox or grid
2//! around their text and shapes.
3//!
4//! Field names are CSS property names in `snake_case`, and keyword values CSS
5//! keywords in `snake_case`. A length is a number (a fraction of the layout
6//! picture's shorter side), `"<n>px"` (pixels of the project canvas), or,
7//! for sizes and insets, `"<n>%"` of the parent's content box, as in CSS
8//! (see [`crate::layout_values`]). Boxes are `border-box`: a width includes
9//! padding and border. Text style (`font_family`, `font_size`, `color`,
10//! `font_weight`, `line_height`, `letter_spacing`, `text_align`, `outline`)
11//! may be set on any box and is inherited by the boxes in it, as CSS
12//! inherits it.
13
14use crate::animation::visit_keyed;
15use crate::checks::{
16    Checked, ValueIssue, ValueProblem, animated, any, non_negative, scalar, unit, validate_paint,
17    vec2,
18};
19use crate::layout_values::{
20    GridEdge, GridPlacement, Insets, LayoutLength, LayoutOffset, LayoutSize, RepeatCount,
21    TrackComponent, TrackList,
22};
23use crate::{
24    Animated, FONT_WEIGHT_RANGE, Interpolation, LETTER_SPACING_RANGE, LINE_HEIGHT_RANGE,
25    MAX_FONT_FAMILY_BYTES, MAX_TEXT_CONTENT_BYTES, MAX_TEXT_OUTLINE_WIDTH, Paint, Rgba, ShapeIssue,
26    ShapeItem, TextAlign, TextOutline, Vec2,
27};
28use serde::{Deserialize, Serialize};
29use std::fmt;
30
31/// Most nodes in one layout tree, the root included.
32pub const MAX_LAYOUT_NODES: usize = 256;
33/// Deepest nesting of one layout tree; the root is depth 1.
34pub const MAX_LAYOUT_DEPTH: usize = 16;
35/// Most nodes with text in one layout tree.
36pub const MAX_LAYOUT_TEXTS: usize = 64;
37/// Most isolated nodes (with a visual field or hidden overflow) in one
38/// layout tree.
39pub const MAX_LAYOUT_ISOLATED: usize = 16;
40/// Largest font size of a layout's text, as a fraction of the picture's
41/// shorter side.
42pub const MAX_LAYOUT_FONT_SIZE: f32 = 1.0;
43/// Most explicit tracks on one axis of a grid, `repeat()` counts expanded.
44pub const MAX_GRID_TRACKS: usize = 64;
45/// The farthest grid line a placement names, from either end.
46const MAX_GRID_LINE: i16 = 65;
47
48/// The initial text style: what the root inherits.
49pub const LAYOUT_TEXT_DEFAULTS: LayoutTextStyle = LayoutTextStyle {
50    font_family: None,
51    font_size: 0.05,
52    color: Rgba::new(1.0, 1.0, 1.0, 1.0),
53    line_height: 1.2,
54    letter_spacing: 0.0,
55    font_weight: 400,
56    text_align: TextAlign::Start,
57    outline: None,
58};
59
60/// A box's text style once inherited: what its text draws with. Lengths are
61/// fractions of the picture's shorter side.
62#[derive(Clone, Debug, PartialEq)]
63pub struct LayoutTextStyle {
64    /// The family the text draws in, as a text style's `font_family`.
65    pub font_family: Option<String>,
66    /// Font size as a fraction of the picture's shorter side.
67    pub font_size: f32,
68    /// Text color.
69    pub color: Rgba,
70    /// Baseline-to-baseline distance in font sizes.
71    pub line_height: f32,
72    /// Space after each character, a fraction of the font size.
73    pub letter_spacing: f32,
74    /// The weight its faces are matched by.
75    pub font_weight: u16,
76    /// How lines align within the content width.
77    pub text_align: TextAlign,
78    /// A band around the glyphs; its width a fraction of the font size.
79    pub outline: Option<TextOutline>,
80}
81
82/// One box of a layout tree.
83///
84/// A box lays its children out by flexbox (`display: flex`, the default) or
85/// grid (`display: grid`). It is a container when it has children, a leaf
86/// when it has text or shape content, and empty otherwise (a spacer or a
87/// decoration). Per box, its background is drawn first, then its border,
88/// then its content. Boxes are drawn in document order; a reversed
89/// direction or a grid placement moves them, not their order.
90#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
91#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
92#[serde(deny_unknown_fields)]
93pub struct LayoutNode {
94    /// Boxes laid out in this one, in document (and paint) order. Not with
95    /// `text` or `shape`.
96    #[serde(default, skip_serializing_if = "Vec::is_empty")]
97    pub children: Vec<LayoutNode>,
98    /// The box's text, wrapped at its content width and drawn in the style
99    /// it inherits; `\n` starts a line. At most [`MAX_TEXT_CONTENT_BYTES`]
100    /// bytes per value; may be empty. Its keyframes all `hold`. Not with
101    /// `children` or `shape`.
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub text: Option<Animated<String>>,
104    /// Shapes drawn on a picture the size of the box's content box. Not with
105    /// `children` or `text`.
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub shape: Option<LayoutShape>,
108
109    /// How the box lays its children out. Default `flex`. Not on a box with
110    /// text or shape content, as every container field.
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub display: Option<Display>,
113    /// The main axis of a flex container's children. Default `row`.
114    #[serde(default, skip_serializing_if = "Option::is_none")]
115    pub flex_direction: Option<FlexDirection>,
116    /// Whether a flex container's children wrap onto more lines. Default
117    /// `nowrap`.
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub flex_wrap: Option<FlexWrap>,
120    /// A grid's explicit columns, as CSS writes them (`"1fr 2fr"`,
121    /// `"repeat(3, minmax(100px, 1fr))"`). Default none.
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub grid_template_columns: Option<TrackList>,
124    /// A grid's explicit rows. Default none.
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub grid_template_rows: Option<TrackList>,
127    /// The size of a grid's implicit columns. Default `auto`.
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub grid_auto_columns: Option<TrackList>,
130    /// The size of a grid's implicit rows. Default `auto`.
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub grid_auto_rows: Option<TrackList>,
133    /// How a grid places items without a placement. Default `row`.
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub grid_auto_flow: Option<GridAutoFlow>,
136    /// Space between children, on both axes. At least 0; default 0.
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub gap: Option<Animated<LayoutSize>>,
139    /// Space between rows (between lines of a wrapping row flex
140    /// container), in place of `gap`. At least 0.
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub row_gap: Option<Animated<LayoutSize>>,
143    /// Space between columns, in place of `gap`. At least 0.
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub column_gap: Option<Animated<LayoutSize>>,
146    /// Where children lie along the main (inline, for a grid) axis. Default
147    /// `start`.
148    #[serde(default, skip_serializing_if = "Option::is_none")]
149    pub justify_content: Option<JustifyContent>,
150    /// Where children lie across the main axis, or in their grid areas'
151    /// rows. Default `stretch`.
152    #[serde(default, skip_serializing_if = "Option::is_none")]
153    pub align_items: Option<AlignItems>,
154    /// Where lines of a wrapping flex container, or a grid's rows, lie
155    /// across the container. Default `stretch`.
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub align_content: Option<AlignContent>,
158    /// Where a grid's children lie in their grid areas' columns. Default
159    /// `stretch`.
160    #[serde(default, skip_serializing_if = "Option::is_none")]
161    pub justify_items: Option<AlignItems>,
162
163    /// Space inside the border: a length, the CSS shorthand
164    /// (`"8px 16px"`), or the sides object. Sides at least 0; default 0.
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    pub padding: Option<Animated<Insets>>,
167    /// What covers the border box, rounded by `border_radius`. Positions
168    /// are normalized to the border box.
169    #[serde(default, skip_serializing_if = "Option::is_none")]
170    pub background: Option<Animated<Paint>>,
171    /// A border inside the border box.
172    #[serde(default, skip_serializing_if = "Option::is_none")]
173    pub border: Option<LayoutBorder>,
174    /// The corner radius of the border box. At least 0; default 0. A radius
175    /// past half the box's shorter side is capped.
176    #[serde(default, skip_serializing_if = "Option::is_none")]
177    pub border_radius: Option<Animated<LayoutSize>>,
178    /// Whether content past the padding box is drawn. Default `visible`.
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    pub overflow: Option<Overflow>,
181
182    /// Border-box width. `None` is auto. Not on the root, as every item
183    /// field.
184    #[serde(default, skip_serializing_if = "Option::is_none")]
185    pub width: Option<Animated<LayoutLength>>,
186    /// Border-box height. `None` is auto.
187    #[serde(default, skip_serializing_if = "Option::is_none")]
188    pub height: Option<Animated<LayoutLength>>,
189    /// Smallest width. `None` is auto, the CSS automatic minimum size.
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub min_width: Option<Animated<LayoutLength>>,
192    /// Smallest height. `None` is auto.
193    #[serde(default, skip_serializing_if = "Option::is_none")]
194    pub min_height: Option<Animated<LayoutLength>>,
195    /// Largest width. `None` is none.
196    #[serde(default, skip_serializing_if = "Option::is_none")]
197    pub max_width: Option<Animated<LayoutLength>>,
198    /// Largest height. `None` is none.
199    #[serde(default, skip_serializing_if = "Option::is_none")]
200    pub max_height: Option<Animated<LayoutLength>>,
201    /// Width over height. Positive.
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub aspect_ratio: Option<f32>,
204    /// Space outside the border, written as `padding` is. Sides may be
205    /// negative; there is no `auto`.
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub margin: Option<Animated<Insets>>,
208    /// How much of a flex container's free space the box takes. At least
209    /// 0; default 0. Not in a grid, as every flex item field.
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub flex_grow: Option<Animated<f32>>,
212    /// How much the box gives up when a flex container's space runs short.
213    /// At least 0; default 1.
214    #[serde(default, skip_serializing_if = "Option::is_none")]
215    pub flex_shrink: Option<Animated<f32>>,
216    /// The main size before growing and shrinking. `None` is auto.
217    #[serde(default, skip_serializing_if = "Option::is_none")]
218    pub flex_basis: Option<Animated<LayoutLength>>,
219    /// Where the box lies across its flex container's main axis, or in its
220    /// grid area's row. `None` follows the parent's `align_items`.
221    #[serde(default, skip_serializing_if = "Option::is_none")]
222    pub align_self: Option<AlignItems>,
223    /// Where the box lies in its grid area's column. `None` follows the
224    /// parent's `justify_items`. Not in a flex container, as every grid
225    /// item field.
226    #[serde(default, skip_serializing_if = "Option::is_none")]
227    pub justify_self: Option<AlignItems>,
228    /// The box's columns in its grid, as CSS writes them (`2`, `"1 / 3"`,
229    /// `"span 2"`). Default `auto`.
230    #[serde(default, skip_serializing_if = "Option::is_none")]
231    pub grid_column: Option<GridPlacement>,
232    /// The box's rows in its grid. Default `auto`.
233    #[serde(default, skip_serializing_if = "Option::is_none")]
234    pub grid_row: Option<GridPlacement>,
235    /// Whether the box is in its parent's flow. Default `relative`.
236    #[serde(default, skip_serializing_if = "Option::is_none")]
237    pub position: Option<Position>,
238    /// Where an `absolute` box lies in its parent's padding box.
239    #[serde(default, skip_serializing_if = "Option::is_none")]
240    pub inset: Option<LayoutInset>,
241
242    /// The family the box's text draws in, as a text style's
243    /// `font_family`. Inherited; the root's is the host's first font.
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    pub font_family: Option<String>,
246    /// Font size: positive, at most [`MAX_LAYOUT_FONT_SIZE`] of the
247    /// picture's shorter side. Inherited; the root's is 0.05.
248    #[serde(default, skip_serializing_if = "Option::is_none")]
249    pub font_size: Option<Animated<LayoutSize>>,
250    /// Text color. Inherited; the root's is white.
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub color: Option<Rgba>,
253    /// Baseline-to-baseline distance in font sizes, in `[0.5, 3]`. Each
254    /// line's em box, 0.8 of a font size above its baseline and 0.2 below,
255    /// is centred in its line when the line is taller than an em.
256    /// Inherited; the root's is 1.2.
257    #[serde(default, skip_serializing_if = "Option::is_none")]
258    pub line_height: Option<f32>,
259    /// Space after each character, as a fraction of the font size, in
260    /// `[-0.5, 1]`, as CSS `letter-spacing` adds it: none after a line's
261    /// last character, and none after a character of a cursive script.
262    /// Not animated. Inherited; the root's is 0.
263    #[serde(default, skip_serializing_if = "Option::is_none")]
264    pub letter_spacing: Option<f32>,
265    /// The weight, in `[1, 1000]`, the family's faces are matched by, as
266    /// CSS Fonts 4 matches `font-weight`; a variable face draws at it. Not
267    /// animated. Inherited; the root's is 400.
268    #[serde(default, skip_serializing_if = "Option::is_none")]
269    pub font_weight: Option<u16>,
270    /// How lines align within the content width. Inherited; the root's is
271    /// `start`.
272    #[serde(default, skip_serializing_if = "Option::is_none")]
273    pub text_align: Option<TextAlign>,
274    /// A band around the glyphs; its width a fraction of the font size.
275    /// Inherited; the root has none.
276    #[serde(default, skip_serializing_if = "Option::is_none")]
277    pub outline: Option<TextOutline>,
278
279    /// Opacity of the box and everything in it, as one group, in `0..=1`.
280    /// Drawn after layout; it never moves a box.
281    #[serde(default, skip_serializing_if = "Option::is_none")]
282    pub opacity: Option<Animated<f32>>,
283    /// Offset of the box and everything in it, after layout: the object, or
284    /// one or two lengths as CSS `translate` writes them.
285    #[serde(default, skip_serializing_if = "Option::is_none")]
286    pub translate: Option<Animated<LayoutOffset>>,
287    /// Per-axis scale of the box and everything in it about its border-box
288    /// centre, after layout.
289    #[serde(default, skip_serializing_if = "Option::is_none")]
290    pub scale: Option<Animated<Vec2>>,
291    /// Clockwise rotation of the box and everything in it about its
292    /// border-box centre, in degrees, after layout.
293    #[serde(default, skip_serializing_if = "Option::is_none")]
294    pub rotation_degrees: Option<Animated<f32>>,
295}
296
297/// A box's shapes, drawn as a shapes generator draws its items on a picture
298/// the size of the box's content box: positions normalized to that box,
299/// lengths fractions of its shorter side. A shape box has no size of its
300/// own.
301#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
302#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
303#[serde(deny_unknown_fields)]
304pub struct LayoutShape {
305    /// The content tree, front first.
306    pub items: Vec<ShapeItem>,
307}
308
309/// A border inside the border box, as wide on every side.
310#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
311#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
312#[serde(deny_unknown_fields)]
313pub struct LayoutBorder {
314    /// Width. At least 0.
315    pub width: Animated<LayoutSize>,
316    /// What covers the border. Positions are normalized to the border box.
317    pub paint: Animated<Paint>,
318}
319
320/// Where an absolute box lies in its parent's padding box. A missing side is
321/// auto: with neither `left` nor `right`, the box keeps its place in the
322/// flow on that axis.
323#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
324#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
325#[serde(deny_unknown_fields)]
326pub struct LayoutInset {
327    /// Distance from the parent's left padding edge.
328    #[serde(default, skip_serializing_if = "Option::is_none")]
329    pub left: Option<Animated<LayoutLength>>,
330    /// Distance from the parent's top padding edge.
331    #[serde(default, skip_serializing_if = "Option::is_none")]
332    pub top: Option<Animated<LayoutLength>>,
333    /// Distance from the parent's right padding edge.
334    #[serde(default, skip_serializing_if = "Option::is_none")]
335    pub right: Option<Animated<LayoutLength>>,
336    /// Distance from the parent's bottom padding edge.
337    #[serde(default, skip_serializing_if = "Option::is_none")]
338    pub bottom: Option<Animated<LayoutLength>>,
339}
340
341impl LayoutInset {
342    fn sides(&self) -> [(&'static str, Option<&Animated<LayoutLength>>); 4] {
343        [
344            ("left", self.left.as_ref()),
345            ("top", self.top.as_ref()),
346            ("right", self.right.as_ref()),
347            ("bottom", self.bottom.as_ref()),
348        ]
349    }
350}
351
352/// How a box lays its children out.
353#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
354#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
355#[serde(rename_all = "snake_case")]
356pub enum Display {
357    /// CSS flexbox.
358    #[default]
359    Flex,
360    /// CSS grid.
361    Grid,
362}
363
364/// The main axis of a flex container.
365#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
366#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
367#[serde(rename_all = "snake_case")]
368pub enum FlexDirection {
369    /// Left to right.
370    #[default]
371    Row,
372    /// Top to bottom.
373    Column,
374    /// Right to left.
375    RowReverse,
376    /// Bottom to top.
377    ColumnReverse,
378}
379
380/// Whether a flex container's children wrap.
381#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
382#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
383pub enum FlexWrap {
384    /// One line.
385    #[default]
386    #[serde(rename = "nowrap")]
387    NoWrap,
388    /// As many lines as the children need.
389    #[serde(rename = "wrap")]
390    Wrap,
391}
392
393/// How a grid places items that have no placement of their own.
394#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
395#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
396#[serde(rename_all = "snake_case")]
397pub enum GridAutoFlow {
398    /// Filling each row, adding rows.
399    #[default]
400    Row,
401    /// Filling each column, adding columns.
402    Column,
403    /// As `row`, filling earlier holes.
404    RowDense,
405    /// As `column`, filling earlier holes.
406    ColumnDense,
407}
408
409/// Where children lie along a container's main axis, or a grid's columns in
410/// it.
411#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
412#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
413#[serde(rename_all = "snake_case")]
414pub enum JustifyContent {
415    /// Packed at the start.
416    #[default]
417    Start,
418    /// Packed in the middle.
419    Center,
420    /// Packed at the end.
421    End,
422    /// The first at the start, the last at the end, equal space between.
423    SpaceBetween,
424    /// Equal space around each.
425    SpaceAround,
426    /// Equal space between each and at both ends.
427    SpaceEvenly,
428}
429
430/// Where the lines of a wrapping flex container, or a grid's rows, lie
431/// across the container.
432#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
433#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
434#[serde(rename_all = "snake_case")]
435pub enum AlignContent {
436    /// Packed at the start.
437    Start,
438    /// Packed in the middle.
439    Center,
440    /// Packed at the end.
441    End,
442    /// The first at the start, the last at the end, equal space between.
443    SpaceBetween,
444    /// Equal space around each.
445    SpaceAround,
446    /// Equal space between each and at both ends.
447    SpaceEvenly,
448    /// Grown to fill the container.
449    #[default]
450    Stretch,
451}
452
453/// Where children lie across a flex container's main axis, or in their grid
454/// areas.
455#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
456#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
457#[serde(rename_all = "snake_case")]
458pub enum AlignItems {
459    /// At the start.
460    Start,
461    /// In the middle.
462    Center,
463    /// At the end.
464    End,
465    /// Across the whole line or area.
466    #[default]
467    Stretch,
468}
469
470/// Whether a box is in its parent's flow.
471#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
472#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
473#[serde(rename_all = "snake_case")]
474pub enum Position {
475    /// In the flow.
476    #[default]
477    Relative,
478    /// Out of the flow, placed by `inset`.
479    Absolute,
480}
481
482/// Whether content past a box's padding box is drawn.
483#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
484#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
485#[serde(rename_all = "snake_case")]
486pub enum Overflow {
487    /// Drawn.
488    #[default]
489    Visible,
490    /// Clipped to the padding box.
491    Hidden,
492}
493
494/// Why a layout tree is invalid.
495#[derive(Copy, Clone, Debug, Eq, PartialEq)]
496#[non_exhaustive]
497pub enum LayoutProblem {
498    /// More than [`MAX_LAYOUT_NODES`] nodes.
499    TooManyNodes,
500    /// Nodes nested deeper than [`MAX_LAYOUT_DEPTH`].
501    TooDeep,
502    /// More than [`MAX_LAYOUT_TEXTS`] nodes with text.
503    TooManyTexts,
504    /// More than [`MAX_LAYOUT_ISOLATED`] nodes with a visual field or hidden
505    /// overflow.
506    TooManyIsolated,
507    /// A node with more than one of children, text, and shape.
508    MixedContent,
509    /// `display` on a node with text or shape content.
510    DisplayOnLeaf,
511    /// A container field on a node with text or shape content.
512    ContainerFieldOnLeaf,
513    /// A size, margin, position, or item field on the root.
514    RootItemField,
515    /// A grid container field (named) on a flex container.
516    GridFieldOnFlex(&'static str),
517    /// A flex container field (named) on a grid container.
518    FlexFieldOnGrid(&'static str),
519    /// A flex item field (named) on a grid's child.
520    FlexItemFieldInGrid(&'static str),
521    /// A grid item field (named) on a flex container's child.
522    GridItemFieldInFlex(&'static str),
523    /// More than [`MAX_GRID_TRACKS`] explicit tracks on one axis.
524    TooManyTracks,
525    /// A `repeat()` count outside 1 to 64.
526    RepeatCount,
527    /// `repeat()` in implicit track sizes.
528    RepeatInAutoTracks,
529    /// An `auto-fill` or `auto-fit` repeat beside another, or with a track
530    /// (in it or beside it) without a fixed size.
531    AutoRepeatNeedsFixedTracks,
532    /// A grid line outside -65 to 65, line 0, or a span outside 1 to 64.
533    GridLineOutOfRange,
534    /// Keyframes that are empty, out of order, or have an unusable curve.
535    InvalidKeyframes,
536    /// A value that is not finite or outside its range.
537    OutOfRange,
538    /// Size keyframes that mix a percent with a length.
539    LengthKeyframesDiffer,
540    /// Paint keyframes that differ in kind, stop count, or spread.
541    PaintKeyframesDiffer,
542    /// A text keyframe whose interpolation is not `hold`.
543    TextKeyframesInterpolate,
544}
545
546impl fmt::Display for LayoutProblem {
547    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
548        match self {
549            Self::GridFieldOnFlex(field) => {
550                return write!(
551                    formatter,
552                    "sets {field}, a grid container field, without display grid"
553                );
554            }
555            Self::FlexFieldOnGrid(field) => {
556                return write!(
557                    formatter,
558                    "sets {field}, a flex container field, on a grid container"
559                );
560            }
561            Self::FlexItemFieldInGrid(field) => {
562                return write!(
563                    formatter,
564                    "sets {field}, a flex item field, in a grid container"
565                );
566            }
567            Self::GridItemFieldInFlex(field) => {
568                return write!(
569                    formatter,
570                    "sets {field}, a grid item field, in a flex container"
571                );
572            }
573            _ => {}
574        }
575        formatter.write_str(match self {
576            Self::TooManyNodes => "has more nodes than a layout may hold",
577            Self::TooDeep => "nests nodes too deeply",
578            Self::TooManyTexts => "has more text nodes than a layout may hold",
579            Self::TooManyIsolated => {
580                "has more nodes with opacity, transforms, or hidden overflow than a layout may hold"
581            }
582            Self::MixedContent => "has more than one of children, text, and shape",
583            Self::DisplayOnLeaf => "sets display on a node with text or shape content",
584            Self::ContainerFieldOnLeaf => {
585                "sets a container field on a node with text or shape content"
586            }
587            Self::RootItemField => "sets a size, margin, position, or item field on the root",
588            Self::TooManyTracks => "has more than 64 tracks",
589            Self::RepeatCount => "repeats tracks a number of times outside 1 to 64",
590            Self::RepeatInAutoTracks => "uses repeat() in implicit track sizes",
591            Self::AutoRepeatNeedsFixedTracks => {
592                "repeats auto-fill or auto-fit beside another, or with a track without a fixed size"
593            }
594            Self::GridLineOutOfRange => {
595                "names a grid line outside -65 to 65 or line 0, or a span outside 1 to 64"
596            }
597            Self::InvalidKeyframes => "has invalid keyframes",
598            Self::OutOfRange => "has a value out of range",
599            Self::LengthKeyframesDiffer => "has size keyframes that differ in kind",
600            Self::PaintKeyframesDiffer => "has paint keyframes that differ in kind or stops",
601            Self::TextKeyframesInterpolate => "has text keyframes that do not hold",
602            Self::GridFieldOnFlex(_)
603            | Self::FlexFieldOnGrid(_)
604            | Self::FlexItemFieldInGrid(_)
605            | Self::GridItemFieldInFlex(_) => unreachable!("written above"),
606        })
607    }
608}
609
610/// Where in a layout tree a problem lies, and what it is.
611#[derive(Clone, Debug, Eq, PartialEq)]
612pub struct LayoutIssue {
613    /// The field's path from the root, like `root.children[2].padding`.
614    pub at: String,
615    /// What is wrong there.
616    pub problem: LayoutProblem,
617}
618
619impl LayoutIssue {
620    fn new(at: impl Into<String>, problem: LayoutProblem) -> Self {
621        Self {
622            at: at.into(),
623            problem,
624        }
625    }
626}
627
628impl From<ValueIssue> for LayoutIssue {
629    fn from(issue: ValueIssue) -> Self {
630        Self::new(
631            issue.at,
632            match issue.problem {
633                ValueProblem::InvalidKeyframes => LayoutProblem::InvalidKeyframes,
634                ValueProblem::OutOfRange => LayoutProblem::OutOfRange,
635                ValueProblem::PaintKeyframesDiffer => LayoutProblem::PaintKeyframesDiffer,
636            },
637        )
638    }
639}
640
641/// Why a generator is invalid.
642#[derive(Clone, Debug, Eq, PartialEq)]
643pub enum GeneratorIssue {
644    /// A fill's or a text's field is out of range.
645    Invalid,
646    /// A problem in a shapes generator's content tree, or in a layout's
647    /// shape node (its path from the layout's root).
648    Shape(ShapeIssue),
649    /// A problem in a layout tree.
650    Layout(LayoutIssue),
651}
652
653impl From<LayoutIssue> for GeneratorIssue {
654    fn from(issue: LayoutIssue) -> Self {
655        Self::Layout(issue)
656    }
657}
658
659impl From<ValueIssue> for GeneratorIssue {
660    fn from(issue: ValueIssue) -> Self {
661        Self::Layout(issue.into())
662    }
663}
664
665/// What the whole tree has used up of its limits so far.
666#[derive(Default)]
667struct Counts {
668    nodes: usize,
669    texts: usize,
670    isolated: usize,
671    shape_items: usize,
672}
673
674type LayoutChecked = Result<(), GeneratorIssue>;
675
676/// What a node is checked against: the picture's shorter side in canvas
677/// pixels, when known, and its parent's `display`.
678#[derive(Copy, Clone)]
679struct Context {
680    shorter_side: Option<f32>,
681    parent: Option<Display>,
682}
683
684/// Checks a layout tree: its limits, its structure, every value's range, and
685/// that animated values agree in kind between keyframes. Shape nodes' items
686/// are checked as a shapes generator's are, counted together. With the
687/// canvas (`(width, height)`), a pixel font size is checked against the
688/// largest a layout's text may be.
689///
690/// # Errors
691/// Returns the first problem in preorder.
692pub(crate) fn validate_layout(root: &LayoutNode, canvas: Option<(u32, u32)>) -> LayoutChecked {
693    let mut counts = Counts::default();
694    if root.has_item_field() {
695        return Err(LayoutIssue::new("root", LayoutProblem::RootItemField).into());
696    }
697    #[allow(clippy::cast_precision_loss)]
698    let shorter_side = canvas.map(|(width, height)| width.min(height) as f32);
699    let context = Context {
700        shorter_side,
701        parent: None,
702    };
703    validate_node(root, "root", 1, context, &mut counts)
704}
705
706fn validate_node(
707    node: &LayoutNode,
708    at: &str,
709    depth: usize,
710    context: Context,
711    counts: &mut Counts,
712) -> LayoutChecked {
713    let issue = |problem| Err(LayoutIssue::new(at, problem).into());
714    counts.nodes += 1;
715    if counts.nodes > MAX_LAYOUT_NODES {
716        return issue(LayoutProblem::TooManyNodes);
717    }
718    if depth > MAX_LAYOUT_DEPTH {
719        return issue(LayoutProblem::TooDeep);
720    }
721    let contents = usize::from(!node.children.is_empty())
722        + usize::from(node.text.is_some())
723        + usize::from(node.shape.is_some());
724    if contents > 1 {
725        return issue(LayoutProblem::MixedContent);
726    }
727    let leaf = node.text.is_some() || node.shape.is_some();
728    if leaf && node.display.is_some() {
729        return issue(LayoutProblem::DisplayOnLeaf);
730    }
731    if leaf && node.has_container_field() {
732        return issue(LayoutProblem::ContainerFieldOnLeaf);
733    }
734    let display = node.display.unwrap_or_default();
735    let mismatch = match display {
736        Display::Flex => node
737            .grid_container_field()
738            .map(LayoutProblem::GridFieldOnFlex),
739        Display::Grid => node
740            .flex_container_field()
741            .map(LayoutProblem::FlexFieldOnGrid),
742    };
743    if let Some(problem) = mismatch {
744        return issue(problem);
745    }
746    let item_mismatch = match context.parent {
747        Some(Display::Flex) => node
748            .grid_item_field()
749            .map(LayoutProblem::GridItemFieldInFlex),
750        Some(Display::Grid) => node
751            .flex_item_field()
752            .map(LayoutProblem::FlexItemFieldInGrid),
753        None => None,
754    };
755    if let Some(problem) = item_mismatch {
756        return issue(problem);
757    }
758    if node.text.is_some() {
759        counts.texts += 1;
760        if counts.texts > MAX_LAYOUT_TEXTS {
761            return issue(LayoutProblem::TooManyTexts);
762        }
763    }
764    if node.is_isolated() {
765        counts.isolated += 1;
766        if counts.isolated > MAX_LAYOUT_ISOLATED {
767            return issue(LayoutProblem::TooManyIsolated);
768        }
769    }
770    validate_fields(node, at)?;
771    validate_grid(node, at)?;
772    validate_style(node, at, context.shorter_side)?;
773    if let Some(text) = &node.text {
774        validate_text(text, &format!("{at}.text"))?;
775    }
776    if let Some(shape) = &node.shape {
777        crate::shapes::validate_items(
778            &shape.items,
779            &format!("{at}.shape.items"),
780            1,
781            &mut counts.shape_items,
782        )
783        .map_err(GeneratorIssue::Shape)?;
784    }
785    let inner = Context {
786        shorter_side: context.shorter_side,
787        parent: Some(display),
788    };
789    for (index, child) in node.children.iter().enumerate() {
790        validate_node(
791            child,
792            &format!("{at}.children[{index}]"),
793            depth + 1,
794            inner,
795            counts,
796        )?;
797    }
798    Ok(())
799}
800
801fn optional<T, E>(value: Option<&T>, check: impl FnOnce(&T) -> Result<(), E>) -> LayoutChecked
802where
803    GeneratorIssue: From<E>,
804{
805    Ok(value.map_or(Ok(()), check)?)
806}
807
808fn validate_fields(node: &LayoutNode, at: &str) -> LayoutChecked {
809    let field = |name: &str| format!("{at}.{name}");
810    for (name, gap) in [
811        ("gap", &node.gap),
812        ("row_gap", &node.row_gap),
813        ("column_gap", &node.column_gap),
814    ] {
815        optional(gap.as_ref(), |gap| size(gap, &field(name), non_negative))?;
816    }
817    optional(node.padding.as_ref(), |padding| {
818        insets(padding, &field("padding"), non_negative)
819    })?;
820    optional(node.background.as_ref(), |paint| {
821        validate_paint(paint, &field("background"))
822    })?;
823    if let Some(border) = &node.border {
824        size(&border.width, &field("border.width"), non_negative)?;
825        validate_paint(&border.paint, &field("border.paint"))?;
826    }
827    optional(node.border_radius.as_ref(), |radius| {
828        size(radius, &field("border_radius"), non_negative)
829    })?;
830    for (name, size) in [
831        ("width", &node.width),
832        ("height", &node.height),
833        ("min_width", &node.min_width),
834        ("min_height", &node.min_height),
835        ("max_width", &node.max_width),
836        ("max_height", &node.max_height),
837    ] {
838        optional(size.as_ref(), |size| {
839            length(size, &field(name), non_negative)
840        })?;
841    }
842    if node
843        .aspect_ratio
844        .is_some_and(|ratio| !ratio.is_finite() || ratio <= 0.0)
845    {
846        return Err(ValueIssue::new(field("aspect_ratio"), ValueProblem::OutOfRange).into());
847    }
848    optional(node.margin.as_ref(), |margin| {
849        insets(margin, &field("margin"), any)
850    })?;
851    optional(node.flex_grow.as_ref(), |grow| {
852        scalar(grow, &field("flex_grow"), non_negative)
853    })?;
854    optional(node.flex_shrink.as_ref(), |shrink| {
855        scalar(shrink, &field("flex_shrink"), non_negative)
856    })?;
857    optional(node.flex_basis.as_ref(), |basis| {
858        length(basis, &field("flex_basis"), non_negative)
859    })?;
860    if let Some(inset) = &node.inset {
861        for (name, side) in inset.sides() {
862            optional(side, |side| {
863                length(side, &field(&format!("inset.{name}")), any)
864            })?;
865        }
866    }
867    optional(node.opacity.as_ref(), |opacity| {
868        scalar(opacity, &field("opacity"), unit)
869    })?;
870    optional(node.translate.as_ref(), |translate| {
871        animated(translate, &field("translate"), |offset| {
872            offset.x.value().is_finite() && offset.y.value().is_finite()
873        })
874    })?;
875    optional(node.scale.as_ref(), |scale| {
876        vec2(scale, &field("scale"), |_| true)
877    })?;
878    optional(node.rotation_degrees.as_ref(), |rotation| {
879        scalar(rotation, &field("rotation_degrees"), any)
880    })
881}
882
883/// The grid fields: track counts and sizes, and placement lines.
884fn validate_grid(node: &LayoutNode, at: &str) -> LayoutChecked {
885    let issue = |name: &str, problem| Err(LayoutIssue::new(format!("{at}.{name}"), problem).into());
886    for (name, tracks, explicit) in [
887        ("grid_template_columns", &node.grid_template_columns, true),
888        ("grid_template_rows", &node.grid_template_rows, true),
889        ("grid_auto_columns", &node.grid_auto_columns, false),
890        ("grid_auto_rows", &node.grid_auto_rows, false),
891    ] {
892        let Some(tracks) = tracks else { continue };
893        if let Some(problem) = track_problem(tracks, explicit) {
894            return issue(name, problem);
895        }
896    }
897    for (name, placement) in [
898        ("grid_column", node.grid_column),
899        ("grid_row", node.grid_row),
900    ] {
901        let in_range = |edge: GridEdge| match edge {
902            GridEdge::Auto => true,
903            GridEdge::Line(line) => line != 0 && line.abs() <= MAX_GRID_LINE,
904            GridEdge::Span(count) => (1..=64).contains(&count),
905        };
906        if placement.is_some_and(|placement| !placement.edges().into_iter().all(in_range)) {
907            return issue(name, LayoutProblem::GridLineOutOfRange);
908        }
909    }
910    Ok(())
911}
912
913fn track_problem(tracks: &TrackList, explicit: bool) -> Option<LayoutProblem> {
914    let lengths_valid = tracks
915        .lengths()
916        .all(|length| length.value().is_finite() && length.value() >= 0.0);
917    let shares_valid = tracks
918        .shares()
919        .all(|share| share.is_finite() && share > 0.0);
920    if !(lengths_valid && shares_valid) {
921        return Some(LayoutProblem::OutOfRange);
922    }
923    let repeats = tracks.0.iter().filter_map(|component| match component {
924        TrackComponent::Repeat(count, _) => Some(*count),
925        TrackComponent::Single(_) => None,
926    });
927    if !explicit && repeats.clone().next().is_some() {
928        return Some(LayoutProblem::RepeatInAutoTracks);
929    }
930    if repeats
931        .clone()
932        .any(|count| matches!(count, RepeatCount::Count(count) if !(1..=64).contains(&count)))
933    {
934        return Some(LayoutProblem::RepeatCount);
935    }
936    if tracks.track_count() > MAX_GRID_TRACKS {
937        return Some(LayoutProblem::TooManyTracks);
938    }
939    let automatic = repeats
940        .filter(|count| !matches!(count, RepeatCount::Count(_)))
941        .count();
942    if automatic > 1
943        || (automatic == 1
944            && !tracks
945                .sizes()
946                .all(crate::layout_values::TrackSize::is_fixed))
947    {
948        return Some(LayoutProblem::AutoRepeatNeedsFixedTracks);
949    }
950    None
951}
952
953/// The inherited text style a node writes, where it writes it.
954fn validate_style(node: &LayoutNode, at: &str, shorter_side: Option<f32>) -> LayoutChecked {
955    let out_of_range = |name: &str| {
956        Err(LayoutIssue::new(format!("{at}.{name}"), LayoutProblem::OutOfRange).into())
957    };
958    if node
959        .font_family
960        .as_ref()
961        .is_some_and(|family| family.is_empty() || family.len() > MAX_FONT_FAMILY_BYTES)
962    {
963        return out_of_range("font_family");
964    }
965    optional(node.font_size.as_ref(), |font_size| {
966        animated(font_size, &format!("{at}.font_size"), |value| {
967            // Pixels compare with the limit on a known canvas only.
968            let fraction = match (*value, shorter_side) {
969                (LayoutSize::Px(_), None) => 0.0,
970                (value, Some(side)) => value.fraction(side),
971                (value, None) => value.value(),
972            };
973            value.value().is_finite() && value.value() > 0.0 && fraction <= MAX_LAYOUT_FONT_SIZE
974        })
975    })?;
976    if node.color.is_some_and(|color| !color.is_valid()) {
977        return out_of_range("color");
978    }
979    if node
980        .line_height
981        .is_some_and(|height| !LINE_HEIGHT_RANGE.contains(&height))
982    {
983        return out_of_range("line_height");
984    }
985    if node
986        .letter_spacing
987        .is_some_and(|spacing| !LETTER_SPACING_RANGE.contains(&spacing))
988    {
989        return out_of_range("letter_spacing");
990    }
991    if node
992        .font_weight
993        .is_some_and(|weight| !FONT_WEIGHT_RANGE.contains(&weight))
994    {
995        return out_of_range("font_weight");
996    }
997    let valid_outline = |outline: TextOutline| {
998        outline.color.is_valid() && outline.width > 0.0 && outline.width <= MAX_TEXT_OUTLINE_WIDTH
999    };
1000    if node.outline.is_some_and(|outline| !valid_outline(outline)) {
1001        return out_of_range("outline");
1002    }
1003    Ok(())
1004}
1005
1006fn insets(animation: &Animated<Insets>, at: &str, check: fn(f32) -> bool) -> Checked {
1007    animated(animation, at, |value| {
1008        value
1009            .sides()
1010            .iter()
1011            .all(|side| side.value().is_finite() && check(side.value()))
1012    })
1013}
1014
1015fn size(animation: &Animated<LayoutSize>, at: &str, check: fn(f32) -> bool) -> Checked {
1016    animated(animation, at, |value| {
1017        value.value().is_finite() && check(value.value())
1018    })
1019}
1020
1021/// A length's values, and that its keyframes share a kind: a percent, or a
1022/// length (a fraction or pixels, both lengths once planned).
1023fn length(
1024    animation: &Animated<LayoutLength>,
1025    at: &str,
1026    check: fn(f32) -> bool,
1027) -> Result<(), GeneratorIssue> {
1028    animated(animation, at, |value| {
1029        value.value().is_finite() && check(value.value())
1030    })?;
1031    if let Animated::Keyframes { keyframes } = animation {
1032        let kind = keyframes[0].value.is_percent();
1033        if keyframes.iter().any(|key| key.value.is_percent() != kind) {
1034            return Err(LayoutIssue::new(at, LayoutProblem::LengthKeyframesDiffer).into());
1035        }
1036    }
1037    Ok(())
1038}
1039
1040fn validate_text(content: &Animated<String>, at: &str) -> LayoutChecked {
1041    animated(content, at, |_| true)?;
1042    if let Animated::Keyframes { keyframes } = content
1043        && keyframes
1044            .iter()
1045            .any(|key| key.interpolation != Interpolation::Hold)
1046    {
1047        return Err(LayoutIssue::new(at, LayoutProblem::TextKeyframesInterpolate).into());
1048    }
1049    animated(content, at, |content| {
1050        content.len() <= MAX_TEXT_CONTENT_BYTES
1051    })?;
1052    Ok(())
1053}
1054
1055impl LayoutNode {
1056    /// Whether any container field is present.
1057    fn has_container_field(&self) -> bool {
1058        self.flex_container_field().is_some()
1059            || self.grid_container_field().is_some()
1060            || self.gap.is_some()
1061            || self.row_gap.is_some()
1062            || self.column_gap.is_some()
1063            || self.justify_content.is_some()
1064            || self.align_items.is_some()
1065            || self.align_content.is_some()
1066    }
1067
1068    /// The first flex container field present.
1069    fn flex_container_field(&self) -> Option<&'static str> {
1070        [
1071            ("flex_direction", self.flex_direction.is_some()),
1072            ("flex_wrap", self.flex_wrap.is_some()),
1073        ]
1074        .into_iter()
1075        .find_map(|(name, present)| present.then_some(name))
1076    }
1077
1078    /// The first grid container field present.
1079    fn grid_container_field(&self) -> Option<&'static str> {
1080        [
1081            (
1082                "grid_template_columns",
1083                self.grid_template_columns.is_some(),
1084            ),
1085            ("grid_template_rows", self.grid_template_rows.is_some()),
1086            ("grid_auto_columns", self.grid_auto_columns.is_some()),
1087            ("grid_auto_rows", self.grid_auto_rows.is_some()),
1088            ("grid_auto_flow", self.grid_auto_flow.is_some()),
1089            ("justify_items", self.justify_items.is_some()),
1090        ]
1091        .into_iter()
1092        .find_map(|(name, present)| present.then_some(name))
1093    }
1094
1095    /// The first flex item field present.
1096    fn flex_item_field(&self) -> Option<&'static str> {
1097        [
1098            ("flex_grow", self.flex_grow.is_some()),
1099            ("flex_shrink", self.flex_shrink.is_some()),
1100            ("flex_basis", self.flex_basis.is_some()),
1101        ]
1102        .into_iter()
1103        .find_map(|(name, present)| present.then_some(name))
1104    }
1105
1106    /// The first grid item field present.
1107    fn grid_item_field(&self) -> Option<&'static str> {
1108        [
1109            ("grid_column", self.grid_column.is_some()),
1110            ("grid_row", self.grid_row.is_some()),
1111            ("justify_self", self.justify_self.is_some()),
1112        ]
1113        .into_iter()
1114        .find_map(|(name, present)| present.then_some(name))
1115    }
1116
1117    /// Whether any field the root may not have is present.
1118    fn has_item_field(&self) -> bool {
1119        self.width.is_some()
1120            || self.height.is_some()
1121            || self.min_width.is_some()
1122            || self.min_height.is_some()
1123            || self.max_width.is_some()
1124            || self.max_height.is_some()
1125            || self.aspect_ratio.is_some()
1126            || self.margin.is_some()
1127            || self.flex_item_field().is_some()
1128            || self.grid_item_field().is_some()
1129            || self.align_self.is_some()
1130            || self.position.is_some()
1131            || self.inset.is_some()
1132    }
1133
1134    /// Whether the node has a visual field: opacity, translate, scale, or
1135    /// rotation.
1136    #[must_use]
1137    pub const fn has_visual(&self) -> bool {
1138        self.opacity.is_some()
1139            || self.translate.is_some()
1140            || self.scale.is_some()
1141            || self.rotation_degrees.is_some()
1142    }
1143
1144    /// Whether the node is drawn into its own composite: it has a visual
1145    /// field or hidden overflow. Presence decides, not values.
1146    #[must_use]
1147    pub const fn is_isolated(&self) -> bool {
1148        self.has_visual() || matches!(self.overflow, Some(Overflow::Hidden))
1149    }
1150
1151    /// Whether the node, its subtree, or a shape in it changes over time.
1152    #[must_use]
1153    pub fn is_animated(&self) -> bool {
1154        let inset = self.inset.as_ref().is_some_and(|inset| {
1155            inset
1156                .sides()
1157                .into_iter()
1158                .any(|(_, value)| keyed_option(value))
1159        });
1160        let shape = self
1161            .shape
1162            .as_ref()
1163            .is_some_and(|shape| shape.items.iter().any(ShapeItem::is_animated));
1164        let border = self
1165            .border
1166            .as_ref()
1167            .is_some_and(|border| keyed(&border.width) || keyed(&border.paint));
1168        [
1169            &self.gap,
1170            &self.row_gap,
1171            &self.column_gap,
1172            &self.border_radius,
1173            &self.font_size,
1174        ]
1175        .into_iter()
1176        .any(|value| keyed_option(value.as_ref()))
1177            || keyed_option(self.padding.as_ref())
1178            || keyed_option(self.background.as_ref())
1179            || border
1180            || [
1181                &self.width,
1182                &self.height,
1183                &self.min_width,
1184                &self.min_height,
1185                &self.max_width,
1186                &self.max_height,
1187                &self.flex_basis,
1188            ]
1189            .into_iter()
1190            .any(|value| keyed_option(value.as_ref()))
1191            || keyed_option(self.margin.as_ref())
1192            || keyed_option(self.flex_grow.as_ref())
1193            || keyed_option(self.flex_shrink.as_ref())
1194            || inset
1195            || keyed_option(self.opacity.as_ref())
1196            || keyed_option(self.translate.as_ref())
1197            || keyed_option(self.scale.as_ref())
1198            || keyed_option(self.rotation_degrees.as_ref())
1199            || keyed_option(self.text.as_ref())
1200            || shape
1201            || self.children.iter().any(Self::is_animated)
1202    }
1203
1204    /// Every node of the tree in preorder, with its path from the root.
1205    pub fn walk(&self, mut visit: impl FnMut(&str, &Self)) {
1206        fn go(node: &LayoutNode, at: &str, visit: &mut impl FnMut(&str, &LayoutNode)) {
1207            visit(at, node);
1208            for (index, child) in node.children.iter().enumerate() {
1209                go(child, &format!("{at}.children[{index}]"), visit);
1210            }
1211        }
1212        go(self, "root", &mut visit);
1213    }
1214
1215    /// Calls `visit` with each animated value of the tree that has keyframes,
1216    /// by its path (`root.children[0].opacity`,
1217    /// `root.shape.items[0].transform.scale`), and their times, in document
1218    /// order.
1219    pub(crate) fn for_each_keyed(
1220        &self,
1221        at: &str,
1222        visit: &mut impl FnMut(&str, &[alpha2_time::MediaTime]),
1223    ) {
1224        let field = |name: &str| format!("{at}.{name}");
1225        if let Some(text) = &self.text {
1226            visit_keyed(&field("text"), text, visit);
1227        }
1228        if let Some(shape) = &self.shape {
1229            crate::shapes::for_each_keyed_item(&shape.items, &field("shape.items"), visit);
1230        }
1231        for (name, value) in [
1232            ("gap", &self.gap),
1233            ("row_gap", &self.row_gap),
1234            ("column_gap", &self.column_gap),
1235            ("border_radius", &self.border_radius),
1236            ("font_size", &self.font_size),
1237        ] {
1238            visit_option(&field(name), value.as_ref(), visit);
1239        }
1240        visit_option(&field("padding"), self.padding.as_ref(), visit);
1241        visit_option(&field("background"), self.background.as_ref(), visit);
1242        if let Some(border) = &self.border {
1243            visit_keyed(&field("border.width"), &border.width, visit);
1244            visit_keyed(&field("border.paint"), &border.paint, visit);
1245        }
1246        for (name, value) in [
1247            ("width", &self.width),
1248            ("height", &self.height),
1249            ("min_width", &self.min_width),
1250            ("min_height", &self.min_height),
1251            ("max_width", &self.max_width),
1252            ("max_height", &self.max_height),
1253            ("flex_basis", &self.flex_basis),
1254        ] {
1255            visit_option(&field(name), value.as_ref(), visit);
1256        }
1257        visit_option(&field("margin"), self.margin.as_ref(), visit);
1258        visit_option(&field("flex_grow"), self.flex_grow.as_ref(), visit);
1259        visit_option(&field("flex_shrink"), self.flex_shrink.as_ref(), visit);
1260        if let Some(inset) = &self.inset {
1261            for (side, value) in inset.sides() {
1262                visit_option(&field(&format!("inset.{side}")), value, visit);
1263            }
1264        }
1265        visit_option(&field("opacity"), self.opacity.as_ref(), visit);
1266        visit_option(&field("translate"), self.translate.as_ref(), visit);
1267        visit_option(&field("scale"), self.scale.as_ref(), visit);
1268        visit_option(
1269            &field("rotation_degrees"),
1270            self.rotation_degrees.as_ref(),
1271            visit,
1272        );
1273        for (index, child) in self.children.iter().enumerate() {
1274            child.for_each_keyed(&format!("{at}.children[{index}]"), visit);
1275        }
1276    }
1277
1278    /// The tree with every pixel length turned into a fraction of a picture
1279    /// `shorter_side` pixels on its shorter side: what a plan evaluates, so
1280    /// that pixels and fractions interpolate as one kind.
1281    #[must_use]
1282    pub fn without_px(&self, shorter_side: f32) -> Self {
1283        let mut node = self.clone();
1284        node.convert_px(shorter_side);
1285        node
1286    }
1287
1288    fn convert_px(&mut self, shorter_side: f32) {
1289        let size = |value: &mut Option<Animated<LayoutSize>>| {
1290            if let Some(value) = value {
1291                *value = value.map(|size| size.without_px(shorter_side));
1292            }
1293        };
1294        let length = |value: &mut Option<Animated<LayoutLength>>| {
1295            if let Some(value) = value {
1296                *value = value.map(|length| length.without_px(shorter_side));
1297            }
1298        };
1299        let insets = |value: &mut Option<Animated<Insets>>| {
1300            if let Some(value) = value {
1301                *value = value.map(|insets| insets.without_px(shorter_side));
1302            }
1303        };
1304        let tracks = |value: &mut Option<TrackList>| {
1305            if let Some(tracks) = value {
1306                *tracks = tracks.without_px(shorter_side);
1307            }
1308        };
1309        tracks(&mut self.grid_template_columns);
1310        tracks(&mut self.grid_template_rows);
1311        tracks(&mut self.grid_auto_columns);
1312        tracks(&mut self.grid_auto_rows);
1313        for value in [
1314            &mut self.gap,
1315            &mut self.row_gap,
1316            &mut self.column_gap,
1317            &mut self.border_radius,
1318            &mut self.font_size,
1319        ] {
1320            size(value);
1321        }
1322        insets(&mut self.padding);
1323        insets(&mut self.margin);
1324        if let Some(border) = &mut self.border {
1325            border.width = border.width.map(|width| width.without_px(shorter_side));
1326        }
1327        for value in [
1328            &mut self.width,
1329            &mut self.height,
1330            &mut self.min_width,
1331            &mut self.min_height,
1332            &mut self.max_width,
1333            &mut self.max_height,
1334            &mut self.flex_basis,
1335        ] {
1336            length(value);
1337        }
1338        if let Some(inset) = &mut self.inset {
1339            for side in [
1340                &mut inset.left,
1341                &mut inset.top,
1342                &mut inset.right,
1343                &mut inset.bottom,
1344            ] {
1345                length(side);
1346            }
1347        }
1348        if let Some(translate) = &mut self.translate {
1349            *translate = translate.map(|offset| offset.without_px(shorter_side));
1350        }
1351        for child in &mut self.children {
1352            child.convert_px(shorter_side);
1353        }
1354    }
1355}
1356
1357fn visit_option<T>(
1358    at: &str,
1359    animation: Option<&Animated<T>>,
1360    visit: &mut impl FnMut(&str, &[alpha2_time::MediaTime]),
1361) {
1362    if let Some(animation) = animation {
1363        visit_keyed(at, animation, visit);
1364    }
1365}
1366
1367const fn keyed<T>(animation: &Animated<T>) -> bool {
1368    matches!(animation, Animated::Keyframes { .. })
1369}
1370
1371const fn keyed_option<T>(animation: Option<&Animated<T>>) -> bool {
1372    matches!(animation, Some(Animated::Keyframes { .. }))
1373}