Layuit
A renderer-agnostic UI layout system. Layuit handles computing the size and position of various
UiNodes in a UiTree. Layuit does not handle rendering, but provides tools for doing so.
Layuit provides several organizational nodes such as HStack and Margin, but allows users
to create their own nodes.
Layuit uses the thunderdome crate for the tree structure. To access nodes from a tree, use
thunderdome::Index.
Core concepts
UiTree: Owns theUiNodes and layout information in an arena and handles computation and access.UiNode: A trait implemented by all UI nodes, containing alignment and any number of children.NodeCache: The cached layout information for a node, produced byUiTree::calculate_layout.Rect: A rectangle in space, represented withf32coordinates.Alignment: An alignment primarily used for determining node placement.NodeVisitor: A trait implemented e.g. by renderers to process and/or manipulate nodes.
Layout process
Layout runs in two passes, when UiTree::calculate_layout is called:
-
Bottom-up: minimum size. Children are visited before their parent. Each node computes its minimum size based on its children through
calculate_min_sizeand stores it in its [NodeCache::min_size]. -
Top-down: rectangles. Starting from the root, each node computes the position and size of its immediate children through
calculate_rects. Each child then uses its restrictedRectto do the same for its children. The [NodeCache::rect] field is populated with the resultingRects.
Caveats
The cache is stale before UiTree::calculate_layout is called, and becomes stale if
children are added, removed, moved, or otherwise changed. The cache always produces valid
results, but they may be out of date or set to 0.
Minimum size is a practice, not a requirement. When implementing custom nodes, be wary of
ensuring each node's minimum size is enforced. This can easily become a problem if the space
required by the entire tree is smaller than the one provided to UiTree::calculate_layout.
Implementing custom nodes
Custom nodes are essential to using Layuit. Without them, no meaningful UI can be rendered. However, it is important to ensure you follow the rules:
-
Children must be accurately reported. Failure to report children will result in them not being updated during
UiTree::calculate_layoutor removed duringUiTree::remove_node. -
Minimum size must be correctly calculated. Under-representing the minimum size can and often will result in nodes overflowing into each other.
-
Rectangles must be properly assigned. Similar to #2, it is the responsibility of the parent node to ensure that each node get both enough space and not too much. Failing to do so will result in nodes overlapping.
One common custom node is the Label:
use ;
/* Label methods and constructors... */
Creating a tree
Every tree needs a root node, which cannot be removed. Good choices are Overlap and either
HStack or VStack. A custom node can also be used.
use ;
use HStack;
// The root node can be any UiNode, but must be specified.
let mut tree = new;
// Create a label wrapped in a 4px margin
let padded_label = new
.with_margins
.with_child;
// Add the label to the root stack
tree.get_root_mut
.
.unwrap
.with_child;
tree.calculate_layout;
// Render the UI tree
let mut renderer = /* ... */;
tree.visit;
Provided nodes
HStack- Horizontal arrangementVStack- Vertical arrangementOverlap- Independent arrangement of childrenMargin- Adds padding to a childMinimum- Creates a minimum size constraint for precise controlSpacer- A leaf node with configurable empty spaceClip- Allows a child to outgrow the node with the assumption that the renderer will clip it, and enables a scroll offset to be applied if the child is larger.AspectRatio- Maintains a horizontal:vertical ratioHSplit- Horizontal split between two childrenVSplit- Vertical split between two childrenPercent- Maintains a percentage of space for a childHEqual- Horizontal arrangement with each child getting equal spaceVEqual- Vertical arrangement with each child getting equal spaceGrid- 2D grid of children