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>"#);

§<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

NameTypeDescription
frameboolfalseFrame::none(), otherwise Frame::default().
fillcolorBackground color.
stroke-widthf32Border width.
stroke-colorcolorBorder color.
padding / padding-left/right/top/bottomf32Inner margin.
margin / margin-left/right/top/bottomf32Outer margin.
idstringStable 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) 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>
"##);

§<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" — egui Id salt 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" — egui Id salt 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" — egui Id to 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());

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"/>"#);

§<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" — egui Id salt to persist the size across frames.

Behavior

  • resizable="true|false" — enable/disable user resizing (default: true in 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 predefined egui text 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

NameTypeDefaultDescription
textureexpr (egui::TextureId, &egui::TextureHandle, or egui::ImageSource)Source texture/handle. Mutually exclusive with src.
srcstring (URI/path)Image URI/path. Mutually exclusive with texture.
widthf32Target width. If both width and height are set, uses exact size.
heightf32Target height. If both width and height are set, uses exact size.
max-widthf32Max width (used if exact size isn’t specified).
max-heightf32Max height (used if exact size isn’t specified).
maintain-aspectboolfalseKeep original aspect ratio when fitting.
roundingu8Uniform corner radius.
tintcolorMultiplies image color (e.g. #FFFFFF80 for 50% fade).
bg-fillcolorBackground fill behind the image rect.
idstringStable id seed (id_source) for consistent layout/caching.
clickableboolfalseIf true, image responds to clicks (Sense::click).
tooltipstringHover text shown on the image.

Either texture or src must be provided (not both). <Image> does not accept children.

§Behavior & sizing rules

  • Exact size: if both width and height are set → the image is fit to that exact vec2(width, height).
  • Max size: otherwise, a max box is computed from max-width/max-height (falling back to width/height if only one side is provided).
  • Aspect: maintain-aspect="true" keeps the original ratio when fitting.
  • Interactivity: with clickable="true" the tag returns a normal Response you can query (.clicked(), etc.). Tooltips are applied via on_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

  • rounding is uniform; per-corner radii can be added later if needed.
  • id helps egui keep the same widget identity across frames when the source is otherwise dynamic.
  • On desktop, prefer texture with a previously allocated TextureId/TextureHandle for performance and control. On web, src can 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>

NameTypeDefaultDescription
activeexprrequiredString/&str expression with the id of the active tab ("home", "logs", …).
gapf32ui.spacing().item_spacing.xSpace between tab headers (px).

Attributes – <Tab>

NameTypeDefaultDescription
idstringUnique tab id. Used for matching and as default title.
titlestringidHeader text.
enabledbooltrueWhen false, the tab header is disabled and cannot be selected.

Behavior

  • Clicking a tab header updates active to that tab’s id. You can read active from 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>

NameTypeDefaultDescription
columnsintrequiredNumber of columns (must be ≥ 1).
stripedboolfalseAlternate row background.
spacing-xf328Horizontal spacing between columns (px).
spacing-yf324Vertical spacing between rows (px).
cell-paddingf320Padding inside each cell (px).
cell-alignenumleftHorizontal alignment: left, center, right.
idstringStable id for grid instance.

Rules

  • Only <Tr> children are allowed inside <Table>.
  • <Tr> may only contain <Td> elements.
  • colspan / rowspan are 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::Grid to 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 on egui_extras::TableBuilder behind 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 for initial / exact)
  • resizable="true|false" (defaults to table’s default-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>

NameTypeDefaultDescription
idstringStable id (push_id) wrapping the whole table.
stripedboolfalseAlternate row background.
resizableboolfalseEnables column resizing globally (can be overridden per column).
default-resizableboolDefault resizable for each <Column> if not set.
header-heightf3222Header row height (px).
row-heightf3222Default body row height (px).
cell-paddingf320Inner padding per cell, in px (applied on both sides).
cell-alignenumleftHorizontal 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 / rowspan are 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>

NameTypeDefaultDescription
columnsintrequiredNumber of columns (≥ 1).
stripedboolfalseAlternate row background.
spacing-xf328Horizontal spacing between columns (px).
spacing-yf324Vertical spacing between rows (px).
cell-paddingf320Inner padding in each cell (px, applied on both sides).
cell-alignenumleftHorizontal alignment in cells: left, center, right.
idstringStable 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 when columns is reached.
  • Returns () (container).

§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.
efx_ctx