1use crate::animation::visit_keyed;
12use crate::checks::{
13 Checked, ValueIssue, ValueProblem, animated, any, non_negative, scalar, unit, validate_paint,
14 vec2,
15};
16use crate::layout_values::{
17 GridEdge, GridPlacement, Insets, LayoutLength, LayoutOffset, LayoutSize, RepeatCount,
18 TrackComponent, TrackList,
19};
20use crate::{
21 Animated, FONT_WEIGHT_RANGE, Interpolation, LETTER_SPACING_RANGE, LINE_HEIGHT_RANGE,
22 MAX_FONT_FAMILY_BYTES, MAX_TEXT_CONTENT_BYTES, Paint, Rgba, ShapeIssue, ShapeItem, TextAlign,
23 TextShadow, TextStroke, Vec2,
24};
25use serde::{Deserialize, Serialize};
26use std::fmt;
27
28pub const MAX_LAYOUT_NODES: usize = 256;
30pub const MAX_LAYOUT_DEPTH: usize = 16;
32pub const MAX_LAYOUT_TEXTS: usize = 64;
34pub const MAX_LAYOUT_ISOLATED: usize = 16;
36pub const MAX_LAYOUT_FONT_SIZE: f32 = 1.0;
38pub const MAX_GRID_TRACKS: usize = 64;
40const MAX_GRID_LINE: i16 = 65;
42
43pub const LAYOUT_TEXT_DEFAULTS: LayoutTextStyle = LayoutTextStyle {
45 font_family: None,
46 font_size: 0.05,
47 fill: Paint::Solid {
48 color: Rgba::new(1.0, 1.0, 1.0, 1.0),
49 },
50 line_height: 1.2,
51 letter_spacing: 0.0,
52 font_weight: 400,
53 text_align: TextAlign::Start,
54 strokes: Vec::new(),
55 shadows: Vec::new(),
56};
57
58#[derive(Clone, Debug, PartialEq)]
60pub struct LayoutTextStyle {
61 pub font_family: Option<String>,
63 pub font_size: f32,
65 pub fill: Paint,
67 pub line_height: f32,
69 pub letter_spacing: f32,
71 pub font_weight: u16,
73 pub text_align: TextAlign,
75 pub strokes: Vec<TextStroke>,
77 pub shadows: Vec<TextShadow>,
80}
81
82#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
89#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
90#[serde(deny_unknown_fields)]
91pub struct LayoutNode {
92 #[serde(default, skip_serializing_if = "Vec::is_empty")]
94 pub children: Vec<LayoutNode>,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub text: Option<Animated<String>>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub shape: Option<LayoutShape>,
104
105 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub display: Option<Display>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub flex_direction: Option<FlexDirection>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub flex_wrap: Option<FlexWrap>,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub grid_template_columns: Option<TrackList>,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub grid_template_rows: Option<TrackList>,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub grid_auto_columns: Option<TrackList>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub grid_auto_rows: Option<TrackList>,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub grid_auto_flow: Option<GridAutoFlow>,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub gap: Option<Animated<LayoutSize>>,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub row_gap: Option<Animated<LayoutSize>>,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub column_gap: Option<Animated<LayoutSize>>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub justify_content: Option<JustifyContent>,
144 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub align_items: Option<AlignItems>,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub align_content: Option<AlignContent>,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub justify_items: Option<AlignItems>,
154
155 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub padding: Option<Animated<Insets>>,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub background: Option<Animated<Paint>>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub border: Option<LayoutBorder>,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub border_radius: Option<Animated<LayoutSize>>,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub overflow: Option<Overflow>,
173
174 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub width: Option<Animated<LayoutLength>>,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub height: Option<Animated<LayoutLength>>,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub min_width: Option<Animated<LayoutLength>>,
183 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub min_height: Option<Animated<LayoutLength>>,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub max_width: Option<Animated<LayoutLength>>,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub max_height: Option<Animated<LayoutLength>>,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub aspect_ratio: Option<f32>,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub margin: Option<Animated<Insets>>,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub flex_grow: Option<Animated<f32>>,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub flex_shrink: Option<Animated<f32>>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub flex_basis: Option<Animated<LayoutLength>>,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub align_self: Option<AlignItems>,
213 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub justify_self: Option<AlignItems>,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub grid_column: Option<GridPlacement>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub grid_row: Option<GridPlacement>,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub position: Option<Position>,
227 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub inset: Option<LayoutInset>,
230
231 #[serde(default, skip_serializing_if = "Option::is_none")]
234 pub font_family: Option<String>,
235 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub font_size: Option<Animated<LayoutSize>>,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub fill: Option<Paint>,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub line_height: Option<f32>,
246 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub letter_spacing: Option<f32>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
253 pub font_weight: Option<u16>,
254 #[serde(default, skip_serializing_if = "Option::is_none")]
256 pub text_align: Option<TextAlign>,
257 #[serde(default, skip_serializing_if = "Option::is_none")]
260 pub strokes: Option<Vec<TextStroke>>,
261 #[serde(default, skip_serializing_if = "Option::is_none")]
264 pub shadows: Option<Vec<TextShadow>>,
265 #[serde(default, skip_serializing_if = "Vec::is_empty")]
269 pub text_animators: Vec<crate::TextAnimator>,
270
271 #[serde(default, skip_serializing_if = "Option::is_none")]
273 pub opacity: Option<Animated<f32>>,
274 #[serde(default, skip_serializing_if = "Option::is_none")]
277 pub translate: Option<Animated<LayoutOffset>>,
278 #[serde(default, skip_serializing_if = "Option::is_none")]
280 pub scale: Option<Animated<Vec2>>,
281 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub rotation_degrees: Option<Animated<f32>>,
285}
286
287#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
290#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
291#[serde(deny_unknown_fields)]
292pub struct LayoutShape {
293 pub items: Vec<ShapeItem>,
295}
296
297#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
299#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
300#[serde(deny_unknown_fields)]
301pub struct LayoutBorder {
302 pub width: Animated<LayoutSize>,
304 pub paint: Animated<Paint>,
306}
307
308#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
311#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
312#[serde(deny_unknown_fields)]
313pub struct LayoutInset {
314 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub left: Option<Animated<LayoutLength>>,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
319 pub top: Option<Animated<LayoutLength>>,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub right: Option<Animated<LayoutLength>>,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub bottom: Option<Animated<LayoutLength>>,
326}
327
328impl LayoutInset {
329 fn sides(&self) -> [(&'static str, Option<&Animated<LayoutLength>>); 4] {
330 [
331 ("left", self.left.as_ref()),
332 ("top", self.top.as_ref()),
333 ("right", self.right.as_ref()),
334 ("bottom", self.bottom.as_ref()),
335 ]
336 }
337}
338
339#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
341#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
342#[serde(rename_all = "snake_case")]
343pub enum Display {
344 #[default]
346 Flex,
347 Grid,
349}
350
351#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
353#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
354#[serde(rename_all = "snake_case")]
355pub enum FlexDirection {
356 #[default]
358 Row,
359 Column,
361 RowReverse,
363 ColumnReverse,
365}
366
367#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
369#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
370pub enum FlexWrap {
371 #[default]
373 #[serde(rename = "nowrap")]
374 NoWrap,
375 #[serde(rename = "wrap")]
377 Wrap,
378}
379
380#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
382#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
383#[serde(rename_all = "snake_case")]
384pub enum GridAutoFlow {
385 #[default]
387 Row,
388 Column,
390 RowDense,
392 ColumnDense,
394}
395
396#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
398#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
399#[serde(rename_all = "snake_case")]
400pub enum JustifyContent {
401 #[default]
403 Start,
404 Center,
406 End,
408 SpaceBetween,
410 SpaceAround,
412 SpaceEvenly,
414}
415
416#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
418#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
419#[serde(rename_all = "snake_case")]
420pub enum AlignContent {
421 Start,
423 Center,
425 End,
427 SpaceBetween,
429 SpaceAround,
431 SpaceEvenly,
433 #[default]
435 Stretch,
436}
437
438#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
440#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
441#[serde(rename_all = "snake_case")]
442pub enum AlignItems {
443 Start,
445 Center,
447 End,
449 #[default]
451 Stretch,
452}
453
454#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
456#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
457#[serde(rename_all = "snake_case")]
458pub enum Position {
459 #[default]
461 Relative,
462 Absolute,
464}
465
466#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
468#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
469#[serde(rename_all = "snake_case")]
470pub enum Overflow {
471 #[default]
473 Visible,
474 Hidden,
476}
477
478#[derive(Copy, Clone, Debug, Eq, PartialEq)]
480#[non_exhaustive]
481pub enum LayoutProblem {
482 TooManyNodes,
484 TooDeep,
486 TooManyTexts,
488 TooManyIsolated,
490 MixedContent,
492 DisplayOnLeaf,
494 ContainerFieldOnLeaf,
496 RootItemField,
498 GridFieldOnFlex(&'static str),
500 FlexFieldOnGrid(&'static str),
502 FlexItemFieldInGrid(&'static str),
504 GridItemFieldInFlex(&'static str),
506 TooManyTracks,
508 RepeatCount,
510 RepeatInAutoTracks,
512 AutoRepeatNeedsFixedTracks,
514 GridLineOutOfRange,
516 InvalidKeyframes,
518 OutOfRange,
520 LengthKeyframesDiffer,
522 PaintKeyframesDiffer,
524 TextKeyframesInterpolate,
526}
527
528impl fmt::Display for LayoutProblem {
529 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
530 match self {
531 Self::GridFieldOnFlex(field) => {
532 return write!(
533 formatter,
534 "sets {field}, a grid container field, without display grid"
535 );
536 }
537 Self::FlexFieldOnGrid(field) => {
538 return write!(
539 formatter,
540 "sets {field}, a flex container field, on a grid container"
541 );
542 }
543 Self::FlexItemFieldInGrid(field) => {
544 return write!(
545 formatter,
546 "sets {field}, a flex item field, in a grid container"
547 );
548 }
549 Self::GridItemFieldInFlex(field) => {
550 return write!(
551 formatter,
552 "sets {field}, a grid item field, in a flex container"
553 );
554 }
555 _ => {}
556 }
557 formatter.write_str(match self {
558 Self::TooManyNodes => "has more nodes than a layout may hold",
559 Self::TooDeep => "nests nodes too deeply",
560 Self::TooManyTexts => "has more text nodes than a layout may hold",
561 Self::TooManyIsolated => {
562 "has more nodes with opacity, transforms, or hidden overflow than a layout may hold"
563 }
564 Self::MixedContent => "has more than one of children, text, and shape",
565 Self::DisplayOnLeaf => "sets display on a node with text or shape content",
566 Self::ContainerFieldOnLeaf => {
567 "sets a container field on a node with text or shape content"
568 }
569 Self::RootItemField => "sets a size, margin, position, or item field on the root",
570 Self::TooManyTracks => "has more than 64 tracks",
571 Self::RepeatCount => "repeats tracks a number of times outside 1 to 64",
572 Self::RepeatInAutoTracks => "uses repeat() in implicit track sizes",
573 Self::AutoRepeatNeedsFixedTracks => {
574 "repeats auto-fill or auto-fit beside another, or with a track without a fixed size"
575 }
576 Self::GridLineOutOfRange => {
577 "names a grid line outside -65 to 65 or line 0, or a span outside 1 to 64"
578 }
579 Self::InvalidKeyframes => "has invalid keyframes",
580 Self::OutOfRange => "has a value out of range",
581 Self::LengthKeyframesDiffer => "has size keyframes that differ in kind",
582 Self::PaintKeyframesDiffer => "has paint keyframes that differ in kind or stops",
583 Self::TextKeyframesInterpolate => "has text keyframes that do not hold",
584 Self::GridFieldOnFlex(_)
585 | Self::FlexFieldOnGrid(_)
586 | Self::FlexItemFieldInGrid(_)
587 | Self::GridItemFieldInFlex(_) => unreachable!("written above"),
588 })
589 }
590}
591
592#[derive(Clone, Debug, Eq, PartialEq)]
594pub struct LayoutIssue {
595 pub at: String,
597 pub problem: LayoutProblem,
599}
600
601impl LayoutIssue {
602 fn new(at: impl Into<String>, problem: LayoutProblem) -> Self {
603 Self {
604 at: at.into(),
605 problem,
606 }
607 }
608}
609
610impl From<ValueIssue> for LayoutIssue {
611 fn from(issue: ValueIssue) -> Self {
612 Self::new(
613 issue.at,
614 match issue.problem {
615 ValueProblem::InvalidKeyframes => LayoutProblem::InvalidKeyframes,
616 ValueProblem::OutOfRange => LayoutProblem::OutOfRange,
617 ValueProblem::PaintKeyframesDiffer => LayoutProblem::PaintKeyframesDiffer,
618 },
619 )
620 }
621}
622
623#[derive(Clone, Debug, Eq, PartialEq)]
625pub enum GeneratorIssue {
626 Invalid,
628 Shape(ShapeIssue),
631 Layout(LayoutIssue),
633}
634
635impl From<LayoutIssue> for GeneratorIssue {
636 fn from(issue: LayoutIssue) -> Self {
637 Self::Layout(issue)
638 }
639}
640
641impl From<ValueIssue> for GeneratorIssue {
642 fn from(issue: ValueIssue) -> Self {
643 Self::Layout(issue.into())
644 }
645}
646
647#[derive(Default)]
649struct Counts {
650 nodes: usize,
651 texts: usize,
652 isolated: usize,
653 shape_items: usize,
654}
655
656type LayoutChecked = Result<(), GeneratorIssue>;
657
658#[derive(Copy, Clone)]
661struct Context {
662 shorter_side: Option<f32>,
663 parent: Option<Display>,
664}
665
666pub(crate) fn validate_layout(root: &LayoutNode, canvas: Option<(u32, u32)>) -> LayoutChecked {
673 let mut counts = Counts::default();
674 if root.has_item_field() {
675 return Err(LayoutIssue::new("root", LayoutProblem::RootItemField).into());
676 }
677 #[allow(clippy::cast_precision_loss)]
678 let shorter_side = canvas.map(|(width, height)| width.min(height) as f32);
679 let context = Context {
680 shorter_side,
681 parent: None,
682 };
683 validate_node(root, "root", 1, context, &mut counts)
684}
685
686fn validate_node(
687 node: &LayoutNode,
688 at: &str,
689 depth: usize,
690 context: Context,
691 counts: &mut Counts,
692) -> LayoutChecked {
693 let issue = |problem| Err(LayoutIssue::new(at, problem).into());
694 counts.nodes += 1;
695 if counts.nodes > MAX_LAYOUT_NODES {
696 return issue(LayoutProblem::TooManyNodes);
697 }
698 if depth > MAX_LAYOUT_DEPTH {
699 return issue(LayoutProblem::TooDeep);
700 }
701 let contents = usize::from(!node.children.is_empty())
702 + usize::from(node.text.is_some())
703 + usize::from(node.shape.is_some());
704 if contents > 1 {
705 return issue(LayoutProblem::MixedContent);
706 }
707 let leaf = node.text.is_some() || node.shape.is_some();
708 if leaf && node.display.is_some() {
709 return issue(LayoutProblem::DisplayOnLeaf);
710 }
711 if leaf && node.has_container_field() {
712 return issue(LayoutProblem::ContainerFieldOnLeaf);
713 }
714 let display = node.display.unwrap_or_default();
715 let mismatch = match display {
716 Display::Flex => node
717 .grid_container_field()
718 .map(LayoutProblem::GridFieldOnFlex),
719 Display::Grid => node
720 .flex_container_field()
721 .map(LayoutProblem::FlexFieldOnGrid),
722 };
723 if let Some(problem) = mismatch {
724 return issue(problem);
725 }
726 let item_mismatch = match context.parent {
727 Some(Display::Flex) => node
728 .grid_item_field()
729 .map(LayoutProblem::GridItemFieldInFlex),
730 Some(Display::Grid) => node
731 .flex_item_field()
732 .map(LayoutProblem::FlexItemFieldInGrid),
733 None => None,
734 };
735 if let Some(problem) = item_mismatch {
736 return issue(problem);
737 }
738 if node.text.is_some() {
739 counts.texts += 1;
740 if counts.texts > MAX_LAYOUT_TEXTS {
741 return issue(LayoutProblem::TooManyTexts);
742 }
743 }
744 if node.is_isolated() {
745 counts.isolated += 1;
746 if counts.isolated > MAX_LAYOUT_ISOLATED {
747 return issue(LayoutProblem::TooManyIsolated);
748 }
749 }
750 validate_fields(node, at)?;
751 validate_grid(node, at)?;
752 validate_style(node, at, context.shorter_side)?;
753 if let Some(text) = &node.text {
754 validate_text(text, &format!("{at}.text"))?;
755 }
756 if let Some(shape) = &node.shape {
757 crate::shapes::validate_items(
758 &shape.items,
759 &format!("{at}.shape.items"),
760 1,
761 &mut counts.shape_items,
762 )
763 .map_err(GeneratorIssue::Shape)?;
764 }
765 let inner = Context {
766 shorter_side: context.shorter_side,
767 parent: Some(display),
768 };
769 for (index, child) in node.children.iter().enumerate() {
770 validate_node(
771 child,
772 &format!("{at}.children[{index}]"),
773 depth + 1,
774 inner,
775 counts,
776 )?;
777 }
778 Ok(())
779}
780
781fn optional<T, E>(value: Option<&T>, check: impl FnOnce(&T) -> Result<(), E>) -> LayoutChecked
782where
783 GeneratorIssue: From<E>,
784{
785 Ok(value.map_or(Ok(()), check)?)
786}
787
788fn validate_fields(node: &LayoutNode, at: &str) -> LayoutChecked {
789 let field = |name: &str| format!("{at}.{name}");
790 for (name, gap) in [
791 ("gap", &node.gap),
792 ("row_gap", &node.row_gap),
793 ("column_gap", &node.column_gap),
794 ] {
795 optional(gap.as_ref(), |gap| size(gap, &field(name), non_negative))?;
796 }
797 optional(node.padding.as_ref(), |padding| {
798 insets(padding, &field("padding"), non_negative)
799 })?;
800 optional(node.background.as_ref(), |paint| {
801 validate_paint(paint, &field("background"))
802 })?;
803 if let Some(border) = &node.border {
804 size(&border.width, &field("border.width"), non_negative)?;
805 validate_paint(&border.paint, &field("border.paint"))?;
806 }
807 optional(node.border_radius.as_ref(), |radius| {
808 size(radius, &field("border_radius"), non_negative)
809 })?;
810 for (name, size) in [
811 ("width", &node.width),
812 ("height", &node.height),
813 ("min_width", &node.min_width),
814 ("min_height", &node.min_height),
815 ("max_width", &node.max_width),
816 ("max_height", &node.max_height),
817 ] {
818 optional(size.as_ref(), |size| {
819 length(size, &field(name), non_negative)
820 })?;
821 }
822 if node
823 .aspect_ratio
824 .is_some_and(|ratio| !ratio.is_finite() || ratio <= 0.0)
825 {
826 return Err(ValueIssue::new(field("aspect_ratio"), ValueProblem::OutOfRange).into());
827 }
828 optional(node.margin.as_ref(), |margin| {
829 insets(margin, &field("margin"), any)
830 })?;
831 optional(node.flex_grow.as_ref(), |grow| {
832 scalar(grow, &field("flex_grow"), non_negative)
833 })?;
834 optional(node.flex_shrink.as_ref(), |shrink| {
835 scalar(shrink, &field("flex_shrink"), non_negative)
836 })?;
837 optional(node.flex_basis.as_ref(), |basis| {
838 length(basis, &field("flex_basis"), non_negative)
839 })?;
840 if let Some(inset) = &node.inset {
841 for (name, side) in inset.sides() {
842 optional(side, |side| {
843 length(side, &field(&format!("inset.{name}")), any)
844 })?;
845 }
846 }
847 optional(node.opacity.as_ref(), |opacity| {
848 scalar(opacity, &field("opacity"), unit)
849 })?;
850 optional(node.translate.as_ref(), |translate| {
851 animated(translate, &field("translate"), |offset| {
852 offset.x.value().is_finite() && offset.y.value().is_finite()
853 })
854 })?;
855 optional(node.scale.as_ref(), |scale| {
856 vec2(scale, &field("scale"), |_| true)
857 })?;
858 optional(node.rotation_degrees.as_ref(), |rotation| {
859 scalar(rotation, &field("rotation_degrees"), any)
860 })
861}
862
863fn validate_grid(node: &LayoutNode, at: &str) -> LayoutChecked {
865 let issue = |name: &str, problem| Err(LayoutIssue::new(format!("{at}.{name}"), problem).into());
866 for (name, tracks, explicit) in [
867 ("grid_template_columns", &node.grid_template_columns, true),
868 ("grid_template_rows", &node.grid_template_rows, true),
869 ("grid_auto_columns", &node.grid_auto_columns, false),
870 ("grid_auto_rows", &node.grid_auto_rows, false),
871 ] {
872 let Some(tracks) = tracks else { continue };
873 if let Some(problem) = track_problem(tracks, explicit) {
874 return issue(name, problem);
875 }
876 }
877 for (name, placement) in [
878 ("grid_column", node.grid_column),
879 ("grid_row", node.grid_row),
880 ] {
881 let in_range = |edge: GridEdge| match edge {
882 GridEdge::Auto => true,
883 GridEdge::Line(line) => line != 0 && line.abs() <= MAX_GRID_LINE,
884 GridEdge::Span(count) => (1..=64).contains(&count),
885 };
886 if placement.is_some_and(|placement| !placement.edges().into_iter().all(in_range)) {
887 return issue(name, LayoutProblem::GridLineOutOfRange);
888 }
889 }
890 Ok(())
891}
892
893fn track_problem(tracks: &TrackList, explicit: bool) -> Option<LayoutProblem> {
894 let lengths_valid = tracks
895 .lengths()
896 .all(|length| length.value().is_finite() && length.value() >= 0.0);
897 let shares_valid = tracks
898 .shares()
899 .all(|share| share.is_finite() && share > 0.0);
900 if !(lengths_valid && shares_valid) {
901 return Some(LayoutProblem::OutOfRange);
902 }
903 let repeats = tracks.0.iter().filter_map(|component| match component {
904 TrackComponent::Repeat(count, _) => Some(*count),
905 TrackComponent::Single(_) => None,
906 });
907 if !explicit && repeats.clone().next().is_some() {
908 return Some(LayoutProblem::RepeatInAutoTracks);
909 }
910 if repeats
911 .clone()
912 .any(|count| matches!(count, RepeatCount::Count(count) if !(1..=64).contains(&count)))
913 {
914 return Some(LayoutProblem::RepeatCount);
915 }
916 if tracks.track_count() > MAX_GRID_TRACKS {
917 return Some(LayoutProblem::TooManyTracks);
918 }
919 let automatic = repeats
920 .filter(|count| !matches!(count, RepeatCount::Count(_)))
921 .count();
922 if automatic > 1
923 || (automatic == 1
924 && !tracks
925 .sizes()
926 .all(crate::layout_values::TrackSize::is_fixed))
927 {
928 return Some(LayoutProblem::AutoRepeatNeedsFixedTracks);
929 }
930 None
931}
932
933fn validate_style(node: &LayoutNode, at: &str, shorter_side: Option<f32>) -> LayoutChecked {
935 let out_of_range = |name: &str| {
936 Err(LayoutIssue::new(format!("{at}.{name}"), LayoutProblem::OutOfRange).into())
937 };
938 if node
939 .font_family
940 .as_ref()
941 .is_some_and(|family| family.is_empty() || family.len() > MAX_FONT_FAMILY_BYTES)
942 {
943 return out_of_range("font_family");
944 }
945 optional(node.font_size.as_ref(), |font_size| {
946 animated(font_size, &format!("{at}.font_size"), |value| {
947 let fraction = match (*value, shorter_side) {
949 (LayoutSize::Px(_), None) => 0.0,
950 (value, Some(side)) => value.fraction(side),
951 (value, None) => value.value(),
952 };
953 value.value().is_finite() && value.value() > 0.0 && fraction <= MAX_LAYOUT_FONT_SIZE
954 })
955 })?;
956 if node.fill.as_ref().is_some_and(|fill| !fill.is_valid()) {
957 return out_of_range("fill");
958 }
959 if node
960 .line_height
961 .is_some_and(|height| !LINE_HEIGHT_RANGE.contains(&height))
962 {
963 return out_of_range("line_height");
964 }
965 if node
966 .letter_spacing
967 .is_some_and(|spacing| !LETTER_SPACING_RANGE.contains(&spacing))
968 {
969 return out_of_range("letter_spacing");
970 }
971 if node
972 .font_weight
973 .is_some_and(|weight| !FONT_WEIGHT_RANGE.contains(&weight))
974 {
975 return out_of_range("font_weight");
976 }
977 if node
978 .strokes
979 .as_deref()
980 .is_some_and(|strokes| !crate::text::valid_strokes(strokes))
981 {
982 return out_of_range("strokes");
983 }
984 if node
985 .shadows
986 .as_deref()
987 .is_some_and(|shadows| !crate::text::valid_shadows(shadows))
988 {
989 return out_of_range("shadows");
990 }
991 if !crate::text_animation::valid_animators(&node.text_animators, true) {
992 return out_of_range("text_animators");
993 }
994 Ok(())
995}
996
997fn insets(animation: &Animated<Insets>, at: &str, check: fn(f32) -> bool) -> Checked {
998 animated(animation, at, |value| {
999 value
1000 .sides()
1001 .iter()
1002 .all(|side| side.value().is_finite() && check(side.value()))
1003 })
1004}
1005
1006fn size(animation: &Animated<LayoutSize>, at: &str, check: fn(f32) -> bool) -> Checked {
1007 animated(animation, at, |value| {
1008 value.value().is_finite() && check(value.value())
1009 })
1010}
1011
1012fn length(
1015 animation: &Animated<LayoutLength>,
1016 at: &str,
1017 check: fn(f32) -> bool,
1018) -> Result<(), GeneratorIssue> {
1019 animated(animation, at, |value| {
1020 value.value().is_finite() && check(value.value())
1021 })?;
1022 if let Animated::Keyframes { keyframes } = animation {
1023 let kind = keyframes[0].value.is_percent();
1024 if keyframes.iter().any(|key| key.value.is_percent() != kind) {
1025 return Err(LayoutIssue::new(at, LayoutProblem::LengthKeyframesDiffer).into());
1026 }
1027 }
1028 Ok(())
1029}
1030
1031fn validate_text(content: &Animated<String>, at: &str) -> LayoutChecked {
1032 animated(content, at, |_| true)?;
1033 if let Animated::Keyframes { keyframes } = content
1034 && keyframes
1035 .iter()
1036 .any(|key| key.interpolation != Interpolation::Hold)
1037 {
1038 return Err(LayoutIssue::new(at, LayoutProblem::TextKeyframesInterpolate).into());
1039 }
1040 animated(content, at, |content| {
1041 content.len() <= MAX_TEXT_CONTENT_BYTES
1042 })?;
1043 Ok(())
1044}
1045
1046impl LayoutNode {
1047 fn has_container_field(&self) -> bool {
1049 self.flex_container_field().is_some()
1050 || self.grid_container_field().is_some()
1051 || self.gap.is_some()
1052 || self.row_gap.is_some()
1053 || self.column_gap.is_some()
1054 || self.justify_content.is_some()
1055 || self.align_items.is_some()
1056 || self.align_content.is_some()
1057 }
1058
1059 fn flex_container_field(&self) -> Option<&'static str> {
1061 [
1062 ("flex_direction", self.flex_direction.is_some()),
1063 ("flex_wrap", self.flex_wrap.is_some()),
1064 ]
1065 .into_iter()
1066 .find_map(|(name, present)| present.then_some(name))
1067 }
1068
1069 fn grid_container_field(&self) -> Option<&'static str> {
1071 [
1072 (
1073 "grid_template_columns",
1074 self.grid_template_columns.is_some(),
1075 ),
1076 ("grid_template_rows", self.grid_template_rows.is_some()),
1077 ("grid_auto_columns", self.grid_auto_columns.is_some()),
1078 ("grid_auto_rows", self.grid_auto_rows.is_some()),
1079 ("grid_auto_flow", self.grid_auto_flow.is_some()),
1080 ("justify_items", self.justify_items.is_some()),
1081 ]
1082 .into_iter()
1083 .find_map(|(name, present)| present.then_some(name))
1084 }
1085
1086 fn flex_item_field(&self) -> Option<&'static str> {
1088 [
1089 ("flex_grow", self.flex_grow.is_some()),
1090 ("flex_shrink", self.flex_shrink.is_some()),
1091 ("flex_basis", self.flex_basis.is_some()),
1092 ]
1093 .into_iter()
1094 .find_map(|(name, present)| present.then_some(name))
1095 }
1096
1097 fn grid_item_field(&self) -> Option<&'static str> {
1099 [
1100 ("grid_column", self.grid_column.is_some()),
1101 ("grid_row", self.grid_row.is_some()),
1102 ("justify_self", self.justify_self.is_some()),
1103 ]
1104 .into_iter()
1105 .find_map(|(name, present)| present.then_some(name))
1106 }
1107
1108 fn has_item_field(&self) -> bool {
1110 self.width.is_some()
1111 || self.height.is_some()
1112 || self.min_width.is_some()
1113 || self.min_height.is_some()
1114 || self.max_width.is_some()
1115 || self.max_height.is_some()
1116 || self.aspect_ratio.is_some()
1117 || self.margin.is_some()
1118 || self.flex_item_field().is_some()
1119 || self.grid_item_field().is_some()
1120 || self.align_self.is_some()
1121 || self.position.is_some()
1122 || self.inset.is_some()
1123 }
1124
1125 #[must_use]
1127 pub const fn has_visual(&self) -> bool {
1128 self.opacity.is_some()
1129 || self.translate.is_some()
1130 || self.scale.is_some()
1131 || self.rotation_degrees.is_some()
1132 }
1133
1134 #[must_use]
1137 pub const fn is_isolated(&self) -> bool {
1138 self.has_visual() || matches!(self.overflow, Some(Overflow::Hidden))
1139 }
1140
1141 #[must_use]
1143 pub fn is_animated(&self) -> bool {
1144 let inset = self.inset.as_ref().is_some_and(|inset| {
1145 inset
1146 .sides()
1147 .into_iter()
1148 .any(|(_, value)| keyed_option(value))
1149 });
1150 let shape = self
1151 .shape
1152 .as_ref()
1153 .is_some_and(|shape| shape.items.iter().any(ShapeItem::is_animated));
1154 let border = self
1155 .border
1156 .as_ref()
1157 .is_some_and(|border| keyed(&border.width) || keyed(&border.paint));
1158 [
1159 &self.gap,
1160 &self.row_gap,
1161 &self.column_gap,
1162 &self.border_radius,
1163 &self.font_size,
1164 ]
1165 .into_iter()
1166 .any(|value| keyed_option(value.as_ref()))
1167 || keyed_option(self.padding.as_ref())
1168 || keyed_option(self.background.as_ref())
1169 || border
1170 || [
1171 &self.width,
1172 &self.height,
1173 &self.min_width,
1174 &self.min_height,
1175 &self.max_width,
1176 &self.max_height,
1177 &self.flex_basis,
1178 ]
1179 .into_iter()
1180 .any(|value| keyed_option(value.as_ref()))
1181 || keyed_option(self.margin.as_ref())
1182 || keyed_option(self.flex_grow.as_ref())
1183 || keyed_option(self.flex_shrink.as_ref())
1184 || inset
1185 || keyed_option(self.opacity.as_ref())
1186 || keyed_option(self.translate.as_ref())
1187 || keyed_option(self.scale.as_ref())
1188 || keyed_option(self.rotation_degrees.as_ref())
1189 || keyed_option(self.text.as_ref())
1190 || shape
1191 || crate::text_animation::animators_have_keyframes(&self.text_animators)
1192 || self.children.iter().any(Self::is_animated)
1193 }
1194
1195 pub fn walk(&self, mut visit: impl FnMut(&str, &Self)) {
1197 fn go(node: &LayoutNode, at: &str, visit: &mut impl FnMut(&str, &LayoutNode)) {
1198 visit(at, node);
1199 for (index, child) in node.children.iter().enumerate() {
1200 go(child, &format!("{at}.children[{index}]"), visit);
1201 }
1202 }
1203 go(self, "root", &mut visit);
1204 }
1205
1206 pub(crate) fn for_each_keyed(
1209 &self,
1210 at: &str,
1211 visit: &mut impl FnMut(&str, &[alpha2_time::MediaTime]),
1212 ) {
1213 let field = |name: &str| format!("{at}.{name}");
1214 if let Some(text) = &self.text {
1215 visit_keyed(&field("text"), text, visit);
1216 }
1217 if let Some(shape) = &self.shape {
1218 crate::shapes::for_each_keyed_item(&shape.items, &field("shape.items"), visit);
1219 }
1220 crate::text_animation::for_each_keyed_animator(
1221 &self.text_animators,
1222 &field("text_animators"),
1223 visit,
1224 );
1225 for (name, value) in [
1226 ("gap", &self.gap),
1227 ("row_gap", &self.row_gap),
1228 ("column_gap", &self.column_gap),
1229 ("border_radius", &self.border_radius),
1230 ("font_size", &self.font_size),
1231 ] {
1232 visit_option(&field(name), value.as_ref(), visit);
1233 }
1234 visit_option(&field("padding"), self.padding.as_ref(), visit);
1235 visit_option(&field("background"), self.background.as_ref(), visit);
1236 if let Some(border) = &self.border {
1237 visit_keyed(&field("border.width"), &border.width, visit);
1238 visit_keyed(&field("border.paint"), &border.paint, visit);
1239 }
1240 for (name, value) in [
1241 ("width", &self.width),
1242 ("height", &self.height),
1243 ("min_width", &self.min_width),
1244 ("min_height", &self.min_height),
1245 ("max_width", &self.max_width),
1246 ("max_height", &self.max_height),
1247 ("flex_basis", &self.flex_basis),
1248 ] {
1249 visit_option(&field(name), value.as_ref(), visit);
1250 }
1251 visit_option(&field("margin"), self.margin.as_ref(), visit);
1252 visit_option(&field("flex_grow"), self.flex_grow.as_ref(), visit);
1253 visit_option(&field("flex_shrink"), self.flex_shrink.as_ref(), visit);
1254 if let Some(inset) = &self.inset {
1255 for (side, value) in inset.sides() {
1256 visit_option(&field(&format!("inset.{side}")), value, visit);
1257 }
1258 }
1259 visit_option(&field("opacity"), self.opacity.as_ref(), visit);
1260 visit_option(&field("translate"), self.translate.as_ref(), visit);
1261 visit_option(&field("scale"), self.scale.as_ref(), visit);
1262 visit_option(
1263 &field("rotation_degrees"),
1264 self.rotation_degrees.as_ref(),
1265 visit,
1266 );
1267 for (index, child) in self.children.iter().enumerate() {
1268 child.for_each_keyed(&format!("{at}.children[{index}]"), visit);
1269 }
1270 }
1271
1272 #[must_use]
1275 pub fn without_px(&self, shorter_side: f32) -> Self {
1276 let mut node = self.clone();
1277 node.convert_px(shorter_side);
1278 node
1279 }
1280
1281 fn convert_px(&mut self, shorter_side: f32) {
1282 let size = |value: &mut Option<Animated<LayoutSize>>| {
1283 if let Some(value) = value {
1284 *value = value.map(|size| size.without_px(shorter_side));
1285 }
1286 };
1287 let length = |value: &mut Option<Animated<LayoutLength>>| {
1288 if let Some(value) = value {
1289 *value = value.map(|length| length.without_px(shorter_side));
1290 }
1291 };
1292 let insets = |value: &mut Option<Animated<Insets>>| {
1293 if let Some(value) = value {
1294 *value = value.map(|insets| insets.without_px(shorter_side));
1295 }
1296 };
1297 let tracks = |value: &mut Option<TrackList>| {
1298 if let Some(tracks) = value {
1299 *tracks = tracks.without_px(shorter_side);
1300 }
1301 };
1302 tracks(&mut self.grid_template_columns);
1303 tracks(&mut self.grid_template_rows);
1304 tracks(&mut self.grid_auto_columns);
1305 tracks(&mut self.grid_auto_rows);
1306 for value in [
1307 &mut self.gap,
1308 &mut self.row_gap,
1309 &mut self.column_gap,
1310 &mut self.border_radius,
1311 &mut self.font_size,
1312 ] {
1313 size(value);
1314 }
1315 insets(&mut self.padding);
1316 insets(&mut self.margin);
1317 if let Some(border) = &mut self.border {
1318 border.width = border.width.map(|width| width.without_px(shorter_side));
1319 }
1320 for value in [
1321 &mut self.width,
1322 &mut self.height,
1323 &mut self.min_width,
1324 &mut self.min_height,
1325 &mut self.max_width,
1326 &mut self.max_height,
1327 &mut self.flex_basis,
1328 ] {
1329 length(value);
1330 }
1331 if let Some(inset) = &mut self.inset {
1332 for side in [
1333 &mut inset.left,
1334 &mut inset.top,
1335 &mut inset.right,
1336 &mut inset.bottom,
1337 ] {
1338 length(side);
1339 }
1340 }
1341 if let Some(translate) = &mut self.translate {
1342 *translate = translate.map(|offset| offset.without_px(shorter_side));
1343 }
1344 for child in &mut self.children {
1345 child.convert_px(shorter_side);
1346 }
1347 }
1348}
1349
1350fn visit_option<T>(
1351 at: &str,
1352 animation: Option<&Animated<T>>,
1353 visit: &mut impl FnMut(&str, &[alpha2_time::MediaTime]),
1354) {
1355 if let Some(animation) = animation {
1356 visit_keyed(at, animation, visit);
1357 }
1358}
1359
1360const fn keyed<T>(animation: &Animated<T>) -> bool {
1361 matches!(animation, Animated::Keyframes { .. })
1362}
1363
1364const fn keyed_option<T>(animation: Option<&Animated<T>>) -> bool {
1365 matches!(animation, Some(Animated::Keyframes { .. }))
1366}