Data display
Avatar, AvatarGroup, Badge, Image, Indicator, List, Table,
Timeline are stateless builders. TableView, DataView, TreeView,
Tabs, TabBar, and Accordion are stateful entities — TableView,
DataView, and TreeView can also bind their data to a
Signal collection.
Avatar
An initials badge (circle by default).
Avatar::new("AL").color(ColorName::Blue)
Avatar::new("GH").color(ColorName::Teal).variant(Variant::Filled).size(Size::Lg)
Methods: new(initials), color (default Gray), variant (default Light),
size (default Md; dims xs 16 … xl 84), radius (omit for a full circle).
AvatarGroup
Overlapping avatars with an optional +N overflow chip. Colors cycle through a
small palette automatically.
AvatarGroup::new()
.avatars(["AL", "GH", "LT", "MK", "PR"])
.limit(3) // show 3, collapse the rest into "+2"
.size(Size::Md)
Methods: new(), avatar(initials), avatars(iter), size, limit(usize).
Badge
A compact status pill.
Badge::new("New").color(ColorName::Teal)
Badge::new("Beta").variant(Variant::Outline).size(Size::Sm)
Methods: new(label), variant (default Light), color (default Blue),
size (default Md).
Indicator
A dot or count overlaid on a child's top-right corner.
Indicator::new(ThemeIcon::new("✉").color(ColorName::Grape)).label("3")
Indicator::new(Avatar::new("AL")).color(ColorName::Green) // plain dot
Methods: new(child), label(impl Into<SharedString>) (count; omit for a dot),
color (default Red), disabled(bool) (hide the indicator).
Image
A themed wrapper around gpui's img() element: remote URIs, embedded asset
paths, and filesystem paths all work, with the theme size/radius vocabulary
and a fallback slot shown while the source is loading or unavailable.
Image::new("https://example.com/cat.png") // or a PathBuf, or an asset path
.width(240.0)
.height(160.0)
.radius(Size::Md)
.fit(ObjectFit::Cover)
.fallback(|| Text::new("no image").dimmed())
| Method | Default | Notes |
|---|---|---|
new(source) |
— | anything Into<gpui::ImageSource>: &str/String (an http(s):// URI, else an embedded-asset path), Path/PathBuf (local file), or raw/decoded image data |
width(f32) / height(f32) |
none | px — give it a size, an unsized image lays out at zero |
radius(Size) |
none | square corners by default, like Mantine |
circle() |
off | clip to a circle (an avatar); pair with equal width/height |
fit(ObjectFit) |
Cover |
Fill / Contain / Cover / ScaleDown / None — re-exported from gpui |
fallback(closure) |
none | Fn() -> impl IntoElement, shown while loading or failed |
gpui also exports a type named gpui::Image — that one is raw
encoded bytes (an Arc<gpui::Image> is itself a valid source here), not
an element. guise::Image is the element.
List
A bulleted or numbered list of text items.
List::new().items(["First", "Second", "Third"])
List::new().ordered(true).item("Step one").item("Step two")
Methods: new(), item(s), items(iter), ordered(bool), size,
spacing(Size), icon(glyph) (custom bullet).
Table
A simple table of string cells; columns size equally.
Table::new()
.with_border(true)
.striped(true)
.highlight_on_hover(true)
.head(["Name", "Role", "Status"])
.row(["Ada", "Admin", "Active"])
.row(["Grace", "Editor", "Active"])
Methods: new(), head(iter), row(iter), striped(bool),
highlight_on_hover(bool), with_border(bool).
For typed rows, sorting, selection, and a virtualized body, use
TableView below.
TableView (entity)
A rich, generic data table over typed rows: sortable headers, click /
⌘-click / ⇧-click selection, drag-resizable columns, and an optionally
virtualized body with a sticky header. Rows are any T: 'static; cells render
through per-column closures, re-invoked every frame so they show live data.
struct User { name: String, age: u32 }
let table = cx.new(|cx| {
TableView::new(cx)
.columns(vec![
Column::new("Name")
.text(|u: &User| u.name.clone().into())
.sortable_by(|a, b| a.name.cmp(&b.name)),
Column::new("Age")
.width(80.0)
.align(Align::End)
.text(|u: &User| u.age.to_string().into())
.sortable_by(|a, b| a.age.cmp(&b.age)),
])
.rows(users) // or .bind_rows(&signal, cx)
.selection_mode(SelectionMode::Multi)
.striped(true)
.with_border(true)
.height(320.0) // fixed height => virtualized body
});
cx.subscribe(&table, |_this, _table, event: &TableViewEvent, _cx| match event {
TableViewEvent::SelectionChanged(rows) => { /* source-row indices */ }
TableViewEvent::Activated(row) => { /* double-click or Enter */ }
TableViewEvent::Sorted(sort) => { /* Some((column, dir)) or None */ }
})
.detach();
| Method | Default | Notes |
|---|---|---|
new(cx) |
— | construct inside cx.new(|cx| ...) |
columns(Vec<Column<T>>) |
empty | the column definitions |
rows(Vec<T>) |
empty | owned snapshot; replace later with set_rows(rows, cx) |
bind_rows(&Signal<Vec<T>>, cx) |
— | live rows: observes the Signal and reads it at render; selection is pruned when rows disappear |
selection_mode(SelectionMode) |
None |
None / Single / Multi |
striped(bool) |
false |
zebra rows, by display position |
highlight_on_hover(bool) |
false |
row hover fill |
with_border(bool) |
false |
rounded outer border |
height(f32) |
auto | fixes the body height; the body becomes a virtualized uniform_list scroll region and the header stays sticky above it |
empty(closure) |
"No data" | (&mut Window, &mut App) -> impl IntoElement, rendered when there are no rows |
Column<T> builders:
| Method | Default | Notes |
|---|---|---|
Column::new(title) |
— | header title |
width(f32) |
flexes | fixed pixel width |
flex(f32) |
1.0 |
grow factor when no fixed width |
min_width(f32) |
60.0 |
floor for both flex sizing and drag-resizing |
align(Align) |
Start |
horizontal alignment of the header and cells |
sortable_by(cmp) |
not sortable | (&T, &T) -> Ordering; a header click cycles asc → desc → unsorted |
text(closure) |
— | (&T) -> SharedString; truncates with an ellipsis when narrow |
cell(closure) |
— | (&T, &mut Window, &mut App) -> impl IntoElement, rebuilt every frame |
Selection follows desktop conventions: click selects, ⌘-click toggles,
⇧-click selects a display-order range; ↑/↓ move the selection (⇧ extends,
and the row scrolls into view when virtualized), Enter activates, Escape
clears (and bubbles only when there is nothing to clear). Sorting stably
reorders display indices — the source Vec<T> is never mutated — and every
index in TableViewEvent refers to the source rows, so selections survive
resorting. Drag the strip at a header's right edge to resize a column; it
becomes fixed-width, honoring min_width. Read state back with selected()
and sort_state().
With height(..) the body is virtualized by gpui's uniform_list,
which requires every row to share one height — keep cells single-line.
DataView (entity)
The collection-binding component — a list or grid bound to a
Signal<Vec<T>>. The view observes the signal and repaints on every write;
filtering and sorting are projections applied at render over the borrowed
data (NSArrayController-style), so the source vector is never copied or
reordered, and selection reports source indices.
let todos = use_state(cx, vec!["Write docs".to_string(), "Ship".to_string()]);
let view = cx.new(|cx| {
DataView::new(cx, &todos)
.item(|todo, _ix, _window, _cx| Text::new(todo.clone()))
.sort_by(|a, b| a.cmp(b))
.selectable()
});
cx.subscribe(&view, |_this, _view, event: &DataViewEvent, _cx| {
let DataViewEvent::Selected(ix) = event; // index into the SOURCE vec
})
.detach();
// Later, from anywhere — the view repaints by itself:
todos.update(cx, |list| list.push("Celebrate".into()));
| Method | Default | Notes |
|---|---|---|
new(cx, &Signal<Vec<T>>) |
— | observes the signal; every set/update repaints |
item(closure) |
— | the item template: Fn(&T, usize, &mut Window, &mut App) -> impl IntoElement, re-invoked every frame with the borrowed item and its source index |
filter(pred) |
none | projection: hides non-matching items, source untouched |
sort_by(cmp) |
none | stable sort of the display order, source untouched |
layout(DataViewLayout) |
List |
List or Grid(cols) — rows of equal-width cells |
gap(Size) |
Sm |
spacing between items (and grid cells) |
empty(closure) |
dimmed "Nothing to show" | shown when the projection yields nothing |
selectable() |
off | single selection: hover highlight, primary-tint selected item |
Emits DataViewEvent::Selected(usize) with the clicked item's source
index (stays valid under any filter/sort). Read back with selected_index().
With selectable() each item gets its own padded, rounded row; without it the
template's element renders bare.
The filter closure gets no cx, so to drive it from live state (a
search box) route the query through a shared Rc<RefCell<String>> that an
observer keeps updated — the gallery's DataView section wires exactly this.
TreeView (entity)
A hierarchical list with expandable branches, single selection, and keyboard
navigation. Nodes are plain TreeNode values (a node with children is a
branch); the view owns expansion and selection and emits TreeViewEvent,
every variant carrying the node id.
let tree = cx.new(|cx| {
TreeView::new(cx)
.nodes(vec![
TreeNode::new("src", "src")
.child(TreeNode::new("main", "main.rs"))
.child(TreeNode::new("lib", "lib.rs")),
TreeNode::new("readme", "README.md"),
])
.expand("src")
});
cx.subscribe(&tree, |_this, _tree, event: &TreeViewEvent, _cx| match event {
TreeViewEvent::Selected(id) => { /* selection moved */ }
TreeViewEvent::Toggled(id, open) => { /* branch expanded/collapsed */ }
TreeViewEvent::Activated(id) => { /* Enter or double-click */ }
})
.detach();
| Method | Default | Notes |
|---|---|---|
new(cx) |
— | construct inside cx.new(|cx| ...) |
nodes(Vec<TreeNode>) |
empty | the tree data |
bind_nodes(&Signal<Vec<TreeNode>>, cx) |
— | live data from a Signal; expansion and selection survive updates (keyed by node id) |
expand(id) / collapse(id) |
all collapsed | per-branch initial expansion |
default_expanded(bool) |
false |
start with every branch expanded (also applies to nodes assigned later) |
TreeNode builders: new(id, label), icon(IconName) (branches fall back to
IconName::Menu, leaves to IconName::Dot), child(node), children(iter);
is_leaf() reads back. Ids ride on every event — keep them unique.
Clicking a row selects it (a branch click also toggles it); double-click or
Enter activates. With focus, ↑/↓ walk the visible rows, → expands a collapsed
branch or steps into an expanded one, ← collapses or steps to the parent. Rows
indent per depth, the chevron rotates when expanded, and the selected row gets
a primary-tint background. Read back with selected_id() and expanded_ids()
(sorted for determinism).
Timeline
A vertical sequence of events with bullets and connectors. Items up to and
including active are highlighted.
Timeline::new()
.active(1)
.item_desc("Created", "Project initialized")
.item_desc("Building", "Compiling sources")
.item("Deploy")
Methods: new(), item(title), item_desc(title, description),
active(usize), color (default Blue).
Tabs (entity)
A tab bar with switchable panels. Panel content is a builder closure, re-invoked each render so panels show live data.
let tabs = cx.new(|cx| {
Tabs::new(cx)
.tab("Overview", |_w, _cx| Text::new("Overview panel").dimmed())
.tab("Members", |_w, _cx| Text::new("Members panel").dimmed())
.active(0)
});
Methods: new(cx), tab(label, |window, app| content), active(usize). Read
with active_index().
For document-style tabs (close buttons, an add button, no panels), use
TabBar below.
TabBar (entity)
A document-style tab strip: a horizontally scrollable row of labeled tabs,
each with a close button (visible while hovered or active), plus an optional
trailing + button. The active tab gets the surface background; inactive tabs
are dimmed. It renders no panels — pair it with your own content keyed off
active_index().
let bar = cx.new(|cx| TabBar::new(cx).tabs(["main.rs", "lib.rs"]).active(0));
cx.subscribe(&bar, |_this, bar, event: &TabBarEvent, cx| match event {
TabBarEvent::Close(i) => {
let i = *i;
bar.update(cx, |b, cx| b.remove_tab(i, cx));
}
TabBarEvent::Add => bar.update(cx, |b, cx| b.add_tab("untitled", cx)),
TabBarEvent::Select(_) => {}
})
.detach();
| Method | Default | Notes |
|---|---|---|
new(cx) |
— | construct inside cx.new(|cx| ...) |
tabs(iter) |
empty | tab labels, any Into<SharedString> items |
active(usize) |
0 |
initially active index |
with_add_button(bool) |
true |
trailing + button |
Runtime: active_index(), len(), is_empty() read; add_tab(label, cx)
appends and activates, remove_tab(index, cx) drops a tab keeping the
selection on the same document where possible, set_tabs(vec, cx) replaces
everything (clamping the active index), set_active(index, cx) switches
programmatically. None of these emit events — events only report user
interaction:
pub enum TabBarEvent {
Select(usize), // a tab was clicked; the bar has already switched to it
Close(usize), // a close button was clicked — the tab is NOT removed
Add, // the trailing + button
}
Closing is a request: the bar never removes the tab itself, so
the parent can intercept (an unsaved-changes prompt) and then call
remove_tab. Close clicks never double as selects.
Accordion (entity)
Collapsible sections. Single-open by default; multiple(true) allows many.
let acc = cx.new(|cx| {
Accordion::new(cx)
.item("What is guise?", |_w, _cx| Text::new("A component library for gpui."))
.item("Is it themeable?", |_w, _cx| Text::new("Yes — light/dark + the full palette."))
.default_open(0)
});
Methods: new(cx), item(label, |window, app| content), multiple(bool),
default_open(usize).
Panel content closures take
(&mut Window, &mut App)and return anyIntoElement. They run every frame, so keep them cheap and side-effect-free.