layout
Every flex and grid attribute <View> understands, one section each.
A visual table rather than an app: seven labelled sections, each drawing what one group of attributes does — direction, justify, align with grow, wrap with gap, nesting, display="grid" with cols, and egui's own containers as leaves. Every section is a box and every child is a filled chip, because the point is where the boxes end up: with nothing painted, justify and grow have nothing to show. The Layout attributes reference is the full list in prose; this page is the part of it you can see.
Two things are being shown at once. The flex and grid sections are the layout engine, which is taffy under an engine of our own. The <Grid>, <Row>, <Vertical> and <Label> sections are egui's own containers, kept as elements for the places where they are more familiar or cheaper: their children are drawn in plain Ui mode, so layout attributes do nothing inside them until a <View> starts a tree again.
The plain egui tab is worth opening on this one in particular. There is no flexbox in egui, so the same pictures are produced by measuring and placing things by hand — which is where the line-count difference comes from, and why layout is the feature this library exists for.
Run it yourself
cargo run -p layout
cargo run -p layout --bin layout-plain # the plain egui version
trunk serve --config examples/layout/Trunk.tomlThe source is examples/layout/src/lib.rs and plain.rs.
use egui_reactor::prelude::*;
use egui_reactor_elements::prelude::*;
/// The two fills the tour is drawn with, and the corner they share.
///
/// From egui's visuals, so the tour follows the theme, and in one place, so
/// the plain egui version paints the same picture — the snapshot test compares
/// the two pixel for pixel.
pub struct Look {
/// The section's box: what the children are laid out inside.
pub box_fill: egui::Color32,
/// One child.
pub chip: egui::Color32,
}
/// The corner radius of both.
pub const RADIUS: f32 = 4.0;
pub fn look(ctx: &egui::Context) -> Look {
let visuals = &ctx.style_of(ctx.theme()).visuals;
Look {
box_fill: visuals.extreme_bg_color,
chip: visuals.widgets.inactive.bg_fill,
}
}
/// One labelled section of the demo: a title, and a box the children are laid
/// out inside.
#[component]
fn Section(cx: &mut Cx, title: &str, children: impl View) {
let look = look(cx.ctx());
rsx! {
<View direction="column" gap={4} mb={12}>
<Text strong>{title}</Text>
<View
direction="column"
gap={4}
p={8}
w="100%"
bg={look.box_fill}
radius={RADIUS}
>
{children}
</View>
</View>
}
}
/// A coloured box, so the layout is visible.
#[component]
fn Chip(cx: &mut Cx, #[prop(default)] style: ItemStyle, label: &str) {
let look = look(cx.ctx());
rsx! {
// The shorthand attributes chain onto whatever `style=` passed in, so a
// wrapper can take its caller's layout and add to it.
<View style={style} p={6} bg={look.chip} radius={RADIUS}>
<Text>{label}</Text>
</View>
}
}
#[component]
pub fn App(cx: &mut Cx) {
rsx! {
// The root fills whatever area it is given, so the gallery can drop it
// into a column of its own.
<View direction="column" grow={1.0}>
<ScrollArea grow={1.0}>
<View direction="column" p={12} w="100%">
<Text size={22.0} strong>"layout"</Text>
<Section title="direction=\"row\"">
<View direction="row" gap={8}>
<Chip label="one"/>
<Chip label="two"/>
<Chip label="three"/>
</View>
</Section>
<Section title="justify">
for justify in ["start", "center", "end", "space-between", "space-around"] {
<View key={justify} direction="row" justify={justify} w="100%" mb={4}>
<Chip label="a"/>
<Chip label="b"/>
<Chip label="c"/>
</View>
}
</Section>
<Section title="align + grow">
<View direction="row" align="center" gap={8} h={64.0} w="100%">
<Chip label="fixed"/>
<Chip grow={1.0} label="grow=1"/>
<Chip label="fixed"/>
</View>
</Section>
<Section title="wrap + gap">
<View direction="row" wrap gap={(8.0, 4.0)} w={260.0}>
for i in 0..8 {
<Chip key={i} label={&format!("item {i}")}/>
}
</View>
</Section>
<Section title="nested">
<View direction="row" gap={8} w="100%">
<View direction="column" gap={4} grow={1.0}>
<Chip label="left top"/>
<Chip label="left bottom"/>
</View>
<View direction="column" gap={4} grow={2.0}>
<Chip label="right top"/>
<Chip label="right bottom"/>
</View>
</View>
</Section>
<Section title="display=\"grid\" cols={3}">
<View display="grid" cols={3} gap={6} w="100%">
<Chip col_span={3} label="spans three columns"/>
for i in 0..6 {
<Chip key={i} label={&format!("cell {i}")}/>
}
</View>
</Section>
<Section title="egui containers as leaves">
<View direction="row" gap={8} align="start" w="100%">
<Grid cols={2}>
<Row>
<Label>"grid a1"</Label>
<Label>"grid b1"</Label>
</Row>
<Row>
<Label>"grid a2"</Label>
<Label>"grid b2"</Label>
</Row>
</Grid>
<Vertical>
<Label>"vertical 1"</Label>
<Label>"vertical 2"</Label>
</Vertical>
</View>
</Section>
</View>
</ScrollArea>
</View>
}
}/// The plain version keeps nothing between frames; the tour has no state.
///
/// It exists so the gallery can drive every plain example through one shape.
#[derive(Default)]
pub struct PlainState;
/// The padding inside a chip, matching `<Chip p={6}>`.
const CHIP_PAD: f32 = 6.0;
use crate::{RADIUS, look};
pub fn ui(ui: &mut egui::Ui, _state: &mut PlainState) {
egui::ScrollArea::vertical().show(ui, |ui| {
egui::Frame::new().inner_margin(12.0).show(ui, |ui| {
ui.spacing_mut().item_spacing.y = 0.0;
ui.label(egui::RichText::new("layout").size(22.0).strong());
section(ui, "direction=\"row\"", |ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 8.0;
for label in ["one", "two", "three"] {
chip(ui, label);
}
});
});
section(ui, "justify", |ui| {
// The rows carry `mb={4}` on top of the section's own `gap={4}`,
// so 8 between them and 4 after the last. Spelled out, because
// egui adds its own spacing around anything it is not told not
// to.
ui.spacing_mut().item_spacing.y = 0.0;
for (i, justify) in ["start", "center", "end", "space-between", "space-around"]
.into_iter()
.enumerate()
{
if i > 0 {
ui.add_space(8.0);
}
justified_row(ui, justify, &["a", "b", "c"]);
}
ui.add_space(4.0);
});
section(ui, "align + grow", align_and_grow);
section(ui, "wrap + gap", |ui| {
let labels: Vec<String> = (0..8).map(|i| format!("item {i}")).collect();
wrapped(ui, &labels, 260.0, egui::vec2(8.0, 4.0));
});
section(ui, "nested", nested);
section(ui, "display=\"grid\" cols={3}", grid);
section(ui, "egui containers as leaves", |ui| {
// The one section that is the same on both sides: `<Grid>` and
// `<Vertical>` are these two containers, wrapped as leaves.
ui.horizontal_top(|ui| {
ui.spacing_mut().item_spacing.x = 8.0;
egui::Grid::new("grid").num_columns(2).show(ui, |ui| {
ui.label("grid a1");
ui.label("grid b1");
ui.end_row();
ui.label("grid a2");
ui.label("grid b2");
ui.end_row();
});
ui.vertical(|ui| {
ui.label("vertical 1");
ui.label("vertical 2");
});
});
});
});
});
}
/// One labelled section: `<Section>` in the egui-reactor version.
fn section(ui: &mut egui::Ui, title: &str, body: impl FnOnce(&mut egui::Ui)) {
ui.label(egui::RichText::new(title).strong());
ui.add_space(4.0);
egui::Frame::new()
.inner_margin(8.0)
.fill(look(ui.ctx()).box_fill)
.corner_radius(RADIUS)
.show(ui, |ui| {
ui.spacing_mut().item_spacing.y = 4.0;
ui.set_width(ui.available_width());
body(ui);
});
ui.add_space(12.0);
}
/// `justify` by hand.
///
/// Flexbox states where the leftover space goes; egui needs it worked out.
/// Every child is measured, the leftover is divided according to the rule, and
/// the pieces are allocated as explicit gaps.
fn justified_row(ui: &mut egui::Ui, justify: &str, labels: &[&str]) {
let widths: Vec<f32> = labels.iter().map(|label| chip_width(ui, label)).collect();
let free = (ui.available_width() - widths.iter().sum::<f32>()).max(0.0);
let n = labels.len() as f32;
let (lead, between) = match justify {
"start" => (0.0, 0.0),
"center" => (free / 2.0, 0.0),
"end" => (free, 0.0),
"space-between" => (0.0, free / (n - 1.0)),
"space-around" => (free / (2.0 * n), free / n),
other => panic!("unknown justify {other:?}"),
};
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.add_space(lead);
for (i, label) in labels.iter().enumerate() {
if i > 0 {
ui.add_space(between);
}
chip(ui, label);
}
});
}
/// `align="center"` on a 64px row, with `grow={1.0}` on the middle chip.
///
/// The row's height has to be allocated up front so there is something to
/// centre against, and the growing chip's width is the leftover, computed.
fn align_and_grow(ui: &mut egui::Ui) {
let gap = 8.0;
let fixed = chip_width(ui, "fixed");
let width = ui.available_width();
let grown = (width - 2.0 * fixed - 2.0 * gap).max(0.0);
ui.allocate_ui_with_layout(
egui::vec2(width, 64.0),
egui::Layout::left_to_right(egui::Align::Center),
|ui| {
// The row keeps its height whatever the chips do with it: the
// other side's `h={64}` is the row's, not the chips'.
ui.set_min_size(egui::vec2(width, 64.0));
ui.spacing_mut().item_spacing.x = gap;
// Every chip is allocated its own box first. A `Frame` fills the
// height it is offered, and here that is the whole 64pt row,
// where the other side puts a chip its own height and centres it.
let h = chip_height(ui);
boxed(ui, egui::vec2(fixed, h), |ui| chip(ui, "fixed"));
boxed(ui, egui::vec2(grown, h), |ui| {
sized_chip(ui, "grow=1", grown)
});
boxed(ui, egui::vec2(fixed, h), |ui| chip(ui, "fixed"));
},
);
}
/// `wrap` by hand: break the children into lines that fit, then draw the lines.
fn wrapped(ui: &mut egui::Ui, labels: &[String], width: f32, gap: egui::Vec2) {
let mut lines: Vec<Vec<&str>> = Vec::new();
let mut line: Vec<&str> = Vec::new();
let mut used = 0.0;
for label in labels {
let needed = if line.is_empty() {
chip_width(ui, label)
} else {
used + gap.x + chip_width(ui, label)
};
if !line.is_empty() && needed > width {
lines.push(std::mem::take(&mut line));
used = chip_width(ui, label);
} else {
used = needed;
}
line.push(label);
}
lines.push(line);
ui.spacing_mut().item_spacing.y = gap.y;
for line in lines {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = gap.x;
for label in line {
chip(ui, label);
}
});
}
}
/// Two columns with `grow={1.0}` and `grow={2.0}`: a third and two thirds.
fn nested(ui: &mut egui::Ui) {
let gap = 8.0;
let labels = [["left top", "left bottom"], ["right top", "right bottom"]];
let grow = [1.0, 2.0];
// `grow` shares out what is *left over*, not the whole row, so each
// column's own width has to be measured before the leftover can be split.
let bases: Vec<f32> = labels
.iter()
.map(|column| {
column
.iter()
.map(|label| chip_width(ui, label))
.fold(0.0, f32::max)
})
.collect();
let free = (ui.available_width() - gap - bases.iter().sum::<f32>()).max(0.0);
let total: f32 = grow.iter().sum();
let widths: Vec<f32> = bases
.iter()
.zip(grow)
.map(|(base, grow)| base + free * grow / total)
.collect();
ui.horizontal_top(|ui| {
ui.spacing_mut().item_spacing.x = gap;
for (width, labels) in widths.iter().zip(labels) {
// A plain `allocate_ui` would inherit the row's left-to-right
// layout and put all four chips in one line, so each column asks
// for its own top-down `Ui` of the width worked out above.
ui.allocate_ui_with_layout(
egui::vec2(*width, 0.0),
egui::Layout::top_down(egui::Align::Min),
|ui| {
// The allocation shrinks to its content unless the column
// insists, and then the second column would start against
// the first one's text instead of at a third of the row.
ui.set_min_width(*width);
ui.spacing_mut().item_spacing.y = 4.0;
for label in labels {
// The other side's chips are stretched to the column
// by the cross-axis default; here the width has just
// been worked out, so it is passed in.
sized_chip(ui, label, *width);
}
},
);
}
});
}
/// `display="grid" cols={3}`, including one cell spanning all three columns.
///
/// `egui::Grid` sizes its columns from their content, so equal columns and a
/// spanning cell both need the column width computed and allocated by hand.
fn grid(ui: &mut egui::Ui) {
let gap = 6.0;
let full = ui.available_width();
let column = (full - 2.0 * gap) / 3.0;
ui.spacing_mut().item_spacing.y = gap;
ui.horizontal(|ui| sized_chip(ui, "spans three columns", full));
for row in 0..2 {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = gap;
for col in 0..3 {
sized_chip(ui, &format!("cell {}", row * 3 + col), column);
}
});
}
}
/// A chip: a label with `<Chip p={6}>`'s padding and fill around it.
fn chip(ui: &mut egui::Ui, label: &str) {
chip_frame(ui).show(ui, |ui| ui.label(label));
}
/// A chip stretched to `width`, for the cases where taffy would have sized it.
fn sized_chip(ui: &mut egui::Ui, label: &str, width: f32) {
chip_frame(ui).show(ui, |ui| {
ui.set_min_width((width - 2.0 * CHIP_PAD).max(0.0));
ui.label(label);
});
}
/// `<Chip>`'s box: the same padding, fill and corner.
fn chip_frame(ui: &egui::Ui) -> egui::Frame {
egui::Frame::new()
.inner_margin(CHIP_PAD)
.fill(look(ui.ctx()).chip)
.corner_radius(RADIUS)
}
/// A box of exactly `size` for one chip to be drawn in.
///
/// `Frame` takes the height it is offered, so a chip in a row taller than
/// itself needs to be offered its own height and no more.
fn boxed(ui: &mut egui::Ui, size: egui::Vec2, body: impl FnOnce(&mut egui::Ui)) {
ui.allocate_ui_with_layout(size, egui::Layout::top_down(egui::Align::Min), body);
}
/// How tall [`chip`] will be: one line of text and the padding around it.
fn chip_height(ui: &egui::Ui) -> f32 {
ui.text_style_height(&egui::TextStyle::Body) + 2.0 * CHIP_PAD
}
/// How wide [`chip`] will be.
fn chip_width(ui: &egui::Ui, label: &str) -> f32 {
let font = egui::TextStyle::Body.resolve(ui.style());
let text = ui
.painter()
.layout_no_wrap(label.to_owned(), font, egui::Color32::PLACEHOLDER)
.size()
.x;
text + 2.0 * CHIP_PAD
}