docs/development/layout-system.md
Note for AI coding assistants (agents): When to load this document: Working on
internal/core/layout.rs,internal/compiler/passes/lower_layout.rs, debugging sizing/positioning issues, or implementing new layout features. For general build commands and project structure, see/AGENTS.md.
Slint's layout system has two phases:
Layout types:
| File | Purpose |
|---|---|
internal/core/layout.rs | Runtime layout solving algorithms |
internal/compiler/layout.rs | Compiler-side layout data structures |
internal/compiler/passes/lower_layout.rs | Lowers layout elements to expressions |
internal/compiler/passes/default_geometry.rs | Sets default width/height (runs after layout lowering) |
internal/compiler/llr/lower_layout_expression.rs | Converts layout expressions to LLR |
pub struct LayoutInfo {
pub min: Coord, // Minimum size
pub max: Coord, // Maximum size
pub min_percent: Coord, // Minimum as % of parent
pub max_percent: Coord, // Maximum as % of parent
pub preferred: Coord, // Preferred size
pub stretch: f32, // Stretch factor (0.0 = don't stretch)
}
When constraints combine (e.g., nested layouts):
Elements can specify these properties:
min-width, min-heightmax-width, max-heightpreferred-width, preferred-heighthorizontal-stretch, vertical-stretchBoth grid and box layouts use the same core algorithm in layout_items():
1. Set initial sizes to preferred values
2. Calculate total size needed
3. If total > available space:
→ Shrink items proportionally (respecting min constraints)
4. If total < available space:
→ Grow items proportionally based on stretch factors
→ Items with stretch=0 stay at preferred size
5. Assign positions sequentially with spacing
When items fit without shrinking, alignment determines positioning:
| Alignment | Behavior |
|---|---|
Stretch | Grow items to fill space (default) |
Start | Pack at beginning |
Center | Pack in center |
End | Pack at end |
SpaceBetween | Equal gaps between items |
SpaceAround | Equal gaps around items |
SpaceEvenly | Equal gaps including edges |
Grid layouts solve independently for each axis:
Cells with colspan/rowspan > 1 require iterative constraint distribution.
Flexbox layout is solved in both axes simultaneously.
The layouting algorithm is provided by the taffy crate, which implements the CSS flexbox algorithm.
The lower_layout.rs pass transforms layout elements:
GridLayout element
↓
lower_grid_layout()
↓
Creates synthetic properties:
- layout-organized-data (cell organization)
- layout-cache-h (horizontal positions/sizes)
- layout-cache-v (vertical positions/sizes)
- layoutinfo-h, layoutinfo-v (constraints)
↓
Child x/y/width/height bound to cache access expressions
| Expression | Purpose |
|---|---|
OrganizeGridLayout | Compute cell row/column assignments |
SolveBoxLayout | Compute positions and sizes for items in a box layout |
SolveGridLayout | Compute positions and sizes for items in a grid layout |
SolveFlexboxLayout | Compute positions and sizes for items in a flexbox layout |
ComputeLayoutInfo | Calculate combined constraints |
LayoutCacheAccess | Read position/size from cache |
GridRepeaterCacheAccess | Two-level indirection cache read (for repeaters in grids) |
// internal/compiler/layout.rs
pub struct GridLayout {
pub elems: Vec<GridLayoutElement>, // Cells
pub geometry: LayoutGeometry, // Padding, spacing, alignment
}
pub struct BoxLayout {
pub orientation: Orientation, // Horizontal or Vertical
pub elems: Vec<LayoutItem>,
pub geometry: LayoutGeometry,
}
pub struct LayoutConstraints {
pub min_width: Option<NamedReference>,
pub max_width: Option<NamedReference>,
// ... other constraint properties as references
}
// internal/core/layout.rs
pub struct GridLayoutData {
pub size: Coord,
pub spacing: Coord,
pub padding: Padding,
pub organized_data: GridLayoutOrganizedData,
}
pub struct BoxLayoutData<'a> {
pub size: Coord,
pub spacing: Coord,
pub padding: Padding,
pub alignment: LayoutAlignment,
pub cells: Slice<'a, LayoutItemInfo>,
}
The layout cache is a flat SharedVector<Coord> (i.e. SharedVector<f32>) storing solved
positions and sizes for all children of a layout. Each child occupies 2 slots: [pos, size]
(e.g. [x, width] for horizontal, [y, height] for vertical). There are separate caches
for horizontal and vertical axes.
When all children are known at compile time, the cache is a simple flat array.
cache = [pos0, size0, pos1, size1, ..., posN, sizeN]
Access: cache[index] where index = child_idx * 2 for pos, child_idx * 2 + 1 for size.
Used by HorizontalLayout/VerticalLayout/FlexboxLayout (via LayoutCacheGenerator).
Static children occupy a fixed slot; each repeater instance contributes exactly one cell (one pos +
one size). When repeaters are present, their instances are stored in a contiguous block at
the end of the cache, with a jump cell in the static region pointing to the start of that
block.
repeater_indices: Pairs of (start_cell_index, instance_count) — one pair per repeater.
Example: 1 fixed cell, then a repeater with 3 instances
repeater_indices = [1, 3] // repeater starts at cell 1, has 3 instances
cache = [
0., 50., // fixed cell: pos=0, size=50
4., 5., // jump cell: points to offset 4 (first dynamic slot)
80., 50., // repeated instance 0
160., 50., // repeated instance 1
240., 50., // repeated instance 2
]
Access: cache[cache[jump_index] + repeater_index * entries_per_item]
jump_index: the cache index of the jump cell (compile-time known)repeater_index: which instance (0..count), runtime valueentries_per_item: 2 for the coordinate cache (pos + size), compile-time knownUsed by GridLayout (via GridLayoutCacheGenerator) for any repeater, whether single-item or multi-child.
Like the standard cache, it uses jump cells for indirection, but with a key difference: the stride is variable and dynamic.
For box layouts, the stride is always fixed at entries_per_item (2 for coordinates). For grid layouts with repeaters,
the stride is step * entries_per_item, where step is the number of children per instance. The stride can be:
This enables grids to handle both single-item repeaters (step=1) and multi-child repeaters (step=N) with potentially nested repeaters inside.
repeater_steps: A vector with one entry per repeater — how many children each instance contributes.
Example: 1 repeater with 3 row instances, each having 2 children (step=2):
slint! {
GridLayout {
for _ in 3: Row {
Rectangle {}
Rectangle {}
}
}
};
repeater_indices = [0, 3] // starts at cell 0, 3 instances
repeater_steps = [2] // 2 children per instance
cache = [
2., 4., // [0-1] jump cell: data_base=2, stride=4 (step*2)
0., 50., 0., 50., // [2-5] row 0 data: child0=(pos=0,size=50), child1=(pos=0,size=50)
50., 50., 50., 50., // [6-9] row 1 data
100., 50., 100., 50., // [10-13] row 2 data
]
If rows have different numbers of children (jagged), the stride is based on the maximum number of children across all rows, and shorter rows are padded to match that stride.
Access: cache[cache[jump_index] + ri * stride + child_offset]
jump_index: compile-time known (index of the jump cell, always jump_cell_pos * 2)ri: repeater instance index (0..count), runtime value from $repeater_indexstride: step * 2 — either a compile-time literal (for static repeater children) or read from cache[jump_index + 1] (for rows containing nested repeaters)child_offset: which child within the rows (0, 2, 4, ...), compile-time known per childDuring compile-time lowering (lower_layout.rs), each child element gets bindings like:
// Static child in a grid:
x: layout_cache_h[4] // direct index, compile-time known
width: layout_cache_h[5]
// Repeated child in box layout — standard cache (LayoutCacheAccess):
x: layout_cache_h[cache[2] + $repeater_index * 2]
width: layout_cache_h[cache[2] + $repeater_index * 2 + 1]
// Repeated element in grid layout (even single-item) — two-level indirection cache (GridRepeaterCacheAccess):
// For single-item: step=1, stride=2 (step * entries_per_item)
// For multiple children per repeater: step=N, stride=N*2
x: layout_cache_h[cache[jump_cell] + $repeater_index * stride + child_offset]
width: layout_cache_h[cache[jump_cell] + $repeater_index * stride + child_offset + 1]
These are represented as Expression::LayoutCacheAccess (standard, for box layouts and static items in grids) or
Expression::GridRepeaterCacheAccess (grid repeaters with any repeater structure) in the expression tree, which
the code generators compile to the appropriate runtime access pattern.
internal/compiler/builtins.slintLayoutGeometry or LayoutConstraints in internal/compiler/layout.rslower_layout.rs to extract and use the propertyinternal/core/layout.rs if neededtests/cases/layout/eprintln! in LayoutInfo::merge()layout_items() to see shrink/grow stepsLayoutCacheAccess indices in generated codeLayoutAlignment enum in internal/core/layout.rssolve_box_layout() alignment switch# Run all layout-specific tests
cargo test -p test-driver-rust --test layout
cargo test -p test-driver-interpreter layout
# Run a specific test case, filtered by substring (don't prepend sh/bash, run_tests.sh is executable)
tests/run_tests.sh rust grid_conditional_row
tests/run_tests.sh interpreter grid_conditional_row
tests/run_tests.sh cpp grid_conditional_row
# Run all interpreter tests (fast)
cargo test -p test-driver-interpreter
# Visual verification (for humans)
cargo run -p gallery