Layout
Layout
Section titled “Layout”Core Idea
Section titled “Core Idea”Layout is constraints-based and compositional. Public UI code builds typed components such as Row, Column, Text,
and Rect, then converts them into the erased Element type at runtime/component boundaries. Internally, each
Element wraps a crate-private node tree made of containers, leaves, and modifier nodes.
A plain empty component has no intrinsic size. Size, padding, alignment, offsets, visuals, and scroll behavior are added by wrapping the component in modifiers.
Modifiers
Section titled “Modifiers”Modifiers are chainable and wrap the current element.
lurq::components::Rect::new(80.0, 40.0) .padding(12.0) .background("#3b82f6") .rounded(8.0)Common modifiers:
| Modifier | Purpose |
|---|---|
.size(width, height) | Force width and height |
.width(width) | Force width |
.height(height) | Force height |
.min_size(width, height) / .max_size(width, height) | Set minimum or maximum width and height |
.min_width(width) / .max_width(width) | Set a width bound |
.min_height(height) / .max_height(height) | Set a height bound |
.padding(...) / .padding_horizontal(...) / .padding_vertical(...) | Add insets around the child from a concrete dimension or SpacingSize |
.background(color) | Fill the element background from a concrete color or PaletteColor |
.rounded(radius) | Set border radius from f32 or RadiusSize |
.border_inside(width, color) | Draw an inside border from a concrete width or BorderSize |
.offset(x, y) | Shift visually without changing parent layout |
.relative(x, y) | Alias for .offset(x, y) |
.absolute(x, y, width, height) | Absolute stack positioning with forced size |
.absolute_position(x, y) | Absolute stack positioning with measured size |
.transform(Transform2D) | Apply a visual 2D transform around the element center |
.align(Alignment) | Override alignment within parent container |
.flex(factor) | Participate in row/column flex distribution |
.flex_shrink(factor) | Give up main-axis space when a row/column overflows |
.shrink_order(order) / .shrink_limit(ShrinkLimit) | When and how far a shrinking child gives way |
.shrink_drop_below(size) | Shrink a child down to size, then drop it |
Sizing modifiers accept Dimension values. Passing a plain f32 is shorthand for Dimension::Px(value). Min/max
sizing clamps a child’s measured size without forcing both bounds. Padding accepts f32, Dimension, or SpacingSize.
use lurq::node::dimension::Dimension;
lurq::components::Spacer::new().width(120.0)lurq::components::Spacer::new().width(Dimension::Pct(50.0))lurq::components::Spacer::new().width(Dimension::Auto)lurq::components::Spacer::new().min_width(160.0).max_height(240.0)lurq::components::Spacer::new().min_size(120.0, 80.0).max_size(320.0, 180.0)Constraints Model
Section titled “Constraints Model”Layout follows the same high-level model as Flutter:
- Parent passes
Constraintsdown to each child. - Child picks a concrete
Sizewithin those constraints. - Parent positions each child with an offset.
pub struct Constraints { pub min_width: f32, pub max_width: f32, pub min_height: f32, pub max_height: f32,}Constraint kinds:
- Tight:
min == max; forces an exact size. - Loose:
min == 0; child can choose any size up to max. - Unbounded:
max == f32::INFINITY; used by scroll containers on the scroll axis.
Application code normally does not call layout directly. Runtime computes layout when rendering, dispatching input, or looking up elements.
Containers
Section titled “Containers”Column
Section titled “Column”lurq::components::Column::new() arranges children top-to-bottom.
lurq::components::Column::new() .spacing(8.0) .align_items(Alignment::Center) .child(lurq::components::Text::new("A")) .child(lurq::components::Text::new("B"))Column layout:
- Lays out non-flex children with loosened vertical constraints.
- Sums child heights plus spacing.
- Uses the max child width.
- Positions children vertically and applies cross-axis alignment.
- Distributes remaining height to flex children when present.
.spacing(...) accepts either a plain pixel value or a SpacingSize from the active theme.
lurq::components::Row::new() arranges children left-to-right.
lurq::components::Row::new() .spacing(8.0) .align_items(Alignment::Center) .child(lurq::components::Text::new("A")) .child(lurq::components::Text::new("B"))Row layout is the horizontal equivalent of column layout.
.spacing(...) accepts either a plain pixel value or a SpacingSize from the active theme.
lurq::components::Stack::new() overlays children. Later children paint on top of earlier children.
lurq::components::Stack::new() .stack_align(StackAlignment::Center) .child(lurq::components::Rect::new(200.0, 120.0)) .child(lurq::components::Rect::new(40.0, 40.0).background("#ef4444"))Stack layout:
- Lays out all children with the same constraints.
- Sizes itself to the max width/height of non-absolute children.
- Positions normal children using stack alignment or per-child
.align(...). - Positions absolute children at their explicit
(x, y)offset.
Absolute children do not contribute to stack size.
Relative Positioning
Section titled “Relative Positioning”lurq::components::Rect::new(50.0, 50.0).relative(10.0, 20.0)Relative positioning is an offset. It moves the child visually but the parent still reserves space as if the offset were zero. Siblings are not moved by the offset.
Absolute Positioning
Section titled “Absolute Positioning”Absolute positioning is intentionally scoped to Stack.
lurq::components::Stack::new() .child(lurq::components::Rect::new(300.0, 120.0).background("#f8fafc")) .child( lurq::components::Rect::new(80.0, 32.0) .background("#f97316") .absolute(190.0, 24.0, 80.0, 32.0), )Use:
.absolute(x, y, width, height)when the positioned child should have a forced size..absolute_position(x, y)when the positioned child should keep its measured size.
There is no z-index. Rendering order is structural: later stack children paint above earlier children.
Alignment
Section titled “Alignment”Row And Column Alignment
Section titled “Row And Column Alignment”lurq::components::Column::new() .align_items(Alignment::Center) .child(lurq::components::Text::new("centered"))pub enum Alignment { Start, Center, End, Stretch,}Per-Child Override
Section titled “Per-Child Override”lurq::components::Column::new() .align_items(Alignment::Start) .child(lurq::components::Text::new("left")) .child(lurq::components::Text::new("right").align(Alignment::End))Stack Alignment
Section titled “Stack Alignment”lurq::components::Stack::new() .stack_align(StackAlignment::BottomEnd) .child(lurq::components::Rect::new(40.0, 40.0))pub enum StackAlignment { TopStart, TopCenter, TopEnd, CenterStart, Center, CenterEnd, BottomStart, BottomCenter, BottomEnd,}Children inside Row or Column can consume remaining space with .flex(factor).
lurq::components::Row::new() .child(lurq::components::Rect::new(100.0, 50.0)) .child(lurq::components::Spacer::new().flex(1.0)) .child(lurq::components::Rect::new(100.0, 50.0))Flex layout:
- Lay out non-flex children first.
- Subtract fixed child sizes and spacing from available space.
- Divide remaining space by flex factor.
- Lay out flex children with tight constraints for their assigned size.
In an unbounded main axis (for example a column inside ScrollVertical, or a column measured by its own content) there
is no remaining space: a flex child gets its basis, or its natural size when it has none.
Shrink
Section titled “Shrink”.flex_shrink(factor) (or .flex_full(grow, shrink, basis)) lets a child give up main-axis space when the children
overflow the row or column. The overflow is split by shrink factor, never below a child’s min_width/min_height.
Each shrunk child is then laid out again with its shrunk size as a tight constraint, so its content matches the box it
got: a shrunk scroll container reports the shrunk viewport and scrolls to its last row, and a shrunk column distributes
its own flex children inside the smaller height.
lurq::components::Column::new() .height(300.0) .child(header) .child(lurq::components::ScrollVertical::new(rows).flex_shrink(1.0)) .child(footer)The scroll area takes its content height while that fits, and the space between header and footer otherwise.
Give-way order and limits
Section titled “Give-way order and limits”By default every shrinking child gives way at once. .shrink_order(order) makes them give way in turn: the children
with the lowest order absorb the overflow, down to their limit, before any child of the next order shrinks at all.
Children of one order share their part by shrink factor, as above. The order is an i32 and defaults to 0, so a child
without one gives way before children with a positive order. Both modifiers apply only to a child with a flex_shrink
factor in a single-line Row or Column.
.shrink_limit(ShrinkLimit) sets how far a child gives way:
ShrinkLimit | The child shrinks |
|---|---|
MinSize | Down to its min_width/min_height, 0 without one (the default) |
Content | Down to its content minimum, never below an explicit min_width/min_height |
Drop | Not at all: it keeps its natural size, or is dropped (zero size, no spacing, not drawn and not hit) |
The content minimum of a Row in a row (or a Column in a column) is its padding and spacing, the natural size of
every child that does not shrink, and the limit of every child that does (a droppable child counts as gone). Anything
else (text, rects, stacks, scroll containers, wrapping rows, lines on the other axis) keeps its natural size, like a
flex item with CSS min-width: auto. Within one order, droppable children are dropped, last first, only while the
shrinking children of that order cannot absorb the rest of the overflow. This holds for the line that gives way, not
inside a shrinking item of it: an item whose row holds droppable children drops them all at once when it collapses
(see below). A dropped child stays mounted and keeps its
state, and it comes back once the line has room for it. While it is dropped, nothing inside it is a Tab stop or takes
focus (focus() and ctx.focus requests for it are refused, keys never activate it), focus inside it moves off (the
focused node is blurred), and the element refs and bounds of everything inside it report a
zero-size rect at its place in the line, where the next child starts. Devtools and the MCP tree mark it dropped.
What gives way whole only accumulates as a line narrows: a child dropped at one width is dropped at every narrower
one. A dropped child frees its whole size and spacing, which can be more than the line needed; the rest goes back to
children that only trimmed (labels, spacers), never to a child that dropped, so the line stays filled without bringing
anything back. The same holds one line down. A shrinking item whose own row holds droppable children (a Drop or
shrink_drop_below child, or such an item nested in it) keeps its natural size until its line needs it to give way.
It then collapses, after the children of its order that drop, as a whole step like a drop: its row drops every one of
them (and collapses every such item in it) at once, so it holds its padding, its other children at their natural size
and the spacing between them, and from there it shrinks on toward its floor like any other child, never growing back.
The line decides this and the item’s own layout follows it, so the two cannot disagree. Inside such an item, children
therefore give way only once it collapses: a trimming child does not trim while a droppable child beside it stays,
whatever their orders. To let a label trim before a time drops, make them separate children of the line that gives
way, the time in a later order than the label. An ellipsizing text with Drop and a shrink factor is
measured at its full width, so it drops rather than ellipsizes.
.shrink_drop_below(size) combines both: the child first shrinks like its limit, but no further than size, and when
its order still cannot absorb the overflow with every shrinking child at its floor, it drops (as with Drop) before
the next order shrinks at all. A child already narrower than size drops without shrinking; a negative or NaN size
counts as 0. Within one order, children with Drop are dropped first, then those with shrink_drop_below, each last
first, and either kind counts as gone in an ancestor’s content minimum. With ShrinkLimit::Drop the size has no
effect.
// The request words trim to 100 px, then drop so the count stays whole.Row::new() .child(label("Designer asks to use pencil").flex_shrink(1.0).shrink_order(1).shrink_drop_below(100.0)) .child(label("1 approval waiting").flex_shrink(1.0).shrink_order(2))A line that uses orders or limits also shares in whole pixels: within one order every child loses a whole number of pixels except the one with the largest shrink factor, which takes the fraction. A sliver of overflow therefore truncates one label instead of putting a stray ellipsis on several, and as the line narrows each child gives up a pixel at a time. If that child reaches its floor, the fraction it cannot take goes to a child that already loses a pixel or more, and only when there is none to one that loses nothing. A line without orders or limits shares exactly by factor.
Use orders, not factors, to say which child gives way first: shares are computed in f32, so keep the factors within
one order of similar magnitude (a factor thousands of times smaller than its neighbours’ is lost in their sum).
use lurq::{ components::{Rect, Row, Text, TextOverflow}, layout::layout_kind::ShrinkLimit,};
let label = |text: &str| Text::new(text).nowrap().text_overflow(TextOverflow::Elipsis);
// Icon and count stay whole; the detail inside the item gives way first.let approvals = Row::new() .child(Rect::new(14.0, 14.0)) .child(label("1 approval waiting")) .child(label("Designer asks to use pencil").flex_shrink(1.0)) .flex_shrink(1.0) .shrink_order(2) .shrink_limit(ShrinkLimit::Content);
Row::new() .spacing(8.0) .child(label("15:32").flex_shrink(1.0).shrink_order(1).shrink_limit(ShrinkLimit::Drop)) .child(approvals) .child(label("Run 3").flex_shrink(1.0).shrink_order(3))As the row narrows, the time is dropped first, then the approval detail is ellipsized down to the icon and count, and only then does the run label shrink.
Scroll
Section titled “Scroll”lurq::components::ScrollVertical::new( lurq::components::Column::new() .spacing(4.0) .with_children(items),).size(300.0, 180.0)Scroll containers give their child unbounded constraints on the scroll axis and apply scroll offsets during
layout/rendering. Without a fixed size on the scroll axis, a scroll container takes its content’s size within its
constraints, so .max_height(...) makes it grow with its content up to the cap and scroll beyond it.
VirtualizedList sizes the same way: .height(...) or .flex(...) gives it a fixed viewport, while .max_height(...)
alone lets a short list take its content height and a long one stop at the cap.
VirtualizedList::new(ctx, items) .max_height(240.0) .mount_keyed::<ItemRow, _, _, _>(|item| item.id, |item| item.clone())Text is measured by the glyph engine and wraps within its width constraint.
lurq::components::Text::styled("hello", TextStyle { font_size: 18.0, ..TextStyle::default()})The glyph engine also records caret positions for text selection. Text::selectable(true) uses those positions for drag
ranges, double-click word selection, and triple-click line selection. Wrapped or multiline selections render separate
highlight rectangles for each selected row.
Transforms are visual-only for layout size, but render output and hit testing use the transformed coordinates. Text selection and text input carets therefore follow transformed text, including text inside a transformed parent.
See Animation And Transforms for transform composition, keyframe animation, and transition details.
Text Transform Modes
Section titled “Text Transform Modes”TextTransformMode::Bitmap is the default. It rasterizes glyphs in their normal orientation and transforms those glyph
quads during rendering. This path preserves float placement and is best for animated transforms because changing the
transform does not create a new glyph atlas entry for every angle.
TextTransformMode::Rasterized is for static transformed text. It bakes the transform into the glyph mask, uses float
screen-space placement for the baked mask, and emits identity-transform glyph quads. This produces sharper rotated edges
than GPU-transforming the normal glyph bitmap, but the transform matrix is part of the glyph cache key, so continuously
animated angles can grow the atlas.
use lurq::node::{TextTransformMode, transform::Transform2D};
lurq::components::Text::new("Static rotated text") .text_transform_mode(TextTransformMode::Rasterized) .transform(Transform2D::rotate_deg(-8.0))