diff --git a/Cargo.lock b/Cargo.lock index 0a7d5eb..290dfac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1420,9 +1420,6 @@ name = "rectree" version = "0.1.0" dependencies = [ "bitflags 2.10.0", - "hashbrown 0.16.1", - "kurbo", - "sparse_map", ] [[package]] @@ -1637,12 +1634,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sparse_map" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "839b579eddeb504ed4349054948eb9f02245488eddab69672e6d8a91a64f448a" - [[package]] name = "spatree" version = "0.1.0" diff --git a/crates/rectree/Cargo.toml b/crates/rectree/Cargo.toml index 2ce3a67..7088ee0 100644 --- a/crates/rectree/Cargo.toml +++ b/crates/rectree/Cargo.toml @@ -10,12 +10,4 @@ categories = ["gui", "data-structures", "no-std"] readme = "README.md" [dependencies] -sparse_map.workspace = true -hashbrown.workspace = true -kurbo.workspace = true bitflags.workspace = true - -[features] -default = ["std"] -std = ["kurbo/std"] -libm = ["kurbo/libm"] diff --git a/crates/rectree/README.md b/crates/rectree/README.md index 02a9a6d..fb37933 100644 --- a/crates/rectree/README.md +++ b/crates/rectree/README.md @@ -23,24 +23,132 @@ Rectree is designed to be: ## Core Concepts -- `Rectree`: a hierarchical tree of rectangular nodes. -- `Constraint`: size limitations flowing *from parent to child*. -- `Size`: resolved dimensions flowing *from child to parent*. -- `LayoutSolver`: user-defined logic that computes constraints, - sizes, and relative translations. +| Type / Trait | Role | +| ----------------- | ---------------------------------------------- | +| [`Rectree`] | tree structure and per-node layout logic | +| [`RectNodes`] | flat mutable storage for per-node numbers | +| [`NodeContext`] | restricted build-time view of `RectNodes` | +| [`RectNode`] | per-node data (size, constraint, translation) | +| [`NodeState`] | bitflags that short-circuit incremental passes | +| [`Constraint`] | min/max size bounds, flowing top-down | +| [`Size`] | resolved dimensions, flowing bottom-up | Rectree itself does not impose a specific layout style (e.g. flexbox, grid). Instead, it provides a strict data-flow model on top of which layout algorithms can be built. +## Three-Pass Layout + +Each call to `layout` runs three passes in order: + +1. **constrain** (top-down): each node derives the constraint it + passes to its children via `Rectree::constrain`. +2. **build** (bottom-up): each node measures itself given its + constraint and child sizes via `Rectree::build`. +3. **propagate_translation** (top-down): local translations set + during build are accumulated into world-space positions. + +Each pass is short-circuited by `NodeState` flags so only nodes +that actually changed are reprocessed. + ### Layout Rules -1. The only data that can flow down the tree is `Constraint`. -2. The only data that can flow up the tree is `Size`. -3. Each child, no matter the order, will recieve the same `Constraint` - from the parent. -4. Given the same `Constraint`, an unmodified node must always - produce the same `Size`. +1. Constraints flow strictly top-down (parent to child). +2. Sizes flow strictly bottom-up (child to parent). +3. A parent may pass a different constraint to each child. +4. Given the same constraint, an unmodified node must always + produce the same size. +5. The build pass must not write child sizes, only child + translations. This is enforced by `NodeContext`. + +## Example + +```rust +use std::collections::HashMap; +use rectree::{ + Constraint, NodeContext, RectNode, RectNodes, + Rectree, Size, layout, +}; + +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +struct Id(u32); + +// Flat node storage backed by a HashMap. +struct Store(HashMap>); + +impl RectNodes for Store { + type Id = Id; + + fn get_node(&self, id: &Id) -> Option<&RectNode> { + self.0.get(id) + } + + fn get_node_mut(&mut self, id: &Id) + -> Option<&mut RectNode> + { + self.0.get_mut(id) + } +} + +// Tree with one root and one child. The root fills its +// constraint; the child has a fixed 100x50 size. +struct Tree { root: Id, child: Id } + +impl Rectree for Tree { + type Id = Id; + type Nodes = Store; + + fn for_each_child( + &self, + id: &Id, + _nodes: &mut Store, + mut f: impl FnMut(&Id, &mut Store), + ) { + if *id == self.root { + f(&self.child, _nodes); + } + } + + // Pass the parent constraint to children unchanged. + fn constrain(&self, _: &Id, _nodes: &Store, parent: Constraint) + -> Constraint + { + parent + } + + fn build( + &self, + id: &Id, + constraint: Constraint, + _nodes: &mut Self::Nodes, + ) -> Size { + if *id == self.child { + Size::new(100.0, 50.0) + } else { + constraint.max + } + } +} + +let root = Id(0); +let child = Id(1); +let tree = Tree { root, child }; + +let mut store = Store(HashMap::new()); +store.0.insert(root, RectNode::new(None)); +store.0.insert(child, RectNode::new(Some(root))); + +// Constrain the root to an 800x600 window. +store.0.get_mut(&root).unwrap().constraint = + Constraint::tight(Size::new(800.0, 600.0)); + +layout(&tree, &mut store, &root); + +// Child is placed at the origin by default. +assert_eq!(store.0[&child].world_translation.x, 0.0); +assert_eq!(store.0[&child].world_translation.y, 0.0); +assert_eq!(store.0[&child].size, Size::new(100.0, 50.0)); +``` ## Join the community! @@ -55,4 +163,3 @@ You can join us on the [Voxell discord server](https://discord.gg/Mhnyp6VYEQ). This means you can select the license you prefer! This dual-licensing approach is the de-facto standard in the Rust ecosystem and there are [very good reasons](https://github.com/bevyengine/bevy/issues/2373) to include both. - diff --git a/crates/rectree/src/geom.rs b/crates/rectree/src/geom.rs new file mode 100644 index 0000000..3e4cd56 --- /dev/null +++ b/crates/rectree/src/geom.rs @@ -0,0 +1,201 @@ +/// A 2D size in resolved pixels. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct Size { + pub width: f32, + pub height: f32, +} + +impl Size { + /// Zero size on both axes. + pub const ZERO: Self = Self::splat(0.0); + + /// Infinite size on both axes. + pub const INFINITY: Self = Self::splat(f32::INFINITY); + + /// Creates a `Size` from explicit `width` and `height`. + #[inline] + pub const fn new(width: f32, height: f32) -> Self { + Self { width, height } + } + + /// Creates a `Size` with the same value on both axes. + #[inline] + pub const fn splat(value: f32) -> Self { + Self::new(value, value) + } +} + +/// A 2D position or translation vector. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct Vec2 { + pub x: f32, + pub y: f32, +} + +impl Vec2 { + /// Zero vector on both axes. + pub const ZERO: Self = Self::splat(0.0); + + /// Creates a `Vec2` from explicit `x` and `y`. + #[inline] + pub const fn new(x: f32, y: f32) -> Self { + Self { x, y } + } + + /// Creates a `Vec2` with the same value on both axes. + #[inline] + pub const fn splat(value: f32) -> Self { + Self::new(value, value) + } +} + +impl core::ops::Add for Vec2 { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self::new(self.x + rhs.x, self.y + rhs.y) + } +} + +/// Min/max size bounds passed top-down through the layout tree. +/// +/// A `Constraint` tells a node how large it is allowed to be. +/// The node measures itself within these bounds and returns a +/// [`Size`]. +/// +/// # Axis independence +/// +/// Each axis is constrained independently. Setting `max` to +/// [`f32::INFINITY`] leaves that axis unconstrained (see +/// [`Self::unbounded`], [`Self::fixed_width`], +/// [`Self::fixed_height`]). +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Constraint { + pub min: Size, + pub max: Size, +} + +impl Constraint { + /// The node must be exactly `size`, with no flexibility. + pub const fn tight(size: Size) -> Self { + Self { + min: size, + max: size, + } + } + + /// The node may choose any size from zero up to `max`. + pub const fn loose(max: Size) -> Self { + Self { + min: Size::ZERO, + max, + } + } + + /// No bounds on either axis; the node may be any size. + /// + /// This is the [`Default`] value for `Constraint`, used for + /// root nodes that have no parent imposing bounds. + pub const fn unbounded() -> Self { + Self { + min: Size::ZERO, + max: Size::INFINITY, + } + } + + /// Bounded width, unbounded height. + /// + /// Use this for vertical scroll containers. + pub const fn fixed_width(width: f32) -> Self { + Self { + min: Size::ZERO, + max: Size { + width, + height: f32::INFINITY, + }, + } + } + + /// Bounded height, unbounded width. + /// + /// Use this for horizontal scroll containers. + pub const fn fixed_height(height: f32) -> Self { + Self { + min: Size::ZERO, + max: Size { + width: f32::INFINITY, + height, + }, + } + } + + /// Clamps `size` so it satisfies this constraint. + /// + /// Each axis is clamped independently to `[min, max]`. Use + /// this at the end of a [`crate::Rectree::build`] + /// implementation to ensure the returned [`Size`] respects + /// the constraint. + pub const fn constrain(&self, size: Size) -> Size { + Size { + width: size.width.max(self.min.width).min(self.max.width), + height: size + .height + .max(self.min.height) + .min(self.max.height), + } + } +} + +impl Default for Constraint { + /// Returns [`Self::unbounded`], the default for root nodes + /// that have no parent imposing bounds. + fn default() -> Self { + Self::unbounded() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_constrain_passes_through_within_bounds() { + let c = Constraint::loose(Size::splat(100.0)); + assert_eq!(c.constrain(Size::splat(50.0)), Size::splat(50.0)); + } + + #[test] + fn test_constrain_clamps_below_min() { + let c = Constraint { + min: Size::splat(10.0), + max: Size::splat(100.0), + }; + assert_eq!(c.constrain(Size::splat(5.0)), Size::splat(10.0)); + } + + #[test] + fn test_constrain_clamps_above_max() { + let c = Constraint::loose(Size::splat(100.0)); + assert_eq!( + c.constrain(Size::splat(200.0)), + Size::splat(100.0) + ); + } + + #[test] + fn test_constrain_tight_forces_exact_size() { + let c = Constraint::tight(Size::new(30.0, 20.0)); + assert_eq!(c.constrain(Size::ZERO), Size::new(30.0, 20.0)); + assert_eq!( + c.constrain(Size::INFINITY), + Size::new(30.0, 20.0) + ); + } + + #[test] + fn test_constrain_unbounded_passes_through() { + let c = Constraint::unbounded(); + let s = Size::new(999.0, 999.0); + assert_eq!(c.constrain(s), s); + } +} diff --git a/crates/rectree/src/layout.rs b/crates/rectree/src/layout.rs deleted file mode 100644 index 3e92cdf..0000000 --- a/crates/rectree/src/layout.rs +++ /dev/null @@ -1,297 +0,0 @@ -use alloc::collections::btree_set::BTreeSet; -use alloc::vec; -use alloc::vec::Vec; -use kurbo::{Size, Vec2}; - -use crate::node::RectNode; -use crate::{NodeId, Rectree}; - -/// Layout execution. -impl Rectree { - /// Check if we need to call [`Self::layout()`]. - pub fn needs_relayout(&self) -> bool { - !self.scheduled_relayout.is_empty() - } - - /// Schedules a node for relayout. - /// - /// Returns `true` if the node was newly scheduled, or `false` - /// if the node does not exist or was already scheduled. - pub fn schedule_relayout(&mut self, id: NodeId) -> bool { - if let Some(node) = self.nodes.get_mut(&id) { - node.state.reset(); - return self - .scheduled_relayout - .insert(DepthNode::new(node.depth, id)); - } - - false - } - - /// Executes the layout pass using the provided [`LayoutWorld`]. - pub fn layout(&mut self, world: &W) - where - W: LayoutWorld, - { - let scheduled_relayout = - core::mem::take(&mut self.scheduled_relayout); - let mut child_stack = Vec::::new(); - let mut build_stack = BTreeSet::::new(); - - for DepthNode { id, .. } in scheduled_relayout.iter() { - let Some(node) = self.try_get_mut(id) else { - continue; - }; - // Check constrain flag, if it has already been - // constrained, skip the entire process. - if node.state.constrained() { - continue; - } - - child_stack.push(*id); - - // Recursively propagate constraint from parent to child. - while let Some(id) = child_stack.pop() { - let node = self.get(&id); - let solver = world.get_solver(&id); - let constraint = - solver.constraint(node.parent_constraint); - - self.nodes.scope(&id, |nodes, node| { - node.state.has_recontrained(); - - for child in node.children() { - let child_node = - Self::get_node_mut(nodes, child); - - // Skip if constraint is still the same. - if child_node.parent_constraint != constraint - { - child_node.parent_constraint = constraint; - child_stack.push(*child); - } - } - }); - - let node = self.get_mut(&id); - node.state.needs_rebuild(); - build_stack.insert(DepthNode::new(node.depth, id)); - } - } - - let mut positioner = Positioner::default(); - let mut translation_stack = scheduled_relayout; - - // Propagate size from child to parent. - while let Some(DepthNode { id, .. }) = build_stack.pop_last() - { - let solver = world.get_solver(&id); - let size = - solver.build(self.get(&id), self, &mut positioner); - positioner.apply(self); - - self.nodes.scope(&id, |nodes, node| { - node.state.has_rebuilt(); - // Parent needs to be rebuilt if size changes. - if node.size != size { - if let Some(parent) = node.parent { - let parent_node = - Self::get_node_mut(nodes, &parent); - // Insert only if parent node is not already set to - // be rebuilt. - if parent_node.state.built() { - parent_node.state.needs_reposition(); - parent_node.state.needs_rebuild(); - - let depth_node = DepthNode::new( - parent_node.depth, - parent, - ); - translation_stack.insert(depth_node); - build_stack.insert(depth_node); - } - } - node.size = size; - } - }); - } - - // Propagate translations from parent to child. - for DepthNode { id, .. } in translation_stack.into_iter() { - let node = self.get(&id); - - // Translation could have already been resolved by a - // previous iteration. - if node.state.positioned() { - continue; - } - - self.propagate_translation(id); - } - } - - /// Propagates world-space translations starting from a node. - /// - /// This updates the node’s world translation and recursively - /// applies it to all descendants, clearing translation mutation - /// flags in the process. - fn propagate_translation(&mut self, id: NodeId) { - let mut node_stack = vec![(id, 0)]; - let mut translation_stack = vec![Vec2::ZERO]; - - while let Some((id, index)) = node_stack.pop() { - let node = self.get_mut(&id); - - node.world_translation = - node.translation + translation_stack[index]; - - // This node is now positioned since the world - // translation has been updated. - node.state.has_repositioned(); - - let new_index = translation_stack.len(); - translation_stack.push(node.world_translation); - - for child in node.children.iter() { - node_stack.push((*child, new_index)); - } - } - } -} - -/// Provides access to layout solvers associated with nodes. -/// -/// Acts as the bridge between [`Rectree`] and layout logic, allowing -/// each node to be resolved by an external [`LayoutSolver`]. -pub trait LayoutWorld { - /// Returns the [`LayoutSolver`] responsible for computing layout - /// for the given [`NodeId`]. - fn get_solver(&self, id: &NodeId) -> &dyn LayoutSolver; -} - -/// Defines how a node participates in layout resolution. -/// -/// A `LayoutSolver` is responsible for: -/// - Propagating constraints from parent to children (top-down). -/// - Computing the node’s final size (bottom-up). -/// - Positioning child nodes relative to the parent. -pub trait LayoutSolver { - /// Computes the constraint to be applied to this node. - /// - /// By default, the parent’s constraint is forwarded unchanged. - /// Implementations may tighten, relax, or otherwise transform the - /// constraint before it is used during layout. - fn constraint( - &self, - parent_constraint: Constraint, - ) -> Constraint { - parent_constraint - } - - /// Builds the layout for a node and returns its resolved size. - /// - /// This method is called during the layout pass after constraints - /// have been propagated. - /// - /// Implementations may: - /// - Inspect the node’s state and children via [`Rectree`]. - /// - Assign local translations to child nodes via - /// [`Positioner`]. - /// - /// All translations written through [`Positioner`] are relative - /// to the parent node. - fn build( - &self, - node: &RectNode, - tree: &Rectree, - positioner: &mut Positioner, - ) -> Size; -} - -/// Collects child translations produced during layout construction. -/// -/// See [`LayoutSolver::build()`]. -#[derive(Default)] -pub struct Positioner { - new_translations: Vec<(NodeId, Vec2)>, -} - -impl Positioner { - /// Sets the local translation for a node. - /// - /// The translation is recorded and applied later as part of the - /// layout commit phase. If multiple translations are set for the - /// same node, the last one wins. - pub fn set(&mut self, id: NodeId, translation: Vec2) { - self.new_translations.push((id, translation)); - } - - /// Applies all recorded translations to the [`Rectree`]. - /// - /// This is called internally after layout resolution to commit - /// the results of [`LayoutSolver::build()`]. - fn apply(&mut self, tree: &mut Rectree) { - for (id, translation) in self.new_translations.drain(..) { - tree.get_mut(&id).translation = translation; - } - } -} - -/// [`NodeId`] cache with depth as the primary value for sorting. -#[derive( - Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, -)] -pub struct DepthNode { - depth: u32, - id: NodeId, -} - -impl DepthNode { - pub fn new(depth: u32, id: NodeId) -> Self { - Self { depth, id } - } -} - -/// Size constraints applied to a node during layout. -/// -/// A value of `Some(f64)` fixes the corresponding dimension to an -/// explicit size, while `None` indicates that the dimension is -/// unconstrained (flexible) and may be determined by layout. -#[derive(Default, Debug, Clone, Copy, PartialEq)] -pub struct Constraint { - // Fixed width constraint, or `None` if flexible. - pub width: Option, - // Fixed height constraint, or `None` if flexible. - pub height: Option, -} - -impl Constraint { - /// Create a constraint with both width and height fixed. - pub fn fixed(width: f64, height: f64) -> Self { - Self { - width: Some(width), - height: Some(height), - } - } - - /// Create a constraint with a fixed width and flexible height. - pub fn fixed_width(width: f64) -> Self { - Self { - width: Some(width), - height: None, - } - } - - /// Create a constraint with a fixed height and flexible width. - pub fn fixed_height(height: f64) -> Self { - Self { - width: None, - height: Some(height), - } - } - - /// Create a fully flexible constraint with no fixed dimensions. - pub fn flexible() -> Self { - Self::default() - } -} diff --git a/crates/rectree/src/lib.rs b/crates/rectree/src/lib.rs index 0807161..f12db47 100644 --- a/crates/rectree/src/lib.rs +++ b/crates/rectree/src/lib.rs @@ -3,218 +3,794 @@ extern crate alloc; -use core::fmt::{Display, Formatter}; -use core::ops::Deref; +pub use geom::{Constraint, Size, Vec2}; +pub use node::{NodeState, RectNode}; -use alloc::collections::btree_set::BTreeSet; -use alloc::vec; -use hashbrown::HashSet; -use sparse_map::{Key, SparseMap}; +pub mod geom; +pub mod node; -use crate::layout::DepthNode; -use crate::node::RectNode; +/// Tree structure and per-node layout logic for a rectree hierarchy. +/// +/// `Rectree` is the read-only half of the layout split. It defines +/// how nodes are connected ([`Self::for_each_child`]) and how each node +/// computes its constraint ([`Self::constrain`]) and size +/// ([`Self::build`]). The mutable per-node data lives separately in +/// [`RectNodes`]. +pub trait Rectree { + type Id; + type Nodes: NodeContext; -pub use kurbo; + /// Calls `f` for each direct child of `id` in layout order. + /// + /// `nodes` is threaded through to the closure so that + /// implementations that dispatch through per-node metadata + /// (e.g. a type-erased tree) can read it without a separate + /// borrow that would conflict with the `&mut Self::Nodes` held + /// by the calling layout pass. + fn for_each_child( + &self, + id: &Self::Id, + nodes: &mut Self::Nodes, + f: impl FnMut(&Self::Id, &mut Self::Nodes), + ); -pub mod layout; -pub mod node; + /// Derives the constraint this node passes to its children + /// from the constraint `parent` imposed on this node. + /// + /// Most nodes return `parent` unchanged (pass-through). A + /// padding container would subtract its insets; a fixed-size + /// container would ignore `parent` and return a tight + /// constraint. + /// + /// `nodes` is a shared view of the node storage, available for + /// implementations that store per-node metadata (such as a + /// type tag) inside the nodes map rather than in the tree. + /// + /// Called top-down by [`constrain`]. + fn constrain( + &self, + id: &Self::Id, + nodes: &Self::Nodes, + parent: Constraint, + ) -> Constraint; -/// A hierarchical tree of rectangular layout nodes. -/// -/// `Rectree` maintains parent–child relationships between [`RectNode`]s, -/// supports multiple root nodes, and provides stable [`NodeId`]s for -/// insertion, lookup, and removal. -/// -/// The tree owns all nodes and ensures structural consistency when -/// inserting or removing subtrees. -#[derive(Default, Debug)] -pub struct Rectree { - /// Identifiers of all root nodes (nodes without a parent). - root_ids: HashSet, - /// Storage for all nodes in the tree, indexed by [`NodeId`]. + /// Measures this node given `constraint` and the already-built + /// children, returning the node's resolved [`Size`]. /// - /// This uses a sparse map to provide stable identifiers while - /// allowing efficient insertion and removal. - nodes: SparseMap, - /// Nodes scheduled for relayout, ordered by depth. + /// Children are guaranteed to be fully built before this is + /// called (bottom-up ordering). The implementation may: /// - /// Deeper nodes are processed first to ensure children are laid - /// out before their parents. - scheduled_relayout: BTreeSet, + /// - Read child sizes via `nodes.get_size(child_id)`. + /// - Write child local translations via + /// `nodes.set_translation(child_id, pos)`. + /// + /// It must not mutate child sizes. `nodes` is a [`NodeContext`] + /// which intentionally limits access to reads and translation + /// writes only. + /// + /// Called bottom-up by [`build`]. + fn build( + &self, + id: &Self::Id, + constraint: Constraint, + nodes: &mut Self::Nodes, + ) -> Size; } -/// Builders. -impl Rectree { - /// Creates an empty [`Rectree`]. - /// - /// This is equivalent to calling [`Default::default`]. - pub fn new() -> Self { - Self::default() +/// Flat storage for [`RectNode`]s keyed by an application-defined +/// `Id`. +/// +/// This is the mutable half of the layout split. It holds only +/// per-node numbers (`constraint`, `size`, `translation`) and +/// exposes them to the rectree free functions. It knows nothing +/// about tree structure or layout logic; those live in [`Rectree`]. +/// +/// Any type that implements `RectNodes` automatically implements +/// [`NodeContext`] through a blanket impl. +/// +/// # Splitting storage from tree logic +/// +/// rectree's free functions take `tree: &T` and `nodes: &mut N` +/// as two separate arguments. This lets Rust borrow `T` immutably +/// (for traversal and logic) and `N` mutably (for data writes) at +/// the same time, which would be impossible if a single type owned +/// both. +pub trait RectNodes { + type Id; + + fn get_node(&self, id: &Self::Id) -> Option<&RectNode>; + + fn get_node_mut( + &mut self, + id: &Self::Id, + ) -> Option<&mut RectNode>; +} + +/// Blanket impl: any [`RectNodes`] storage is automatically a +/// [`NodeContext`]. +/// +/// This means you never implement `NodeContext` by hand. Just +/// implement `RectNodes` and the restricted build-time view +/// comes for free. +impl NodeContext for N { + type Id = N::Id; + + fn get_size(&self, id: &Self::Id) -> Size { + self.get_node(id).map(|n| n.size).unwrap_or(Size::ZERO) + } + + fn set_translation(&mut self, id: &Self::Id, translation: Vec2) { + if let Some(n) = self.get_node_mut(id) { + n.translation = translation; + } } +} + +/// Restricted view of [`RectNodes`] exposed to [`Rectree::build`]. +/// +/// During the build pass, a widget must be able to: +/// +/// - Read the resolved [`Size`] of its children (`get_size`). +/// - Write local translations to position its children +/// (`set_translation`). +/// +/// It must not mutate child sizes directly, because the build +/// pass processes nodes bottom-up and a size written here would +/// silently invalidate the ordering guarantee. +/// +/// `NodeContext` is never implemented manually. Any type that +/// implements [`RectNodes`] gets `NodeContext` for free through +/// a blanket impl in `lib.rs`. +pub trait NodeContext { + type Id; - /// Inserts a node into the tree while keeping track of the - /// parent-child relationship. + /// Returns the resolved size of the node identified by `id`. /// - /// # Panics + /// Returns [`Size::ZERO`] if the id is not found. + fn get_size(&self, id: &Self::Id) -> Size; + + /// Sets the local translation of the node identified by `id`. /// - /// Panics if an invalid parent [`NodeId`] is used. - pub fn insert(&mut self, mut node: RectNode) -> NodeId { - let key = self.nodes.insert_with_key(|nodes, key| { - let id = NodeId(key); - if let Some(parent) = node.parent { - let parent_node = - nodes.get_mut(&parent).unwrap_or_else(|| { - panic!("Invalid parent Id ({parent}).") - }); - - parent_node.children.insert(id); - node.depth = parent_node.depth + 1; - } else { - // No parent, meaning that it's a root id. - self.root_ids.insert(id); - } + /// This is the position relative to the parent's origin. + /// [`propagate_translation`] later accumulates these + /// into absolute world positions. + fn set_translation(&mut self, id: &Self::Id, position: Vec2); +} - self.scheduled_relayout - .insert(DepthNode::new(node.depth, id)); +/// Runs a full layout cycle on the subtree rooted at `id`. +/// +/// Executes the three passes in order: +/// +/// 1. [`constrain`] (top-down): propagates constraints from +/// parent to children. +/// 2. [`build`] (bottom-up): measures nodes and writes child +/// translations. +/// 3. [`propagate_translation`] (top-down): accumulates local +/// translations into absolute `world_translation` values. +/// +/// Each pass is short-circuited by [`NodeState`] flags so only +/// nodes that actually changed are reprocessed. To force a full +/// re-layout of the subtree, reset the root node's state before +/// calling: +/// +/// ```rust,ignore +/// nodes.get_node_mut(&root_id).unwrap().state.reset(); +/// layout(&tree, &mut nodes, &root_id); +/// ``` +/// +/// If the node's size changes and it has a parent, the parent +/// and ancestors are re-measured via an upward rebuild pass +/// before translation is propagated. +/// +/// # Panics +/// +/// Panics if `id` is not present in `nodes`. +pub fn layout< + Id: Copy, + T: Rectree, + N: RectNodes, +>( + tree: &T, + nodes: &mut N, + id: &Id, +) { + let node = nodes.get_node(id).expect("Id is invalid!"); - node - }); + let old_size = node.size; + let parent = node.parent_id; - NodeId(key) + if node.state.is_ready() { + return; } - /// Removes a node and all of its descendants from the tree. - /// - /// Returns `true` if the node existed and was removed, or `false` - /// if the given [`NodeId`] does not exist. - pub fn remove(&mut self, id: &NodeId) -> bool { - if let Some(node) = self.nodes.get(id) { - if let Some(parent) = - node.parent.and_then(|id| self.nodes.get_mut(&id)) - { - // Bookeeping. - parent.children.remove(id); - } else { - // No parent, meaning that it's a root id. - self.root_ids.remove(id); - } + // 1. Constrain down the hierarchy. + constrain(tree, nodes, id, node.constraint); - self.remove_recursive(id); - return true; - } + // 2. Build sizes up the hierarchy. + build(tree, nodes, id); - false + let new_size = nodes.get_size(id); + + // Size changed; propagate upward without re-traversing children. + let mut bubbled_id = *id; + if new_size != old_size + && let Some(ref parent_id) = parent + { + bubbled_id = build_up(tree, nodes, parent_id); } - /// Recursively removes a node and all of its descendants. - /// - /// This is an internal helper used by [`Self::remove()`]. - /// It assumes that any necessary parent bookkeeping has already - /// been handled. - fn remove_recursive(&mut self, id: &NodeId) { - let mut child_stack = vec![*id]; + // 3. Propagate translation. + let parent_world = nodes + .get_node(&bubbled_id) + .expect("Id is invalid!") + .world_translation; + propagate_translation(tree, nodes, &bubbled_id, parent_world); +} + +/// Propagates a constraint top-down through the subtree rooted +/// at `id`. +/// +/// `parent` is the constraint imposed on this node by its +/// parent. It is stored on the node then narrowed via +/// [`Rectree::constrain`] to produce the constraint passed to +/// children. +/// +/// # Short-circuit behaviour +/// +/// If the node already has the [`NodeState::CONSTRAINED`] flag +/// set and the incoming constraint is unchanged, the entire +/// subtree is skipped. Otherwise the flag is set, the stored +/// constraint is updated, and propagation continues to children. +/// +/// When the constraint changes, the [`NodeState::BUILT`] flag is +/// also cleared so the subsequent [`build`] pass re-measures the +/// node. +/// +/// # Panics +/// +/// Panics if `id` is not present in `nodes`. +pub fn constrain< + Id, + T: Rectree, + N: RectNodes, +>( + tree: &T, + nodes: &mut N, + id: &T::Id, + parent: Constraint, +) { + let node = nodes.get_node(id).expect("Id is invalid!"); + + let old_constraint = node.constraint; + let constraint_unchanged = parent == old_constraint; - while let Some(id) = child_stack.pop() { - let node = self.get(&id); + if let Some(n) = nodes.get_node_mut(id) { + // Skip the subtree if the constraint stays the same. + if n.state.is_constrained() && constraint_unchanged { + return; + } + + n.state.has_reconstrained(); - child_stack.extend(node.children()); - self.nodes.remove(&id); + n.constraint = parent; + // Constraint changed means the built size is now stale. + if !constraint_unchanged { + n.state.needs_rebuild(); } } + + // Derive this node's constraint from the parent's. + let constraint = tree.constrain(id, nodes, parent); + + // Propagate the resolved constraint down to children. + tree.for_each_child(id, nodes, |child, nodes| { + constrain(tree, nodes, child, constraint); + }); } -/// Node retrieval. -impl Rectree { - /// Returns an immutable reference to a node if it exists. - pub fn try_get(&self, id: &NodeId) -> Option<&RectNode> { - self.nodes.get(id) +/// Recursively builds the layout tree bottom-up. +/// +/// Children are built before their parent so that each parent +/// can read child sizes when computing its own size. After +/// measuring, the node's [`NodeState::BUILT`] flag is set and +/// its [`NodeState::POSITIONED`] flag is cleared because a new +/// size may require new child translations. +/// +/// # Short-circuit behaviour +/// +/// If the node's [`NodeState::BUILT`] flag is already set, the +/// entire subtree is skipped - its sizes and child translations +/// are still current. +/// +/// # Panics +/// +/// Panics if `id` is not present in `nodes`. +pub fn build< + Id, + T: Rectree, + N: RectNodes, +>( + tree: &T, + nodes: &mut N, + id: &T::Id, +) { + let node = nodes.get_node(id).expect("Id is invalid!"); + + // Already up-to-date; skip this entire subtree. + if node.state.is_built() { + return; } - /// Returns a mutable reference to a node if it exists. - fn try_get_mut(&mut self, id: &NodeId) -> Option<&mut RectNode> { - self.nodes.get_mut(id) + let constraint = node.constraint; + + tree.for_each_child(id, nodes, |child, nodes| { + build(tree, nodes, child); + }); + + // All children are now measured; measure self. + let size = tree.build(id, constraint, nodes); + + if let Some(n) = nodes.get_node_mut(id) { + n.size = size; + n.state.needs_reposition(); + n.state.has_rebuilt(); } +} - /// Returns an immutable reference to a node. - /// - /// # Panics - /// - /// Panics if the given [`NodeId`] does not exist in the tree. - pub fn get(&self, id: &NodeId) -> &RectNode { - self.try_get(id).unwrap_or_else(|| { - panic!("{id} does not exists in tree.") - }) +/// Re-measures a single node and walks upward if its size +/// changed. +/// +/// Called after a child's size change has already been +/// recorded. Unlike [`build`], it does not recurse into +/// children. It assumes their sizes in `nodes` are current +/// and re-invokes [`Rectree::build`] on `id` to let it +/// re-measure from the current child sizes. +/// +/// If the resulting size differs from the previous one, the +/// parent is re-measured recursively until the size stabilises +/// or a root is reached. Returns the highest node that was +/// re-measured, used as the start for [`propagate_translation`]. +/// +/// # Panics +/// +/// Panics if `id` is not present in `nodes`. +pub fn build_up< + Id: Copy, + T: Rectree, + N: RectNodes, +>( + tree: &T, + nodes: &mut N, + id: &T::Id, +) -> Id { + let node = nodes.get_node(id).expect("Id is invalid!"); + + let constraint = node.constraint; + let old_size = node.size; + let parent = nodes.get_node(id).and_then(|n| n.parent_id); + + let size = tree.build(id, constraint, nodes); + + if let Some(n) = nodes.get_node_mut(id) { + n.size = size; + n.state.needs_reposition(); + n.state.has_rebuilt(); } - /// Returns a mutable reference to a node. - /// - /// # Panics - /// - /// Panics if the given [`NodeId`] does not exist in the tree. - fn get_mut(&mut self, id: &NodeId) -> &mut RectNode { - self.try_get_mut(id).unwrap_or_else(|| { - panic!("{id} does not exists in tree.") - }) + if size != old_size + && let Some(ref parent_id) = parent + { + return build_up(tree, nodes, parent_id); } - /// Returns the set of root node identifiers. - /// - /// Root nodes are nodes that do not have a parent. - pub fn root_ids(&self) -> &HashSet { - &self.root_ids + *id +} + +/// Propagates world-space translations top-down through the +/// subtree. +/// +/// `parent_world` is the absolute world translation of `id`'s +/// parent (use [`Vec2::ZERO`] for root nodes). For each node +/// the world translation is computed as +/// `parent_world + node.translation` and stored in +/// `node.world_translation`. +/// +/// # Short-circuit behaviour +/// +/// If the node's [`NodeState::POSITIONED`] flag is already set, +/// the node and its entire subtree are skipped - their world +/// translations are still current. +/// +/// # Panics +/// +/// Panics if `id` is not present in `nodes`. +pub fn propagate_translation< + Id, + T: Rectree, + N: RectNodes, +>( + tree: &T, + nodes: &mut N, + id: &T::Id, + parent_world: Vec2, +) { + let node = nodes.get_node(id).expect("Id is invalid!"); + + // Already up-to-date; skip this entire subtree. + if node.state.is_positioned() { + return; } - /// Returns an immutable reference to a node. - /// - /// This is a workaround for [`Self::get()`] due to lifetime - /// constraints. - /// - /// # Panics - /// - /// Panics if the given [`NodeId`] does not exist in the tree. - #[expect(dead_code)] - fn get_node<'a>( - nodes: &'a SparseMap, - id: &NodeId, - ) -> &'a RectNode { - nodes.get(id).unwrap_or_else(|| { - panic!("{id} does not exists in tree.") - }) - } - - /// Returns a mutable reference to a node. - /// - /// This is a workaround for [`Self::get_mut()`] due to lifetime - /// constraints. - /// - /// # Panics - /// - /// Panics if the given [`NodeId`] does not exist in the tree. - fn get_node_mut<'a>( - nodes: &'a mut SparseMap, - id: &NodeId, - ) -> &'a mut RectNode { - nodes.get_mut(id).unwrap_or_else(|| { - panic!("{id} does not exists in tree.") - }) + let world = parent_world + node.translation; + + if let Some(n) = nodes.get_node_mut(id) { + n.world_translation = world; + n.state.has_repositioned(); } + + tree.for_each_child(id, nodes, |child, nodes| { + propagate_translation(tree, nodes, child, world); + }); } -#[derive( - Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, -)] -pub struct NodeId(Key); +#[cfg(test)] +mod tests { + use alloc::collections::BTreeMap; + use alloc::vec; + use alloc::vec::Vec; + use core::cell::Cell; + + use super::*; + + #[test] + fn test_layout_full_pass() { + let mut tree = WidgetTree::default(); + + tree.add_column(0, None, vec![1, 2]); + tree.add_fixed(1, Some(0), Size::new(10.0, 10.0)); + tree.add_fixed(2, Some(0), Size::new(20.0, 5.0)); + + tree.layout(&0); + + // 3 constrains + 3 builds + 3 translation propagations. + assert_eq!(tree.tree.for_each_child_calls.get(), 9); + assert_eq!(tree.tree.constrain_calls.get(), 3); + assert_eq!(tree.tree.build_calls.get(), 3); + + assert_eq!(tree.nodes.0[&1].size, Size::new(10.0, 10.0)); + assert_eq!(tree.nodes.0[&2].size, Size::new(20.0, 5.0)); + // Width = max(10, 20) = 20; Height = 10 + 5 = 15. + assert_eq!(tree.nodes.0[&0].size, Size::new(20.0, 15.0)); + + assert!(tree.nodes.0[&0].state.is_ready()); + assert!(tree.nodes.0[&1].state.is_ready()); + assert!(tree.nodes.0[&2].state.is_ready()); -impl Deref for NodeId { - type Target = Key; + let fec = tree.tree.for_each_child_calls.get(); + let cc = tree.tree.constrain_calls.get(); + let bc = tree.tree.build_calls.get(); - fn deref(&self) -> &Self::Target { - &self.0 + // Further layouts should have nothing rebuilt. + tree.layout(&0); + tree.layout(&1); + tree.layout(&2); + + assert_eq!(tree.tree.for_each_child_calls.get(), fec); + assert_eq!(tree.tree.constrain_calls.get(), cc); + assert_eq!(tree.tree.build_calls.get(), bc); } -} -impl Display for NodeId { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - f.write_fmt(format_args!("NodeId({})", self.0)) + #[test] + fn test_constrain_stores_constraint() { + let mut wt = WidgetTree::default(); + wt.add_fixed(0, None, Size::splat(10.0)); + + let c = Constraint::tight(Size::splat(100.0)); + constrain(&wt.tree, &mut wt.nodes, &0, c); + + assert_eq!(wt.nodes.0[&0].constraint, c); + assert!(wt.nodes.0[&0].state.is_constrained()); + } + + #[test] + fn test_constrain_propagates_to_children() { + let mut wt = WidgetTree::default(); + wt.add_column(0, None, vec![1]); + wt.add_fixed(1, Some(0), Size::splat(10.0)); + + let c = Constraint::loose(Size::splat(100.0)); + constrain(&wt.tree, &mut wt.nodes, &0, c); + + // Column passes constraint through unchanged. + assert_eq!(wt.nodes.0[&1].constraint, c); + assert_eq!(wt.tree.constrain_calls.get(), 2); + } + + #[test] + fn test_constrain_short_circuits_if_unchanged() { + let mut wt = WidgetTree::default(); + wt.add_column(0, None, vec![1]); + wt.add_fixed(1, Some(0), Size::splat(10.0)); + + let c = Constraint::loose(Size::splat(100.0)); + constrain(&wt.tree, &mut wt.nodes, &0, c); + let calls = wt.tree.constrain_calls.get(); + + // Same constraint: entire subtree is skipped. + constrain(&wt.tree, &mut wt.nodes, &0, c); + assert_eq!(wt.tree.constrain_calls.get(), calls); + } + + #[test] + fn test_constrain_change_clears_built() { + let mut wt = WidgetTree::default(); + wt.add_fixed(0, None, Size::splat(10.0)); + + constrain( + &wt.tree, + &mut wt.nodes, + &0, + Constraint::loose(Size::splat(100.0)), + ); + build(&wt.tree, &mut wt.nodes, &0); + assert!(wt.nodes.0[&0].state.is_built()); + + // A different constraint must clear the BUILT flag. + constrain( + &wt.tree, + &mut wt.nodes, + &0, + Constraint::loose(Size::splat(200.0)), + ); + assert!(!wt.nodes.0[&0].state.is_built()); + } + + #[test] + fn test_build_sets_size() { + let mut wt = WidgetTree::default(); + wt.add_fixed(0, None, Size::new(30.0, 20.0)); + build(&wt.tree, &mut wt.nodes, &0); + + assert_eq!(wt.nodes.0[&0].size, Size::new(30.0, 20.0)); + assert!(wt.nodes.0[&0].state.is_built()); + } + + #[test] + fn test_build_column_sums_children() { + let mut wt = WidgetTree::default(); + wt.add_column(0, None, vec![1, 2]); + wt.add_fixed(1, Some(0), Size::new(10.0, 10.0)); + wt.add_fixed(2, Some(0), Size::new(20.0, 5.0)); + build(&wt.tree, &mut wt.nodes, &0); + + // Width = max(10, 20) = 20; Height = 10 + 5 = 15. + assert_eq!(wt.nodes.0[&0].size, Size::new(20.0, 15.0)); + } + + #[test] + fn test_build_short_circuits_if_built() { + let mut wt = WidgetTree::default(); + wt.add_fixed(0, None, Size::splat(10.0)); + build(&wt.tree, &mut wt.nodes, &0); + let calls = wt.tree.build_calls.get(); + + // Already built; no further calls. + build(&wt.tree, &mut wt.nodes, &0); + assert_eq!(wt.tree.build_calls.get(), calls); + } + + #[test] + fn test_propagate_translation_sets_world_pos() { + let mut wt = WidgetTree::default(); + wt.add_column(0, None, vec![1]); + wt.add_fixed(1, Some(0), Size::splat(10.0)); + + wt.nodes.0.get_mut(&1).unwrap().translation = + Vec2::new(10.0, 5.0); + propagate_translation( + &wt.tree, + &mut wt.nodes, + &0, + Vec2::ZERO, + ); + + assert_eq!(wt.nodes.0[&0].world_translation, Vec2::ZERO); + assert_eq!( + wt.nodes.0[&1].world_translation, + Vec2::new(10.0, 5.0), + ); + } + + #[test] + fn test_propagate_translation_accumulates() { + let mut wt = WidgetTree::default(); + wt.add_column(0, None, vec![1, 2]); + wt.add_fixed(1, Some(0), Size::new(10.0, 10.0)); + wt.add_fixed(2, Some(0), Size::new(20.0, 5.0)); + + // build positions children: node 1 at y=0, node 2 at y=10. + build(&wt.tree, &mut wt.nodes, &0); + propagate_translation( + &wt.tree, + &mut wt.nodes, + &0, + Vec2::ZERO, + ); + + assert_eq!(wt.nodes.0[&1].world_translation, Vec2::ZERO); + assert_eq!( + wt.nodes.0[&2].world_translation, + Vec2::new(0.0, 10.0), + ); + } + + #[test] + fn test_propagate_translation_short_circuits_if_positioned() { + let mut wt = WidgetTree::default(); + wt.add_column(0, None, vec![1]); + wt.add_fixed(1, Some(0), Size::splat(10.0)); + + propagate_translation( + &wt.tree, + &mut wt.nodes, + &0, + Vec2::ZERO, + ); + let calls = wt.tree.for_each_child_calls.get(); + + // All nodes POSITIONED; entire subtree is skipped. + propagate_translation( + &wt.tree, + &mut wt.nodes, + &0, + Vec2::ZERO, + ); + assert_eq!(wt.tree.for_each_child_calls.get(), calls); + } + + type Id = usize; + + /// Flat node storage backed by a [`BTreeMap`]. + #[derive(Default)] + struct Nodes(BTreeMap>); + + impl Nodes { + fn add(&mut self, id: Id, parent: Option) { + self.0.insert(id, RectNode::new(parent)); + } + } + + impl RectNodes for Nodes { + type Id = Id; + + fn get_node(&self, id: &Id) -> Option<&RectNode> { + self.0.get(id) + } + + fn get_node_mut( + &mut self, + id: &Id, + ) -> Option<&mut RectNode> { + self.0.get_mut(id) + } + } + + enum Widget { + Column(Vec), + Fixed(Size), + } + + #[derive(Default)] + struct Tree { + widgets: BTreeMap, + for_each_child_calls: Cell, + constrain_calls: Cell, + build_calls: Cell, + } + + impl Rectree for Tree { + type Id = Id; + type Nodes = Nodes; + + fn for_each_child( + &self, + id: &Id, + nodes: &mut Nodes, + mut f: impl FnMut(&Id, &mut Nodes), + ) { + self.for_each_child_calls + .set(self.for_each_child_calls.get() + 1); + + let Some(widget) = self.widgets.get(id) else { + return; + }; + + if let Widget::Column(children) = widget { + for child in children { + f(child, nodes); + } + } + } + + fn constrain( + &self, + id: &Id, + _nodes: &Nodes, + parent: Constraint, + ) -> Constraint { + self.constrain_calls.set(self.constrain_calls.get() + 1); + + let widget = + self.widgets.get(id).expect("Id is invalid!"); + + match widget { + Widget::Column(_) => parent, + Widget::Fixed(size) => Constraint::tight(*size), + } + } + + fn build( + &self, + id: &Id, + constraint: Constraint, + nodes: &mut Nodes, + ) -> Size { + self.build_calls.set(self.build_calls.get() + 1); + + let widget = + self.widgets.get(id).expect("Id is invalid!"); + + let size = match widget { + Widget::Column(children) => { + let mut size = Size::ZERO; + for child in children { + let child_size = nodes.get_size(child); + nodes.set_translation( + child, + Vec2::new(0.0, size.height), + ); + size.width = size.width.max(child_size.width); + size.height += child_size.height; + } + + size + } + Widget::Fixed(size) => *size, + }; + + constraint.constrain(size) + } + } + + #[derive(Default)] + struct WidgetTree { + tree: Tree, + nodes: Nodes, + } + + impl WidgetTree { + pub fn add_fixed( + &mut self, + id: Id, + parent: Option, + size: Size, + ) { + self.tree.widgets.insert(id, Widget::Fixed(size)); + self.nodes.add(id, parent); + } + + pub fn add_column( + &mut self, + id: Id, + parent: Option, + column: Vec, + ) { + self.tree.widgets.insert(id, Widget::Column(column)); + self.nodes.add(id, parent); + } + + pub fn layout(&mut self, id: &Id) { + layout(&self.tree, &mut self.nodes, id); + } } } diff --git a/crates/rectree/src/node.rs b/crates/rectree/src/node.rs index ef0e046..68abcb9 100644 --- a/crates/rectree/src/node.rs +++ b/crates/rectree/src/node.rs @@ -1,214 +1,222 @@ use bitflags::bitflags; -use hashbrown::HashSet; -use kurbo::{Rect, Size, Vec2}; -use crate::NodeId; -use crate::layout::Constraint; +use crate::geom::{Constraint, Size, Vec2}; -/// An axis-aligned rectangle in the layout tree. +/// Per-node layout data stored in [`crate::RectNodes`]. /// -/// The rectangle is defined in **local space** by a translation and -/// a size. `local_translation` denotes the **top-left corner** -/// relative to the parent. The final position in world space is -/// stored in `world_translation` after layout resolution. -/// -/// ```text -/// translation -/// ^ -/// +--------+ -/// | | height -/// +--------+ -/// width -/// ``` -#[derive(Default, Debug, Clone)] -pub struct RectNode { - /// See [`Self::translation()`]. - pub(crate) translation: Vec2, - /// See [`Self::size()`]. - pub(crate) size: Size, - /// See [`Self::parent_constraint()`]. - pub(crate) parent_constraint: Constraint, - /// See [`Self::world_translation()`]. - pub(crate) world_translation: Vec2, - /// See [`Self::parent()`]. - pub(crate) parent: Option, - /// See [`Self::children()`]. - pub(crate) children: HashSet, - /// See [`Self::depth()`]. - pub(crate) depth: u32, - /// The state of the current node. - pub(crate) state: NodeState, -} - -/// Builders. -impl RectNode { - pub fn new() -> Self { - Self::default() - } - - pub fn from_translation(translation: impl Into) -> Self { - Self::new().with_translation(translation) - } - - pub fn from_size(size: impl Into) -> Self { - Self::new().with_size(size) - } - - pub fn from_translation_size( - translation: impl Into, - size: impl Into, - ) -> Self { - Self::new().with_translation(translation).with_size(size) - } - - pub fn from_rect(rect: impl Into) -> Self { - let rect: Rect = rect.into(); - Self::new() - .with_translation(Vec2::new(rect.min_x(), rect.min_y())) - .with_size(rect.size()) - } - - pub fn with_translation( - mut self, - translation: impl Into, - ) -> Self { - self.translation = translation.into(); - self - } - - pub fn with_size(mut self, size: impl Into) -> Self { - self.size = size.into(); - self - } - - pub fn with_parent(mut self, parent: NodeId) -> Self { - self.parent = Some(parent); - self - } -} - -/// Getters. -impl RectNode { - /// Local translation, relative to the parent. - pub fn translation(&self) -> Vec2 { - self.translation - } - - /// Size of the node. +/// A `RectNode` holds all the numbers that rectree reads and +/// writes during a layout cycle. It carries no widget logic; +/// that lives in your [`crate::Rectree`] implementation. +pub struct RectNode { + /// The parent node's ID, or `None` for root nodes. /// - /// This is the resolved size after - /// [`crate::layout::LayoutSolver::build()`]. - pub fn size(&self) -> Size { - self.size - } + /// Used by the rebuild pass to walk upward when a child's + /// size changes. + pub parent_id: Option, - /// Constraint imposed by the parent onto this node. + /// Flags tracking which layout passes are current for this + /// node. /// - /// This is computed during the top-down constraint pass via - /// [`crate::layout::LayoutSolver::constraint()`]. - pub fn parent_constraint(&self) -> Constraint { - self.parent_constraint - } + /// Check and clear these flags to control incremental + /// re-layout. + pub state: NodeState, - /// World-space translation of this node. + /// The constraint imposed on this node by its parent. /// - /// This is the accumulated translation from the root and is - /// computed during transform propagation. - pub fn world_translation(&self) -> Vec2 { - self.world_translation - } - - /// Parent node in the hierarchy, if any. - pub fn parent(&self) -> Option { - self.parent - } + /// Written by [`crate::constrain`]; read by [`crate::build`]. + pub constraint: Constraint, - /// Child nodes of this node. - pub fn children(&self) -> &HashSet { - &self.children - } + /// The resolved size of this node. + /// + /// Written by [`crate::build`]; read by parent nodes during + /// their own build step and by + /// [`crate::propagate_translation`]. + pub size: Size, - /// How deep in the hierarchy is this node (0 for root nodes). + /// Local translation relative to the parent node's origin. /// - /// This value is assigned and maintained by [`crate::Rectree`] - /// and must not be modified externally. - pub fn depth(&self) -> u32 { - self.depth - } + /// Written by the parent's build step via + /// `NodeContext::set_translation`. Zero by default. + pub translation: Vec2, - /// Compute the world space [`Rect`] from - /// [`Self::world_translation`] and [`Self::size`]. - pub fn world_rect(&self) -> Rect { - Rect::new( - self.world_translation.x, - self.world_translation.y, - self.world_translation.x + self.size.width, - self.world_translation.y + self.size.height, - ) - } + /// Absolute world-space position of this node's origin. + /// + /// Written by [`crate::propagate_translation`] as the sum of + /// all ancestor translations. Use this value for rendering. + pub world_translation: Vec2, +} - /// Returns `true` if [`Self::parent`] is `None`. - pub fn is_root(&self) -> bool { - self.parent.is_none() +impl RectNode { + /// Creates a new node with the given parent and all other + /// fields at their defaults (`Constraint::unbounded()`, + /// `Size::ZERO`, `Vec2::ZERO`, empty [`NodeState`]). + pub fn new(parent_id: Option) -> Self { + Self { + parent_id, + state: NodeState::default(), + constraint: Constraint::default(), + size: Size::default(), + translation: Vec2::default(), + world_translation: Vec2::default(), + } } } bitflags! { - #[derive(Default, Debug, Clone, Copy)] + /// Tracks which layout passes have completed for a [`RectNode`]. + /// + /// Each pass sets its flag when it finishes processing a node. + /// Before processing, a pass checks the flag and skips the + /// node (and its subtree) if it is already set and the + /// relevant input is unchanged. + /// + /// Clearing a flag (via `needs_*`) schedules the node for + /// reprocessing on the next layout call. + /// + /// # Flag lifecycle + /// + /// | Event | Flags changed | + /// | ------------------------------ | ---------------------------------- | + /// | Node created or [`Self::reset`]| all cleared | + /// | Constraint passes through | `CONSTRAINED` set | + /// | Constraint changes | `CONSTRAINED` set, `BUILT` cleared | + /// | Build completes | `BUILT` set, `POSITIONED` cleared | + /// | Translation propagated | `POSITIONED` set | + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub struct NodeState: u8 { - const POSITIONED = 1; + /// Set by [`crate::propagate_translation`] when + /// `world_translation` is current. + const POSITIONED = 1; + + /// Set by [`crate::constrain`] when the stored constraint + /// matches the value last propagated from the parent. const CONSTRAINED = 1 << 1; - const BUILT = 1 << 2; + + /// Set by [`crate::build`] or [`crate::build_up`] when + /// `size` and child translations are current. + const BUILT = 1 << 2; } } impl NodeState { - /// Returns the [`Self::POSITIONED`] flag value. - pub fn positioned(&self) -> bool { + /// Clears all flags, scheduling the node for a full + /// re-layout. + pub fn reset(&mut self) { + *self = Self::empty(); + } + + /// Returns `true` when all three passes are current. + /// + /// Used by the `layout` free function to short-circuit the + /// entire cycle. + pub fn is_ready(&self) -> bool { + *self == Self::all() + } + + /// Returns `true` if the `POSITIONED` flag is set. + pub fn is_positioned(&self) -> bool { self.intersects(Self::POSITIONED) } - /// Returns the [`Self::CONSTRAINED`] flag value. - pub fn constrained(&self) -> bool { + /// Returns `true` if the `CONSTRAINED` flag is set. + pub fn is_constrained(&self) -> bool { self.intersects(Self::CONSTRAINED) } - /// Returns the [`Self::BUILT`] flag value. - pub fn built(&self) -> bool { + /// Returns `true` if the `BUILT` flag is set. + pub fn is_built(&self) -> bool { self.intersects(Self::BUILT) } - pub fn reset(&mut self) { - *self = Self::empty(); - } - - /// Set [`Self::POSITIONED`] flag to `false`. + /// Clears `POSITIONED`, marking translation as stale. pub fn needs_reposition(&mut self) { self.remove(Self::POSITIONED); } - /// Set [`Self::CONSTRAINED`] flag to `false`. + /// Clears `CONSTRAINED`, marking constraint as stale. pub fn needs_reconstrain(&mut self) { self.remove(Self::CONSTRAINED); } - /// Set [`Self::BUILT`] flag to `false`. + /// Clears `BUILT`, marking size and translations as stale. pub fn needs_rebuild(&mut self) { self.remove(Self::BUILT); } - /// Set [`Self::POSITIONED`] flag to `true`. + /// Sets `POSITIONED`, marking translation as current. pub fn has_repositioned(&mut self) { self.insert(Self::POSITIONED); } - /// Set [`Self::CONSTRAINED`] flag to `true`. - pub fn has_recontrained(&mut self) { + /// Sets `CONSTRAINED`, marking constraint as current. + pub fn has_reconstrained(&mut self) { self.insert(Self::CONSTRAINED); } - /// Set [`Self::BUILT`] flag to `true`. + /// Sets `BUILT`, marking size and translations as current. pub fn has_rebuilt(&mut self) { self.insert(Self::BUILT); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_has_no_flags() { + let s = NodeState::default(); + assert!(!s.is_constrained()); + assert!(!s.is_built()); + assert!(!s.is_positioned()); + assert!(!s.is_ready()); + } + + #[test] + fn test_is_ready_only_when_all_flags_set() { + let mut s = NodeState::default(); + s.has_reconstrained(); + assert!(!s.is_ready()); + s.has_rebuilt(); + assert!(!s.is_ready()); + s.has_repositioned(); + assert!(s.is_ready()); + } + + #[test] + fn test_reset_clears_all_flags() { + let mut s = NodeState::all(); + s.reset(); + assert!(!s.is_constrained()); + assert!(!s.is_built()); + assert!(!s.is_positioned()); + assert!(!s.is_ready()); + } + + #[test] + fn test_needs_rebuild_clears_built() { + let mut s = NodeState::default(); + s.has_rebuilt(); + assert!(s.is_built()); + s.needs_rebuild(); + assert!(!s.is_built()); + } + + #[test] + fn test_needs_reposition_clears_positioned() { + let mut s = NodeState::default(); + s.has_repositioned(); + assert!(s.is_positioned()); + s.needs_reposition(); + assert!(!s.is_positioned()); + } + + #[test] + fn test_needs_reconstrain_clears_constrained() { + let mut s = NodeState::default(); + s.has_reconstrained(); + assert!(s.is_constrained()); + s.needs_reconstrain(); + assert!(!s.is_constrained()); + } +} diff --git a/examples/vello_winit_examples/examples/layout_basic.rs b/examples/vello_winit_examples/examples/layout_basic.rs index 816abf4..1a0de45 100644 --- a/examples/vello_winit_examples/examples/layout_basic.rs +++ b/examples/vello_winit_examples/examples/layout_basic.rs @@ -1,12 +1,48 @@ +//! # layout_basic +//! +//! This example demonstrates how to wire up the `rectree` layout +//! crate into a real application. +//! +//! ## How rectree layout works +//! +//! rectree uses a three-pass algorithm, applied top-down then +//! bottom-up: +//! +//! 1. **Constrain** (top-down): each node receives its parent's +//! constraint and narrows it for its children. The resolved +//! `Constraint` is stored on the node. +//! +//! 2. **Build** (bottom-up): children are measured before parents. +//! A node calls `Widget::build`, which reads child *sizes* and +//! writes child *translations*, then returns the node's own size. +//! Sizes flow upward; positions flow downward within the same +//! pass. +//! +//! 3. **Propagate translation** (top-down): accumulates local +//! translations into absolute `world_translation` values used +//! for rendering. +//! +//! ## Split between `World` and `Nodes` +//! +//! rectree requires two separate objects at call sites: +//! +//! - `&T: LayoutTree` — owns the *tree structure* and widget logic +//! (read-only during layout). +//! - `&mut N: LayoutNode` — owns the *per-node data* (constraint, +//! size, translation) and is mutated by the layout passes. +//! +//! This split is necessary because Rust cannot hold `&T` and +//! `&mut T` at the same time when `T == N`. Here `World` is `T` +//! and `Nodes` is `N`. + use std::any::Any; use hashbrown::HashMap; -use kurbo::{Affine, Circle, Rect, Size, Stroke, Vec2}; -use rectree::layout::{ - Constraint, LayoutSolver, LayoutWorld, Positioner, +use kurbo::{Affine, Circle, Point, Rect, Size as KSize, Stroke}; +use rectree::{ + Constraint, NodeContext, RectNode, RectNodes, Rectree, Size, + Vec2, layout, }; -use rectree::node::RectNode; -use rectree::{NodeId, Rectree}; use vello::Scene; use vello::peniko::Color; use vello::peniko::color::palette::css; @@ -16,11 +52,14 @@ use winit::event_loop::EventLoop; fn main() { let event_loop = EventLoop::new().unwrap(); let mut demo = LayoutDemo::new(); + let root_size = demo.nodes.window_size; let mut builder = demo.builder(); + // Helper closure: builds a vertical column of seven colored + // boxes with a diamond-shaped height profile (40→100→40 px). let create_column = |b: &mut Builder| { Vertical::new(10.0).show(b, |b| { - const WIDTH: f64 = 200.0; + const WIDTH: f32 = 200.0; vec![ FixedSizeWidget::new(Size::new(WIDTH, 40.0)) .with_color(css::RED) @@ -47,191 +86,331 @@ fn main() { }) }; - let root_id = FixedSizeWidget::new(builder.demo.window_size) - .show_with_child(&mut builder, |b| { - PlaceWidget::new(Alignment::Both { - h: HAlign::Center, - v: VAlign::Horizon, - }) - .show(b, |b| { - Padding::all(20.0).show(b, |b| { - Vertical::new(20.0).show(b, |b| { - const HEIGHT: f64 = 60.0; - vec![ - Horizontal::new(50.0).show(b, |b| { - vec![ - create_column(b), - create_column(b), - create_column(b), - ] - }), - FixedSizeWidget::new(Size::new( - 50.0, HEIGHT, - )) - .with_color(css::CYAN) - .show(b), - FixedSizeWidget::new(Size::new( - 200.0, HEIGHT, - )) - .with_color(css::SALMON) - .show(b), - FixedSizeWidget::new(Size::new( - 800.0, HEIGHT, - )) - .with_color(css::RED) - .show(b), - ] + // Build the widget tree using the declarative `Builder` API. + // Each `show` / `show_with_child` call allocates a `NodeId`, + // registers the widget in `World`, and records the parent-child + // relationship so `LayoutTree::children` can traverse it. + let root_id = FixedSizeWidget::new(root_size).show_with_child( + &mut builder, + |b| { + PlaceWidget::show( + Alignment::Both { + h: HAlign::Center, + v: VAlign::Horizon, + }, + b, + |b| { + Padding::all(20.0).show(b, |b| { + Vertical::new(20.0).show(b, |b| { + const HEIGHT: f32 = 60.0; + vec![ + Horizontal::new(50.0).show(b, |b| { + vec![ + create_column(b), + create_column(b), + create_column(b), + ] + }), + FixedSizeWidget::new(Size::new( + 50.0, HEIGHT, + )) + .with_color(css::CYAN) + .show(b), + FixedSizeWidget::new(Size::new( + 200.0, HEIGHT, + )) + .with_color(css::SALMON) + .show(b), + FixedSizeWidget::new(Size::new( + 800.0, HEIGHT, + )) + .with_color(css::RED) + .show(b), + ] + }) }) - }); - }); - }); + }, + ) + }, + ); - // Store the root ID for future reference. demo.root_id = Some(root_id); - // Initial layout. - demo.tree.layout(&demo.world); + // Run the initial layout before opening the window. + demo.layout(); let mut app = VelloWinitApp::new(demo); - event_loop.run_app(&mut app).unwrap(); } -pub struct World { - widgets: HashMap>, +/// Opaque handle that identifies a single node. +/// +/// Must be `Copy + Eq + Hash` so rectree can use it as a map key +/// and pass it around without cloning. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, +)] +pub struct NodeId(u32); + +/// Flat storage for every node's layout data. +/// +/// This is the mutable half of the layout split. It only holds +/// per-node numbers (`constraint`, `size`, `translation`, …) — it +/// knows nothing about the widget logic or tree structure. +/// +/// `rectree` mutates this through the [`LayoutNode`] trait. +pub struct Nodes { + data: HashMap>, + next_id: u32, + pub window_size: Size, } -impl World { +impl Nodes { fn new() -> Self { Self { - widgets: HashMap::new(), + data: HashMap::new(), + next_id: 0, + window_size: Size::new(800.0, 600.0), } } -} -impl LayoutWorld for World { - fn get_solver(&self, id: &NodeId) -> &dyn LayoutSolver { - &**self.widgets.get(id).unwrap() + /// Allocate a new node, recording its parent link so the + /// bottom-up `build` pass can walk upward when a size changes. + fn insert(&mut self, parent: Option) -> NodeId { + let id = NodeId(self.next_id); + self.next_id += 1; + self.data.insert(id, RectNode::new(parent)); + id + } + + /// Clear all `NodeState` flags on one node so the next + /// `layout()` call re-runs all three passes for its subtree. + fn invalidate(&mut self, id: NodeId) { + if let Some(n) = self.data.get_mut(&id) { + n.state.reset(); + } } } -pub trait Widget: LayoutSolver + Any {} +/// Implement `LayoutNode` by forwarding to the flat `data` map. +/// rectree only ever calls `get_node` / `get_node_mut` through this +/// trait, keeping the storage details encapsulated. +impl RectNodes for Nodes { + type Id = NodeId; -impl Widget for T where T: LayoutSolver + Any {} + fn get_node(&self, id: &NodeId) -> Option<&RectNode> { + self.data.get(id) + } -pub struct LayoutDemo { - tree: Rectree, - world: World, - window_size: Size, - root_id: Option, + fn get_node_mut( + &mut self, + id: &NodeId, + ) -> Option<&mut RectNode> { + self.data.get_mut(id) + } } -pub struct Builder<'a> { - pub demo: &'a mut LayoutDemo, - pub parent_id: Option, +/// The read-only half of the layout split. +/// +/// `World` owns: +/// - the widget instances (their logic and child relationships). +/// +/// It is passed as `&World` to the rectree free functions, while +/// `&mut Nodes` is passed separately — avoiding the borrow conflict +/// that would arise if a single type owned both. +pub struct World { + widgets: HashMap>, } -impl Builder<'_> { - pub fn add_widget( - &mut self, - add_content: impl FnOnce(&mut Builder) -> W, - ) -> NodeId { - let mut node = RectNode::new(); - if let Some(parent_id) = self.parent_id { - node = node.with_parent(parent_id); +impl World { + fn new() -> Self { + Self { + widgets: HashMap::new(), } - let id = self.demo.tree.insert(node); + } +} - let w = Box::new(add_content(&mut Builder { - demo: self.demo, - parent_id: Some(id), - })); - self.demo.world.widgets.insert(id, w); +/// Implement `LayoutTree` by delegating to the stored widget +/// instances. rectree calls these methods during the constrain and +/// build passes. +impl Rectree for World { + type Id = NodeId; + type Nodes = Nodes; - id + /// Calls `f` for each child of `id` in insertion order. + fn for_each_child( + &self, + id: &NodeId, + nodes: &mut Nodes, + mut f: impl FnMut(&NodeId, &mut Nodes), + ) { + if let Some(w) = self.widgets.get(id) { + w.for_each_child(&mut |child| f(child, nodes)); + } + } + + /// Asks the widget to derive the node's own constraint from its + /// parent's. Most widgets pass through unchanged; containers + /// like `PaddingWidget` subtract their insets. + fn constrain( + &self, + id: &NodeId, + _nodes: &Nodes, + parent: Constraint, + ) -> Constraint { + self.widgets + .get(id) + .map(|w| w.constraint(parent)) + .unwrap_or(parent) + } + + /// Asks the widget to measure itself and position its children. + /// Children have already been built by the time this is called + /// (bottom-up order), so their sizes are available via `nodes`. + /// + /// Widgets can *read* child sizes and *write* child translations + /// — but cannot mutate child sizes directly. + fn build( + &self, + id: &NodeId, + constraint: Constraint, + nodes: &mut Nodes, + ) -> Size { + self.widgets + .get(id) + .map(|w| w.build(constraint, nodes)) + .unwrap_or(Size::ZERO) } } +/// A widget defines *how* a node behaves during layout. +/// +/// - [`constraint`](Widget::constraint): narrows the parent's +/// `Constraint` for this node's children (e.g. subtract padding). +/// - [`build`](Widget::build): given the node's own constraint and +/// the already-built children, compute this node's `Size` and set +/// children's local translations. +pub trait Widget: Any { + /// Default: pass the parent constraint through unchanged. + fn constraint(&self, parent: Constraint) -> Constraint { + parent + } + + fn for_each_child(&self, _f: &mut dyn FnMut(&NodeId)) {} + + fn build( + &self, + constraint: Constraint, + nodes: &mut Nodes, + ) -> Size; +} + +pub struct LayoutDemo { + /// Read-only tree: widget logic + parent-child relationships. + world: World, + /// Mutable storage: per-node layout numbers. + nodes: Nodes, + /// The root `NodeId`; stored so `size_changed` can update it. + root_id: Option, +} + impl LayoutDemo { pub fn new() -> Self { Self { - tree: Rectree::new(), world: World::new(), - window_size: Size::new(800.0, 600.0), + nodes: Nodes::new(), root_id: None, } } pub fn builder(&mut self) -> Builder<'_> { Builder { - demo: self, + world: &mut self.world, + nodes: &mut self.nodes, parent_id: None, } } - fn draw_tree(&self, scene: &mut Scene, transform: Affine) { - // Start traversal from the root IDs provided by the tree. - for root_id in self.tree.root_ids() { - let mut stack = vec![*root_id]; - - while let Some(node_id) = stack.pop() { - // Get node from tree. - let node = self.tree.get(&node_id); - - // Get world_translation. - let world_pos = node.world_translation(); - - // Reconstruct rect from world pos and size. - let world_rect = Rect::from_origin_size( - world_pos.to_point(), - node.size(), - ); - - // Hack to get the color of `FixedSizeWidget`. - // In real world scenario, you would want to - // implement a `draw` method for your `Widget` trait. - if let Some(color) = - self.world.widgets.get(&node_id).and_then( - |widget| { - let widget: &dyn Any = widget.as_ref(); - widget - .downcast_ref::() - .map(|f| f.color) - }, - ) - { - scene.fill( - vello::peniko::Fill::NonZero, - transform, - color, - None, - &world_rect, - ); - } - - scene.stroke( - &Stroke::new(2.0), - transform, - Color::WHITE, - None, - &world_rect, - ); - - // Origin markers. - let origin = Circle::new(world_rect.origin(), 5.0); + /// Run all three layout passes for every root node. + /// + /// `layout` (the rectree free function) runs constrain→build→ + /// propagate_translation internally and short-circuits each pass + /// via `NodeState` flags — so re-calling this every frame is + /// cheap when nothing changed. + fn layout(&mut self) { + if let Some(root) = self.root_id { + self.nodes.get_node_mut(&root).unwrap().state.reset(); + layout(&self.world, &mut self.nodes, &root); + } + } + /// Walk the node tree and draw each node's bounding box. + /// + /// - Colored `FixedSizeWidget`s are filled with their color. + /// - Every node gets a white stroke so the layout boxes are + /// visible. + /// - A small red dot marks each node's origin point. + fn draw_tree(&self, scene: &mut Scene, transform: Affine) { + let Some(root_id) = self.root_id else { return }; + // Iterative DFS using a stack to avoid recursion limits. + let mut stack = vec![root_id]; + + while let Some(node_id) = stack.pop() { + let Some(node) = self.nodes.get_node(&node_id) else { + continue; + }; + + // `world_translation` is set by `propagate_translation` + // and holds the node's absolute position in window space. + let world_pos = node.world_translation; + let size = node.size; + let world_rect = Rect::from_origin_size( + Point::new(world_pos.x as f64, world_pos.y as f64), + KSize::new(size.width as f64, size.height as f64), + ); + + // Only `FixedSizeWidget`s carry a fill color; other + // nodes are transparent (only their border is drawn). + if let Some(color) = + self.world.widgets.get(&node_id).and_then(|widget| { + let widget: &dyn Any = widget.as_ref(); + widget + .downcast_ref::() + .map(|f| f.color) + }) + { scene.fill( vello::peniko::Fill::NonZero, transform, - css::RED, + color, None, - &origin, + &world_rect, ); + } - // Traverse to children. - for child_id in node.children().iter() { + // White border shows the layout box of every node. + scene.stroke( + &Stroke::new(2.0), + transform, + Color::WHITE, + None, + &world_rect, + ); + + // Red dot at the node's top-left origin. + let origin = Circle::new(world_rect.origin(), 5.0); + scene.fill( + vello::peniko::Fill::NonZero, + transform, + css::RED, + None, + &origin, + ); + + if let Some(w) = self.world.widgets.get(&node_id) { + w.for_each_child(&mut |child_id| { stack.push(*child_id); - } + }); } } } @@ -249,26 +428,29 @@ impl VelloDemo for LayoutDemo { } fn initial_logical_size(&self) -> (f64, f64) { - (self.window_size.width, self.window_size.height) + ( + self.nodes.window_size.width as f64, + self.nodes.window_size.height as f64, + ) } + /// Called by the harness whenever the window is resized. + /// + /// We update the root `FixedSizeWidget`'s size to match the new + /// window dimensions and invalidate the root node so the next + /// `layout()` call re-runs the passes from the top. fn size_changed(&mut self, size: Size) { - self.window_size = size; + self.nodes.window_size = size; - // Propagate size change to the root widget. let Some(root_id) = self.root_id else { return }; - let Some(widget) = self.world.widgets.get_mut(&root_id) - else { - return; - }; - - if let Some(fixed_widget) = (widget.as_mut() as &mut dyn Any) - .downcast_mut::() + if let Some(widget) = self.world.widgets.get_mut(&root_id) + && let Some(fixed_widget) = (widget.as_mut() + as &mut dyn Any) + .downcast_mut::() { fixed_widget.size = size; - // Trigger relayout for the root. - self.tree.schedule_relayout(root_id); + self.nodes.invalidate(root_id); } } @@ -277,15 +459,48 @@ impl VelloDemo for LayoutDemo { scene: &mut Scene, scale_factor: f64, ) { - // Perform layouting. - self.tree.layout(&self.world); - + self.layout(); self.draw_tree(scene, Affine::scale(scale_factor)); } } -// Below are some demo widgets to demonstrate how a UI library could -// potentially use `rectree` as a backend! +/// Accumulates nodes into `World` and `Nodes` during tree +/// construction. +/// +/// `parent_id` tracks the current insertion point; each `add_widget` +/// call creates a child under it and recurses with itself as the new +/// parent, producing a depth-first construction order. +pub struct Builder<'a> { + world: &'a mut World, + nodes: &'a mut Nodes, + /// The node that newly created nodes will be children of. + /// `None` means the next node becomes a root. + parent_id: Option, +} + +impl Builder<'_> { + /// Create a node, run `add_content` to build its children, then + /// return the node's `NodeId`. + /// + /// `add_content` is a closure that receives a `Builder` already + /// scoped to the new node as its parent, so any widgets created + /// inside it automatically become children. + pub fn add_widget( + &mut self, + add_content: impl FnOnce(&mut Builder) -> W, + ) -> NodeId { + let id = self.nodes.insert(self.parent_id); + + let w = Box::new(add_content(&mut Builder { + world: self.world, + nodes: self.nodes, + parent_id: Some(id), + })); + self.world.widgets.insert(id, w); + + id + } +} #[derive(Debug, Clone, Copy)] pub enum HAlign { @@ -297,6 +512,7 @@ pub enum HAlign { #[derive(Debug, Clone, Copy)] pub enum VAlign { Top, + /// Vertically centered. Horizon, Bottom, } @@ -308,95 +524,87 @@ pub enum Alignment { Vertical(VAlign), } -/// Place the child widget in a certain alignment +/// Positions its single child within the available space according +/// to an [`Alignment`]. +/// +/// Does not contribute any size itself (`Size::ZERO`) — it is purely +/// a positioning container. pub struct PlaceWidget { pub alignment: Alignment, + pub child: NodeId, } impl PlaceWidget { - pub fn new(alignment: Alignment) -> Self { - Self { alignment } - } - pub fn show( - self, + alignment: Alignment, b: &mut Builder, - add_content: impl FnOnce(&mut Builder), + add_content: impl FnOnce(&mut Builder) -> NodeId, ) -> NodeId { b.add_widget(|b| { - add_content(b); - self + let child = add_content(b); + PlaceWidget { alignment, child } }) } } -impl LayoutSolver for PlaceWidget { +impl Widget for PlaceWidget { + fn for_each_child(&self, f: &mut dyn FnMut(&NodeId)) { + f(&self.child); + } + fn build( &self, - node: &RectNode, - tree: &Rectree, - positioner: &mut Positioner, + constraint: Constraint, + nodes: &mut Nodes, ) -> Size { - let constraint = node.parent_constraint(); + let child_size = nodes.get_size(&self.child); + + // The available space is the maximum of our own constraint. + let avail_w = constraint.max.width; + let avail_h = constraint.max.height; + let (halign, valign) = match self.alignment { Alignment::Both { h, v } => (Some(h), Some(v)), - Alignment::Horizontal(halign) => (Some(halign), None), - Alignment::Vertical(valign) => (None, Some(valign)), + Alignment::Horizontal(h) => (Some(h), None), + Alignment::Vertical(v) => (None, Some(v)), }; - for (id, child) in - node.children().iter().map(|id| (id, tree.get(id))) - { - let child_size = child.size(); - let mut translation = Vec2::ZERO; - let mut should_position = false; - - if let Some(halign) = halign - && let Some(width) = constraint.width - { - should_position = true; - translation.x = match halign { - HAlign::Left => 0.0, - HAlign::Center => { - width * 0.5 - child_size.width * 0.5 - } - HAlign::Right => width - child_size.width, - }; + let x = match halign { + Some(HAlign::Left) => 0.0, + Some(HAlign::Center) => { + (avail_w - child_size.width) / 2.0 } - - if let Some(valign) = valign - && let Some(height) = constraint.height - { - should_position = true; - translation.y = match valign { - VAlign::Top => 0.0, - VAlign::Horizon => { - height * 0.5 - child_size.height * 0.5 - } - VAlign::Bottom => height - child_size.height, - }; + Some(HAlign::Right) => avail_w - child_size.width, + None => 0.0, + }; + let y = match valign { + Some(VAlign::Top) => 0.0, + Some(VAlign::Horizon) => { + (avail_h - child_size.height) / 2.0 } + Some(VAlign::Bottom) => avail_h - child_size.height, + None => 0.0, + }; - if should_position { - positioner.set(*id, translation); - } - } + // Write the child's local translation. `propagate_translation` + // will later accumulate this into an absolute world position. + nodes.set_translation(&self.child, Vec2::new(x, y)); - // Placing the widget should not allocate any size. Size::ZERO } } -/// [`HorizontalWidget`] builder. +/// Builder for [`HorizontalWidget`]. #[derive(Debug, Clone)] pub struct Horizontal { - pub spacing: f64, + pub spacing: f32, } impl Horizontal { - pub fn new(spacing: f64) -> Self { + pub fn new(spacing: f32) -> Self { Self { spacing } } + pub fn show( self, builder: &mut Builder, @@ -409,54 +617,64 @@ impl Horizontal { } } -/// Horizontal layout widget. +/// Lays out children left-to-right with uniform spacing. +/// +/// The children's `NodeId`s are stored directly on the widget so +/// `build` can iterate them without going through the tree. #[derive(Debug, Clone)] pub struct HorizontalWidget { pub style: Horizontal, pub children: Vec, } -impl LayoutSolver for HorizontalWidget { +impl Widget for HorizontalWidget { + fn for_each_child(&self, f: &mut dyn FnMut(&NodeId)) { + for child in &self.children { + f(child); + } + } + fn build( &self, - _node: &RectNode, - tree: &Rectree, - positioner: &mut Positioner, + constraint: Constraint, + nodes: &mut Nodes, ) -> Size { - let mut max_height = 0.0; - let mut x_cursor = 0.0; + let mut height = 0.0; + let mut width = 0.0; - for id in self.children.iter() { - let child_node = tree.get(id); - let child_size = child_node.size(); + for child_id in &self.children { + // Children are already built (bottom-up), so their + // sizes are final. + let child_size = nodes.get_size(child_id); - positioner.set(*id, Vec2::new(x_cursor, 0.0)); - x_cursor += child_size.width + self.style.spacing; + // Place this child at the current x cursor. + nodes.set_translation(child_id, Vec2::new(width, 0.0)); - // Track the tallest child - if child_size.height > max_height { - max_height = child_size.height; - } + width += child_size.width + self.style.spacing; + height = child_size.height.max(height); } - // Remove the last added spacing + // Strip the trailing gap added after the last child. if !self.children.is_empty() { - x_cursor -= self.style.spacing; + width -= self.style.spacing; } - Size::new(x_cursor, max_height) + // `Constraint::constrain` clamps the intrinsic size to the + // min/max bounds, so the widget respects its constraint. + constraint.constrain(Size::new(width, height)) } } -/// [`VerticalWidget`] builder. +/// Builder for [`VerticalWidget`]. #[derive(Debug, Clone)] pub struct Vertical { - pub spacing: f64, + pub spacing: f32, } impl Vertical { - pub fn new(spacing: f64) -> Self { + pub fn new(spacing: f32) -> Self { Self { spacing } } + pub fn show( self, builder: &mut Builder, @@ -469,55 +687,55 @@ impl Vertical { } } -/// Vertical layout widget. +/// Lays out children top-to-bottom with uniform spacing. #[derive(Debug, Clone)] pub struct VerticalWidget { pub style: Vertical, pub children: Vec, } -impl LayoutSolver for VerticalWidget { +impl Widget for VerticalWidget { + fn for_each_child(&self, f: &mut dyn FnMut(&NodeId)) { + for child in &self.children { + f(child); + } + } + fn build( &self, - _node: &RectNode, - tree: &Rectree, - positioner: &mut Positioner, + constraint: Constraint, + nodes: &mut Nodes, ) -> Size { - let mut max_width = 0.0; - let mut y_cursor = 0.0; + let mut width = 0.0; + let mut height = 0.0; - for id in self.children.iter() { - let child_node = tree.get(id); - let child_size = child_node.size(); + for child_id in &self.children { + let child_size = nodes.get_size(child_id); - positioner.set(*id, Vec2::new(0.0, y_cursor)); + nodes.set_translation(child_id, Vec2::new(0.0, height)); - y_cursor += child_size.height + self.style.spacing; - // Track the widest child - if child_size.width > max_width { - max_width = child_size.width; - } + height += child_size.height + self.style.spacing; + width = child_size.width.max(width); } - // Remove the last added spacing if !self.children.is_empty() { - y_cursor -= self.style.spacing; + height -= self.style.spacing; } - Size::new(max_width, y_cursor) + constraint.constrain(Size::new(width, height)) } } -/// [`PaddingWidget`] builder. +/// Builder for [`PaddingWidget`]. #[derive(Debug, Clone, Copy)] pub struct Padding { - pub left: f64, - pub right: f64, - pub top: f64, - pub bottom: f64, + pub left: f32, + pub right: f32, + pub top: f32, + pub bottom: f32, } impl Padding { - fn all(padding: f64) -> Self { + fn all(padding: f32) -> Self { Self { left: padding, right: padding, @@ -538,90 +756,71 @@ impl Padding { } } -/// A container widget that applies specific padding to each side. +/// Wraps a single child with configurable insets on each side. +/// +/// The insets are applied in two places: +/// - `constraint`: subtracts them from the available space so the +/// child doesn't overflow. +/// - `build`: offsets the child's translation inward and grows the +/// returned size to include the insets. #[derive(Debug)] pub struct PaddingWidget { pub style: Padding, pub child: NodeId, } -impl LayoutSolver for PaddingWidget { - fn constraint( - &self, - parent_constraint: Constraint, - ) -> Constraint { - let Padding { - left, - right, - top, - bottom, - } = self.style; +impl Widget for PaddingWidget { + fn for_each_child(&self, f: &mut dyn FnMut(&NodeId)) { + f(&self.child); + } + + /// Reduce the parent constraint by the padding amounts so the + /// child is told it has less space to fill. + fn constraint(&self, parent: Constraint) -> Constraint { + let h_pad = self.style.left + self.style.right; + let v_pad = self.style.top + self.style.bottom; Constraint { - // Subtract horizontal padding from width - width: parent_constraint - .width - .map(|w| (w - (left + right)).max(0.0)), - // Subtract vertical padding from height - height: parent_constraint - .height - .map(|h| (h - (top + bottom)).max(0.0)), + min: Size::ZERO, + max: Size { + width: (parent.max.width - h_pad).max(0.0), + height: (parent.max.height - v_pad).max(0.0), + }, } } - /// Determines the final size and position of the padding widget and its child. - /// - /// Retrieves the child's final calculated size. - /// Offsets the child's position by the padding amount. - /// Returns the total size of this widget, - /// which includes the child's size plus the padding on all sides. fn build( &self, - _node: &RectNode, - tree: &Rectree, - positioner: &mut Positioner, + _constraint: Constraint, + nodes: &mut Nodes, ) -> Size { - let Padding { - left, - right, - top, - bottom, - } = self.style; - - let child_node = tree.get(&self.child); - let child_size = child_node.size(); + let child_size = nodes.get_size(&self.child); - // Position the child with the specified padding offsets - positioner.set(self.child, Vec2::new(left, top)); + // Shift the child inward by the padding amounts. + nodes.set_translation( + &self.child, + Vec2::new(self.style.left, self.style.top), + ); + // Our own size wraps the child plus the padding on both + // sides. Size::new( - child_size.width + left + right, - child_size.height + top + bottom, + child_size.width + self.style.left + self.style.right, + child_size.height + self.style.top + self.style.bottom, ) } } -/// A widget that forces a specific size that ignore parent constraints. +/// A leaf widget that returns a fixed size regardless of the +/// constraint passed in from its parent. +/// +/// Used both as the window root (to propagate the window size as a +/// tight constraint downward) and as colored leaf boxes in the demo. #[derive(Debug, Clone)] pub struct FixedSizeWidget { pub size: Size, pub color: Color, -} - -impl LayoutSolver for FixedSizeWidget { - fn constraint(&self, _parent: Constraint) -> Constraint { - // Fixed size yield fixed contraint. - Constraint::fixed(self.size.width, self.size.height) - } - - fn build( - &self, - _node: &RectNode, - _tree: &Rectree, - _positioner: &mut Positioner, - ) -> Size { - self.size - } + pub child: Option, } impl FixedSizeWidget { @@ -629,6 +828,7 @@ impl FixedSizeWidget { Self { size, color: Color::TRANSPARENT, + child: None, } } @@ -637,18 +837,43 @@ impl FixedSizeWidget { self } + /// Show as a leaf node (no children). pub fn show(self, b: &mut Builder) -> NodeId { b.add_widget(|_| self) } + /// Show with a single inner child. pub fn show_with_child( self, b: &mut Builder, - add_content: impl FnOnce(&mut Builder), + add_content: impl FnOnce(&mut Builder) -> NodeId, ) -> NodeId { - b.add_widget(|b| { - add_content(b); - self + b.add_widget(|b| FixedSizeWidget { + child: Some(add_content(b)), + ..self }) } } + +impl Widget for FixedSizeWidget { + fn for_each_child(&self, f: &mut dyn FnMut(&NodeId)) { + if let Some(child) = &self.child { + f(child); + } + } + + /// Override the parent constraint entirely with a tight box + /// around `self.size`. Children (if any) will be told they + /// have exactly this much space. + fn constraint(&self, _parent: Constraint) -> Constraint { + Constraint::tight(self.size) + } + + fn build( + &self, + _constraint: Constraint, + _nodes: &mut Nodes, + ) -> Size { + self.size + } +} diff --git a/examples/vello_winit_examples/src/lib.rs b/examples/vello_winit_examples/src/lib.rs index 8c36a03..4cc56b1 100644 --- a/examples/vello_winit_examples/src/lib.rs +++ b/examples/vello_winit_examples/src/lib.rs @@ -1,5 +1,6 @@ -use kurbo::Size; use std::num::NonZeroUsize; + +use rectree::Size; use std::sync::Arc; use vello::peniko::Color; use vello::util::{RenderContext, RenderSurface}; @@ -140,8 +141,10 @@ impl<'s, D: VelloDemo> VelloWinitApp<'s, D> { let logical_width = size.width as f64 / scale_factor; let logical_height = size.height as f64 / scale_factor; - self.demo - .size_changed(Size::new(logical_width, logical_height)); + self.demo.size_changed(Size::new( + logical_width as f32, + logical_height as f32, + )); } }