Skip to main content

alpha2_eval/
layout.rs

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