layuit 0.2.0

A UI layout library for Rust
Documentation
layuit-0.2.0 has been yanked.

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 the UiNodes 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 by UiTree::calculate_layout.
  • Rect: A rectangle in space, represented with f32 coordinates.
  • 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:

  1. Bottom-up: minimum size. Children are visited before their parent. Each node computes its minimum size based on its children through calculate_min_size and stores it in its [NodeCache::min_size].

  2. 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 restricted Rect to do the same for its children. The [NodeCache::rect] field is populated with the resulting Rects.

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:

  1. Children must be accurately reported. Failure to report children will result in them not being updated during UiTree::calculate_layout or removed during UiTree::remove_node.

  2. Minimum size must be correctly calculated. Under-representing the minimum size can and often will result in nodes overflowing into each other.

  3. 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 layuit::{Alignment, NodeCache, Rect, UiTree, UiNode};

pub struct Label {
    text: String,
    align: (Alignment, Alignment),
    cached_size: (f32, f32),
}

/* Label methods and constructors... */

impl UiNode for Label {
    fn get_align(&self) -> (Alignment, Alignment) { self.align }
    fn get_align_mut(&mut self) -> (&mut Alignment, &mut Alignment) {
        (&mut self.align.0, &mut self.align.1)
    }

    fn calculate_min_size(&mut self, _tree: &UiTree, _cache: &mut NodeCache) -> (f32, f32) {
        self.cached_size
    }

    // calculate_rects and get_children are omitted for leaf nodes
}

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 layuit::{UiTree, UiNode, NodeVisitor};
use layuit::stacks::HStack;

// The root node can be any UiNode, but must be specified.
let mut tree = UiTree::new(HStack::new().with_spacing(4.0));

// Create a label wrapped in a 4px margin
let padded_label = Margin::new()
    .with_margins(4.0, 4.0, 4.0, 4.0)
    .with_child(Label::new("Hello, world!"), &mut tree);

// Add the label to the root stack
tree.get_root_mut()
    .downcast_mut::<HStack>()
    .unwrap()
    .with_child(padded_label, &mut tree);

tree.calculate_layout(Rect::new(0.0, 0.0, 640.0, 480.0));

// Render the UI tree

struct Renderer {
    // ...
}

impl NodeVisitor for Renderer {
    fn visit(&mut self, node: &mut dyn UiNode, rect: layuit::Rect) {
        if let Some(label) = node.downcast_mut::<Label>() {
            // ...
        }
    }
}

let mut renderer = /* ... */;
tree.visit(&mut renderer);

Provided nodes

Containers:

  • HStack - Horizontal arrangement
  • VStack - Vertical arrangement
  • Overlap - Independent arrangement of children
  • Margin - Adds padding to a child
  • Minimum - Creates a minimum size constraint for precise control