Expand description
§Elements, components and views
Everything on screen is an element. GPUI gives you three ways to produce elements; choosing the one that matches your state and control needs keeps code simple:
| You want… | Write | It is |
|---|---|---|
| A piece of interface with its own state | A view: a type that implements Render | An entity, kept between frames |
| A reusable piece of interface without state | A component: a type that implements RenderOnce | A value, consumed when rendered |
| Control over layout or painting | An element: a type that implements Element | A value, driven through the frame’s phases |
Most code uses views and components built from div. Use a custom element
when you need control over layout or painting, such as for a text editor or
chart.
§Views
A view is an entity whose type implements Render. Its render method gets
&mut self for reading and updating view state, plus a Context that can
attach handlers back to that entity:
struct Toggle {
on: bool,
}
impl Render for Toggle {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.id("toggle")
.child(if self.on { "On" } else { "Off" })
.on_click(cx.listener(|toggle, _event, _window, cx| {
toggle.on = !toggle.on;
cx.notify();
}))
}
}Use a view when interface state must survive between frames, or when other code
needs to hold a handle to it. A view shows another view by passing its
Entity as a child, as in div().child(self.sidebar.clone()).
§Components
A component is a plain value that describes some elements. Implement
RenderOnce and derive IntoElement, and the component can be used
anywhere an element can:
#[derive(IntoElement)]
struct Badge {
label: SharedString,
count: usize,
}
impl RenderOnce for Badge {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
div()
.flex()
.gap_1()
.child(self.label)
.child(self.count.to_string())
}
}
fn inbox(unread: usize) -> impl IntoElement {
div().child(Badge { label: "Inbox".into(), count: unread })
}RenderOnce::render takes self by value: GPUI builds, renders and drops the
component on every frame. It has no Context of its own, so it cannot
update itself. To report events, give the component callbacks and let the view
that uses it decide what to do:
#[derive(IntoElement)]
struct Button {
id: ElementId,
label: SharedString,
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>,
}
impl RenderOnce for Button {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
div()
.id(self.id)
.px_3()
.py_1()
.rounded_md()
.child(self.label)
.on_click(self.on_click)
}
}A view passes Box::new(cx.listener(...)) as on_click, and the handler
updates the view. This separates a component library’s presentation from its
state: components own their look, while views own the state.
§Elements are values
In every case, render returns values that GPUI consumes during the frame and
then drops. The next frame calls render again and receives new values. Build
those values from current state; keep state in views or entities, not in the
elements being rebuilt.
Because elements are ordinary values, normal Rust tools work for building them.
Child lists can come from iterators, and FluentBuilder
adds when, when_some, and map for conditional styling without breaking a
builder chain:
fn row(name: &str, selected: bool, note: Option<&str>) -> impl IntoElement {
div()
.child(name.to_string())
.when(selected, |row| row.bg(rgb(0xdbeafe)))
.when_some(note, |row, note| row.child(note.to_string()))
}A function must return one concrete type. When branches produce different
element types, convert them with
into_any_element to the type-erased
AnyElement.
§Identity and element state
Some state belongs to an element rather than to your program: whether it is
being pressed, how far it is scrolled, where an animation is. GPUI keeps this
state for you between frames. To find it again on the next frame it needs to
recognize the element, and for that the element needs an ElementId:
div()
.id("messages")
.overflow_y_scroll()
.children((0..100usize).map(|index| {
div().id(("message", index)).child(format!("Message {index}"))
}))An element’s identity is the path of ids from the window down to it, so an id
only has to be unique among the elements that share its nearest identified
ancestor. Ids can be strings, numbers, or a (name, number) pair as above. Two
rules follow:
- Interactive elements need an id. Calling
idon aDivturns it into aStateful<Div>, which unlocks clicks, scrolling, drag and drop, and active styles, because all of those keep state between frames. - Ids must be stable. If a row’s id changes from one frame to the next, its state is lost. Use something that identifies the row’s content, such as a database id, rather than its position, when rows can move.
Your own code can keep state per element too.
Window::use_keyed_state returns an entity that lives as long as an element
with that id keeps being rendered, which is handy for components that need a
little private state.
§Custom elements
Implement Element when the built-in elements cannot express what you need:
a custom layout algorithm, a canvas with thousands of hit-testable shapes, a
text editor. The trait has one method per phase of the frame:
request_layouttells GPUI how big the element wants to be.prepaintruns once bounds are known. Insert hitboxes and compute anything paint will need.paintadds primitives to the scene and registers event listeners.
The associated types carry data from one phase to the next. For one-off custom
drawing, canvas is simpler: it gives you prepaint and paint callbacks
without a new type. The custom drawing guide
and the custom_element example walk through a complete element.