gioser

tawasuyu/taffy

A high performance rust-powered UI layout library

HTML 5364Rust 86Markdown 12TOML 91841 Definiciones

Unit test `compute_constants()` (#184)

* Begin testing algo-constants

* Separating out resolve into own trait + module

* Fix tests

* Add convenience methods to Size

* Move tests to resolve

* Add tests for maybe_resolve Size<Dimension>

* Move resolve tests to resolve.rs

* Rewording parent -> context

* Add last test

* Add tests for resolve_or_default for Dimension

* Adding rect constructors

* Using consts instead of constructors

* from_some() -> new()

* Add note to releases

* Turning constructors to consts

* Rect convenience constructors

* Add test-suite for rect resolve_or_default

* Apply suggestions from code review

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>

* Fix clippy lints & add docs

* Add tests for Rect -> Option -> Rect

* Finish testing algo-consts

* Apply suggestions from alice

* Tweak RELEASES.md wording

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>

7762c2490a21a7f3f4e911efd097bdee7b0ef108 · por Andreas Weibye · (hace 4 años) · padre 4b89afd5e · ver el árbol en este commit

Delta semántico · 71

src/flexbox.rs

src/geometry.rs

src/layout.rs

  • cambió por dentro def new

src/lib.rs

src/node.rs

src/style.rs

8 archivos · +547 −158

diff --git a/RELEASES.md b/RELEASES.md
index 106462d35..4f9560ed8 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -9,6 +9,8 @@
 
 ### 0.2.0 Changed
 
+- `Size<f32>.zero()` is now `Size::<f32>::ZERO`
+- `Point<f32>.zero()` is now  `Point::<f32>::ZERO`
 - renamed `taffy::node::Taffy.new_leaf()` -> `taffy::node::Taffy.new_leaf_with_measure()`
 - removed the public `Number` type; a more idiomatic `Option<f32>` is used instead
   - the associated public `MinMax` and `OrElse` traits have also been removed; these should never have been public
diff --git a/src/flexbox.rs b/src/flexbox.rs
index 072eeb4c7..bfb0d9ff2 100644
--- a/src/flexbox.rs
+++ b/src/flexbox.rs
@@ -8,6 +8,7 @@ use crate::geometry::{Point, Rect, Size};
 use crate::layout::{Cache, Layout};
 use crate::math::MaybeMath;
 use crate::node::{MeasureFunc, NodeId};
+use crate::resolve::{MaybeResolve, ResolveOrDefault};
 use crate::style::{AlignContent, AlignSelf, Dimension, Display, FlexWrap, JustifyContent, PositionType};
 use crate::style::{FlexDirection, FlexboxLayout};
 use crate::sys::{abs, round, ChildrenVec, Vec};
@@ -112,20 +113,20 @@ impl Forest {
             || style.max_size.height.is_defined();
 
         let preliminary_size = if has_root_min_max {
-            let first_pass = self.compute_preliminary(root, style.size.resolve(size), size, false, true);
+            let first_pass = self.compute_preliminary(root, style.size.maybe_resolve(size), size, false, true);
 
             self.compute_preliminary(
                 root,
                 Size {
                     width: first_pass
                         .width
-                        .maybe_max(style.min_size.width.resolve(size.width))
-                        .maybe_min(style.max_size.width.resolve(size.width))
+                        .maybe_max(style.min_size.width.maybe_resolve(size.width))
+                        .maybe_min(style.max_size.width.maybe_resolve(size.width))
                         .into(),
                     height: first_pass
                         .height
-                        .maybe_max(style.min_size.height.resolve(size.height))
-                        .maybe_min(style.max_size.height.resolve(size.height))
+                        .maybe_max(style.min_size.height.maybe_resolve(size.height))
+                        .maybe_min(style.max_size.height.maybe_resolve(size.height))
                         .into(),
                 },
                 size,
@@ -133,10 +134,10 @@ impl Forest {
                 true,
             )
         } else {
-            self.compute_preliminary(root, style.size.resolve(size), size, true, true)
+            self.compute_preliminary(root, style.size.maybe_resolve(size), size, true, true)
         };
 
-        self.nodes[root].layout = Layout { order: 0, size: preliminary_size, location: Point::zero() };
+        self.nodes[root].layout = Layout { order: 0, size: preliminary_size, location: Point::ZERO };
 
         Self::round_layout(&mut self.nodes, &self.children, root, 0.0, 0.0);
     }
@@ -207,19 +208,18 @@ impl Forest {
     /// Compute constants that can be reused during the flexbox algorithm.
     #[inline]
     fn compute_constants(
-        &self,
-        node: NodeId,
+        node: &NodeData,
         node_size: Size<Option<f32>>,
         parent_size: Size<Option<f32>>,
     ) -> AlgoConstants {
-        let dir = self.nodes[node].style.flex_direction;
+        let dir = node.style.flex_direction;
         let is_row = dir.is_row();
         let is_column = dir.is_column();
-        let is_wrap_reverse = self.nodes[node].style.flex_wrap == FlexWrap::WrapReverse;
+        let is_wrap_reverse = node.style.flex_wrap == FlexWrap::WrapReverse;
 
-        let margin = self.nodes[node].style.margin.map(|n| n.resolve(parent_size.width).unwrap_or(0.0));
-        let padding = self.nodes[node].style.padding.map(|n| n.resolve(parent_size.width).unwrap_or(0.0));
-        let border = self.nodes[node].style.border.map(|n| n.resolve(parent_size.width).unwrap_or(0.0));
+        let margin = node.style.margin.resolve_or_default(parent_size.width);
+        let padding = node.style.padding.resolve_or_default(parent_size.width);
+        let border = node.style.border.resolve_or_default(parent_size.width);
 
         let padding_border = Rect {
             start: padding.start + border.start,
@@ -233,8 +233,8 @@ impl Forest {
             height: node_size.height.maybe_sub(padding_border.vertical_axis_sum()),
         };
 
-        let container_size = Size::zero();
-        let inner_container_size = Size::zero();
+        let container_size = Size::ZERO;
+        let inner_container_size = Size::ZERO;
 
         AlgoConstants {
             dir,
@@ -264,24 +264,23 @@ impl Forest {
             .filter(|(_, style)| style.display != Display::None)
             .map(|(child, child_style)| FlexItem {
                 node: *child,
-                size: child_style.size.resolve(constants.node_inner_size),
-                min_size: child_style.min_size.resolve(constants.node_inner_size),
-                max_size: child_style.max_size.resolve(constants.node_inner_size),
-
-                position: child_style.position.zip_size(constants.node_inner_size, |p, s| p.resolve(s)),
-                margin: child_style.margin.map(|m| m.resolve(constants.node_inner_size.width).unwrap_or(0.0)),
-                padding: child_style.padding.map(|p| p.resolve(constants.node_inner_size.width).unwrap_or(0.0)),
-                border: child_style.border.map(|b| b.resolve(constants.node_inner_size.width).unwrap_or(0.0)),
-
+                size: child_style.size.maybe_resolve(constants.node_inner_size),
+                min_size: child_style.min_size.maybe_resolve(constants.node_inner_size),
+                max_size: child_style.max_size.maybe_resolve(constants.node_inner_size),
+
+                position: child_style.position.zip_size(constants.node_inner_size, |p, s| p.maybe_resolve(s)),
+                margin: child_style.margin.resolve_or_default(constants.node_inner_size.width),
+                padding: child_style.padding.resolve_or_default(constants.node_inner_size.width),
+                border: child_style.border.resolve_or_default(constants.node_inner_size.width),
                 flex_basis: 0.0,
                 inner_flex_basis: 0.0,
                 violation: 0.0,
                 frozen: false,
 
-                hypothetical_inner_size: Size::zero(),
-                hypothetical_outer_size: Size::zero(),
-                target_size: Size::zero(),
-                outer_target_size: Size::zero(),
+                hypothetical_inner_size: Size::ZERO,
+                hypothetical_outer_size: Size::ZERO,
+                target_size: Size::ZERO,
+                outer_target_size: Size::ZERO,
 
                 baseline: 0.0,
 
@@ -368,7 +367,7 @@ impl Forest {
 
             // A. If the item has a definite used flex basis, that’s the flex base size.
 
-            let flex_basis = child_style.flex_basis.resolve(constants.node_inner_size.main(constants.dir));
+            let flex_basis = child_style.flex_basis.maybe_resolve(constants.node_inner_size.main(constants.dir));
             if flex_basis.is_some() {
                 child.flex_basis = flex_basis.unwrap_or(0.0);
                 continue;
@@ -866,7 +865,7 @@ impl Forest {
                     &Layout {
                         order: self.children[node].iter().position(|n| *n == child.node).unwrap() as u32,
                         size: preliminary_size,
-                        location: Point::zero(),
+                        location: Point::ZERO,
                     },
                 );
             }
@@ -1416,21 +1415,21 @@ impl Forest {
             let child_style = self.nodes[child].style;
 
             // X-axis
-            let child_position_start = child_style.position.start.resolve(container_width);
-            let child_margin_start = child_style.margin.start.resolve(container_width);
+            let child_position_start = child_style.position.start.maybe_resolve(container_width);
+            let child_margin_start = child_style.margin.start.maybe_resolve(container_width);
             let start = child_position_start.maybe_add(child_margin_start);
 
-            let child_position_end = child_style.position.end.resolve(container_width);
-            let child_margin_end = child_style.margin.end.resolve(container_width);
+            let child_position_end = child_style.position.end.maybe_resolve(container_width);
+            let child_margin_end = child_style.margin.end.maybe_resolve(container_width);
             let end = child_position_end.maybe_add(child_margin_end);
 
             // Y-axis
-            let child_position_top = child_style.position.top.resolve(container_height);
-            let child_margin_top = child_style.margin.top.resolve(container_height);
+            let child_position_top = child_style.position.top.maybe_resolve(container_height);
+            let child_margin_top = child_style.margin.top.maybe_resolve(container_height);
             let top = child_position_top.maybe_add(child_margin_top);
 
-            let child_position_bottom = child_style.position.bottom.resolve(container_height);
-            let child_margin_bottom = child_style.margin.bottom.resolve(container_height);
+            let child_position_bottom = child_style.position.bottom.maybe_resolve(container_height);
+            let child_margin_bottom = child_style.margin.bottom.maybe_resolve(container_height);
             let bottom = child_position_bottom.maybe_add(child_margin_bottom);
 
             let (start_main, end_main) = if constants.is_row { (start, end) } else { (top, bottom) };
@@ -1439,9 +1438,9 @@ impl Forest {
             let mut width = child_style
                 .size
                 .width
-                .resolve(container_width)
-                .maybe_max(child_style.min_size.width.resolve(container_width))
-                .maybe_min(child_style.max_size.width.resolve(container_width));
+                .maybe_resolve(container_width)
+                .maybe_max(child_style.min_size.width.maybe_resolve(container_width))
+                .maybe_min(child_style.max_size.width.maybe_resolve(container_width));
 
             if width.is_none() && start.is_some() && end.is_some() {
                 width = container_width.maybe_sub(start).maybe_sub(end);
@@ -1450,9 +1449,9 @@ impl Forest {
             let mut height: Option<f32> = child_style
                 .size
                 .height
-                .resolve(container_height)
-                .maybe_max(child_style.min_size.height.resolve(container_height))
-                .maybe_min(child_style.max_size.height.resolve(container_height));
+                .maybe_resolve(container_height)
+                .maybe_max(child_style.min_size.height.maybe_resolve(container_height))
+                .maybe_min(child_style.max_size.height.maybe_resolve(container_height));
 
             if height.is_none() && top.is_some() && bottom.is_some() {
                 height = container_height.maybe_sub(top).maybe_sub(bottom);
@@ -1470,10 +1469,14 @@ impl Forest {
                 - preliminary_size
                     .main(constants.dir)
                     .maybe_max(
-                        child_style.min_main_size(constants.dir).resolve(constants.node_inner_size.main(constants.dir)),
+                        child_style
+                            .min_main_size(constants.dir)
+                            .maybe_resolve(constants.node_inner_size.main(constants.dir)),
                     )
                     .maybe_min(
-                        child_style.max_main_size(constants.dir).resolve(constants.node_inner_size.main(constants.dir)),
+                        child_style
+                            .max_main_size(constants.dir)
+                            .maybe_resolve(constants.node_inner_size.main(constants.dir)),
                     );
 
             let free_cross_space = constants.container_size.cross(constants.dir)
@@ -1482,12 +1485,12 @@ impl Forest {
                     .maybe_max(
                         child_style
                             .min_cross_size(constants.dir)
-                            .resolve(constants.node_inner_size.cross(constants.dir)),
+                            .maybe_resolve(constants.node_inner_size.cross(constants.dir)),
                     )
                     .maybe_min(
                         child_style
                             .max_cross_size(constants.dir)
-                            .resolve(constants.node_inner_size.cross(constants.dir)),
+                            .maybe_resolve(constants.node_inner_size.cross(constants.dir)),
                     );
 
             let offset_main = if start_main.is_some() {
@@ -1567,7 +1570,7 @@ impl Forest {
         }
 
         // Define some general constants we will need for the remainder of the algorithm.
-        let mut constants = self.compute_constants(node, node_size, parent_size);
+        let mut constants = Forest::compute_constants(&self.nodes[node], node_size, parent_size);
 
         // If this is a leaf node we can skip a lot of this function in some cases
         if self.children[node].is_empty() {
@@ -1716,7 +1719,7 @@ impl Forest {
         ///
         /// Each hidden node has zero size and is placed at the origin
         fn hidden_layout(nodes: &mut [NodeData], children: &[ChildrenVec<NodeId>], node: NodeId, order: u32) {
-            nodes[node].layout = Layout { order, size: Size::zero(), location: Point::zero() };
+            nodes[node].layout = Layout { order, size: Size::ZERO, location: Point::ZERO };
 
             for (order, child) in children[node].iter().enumerate() {
                 hidden_layout(nodes, children, *child, order as _);
@@ -1735,3 +1738,61 @@ impl Forest {
         container_size
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use crate::{
+        forest::Forest,
+        math::MaybeMath,
+        prelude::{Rect, Size},
+        resolve::ResolveOrDefault,
+        style::{FlexWrap, FlexboxLayout},
+    };
+
+    // Make sure we get correct constants
+    #[test]
+    fn correct_constants() {
+        let mut forest = Forest::with_capacity(16);
+
+        let style = FlexboxLayout::default();
+        let node_id = forest.new_leaf(style);
+
+        let node_size = Size::undefined();
+        let parent_size = Size::undefined();
+
+        let constants = Forest::compute_constants(&forest.nodes[node_id], node_size, parent_size);
+
+        assert!(constants.dir == style.flex_direction);
+        assert!(constants.is_row == style.flex_direction.is_row());
+        assert!(constants.is_column == style.flex_direction.is_column());
+        assert!(constants.is_wrap_reverse == (style.flex_wrap == FlexWrap::WrapReverse));
+
+        let margin = style.margin.resolve_or_default(parent_size);
+        assert_eq!(constants.margin, margin);
+
+        let border = style.border.resolve_or_default(parent_size);
+        assert_eq!(constants.border, border);
+
+        let padding = style.padding.resolve_or_default(parent_size);
+
+        // TODO: Replace with something less hardcoded?
+        let padding_border = Rect {
+            start: padding.start + border.start,
+            end: padding.end + border.end,
+            top: padding.top + border.top,
+            bottom: padding.bottom + border.bottom,
+        };
+
+        assert_eq!(constants.padding_border, padding_border);
+
+        // TODO: Replace with something less hardcoded?
+        let inner_size = Size {
+            width: node_size.width.maybe_sub(padding_border.horizontal_axis_sum()),
+            height: node_size.height.maybe_sub(padding_border.vertical_axis_sum()),
+        };
+        assert_eq!(constants.node_inner_size, inner_size);
+
+        assert_eq!(constants.container_size, Size::ZERO);
+        assert_eq!(constants.inner_container_size, Size::ZERO);
+    }
+}
diff --git a/src/geometry.rs b/src/geometry.rs
index 110f20179..c976d667a 100644
--- a/src/geometry.rs
+++ b/src/geometry.rs
@@ -29,16 +29,6 @@ pub struct Rect<T> {
 }
 
 impl<T> Rect<T> {
-    /// Applies the function `f` to all four sides of the [`Rect`]
-    ///
-    /// This is used to transform a `Rect<T>` into a `Rect<R>`.
-    pub(crate) fn map<R, F>(self, f: F) -> Rect<R>
-    where
-        F: Fn(T) -> R,
-    {
-        Rect { start: f(self.start), end: f(self.end), top: f(self.top), bottom: f(self.bottom) }
-    }
-
     /// Applies the function `f` to all four sides of the rect
     ///
     /// When applied to the left and right sides, the width is used
@@ -148,6 +138,17 @@ where
     }
 }
 
+impl Rect<f32> {
+    /// Creates a new Rect with `0.0` as all parameters
+    pub const ZERO: Rect<f32> = Self { start: 0.0, end: 0.0, top: 0.0, bottom: 0.0 };
+
+    /// Creates a new Rect
+    #[must_use]
+    pub fn new(start: f32, end: f32, top: f32, bottom: f32) -> Self {
+        Self { start, end, top, bottom }
+    }
+}
+
 /// The width and height of a [`Rect`]
 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@@ -225,27 +226,38 @@ impl<T> Size<T> {
 
 impl Size<f32> {
     /// A [`Size`] with zero width and height
+    pub const ZERO: Size<f32> = Self { width: 0.0, height: 0.0 };
+}
+
+impl Size<Option<f32>> {
+    /// A [`Size`] with `None` width and height
+    pub const NONE: Size<Option<f32>> = Self { width: None, height: None };
+
+    /// A [`Size<Option<f32>>`] with `Some(width)` and `Some(height)` as parameters
     #[must_use]
-    pub fn zero() -> Self {
-        Self { width: 0.0, height: 0.0 }
+    pub fn new(width: f32, height: f32) -> Self {
+        Size { width: Some(width), height: Some(height) }
     }
 }
 
 impl Size<Dimension> {
     /// Generates a [`Size<Dimension>`] using [`Dimension::Points`] values
+    #[must_use]
     pub fn from_points(width: f32, height: f32) -> Self {
         Size { width: Dimension::Points(width), height: Dimension::Points(height) }
     }
 
     /// Generates a [`Size<Dimension>`] using [`Dimension::Percent`] values
+    #[must_use]
     pub fn from_percent(width: f32, height: f32) -> Self {
         Size { width: Dimension::Percent(width), height: Dimension::Percent(height) }
     }
 
-    /// Converts any `parent`-relative values for size into an absolute size
-    pub(crate) fn resolve(&self, parent: Size<Option<f32>>) -> Size<Option<f32>> {
-        Size { width: self.width.resolve(parent.width), height: self.height.resolve(parent.height) }
-    }
+    /// Generates a [`Size<Dimension>`] using [`Dimension::Auto`] in both width and height
+    pub const AUTO: Size<Dimension> = Self { width: Dimension::Auto, height: Dimension::Auto };
+
+    /// Generates a [`Size<Dimension>`] using [`Dimension::Undefined`] in both width and height
+    pub const UNDEFINED: Size<Dimension> = Self { width: Dimension::Undefined, height: Dimension::Undefined };
 }
 
 /// A 2-dimensional coordinate.
@@ -261,42 +273,5 @@ pub struct Point<T> {
 
 impl Point<f32> {
     /// A [`Point`] with values (0,0), representing the origin
-    #[must_use]
-    pub fn zero() -> Self {
-        Self { x: 0.0, y: 0.0 }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    mod test_resolve_size {
-        use crate::geometry::Size;
-        use crate::style::Dimension;
-        use rstest::rstest;
-
-        #[rstest]
-        #[case(Size { width: Dimension::Undefined, height: Dimension::Undefined }, Size { width: None, height: None }, Size { width: None, height: None })]
-        #[case(Size { width: Dimension::Undefined, height: Dimension::Undefined }, Size { width: Some(0.0), height: Some(0.0) }, Size { width: None, height: None })]
-        #[case(Size { width: Dimension::Undefined, height: Dimension::Undefined }, Size { width: Some(1.0), height: Some(1.0) }, Size { width: None, height: None })]
-        #[case(Size { width: Dimension::Undefined, height: Dimension::Undefined }, Size { width: Some(-1.0), height: Some(-1.0) }, Size { width: None, height: None })]
-        #[case(Size { width: Dimension::Auto, height: Dimension::Auto }, Size { width: None, height: None }, Size { width: None, height: None })]
-        #[case(Size { width: Dimension::Auto, height: Dimension::Auto }, Size { width: Some(0.0), height: Some(0.0) }, Size { width: None, height: None })]
-        #[case(Size { width: Dimension::Auto, height: Dimension::Auto }, Size { width: Some(1.0), height: Some(1.0) }, Size { width: None, height: None })]
-        #[case(Size { width: Dimension::Auto, height: Dimension::Auto }, Size { width: Some(-1.0), height: Some(-1.0) }, Size { width: None, height: None })]
-        #[case(Size { width: Dimension::Points(5.0), height: Dimension::Points(5.0) }, Size { width: None, height: None }, Size { width: Some(5.0), height: Some(5.0) })]
-        #[case(Size { width: Dimension::Points(5.0), height: Dimension::Points(5.0) }, Size { width: Some(0.0), height: Some(0.0) }, Size { width: Some(5.0), height: Some(5.0) })]
-        #[case(Size { width: Dimension::Points(5.0), height: Dimension::Points(5.0) }, Size { width: Some(1.0), height: Some(1.0) }, Size { width: Some(5.0), height: Some(5.0) })]
-        #[case(Size { width: Dimension::Points(5.0), height: Dimension::Points(5.0) }, Size { width: Some(-1.0), height: Some(-1.0) }, Size { width: Some(5.0), height: Some(5.0) })]
-        #[case(Size { width: Dimension::Percent(50.0), height: Dimension::Percent(50.0) }, Size { width: None, height: None }, Size { width: None, height: None })]
-        #[case(Size { width: Dimension::Percent(50.0), height: Dimension::Percent(50.0) }, Size { width: Some(0.0), height: Some(0.0) }, Size { width: Some(0.0), height: Some(0.0) })]
-        #[case(Size { width: Dimension::Percent(50.0), height: Dimension::Percent(50.0) }, Size { width: Some(1.0), height: Some(1.0) }, Size { width: Some(50.0), height: Some(50.0) })]
-        #[case(Size { width: Dimension::Percent(50.0), height: Dimension::Percent(50.0) }, Size { width: Some(-1.0), height: Some(-1.0) }, Size { width: Some(-50.0), height: Some(-50.0) })]
-        fn resolve_size(
-            #[case] input: Size<Dimension>,
-            #[case] parent: Size<Option<f32>>,
-            #[case] expected: Size<Option<f32>>,
-        ) {
-            assert_eq!(input.resolve(parent), expected);
-        }
-    }
+    pub const ZERO: Point<f32> = Self { x: 0.0, y: 0.0 };
 }
diff --git a/src/layout.rs b/src/layout.rs
index cbf323a90..397a7d434 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -20,7 +20,7 @@ impl Layout {
     /// Creates a new [`Layout`] struct with zero size positioned at the origin
     #[must_use]
     pub(crate) fn new() -> Self {
-        Self { order: 0, size: Size::zero(), location: Point::zero() }
+        Self { order: 0, size: Size::ZERO, location: Point::ZERO }
     }
 }
 
diff --git a/src/lib.rs b/src/lib.rs
index 46bc39bdc..3ccf788bd 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -29,6 +29,7 @@ mod flexbox;
 mod forest;
 #[cfg(all(not(feature = "alloc"), not(feature = "std")))]
 mod indexmap;
+mod resolve;
 mod sys;
 
 pub use crate::node::Taffy;
diff --git a/src/node.rs b/src/node.rs
index b86d3fd5a..1df0178b2 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -429,7 +429,7 @@ mod tests {
     fn new_leaf_with_measure() {
         let mut taffy = Taffy::new();
 
-        let res = taffy.new_leaf_with_measure(FlexboxLayout::default(), MeasureFunc::Raw(|_| Size::zero()));
+        let res = taffy.new_leaf_with_measure(FlexboxLayout::default(), MeasureFunc::Raw(|_| Size::ZERO));
         assert!(res.is_ok());
         let node = res.unwrap();
 
diff --git a/src/resolve.rs b/src/resolve.rs
new file mode 100644
index 000000000..bfe62d682
--- /dev/null
+++ b/src/resolve.rs
@@ -0,0 +1,363 @@
+//! Helper trait to calculate dimensions during layout resolution
+
+use crate::prelude::{Dimension, Rect, Size};
+
+/// Trait to encapsulate behaviour where we need to resolve from a
+/// potentially context-dependent size or dimension into
+/// a context-independent size or dimension.
+///
+/// Will return a default value if it unable to resolve.
+pub(crate) trait ResolveOrDefault<TContext, TOutput> {
+    /// Resolve a dimension that might be dependent on a context, with a default fallback value
+    fn resolve_or_default(self, context: TContext) -> TOutput;
+}
+
+/// Trait to encapsulate behaviour where we need to resolve from a
+/// potentially context-dependent size or dimension into
+/// a context-independent size or dimension.
+///
+/// Will return a `None` if it unable to resolve.
+pub(crate) trait MaybeResolve<T> {
+    /// Resolve a dimension that might be dependent on a context, with `None` as fallback value
+    fn maybe_resolve(self, context: T) -> T;
+}
+
+impl MaybeResolve<Option<f32>> for Dimension {
+    /// Converts the given [`Dimension`] into a concrete value of points
+    ///
+    /// Can return `None`
+    fn maybe_resolve(self, context: Option<f32>) -> Option<f32> {
+        match self {
+            Dimension::Points(points) => Some(points),
+            // parent_dim * percent
+            Dimension::Percent(percent) => context.map(|dim| dim * percent),
+            _ => None,
+        }
+    }
+}
+
+impl MaybeResolve<Size<Option<f32>>> for Size<Dimension> {
+    /// Converts any `parent`-relative values for size into an absolute size
+    fn maybe_resolve(self, context: Size<Option<f32>>) -> Size<Option<f32>> {
+        Size { width: self.width.maybe_resolve(context.width), height: self.height.maybe_resolve(context.height) }
+    }
+}
+
+impl ResolveOrDefault<Option<f32>, f32> for Dimension {
+    /// Will return a default value of result is evaluated to `None`
+    fn resolve_or_default(self, context: Option<f32>) -> f32 {
+        self.maybe_resolve(context).unwrap_or(0.0)
+    }
+}
+
+impl ResolveOrDefault<Size<Option<f32>>, Rect<f32>> for Rect<Dimension> {
+    fn resolve_or_default(self, context: Size<Option<f32>>) -> Rect<f32> {
+        Rect {
+            start: self.start.resolve_or_default(context.width),
+            end: self.end.resolve_or_default(context.width),
+            top: self.top.resolve_or_default(context.height),
+            bottom: self.bottom.resolve_or_default(context.height),
+        }
+    }
+}
+
+impl ResolveOrDefault<Option<f32>, Rect<f32>> for Rect<Dimension> {
+    fn resolve_or_default(self, context: Option<f32>) -> Rect<f32> {
+        Rect {
+            start: self.start.resolve_or_default(context),
+            end: self.end.resolve_or_default(context),
+            top: self.top.resolve_or_default(context),
+            bottom: self.bottom.resolve_or_default(context),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    mod maybe_resolve_dimension {
+
+        use crate::resolve::MaybeResolve;
+        use crate::style::Dimension;
+        use rstest::rstest;
+
+        /// `Dimension::Undefined` should always return `None`
+        ///
+        /// The parent / context should not affect the outcome.
+        #[rstest]
+        #[case(Dimension::Undefined, None, None)]
+        #[case(Dimension::Undefined, Some(5.0), None)]
+        #[case(Dimension::Undefined, Some(-5.0), None)]
+        #[case(Dimension::Undefined, Some(0.), None)]
+        fn resolve_undefined(#[case] input: Dimension, #[case] context: Option<f32>, #[case] expected: Option<f32>) {
+            assert_eq!(input.maybe_resolve(context), expected);
+        }
+
+        /// `Dimension::Auto` should always return `None`
+        ///
+        /// The parent / context should not affect the outcome.
+        #[rstest]
+        #[case(Dimension::Auto, None, None)]
+        #[case(Dimension::Auto, Some(5.0), None)]
+        #[case(Dimension::Auto, Some(-5.0), None)]
+        #[case(Dimension::Auto, Some(0.), None)]
+        fn resolve_auto(#[case] input: Dimension, #[case] context: Option<f32>, #[case] expected: Option<f32>) {
+            assert_eq!(input.maybe_resolve(context), expected);
+        }
+
+        /// `Dimension::Points` should always return `Some(f32)`
+        /// where the f32 value is the inner value of the points.
+        ///
+        /// The parent / context should not affect the outcome.
+        #[rstest]
+        #[case(Dimension::Points(1.0), None, Some(1.0))]
+        #[case(Dimension::Points(1.0), Some(5.0), Some(1.0))]
+        #[case(Dimension::Points(1.0), Some(-5.0), Some(1.0))]
+        #[case(Dimension::Points(1.0), Some(0.), Some(1.0))]
+        fn resolve_points(#[case] input: Dimension, #[case] context: Option<f32>, #[case] expected: Option<f32>) {
+            assert_eq!(input.maybe_resolve(context), expected);
+        }
+
+        /// `Dimension::Percent` should return `None` if context is  `None`.
+        /// Otherwise it should return `Some(f32)`
+        /// where the f32 value is the inner value of the percent * context value.
+        ///
+        /// The parent / context __should__ affect the outcome.
+        #[rstest]
+        #[case(Dimension::Percent(1.0), None, None)]
+        #[case(Dimension::Percent(1.0), Some(5.0), Some(5.0))]
+        #[case(Dimension::Percent(1.0), Some(-5.0), Some(-5.0))]
+        #[case(Dimension::Percent(1.0), Some(50.0), Some(50.0))]
+        fn resolve_percent(#[case] input: Dimension, #[case] context: Option<f32>, #[case] expected: Option<f32>) {
+            assert_eq!(input.maybe_resolve(context), expected);
+        }
+    }
+
+    mod maybe_resolve_size_dimension {
+        use crate::{prelude::Size, resolve::MaybeResolve, style::Dimension};
+        use rstest::rstest;
+
+        /// Size<Dimension::Undefined> should always return Size<None>
+        ///
+        /// The parent / context should not affect the outcome.
+        #[rstest]
+        #[case(Size::UNDEFINED, Size::NONE, Size::NONE)]
+        #[case(Size::UNDEFINED, Size::new(5.0, 5.0), Size::NONE)]
+        #[case(Size::UNDEFINED, Size::new(-5.0, -5.0), Size::NONE)]
+        #[case(Size::UNDEFINED, Size::new(0.0, 0.0), Size::NONE)]
+        fn maybe_resolve_undefined(
+            #[case] input: Size<Dimension>,
+            #[case] context: Size<Option<f32>>,
+            #[case] expected: Size<Option<f32>>,
+        ) {
+            assert_eq!(input.maybe_resolve(context), expected);
+        }
+
+        /// Size<Dimension::Auto> should always return Size<None>
+        ///
+        /// The parent / context should not affect the outcome.
+        #[rstest]
+        #[case(Size::AUTO, Size::NONE, Size::NONE)]
+        #[case(Size::AUTO, Size::new(5.0, 5.0), Size::NONE)]
+        #[case(Size::AUTO, Size::new(-5.0, -5.0), Size::NONE)]
+        #[case(Size::AUTO, Size::new(0.0, 0.0), Size::NONE)]
+        fn maybe_resolve_auto(
+            #[case] input: Size<Dimension>,
+            #[case] context: Size<Option<f32>>,
+            #[case] expected: Size<Option<f32>>,
+        ) {
+            assert_eq!(input.maybe_resolve(context), expected);
+        }
+
+        /// Size<Dimension::Points> should always return a Size<Some(f32)>
+        /// where the f32 values are the inner value of the points.
+        ///
+        /// The parent / context should not affect the outcome.
+        #[rstest]
+        #[case(Size::from_points(5.0, 5.0), Size::NONE, Size::new(5.0, 5.0))]
+        #[case(Size::from_points(5.0, 5.0), Size::new(5.0, 5.0), Size::new(5.0, 5.0))]
+        #[case(Size::from_points(5.0, 5.0), Size::new(-5.0, -5.0), Size::new(5.0, 5.0))]
+        #[case(Size::from_points(5.0, 5.0), Size::new(0.0, 0.0), Size::new(5.0, 5.0))]
+        fn maybe_resolve_points(
+            #[case] input: Size<Dimension>,
+            #[case] context: Size<Option<f32>>,
+            #[case] expected: Size<Option<f32>>,
+        ) {
+            assert_eq!(input.maybe_resolve(context), expected);
+        }
+
+        /// `Size<Dimension::Percent>` should return `Size<None>` if context is `Size<None>`.
+        /// Otherwise it should return `Size<Some(f32)>`
+        /// where the f32 value is the inner value of the percent * context value.
+        ///
+        /// The context __should__ affect the outcome.
+        #[rstest]
+        #[case(Size::from_percent(5.0, 5.0), Size::NONE, Size::NONE)]
+        #[case(Size::from_percent(5.0, 5.0), Size::new(5.0, 5.0), Size::new(25.0, 25.0))]
+        #[case(Size::from_percent(5.0, 5.0), Size::new(-5.0, -5.0), Size::new(-25.0, -25.0))]
+        #[case(Size::from_percent(5.0, 5.0), Size::new(0.0, 0.0), Size::new(0.0, 0.0))]
+        fn maybe_resolve_percent(
+            #[case] input: Size<Dimension>,
+            #[case] context: Size<Option<f32>>,
+            #[case] expected: Size<Option<f32>>,
+        ) {
+            assert_eq!(input.maybe_resolve(context), expected);
+        }
+    }
+
+    mod resolve_or_default_dimension_to_option_f32 {
+        use crate::resolve::ResolveOrDefault;
+        use crate::style::Dimension;
+        use rstest::rstest;
+
+        #[rstest]
+        #[case(Dimension::Undefined, None, 0.0)]
+        #[case(Dimension::Undefined, Some(5.0), 0.0)]
+        #[case(Dimension::Undefined, Some(-5.0), 0.0)]
+        #[case(Dimension::Undefined, Some(0.0), 0.0)]
+        fn resolve_or_default_undefined(#[case] input: Dimension, #[case] context: Option<f32>, #[case] expected: f32) {
+            assert_eq!(input.resolve_or_default(context), expected);
+        }
+        #[rstest]
+        #[case(Dimension::Auto, None, 0.0)]
+        #[case(Dimension::Auto, Some(5.0), 0.0)]
+        #[case(Dimension::Auto, Some(-5.0), 0.0)]
+        #[case(Dimension::Auto, Some(0.0), 0.0)]
+        fn resolve_or_default_auto(#[case] input: Dimension, #[case] context: Option<f32>, #[case] expected: f32) {
+            assert_eq!(input.resolve_or_default(context), expected);
+        }
+        #[rstest]
+        #[case(Dimension::Points(5.0), None, 5.0)]
+        #[case(Dimension::Points(5.0), Some(5.0), 5.0)]
+        #[case(Dimension::Points(5.0), Some(-5.0), 5.0)]
+        #[case(Dimension::Points(5.0), Some(0.0), 5.0)]
+        fn resolve_or_default_points(#[case] input: Dimension, #[case] context: Option<f32>, #[case] expected: f32) {
+            assert_eq!(input.resolve_or_default(context), expected);
+        }
+        #[rstest]
+        #[case(Dimension::Percent(5.0), None, 0.0)]
+        #[case(Dimension::Percent(5.0), Some(5.0), 25.0)]
+        #[case(Dimension::Percent(5.0), Some(-5.0), -25.0)]
+        #[case(Dimension::Percent(5.0), Some(0.0), 0.0)]
+        fn resolve_or_default_percent(#[case] input: Dimension, #[case] context: Option<f32>, #[case] expected: f32) {
+            assert_eq!(input.resolve_or_default(context), expected);
+        }
+    }
+
+    mod resolve_or_default_rect_dimension_to_rect {
+        use crate::geometry::{Rect, Size};
+        use crate::resolve::ResolveOrDefault;
+        use crate::style::Dimension;
+        use rstest::rstest;
+
+        #[rstest]
+        #[case(Rect::UNDEFINED, Size::NONE, Rect::ZERO)]
+        #[case(Rect::UNDEFINED, Size::new(5.0, 5.0), Rect::ZERO)]
+        #[case(Rect::UNDEFINED, Size::new(-5.0, -5.0), Rect::ZERO)]
+        #[case(Rect::UNDEFINED, Size::new(0.0, 0.0), Rect::ZERO)]
+        fn resolve_or_default_undefined(
+            #[case] input: Rect<Dimension>,
+            #[case] context: Size<Option<f32>>,
+            #[case] expected: Rect<f32>,
+        ) {
+            assert_eq!(input.resolve_or_default(context), expected);
+        }
+
+        #[rstest]
+        #[case(Rect::AUTO, Size::NONE, Rect::ZERO)]
+        #[case(Rect::AUTO, Size::new(5.0, 5.0), Rect::ZERO)]
+        #[case(Rect::AUTO, Size::new(-5.0, -5.0), Rect::ZERO)]
+        #[case(Rect::AUTO, Size::new(0.0, 0.0), Rect::ZERO)]
+        fn resolve_or_default_auto(
+            #[case] input: Rect<Dimension>,
+            #[case] context: Size<Option<f32>>,
+            #[case] expected: Rect<f32>,
+        ) {
+            assert_eq!(input.resolve_or_default(context), expected);
+        }
+
+        #[rstest]
+        #[case(Rect::from_points(5.0, 5.0, 5.0, 5.0), Size::NONE, Rect::new(5.0, 5.0, 5.0, 5.0))]
+        #[case(Rect::from_points(5.0, 5.0, 5.0, 5.0), Size::new(5.0, 5.0), Rect::new(5.0, 5.0, 5.0, 5.0))]
+        #[case(Rect::from_points(5.0, 5.0, 5.0, 5.0), Size::new(-5.0, -5.0), Rect::new(5.0, 5.0, 5.0, 5.0))]
+        #[case(Rect::from_points(5.0, 5.0, 5.0, 5.0), Size::new(0.0, 0.0), Rect::new(5.0, 5.0, 5.0, 5.0))]
+        fn resolve_or_default_points(
+            #[case] input: Rect<Dimension>,
+            #[case] context: Size<Option<f32>>,
+            #[case] expected: Rect<f32>,
+        ) {
+            assert_eq!(input.resolve_or_default(context), expected);
+        }
+
+        #[rstest]
+        #[case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Size::NONE, Rect::ZERO)]
+        #[case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Size::new(5.0, 5.0), Rect::new(25.0, 25.0, 25.0, 25.0))]
+        #[case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Size::new(-5.0, -5.0), Rect::new(-25.0, -25.0, -25.0, -25.0))]
+        #[case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Size::new(0.0, 0.0), Rect::ZERO)]
+        fn resolve_or_default_percent(
+            #[case] input: Rect<Dimension>,
+            #[case] context: Size<Option<f32>>,
+            #[case] expected: Rect<f32>,
+        ) {
+            assert_eq!(input.resolve_or_default(context), expected);
+        }
+    }
+
+    mod resolve_or_default_rect_dimension_to_rect_f32_via_option {
+        use crate::geometry::Rect;
+        use crate::resolve::ResolveOrDefault;
+        use crate::style::Dimension;
+        use rstest::rstest;
+
+        #[rstest]
+        #[case(Rect::UNDEFINED, None, Rect::ZERO)]
+        #[case(Rect::UNDEFINED, Some(5.0), Rect::ZERO)]
+        #[case(Rect::UNDEFINED, Some(-5.0), Rect::ZERO)]
+        #[case(Rect::UNDEFINED, Some(0.0), Rect::ZERO)]
+        fn resolve_or_default_undefined(
+            #[case] input: Rect<Dimension>,
+            #[case] context: Option<f32>,
+            #[case] expected: Rect<f32>,
+        ) {
+            assert_eq!(input.resolve_or_default(context), expected);
+        }
+
+        #[rstest]
+        #[case(Rect::AUTO, None, Rect::ZERO)]
+        #[case(Rect::AUTO, Some(5.0), Rect::ZERO)]
+        #[case(Rect::AUTO, Some(-5.0), Rect::ZERO)]
+        #[case(Rect::AUTO, Some(0.0), Rect::ZERO)]
+        fn resolve_or_default_auto(
+            #[case] input: Rect<Dimension>,
+            #[case] context: Option<f32>,
+            #[case] expected: Rect<f32>,
+        ) {
+            assert_eq!(input.resolve_or_default(context), expected);
+        }
+
+        #[rstest]
+        #[case(Rect::from_points(5.0, 5.0, 5.0, 5.0), None, Rect::new(5.0, 5.0, 5.0, 5.0))]
+        #[case(Rect::from_points(5.0, 5.0, 5.0, 5.0), Some(5.0), Rect::new(5.0, 5.0, 5.0, 5.0))]
+        #[case(Rect::from_points(5.0, 5.0, 5.0, 5.0), Some(-5.0), Rect::new(5.0, 5.0, 5.0, 5.0))]
+        #[case(Rect::from_points(5.0, 5.0, 5.0, 5.0), Some(0.0), Rect::new(5.0, 5.0, 5.0, 5.0))]
+        fn resolve_or_default_points(
+            #[case] input: Rect<Dimension>,
+            #[case] context: Option<f32>,
+            #[case] expected: Rect<f32>,
+        ) {
+            assert_eq!(input.resolve_or_default(context), expected);
+        }
+
+        #[rstest]
+        #[case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), None, Rect::ZERO)]
+        #[case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Some(5.0), Rect::new(25.0, 25.0, 25.0, 25.0))]
+        #[case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Some(-5.0), Rect::new(-25.0, -25.0, -25.0, -25.0))]
+        #[case(Rect::from_percent(5.0, 5.0, 5.0, 5.0), Some(0.0), Rect::ZERO)]
+        fn resolve_or_default_percent(
+            #[case] input: Rect<Dimension>,
+            #[case] context: Option<f32>,
+            #[case] expected: Rect<f32>,
+        ) {
+            assert_eq!(input.resolve_or_default(context), expected);
+        }
+    }
+}
diff --git a/src/style.rs b/src/style.rs
index 8403093ed..d57e38358 100644
--- a/src/style.rs
+++ b/src/style.rs
@@ -270,16 +270,6 @@ impl Default for Dimension {
 }
 
 impl Dimension {
-    /// Converts the given [`Dimension`] into a concrete value of points
-    pub(crate) fn resolve(self, parent_dim: Option<f32>) -> Option<f32> {
-        match self {
-            Dimension::Points(points) => Some(points),
-            // parent_dim * percent
-            Dimension::Percent(percent) => parent_dim.map(|dim| dim * percent),
-            _ => None,
-        }
-    }
-
     /// Is this value defined?
     pub(crate) fn is_defined(self) -> bool {
         matches!(self, Dimension::Points(_) | Dimension::Percent(_))
@@ -294,24 +284,62 @@ impl Default for Rect<Dimension> {
 
 impl Rect<Dimension> {
     /// Generates a [`Rect<Dimension>`] using [`Dimension::Points`] values for `start` and `top`
+    #[must_use]
     pub fn top_from_points(start: f32, top: f32) -> Rect<Dimension> {
         Rect { start: Dimension::Points(start), top: Dimension::Points(top), ..Default::default() }
     }
 
     /// Generates a [`Rect<Dimension>`] using [`Dimension::Points`] values for `end` and `bottom`
+    #[must_use]
     pub fn bot_from_points(end: f32, bottom: f32) -> Rect<Dimension> {
         Rect { end: Dimension::Points(end), bottom: Dimension::Points(bottom), ..Default::default() }
     }
 
     /// Generates a [`Rect<Dimension>`] using [`Dimension::Percent`] values for `start` and `top`
+    #[must_use]
     pub fn top_from_percent(start: f32, top: f32) -> Rect<Dimension> {
         Rect { start: Dimension::Percent(start), top: Dimension::Percent(top), ..Default::default() }
     }
 
     /// Generates a [`Rect<Dimension>`] using [`Dimension::Percent`] values for `end` and `bottom`
+    #[must_use]
     pub fn bot_from_percent(end: f32, bottom: f32) -> Rect<Dimension> {
         Rect { end: Dimension::Percent(end), bottom: Dimension::Percent(bottom), ..Default::default() }
     }
+
+    /// Generates a [`Rect<Dimension>`] using [`Dimension::Undefined`] for all values
+    pub const UNDEFINED: Rect<Dimension> = Self {
+        start: Dimension::Undefined,
+        end: Dimension::Undefined,
+        top: Dimension::Undefined,
+        bottom: Dimension::Undefined,
+    };
+
+    /// Generates a [`Rect<Dimension>`] using [`Dimension::Auto`] for all values
+    pub const AUTO: Rect<Dimension> =
+        Self { start: Dimension::Auto, end: Dimension::Auto, top: Dimension::Auto, bottom: Dimension::Auto };
+
+    /// Create a new Rect with [`Dimension::Points`]
+    #[must_use]
+    pub fn from_points(start: f32, end: f32, top: f32, bottom: f32) -> Self {
+        Rect {
+            start: Dimension::Points(start),
+            end: Dimension::Points(end),
+            top: Dimension::Points(top),
+            bottom: Dimension::Points(bottom),
+        }
+    }
+
+    /// Create a new Rect with [`Dimension::Percent`]
+    #[must_use]
+    pub fn from_percent(start: f32, end: f32, top: f32, bottom: f32) -> Self {
+        Rect {
+            start: Dimension::Percent(start),
+            end: Dimension::Percent(end),
+            top: Dimension::Percent(top),
+            bottom: Dimension::Percent(bottom),
+        }
+    }
 }
 
 impl Default for Size<Dimension> {
@@ -514,47 +542,6 @@ impl FlexboxLayout {
 
 #[cfg(test)]
 mod tests {
-    mod test_resolve_dimensions {
-        use crate::style::Dimension;
-        use rstest::rstest;
-
-        #[rstest]
-        #[case(Dimension::Undefined, None, None)]
-        #[case(Dimension::Undefined, Some(5.0), None)]
-        #[case(Dimension::Undefined, Some(-5.0), None)]
-        #[case(Dimension::Undefined, Some(0.), None)]
-        fn resolve_undefined(#[case] input: Dimension, #[case] parent: Option<f32>, #[case] expected: Option<f32>) {
-            assert_eq!(input.resolve(parent), expected);
-        }
-
-        #[rstest]
-        #[case(Dimension::Auto, None, None)]
-        #[case(Dimension::Auto, Some(5.0), None)]
-        #[case(Dimension::Auto, Some(-5.0), None)]
-        #[case(Dimension::Auto, Some(0.), None)]
-        fn resolve_auto(#[case] input: Dimension, #[case] parent: Option<f32>, #[case] expected: Option<f32>) {
-            assert_eq!(input.resolve(parent), expected);
-        }
-
-        #[rstest]
-        #[case(Dimension::Points(0.), None, Some(0.))]
-        #[case(Dimension::Points(1.), Some(5.0), Some(1.))]
-        #[case(Dimension::Points(-1.), Some(-5.0), Some(-1.))]
-        #[case(Dimension::Points(1.0), Some(0.), Some(1.0))]
-        fn resolve_points(#[case] input: Dimension, #[case] parent: Option<f32>, #[case] expected: Option<f32>) {
-            assert_eq!(input.resolve(parent), expected);
-        }
-
-        #[rstest]
-        #[case(Dimension::Percent(1.0), None, None)]
-        #[case(Dimension::Percent(1.0), Some(5.0), Some(5.0))]
-        #[case(Dimension::Percent(1.0), Some(-5.0), Some(-5.0))]
-        #[case(Dimension::Percent(10.0), Some(5.0), Some(50.0))]
-        fn resolve_percent(#[case] input: Dimension, #[case] parent: Option<f32>, #[case] expected: Option<f32>) {
-            assert_eq!(input.resolve(parent), expected);
-        }
-    }
-
     mod test_flex_direction {
         use crate::style::*;