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    /// How lines align in the content width.
270    pub text_align: TextAlign,
271    /// A band around the glyphs.
272    pub outline: Option<TextOutline>,
273}
274
275fn non_negative(animation: Option<&Animated<f32>>, time: MediaTime, default: f32) -> f32 {
276    animation.map_or(default, |value| sample_scalar(value, time).max(0.0))
277}
278
279fn size_or(
280    animation: Option<&Animated<alpha2_model::LayoutSize>>,
281    time: MediaTime,
282    default: f32,
283) -> f32 {
284    animation.map_or(default, |value| sample_size(value, time).max(0.0))
285}
286
287fn length(animation: Option<&Animated<LayoutLength>>, time: MediaTime) -> Option<LayoutLength> {
288    animation.map(|value| sample_length(value, time))
289}
290
291fn size(animation: Option<&Animated<LayoutLength>>, time: MediaTime) -> Option<LayoutLength> {
292    length(animation, time).map(|length| length.with_value(length.value().max(0.0)))
293}
294
295/// What flexbox or grid reads of `node` at `time`; `border_width` is the
296/// sampled border's.
297fn node_layout(node: &LayoutNode, time: MediaTime, border_width: f32) -> NodeLayout {
298    let insets = |value: Option<&Animated<alpha2_model::Insets>>| {
299        value.map_or_else(Sides::default, |value| {
300            Sides::from_array(sample_insets(value, time))
301        })
302    };
303    let padding = Sides::from_array(
304        insets(node.padding.as_ref())
305            .to_array()
306            .map(|side| side.max(0.0)),
307    );
308    let inset = node.inset.as_ref().map_or([None; 4], |inset| {
309        [
310            length(inset.left.as_ref(), time),
311            length(inset.top.as_ref(), time),
312            length(inset.right.as_ref(), time),
313            length(inset.bottom.as_ref(), time),
314        ]
315    });
316    let gap = size_or(node.gap.as_ref(), time, 0.0);
317    NodeLayout {
318        display: node.display.unwrap_or_default(),
319        flex_direction: node.flex_direction.unwrap_or_default(),
320        flex_wrap: node.flex_wrap.unwrap_or_default(),
321        grid_template_columns: node.grid_template_columns.clone(),
322        grid_template_rows: node.grid_template_rows.clone(),
323        grid_auto_columns: node.grid_auto_columns.clone(),
324        grid_auto_rows: node.grid_auto_rows.clone(),
325        grid_auto_flow: node.grid_auto_flow.unwrap_or_default(),
326        row_gap: size_or(node.row_gap.as_ref(), time, gap),
327        column_gap: size_or(node.column_gap.as_ref(), time, gap),
328        justify_content: node.justify_content.unwrap_or_default(),
329        align_items: node.align_items.unwrap_or_default(),
330        align_content: node.align_content.unwrap_or_default(),
331        justify_items: node.justify_items.unwrap_or_default(),
332        padding,
333        border_width,
334        overflow: node.overflow.unwrap_or_default(),
335        width: size(node.width.as_ref(), time),
336        height: size(node.height.as_ref(), time),
337        min_width: size(node.min_width.as_ref(), time),
338        min_height: size(node.min_height.as_ref(), time),
339        max_width: size(node.max_width.as_ref(), time),
340        max_height: size(node.max_height.as_ref(), time),
341        aspect_ratio: node.aspect_ratio,
342        margin: insets(node.margin.as_ref()),
343        flex_grow: non_negative(node.flex_grow.as_ref(), time, 0.0),
344        flex_shrink: non_negative(node.flex_shrink.as_ref(), time, 1.0),
345        flex_basis: size(node.flex_basis.as_ref(), time),
346        align_self: node.align_self,
347        justify_self: node.justify_self,
348        grid_column: node.grid_column.unwrap_or_default(),
349        grid_row: node.grid_row.unwrap_or_default(),
350        position: node.position.unwrap_or_default(),
351        inset,
352    }
353}
354
355/// The style `node` gives its text and its children: what it sets, and what
356/// it inherits otherwise, sampled at `time`.
357fn text_style(node: &LayoutNode, time: MediaTime, inherited: &LayoutTextStyle) -> LayoutTextStyle {
358    LayoutTextStyle {
359        font_family: node
360            .font_family
361            .clone()
362            .or_else(|| inherited.font_family.clone()),
363        font_size: node.font_size.as_ref().map_or(inherited.font_size, |size| {
364            sample_size(size, time).max(f32::MIN_POSITIVE)
365        }),
366        color: node.color.unwrap_or(inherited.color),
367        line_height: node.line_height.unwrap_or(inherited.line_height),
368        text_align: node.text_align.unwrap_or(inherited.text_align),
369        outline: node.outline.or(inherited.outline),
370    }
371}
372
373fn evaluate_node<'a>(
374    node: &LayoutNode,
375    time: MediaTime,
376    shapes: &mut impl Iterator<Item = &'a (Arc<ShapesPlan>, u128)>,
377    inherited: &LayoutTextStyle,
378) -> EvaluatedNode {
379    let border = node.border.as_ref().map(|border| {
380        (
381            sample_size(&border.width, time).max(0.0),
382            sample_paint(&border.paint, time),
383        )
384    });
385    let layout = node_layout(node, time, border.as_ref().map_or(0.0, |(width, _)| *width));
386    let paint = NodePaint {
387        background: node
388            .background
389            .as_ref()
390            .map(|paint| sample_paint(paint, time)),
391        border,
392        border_radius: size_or(node.border_radius.as_ref(), time, 0.0),
393    };
394    let visual = node.has_visual().then(|| NodeVisual {
395        opacity: node
396            .opacity
397            .as_ref()
398            .map_or(1.0, |value| sample_scalar(value, time).clamp(0.0, 1.0)),
399        translate: node
400            .translate
401            .as_ref()
402            .map_or_else(Vec2::default, |value| sample_offset(value, time)),
403        scale: node
404            .scale
405            .as_ref()
406            .map_or(Vec2::new(1.0, 1.0), |value| sample_vec2(value, time)),
407        rotation_degrees: node
408            .rotation_degrees
409            .as_ref()
410            .map_or(0.0, |value| sample_scalar(value, time)),
411    });
412    let style = text_style(node, time, inherited);
413    let content = if let Some(text) = &node.text {
414        NodeContent::Text(EvaluatedText {
415            content: sample_hold(text, time),
416            font_family: style.font_family,
417            font_size: style.font_size,
418            color: style.color,
419            line_height: style.line_height,
420            text_align: style.text_align,
421            outline: style.outline,
422        })
423    } else if node.shape.is_some() {
424        let (plan, _) = shapes.next().expect("one plan per shape node");
425        NodeContent::Shape(Arc::clone(plan))
426    } else if node.children.is_empty() {
427        NodeContent::Empty
428    } else {
429        NodeContent::Children(
430            node.children
431                .iter()
432                .map(|child| evaluate_node(child, time, shapes, &style))
433                .collect(),
434        )
435    };
436    EvaluatedNode {
437        layout,
438        paint,
439        visual,
440        clip: matches!(node.overflow, Some(Overflow::Hidden)),
441        content,
442    }
443}
444
445fn hash_length(hash: &mut StableHash, length: Option<LayoutLength>) {
446    match length {
447        None => hash.u32(0),
448        Some(LayoutLength::Fraction(value)) => {
449            hash.u32(1);
450            hash.f32(value);
451        }
452        Some(LayoutLength::Percent(value)) => {
453            hash.u32(2);
454            hash.f32(value);
455        }
456        Some(LayoutLength::Px(value)) => {
457            hash.u32(3);
458            hash.f32(value);
459        }
460    }
461}
462
463fn hash_sides(hash: &mut StableHash, sides: Sides) {
464    for side in sides.to_array() {
465        hash.f32(side);
466    }
467}
468
469fn hash_color(hash: &mut StableHash, color: Rgba) {
470    for component in [color.red, color.green, color.blue, color.alpha] {
471        hash.f32(component);
472    }
473}
474
475fn hash_paint(hash: &mut StableHash, paint: Option<&Paint>) {
476    let stops = |hash: &mut StableHash, stops: &[alpha2_model::GradientStop]| {
477        hash.u64(stops.len() as u64);
478        for stop in stops {
479            hash.f32(stop.offset);
480            hash_color(hash, stop.color);
481        }
482    };
483    match paint {
484        None => hash.u32(0),
485        Some(Paint::Solid { color }) => {
486            hash.u32(1);
487            hash_color(hash, *color);
488        }
489        Some(Paint::LinearGradient {
490            start,
491            end,
492            stops: list,
493            spread,
494        }) => {
495            hash.u32(2);
496            for value in [start.x, start.y, end.x, end.y] {
497                hash.f32(value);
498            }
499            stops(hash, list);
500            hash.u32(*spread as u32);
501        }
502        Some(Paint::RadialGradient {
503            center,
504            radius,
505            stops: list,
506            spread,
507        }) => {
508            hash.u32(3);
509            for value in [center.x, center.y, *radius] {
510                hash.f32(value);
511            }
512            stops(hash, list);
513            hash.u32(*spread as u32);
514        }
515        Some(Paint::ConicGradient {
516            center,
517            angle_degrees,
518            stops: list,
519        }) => {
520            hash.u32(4);
521            for value in [center.x, center.y, *angle_degrees] {
522                hash.f32(value);
523            }
524            stops(hash, list);
525        }
526    }
527}
528
529fn hash_node(hash: &mut StableHash, node: &EvaluatedNode, shapes: &[(Arc<ShapesPlan>, u128)]) {
530    let mut shape_index = 0;
531    hash_node_in(hash, node, shapes, &mut shape_index);
532}
533
534fn hash_layout(hash: &mut StableHash, layout: &NodeLayout) {
535    for value in [
536        layout.display as u32,
537        layout.flex_direction as u32,
538        layout.flex_wrap as u32,
539        layout.grid_auto_flow as u32,
540        layout.justify_content as u32,
541        layout.align_items as u32,
542        layout.align_content as u32,
543        layout.justify_items as u32,
544        layout.overflow as u32,
545        layout.position as u32,
546        layout.align_self.map_or(u32::MAX, |align| align as u32),
547        layout.justify_self.map_or(u32::MAX, |align| align as u32),
548    ] {
549        hash.u32(value);
550    }
551    // A track list's text is all of it: its canonical form.
552    for tracks in [
553        &layout.grid_template_columns,
554        &layout.grid_template_rows,
555        &layout.grid_auto_columns,
556        &layout.grid_auto_rows,
557    ] {
558        match tracks {
559            None => hash.u32(0),
560            Some(tracks) => {
561                hash.u32(1);
562                hash.bytes(tracks.to_string().as_bytes());
563            }
564        }
565    }
566    for placement in [layout.grid_column, layout.grid_row] {
567        hash.bytes(placement.to_string().as_bytes());
568    }
569    for value in [
570        layout.row_gap,
571        layout.column_gap,
572        layout.border_width,
573        layout.flex_grow,
574        layout.flex_shrink,
575        layout.aspect_ratio.unwrap_or(-1.0),
576    ] {
577        hash.f32(value);
578    }
579    hash_sides(hash, layout.padding);
580    hash_sides(hash, layout.margin);
581    for length in [
582        layout.width,
583        layout.height,
584        layout.min_width,
585        layout.min_height,
586        layout.max_width,
587        layout.max_height,
588        layout.flex_basis,
589    ]
590    .into_iter()
591    .chain(layout.inset)
592    {
593        hash_length(hash, length);
594    }
595}
596
597fn hash_node_in(
598    hash: &mut StableHash,
599    node: &EvaluatedNode,
600    shapes: &[(Arc<ShapesPlan>, u128)],
601    shape_index: &mut usize,
602) {
603    hash_layout(hash, &node.layout);
604    hash_paint(hash, node.paint.background.as_ref());
605    match &node.paint.border {
606        None => hash.u32(0),
607        Some((width, paint)) => {
608            hash.u32(1);
609            hash.f32(*width);
610            hash_paint(hash, Some(paint));
611        }
612    }
613    hash.f32(node.paint.border_radius);
614    match node.visual {
615        None => hash.u32(0),
616        Some(visual) => {
617            hash.u32(1);
618            for value in [
619                visual.opacity,
620                visual.translate.x,
621                visual.translate.y,
622                visual.scale.x,
623                visual.scale.y,
624                visual.rotation_degrees,
625            ] {
626                hash.f32(value);
627            }
628        }
629    }
630    hash.u32(u32::from(node.clip));
631    match &node.content {
632        NodeContent::Empty => hash.u32(0),
633        NodeContent::Children(children) => {
634            hash.u32(1);
635            hash.u64(children.len() as u64);
636            for child in children {
637                hash_node_in(hash, child, shapes, shape_index);
638            }
639        }
640        NodeContent::Text(text) => {
641            hash.u32(2);
642            hash.bytes(text.content.as_bytes());
643            match &text.font_family {
644                None => hash.u32(0),
645                Some(family) => {
646                    hash.u32(1);
647                    hash.bytes(family.as_bytes());
648                }
649            }
650            hash.f32(text.font_size);
651            hash_color(hash, text.color);
652            hash.f32(text.line_height);
653            hash.u32(text.text_align as u32);
654            match text.outline {
655                None => hash.u32(0),
656                Some(outline) => {
657                    hash.u32(1);
658                    hash_color(hash, outline.color);
659                    hash.f32(outline.width);
660                }
661            }
662        }
663        NodeContent::Shape(_) => {
664            hash.u32(3);
665            let digest = shapes.get(*shape_index).map_or(0, |(_, digest)| *digest);
666            *shape_index += 1;
667            hash.bytes(&digest.to_le_bytes());
668        }
669    }
670}
671
672/// What a layout tree holds, for the plan budget: each node, its keyframes,
673/// its text, its track lists, and its shape items. Its plan holds about as
674/// much again.
675#[must_use]
676pub(crate) fn layout_bytes(
677    node: &LayoutNode,
678    shape_items_bytes: &impl Fn(&[alpha2_model::ShapeItem]) -> usize,
679) -> usize {
680    fn keys<T>(animation: Option<&Animated<T>>) -> usize {
681        match animation {
682            Some(Animated::Keyframes { keyframes }) => {
683                keyframes.len() * size_of::<alpha2_model::Keyframe<T>>()
684            }
685            _ => 0,
686        }
687    }
688    let text = node.text.as_ref().map_or(0, |text| match text {
689        Animated::Constant { value } => value.len(),
690        Animated::Keyframes { keyframes } => keyframes
691            .iter()
692            .map(|keyframe| size_of::<alpha2_model::Keyframe<String>>() + keyframe.value.len())
693            .sum(),
694    });
695    let style = node.font_family.as_ref().map_or(0, String::len) + keys(node.font_size.as_ref());
696    let tracks = [
697        &node.grid_template_columns,
698        &node.grid_template_rows,
699        &node.grid_auto_columns,
700        &node.grid_auto_rows,
701    ]
702    .into_iter()
703    .flatten()
704    .map(|tracks| tracks.track_count() * size_of::<alpha2_model::TrackSize>())
705    .sum::<usize>();
706    let paints = |paint: Option<&Animated<Paint>>| match paint {
707        Some(Animated::Keyframes { keyframes }) => {
708            keyframes.len() * size_of::<alpha2_model::Keyframe<Paint>>()
709        }
710        Some(Animated::Constant { .. }) | None => 0,
711    };
712    let inset = node.inset.as_ref().map_or(0, |inset| {
713        [&inset.left, &inset.top, &inset.right, &inset.bottom]
714            .into_iter()
715            .map(|side| keys(side.as_ref()))
716            .sum()
717    });
718    size_of::<LayoutNode>()
719        + text
720        + style
721        + tracks
722        + node
723            .shape
724            .as_ref()
725            .map_or(0, |shape| shape_items_bytes(&shape.items))
726        + [
727            &node.gap,
728            &node.row_gap,
729            &node.column_gap,
730            &node.border_radius,
731        ]
732        .into_iter()
733        .map(|value| keys(value.as_ref()))
734        .sum::<usize>()
735        + keys(node.padding.as_ref())
736        + paints(node.background.as_ref())
737        + node.border.as_ref().map_or(0, |border| {
738            keys(Some(&border.width)) + paints(Some(&border.paint))
739        })
740        + [
741            &node.width,
742            &node.height,
743            &node.min_width,
744            &node.min_height,
745            &node.max_width,
746            &node.max_height,
747            &node.flex_basis,
748        ]
749        .into_iter()
750        .map(|length| keys(length.as_ref()))
751        .sum::<usize>()
752        + keys(node.margin.as_ref())
753        + keys(node.flex_grow.as_ref())
754        + keys(node.flex_shrink.as_ref())
755        + inset
756        + keys(node.opacity.as_ref())
757        + keys(node.translate.as_ref())
758        + keys(node.scale.as_ref())
759        + keys(node.rotation_degrees.as_ref())
760        + node
761            .children
762            .iter()
763            .map(|child| layout_bytes(child, shape_items_bytes))
764            .sum::<usize>()
765}
766
767/// A generator's tree, planned once: what a shapes or layout generator
768/// evaluates each frame from. Other generators need none.
769#[derive(Clone, Debug, PartialEq)]
770pub enum GeneratorPlan {
771    /// A shapes generator's content tree.
772    Shapes(ShapesPlan),
773    /// A layout generator's box tree.
774    Layout(LayoutPlan),
775}
776
777impl GeneratorPlan {
778    /// The plan `generator` draws from on a project canvas of `canvas`
779    /// pixels, if it needs one.
780    #[must_use]
781    pub fn new(generator: &alpha2_model::Generator, canvas: (u32, u32)) -> Option<Self> {
782        match generator {
783            alpha2_model::Generator::Shapes { items } => Some(Self::Shapes(ShapesPlan::new(items))),
784            alpha2_model::Generator::Layout { root } => {
785                Some(Self::Layout(LayoutPlan::new(root, canvas)))
786            }
787            alpha2_model::Generator::Fill { .. } | alpha2_model::Generator::Text { .. } => None,
788        }
789    }
790
791    /// The shapes plan, for a shapes generator.
792    #[must_use]
793    pub const fn shapes(&self) -> Option<&ShapesPlan> {
794        match self {
795            Self::Shapes(plan) => Some(plan),
796            Self::Layout(_) => None,
797        }
798    }
799
800    /// The layout plan, for a layout generator.
801    #[must_use]
802    pub const fn layout(&self) -> Option<&LayoutPlan> {
803        match self {
804            Self::Layout(plan) => Some(plan),
805            Self::Shapes(_) => None,
806        }
807    }
808}
809
810#[cfg(test)]
811#[path = "layout_tests.rs"]
812mod tests;