styles
style 属性を表に全部: 名前、使うコード、描かれる姿。
属性 1 つにつき 1 行。名前、コード、そのコードが作る絵です。どの属性も、すべての要素が取る 1 つの style prop を通ります。レイアウト側(w、grow、p など)はレイアウトエンジンが解決し、描画側(bg、border、radius、shadow、custom_shadow、opacity)はそのノード自身のボックスに描かれます。
注目に値するのは真ん中の列です。手で打ち込んだものではありません。各行は「描かれる要素」として 1 回だけ書かれ、マクロが同じトークンを stringify! してコード列にします。だからテキストと絵がずれることはありません。これはマクロ自身の規則の小さな実演でもあります。macro_rules! を通して渡された要素も、それが座っている rsx! のクロージャに解決されるからです。
描画の挙動が 2 つ、散文で読むよりここで見るほうが分かりやすいです。1 つ、枠線はレイアウトです。その太さはパディングの縁に乗るので、子はストロークの内側から始まり、枠線は子が入らなかったその帯にちょうど描かれます。2 つ、自分でボックスを描くウィジェット ― <Button> や <TextEdit> ― は、こちらがボックスを描いた時点で自分のボックスを譲るので、二重になりません。表はレイアウト属性 にあります。
自分で動かす
sh
cargo run -p styles
trunk serve --config examples/styles/Trunk.tomlソースは examples/styles/src/lib.rs です。
rust
use egui_reactor::prelude::*;
use egui_reactor_elements::prelude::*;
/// The stage every row draws on: wide enough for two chips side by side,
/// tall enough for a chip to be clamped well short of the top.
const STAGE_W: f32 = 180.0;
const STAGE_H: f32 = 60.0;
/// A shadow of our own, for the `custom_shadow` row.
const SHADOW: egui::Shadow = egui::Shadow {
offset: [6, 6],
blur: 4,
spread: 0,
color: egui::Color32::from_rgba_premultiplied(0, 0, 0, 160),
};
/// The colours the table is drawn with, read once from the theme.
#[derive(Clone, Copy)]
struct Look {
/// A chip's background, which is where its padding shows.
chip: egui::Color32,
/// The box the text sits in, inside the padding.
content: egui::Color32,
/// The box around a chip's margin, on the rows that have one.
margin: egui::Color32,
/// The stage behind the chips.
stage: egui::Color32,
/// The line between two rows.
line: egui::Color32,
}
impl Look {
fn of(visuals: &egui::Visuals) -> Self {
Self {
chip: visuals.selection.bg_fill,
// A translucent white, so the content box is a lighter shade of
// whatever the chip's background is, on the `bg` row too.
content: egui::Color32::from_rgba_unmultiplied(0xff, 0xff, 0xff, 0x30),
margin: egui::Color32::from_rgba_unmultiplied(0xff, 0xa0, 0x30, 0x50),
stage: visuals.extreme_bg_color,
line: visuals.widgets.noninteractive.bg_stroke.color,
}
}
}
/// The box a row draws its attribute on: a `<View>` with a background and a
/// little padding, holding a text in a lighter box of its own, so the padding
/// is the band between the two.
///
/// The caller's `style` wins: a row about `bg` or `p` sets its own, and the
/// defaults only fill in what the row left unsaid.
#[component]
fn Chip(cx: &mut Cx, #[prop(default)] style: ItemStyle, look: Look) {
let mut style = style;
if style.paint.bg.is_none() {
style = style.bg(look.chip);
}
if style.p.is_none() && style.px.is_none() && style.py.is_none() {
style = style.p(6);
}
rsx! {
<View style={style} justify="center" align="center">
<Text bg={look.content} px={4}>"box"</Text>
</View>
}
}
/// One row of the table: the attribute's name, the code, and the stage the
/// code draws on. `grid` swaps the stage for a three-column grid, twice as
/// tall, for the two grid attributes. `margins` puts the chips in a box of
/// the margin colour, so the margin is a band and not just a gap.
#[component]
fn Row(
cx: &mut Cx,
look: Look,
name: &str,
code: String,
#[prop(default)] grid: bool,
#[prop(default)] margins: bool,
children: impl View,
) {
// A flex stage lines its chips up at the top, so a chip's own height and
// `align_self` show; a grid stage stretches them into their cells, so a
// spanning chip fills what it spans.
let (display, align, h) = if grid {
("grid", "normal", STAGE_H * 2.0)
} else {
("flex", "start", STAGE_H)
};
rsx! {
<View direction="column" w="100%">
<View direction="row" align="center" gap={12} w="100%" py={6}>
<Text w={90.0} strong>{name}</Text>
// The code, in the code font, wrapped to whatever width is
// left once the name and the stage have taken theirs.
<Text grow={1.0} wrap>{egui::RichText::new(code).monospace()}</Text>
<View
display={display}
cols={3}
direction="row"
align={align}
gap={4}
w={STAGE_W}
h={h}
shrink={0.0}
bg={look.stage}
>
// No padding on the stage: a chip with no margin touches
// its edge, so a margin is the only thing that can move it.
if margins {
<View direction="row" align="start" bg={look.margin}>
{children}
</View>
} else {
{children}
}
</View>
</View>
// The line under the row: a painted `<View>`, one point tall.
<View h={1.0} w="100%" bg={look.line}/>
</View>
}
}
/// A heading between two groups of rows.
#[component]
fn Group(cx: &mut Cx, title: &str) {
rsx! {
<Text size={16.0} strong mt={16} mb={4}>{title}</Text>
}
}
/// One entry of the legend: a square of the colour and what it stands for.
#[component]
fn Swatch(cx: &mut Cx, color: egui::Color32, label: &str) {
rsx! {
<View direction="row" align="center" gap={4}>
<View w={12.0} h={12.0} bg={color}/>
<Text>{label}</Text>
</View>
}
}
/// `stringify!` puts a space around every token; take them back out so the
/// code column reads as it was written.
pub fn tidy(code: &str) -> String {
let mut out = code.to_owned();
for (from, to) in [
("< ", "<"),
(" >", ">"),
(" / >", "/>"),
(" />", "/>"),
(" = ", "="),
("{ ", "{"),
(" }", "}"),
("( ", "("),
(" )", ")"),
("[ ", "["),
(" ]", "]"),
(" ,", ","),
(" :: ", "::"),
(" . ", "."),
] {
out = out.replace(from, to);
}
out
}
/// One row, written once: the tokens draw the stage and, `stringify!`ed,
/// fill the code column. `key` is the name, so two rows expanded from the
/// same macro line do not share a scope.
macro_rules! row {
($look:expr, $name:literal, grid, $($body:tt)*) => {
rsx! {
<Row key={$name} look={$look} name={$name} code={tidy(stringify!($($body)*))} grid>
$($body)*
</Row>
}
};
($look:expr, $name:literal, margins, $($body:tt)*) => {
rsx! {
<Row key={$name} look={$look} name={$name} code={tidy(stringify!($($body)*))} margins>
$($body)*
</Row>
}
};
($look:expr, $name:literal, $($body:tt)*) => {
rsx! {
<Row key={$name} look={$look} name={$name} code={tidy(stringify!($($body)*))}>
$($body)*
</Row>
}
};
}
#[component]
pub fn App(cx: &mut Cx) {
let look = Look::of(cx.ui().visuals());
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>"styles"</Text>
<Text>
"Every attribute the `style` prop takes. `Chip` is a `<View p={6}>` around a text."
</Text>
<View direction="row" align="center" gap={12} my={8}>
<Swatch color={look.margin} label="margin"/>
<Swatch color={look.chip} label="padding"/>
<Swatch color={look.content} label="content"/>
</View>
<View direction="row" align="center" gap={12} w="100%" py={6}>
<Text w={90.0} strong>"style"</Text>
<Text grow={1.0} strong>"code"</Text>
<Text w={STAGE_W} strong>"output"</Text>
</View>
<View h={1.0} w="100%" bg={look.line}/>
<Group title="size"/>
{row!(look, "w", <Chip look={look} w={120.0}/>)}
{row!(look, "h", <Chip look={look} h={44.0}/>)}
// Two chips that do not fit: both shrink, one not below
// its minimum. Two that grow: one not past its maximum.
{row!(look, "min_w", <Chip look={look} w={150.0} min_w={120.0}/> <Chip look={look} w={150.0}/>)}
{row!(look, "min_h", <Chip look={look} h="50%" min_h={44.0}/> <Chip look={look} h="50%"/>)}
{row!(look, "max_w", <Chip look={look} grow={1.0} max_w={100.0}/> <Chip look={look} grow={1.0}/>)}
{row!(look, "max_h", <Chip look={look} h="100%" max_h={32.0}/> <Chip look={look} h="100%"/>)}
<Group title="flex item"/>
{row!(look, "grow", <Chip look={look} grow={1.0}/> <Chip look={look}/>)}
{row!(look, "shrink", <Chip look={look} w={120.0} shrink={0.0}/> <Chip look={look} w={120.0}/>)}
{row!(look, "basis", <Chip look={look} basis={100.0}/> <Chip look={look}/>)}
{row!(look, "align_self", <Chip look={look} align_self="end"/> <Chip look={look}/>)}
<Group title="margin"/>
{row!(look, "m", margins, <Chip look={look} m={8}/>)}
{row!(look, "mx", margins, <Chip look={look} mx={8}/>)}
{row!(look, "my", margins, <Chip look={look} my={8}/>)}
{row!(look, "mt", margins, <Chip look={look} mt={8}/>)}
{row!(look, "mr", margins, <Chip look={look} mr={8}/> <Chip look={look}/>)}
{row!(look, "mb", margins, <Chip look={look} mb={8}/>)}
{row!(look, "ml", margins, <Chip look={look} ml={8}/>)}
<Group title="padding"/>
{row!(look, "p", <Chip look={look} p={12}/>)}
{row!(look, "px", <Chip look={look} px={12}/>)}
{row!(look, "py", <Chip look={look} py={12}/>)}
{row!(look, "pt", <Chip look={look} pt={12}/>)}
{row!(look, "pr", <Chip look={look} pr={12}/>)}
{row!(look, "pb", <Chip look={look} pb={12}/>)}
{row!(look, "pl", <Chip look={look} pl={12}/>)}
<Group title="grid item"/>
{row!(look, "col_span", grid, <Chip look={look} col_span={2}/> <Chip look={look}/> <Chip look={look}/>)}
{row!(look, "row_span", grid, <Chip look={look} row_span={2}/> <Chip look={look}/> <Chip look={look}/> <Chip look={look}/> <Chip look={look}/>)}
<Group title="paint"/>
{row!(look, "bg", <Chip look={look} bg={egui::Color32::from_rgb(0xd0, 0x60, 0x40)}/>)}
{row!(look, "border", <Chip look={look} border={egui::Stroke::new(2.0, egui::Color32::RED)}/>)}
{row!(look, "radius", <Chip look={look} radius={12.0}/>)}
{row!(look, "shadow", <Chip look={look} m={8} shadow/>)}
{row!(look, "custom_shadow", <Chip look={look} m={8} custom_shadow={SHADOW}/>)}
{row!(look, "opacity", <Chip look={look} opacity={0.4}/>)}
</View>
</ScrollArea>
</View>
}
}