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