
Gpui
- 138 installs
- 12.4k repo stars
- Updated August 4, 2026
- longbridge/gpui-component
Wire keyboard shortcuts and declarative actions in a GPUI (Rust) desktop UI without ad-hoc event handling.
About
The gpui skill packages procedural knowledge for GPUI’s actions and keybindings system—how solo and indie builders declare keyboard-driven UI behavior in Rust desktop apps. It walks through defining action types, binding keys at app init, attaching listeners on rendered elements, and scoping shortcuts with key contexts so the same key means different things in different panes. For builders shipping native tooling, editors, or internal utilities on GPUI (longbridge/gpui-component), this reduces trial-and-error around the `actions!` macro, `KeyBinding::new`, and the listener pattern compared to reading scattered framework docs. Use when you are implementing or refactoring keyboard UX in a GPUI view, not when choosing a UI stack or designing visual layout. It is reference-grade implementation guidance aligned with declarative GPUI patterns rather than a full application scaffold.
- Defines actions via `actions!` macro or `#[derive(Action)]` and registers bindings with `cx.bind_keys()`
- Handles input with `.on_action(cx.listener(...))` on elements scoped by `key_context()`
- Documents key string formats (e.g. `cmd-s`, `up`) and context-aware binding with named contexts like `Editor`
- Quick-start flow: init bindings in `App`, implement handlers on the view, call `cx.notify()` after state changes
Gpui by the numbers
- 138 all-time installs (skills.sh)
- +24 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #60 of 121 Rust skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/longbridge/gpui-component --skill gpuiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 138 |
|---|---|
| repo stars | ★ 12.4k |
| Last updated | August 4, 2026 |
| Repository | longbridge/gpui-component ↗ |
What it does
Wire keyboard shortcuts and declarative actions in a GPUI (Rust) desktop UI without ad-hoc event handling.
Files
Navigation
Load the relevant reference file based on the task:
| Topic | File | When to load |
|---|---|---|
| Actions & keybindings | action.md | actions!, bind_keys, on_action, key_context |
| Async & background tasks | async.md | cx.spawn, background_spawn, Task, async I/O |
| Context management | context.md | App, Window, Context<T>, AsyncApp |
| Custom elements (low-level) | element.md | Element trait, request_layout, prepaint, paint |
| Entity state | entity.md | Entity<T>, WeakEntity, state management |
| Events & subscriptions | event.md | cx.emit, cx.subscribe, cx.observe |
| Focus & keyboard nav | focus-handle.md | FocusHandle, track_focus, Tab navigation |
| Global state | global.md | Global trait, cx.set_global, app-wide config |
| Layout & styling | layout-style.md | div(), h_flex(), v_flex(), flexbox, overflow, positioning |
| ElementId | element-id.md | ElementId, .id(), uniqueness rules, stateful elements |
| Testing | test.md | #[gpui::test], TestAppContext, VisualTestContext |
Extended References
For deep-dive topics, additional reference files are available:
Element trait:
- element-api.md — complete API, hitbox system, event handling
- element-patterns.md — text, interactive, container, composite patterns
- element-examples.md — full examples: text, interactive, complex elements
- element-best-practices.md — performance, state, common pitfalls
- element-advanced.md — masonry/circular layouts, async updates, virtual lists
Entity management:
- entity-api.md — complete Entity API, methods, lifecycle
- entity-patterns.md — model-view, cross-entity communication, observer
- entity-best-practices.md — memory, performance, lifecycle
- entity-advanced.md — collections, registry, debounce, state machines
Testing:
- test-examples.md — testing examples and patterns
- test-reference.md — complete testing API reference
Actions & Keybindings
Contents: Overview · Quick Start · Key Formats · Action Naming · Context-Aware Bindings · Best Practices
Overview
Actions provide declarative keyboard-driven UI interactions in GPUI.
Key Concepts:
- Define actions with
actions!macro or#[derive(Action)] - Bind keys with
cx.bind_keys() - Handle with
.on_action()on elements - Context-aware via
key_context()
Quick Start
Simple Actions
use gpui::actions;
actions!(editor, [MoveUp, MoveDown, Save, Quit]);
const CONTEXT: &str = "Editor";
pub fn init(cx: &mut App) {
cx.bind_keys([
KeyBinding::new("up", MoveUp, Some(CONTEXT)),
KeyBinding::new("down", MoveDown, Some(CONTEXT)),
KeyBinding::new("cmd-s", Save, Some(CONTEXT)),
KeyBinding::new("cmd-q", Quit, Some(CONTEXT)),
]);
}
impl Render for Editor {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.key_context(CONTEXT)
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.on_action(cx.listener(Self::save))
}
}
impl Editor {
fn move_up(&mut self, _: &MoveUp, cx: &mut Context<Self>) {
// Handle move up
cx.notify();
}
fn move_down(&mut self, _: &MoveDown, cx: &mut Context<Self>) {
cx.notify();
}
fn save(&mut self, _: &Save, cx: &mut Context<Self>) {
// Save logic
cx.notify();
}
}Actions with Parameters
#[derive(Clone, PartialEq, Action, Deserialize)]
#[action(namespace = editor)]
pub struct InsertText {
pub text: String,
}
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
#[action(namespace = editor, no_json)]
pub struct Digit(pub u8);
cx.bind_keys([
KeyBinding::new("0", Digit(0), Some(CONTEXT)),
KeyBinding::new("1", Digit(1), Some(CONTEXT)),
// ...
]);
impl Editor {
fn on_digit(&mut self, action: &Digit, cx: &mut Context<Self>) {
self.insert_digit(action.0, cx);
}
}Key Formats
// Modifiers
"cmd-s" // Command (macOS) / Ctrl (Windows/Linux)
"ctrl-c" // Control
"alt-f" // Alt
"shift-tab" // Shift
"cmd-ctrl-f" // Multiple modifiers
// Keys
"a-z", "0-9" // Letters and numbers
"f1-f12" // Function keys
"up", "down", "left", "right"
"enter", "escape", "space", "tab"
"backspace", "delete"
"-", "=", "[", "]", etc. // Special charactersAction Naming
Prefer verb-noun pattern:
actions!([
OpenFile, // ✅ Good
CloseWindow, // ✅ Good
ToggleSidebar, // ✅ Good
Save, // ✅ Good (common exception)
]);Context-Aware Bindings
const EDITOR_CONTEXT: &str = "Editor";
const MODAL_CONTEXT: &str = "Modal";
// Same key, different contexts
cx.bind_keys([
KeyBinding::new("escape", CloseModal, Some(MODAL_CONTEXT)),
KeyBinding::new("escape", ClearSelection, Some(EDITOR_CONTEXT)),
]);
// Set context on element
div()
.key_context(EDITOR_CONTEXT)
.child(editor_content)Best Practices
✅ Use Contexts
// ✅ Good: Context-aware
div()
.key_context("MyComponent")
.on_action(cx.listener(Self::handle))✅ Name Actions Clearly
// ✅ Good: Clear intent
actions!([
SaveDocument,
CloseTab,
TogglePreview,
]);✅ Handle with Listeners
// ✅ Good: Proper handler naming
impl MyComponent {
fn on_action_save(&mut self, _: &Save, cx: &mut Context<Self>) {
// Handle save
cx.notify();
}
}
div().on_action(cx.listener(Self::on_action_save))Async & Background Tasks
Contents: Overview · Quick Start · Core Patterns · Common Pitfalls
Overview
GPUI provides integrated async runtime for foreground UI updates and background computation.
Key Concepts:
- Foreground tasks: UI thread, can update entities (
cx.spawn) - Background tasks: Worker threads, CPU-intensive work (
cx.background_spawn) - All entity updates happen on foreground thread
Quick Start
Foreground Tasks (UI Updates)
When spawned from Context<Self>, the closure receives (WeakEntity<Self>, &mut AsyncApp):
impl MyComponent {
fn fetch_data(&mut self, cx: &mut Context<Self>) {
cx.spawn(async move |this, cx: &mut AsyncApp| {
// Runs on UI thread, can await and update entities
let data = fetch_from_api().await;
this.update(cx, |state, cx| {
state.data = Some(data);
cx.notify();
}).ok();
}).detach();
}
}When spawned from &mut App (not inside an entity), the closure receives only (cx: &mut AsyncApp):
cx.spawn(async move |cx: &mut AsyncApp| {
// No entity reference
}).detach();Spawn with Window Context (spawn_in)
Use spawn_in when the task also needs window access (update_in):
impl MyComponent {
fn animate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
cx.spawn_in(window, async move |this, cx| {
// cx here is AsyncWindowContext
this.update_in(cx, |state, window, cx| {
// Can access window here
state.frame += 1;
cx.notify();
}).ok();
}).detach();
}
}Background Tasks (Heavy Work)
impl MyComponent {
fn process_file(&mut self, cx: &mut Context<Self>) {
let entity = cx.entity().downgrade();
cx.background_spawn(async move {
// Runs on background thread, CPU-intensive
let result = heavy_computation().await;
result
})
.then(cx.spawn(move |result, cx| {
// Back to foreground to update UI
entity.update(cx, |state, cx| {
state.result = result;
cx.notify();
}).ok();
}))
.detach();
}
}Task Management
struct MyView {
_task: Task<()>, // Prefix with _ if stored but not accessed
}
impl MyView {
fn new(cx: &mut Context<Self>) -> Self {
let _task = cx.spawn(async move |this, cx: &mut AsyncApp| {
// Task automatically cancelled when dropped
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
this.update(cx, |state, cx| {
state.tick();
cx.notify();
}).ok();
}
});
Self { _task }
}
}Core Patterns
1. Async Data Fetching (from Context<Self>)
cx.spawn(async move |this, cx: &mut AsyncApp| {
let data = fetch_data().await?;
this.update(cx, |state, cx| {
state.data = Some(data);
cx.notify();
})?;
Ok::<_, anyhow::Error>(())
}).detach();2. Background Computation + UI Update
cx.background_spawn(async move {
heavy_work()
})
.then(cx.spawn(move |this, cx: &mut AsyncApp| {
this.update(cx, |state, cx| {
state.result = result;
cx.notify();
}).ok();
}))
.detach();3. Periodic Tasks
cx.spawn(async move |this, cx: &mut AsyncApp| {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
this.update(cx, |state, cx| {
state.tick();
cx.notify();
}).ok();
}
}).detach();4. Task Cancellation
Tasks are automatically cancelled when dropped. Store in struct to keep alive.
Common Pitfalls
❌ Don't: Use defer_in and then update the same entity through its handle
cx.defer_in(window, callback) schedules callback to run on the current entity — GPUI re-acquires that entity's lock to execute it. Calling entity.update(cx, …) on the same entity from within the deferred callback re-enters the lock and panics:
cannot update … while it is already being updated// ❌ Panic: list entity is locked for the defer_in; calling list.update re-enters
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
cx.defer_in(window, |list_state, window, cx| {
parent.update(cx, |this, cx| {
this.inner_list.update(cx, |_, _| {}); // PANIC if inner_list == the deferred entity
});
});
}// ✅ Correct: use the direct &mut reference — no lock needed
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
cx.defer_in(window, |list_state, window, cx| {
// Access list data directly through the &mut reference
list_state.delegate_mut().some_method();
// Update a *different* entity — fine, different lock
parent.update(cx, |this, cx| { /* … */ });
// Sync list state directly after parent update — no lock needed
list_state.delegate_mut().update_snapshot(new_val);
});
}The rule: inside a defer_in callback, never call `entity.update(cx, …)` or `entity.read(cx)` on the entity the `defer_in` was scheduled on. Use the &mut Entity direct reference the callback provides instead.
❌ Don't: Update entities from background tasks
// ❌ Wrong: Can't update entities from background thread
cx.background_spawn(async move {
entity.update(cx, |state, cx| { // Compile error!
state.data = data;
});
});✅ Do: Use foreground task or chain
// ✅ Correct: Chain with foreground task
cx.background_spawn(async move { data })
.then(cx.spawn(move |data, cx| {
entity.update(cx, |state, cx| {
state.data = data;
cx.notify();
}).ok();
}))
.detach();Context Management
Contents: Overview · Quick Start · Common Operations · Context Hierarchy · cx.listener · subscribe_in · observe_window_activation · observe_global · defer / defer_in · Naming Convention
Overview
GPUI uses different context types for different scenarios:
Context Types:
- `App`: Global app state, entity creation
- `Window`: Window-specific operations, painting, layout
- `Context<T>`: Entity-specific context for component
T - `AsyncApp`: Async context for foreground tasks
- `AsyncWindowContext`: Async context with window access
Quick Start
Context<T> - Component Context
impl MyComponent {
fn update_state(&mut self, cx: &mut Context<Self>) {
self.value = 42;
cx.notify(); // Trigger re-render
// Spawn async task
cx.spawn(async move |cx| {
// Async work
}).detach();
// Get current entity
let entity = cx.entity();
}
}App - Global Context
fn main() {
let app = Application::new();
app.run(|cx: &mut App| {
// Create entities
let entity = cx.new(|cx| MyState::default());
// Open windows
cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|cx| Root::new(view, window, cx))
});
});
}Window - Window Context
impl Render for MyView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
// Window operations
let is_focused = window.is_window_focused();
let bounds = window.bounds();
div().child("Content")
}
}AsyncApp - Async Context
cx.spawn(async move |cx: &mut AsyncApp| {
let data = fetch_data().await;
entity.update(cx, |state, inner_cx| {
state.data = data;
inner_cx.notify();
}).ok();
}).detach();Common Operations
Entity Operations
// Create entity
let entity = cx.new(|cx| MyState::default());
// Update entity
entity.update(cx, |state, cx| {
state.value = 42;
cx.notify();
});
// Read entity
let value = entity.read(cx).value;Notifications and Events
// Trigger re-render
cx.notify();
// Emit event
cx.emit(MyEvent::Updated);
// Observe entity
cx.observe(&entity, |this, observed, cx| {
// React to changes
}).detach();
// Subscribe to events
cx.subscribe(&entity, |this, source, event, cx| {
// Handle event
}).detach();Window Operations
// Window state
let focused = window.is_window_focused();
let bounds = window.bounds();
let scale = window.scale_factor();
// Close window
window.remove_window();Async Operations
// Spawn foreground task
cx.spawn(async move |cx| {
// Async work with entity access
}).detach();
// Spawn background task
cx.background_spawn(async move {
// Heavy computation
}).detach();Context Hierarchy
App (Global)
└─ Window (Per-window)
└─ Context<T> (Per-component)
└─ AsyncApp (In async tasks)
└─ AsyncWindowContext (Async + Window)cx.listener — Binding Callbacks to Self
cx.listener creates a callback that borrows &mut self (the current entity). Use it for on_click, on_action, and other element event handlers:
impl Render for MyView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.on_action(cx.listener(Self::on_save))
.child(
Button::new("btn")
.on_click(cx.listener(|this, _event, _window, cx| {
this.count += 1;
cx.notify();
}))
)
}
}
impl MyView {
fn on_save(&mut self, _: &Save, _window: &mut Window, cx: &mut Context<Self>) {
cx.notify();
}
}cx.listener(Self::method) is equivalent to creating a closure that calls self.method(...).
subscribe_in — Subscribe with Window Access
Use subscribe_in (instead of subscribe) when the callback needs &mut Window:
let _subscription = cx.subscribe_in(&input, window, |this, state, event, window, cx| {
match event {
InputEvent::Change => {
let val = state.read(cx).value();
this.on_input_change(val, window, cx);
}
_ => {}
}
});
// Store _subscription in struct to keep it alivesubscribe vs subscribe_in:
subscribe(&entity, |this, source, event, cx|)— no window accesssubscribe_in(&entity, window, |this, source, event, window, cx|)— has window access
observe_window_activation
React when the window gains or loses focus:
let _sub = cx.observe_window_activation(window, |this, window, cx| {
if window.is_window_active() {
this.resume(cx);
} else {
this.pause(cx);
}
});observe_global
React when a global value changes:
cx.observe_global::<Theme>(|cx| {
// Theme changed — react
cx.notify();
});defer and defer_in
Schedule work after the current update completes:
// defer: runs after current App update, no window access
cx.defer(|cx| {
// Runs after current entity update is done
});
// defer_in: runs after update, with window access
cx.defer_in(window, |this, window, cx| {
// Can access window here
// CAUTION: never call entity.update(cx) on *this same entity* inside defer_in
// — it re-enters the lock and panics. Use the &mut self reference directly.
this.some_method(window, cx);
});Context Naming Convention
Always name contexts cx regardless of type:
fn new(window: &mut Window, cx: &mut App) {} // cx = App
impl Render for View {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) {} // cx = Context<Self>
}
cx.spawn(async move |this, cx: &mut AsyncApp| {}) // cx = AsyncAppAdvanced Element Patterns
Contents: Custom Layout Algorithms · Element Composition with Traits · Async Element Updates · Element Memoization · Virtual List Pattern
Custom Layout Algorithms
Implementing custom layout algorithms not supported by GPUI's built-in layouts.
Masonry Layout (Pinterest-Style)
pub struct MasonryLayout {
id: ElementId,
columns: usize,
gap: Pixels,
children: Vec<AnyElement>,
}
struct MasonryLayoutState {
column_layouts: Vec<Vec<LayoutId>>,
column_heights: Vec<Pixels>,
}
struct MasonryPaintState {
child_bounds: Vec<Bounds<Pixels>>,
}
impl Element for MasonryLayout {
type RequestLayoutState = MasonryLayoutState;
type PrepaintState = MasonryPaintState;
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, MasonryLayoutState) {
// Initialize columns
let mut columns: Vec<Vec<LayoutId>> = vec![Vec::new(); self.columns];
let mut column_heights = vec![px(0.); self.columns];
// Distribute children across columns
for child in &mut self.children {
let (child_layout_id, _) = child.request_layout(
global_id,
inspector_id,
window,
cx
);
let child_size = window.layout_bounds(child_layout_id).size;
// Find shortest column
let min_column_idx = column_heights
.iter()
.enumerate()
.min_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.unwrap()
.0;
// Add child to shortest column
columns[min_column_idx].push(child_layout_id);
column_heights[min_column_idx] += child_size.height + self.gap;
}
// Calculate total layout size
let column_width = px(200.); // Fixed column width
let total_width = column_width * self.columns as f32
+ self.gap * (self.columns - 1) as f32;
let total_height = column_heights.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap())
.copied()
.unwrap_or(px(0.));
let layout_id = window.request_layout(
Style {
size: size(total_width, total_height),
..default()
},
columns.iter().flatten().copied().collect(),
cx
);
(layout_id, MasonryLayoutState {
column_layouts: columns,
column_heights,
})
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
layout_state: &mut MasonryLayoutState,
window: &mut Window,
cx: &mut App
) -> MasonryPaintState {
let column_width = px(200.);
let mut child_bounds = Vec::new();
// Position children in columns
for (col_idx, column) in layout_state.column_layouts.iter().enumerate() {
let x_offset = bounds.left()
+ (column_width + self.gap) * col_idx as f32;
let mut y_offset = bounds.top();
for (child_idx, layout_id) in column.iter().enumerate() {
let child_size = window.layout_bounds(*layout_id).size;
let child_bound = Bounds::new(
point(x_offset, y_offset),
size(column_width, child_size.height)
);
self.children[child_idx].prepaint(
global_id,
inspector_id,
child_bound,
window,
cx
);
child_bounds.push(child_bound);
y_offset += child_size.height + self.gap;
}
}
MasonryPaintState { child_bounds }
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_layout_state: &mut MasonryLayoutState,
paint_state: &mut MasonryPaintState,
window: &mut Window,
cx: &mut App
) {
for (child, bounds) in self.children.iter_mut().zip(&paint_state.child_bounds) {
child.paint(global_id, inspector_id, *bounds, window, cx);
}
}
}Circular Layout
pub struct CircularLayout {
id: ElementId,
radius: Pixels,
children: Vec<AnyElement>,
}
impl Element for CircularLayout {
type RequestLayoutState = Vec<LayoutId>;
type PrepaintState = Vec<Bounds<Pixels>>;
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, Vec<LayoutId>) {
let child_layouts: Vec<_> = self.children
.iter_mut()
.map(|child| child.request_layout(global_id, inspector_id, window, cx).0)
.collect();
let diameter = self.radius * 2.;
let layout_id = window.request_layout(
Style {
size: size(diameter, diameter),
..default()
},
child_layouts.clone(),
cx
);
(layout_id, child_layouts)
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
layout_ids: &mut Vec<LayoutId>,
window: &mut Window,
cx: &mut App
) -> Vec<Bounds<Pixels>> {
let center = bounds.center();
let angle_step = 2.0 * std::f32::consts::PI / self.children.len() as f32;
let mut child_bounds = Vec::new();
for (i, (child, layout_id)) in self.children.iter_mut()
.zip(layout_ids.iter())
.enumerate()
{
let angle = angle_step * i as f32;
let child_size = window.layout_bounds(*layout_id).size;
// Position child on circle
let x = center.x + self.radius * angle.cos() - child_size.width / 2.;
let y = center.y + self.radius * angle.sin() - child_size.height / 2.;
let child_bound = Bounds::new(point(x, y), child_size);
child.prepaint(global_id, inspector_id, child_bound, window, cx);
child_bounds.push(child_bound);
}
child_bounds
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_layout_ids: &mut Vec<LayoutId>,
child_bounds: &mut Vec<Bounds<Pixels>>,
window: &mut Window,
cx: &mut App
) {
for (child, bounds) in self.children.iter_mut().zip(child_bounds) {
child.paint(global_id, inspector_id, *bounds, window, cx);
}
}
}Element Composition with Traits
Create reusable behaviors via traits for element composition.
Hoverable Trait
pub trait Hoverable: Element {
fn on_hover<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&mut Window, &mut App) + 'static;
fn on_hover_end<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&mut Window, &mut App) + 'static;
}
// Implementation for custom element
pub struct HoverableElement {
id: ElementId,
content: AnyElement,
hover_handlers: Vec<Box<dyn Fn(&mut Window, &mut App)>>,
hover_end_handlers: Vec<Box<dyn Fn(&mut Window, &mut App)>>,
was_hovered: bool,
}
impl Hoverable for HoverableElement {
fn on_hover<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&mut Window, &mut App) + 'static
{
self.hover_handlers.push(Box::new(f));
self
}
fn on_hover_end<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&mut Window, &mut App) + 'static
{
self.hover_end_handlers.push(Box::new(f));
self
}
}
impl Element for HoverableElement {
type RequestLayoutState = LayoutId;
type PrepaintState = Hitbox;
fn paint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_layout: &mut LayoutId,
hitbox: &mut Hitbox,
window: &mut Window,
cx: &mut App
) {
let is_hovered = hitbox.is_hovered(window);
// Trigger hover events
if is_hovered && !self.was_hovered {
for handler in &self.hover_handlers {
handler(window, cx);
}
} else if !is_hovered && self.was_hovered {
for handler in &self.hover_end_handlers {
handler(window, cx);
}
}
self.was_hovered = is_hovered;
// Paint content
self.content.paint(bounds, window, cx);
}
// ... other methods
}Clickable Trait
pub trait Clickable: Element {
fn on_click<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static;
fn on_double_click<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static;
}
pub struct ClickableElement {
id: ElementId,
content: AnyElement,
click_handlers: Vec<Box<dyn Fn(&MouseUpEvent, &mut Window, &mut App)>>,
double_click_handlers: Vec<Box<dyn Fn(&MouseUpEvent, &mut Window, &mut App)>>,
last_click_time: Option<Instant>,
}
impl Clickable for ClickableElement {
fn on_click<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static
{
self.click_handlers.push(Box::new(f));
self
}
fn on_double_click<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static
{
self.double_click_handlers.push(Box::new(f));
self
}
}Async Element Updates
Elements that update based on async operations.
pub struct AsyncElement {
id: ElementId,
state: Entity<AsyncState>,
loading: bool,
data: Option<String>,
}
pub struct AsyncState {
loading: bool,
data: Option<String>,
}
impl Element for AsyncElement {
type RequestLayoutState = ();
type PrepaintState = Hitbox;
fn paint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_layout: &mut (),
hitbox: &mut Hitbox,
window: &mut Window,
cx: &mut App
) {
// Display loading or data
if self.loading {
// Paint loading indicator
self.paint_loading(bounds, window, cx);
} else if let Some(data) = &self.data {
// Paint data
self.paint_data(data, bounds, window, cx);
}
// Trigger async update on click
window.on_mouse_event({
let state = self.state.clone();
let hitbox = hitbox.clone();
move |event: &MouseUpEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Spawn async task
cx.spawn({
let state = state.clone();
async move {
// Perform async operation
let result = fetch_data_async().await;
// Update state on completion
state.update(cx, |state, cx| {
state.loading = false;
state.data = Some(result);
cx.notify();
});
}
}).detach();
// Set loading state immediately
state.update(cx, |state, cx| {
state.loading = true;
cx.notify();
});
cx.stop_propagation();
}
}
});
}
// ... other methods
}
async fn fetch_data_async() -> String {
// Simulate async operation
tokio::time::sleep(Duration::from_secs(1)).await;
"Data loaded!".to_string()
}Element Memoization
Optimize performance by memoizing expensive element computations.
pub struct MemoizedElement<T: PartialEq + Clone + 'static> {
id: ElementId,
value: T,
render_fn: Box<dyn Fn(&T) -> AnyElement>,
cached_element: Option<AnyElement>,
last_value: Option<T>,
}
impl<T: PartialEq + Clone + 'static> MemoizedElement<T> {
pub fn new<F>(id: ElementId, value: T, render_fn: F) -> Self
where
F: Fn(&T) -> AnyElement + 'static,
{
Self {
id,
value,
render_fn: Box::new(render_fn),
cached_element: None,
last_value: None,
}
}
}
impl<T: PartialEq + Clone + 'static> Element for MemoizedElement<T> {
type RequestLayoutState = LayoutId;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, LayoutId) {
// Check if value changed
if self.last_value.as_ref() != Some(&self.value) || self.cached_element.is_none() {
// Recompute element
self.cached_element = Some((self.render_fn)(&self.value));
self.last_value = Some(self.value.clone());
}
// Request layout for cached element
let (layout_id, _) = self.cached_element
.as_mut()
.unwrap()
.request_layout(global_id, inspector_id, window, cx);
(layout_id, layout_id)
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_layout_id: &mut LayoutId,
window: &mut Window,
cx: &mut App
) -> () {
self.cached_element
.as_mut()
.unwrap()
.prepaint(global_id, inspector_id, bounds, window, cx);
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_layout_id: &mut LayoutId,
_: &mut (),
window: &mut Window,
cx: &mut App
) {
self.cached_element
.as_mut()
.unwrap()
.paint(global_id, inspector_id, bounds, window, cx);
}
}
// Usage
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
MemoizedElement::new(
ElementId::Name("memoized".into()),
self.expensive_value.clone(),
|value| {
// Expensive rendering function only called when value changes
div().child(format!("Computed: {}", value))
}
)
}Virtual List Pattern
Efficiently render large lists by only rendering visible items.
pub struct VirtualList {
id: ElementId,
item_count: usize,
item_height: Pixels,
viewport_height: Pixels,
scroll_offset: Pixels,
render_item: Box<dyn Fn(usize) -> AnyElement>,
}
struct VirtualListState {
visible_range: Range<usize>,
visible_item_layouts: Vec<LayoutId>,
}
impl Element for VirtualList {
type RequestLayoutState = VirtualListState;
type PrepaintState = Hitbox;
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, VirtualListState) {
// Calculate visible range
let start_idx = (self.scroll_offset / self.item_height).floor() as usize;
let end_idx = ((self.scroll_offset + self.viewport_height) / self.item_height)
.ceil() as usize;
let visible_range = start_idx..end_idx.min(self.item_count);
// Request layout only for visible items
let visible_item_layouts: Vec<_> = visible_range.clone()
.map(|i| {
let mut item = (self.render_item)(i);
item.request_layout(global_id, inspector_id, window, cx).0
})
.collect();
let total_height = self.item_height * self.item_count as f32;
let layout_id = window.request_layout(
Style {
size: size(relative(1.0), self.viewport_height),
overflow: Overflow::Hidden,
..default()
},
visible_item_layouts.clone(),
cx
);
(layout_id, VirtualListState {
visible_range,
visible_item_layouts,
})
}
fn prepaint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
state: &mut VirtualListState,
window: &mut Window,
_cx: &mut App
) -> Hitbox {
// Prepaint visible items at correct positions
for (i, layout_id) in state.visible_item_layouts.iter().enumerate() {
let item_idx = state.visible_range.start + i;
let y = item_idx as f32 * self.item_height - self.scroll_offset;
let item_bounds = Bounds::new(
point(bounds.left(), bounds.top() + y),
size(bounds.width(), self.item_height)
);
// Prepaint if visible
if item_bounds.intersects(&bounds) {
// Prepaint item...
}
}
window.insert_hitbox(bounds, HitboxBehavior::Normal)
}
fn paint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
state: &mut VirtualListState,
hitbox: &mut Hitbox,
window: &mut Window,
cx: &mut App
) {
// Paint visible items
for (i, _layout_id) in state.visible_item_layouts.iter().enumerate() {
let item_idx = state.visible_range.start + i;
let y = item_idx as f32 * self.item_height - self.scroll_offset;
let item_bounds = Bounds::new(
point(bounds.left(), bounds.top() + y),
size(bounds.width(), self.item_height)
);
if item_bounds.intersects(&bounds) {
let mut item = (self.render_item)(item_idx);
item.paint(item_bounds, window, cx);
}
}
// Handle scroll
window.on_mouse_event({
let hitbox = hitbox.clone();
let total_height = self.item_height * self.item_count as f32;
move |event: &ScrollWheelEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
self.scroll_offset -= event.delta.y;
self.scroll_offset = self.scroll_offset
.max(px(0.))
.min(total_height - self.viewport_height);
cx.notify();
cx.stop_propagation();
}
}
});
}
}
// Usage: Efficiently render 10,000 items
let virtual_list = VirtualList {
id: ElementId::Name("large-list".into()),
item_count: 10_000,
item_height: px(40.),
viewport_height: px(400.),
scroll_offset: px(0.),
render_item: Box::new(|index| {
div().child(format!("Item {}", index))
}),
};These advanced patterns enable sophisticated element implementations while maintaining performance and code quality.
Element API Reference
Contents: Element Trait Structure · Associated Types · Methods · IntoElement Integration · Layout System Integration · Hitbox System · Event Handling · Cursor Styles
Element Trait Structure
The Element trait requires implementing three associated types and five methods:
pub trait Element: 'static + IntoElement {
type RequestLayoutState: 'static;
type PrepaintState: 'static;
fn id(&self) -> Option<ElementId>;
fn source_location(&self) -> Option<&'static std::panic::Location<'static>>;
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState);
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState;
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
);
}Associated Types
RequestLayoutState
Data passed from request_layout to prepaint and paint phases.
Usage:
- Store layout calculations (styled text, child layout IDs)
- Cache expensive computations
- Pass child state between phases
Examples:
// Simple: no state needed
type RequestLayoutState = ();
// Single value
type RequestLayoutState = StyledText;
// Multiple values
type RequestLayoutState = (StyledText, Vec<ChildLayout>);
// Complex struct
pub struct MyLayoutState {
pub styled_text: StyledText,
pub child_layouts: Vec<(LayoutId, ChildState)>,
pub computed_bounds: Bounds<Pixels>,
}
type RequestLayoutState = MyLayoutState;PrepaintState
Data passed from prepaint to paint phase.
Usage:
- Store hitboxes for interaction
- Cache visual bounds
- Store prepaint results
Examples:
// Simple: just a hitbox
type PrepaintState = Hitbox;
// Optional hitbox
type PrepaintState = Option<Hitbox>;
// Multiple values
type PrepaintState = (Hitbox, Vec<Bounds<Pixels>>);
// Complex struct
pub struct MyPaintState {
pub hitbox: Hitbox,
pub child_bounds: Vec<Bounds<Pixels>>,
pub visible_range: Range<usize>,
}
type PrepaintState = MyPaintState;Methods
id()
Returns optional unique identifier for debugging and inspection.
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
// Or if no ID needed
fn id(&self) -> Option<ElementId> {
None
}source_location()
Returns source location for debugging. Usually returns None unless debugging is needed.
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}request_layout()
Calculates sizes and positions for the element tree.
Parameters:
global_id: Global element identifier (optional)inspector_id: Inspector element identifier (optional)window: Mutable window referencecx: Mutable app context
Returns:
(LayoutId, Self::RequestLayoutState): Layout ID and state for next phases
Responsibilities: 1. Calculate child layouts by calling child.request_layout() 2. Create own layout using window.request_layout() 3. Return layout ID and state to pass to next phases
Example:
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
// 1. Calculate child layouts
let child_layout_id = self.child.request_layout(
global_id,
inspector_id,
window,
cx
).0;
// 2. Create own layout
let layout_id = window.request_layout(
Style {
size: size(px(200.), px(100.)),
..default()
},
vec![child_layout_id],
cx
);
// 3. Return layout ID and state
(layout_id, MyLayoutState { child_layout_id })
}prepaint()
Prepares for painting by creating hitboxes and computing final bounds.
Parameters:
global_id: Global element identifier (optional)inspector_id: Inspector element identifier (optional)bounds: Final bounds calculated by layout enginerequest_layout: Mutable reference to layout statewindow: Mutable window referencecx: Mutable app context
Returns:
Self::PrepaintState: State for paint phase
Responsibilities: 1. Compute final child bounds based on layout bounds 2. Call child.prepaint() for all children 3. Create hitboxes using window.insert_hitbox() 4. Return state for paint phase
Example:
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
// 1. Compute child bounds
let child_bounds = bounds; // or calculated subset
// 2. Prepaint children
self.child.prepaint(
global_id,
inspector_id,
child_bounds,
&mut request_layout.child_state,
window,
cx
);
// 3. Create hitboxes
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
// 4. Return paint state
MyPaintState { hitbox }
}paint()
Renders the element and handles interactions.
Parameters:
global_id: Global element identifier (optional)inspector_id: Inspector element identifier (optional)bounds: Final bounds for renderingrequest_layout: Mutable reference to layout stateprepaint: Mutable reference to prepaint statewindow: Mutable window referencecx: Mutable app context
Responsibilities: 1. Paint children first (bottom to top) 2. Paint own content (backgrounds, borders, etc.) 3. Set up interactions (mouse events, cursor styles)
Example:
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
// 1. Paint children first
self.child.paint(
global_id,
inspector_id,
child_bounds,
&mut request_layout.child_state,
&mut prepaint.child_paint_state,
window,
cx
);
// 2. Paint own content
window.paint_quad(paint_quad(
bounds,
Anchor::all(px(4.)),
cx.theme().background,
));
// 3. Set up interactions
window.on_mouse_event({
let hitbox = prepaint.hitbox.clone();
move |event: &MouseDownEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Handle click
cx.stop_propagation();
}
}
});
window.set_cursor_style(CursorStyle::PointingHand, &prepaint.hitbox);
}IntoElement Integration
Elements must also implement IntoElement to be used as children:
impl IntoElement for MyElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}This allows your custom element to be used directly in the element tree:
div()
.child(MyElement::new()) // Works because of IntoElementCommon Parameters
Global and Inspector IDs
Both are optional identifiers used for debugging and inspection:
global_id: Unique identifier across entire appinspector_id: Identifier for dev tools/inspector
Usually passed through to children without modification.
Window and Context
window: &mut Window: Window-specific operations (painting, hitboxes, events)cx: &mut App: App-wide operations (spawning tasks, accessing globals)
Layout System Integration
window.request_layout()
Creates a layout node with specified style and children:
let layout_id = window.request_layout(
Style {
size: size(px(200.), px(100.)),
flex: Flex::Column,
gap: px(8.),
..default()
},
vec![child1_layout_id, child2_layout_id],
cx
);Bounds<Pixels>
Represents rectangular region:
pub struct Bounds<T> {
pub origin: Point<T>,
pub size: Size<T>,
}
// Create bounds
let bounds = Bounds::new(
point(px(10.), px(20.)),
size(px(100.), px(50.))
);
// Access properties
bounds.left() // origin.x
bounds.top() // origin.y
bounds.right() // origin.x + size.width
bounds.bottom() // origin.y + size.height
bounds.center() // center pointHitbox System
Creating Hitboxes
// Normal hitbox (blocks events)
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
// Transparent hitbox (passes events through)
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Transparent);Using Hitboxes
// Check if hovered
if hitbox.is_hovered(window) {
// ...
}
// Set cursor style
window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
// Use in event handlers
window.on_mouse_event(move |event, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Handle event
}
});Event Handling
Mouse Events
// Mouse down
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
if phase.bubble() && bounds.contains(&event.position) {
// Handle mouse down
cx.stop_propagation(); // Prevent bubbling
}
});
// Mouse up
window.on_mouse_event(move |event: &MouseUpEvent, phase, window, cx| {
// Handle mouse up
});
// Mouse move
window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
// Handle mouse move
});
// Scroll
window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
// Handle scroll
});Event Phase
Events go through two phases:
- Capture: Top-down (parent → child)
- Bubble: Bottom-up (child → parent)
move |event, phase, window, cx| {
if phase.capture() {
// Handle in capture phase
} else if phase.bubble() {
// Handle in bubble phase
}
cx.stop_propagation(); // Stop event from continuing
}Cursor Styles
Available cursor styles:
CursorStyle::Arrow
CursorStyle::IBeam // Text selection
CursorStyle::PointingHand // Clickable
CursorStyle::ResizeLeft
CursorStyle::ResizeRight
CursorStyle::ResizeUp
CursorStyle::ResizeDown
CursorStyle::ResizeLeftRight
CursorStyle::ResizeUpDown
CursorStyle::Crosshair
CursorStyle::OperationNotAllowedUsage:
window.set_cursor_style(CursorStyle::PointingHand, &hitbox);Element Best Practices
Contents: State Management · Performance Considerations · Interaction Handling · Layout Strategies · Error Handling · Testing Element Implementations · Common Pitfalls · Performance Checklist
State Management
Using Associated Types Effectively
Good: Use associated types to pass meaningful data between phases
// Good: Structured state with type safety
type RequestLayoutState = (StyledText, Vec<ChildLayout>);
type PrepaintState = (Hitbox, Vec<ChildBounds>);Bad: Using empty state when you need data
// Bad: No state when you need to pass data
type RequestLayoutState = ();
type PrepaintState = ();
// Now you can't pass layout info to paint phase!Managing Complex State
For elements with complex state, create dedicated structs:
// Good: Dedicated struct for complex state
pub struct TextElementState {
pub styled_text: StyledText,
pub text_layout: TextLayout,
pub child_states: Vec<ChildState>,
}
type RequestLayoutState = TextElementState;Benefits:
- Clear documentation of state structure
- Easy to extend
- Type-safe access
State Lifecycle
Golden Rule: State flows in one direction through the phases
request_layout → RequestLayoutState →
prepaint → PrepaintState →
paintDon't:
- Store state in the element struct that should be in associated types
- Try to mutate element state in paint phase (use
cx.notify()to schedule re-render) - Pass mutable references across phase boundaries
Performance Considerations
Minimize Allocations in Paint Phase
Critical: Paint phase is called every frame during animations. Minimize allocations.
Good: Pre-allocate in request_layout or prepaint
impl Element for MyElement {
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, Vec<StyledText>)
{
// Allocate once during layout
let styled_texts = self.children
.iter()
.map(|child| StyledText::new(child.text.clone()))
.collect();
(layout_id, styled_texts)
}
fn paint(&mut self, .., styled_texts: &mut Vec<StyledText>, ..) {
// Just use pre-allocated styled_texts
for text in styled_texts {
text.paint(..);
}
}
}Bad: Allocate in paint phase
fn paint(&mut self, ..) {
// Bad: Allocation in paint phase!
let styled_texts: Vec<_> = self.children
.iter()
.map(|child| StyledText::new(child.text.clone()))
.collect();
}Cache Expensive Computations
Use memoization for expensive operations:
pub struct CachedElement {
// Cache key
last_text: Option<SharedString>,
last_width: Option<Pixels>,
// Cached result
cached_layout: Option<TextLayout>,
}
impl Element for CachedElement {
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, TextLayout)
{
let current_width = window.bounds().width();
// Check if cache is valid
if self.last_text.as_ref() != Some(&self.text)
|| self.last_width != Some(current_width)
|| self.cached_layout.is_none()
{
// Recompute expensive layout
self.cached_layout = Some(self.compute_text_layout(current_width));
self.last_text = Some(self.text.clone());
self.last_width = Some(current_width);
}
// Use cached layout
let layout = self.cached_layout.as_ref().unwrap();
(layout_id, layout.clone())
}
}Lazy Child Rendering
Only render visible children in scrollable containers:
fn paint(&mut self, .., bounds: Bounds<Pixels>, paint_state: &mut Self::PrepaintState, ..) {
for (i, child) in self.children.iter_mut().enumerate() {
let child_bounds = paint_state.child_bounds[i];
// Only paint visible children
if self.is_visible(&child_bounds, &bounds) {
child.paint(..);
}
}
}
fn is_visible(&self, child_bounds: &Bounds<Pixels>, container_bounds: &Bounds<Pixels>) -> bool {
child_bounds.bottom() >= container_bounds.top() &&
child_bounds.top() <= container_bounds.bottom()
}Interaction Handling
Proper Event Bubbling
Always check phase and bounds before handling events:
fn paint(&mut self, .., window: &mut Window, cx: &mut App) {
window.on_mouse_event({
let hitbox = self.hitbox.clone();
move |event: &MouseDownEvent, phase, window, cx| {
// Check phase first
if !phase.bubble() {
return;
}
// Check if event is within bounds
if !hitbox.is_hovered(window) {
return;
}
// Handle event
self.handle_click(event);
// Stop propagation if handled
cx.stop_propagation();
}
});
}Don't forget:
- Check
phase.bubble()orphase.capture()as appropriate - Check hitbox hover state or bounds
- Call
cx.stop_propagation()if you handle the event
Hitbox Management
Create hitboxes in prepaint phase, not paint:
Good:
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, window: &mut Window, ..) -> Hitbox {
// Create hitbox in prepaint
window.insert_hitbox(bounds, HitboxBehavior::Normal)
}
fn paint(&mut self, .., hitbox: &mut Hitbox, window: &mut Window, ..) {
// Use hitbox in paint
window.set_cursor_style(CursorStyle::PointingHand, hitbox);
}Hitbox Behaviors:
// Normal: Blocks events from passing through
HitboxBehavior::Normal
// Transparent: Allows events to pass through to elements below
HitboxBehavior::TransparentCursor Style Guidelines
Set appropriate cursor styles for interactivity cues:
// Text selection
window.set_cursor_style(CursorStyle::IBeam, &hitbox);
// Clickable elements (desktop convention: use default, not pointing hand)
window.set_cursor_style(CursorStyle::Arrow, &hitbox);
// Links (web convention: use pointing hand)
window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
// Resizable edges
window.set_cursor_style(CursorStyle::ResizeLeftRight, &hitbox);Desktop vs Web Convention:
- Desktop apps: Use
Arrowfor buttons - Web apps: Use
PointingHandfor links only
Layout Strategies
Fixed Size Elements
For elements with known, unchanging size:
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App) -> (LayoutId, ()) {
let layout_id = window.request_layout(
Style {
size: size(px(200.), px(100.)),
..default()
},
vec![], // No children
cx
);
(layout_id, ())
}Content-Based Sizing
For elements sized by their content:
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, Size<Pixels>)
{
// Measure content
let text_bounds = self.measure_text(window);
let padding = px(16.);
let layout_id = window.request_layout(
Style {
size: size(
text_bounds.width() + padding * 2.,
text_bounds.height() + padding * 2.,
),
..default()
},
vec![],
cx
);
(layout_id, text_bounds)
}Flexible Layouts
For elements that adapt to available space:
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, Vec<LayoutId>)
{
let mut child_layout_ids = Vec::new();
for child in &mut self.children {
let (layout_id, _) = child.request_layout(window, cx);
child_layout_ids.push(layout_id);
}
let layout_id = window.request_layout(
Style {
flex_direction: FlexDirection::Row,
gap: px(8.),
size: Size {
width: relative(1.0), // Fill parent width
height: auto(), // Auto height
},
..default()
},
child_layout_ids.clone(),
cx
);
(layout_id, child_layout_ids)
}Error Handling
Graceful Degradation
Handle errors gracefully, don't panic:
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, Option<TextLayout>)
{
// Try to create styled text
match StyledText::new(self.text.clone()).request_layout(None, None, window, cx) {
Ok((layout_id, text_layout)) => {
(layout_id, Some(text_layout))
}
Err(e) => {
// Log error
eprintln!("Failed to layout text: {}", e);
// Fallback to simple text
let fallback_text = StyledText::new("(Error loading text)".into());
let (layout_id, _) = fallback_text.request_layout(None, None, window, cx);
(layout_id, None)
}
}
}Defensive Bounds Checking
Always validate bounds and indices:
fn paint_selection(&self, selection: &Selection, text_layout: &TextLayout, ..) {
// Validate selection bounds
let start = selection.start.min(self.text.len());
let end = selection.end.min(self.text.len());
if start >= end {
return; // Invalid selection
}
let rects = text_layout.rects_for_range(start..end);
// Paint selection...
}Testing Element Implementations
Layout Tests
Test that layout calculations are correct:
#[cfg(test)]
mod tests {
use super::*;
use gpui::TestAppContext;
#[gpui::test]
fn test_element_layout(cx: &mut TestAppContext) {
cx.update(|cx| {
let mut window = cx.open_window(Default::default(), |_, _| ()).unwrap();
window.update(cx, |window, cx| {
let mut element = MyElement::new();
let (layout_id, layout_state) = element.request_layout(
None,
None,
window,
cx
);
// Assert layout properties
let bounds = window.layout_bounds(layout_id);
assert_eq!(bounds.size.width, px(200.));
assert_eq!(bounds.size.height, px(100.));
});
});
}
}Interaction Tests
Test that interactions work correctly:
#[gpui::test]
fn test_element_click(cx: &mut TestAppContext) {
cx.update(|cx| {
let mut window = cx.open_window(Default::default(), |_, cx| {
cx.new(|_| MyElement::new())
}).unwrap();
window.update(cx, |window, cx| {
let view = window.root_view().unwrap();
// Simulate click
let position = point(px(10.), px(10.));
window.dispatch_event(MouseDownEvent {
position,
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
// Assert element responded
view.read(cx).assert_clicked();
});
});
}Common Pitfalls
❌ Storing Layout State in Element Struct
Bad:
pub struct MyElement {
id: ElementId,
// Bad: This should be in RequestLayoutState
cached_layout: Option<TextLayout>,
}Good:
pub struct MyElement {
id: ElementId,
text: SharedString,
}
type RequestLayoutState = TextLayout; // Good: State in associated type❌ Mutating Element in Paint Phase
Bad:
fn paint(&mut self, ..) {
self.counter += 1; // Bad: Mutating element in paint
}Good:
fn paint(&mut self, .., window: &mut Window, cx: &mut App) {
window.on_mouse_event(move |event, phase, window, cx| {
if phase.bubble() {
self.counter += 1;
cx.notify(); // Schedule re-render
}
});
}❌ Creating Hitboxes in Paint Phase
Bad:
fn paint(&mut self, .., bounds: Bounds<Pixels>, window: &mut Window, ..) {
// Bad: Creating hitbox in paint
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
}Good:
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, window: &mut Window, ..) -> Hitbox {
// Good: Creating hitbox in prepaint
window.insert_hitbox(bounds, HitboxBehavior::Normal)
}❌ Ignoring Event Phase
Bad:
window.on_mouse_event(move |event, phase, window, cx| {
// Bad: Not checking phase
self.handle_click(event);
});Good:
window.on_mouse_event(move |event, phase, window, cx| {
// Good: Checking phase
if !phase.bubble() {
return;
}
self.handle_click(event);
});Performance Checklist
Before shipping an element implementation, verify:
- [ ] No allocations in
paintphase (except event handlers) - [ ] Expensive computations are cached/memoized
- [ ] Only visible children are rendered in scrollable containers
- [ ] Hitboxes created in
prepaint, notpaint - [ ] Event handlers check phase and bounds
- [ ] Layout state is passed through associated types, not stored in element
- [ ] Element implements proper error handling with fallbacks
- [ ] Tests cover layout calculations and interactions
Element Implementation Examples
Complete examples of implementing custom elements for various scenarios.
Table of Contents
1. Simple Text Element 2. Interactive Element with Selection 3. Complex Element with Child Management
Simple Text Element
A basic text element with syntax highlighting support.
pub struct SimpleText {
id: ElementId,
text: SharedString,
highlights: Vec<(Range<usize>, HighlightStyle)>,
}
impl IntoElement for SimpleText {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for SimpleText {
type RequestLayoutState = StyledText;
type PrepaintState = Hitbox;
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, Self::RequestLayoutState) {
// Create styled text with highlights
let mut runs = Vec::new();
let mut ix = 0;
for (range, highlight) in &self.highlights {
// Add unstyled text before highlight
if ix < range.start {
runs.push(window.text_style().to_run(range.start - ix));
}
// Add highlighted text
runs.push(
window.text_style()
.highlight(*highlight)
.to_run(range.len())
);
ix = range.end;
}
// Add remaining unstyled text
if ix < self.text.len() {
runs.push(window.text_style().to_run(self.text.len() - ix));
}
let styled_text = StyledText::new(self.text.clone()).with_runs(runs);
let (layout_id, _) = styled_text.request_layout(
global_id,
inspector_id,
window,
cx
);
(layout_id, styled_text)
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
styled_text: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App
) -> Self::PrepaintState {
// Prepaint the styled text
styled_text.prepaint(
global_id,
inspector_id,
bounds,
&mut (),
window,
cx
);
// Create hitbox for interaction
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
hitbox
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
styled_text: &mut Self::RequestLayoutState,
hitbox: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App
) {
// Paint the styled text
styled_text.paint(
global_id,
inspector_id,
bounds,
&mut (),
&mut (),
window,
cx
);
// Set cursor style for text
window.set_cursor_style(CursorStyle::IBeam, hitbox);
}
}Interactive Element with Selection
A text element that supports text selection via mouse interaction.
#[derive(Clone)]
pub struct Selection {
pub start: usize,
pub end: usize,
}
pub struct SelectableText {
id: ElementId,
text: SharedString,
selectable: bool,
selection: Option<Selection>,
}
impl IntoElement for SelectableText {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for SelectableText {
type RequestLayoutState = TextLayout;
type PrepaintState = Option<Hitbox>;
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, Self::RequestLayoutState) {
let styled_text = StyledText::new(self.text.clone());
let (layout_id, _) = styled_text.request_layout(
global_id,
inspector_id,
window,
cx
);
// Extract text layout for selection painting
let text_layout = styled_text.layout().clone();
(layout_id, text_layout)
}
fn prepaint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_text_layout: &mut Self::RequestLayoutState,
window: &mut Window,
_cx: &mut App
) -> Self::PrepaintState {
// Only create hitbox if selectable
if self.selectable {
Some(window.insert_hitbox(bounds, HitboxBehavior::Normal))
} else {
None
}
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
text_layout: &mut Self::RequestLayoutState,
hitbox: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App
) {
// Paint text
let styled_text = StyledText::new(self.text.clone());
styled_text.paint(
global_id,
inspector_id,
bounds,
&mut (),
&mut (),
window,
cx
);
// Paint selection if any
if let Some(selection) = &self.selection {
Self::paint_selection(selection, text_layout, &bounds, window, cx);
}
// Handle mouse events for selection
if let Some(hitbox) = hitbox {
window.set_cursor_style(CursorStyle::IBeam, hitbox);
// Mouse down to start selection
window.on_mouse_event({
let bounds = bounds.clone();
move |event: &MouseDownEvent, phase, window, cx| {
if bounds.contains(&event.position) && phase.bubble() {
// Start selection at mouse position
let char_index = Self::position_to_index(
event.position,
&bounds,
text_layout
);
self.selection = Some(Selection {
start: char_index,
end: char_index,
});
cx.notify();
cx.stop_propagation();
}
}
});
// Mouse drag to extend selection
window.on_mouse_event({
let bounds = bounds.clone();
move |event: &MouseMoveEvent, phase, window, cx| {
if let Some(selection) = &mut self.selection {
if phase.bubble() {
let char_index = Self::position_to_index(
event.position,
&bounds,
text_layout
);
selection.end = char_index;
cx.notify();
}
}
}
});
}
}
}
impl SelectableText {
fn paint_selection(
selection: &Selection,
text_layout: &TextLayout,
bounds: &Bounds<Pixels>,
window: &mut Window,
cx: &mut App
) {
// Calculate selection bounds from text layout
let selection_rects = text_layout.rects_for_range(
selection.start..selection.end
);
// Paint selection background
for rect in selection_rects {
window.paint_quad(paint_quad(
Bounds::new(
point(bounds.left() + rect.origin.x, bounds.top() + rect.origin.y),
rect.size
),
Anchor::default(),
cx.theme().selection_background,
));
}
}
fn position_to_index(
position: Point<Pixels>,
bounds: &Bounds<Pixels>,
text_layout: &TextLayout
) -> usize {
// Convert screen position to character index
let relative_pos = point(
position.x - bounds.left(),
position.y - bounds.top()
);
text_layout.index_for_position(relative_pos)
}
}Complex Element with Child Management
A container element that manages multiple children with scrolling support.
pub struct ComplexElement {
id: ElementId,
children: Vec<Box<dyn Element<RequestLayoutState = (), PrepaintState = ()>>>,
scrollable: bool,
scroll_offset: Point<Pixels>,
}
struct ComplexLayoutState {
child_layouts: Vec<LayoutId>,
total_height: Pixels,
}
struct ComplexPaintState {
child_bounds: Vec<Bounds<Pixels>>,
hitbox: Hitbox,
}
impl IntoElement for ComplexElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for ComplexElement {
type RequestLayoutState = ComplexLayoutState;
type PrepaintState = ComplexPaintState;
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, Self::RequestLayoutState) {
let mut child_layouts = Vec::new();
let mut total_height = px(0.);
// Request layout for all children
for child in &mut self.children {
let (child_layout_id, _) = child.request_layout(
global_id,
inspector_id,
window,
cx
);
child_layouts.push(child_layout_id);
// Get child size from layout
let child_size = window.layout_bounds(child_layout_id).size();
total_height += child_size.height;
}
// Create container layout
let layout_id = window.request_layout(
Style {
flex_direction: FlexDirection::Column,
gap: px(8.),
size: Size {
width: relative(1.0),
height: if self.scrollable {
// Fixed height for scrollable
px(400.)
} else {
// Auto height for non-scrollable
total_height
},
},
..default()
},
child_layouts.clone(),
cx
);
(layout_id, ComplexLayoutState {
child_layouts,
total_height,
})
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
layout_state: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App
) -> Self::PrepaintState {
let mut child_bounds = Vec::new();
let mut y_offset = self.scroll_offset.y;
// Calculate child bounds and prepaint children
for (child, layout_id) in self.children.iter_mut()
.zip(&layout_state.child_layouts)
{
let child_size = window.layout_bounds(*layout_id).size();
let child_bound = Bounds::new(
point(bounds.left(), bounds.top() + y_offset),
child_size
);
// Only prepaint visible children
if self.is_visible(&child_bound, &bounds) {
child.prepaint(
global_id,
inspector_id,
child_bound,
&mut (),
window,
cx
);
}
child_bounds.push(child_bound);
y_offset += child_size.height + px(8.); // gap
}
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
ComplexPaintState {
child_bounds,
hitbox,
}
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
layout_state: &mut Self::RequestLayoutState,
paint_state: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App
) {
// Paint background
window.paint_quad(paint_quad(
bounds,
Anchor::all(px(4.)),
cx.theme().background,
));
// Paint visible children only
for (i, child) in self.children.iter_mut().enumerate() {
let child_bounds = paint_state.child_bounds[i];
if self.is_visible(&child_bounds, &bounds) {
child.paint(
global_id,
inspector_id,
child_bounds,
&mut (),
&mut (),
window,
cx
);
}
}
// Paint scrollbar if scrollable
if self.scrollable {
self.paint_scrollbar(bounds, layout_state, window, cx);
}
// Handle scroll events
if self.scrollable {
window.on_mouse_event({
let hitbox = paint_state.hitbox.clone();
let total_height = layout_state.total_height;
let visible_height = bounds.size.height;
move |event: &ScrollWheelEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Update scroll offset
self.scroll_offset.y -= event.delta.y;
// Clamp scroll offset
let max_scroll = (total_height - visible_height).max(px(0.));
self.scroll_offset.y = self.scroll_offset.y
.max(px(0.))
.min(max_scroll);
cx.notify();
cx.stop_propagation();
}
}
});
}
}
}
impl ComplexElement {
fn is_visible(&self, child_bounds: &Bounds<Pixels>, container_bounds: &Bounds<Pixels>) -> bool {
// Check if child is within visible area
child_bounds.bottom() >= container_bounds.top() &&
child_bounds.top() <= container_bounds.bottom()
}
fn paint_scrollbar(
&self,
bounds: Bounds<Pixels>,
layout_state: &ComplexLayoutState,
window: &mut Window,
cx: &mut App
) {
let scrollbar_width = px(8.);
let visible_height = bounds.size.height;
let total_height = layout_state.total_height;
if total_height <= visible_height {
return; // No need for scrollbar
}
// Calculate scrollbar position and size
let scroll_ratio = self.scroll_offset.y / (total_height - visible_height);
let thumb_height = (visible_height / total_height) * visible_height;
let thumb_y = scroll_ratio * (visible_height - thumb_height);
// Paint scrollbar track
let track_bounds = Bounds::new(
point(bounds.right() - scrollbar_width, bounds.top()),
size(scrollbar_width, visible_height)
);
window.paint_quad(paint_quad(
track_bounds,
Anchor::default(),
cx.theme().scrollbar_track,
));
// Paint scrollbar thumb
let thumb_bounds = Bounds::new(
point(bounds.right() - scrollbar_width, bounds.top() + thumb_y),
size(scrollbar_width, thumb_height)
);
window.paint_quad(paint_quad(
thumb_bounds,
Anchor::all(px(4.)),
cx.theme().scrollbar_thumb,
));
}
}Usage Examples
Using SimpleText
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.child(SimpleText {
id: ElementId::Name("code-text".into()),
text: "fn main() { println!(\"Hello\"); }".into(),
highlights: vec![
(0..2, HighlightStyle::keyword()),
(3..7, HighlightStyle::function()),
],
})
}Using SelectableText
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.child(SelectableText {
id: ElementId::Name("selectable-text".into()),
text: "Select this text with your mouse".into(),
selectable: true,
selection: self.current_selection.clone(),
})
}Using ComplexElement
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let children: Vec<Box<dyn Element<_, _>>> = self.items
.iter()
.map(|item| Box::new(div().child(item.name.clone())) as Box<_>)
.collect();
div()
.child(ComplexElement {
id: ElementId::Name("scrollable-list".into()),
children,
scrollable: true,
scroll_offset: self.scroll_offset,
})
}ElementId
ElementId is a unique identifier for a GPUI element. It is required for elements that need:
- Mouse event handling (
on_click,on_hover, etc.) - State storage via
window.use_keyed_state - Interaction tracking
Making an Element Stateful
Call .id() on a div() to create a Stateful<Div>:
div().id("my-element") // ElementId from &str
div().id(42usize) // ElementId from usize
div().id(ElementId::from(idx)) // ExplicitWithout .id(), a div cannot receive mouse events or store state.
Accepted Types
impl Into<ElementId> for &str // "my-id"
impl Into<ElementId> for String // String::from("my-id")
impl Into<ElementId> for usize // 0, 1, 2, ...
impl Into<ElementId> for u64
impl Into<ElementId> for SharedStringUniqueness Rules
IDs must be unique within the same stateful parent's scope — not globally. GPUI builds a GlobalElementId by chaining parent IDs:
div().id("app").child(
div().id("list1").children(vec![
div().id(1usize).child("Item 1"), // GlobalId: ["app", "list1", 1]
div().id(2usize).child("Item 2"), // GlobalId: ["app", "list1", 2]
])
).child(
div().id("list2").children(vec![
div().id(1usize).child("Item 1"), // GlobalId: ["app", "list2", 1] — no conflict
])
)Items in different parent scopes can reuse simple IDs (integers, short strings).
In Component Structs
Components always store id: ElementId and pass it in new():
#[derive(IntoElement)]
pub struct Button {
id: ElementId,
base: Stateful<Div>,
// ...
}
impl Button {
pub fn new(id: impl Into<ElementId>) -> Self {
let id = id.into();
Self {
id: id.clone(),
base: div().id(id), // id applied to base
// ...
}
}
}
impl RenderOnce for Button {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
self.base // already has .id() applied
.on_click(/* ... */)
}
}Usage at Call Sites
// Use unique string IDs for named components
Button::new("save-btn").label("Save")
Button::new("cancel-btn").label("Cancel")
// Use index-based IDs in lists
for (i, item) in items.iter().enumerate() {
div().id(i) // unique within this parent
}
// Use descriptive IDs for debugging
Input::new("search-input")
Select::new("country-select")Common Element Patterns
Contents: Text Rendering Elements · Container Elements · Interactive Elements · Composite Elements · Scrollable Elements · Pattern Selection Guide
Text Rendering Elements
Elements that display and manipulate text content.
Pattern Characteristics
- Use
StyledTextfor text layout and rendering - Handle text selection in
paintphase with hitbox interaction - Create hitboxes for text interaction in
prepaint - Support text highlighting and custom styling via runs
Implementation Template
pub struct TextElement {
id: ElementId,
text: SharedString,
style: TextStyle,
}
impl Element for TextElement {
type RequestLayoutState = StyledText;
type PrepaintState = Hitbox;
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, StyledText)
{
let styled_text = StyledText::new(self.text.clone())
.with_style(self.style);
let (layout_id, _) = styled_text.request_layout(None, None, window, cx);
(layout_id, styled_text)
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, styled_text: &mut StyledText,
window: &mut Window, cx: &mut App) -> Hitbox
{
styled_text.prepaint(None, None, bounds, &mut (), window, cx);
window.insert_hitbox(bounds, HitboxBehavior::Normal)
}
fn paint(&mut self, .., bounds: Bounds<Pixels>, styled_text: &mut StyledText,
hitbox: &mut Hitbox, window: &mut Window, cx: &mut App)
{
styled_text.paint(None, None, bounds, &mut (), &mut (), window, cx);
window.set_cursor_style(CursorStyle::IBeam, hitbox);
}
}Use Cases
- Code editors with syntax highlighting
- Rich text displays
- Labels with custom formatting
- Selectable text areas
Container Elements
Elements that manage and layout child elements.
Pattern Characteristics
- Manage child element layouts and positions
- Handle scrolling and clipping when needed
- Implement flex/grid-like layouts
- Coordinate child interactions and event delegation
Implementation Template
pub struct ContainerElement {
id: ElementId,
children: Vec<AnyElement>,
direction: FlexDirection,
gap: Pixels,
}
impl Element for ContainerElement {
type RequestLayoutState = Vec<LayoutId>;
type PrepaintState = Vec<Bounds<Pixels>>;
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, Vec<LayoutId>)
{
let child_layout_ids: Vec<_> = self.children
.iter_mut()
.map(|child| child.request_layout(window, cx).0)
.collect();
let layout_id = window.request_layout(
Style {
flex_direction: self.direction,
gap: self.gap,
..default()
},
child_layout_ids.clone(),
cx
);
(layout_id, child_layout_ids)
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, layout_ids: &mut Vec<LayoutId>,
window: &mut Window, cx: &mut App) -> Vec<Bounds<Pixels>>
{
let mut child_bounds = Vec::new();
for (child, layout_id) in self.children.iter_mut().zip(layout_ids.iter()) {
let child_bound = window.layout_bounds(*layout_id);
child.prepaint(child_bound, window, cx);
child_bounds.push(child_bound);
}
child_bounds
}
fn paint(&mut self, .., child_bounds: &mut Vec<Bounds<Pixels>>,
window: &mut Window, cx: &mut App)
{
for (child, bounds) in self.children.iter_mut().zip(child_bounds.iter()) {
child.paint(*bounds, window, cx);
}
}
}Use Cases
- Panels and split views
- List containers
- Grid layouts
- Tab containers
Interactive Elements
Elements that respond to user input (mouse, keyboard, touch).
Pattern Characteristics
- Create appropriate hitboxes for interaction areas
- Handle mouse/keyboard/touch events properly
- Manage focus and cursor styles
- Support hover, active, and disabled states
Implementation Template
pub struct InteractiveElement {
id: ElementId,
content: AnyElement,
on_click: Option<Box<dyn Fn(&MouseUpEvent, &mut Window, &mut App)>>,
hover_style: Option<Style>,
}
impl Element for InteractiveElement {
type RequestLayoutState = LayoutId;
type PrepaintState = (Hitbox, bool); // hitbox and is_hovered
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, LayoutId)
{
let (content_layout, _) = self.content.request_layout(window, cx);
(content_layout, content_layout)
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, content_layout: &mut LayoutId,
window: &mut Window, cx: &mut App) -> (Hitbox, bool)
{
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
let is_hovered = hitbox.is_hovered(window);
self.content.prepaint(bounds, window, cx);
(hitbox, is_hovered)
}
fn paint(&mut self, .., bounds: Bounds<Pixels>, content_layout: &mut LayoutId,
prepaint: &mut (Hitbox, bool), window: &mut Window, cx: &mut App)
{
let (hitbox, is_hovered) = prepaint;
// Paint hover background if hovered
if *is_hovered {
if let Some(hover_style) = &self.hover_style {
window.paint_quad(paint_quad(
bounds,
Anchor::all(px(4.)),
hover_style.background_color.unwrap_or(cx.theme().hover),
));
}
}
// Paint content
self.content.paint(bounds, window, cx);
// Handle click
if let Some(on_click) = self.on_click.as_ref() {
window.on_mouse_event({
let on_click = on_click.clone();
let hitbox = hitbox.clone();
move |event: &MouseUpEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
on_click(event, window, cx);
cx.stop_propagation();
}
}
});
}
// Set cursor style
window.set_cursor_style(CursorStyle::PointingHand, hitbox);
}
}Use Cases
- Buttons
- Links
- Clickable cards
- Drag handles
- Menu items
Composite Elements
Elements that combine multiple child elements with complex coordination.
Pattern Characteristics
- Combine multiple child elements with different types
- Manage complex state across children
- Coordinate animations and transitions
- Handle focus delegation between children
Implementation Template
pub struct CompositeElement {
id: ElementId,
header: AnyElement,
content: AnyElement,
footer: Option<AnyElement>,
}
struct CompositeLayoutState {
header_layout: LayoutId,
content_layout: LayoutId,
footer_layout: Option<LayoutId>,
}
struct CompositePaintState {
header_bounds: Bounds<Pixels>,
content_bounds: Bounds<Pixels>,
footer_bounds: Option<Bounds<Pixels>>,
}
impl Element for CompositeElement {
type RequestLayoutState = CompositeLayoutState;
type PrepaintState = CompositePaintState;
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, CompositeLayoutState)
{
let (header_layout, _) = self.header.request_layout(window, cx);
let (content_layout, _) = self.content.request_layout(window, cx);
let footer_layout = self.footer.as_mut()
.map(|f| f.request_layout(window, cx).0);
let mut children = vec![header_layout, content_layout];
if let Some(footer) = footer_layout {
children.push(footer);
}
let layout_id = window.request_layout(
Style {
flex_direction: FlexDirection::Column,
size: Size {
width: relative(1.0),
height: auto(),
},
..default()
},
children,
cx
);
(layout_id, CompositeLayoutState {
header_layout,
content_layout,
footer_layout,
})
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, layout: &mut CompositeLayoutState,
window: &mut Window, cx: &mut App) -> CompositePaintState
{
let header_bounds = window.layout_bounds(layout.header_layout);
let content_bounds = window.layout_bounds(layout.content_layout);
let footer_bounds = layout.footer_layout
.map(|id| window.layout_bounds(id));
self.header.prepaint(header_bounds, window, cx);
self.content.prepaint(content_bounds, window, cx);
if let (Some(footer), Some(bounds)) = (&mut self.footer, footer_bounds) {
footer.prepaint(bounds, window, cx);
}
CompositePaintState {
header_bounds,
content_bounds,
footer_bounds,
}
}
fn paint(&mut self, .., paint_state: &mut CompositePaintState,
window: &mut Window, cx: &mut App)
{
self.header.paint(paint_state.header_bounds, window, cx);
self.content.paint(paint_state.content_bounds, window, cx);
if let (Some(footer), Some(bounds)) = (&mut self.footer, paint_state.footer_bounds) {
footer.paint(bounds, window, cx);
}
}
}Use Cases
- Dialog boxes (header + content + footer)
- Cards with multiple sections
- Form layouts
- Panels with toolbars
Scrollable Elements
Elements with scrollable content areas.
Pattern Characteristics
- Manage scroll state (offset, velocity)
- Handle scroll events (wheel, drag, touch)
- Paint scrollbars (track and thumb)
- Clip content to visible area
Implementation Template
pub struct ScrollableElement {
id: ElementId,
content: AnyElement,
scroll_offset: Point<Pixels>,
content_size: Size<Pixels>,
}
struct ScrollPaintState {
hitbox: Hitbox,
visible_bounds: Bounds<Pixels>,
}
impl Element for ScrollableElement {
type RequestLayoutState = (LayoutId, Size<Pixels>);
type PrepaintState = ScrollPaintState;
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, (LayoutId, Size<Pixels>))
{
let (content_layout, _) = self.content.request_layout(window, cx);
let content_size = window.layout_bounds(content_layout).size;
let layout_id = window.request_layout(
Style {
size: Size {
width: relative(1.0),
height: px(400.), // Fixed viewport height
},
overflow: Overflow::Hidden,
..default()
},
vec![content_layout],
cx
);
(layout_id, (content_layout, content_size))
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, layout: &mut (LayoutId, Size<Pixels>),
window: &mut Window, cx: &mut App) -> ScrollPaintState
{
let (content_layout, content_size) = layout;
// Calculate content bounds with scroll offset
let content_bounds = Bounds::new(
point(bounds.left(), bounds.top() - self.scroll_offset.y),
*content_size
);
self.content.prepaint(content_bounds, window, cx);
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
ScrollPaintState {
hitbox,
visible_bounds: bounds,
}
}
fn paint(&mut self, .., layout: &mut (LayoutId, Size<Pixels>),
paint_state: &mut ScrollPaintState, window: &mut Window, cx: &mut App)
{
let (_, content_size) = layout;
// Paint content
self.content.paint(paint_state.visible_bounds, window, cx);
// Paint scrollbar
self.paint_scrollbar(paint_state.visible_bounds, *content_size, window, cx);
// Handle scroll events
window.on_mouse_event({
let hitbox = paint_state.hitbox.clone();
let content_height = content_size.height;
let visible_height = paint_state.visible_bounds.size.height;
move |event: &ScrollWheelEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Update scroll offset
self.scroll_offset.y -= event.delta.y;
// Clamp to valid range
let max_scroll = (content_height - visible_height).max(px(0.));
self.scroll_offset.y = self.scroll_offset.y
.max(px(0.))
.min(max_scroll);
cx.notify();
cx.stop_propagation();
}
}
});
}
}
impl ScrollableElement {
fn paint_scrollbar(
&self,
bounds: Bounds<Pixels>,
content_size: Size<Pixels>,
window: &mut Window,
cx: &mut App
) {
let visible_height = bounds.size.height;
let content_height = content_size.height;
if content_height <= visible_height {
return; // No scrollbar needed
}
let scrollbar_width = px(8.);
// Calculate thumb position and size
let scroll_ratio = self.scroll_offset.y / (content_height - visible_height);
let thumb_height = (visible_height / content_height) * visible_height;
let thumb_y = scroll_ratio * (visible_height - thumb_height);
// Paint track
window.paint_quad(paint_quad(
Bounds::new(
point(bounds.right() - scrollbar_width, bounds.top()),
size(scrollbar_width, visible_height)
),
Anchor::default(),
cx.theme().scrollbar_track,
));
// Paint thumb
window.paint_quad(paint_quad(
Bounds::new(
point(bounds.right() - scrollbar_width, bounds.top() + thumb_y),
size(scrollbar_width, thumb_height)
),
Anchor::all(px(4.)),
cx.theme().scrollbar_thumb,
));
}
}Use Cases
- Scrollable lists
- Code editors with large files
- Long-form text content
- Image galleries
Pattern Selection Guide
| Need | Pattern | Complexity |
|---|---|---|
| Display styled text | Text Rendering | Low |
| Layout multiple children | Container | Low-Medium |
| Handle clicks/hovers | Interactive | Medium |
| Complex multi-part UI | Composite | Medium-High |
| Large content with scrolling | Scrollable | High |
Choose the simplest pattern that meets your requirements, then extend as needed.
When to Use
Use the low-level Element trait when:
- Need fine-grained control over layout calculation
- Building complex, performance-critical components
- Implementing custom layout algorithms (masonry, circular, etc.)
- High-level
Render/RenderOnceAPIs are insufficient
Prefer `Render`/`RenderOnce` for: Simple components, standard layouts, declarative UI
Quick Start
The Element trait provides direct control over three rendering phases:
impl Element for MyElement {
type RequestLayoutState = MyLayoutState; // Data passed to later phases
type PrepaintState = MyPaintState; // Data for painting
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
// Phase 1: Calculate sizes and positions
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, Self::RequestLayoutState)
{
let layout_id = window.request_layout(
Style { size: size(px(200.), px(100.)), ..default() },
vec![],
cx
);
(layout_id, MyLayoutState { /* ... */ })
}
// Phase 2: Create hitboxes, prepare for painting
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, layout: &mut Self::RequestLayoutState,
window: &mut Window, cx: &mut App) -> Self::PrepaintState
{
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
MyPaintState { hitbox }
}
// Phase 3: Render and handle interactions
fn paint(&mut self, .., bounds: Bounds<Pixels>, layout: &mut Self::RequestLayoutState,
paint_state: &mut Self::PrepaintState, window: &mut Window, cx: &mut App)
{
window.paint_quad(paint_quad(bounds, Anchor::all(px(4.)), cx.theme().background));
window.on_mouse_event({
let hitbox = paint_state.hitbox.clone();
move |event: &MouseDownEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Handle interaction
cx.stop_propagation();
}
}
});
}
}
// Enable element to be used as child
impl IntoElement for MyElement {
type Element = Self;
fn into_element(self) -> Self::Element { self }
}Core Concepts
Three-Phase Rendering
1. request_layout: Calculate sizes and positions, return layout ID and state 2. prepaint: Create hitboxes, compute final bounds, prepare for painting 3. paint: Render element, set up interactions (mouse events, cursor styles)
State Flow
RequestLayoutState → PrepaintState → paintState flows in one direction through associated types, passed as mutable references between phases.
Key Operations
- Layout:
window.request_layout(style, children, cx)- Create layout node - Hitboxes:
window.insert_hitbox(bounds, behavior)- Create interaction area - Painting:
window.paint_quad(...)- Render visual content - Events:
window.on_mouse_event(handler)- Handle user input
Reference Documentation
Complete API Documentation
- API: See element-api.md — associated types, hitbox system, event handling, cursor styles
- Examples: See element-examples.md — text, interactive, complex elements
- Patterns: See element-patterns.md — text, container, interactive, composite, scrollable
- Best Practices: See element-best-practices.md — performance, state, common pitfalls
- Advanced: See element-advanced.md — masonry/circular layouts, memoization, virtual lists
Advanced Entity Patterns
Contents: Entity Collections Management · Conditional Update Patterns · Entity State Machine Pattern · Entity Proxy Pattern · Cascading Updates Pattern · Entity Snapshot Pattern · Entity Transaction Pattern · Entity Pool Pattern
Entity Collections Management
Dynamic Collection with Cleanup
struct EntityCollection<T> {
strong_refs: Vec<Entity<T>>,
weak_refs: Vec<WeakEntity<T>>,
}
impl<T> EntityCollection<T> {
fn new() -> Self {
Self {
strong_refs: Vec::new(),
weak_refs: Vec::new(),
}
}
fn add(&mut self, entity: Entity<T>, cx: &mut App) {
self.strong_refs.push(entity.clone());
self.weak_refs.push(entity.downgrade());
}
fn remove(&mut self, entity_id: EntityId, cx: &mut App) {
self.strong_refs.retain(|e| e.entity_id() != entity_id);
self.weak_refs.retain(|w| {
w.upgrade()
.map(|e| e.entity_id() != entity_id)
.unwrap_or(false)
});
}
fn cleanup_invalid(&mut self, cx: &mut App) {
self.weak_refs.retain(|weak| weak.upgrade().is_some());
}
fn for_each<F>(&self, cx: &mut App, mut f: F)
where
F: FnMut(&Entity<T>, &mut App),
{
for entity in &self.strong_refs {
f(entity, cx);
}
}
fn for_each_weak<F>(&mut self, cx: &mut App, mut f: F)
where
F: FnMut(Entity<T>, &mut App),
{
self.weak_refs.retain(|weak| {
if let Some(entity) = weak.upgrade() {
f(entity, cx);
true
} else {
false // Remove invalid weak references
}
});
}
}Entity Registry Pattern
use std::collections::HashMap;
struct EntityRegistry<T> {
entities: HashMap<EntityId, WeakEntity<T>>,
}
impl<T> EntityRegistry<T> {
fn new() -> Self {
Self {
entities: HashMap::new(),
}
}
fn register(&mut self, entity: &Entity<T>) {
self.entities.insert(entity.entity_id(), entity.downgrade());
}
fn unregister(&mut self, entity_id: EntityId) {
self.entities.remove(&entity_id);
}
fn get(&self, entity_id: EntityId) -> Option<Entity<T>> {
self.entities.get(&entity_id)?.upgrade()
}
fn cleanup(&mut self) {
self.entities.retain(|_, weak| weak.upgrade().is_some());
}
fn count(&self) -> usize {
self.entities.len()
}
fn all_entities(&self) -> Vec<Entity<T>> {
self.entities
.values()
.filter_map(|weak| weak.upgrade())
.collect()
}
}Conditional Update Patterns
Debounced Updates
use std::time::{Duration, Instant};
struct DebouncedEntity<T> {
entity: Entity<T>,
last_update: Instant,
debounce_duration: Duration,
pending_update: Option<Box<dyn FnOnce(&mut T, &mut Context<T>)>>,
}
impl<T: 'static> DebouncedEntity<T> {
fn new(entity: Entity<T>, debounce_ms: u64) -> Self {
Self {
entity,
last_update: Instant::now(),
debounce_duration: Duration::from_millis(debounce_ms),
pending_update: None,
}
}
fn update<F>(&mut self, cx: &mut App, update_fn: F)
where
F: FnOnce(&mut T, &mut Context<T>) + 'static,
{
let now = Instant::now();
let elapsed = now.duration_since(self.last_update);
if elapsed >= self.debounce_duration {
// Execute immediately
self.entity.update(cx, update_fn);
self.last_update = now;
self.pending_update = None;
} else {
// Store for later
self.pending_update = Some(Box::new(update_fn));
// Schedule execution
let entity = self.entity.clone();
let delay = self.debounce_duration - elapsed;
cx.spawn(async move |cx| {
tokio::time::sleep(delay).await;
if let Some(update) = self.pending_update.take() {
entity.update(cx, |state, inner_cx| {
update(state, inner_cx);
});
}
}).detach();
}
}
}Throttled Updates
struct ThrottledEntity<T> {
entity: Entity<T>,
last_update: Instant,
throttle_duration: Duration,
}
impl<T: 'static> ThrottledEntity<T> {
fn new(entity: Entity<T>, throttle_ms: u64) -> Self {
Self {
entity,
last_update: Instant::now(),
throttle_duration: Duration::from_millis(throttle_ms),
}
}
fn try_update<F>(&mut self, cx: &mut App, update_fn: F) -> bool
where
F: FnOnce(&mut T, &mut Context<T>),
{
let now = Instant::now();
let elapsed = now.duration_since(self.last_update);
if elapsed >= self.throttle_duration {
self.entity.update(cx, update_fn);
self.last_update = now;
true
} else {
false // Update throttled
}
}
}Entity State Machine Pattern
enum AppState {
Idle,
Loading,
Loaded(String),
Error(String),
}
struct StateMachine {
state: AppState,
}
impl StateMachine {
fn new() -> Self {
Self {
state: AppState::Idle,
}
}
fn start_loading(&mut self, cx: &mut Context<Self>) {
if matches!(self.state, AppState::Idle | AppState::Error(_)) {
self.state = AppState::Loading;
cx.notify();
let weak_entity = cx.entity().downgrade();
cx.spawn(async move |cx| {
let result = perform_load().await;
let _ = weak_entity.update(cx, |state, cx| {
match result {
Ok(data) => state.on_load_success(data, cx),
Err(e) => state.on_load_error(e.to_string(), cx),
}
});
}).detach();
}
}
fn on_load_success(&mut self, data: String, cx: &mut Context<Self>) {
if matches!(self.state, AppState::Loading) {
self.state = AppState::Loaded(data);
cx.notify();
}
}
fn on_load_error(&mut self, error: String, cx: &mut Context<Self>) {
if matches!(self.state, AppState::Loading) {
self.state = AppState::Error(error);
cx.notify();
}
}
fn reset(&mut self, cx: &mut Context<Self>) {
self.state = AppState::Idle;
cx.notify();
}
}
async fn perform_load() -> Result<String, anyhow::Error> {
// Actual load implementation
Ok("Data".to_string())
}Entity Proxy Pattern
struct EntityProxy<T> {
entity: WeakEntity<T>,
}
impl<T> EntityProxy<T> {
fn new(entity: &Entity<T>) -> Self {
Self {
entity: entity.downgrade(),
}
}
fn with<F, R>(&self, cx: &mut App, f: F) -> Result<R, anyhow::Error>
where
F: FnOnce(&T, &App) -> R,
{
self.entity.read_with(cx, f)
}
fn update<F, R>(&self, cx: &mut App, f: F) -> Result<R, anyhow::Error>
where
F: FnOnce(&mut T, &mut Context<T>) -> R,
{
self.entity.update(cx, f)
}
fn is_valid(&self, cx: &App) -> bool {
self.entity.upgrade().is_some()
}
}Cascading Updates Pattern
struct CascadingUpdater {
entities: Vec<WeakEntity<UpdateTarget>>,
}
impl CascadingUpdater {
fn new() -> Self {
Self {
entities: Vec::new(),
}
}
fn add_target(&mut self, entity: &Entity<UpdateTarget>) {
self.entities.push(entity.downgrade());
}
fn cascade_update<F>(&mut self, cx: &mut App, update_fn: F)
where
F: Fn(&mut UpdateTarget, &mut Context<UpdateTarget>) + Clone,
{
// Update all entities in sequence
self.entities.retain(|weak| {
if let Ok(_) = weak.update(cx, |state, inner_cx| {
update_fn.clone()(state, inner_cx);
}) {
true // Keep valid entity
} else {
false // Remove invalid entity
}
});
}
}
struct UpdateTarget {
value: i32,
}Entity Snapshot Pattern
use serde::{Serialize, Deserialize};
#[derive(Clone, Serialize, Deserialize)]
struct EntitySnapshot {
data: String,
timestamp: u64,
}
struct SnapshotableEntity {
data: String,
snapshots: Vec<EntitySnapshot>,
}
impl SnapshotableEntity {
fn new(data: String) -> Self {
Self {
data,
snapshots: Vec::new(),
}
}
fn take_snapshot(&mut self, cx: &mut Context<Self>) {
let snapshot = EntitySnapshot {
data: self.data.clone(),
timestamp: current_timestamp(),
};
self.snapshots.push(snapshot);
cx.notify();
}
fn restore_snapshot(&mut self, index: usize, cx: &mut Context<Self>) -> Result<(), String> {
if let Some(snapshot) = self.snapshots.get(index) {
self.data = snapshot.data.clone();
cx.notify();
Ok(())
} else {
Err("Invalid snapshot index".to_string())
}
}
fn clear_old_snapshots(&mut self, keep_last: usize, cx: &mut Context<Self>) {
if self.snapshots.len() > keep_last {
self.snapshots.drain(0..self.snapshots.len() - keep_last);
cx.notify();
}
}
}
fn current_timestamp() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
}Entity Transaction Pattern
struct Transaction<T> {
entity: Entity<T>,
original_state: Option<T>,
}
impl<T: Clone> Transaction<T> {
fn begin(entity: Entity<T>, cx: &mut App) -> Self {
let original_state = entity.read(cx).clone();
Self {
entity,
original_state: Some(original_state),
}
}
fn update<F>(&mut self, cx: &mut App, update_fn: F)
where
F: FnOnce(&mut T, &mut Context<T>),
{
self.entity.update(cx, update_fn);
}
fn commit(mut self, cx: &mut App) {
self.original_state = None; // Don't rollback
self.entity.update(cx, |_, cx| {
cx.notify();
});
}
fn rollback(mut self, cx: &mut App) {
if let Some(original) = self.original_state.take() {
self.entity.update(cx, |state, cx| {
*state = original;
cx.notify();
});
}
}
}
impl<T> Drop for Transaction<T> {
fn drop(&mut self) {
// Auto-rollback if not committed
if self.original_state.is_some() {
eprintln!("Warning: Transaction dropped without commit");
}
}
}
// Usage
fn perform_transaction(entity: Entity<MyState>, cx: &mut App) -> Result<(), String> {
let mut tx = Transaction::begin(entity, cx);
tx.update(cx, |state, cx| {
state.value = 42;
});
if validate_state(&tx.entity, cx)? {
tx.commit(cx);
Ok(())
} else {
tx.rollback(cx);
Err("Validation failed".to_string())
}
}Entity Pool Pattern
struct EntityPool<T> {
available: Vec<Entity<T>>,
in_use: Vec<WeakEntity<T>>,
factory: Box<dyn Fn(&mut App) -> Entity<T>>,
}
impl<T: 'static> EntityPool<T> {
fn new<F>(factory: F) -> Self
where
F: Fn(&mut App) -> Entity<T> + 'static,
{
Self {
available: Vec::new(),
in_use: Vec::new(),
factory: Box::new(factory),
}
}
fn acquire(&mut self, cx: &mut App) -> Entity<T> {
let entity = if let Some(entity) = self.available.pop() {
entity
} else {
(self.factory)(cx)
};
self.in_use.push(entity.downgrade());
entity
}
fn release(&mut self, entity: Entity<T>, cx: &mut App) {
// Reset entity state if needed
entity.update(cx, |state, cx| {
// Reset logic here
cx.notify();
});
self.available.push(entity);
self.cleanup_in_use();
}
fn cleanup_in_use(&mut self) {
self.in_use.retain(|weak| weak.upgrade().is_some());
}
fn pool_size(&self) -> (usize, usize) {
(self.available.len(), self.in_use.len())
}
}These advanced patterns provide powerful abstractions for managing complex entity scenarios while maintaining code quality and performance.
Entity API Reference
Contents: Entity Types · Entity Creation · Entity Operations · Context Methods · Async Operations · Entity Lifecycle · EntityId · Error Handling · Type Conversions
Entity Types
Entity<T>
A strong reference to state of type T.
Methods:
entity_id()→EntityId- Returns unique identifierdowngrade()→WeakEntity<T>- Creates weak referenceread(cx)→&T- Immutable access to stateread_with(cx, |state, cx| ...)→R- Read with closure, returns closure resultupdate(cx, |state, cx| ...)→R- Mutable update withContext<T>, returns closure resultupdate_in(cx, |state, window, cx| ...)→R- Update withWindowaccess (requiresAsyncWindowContextorVisualTestContext)
Important Notes:
- Trying to update an entity while it's already being updated will panic
- Within closures, use the inner
cxprovided to avoid multiple borrow issues - With async contexts, return values are wrapped in
anyhow::Result
WeakEntity<T>
A weak reference to state of type T.
Methods:
upgrade()→Option<Entity<T>>- Convert to strong reference if still aliveread_with(cx, |state, cx| ...)→Result<R>- Read if entity existsupdate(cx, |state, cx| ...)→Result<R>- Update if entity existsupdate_in(cx, |state, window, cx| ...)→Result<R>- Update with window if entity exists
Use Cases:
- Avoid circular dependencies between entities
- Store references in closures/callbacks without preventing cleanup
- Optional relationships between components
Important: All operations return Result since the entity may no longer exist.
AnyEntity
Dynamically-typed entity handle for storing entities of different types.
AnyWeakEntity
Dynamically-typed weak entity handle.
Entity Creation
cx.new()
Create new entity with initial state.
let entity = cx.new(|cx| MyState {
count: 0,
name: "Default".to_string(),
});Parameters:
cx: &mut Appor other context type- Closure receiving
&mut Context<T>returning initial stateT
Returns: Entity<T>
Entity Operations
Reading State
read()
Direct read-only access to state.
let count = my_entity.read(cx).count;Use when: Simple field access, no context operations needed.
read_with()
Read with context access in closure.
let count = my_entity.read_with(cx, |state, cx| {
// Can access both state and context
state.count
});
// Return multiple values
let (count, theme) = my_entity.read_with(cx, |state, cx| {
(state.count, cx.theme().clone())
});Use when: Need context operations, multiple return values, complex logic.
Updating State
update()
Mutable update with Context<T>.
my_entity.update(cx, |state, cx| {
state.count += 1;
cx.notify(); // Trigger re-render
});Available Operations:
cx.notify()- Trigger re-rendercx.entity()- Get current entitycx.emit(event)- Emit eventcx.spawn(task)- Spawn async task- Other
Context<T>methods
update_in()
Update with both Window and Context<T> access.
my_entity.update_in(cx, |state, window, cx| {
state.focused = window.is_window_focused();
cx.notify();
});Requires: AsyncWindowContext or VisualTestContext
Use when: Need window-specific operations like focus state, window bounds, etc.
Context Methods for Entities
cx.entity()
Get current entity being updated.
impl MyComponent {
fn some_method(&mut self, cx: &mut Context<Self>) {
let current_entity = cx.entity(); // Entity<MyComponent>
let weak = current_entity.downgrade();
}
}cx.observe()
Observe entity for changes.
cx.observe(&entity, |this, observed_entity, cx| {
// Called when observed_entity.update() calls cx.notify()
println!("Entity changed");
}).detach();Returns: Subscription - Call .detach() to make permanent
cx.subscribe()
Subscribe to events from entity.
cx.subscribe(&entity, |this, emitter, event: &SomeEvent, cx| {
// Called when emitter emits SomeEvent
match event {
SomeEvent::DataChanged => {
cx.notify();
}
}
}).detach();Returns: Subscription - Call .detach() to make permanent
cx.observe_new_entities()
Register callback for new entities of a type.
cx.observe_new_entities::<MyState>(|entity, cx| {
println!("New entity created: {:?}", entity.entity_id());
}).detach();Async Operations
cx.spawn()
Spawn foreground task (UI thread).
cx.spawn(async move |this, cx| {
// `this`: WeakEntity<T>
// `cx`: &mut AsyncApp
let result = some_async_work().await;
// Update entity safely
let _ = this.update(cx, |state, cx| {
state.data = result;
cx.notify();
});
}).detach();Note: Always use weak entity reference in spawned tasks to prevent retain cycles.
cx.background_spawn()
Spawn background task (background thread).
cx.background_spawn(async move {
// Long-running computation
let result = heavy_computation().await;
// Cannot directly update entities here
// Use channels or spawn foreground task to update
}).detach();Entity Lifecycle
Creation
Entities are created via cx.new() and immediately registered in the app.
Reference Counting
Entity<T>is a strong reference (increases reference count)WeakEntity<T>is a weak reference (does not increase reference count)- Cloning
Entity<T>increases reference count
Disposal
Entities are automatically disposed when all strong references are dropped.
{
let entity = cx.new(|cx| MyState::default());
// entity exists
} // entity dropped here if no other strong references existMemory Leak Prevention:
- Use
WeakEntityin closures/callbacks - Use
WeakEntityfor parent-child relationships - Avoid circular strong references
EntityId
Every entity has a unique identifier.
let id: EntityId = entity.entity_id();
// EntityIds can be compared
if entity1.entity_id() == entity2.entity_id() {
// Same entity
}Use Cases:
- Debugging and logging
- Entity comparison without borrowing
- Hash maps keyed by entity
Error Handling
WeakEntity Operations
All WeakEntity operations return Result:
let weak = entity.downgrade();
// Handle potential failure
match weak.read_with(cx, |state, cx| state.count) {
Ok(count) => println!("Count: {}", count),
Err(e) => eprintln!("Entity no longer exists: {}", e),
}
// Or use Result combinators
let _ = weak.update(cx, |state, cx| {
state.count += 1;
cx.notify();
}).ok(); // Ignore errorsUpdate Panics
Nested updates on the same entity will panic:
// ❌ Will panic
entity.update(cx, |state1, cx| {
entity.update(cx, |state2, cx| {
// Panic: entity already borrowed
});
});Solution: Perform updates sequentially or use different entities.
Type Conversions
Entity → WeakEntity
let entity: Entity<T> = cx.new(|cx| T::default());
let weak: WeakEntity<T> = entity.downgrade();WeakEntity → Entity
let weak: WeakEntity<T> = entity.downgrade();
let strong: Option<Entity<T>> = weak.upgrade();AnyEntity
let any: AnyEntity = entity.into();
let typed: Option<Entity<T>> = any.downcast::<T>();Best Practice Guidelines
Always Use Inner cx
// ✅ Good: Use inner cx
entity.update(cx, |state, inner_cx| {
inner_cx.notify(); // Use inner_cx, not outer cx
});
// ❌ Bad: Use outer cx
entity.update(cx, |state, inner_cx| {
cx.notify(); // Wrong! Multiple borrow error
});Weak References in Closures
// ✅ Good: Weak reference
let weak = cx.entity().downgrade();
callback(move || {
let _ = weak.update(cx, |state, cx| {
cx.notify();
});
});
// ❌ Bad: Strong reference (retain cycle)
let strong = cx.entity();
callback(move || {
strong.update(cx, |state, cx| {
// May never be dropped
cx.notify();
});
});Sequential Updates
// ✅ Good: Sequential updates
entity1.update(cx, |state, cx| { /* ... */ });
entity2.update(cx, |state, cx| { /* ... */ });
// ❌ Bad: Nested updates
entity1.update(cx, |_, cx| {
entity2.update(cx, |_, cx| {
// May panic if entities are related
});
});Focus & Keyboard Navigation
Contents: Overview · Quick Start · Focus Events · Keyboard Navigation · Common Patterns · Best Practices
Overview
GPUI's focus system enables keyboard navigation and focus management.
Key Concepts:
- FocusHandle: Reference to focusable element
- Focus tracking: Current focused element
- Keyboard navigation: Tab/Shift-Tab between elements
- Focus events: on_focus, on_blur
Quick Start
Creating Focus Handles
struct FocusableComponent {
focus_handle: FocusHandle,
}
impl FocusableComponent {
fn new(cx: &mut Context<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
}
}
}Making Elements Focusable
impl Render for FocusableComponent {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::on_enter))
.child("Focusable content")
}
fn on_enter(&mut self, _: &Enter, cx: &mut Context<Self>) {
// Handle Enter key when focused
cx.notify();
}
}Focus Management
impl MyComponent {
fn focus(&mut self, cx: &mut Context<Self>) {
self.focus_handle.focus(cx);
}
fn is_focused(&self, cx: &App) -> bool {
self.focus_handle.is_focused(cx)
}
fn blur(&mut self, cx: &mut Context<Self>) {
cx.blur();
}
}Focus Events
Handling Focus Changes
impl Render for MyInput {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_focused = self.focus_handle.is_focused(cx);
div()
.track_focus(&self.focus_handle)
.on_focus(cx.listener(|this, _event, cx| {
this.on_focus(cx);
}))
.on_blur(cx.listener(|this, _event, cx| {
this.on_blur(cx);
}))
.when(is_focused, |el| {
el.bg(cx.theme().focused_background)
})
.child(self.render_content())
}
}
impl MyInput {
fn on_focus(&mut self, cx: &mut Context<Self>) {
// Handle focus gained
cx.notify();
}
fn on_blur(&mut self, cx: &mut Context<Self>) {
// Handle focus lost
cx.notify();
}
}Keyboard Navigation
Tab Order
Elements with track_focus() automatically participate in Tab navigation.
div()
.child(
input1.track_focus(&focus1) // Tab order: 1
)
.child(
input2.track_focus(&focus2) // Tab order: 2
)
.child(
input3.track_focus(&focus3) // Tab order: 3
)Focus Within Containers
impl Container {
fn focus_first(&mut self, cx: &mut Context<Self>) {
if let Some(first) = self.children.first() {
first.update(cx, |child, cx| {
child.focus_handle.focus(cx);
});
}
}
fn focus_next(&mut self, cx: &mut Context<Self>) {
// Custom focus navigation logic
}
}Common Patterns
1. Auto-focus on Mount
impl MyDialog {
fn new(cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
// Focus when created
focus_handle.focus(cx);
Self { focus_handle }
}
}2. Focus Trap (Modal)
impl Modal {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.on_key_down(cx.listener(|this, event: &KeyDownEvent, cx| {
if event.key == Key::Tab {
// Keep focus within modal
this.focus_next_in_modal(cx);
cx.stop_propagation();
}
}))
.child(self.render_content())
}
}3. Conditional Focus
impl Searchable {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.when(self.search_active, |el| {
el.on_mount(cx.listener(|this, _, cx| {
this.focus_handle.focus(cx);
}))
})
.child(self.search_input())
}
}Best Practices
✅ Track Focus on Interactive Elements
// ✅ Good: Track focus for keyboard interaction
input()
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::on_enter))✅ Provide Visual Focus Indicators
let is_focused = self.focus_handle.is_focused(cx);
div()
.when(is_focused, |el| {
el.border_color(cx.theme().focused_border)
})❌ Don't: Forget to Track Focus
// ❌ Bad: No track_focus, keyboard navigation won't work
div()
.on_action(cx.listener(Self::on_enter))