Crate efx

Crate efx 

Source
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 pin eframe = "0.32" for egui 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 a ComboBox 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());

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 type String, 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 with multiline="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) or none (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 inside Label/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 no eval, 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-written egui 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.