Expand description
§EFx
efx — declarative UI template engine in Rust
efx! is a procedural macro that transforms compact XML-like markup into method calls to your UI (e.g. wrappers over egui/eframe).
§Minimal example
use efx_core::doc_prelude::*;
use efx::*;
efx!(Ui::default(), r#"
<Column>
<Label>Hello</Label>
<Separator/>
<Row><Label>Row</Label></Row>
</Column>
"#);Key Features 0.5
- Tags:
Column,Row,Label,Separator,Button. - Insert expressions:
{expr}within text. - Escaping:
{{→{,}}→}. - Tag attributes are parsed.
§EFx Sandbox (local playground)
efx-sandbox is a helper binary crate kept in this repository. It’s used for manual testing of tags and as a “live” example of how to use the templating macro in a real egui app.
Why use it
- Quickly verify tag behavior in a native window (
eframe/egui). - Keep rich examples and “scenes” outside doctests (no test harness limitations).
- Demonstrate how
efx!integrates with application state.
Where it lives
/efx-sandbox
This crate is part of the workspace and is not published.
How to run
cargo run -p efx-sandboxMake sure
eframe/eguiversions match those used by EFx (we pineframe = "0.32"foregui 0.32.x).
Minimal main.rs example
use eframe::{egui, NativeOptions};
use efx::*; // the efx! macro
use efx_core::doc_prelude::*; // convenient egui prelude
fn main() -> eframe::Result<()> {
eframe::run_native(
"EFx Sandbox",
NativeOptions::default(),
Box::new(|_cc| Box::new(App::default())),
)
}
#[derive(Default)]
struct App {
counter: i32,
input: String,
}
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
// Header
let _ = efx!(ui, r#"
<Column gap="8">
<Label size="20" bold="true">EFx sandbox</Label>
<Separator/>
</Column>
"#);
// Buttons returning Response
ui.horizontal(|ui| {
let inc = efx!(ui, r#"<Button tooltip="Increment">+1</Button>"#);
if inc.clicked() { self.counter += 1; }
let dec = efx!(ui, r#"<Button tooltip="Decrement">-1</Button>"#);
if dec.clicked() { self.counter -= 1; }
});
// Dynamic text
let _ = efx!(ui, r#"<Label>Counter: {self.counter}</Label>"#);
// Text input
let _ = efx!(ui, r#"<TextField value="self.input" hint="type here…"/>"#);
// Scroll + links + styled buttons
let _ = efx!(ui, r#"
<ScrollArea axis="vertical" max_height="160" always_show="true" id="demo-log">
<Column gap="6">
<Label monospace="true">You typed: {self.input.clone()}</Label>
<Row gap="8">
<Hyperlink url="https://efxui.com" tooltip="Project site"/>
<Hyperlink url="help:about" open_external="false">About</Hyperlink>
</Row>
<Separator/>
<Row gap="10" wrap="true">
<Button fill="#333333AA" rounding="8">A</Button>
<Button frame="false">B</Button>
<Button min_width="100" tooltip="Wide">Wide</Button>
</Row>
</Column>
</ScrollArea>
"#);
});
}
}Tips
- Keep several example “scenes” as
&'static strand switch them via aComboBoxto test different tag sets. - Prefer snake_case attributes (
max_height,always_show,stroke_width, …). If a tag supports kebab-case aliases, the tag’s section will mention it. - Colors are
#RRGGBBor#RRGGBBAA(short#RGB/#RGBAis not supported yet).
Why sandbox instead of doctests
Doctests are great for syntax and error messages, but egui requires a proper render loop (Context::run()), which doctests don’t provide. The sandbox runs a real app, while examples in this documentation are marked rust,ignore to avoid execution.
For more information, see the sections below: Supported Tags and Syntax Guide.
§Supported Tags (v0.4+)
Starting with 0.5 some tags support attributes. Unknown attributes result in
compile_error!.
§Column
Vertical container. Generates ui.vertical(|ui| { ... }).
Attributes
align="left|center|right"— horizontal alignment of children.gap="N"— vertical spacing between children (f32).padding="N"— extra top/bottom padding (f32).
use efx_core::doc_prelude::*;
use efx::*;
efx!(Ui::default(), r#"<Column gap="10" padding="6" align="center">
<Label>Title</Label>
<Label size="12">Subtitle</Label>
</Column>"#);§Row
Horizontal container. Generates ui.horizontal(|ui| { ... }).
Attributes
align="top|center|bottom"— vertical alignment of children.gap="N"— horizontal spacing between children (f32).wrap="true|false"— wrap children to next line if overflow.padding="N"— extra left/right padding (f32).
use efx_core::doc_prelude::*;
use efx::*;
efx!(Ui::default(), r#"<Row gap="8" padding="4" align="center"><Label>A</Label><Label>B</Label></Row>"#);
efx!(Ui::default(), r#"<Row wrap="true"><Label>Item1</Label><Label>Item2</Label><Label>Item3</Label></Row>"#);
§<Panel>
A lightweight frame container to group content with background, padding and stroke. Unlike Top/Bottom/Side/CentralPanel, this tag is not a context-root and can be placed anywhere in the UI.
Syntax
<Panel fill="#15151A" padding="8" stroke-width="1" stroke-color="#262A33" id="card-1">
<Column gap="6">
<Label bold="true">Card title</Label>
<Label size="12" color="#AAAAAA">Some description</Label>
</Column>
</Panel>§Attributes
| Name | Type | Description |
|---|---|---|
frame | bool | false → Frame::none(), otherwise Frame::default(). |
fill | color | Background color. |
stroke-width | f32 | Border width. |
stroke-color | color | Border color. |
padding / padding-left/right/top/bottom | f32 | Inner margin. |
margin / margin-left/right/top/bottom | f32 | Outer margin. |
id | string | Stable push_id seed for the panel. |
Returns () (container). Children are rendered inside the frame.
§CentralPanel
Main content area that fills all remaining space. Wraps children in egui::CentralPanel and applies an optional Frame.
Attributes
frame="true|false"— use default frame (true, default) ornone(false).fill="name|#RRGGBB[AA]"— background fill color.stroke-width="N"— frame stroke width (f32).stroke-color="name|#RRGGBB[AA]"— frame stroke color.padding="N"— inner margin on all sides (f32).padding-left|padding-right|padding-top|padding-bottom="N"— per-side inner margin.margin="N"— outer margin on all sides (f32).margin-left|margin-right|margin-top|margin-bottom="N"— per-side outer margin.
use efx_core::doc_prelude::*;
use efx::*;
efx!(Ui::default(), r##"
<CentralPanel fill="#101014" padding="12" stroke-width="1" stroke-color="#222638">
<Column gap="8">
<Label size="18" bold="true">Dashboard</Label>
<Separator space="6"/>
<Row gap="12">
<Label>Welcome!</Label>
<Hyperlink url="https://efxui.com">Docs</Hyperlink>
</Row>
</Column>
</CentralPanel>
"##);§<SidePanel>
Docked panel attached to the left or right edge of the window.
Typically used for navigation, tool palettes, or context inspectors.
Children: rendered inside the panel.
Required attributes
side="left|right"— which edge to dock to.id="string"— eguiIdsalt to keep layout state (width, resize state).
Frame & styling
frame="true|false"— enable/disable the default frame (default:true).fill="#RRGGBB[AA]"— background color.stroke-width="number"— border width, in points.stroke-color="#RRGGBB[AA]"— border color.padding,padding-left|right|top|bottom— inner margin (content padding).margin,margin-left|right|top|bottom— outer margin.
Sizing & behavior
default-width="number"— initial width.min-width="number"— lower width bound.max-width="number"— upper width bound.resizable="true|false"— whether the user can drag to resize (default:true).
Example
<SidePanel side="left" id="nav" default-width="240" min-width="160" resizable="true" fill="#15151A">
<Column gap="8" padding="8">
<Label size="16" bold="true">Navigation</Label>
<Separator/>
<Button frame="false">Home</Button>
<Button frame="false">Projects</Button>
<Button frame="false">Settings</Button>
</Column>
</SidePanel>§<TopPanel>
A docked panel attached to the top edge of the window.
Useful for app bars, toolbars, status strips, or context headers.
Children: rendered inside the panel.
Required attributes
id="string"— eguiIdsalt to persist panel state.
Frame & styling
frame="true|false"— enable/disable default frame (default:true).fill="#RRGGBB[AA]"— background color.stroke-width="number"— border width (points).stroke-color="#RRGGBB[AA]"— border color.padding,padding-left|right|top|bottom— inner margin.margin,margin-left|right|top|bottom— outer margin.
Sizing & behavior
default-height="number"— initial height.min-height="number"— minimum height.max-height="number"— maximum height.resizable="true|false"— allow user resize (default:true).
Example
<TopPanel id="appbar" default-height="36" fill="#15151A" stroke-width="1" stroke-color="#262A33">
<Row gap="8" padding="6">
<Label bold="true">EFx App</Label>
<Separator/>
<Button frame="false">File</Button>
<Button frame="false">Edit</Button>
<Button frame="false">View</Button>
</Row>
</TopPanel>§<BottomPanel>
A docked panel attached to the bottom edge of the window. Great for logs, consoles, timelines, or status bars.
Children: rendered inside the panel.
Required attributes
id="string"— egui Id salt.
Frame & styling
frame="true|false",fill,stroke-width,stroke-color,padding*/margin*— same as<TopPanel>.
Sizing & behavior
default-height,min-height,max-height,resizable— same as.
Example
<BottomPanel id="console" default-height="200" resizable="true" fill="#0F1116">
<ScrollArea axis="vertical" max-height="180" id="console-scroll">
<Column gap="4" padding="6">
<Label monospace="true">[12:00:01] Ready.</Label>
<Label monospace="true">[12:00:02] Build succeeded.</Label>
</Column>
</ScrollArea>
</BottomPanel>§ScrollArea
Scrollable container backed by egui::ScrollArea. Wraps its children and provides vertical/horizontal/both scrolling.
Attributes
axis="vertical|horizontal|both"— scroll axis (default: vertical).always-show="true|false"— always show scrollbar even if content fits.max-height="N"— maximum height of the scroll area (f32).max-width="N"— maximum width of the scroll area (f32).id="text"— id source to persist scroll state between frames.bottom="true|false"— keep view pinned to bottom when new content arrives (useful for logs/chats).right="true|false"— keep view pinned to right on updates.
use efx_core::doc_prelude::*;
use efx::*;
// Vertical log panel with sticky bottom
efx!(Ui::default(), r#"
<ScrollArea axis="vertical" max_height="200" always_show="true" id="log-pane" stick_to_bottom="true">
<Column gap="6">
<Label bold="true">Log:</Label>
<Label>Line 1</Label>
<Label>Line 2</Label>
<Label>Line 3</Label>
</Column>
</ScrollArea>
"#);
// Horizontal scroller
efx!(Ui::default(), r#"
<ScrollArea axis="horizontal" max_width="320" always_show="true">
<Row gap="12">
<Label>Item 1</Label>
<Label>Item 2</Label>
<Label>Item 3</Label>
<Label>Item 4</Label>
</Row>
</ScrollArea>
"#);
// Both directions (e.g., big grid)
efx!(Ui::default(), r#"
<ScrollArea axis="both" max_width="400" max_height="220">
<Column gap="8">
<Row gap="8"><Label>A1</Label><Label>A2</Label><Label>A3</Label><Label>A4</Label></Row>
<Row gap="8"><Label>B1</Label><Label>B2</Label><Label>B3</Label><Label>B4</Label></Row>
<Row gap="8"><Label>C1</Label><Label>C2</Label><Label>C3</Label><Label>C4</Label></Row>
<Row gap="8"><Label>D1</Label><Label>D2</Label><Label>D3</Label><Label>D4</Label></Row>
</Column>
</ScrollArea>
"#);§<Window>
An independent floating window (overlay) with optional frame and persistent state.
Children: rendered inside the window.
Required attributes
title="string"— window title.
Optional
id="string"— eguiIdto persist window state (position/size). If omitted, egui derives an id from the title.
Behavior
open="{expr_bool}"— binds to a boolean state; user closing the window writes back to the expression.movable="true|false"— allow dragging.resizable="true|false"— allow resizing.collapsible="true|false"— allow collapsing to title bar.title-bar="true|false"— show/hide title bar.enabled="true|false"— disable all contents when false.constrain="true|false"— constrain to viewport.auto-sized="true"— size to fit content initially.
Positioning
default-x="number",default-y="number"— initial position.pos-x="number",pos-y="number"— force current position each frame.anchor-h="left|center|right",anchor-v="top|center|bottom",anchor-x="number",anchor-y="number"— anchor to a screen corner/edge with an offset.
Sizing
default-width,default-height— initial size.min-width,min-height— lower bounds.max-width,max-height— upper bounds.
Frame & styling
frame="true|false"— enable/disable default frame (default:true).fill="#RRGGBB[AA]"— background color.stroke-width="number"— border width.stroke-color="#RRGGBB[AA]"— border color.padding,padding-left|right|top|bottom— inner margin.margin,margin-left|right|top|bottom— outer margin.
Example
<Window
id="settings"
title="Settings"
open="{self.show_settings}"
movable="true"
resizable="true"
default-width="360"
default-height="240"
anchor-h="right"
anchor-v="top"
anchor-x="-12"
anchor-y="12"
fill="#14161B"
stroke-width="1"
stroke-color="#262A33"
>
<Column gap="8" padding="8">
<Label bold="true">Preferences</Label>
<Separator/>
<Row gap="8">
<Label>Theme</Label>
<Button min_width="120">System</Button>
<Button min_width="120">Dark</Button>
<Button min_width="120">Light</Button>
</Row>
</Column>
</Window>§Label
Text widget. Only text and interpolations ({expr}) in child nodes are allowed.
Attributes
color="name|#RRGGBB[AA]"— text color.size="N"— font size (f32).bold="true|false".italic="true|false".underline="true|false".strike="true|false".monospace="true|false".wrap="true|false"— enable line wrapping.
use efx_core::doc_prelude::*;
use efx::*;
efx!(Ui::default(), r##"<Label color="#66CCFF" size="16" bold="true">Hello user</Label>"##);§Separator
Self-closing divider. No children allowed (otherwise compile_error!).
Attributes
space="N"— uniform spacing before & after (f32).space_before="N"— spacing above.space_after="N"— spacing below.
use efx_core::doc_prelude::*;
use efx::*;
efx!(Ui::default(), r#"<Separator space="12"/>"#);
efx!(Ui::default(), r#"<Separator space_before="8" space_after="4"/>"#);use efx_core::doc_prelude::*;
use efx::*;
/// compile_fail
efx!(Ui::default(), "<Separator>child</Separator>");§Button
Button is the only tag that returns a response value (Resp) at the root of an expression.
Attributes
fill="color“ — background fill color.rounding="N"— rounding radius (f32).min_width="N", min_height="N"— minimum size.frame="true|false"— draw background/border.enabled="true|false"— disable/enable button.tooltip="text"— hover tooltip.
use efx_core::doc_prelude::*;
use efx::*;
let resp: Resp = efx!(Ui::default(), r#"<Button rounding="8" enabled="false" tooltip="Soon">Run</Button>"#);
assert!(!resp.clicked());§Hyperlink
Clickable link widget. Generates ui.hyperlink(url) or ui.hyperlink_to(label, url).
Attributes
url="..."— destination address (string, required).open_external="true|false"— open link in system browser (default true).color="name|#RRGGBB[AA]"— link text color.underline="true|false"— underline link text (default true).tooltip="text"— hover tooltip.
Cross-platform usage
- Web: renders as standard
<a>link. - Desktop (eframe, bevy_egui): opens system browser via
ui.hyperlink(...). - Game/tool overlays: convenient way to link to docs, repos, or help.
- Offline apps: with custom URL schemes (e.g.
help://topic) may open in-app help instead of browser.
use efx_core::doc_prelude::*;
use efx::*;
efx!(Ui::default(), r##"
<Column>
<Hyperlink url="https://efxui.com" color="#66CCFF" tooltip="Project site"/>
<Hyperlink url="help://about" open_external="false">About</Hyperlink>
</Column>
"##);§TextField
Single-line or multi-line text input. Generates egui::TextEdit and inserts it via ui.add(...). Must be self-closing (no children).
Attributes
value="<expr>"— required. Rust lvalue expression of typeString, e.g.state.name. The generator takes&mut (<expr>)automatically.hint="text"— placeholder text shown when empty.password="true|false"— mask characters (applies to single-line; ignored withmultiline="true").width="N"— desired width in points (f32).multiline="true|false"— multi-line editor (TextEdit::multiline).
use efx_core::doc_prelude::*;
use efx::*;
#[derive(Default)]
struct State { name: String }
let mut state = State::default();
// Single-line with placeholder and width
efx!(Ui::default(), r#"<TextField value="state.name" hint="Your name" width="220"/>"#);
// Password field (single-line)
efx!(Ui::default(), r#"<TextField value="state.name" password="true"/>"#);
// Multiline editor
efx!(Ui::default(), r#"<TextField value="state.name" multiline="true" width="320"/>"#);§<Resize>
A resizable container that lets the user drag a handle to change the size of its content.
Useful for side views, inspectors, consoles, etc., when a full docked panel is too heavy.
Children: rendered inside the resizable area.
Required attributes
id="string"— eguiIdsalt to persist the size across frames.
Behavior
resizable="true|false"— enable/disable user resizing (default:truein egui).
Sizing
default-width="number",default-height="number"— initial size.min-width="number",min-height="number"— lower bounds.max-width="number",max-height="number"— upper bounds.
Each dimension is optional. If only one dimension is provided, the other falls back to
0.0(for min/default) or∞(for max).
Example
<CentralPanel fill="#101014">
<Resize id="console" default-height="200" min-height="120">
<ScrollArea axis="vertical" max_height="9999" id="console-scroll">
<Column gap="6" padding="6">
<Label monospace="true">[12:00:01] Ready.</Label>
<Label monospace="true">[12:00:02] Build succeeded.</Label>
</Column>
</ScrollArea>
</Resize>
</CentralPanel>§Heading
Text heading. Generates ui.heading(text) with optional style overrides.
Attributes
level="1..6"— heading level (integer).
Default:1. Maps to predefinedeguitext styles.size="N"— overrides the font size (f32).color="name|#RRGGBB[AA]"— text color.tooltip="text"— hover tooltip.
use efx_core::doc_prelude::*;
use efx::*;
efx!(Ui::default(), r##"
<Column gap="8">
<Heading level="1">Main title</Heading>
<Heading level="2" color="#66CCFF">Section</Heading>
<Heading level="3" size="14" tooltip="Subheading">Small note</Heading>
</Column>
"##);The level attribute controls the base style (h1–h6), while size and color can further adjust the appearance.
§<Image>
Display a bitmap/texture in the UI. Works both with a preloaded texture handle/id (recommended for desktop) and with a URI-like source (useful on web or when you have your own loader).
§Syntax
<Image
texture="self.logo_tex_id"
width="128"
height="128"
rounding="6"
clickable="true"
tooltip="Click to open"
/>or
<Image
src="assets/logo.png"
max-width="256"
maintain-aspect="true"
id="logo-1"
/>§Attributes
| Name | Type | Default | Description |
|---|---|---|---|
texture | expr (egui::TextureId, &egui::TextureHandle, or egui::ImageSource) | — | Source texture/handle. Mutually exclusive with src. |
src | string (URI/path) | — | Image URI/path. Mutually exclusive with texture. |
width | f32 | — | Target width. If both width and height are set, uses exact size. |
height | f32 | — | Target height. If both width and height are set, uses exact size. |
max-width | f32 | ∞ | Max width (used if exact size isn’t specified). |
max-height | f32 | ∞ | Max height (used if exact size isn’t specified). |
maintain-aspect | bool | false | Keep original aspect ratio when fitting. |
rounding | u8 | — | Uniform corner radius. |
tint | color | — | Multiplies image color (e.g. #FFFFFF80 for 50% fade). |
bg-fill | color | — | Background fill behind the image rect. |
id | string | — | Stable id seed (id_source) for consistent layout/caching. |
clickable | bool | false | If true, image responds to clicks (Sense::click). |
tooltip | string | — | Hover text shown on the image. |
Either
textureorsrcmust be provided (not both).<Image>does not accept children.
§Behavior & sizing rules
- Exact size: if both
widthandheightare set → the image is fit to that exactvec2(width, height). - Max size: otherwise, a max box is computed from
max-width/max-height(falling back towidth/heightif only one side is provided). - Aspect:
maintain-aspect="true"keeps the original ratio when fitting. - Interactivity: with
clickable="true"the tag returns a normalResponseyou can query (.clicked(), etc.). Tooltips are applied viaon_hover_text.
§Examples
URI/path source (web / custom loader):
<Image src="assets/logo.png" max-width="200" maintain-aspect="true" id="logo-main"/>Tint + background fill:
<Image texture="self.icon_tex" tint="#FFFFFFCC" bg-fill="#00000022" rounding="4"/>§Notes
roundingis uniform; per-corner radii can be added later if needed.idhelps egui keep the same widget identity across frames when the source is otherwise dynamic.- On desktop, prefer
texturewith a previously allocatedTextureId/TextureHandlefor performance and control. On web,srccan be convenient alongside your asset loader.
§<Tabs> and <Tab>
Tabbed container. Controlled via a string-like active binding that holds the id of the currently selected tab.
Syntax
<Tabs active="self.active_tab" gap="8">
<Tab id="home" title="Home">
<Label>Welcome home!</Label>
</Tab>
<Tab id="logs" title="Logs">
<ScrollArea axis="vertical" max-height="180">
<Label monospace="true">[12:00:01] Ready.</Label>
</ScrollArea>
</Tab>
<Tab id="about" title="About" enabled="false">
<Label>This tab is disabled</Label>
</Tab>
</Tabs>Attributes – <Tabs>
| Name | Type | Default | Description |
|---|---|---|---|
active | expr | required | String/&str expression with the id of the active tab ("home", "logs", …). |
gap | f32 | ui.spacing().item_spacing.x | Space between tab headers (px). |
Attributes – <Tab>
| Name | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique tab id. Used for matching and as default title. |
title | string | id | Header text. |
enabled | bool | true | When false, the tab header is disabled and cannot be selected. |
Behavior
-
Clicking a tab header updates active to that tab’s
id. You can readactivefrom your state to switch content. -
<Tab>is only allowed as a child of<Tabs>and may contain any regular EFx content in its body. -
Returns
()(container).
§<Table>, <Tr>, <Td>
Lightweight tables built on top of egui::Grid. Suitable for most static layouts. For resizable/feature-rich tables we plan a <DataTable> based on egui_extras::TableBuilder (future work).
Syntax
<Table columns="3" striped="true" spacing-x="8" spacing-y="4" cell-align="left" cell-padding="4" id="users">
<Tr>
<Td><Label bold="true">Name</Label></Td>
<Td><Label bold="true">Email</Label></Td>
<Td><Label bold="true">Role</Label></Td>
</Tr>
<Tr>
<Td><Label>Alice</Label></Td>
<Td><Label>alice@example.com</Label></Td>
<Td><Label>Admin</Label></Td>
</Tr>
</Table>Attributes – <Table>
| Name | Type | Default | Description |
|---|---|---|---|
columns | int | required | Number of columns (must be ≥ 1). |
striped | bool | false | Alternate row background. |
spacing-x | f32 | 8 | Horizontal spacing between columns (px). |
spacing-y | f32 | 4 | Vertical spacing between rows (px). |
cell-padding | f32 | 0 | Padding inside each cell (px). |
cell-align | enum | left | Horizontal alignment: left, center, right. |
id | string | — | Stable id for grid instance. |
Rules
- Only
<Tr>children are allowed inside<Table>. <Tr>may only contain<Td>elements.colspan/rowspanare not supported in this version (a compile error is emitted if used).
Returns () (container). Content inside <Td> can be any EFx widgets.
Notes
- This implementation uses
egui::Gridto keep dependencies minimal and performance high. - If you need column resizing, multi-row headers, scrolling inside the table, etc., we’ll introduce a
<DataTable>tag based onegui_extras::TableBuilderbehind an optional feature flag in a follow-up.
§<DataTable> (requires features = ["extras"])
Feature-rich table built on top of egui_extras::TableBuilder.
Columns
Declare columns via <Columns> and <Column>:
mode="auto|initial|exact|remainder"width="..."(required forinitial/exact)resizable="true|false"(defaults to table’sdefault-resizable)clip="true|false"
Header
Single header row specified via <Header> with <Td> children (one per column).
Body
Body consists of <Tr> rows with <Td> cells (missing cells are filled with blanks). Per-row height can be set via height on <Tr>.
Example
<DataTable id="users" striped="true" resizable="true"
default-resizable="true" header-height="24" row-height="22"
cell-align="left" cell-padding="4">
<Columns>
<Column mode="initial" width="160" resizable="true"/>
<Column mode="auto"/>
<Column mode="remainder" resizable="true" clip="true"/>
</Columns>
<Header>
<Td><Label bold="true">Name</Label></Td>
<Td><Label bold="true">Email</Label></Td>
<Td><Label bold="true">Role</Label></Td>
</Header>
<Tr>
<Td><Label>Alice</Label></Td>
<Td><Label>alice@example.com</Label></Td>
<Td><Label>Admin</Label></Td>
</Tr>
<Tr>
<Td><Label>Bob</Label></Td>
<Td><Label>bob@example.com</Label></Td>
<Td><Label>User</Label></Td>
</Tr>
</DataTable>Attributes – <DataTable>
| Name | Type | Default | Description |
|---|---|---|---|
id | string | — | Stable id (push_id) wrapping the whole table. |
striped | bool | false | Alternate row background. |
resizable | bool | false | Enables column resizing globally (can be overridden per column). |
default-resizable | bool | — | Default resizable for each <Column> if not set. |
header-height | f32 | 22 | Header row height (px). |
row-height | f32 | 22 | Default body row height (px). |
cell-padding | f32 | 0 | Inner padding per cell, in px (applied on both sides). |
cell-align | enum | left | Horizontal alignment in cells: left, center, right. |
Children order
<Columns> → optional <Header> → <Tr>*.
Multiple <Header> are not allowed; <Header> must have exactly one row, with exactly one <Td> per column.
Notes
colspan/rowspanare not supported in this version (compile error if used).- Returns
()(container).
§<Grid> and <GridBreak/>
General-purpose layout grid built on top of egui::Grid. Children are placed row-major (left-to-right, top-to-bottom). Use <GridBreak/> to start a new row explicitly.
Syntax
<Grid columns="3" spacing-x="8" spacing-y="4" cell-align="left" cell-padding="4" striped="true" id="cards">
<Panel fill="#15151A" padding="8"><Label>Card A</Label></Panel>
<Panel fill="#15151A" padding="8"><Label>Card B</Label></Panel>
<Panel fill="#15151A" padding="8"><Label>Card C</Label></Panel>
<GridBreak/>
<Panel fill="#15151A" padding="8"><Label>Card D</Label></Panel>
</Grid>Attributes – <Grid>
| Name | Type | Default | Description |
|---|---|---|---|
columns | int | required | Number of columns (≥ 1). |
striped | bool | false | Alternate row background. |
spacing-x | f32 | 8 | Horizontal spacing between columns (px). |
spacing-y | f32 | 4 | Vertical spacing between rows (px). |
cell-padding | f32 | 0 | Inner padding in each cell (px, applied on both sides). |
cell-align | enum | left | Horizontal alignment in cells: left, center, right. |
id | string | — | Stable id for the grid instance. |
Rules
- Direct children must be elements (widgets/containers). Text nodes are not allowed at top level.
<GridBreak/>starts a new row. Without it, rows wrap automatically whencolumnsis reached.- Returns
()(container).
§Syntax guide
§Structure
- Elements:
<Name ...>children</Name>and self-closing<Name .../>. - Text nodes and
{expr}interpolations are allowed insideLabel/Button. - Multiple elements are allowed on the root - a block with a list of expressions will be generated.
§Interpolations
You can insert arbitrary Rust expressions inside the text:
use efx_core::doc_prelude::*;
use efx::*;
efx!(Ui::default(), r#"<Label>Hello {1 + 1}</Label>"#);§Safety of {expr} interpolations
Sometimes developers familiar with PHP or JavaScript templating engines may worry that expressions inside templates could be unsafe or mix logic with markup.
EFx works differently:
-
Compile-time only:
{expr}is expanded by the Rust compiler. There is noeval, no dynamic string execution at runtime. -
Type-safe: inserted code is just normal Rust, fully checked by the compiler. If the expression does not compile, the template fails to compile.
-
Limited scope: interpolations are only allowed inside textual tags such as
<Label>or<Button>, where they expand into calls like:use efx_core::doc_prelude::*; use efx::efx; let user_name = "Max"; efx!(Ui::default(), "<Label>Hello {user_name}</Label>"); // expands to: Ui::default().label(format!("Hello {}", user_name)); -
No injection risk: unlike PHP templating, there is no way for untrusted data to introduce new code. All values are rendered through
format!/Display.
In short, EFx keeps declarative style while preserving Rust’s compile-time guarantees. This makes interpolation safe and predictable, not the dynamic and unsafe practice associated with classic PHP templates.
§Isn’t writing UI code directly in Rust already safe?
Yes — writing plain Rust with egui is already memory-safe.
EFx does not add any “extra” safety here. Its purpose is different:
- Reduce boilerplate: instead of multiple nested closures you can express layouts in compact XML-like markup.
- Keep Rust guarantees: interpolations
{expr}are just Rust code, checked by the compiler. - Stay compatible: EFx expands into regular
ui.*calls, so you can freely mix EFx snippets with hand-writteneguicode.
In short: Rust already gives you memory safety. EFx gives you developer ergonomics on top of it, without sacrificing safety or control.
§Escaping curly braces
The text { and } can be obtained as {{ and }} respectively.
§Tag attributes (since 0.4)
They are written as in XML: name="value". At the moment, attributes are parsed and available in the AST,
but the renderer does not use them - the processing API will be added in future versions.
<Label color="green" size="lg">Hi</Label>§Compilation errors
- Unknown tag →
compile_error!. - Violation of tag restrictions (e.g. children of
<Separator/>) →compile_error!. - Invalid fragment in interpolation
{ … }→compile_error!with source fragment.
§Debugging
If you want to see what efx! generates, compile with RUSTFLAGS="--emit=mir,llvm-ir".