Component model
guise components come in two flavors, chosen to match how each one behaves in
gpui's retained-mode renderer.
1. Stateless builders (RenderOnce)
Most components are RenderOnce builder structs with #[derive(IntoElement)].
You construct one, chain configuration methods, and hand it to .child(...).
The parent view owns any state.
Button::new("save", "Save")
.variant(Variant::Filled)
.color(ColorName::Blue)
.size(Size::Md)
This is the same pattern zed's own ui crate uses. Builders are cheap values
— create them fresh each render.
Event handlers compose with cx.listener
Click/change handlers take Fn(&ClickEvent, &mut Window, &mut App). Inside a
view, cx.listener(...) produces exactly that signature while giving you
&mut Self, so controlled components drive your view's state directly:
Checkbox::new("agree")
.checked(self.agree)
.on_change(cx.listener(|this, _ev, _window, cx| {
this.agree = !this.agree;
cx.notify();
}))
"Controlled" means the component renders from a value you pass in (checked,
active, value) and reports changes through a handler — the value lives in
your view, exactly like React's controlled inputs.
2. Stateful entities (Render + events)
Components that own intrinsic, frame-to-frame state are gpui entities instead.
These are: TextInput, Select, SegmentedControl, Tabs, Accordion,
Pagination, Menu, MenuBar. You create them with cx.new(...), store the Entity,
and add it as a child:
struct MyView {
name: Entity<TextInput>,
}
impl MyView {
fn new(cx: &mut Context<Self>) -> Self {
let name = cx.new(|cx| TextInput::new(cx).label("Name").placeholder("Ada"));
MyView { name }
}
}
impl Render for MyView {
fn render(&mut self, _w: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.name.clone() // Entity<V: Render> is itself an element
}
}
Entities emit typed events (TextInputEvent, SelectEvent, …). Subscribe with
cx.subscribe(&entity, ...) to react to them, or just read the entity's state
(self.name.read(cx).text()).
Shared conventions
Almost every component supports the same vocabulary:
Variant—Filled,Light,Outline,Subtle,Default,Transparent,White. Resolved against(color, variant)byguise::surface.ColorName— a named palette color (defaults toBlue, or the relevant neutral). Filled components pick a readable foreground automatically.Size—Xs..Xlcontrols height, padding, and font size together.radius— falls back to the theme'sdefault_radiuswhen unset.disabled— dims the control and drops its handler.
Everything visual is resolved from the theme at render time, so all components restyle automatically when you switch light/dark or change the palette.
Components report themselves
Every component's render ends by tagging its root element:
element
.probe("Button")
.attr("variant", self.variant.label())
.attr("size", self.size.label())
That is what puts it in the DevTools Elements tree, with its real style, bounds and source location. Do the same in your own components and they appear beside the library's. It costs one boolean check per element per frame while the inspector is closed, and allocates nothing in that state — so there is no reason to leave it out.
The Variant system
guise::surface(theme, color, variant) returns a Surface { bg, bg_hover, fg, border }. This is the shared resolver behind Button, Badge, Alert,
ActionIcon, Avatar, Chip, and more — so a (color, variant) pair looks
identical across components.
use guise::{surface, Variant};
let s = surface(theme(cx), ColorName::Teal, Variant::Light);
div().bg(s.bg).text_color(s.fg)
| Variant | Background | Foreground | Border |
|---|---|---|---|
Filled |
solid color | contrasting | — |
Light |
tinted | colored | — |
Outline |
transparent | colored | colored |
Subtle |
transparent (fills on hover) | colored | — |
Default |
surface | text | border |
Transparent |
none | colored | — |
White |
white | colored | — |