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