Skip to content

Stacks

The containers that lay children along one axis: a column, a row, the macros that build either from a list of views, and where children sit across the axis. Stacks take views by value, so a tree is built without ever writing Box::new.

A settings screen whose content is a column: the heading Storage, the rows Books reading 128 and Free space reading 182 KB, and the line Last synced at 09 pushed down to the bottom of the band

The tutorial builds a screen from these. The space between and around views, and scrolling, are in Layout, and the chainable wrappers, .frame, .flexible and .on_tap, are in Modifiers. This page is how a stack measures, and what each piece does.

VStackLays children out top to bottom.
HStackLays children out left to right.
vstack!A vertical stack.
hstack!A horizontal stack, written the same way as vstack!.
AlignmentWhere children sit across the stacking axis.

VStack and HStack are one algorithm with the axes swapped, and it runs in two passes. Most surprises in a layout come from one of them.

  1. Fixed children first. Every child that is not flexible is measured against the whole space the stack was offered, not against what the children before it left.
  2. Flexible children share the rest. A Spacer, or any view made .flexible(), is measured last, against an equal share of what the fixed children and the gaps left. A share is never negative, and a remainder that does not divide evenly goes unused.

The stack is then as long as its children plus a gap between each pair, and as wide across as the widest child that counts. A spacer does not count across: it records the whole cross extent it was offered, and counting it would make an hstack! as tall as the screen.

WhenWhat happens
The fixed children are longer than the spaceThe stack is longer than it was offered, and nothing clips it. Flexible children get nothing. Wrap the content in a ScrollView.
A stack holds a spacerThe stack is exactly as long as it was offered.
A child measures zeroIt still has a gap on each side. push_if leaves out the child and its gap.
Children are alignedAcross the stack's own extent, which is its widest child, not the panel. See Alignment.
Rust
use xpui::{Size, Spacer, Text, VStack, View, vstack};
let offered = Size::new(480, 800);
let mut line = Text::new("A longer line");
View::<()>::measure(&mut line, offered);
let line = View::<()>::size(&line);
let mut column: VStack<()> = vstack![10; Text::new("Title"), Text::new("A longer line")];
column.measure(offered);
assert_eq!(column.size(), Size::new(line.width, 2 * line.height + 10));
let mut pushed: VStack<()> = vstack![10; Text::new("Title"), Spacer::new(), Text::new("Footer")];
pushed.measure(offered);
assert_eq!(pushed.size().height, 800, "a spacer makes the stack as tall as its offer");

Text is a view for every message type, so measuring a bare one names the type, View::<()>::measure. A stack knows its own and passes it down.

Lays children out top to bottom.

pub struct VStack<M>(Stack<M>)

A column of three lines eight pixels apart: Wi-Fi in bold, Connected to Home, and Signal strong

M is the screen's message type. Children are drawn in the order they were added, the first at the top, each against the left edge unless align says otherwise. How tall the column is, and what a spacer in it does, is how a stack measures.

BuilderSetsWhen not called
VStack::newthe gap between children, in pixels—
push · push_if · push_some · extendthe children, in orderno children: the stack measures zero
alignwhere children sit across the columnAlignment::Start, the left edge

The spacing is the framework's to lay out, not the theme's: pass a value from Theme::metric when the gap should follow the device.

Example — a column built by hand

Rust
use xpui::{Text, VStack};
let column: VStack<()> = VStack::new(8)
.push(Text::new("Wi-Fi").bold())
.push(Text::new("Connected to Home"))
.push(Text::new("Signal strong"));

Example — a line only while it applies

A column reading Library in bold, then Syncing…, then 128 books, with no line for the absent warning

Rust
use xpui::{Text, VStack};
let syncing = true;
let warning: Option<&str> = None;
let column: VStack<()> = VStack::new(8)
.push(Text::new("Library").bold())
.push_if(syncing, Text::new("Syncing…"))
.push_some(warning.map(Text::new))
.push(Text::new("128 books"));

