use crate::checks::{
Checked, ValueIssue, ValueProblem, animated, any, non_negative, scalar, unit, validate_paint,
vec2,
};
use crate::layout_values::{
GridEdge, GridPlacement, Insets, LayoutLength, LayoutOffset, LayoutSize, RepeatCount,
TrackComponent, TrackList,
};
use crate::{
Animated, Interpolation, LINE_HEIGHT_RANGE, MAX_FONT_FAMILY_BYTES, MAX_TEXT_CONTENT_BYTES,
MAX_TEXT_OUTLINE_WIDTH, Paint, Rgba, ShapeIssue, ShapeItem, TextAlign, TextOutline, Vec2,
};
use serde::{Deserialize, Serialize};
use std::fmt;
pub const MAX_LAYOUT_NODES: usize = 256;
pub const MAX_LAYOUT_DEPTH: usize = 16;
pub const MAX_LAYOUT_TEXTS: usize = 64;
pub const MAX_LAYOUT_ISOLATED: usize = 16;
pub const MAX_LAYOUT_FONT_SIZE: f32 = 1.0;
pub const MAX_GRID_TRACKS: usize = 64;
const MAX_GRID_LINE: i16 = 65;
pub const LAYOUT_TEXT_DEFAULTS: LayoutTextStyle = LayoutTextStyle {
font_family: None,
font_size: 0.05,
color: Rgba::new(1.0, 1.0, 1.0, 1.0),
line_height: 1.2,
text_align: TextAlign::Start,
outline: None,
};
#[derive(Clone, Debug, PartialEq)]
pub struct LayoutTextStyle {
pub font_family: Option<String>,
pub font_size: f32,
pub color: Rgba,
pub line_height: f32,
pub text_align: TextAlign,
pub outline: Option<TextOutline>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct LayoutNode {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub children: Vec<LayoutNode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<Animated<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shape: Option<LayoutShape>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display: Option<Display>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flex_direction: Option<FlexDirection>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flex_wrap: Option<FlexWrap>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub grid_template_columns: Option<TrackList>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub grid_template_rows: Option<TrackList>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub grid_auto_columns: Option<TrackList>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub grid_auto_rows: Option<TrackList>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub grid_auto_flow: Option<GridAutoFlow>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gap: Option<Animated<LayoutSize>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub row_gap: Option<Animated<LayoutSize>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub column_gap: Option<Animated<LayoutSize>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub justify_content: Option<JustifyContent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub align_items: Option<AlignItems>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub align_content: Option<AlignContent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub justify_items: Option<AlignItems>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub padding: Option<Animated<Insets>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub background: Option<Animated<Paint>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub border: Option<LayoutBorder>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub border_radius: Option<Animated<LayoutSize>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub overflow: Option<Overflow>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub width: Option<Animated<LayoutLength>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub height: Option<Animated<LayoutLength>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_width: Option<Animated<LayoutLength>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_height: Option<Animated<LayoutLength>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_width: Option<Animated<LayoutLength>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_height: Option<Animated<LayoutLength>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub aspect_ratio: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub margin: Option<Animated<Insets>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flex_grow: Option<Animated<f32>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flex_shrink: Option<Animated<f32>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flex_basis: Option<Animated<LayoutLength>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub align_self: Option<AlignItems>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub justify_self: Option<AlignItems>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub grid_column: Option<GridPlacement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub grid_row: Option<GridPlacement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub position: Option<Position>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inset: Option<LayoutInset>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub font_family: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub font_size: Option<Animated<LayoutSize>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<Rgba>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub line_height: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text_align: Option<TextAlign>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub outline: Option<TextOutline>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub opacity: Option<Animated<f32>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub translate: Option<Animated<LayoutOffset>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scale: Option<Animated<Vec2>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rotation_degrees: Option<Animated<f32>>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct LayoutShape {
pub items: Vec<ShapeItem>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct LayoutBorder {
pub width: Animated<LayoutSize>,
pub paint: Animated<Paint>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct LayoutInset {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub left: Option<Animated<LayoutLength>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub top: Option<Animated<LayoutLength>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub right: Option<Animated<LayoutLength>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bottom: Option<Animated<LayoutLength>>,
}
impl LayoutInset {
fn sides(&self) -> [(&'static str, Option<&Animated<LayoutLength>>); 4] {
[
("left", self.left.as_ref()),
("top", self.top.as_ref()),
("right", self.right.as_ref()),
("bottom", self.bottom.as_ref()),
]
}
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum Display {
#[default]
Flex,
Grid,
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum FlexDirection {
#[default]
Row,
Column,
RowReverse,
ColumnReverse,
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum FlexWrap {
#[default]
#[serde(rename = "nowrap")]
NoWrap,
#[serde(rename = "wrap")]
Wrap,
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum GridAutoFlow {
#[default]
Row,
Column,
RowDense,
ColumnDense,
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum JustifyContent {
#[default]
Start,
Center,
End,
SpaceBetween,
SpaceAround,
SpaceEvenly,
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum AlignContent {
Start,
Center,
End,
SpaceBetween,
SpaceAround,
SpaceEvenly,
#[default]
Stretch,
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum AlignItems {
Start,
Center,
End,
#[default]
Stretch,
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum Position {
#[default]
Relative,
Absolute,
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum Overflow {
#[default]
Visible,
Hidden,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum LayoutProblem {
TooManyNodes,
TooDeep,
TooManyTexts,
TooManyIsolated,
MixedContent,
DisplayOnLeaf,
ContainerFieldOnLeaf,
RootItemField,
GridFieldOnFlex(&'static str),
FlexFieldOnGrid(&'static str),
FlexItemFieldInGrid(&'static str),
GridItemFieldInFlex(&'static str),
TooManyTracks,
RepeatCount,
RepeatInAutoTracks,
AutoRepeatNeedsFixedTracks,
GridLineOutOfRange,
InvalidKeyframes,
OutOfRange,
LengthKeyframesDiffer,
PaintKeyframesDiffer,
TextKeyframesInterpolate,
}
impl fmt::Display for LayoutProblem {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::GridFieldOnFlex(field) => {
return write!(
formatter,
"sets {field}, a grid container field, without display grid"
);
}
Self::FlexFieldOnGrid(field) => {
return write!(
formatter,
"sets {field}, a flex container field, on a grid container"
);
}
Self::FlexItemFieldInGrid(field) => {
return write!(
formatter,
"sets {field}, a flex item field, in a grid container"
);
}
Self::GridItemFieldInFlex(field) => {
return write!(
formatter,
"sets {field}, a grid item field, in a flex container"
);
}
_ => {}
}
formatter.write_str(match self {
Self::TooManyNodes => "has more nodes than a layout may hold",
Self::TooDeep => "nests nodes too deeply",
Self::TooManyTexts => "has more text nodes than a layout may hold",
Self::TooManyIsolated => {
"has more nodes with opacity, transforms, or hidden overflow than a layout may hold"
}
Self::MixedContent => "has more than one of children, text, and shape",
Self::DisplayOnLeaf => "sets display on a node with text or shape content",
Self::ContainerFieldOnLeaf => {
"sets a container field on a node with text or shape content"
}
Self::RootItemField => "sets a size, margin, position, or item field on the root",
Self::TooManyTracks => "has more than 64 tracks",
Self::RepeatCount => "repeats tracks a number of times outside 1 to 64",
Self::RepeatInAutoTracks => "uses repeat() in implicit track sizes",
Self::AutoRepeatNeedsFixedTracks => {
"repeats auto-fill or auto-fit beside another, or with a track without a fixed size"
}
Self::GridLineOutOfRange => {
"names a grid line outside -65 to 65 or line 0, or a span outside 1 to 64"
}
Self::InvalidKeyframes => "has invalid keyframes",
Self::OutOfRange => "has a value out of range",
Self::LengthKeyframesDiffer => "has size keyframes that differ in kind",
Self::PaintKeyframesDiffer => "has paint keyframes that differ in kind or stops",
Self::TextKeyframesInterpolate => "has text keyframes that do not hold",
Self::GridFieldOnFlex(_)
| Self::FlexFieldOnGrid(_)
| Self::FlexItemFieldInGrid(_)
| Self::GridItemFieldInFlex(_) => unreachable!("written above"),
})
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LayoutIssue {
pub at: String,
pub problem: LayoutProblem,
}
impl LayoutIssue {
fn new(at: impl Into<String>, problem: LayoutProblem) -> Self {
Self {
at: at.into(),
problem,
}
}
}
impl From<ValueIssue> for LayoutIssue {
fn from(issue: ValueIssue) -> Self {
Self::new(
issue.at,
match issue.problem {
ValueProblem::InvalidKeyframes => LayoutProblem::InvalidKeyframes,
ValueProblem::OutOfRange => LayoutProblem::OutOfRange,
ValueProblem::PaintKeyframesDiffer => LayoutProblem::PaintKeyframesDiffer,
},
)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum GeneratorIssue {
Invalid,
Shape(ShapeIssue),
Layout(LayoutIssue),
}
impl From<LayoutIssue> for GeneratorIssue {
fn from(issue: LayoutIssue) -> Self {
Self::Layout(issue)
}
}
impl From<ValueIssue> for GeneratorIssue {
fn from(issue: ValueIssue) -> Self {
Self::Layout(issue.into())
}
}
#[derive(Default)]
struct Counts {
nodes: usize,
texts: usize,
isolated: usize,
shape_items: usize,
}
type LayoutChecked = Result<(), GeneratorIssue>;
#[derive(Copy, Clone)]
struct Context {
shorter_side: Option<f32>,
parent: Option<Display>,
}
pub(crate) fn validate_layout(root: &LayoutNode, canvas: Option<(u32, u32)>) -> LayoutChecked {
let mut counts = Counts::default();
if root.has_item_field() {
return Err(LayoutIssue::new("root", LayoutProblem::RootItemField).into());
}
#[allow(clippy::cast_precision_loss)]
let shorter_side = canvas.map(|(width, height)| width.min(height) as f32);
let context = Context {
shorter_side,
parent: None,
};
validate_node(root, "root", 1, context, &mut counts)
}
fn validate_node(
node: &LayoutNode,
at: &str,
depth: usize,
context: Context,
counts: &mut Counts,
) -> LayoutChecked {
let issue = |problem| Err(LayoutIssue::new(at, problem).into());
counts.nodes += 1;
if counts.nodes > MAX_LAYOUT_NODES {
return issue(LayoutProblem::TooManyNodes);
}
if depth > MAX_LAYOUT_DEPTH {
return issue(LayoutProblem::TooDeep);
}
let contents = usize::from(!node.children.is_empty())
+ usize::from(node.text.is_some())
+ usize::from(node.shape.is_some());
if contents > 1 {
return issue(LayoutProblem::MixedContent);
}
let leaf = node.text.is_some() || node.shape.is_some();
if leaf && node.display.is_some() {
return issue(LayoutProblem::DisplayOnLeaf);
}
if leaf && node.has_container_field() {
return issue(LayoutProblem::ContainerFieldOnLeaf);
}
let display = node.display.unwrap_or_default();
let mismatch = match display {
Display::Flex => node
.grid_container_field()
.map(LayoutProblem::GridFieldOnFlex),
Display::Grid => node
.flex_container_field()
.map(LayoutProblem::FlexFieldOnGrid),
};
if let Some(problem) = mismatch {
return issue(problem);
}
let item_mismatch = match context.parent {
Some(Display::Flex) => node
.grid_item_field()
.map(LayoutProblem::GridItemFieldInFlex),
Some(Display::Grid) => node
.flex_item_field()
.map(LayoutProblem::FlexItemFieldInGrid),
None => None,
};
if let Some(problem) = item_mismatch {
return issue(problem);
}
if node.text.is_some() {
counts.texts += 1;
if counts.texts > MAX_LAYOUT_TEXTS {
return issue(LayoutProblem::TooManyTexts);
}
}
if node.is_isolated() {
counts.isolated += 1;
if counts.isolated > MAX_LAYOUT_ISOLATED {
return issue(LayoutProblem::TooManyIsolated);
}
}
validate_fields(node, at)?;
validate_grid(node, at)?;
validate_style(node, at, context.shorter_side)?;
if let Some(text) = &node.text {
validate_text(text, &format!("{at}.text"))?;
}
if let Some(shape) = &node.shape {
crate::shapes::validate_items(
&shape.items,
&format!("{at}.shape.items"),
1,
&mut counts.shape_items,
)
.map_err(GeneratorIssue::Shape)?;
}
let inner = Context {
shorter_side: context.shorter_side,
parent: Some(display),
};
for (index, child) in node.children.iter().enumerate() {
validate_node(
child,
&format!("{at}.children[{index}]"),
depth + 1,
inner,
counts,
)?;
}
Ok(())
}
fn optional<T, E>(value: Option<&T>, check: impl FnOnce(&T) -> Result<(), E>) -> LayoutChecked
where
GeneratorIssue: From<E>,
{
Ok(value.map_or(Ok(()), check)?)
}
fn validate_fields(node: &LayoutNode, at: &str) -> LayoutChecked {
let field = |name: &str| format!("{at}.{name}");
for (name, gap) in [
("gap", &node.gap),
("row_gap", &node.row_gap),
("column_gap", &node.column_gap),
] {
optional(gap.as_ref(), |gap| size(gap, &field(name), non_negative))?;
}
optional(node.padding.as_ref(), |padding| {
insets(padding, &field("padding"), non_negative)
})?;
optional(node.background.as_ref(), |paint| {
validate_paint(paint, &field("background"))
})?;
if let Some(border) = &node.border {
size(&border.width, &field("border.width"), non_negative)?;
validate_paint(&border.paint, &field("border.paint"))?;
}
optional(node.border_radius.as_ref(), |radius| {
size(radius, &field("border_radius"), non_negative)
})?;
for (name, size) in [
("width", &node.width),
("height", &node.height),
("min_width", &node.min_width),
("min_height", &node.min_height),
("max_width", &node.max_width),
("max_height", &node.max_height),
] {
optional(size.as_ref(), |size| {
length(size, &field(name), non_negative)
})?;
}
if node
.aspect_ratio
.is_some_and(|ratio| !ratio.is_finite() || ratio <= 0.0)
{
return Err(ValueIssue::new(field("aspect_ratio"), ValueProblem::OutOfRange).into());
}
optional(node.margin.as_ref(), |margin| {
insets(margin, &field("margin"), any)
})?;
optional(node.flex_grow.as_ref(), |grow| {
scalar(grow, &field("flex_grow"), non_negative)
})?;
optional(node.flex_shrink.as_ref(), |shrink| {
scalar(shrink, &field("flex_shrink"), non_negative)
})?;
optional(node.flex_basis.as_ref(), |basis| {
length(basis, &field("flex_basis"), non_negative)
})?;
if let Some(inset) = &node.inset {
for (name, side) in inset.sides() {
optional(side, |side| {
length(side, &field(&format!("inset.{name}")), any)
})?;
}
}
optional(node.opacity.as_ref(), |opacity| {
scalar(opacity, &field("opacity"), unit)
})?;
optional(node.translate.as_ref(), |translate| {
animated(translate, &field("translate"), |offset| {
offset.x.value().is_finite() && offset.y.value().is_finite()
})
})?;
optional(node.scale.as_ref(), |scale| {
vec2(scale, &field("scale"), |_| true)
})?;
optional(node.rotation_degrees.as_ref(), |rotation| {
scalar(rotation, &field("rotation_degrees"), any)
})
}
fn validate_grid(node: &LayoutNode, at: &str) -> LayoutChecked {
let issue = |name: &str, problem| Err(LayoutIssue::new(format!("{at}.{name}"), problem).into());
for (name, tracks, explicit) in [
("grid_template_columns", &node.grid_template_columns, true),
("grid_template_rows", &node.grid_template_rows, true),
("grid_auto_columns", &node.grid_auto_columns, false),
("grid_auto_rows", &node.grid_auto_rows, false),
] {
let Some(tracks) = tracks else { continue };
if let Some(problem) = track_problem(tracks, explicit) {
return issue(name, problem);
}
}
for (name, placement) in [
("grid_column", node.grid_column),
("grid_row", node.grid_row),
] {
let in_range = |edge: GridEdge| match edge {
GridEdge::Auto => true,
GridEdge::Line(line) => line != 0 && line.abs() <= MAX_GRID_LINE,
GridEdge::Span(count) => (1..=64).contains(&count),
};
if placement.is_some_and(|placement| !placement.edges().into_iter().all(in_range)) {
return issue(name, LayoutProblem::GridLineOutOfRange);
}
}
Ok(())
}
fn track_problem(tracks: &TrackList, explicit: bool) -> Option<LayoutProblem> {
let lengths_valid = tracks
.lengths()
.all(|length| length.value().is_finite() && length.value() >= 0.0);
let shares_valid = tracks
.shares()
.all(|share| share.is_finite() && share > 0.0);
if !(lengths_valid && shares_valid) {
return Some(LayoutProblem::OutOfRange);
}
let repeats = tracks.0.iter().filter_map(|component| match component {
TrackComponent::Repeat(count, _) => Some(*count),
TrackComponent::Single(_) => None,
});
if !explicit && repeats.clone().next().is_some() {
return Some(LayoutProblem::RepeatInAutoTracks);
}
if repeats
.clone()
.any(|count| matches!(count, RepeatCount::Count(count) if !(1..=64).contains(&count)))
{
return Some(LayoutProblem::RepeatCount);
}
if tracks.track_count() > MAX_GRID_TRACKS {
return Some(LayoutProblem::TooManyTracks);
}
let automatic = repeats
.filter(|count| !matches!(count, RepeatCount::Count(_)))
.count();
if automatic > 1
|| (automatic == 1
&& !tracks
.sizes()
.all(crate::layout_values::TrackSize::is_fixed))
{
return Some(LayoutProblem::AutoRepeatNeedsFixedTracks);
}
None
}
fn validate_style(node: &LayoutNode, at: &str, shorter_side: Option<f32>) -> LayoutChecked {
let out_of_range = |name: &str| {
Err(LayoutIssue::new(format!("{at}.{name}"), LayoutProblem::OutOfRange).into())
};
if node
.font_family
.as_ref()
.is_some_and(|family| family.is_empty() || family.len() > MAX_FONT_FAMILY_BYTES)
{
return out_of_range("font_family");
}
optional(node.font_size.as_ref(), |font_size| {
animated(font_size, &format!("{at}.font_size"), |value| {
let fraction = match (*value, shorter_side) {
(LayoutSize::Px(_), None) => 0.0,
(value, Some(side)) => value.fraction(side),
(value, None) => value.value(),
};
value.value().is_finite() && value.value() > 0.0 && fraction <= MAX_LAYOUT_FONT_SIZE
})
})?;
if node.color.is_some_and(|color| !color.is_valid()) {
return out_of_range("color");
}
if node
.line_height
.is_some_and(|height| !LINE_HEIGHT_RANGE.contains(&height))
{
return out_of_range("line_height");
}
let valid_outline = |outline: TextOutline| {
outline.color.is_valid() && outline.width > 0.0 && outline.width <= MAX_TEXT_OUTLINE_WIDTH
};
if node.outline.is_some_and(|outline| !valid_outline(outline)) {
return out_of_range("outline");
}
Ok(())
}
fn insets(animation: &Animated<Insets>, at: &str, check: fn(f32) -> bool) -> Checked {
animated(animation, at, |value| {
value
.sides()
.iter()
.all(|side| side.value().is_finite() && check(side.value()))
})
}
fn size(animation: &Animated<LayoutSize>, at: &str, check: fn(f32) -> bool) -> Checked {
animated(animation, at, |value| {
value.value().is_finite() && check(value.value())
})
}
fn length(
animation: &Animated<LayoutLength>,
at: &str,
check: fn(f32) -> bool,
) -> Result<(), GeneratorIssue> {
animated(animation, at, |value| {
value.value().is_finite() && check(value.value())
})?;
if let Animated::Keyframes { keyframes } = animation {
let kind = keyframes[0].value.is_percent();
if keyframes.iter().any(|key| key.value.is_percent() != kind) {
return Err(LayoutIssue::new(at, LayoutProblem::LengthKeyframesDiffer).into());
}
}
Ok(())
}
fn validate_text(content: &Animated<String>, at: &str) -> LayoutChecked {
animated(content, at, |_| true)?;
if let Animated::Keyframes { keyframes } = content
&& keyframes
.iter()
.any(|key| key.interpolation != Interpolation::Hold)
{
return Err(LayoutIssue::new(at, LayoutProblem::TextKeyframesInterpolate).into());
}
animated(content, at, |content| {
content.len() <= MAX_TEXT_CONTENT_BYTES
})?;
Ok(())
}
impl LayoutNode {
fn has_container_field(&self) -> bool {
self.flex_container_field().is_some()
|| self.grid_container_field().is_some()
|| self.gap.is_some()
|| self.row_gap.is_some()
|| self.column_gap.is_some()
|| self.justify_content.is_some()
|| self.align_items.is_some()
|| self.align_content.is_some()
}
fn flex_container_field(&self) -> Option<&'static str> {
[
("flex_direction", self.flex_direction.is_some()),
("flex_wrap", self.flex_wrap.is_some()),
]
.into_iter()
.find_map(|(name, present)| present.then_some(name))
}
fn grid_container_field(&self) -> Option<&'static str> {
[
(
"grid_template_columns",
self.grid_template_columns.is_some(),
),
("grid_template_rows", self.grid_template_rows.is_some()),
("grid_auto_columns", self.grid_auto_columns.is_some()),
("grid_auto_rows", self.grid_auto_rows.is_some()),
("grid_auto_flow", self.grid_auto_flow.is_some()),
("justify_items", self.justify_items.is_some()),
]
.into_iter()
.find_map(|(name, present)| present.then_some(name))
}
fn flex_item_field(&self) -> Option<&'static str> {
[
("flex_grow", self.flex_grow.is_some()),
("flex_shrink", self.flex_shrink.is_some()),
("flex_basis", self.flex_basis.is_some()),
]
.into_iter()
.find_map(|(name, present)| present.then_some(name))
}
fn grid_item_field(&self) -> Option<&'static str> {
[
("grid_column", self.grid_column.is_some()),
("grid_row", self.grid_row.is_some()),
("justify_self", self.justify_self.is_some()),
]
.into_iter()
.find_map(|(name, present)| present.then_some(name))
}
fn has_item_field(&self) -> bool {
self.width.is_some()
|| self.height.is_some()
|| self.min_width.is_some()
|| self.min_height.is_some()
|| self.max_width.is_some()
|| self.max_height.is_some()
|| self.aspect_ratio.is_some()
|| self.margin.is_some()
|| self.flex_item_field().is_some()
|| self.grid_item_field().is_some()
|| self.align_self.is_some()
|| self.position.is_some()
|| self.inset.is_some()
}
#[must_use]
pub const fn has_visual(&self) -> bool {
self.opacity.is_some()
|| self.translate.is_some()
|| self.scale.is_some()
|| self.rotation_degrees.is_some()
}
#[must_use]
pub const fn is_isolated(&self) -> bool {
self.has_visual() || matches!(self.overflow, Some(Overflow::Hidden))
}
#[must_use]
pub fn is_animated(&self) -> bool {
let inset = self.inset.as_ref().is_some_and(|inset| {
inset
.sides()
.into_iter()
.any(|(_, value)| keyed_option(value))
});
let shape = self
.shape
.as_ref()
.is_some_and(|shape| shape.items.iter().any(ShapeItem::is_animated));
let border = self
.border
.as_ref()
.is_some_and(|border| keyed(&border.width) || keyed(&border.paint));
[
&self.gap,
&self.row_gap,
&self.column_gap,
&self.border_radius,
&self.font_size,
]
.into_iter()
.any(|value| keyed_option(value.as_ref()))
|| keyed_option(self.padding.as_ref())
|| keyed_option(self.background.as_ref())
|| border
|| [
&self.width,
&self.height,
&self.min_width,
&self.min_height,
&self.max_width,
&self.max_height,
&self.flex_basis,
]
.into_iter()
.any(|value| keyed_option(value.as_ref()))
|| keyed_option(self.margin.as_ref())
|| keyed_option(self.flex_grow.as_ref())
|| keyed_option(self.flex_shrink.as_ref())
|| inset
|| keyed_option(self.opacity.as_ref())
|| keyed_option(self.translate.as_ref())
|| keyed_option(self.scale.as_ref())
|| keyed_option(self.rotation_degrees.as_ref())
|| keyed_option(self.text.as_ref())
|| shape
|| self.children.iter().any(Self::is_animated)
}
pub fn walk(&self, mut visit: impl FnMut(&str, &Self)) {
fn go(node: &LayoutNode, at: &str, visit: &mut impl FnMut(&str, &LayoutNode)) {
visit(at, node);
for (index, child) in node.children.iter().enumerate() {
go(child, &format!("{at}.children[{index}]"), visit);
}
}
go(self, "root", &mut visit);
}
#[must_use]
pub fn without_px(&self, shorter_side: f32) -> Self {
let mut node = self.clone();
node.convert_px(shorter_side);
node
}
fn convert_px(&mut self, shorter_side: f32) {
let size = |value: &mut Option<Animated<LayoutSize>>| {
if let Some(value) = value {
*value = value.map(|size| size.without_px(shorter_side));
}
};
let length = |value: &mut Option<Animated<LayoutLength>>| {
if let Some(value) = value {
*value = value.map(|length| length.without_px(shorter_side));
}
};
let insets = |value: &mut Option<Animated<Insets>>| {
if let Some(value) = value {
*value = value.map(|insets| insets.without_px(shorter_side));
}
};
let tracks = |value: &mut Option<TrackList>| {
if let Some(tracks) = value {
*tracks = tracks.without_px(shorter_side);
}
};
tracks(&mut self.grid_template_columns);
tracks(&mut self.grid_template_rows);
tracks(&mut self.grid_auto_columns);
tracks(&mut self.grid_auto_rows);
for value in [
&mut self.gap,
&mut self.row_gap,
&mut self.column_gap,
&mut self.border_radius,
&mut self.font_size,
] {
size(value);
}
insets(&mut self.padding);
insets(&mut self.margin);
if let Some(border) = &mut self.border {
border.width = border.width.map(|width| width.without_px(shorter_side));
}
for value in [
&mut self.width,
&mut self.height,
&mut self.min_width,
&mut self.min_height,
&mut self.max_width,
&mut self.max_height,
&mut self.flex_basis,
] {
length(value);
}
if let Some(inset) = &mut self.inset {
for side in [
&mut inset.left,
&mut inset.top,
&mut inset.right,
&mut inset.bottom,
] {
length(side);
}
}
if let Some(translate) = &mut self.translate {
*translate = translate.map(|offset| offset.without_px(shorter_side));
}
for child in &mut self.children {
child.convert_px(shorter_side);
}
}
}
const fn keyed<T>(animation: &Animated<T>) -> bool {
matches!(animation, Animated::Keyframes { .. })
}
const fn keyed_option<T>(animation: Option<&Animated<T>>) -> bool {
matches!(animation, Some(Animated::Keyframes { .. }))
}