Просмотр исходного кода

Initial geometery and layout framework.

Kestrel 5 месяцев назад
Сommit
dc2977b125
8 измененных файлов с 870 добавлено и 0 удалено
  1. 2 0
      .gitignore
  2. 10 0
      Cargo.toml
  3. 225 0
      src/geom.rs
  4. 258 0
      src/layout.rs
  5. 116 0
      src/layout/arr.rs
  6. 86 0
      src/layout/cache.rs
  7. 171 0
      src/layout/calc.rs
  8. 2 0
      src/lib.rs

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+/target
+.*.swp

+ 10 - 0
Cargo.toml

@@ -0,0 +1,10 @@
+[package]
+name = "patina"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+smallvec = "1.13"
+log = "0.4"

+ 225 - 0
src/geom.rs

@@ -0,0 +1,225 @@
+pub type IVector = Vector<i32>;
+pub type IPoint = Point<i32>;
+pub type IRect = Rect<i32>;
+
+pub type FVector = Vector<f32>;
+pub type FPoint = Point<f32>;
+pub type FRect = Rect<f32>;
+
+pub mod prelude {
+    pub use super::{FPoint, FRect, FVector};
+    pub use super::{IPoint, IRect, IVector};
+}
+
+pub trait GeomField:
+    Sized
+    + Copy
+    + std::ops::Add<Self, Output = Self>
+    + std::ops::AddAssign<Self>
+    + std::ops::Neg<Output = Self>
+    + std::ops::Sub<Self, Output = Self>
+    + std::ops::Mul<Self, Output = Self>
+    + std::ops::MulAssign<Self>
+    + PartialEq
+    + PartialOrd
+{
+    const ZERO: Self;
+
+    fn min(&self, other: Self) -> Self {
+        if *self < other {
+            *self
+        } else {
+            other
+        }
+    }
+
+    fn max(&self, other: Self) -> Self {
+        if *self > other {
+            *self
+        } else {
+            other
+        }
+    }
+}
+
+impl GeomField for i32 {
+    const ZERO: Self = 0;
+}
+
+impl GeomField for f32 {
+    const ZERO: Self = 0.;
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)]
+pub struct Point<GF: GeomField> {
+    pub x: GF,
+    pub y: GF,
+}
+
+impl<GF: GeomField> Point<GF> {
+    pub const ORIGIN: Self = Self {
+        x: GF::ZERO,
+        y: GF::ZERO,
+    };
+}
+
+impl<GF: GeomField> std::ops::Add<Vector<GF>> for Point<GF> {
+    type Output = Self;
+    fn add(self, rhs: Vector<GF>) -> Self::Output {
+        Self {
+            x: self.x + rhs.x,
+            y: self.x + rhs.x,
+        }
+    }
+}
+
+impl<GF: GeomField> std::ops::Sub for Point<GF> {
+    type Output = Vector<GF>;
+    fn sub(self, rhs: Self) -> Self::Output {
+        Vector {
+            x: self.x - rhs.x,
+            y: self.y - rhs.y,
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)]
+pub struct Vector<GF: GeomField> {
+    pub x: GF,
+    pub y: GF,
+}
+
+impl<GF: GeomField> Vector<GF> {
+    pub const ZERO: Self = Self {
+        x: GF::ZERO,
+        y: GF::ZERO,
+    };
+}
+
+impl<GF: GeomField> std::ops::Add for Vector<GF> {
+    type Output = Self;
+    fn add(self, rhs: Self) -> Self::Output {
+        Self {
+            x: self.x + rhs.x,
+            y: self.y + rhs.y,
+        }
+    }
+}
+
+impl<GF: GeomField> std::ops::Add<Point<GF>> for Vector<GF> {
+    type Output = Point<GF>;
+    fn add(self, rhs: Point<GF>) -> Self::Output {
+        Point {
+            x: self.x + rhs.x,
+            y: self.y + rhs.y,
+        }
+    }
+}
+
+impl<GF: GeomField> std::ops::Sub for Vector<GF> {
+    type Output = Vector<GF>;
+    fn sub(self, rhs: Self) -> Self::Output {
+        Vector {
+            x: self.x - rhs.x,
+            y: self.y - rhs.y,
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)]
+pub struct Rect<GF: GeomField> {
+    pub base: Point<GF>,
+    pub size: Vector<GF>,
+}
+
+impl<GF: GeomField> Rect<GF> {
+    pub fn new_from_size(pt: Point<GF>, sz: Vector<GF>) -> Self {
+        Self { base: pt, size: sz }
+    }
+
+    pub fn new_from_points(p1: Point<GF>, p2: Point<GF>) -> Self {
+        let xmin = p1.x.min(p2.x);
+        let ymin = p1.y.min(p2.y);
+        let xmax = p1.x.max(p2.x);
+        let ymax = p1.y.max(p2.y);
+        Self {
+            base: Point { x: xmin, y: ymin },
+            size: Vector {
+                x: xmax - xmin,
+                y: ymax - ymin,
+            },
+        }
+    }
+
+    pub fn width(&self) -> GF {
+        self.size.x
+    }
+
+    pub fn height(&self) -> GF {
+        self.size.y
+    }
+
+    pub fn normalize(&mut self) {
+        if self.size.x < GF::ZERO {
+            self.base.x += self.size.x;
+            self.size.x = -self.size.x;
+        }
+        if self.size.y < GF::ZERO {
+            self.base.y += self.size.y;
+            self.size.y = -self.size.y;
+        }
+    }
+
+    pub fn ll(&self) -> Point<GF> {
+        self.base
+    }
+
+    pub fn ul(&self) -> Point<GF> {
+        self.base
+            + Vector {
+                x: GF::ZERO,
+                y: self.size.y,
+            }
+    }
+
+    pub fn lr(&self) -> Point<GF> {
+        self.base
+            + Vector {
+                x: self.size.x,
+                y: GF::ZERO,
+            }
+    }
+
+    pub fn ur(&self) -> Point<GF> {
+        self.extent()
+    }
+
+    pub fn base(&self) -> Point<GF> {
+        self.base
+    }
+
+    pub fn extent(&self) -> Point<GF> {
+        self.base + self.size
+    }
+
+    pub fn size(&self) -> Vector<GF> {
+        self.size
+    }
+
+    pub fn intersect(&self, other: &Self) -> Option<Self> {
+        /*
+        Some(Self {
+            minpoint: Point { x: self.minpoint.x.max(other.minpoint.x), y: self.minpoint.y.max(other.minpoint.y) },
+            maxpoint: Point { x: self.maxpoint.x.min(other.maxpoint.x), y: self.maxpoint.y.min(other.maxpoint.y) },
+        })
+        */
+        None
+    }
+
+    pub fn contains(&self, pt: Point<GF>) -> bool {
+        (self.base.x <= pt.x)
+            && (pt.x < self.extent().x)
+            && (self.base.y <= pt.y)
+            && (pt.y < self.extent().y)
+    }
+}

+ 258 - 0
src/layout.rs

@@ -0,0 +1,258 @@
+use std::{
+    ops::{Deref, DerefMut},
+    rc::Rc,
+};
+
+use cache::{Layer, LayoutCache, LayoutCacheKey, NodeState};
+use crate::geom::IRect;
+
+mod arr;
+mod cache;
+mod calc;
+
+pub use calc::recalculate;
+
+/// Sizing policy for a layout dimension
+#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
+pub struct SizePolicy {
+    /// The minimum number of pixels required along this dimension to avoid overlap and other
+    /// issues.
+    pub minimum: usize,
+    /// The number of pixels requested to have a comfortable amount of space and avoid ugly-looking
+    /// UI.
+    pub desired: usize,
+    /// How much to take up any remaining slack space.
+    pub slack_weight: usize,
+}
+
+impl SizePolicy {
+    pub fn max(self, rhs: SizePolicy) -> SizePolicy {
+        Self {
+            minimum: self.minimum.max(rhs.minimum),
+            desired: self.desired.max(rhs.desired),
+            slack_weight: self.slack_weight.max(rhs.slack_weight),
+        }
+    }
+
+    pub fn max_preserve_slack(self, rhs: SizePolicy) -> SizePolicy {
+        Self {
+            minimum: self.minimum.max(rhs.minimum),
+            desired: self.desired.max(rhs.desired),
+            slack_weight: self.slack_weight,
+        }
+    }
+}
+
+impl std::ops::Add<Self> for SizePolicy {
+    type Output = Self;
+    fn add(self, rhs: Self) -> Self::Output {
+        Self {
+            minimum: self.minimum + rhs.minimum,
+            desired: self.desired + rhs.desired,
+            slack_weight: self.slack_weight + rhs.slack_weight,
+        }
+    }
+}
+
+#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
+pub struct BoxMeasure {
+    pub left: usize,
+    pub right: usize,
+    pub top: usize,
+    pub bottom: usize,
+}
+
+impl From<usize> for BoxMeasure {
+    fn from(value: usize) -> Self {
+        Self::new_from_value(value)
+    }
+}
+
+impl BoxMeasure {
+    pub fn new_from_value(value: usize) -> Self {
+        Self {
+            left: value,
+            right: value,
+            top: value,
+            bottom: value,
+        }
+    }
+}
+
+/// What sort of layout does a node represent?
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+pub enum NodeBehaviour {
+    /// A fixed rendering area, probably a root node.
+    Fixed { rect: IRect },
+    /// An ordinary box sitting inside another node, hinting its size and receiving a final
+    /// size assignment from its parent.
+    Element,
+    /// A node that floats above other content, anchored with a zero-size flexbox.
+    Anchored,
+}
+
+#[derive(Clone)]
+pub enum ChildArrangement {
+    Custom(std::rc::Rc<dyn arr::ArrangementCalculator>),
+    Column,
+    Row,
+    Table,
+}
+
+impl std::fmt::Debug for ChildArrangement {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Custom(_) => f.write_str("ChildArrangement::Custom"),
+            Self::Column => f.write_str("ChildArrangement::Column"),
+            Self::Row => f.write_str("ChildArrangement::Row"),
+            Self::Table => f.write_str("ChildArrangement::Table"),
+        }
+    }
+}
+
+impl Deref for ChildArrangement {
+    type Target = dyn arr::ArrangementCalculator;
+    fn deref(&self) -> &Self::Target {
+        match self {
+            Self::Custom(calc) => calc.as_ref(),
+            Self::Column => &arr::LineArrangement::Column,
+            Self::Row => &arr::LineArrangement::Row,
+            Self::Table => todo!(),
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct LayoutNode {
+    cache_key: LayoutCacheKey,
+    cache: Rc<LayoutCache>,
+    behaviour: NodeBehaviour,
+    child_arrangement: ChildArrangement,
+    width_policy: SizePolicy,
+    height_policy: SizePolicy,
+
+    padding: BoxMeasure,
+    margin: BoxMeasure,
+}
+
+impl LayoutNode {
+    pub fn new(cache: Rc<LayoutCache>) -> Self {
+        let cache_key = LayoutCacheKey::generate();
+        cache.update_queue.borrow_mut().push(cache_key);
+        Self {
+            cache_key,
+            cache,
+            behaviour: NodeBehaviour::Element,
+            child_arrangement: ChildArrangement::Column,
+            width_policy: SizePolicy::default(),
+            height_policy: SizePolicy::default(),
+            padding: 0.into(),
+            margin: 0.into(),
+        }
+    }
+
+    pub fn relayout(&self) {
+        self.cache.update_queue.borrow_mut().push(self.cache_key);
+    }
+}
+
+// accessors
+impl LayoutNode {
+    pub fn behaviour(&self) -> NodeBehaviour {
+        self.behaviour
+    }
+    pub fn set_behaviour(&mut self, mode: NodeBehaviour) {
+        self.behaviour = mode;
+        self.relayout();
+    }
+
+    pub fn arrangement(&self) -> &ChildArrangement {
+        &self.child_arrangement
+    }
+    pub fn set_arrangement(&mut self, arr: ChildArrangement) {
+        self.child_arrangement = arr;
+        self.relayout();
+    }
+
+    pub fn width_policy(&self) -> SizePolicy {
+        self.width_policy
+    }
+    pub fn set_width_policy(&mut self, policy: SizePolicy) {
+        self.width_policy = policy;
+        self.relayout();
+    }
+    pub fn height_policy(&self) -> SizePolicy {
+        self.height_policy
+    }
+    pub fn set_height_policy(&mut self, policy: SizePolicy) {
+        self.height_policy = policy;
+        self.relayout();
+    }
+
+    pub fn padding(&self) -> BoxMeasure {
+        self.padding
+    }
+    pub fn padding_mut(&mut self) -> &mut BoxMeasure {
+        &mut self.padding
+    }
+    pub fn margin(&self) -> BoxMeasure {
+        self.margin
+    }
+    pub fn margin_mut(&mut self) -> &mut BoxMeasure {
+        &mut self.margin
+    }
+}
+
+impl Clone for LayoutNode {
+    fn clone(&self) -> Self {
+        Self {
+            cache_key: LayoutCacheKey::generate(),
+            cache: self.cache.clone(),
+            behaviour: self.behaviour,
+            child_arrangement: self.child_arrangement.clone(),
+            width_policy: self.width_policy,
+            height_policy: self.height_policy,
+            padding: self.padding,
+            margin: self.margin,
+        }
+    }
+}
+
+impl Drop for LayoutNode {
+    fn drop(&mut self) {
+        self.cache.update_queue.borrow_mut().push(self.cache_key);
+    }
+}
+
+#[derive(Clone)]
+pub struct LayoutChildIter<'l> {
+    lna: &'l dyn LayoutNodeAccess,
+    next_index: usize,
+}
+
+impl<'l> LayoutChildIter<'l> {
+    pub fn new(lna: &'l dyn LayoutNodeAccess) -> Self {
+        Self { lna, next_index: 0 }
+    }
+}
+
+impl<'l> Iterator for LayoutChildIter<'l> {
+    type Item = &'l dyn LayoutNodeAccess;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let index = self.next_index;
+        if index >= self.lna.child_count() {
+            None
+        } else {
+            self.next_index += 1;
+            self.lna.child(index)
+        }
+    }
+}
+
+/// Accessor trait for a [`LayoutNode`] tree, which has externally-stored relationships.
+pub trait LayoutNodeAccess: Deref<Target = LayoutNode> + DerefMut<Target = LayoutNode> {
+    fn child_count(&self) -> usize;
+    fn child(&self, ndx: usize) -> Option<&dyn LayoutNodeAccess>;
+    fn child_iter(&self) -> LayoutChildIter;
+}

+ 116 - 0
src/layout/arr.rs

@@ -0,0 +1,116 @@
+use std::ops::Add;
+
+use crate::geom::{IPoint, IRect, IVector};
+
+use super::{cache::NodeState, LayoutNodeAccess, SizePolicy};
+
+pub trait ArrangementCalculator {
+    fn arrange_step(
+        &self,
+        node: &dyn LayoutNodeAccess,
+        child_policies: Vec<(SizePolicy, SizePolicy)>,
+    ) -> (SizePolicy, SizePolicy);
+    fn layout_step(&self, node: &dyn LayoutNodeAccess, inside: IRect);
+}
+
+#[derive(Clone, Debug)]
+pub enum LineArrangement {
+    Column,
+    Row,
+}
+
+impl LineArrangement {
+    fn is_column(&self) -> bool {
+        match self {
+            Self::Column => true,
+            Self::Row => false,
+        }
+    }
+}
+
+impl ArrangementCalculator for LineArrangement {
+    fn arrange_step(
+        &self,
+        node: &dyn LayoutNodeAccess,
+        child_policies: Vec<(SizePolicy, SizePolicy)>,
+    ) -> (SizePolicy, SizePolicy) {
+        if child_policies.is_empty() {
+            return (node.width_policy, node.height_policy);
+        }
+
+        let cw_policy = child_policies
+            .iter()
+            .map(|v| v.0)
+            .reduce(if self.is_column() { SizePolicy::max } else { SizePolicy::add })
+            .unwrap();
+        let ch_policy: SizePolicy = child_policies
+            .iter()
+            .map(|v| v.1)
+            .reduce(if self.is_column() { SizePolicy::add } else { SizePolicy::max })
+            .unwrap();
+        (
+            cw_policy.max_preserve_slack(node.width_policy),
+            ch_policy.max_preserve_slack(node.height_policy),
+        )
+    }
+
+    fn layout_step(&self, node: &dyn LayoutNodeAccess, inside: IRect) {
+        // do the final children layouts
+        node.cache
+            .with_state(node.cache_key, |ns| ns.rect = Some(inside));
+
+        // expansion direction extraction lambda
+        let ee = if self.is_column() {
+            |ns: &mut NodeState| ns.net_policy.1
+        } else {
+            |ns: &mut NodeState| ns.net_policy.0
+        };
+
+        let policies = node.child_iter().map(|c| node.cache.with_state(c.cache_key, ee).unwrap());
+        let mut last_offset = 0;
+        for (offset, child) in do_fit(if self.is_column() { inside.height() } else { inside.width() }, policies).zip(node.child_iter()) {
+            let crect = if self.is_column() {
+                IRect::new_from_size(
+                    IPoint { x: inside.base().x, y: inside.ul().y + last_offset },
+                    IVector { x: inside.width(), y: (offset - last_offset) }
+                )
+            } else {
+                IRect::new_from_size(
+                    IPoint { x: inside.ul().x + last_offset, y: inside.base().y },
+                    IVector { x: (offset - last_offset), y: inside.height() }
+                )
+            };
+
+            self.layout_step(child, crect);
+            last_offset = offset;
+        }
+    }
+}
+
+fn do_fit<'l>(total: i32, policies: impl Iterator<Item = SizePolicy> + Clone) -> impl Iterator<Item = i32> {
+    // first pass over children: collect sum total of minimum/desired sizes and/slack weights
+    let policy_sum = policies.clone().reduce(SizePolicy::add).unwrap_or_default();
+
+    // how much of the desired size can we fit?
+    let fit_coeff =
+        (total - policy_sum.minimum as i32) as f32 / (policy_sum.desired - policy_sum.minimum) as f32;
+    let fit_coeff = fit_coeff.min(1.0); // not more than 100% of desired
+
+    // how much slack space do we have left?
+    let slack_coeff = if policy_sum.slack_weight > 0 && fit_coeff > 1.0 {
+        (total - policy_sum.desired as i32) as f32 / policy_sum.slack_weight as f32
+    } else {
+        0.0
+    };
+
+    let mut offset = 0;
+    // second pass over children: actually space out according to size policies and calculated coefficients
+    policies.map(move |policy| {
+        let mut amount = policy.minimum as f32;
+        amount += policy.desired as f32 * fit_coeff;
+        amount += policy.slack_weight as f32 * slack_coeff;
+        let amount = amount.round() as i32;
+        offset += amount;
+        offset
+    })
+}

