Skip to Content
ftui-layoutFlex & Grid

Flex and Grid

Flex is a 1D constraint solver. Grid is its 2D sibling. Both take a Rect and a vector of Constraints and return a stack-inlined Rects (which is SmallVec<[Rect; 8]>). You build them once — usually inline inside view() — and throw them away at the end of the frame. They own no state.

The constraint vocabulary

Constraint is an enum in ftui_layout::Constraint. Every variant expresses a different kind of intent.

VariantMeaning
Fixed(u16)Exact cell count. Non-negotiable.
Percentage(f32)Ratio of the total available space, 0.0..=100.0.
Min(u16)Floor. Will be at least this many cells; grows if space available.
Max(u16)Ceiling. Will never exceed this many cells.
Ratio(u32, u32)Weight-based. Ratio(3, 5) takes 3 parts out of every 5.
FillConsume whatever is left after the other constraints solve.
FitContentSize from a LayoutSizeHint callback (see intrinsic sizing).
FitContentBounded { min, max }Same as FitContent but clamped to [min, max].
FitMinShrink-to-fit: take only the widget’s reported minimum.

The research docs sometimes call Fixed(u16) Length. In the live crate the variant is Constraint::Fixed(u16). Both names describe the same thing — an exact cell allocation. Use Fixed.

The Flex builder

hello_flex.rs
use ftui_layout::{Flex, Constraint}; use ftui_render::Rect; // A three-row vertical split: header, body that fills, 1-row footer. let flex = Flex::vertical() .constraints([ Constraint::Fixed(3), // header Constraint::Fill, // body Constraint::Fixed(1), // footer ]); let area = Rect::new(0, 0, 80, 24); let rects = flex.split(area); assert_eq!(rects[0], Rect::new(0, 0, 80, 3)); // header assert_eq!(rects[1], Rect::new(0, 3, 80, 20)); // body assert_eq!(rects[2], Rect::new(0, 23, 80, 1)); // footer

Flex::horizontal() / Flex::vertical() both return a builder you can chain. The important chainable methods:

Flex::horizontal() .constraints([Constraint::Percentage(30.0), Constraint::Fill]) .margin(Sides::all(1)) // outer padding .gap(1) // space between children .alignment(Alignment::Center) .overflow(OverflowBehavior::Clip);
  • .constraints(iter) — per-child constraint vector.
  • .margin(Sides)top/right/bottom/left insets on the outer area. Defaults to zero.
  • .gap(u16) — empty cells between children.
  • .alignment(Alignment) — how leftover space is distributed. Choices are Start, Center, End, SpaceAround, SpaceBetween.
  • .overflow(OverflowBehavior)Clip (default), Visible, Scroll { max_content }, Wrap.

split(area) semantics

pub fn split(&self, area: Rect) -> Rects;
  • The returned Rects is a SmallVec<[Rect; 8]> — stack-inlined for up to 8 children, transparently heap-allocated beyond that.
  • Rects cover the area exactly, minus any margin/gap you configured. They never overlap.
  • For Direction::Horizontal, rect i+1 is immediately to the right of rect i (plus gap).
  • For Direction::Vertical, rect i+1 is immediately below rect i.

Solver pass order

┌──────────────────────────────────────────────────┐ │ 1. Allocate Fixed(n) constraints │ │ 2. Allocate Percentage(p) of remaining space │ │ 3. Allocate Ratio(n, d) using remaining weight │ │ 4. Enforce Min(n) floors │ │ 5. Enforce Max(n) ceilings │ │ 6. FitContent / FitContentBounded / FitMin │ │ consult the measurer callback (if any) │ │ 7. Fill consumes whatever is left │ └──────────────────────────────────────────────────┘

If the sum of Fixed / Percentage / Min constraints exceeds the container, the solver still produces a non-negative layout — later items may receive zero cells rather than negative sizes. There is no panic; there is no error. See the pitfalls below.

Intrinsic sizing in one line

For content-aware layouts, use split_with_measurer:

let rects = flex.split_with_measurer(area, |child_index, constraint| { match child_index { 0 => LayoutSizeHint::exact(labels[0].width()), 1 => LayoutSizeHint::at_least(10, 30), _ => LayoutSizeHint::ZERO, } });

The closure returns a LayoutSizeHint { min, preferred, max } for any child whose constraint is FitContent, FitContentBounded, or FitMin. See intrinsic sizing for the full story.

Grid — the 2D sibling

Grid takes two constraint vectors (one for columns, one for rows) and returns a Vec<Rects> where result[row][col] is the cell’s rectangle. The constraint vocabulary is identical; the solver runs independently on each axis.

grid_three_by_two.rs
use ftui_layout::{Grid, Constraint}; use ftui_render::Rect; let grid = Grid::new() .columns([ Constraint::Percentage(33.0), Constraint::Fill, Constraint::Fixed(20), ]) .rows([ Constraint::Fixed(3), Constraint::Fill, ]); let cells = grid.split(Rect::new(0, 0, 120, 30)); // cells[0][0] is the top-left rect, cells[1][2] is the bottom-right rect.

Grid also supports cell spanning and gap control; see the crate rustdoc for the full signature.

Alignment when you have leftover space

If the constraints don’t fill the container (e.g. three Fixed(10)s in a 100-wide container), alignment decides what to do with the remaining 70 cells:

Start: [AAA][BBB][CCC].......................... Center: ...............[AAA][BBB][CCC]........... End: ..........................[AAA][BBB][CCC] SpaceAround: ......[AAA].....[BBB].....[CCC].......... SpaceBetween: [AAA]..................[BBB].........[CCC]

Pitfalls

Overspecification. Fixed(100) + Fixed(100) in a 150-wide container will give you a rect of width 100 and a rect of width 50 (not 100). Later children get truncated. The solver never panics; it silently clips. If you need to detect this, check rects[i].width against what you asked for.

Percentage plus Fill is redundant. If your constraints are [Percentage(30.0), Fill] that is the same as [Percentage(30.0), Percentage(70.0)]. Mixing them is fine but verbose.

f32 percentages. Percentage(33.3) is not the same as Percentage(33.333333). If you need exact thirds use Ratio(1, 3); the solver will distribute rounding errors deterministically.

Where to go next