ToffeeSourceHigh-performance CSS layout engine implementing flexbox, CSS grid, and block layout.
Toffee is an OCaml port of the Taffy layout library. It computes the spatial arrangement of UI nodes based on CSS layout algorithms, producing precise positions and dimensions for rendering.
Layout computation operates on a tree of nodes, where each node has a style (CSS properties like display, flex_direction, width) and optional children. The engine computes layouts by traversing the tree, applying layout algorithms based on the display property, and resolving dimensions under space constraints.
Nodes are identified by Node_id.t, which uses generational indices to prevent use-after-free errors. The tree parameterizes over a context type 'context for associating custom data with nodes.
The primary workflow: 1. Create a tree with new_tree. 2. Build the node hierarchy with new_leaf, new_with_children, and add_child. 3. Compute layout with compute_layout, providing available space constraints. 4. Query computed layouts with layout.
Create a simple flexbox layout:
let tree = Toffee.new_tree () in
(* Create a container with two children *)
let child1 =
Toffee.new_leaf tree
Style.(
make
~size:
Size.{ width = Dimension.px 100.; height = Dimension.px 50. }
())
|> Result.get_ok
in
let child2 =
Toffee.new_leaf tree Style.(make ~flex_grow:1.0 ()) |> Result.get_ok
in
let root =
Toffee.new_with_children tree
Style.(make ~display:Display.Flex ())
[| child1; child2 |]
|> Result.get_ok
in
(* Compute layout with 500x300 available space *)
Toffee.compute_layout tree root
Size.
{
width = Available_space.of_length 500.;
height = Available_space.of_length 300.;
}
|> Result.get_ok;
(* Query the computed layout *)
let root_layout = Toffee.layout tree root |> Result.get_ok in
Printf.printf "Root: %fx%f\n" root_layout.Layout.size.width
root_layout.size.heightToffee implements CSS layout algorithms selected via the display property:
Display.Flex: Flexbox layout (CSS Flexible Box Layout Module Level 1)Display.Grid: CSS Grid layout (CSS Grid Layout Module Level 1)Display.Block: Block layout (CSS Box Model)Display.None: The node and its descendants are excluded from layoutEach algorithm has distinct behavior for sizing, positioning, and alignment. Refer to Style for properties controlling each algorithm.
Layout computation requires Available_space.t constraints for width and height. These represent:
Definite n: Exactly n pixels available.Min_content: Shrink-wrap to minimum intrinsic size.Max_content: Expand to maximum intrinsic size.Intrinsic sizing depends on content and measure functions for leaf nodes.
Leaf nodes without children require measure functions to compute intrinsic dimensions based on content (text, images, etc.). The function receives:
known_dimensions: Dimensions already resolved (e.g., from explicit width/height).available_space: Space constraints from the parent.node_id: The node being measured.context: Custom data attached via new_leaf_with_context.style: The node's style.It returns the measured Geometry.size. The default measure function (used by compute_layout) returns zero size.
Example with text measurement:
type text_context = { content : string; font_size : float }
let measure known_dims available _node_id context _style =
match context with
| None -> Geometry.Size.zero
| Some { content; font_size } ->
(* Compute text dimensions based on font_size and content *)
let char_width = font_size *. 0.6 in
let char_count = String.length content in
Geometry.Size.{
width = float_of_int char_count *. char_width;
height = font_size;
}
in
let tree : text_context tree = new_tree () in
let text_node =
new_leaf_with_context tree Style.default
{ content = "Hello"; font_size = 16. }
|> Result.get_ok
in
compute_layout_with_measure tree text_node available measure
|> Result.get_okThe engine caches layout results to avoid redundant computation. Caches are invalidated automatically when styles change via set_style. Manual invalidation uses mark_dirty, which marks the node and all ancestors as dirty, triggering recomputation on the next compute_layout.
Layout values are floating-point. Enable rounding via enable_rounding (default) to round border box dimensions and positions to integer pixels, reducing sub-pixel rendering artifacts. Disable via disable_rounding for precise measurements (e.g., testing). Query rounded layouts with layout or unrounded with unrounded_layout.
Operations return ('a, Error.t) result. Errors occur when:
Error.t.Invalid_parent_node, etc.).Error.t.Child_index_out_of_bounds).All node identifiers are validated before use. Stale identifiers from removed nodes produce errors.
Prefer bulk operations like new_with_children and set_children over incremental add_child when constructing large subtrees.
Unique identifiers for tree nodes.
Computed layout data.
Space constraints for layout computation.
Input parameters for layout algorithms.
Output produced by layout algorithms.
Execution mode for layout computation (e.g., perform layout vs. compute intrinsic size).
Layout caching for performance.
('a, Error.t) result is the result type for tree operations.
Ok value on success; Error error when a node is invalid or an index is out of bounds.
'context tree represents a tree of layout nodes, parameterized by the type of custom data associated with each node.
Use unit tree if no custom data is needed. Associate context via new_leaf_with_context and query via get_node_context.
new_tree () creates an empty tree with default configuration.
Rounding is enabled by default. The tree initially has zero nodes.
with_capacity n creates an empty tree with pre-allocated capacity for n nodes.
This avoids reallocation when the node count is known in advance. The tree grows automatically if n is exceeded.
config holds layout computation options.
use_rounding: Round layout values to integers if true.enable_rounding tree sets rounding to enabled.
Layout values (positions and sizes) are rounded to the nearest integer pixel. This is the default and recommended for rendering to avoid sub-pixel artifacts.
disable_rounding tree sets rounding to disabled.
Layout values remain as computed floating-point values. Useful for testing or when precise fractional layouts are required.
new_leaf tree style creates a leaf node with style and no children.
Returns Ok node_id on success. Leaf nodes require measure functions during layout computation to determine intrinsic size.
new_leaf_with_context tree style context creates a leaf node with style and associates context data.
The context is accessible in measure functions via the context parameter.
new_with_children tree style children creates a node with style and children.
The node becomes the parent of all children. Returns Error (Invalid_child_node id) if any child id is invalid.
clear tree removes all nodes from tree.
After clear, all previously created node identifiers become invalid. The tree's capacity is preserved.
remove tree node_id removes node_id and all its descendants from tree.
Returns Ok node_id on success. The removed node_id and all descendant identifiers become invalid. Returns Error (Invalid_input_node node_id) if node_id does not exist.
total_node_count tree returns the number of nodes currently in tree.
Includes all nodes (roots, internal nodes, and leaves). Removed nodes are not counted.
set_node_context tree node_id context associates context data with node_id.
Pass Some context to set or update the context; pass None to remove it. Returns Error (Invalid_input_node node_id) if node_id does not exist.
get_node_context tree node_id returns the context data associated with node_id, or None if no context is set.
Returns None if node_id is invalid.
get_node_context_mut tree node_id returns the context data associated with node_id, or None if no context is set.
In this functional interface, this is equivalent to get_node_context. The name reflects the original Rust API.
add_child tree parent child appends child to parent's child list.
Returns Ok () on success. Returns Error (Invalid_parent_node parent) if parent does not exist, or Error (Invalid_child_node child) if child does not exist.
insert_child_at_index tree parent index child inserts child at index in parent's child list.
Existing children at index and beyond are shifted right. Returns Ok () on success. Returns Error (Invalid_parent_node parent) if parent does not exist, Error (Invalid_child_node child) if child does not exist, or Error (Child_index_out_of_bounds { parent; child_index = index; child_count }) if index > child_count.
set_children tree parent children replaces all of parent's children with children.
Returns Ok () on success. Returns Error (Invalid_parent_node parent) if parent does not exist, or Error (Invalid_child_node child) if any child in children does not exist.
remove_child tree parent child removes child from parent's child list.
Returns Ok child on success. Returns Error (Invalid_parent_node parent) if parent does not exist, or Error (Invalid_child_node child) if child is not a child of parent.
remove_child_at_index tree parent index removes the child at index from parent's child list.
Returns Ok child_id where child_id is the removed child. Returns Error (Invalid_parent_node parent) if parent does not exist, or Error (Child_index_out_of_bounds { parent; child_index = index; child_count }) if index >= child_count.
remove_children_range tree parent (start, end) removes children at indices start through end (inclusive) from parent's child list.
Returns Ok () on success. Returns Error (Invalid_parent_node parent) if parent does not exist, or Error (Child_index_out_of_bounds { parent; child_index; child_count }) if start > end, start < 0, or end >= child_count.
val replace_child_at_index :
'context tree ->
Node_id.t ->
int ->
Node_id.t ->
Node_id.t resultreplace_child_at_index tree parent index new_child replaces the child at index in parent's child list with new_child.
Returns Ok old_child where old_child is the replaced child. Returns Error (Invalid_parent_node parent) if parent does not exist, Error (Invalid_child_node new_child) if new_child does not exist, or Error (Child_index_out_of_bounds { parent; child_index = index; child_count }) if index >= child_count.
child_at_index tree parent index returns the child at index in parent's child list.
Returns Ok child_id on success. Returns Error (Invalid_parent_node parent) if parent does not exist, or Error (Child_index_out_of_bounds { parent; child_index = index; child_count }) if index < 0 or index >= child_count.
parent tree node_id returns the parent of node_id, or None if node_id is a root or invalid.
Nodes can have at most one parent.
children tree parent returns the list of parent's children in order.
Returns Ok [] if parent has no children. Returns Error (Invalid_parent_node parent) if parent does not exist.
set_style tree node_id style sets the style of node_id to style.
This invalidates the layout cache for node_id and all ancestors. Returns Ok () on success. Returns Error (Invalid_input_node node_id) if node_id does not exist.
style tree node_id returns the style of node_id.
Returns Ok style on success. Returns Error (Invalid_input_node node_id) if node_id does not exist.
layout tree node_id returns the computed layout of node_id.
Returns the rounded layout if rounding is enabled (via enable_rounding), otherwise the unrounded layout. Returns Ok layout on success. Returns Error (Invalid_input_node node_id) if node_id does not exist.
The layout is valid only after calling compute_layout on node_id or an ancestor. Querying before computation yields a default zero layout.
unrounded_layout tree node_id returns the unrounded computed layout of node_id.
Returns the layout with precise floating-point values regardless of the rounding setting. Returns a default zero layout if node_id is invalid or layout has not been computed.
mark_dirty tree node_id invalidates the layout cache for node_id and all ancestors.
This forces recomputation during the next compute_layout call. Use when external factors affect layout but the style has not changed (e.g., updated measure function data). Returns Ok () on success. Returns Error (Invalid_input_node node_id) if node_id does not exist.
dirty tree node_id checks if node_id needs layout recomputation.
Returns Ok true if the node is dirty (cache invalid); Ok false if clean (cache valid). Returns Error (Invalid_input_node node_id) if node_id does not exist.
type 'context measure_function =
float option Geometry.size ->
Available_space.t Geometry.size ->
Node_id.t ->
'context option ->
Style.t ->
float Geometry.size'context measure_function computes intrinsic size for leaf nodes.
Parameters:
known_dimensions: Dimensions already resolved from the style (e.g., explicit width or height). Some w indicates width is known; None requires computation.available_space: Space constraints from the parent. Definite n provides n pixels; Min_content and Max_content guide intrinsic sizing.node_id: The node being measured.context: Custom data associated with the node via new_leaf_with_context, or None.style: The node's style.Returns the measured size. The function should respect known_dimensions: if known_dimensions.width is Some w, return size.width = w. When both dimensions are known, computation may be skipped.
val compute_layout_with_measure :
'context tree ->
Node_id.t ->
Available_space.t Geometry.size ->
'context measure_function ->
unit resultcompute_layout_with_measure tree node_id available_space measure computes layout for node_id and all descendants using measure for leaf nodes.
The available_space specifies width and height constraints. The measure function computes intrinsic size for leaves. Returns Ok () on success. Returns Error (Invalid_input_node node_id) if node_id does not exist.
After computation, query layouts with layout.
val compute_layout :
'context tree ->
Node_id.t ->
Available_space.t Geometry.size ->
unit resultcompute_layout tree node_id available_space computes layout for node_id and all descendants.
Uses a default measure function that returns zero size. This is suitable for layouts without leaf content (e.g., pure container hierarchies). Returns Ok () on success. Returns Error (Invalid_input_node node_id) if node_id does not exist.
These modules provide low-level traversal operations used by layout algorithms. Most users do not need these; they are exposed for advanced use cases and integration with custom algorithms.