+ 86 - 0
src/layout/cache.rs

@@ -0,0 +1,86 @@
+use std::{cell::RefCell, collections::HashMap, rc::Rc};
+
+use crate::geom::prelude::*;
+
+use super::SizePolicy;
+
+#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
+pub struct LayoutCacheKey(usize);
+static NEXT_CACHE_KEY: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(1);
+impl LayoutCacheKey {
+    pub fn generate() -> Self {
+        Self(NEXT_CACHE_KEY.fetch_add(1, std::sync::atomic::Ordering::SeqCst))
+    }
+}
+
+#[derive(Clone, Debug)]
+pub struct NodeState {
+    pub(super) needs_update: bool,
+    pub(super) net_policy: (SizePolicy, SizePolicy),
+    pub(super) layer: Layer,
+    pub(super) rect: Option<IRect>,
+    pub(super) children: Vec<LayoutCacheKey>,
+}
+
+#[derive(Debug, Default)]
+pub struct LayoutCache {
+    pub(super) update_queue: RefCell<Vec<LayoutCacheKey>>,
+    states: RefCell<HashMap<LayoutCacheKey, NodeState>>,
+    parents: RefCell<HashMap<LayoutCacheKey, LayoutCacheKey>>,
+}
+
+impl LayoutCache {
+    pub fn new() -> Rc<LayoutCache> {
+        Rc::new(Self {
+            update_queue: Default::default(),
+            states: Default::default(),
+            parents: Default::default(),
+        })
+    }
+
+    pub fn propagate_relayouts(&self) {
+        let mut updq = self.update_queue.borrow_mut();
+        let mut states = self.states.borrow_mut();
+        let parents = self.parents.borrow();
+        while let Some(next) = updq.pop() {
+            states.get_mut(&next).map(|v| {
+                if !v.needs_update {
+                    parents.get(&next).map(|p| updq.push(*p));
+                    v.needs_update = true;
+                }
+            });
+        }
+    }
+
+    pub fn with_state<R, F: FnOnce(&mut NodeState) -> R>(
+        &self,
+        ckey: LayoutCacheKey,
+        f: F,
+    ) -> Option<R> {
+        self.states.borrow_mut().get_mut(&ckey).map(f)
+    }
+
+    pub fn has_state_for(&self, ckey: LayoutCacheKey) -> bool {
+        self.states.borrow().contains_key(&ckey)
+    }
+
+    pub fn store(&self, ckey: LayoutCacheKey, parent: Option<LayoutCacheKey>, ns: NodeState) {
+        self.states.borrow_mut().insert(ckey, ns);
+        if let Some(pck) = parent {
+            self.parents.borrow_mut().insert(ckey, pck);
+        }
+    }
+}
+
+#[derive(Clone, Copy, PartialEq, PartialOrd, Debug, Default)]
+pub struct Layer(u16, u16);
+
+impl Layer {
+    pub fn nest(&self) -> Self {
+        Layer(self.0, self.1 + 1)
+    }
+
+    pub fn float(&self) -> Self {
+        Layer(self.0 + 1, 0)
+    }
+}

