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.
| Variant | Meaning |
|---|---|
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. |
Fill | Consume whatever is left after the other constraints solve. |
FitContent | Size from a LayoutSizeHint callback (see intrinsic sizing). |
FitContentBounded { min, max } | Same as FitContent but clamped to [min, max]. |
FitMin | Shrink-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
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)); // footerFlex::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/leftinsets on the outer area. Defaults to zero..gap(u16)— empty cells between children..alignment(Alignment)— how leftover space is distributed. Choices areStart,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
Rectsis aSmallVec<[Rect; 8]>— stack-inlined for up to 8 children, transparently heap-allocated beyond that. - Rects cover the
areaexactly, minus any margin/gap you configured. They never overlap. - For
Direction::Horizontal, recti+1is immediately to the right of recti(plus gap). - For
Direction::Vertical, recti+1is immediately below recti.
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.
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
FitContent, LayoutSizeHint, and the measurer callback in depth.
Switch Flex configurations by terminal width bucket.
How equivalence saturation finds cheaper layouts.
E-graph optimizerHow widgets call split inside their render().
How this piece fits in layout.
Layout overview