1use crate::checks::{
14 Checked, ValueIssue, ValueProblem, animated, any, non_negative, scalar, unit, validate_paint,
15 vec2,
16};
17use crate::layout_values::{
18 GridEdge, GridPlacement, Insets, LayoutLength, LayoutOffset, LayoutSize, RepeatCount,
19 TrackComponent, TrackList,
20};
21use crate::{
22 Animated, Interpolation, LINE_HEIGHT_RANGE, MAX_FONT_FAMILY_BYTES, MAX_TEXT_CONTENT_BYTES,
23 MAX_TEXT_OUTLINE_WIDTH, Paint, Rgba, ShapeIssue, ShapeItem, TextAlign, TextOutline, 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;
37pub const MAX_LAYOUT_FONT_SIZE: f32 = 1.0;
40pub const MAX_GRID_TRACKS: usize = 64;
42const MAX_GRID_LINE: i16 = 65;
44
45pub const LAYOUT_TEXT_DEFAULTS: LayoutTextStyle = LayoutTextStyle {
47 font_family: None,
48 font_size: 0.05,
49 color: Rgba::new(1.0, 1.0, 1.0, 1.0),
50 line_height: 1.2,
51 text_align: TextAlign::Start,
52 outline: None,
53};
54
55#[derive(Clone, Debug, PartialEq)]
58pub struct LayoutTextStyle {
59 pub font_family: Option<String>,
61 pub font_size: f32,
63 pub color: Rgba,
65 pub line_height: f32,
67 pub text_align: TextAlign,
69 pub outline: Option<TextOutline>,
71}
72
73#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
82#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
83#[serde(deny_unknown_fields)]
84pub struct LayoutNode {
85 #[serde(default, skip_serializing_if = "Vec::is_empty")]
88 pub children: Vec<LayoutNode>,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub text: Option<Animated<String>>,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub shape: Option<LayoutShape>,
99
100 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub display: Option<Display>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub flex_direction: Option<FlexDirection>,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub flex_wrap: Option<FlexWrap>,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub grid_template_columns: Option<TrackList>,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub grid_template_rows: Option<TrackList>,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub grid_auto_columns: Option<TrackList>,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub grid_auto_rows: Option<TrackList>,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub grid_auto_flow: Option<GridAutoFlow>,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub gap: Option<Animated<LayoutSize>>,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub row_gap: Option<Animated<LayoutSize>>,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub column_gap: Option<Animated<LayoutSize>>,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub justify_content: Option<JustifyContent>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub align_items: Option<AlignItems>,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub align_content: Option<AlignContent>,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub justify_items: Option<AlignItems>,
153
154 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub padding: Option<Animated<Insets>>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub background: Option<Animated<Paint>>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub border: Option<LayoutBorder>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub border_radius: Option<Animated<LayoutSize>>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub overflow: Option<Overflow>,
172
173 #[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")]
198 pub margin: Option<Animated<Insets>>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub flex_grow: Option<Animated<f32>>,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub flex_shrink: Option<Animated<f32>>,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub flex_basis: Option<Animated<LayoutLength>>,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub align_self: Option<AlignItems>,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub justify_self: Option<AlignItems>,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub grid_column: Option<GridPlacement>,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub grid_row: Option<GridPlacement>,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub position: Option<Position>,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub inset: Option<LayoutInset>,
232
233 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub font_family: Option<String>,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub font_size: Option<Animated<LayoutSize>>,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
243 pub color: Option<Rgba>,
244 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub line_height: Option<f32>,
248 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub text_align: Option<TextAlign>,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub outline: Option<TextOutline>,
256
257 #[serde(default, skip_serializing_if = "Option::is_none")]
260 pub opacity: Option<Animated<f32>>,
261 #[serde(default, skip_serializing_if = "Option::is_none")]
264 pub translate: Option<Animated<LayoutOffset>>,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
268 pub scale: Option<Animated<Vec2>>,
269 #[serde(default, skip_serializing_if = "Option::is_none")]
272 pub rotation_degrees: Option<Animated<f32>>,
273}
274
275#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
280#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
281#[serde(deny_unknown_fields)]
282pub struct LayoutShape {
283 pub items: Vec<ShapeItem>,
285}
286
287#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
289#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
290#[serde(deny_unknown_fields)]
291pub struct LayoutBorder {
292 pub width: Animated<LayoutSize>,
294 pub paint: Animated<Paint>,
296}
297
298#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
302#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
303#[serde(deny_unknown_fields)]
304pub struct LayoutInset {
305 #[serde(default, skip_serializing_if = "Option::is_none")]
307 pub left: Option<Animated<LayoutLength>>,
308 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub top: Option<Animated<LayoutLength>>,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
313 pub right: Option<Animated<LayoutLength>>,
314 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub bottom: Option<Animated<LayoutLength>>,
317}
318
319impl LayoutInset {
320 fn sides(&self) -> [(&'static str, Option<&Animated<LayoutLength>>); 4] {
321 [
322 ("left", self.left.as_ref()),
323 ("top", self.top.as_ref()),
324 ("right", self.right.as_ref()),
325 ("bottom", self.bottom.as_ref()),
326 ]
327 }
328}
329
330#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
332#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
333#[serde(rename_all = "snake_case")]
334pub enum Display {
335 #[default]
337 Flex,
338 Grid,
340}
341
342#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
344#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
345#[serde(rename_all = "snake_case")]
346pub enum FlexDirection {
347 #[default]
349 Row,
350 Column,
352 RowReverse,
354 ColumnReverse,
356}
357
358#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
360#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
361pub enum FlexWrap {
362 #[default]
364 #[serde(rename = "nowrap")]
365 NoWrap,
366 #[serde(rename = "wrap")]
368 Wrap,
369}
370
371#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
373#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
374#[serde(rename_all = "snake_case")]
375pub enum GridAutoFlow {
376 #[default]
378 Row,
379 Column,
381 RowDense,
383 ColumnDense,
385}
386
387#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
390#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
391#[serde(rename_all = "snake_case")]
392pub enum JustifyContent {
393 #[default]
395 Start,
396 Center,
398 End,
400 SpaceBetween,
402 SpaceAround,
404 SpaceEvenly,
406}
407
408#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
411#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
412#[serde(rename_all = "snake_case")]
413pub enum AlignContent {
414 Start,
416 Center,
418 End,
420 SpaceBetween,
422 SpaceAround,
424 SpaceEvenly,
426 #[default]
428 Stretch,
429}
430
431#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
434#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
435#[serde(rename_all = "snake_case")]
436pub enum AlignItems {
437 Start,
439 Center,
441 End,
443 #[default]
445 Stretch,
446}
447
448#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
450#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
451#[serde(rename_all = "snake_case")]
452pub enum Position {
453 #[default]
455 Relative,
456 Absolute,
458}
459
460#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
462#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
463#[serde(rename_all = "snake_case")]
464pub enum Overflow {
465 #[default]
467 Visible,
468 Hidden,
470}
471
472#[derive(Copy, Clone, Debug, Eq, PartialEq)]
474#[non_exhaustive]
475pub enum LayoutProblem {
476 TooManyNodes,
478 TooDeep,
480 TooManyTexts,
482 TooManyIsolated,
485 MixedContent,
487 DisplayOnLeaf,
489 ContainerFieldOnLeaf,
491 RootItemField,
493 GridFieldOnFlex(&'static str),
495 FlexFieldOnGrid(&'static str),
497 FlexItemFieldInGrid(&'static str),
499 GridItemFieldInFlex(&'static str),
501 TooManyTracks,
503 RepeatCount,
505 RepeatInAutoTracks,
507 AutoRepeatNeedsFixedTracks,
510 GridLineOutOfRange,
512 InvalidKeyframes,
514 OutOfRange,
516 LengthKeyframesDiffer,
518 PaintKeyframesDiffer,
520 TextKeyframesInterpolate,
522}
523
524impl fmt::Display for LayoutProblem {
525 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
526 match self {
527 Self::GridFieldOnFlex(field) => {
528 return write!(
529 formatter,
530 "sets {field}, a grid container field, without display grid"
531 );
532 }
533 Self::FlexFieldOnGrid(field) => {
534 return write!(
535 formatter,
536 "sets {field}, a flex container field, on a grid container"
537 );
538 }
539 Self::FlexItemFieldInGrid(field) => {
540 return write!(
541 formatter,
542 "sets {field}, a flex item field, in a grid container"
543 );
544 }
545 Self::GridItemFieldInFlex(field) => {
546 return write!(
547 formatter,
548 "sets {field}, a grid item field, in a flex container"
549 );
550 }
551 _ => {}
552 }
553 formatter.write_str(match self {
554 Self::TooManyNodes => "has more nodes than a layout may hold",
555 Self::TooDeep => "nests nodes too deeply",
556 Self::TooManyTexts => "has more text nodes than a layout may hold",
557 Self::TooManyIsolated => {
558 "has more nodes with opacity, transforms, or hidden overflow than a layout may hold"
559 }
560 Self::MixedContent => "has more than one of children, text, and shape",
561 Self::DisplayOnLeaf => "sets display on a node with text or shape content",
562 Self::ContainerFieldOnLeaf => {
563 "sets a container field on a node with text or shape content"
564 }
565 Self::RootItemField => "sets a size, margin, position, or item field on the root",
566 Self::TooManyTracks => "has more than 64 tracks",
567 Self::RepeatCount => "repeats tracks a number of times outside 1 to 64",
568 Self::RepeatInAutoTracks => "uses repeat() in implicit track sizes",
569 Self::AutoRepeatNeedsFixedTracks => {
570 "repeats auto-fill or auto-fit beside another, or with a track without a fixed size"
571 }
572 Self::GridLineOutOfRange => {
573 "names a grid line outside -65 to 65 or line 0, or a span outside 1 to 64"
574 }
575 Self::InvalidKeyframes => "has invalid keyframes",
576 Self::OutOfRange => "has a value out of range",
577 Self::LengthKeyframesDiffer => "has size keyframes that differ in kind",
578 Self::PaintKeyframesDiffer => "has paint keyframes that differ in kind or stops",
579 Self::TextKeyframesInterpolate => "has text keyframes that do not hold",
580 Self::GridFieldOnFlex(_)
581 | Self::FlexFieldOnGrid(_)
582 | Self::FlexItemFieldInGrid(_)
583 | Self::GridItemFieldInFlex(_) => unreachable!("written above"),
584 })
585 }
586}
587
588#[derive(Clone, Debug, Eq, PartialEq)]
590pub struct LayoutIssue {
591 pub at: String,
593 pub problem: LayoutProblem,
595}
596
597impl LayoutIssue {
598 fn new(at: impl Into<String>, problem: LayoutProblem) -> Self {
599 Self {
600 at: at.into(),
601 problem,
602 }
603 }
604}
605
606impl From<ValueIssue> for LayoutIssue {
607 fn from(issue: ValueIssue) -> Self {
608 Self::new(
609 issue.at,
610 match issue.problem {
611 ValueProblem::InvalidKeyframes => LayoutProblem::InvalidKeyframes,
612 ValueProblem::OutOfRange => LayoutProblem::OutOfRange,
613 ValueProblem::PaintKeyframesDiffer => LayoutProblem::PaintKeyframesDiffer,
614 },
615 )
616 }
617}
618
619#[derive(Clone, Debug, Eq, PartialEq)]
621pub enum GeneratorIssue {
622 Invalid,
624 Shape(ShapeIssue),
627 Layout(LayoutIssue),
629}
630
631impl From<LayoutIssue> for GeneratorIssue {
632 fn from(issue: LayoutIssue) -> Self {
633 Self::Layout(issue)
634 }
635}
636
637impl From<ValueIssue> for GeneratorIssue {
638 fn from(issue: ValueIssue) -> Self {
639 Self::Layout(issue.into())
640 }
641}
642
643#[derive(Default)]
645struct Counts {
646 nodes: usize,
647 texts: usize,
648 isolated: usize,
649 shape_items: usize,
650}
651
652type LayoutChecked = Result<(), GeneratorIssue>;
653
654#[derive(Copy, Clone)]
657struct Context {
658 shorter_side: Option<f32>,
659 parent: Option<Display>,
660}
661
662pub(crate) fn validate_layout(root: &LayoutNode, canvas: Option<(u32, u32)>) -> LayoutChecked {
671 let mut counts = Counts::default();
672 if root.has_item_field() {
673 return Err(LayoutIssue::new("root", LayoutProblem::RootItemField).into());
674 }
675 #[allow(clippy::cast_precision_loss)]
676 let shorter_side = canvas.map(|(width, height)| width.min(height) as f32);
677 let context = Context {
678 shorter_side,
679 parent: None,
680 };
681 validate_node(root, "root", 1, context, &mut counts)
682}
683
684fn validate_node(
685 node: &LayoutNode,
686 at: &str,
687 depth: usize,
688 context: Context,
689 counts: &mut Counts,
690) -> LayoutChecked {
691 let issue = |problem| Err(LayoutIssue::new(at, problem).into());
692 counts.nodes += 1;
693 if counts.nodes > MAX_LAYOUT_NODES {
694 return issue(LayoutProblem::TooManyNodes);
695 }
696 if depth > MAX_LAYOUT_DEPTH {
697 return issue(LayoutProblem::TooDeep);
698 }
699 let contents = usize::from(!node.children.is_empty())
700 + usize::from(node.text.is_some())
701 + usize::from(node.shape.is_some());
702 if contents > 1 {
703 return issue(LayoutProblem::MixedContent);
704 }
705 let leaf = node.text.is_some() || node.shape.is_some();
706 if leaf && node.display.is_some() {
707 return issue(LayoutProblem::DisplayOnLeaf);
708 }
709 if leaf && node.has_container_field() {
710 return issue(LayoutProblem::ContainerFieldOnLeaf);
711 }
712 let display = node.display.unwrap_or_default();
713 let mismatch = match display {
714 Display::Flex => node
715 .grid_container_field()
716 .map(LayoutProblem::GridFieldOnFlex),
717 Display::Grid => node
718 .flex_container_field()
719 .map(LayoutProblem::FlexFieldOnGrid),
720 };
721 if let Some(problem) = mismatch {
722 return issue(problem);
723 }
724 let item_mismatch = match context.parent {
725 Some(Display::Flex) => node
726 .grid_item_field()
727 .map(LayoutProblem::GridItemFieldInFlex),
728 Some(Display::Grid) => node
729 .flex_item_field()
730 .map(LayoutProblem::FlexItemFieldInGrid),
731 None => None,
732 };
733 if let Some(problem) = item_mismatch {
734 return issue(problem);
735 }
736 if node.text.is_some() {
737 counts.texts += 1;
738 if counts.texts > MAX_LAYOUT_TEXTS {
739 return issue(LayoutProblem::TooManyTexts);
740 }
741 }
742 if node.is_isolated() {
743 counts.isolated += 1;
744 if counts.isolated > MAX_LAYOUT_ISOLATED {
745 return issue(LayoutProblem::TooManyIsolated);
746 }
747 }
748 validate_fields(node, at)?;
749 validate_grid(node, at)?;
750 validate_style(node, at, context.shorter_side)?;
751 if let Some(text) = &node.text {
752 validate_text(text, &format!("{at}.text"))?;
753 }
754 if let Some(shape) = &node.shape {
755 crate::shapes::validate_items(
756 &shape.items,
757 &format!("{at}.shape.items"),
758 1,
759 &mut counts.shape_items,
760 )
761 .map_err(GeneratorIssue::Shape)?;
762 }
763 let inner = Context {
764 shorter_side: context.shorter_side,
765 parent: Some(display),
766 };
767 for (index, child) in node.children.iter().enumerate() {
768 validate_node(
769 child,
770 &format!("{at}.children[{index}]"),
771 depth + 1,
772 inner,
773 counts,
774 )?;
775 }
776 Ok(())
777}
778
779fn optional<T, E>(value: Option<&T>, check: impl FnOnce(&T) -> Result<(), E>) -> LayoutChecked
780where
781 GeneratorIssue: From<E>,
782{
783 Ok(value.map_or(Ok(()), check)?)
784}
785
786fn validate_fields(node: &LayoutNode, at: &str) -> LayoutChecked {
787 let field = |name: &str| format!("{at}.{name}");
788 for (name, gap) in [
789 ("gap", &node.gap),
790 ("row_gap", &node.row_gap),
791 ("column_gap", &node.column_gap),
792 ] {
793 optional(gap.as_ref(), |gap| size(gap, &field(name), non_negative))?;
794 }
795 optional(node.padding.as_ref(), |padding| {
796 insets(padding, &field("padding"), non_negative)
797 })?;
798 optional(node.background.as_ref(), |paint| {
799 validate_paint(paint, &field("background"))
800 })?;
801 if let Some(border) = &node.border {
802 size(&border.width, &field("border.width"), non_negative)?;
803 validate_paint(&border.paint, &field("border.paint"))?;
804 }
805 optional(node.border_radius.as_ref(), |radius| {
806 size(radius, &field("border_radius"), non_negative)
807 })?;
808 for (name, size) in [
809 ("width", &node.width),
810 ("height", &node.height),
811 ("min_width", &node.min_width),
812 ("min_height", &node.min_height),
813 ("max_width", &node.max_width),
814 ("max_height", &node.max_height),
815 ] {
816 optional(size.as_ref(), |size| {
817 length(size, &field(name), non_negative)
818 })?;
819 }
820 if node
821 .aspect_ratio
822 .is_some_and(|ratio| !ratio.is_finite() || ratio <= 0.0)
823 {
824 return Err(ValueIssue::new(field("aspect_ratio"), ValueProblem::OutOfRange).into());
825 }
826 optional(node.margin.as_ref(), |margin| {
827 insets(margin, &field("margin"), any)
828 })?;
829 optional(node.flex_grow.as_ref(), |grow| {
830 scalar(grow, &field("flex_grow"), non_negative)
831 })?;
832 optional(node.flex_shrink.as_ref(), |shrink| {
833 scalar(shrink, &field("flex_shrink"), non_negative)
834 })?;
835 optional(node.flex_basis.as_ref(), |basis| {
836 length(basis, &field("flex_basis"), non_negative)
837 })?;
838 if let Some(inset) = &node.inset {
839 for (name, side) in inset.sides() {
840 optional(side, |side| {
841 length(side, &field(&format!("inset.{name}")), any)
842 })?;
843 }
844 }
845 optional(node.opacity.as_ref(), |opacity| {
846 scalar(opacity, &field("opacity"), unit)
847 })?;
848 optional(node.translate.as_ref(), |translate| {
849 animated(translate, &field("translate"), |offset| {
850 offset.x.value().is_finite() && offset.y.value().is_finite()
851 })
852 })?;
853 optional(node.scale.as_ref(), |scale| {
854 vec2(scale, &field("scale"), |_| true)
855 })?;
856 optional(node.rotation_degrees.as_ref(), |rotation| {
857 scalar(rotation, &field("rotation_degrees"), any)
858 })
859}
860
861fn validate_grid(node: &LayoutNode, at: &str) -> LayoutChecked {
863 let issue = |name: &str, problem| Err(LayoutIssue::new(format!("{at}.{name}"), problem).into());
864 for (name, tracks, explicit) in [
865 ("grid_template_columns", &node.grid_template_columns, true),
866 ("grid_template_rows", &node.grid_template_rows, true),
867 ("grid_auto_columns", &node.grid_auto_columns, false),
868 ("grid_auto_rows", &node.grid_auto_rows, false),
869 ] {
870 let Some(tracks) = tracks else { continue };
871 if let Some(problem) = track_problem(tracks, explicit) {
872 return issue(name, problem);
873 }
874 }
875 for (name, placement) in [
876 ("grid_column", node.grid_column),
877 ("grid_row", node.grid_row),
878 ] {
879 let in_range = |edge: GridEdge| match edge {
880 GridEdge::Auto => true,
881 GridEdge::Line(line) => line != 0 && line.abs() <= MAX_GRID_LINE,
882 GridEdge::Span(count) => (1..=64).contains(&count),
883 };
884 if placement.is_some_and(|placement| !placement.edges().into_iter().all(in_range)) {
885 return issue(name, LayoutProblem::GridLineOutOfRange);
886 }
887 }
888 Ok(())
889}
890
891fn track_problem(tracks: &TrackList, explicit: bool) -> Option<LayoutProblem> {
892 let lengths_valid = tracks
893 .lengths()
894 .all(|length| length.value().is_finite() && length.value() >= 0.0);
895 let shares_valid = tracks
896 .shares()
897 .all(|share| share.is_finite() && share > 0.0);
898 if !(lengths_valid && shares_valid) {
899 return Some(LayoutProblem::OutOfRange);
900 }
901 let repeats = tracks.0.iter().filter_map(|component| match component {
902 TrackComponent::Repeat(count, _) => Some(*count),
903 TrackComponent::Single(_) => None,
904 });
905 if !explicit && repeats.clone().next().is_some() {
906 return Some(LayoutProblem::RepeatInAutoTracks);
907 }
908 if repeats
909 .clone()
910 .any(|count| matches!(count, RepeatCount::Count(count) if !(1..=64).contains(&count)))
911 {
912 return Some(LayoutProblem::RepeatCount);
913 }
914 if tracks.track_count() > MAX_GRID_TRACKS {
915 return Some(LayoutProblem::TooManyTracks);
916 }
917 let automatic = repeats
918 .filter(|count| !matches!(count, RepeatCount::Count(_)))
919 .count();
920 if automatic > 1
921 || (automatic == 1
922 && !tracks
923 .sizes()
924 .all(crate::layout_values::TrackSize::is_fixed))
925 {
926 return Some(LayoutProblem::AutoRepeatNeedsFixedTracks);
927 }
928 None
929}
930
931fn validate_style(node: &LayoutNode, at: &str, shorter_side: Option<f32>) -> LayoutChecked {
933 let out_of_range = |name: &str| {
934 Err(LayoutIssue::new(format!("{at}.{name}"), LayoutProblem::OutOfRange).into())
935 };
936 if node
937 .font_family
938 .as_ref()
939 .is_some_and(|family| family.is_empty() || family.len() > MAX_FONT_FAMILY_BYTES)
940 {
941 return out_of_range("font_family");
942 }
943 optional(node.font_size.as_ref(), |font_size| {
944 animated(font_size, &format!("{at}.font_size"), |value| {
945 let fraction = match (*value, shorter_side) {
947 (LayoutSize::Px(_), None) => 0.0,
948 (value, Some(side)) => value.fraction(side),
949 (value, None) => value.value(),
950 };
951 value.value().is_finite() && value.value() > 0.0 && fraction <= MAX_LAYOUT_FONT_SIZE
952 })
953 })?;
954 if node.color.is_some_and(|color| !color.is_valid()) {
955 return out_of_range("color");
956 }
957 if node
958 .line_height
959 .is_some_and(|height| !LINE_HEIGHT_RANGE.contains(&height))
960 {
961 return out_of_range("line_height");
962 }
963 let valid_outline = |outline: TextOutline| {
964 outline.color.is_valid() && outline.width > 0.0 && outline.width <= MAX_TEXT_OUTLINE_WIDTH
965 };
966 if node.outline.is_some_and(|outline| !valid_outline(outline)) {
967 return out_of_range("outline");
968 }
969 Ok(())
970}
971
972fn insets(animation: &Animated<Insets>, at: &str, check: fn(f32) -> bool) -> Checked {
973 animated(animation, at, |value| {
974 value
975 .sides()
976 .iter()
977 .all(|side| side.value().is_finite() && check(side.value()))
978 })
979}
980
981fn size(animation: &Animated<LayoutSize>, at: &str, check: fn(f32) -> bool) -> Checked {
982 animated(animation, at, |value| {
983 value.value().is_finite() && check(value.value())
984 })
985}
986
987fn length(
990 animation: &Animated<LayoutLength>,
991 at: &str,
992 check: fn(f32) -> bool,
993) -> Result<(), GeneratorIssue> {
994 animated(animation, at, |value| {
995 value.value().is_finite() && check(value.value())
996 })?;
997 if let Animated::Keyframes { keyframes } = animation {
998 let kind = keyframes[0].value.is_percent();
999 if keyframes.iter().any(|key| key.value.is_percent() != kind) {
1000 return Err(LayoutIssue::new(at, LayoutProblem::LengthKeyframesDiffer).into());
1001 }
1002 }
1003 Ok(())
1004}
1005
1006fn validate_text(content: &Animated<String>, at: &str) -> LayoutChecked {
1007 animated(content, at, |_| true)?;
1008 if let Animated::Keyframes { keyframes } = content
1009 && keyframes
1010 .iter()
1011 .any(|key| key.interpolation != Interpolation::Hold)
1012 {
1013 return Err(LayoutIssue::new(at, LayoutProblem::TextKeyframesInterpolate).into());
1014 }
1015 animated(content, at, |content| {
1016 content.len() <= MAX_TEXT_CONTENT_BYTES
1017 })?;
1018 Ok(())
1019}
1020
1021impl LayoutNode {
1022 fn has_container_field(&self) -> bool {
1024 self.flex_container_field().is_some()
1025 || self.grid_container_field().is_some()
1026 || self.gap.is_some()
1027 || self.row_gap.is_some()
1028 || self.column_gap.is_some()
1029 || self.justify_content.is_some()
1030 || self.align_items.is_some()
1031 || self.align_content.is_some()
1032 }
1033
1034 fn flex_container_field(&self) -> Option<&'static str> {
1036 [
1037 ("flex_direction", self.flex_direction.is_some()),
1038 ("flex_wrap", self.flex_wrap.is_some()),
1039 ]
1040 .into_iter()
1041 .find_map(|(name, present)| present.then_some(name))
1042 }
1043
1044 fn grid_container_field(&self) -> Option<&'static str> {
1046 [
1047 (
1048 "grid_template_columns",
1049 self.grid_template_columns.is_some(),
1050 ),
1051 ("grid_template_rows", self.grid_template_rows.is_some()),
1052 ("grid_auto_columns", self.grid_auto_columns.is_some()),
1053 ("grid_auto_rows", self.grid_auto_rows.is_some()),
1054 ("grid_auto_flow", self.grid_auto_flow.is_some()),
1055 ("justify_items", self.justify_items.is_some()),
1056 ]
1057 .into_iter()
1058 .find_map(|(name, present)| present.then_some(name))
1059 }
1060
1061 fn flex_item_field(&self) -> Option<&'static str> {
1063 [
1064 ("flex_grow", self.flex_grow.is_some()),
1065 ("flex_shrink", self.flex_shrink.is_some()),
1066 ("flex_basis", self.flex_basis.is_some()),
1067 ]
1068 .into_iter()
1069 .find_map(|(name, present)| present.then_some(name))
1070 }
1071
1072 fn grid_item_field(&self) -> Option<&'static str> {
1074 [
1075 ("grid_column", self.grid_column.is_some()),
1076 ("grid_row", self.grid_row.is_some()),
1077 ("justify_self", self.justify_self.is_some()),
1078 ]
1079 .into_iter()
1080 .find_map(|(name, present)| present.then_some(name))
1081 }
1082
1083 fn has_item_field(&self) -> bool {
1085 self.width.is_some()
1086 || self.height.is_some()
1087 || self.min_width.is_some()
1088 || self.min_height.is_some()
1089 || self.max_width.is_some()
1090 || self.max_height.is_some()
1091 || self.aspect_ratio.is_some()
1092 || self.margin.is_some()
1093 || self.flex_item_field().is_some()
1094 || self.grid_item_field().is_some()
1095 || self.align_self.is_some()
1096 || self.position.is_some()
1097 || self.inset.is_some()
1098 }
1099
1100 #[must_use]
1103 pub const fn has_visual(&self) -> bool {
1104 self.opacity.is_some()
1105 || self.translate.is_some()
1106 || self.scale.is_some()
1107 || self.rotation_degrees.is_some()
1108 }
1109
1110 #[must_use]
1113 pub const fn is_isolated(&self) -> bool {
1114 self.has_visual() || matches!(self.overflow, Some(Overflow::Hidden))
1115 }
1116
1117 #[must_use]
1119 pub fn is_animated(&self) -> bool {
1120 let inset = self.inset.as_ref().is_some_and(|inset| {
1121 inset
1122 .sides()
1123 .into_iter()
1124 .any(|(_, value)| keyed_option(value))
1125 });
1126 let shape = self
1127 .shape
1128 .as_ref()
1129 .is_some_and(|shape| shape.items.iter().any(ShapeItem::is_animated));
1130 let border = self
1131 .border
1132 .as_ref()
1133 .is_some_and(|border| keyed(&border.width) || keyed(&border.paint));
1134 [
1135 &self.gap,
1136 &self.row_gap,
1137 &self.column_gap,
1138 &self.border_radius,
1139 &self.font_size,
1140 ]
1141 .into_iter()
1142 .any(|value| keyed_option(value.as_ref()))
1143 || keyed_option(self.padding.as_ref())
1144 || keyed_option(self.background.as_ref())
1145 || border
1146 || [
1147 &self.width,
1148 &self.height,
1149 &self.min_width,
1150 &self.min_height,
1151 &self.max_width,
1152 &self.max_height,
1153 &self.flex_basis,
1154 ]
1155 .into_iter()
1156 .any(|value| keyed_option(value.as_ref()))
1157 || keyed_option(self.margin.as_ref())
1158 || keyed_option(self.flex_grow.as_ref())
1159 || keyed_option(self.flex_shrink.as_ref())
1160 || inset
1161 || keyed_option(self.opacity.as_ref())
1162 || keyed_option(self.translate.as_ref())
1163 || keyed_option(self.scale.as_ref())
1164 || keyed_option(self.rotation_degrees.as_ref())
1165 || keyed_option(self.text.as_ref())
1166 || shape
1167 || self.children.iter().any(Self::is_animated)
1168 }
1169
1170 pub fn walk(&self, mut visit: impl FnMut(&str, &Self)) {
1172 fn go(node: &LayoutNode, at: &str, visit: &mut impl FnMut(&str, &LayoutNode)) {
1173 visit(at, node);
1174 for (index, child) in node.children.iter().enumerate() {
1175 go(child, &format!("{at}.children[{index}]"), visit);
1176 }
1177 }
1178 go(self, "root", &mut visit);
1179 }
1180
1181 #[must_use]
1185 pub fn without_px(&self, shorter_side: f32) -> Self {
1186 let mut node = self.clone();
1187 node.convert_px(shorter_side);
1188 node
1189 }
1190
1191 fn convert_px(&mut self, shorter_side: f32) {
1192 let size = |value: &mut Option<Animated<LayoutSize>>| {
1193 if let Some(value) = value {
1194 *value = value.map(|size| size.without_px(shorter_side));
1195 }
1196 };
1197 let length = |value: &mut Option<Animated<LayoutLength>>| {
1198 if let Some(value) = value {
1199 *value = value.map(|length| length.without_px(shorter_side));
1200 }
1201 };
1202 let insets = |value: &mut Option<Animated<Insets>>| {
1203 if let Some(value) = value {
1204 *value = value.map(|insets| insets.without_px(shorter_side));
1205 }
1206 };
1207 let tracks = |value: &mut Option<TrackList>| {
1208 if let Some(tracks) = value {
1209 *tracks = tracks.without_px(shorter_side);
1210 }
1211 };
1212 tracks(&mut self.grid_template_columns);
1213 tracks(&mut self.grid_template_rows);
1214 tracks(&mut self.grid_auto_columns);
1215 tracks(&mut self.grid_auto_rows);
1216 for value in [
1217 &mut self.gap,
1218 &mut self.row_gap,
1219 &mut self.column_gap,
1220 &mut self.border_radius,
1221 &mut self.font_size,
1222 ] {
1223 size(value);
1224 }
1225 insets(&mut self.padding);
1226 insets(&mut self.margin);
1227 if let Some(border) = &mut self.border {
1228 border.width = border.width.map(|width| width.without_px(shorter_side));
1229 }
1230 for value in [
1231 &mut self.width,
1232 &mut self.height,
1233 &mut self.min_width,
1234 &mut self.min_height,
1235 &mut self.max_width,
1236 &mut self.max_height,
1237 &mut self.flex_basis,
1238 ] {
1239 length(value);
1240 }
1241 if let Some(inset) = &mut self.inset {
1242 for side in [
1243 &mut inset.left,
1244 &mut inset.top,
1245 &mut inset.right,
1246 &mut inset.bottom,
1247 ] {
1248 length(side);
1249 }
1250 }
1251 if let Some(translate) = &mut self.translate {
1252 *translate = translate.map(|offset| offset.without_px(shorter_side));
1253 }
1254 for child in &mut self.children {
1255 child.convert_px(shorter_side);
1256 }
1257 }
1258}
1259
1260const fn keyed<T>(animation: &Animated<T>) -> bool {
1261 matches!(animation, Animated::Keyframes { .. })
1262}
1263
1264const fn keyed_option<T>(animation: Option<&Animated<T>>) -> bool {
1265 matches!(animation, Some(Animated::Keyframes { .. }))
1266}