The chain keeps one type whichever way the conditions go, so body needs no if whose branches would have to agree.

Example — rows from data

A column reading Networks in bold, then Home, Office and Library

Rust
use xpui::{Text, VStack};
let networks = ["Home", "Office", "Library"];
let column: VStack<()> = VStack::new(8)
.push(Text::new("Networks").bold())
.extend(networks.into_iter().map(Text::new));

Example — children of different types from data

Rust
use xpui::{Divider, Text, VStack, View, ViewExt};
let rows: Vec<Box<dyn View<()>>> = vec![
Text::new("Wi-Fi").boxed(),
Divider::new().boxed(),
Text::new("Bluetooth").boxed(),
];
let column = VStack::new(4).extend(rows);

A vertical stack with spacing pixels between children.

pub fn new(spacing: i32) -> Self
ParameterMeaning
spacingPixels between each pair of children, and none before the first or after the last.

Appends a child.

pub fn push(self, child: impl View<M> + 'static) -> Self

Takes any view by value and boxes it, so a tree is built without Box::new. The child must be a view for this stack's message type.

Appends a child only when condition holds, so callers can build conditional trees without wrapping the whole chain in an if.

pub fn push_if(self, condition: bool, child: impl View<M> + 'static) -> Self

The child is built either way, and dropped when condition is false. A child left out takes no gap.

Appends a child when there is one.

pub fn push_some(self, child: Option<impl View<M> + 'static>) -> Self

None adds nothing, not even a gap.

Appends every view an iterator yields, for lists built at run time.

pub fn extend<V: View<M> + 'static>(self, children: impl IntoIterator<Item = V>) -> Self

Every item is one type. To mix types, box each to Box<dyn View<M>> first, which is itself a view; ViewExt::boxed is the short way.

Positions children across the stacking axis, at Alignment::Start unless this is called.

pub fn align(self, alignment: Alignment) -> Self

In a column, Start is the left edge, Center the middle and End the right edge, all of the column's own width.

See also: HStack, vstack!, Spacer, Alignment

Lays children out left to right.

pub struct HStack<M>(Stack<M>)

A row of three words twenty-four pixels apart: Books, Fonts and Notes

The same stack as VStack on the other axis, with the same builders. Children run from the left, each against the top edge unless align says otherwise: a row mixing an icon with a line of text almost always wants Alignment::Center.

A row with no spacer in it is as wide as its children. A row with one is as wide as it was offered, which in a column is the width of the band.

Example — a row built by hand

Rust
use xpui::{HStack, Text};
let row: HStack<()> = HStack::new(24)
.push(Text::new("Books"))
.push(Text::new("Fonts"))
.push(Text::new("Notes"));

A horizontal stack with spacing pixels between children.

pub fn new(spacing: i32) -> Self

Appends a child.

pub fn push(self, child: impl View<M> + 'static) -> Self

Appends a child only when condition holds, so callers can build conditional trees without wrapping the whole chain in an if.

pub fn push_if(self, condition: bool, child: impl View<M> + 'static) -> Self

Appends a child when there is one.

pub fn push_some(self, child: Option<impl View<M> + 'static>) -> Self

Appends every view an iterator yields, for lists built at run time.

pub fn extend<V: View<M> + 'static>(self, children: impl IntoIterator<Item = V>) -> Self

Each behaves as its VStack counterpart does.

Positions children across the stacking axis, at Alignment::Start unless this is called.

pub fn align(self, alignment: Alignment) -> Self

In a row, Start is the top edge, Center the middle and End the bottom, all of the row's own height: the tallest child's.

See also: VStack, hstack!, Alignment

A vertical stack.

macro_rules! vstack
vstack![spacing; child, child, …]
vstack![spacing]

Shorthand for a column whose children are fixed: it expands to VStack::new(spacing) followed by one .push(child) per child, and hides nothing. A trailing comma is allowed. The second form is an empty stack. Use the builder when a child depends on a condition or comes from data: push_if, push_some, extend.

Example — a titled column with a footer

A settings screen whose content is a column: the heading Storage, the rows Books reading 128 and Free space reading 182 KB, and the line Last synced at 09 pushed down to the bottom of the band

Rust
use xpui::{Spacer, Text, VStack, hstack, vstack};
let column: VStack<()> = vstack![12;
Text::new("Storage").bold(),
hstack![8; Text::new("Books"), Spacer::new(), Text::new("128")],
hstack![8; Text::new("Free space"), Spacer::new(), Text::new("182 KB")],
Spacer::new(),
Text::new("Last synced at 09:14"),
];

The spacer takes the height the other children leave, so the footer sits on the bottom of whatever band the column is given. Each row's own spacer pushes its value to the right edge.

Example — a macro stack in a screen

Rust
use xpui::{NavigationScreen, Screen, Text, View, vstack};
struct About;
impl Screen for About {
type Message = ();
fn body(&self) -> impl View<()> {
NavigationScreen::new(vstack![16;
Text::new("Firmware").bold(),
Text::new("1.4.2"),
])
}
fn update(&mut self, _message: ()) {}
fn title(&self) -> Option<&'static str> {
Some("About")
}
}

See also: VStack, hstack!

A horizontal stack, written the same way as vstack!.

macro_rules! hstack
hstack![spacing; child, child, …]
hstack![spacing]

A row reading Battery, then 72% in bold, eight pixels to its right

It expands to HStack::new(spacing) followed by one .push(child) per child.

Example — a label and its value

Rust
use xpui::{HStack, Text, hstack};
let row: HStack<()> = hstack![8; Text::new("Battery"), Text::new("72%").bold()];

Example — an icon-sized frame beside text

A sun icon centred in a 44-pixel square, with Frontlight beside it, centred on the same line

Rust
use xpui::{Alignment, HStack, Icon, IconRef, Modifiers, Text, hstack};
let row: HStack<()> = hstack![12;
Modifiers::<()>::frame(Icon::new(Glyph::Sun), 44, 44),
Text::new("Frontlight"),
]
.align(Alignment::Center);

Without Alignment::Center the text sits against the top of the 44-pixel row. The frame names its message type because Icon is a view for every one; see Frame.

Example — a spacer between

A row reading Battery at the left edge and 72% at the right edge

Rust
use xpui::{HStack, Spacer, Text, hstack};
let row: HStack<()> = hstack![8; Text::new("Battery"), Spacer::new(), Text::new("72%")];

See also: HStack, Spacer, Alignment

Where children sit across the stacking axis.

pub enum Alignment

Three rows separated by rules, each a sun icon in a 56-pixel square beside a word: Start with the word against the top of its row, Center with it level with the icon, and End with it against the bottom

Variant
Alignment::StartAgainst the leading edge: left in a vstack!, top in an hstack!.
Alignment::CenterCentred across the stack.
Alignment::EndAgainst the trailing edge: right in a vstack!, bottom in an hstack!.

Start is the default, which a row mixing a 32-pixel icon with a line of text almost never wants: the text hangs off the top. It is Copy, Default, Debug and Eq.

Example — the three alignments side by side

Rust
use xpui::{Alignment, Divider, HStack, Icon, IconRef, Modifiers, Text, VStack, hstack, vstack};
fn aligned(alignment: Alignment, label: &'static str) -> HStack<()> {
hstack![12; Modifiers::<()>::frame(Icon::new(Glyph::Sun), 56, 56), Text::new(label)]
.align(alignment)
}
let rows: VStack<()> = vstack![0;
aligned(Alignment::Start, "Start"),
Divider::new(),
aligned(Alignment::Center, "Center"),
Divider::new(),
aligned(Alignment::End, "End"),
];
assert_eq!(Alignment::default(), Alignment::Start);

See also: VStack::align, HStack::align