Skip to main content

alpha2_eval/
layout.rs

1//! A layout generator's box tree evaluated at a time.
2//!
3//! A [`LayoutPlan`] is built with the index for the project canvas. It converts pixel lengths
4//! to fractions of the canvas's shorter side and pre-evaluates a tree without keyframes, so every
5//! frame shares one value and key. Text style is inherited during evaluation.
6
7use crate::ShapesPlan;
8use crate::animation::{
9    sample_hold, sample_insets, sample_length, sample_offset, sample_paint, sample_scalar,
10    sample_size, sample_vec2,
11};
12use alpha2_geometry::StableHash;
13use alpha2_model::{
14    AlignContent, AlignItems, Animated, Display, FlexDirection, FlexWrap, GridAutoFlow,
15    GridPlacement, JustifyContent, LAYOUT_TEXT_DEFAULTS, LayoutLength, LayoutNode, LayoutTextStyle,
16    Overflow, Paint, Position, Rgba, TextAlign, TextShadow, TextStroke, TrackList, Vec2,
17};
18use alpha2_time::MediaTime;
19use std::sync::Arc;
20
21/// A layout tree, evaluated once when it does not animate.
22#[derive(Clone, Debug)]
23pub struct LayoutPlan {
24    /// The tree with its pixels turned into fractions.
25    root: Box<LayoutNode>,
26    /// Each shape node's plan, in preorder, with a digest of its items.
27    shapes: Vec<(Arc<ShapesPlan>, u128)>,
28    /// Whether a shape node animates, making the time part of the key.
29    shapes_animate: bool,
30    /// The whole tree, when nothing in it animates.
31    fixed: Option<Arc<EvaluatedLayout>>,
32}
33
34impl PartialEq for LayoutPlan {
35    fn eq(&self, other: &Self) -> bool {
36        self.root == other.root
37    }
38}
39
40impl LayoutPlan {
41    /// Plans `root` for a canvas of `(width, height)` pixels.
42    #[must_use]
43    pub fn new(root: &LayoutNode, canvas: (u32, u32)) -> Self {
44        #[allow(clippy::cast_precision_loss)]
45        let shorter_side = canvas.0.min(canvas.1).max(1) as f32;
46        let root = root.without_px(shorter_side);
47        let mut shapes = Vec::new();
48        root.walk(|_, node| {
49            if let Some(shape) = &node.shape {
50                let mut hash = StableHash::new();
51                hash.bytes(format!("{:?}", shape.items).as_bytes());
52                shapes.push((Arc::new(ShapesPlan::new(&shape.items)), hash.finish()));
53            }
54        });
55        let shapes_animate = shapes.iter().any(|(plan, _)| plan.is_animated());
56        let animated = root.is_animated();
57        let mut plan = Self {
58            root: Box::new(root),
59            shapes,
60            shapes_animate,
61            fixed: None,
62        };
63        if !animated {
64            plan.fixed = Some(Arc::new(plan.evaluate_now(MediaTime::ZERO)));
65        }
66        plan
67    }
68
69    /// The tree at picture time `time`.
70    #[must_use]
71    pub fn evaluate(&self, time: MediaTime) -> Arc<EvaluatedLayout> {
72        self.fixed
73            .clone()
74            .unwrap_or_else(|| Arc::new(self.evaluate_now(time)))
75    }
76
77    /// Whether anything in the tree animates.
78    #[must_use]
79    pub fn is_animated(&self) -> bool {
80        self.fixed.is_none()
81    }
82
83    /// The model tree with pixels converted to fractions of the canvas's shorter side.
84    #[must_use]
85    pub const fn root(&self) -> &LayoutNode {
86        &self.root
87    }
88
89    fn evaluate_now(&self, time: MediaTime) -> EvaluatedLayout {
90        let mut shapes = self.shapes.iter();
91        let root = evaluate_node(&self.root, time, &mut shapes, &LAYOUT_TEXT_DEFAULTS);
92        let mut hash = StableHash::new();
93        hash_node(&mut hash, &root, &self.shapes);
94        if self.shapes_animate {
95            hash.u64(time.ticks().cast_unsigned());
96        }
97        EvaluatedLayout {
98            root,
99            time,
100            key: hash.finish(),
101        }
102    }
103}
104
105/// A layout tree with every value sampled.
106#[derive(Clone, Debug, PartialEq)]
107pub struct EvaluatedLayout {
108    /// The root box: the whole picture.
109    pub root: EvaluatedNode,
110    /// The picture time shape nodes are drawn at.
111    pub time: MediaTime,
112    /// Equal for trees that lay out and draw alike (covers `time` only when a shape animates).
113    pub key: u128,
114}
115
116/// One box with its values sampled. Lengths are fractions of the picture's shorter side.
117#[derive(Clone, Debug, PartialEq)]
118pub struct EvaluatedNode {
119    /// What flexbox or grid reads.
120    pub layout: NodeLayout,
121    /// What is drawn under the content.
122    pub paint: NodePaint,
123    /// Present when the node has any visual field, with defaults filled.
124    pub visual: Option<NodeVisual>,
125    /// Whether content past the padding box is clipped.
126    pub clip: bool,
127    /// Children, text, a shape, or nothing.
128    pub content: NodeContent,
129}
130
131impl EvaluatedNode {
132    /// Whether the node is drawn into its own composite.
133    #[must_use]
134    pub const fn is_isolated(&self) -> bool {
135        self.visual.is_some() || self.clip
136    }
137}
138
139/// A length on each side of a box, as a fraction of the picture's shorter side.
140#[derive(Copy, Clone, Debug, Default, PartialEq)]
141#[allow(missing_docs)]
142pub struct Sides {
143    pub top: f32,
144    pub right: f32,
145    pub bottom: f32,
146    pub left: f32,
147}
148
149impl Sides {
150    const fn from_array([top, right, bottom, left]: [f32; 4]) -> Self {
151        Self {
152            top,
153            right,
154            bottom,
155            left,
156        }
157    }
158
159    const fn to_array(self) -> [f32; 4] {
160        [self.top, self.right, self.bottom, self.left]
161    }
162}
163
164/// A box's sampled box-model, flex and grid values. Lengths are fractions or percents.
165#[derive(Clone, Debug, PartialEq)]
166#[allow(missing_docs)]
167pub struct NodeLayout {
168    pub display: Display,
169    pub flex_direction: FlexDirection,
170    pub flex_wrap: FlexWrap,
171    pub grid_template_columns: Option<TrackList>,
172    pub grid_template_rows: Option<TrackList>,
173    pub grid_auto_columns: Option<TrackList>,
174    pub grid_auto_rows: Option<TrackList>,
175    pub grid_auto_flow: GridAutoFlow,
176    /// The gap between rows: `row_gap`, else `gap`.
177    pub row_gap: f32,
178    /// The gap between columns: `column_gap`, else `gap`.
179    pub column_gap: f32,
180    pub justify_content: JustifyContent,
181    pub align_items: AlignItems,
182    pub align_content: AlignContent,
183    pub justify_items: AlignItems,
184    pub padding: Sides,
185    /// The border's width; zero without one.
186    pub border_width: f32,
187    pub overflow: Overflow,
188    pub width: Option<LayoutLength>,
189    pub height: Option<LayoutLength>,
190    pub min_width: Option<LayoutLength>,
191    pub min_height: Option<LayoutLength>,
192    pub max_width: Option<LayoutLength>,
193    pub max_height: Option<LayoutLength>,
194    pub aspect_ratio: Option<f32>,
195    pub margin: Sides,
196    pub flex_grow: f32,
197    pub flex_shrink: f32,
198    pub flex_basis: Option<LayoutLength>,
199    pub align_self: Option<AlignItems>,
200    pub justify_self: Option<AlignItems>,
201    pub grid_column: GridPlacement,
202    pub grid_row: GridPlacement,
203    pub position: Position,
204    /// Left, top, right, bottom; `None` is auto.
205    pub inset: [Option<LayoutLength>; 4],
206}
207
208/// What a box draws under its content.
209#[derive(Clone, Debug, PartialEq)]
210pub struct NodePaint {
211    /// What covers the border box.
212    pub background: Option<Paint>,
213    /// The border's width and paint.
214    pub border: Option<(f32, Paint)>,
215    /// The border box's corner radius.
216    pub border_radius: f32,
217}
218
219/// How a box and its subtree are drawn after layout.
220#[derive(Copy, Clone, Debug, PartialEq)]
221pub struct NodeVisual {
222    /// Group opacity, in `0..=1`.
223    pub opacity: f32,
224    /// Offset, a fraction of the picture's shorter side.
225    pub translate: Vec2,
226    /// Per-axis scale about the border-box centre.
227    pub scale: Vec2,
228    /// Clockwise rotation about the border-box centre, in degrees.
229    pub rotation_degrees: f32,
230}
231
232/// A box's content.
233#[derive(Clone, Debug, PartialEq)]
234pub enum NodeContent {
235    /// Nothing: a spacer or a decoration.
236    Empty,
237    /// Boxes laid out in this one, in document order.
238    Children(Vec<EvaluatedNode>),
239    /// One paragraph of text.
240    Text(EvaluatedText),
241    /// Shapes, evaluated at draw time because spatial curves need the post-layout aspect.
242    Shape(Arc<ShapesPlan>),
243}
244
245/// A text node's sampled text and the style it inherits.
246#[derive(Clone, Debug, PartialEq)]
247pub struct EvaluatedText {
248    /// The text; may be empty.
249    pub content: String,
250    /// The family it draws in.
251    pub font_family: Option<String>,
252    /// Font size, a fraction of the picture's shorter side; positive.
253    pub font_size: f32,
254    /// What paints the glyphs, a gradient over the text's layout box.
255    pub fill: Paint,
256    /// Baseline-to-baseline distance in font sizes.
257    pub line_height: f32,
258    /// Space after each character, a fraction of the font size.
259    pub letter_spacing: f32,
260    /// The weight its faces are matched by.
261    pub font_weight: u16,
262    /// How lines align in the content width.
263    pub text_align: TextAlign,
264    /// Bands around the glyphs, innermost first.
265    pub strokes: Vec<TextStroke>,
266    /// Shadows of the glyphs and strokes, the first on top.
267    pub shadows: Vec<TextShadow>,
268    /// The node's text animators at the layout's picture time.
269    pub animators: Vec<crate::SampledAnimator>,
270}
271
272fn non_negative(animation: Option<&Animated<f32>>, time: MediaTime, default: f32) -> f32 {
273    animation.map_or(default, |value| sample_scalar(value, time).max(0.0))
274}
275
276fn size_or(
277    animation: Option<&Animated<alpha2_model::LayoutSize>>,
278    time: MediaTime,
279    default: f32,
280) -> f32 {
281    animation.map_or(default, |value| sample_size(value, time).max(0.0))
282}
283
284fn length(animation: Option<&Animated<LayoutLength>>, time: MediaTime) -> Option<LayoutLength> {
285    animation.map(|value| sample_length(value, time))
286}
287
288fn size(animation: Option<&Animated<LayoutLength>>, time: MediaTime) -> Option<LayoutLength> {
289    length(animation, time).map(|length| length.with_value(length.value().max(0.0)))
290}
291
292fn node_layout(node: &LayoutNode, time: MediaTime, border_width: f32) -> NodeLayout {
293    let insets = |value: Option<&Animated<alpha2_model::Insets>>| {
294        value.map_or_else(Sides::default, |value| {
295            Sides::from_array(sample_insets(value, time))
296        })
297    };
298    let padding = Sides::from_array(
299        insets(node.padding.as_ref())
300            .to_array()
301            .map(|side| side.max(0.0)),
302    );
303    let inset = node.inset.as_ref().map_or([None; 4], |inset| {
304        [
305            length(inset.left.as_ref(), time),
306            length(inset.top.as_ref(), time),
307            length(inset.right.as_ref(), time),
308            length(inset.bottom.as_ref(), time),
309        ]
310    });
311    let gap = size_or(node.gap.as_ref(), time, 0.0);
312    NodeLayout {
313        display: node.display.unwrap_or_default(),
314        flex_direction: node.flex_direction.unwrap_or_default(),
315        flex_wrap: node.flex_wrap.unwrap_or_default(),
316        grid_template_columns: node.grid_template_columns.clone(),
317        grid_template_rows: node.grid_template_rows.clone(),
318        grid_auto_columns: node.grid_auto_columns.clone(),
319        grid_auto_rows: node.grid_auto_rows.clone(),
320        grid_auto_flow: node.grid_auto_flow.unwrap_or_default(),
321        row_gap: size_or(node.row_gap.as_ref(), time, gap),
322        column_gap: size_or(node.column_gap.as_ref(), time, gap),
323        justify_content: node.justify_content.unwrap_or_default(),
324        align_items: node.align_items.unwrap_or_default(),
325        align_content: node.align_content.unwrap_or_default(),
326        justify_items: node.justify_items.unwrap_or_default(),
327        padding,
328        border_width,
329        overflow: node.overflow.unwrap_or_default(),
330        width: size(node.width.as_ref(), time),
331        height: size(node.height.as_ref(), time),
332        min_width: size(node.min_width.as_ref(), time),
333        min_height: size(node.min_height.as_ref(), time),
334        max_width: size(node.max_width.as_ref(), time),
335        max_height: size(node.max_height.as_ref(), time),
336        aspect_ratio: node.aspect_ratio,
337        margin: insets(node.margin.as_ref()),
338        flex_grow: non_negative(node.flex_grow.as_ref(), time, 0.0),
339        flex_shrink: non_negative(node.flex_shrink.as_ref(), time, 1.0),
340        flex_basis: size(node.flex_basis.as_ref(), time),
341        align_self: node.align_self,
342        justify_self: node.justify_self,
343        grid_column: node.grid_column.unwrap_or_default(),
344        grid_row: node.grid_row.unwrap_or_default(),
345        position: node.position.unwrap_or_default(),
346        inset,
347    }
348}
349
350/// The text style `node` passes to its text and children, sampled at `time`.
351fn text_style(node: &LayoutNode, time: MediaTime, inherited: &LayoutTextStyle) -> LayoutTextStyle {
352    LayoutTextStyle {
353        font_family: node
354            .font_family
355            .clone()
356            .or_else(|| inherited.font_family.clone()),
357        font_size: node.font_size.as_ref().map_or(inherited.font_size, |size| {
358            sample_size(size, time).max(f32::MIN_POSITIVE)
359        }),
360        fill: node.fill.clone().unwrap_or_else(|| inherited.fill.clone()),
361        line_height: node.line_height.unwrap_or(inherited.line_height),
362        letter_spacing: node.letter_spacing.unwrap_or(inherited.letter_spacing),
363        font_weight: node.font_weight.unwrap_or(inherited.font_weight),
364        text_align: node.text_align.unwrap_or(inherited.text_align),
365        strokes: node
366            .strokes
367            .clone()
368            .unwrap_or_else(|| inherited.strokes.clone()),
369        shadows: node
370            .shadows
371            .clone()
372            .unwrap_or_else(|| inherited.shadows.clone()),
373    }
374}
375
376fn evaluate_node<'a>(
377    node: &LayoutNode,
378    time: MediaTime,
379    shapes: &mut impl Iterator<Item = &'a (Arc<ShapesPlan>, u128)>,
380    inherited: &LayoutTextStyle,
381) -> EvaluatedNode {
382    let border = node.border.as_ref().map(|border| {
383        (
384            sample_size(&border.width, time).max(0.0),
385            sample_paint(&border.paint, time),
386        )
387    });
388    let layout = node_layout(node, time, border.as_ref().map_or(0.0, |(width, _)| *width));
389    let paint = NodePaint {
390        background: node
391            .background
392            .as_ref()
393            .map(|paint| sample_paint(paint, time)),
394        border,
395        border_radius: size_or(node.border_radius.as_ref(), time, 0.0),
396    };
397    let visual = node.has_visual().then(|| NodeVisual {
398        opacity: node
399            .opacity
400            .as_ref()
401            .map_or(1.0, |value| sample_scalar(value, time).clamp(0.0, 1.0)),
402        translate: node
403            .translate
404            .as_ref()
405            .map_or_else(Vec2::default, |value| sample_offset(value, time)),
406        scale: node
407            .scale
408            .as_ref()
409            .map_or(Vec2::new(1.0, 1.0), |value| sample_vec2(value, time)),
410        rotation_degrees: node
411            .rotation_degrees
412            .as_ref()
413            .map_or(0.0, |value| sample_scalar(value, time)),
414    });
415    let style = text_style(node, time, inherited);
416    let content = if let Some(text) = &node.text {
417        NodeContent::Text(EvaluatedText {
418            content: sample_hold(text, time),
419            font_family: style.font_family,
420            font_size: style.font_size,
421            fill: style.fill.clone(),
422            line_height: style.line_height,
423            letter_spacing: style.letter_spacing,
424            font_weight: style.font_weight,
425            text_align: style.text_align,
426            strokes: style.strokes.clone(),
427            shadows: style.shadows.clone(),
428            animators: crate::sample_text_animators(&node.text_animators, time),
429        })
430    } else if node.shape.is_some() {
431        let (plan, _) = shapes.next().expect("one plan per shape node");
432        NodeContent::Shape(Arc::clone(plan))
433    } else if node.children.is_empty() {
434        NodeContent::Empty
435    } else {
436        NodeContent::Children(
437            node.children
438                .iter()
439                .map(|child| evaluate_node(child, time, shapes, &style))
440                .collect(),
441        )
442    };
443    EvaluatedNode {
444        layout,
445        paint,
446        visual,
447        clip: matches!(node.overflow, Some(Overflow::Hidden)),
448        content,
449    }
450}
451
452fn hash_length(hash: &mut StableHash, length: Option<LayoutLength>) {
453    match length {
454        None => hash.u32(0),
455        Some(LayoutLength::Fraction(value)) => {
456            hash.u32(1);
457            hash.f32(value);
458        }
459        Some(LayoutLength::Percent(value)) => {
460            hash.u32(2);
461            hash.f32(value);
462        }
463        Some(LayoutLength::Px(value)) => {
464            hash.u32(3);
465            hash.f32(value);
466        }
467    }
468}
469
470fn hash_sides(hash: &mut StableHash, sides: Sides) {
471    for side in sides.to_array() {
472        hash.f32(side);
473    }
474}
475
476fn hash_color(hash: &mut StableHash, color: Rgba) {
477    for component in [color.red, color.green, color.blue, color.alpha] {
478        hash.f32(component);
479    }
480}
481
482fn hash_paint(hash: &mut StableHash, paint: Option<&Paint>) {
483    let stops = |hash: &mut StableHash, stops: &[alpha2_model::GradientStop]| {
484        hash.u64(stops.len() as u64);
485        for stop in stops {
486            hash.f32(stop.offset);
487            hash_color(hash, stop.color);
488        }
489    };
490    match paint {
491        None => hash.u32(0),
492        Some(Paint::Solid { color }) => {
493            hash.u32(1);
494            hash_color(hash, *color);
495        }
496        Some(Paint::LinearGradient {
497            start,
498            end,
499            stops: list,
500            spread,
501        }) => {
502            hash.u32(2);
503            for value in [start.x, start.y, end.x, end.y] {
504                hash.f32(value);
505            }
506            stops(hash, list);
507            hash.u32(*spread as u32);
508        }
509        Some(Paint::RadialGradient {
510            center,
511            radius,
512            stops: list,
513            spread,
514        }) => {
515            hash.u32(3);
516            for value in [center.x, center.y, *radius] {
517                hash.f32(value);
518            }
519            stops(hash, list);
520            hash.u32(*spread as u32);
521        }
522        Some(Paint::ConicGradient {
523            center,
524            angle_degrees,
525            stops: list,
526        }) => {
527            hash.u32(4);
528            for value in [center.x, center.y, *angle_degrees] {
529                hash.f32(value);
530            }
531            stops(hash, list);
532        }
533    }
534}
535
536fn hash_node(hash: &mut StableHash, node: &EvaluatedNode, shapes: &[(Arc<ShapesPlan>, u128)]) {
537    let mut shape_index = 0;
538    hash_node_in(hash, node, shapes, &mut shape_index);
539}
540
541fn hash_layout(hash: &mut StableHash, layout: &NodeLayout) {
542    for value in [
543        layout.display as u32,
544        layout.flex_direction as u32,
545        layout.flex_wrap as u32,
546        layout.grid_auto_flow as u32,
547        layout.justify_content as u32,
548        layout.align_items as u32,
549        layout.align_content as u32,
550        layout.justify_items as u32,
551        layout.overflow as u32,
552        layout.position as u32,
553        layout.align_self.map_or(u32::MAX, |align| align as u32),
554        layout.justify_self.map_or(u32::MAX, |align| align as u32),
555    ] {
556        hash.u32(value);
557    }
558    // A track list's text is its canonical form.
559    for tracks in [
560        &layout.grid_template_columns,
561        &layout.grid_template_rows,
562        &layout.grid_auto_columns,
563        &layout.grid_auto_rows,
564    ] {
565        match tracks {
566            None => hash.u32(0),
567            Some(tracks) => {
568                hash.u32(1);
569                hash.bytes(tracks.to_string().as_bytes());
570            }
571        }
572    }
573    for placement in [layout.grid_column, layout.grid_row] {
574        hash.bytes(placement.to_string().as_bytes());
575    }
576    for value in [
577        layout.row_gap,
578        layout.column_gap,
579        layout.border_width,
580        layout.flex_grow,
581        layout.flex_shrink,
582        layout.aspect_ratio.unwrap_or(-1.0),
583    ] {
584        hash.f32(value);
585    }
586    hash_sides(hash, layout.padding);
587    hash_sides(hash, layout.margin);
588    for length in [
589        layout.width,
590        layout.height,
591        layout.min_width,
592        layout.min_height,
593        layout.max_width,
594        layout.max_height,
595        layout.flex_basis,
596    ]
597    .into_iter()
598    .chain(layout.inset)
599    {
600        hash_length(hash, length);
601    }
602}
603
604fn hash_node_in(
605    hash: &mut StableHash,
606    node: &EvaluatedNode,
607    shapes: &[(Arc<ShapesPlan>, u128)],
608    shape_index: &mut usize,
609) {
610    hash_layout(hash, &node.layout);
611    hash_paint(hash, node.paint.background.as_ref());
612    match &node.paint.border {
613        None => hash.u32(0),
614        Some((width, paint)) => {
615            hash.u32(1);
616            hash.f32(*width);
617            hash_paint(hash, Some(paint));
618        }
619    }
620    hash.f32(node.paint.border_radius);
621    match node.visual {
622        None => hash.u32(0),
623        Some(visual) => {
624            hash.u32(1);
625            for value in [
626                visual.opacity,
627                visual.translate.x,
628                visual.translate.y,
629                visual.scale.x,
630                visual.scale.y,
631                visual.rotation_degrees,
632            ] {
633                hash.f32(value);
634            }
635        }
636    }
637    hash.u32(u32::from(node.clip));
638    match &node.content {
639        NodeContent::Empty => hash.u32(0),
640        NodeContent::Children(children) => {
641            hash.u32(1);
642            hash.u64(children.len() as u64);
643            for child in children {
644                hash_node_in(hash, child, shapes, shape_index);
645            }
646        }
647        NodeContent::Text(text) => {
648            hash.u32(2);
649            hash.bytes(text.content.as_bytes());
650            match &text.font_family {
651                None => hash.u32(0),
652                Some(family) => {
653                    hash.u32(1);
654                    hash.bytes(family.as_bytes());
655                }
656            }
657            hash.f32(text.font_size);
658            hash_paint(hash, Some(&text.fill));
659            hash.f32(text.line_height);
660            hash.f32(text.letter_spacing);
661            hash.u32(u32::from(text.font_weight));
662            hash.u32(text.text_align as u32);
663            hash.u64(text.strokes.len() as u64);
664            for stroke in &text.strokes {
665                hash_color(hash, stroke.color);
666                hash.f32(stroke.width);
667            }
668            hash.u64(text.shadows.len() as u64);
669            for shadow in &text.shadows {
670                hash_color(hash, shadow.color);
671                hash.f32(shadow.offset.x);
672                hash.f32(shadow.offset.y);
673                hash.f32(shadow.blur);
674            }
675            // Nothing for a text without animators, so a static layout keeps its key.
676            if !text.animators.is_empty() {
677                crate::hash_sampled_animators(hash, &text.animators);
678            }
679        }
680        NodeContent::Shape(_) => {
681            hash.u32(3);
682            let digest = shapes.get(*shape_index).map_or(0, |(_, digest)| *digest);
683            *shape_index += 1;
684            hash.bytes(&digest.to_le_bytes());
685        }
686    }
687}
688
689/// Approximate bytes a layout tree holds, for the plan budget. Its plan holds about as much again.
690#[must_use]
691pub(crate) fn layout_bytes(
692    node: &LayoutNode,
693    shape_items_bytes: &impl Fn(&[alpha2_model::ShapeItem]) -> usize,
694) -> usize {
695    fn keys<T>(animation: Option<&Animated<T>>) -> usize {
696        match animation {
697            Some(Animated::Keyframes { keyframes }) => {
698                keyframes.len() * size_of::<alpha2_model::Keyframe<T>>()
699            }
700            _ => 0,
701        }
702    }
703    let text = node.text.as_ref().map_or(0, |text| match text {
704        Animated::Constant { value } => value.len(),
705        Animated::Keyframes { keyframes } => keyframes
706            .iter()
707            .map(|keyframe| size_of::<alpha2_model::Keyframe<String>>() + keyframe.value.len())
708            .sum(),
709    });
710    let style = node.font_family.as_ref().map_or(0, String::len) + keys(node.font_size.as_ref());
711    let tracks = [
712        &node.grid_template_columns,
713        &node.grid_template_rows,
714        &node.grid_auto_columns,
715        &node.grid_auto_rows,
716    ]
717    .into_iter()
718    .flatten()
719    .map(|tracks| tracks.track_count() * size_of::<alpha2_model::TrackSize>())
720    .sum::<usize>();
721    let paints = |paint: Option<&Animated<Paint>>| match paint {
722        Some(Animated::Keyframes { keyframes }) => {
723            keyframes.len() * size_of::<alpha2_model::Keyframe<Paint>>()
724        }
725        Some(Animated::Constant { .. }) | None => 0,
726    };
727    let inset = node.inset.as_ref().map_or(0, |inset| {
728        [&inset.left, &inset.top, &inset.right, &inset.bottom]
729            .into_iter()
730            .map(|side| keys(side.as_ref()))
731            .sum()
732    });
733    size_of::<LayoutNode>()
734        + text
735        + style
736        + tracks
737        + node
738            .shape
739            .as_ref()
740            .map_or(0, |shape| shape_items_bytes(&shape.items))
741        + [
742            &node.gap,
743            &node.row_gap,
744            &node.column_gap,
745            &node.border_radius,
746        ]
747        .into_iter()
748        .map(|value| keys(value.as_ref()))
749        .sum::<usize>()
750        + keys(node.padding.as_ref())
751        + paints(node.background.as_ref())
752        + node.border.as_ref().map_or(0, |border| {
753            keys(Some(&border.width)) + paints(Some(&border.paint))
754        })
755        + [
756            &node.width,
757            &node.height,
758            &node.min_width,
759            &node.min_height,
760            &node.max_width,
761            &node.max_height,
762            &node.flex_basis,
763        ]
764        .into_iter()
765        .map(|length| keys(length.as_ref()))
766        .sum::<usize>()
767        + keys(node.margin.as_ref())
768        + keys(node.flex_grow.as_ref())
769        + keys(node.flex_shrink.as_ref())
770        + inset
771        + keys(node.opacity.as_ref())
772        + keys(node.translate.as_ref())
773        + keys(node.scale.as_ref())
774        + keys(node.rotation_degrees.as_ref())
775        + node
776            .children
777            .iter()
778            .map(|child| layout_bytes(child, shape_items_bytes))
779            .sum::<usize>()
780}
781
782/// A shapes or layout generator's tree, planned once. Other generators need none.
783#[derive(Clone, Debug, PartialEq)]
784pub enum GeneratorPlan {
785    /// A shapes generator's content tree.
786    Shapes(ShapesPlan),
787    /// A layout generator's box tree.
788    Layout(LayoutPlan),
789}
790
791impl GeneratorPlan {
792    /// The plan for `generator` on a canvas of `canvas` pixels, if it needs one.
793    #[must_use]
794    pub fn new(generator: &alpha2_model::Generator, canvas: (u32, u32)) -> Option<Self> {
795        match generator {
796            alpha2_model::Generator::Shapes { items } => Some(Self::Shapes(ShapesPlan::new(items))),
797            alpha2_model::Generator::Layout { root } => {
798                Some(Self::Layout(LayoutPlan::new(root, canvas)))
799            }
800            alpha2_model::Generator::Fill { .. } | alpha2_model::Generator::Text { .. } => None,
801        }
802    }
803
804    /// The shapes plan, for a shapes generator.
805    #[must_use]
806    pub const fn shapes(&self) -> Option<&ShapesPlan> {
807        match self {
808            Self::Shapes(plan) => Some(plan),
809            Self::Layout(_) => None,
810        }
811    }
812
813    /// The layout plan, for a layout generator.
814    #[must_use]
815    pub const fn layout(&self) -> Option<&LayoutPlan> {
816        match self {
817            Self::Layout(plan) => Some(plan),
818            Self::Shapes(_) => None,
819        }
820    }
821}
822
823#[cfg(test)]
824#[path = "layout_tests.rs"]
825mod tests;