Skip to main content

alpha2_model/
layout.rs

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