+ 171 - 0
src/layout/calc.rs

@@ -0,0 +1,171 @@
+use crate::geom::{IRect, IVector};
+
+use super::{cache::LayoutCacheKey, Layer, LayoutNode, LayoutNodeAccess, NodeBehaviour, NodeState};
+
+pub fn recalculate(node: &dyn LayoutNodeAccess) {
+    if let NodeBehaviour::Fixed { rect } = node.behaviour() {
+        let layer = Layer::default();
+
+        // propagate relayout flags up to the root
+        node.cache.propagate_relayouts();
+
+        arrangement_pass(None, node, layer);
+
+        println!("invoking layout_step with rect {rect:?}");
+
+        node.child_arrangement.layout_step(node, rect);
+    } else {
+        log::warn!("Layout root node does not have Fixed mode");
+    }
+}
+
+fn arrangement_pass(parent: Option<LayoutCacheKey>, node: &dyn LayoutNodeAccess, layer: Layer) {
+    println!(
+        "arrangement_pass({:?}, {:?}, {:?})",
+        parent, node.cache_key, layer
+    );
+    // early-out check
+    if node.cache.with_state(node.cache_key, |st| st.needs_update) == Some(false) {
+        return;
+    }
+
+    for child in node.child_iter() {
+        arrangement_pass(Some(node.cache_key), child, layer.nest());
+    }
+
+    // construct net size policy
+    let child_policies = node
+        .child_iter()
+        .map(|child| {
+            node.cache
+                .with_state(child.cache_key, |s| s.net_policy)
+                .unwrap()
+        })
+        .collect::<Vec<_>>();
+
+    let (wpol, hpol) = node.child_arrangement.arrange_step(node, child_policies);
+
+    if node.cache.has_state_for(node.cache_key) {
+        node.cache.with_state(node.cache_key, |ns| {
+            ns.net_policy = (wpol, hpol);
+        });
+    } else {
+        node.cache.store(
+            node.cache_key,
+            parent,
+            NodeState {
+                needs_update: false,
+                net_policy: (wpol, hpol),
+                rect: None,
+                layer,
+                children: vec![],
+            },
+        );
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use std::ops::{Deref, DerefMut};
+
+    use crate::{
+        geom::{IPoint, IRect, IVector},
+        layout::{
+            cache::LayoutCache, ChildArrangement, LayoutChildIter, LayoutNode, LayoutNodeAccess,
+            NodeBehaviour, SizePolicy,
+        },
+    };
+
+    use super::recalculate;
+
+    struct LayoutTree {
+        children: Vec<LayoutTree>,
+        node: LayoutNode,
+    }
+
+    impl Deref for LayoutTree {
+        type Target = LayoutNode;
+        fn deref(&self) -> &Self::Target {
+            &self.node
+        }
+    }
+    impl DerefMut for LayoutTree {
+        fn deref_mut(&mut self) -> &mut Self::Target {
+            &mut self.node
+        }
+    }
+
+    impl<'l> LayoutNodeAccess for LayoutTree {
+        fn child(&self, ndx: usize) -> Option<&dyn LayoutNodeAccess> {
+            self.children.get(ndx).map(|v| v as &dyn LayoutNodeAccess)
+        }
+        fn child_iter(&self) -> crate::layout::LayoutChildIter {
+            LayoutChildIter::new(self)
+        }
+        fn child_count(&self) -> usize {
+            self.children.len()
+        }
+    }
+
+    #[test]
+    fn simple_test() {
+        let cache = LayoutCache::new();
+
+        let mut root = LayoutTree {
+            children: vec![
+                {
+                    let mut lt = LayoutTree {
+                        children: vec![],
+                        node: LayoutNode::new(cache.clone()),
+                    };
+                    lt.set_height_policy(SizePolicy {
+                        minimum: 1,
+                        desired: 1,
+                        slack_weight: 1,
+                    });
+                    lt
+                },
+                {
+                    let mut lt = LayoutTree {
+                        children: vec![],
+                        node: LayoutNode::new(cache.clone()),
+                    };
+                    lt.set_height_policy(SizePolicy {
+                        minimum: 2,
+                        desired: 3,
+                        slack_weight: 0,
+                    });
+                    lt
+                },
+            ],
+            node: LayoutNode::new(cache.clone()),
+        };
+        root.set_behaviour(NodeBehaviour::Fixed {
+            rect: IRect::new_from_size(IPoint { x: 1, y: 1 }, IVector { x: 2, y: 5 }),
+        });
+        root.child_arrangement = ChildArrangement::Column;
+
+        recalculate(&root);
+
+        // check that final rects match expectations
+        assert_eq!(
+            cache.with_state(root.cache_key, |ns| ns.rect).flatten(),
+            Some(IRect::new_from_size(
+                IPoint { x: 1, y: 1 },
+                IVector { x: 2, y: 5 }
+            ))
+        );
+
+        assert_eq!(
+            cache
+                .with_state(root.children[0].cache_key, |ns| ns.rect)
+                .flatten(),
+            Some(IRect::new_from_size(
+                IPoint { x: 1, y: 1 },
+                IVector { x: 2, y: 2 }
+            ))
+        );
+
+        println!("cache: {:?}", cache);
+    }
+}

+ 2 - 0
src/lib.rs

@@ -0,0 +1,2 @@
+pub mod geom;
+pub mod layout;