1use crate::ShapesPlan;
11use crate::animation::{
12 sample_hold, sample_insets, sample_length, sample_offset, sample_paint, sample_scalar,
13 sample_size, sample_vec2,
14};
15use alpha2_geometry::StableHash;
16use alpha2_model::{
17 AlignContent, AlignItems, Animated, Display, FlexDirection, FlexWrap, GridAutoFlow,
18 GridPlacement, JustifyContent, LAYOUT_TEXT_DEFAULTS, LayoutLength, LayoutNode, LayoutTextStyle,
19 Overflow, Paint, Position, Rgba, TextAlign, TextOutline, TrackList, Vec2,
20};
21use alpha2_time::MediaTime;
22use std::sync::Arc;
23
24#[derive(Clone, Debug)]
26pub struct LayoutPlan {
27 root: Box<LayoutNode>,
29 shapes: Vec<(Arc<ShapesPlan>, u128)>,
31 shapes_animate: bool,
33 fixed: Option<Arc<EvaluatedLayout>>,
35}
36
37impl PartialEq for LayoutPlan {
38 fn eq(&self, other: &Self) -> bool {
39 self.root == other.root
40 }
41}
42
43impl LayoutPlan {
44 #[must_use]
47 pub fn new(root: &LayoutNode, canvas: (u32, u32)) -> Self {
48 #[allow(clippy::cast_precision_loss)]
49 let shorter_side = canvas.0.min(canvas.1).max(1) as f32;
50 let root = root.without_px(shorter_side);
51 let mut shapes = Vec::new();
52 root.walk(|_, node| {
53 if let Some(shape) = &node.shape {
54 let mut hash = StableHash::new();
55 hash.bytes(format!("{:?}", shape.items).as_bytes());
56 shapes.push((Arc::new(ShapesPlan::new(&shape.items)), hash.finish()));
57 }
58 });
59 let shapes_animate = shapes.iter().any(|(plan, _)| plan.is_animated());
60 let animated = root.is_animated();
61 let mut plan = Self {
62 root: Box::new(root),
63 shapes,
64 shapes_animate,
65 fixed: None,
66 };
67 if !animated {
68 plan.fixed = Some(Arc::new(plan.evaluate_now(MediaTime::ZERO)));
69 }
70 plan
71 }
72
73 #[must_use]
76 pub fn evaluate(&self, time: MediaTime) -> Arc<EvaluatedLayout> {
77 self.fixed
78 .clone()
79 .unwrap_or_else(|| Arc::new(self.evaluate_now(time)))
80 }
81
82 #[must_use]
84 pub fn is_animated(&self) -> bool {
85 self.fixed.is_none()
86 }
87
88 #[must_use]
91 pub const fn root(&self) -> &LayoutNode {
92 &self.root
93 }
94
95 fn evaluate_now(&self, time: MediaTime) -> EvaluatedLayout {
96 let mut shapes = self.shapes.iter();
97 let root = evaluate_node(&self.root, time, &mut shapes, &LAYOUT_TEXT_DEFAULTS);
98 let mut hash = StableHash::new();
99 hash_node(&mut hash, &root, &self.shapes);
100 if self.shapes_animate {
101 hash.u64(time.ticks().cast_unsigned());
102 }
103 EvaluatedLayout {
104 root,
105 time,
106 key: hash.finish(),
107 }
108 }
109}
110
111#[derive(Clone, Debug, PartialEq)]
113pub struct EvaluatedLayout {
114 pub root: EvaluatedNode,
116 pub time: MediaTime,
118 pub key: u128,
121}
122
123#[derive(Clone, Debug, PartialEq)]
126pub struct EvaluatedNode {
127 pub layout: NodeLayout,
129 pub paint: NodePaint,
131 pub visual: Option<NodeVisual>,
133 pub clip: bool,
135 pub content: NodeContent,
137}
138
139impl EvaluatedNode {
140 #[must_use]
142 pub const fn is_isolated(&self) -> bool {
143 self.visual.is_some() || self.clip
144 }
145}
146
147#[derive(Copy, Clone, Debug, Default, PartialEq)]
150#[allow(missing_docs)]
151pub struct Sides {
152 pub top: f32,
153 pub right: f32,
154 pub bottom: f32,
155 pub left: f32,
156}
157
158impl Sides {
159 const fn from_array([top, right, bottom, left]: [f32; 4]) -> Self {
160 Self {
161 top,
162 right,
163 bottom,
164 left,
165 }
166 }
167
168 const fn to_array(self) -> [f32; 4] {
169 [self.top, self.right, self.bottom, self.left]
170 }
171}
172
173#[derive(Clone, Debug, PartialEq)]
176#[allow(missing_docs)]
177pub struct NodeLayout {
178 pub display: Display,
179 pub flex_direction: FlexDirection,
180 pub flex_wrap: FlexWrap,
181 pub grid_template_columns: Option<TrackList>,
182 pub grid_template_rows: Option<TrackList>,
183 pub grid_auto_columns: Option<TrackList>,
184 pub grid_auto_rows: Option<TrackList>,
185 pub grid_auto_flow: GridAutoFlow,
186 pub row_gap: f32,
188 pub column_gap: f32,
190 pub justify_content: JustifyContent,
191 pub align_items: AlignItems,
192 pub align_content: AlignContent,
193 pub justify_items: AlignItems,
194 pub padding: Sides,
195 pub border_width: f32,
197 pub overflow: Overflow,
198 pub width: Option<LayoutLength>,
199 pub height: Option<LayoutLength>,
200 pub min_width: Option<LayoutLength>,
201 pub min_height: Option<LayoutLength>,
202 pub max_width: Option<LayoutLength>,
203 pub max_height: Option<LayoutLength>,
204 pub aspect_ratio: Option<f32>,
205 pub margin: Sides,
206 pub flex_grow: f32,
207 pub flex_shrink: f32,
208 pub flex_basis: Option<LayoutLength>,
209 pub align_self: Option<AlignItems>,
210 pub justify_self: Option<AlignItems>,
211 pub grid_column: GridPlacement,
212 pub grid_row: GridPlacement,
213 pub position: Position,
214 pub inset: [Option<LayoutLength>; 4],
216}
217
218#[derive(Clone, Debug, PartialEq)]
220pub struct NodePaint {
221 pub background: Option<Paint>,
223 pub border: Option<(f32, Paint)>,
225 pub border_radius: f32,
227}
228
229#[derive(Copy, Clone, Debug, PartialEq)]
231pub struct NodeVisual {
232 pub opacity: f32,
234 pub translate: Vec2,
236 pub scale: Vec2,
238 pub rotation_degrees: f32,
240}
241
242#[derive(Clone, Debug, PartialEq)]
244pub enum NodeContent {
245 Empty,
247 Children(Vec<EvaluatedNode>),
249 Text(EvaluatedText),
251 Shape(Arc<ShapesPlan>),
254}
255
256#[derive(Clone, Debug, PartialEq)]
258pub struct EvaluatedText {
259 pub content: String,
261 pub font_family: Option<String>,
263 pub font_size: f32,
265 pub color: Rgba,
267 pub line_height: f32,
269 pub letter_spacing: f32,
271 pub font_weight: u16,
273 pub text_align: TextAlign,
275 pub outline: Option<TextOutline>,
277}
278
279fn non_negative(animation: Option<&Animated<f32>>, time: MediaTime, default: f32) -> f32 {
280 animation.map_or(default, |value| sample_scalar(value, time).max(0.0))
281}
282
283fn size_or(
284 animation: Option<&Animated<alpha2_model::LayoutSize>>,
285 time: MediaTime,
286 default: f32,
287) -> f32 {
288 animation.map_or(default, |value| sample_size(value, time).max(0.0))
289}
290
291fn length(animation: Option<&Animated<LayoutLength>>, time: MediaTime) -> Option<LayoutLength> {
292 animation.map(|value| sample_length(value, time))
293}
294
295fn size(animation: Option<&Animated<LayoutLength>>, time: MediaTime) -> Option<LayoutLength> {
296 length(animation, time).map(|length| length.with_value(length.value().max(0.0)))
297}
298
299fn node_layout(node: &LayoutNode, time: MediaTime, border_width: f32) -> NodeLayout {
302 let insets = |value: Option<&Animated<alpha2_model::Insets>>| {
303 value.map_or_else(Sides::default, |value| {
304 Sides::from_array(sample_insets(value, time))
305 })
306 };
307 let padding = Sides::from_array(
308 insets(node.padding.as_ref())
309 .to_array()
310 .map(|side| side.max(0.0)),
311 );
312 let inset = node.inset.as_ref().map_or([None; 4], |inset| {
313 [
314 length(inset.left.as_ref(), time),
315 length(inset.top.as_ref(), time),
316 length(inset.right.as_ref(), time),
317 length(inset.bottom.as_ref(), time),
318 ]
319 });
320 let gap = size_or(node.gap.as_ref(), time, 0.0);
321 NodeLayout {
322 display: node.display.unwrap_or_default(),
323 flex_direction: node.flex_direction.unwrap_or_default(),
324 flex_wrap: node.flex_wrap.unwrap_or_default(),
325 grid_template_columns: node.grid_template_columns.clone(),
326 grid_template_rows: node.grid_template_rows.clone(),
327 grid_auto_columns: node.grid_auto_columns.clone(),
328 grid_auto_rows: node.grid_auto_rows.clone(),
329 grid_auto_flow: node.grid_auto_flow.unwrap_or_default(),
330 row_gap: size_or(node.row_gap.as_ref(), time, gap),
331 column_gap: size_or(node.column_gap.as_ref(), time, gap),
332 justify_content: node.justify_content.unwrap_or_default(),
333 align_items: node.align_items.unwrap_or_default(),
334 align_content: node.align_content.unwrap_or_default(),
335 justify_items: node.justify_items.unwrap_or_default(),
336 padding,
337 border_width,
338 overflow: node.overflow.unwrap_or_default(),
339 width: size(node.width.as_ref(), time),
340 height: size(node.height.as_ref(), time),
341 min_width: size(node.min_width.as_ref(), time),
342 min_height: size(node.min_height.as_ref(), time),
343 max_width: size(node.max_width.as_ref(), time),
344 max_height: size(node.max_height.as_ref(), time),
345 aspect_ratio: node.aspect_ratio,
346 margin: insets(node.margin.as_ref()),
347 flex_grow: non_negative(node.flex_grow.as_ref(), time, 0.0),
348 flex_shrink: non_negative(node.flex_shrink.as_ref(), time, 1.0),
349 flex_basis: size(node.flex_basis.as_ref(), time),
350 align_self: node.align_self,
351 justify_self: node.justify_self,
352 grid_column: node.grid_column.unwrap_or_default(),
353 grid_row: node.grid_row.unwrap_or_default(),
354 position: node.position.unwrap_or_default(),
355 inset,
356 }
357}
358
359fn text_style(node: &LayoutNode, time: MediaTime, inherited: &LayoutTextStyle) -> LayoutTextStyle {
362 LayoutTextStyle {
363 font_family: node
364 .font_family
365 .clone()
366 .or_else(|| inherited.font_family.clone()),
367 font_size: node.font_size.as_ref().map_or(inherited.font_size, |size| {
368 sample_size(size, time).max(f32::MIN_POSITIVE)
369 }),
370 color: node.color.unwrap_or(inherited.color),
371 line_height: node.line_height.unwrap_or(inherited.line_height),
372 letter_spacing: node.letter_spacing.unwrap_or(inherited.letter_spacing),
373 font_weight: node.font_weight.unwrap_or(inherited.font_weight),
374 text_align: node.text_align.unwrap_or(inherited.text_align),
375 outline: node.outline.or(inherited.outline),
376 }
377}
378
379fn evaluate_node<'a>(
380 node: &LayoutNode,
381 time: MediaTime,
382 shapes: &mut impl Iterator<Item = &'a (Arc<ShapesPlan>, u128)>,
383 inherited: &LayoutTextStyle,
384) -> EvaluatedNode {
385 let border = node.border.as_ref().map(|border| {
386 (
387 sample_size(&border.width, time).max(0.0),
388 sample_paint(&border.paint, time),
389 )
390 });
391 let layout = node_layout(node, time, border.as_ref().map_or(0.0, |(width, _)| *width));
392 let paint = NodePaint {
393 background: node
394 .background
395 .as_ref()
396 .map(|paint| sample_paint(paint, time)),
397 border,
398 border_radius: size_or(node.border_radius.as_ref(), time, 0.0),
399 };
400 let visual = node.has_visual().then(|| NodeVisual {
401 opacity: node
402 .opacity
403 .as_ref()
404 .map_or(1.0, |value| sample_scalar(value, time).clamp(0.0, 1.0)),
405 translate: node
406 .translate
407 .as_ref()
408 .map_or_else(Vec2::default, |value| sample_offset(value, time)),
409 scale: node
410 .scale
411 .as_ref()
412 .map_or(Vec2::new(1.0, 1.0), |value| sample_vec2(value, time)),
413 rotation_degrees: node
414 .rotation_degrees
415 .as_ref()
416 .map_or(0.0, |value| sample_scalar(value, time)),
417 });
418 let style = text_style(node, time, inherited);
419 let content = if let Some(text) = &node.text {
420 NodeContent::Text(EvaluatedText {
421 content: sample_hold(text, time),
422 font_family: style.font_family,
423 font_size: style.font_size,
424 color: style.color,
425 line_height: style.line_height,
426 letter_spacing: style.letter_spacing,
427 font_weight: style.font_weight,
428 text_align: style.text_align,
429 outline: style.outline,
430 })
431 } else if node.shape.is_some() {
432 let (plan, _) = shapes.next().expect("one plan per shape node");
433 NodeContent::Shape(Arc::clone(plan))
434 } else if node.children.is_empty() {
435 NodeContent::Empty
436 } else {
437 NodeContent::Children(
438 node.children
439 .iter()
440 .map(|child| evaluate_node(child, time, shapes, &style))
441 .collect(),
442 )
443 };
444 EvaluatedNode {
445 layout,
446 paint,
447 visual,
448 clip: matches!(node.overflow, Some(Overflow::Hidden)),
449 content,
450 }
451}
452
453fn hash_length(hash: &mut StableHash, length: Option<LayoutLength>) {
454 match length {
455 None => hash.u32(0),
456 Some(LayoutLength::Fraction(value)) => {
457 hash.u32(1);
458 hash.f32(value);
459 }
460 Some(LayoutLength::Percent(value)) => {
461 hash.u32(2);
462 hash.f32(value);
463 }
464 Some(LayoutLength::Px(value)) => {
465 hash.u32(3);
466 hash.f32(value);
467 }
468 }
469}
470
471fn hash_sides(hash: &mut StableHash, sides: Sides) {
472 for side in sides.to_array() {
473 hash.f32(side);
474 }
475}
476
477fn hash_color(hash: &mut StableHash, color: Rgba) {
478 for component in [color.red, color.green, color.blue, color.alpha] {
479 hash.f32(component);
480 }
481}
482
483fn hash_paint(hash: &mut StableHash, paint: Option<&Paint>) {
484 let stops = |hash: &mut StableHash, stops: &[alpha2_model::GradientStop]| {
485 hash.u64(stops.len() as u64);
486 for stop in stops {
487 hash.f32(stop.offset);
488 hash_color(hash, stop.color);
489 }
490 };
491 match paint {
492 None => hash.u32(0),
493 Some(Paint::Solid { color }) => {
494 hash.u32(1);
495 hash_color(hash, *color);
496 }
497 Some(Paint::LinearGradient {
498 start,
499 end,
500 stops: list,
501 spread,
502 }) => {
503 hash.u32(2);
504 for value in [start.x, start.y, end.x, end.y] {
505 hash.f32(value);
506 }
507 stops(hash, list);
508 hash.u32(*spread as u32);
509 }
510 Some(Paint::RadialGradient {
511 center,
512 radius,
513 stops: list,
514 spread,
515 }) => {
516 hash.u32(3);
517 for value in [center.x, center.y, *radius] {
518 hash.f32(value);
519 }
520 stops(hash, list);
521 hash.u32(*spread as u32);
522 }
523 Some(Paint::ConicGradient {
524 center,
525 angle_degrees,
526 stops: list,
527 }) => {
528 hash.u32(4);
529 for value in [center.x, center.y, *angle_degrees] {
530 hash.f32(value);
531 }
532 stops(hash, list);
533 }
534 }
535}
536
537fn hash_node(hash: &mut StableHash, node: &EvaluatedNode, shapes: &[(Arc<ShapesPlan>, u128)]) {
538 let mut shape_index = 0;
539 hash_node_in(hash, node, shapes, &mut shape_index);
540}
541
542fn hash_layout(hash: &mut StableHash, layout: &NodeLayout) {
543 for value in [
544 layout.display as u32,
545 layout.flex_direction as u32,
546 layout.flex_wrap as u32,
547 layout.grid_auto_flow as u32,
548 layout.justify_content as u32,
549 layout.align_items as u32,
550 layout.align_content as u32,
551 layout.justify_items as u32,
552 layout.overflow as u32,
553 layout.position as u32,
554 layout.align_self.map_or(u32::MAX, |align| align as u32),
555 layout.justify_self.map_or(u32::MAX, |align| align as u32),
556 ] {
557 hash.u32(value);
558 }
559 for tracks in [
561 &layout.grid_template_columns,
562 &layout.grid_template_rows,
563 &layout.grid_auto_columns,
564 &layout.grid_auto_rows,
565 ] {
566 match tracks {
567 None => hash.u32(0),
568 Some(tracks) => {
569 hash.u32(1);
570 hash.bytes(tracks.to_string().as_bytes());
571 }
572 }
573 }
574 for placement in [layout.grid_column, layout.grid_row] {
575 hash.bytes(placement.to_string().as_bytes());
576 }
577 for value in [
578 layout.row_gap,
579 layout.column_gap,
580 layout.border_width,
581 layout.flex_grow,
582 layout.flex_shrink,
583 layout.aspect_ratio.unwrap_or(-1.0),
584 ] {
585 hash.f32(value);
586 }
587 hash_sides(hash, layout.padding);
588 hash_sides(hash, layout.margin);
589 for length in [
590 layout.width,
591 layout.height,
592 layout.min_width,
593 layout.min_height,
594 layout.max_width,
595 layout.max_height,
596 layout.flex_basis,
597 ]
598 .into_iter()
599 .chain(layout.inset)
600 {
601 hash_length(hash, length);
602 }
603}
604
605fn hash_node_in(
606 hash: &mut StableHash,
607 node: &EvaluatedNode,
608 shapes: &[(Arc<ShapesPlan>, u128)],
609 shape_index: &mut usize,
610) {
611 hash_layout(hash, &node.layout);
612 hash_paint(hash, node.paint.background.as_ref());
613 match &node.paint.border {
614 None => hash.u32(0),
615 Some((width, paint)) => {
616 hash.u32(1);
617 hash.f32(*width);
618 hash_paint(hash, Some(paint));
619 }
620 }
621 hash.f32(node.paint.border_radius);
622 match node.visual {
623 None => hash.u32(0),
624 Some(visual) => {
625 hash.u32(1);
626 for value in [
627 visual.opacity,
628 visual.translate.x,
629 visual.translate.y,
630 visual.scale.x,
631 visual.scale.y,
632 visual.rotation_degrees,
633 ] {
634 hash.f32(value);
635 }
636 }
637 }
638 hash.u32(u32::from(node.clip));
639 match &node.content {
640 NodeContent::Empty => hash.u32(0),
641 NodeContent::Children(children) => {
642 hash.u32(1);
643 hash.u64(children.len() as u64);
644 for child in children {
645 hash_node_in(hash, child, shapes, shape_index);
646 }
647 }
648 NodeContent::Text(text) => {
649 hash.u32(2);
650 hash.bytes(text.content.as_bytes());
651 match &text.font_family {
652 None => hash.u32(0),
653 Some(family) => {
654 hash.u32(1);
655 hash.bytes(family.as_bytes());
656 }
657 }
658 hash.f32(text.font_size);
659 hash_color(hash, text.color);
660 hash.f32(text.line_height);
661 hash.f32(text.letter_spacing);
662 hash.u32(u32::from(text.font_weight));
663 hash.u32(text.text_align as u32);
664 match text.outline {
665 None => hash.u32(0),
666 Some(outline) => {
667 hash.u32(1);
668 hash_color(hash, outline.color);
669 hash.f32(outline.width);
670 }
671 }
672 }
673 NodeContent::Shape(_) => {
674 hash.u32(3);
675 let digest = shapes.get(*shape_index).map_or(0, |(_, digest)| *digest);
676 *shape_index += 1;
677 hash.bytes(&digest.to_le_bytes());
678 }
679 }
680}
681
682#[must_use]
686pub(crate) fn layout_bytes(
687 node: &LayoutNode,
688 shape_items_bytes: &impl Fn(&[alpha2_model::ShapeItem]) -> usize,
689) -> usize {
690 fn keys<T>(animation: Option<&Animated<T>>) -> usize {
691 match animation {
692 Some(Animated::Keyframes { keyframes }) => {
693 keyframes.len() * size_of::<alpha2_model::Keyframe<T>>()
694 }
695 _ => 0,
696 }
697 }
698 let text = node.text.as_ref().map_or(0, |text| match text {
699 Animated::Constant { value } => value.len(),
700 Animated::Keyframes { keyframes } => keyframes
701 .iter()
702 .map(|keyframe| size_of::<alpha2_model::Keyframe<String>>() + keyframe.value.len())
703 .sum(),
704 });
705 let style = node.font_family.as_ref().map_or(0, String::len) + keys(node.font_size.as_ref());
706 let tracks = [
707 &node.grid_template_columns,
708 &node.grid_template_rows,
709 &node.grid_auto_columns,
710 &node.grid_auto_rows,
711 ]
712 .into_iter()
713 .flatten()
714 .map(|tracks| tracks.track_count() * size_of::<alpha2_model::TrackSize>())
715 .sum::<usize>();
716 let paints = |paint: Option<&Animated<Paint>>| match paint {
717 Some(Animated::Keyframes { keyframes }) => {
718 keyframes.len() * size_of::<alpha2_model::Keyframe<Paint>>()
719 }
720 Some(Animated::Constant { .. }) | None => 0,
721 };
722 let inset = node.inset.as_ref().map_or(0, |inset| {
723 [&inset.left, &inset.top, &inset.right, &inset.bottom]
724 .into_iter()
725 .map(|side| keys(side.as_ref()))
726 .sum()
727 });
728 size_of::<LayoutNode>()
729 + text
730 + style
731 + tracks
732 + node
733 .shape
734 .as_ref()
735 .map_or(0, |shape| shape_items_bytes(&shape.items))
736 + [
737 &node.gap,
738 &node.row_gap,
739 &node.column_gap,
740 &node.border_radius,
741 ]
742 .into_iter()
743 .map(|value| keys(value.as_ref()))
744 .sum::<usize>()
745 + keys(node.padding.as_ref())
746 + paints(node.background.as_ref())
747 + node.border.as_ref().map_or(0, |border| {
748 keys(Some(&border.width)) + paints(Some(&border.paint))
749 })
750 + [
751 &node.width,
752 &node.height,
753 &node.min_width,
754 &node.min_height,
755 &node.max_width,
756 &node.max_height,
757 &node.flex_basis,
758 ]
759 .into_iter()
760 .map(|length| keys(length.as_ref()))
761 .sum::<usize>()
762 + keys(node.margin.as_ref())
763 + keys(node.flex_grow.as_ref())
764 + keys(node.flex_shrink.as_ref())
765 + inset
766 + keys(node.opacity.as_ref())
767 + keys(node.translate.as_ref())
768 + keys(node.scale.as_ref())
769 + keys(node.rotation_degrees.as_ref())
770 + node
771 .children
772 .iter()
773 .map(|child| layout_bytes(child, shape_items_bytes))
774 .sum::<usize>()
775}
776
777#[derive(Clone, Debug, PartialEq)]
780pub enum GeneratorPlan {
781 Shapes(ShapesPlan),
783 Layout(LayoutPlan),
785}
786
787impl GeneratorPlan {
788 #[must_use]
791 pub fn new(generator: &alpha2_model::Generator, canvas: (u32, u32)) -> Option<Self> {
792 match generator {
793 alpha2_model::Generator::Shapes { items } => Some(Self::Shapes(ShapesPlan::new(items))),
794 alpha2_model::Generator::Layout { root } => {
795 Some(Self::Layout(LayoutPlan::new(root, canvas)))
796 }
797 alpha2_model::Generator::Fill { .. } | alpha2_model::Generator::Text { .. } => None,
798 }
799 }
800
801 #[must_use]
803 pub const fn shapes(&self) -> Option<&ShapesPlan> {
804 match self {
805 Self::Shapes(plan) => Some(plan),
806 Self::Layout(_) => None,
807 }
808 }
809
810 #[must_use]
812 pub const fn layout(&self) -> Option<&LayoutPlan> {
813 match self {
814 Self::Layout(plan) => Some(plan),
815 Self::Shapes(_) => None,
816 }
817 }
818}
819
820#[cfg(test)]
821#[path = "layout_tests.rs"]
822mod tests;