Rectree proposes a simple concept towards user interfaces, that everything can be represented as a tree of axis-aligned bounding boxes (AABB). In Rectree, these are represented as rectangles, hence the name "rect-tree".
Rectree is designed to be:
- Deterministic: identical inputs always produce identical layouts.
- Incremental: only affected subtrees are recomputed.
- Policy-free: layout behavior is defined by user-provided algorithms.
| Type / Trait | Role |
|---|---|
[Rectree] |
tree structure and per-node layout logic |
[RectNodes] |
flat mutable storage for per-node numbers |
[NodeContext] |
restricted build-time view of RectNodes |
[RectNode] |
per-node data (size, constraint, translation) |
[NodeState] |
bitflags that short-circuit incremental passes |
[Constraint] |
min/max size bounds, flowing top-down |
[Size] |
resolved dimensions, flowing bottom-up |
Rectree itself does not impose a specific layout style (e.g. flexbox, grid). Instead, it provides a strict data-flow model on top of which layout algorithms can be built.
Each call to layout runs three passes in order:
- constrain (top-down): each node derives the constraint it
passes to its children via
Rectree::constrain. - build (bottom-up): each node measures itself given its
constraint and child sizes via
Rectree::build. - 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.
- Constraints flow strictly top-down (parent to child).
- Sizes flow strictly bottom-up (child to parent).
- A parent may pass a different constraint to each child.
- Given the same constraint, an unmodified node must always produce the same size.
- The build pass must not write child sizes, only child
translations. This is enforced by
NodeContext.
use std::collections::HashMap;
use rectree::{
Constraint, NodeContext, RectNode, RectNodes,
Rectree, Size, layout,
};
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct Id(u32);
// Flat node storage backed by a HashMap.
struct Store(HashMap<Id, RectNode<Id>>);
impl RectNodes for Store {
type Id = Id;
fn get_node(&self, id: &Id) -> Option<&RectNode<Id>> {
self.0.get(id)
}
fn get_node_mut(&mut self, id: &Id)
-> Option<&mut RectNode<Id>>
{
self.0.get_mut(id)
}
}
// Tree with one root and one child. The root fills its
// constraint; the child has a fixed 100x50 size.
struct Tree { root: Id, child: Id }
impl Rectree for Tree {
type Id = Id;
type Nodes = Store;
fn for_each_child(
&self,
id: &Id,
_nodes: &mut Store,
mut f: impl FnMut(&Id, &mut Store),
) {
if *id == self.root {
f(&self.child, _nodes);
}
}
// Pass the parent constraint to children unchanged.
fn constrain(&self, _: &Id, _nodes: &Store, parent: Constraint)
-> Constraint
{
parent
}
fn build(
&self,
id: &Id,
constraint: Constraint,
_nodes: &mut Self::Nodes,
) -> Size {
if *id == self.child {
Size::new(100.0, 50.0)
} else {
constraint.max
}
}
}
let root = Id(0);
let child = Id(1);
let tree = Tree { root, child };
let mut store = Store(HashMap::new());
store.0.insert(root, RectNode::new(None));
store.0.insert(child, RectNode::new(Some(root)));
// Constrain the root to an 800x600 window.
store.0.get_mut(&root).unwrap().constraint =
Constraint::tight(Size::new(800.0, 600.0));
layout(&tree, &mut store, &root);
// Child is placed at the origin by default.
assert_eq!(store.0[&child].world_translation.x, 0.0);
assert_eq!(store.0[&child].world_translation.y, 0.0);
assert_eq!(store.0[&child].size, Size::new(100.0, 50.0));You can join us on the Voxell discord server.
rectree is dual-licensed under either:
- MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
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 to include both.
