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-sandbox
Make sure
eframe/egui
versions 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 str
and switch them via aComboBox
to 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
#RRGGBB
or#RRGGBBAA
(short#RGB/#RGBA
is 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>"#);
§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"/>"#);
§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>
"##);
§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>
"#);
§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-writtenegui
code.
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"
.
Macros§
- efx
- Functional procedural macro
efx!
- parses compact XML-like markup and executes it against the passed UI context.