| Crates.io | gpui-markup |
| lib.rs | gpui-markup |
| version | 0.5.2 |
| created_at | 2026-01-09 23:26:16.609945+00 |
| updated_at | 2026-01-11 20:05:00.049567+00 |
| description | A declarative markup DSL for building GPUI applications |
| homepage | |
| repository | https://github.com/so1ve/gpui-markup |
| max_upload_size | |
| id | 2033141 |
| size | 242,512 |
A declarative markup DSL for building GPUI applications.
cargo add gpui-markup
use gpui::prelude::*;
use gpui_markup::ui;
fn my_view(cx: &mut ViewContext<Self>) -> impl IntoElement {
ui! {
div @[flex, flex_col, gap_2, p_4, bg: cx.theme().colors().background] {
div @[text_size: px(24.0), font_weight: FontWeight::BOLD] {
"Hello, GPUI!",
},
div @[text_color: cx.theme().colors().text_muted] {
"A declarative way to build UIs",
},
}
}
}
All elements require braces {}. Attributes go before braces with @[...]:
// Empty div
ui! { div {} }
// -> div()
// Div with attributes
ui! { div @[flex, flex_col] {} }
// -> div().flex().flex_col()
// Div with children
ui! { div { "content" } }
// -> gpui::ParentElement::child(div(), "content")
// Full form: attributes before braces, children inside
ui! { div @[flex] { "content" } }
// -> gpui::ParentElement::child(div().flex(), "content")
Attributes use @[...] before braces, comma-separated:
// Flag attributes (no value)
ui! { div @[flex, flex_col] {} }
// -> div().flex().flex_col()
// Key-value attributes
ui! { div @[w: px(200.0), h: px(100.0)] {} }
// -> div().w(px(200.0)).h(px(100.0))
// Multi-value attributes (use tuples)
ui! { div @[when: (condition, |d| d.bg(red()))] {} }
// -> div().when(condition, |d| d.bg(red()))
Children go inside {...}, comma-separated:
ui! {
div {
"First",
"Second",
div @[bold] { "Nested" },
}
}
// -> gpui::ParentElement::child(
// gpui::ParentElement::child(
// gpui::ParentElement::child(div(), "First"),
// "Second"
// ),
// gpui::ParentElement::child(div().bold(), "Nested")
// )
The deferred element wraps content for deferred rendering:
ui! {
deferred {
div { "Deferred content" },
}
}
// -> deferred(gpui::IntoElement::into_any_element(gpui::ParentElement::child(div(), "Deferred content")))
Use ..expr to spread an iterable as children:
let items: Vec<Div> = vec![div().child("A"), div().child("B")];
ui! {
div {
..items,
}
}
// -> gpui::ParentElement::children(div(), items)
// Can be mixed with regular children
ui! {
div {
"Header",
..items,
"Footer",
}
}
// -> gpui::ParentElement::child(
// gpui::ParentElement::children(
// gpui::ParentElement::child(div(), "Header"),
// items
// ),
// "Footer"
// )
Use .method(args) to insert method calls at any position:
ui! {
div {
"static child",
.when(condition, |d| d.child("dynamic")),
.flex().gap_2(),
.map::<Div, _>(|d| d),
}
}
Use standard Rust comments inside ui!:
ui! {
div {
// This is a comment
"Visible content",
/* Multi-line
comment */
}
}
// -> gpui::ParentElement::child(div(), "Visible content")
Components are any uppercase non-native element names. They automatically call ::new():
// Simple component
ui! { Header {} }
// -> Header::new()
// Component with attributes
ui! { Button @[style: Primary] {} }
// -> Button::new().style(Primary)
// Component with children
ui! {
Container {
"Content",
Footer {},
}
}
// -> gpui::ParentElement::child(gpui::ParentElement::child(Container::new(), "Content"), Footer::new())
Any expression can be used as an element at the top level (braces required):
// Custom constructor
ui! { Button::with_label("Click") {} }
// -> Button::with_label("Click")
// Expression with attributes
ui! { Button::with_label("Click") @[style: Primary] {} }
// -> Button::with_label("Click").style(Primary)
// Builder pattern expression
ui! {
div().flex() @[flex_col] {
"Content",
}
}
// -> gpui::ParentElement::child(div().flex().flex_col(), "Content")
// Parentheses for complex expressions (braces optional)
ui! { (a + b) }
// -> a + b
Why braces are required at top level?
The ui! macro builds a GPUI component tree. At the top level, {} declares "this is a UI element":
{} triggers the implicit ::new() call@[...] and children// Clear: defining a UI element, Header::new() is called
ui! { Header {} }
// As a child, context already indicates it's part of the tree
div {
Header::new(), // braces optional here, but no implicit ::new()
}
When using closure parameters (e.g., from .when(), .map()) in nested ui! macros, lowercase identifiers are treated as expression elements, not components:
// Closure parameter as element
ui! {
div {
.when(selected, |s| {
ui! {
s {} // `s` is treated as an expression element, not a component
}
})
}
}
// -> div().when(selected, |s| { s })
// With attributes and children
ui! {
div {
.when(condition, |styled| {
ui! {
styled @[flex, gap_2] {
"Content",
}
}
})
}
}
// -> div().when(condition, |styled| {
// gpui::ParentElement::child(styled.flex().gap_2(), "Content")
// })
How it works:
Header, Button) → Components, call ::new() implicitlydiv, svg, anchored) → Native GPUI elementss, element, styled) → Expression elements (variables, parameters)This allows seamless use of closure parameters from GPUI's builder methods like .when(), .map(), .hover(), etc.
ui! {
div @[flex, flex_col, gap_4] {
div @[flex, justify_between] {
Label {},
Button @[on_click: handle_click] {},
},
div @[flex: 1, overflow: hidden] {
ScrollView { content },
},
}
}
The ui! macro transforms the markup syntax into GPUI's builder pattern at compile time:
| Markup | Generated Code |
|---|---|
div {} |
div() |
div @[flex] {} |
div().flex() |
div @[w: x] {} |
div().w(x) |
div @[when: (a, b)] {} |
div().when(a, b) |
div { a, b } |
gpui::ParentElement::child(gpui::ParentElement::child(div(), a), b) |
div { ..items } |
gpui::ParentElement::children(div(), items) |
div { .a().b() } |
div().a().b() |
deferred { e } |
deferred(gpui::IntoElement::into_any_element(e)) |
Header {} |
Header::new() |
Header @[a] {} |
Header::new().a() |
expr {} |
expr |
expr @[a] {} |
expr.a() |
(expr) |
expr |