1use 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#[derive(Clone, Debug)]
26pub struct LayoutPlan {
27 root: Box<LayoutNode>,
29 shapes: Vec<(Arc<ShapesPlan>, u128)>,
31 shapes_animate: bool,
33 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 #[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 #[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 #[must_use]
84 pub fn is_animated(&self) -> bool {
85 self.fixed.is_none()
86 }
87
88 #[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#[derive(Clone, Debug, PartialEq)]
113pub struct EvaluatedLayout {
114 pub root: EvaluatedNode,
116 pub time: MediaTime,
118 pub key: u128,
121}
122
123#[derive(Clone, Debug, PartialEq)]
126pub struct EvaluatedNode {
127 pub layout: NodeLayout,
129 pub paint: NodePaint,
131 pub visual: Option<NodeVisual>,
133 pub clip: bool,
135 pub content: NodeContent,
137}
138
139impl EvaluatedNode {
140 #[must_use]
142 pub const fn is_isolated(&self) -> bool {
143 self.visual.is_some() || self.clip
144 }
145}
146
147#[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#[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 pub row_gap: f32,
188 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 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 pub inset: [Option<LayoutLength>; 4],
216}
217
218#[derive(Clone, Debug, PartialEq)]
220pub struct NodePaint {
221 pub background: Option<Paint>,
223 pub border: Option<(f32, Paint)>,
225 pub border_radius: f32,
227}
228
229#[derive(Copy, Clone, Debug, PartialEq)]
231pub struct NodeVisual {
232 pub opacity: f32,
234 pub translate: Vec2,
236 pub scale: Vec2,
238 pub rotation_degrees: f32,
240}
241
242#[derive(Clone, Debug, PartialEq)]
244pub enum NodeContent {
245 Empty,
247 Children(Vec<EvaluatedNode>),
249 Text(EvaluatedText),
251 Shape(Arc<ShapesPlan>),
254}
255
256#[derive(Clone, Debug, PartialEq)]
258pub struct EvaluatedText {
259 pub content: String,
261 pub font_family: Option<String>,
263 pub font_size: f32,
265 pub color: Rgba,
267 pub line_height: f32,
269 pub text_align: TextAlign,
271 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
295fn 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
355fn 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 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#[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#[derive(Clone, Debug, PartialEq)]
770pub enum GeneratorPlan {
771 Shapes(ShapesPlan),
773 Layout(LayoutPlan),
775}
776
777impl GeneratorPlan {
778 #[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 #[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 #[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;