From eaefae202039603c8884c347f5efd8bc4a33f089 Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:11:04 +0800 Subject: [PATCH 01/11] Merge `LayoutSolver` into `LayoutWorld` --- crates/rectree/src/layout.rs | 74 ++++++++++++++---------------------- crates/rectree/src/lib.rs | 2 +- crates/rectree/src/node.rs | 4 +- 3 files changed, 32 insertions(+), 48 deletions(-) diff --git a/crates/rectree/src/layout.rs b/crates/rectree/src/layout.rs index 3e92cdf..8605af0 100644 --- a/crates/rectree/src/layout.rs +++ b/crates/rectree/src/layout.rs @@ -52,10 +52,10 @@ impl Rectree { // 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 parent_constraint = + self.get(&id).parent_constraint; let constraint = - solver.constraint(node.parent_constraint); + world.constraint(&id, parent_constraint); self.nodes.scope(&id, |nodes, node| { node.state.has_recontrained(); @@ -85,9 +85,12 @@ impl Rectree { // 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); + let size = world.build( + &id, + self.get(&id), + self, + &mut positioner, + ); positioner.apply(self); self.nodes.scope(&id, |nodes, node| { @@ -97,8 +100,8 @@ impl Rectree { 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. + // 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(); @@ -132,7 +135,7 @@ impl Rectree { /// Propagates world-space translations starting from a node. /// - /// This updates the node’s world translation and recursively + /// 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) { @@ -159,58 +162,39 @@ impl Rectree { } } -/// Provides access to layout solvers associated with nodes. +/// Provides the layout logic for each node in the tree. /// -/// Acts as the bridge between [`Rectree`] and layout logic, allowing -/// each node to be resolved by an external [`LayoutSolver`]. +/// Acts as the bridge between [`Rectree`] and the application's +/// element system. 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. + /// Computes the constraint this node propagates to its children. /// - /// By default, the parent’s constraint is forwarded unchanged. - /// Implementations may tighten, relax, or otherwise transform the - /// constraint before it is used during layout. + /// `parent` is the constraint imposed on this node by its own + /// parent. The return value is applied to each child before + /// their build pass. fn constraint( &self, - parent_constraint: Constraint, - ) -> Constraint { - parent_constraint - } + id: &NodeId, + parent: Constraint, + ) -> 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. + /// Called bottom-up after constraints have been propagated. + /// Implementations may inspect the tree and assign child + /// translations via [`Positioner`]. fn build( &self, + id: &NodeId, node: &RectNode, tree: &Rectree, - positioner: &mut Positioner, + pos: &mut Positioner, ) -> Size; } /// Collects child translations produced during layout construction. /// -/// See [`LayoutSolver::build()`]. +/// See [`LayoutWorld::build()`]. #[derive(Default)] pub struct Positioner { new_translations: Vec<(NodeId, Vec2)>, @@ -229,7 +213,7 @@ impl Positioner { /// Applies all recorded translations to the [`Rectree`]. /// /// This is called internally after layout resolution to commit - /// the results of [`LayoutSolver::build()`]. + /// the results of [`LayoutWorld::build()`]. fn apply(&mut self, tree: &mut Rectree) { for (id, translation) in self.new_translations.drain(..) { tree.get_mut(&id).translation = translation; diff --git a/crates/rectree/src/lib.rs b/crates/rectree/src/lib.rs index 0807161..e5cf3e7 100644 --- a/crates/rectree/src/lib.rs +++ b/crates/rectree/src/lib.rs @@ -12,9 +12,9 @@ use hashbrown::HashSet; use sparse_map::{Key, SparseMap}; use crate::layout::DepthNode; -use crate::node::RectNode; pub use kurbo; +pub use node::RectNode; pub mod layout; pub mod node; diff --git a/crates/rectree/src/node.rs b/crates/rectree/src/node.rs index ef0e046..6af4ce4 100644 --- a/crates/rectree/src/node.rs +++ b/crates/rectree/src/node.rs @@ -97,7 +97,7 @@ impl RectNode { /// Size of the node. /// /// This is the resolved size after - /// [`crate::layout::LayoutSolver::build()`]. + /// [`crate::layout::LayoutWorld::build()`]. pub fn size(&self) -> Size { self.size } @@ -105,7 +105,7 @@ impl RectNode { /// Constraint imposed by the parent onto this node. /// /// This is computed during the top-down constraint pass via - /// [`crate::layout::LayoutSolver::constraint()`]. + /// [`crate::layout::LayoutWorld::constraint()`]. pub fn parent_constraint(&self) -> Constraint { self.parent_constraint } From f0d08654b1b9d2f7f6fd4b9ba0690b1349e64eae Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Mon, 30 Mar 2026 16:16:08 +0800 Subject: [PATCH 02/11] Update example to new LayoutWorld API --- Cargo.lock | 1 - crates/rectree/Cargo.toml | 6 - crates/rectree/src/layout.rs | 186 +++++++++++++----- crates/rectree/src/lib.rs | 8 +- crates/rectree/src/node.rs | 40 +--- .../examples/layout_basic.rs | 160 ++++++++------- examples/vello_winit_examples/src/lib.rs | 9 +- 7 files changed, 252 insertions(+), 158 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0a7d5eb..1e078f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1421,7 +1421,6 @@ version = "0.1.0" dependencies = [ "bitflags 2.10.0", "hashbrown 0.16.1", - "kurbo", "sparse_map", ] diff --git a/crates/rectree/Cargo.toml b/crates/rectree/Cargo.toml index 2ce3a67..4039353 100644 --- a/crates/rectree/Cargo.toml +++ b/crates/rectree/Cargo.toml @@ -12,10 +12,4 @@ 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/src/layout.rs b/crates/rectree/src/layout.rs index 8605af0..c10e154 100644 --- a/crates/rectree/src/layout.rs +++ b/crates/rectree/src/layout.rs @@ -1,11 +1,151 @@ 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}; +/// A 2D size in resolved pixels. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct Size { + pub width: f32, + pub height: f32, +} + +impl Size { + pub const ZERO: Self = Self::splat(0.0); + + pub const INFINITY: Self = Self::splat(f32::INFINITY); + + #[inline] + pub const fn new(width: f32, height: f32) -> Self { + Self { width, height } + } + + #[inline] + pub const fn splat(value: f32) -> Self { + Self::new(value, value) + } +} + +/// A 2D position or translation. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct Vec2 { + pub x: f32, + pub y: f32, +} + +impl Vec2 { + pub const ZERO: Self = Self::splat(0.0); + + #[inline] + pub const fn new(x: f32, y: f32) -> Self { + Self { x, y } + } + + #[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) + } +} + +/// A min/max size constraint passed down the element tree. +/// +/// `max` fields set to [`f32::INFINITY`] indicate an unconstrained +/// axis. Use the constructor helpers [`Self::tight()`], +/// [`Self::loose()`], [`Self::unbounded()`] rather than constructing +/// directly where possible. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Constraint { + pub min: Size, + pub max: Size, +} + +impl Constraint { + /// Forces the child to be exactly `size`. + pub const fn tight(size: Size) -> Self { + Self { + min: size, + max: size, + } + } + + /// Child 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. + pub const fn unbounded() -> Self { + Self { + min: Size::ZERO, + max: Size::INFINITY, + } + } + + /// Bounded width, unbounded height + /// (e.g. vertical scroll container). + pub const fn fixed_width(width: f32) -> Self { + Self { + min: Size::ZERO, + max: Size { + width, + height: f32::INFINITY, + }, + } + } + + /// Bounded height, unbounded width + /// (e.g. horizontal scroll container). + 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. + 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 { + fn default() -> Self { + Self::unbounded() + } +} + +/// Callback interface for reading and writing child layout state +/// during [`crate::layout::LayoutWorld::build`]. +pub trait Layouter { + type Id; + + fn get_size(&self, id: &Self::Id) -> Size; + + fn set_position(&mut self, id: &Self::Id, position: Vec2); +} + /// Layout execution. impl Rectree { /// Check if we need to call [`Self::layout()`]. @@ -235,47 +375,3 @@ impl DepthNode { 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 e5cf3e7..cd793fa 100644 --- a/crates/rectree/src/lib.rs +++ b/crates/rectree/src/lib.rs @@ -11,10 +11,10 @@ use alloc::vec; use hashbrown::HashSet; use sparse_map::{Key, SparseMap}; -use crate::layout::DepthNode; - -pub use kurbo; -pub use node::RectNode; +pub use layout::{ + Constraint, DepthNode, Layouter, Positioner, Size, Vec2, +}; +pub use node::{NodeState, RectNode}; pub mod layout; pub mod node; diff --git a/crates/rectree/src/node.rs b/crates/rectree/src/node.rs index 6af4ce4..1b33118 100644 --- a/crates/rectree/src/node.rs +++ b/crates/rectree/src/node.rs @@ -1,9 +1,8 @@ use bitflags::bitflags; use hashbrown::HashSet; -use kurbo::{Rect, Size, Vec2}; use crate::NodeId; -use crate::layout::Constraint; +use crate::layout::{Constraint, Size, Vec2}; /// An axis-aligned rectangle in the layout tree. /// @@ -46,38 +45,28 @@ impl RectNode { Self::default() } - pub fn from_translation(translation: impl Into) -> Self { + pub fn from_translation(translation: Vec2) -> Self { Self::new().with_translation(translation) } - pub fn from_size(size: impl Into) -> Self { + pub fn from_size(size: Size) -> Self { Self::new().with_size(size) } pub fn from_translation_size( - translation: impl Into, - size: impl Into, + translation: Vec2, + size: Size, ) -> 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(); + pub fn with_translation(mut self, translation: Vec2) -> Self { + self.translation = translation; self } - pub fn with_size(mut self, size: impl Into) -> Self { - self.size = size.into(); + pub fn with_size(mut self, size: Size) -> Self { + self.size = size; self } @@ -136,17 +125,6 @@ impl RectNode { self.depth } - /// 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, - ) - } - /// Returns `true` if [`Self::parent`] is `None`. pub fn is_root(&self) -> bool { self.parent.is_none() diff --git a/examples/vello_winit_examples/examples/layout_basic.rs b/examples/vello_winit_examples/examples/layout_basic.rs index 816abf4..9aa9b97 100644 --- a/examples/vello_winit_examples/examples/layout_basic.rs +++ b/examples/vello_winit_examples/examples/layout_basic.rs @@ -1,12 +1,9 @@ use std::any::Any; use hashbrown::HashMap; -use kurbo::{Affine, Circle, Rect, Size, Stroke, Vec2}; -use rectree::layout::{ - Constraint, LayoutSolver, LayoutWorld, Positioner, -}; -use rectree::node::RectNode; -use rectree::{NodeId, Rectree}; +use kurbo::{Affine, Circle, Point, Rect, Size as KSize, Stroke}; +use rectree::layout::{LayoutWorld, Positioner}; +use rectree::{Constraint, NodeId, RectNode, Rectree, Size, Vec2}; use vello::Scene; use vello::peniko::Color; use vello::peniko::color::palette::css; @@ -16,11 +13,12 @@ use winit::event_loop::EventLoop; fn main() { let event_loop = EventLoop::new().unwrap(); let mut demo = LayoutDemo::new(); + let root_size = demo.window_size; let mut builder = demo.builder(); 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,7 +45,8 @@ fn main() { }) }; - let root_id = FixedSizeWidget::new(builder.demo.window_size) + let root_id = + FixedSizeWidget::new(root_size) .show_with_child(&mut builder, |b| { PlaceWidget::new(Alignment::Both { h: HAlign::Center, @@ -56,7 +55,7 @@ fn main() { .show(b, |b| { Padding::all(20.0).show(b, |b| { Vertical::new(20.0).show(b, |b| { - const HEIGHT: f64 = 60.0; + const HEIGHT: f32 = 60.0; vec![ Horizontal::new(50.0).show(b, |b| { vec![ @@ -110,14 +109,43 @@ impl World { } impl LayoutWorld for World { - fn get_solver(&self, id: &NodeId) -> &dyn LayoutSolver { - &**self.widgets.get(id).unwrap() + fn constraint( + &self, + id: &NodeId, + parent: Constraint, + ) -> Constraint { + self.widgets + .get(id) + .map(|w| w.constraint(parent)) + .unwrap_or(parent) + } + + fn build( + &self, + id: &NodeId, + node: &RectNode, + tree: &Rectree, + pos: &mut Positioner, + ) -> Size { + self.widgets + .get(id) + .map(|w| w.build(node, tree, pos)) + .unwrap_or(Size::ZERO) } } -pub trait Widget: LayoutSolver + Any {} +pub trait Widget: Any { + fn constraint(&self, parent: Constraint) -> Constraint { + parent + } -impl Widget for T where T: LayoutSolver + Any {} + fn build( + &self, + node: &RectNode, + tree: &Rectree, + positioner: &mut Positioner, + ) -> Size; +} pub struct LayoutDemo { tree: Rectree, @@ -178,13 +206,18 @@ impl LayoutDemo { // Get node from tree. let node = self.tree.get(&node_id); - // Get world_translation. + // Convert layout types to kurbo for rendering. let world_pos = node.world_translation(); - - // Reconstruct rect from world pos and size. + let size = node.size(); let world_rect = Rect::from_origin_size( - world_pos.to_point(), - node.size(), + Point::new( + world_pos.x as f64, + world_pos.y as f64, + ), + KSize::new( + size.width as f64, + size.height as f64, + ), ); // Hack to get the color of `FixedSizeWidget`. @@ -249,7 +282,10 @@ impl VelloDemo for LayoutDemo { } fn initial_logical_size(&self) -> (f64, f64) { - (self.window_size.width, self.window_size.height) + ( + self.window_size.width as f64, + self.window_size.height as f64, + ) } fn size_changed(&mut self, size: Size) { @@ -308,7 +344,7 @@ pub enum Alignment { Vertical(VAlign), } -/// Place the child widget in a certain alignment +/// Place the child widget in a certain alignment. pub struct PlaceWidget { pub alignment: Alignment, } @@ -330,7 +366,7 @@ impl PlaceWidget { } } -impl LayoutSolver for PlaceWidget { +impl Widget for PlaceWidget { fn build( &self, node: &RectNode, @@ -352,8 +388,9 @@ impl LayoutSolver for PlaceWidget { let mut should_position = false; if let Some(halign) = halign - && let Some(width) = constraint.width + && constraint.max.width.is_finite() { + let width = constraint.max.width; should_position = true; translation.x = match halign { HAlign::Left => 0.0, @@ -365,8 +402,9 @@ impl LayoutSolver for PlaceWidget { } if let Some(valign) = valign - && let Some(height) = constraint.height + && constraint.max.height.is_finite() { + let height = constraint.max.height; should_position = true; translation.y = match valign { VAlign::Top => 0.0, @@ -390,13 +428,14 @@ impl LayoutSolver for PlaceWidget { /// [`HorizontalWidget`] builder. #[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, @@ -416,7 +455,7 @@ pub struct HorizontalWidget { pub children: Vec, } -impl LayoutSolver for HorizontalWidget { +impl Widget for HorizontalWidget { fn build( &self, _node: &RectNode, @@ -433,12 +472,12 @@ impl LayoutSolver for HorizontalWidget { positioner.set(*id, Vec2::new(x_cursor, 0.0)); x_cursor += child_size.width + self.style.spacing; - // Track the tallest child + // Track the tallest child. if child_size.height > max_height { max_height = child_size.height; } } - // Remove the last added spacing + // Remove the last added spacing. if !self.children.is_empty() { x_cursor -= self.style.spacing; } @@ -450,13 +489,14 @@ impl LayoutSolver for HorizontalWidget { /// [`VerticalWidget`] builder. #[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, @@ -476,7 +516,7 @@ pub struct VerticalWidget { pub children: Vec, } -impl LayoutSolver for VerticalWidget { +impl Widget for VerticalWidget { fn build( &self, _node: &RectNode, @@ -493,12 +533,12 @@ impl LayoutSolver for VerticalWidget { positioner.set(*id, Vec2::new(0.0, y_cursor)); y_cursor += child_size.height + self.style.spacing; - // Track the widest child + // Track the widest child. if child_size.width > max_width { max_width = child_size.width; } } - // Remove the last added spacing + // Remove the last added spacing. if !self.children.is_empty() { y_cursor -= self.style.spacing; } @@ -510,14 +550,14 @@ impl LayoutSolver for VerticalWidget { /// [`PaddingWidget`] builder. #[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, @@ -545,36 +585,20 @@ pub struct PaddingWidget { 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 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, @@ -591,7 +615,7 @@ impl LayoutSolver for PaddingWidget { let child_node = tree.get(&self.child); let child_size = child_node.size(); - // Position the child with the specified padding offsets + // Position the child with the specified padding offsets. positioner.set(self.child, Vec2::new(left, top)); Size::new( @@ -601,17 +625,17 @@ impl LayoutSolver for PaddingWidget { } } -/// A widget that forces a specific size that ignore parent constraints. +/// A widget that forces a specific size that ignores parent constraints. #[derive(Debug, Clone)] pub struct FixedSizeWidget { pub size: Size, pub color: Color, } -impl LayoutSolver for FixedSizeWidget { +impl Widget for FixedSizeWidget { fn constraint(&self, _parent: Constraint) -> Constraint { - // Fixed size yield fixed contraint. - Constraint::fixed(self.size.width, self.size.height) + // Fixed size yields a tight constraint. + Constraint::tight(self.size) } fn build( 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, + )); } } From 1b87d0369fc24da4bb619b82ee136c6dc73b8406 Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Tue, 31 Mar 2026 23:56:23 +0800 Subject: [PATCH 03/11] Refactor API, fix layout bugs, and document the crate/ - Rename traits to match crate name: Rectree, RectNodes, RectContext - Rename layout.rs to geom.rs to avoid ambiguity with the layout fn - Introduce RectContext as a restricted build-time view of RectNodes, preventing child size mutation during the build pass - Fix rebuild pass missing needs_reposition(), causing stale world translations after a child size change - Fix constrain() unconditionally clearing BUILT; now only clears on actual constraint change - Fix layout() early-exit to check all three NodeState flags via is_ready(), not just CONSTRAINED - Make constrain, build, build_up, and propagate_translation public - Rename rebuild() to build_up() for clarity - Rewrite all doc comments to comply with style rules: 70 char lines, no em-dashes, full stops, aligned Markdown tables, Self:: links - Add runnable README example with asserts - Fix README: update Core Concepts table, correct layout rules, add Three-Pass Layout section --- Cargo.lock | 8 - crates/rectree/Cargo.toml | 2 - crates/rectree/README.md | 125 ++- crates/rectree/src/geom.rs | 155 ++++ crates/rectree/src/layout.rs | 377 --------- crates/rectree/src/lib.rs | 536 ++++++++----- crates/rectree/src/node.rs | 240 +++--- .../examples/layout_basic.rs | 722 ++++++++++++------ 8 files changed, 1210 insertions(+), 955 deletions(-) create mode 100644 crates/rectree/src/geom.rs delete mode 100644 crates/rectree/src/layout.rs diff --git a/Cargo.lock b/Cargo.lock index 1e078f0..290dfac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1420,8 +1420,6 @@ name = "rectree" version = "0.1.0" dependencies = [ "bitflags 2.10.0", - "hashbrown 0.16.1", - "sparse_map", ] [[package]] @@ -1636,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 4039353..7088ee0 100644 --- a/crates/rectree/Cargo.toml +++ b/crates/rectree/Cargo.toml @@ -10,6 +10,4 @@ categories = ["gui", "data-structures", "no-std"] readme = "README.md" [dependencies] -sparse_map.workspace = true -hashbrown.workspace = true bitflags.workspace = true diff --git a/crates/rectree/README.md b/crates/rectree/README.md index 02a9a6d..aaff864 100644 --- a/crates/rectree/README.md +++ b/crates/rectree/README.md @@ -23,24 +23,126 @@ 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 | +| `RectContext` | 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 `RectContext`. + +## Example + +```rust +use std::collections::HashMap; +use rectree::{ + Constraint, RectContext, 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; + + fn children(&self, id: &Id) + -> impl IntoIterator + { + if *id == self.root { Some(&self.child) } else { None } + } + + // Pass the parent constraint to children unchanged. + fn constrain(&self, _: &Id, parent: Constraint) + -> Constraint + { + parent + } + + fn build>( + &self, + id: &Id, + constraint: Constraint, + _nodes: &mut N, + ) -> 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 +157,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..8125440 --- /dev/null +++ b/crates/rectree/src/geom.rs @@ -0,0 +1,155 @@ +/// 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() + } +} diff --git a/crates/rectree/src/layout.rs b/crates/rectree/src/layout.rs deleted file mode 100644 index c10e154..0000000 --- a/crates/rectree/src/layout.rs +++ /dev/null @@ -1,377 +0,0 @@ -use alloc::collections::btree_set::BTreeSet; -use alloc::vec; -use alloc::vec::Vec; - -use crate::node::RectNode; -use crate::{NodeId, Rectree}; - -/// A 2D size in resolved pixels. -#[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct Size { - pub width: f32, - pub height: f32, -} - -impl Size { - pub const ZERO: Self = Self::splat(0.0); - - pub const INFINITY: Self = Self::splat(f32::INFINITY); - - #[inline] - pub const fn new(width: f32, height: f32) -> Self { - Self { width, height } - } - - #[inline] - pub const fn splat(value: f32) -> Self { - Self::new(value, value) - } -} - -/// A 2D position or translation. -#[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct Vec2 { - pub x: f32, - pub y: f32, -} - -impl Vec2 { - pub const ZERO: Self = Self::splat(0.0); - - #[inline] - pub const fn new(x: f32, y: f32) -> Self { - Self { x, y } - } - - #[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) - } -} - -/// A min/max size constraint passed down the element tree. -/// -/// `max` fields set to [`f32::INFINITY`] indicate an unconstrained -/// axis. Use the constructor helpers [`Self::tight()`], -/// [`Self::loose()`], [`Self::unbounded()`] rather than constructing -/// directly where possible. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Constraint { - pub min: Size, - pub max: Size, -} - -impl Constraint { - /// Forces the child to be exactly `size`. - pub const fn tight(size: Size) -> Self { - Self { - min: size, - max: size, - } - } - - /// Child 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. - pub const fn unbounded() -> Self { - Self { - min: Size::ZERO, - max: Size::INFINITY, - } - } - - /// Bounded width, unbounded height - /// (e.g. vertical scroll container). - pub const fn fixed_width(width: f32) -> Self { - Self { - min: Size::ZERO, - max: Size { - width, - height: f32::INFINITY, - }, - } - } - - /// Bounded height, unbounded width - /// (e.g. horizontal scroll container). - 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. - 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 { - fn default() -> Self { - Self::unbounded() - } -} - -/// Callback interface for reading and writing child layout state -/// during [`crate::layout::LayoutWorld::build`]. -pub trait Layouter { - type Id; - - fn get_size(&self, id: &Self::Id) -> Size; - - fn set_position(&mut self, id: &Self::Id, position: Vec2); -} - -/// 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 parent_constraint = - self.get(&id).parent_constraint; - let constraint = - world.constraint(&id, 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 size = world.build( - &id, - 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 the layout logic for each node in the tree. -/// -/// Acts as the bridge between [`Rectree`] and the application's -/// element system. -pub trait LayoutWorld { - /// Computes the constraint this node propagates to its children. - /// - /// `parent` is the constraint imposed on this node by its own - /// parent. The return value is applied to each child before - /// their build pass. - fn constraint( - &self, - id: &NodeId, - parent: Constraint, - ) -> Constraint; - - /// Builds the layout for a node and returns its resolved size. - /// - /// Called bottom-up after constraints have been propagated. - /// Implementations may inspect the tree and assign child - /// translations via [`Positioner`]. - fn build( - &self, - id: &NodeId, - node: &RectNode, - tree: &Rectree, - pos: &mut Positioner, - ) -> Size; -} - -/// Collects child translations produced during layout construction. -/// -/// See [`LayoutWorld::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 [`LayoutWorld::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 } - } -} diff --git a/crates/rectree/src/lib.rs b/crates/rectree/src/lib.rs index cd793fa..4272bfc 100644 --- a/crates/rectree/src/lib.rs +++ b/crates/rectree/src/lib.rs @@ -3,218 +3,410 @@ extern crate alloc; -use core::fmt::{Display, Formatter}; -use core::ops::Deref; - -use alloc::collections::btree_set::BTreeSet; -use alloc::vec; -use hashbrown::HashSet; -use sparse_map::{Key, SparseMap}; - -pub use layout::{ - Constraint, DepthNode, Layouter, Positioner, Size, Vec2, -}; +pub use geom::{Constraint, Size, Vec2}; pub use node::{NodeState, RectNode}; -pub mod layout; +pub mod geom; pub mod node; -/// A hierarchical tree of rectangular layout nodes. +/// Tree structure and per-node layout logic for a rectree hierarchy. /// -/// `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`]. +/// `Rectree` is the read-only half of the layout split. It defines +/// how nodes are connected ([`Self::children`]) 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; + + /// Returns the direct children of `id` in layout order. + fn children( + &self, + id: &Self::Id, + ) -> impl IntoIterator; + + /// Derives the constraint this node passes to its children + /// from the constraint `parent` imposed on this node. /// - /// This uses a sparse map to provide stable identifiers while - /// allowing efficient insertion and removal. - nodes: SparseMap, - /// Nodes scheduled for relayout, ordered by depth. + /// 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. /// - /// Deeper nodes are processed first to ensure children are laid - /// out before their parents. - scheduled_relayout: BTreeSet, -} + /// Called top-down by [`constrain`]. + fn constrain( + &self, + id: &Self::Id, + parent: Constraint, + ) -> Constraint; -/// Builders. -impl Rectree { - /// Creates an empty [`Rectree`]. + /// Measures this node given `constraint` and the already-built + /// children, returning the node's resolved [`Size`]. /// - /// This is equivalent to calling [`Default::default`]. - pub fn new() -> Self { - Self::default() - } - - /// Inserts a node into the tree while keeping track of the - /// parent-child relationship. + /// Children are guaranteed to be fully built before this is + /// called (bottom-up ordering). The implementation may: + /// + /// - Read child sizes via `nodes.get_size(child_id)`. + /// - Write child local translations via + /// `nodes.set_translation(child_id, pos)`. /// - /// # Panics + /// It must not mutate child sizes. `nodes` is a [`RectContext`] + /// which intentionally limits access to reads and translation + /// writes only. /// - /// 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); - } - - self.scheduled_relayout - .insert(DepthNode::new(node.depth, id)); - - node - }); - - NodeId(key) + /// Called bottom-up by [`build`]. + fn build>( + &self, + id: &Self::Id, + constraint: Constraint, + nodes: &mut N, + ) -> Size; +} + +/// 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 +/// [`RectContext`] 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 +/// [`RectContext`]. +/// +/// This means you never implement `RectContext` by hand. Just +/// implement `RectNodes` and the restricted build-time view +/// comes for free. +impl RectContext 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) } - /// 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); - } - - self.remove_recursive(id); - return true; + fn set_translation(&mut self, id: &Self::Id, translation: Vec2) { + if let Some(n) = self.get_node_mut(id) { + n.translation = translation; } - - false } +} - /// Recursively removes a node and all of its descendants. +/// 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. +/// +/// `RectContext` is never implemented manually. Any type that +/// implements [`RectNodes`] gets `RectContext` for free through +/// a blanket impl in `lib.rs`. +pub trait RectContext { + type Id; + + /// Returns the resolved size of the node identified by `id`. /// - /// 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]; + /// Returns [`Size::ZERO`] if the id is not found. + fn get_size(&self, id: &Self::Id) -> Size; - while let Some(id) = child_stack.pop() { - let node = self.get(&id); + /// Sets the local translation of the node identified by `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); +} - child_stack.extend(node.children()); - self.nodes.remove(&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!"); + + let old_size = node.size; + let parent = node.parent_id; + + if node.state.is_ready() { + return; + } + + // 1. Constrain down the hierarchy. + constrain(tree, nodes, id, node.constraint); + + // 2. Build sizes up the hierarchy. + build(tree, nodes, id); + + 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); } + + // 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); } -/// 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) +/// 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, 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; + + 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(); + + n.constraint = parent; + // Constraint changed means the built size is now stale. + if !constraint_unchanged { + n.state.needs_rebuild(); + } } - /// 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) + // Derive this node's constraint from the parent's. + let constraint = tree.constrain(id, parent); + + // Propagate the resolved constraint down to children. + for child in tree.children(id) { + constrain(tree, nodes, child, constraint); } +} - /// 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.") - }) +/// 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, 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. - /// - /// # 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.") - }) + let constraint = node.constraint; + + for child in tree.children(id) { + build(tree, nodes, child); } - /// 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 + // 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. - /// - /// 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.") - }) +/// 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. - /// - /// 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.") - }) + if size != old_size + && let Some(ref parent_id) = parent + { + return build_up(tree, nodes, parent_id); } + + *id } -#[derive( - Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, -)] -pub struct NodeId(Key); +/// 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; + } -impl Deref for NodeId { - type Target = Key; + let world = parent_world + node.translation; - fn deref(&self) -> &Self::Target { - &self.0 + if let Some(n) = nodes.get_node_mut(id) { + n.world_translation = world; + n.state.has_repositioned(); } -} -impl Display for NodeId { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - f.write_fmt(format_args!("NodeId({})", self.0)) + for child in tree.children(id) { + propagate_translation(tree, nodes, child, world); } } diff --git a/crates/rectree/src/node.rs b/crates/rectree/src/node.rs index 1b33118..ac7bdd5 100644 --- a/crates/rectree/src/node.rs +++ b/crates/rectree/src/node.rs @@ -1,191 +1,159 @@ use bitflags::bitflags; -use hashbrown::HashSet; -use crate::NodeId; -use crate::layout::{Constraint, Size, Vec2}; +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: Vec2) -> Self { - Self::new().with_translation(translation) - } - - pub fn from_size(size: Size) -> Self { - Self::new().with_size(size) - } - - pub fn from_translation_size( - translation: Vec2, - size: Size, - ) -> Self { - Self::new().with_translation(translation).with_size(size) - } - - pub fn with_translation(mut self, translation: Vec2) -> Self { - self.translation = translation; - self - } - - pub fn with_size(mut self, size: Size) -> Self { - self.size = size; - 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::LayoutWorld::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::LayoutWorld::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 - } + /// Written by [`crate::constrain`]; read by [`crate::build`]. + pub constraint: Constraint, - /// Parent node in the hierarchy, if any. - pub fn parent(&self) -> Option { - self.parent - } + /// 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, - /// Child nodes of this node. - pub fn children(&self) -> &HashSet { - &self.children - } + /// Local translation relative to the parent node's origin. + /// + /// Written by the parent's build step via + /// `RectContext::set_translation`. Zero by default. + pub translation: Vec2, - /// How deep in the hierarchy is this node (0 for root nodes). + /// Absolute world-space position of this 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 [`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); } diff --git a/examples/vello_winit_examples/examples/layout_basic.rs b/examples/vello_winit_examples/examples/layout_basic.rs index 9aa9b97..3446d6e 100644 --- a/examples/vello_winit_examples/examples/layout_basic.rs +++ b/examples/vello_winit_examples/examples/layout_basic.rs @@ -1,9 +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, Point, Rect, Size as KSize, Stroke}; -use rectree::layout::{LayoutWorld, Positioner}; -use rectree::{Constraint, NodeId, RectNode, Rectree, Size, Vec2}; +use rectree::{ + Constraint, RectContext, RectNode, RectNodes, Rectree, Size, + Vec2, layout, +}; use vello::Scene; use vello::peniko::Color; use vello::peniko::color::palette::css; @@ -13,9 +52,11 @@ use winit::event_loop::EventLoop; fn main() { let event_loop = EventLoop::new().unwrap(); let mut demo = LayoutDemo::new(); - let root_size = demo.window_size; + 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: f32 = 200.0; @@ -45,71 +86,199 @@ fn main() { }) }; - let root_id = - FixedSizeWidget::new(root_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: 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), - ] + // 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(); } +// --------------------------------------------------------------------------- +// ID type +// --------------------------------------------------------------------------- + +/// 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); + +// --------------------------------------------------------------------------- +// Node storage (`N: LayoutNode`) +// --------------------------------------------------------------------------- + +/// 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 Nodes { + fn new() -> Self { + Self { + data: HashMap::new(), + next_id: 0, + window_size: Size::new(800.0, 600.0), + } + } + + /// 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(); + } + } +} + +/// 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; + + fn get_node(&self, id: &NodeId) -> Option<&RectNode> { + self.data.get(id) + } + + fn get_node_mut( + &mut self, + id: &NodeId, + ) -> Option<&mut RectNode> { + self.data.get_mut(id) + } +} + +// --------------------------------------------------------------------------- +// Tree structure + widget logic (`T: LayoutTree`) +// --------------------------------------------------------------------------- + +/// The read-only half of the layout split. +/// +/// `World` owns: +/// - the widget instances (their logic), +/// - the parent→children mapping (tree structure). +/// +/// 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>, + /// Maps every node to its ordered list of children. + children: HashMap>, + /// Nodes that have no parent (typically one: the window root). + roots: Vec, } impl World { fn new() -> Self { Self { widgets: HashMap::new(), + children: HashMap::new(), + roots: Vec::new(), + } + } + + /// Register a new node in the tree. Called by [`Builder`] + /// immediately after `Nodes::insert` so both halves stay in + /// sync. + fn add_node(&mut self, id: NodeId, parent: Option) { + self.children.entry(id).or_default(); + if let Some(p) = parent { + self.children.entry(p).or_default().push(id); + } else { + self.roots.push(id); } } } -impl LayoutWorld for World { - fn constraint( +/// 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; + + /// Returns the children of `id` in insertion order. + fn children<'a>( + &'a self, + id: &NodeId, + ) -> impl IntoIterator { + self.children.get(id).map(|v| v.as_slice()).unwrap_or(&[]) + } + + /// 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, parent: Constraint, @@ -120,109 +289,126 @@ impl LayoutWorld for World { .unwrap_or(parent) } - fn build( + /// 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`. + /// + /// Note: `nodes` is a [`Layouter`], not a full [`LayoutNode`]. + /// Widgets can *read* child sizes and *write* child translations + /// — but cannot mutate child sizes directly. + fn build>( &self, id: &NodeId, - node: &RectNode, - tree: &Rectree, - pos: &mut Positioner, + constraint: Constraint, + nodes: &mut N, ) -> Size { self.widgets .get(id) - .map(|w| w.build(node, tree, pos)) + .map(|w| w.build(constraint, nodes)) .unwrap_or(Size::ZERO) } } +// --------------------------------------------------------------------------- +// Widget trait +// --------------------------------------------------------------------------- + +/// 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 build( &self, - node: &RectNode, - tree: &Rectree, - positioner: &mut Positioner, + constraint: Constraint, + nodes: &mut dyn RectContext, ) -> Size; } +// --------------------------------------------------------------------------- +// LayoutDemo +// --------------------------------------------------------------------------- + pub struct LayoutDemo { - tree: Rectree, + /// Read-only tree: widget logic + parent-child relationships. world: World, - window_size: Size, + /// Mutable storage: per-node layout numbers. + nodes: Nodes, + /// The root `NodeId`; stored so `size_changed` can update it. root_id: Option, } -pub struct Builder<'a> { - pub demo: &'a mut LayoutDemo, - pub parent_id: Option, -} - -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); - } - 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); - - id - } -} - 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, } } + /// 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) { + for root in self.world.roots.iter() { + // Reset the root so the passes start fresh. Children + // are only re-processed when their constraint or size + // actually changes, thanks to `NodeState` guards. + 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) { - // Start traversal from the root IDs provided by the tree. - for root_id in self.tree.root_ids() { + for root_id in &self.world.roots { + // Iterative DFS using a stack to avoid recursion limits. let mut stack = vec![*root_id]; while let Some(node_id) = stack.pop() { - // Get node from tree. - let node = self.tree.get(&node_id); + let Some(node) = self.nodes.get_node(&node_id) else { + continue; + }; - // Convert layout types to kurbo for rendering. - let world_pos = node.world_translation(); - let size = node.size(); + // `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, - ), + KSize::new(size.width as f64, size.height as f64), ); - // Hack to get the color of `FixedSizeWidget`. - // In real world scenario, you would want to - // implement a `draw` method for your `Widget` trait. + // 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| { @@ -242,6 +428,7 @@ impl LayoutDemo { ); } + // White border shows the layout box of every node. scene.stroke( &Stroke::new(2.0), transform, @@ -250,9 +437,8 @@ impl LayoutDemo { &world_rect, ); - // Origin markers. + // 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, @@ -261,9 +447,12 @@ impl LayoutDemo { &origin, ); - // Traverse to children. - for child_id in node.children().iter() { - stack.push(*child_id); + if let Some(children) = + self.world.children.get(&node_id) + { + for child_id in children { + stack.push(*child_id); + } } } } @@ -283,28 +472,28 @@ impl VelloDemo for LayoutDemo { fn initial_logical_size(&self) -> (f64, f64) { ( - self.window_size.width as f64, - self.window_size.height as f64, + 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); } } @@ -313,15 +502,57 @@ 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! +// --------------------------------------------------------------------------- +// Builder +// --------------------------------------------------------------------------- + +/// 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); + self.world.add_node(id, 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 + } +} + +// --------------------------------------------------------------------------- +// Demo widgets +// --------------------------------------------------------------------------- #[derive(Debug, Clone, Copy)] pub enum HAlign { @@ -333,6 +564,7 @@ pub enum HAlign { #[derive(Debug, Clone, Copy)] pub enum VAlign { Top, + /// Vertically centered. Horizon, Bottom, } @@ -344,24 +576,25 @@ 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 } }) } } @@ -369,63 +602,47 @@ impl PlaceWidget { impl Widget for PlaceWidget { fn build( &self, - node: &RectNode, - tree: &Rectree, - positioner: &mut Positioner, + constraint: Constraint, + nodes: &mut dyn RectContext, ) -> 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 - && constraint.max.width.is_finite() - { - let width = constraint.max.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 - && constraint.max.height.is_finite() - { - let height = constraint.max.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: f32, @@ -448,7 +665,10 @@ 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, @@ -458,35 +678,35 @@ pub struct HorizontalWidget { impl Widget for HorizontalWidget { fn build( &self, - _node: &RectNode, - tree: &Rectree, - positioner: &mut Positioner, + constraint: Constraint, + nodes: &mut dyn RectContext, ) -> 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: f32, @@ -509,7 +729,7 @@ impl Vertical { } } -/// Vertical layout widget. +/// Lays out children top-to-bottom with uniform spacing. #[derive(Debug, Clone)] pub struct VerticalWidget { pub style: Vertical, @@ -519,35 +739,29 @@ pub struct VerticalWidget { impl Widget for VerticalWidget { fn build( &self, - _node: &RectNode, - tree: &Rectree, - positioner: &mut Positioner, + constraint: Constraint, + nodes: &mut dyn RectContext, ) -> 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: f32, @@ -578,7 +792,13 @@ 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, @@ -586,6 +806,8 @@ pub struct PaddingWidget { } impl Widget for PaddingWidget { + /// 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; @@ -601,53 +823,37 @@ impl Widget for PaddingWidget { fn build( &self, - _node: &RectNode, - tree: &Rectree, - positioner: &mut Positioner, + _constraint: Constraint, + nodes: &mut dyn RectContext, ) -> 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 ignores 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 Widget for FixedSizeWidget { - fn constraint(&self, _parent: Constraint) -> Constraint { - // Fixed size yields a tight constraint. - Constraint::tight(self.size) - } - - fn build( - &self, - _node: &RectNode, - _tree: &Rectree, - _positioner: &mut Positioner, - ) -> Size { - self.size - } -} - impl FixedSizeWidget { pub fn new(size: Size) -> Self { Self { @@ -661,10 +867,13 @@ impl FixedSizeWidget { self } + /// Show as a leaf node (no children). pub fn show(self, b: &mut Builder) -> NodeId { b.add_widget(|_| self) } + /// Show with an inner subtree; the children are built by + /// `add_content` before the widget is constructed. pub fn show_with_child( self, b: &mut Builder, @@ -676,3 +885,20 @@ impl FixedSizeWidget { }) } } + +impl Widget for FixedSizeWidget { + /// 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 dyn RectContext, + ) -> Size { + self.size + } +} From fa7d5741ab8c9e78f569d39ecd023071301ec14d Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:03:06 +0800 Subject: [PATCH 04/11] Update README.md --- crates/rectree/README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/rectree/README.md b/crates/rectree/README.md index aaff864..7186116 100644 --- a/crates/rectree/README.md +++ b/crates/rectree/README.md @@ -23,15 +23,15 @@ Rectree is designed to be: ## Core Concepts -| Type / Trait | Role | -| --------------- | ---------------------------------------------- | -| `Rectree` | tree structure and per-node layout logic | -| `RectNodes` | flat mutable storage for per-node numbers | -| `RectContext` | 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 | +| Type / Trait | Role | +| ----------------- | ---------------------------------------------- | +| [`Rectree`] | tree structure and per-node layout logic | +| [`RectNodes`] | flat mutable storage for per-node numbers | +| [`RectContext`] | 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 From e0d39bb4556cd9ef262f38d38893ce0a2dc017c8 Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:49:37 +0800 Subject: [PATCH 05/11] Move `Rectree::build()` generic to the `Rectree` trait --- crates/rectree/README.md | 8 ++--- crates/rectree/src/lib.rs | 31 +++++++++++-------- crates/rectree/src/node.rs | 2 +- .../examples/layout_basic.rs | 20 ++++++------ 4 files changed, 33 insertions(+), 28 deletions(-) diff --git a/crates/rectree/README.md b/crates/rectree/README.md index 7186116..8891ba2 100644 --- a/crates/rectree/README.md +++ b/crates/rectree/README.md @@ -27,7 +27,7 @@ Rectree is designed to be: | ----------------- | ---------------------------------------------- | | [`Rectree`] | tree structure and per-node layout logic | | [`RectNodes`] | flat mutable storage for per-node numbers | -| [`RectContext`] | restricted build-time view of `RectNodes` | +| [`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 | @@ -59,14 +59,14 @@ that actually changed are reprocessed. 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 `RectContext`. + translations. This is enforced by `NodeContext`. ## Example ```rust use std::collections::HashMap; use rectree::{ - Constraint, RectContext, RectNode, RectNodes, + Constraint, NodeContext, RectNode, RectNodes, Rectree, Size, layout, }; @@ -110,7 +110,7 @@ impl Rectree for Tree { parent } - fn build>( + fn build>( &self, id: &Id, constraint: Constraint, diff --git a/crates/rectree/src/lib.rs b/crates/rectree/src/lib.rs index 4272bfc..5ffee5a 100644 --- a/crates/rectree/src/lib.rs +++ b/crates/rectree/src/lib.rs @@ -18,6 +18,7 @@ pub mod node; /// [`RectNodes`]. pub trait Rectree { type Id; + type Nodes: NodeContext; /// Returns the direct children of `id` in layout order. fn children( @@ -50,16 +51,16 @@ pub trait Rectree { /// - Write child local translations via /// `nodes.set_translation(child_id, pos)`. /// - /// It must not mutate child sizes. `nodes` is a [`RectContext`] + /// 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>( + fn build( &self, id: &Self::Id, constraint: Constraint, - nodes: &mut N, + nodes: &mut Self::Nodes, ) -> Size; } @@ -72,7 +73,7 @@ pub trait Rectree { /// about tree structure or layout logic; those live in [`Rectree`]. /// /// Any type that implements `RectNodes` automatically implements -/// [`RectContext`] through a blanket impl. +/// [`NodeContext`] through a blanket impl. /// /// # Splitting storage from tree logic /// @@ -93,12 +94,12 @@ pub trait RectNodes { } /// Blanket impl: any [`RectNodes`] storage is automatically a -/// [`RectContext`]. +/// [`NodeContext`]. /// -/// This means you never implement `RectContext` by hand. Just +/// This means you never implement `NodeContext` by hand. Just /// implement `RectNodes` and the restricted build-time view /// comes for free. -impl RectContext for N { +impl NodeContext for N { type Id = N::Id; fn get_size(&self, id: &Self::Id) -> Size { @@ -124,10 +125,10 @@ impl RectContext for N { /// pass processes nodes bottom-up and a size written here would /// silently invalidate the ordering guarantee. /// -/// `RectContext` is never implemented manually. Any type that -/// implements [`RectNodes`] gets `RectContext` for free through +/// `NodeContext` is never implemented manually. Any type that +/// implements [`RectNodes`] gets `NodeContext` for free through /// a blanket impl in `lib.rs`. -pub trait RectContext { +pub trait NodeContext { type Id; /// Returns the resolved size of the node identified by `id`. @@ -173,7 +174,7 @@ pub trait RectContext { /// Panics if `id` is not present in `nodes`. pub fn layout< Id: Copy, - T: Rectree, + T: Rectree, N: RectNodes, >( tree: &T, @@ -287,7 +288,11 @@ pub fn constrain, N: RectNodes>( /// # Panics /// /// Panics if `id` is not present in `nodes`. -pub fn build, N: RectNodes>( +pub fn build< + Id, + T: Rectree, + N: RectNodes, +>( tree: &T, nodes: &mut N, id: &T::Id, @@ -334,7 +339,7 @@ pub fn build, N: RectNodes>( /// Panics if `id` is not present in `nodes`. pub fn build_up< Id: Copy, - T: Rectree, + T: Rectree, N: RectNodes, >( tree: &T, diff --git a/crates/rectree/src/node.rs b/crates/rectree/src/node.rs index ac7bdd5..4f4612e 100644 --- a/crates/rectree/src/node.rs +++ b/crates/rectree/src/node.rs @@ -36,7 +36,7 @@ pub struct RectNode { /// Local translation relative to the parent node's origin. /// /// Written by the parent's build step via - /// `RectContext::set_translation`. Zero by default. + /// `NodeContext::set_translation`. Zero by default. pub translation: Vec2, /// Absolute world-space position of this node's origin. diff --git a/examples/vello_winit_examples/examples/layout_basic.rs b/examples/vello_winit_examples/examples/layout_basic.rs index 3446d6e..b02696a 100644 --- a/examples/vello_winit_examples/examples/layout_basic.rs +++ b/examples/vello_winit_examples/examples/layout_basic.rs @@ -40,7 +40,7 @@ use std::any::Any; use hashbrown::HashMap; use kurbo::{Affine, Circle, Point, Rect, Size as KSize, Stroke}; use rectree::{ - Constraint, RectContext, RectNode, RectNodes, Rectree, Size, + Constraint, NodeContext, RectNode, RectNodes, Rectree, Size, Vec2, layout, }; use vello::Scene; @@ -266,6 +266,7 @@ impl World { /// build passes. impl Rectree for World { type Id = NodeId; + type Nodes = Nodes; /// Returns the children of `id` in insertion order. fn children<'a>( @@ -293,14 +294,13 @@ impl Rectree for World { /// Children have already been built by the time this is called /// (bottom-up order), so their sizes are available via `nodes`. /// - /// Note: `nodes` is a [`Layouter`], not a full [`LayoutNode`]. /// Widgets can *read* child sizes and *write* child translations /// — but cannot mutate child sizes directly. - fn build>( + fn build( &self, id: &NodeId, constraint: Constraint, - nodes: &mut N, + nodes: &mut Nodes, ) -> Size { self.widgets .get(id) @@ -329,7 +329,7 @@ pub trait Widget: Any { fn build( &self, constraint: Constraint, - nodes: &mut dyn RectContext, + nodes: &mut Nodes, ) -> Size; } @@ -603,7 +603,7 @@ impl Widget for PlaceWidget { fn build( &self, constraint: Constraint, - nodes: &mut dyn RectContext, + nodes: &mut Nodes, ) -> Size { let child_size = nodes.get_size(&self.child); @@ -679,7 +679,7 @@ impl Widget for HorizontalWidget { fn build( &self, constraint: Constraint, - nodes: &mut dyn RectContext, + nodes: &mut Nodes, ) -> Size { let mut height = 0.0; let mut width = 0.0; @@ -740,7 +740,7 @@ impl Widget for VerticalWidget { fn build( &self, constraint: Constraint, - nodes: &mut dyn RectContext, + nodes: &mut Nodes, ) -> Size { let mut width = 0.0; let mut height = 0.0; @@ -824,7 +824,7 @@ impl Widget for PaddingWidget { fn build( &self, _constraint: Constraint, - nodes: &mut dyn RectContext, + nodes: &mut Nodes, ) -> Size { let child_size = nodes.get_size(&self.child); @@ -897,7 +897,7 @@ impl Widget for FixedSizeWidget { fn build( &self, _constraint: Constraint, - _nodes: &mut dyn RectContext, + _nodes: &mut Nodes, ) -> Size { self.size } From cf7e3b367bc401f64c661403a6011688189ac851 Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:57:16 +0800 Subject: [PATCH 06/11] Fix `README` example --- crates/rectree/README.md | 5 +++-- crates/rectree/src/lib.rs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/rectree/README.md b/crates/rectree/README.md index 8891ba2..0c9a8ed 100644 --- a/crates/rectree/README.md +++ b/crates/rectree/README.md @@ -96,6 +96,7 @@ struct Tree { root: Id, child: Id } impl Rectree for Tree { type Id = Id; + type Nodes = Store; fn children(&self, id: &Id) -> impl IntoIterator @@ -110,11 +111,11 @@ impl Rectree for Tree { parent } - fn build>( + fn build( &self, id: &Id, constraint: Constraint, - _nodes: &mut N, + _nodes: &mut Self::Nodes, ) -> Size { if *id == self.child { Size::new(100.0, 50.0) diff --git a/crates/rectree/src/lib.rs b/crates/rectree/src/lib.rs index 5ffee5a..80c9719 100644 --- a/crates/rectree/src/lib.rs +++ b/crates/rectree/src/lib.rs @@ -24,7 +24,7 @@ pub trait Rectree { fn children( &self, id: &Self::Id, - ) -> impl IntoIterator; + ) -> impl IntoIterator; /// Derives the constraint this node passes to its children /// from the constraint `parent` imposed on this node. From 6d2bd656e5f33b032e7272ea7bf3b56a58d9560f Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:01:09 +0800 Subject: [PATCH 07/11] Revert changes --- crates/rectree/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rectree/src/lib.rs b/crates/rectree/src/lib.rs index 80c9719..5ffee5a 100644 --- a/crates/rectree/src/lib.rs +++ b/crates/rectree/src/lib.rs @@ -24,7 +24,7 @@ pub trait Rectree { fn children( &self, id: &Self::Id, - ) -> impl IntoIterator; + ) -> impl IntoIterator; /// Derives the constraint this node passes to its children /// from the constraint `parent` imposed on this node. From d38876cfa8428bafc28dc059c6dcb6c9912cefb0 Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Wed, 1 Apr 2026 23:23:06 +0800 Subject: [PATCH 08/11] Changed API from `children` to `for_each_child` --- crates/rectree/README.md | 15 +- crates/rectree/src/lib.rs | 229 ++++++++++++++++- .../examples/layout_basic.rs | 231 ++++++++---------- 3 files changed, 330 insertions(+), 145 deletions(-) diff --git a/crates/rectree/README.md b/crates/rectree/README.md index 0c9a8ed..fb37933 100644 --- a/crates/rectree/README.md +++ b/crates/rectree/README.md @@ -98,14 +98,19 @@ impl Rectree for Tree { type Id = Id; type Nodes = Store; - fn children(&self, id: &Id) - -> impl IntoIterator - { - if *id == self.root { Some(&self.child) } else { None } + 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, parent: Constraint) + fn constrain(&self, _: &Id, _nodes: &Store, parent: Constraint) -> Constraint { parent diff --git a/crates/rectree/src/lib.rs b/crates/rectree/src/lib.rs index 5ffee5a..2d998bf 100644 --- a/crates/rectree/src/lib.rs +++ b/crates/rectree/src/lib.rs @@ -12,7 +12,7 @@ pub mod node; /// 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::children`]) and how each node +/// 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`]. @@ -20,11 +20,19 @@ pub trait Rectree { type Id; type Nodes: NodeContext; - /// Returns the direct children of `id` in layout order. - fn children( + /// 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, - ) -> impl IntoIterator; + nodes: &mut Self::Nodes, + f: impl FnMut(&Self::Id, &mut Self::Nodes), + ); /// Derives the constraint this node passes to its children /// from the constraint `parent` imposed on this node. @@ -34,10 +42,15 @@ pub trait Rectree { /// 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; @@ -236,7 +249,11 @@ pub fn layout< /// # Panics /// /// Panics if `id` is not present in `nodes`. -pub fn constrain, N: RectNodes>( +pub fn constrain< + Id, + T: Rectree, + N: RectNodes, +>( tree: &T, nodes: &mut N, id: &T::Id, @@ -263,12 +280,12 @@ pub fn constrain, N: RectNodes>( } // Derive this node's constraint from the parent's. - let constraint = tree.constrain(id, parent); + let constraint = tree.constrain(id, nodes, parent); // Propagate the resolved constraint down to children. - for child in tree.children(id) { + tree.for_each_child(id, nodes, |child, nodes| { constrain(tree, nodes, child, constraint); - } + }); } /// Recursively builds the layout tree bottom-up. @@ -306,9 +323,9 @@ pub fn build< let constraint = node.constraint; - for child in tree.children(id) { + 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); @@ -389,7 +406,7 @@ pub fn build_up< /// Panics if `id` is not present in `nodes`. pub fn propagate_translation< Id, - T: Rectree, + T: Rectree, N: RectNodes, >( tree: &T, @@ -411,7 +428,195 @@ pub fn propagate_translation< n.state.has_repositioned(); } - for child in tree.children(id) { + tree.for_each_child(id, nodes, |child, nodes| { propagate_translation(tree, nodes, child, world); + }); +} + +#[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() { + let mut tree = WidgetTree::default(); + + let id0 = 0; + let id1 = 1; + let id2 = 2; + + tree.add_column(id0, None, vec![id1]); + tree.add_column(id1, Some(id0), vec![id2]); + tree.add_fixed(id2, Some(id1), Size::splat(10.0)); + + tree.layout(&id0); + + 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!(tree.nodes.0[&id0].state.is_ready()); + + // On second layout, nothing should be rebuilt. + tree.layout(&id0); + + 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); + } + + 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), + } + + /// General-purpose test tree. + /// + /// - [`Rectree::constrain`] always passes the parent constraint + /// through unchanged. + /// - [`Rectree::build`] returns a fixed size when one is set for + /// the node; for containers it sums children widths and takes + /// the maximum height; for leaves it fills `constraint.max`. + /// - `build_calls` counts every invocation for incremental tests. + #[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); + 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/examples/vello_winit_examples/examples/layout_basic.rs b/examples/vello_winit_examples/examples/layout_basic.rs index b02696a..1a0de45 100644 --- a/examples/vello_winit_examples/examples/layout_basic.rs +++ b/examples/vello_winit_examples/examples/layout_basic.rs @@ -130,7 +130,7 @@ fn main() { }) }) }, - ); + ) }, ); @@ -143,10 +143,6 @@ fn main() { event_loop.run_app(&mut app).unwrap(); } -// --------------------------------------------------------------------------- -// ID type -// --------------------------------------------------------------------------- - /// Opaque handle that identifies a single node. /// /// Must be `Copy + Eq + Hash` so rectree can use it as a map key @@ -156,10 +152,6 @@ fn main() { )] pub struct NodeId(u32); -// --------------------------------------------------------------------------- -// Node storage (`N: LayoutNode`) -// --------------------------------------------------------------------------- - /// Flat storage for every node's layout data. /// /// This is the mutable half of the layout split. It only holds @@ -218,45 +210,22 @@ impl RectNodes for Nodes { } } -// --------------------------------------------------------------------------- -// Tree structure + widget logic (`T: LayoutTree`) -// --------------------------------------------------------------------------- - /// The read-only half of the layout split. /// /// `World` owns: -/// - the widget instances (their logic), -/// - the parent→children mapping (tree structure). +/// - 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>, - /// Maps every node to its ordered list of children. - children: HashMap>, - /// Nodes that have no parent (typically one: the window root). - roots: Vec, } impl World { fn new() -> Self { Self { widgets: HashMap::new(), - children: HashMap::new(), - roots: Vec::new(), - } - } - - /// Register a new node in the tree. Called by [`Builder`] - /// immediately after `Nodes::insert` so both halves stay in - /// sync. - fn add_node(&mut self, id: NodeId, parent: Option) { - self.children.entry(id).or_default(); - if let Some(p) = parent { - self.children.entry(p).or_default().push(id); - } else { - self.roots.push(id); } } } @@ -268,12 +237,16 @@ impl Rectree for World { type Id = NodeId; type Nodes = Nodes; - /// Returns the children of `id` in insertion order. - fn children<'a>( - &'a self, + /// Calls `f` for each child of `id` in insertion order. + fn for_each_child( + &self, id: &NodeId, - ) -> impl IntoIterator { - self.children.get(id).map(|v| v.as_slice()).unwrap_or(&[]) + 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 @@ -282,6 +255,7 @@ impl Rectree for World { fn constrain( &self, id: &NodeId, + _nodes: &Nodes, parent: Constraint, ) -> Constraint { self.widgets @@ -309,10 +283,6 @@ impl Rectree for World { } } -// --------------------------------------------------------------------------- -// Widget trait -// --------------------------------------------------------------------------- - /// A widget defines *how* a node behaves during layout. /// /// - [`constraint`](Widget::constraint): narrows the parent's @@ -326,6 +296,8 @@ pub trait Widget: Any { parent } + fn for_each_child(&self, _f: &mut dyn FnMut(&NodeId)) {} + fn build( &self, constraint: Constraint, @@ -333,10 +305,6 @@ pub trait Widget: Any { ) -> Size; } -// --------------------------------------------------------------------------- -// LayoutDemo -// --------------------------------------------------------------------------- - pub struct LayoutDemo { /// Read-only tree: widget logic + parent-child relationships. world: World, @@ -370,12 +338,9 @@ impl LayoutDemo { /// via `NodeState` flags — so re-calling this every frame is /// cheap when nothing changed. fn layout(&mut self) { - for root in self.world.roots.iter() { - // Reset the root so the passes start fresh. Children - // are only re-processed when their constraint or size - // actually changes, thanks to `NodeState` guards. - self.nodes.get_node_mut(root).unwrap().state.reset(); - layout(&self.world, &mut self.nodes, root); + if let Some(root) = self.root_id { + self.nodes.get_node_mut(&root).unwrap().state.reset(); + layout(&self.world, &mut self.nodes, &root); } } @@ -386,74 +351,66 @@ impl LayoutDemo { /// visible. /// - A small red dot marks each node's origin point. fn draw_tree(&self, scene: &mut Scene, transform: Affine) { - for root_id in &self.world.roots { - // 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, - color, - None, - &world_rect, - ); - } - - // White border shows the layout box of every node. - scene.stroke( - &Stroke::new(2.0), - transform, - Color::WHITE, - None, - &world_rect, - ); + 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), + ); - // Red dot at the node's top-left origin. - let origin = Circle::new(world_rect.origin(), 5.0); + // 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, ); + } + + // White border shows the layout box of every node. + scene.stroke( + &Stroke::new(2.0), + transform, + Color::WHITE, + None, + &world_rect, + ); - if let Some(children) = - self.world.children.get(&node_id) - { - for child_id in children { - stack.push(*child_id); - } - } + // 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); + }); } } } @@ -507,10 +464,6 @@ impl VelloDemo for LayoutDemo { } } -// --------------------------------------------------------------------------- -// Builder -// --------------------------------------------------------------------------- - /// Accumulates nodes into `World` and `Nodes` during tree /// construction. /// @@ -537,7 +490,6 @@ impl Builder<'_> { add_content: impl FnOnce(&mut Builder) -> W, ) -> NodeId { let id = self.nodes.insert(self.parent_id); - self.world.add_node(id, self.parent_id); let w = Box::new(add_content(&mut Builder { world: self.world, @@ -550,10 +502,6 @@ impl Builder<'_> { } } -// --------------------------------------------------------------------------- -// Demo widgets -// --------------------------------------------------------------------------- - #[derive(Debug, Clone, Copy)] pub enum HAlign { Left, @@ -600,6 +548,10 @@ impl PlaceWidget { } impl Widget for PlaceWidget { + fn for_each_child(&self, f: &mut dyn FnMut(&NodeId)) { + f(&self.child); + } + fn build( &self, constraint: Constraint, @@ -676,6 +628,12 @@ pub struct 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, constraint: Constraint, @@ -737,6 +695,12 @@ pub struct 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, constraint: Constraint, @@ -806,6 +770,10 @@ pub struct PaddingWidget { } 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 { @@ -852,6 +820,7 @@ impl Widget for PaddingWidget { pub struct FixedSizeWidget { pub size: Size, pub color: Color, + pub child: Option, } impl FixedSizeWidget { @@ -859,6 +828,7 @@ impl FixedSizeWidget { Self { size, color: Color::TRANSPARENT, + child: None, } } @@ -872,21 +842,26 @@ impl FixedSizeWidget { b.add_widget(|_| self) } - /// Show with an inner subtree; the children are built by - /// `add_content` before the widget is constructed. + /// 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. From 2859b0aa0a5f1835c2c833534a40e399033c475f Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:35:43 +0800 Subject: [PATCH 09/11] Add tests for `lib.rs` --- crates/rectree/src/lib.rs | 220 ++++++++++++++++++++++++++++++++++---- 1 file changed, 197 insertions(+), 23 deletions(-) diff --git a/crates/rectree/src/lib.rs b/crates/rectree/src/lib.rs index 2d998bf..f12db47 100644 --- a/crates/rectree/src/lib.rs +++ b/crates/rectree/src/lib.rs @@ -443,31 +443,209 @@ mod tests { use super::*; #[test] - fn test_layout() { + fn test_layout_full_pass() { let mut tree = WidgetTree::default(); - let id0 = 0; - let id1 = 1; - let id2 = 2; + 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.add_column(id0, None, vec![id1]); - tree.add_column(id1, Some(id0), vec![id2]); - tree.add_fixed(id2, Some(id1), Size::splat(10.0)); - - tree.layout(&id0); + 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!(tree.nodes.0[&id0].state.is_ready()); + 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)); - // On second layout, nothing should be rebuilt. - tree.layout(&id0); + assert!(tree.nodes.0[&0].state.is_ready()); + assert!(tree.nodes.0[&1].state.is_ready()); + assert!(tree.nodes.0[&2].state.is_ready()); - 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); + let fec = tree.tree.for_each_child_calls.get(); + let cc = tree.tree.constrain_calls.get(); + let bc = tree.tree.build_calls.get(); + + // 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); + } + + #[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; @@ -502,14 +680,6 @@ mod tests { Fixed(Size), } - /// General-purpose test tree. - /// - /// - [`Rectree::constrain`] always passes the parent constraint - /// through unchanged. - /// - [`Rectree::build`] returns a fixed size when one is set for - /// the node; for containers it sums children widths and takes - /// the maximum height; for leaves it fills `constraint.max`. - /// - `build_calls` counts every invocation for incremental tests. #[derive(Default)] struct Tree { widgets: BTreeMap, @@ -575,6 +745,10 @@ mod tests { 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; } From 15cd9683517f2d53f7792b3c736fc8c9ed260eb8 Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:39:32 +0800 Subject: [PATCH 10/11] Add tests for `NodeState` & `Constraint` --- crates/rectree/src/geom.rs | 40 ++++++++++++++++++++++++ crates/rectree/src/node.rs | 62 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/crates/rectree/src/geom.rs b/crates/rectree/src/geom.rs index 8125440..b01fe10 100644 --- a/crates/rectree/src/geom.rs +++ b/crates/rectree/src/geom.rs @@ -153,3 +153,43 @@ impl Default for Constraint { 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/node.rs b/crates/rectree/src/node.rs index 4f4612e..68abcb9 100644 --- a/crates/rectree/src/node.rs +++ b/crates/rectree/src/node.rs @@ -158,3 +158,65 @@ impl NodeState { 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()); + } +} From e22dc330b27f069dc8b6b3c518efe40935d0aa0a Mon Sep 17 00:00:00 2001 From: Nixon <43715558+nixonyh@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:45:08 +0800 Subject: [PATCH 11/11] Run cargo fmt --- crates/rectree/src/geom.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/rectree/src/geom.rs b/crates/rectree/src/geom.rs index b01fe10..3e4cd56 100644 --- a/crates/rectree/src/geom.rs +++ b/crates/rectree/src/geom.rs @@ -176,14 +176,20 @@ mod tests { #[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)); + 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)); + assert_eq!( + c.constrain(Size::INFINITY), + Size::new(30.0, 20.0) + ); } #[test]