Skip to content

layout

<View> が解釈する flex と grid の属性を、1 節につき 1 つずつ。

アプリというより、目で見る表です。ラベル付きの節が 7 つあり、それぞれが属性の1 グループの働きを描きます。direction、justify と align に grow、wrap と gap、入れ子、display="grid" と cols、そして葉としての egui 自身のコンテナ。どの節も箱で、どの子も塗りつぶしたチップです。要点は 箱がどこに落ち着くか だからです。何も塗らなければ、justify も grow も見せるものがありません。散文での完全な一覧はレイアウト属性 にあります。このページは、そのうち目で見える部分です。

同時に 2 つのことを見せています。flex と grid の節はレイアウトエンジンです。自前のエンジンの下に taffy がいます。<Grid>、<Row>、<Vertical>、<Label> の節は egui 自身の コンテナで、そのほうが馴染みがあるか安い場面のために要素として残してあります。その子は素の Ui モードで描かれるので、<View> がまた木を始めるまで、中でレイアウト属性は何もしません。

素の egui のタブは、とくにこのページで開く価値があります。egui に flexbox はありません。だから同じ絵を、手で測って手で置いて作ることになります。行数の差はそこから来ますし、レイアウトこそ、このライブラリが存在する理由です。

自分で動かす ​

sh
cargo run -p layout
cargo run -p layout --bin layout-plain    # 素の egui 版
trunk serve --config examples/layout/Trunk.toml

ソースは examples/layout/src/lib.rsと plain.rs です。

rust
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>
    }
}
rust
/// 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
}

MIT または Apache-2.0 ライセンスで公開しています。