Back to Slint

Layout System Internals

docs/development/layout-system.md

1.16.112.5 KB
Original Source

Layout System Internals

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.

Overview

Slint's layout system has two phases:

  1. Compile-time: Layout elements are lowered to constraint expressions and cache structures
  2. Runtime: Constraints are evaluated and positions/sizes are calculated

Layout types:

  • HorizontalLayout / VerticalLayout - Linear box layouts
  • GridLayout - 2D grid with row/column positioning, spans
  • Dialog - Special grid with platform-specific button ordering
  • FlexboxLayout - CSS Flexbox layout

Key Files

FilePurpose
internal/core/layout.rsRuntime layout solving algorithms
internal/compiler/layout.rsCompiler-side layout data structures
internal/compiler/passes/lower_layout.rsLowers layout elements to expressions
internal/compiler/passes/default_geometry.rsSets default width/height (runs after layout lowering)
internal/compiler/llr/lower_layout_expression.rsConverts layout expressions to LLR

Constraint System

LayoutInfo (Runtime)

rust
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)
}

Constraint Merging

When constraints combine (e.g., nested layouts):

  • min: Take the larger (tightest constraint)
  • max: Take the smaller (tightest constraint)
  • preferred: Take the larger
  • stretch: Take the smaller

Constraint Properties

Elements can specify these properties:

  • min-width, min-height
  • max-width, max-height
  • preferred-width, preferred-height
  • horizontal-stretch, vertical-stretch

Layout Solving Algorithm

Both 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

Box Layout Alignment

When items fit without shrinking, alignment determines positioning:

AlignmentBehavior
StretchGrow items to fill space (default)
StartPack at beginning
CenterPack in center
EndPack at end
SpaceBetweenEqual gaps between items
SpaceAroundEqual gaps around items
SpaceEvenlyEqual gaps including edges

Grid Layout

Grid layouts solve independently for each axis:

  1. Organize: Convert cell definitions to row/column assignments
  2. Solve horizontal: Calculate column widths and x positions
  3. Solve vertical: Calculate row heights and y positions

Cells with colspan/rowspan > 1 require iterative constraint distribution.

Flexbox layout

Flexbox layout is solved in both axes simultaneously. The layouting algorithm is provided by the taffy crate, which implements the CSS flexbox algorithm.

Compile-Time Lowering

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

Key Expressions Generated

ExpressionPurpose
OrganizeGridLayoutCompute cell row/column assignments
SolveBoxLayoutCompute positions and sizes for items in a box layout
SolveGridLayoutCompute positions and sizes for items in a grid layout
SolveFlexboxLayoutCompute positions and sizes for items in a flexbox layout
ComputeLayoutInfoCalculate combined constraints
LayoutCacheAccessRead position/size from cache
GridRepeaterCacheAccessTwo-level indirection cache read (for repeaters in grids)

Key Data Structures

Compiler-Side

rust
// 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
}

Runtime

rust
// 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>,
}

Layout Cache Formats

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.

Static-only layout (no repeaters)

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.

Standard cache (box layouts)

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 value
  • entries_per_item: 2 for the coordinate cache (pos + size), compile-time known

Two-level indirection cache (grid layouts with repeaters)

Used 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:

  • Compile-time constant: When all repeater children are static
  • Runtime value: When a repeater instance contains nested repeaters, retrieved from the jump cell itself

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_index
  • stride: 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 child

How children read from the cache

During 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.

Common Modification Patterns

Adding a New Layout Property

  1. Add property to builtin layout element in internal/compiler/builtins.slint
  2. Handle in LayoutGeometry or LayoutConstraints in internal/compiler/layout.rs
  3. Update lower_layout.rs to extract and use the property
  4. Update runtime structs in internal/core/layout.rs if needed
  5. Add tests in tests/cases/layout/

Debugging Layout Issues

  1. Check constraint propagation: Add eprintln! in LayoutInfo::merge()
  2. Check solving: Add logging in layout_items() to see shrink/grow steps
  3. Verify cache access: Check LayoutCacheAccess indices in generated code
  4. Use inspector: Run with Slint inspector to see element bounds

Adding a New Alignment Mode

  1. Add variant to LayoutAlignment enum in internal/core/layout.rs
  2. Handle in solve_box_layout() alignment switch
  3. Add parsing in compiler if new syntax needed
  4. Add tests for the new alignment

Key Concepts for Agents

  1. Two-phase architecture: Compile-time creates structure, runtime evaluates values
  2. Independent axis solving: Horizontal and vertical are solved separately (for horizontal, vertical and grid layouts)
  3. Constraint tightening: Merging takes the most restrictive bounds
  4. Stretch factors: Control how extra space is distributed (0 = don't grow)
  5. Cache indirection: Enables repeaters without runtime structure changes
  6. Default geometry: Elements default to 100% of parent unless content-sized

Testing Layout Changes

sh
# 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