GPUI Patterns
Essential patterns for building UI with GPUI in Script Kit.
Quick Reference (Things That Break Most Often)
- Layout chain order: Layout (
flex*) → Sizing (w/h) → Spacing (px/gap) → Visual (bg/border) - Lists:
uniform_list(fixed height 52px) +UniformListScrollHandle - Theme colors: use
theme.colors.*(neverrgb(0x...)) - Focus colors: use
theme.get_colors(is_focused); re-render on focus change - State updates: after render-affecting changes, must
cx.notify() - Keyboard: use
cx.listener(); coalesce rapid events (20ms) - Arrow keys: match both
"up"|"arrowup","down"|"arrowdown", etc.
Keyboard Key Names (CRITICAL)
GPUI often sends short arrow keys on macOS. Always match both:
match key.as_str() {
"up" | "arrowup" => self.move_up(),
"down" | "arrowdown" => self.move_down(),
"left" | "arrowleft" => self.move_left(),
"right" | "arrowright" => self.move_right(),
_ => {}
}
Also handle: "enter"|"Enter", "escape"|"Escape", "tab"|"Tab"
Layout System
Chain in order: layout → sizing → spacing → visual → children.
div().flex().flex_row().items_center().gap_2();
div().flex().flex_col().w_full();
div().flex().items_center().justify_center();
div().flex_1(); // fill remaining space
Conditional rendering:
div().when(is_selected, |d| d.bg(selected)).when_some(desc, |d, s| d.child(s));
List Virtualization
Use uniform_list with fixed-height rows (~52px):
uniform_list("script-list", filtered.len(), cx.processor(|this, range, _w, _cx| {
this.render_list_items(range)
}))
.h_full()
.track_scroll(&self.list_scroll_handle);
Scroll to item:
self.list_scroll_handle.scroll_to_item(selected_index, ScrollStrategy::Nearest);
Theme System
let colors = &self.theme.colors;
div().bg(rgb(colors.background.main)).border_color(rgb(colors.ui.border));
Focus-aware:
- compute
is_focused = self.focus_handle.is_focused(window) - if changed: update state +
cx.notify() - use
let colors = self.theme.get_colors(is_focused);
For closures: extract copyable structs like colors.list_item_colors().
Events + Focus
let focus_handle = cx.focus_handle();
focus_handle.focus(window);
window.on_key_down(cx.listener(|this, e: &KeyDownEvent, window, cx| {
let key = e.key.as_ref().map(|k| k.as_str()).unwrap_or("");
match key {
"up"|"arrowup" => this.move_up(cx),
"escape"|"Escape" => this.cancel(cx),
_ => {}
}
}));
State Management
After any state mutation affecting rendering: cx.notify()
Shared state: Arc<Mutex<T>> or channels; for async, use mpsc sender → UI receiver.
References
- Anti-Patterns - Common mistakes that cause bugs
- Smart Pointers - Arc, Rc, Mutex patterns
- Window Management - Multi-monitor, floating panels
- Scroll Performance - Rapid-key coalescing
- Testing Patterns - GPUI test organization