# GPUI Kit
> A comprehensive Rust framework for building fantastic, high-performance desktop apps with GPUI.
---
# Style
Source: /docs/style
GPUI styles an [Element](./element) where it is built. The `Styled` trait supplies chainable methods for layout, spacing, color, borders, and text. Many names deliberately correspond to Tailwind CSS utilities: `flex items-center gap-2 px-3` becomes `.flex().items_center().gap_2().px_3()` in Rust. This is a useful way to read and write GPUI layouts, but the values are typed Rust values rather than CSS classes.
```rust
use gpui_kit::*;
use gpui_kit::component::ActiveTheme as _;
div()
.flex()
.items_center()
.gap_2()
.px_3()
.py_2()
.bg(cx.theme().background)
.text_color(cx.theme().foreground)
.child("Search results")
```
The builder consumes and returns an element on each call. Rendering can build a fresh tree from current state; persistent application state belongs in an [`Entity`](./entity) or keyed element state. A style chain describes this frame's presentation, not a stylesheet or a retained component instance. See [RenderOnce](./render-once) for frame-local component construction.
## Build a first layout
Start with the region that owns the available space, then decide which child has a fixed width and which child can grow. Replace `examples/hello_world/src/main.rs` with this complete program, then run `cargo run -p hello_world` from the repository root:
```rust
use gpui_kit::*;
use gpui_kit::component::{h_flex, v_flex, ActiveTheme as _};
use gpui_kit::prelude::FluentBuilder as _;
struct StyleExample;
impl Render for StyleExample {
fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
let compact = window.viewport_size().width < px(600.);
let layout = if compact { v_flex() } else { h_flex() };
let documents = (1..=60).map(|number| {
div().p_2().child(format!("Document {number:02}"))
});
layout
.items_stretch()
.size_full()
.bg(cx.theme().background)
.text_color(cx.theme().foreground)
.child(
v_flex()
.when(!compact, |nav| nav.w_64().flex_shrink_0())
.p_3()
.gap_2()
.child("Navigation")
.child("Overview · Documents"),
)
.child(
v_flex()
.flex_1()
.min_w_0()
.min_h_0()
.child(h_flex().p_3().child("Documents"))
.child(
v_flex()
.id("document-list")
.flex_1()
.min_h_0()
.overflow_y_scroll()
.child(v_flex().p_3().gap_2().children(documents)),
),
)
}
}
fn main() {
gpui_kit::application().run(|cx| {
gpui_kit::init(cx);
gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| {
cx.new(|_| StyleExample)
})
.expect("failed to open window");
});
}
```
In a wide window, navigation occupies a fixed `w_64()` rail and the document pane fills the rest. Narrow the content area below 600 logical pixels: the same navigation moves above the document pane, while the document list remains independently scrollable. Scroll to `Document 60`, then resize the window in both directions. The header stays in place while the list scrolls. This threshold is an explicit Rust condition evaluated when the view renders, not a Tailwind responsive prefix. The navigation in this small exercise is illustrative text; a real application should use reachable navigation controls.
`h_flex()` makes a row and centers its children on the cross axis. `.items_stretch()` overrides that default so both panes occupy the row's height. `v_flex()` makes a column whose children stretch across its width. The fixed navigation pane does not shrink in the wide layout; the document pane takes the remaining width. The scroll area takes the remaining height below the header. The window-sized root gives `.size_full()` a resolved height; the `min_h_0()` calls let its flexible descendants shrink into a scroll viewport.
### Decide where each size belongs
| Need | Put it on | Why |
| --- | --- | --- |
| Space between siblings | The parent with `.gap_3()` | Gap separates children without adding padding at the outer edge. |
| Space inside a surface | The surface with `.p_3()` | Padding moves its content inward and participates in its layout size. |
| A fixed rail beside flexible content | Rail `.w_64().flex_shrink_0()`; content `.flex_1().min_w_0()` | The rail keeps its width while the content may shrink below its natural text width. |
| A header above scrolling content | Column with a height; scroll child `.flex_1().min_h_0()` | The child can shrink into the available height, creating a real scroll viewport. |
| Half the parent width | `.w(relative(0.5))` on the child | The fraction resolves against the relevant parent dimension during layout. |
`w_full()` and `h_full()` mean the full *available* dimension. A percentage height still needs a definite height upstream. Min and max sizes constrain the result; they do not give an otherwise unbounded scroll area a viewport. For a long single-line label in a row, combine `.flex_1().min_w_0().truncate()` on the label container. `.truncate()` only changes text overflow; it cannot force an inflexible sibling to give up width.
### Choose clipping, scrolling, or positioning
`.overflow_hidden()` clips content; it does not make that content scrollable. On a stateful element, `.overflow_y_scroll()` creates vertical scrolling once the element has a bounded height. GPUI Kit's `.overflow_y_scrollbar()` adds a visible scrollbar and wraps the original element as its scroll area; it is an extension from `ScrollableElement`, not a `Styled` method. Keep one owner for each scroll region, and put content padding inside that region if its scrollbar should sit at the pane edge. See [Coding Guides](./coding-guides) for scroll ownership and measurement.
Normal flex children consume layout space. Use `.relative()` on a container and `.absolute().top_0().right_0()` on a badge when the badge should overlay content without consuming a row or column slot. Offset setters position an absolute child; they do not make an ordinary flex child absolute. Later siblings normally paint over earlier siblings; general `Styled` has no `z_index(...)` method.
### Theme and scale
Use semantic colors and radius from `cx.theme()` for application surfaces. GPUI Kit components already apply their normal theme appearance; style their instances for local layout or an intentional refinement. The named spacing and size helpers are rem based: `_1` is `0.25rem`, `_2` is `0.5rem`, `_3` is `0.75rem`, and `_4` is `1rem`. In a GPUI Kit `Root`, the active theme's base font size sets the window rem size, so a font size or zoom change also changes rem based geometry. Use typed setters such as `.gap(rems(0.625))` when the scale has no suitable step; reserve `px(...)` for a dimension that truly needs pixels. See [Geometry](./geometry) for length types and [Fonts](./fonts) for the theme's rem setup.
### Troubleshoot the result
| Symptom | Check |
| --- | --- |
| The rail does not move above the documents when the window narrows | Resize the drawable content area below 600 logical pixels. The condition reads `window.viewport_size().width` during `render`; changing only display scale does not cross this logical-pixel threshold. |
| `Document 60` cannot be reached | Place the pointer over the document list and scroll there. Keep `.id("document-list").overflow_y_scroll()` on the bounded list region, with `.flex_1().min_h_0()` on it and its containing column. |
| The header moves when scrolling | Ensure the header is a sibling of the list viewport, not a child inside the scrolling element. |
| A pane header disappears at the top | `h_flex()` centers children by default; stretch the row's children or give that pane full height. |
| A title overflows instead of truncating | Release the flexible child's minimum width with `.min_w_0()` and bound the text width. |
| A list grows past the window instead of scrolling | Give its ancestors a resolved height, let the flexible child shrink with `.min_h_0()`, and put scrolling on the intended viewport. |
| A scrollbar sits inside the pane edge | Check which element owns scrolling and whether padding wraps the scroll owner. |
| Layout changes after theme zoom | Recheck rem based dimensions and any cached measurements that assumed the old rem size. |
## Common `Styled` methods
GPUI uses underscores where Tailwind uses hyphens. Where a matching concept exists, the first column links to its official Tailwind CSS reference in a new tab. The names are GPUI methods; call them on a `Styled` value, such as `div().gap_2()`. These tables cover the distinct `Styled` operations and the generic setters generated by its macros. Numeric variants follow the families described below, rather than occupying thousands of near-identical rows. Linked pages explain the corresponding styling concept; they do not imply identical behavior in GPUI and a browser.
### Display and visibility
| Method | Description |
| --- | --- |
| block | Use block layout. |
| flex | Use Flexbox layout. |
| grid | Use Grid layout. |
| hidden | Remove the element from layout and painting. |
| invisible | Keep its layout space but do not paint it. |
| visible | Restore painting while retaining the element's layout. |
### Flexbox and Grid
| Method | Description |
| --- | --- |
| flex_row | Place flex children along a row. |
| flex_col | Place flex children along a column. |
| flex_wrap | Allow flex children to wrap. |
| flex_nowrap | Keep flex children on one line. |
| items_start | Align children to the start of the cross axis. |
| items_center | Center children on the cross axis. |
| items_stretch | Stretch children along the cross axis. |
| self_center | Center this child on its parent's cross axis. |
| self_stretch | Stretch this child on its parent's cross axis. |
| justify_center | Center children on the main axis. |
| justify_between | Put free space between children on the main axis. |
| content_between | Distribute wrapped lines along the cross axis. |
| flex_basis | Set a typed initial main axis size. |
| flex_1 | Grow and shrink with a zero flex basis. |
| flex_auto | Grow and shrink from the item's automatic basis. |
| flex_grow_1 | Allow a flex child to grow. |
| flex_shrink_0 | Prevent a flex child from shrinking. |
| grid_cols | Set a numbered column template. |
| grid_rows | Set a numbered row template. |
| col_span | Span a specified number of grid columns. |
| row_span | Span a specified number of grid rows. |
| flex_row_reverse | Reverse row order. |
| flex_col_reverse | Reverse column order. |
| flex_wrap_reverse | Wrap flex lines in reverse order. |
| items_end | Align children to the cross-axis end. |
| items_baseline | Align children's text baselines. |
| self_start | Align this item to the cross-axis start. |
| self_end | Align this item to the cross-axis end. |
| self_flex_start | Align this item to flex start. |
| self_flex_end | Align this item to flex end. |
| self_baseline | Align this item's text baseline. |
| justify_start | Pack children at the main-axis start. |
| justify_end | Pack children at the main-axis end. |
| justify_around | Distribute space around children. |
| justify_evenly | Distribute equal spaces along the main axis. |
| content_normal | Use the default cross-axis line packing. |
| content_start | Pack wrapped lines at cross-axis start. |
| content_center | Center wrapped lines on the cross axis. |
| content_end | Pack wrapped lines at cross-axis end. |
| content_around | Distribute space around wrapped lines. |
| content_evenly | Distribute equal space between wrapped lines. |
| content_stretch | Stretch wrapped lines along the cross axis. |
| flex_initial | Use an automatic basis and shrink without growing. |
| flex_none | Prevent both flex growth and shrinkage. |
| flex_grow | Set a numeric flex growth factor. |
| flex_grow_0 | Prevent flex growth. |
| flex_shrink | Set a numeric flex shrink factor. |
| flex_shrink_1 | Allow flex shrinkage. |
| grid_cols_min_content | Create columns with min-content minimums. |
| grid_cols_max_content | Create columns with max-content limits. |
| grid_rows_min_content | Create rows with min-content minimums. |
| grid_rows_max_content | Create rows with max-content limits. |
| col_start | Set the starting grid column line. |
| col_start_auto | Use automatic column start placement. |
| col_end | Set the ending grid column line. |
| col_end_auto | Use automatic column end placement. |
| col_span_full | Span the full grid column range. |
| row_start | Set the starting grid row line. |
| row_start_auto | Use automatic row start placement. |
| row_end | Set the ending grid row line. |
| row_end_auto | Use automatic row end placement. |
| row_span_full | Span the full grid row range. |
### Space and size
| Method | Description |
| --- | --- |
| gap_2 | Set row and column gaps to `0.5rem`. |
| gap | Set a typed gap on both axes. |
| gap_x_2 | Set the column gap to `0.5rem`. |
| gap_y_2 | Set the row gap to `0.5rem`. |
| p_4 | Set padding on all sides to `1rem`. |
| p | Set typed padding on all sides. |
| px_3 | Set horizontal padding to `0.75rem`. |
| py_2 | Set vertical padding to `0.5rem`. |
| mt_4 | Set top margin to `1rem`. |
| m | Set typed margins on all sides. |
| m_auto | Set automatic margins on all sides. |
| w | Set a typed width. |
| w_full | Fill the available width. |
| h | Set a typed height. |
| h_full | Fill the available height. |
| min_w | Set a typed minimum width. |
| min_w_0 | Permit width to shrink to zero. |
| max_w | Set a typed maximum width. |
| size | Set typed width and height together. |
| size_4 | Set width and height to `1rem`. |
| aspect_square | Keep a 1:1 width to height ratio. |
| mt | Set a typed top margin. |
| mb | Set a typed bottom margin. |
| mx | Set typed horizontal margins. |
| my | Set typed vertical margins. |
| ml | Set a typed left margin. |
| mr | Set a typed right margin. |
| px | Set typed horizontal padding. |
| py | Set typed vertical padding. |
| pt | Set a typed top padding. |
| pb | Set a typed bottom padding. |
| pl | Set a typed left padding. |
| pr | Set a typed right padding. |
| gap_x | Set a typed column gap. |
| gap_y | Set a typed row gap. |
| min_h | Set a typed minimum height. |
| max_h | Set a typed maximum height. |
| aspect_ratio | Set a numeric width to height ratio. |
### Position and overflow
| Method | Description |
| --- | --- |
| relative | Keep normal layout placement and establish a positioned ancestor. |
| absolute | Position relative to an ancestor using inset offsets. |
| inset | Set a typed offset on all four sides. |
| inset_0 | Set top, right, bottom, and left offsets to zero. |
| top | Set a typed top offset. |
| top_0 | Set the top offset to zero. |
| overflow_hidden | Clip overflowing content on both axes. |
| overflow_x_hidden | Clip horizontal overflow only. |
| overflow_y_hidden | Clip vertical overflow only. |
| bottom | Set a typed bottom offset. |
| left | Set a typed left offset. |
| right | Set a typed right offset. |
| scrollbar_width | Reserve a typed scrollbar width for scrolling layout. |
### Color and borders
| Method | Description |
| --- | --- |
| bg | Set a typed background fill. |
| text_color | Set a typed foreground color. |
| border_1 | Set a one pixel border on all sides. |
| border_t_1 | Set a one pixel top border. |
| border_color | Set a typed border color. |
| border_dashed | Draw borders with a dashed style. |
| rounded | Set a typed corner radius. |
| rounded_lg | Use the named large corner radius. |
| rounded_full | Round corners as far as the size permits. |
| border | Set a typed border width on all sides. |
| border_t | Set a typed top border width. |
| border_b | Set a typed bottom border width. |
| border_l | Set a typed left border width. |
| border_r | Set a typed right border width. |
| border_x | Set typed left and right border widths. |
| border_y | Set typed top and bottom border widths. |
| rounded_t | Set typed radii on the top corners. |
| rounded_b | Set typed radii on the bottom corners. |
| rounded_l | Set typed radii on the left corners. |
| rounded_r | Set typed radii on the right corners. |
| rounded_tl | Set a typed top-left radius. |
| rounded_tr | Set a typed top-right radius. |
| rounded_bl | Set a typed bottom-left radius. |
| rounded_br | Set a typed bottom-right radius. |
### Typography
| Method | Description |
| --- | --- |
| font_family | Set a font family by name. |
| font_weight | Set a typed font weight. |
| italic | Use italic text. |
| text_size | Set a typed font size. |
| text_xs | Use the extra small text size. |
| text_sm | Use the small text size. |
| text_lg | Use the large text size. |
| text_left | Align text to the left. |
| text_center | Center text within its line. |
| text_right | Align text to the right. |
| line_height | Set a typed line height. |
| whitespace_normal | Allow normal text wrapping. |
| whitespace_nowrap | Prevent text from wrapping. |
| text_ellipsis | Truncate overflowing text at the end with an ellipsis. |
| truncate | Clip single line text and add an ellipsis. |
| line_clamp | Limit text to a chosen number of lines. |
| text_base | Use the base text size. |
| text_xl | Use the extra large text size. |
| text_2xl | Use the 2× large text size. |
| text_3xl | Use the 3× large text size. |
| text_align | Set a typed text alignment. |
| not_italic | Use upright text. |
| underline | Underline the text. |
| line_through | Strike through the text. |
| text_decoration_none | Remove text decoration. |
| text_decoration_color | Set text decoration color. |
| text_decoration_solid | Use a solid text decoration line. |
| text_decoration_wavy | Use a wavy text decoration line. |
| text_decoration_0 | Set decoration thickness to zero. |
| text_decoration_1 | Set decoration thickness to one pixel. |
| text_decoration_2 | Set decoration thickness to two pixels. |
| text_decoration_4 | Set decoration thickness to four pixels. |
| text_decoration_8 | Set decoration thickness to eight pixels. |
| text_overflow | Set typed text overflow behavior. |
| font_features | Set OpenType font features. |
### Effects and cursor
| Method | Description |
| --- | --- |
| shadow_none | Remove box shadows. |
| shadow_sm | Apply the named small box shadow. |
| shadow_md | Apply the named medium box shadow. |
| opacity | Set opacity with a floating point value. |
| cursor_pointer | Use the pointing hand cursor on hover. |
| cursor_text | Use a text insertion cursor on hover. |
| shadow | Set a typed list of box shadows. |
| shadow_2xs | Apply the named 2× extra small shadow. |
| shadow_xs | Apply the named extra small shadow. |
| shadow_lg | Apply the named large shadow. |
| shadow_xl | Apply the named extra large shadow. |
| shadow_2xl | Apply the named 2× extra large shadow. |
| cursor | Set a typed mouse cursor. |
| cursor_default | Use the default cursor. |
| cursor_move | Use a move cursor. |
| cursor_not_allowed | Use the not-allowed cursor. |
| cursor_context_menu | Use a context-menu cursor. |
| cursor_crosshair | Use a crosshair cursor. |
| cursor_vertical_text | Use a vertical-text cursor. |
| cursor_alias | Use an alias cursor. |
| cursor_copy | Use a copy cursor. |
| cursor_no_drop | Use a no-drop cursor. |
| cursor_grab | Use a grab cursor. |
| cursor_grabbing | Use a grabbing cursor. |
| cursor_ew_resize | Use a horizontal resize cursor. |
| cursor_ns_resize | Use a vertical resize cursor. |
| cursor_nesw_resize | Use a northeast to southwest resize cursor. |
| cursor_nwse_resize | Use a northwest to southeast resize cursor. |
| cursor_col_resize | Use a column resize cursor. |
| cursor_row_resize | Use a row resize cursor. |
| cursor_n_resize | Use an upward resize cursor. |
| cursor_e_resize | Use a rightward resize cursor. |
| cursor_s_resize | Use a downward resize cursor. |
| cursor_w_resize | Use a leftward resize cursor. |
### GPUI-specific methods
These APIs have no direct Tailwind utility. Their names remain unlinked so the table does not suggest a false correspondence.
| Method | Description |
| --- | --- |
| `font` | Replace the text font with a typed GPUI `Font`. |
| `min_size` | Set typed minimum width and height together. |
| `max_size` | Set typed maximum width and height together. |
| `text_bg` | Set the background color of text runs, rather than the element box. |
| `text_ellipsis_start` | Truncate at the start to preserve the end of text. |
| `text_ellipsis_middle` | Truncate in the middle to preserve both ends. |
| `debug` | Draw a debug outline in debug builds. |
| `debug_below` | Draw debug outlines for this element and conforming descendants in debug builds. |
| `style` | Return the mutable `StyleRefinement` used by the element; this is the trait's low-level accessor. |
| `text_style` | Return the mutable text refinement within the element style. |
| `grid_location_mut` | Access the mutable grid placement within a `StyleRefinement`. |
The macros also generate methods for the size (`w`, `h`, `size`, `min_size`, `min_w`, `min_h`, `max_size`, `max_w`, `max_h`), gap (`gap`, `gap_x`, `gap_y`), margin (`m`, `mt`, `mb`, `mx`, `my`, `ml`, `mr`), padding (`p`, `pt`, `pb`, `px`, `py`, `pl`, `pr`), and inset (`inset`, `top`, `bottom`, `left`, `right`) families. For example, `w_64`, `px_3`, and `top_0` are real methods. Their shared numeric suffixes are `0`, `0p5`, `1`, `1p5`, `2`, `2p5`, `3`, `3p5`, every integer from `4` to `12`, then `16`, `20`, `24`, `32`, `40`, `48`, `56`, `64`, `72`, `80`, `96`, `112`, and `128`. They also include `_px`, `_full`, and fractional suffixes such as `_1_2`; only families that accept `auto` generate `_auto`. Non-auto values also have `_neg_` forms, such as `mt_neg_2`. Border sides (`border`, `border_t`, `border_b`, `border_l`, `border_r`, `border_x`, `border_y`) have pixel-width suffixes `0` through `12`, plus `16`, `20`, `24`, and `32`. Rounded sides and corners use `none`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl`, `3xl`, and `full`. Use these mechanically generated names only when the resulting property makes sense for the layout.
Spacing helpers use a rem based scale: `_1` is `0.25rem`, `_2` is `0.5rem`, `_3` is `0.75rem`, and `_4` is `1rem`. For a value outside the named helpers, use a typed setter such as `.gap(rems(0.625))`, `.w(px(240.))`, or `.w(relative(0.5))`. `relative(0.5)` expresses half the available relative size; `px(...)` expresses pixels. The named scale and method set are GPUI's implementation, so check the actual API rather than assuming every Tailwind class exists.
## What a style call changes
Every `Styled` element exposes `fn style(&mut self) -> &mut StyleRefinement`. A call such as `.px_3()` writes the relevant optional padding fields; `.bg(...)` writes the background field. Fields left unset do not replace existing values when refinements are merged. The resolved `Style` holds both layout data and presentation data.
```text
Styled calls → StyleRefinement → resolved Style
├─ layout fields → Taffy → bounds
└─ color, text, shadow, cursor → GPUI paint and interaction
```
In an element's `request_layout` phase, GPUI passes layout fields such as display, size, padding, gap, flex alignment, position, and grid placement, together with child layout IDs, to Taffy. Taffy computes the geometry. GPUI then uses the bounds in `prepaint` and the [Paint](./paint) phase for drawing and hit testing. Taffy does not implement GPUI's text shaping, hover listeners, [Actions](./action), or painting.
`StyleRefinement` itself implements `Styled`, so a state style closure can use the same utility methods. Interaction variants such as `.hover(|style| style.bg(...))` belong to `InteractiveElement`, and need an interactive element. Conditional builder calls are different: `.when(...)` chooses a chain step while this frame is built.
## Fluent composition and trait boundaries
The fluent surface combines several traits. `Styled` supplies style methods. `ParentElement` supplies `.child(...)`. `InteractiveElement` supplies `.id(...)`, returning a `Stateful
` that supports identity dependent methods such as `.overflow_y_scroll()`. `InteractiveElement` also supplies state style refinements such as `.hover(...)`. Every `IntoElement` implements `FluentBuilder`; `IntoElement` alone does not grant styling, children, or interaction. If a method is missing, check the receiver's trait and whether a preceding call changed its type.
| `FluentBuilder` method | Effect |
| --- | --- |
| `map` | Transform the value and optionally change its return type. |
| `when` | Apply a builder step when a boolean is true. |
| `when_else` | Choose between two steps that both return the same builder type. |
| `when_some` | Apply a step and pass the value inside an `Option`. |
| `when_none` | Apply a step when a referenced `Option` is empty. |
```rust
use gpui_kit::*;
use gpui_kit::component::ActiveTheme as _;
div()
.id("result-row")
.px_3()
.py_2()
.when(selected, |row| row.bg(cx.theme().selection))
.when_some(subtitle, |row, text| row.child(text))
.child(title)
```
Conditional closures consume and return the builder. The condition is evaluated during the current render; it is not a subscription. `.map(...)` can return a different type. Use an `Entity` to own state that changes across frames.
For example, `.when(selected, ...)` evaluates `selected` while building this frame; `.hover(|style| ...)` installs a style refinement for pointer hover. A scroll call needs a stateful element, so give the intended scroll owner an `.id(...)` first. An arbitrary component implementing `IntoElement` cannot be assumed to accept `.child(...)` or `.bg(...)`; inspect its own builder API or wrap it in a `div()` that owns those styles.
GPUI Kit adds `StyledExt` for neutral helpers such as `h_flex`, `v_flex`, and `refine_style`, and `ThemeStyled` for theme driven appearance such as `popover_style(cx)`. These are extensions on top of GPUI's `Styled`, not Tailwind utilities. When implementing a custom element, return its `StyleRefinement` from `style()` and apply the resolved style during layout and paint. Implement only the child and interaction traits that the element can actually support.
The correspondence with Tailwind is deliberately scoped: there are no CSS selectors, cascade, responsive prefixes, or promise that every Tailwind utility has a GPUI method. Use GPUI's typed layout helpers for geometry, GPUI Kit theme tokens for product appearance, and explicit element or entity APIs for behavior.
---
# Paint
Source: /docs/paint
GPUI paints after layout and `prepaint`. The resolved `Bounds` tell a custom [`Element`](./element) where it can draw. `paint` records drawing commands for the current frame; it does not establish layout or input geometry. Use `window.paint_quad` for rectangles and borders, existing image and text elements for those media, and `window.paint_path` for freeform shapes. A `canvas` is a convenient way to run prepaint and paint callbacks without implementing the whole `Element` trait.
## Start with one painted triangle
This exercise needs no new crate or dependency. In a local checkout, replace the contents of the existing `examples/hello_world/src/main.rs` with the following code, then run `cargo run -p hello_world` from the repository root. You should see a blue triangle near the upper-left corner of the window. Restore the example file after experimenting if you do not want to keep the change.
```rust
use gpui_kit::*;
struct PaintedTriangle;
impl Render for PaintedTriangle {
fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement {
div().size_full().child(
canvas(
|bounds, _, _| bounds,
|_, bounds, window, _| {
let mut path = PathBuilder::fill();
let x = bounds.origin.x;
let y = bounds.origin.y;
path.move_to(point(x + px(24.), y + px(24.)));
path.line_to(point(x + px(144.), y + px(24.)));
path.line_to(point(x + px(84.), y + px(128.)));
path.close();
if let Ok(path) = path.build() {
window.paint_path(path, rgb(0x3b82f6));
}
},
)
.size_full(),
)
}
}
fn main() {
gpui_kit::application().run(|cx| {
gpui_kit::init(cx);
gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| {
cx.new(|_| PaintedTriangle)
})
.expect("failed to open window");
});
}
```
The parent `div` and child `canvas` both have `.size_full()`, so layout gives the callback a usable rectangle. The first callback runs during `prepaint` and returns the resolved bounds as its state `T`; the second receives that same value during `paint`. Here it builds one filled triangle in window coordinates and passes the resulting `Path` to `paint_path`. `close()` joins the last point back to the first. The `if let Ok` handles possible tessellation failure instead of panicking.
The two `canvas` callbacks are `FnOnce` callbacks for one element pass. Return owned prepared data from the first callback when painting needs it; do not save references to `Window` or `App` for a later frame. The `canvas` itself is a frame-local element, while retained drawing data belongs in an Entity or keyed window state if it must survive another render.
Try changing `px(84.)` to `px(120.)` in the third point, then run again: only the triangle's geometry changes. Replace `PathBuilder::fill()` with `PathBuilder::stroke(px(4.))` to draw its outline instead. The element tree is recreated when GPUI renders a new frame; the path here is also rebuilt during paint. This is suitable for a tiny illustration, but a large or frequently redrawn path may merit a cache after measurement.
The coordinate handoff is the key idea:
```text
layout: canvas bounds = origin (x, y) + size (width, height)
prepaint: pass resolved bounds to paint
paint: local point (24, 24) + origin (x, y) -> window point -> PathBuilder
```
The bounds establish a coordinate system; they do not automatically clip paths to the canvas. If a shape extends past the canvas, apply a content mask or an appropriate clipping style to its container.
## Make drawing respond to a click
Replace `examples/hello_world/src/main.rs` with this complete example and run `cargo run -p hello_world` again. Each left click adds a small blue triangle at the pointer. The triangles remain visible after later clicks because their positions live in the `ClickPainter` Entity, not in a one-frame `canvas` value.
```rust
use gpui_kit::base::ElementExt;
use gpui_kit::*;
struct ClickPainter {
marks: Vec>,
canvas_bounds: Option>,
}
impl ClickPainter {
fn add_mark(
&mut self,
event: &MouseDownEvent,
_: &mut Window,
cx: &mut Context,
) {
if let Some(bounds) = self.canvas_bounds {
self.marks.push(point(
event.position.x - bounds.origin.x,
event.position.y - bounds.origin.y,
));
cx.notify();
}
}
}
impl Render for ClickPainter {
fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement {
let marks = self.marks.clone();
let painter = cx.entity().clone();
div()
.size_full()
.relative()
.on_mouse_down(MouseButton::Left, cx.listener(Self::add_mark))
.on_prepaint(move |bounds, _, cx| {
painter.update(cx, |this, _| {
this.canvas_bounds = Some(bounds);
});
})
.child(
canvas(
move |bounds, _, _| (bounds.origin, marks),
|_, (origin, marks), window, _| {
for mark in marks {
let center = point(origin.x + mark.x, origin.y + mark.y);
let mut path = PathBuilder::fill();
path.move_to(point(center.x, center.y - px(16.)));
path.line_to(point(center.x + px(16.), center.y + px(12.)));
path.line_to(point(center.x - px(16.), center.y + px(12.)));
path.close();
if let Ok(path) = path.build() {
window.paint_path(path, rgb(0x3b82f6));
}
}
},
)
.absolute()
.size_full(),
)
}
}
fn main() {
gpui_kit::application().run(|cx| {
gpui_kit::init(cx);
gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| {
cx.new(|_| ClickPainter {
marks: Vec::new(),
canvas_bounds: None,
})
})
.expect("failed to open window");
});
}
```
The parent `div` receives the mouse event. Its `on_prepaint` hook records its resolved bounds for the next input event without requesting another render. `add_mark` subtracts that origin to retain a canvas-local position, then `cx.notify()` requests a new frame. During the next render, the canvas receives a snapshot of the marks; its paint callback adds the **current** origin and builds a path for each one. Try clicking twice, then resize the window: both marks should remain in place relative to the canvas. If a click does nothing, check that the parent has nonzero size, that `canvas_bounds` was recorded, and that the handler calls `cx.notify()` after changing `marks`.
## Follow a real drawing app
Run the existing [Brush example](https://github.com/MohsenDastaran/uni-kit/blob/main/examples/brush/src/main.rs) from the repository root:
```sh
cargo run -p example-brush
```
Drag inside **Drawing Canvas** to make a stroke. Try **Size**, **Opacity**, a color swatch, **Show Grid**, and **Clear Canvas**. The following steps trace one stroke through the example's input, layout, and paint code.
The frame has a useful order to keep in mind: a mouse handler changes retained `BrushStory` state and calls `cx.notify()`; `render` builds a new element tree; layout resolves sizes and positions; `prepaint` receives bounds; `paint` submits paths for that frame. The stored stroke points survive between frames. The `Path` objects in this example are built again during painting.
### 1. Give the canvas space and collect input
`render_canvas` puts the painting canvas inside a full-size `div`. The parent gets space from the flexible **Drawing Canvas** section. The `div` handles mouse events; the child canvas fills that same area:
```rust
let base_div = div()
.id("canvas")
.size_full()
.bg(theme.background)
.cursor_crosshair()
.relative()
.on_mouse_down(MouseButton::Left, cx.listener(Self::handle_mouse_down))
.on_mouse_move(cx.listener(Self::handle_mouse_move))
.on_mouse_up(MouseButton::Left, cx.listener(Self::handle_mouse_up))
.on_prepaint(move |bounds, _window, cx| {
state_entity.update(cx, |state, _| {
state.canvas_bounds = Some(bounds);
})
});
```
The example adds `canvas(...).absolute().size_full()` as this `div`'s child. A canvas needs a size from its own styles or its parent; otherwise there may be no useful drawing area. The parent's `on_prepaint` saves its resolved `Bounds` for the mouse handlers. Bounds and mouse positions use **window coordinates**, so the canvas origin is generally not `(0, 0)`.
Saving bounds in `on_prepaint` does not call `cx.notify()`: it records geometry needed by later input, without requesting another render during every frame.
### 2. Store points relative to the canvas
On mouse down, `BrushStory::handle_mouse_down` starts a `Stroke`. It subtracts the saved origin before storing the pointer position; mouse move uses the same conversion for subsequent points:
```rust
let local_pos = if let Some(bounds) = self.canvas_bounds {
Point::new(
event.position.x - bounds.origin.x,
event.position.y - bounds.origin.y,
)
} else {
event.position
};
```
`BrushStory` retains completed strokes in `strokes` and the active one in `current_stroke`. The handlers call `cx.notify()` when a stroke changes, so GPUI renders a new frame. A single mouse-down point does not yet form a visible line: `build_stroke_path` requires at least two points. Releasing the mouse adds a stroke with two or more points to the completed list.
### 3. Pass resolved bounds into painting
In `render_canvas`, the canvas's first callback receives its final bounds and returns them with the strokes, active stroke, grid setting, and theme. The second callback receives that value and paints. This excerpt shows the handoff; the [source](https://github.com/MohsenDastaran/uni-kit/blob/main/examples/brush/src/main.rs) also draws the optional grid and active stroke:
```rust
canvas(
move |bounds, _window, _cx| {
(
strokes_for_prepaint,
current_stroke_for_prepaint,
show_grid_for_prepaint,
theme_for_prepaint,
bounds,
)
},
move |_bounds,
(strokes, current_stroke, show_grid, theme, prepaint_bounds),
window,
_cx| {
for stroke in strokes.iter() {
if let Some(path) = BrushStory::build_stroke_path(stroke, &prepaint_bounds) {
window.paint_path(path, stroke.color);
}
}
// The example also paints the grid and current_stroke here.
},
)
.absolute()
.size_full()
```
`prepaint` happens after layout, when bounds are known. `paint_path` submits drawing for this frame; it does not change state or schedule another frame.
### 4. Build the path in window coordinates
`build_stroke_path` converts stored canvas-local points back to window coordinates before tessellating a stroked path:
```rust
let mut builder = PathBuilder::stroke(px(stroke.size));
let first_point = Point::new(
bounds.origin.x + stroke.points[0].x,
bounds.origin.y + stroke.points[0].y,
);
builder.move_to(first_point);
for point in stroke.points.iter().skip(1) {
let abs_point = Point::new(bounds.origin.x + point.x, bounds.origin.y + point.y);
builder.line_to(abs_point);
}
builder.build().ok()
```
Try increasing **Size** and drawing another line: `stroke.size` sets the width of each new stroke, while earlier strokes keep their stored widths. **Opacity** and color are likewise captured when a stroke begins. Toggle **Show Grid** to see another path drawn in the same paint callback, or click **Clear Canvas** to clear the retained strokes and request a frame. If layout moves or resizes the canvas, each paint uses its current bounds origin to place the stored points.
If a stroke does not appear, check the failure point in order:
| Symptom | Check |
| --- | --- |
| Nothing appears, including the grid | Confirm the parent and canvas have nonzero layout size; a path does not allocate its own space. |
| A click leaves no mark | A single point is not a line in this example; drag far enough for a second sampled point. |
| The stroke is offset after moving the canvas | Subtract the canvas origin on input and add the **current** origin when painting. |
| Stored points change but the image does not | Make sure the state owner calls `cx.notify()` after a meaningful change. |
| Some geometry silently disappears | Inspect the `PathBuilder::build()` result; this example converts errors to `None`. |
The example treats a failed build as “draw nothing”; report or retain errors when geometry comes from user data. Input handlers live on the containing `div`; a path alone has no hitbox or accessibility behavior.
## Build a path
`PathBuilder` describes vector geometry and tessellates it into a `Path` when `build()` succeeds. Choose `fill()` for a closed area or `stroke(width)` for a line. Its points use window pixel coordinates, so add the element's resolved origin when your data is local to its bounds.
```rust
use gpui_kit::*;
fn paint_triangle(bounds: Bounds, color: Hsla, window: &mut Window) {
let mut builder = PathBuilder::fill();
builder.move_to(bounds.origin);
builder.line_to(point(bounds.right(), bounds.top()));
builder.line_to(point(bounds.left() + bounds.size.width / 2., bounds.bottom()));
builder.close();
if let Ok(path) = builder.build() {
window.paint_path(path, color);
}
}
```
`move_to`, `line_to`, `curve_to` (quadratic), `cubic_bezier_to`, `arc_to`, `add_polygon`, and `close` describe segments. `translate`, `scale`, and `rotate` transform the path before tessellation. `PathBuilder::stroke(px(1.)).dash_array(&[px(4.), px(2.)])` produces a dashed outline. `build()` returns a `Result`; handle failure rather than assuming arbitrary geometry can always be tessellated. A built `Path` can be cloned and painted in more than one color or frame when its geometry has not changed.
### From SVG paths to GPUI
`PathBuilder` was introduced to GPUI for candlestick chart drawing needs. It uses Lyon's SVG path builder internally, so its segment vocabulary is familiar from SVG. If you know [SVG path commands](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/d), the segment concepts transfer directly:
| SVG path | GPUI builder | Meaning |
| --- | --- | --- |
| `M x y` | `move_to(point)` | Start a subpath. |
| `L x y` | `line_to(point)` | Draw a straight segment. |
| `Q cx cy x y` | `curve_to(end, control)` | Quadratic Bézier. |
| `C c1x c1y c2x c2y x y` | `cubic_bezier_to(end, control_a, control_b)` | Cubic Bézier. |
| `A rx ry rotation large sweep x y` | `arc_to(radii, rotation, large_arc, sweep, end)` | Elliptical arc. |
| `Z` | `close()` | Close the current subpath. |
The geometry model is familiar, but the Rust argument order is not a literal transcription of SVG syntax: `curve_to` and `cubic_bezier_to` take the **end point first**, followed by control points. `x_rotation` is passed as a `Pixels` value whose numeric part represents degrees in the current API. Coordinates are typed `Point`, and `build()` tessellates the path before `paint_path` submits it. This makes porting SVG drawing logic straightforward while keeping GPUI's typed coordinate and error handling rules explicit.
### The GPUI Kit mark in two path notations
The GPUI Kit mark makes the relationship between SVG paths and `PathBuilder` concrete. Its two closed paths paint separately: the outer shape uses the theme foreground and the inner stroke uses the theme blue accent. Switch between the GPUI and SVG source, then compare them with the rendered mark below. The example uses a local 32 × 32 coordinate space; the GPUI code adds the bounds origin to each point. `foreground` and `accent_color` are colors supplied by the caller.
Two filled paths, with foreground and the theme blue accent painted separately.
GPUI’s `PathBuilder` takes full point coordinates; it has no SVG `H` or `V` shorthand and does not parse an SVG `d` string directly. The SVG tab spells out every `L` coordinate to make the correspondence visible. If the source is already an SVG file and no `Path` is needed, render the asset with `svg().path("icons/logo.svg")`. Converting arbitrary SVG path data into a GPUI `Path` requires a separate parser that feeds segments into `PathBuilder`.
## Choose the right phase
| Work | Phase | Reason |
| --- | --- | --- |
| Declare size and child layout nodes | `request_layout` | Taffy needs the style before bounds exist. |
| Prepare geometry shared by hit testing and drawing; insert a `Hitbox` | `prepaint` | Bounds and current frame input geometry are available. |
| Build paint-only geometry if necessary; call `paint_path`, `paint_quad`, or paint prepared children | `paint` | Drawing order is now known; Brush builds its paths here. |
For a small decorative shape, `canvas(prepaint, paint)` is enough. [GPUI Kit's Plot line](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/plot/shape/line.rs) builds a stroke from data points and paints it into the chart. The [input element](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/src/input/base/element.rs) uses paths for selections and text range decorations; it paints blinking carets as quads. Both use `PathBuilder`, but the input must also coordinate text metrics and hit testing.
The scene can clip drawing with `window.with_content_mask`. Clipping, input hitboxes, and [accessibility](./accessibility) are separate contracts: a painted path is not automatically clickable or announced to assistive technology. A chart with point interaction must also establish hitboxes or an equivalent pointer mapping, and a semantic chart needs an accessible representation.
## Avoid unnecessary tessellation
Path tessellation has real cost. Keep a path when its source points and dimensions are unchanged; rebuild it when either changes. Do not cache a path that contains absolute window coordinates across relocation unless the cache also accounts for the new origin. For simple boxes, prefer `paint_quad` or a styled `div()` so GPUI can use its standard painting and interaction machinery.
## Three GPUI Kit examples, three ownership choices
The drawing primitives are the same, but each GPUI Kit feature keeps its work at a different lifetime. The choice follows where the data lives and how often it changes.
### A model-owned gauge: an Entity keeps geometry
A gauge driven by an [Entity](./entity) can keep separate `Option>` values for its background, value arc, and needle. On a value change, clear only the value arc and needle. On an origin change, clear all paths if they contain absolute window coordinates. A `canvas` callback can build missing paths from its bounds during prepaint, then paint them with current theme colors. This keeps geometry invalidation separate from color selection. This pattern fits a view that already owns model subscriptions; avoid calling `cx.notify()` unconditionally from prepaint, because that can schedule an extra render every frame.
### Plot: a value-like element uses keyed window state
This cache depends on a stable [ElementId](./element_id) across frames. [`Line`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/plot/shape/line.rs) is recreated as a value during render. Storing a cache on that value would lose it on the next frame. [`PathCaches::for_paint`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/plot/path_cache.rs) instead uses `window.use_keyed_state` under the plot's current element ID. `Line::paint_cached` hashes the projected points, stroke width, and curve style; `PathCache::get` tessellates only when the key changes. It builds the path relative to zero, then clones and translates cached vertices to this frame's origin, so scrolling does not trigger tessellation, although translation still costs work. Dots remain cheap quads painted at the new origin. This pattern depends on stable element identity and benefits from using the same slot for the same series across frames; reordering series by index causes avoidable cache misses when their shape keys differ.
### Input: a text editor owns the whole Element pipeline
GPUI Kit's [input element](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/src/input/base/element.rs) owns much more than its selection paths. It measures text, tracks wrapping and viewport geometry, inserts a hitbox in prepaint, then paints selection paths, caret quads, and text from prepared layout. Its input handlers and focus behavior depend on the same geometry. This is why a complex editor needs a custom `Element`: text layout, hit testing, input routing, and drawing must agree on one snapshot. Paths for selections and text range decorations are only part of that pipeline.
The progression is useful when choosing an API: use `canvas` for a focused decoration owned by an existing Entity; use keyed window state when a value-like drawing element needs a cache across frames; implement `Element` when layout, text, hit testing, and input must be coordinated directly. See the [Element lifecycle](./element) for the trait methods and the [Event guide](./event) for input propagation.
---
# Fonts
Source: /docs/fonts
This page covers which fonts an application supplies. See [TextSystem](./text-system) for how GPUI resolves, shapes, measures, and paints their glyphs.
## Start here
For a desktop app, start with the theme defaults. If you need a specific look,
choose a family installed on every machine you support, or bundle its font
file. Set the UI and monospace families through `Theme::update`; use
`.font_family(...)` only for a particular element. Check the result with real
Latin, CJK, emoji, and mixed-script content on each target platform. A family
name alone does not guarantee that all those glyphs exist.
For a WebAssembly app, register font files before the first window or text
measurement. The browser's installed font list is not GPUI Web's font
collection. The [WebAssembly setup](#webassembly-choose-a-font-supply-strategy)
below shows the extra choices for browser builds.
## Default fonts
Every app starts with a UI font and a monospace font from the theme:
| Role | Family | Size |
| --- | --- | --- |
| UI text | `.SystemUIFont` | 16px |
| Code / monospace | macOS: `Menlo`, Windows: `Consolas`, Linux: `DejaVu Sans Mono` | 13px |
The editor paints its code in `mono_font_family` at `mono_font_size`. See
[Editor](/component/editor) for details.
Both defaults are checked against the installed fonts when the theme is
applied. A missing monospace default is swapped for an installed alternative,
and when `.SystemUIFont` resolves to one of GPUI's fallback families rather
than the system font itself (Linux desktops without the family GPUI maps it
to), the theme names that family directly so text lookups stay cached. A
family you set yourself is used as-is.
## System fonts
Desktop apps can use **any font installed on the OS** by name — no bundling,
no config. GPUI resolves the family live against the system collection
(CoreText on macOS, DirectWrite on Windows, fontconfig on Linux).
```rust
div().font_family("Segoe UI")
Editor::new(&editor).font_family("JetBrains Mono")
```
Common examples per platform:
- macOS: `SF Pro`, `Helvetica`, `Arial`, `Times New Roman`, `Menlo`, `Monaco`
- Windows: `Segoe UI`, `Arial`, `Consolas`, `Courier New`
- Linux: `Noto Sans`, `DejaVu Sans`, `Liberation Sans`, `DejaVu Sans Mono`
If a requested family cannot load, `TextSystem::resolve_font` tries GPUI's
default font stack. If none loads, layout panics. A family that loads but lacks
a particular glyph is a different problem: glyph fallback may draw that
character in another face. Verify both the family name and the glyphs you
need on each target platform. `Font::fallbacks` controls missing-glyph
fallbacks after the requested family has loaded; it does not make an absent
primary family available.
To inspect names GPUI currently sees, run this after initialization and after
any bundled fonts have been registered:
```rust
let families = cx.text_system().all_font_names();
println!("Available font families: {families:?}");
```
This lists family names, including fonts installed with `add_fonts`, but does
not prove that a face contains every character or requested weight.
## Changing fonts via Theme
Set the app-wide fonts through `Theme::update`, which syncs the base layer and refreshes every window:
```rust
Theme::update(cx, |theme| {
theme.font_family = "Inter".into();
theme.mono_font_family = "JetBrains Mono".into();
theme.font_size = px(18.);
});
```
`font_size` doubles as the application zoom control — `Root` calls
`window.set_rem_size(cx.theme().font_size)`, so [`rem`-based spacing](./geometry)
scales with it. See [Coding Guides](/docs/coding-guides) for details.
## Per-element override
Elements implementing `Styled` accept a font override without touching the theme:
```rust
div()
.font_family("JetBrains Mono")
.text_size(px(15.))
.font_weight(FontWeight::BOLD)
```
These are ordinary [`Styled`](https://docs.rs/gpui-pre/{{gpui_pre_version}}/gpui/trait.Styled.html)
methods, so they compose with the rest of the style chain.
## Bundling custom fonts
Fonts that are not installed on the user's system must be bundled and
registered with the text system **before the first frame**. Put the font file
in your app and call `add_fonts` during application startup, before opening a
window or constructing anything that measures text:
```rust
use std::borrow::Cow;
cx.text_system()
.add_fonts(vec![Cow::Borrowed(
include_bytes!("../fonts/MyFont-Regular.ttf").as_slice(),
)])
.expect("Failed to load fonts");
```
Then reference them by family name as usual:
```rust
Theme::update(cx, |theme| theme.font_family = "MyFont".into());
```
Use the family name stored *inside* the font file, which may differ from its
filename. Register every face you need for predictable regular, bold, and
italic text; a single regular file is not a promise of all styles. Bundling
also makes a desktop app independent of whether that family is installed on
the user's machine. Keep the font's redistribution license with the app.
The [GPUI Kit web gallery](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story-web/src/lib.rs) bundles `Inter Variable`, `JetBrains Mono`, a subset of `Noto Sans SC`, and `IBM Plex Sans` this way. Rust's [`include_bytes!`](https://doc.rust-lang.org/std/macro.include_bytes.html) puts those font bytes into the WebAssembly download. The gallery's CJK subset is about 42 KB, compared with about 1.2 MB for its source font: subset known interface copy to limit initial payload, then plan separately for arbitrary text entered by users.
### Try a bundled font in `hello_world`
The repository already contains `crates/story-web/fonts/Inter-Regular.ttf`; its internal family name is `Inter Variable`. Replace `examples/hello_world/src/main.rs` with this complete example, then run `cargo run -p hello_world` from the repository root. The `include_bytes!` path below is relative to that `main.rs` file. Your own application should place a licensed font in its own assets and adjust the path.
```rust
use std::borrow::Cow;
use gpui_kit::component::theme::Theme;
use gpui_kit::*;
struct FontLab;
impl Render for FontLab {
fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement {
div()
.flex()
.flex_col()
.gap_2()
.p_4()
.child("Theme font: Inter Variable")
.child(
div()
.font_family(".SystemUIFont")
.child("System UI font for comparison"),
)
}
}
fn main() {
application().run(|cx| {
init(cx);
cx.text_system()
.add_fonts(vec![Cow::Borrowed(
include_bytes!("../../../crates/story-web/fonts/Inter-Regular.ttf").as_slice(),
)])
.expect("Failed to load bundled font");
let families = cx.text_system().all_font_names();
assert!(families.iter().any(|family| family == "Inter Variable"));
println!("Registered font: Inter Variable");
Theme::update(cx, |theme| theme.font_family = "Inter Variable".into());
open_window(WindowOptions::default(), cx, |_, cx| cx.new(|_| FontLab))
.expect("Failed to open window");
});
}
```
The terminal should print `Registered font: Inter Variable`, and the window should show two labels. The first inherits the theme's registered font; the second explicitly requests the system UI family. Their appearance may differ by platform. This checks registration and theme selection, not glyph coverage: try another string and the target platforms before relying on a font for user-entered text. Registration happens before `Theme::update` and `open_window`, so the first layout uses the new face.
## Font changes affect layout
Different faces have different advances, ascent, descent, and glyph coverage.
A substituted family or newly loaded CJK face can change line wrapping,
control height, caret placement, and alignment even at the same `px` size.
Theme changes refresh windows, and `add_fonts` invalidates font resolution
and cached line layouts; if fonts are installed while a window is already
visible, call `cx.refresh_windows()` after registration. Recheck text after
the new frame, especially in narrow controls and mixed-script paragraphs.
For custom measurements, use the shaped line from [TextSystem](./text-system)
instead of estimating width from character count.
## Theme JSON config
Font families and sizes can also come from a theme file:
```json
{
"font.family": "Inter",
"font.size": 16,
"mono_font.family": "JetBrains Mono",
"mono_font.size": 13
}
```
In a desktop application's startup callback, after `init(cx)`, choose a theme name and watch a directory containing theme files. This is a contextual snippet: `cx` comes from the callback, and `"My Theme"` must match the name inside a theme file in `./themes`.
```rust
use std::path::PathBuf;
use gpui_kit::component::theme::{Theme, ThemeRegistry};
use gpui_kit::SharedString;
let theme_name: SharedString = "My Theme".into();
ThemeRegistry::watch_dir(PathBuf::from("./themes"), cx, move |cx| {
if let Some(theme) = ThemeRegistry::global(cx).themes().get(&theme_name).cloned() {
Theme::update(cx, |current| current.apply_config(&theme));
}
})
.expect("Failed to watch theme directory");
```
See [Theme](/component/theme) for the full config reference.
## WebAssembly: choose a font supply strategy
See the [WebAssembly guide](./webassembly) for the browser build and its font setup.
GPUI's Web text system does **not enumerate or load the browser's installed fonts** as its main font collection. Register every family that must be shaped reliably, including the family used by the initial text style, before creating a window or measuring its first text. On this Web platform, `.SystemUIFont` maps to `IBM Plex Sans`; if that alias can be used before the theme applies, register that family too. Apply or change the theme **after** registration and keep its `font_family` and `mono_font_family` pointed at loaded families. A theme file naming an unavailable desktop font can otherwise fail font resolution. The [gallery initialization](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story-web/src/lib.rs) follows this order: initialize GPUI Kit, register font bytes, apply the theme, then open the window; later theme changes reassert its loaded families.
There are three useful supply choices:
| Choice | Initial download | Coverage and tradeoff |
| --- | --- | --- |
| Bundle full fonts with `include_bytes!` | Larger WebAssembly payload | Offline and predictable, including user-entered text within the font's coverage. |
| Bundle subsets for known UI strings | Smaller payload | Other CJK characters and newly entered text need another source. |
| Fetch a font file when needed | Smaller initial payload; later network request | Install it at runtime, then refresh windows so text is shaped again. Handle loading and failure states. |
For a fetched font, pass owned bytes to the **same** `TextSystem::add_fonts` API. The HTTP download can come from the application's `cx.http_client()` or another client; the registration step is:
```rust
use std::borrow::Cow;
use gpui_kit::*;
fn install_downloaded_font(cx: &mut App, bytes: Vec) -> Result<()> {
cx.text_system().add_fonts(vec![Cow::Owned(bytes)])?;
cx.refresh_windows();
Ok(())
}
```
`add_fonts` invalidates font resolution and line-layout caches, but an already visible window needs `refresh_windows()` to show the newly shaped text. Validate the HTTP response and nonempty bytes before installing. Supply an actual supported font file such as raw TTF; a font-service CSS URL may return a stylesheet or WOFF2 subset rather than bytes this text system accepts. Browser cross-origin rules apply to external requests. Download only fonts the current content needs, and deduplicate concurrent requests.
`App::on_missing_glyphs(callback) -> Subscription` can report unresolved grapheme clusters after shaping. Keep the subscription alive, inspect `MissingGlyph::grapheme()` and `font_class()`, and use it to request a script-specific font once. A new registration replaces the previous callback; reports are deduplicated and bounded, so this is a loading hint rather than a guaranteed complete inventory. If Canvas fallback can already draw a CJK grapheme, that grapheme will **not** produce a missing-glyph report. For accurate CJK typography, trigger loading from the selected language or known content coverage instead of relying only on missing-glyph reports.
## Diagnose a font problem
| Symptom | Check |
| --- | --- |
| App panics while laying out first text | List `all_font_names()` before opening the window. Confirm the primary family and a usable default family were registered, especially `.SystemUIFont`'s Web mapping. |
| Text appears in an unexpected face | Compare the requested family with `all_font_names()` and the family embedded in the file. Check whether a theme switch replaced your choice. |
| Squares or mixed faces in CJK text | Confirm the font contains the exact characters, not just a family name. A subset may cover labels but omit user input; load broader coverage or an appropriate script font. |
| Text wraps differently after a font loads | Recheck the layout with the installed font's metrics; refresh an already visible window after `add_fonts`. |
| Missing-glyph callback never fires on Web | Check whether Canvas fallback already drew the grapheme. Use content or language selection to trigger a font needed for consistent typography. |
## Browser Canvas fallback
Text the loaded fonts cannot draw can sometimes come from the browser. The Web platform can render eligible emoji through Canvas 2D with the visitor's local fonts, avoiding a bundled emoji font. Choose the policy when constructing the platform; it cannot change afterwards:
| `CanvasFontFallback` | Browser draws |
| --- | --- |
| `Emoji` (default) | Emoji, including skin tones, flags, keycaps and ZWJ sequences |
| `EmojiAndCjk` | Emoji plus eligible horizontal Han, kana, modern Hangul, and related punctuation |
| `Disabled` | Nothing; only bundled fonts are used |
`gpui_kit::application()` and `gpui_kit::platform::single_threaded_web()` keep
the default. To widen it, build the platform yourself:
```rust
use gpui_kit::web::{CanvasFontFallback, WebBackendPreference, WebPlatform};
let platform = Rc::new(WebPlatform::new_with_backend_and_font_fallback(
false,
WebBackendPreference::Auto,
CanvasFontFallback::EmojiAndCjk,
));
let http_client = Arc::new(platform.fetch_http_client());
let app = Application::with_platform(platform).with_http_client(http_client);
```
Loaded fonts stay preferred wherever they have the glyph and requested presentation. Canvas fallback is limited to eligible **single complete graphemes**; it is not a general system-font API or a guarantee that the browser has a matching glyph. CJK fallback draws eligible graphemes independently and horizontally, so it favors readable coverage over exact spacing, shaping, and font features. Its appearance depends on the visitor's fonts. Use a real CJK font for text whose metrics, line breaks, or visual consistency matter. The gallery opts into `EmojiAndCjk` because its bundled CJK subset covers known gallery copy while visitors can type other characters into inputs.
---
# Installation
Source: /docs/installation
Install the native toolchain for your operating system, then add the `gpui-kit` crate to a Rust application. The steps below are for desktop development; [WebAssembly](./webassembly) and [Mobile](./mobile) have separate target setup.
## Platform requirements
macOS 15 or later
Xcode Command Line Tools, installed with xcode-select --install. Run xcode-select -p afterward; it should print the selected developer directory.
Windows 10 or later
Visual Studio 2022 Build Tools or Community with the Desktop development with C++ workload, including MSVC and a Windows SDK. Use the MSVC Rust toolchain, not the GNU toolchain.
CMake available on PATH; check with cmake --version in the terminal where you will run Cargo.
The following packages are verified on Ubuntu 24.04:
This matches the repository's script/install-linux.sh for Ubuntu 24.04. Other distributions need equivalent development packages. To display a window, run in a graphical Wayland or X11 session with a working Vulkan driver; installing libvulkan1 alone does not install a GPU driver.
## Rust and Cargo
Install Rust and Cargo with the [official Rust installer](https://rust-lang.org/tools/install/). Use Rust 1.92 or later for this repository's current locked dependency graph: its Linux GPUI platform dependency includes `oo7 0.6.0`, which declares Rust 1.92 as its minimum. Then check the tools in the same terminal that will build the app:
```sh
rustc --version
cargo --version
```
Both commands should print a version. On Windows, rustup show active-toolchain should identify an msvc target. If a terminal cannot find Cargo immediately after installation, open a new terminal and run the checks again.
Add GPUI Kit to the application's `Cargo.toml` under `[dependencies]`:
```toml
gpui-kit = "{{gpui_kit_version}}"
```
The `{{gpui_kit_version}}` requirement selects a compatible Kit release. Kit's default features include the styled components and default icon assets. It brings in matching GPUI crates, so an application using this setup does not need to list GPUI separately. `use gpui_kit::*;` imports GPUI's re-exported API; the layers are reachable as `gpui_kit::component`, `gpui_kit::base`, `gpui_kit::assets`, and `gpui_kit::platform`.
### Why the dependency is named `gpui-pre`
Throughout these docs, **GPUI** means [Zed's GPUI](https://github.com/zed-industries/zed/tree/main/crates/gpui). `gpui-pre` is the crates.io package name used to publish a snapshot of GPUI from a recorded Zed commit, alongside its related GPUI crates. It provides a reproducible publication and version alignment path for GPUI Kit; it is not another rendering implementation. The publication process adjusts package names and dependency manifests for crates.io, so API references in this manual target the GPUI version pinned by this Kit release. In this repository that is `gpui-pre = {{gpui_pre_version}}`; application code normally depends only on `gpui-kit` and imports GPUI through `gpui_kit::*`. A newer `gpui-pre` snapshot does not by itself mean that the current GPUI Kit release supports it.
## Verify the installation
If you checked out this repository, run its existing example from the repository root:
```sh
cargo run -p hello_world
```
The first build downloads and compiles dependencies and may take several minutes. Once it starts, a window shows “Hello, World!” and a “Let's Go!” button. Clicking the button prints `Clicked!` in the terminal. Close the window to end the process. This checks the build toolchain, window creation, rendering, and input on your machine; it is not a performance benchmark.
For a new project instead, follow [Getting Started](./getting-started). It creates an application with the same single dependency and explains each part of the first [View](./render) and [Window](./window).
## Troubleshooting
| Symptom | Check |
| --- | --- |
| `cargo` or `rustc` is not found. | Install Rust with rustup, open a new terminal, and rerun the version checks above. |
| A build reports an unsupported Rust version. | Check `rustc --version`; update the active Rust toolchain. A dependency may require a newer compiler than this guide's baseline. |
| Windows reports `link.exe` missing or cannot find a Windows SDK. | Confirm the Visual Studio C++ workload and SDK are installed, then build with an MSVC Rust toolchain from a Visual Studio Developer PowerShell if needed. |
| Linux reports a missing `pkg-config` executable, X11, Wayland, fontconfig, or WebKit header. | Install the Ubuntu packages above, or their equivalents for your distribution. If `pkg-config` itself is missing, install the `pkg-config` package too. The error identifies the missing tool or system library. |
| The program compiles but no window appears on Linux. | Check that the process is running in a graphical Wayland or X11 session and that a Vulkan driver works for that session. A headless shell or Vulkan loader without a driver is insufficient. |
| Cargo cannot resolve `gpui-pre` or APIs differ from these examples. | Keep the `gpui-kit` requirement and update dependencies together. Kit pins a matching `gpui-pre-*` snapshot; do not override one GPUI package to a different version. In a repository checkout, use the checked-in `Cargo.lock`. |
For errors after a window opens, continue with [Getting Started](./getting-started) and inspect the relevant guide for the feature you are using.
## Improve development runtime performance
Rust Debug builds leave GPUI, the component library, layout, and text rendering
largely unoptimized. As a result, an application started with `cargo run` can
render and respond much more slowly than its release build. The profile below
optimizes those framework dependencies while your application code remains in
Debug mode and keeps its normal debugging workflow.
This setting does **not** make compilation faster. Compiling the optimized
dependencies can take longer, especially on the first build; the benefit is
better runtime performance while developing and running the application.
Cargo's [package profile overrides](https://doc.rust-lang.org/cargo/reference/profiles.html#overrides) only take effect in the root `Cargo.toml` of your application
or workspace:
```toml
[profile.dev.package]
gpui-pre = { opt-level = 3 }
gpui-component = { opt-level = 3 }
gpui-kit = { opt-level = 3 }
gpui-kit-assets = { opt-level = 3 }
gpui-pre-macros = { opt-level = 3 }
gpui-pre-platform = { opt-level = 3 }
rustybuzz = { opt-level = 3 }
taffy = { opt-level = 3 }
ttf-parser = { opt-level = 3 }
```
---
# RenderOnce
Source: /docs/render-once
The core distinction is ownership. **`RenderOnce::render(self, ...)` consumes a component value**: its parent normally constructs a fresh, lightweight description when the parent renders. **[`Render::render(&mut self, ...)`](./render) borrows a retained View** stored in an [Entity](./entity). Use `RenderOnce` for declarative inputs that describe a reusable piece of UI for this render, and `Render` when a View itself must keep state and a lifecycle across renders. `Entity` can also hold a model or other data that does not implement `Render`; an Entity becomes a renderable View when its type implements that trait.
```rust
// RenderOnce
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement;
// Render
fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement;
```
That is why most reusable GPUI Kit components are `RenderOnce`: a caller can supply props and a handler, and the component can return existing semantic elements without creating an Entity for every button, row, or badge. A file tree, chat list, chart workspace, or other feature View that owns a collection, subscriptions, async work, or coordinated selection usually needs a retained `Entity` and `Render`. A complex feature View can still create many `RenderOnce` children.
```rust
use gpui_kit::*;
use gpui_kit::prelude::*;
#[derive(IntoElement)]
struct MessageRow {
author: SharedString,
body: SharedString,
action: Option,
}
impl MessageRow {
fn new(author: impl Into, body: impl Into) -> Self {
Self {
author: author.into(),
body: body.into(),
action: None,
}
}
fn action(mut self, action: impl IntoElement) -> Self {
self.action = Some(action.into_any_element());
self
}
}
impl RenderOnce for MessageRow {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let MessageRow { author, body, action } = self;
div()
.flex()
.gap_2()
.child(div().font_semibold().child(author))
.child(body)
.when_some(action, |row, action| row.child(action))
}
}
```
`#[derive(IntoElement)]` generates the conversion that lets the value participate in GPUI's fluent [Element](./element) tree:
```rust
div().child(MessageRow::new("You", "Explain RenderOnce"))
```
The derive does not render the component eagerly. It generates an `IntoElement` implementation whose element is `ViewElement`; GPUI consumes and renders the value as part of the surrounding tree. Implementing `RenderOnce` alone gives the value a `View` implementation, but does not let you pass it directly to `.child(...)`: that also requires `IntoElement`, which this derive supplies. The derive does **not** implement `Styled`, `ParentElement`, focus, or accessibility semantics for your type. Those capabilities come from the elements you build or from additional traits you implement.
## Try it: rebuild a value, keep the count
In an app that depends on `gpui-kit`, replace `src/main.rs` with this complete example and run `cargo run`. The `CounterLabel` is a `RenderOnce` value. `CounterView` is the retained `Entity` that owns the count.
```rust
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::*;
#[derive(IntoElement)]
struct CounterLabel {
count: u32,
}
impl RenderOnce for CounterLabel {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
div().child(format!("Count: {}", self.count))
}
}
struct CounterView {
count: u32,
}
impl Render for CounterView {
fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement {
div()
.flex()
.flex_col()
.size_full()
.items_center()
.justify_center()
.gap_2()
.child(CounterLabel { count: self.count })
.child(
Button::new("increment")
.primary()
.label("Add one")
.on_click(cx.listener(|this, _, _, cx| {
this.count += 1;
cx.notify();
})),
)
}
}
fn main() {
application().with_assets(assets::Assets).run(|cx| {
init(cx);
open_window(WindowOptions::default(), cx, |_, cx| {
cx.new(|_| CounterView { count: 0 })
})
.expect("Failed to open window");
});
}
```
The window starts at `Count: 0`. Click **Add one** twice: it should show `Count: 1`, then `Count: 2`. Each `CounterView::render` call constructs a new `CounterLabel` from the current count; its earlier value is consumed. The count survives because the same `CounterView` Entity owns it. `cx.notify()` requests another render after the click changes that owner. The component's name does not promise exactly one render per display frame; see [Render](./render) for when GPUI renders.
If the text stays at zero, check that the listener writes `this.count` and calls `cx.notify()`. If it always returns to one, check that `CounterView { count: 0 }` is created in the window closure, not inside `render`. If `.child(CounterLabel { ... })` does not compile, keep `#[derive(IntoElement)]` and `use gpui_kit::*;` in scope. The later snippets in this page illustrate separate variations; use this full example as the copyable starting point.
## Build and use the component
`MessageRow` has a small builder surface: `new` supplies required text, while `action` is an optional **slot**. `AnyElement` stores whichever concrete element the caller supplies. The slot is rendered after the body; calling `.action(...)` twice replaces the earlier element. This follows the repository's [`Empty` component](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/empty.rs), which uses named optional slots and renders them in a fixed order. If the component must accept several children, store `Vec` and implement `ParentElement`; one named slot does not need that trait.
The caller can put a semantic control in the slot and keep the behavior in its retained View. In this example the `Editor` below owns `opened`; its click listener changes that field and requests another render:
```rust
// Inside Editor::render, where cx: &mut Context is available.
MessageRow::new("You", "Explain RenderOnce").action(
Button::new("open-message")
.label("Open")
.on_click(cx.listener(|this, _, _, cx| {
this.opened = true;
cx.notify();
})),
)
```
Import `Button` with `use gpui_kit::component::button::Button;` and give `Editor` an `opened: bool` field. For repeated messages, pass each button a stable ID derived from that message's domain ID (for example, `("open-message", message.id)` if that ID is a supported `ElementId` part). The label is display text, not an identity. `MessageRow` itself has no Entity, listener context, or callback ownership: it consumes the already-built action. A custom component that owns its own callback can instead store an owned `'static` handler in a private field and forward it to a semantic control during `render`; use that design when the callback is part of the component's contract.
## Owned values and the render lifecycle
The [`Render`](./render) guide covers the retained View. `RenderOnce` requires `Self: 'static`, and the value component's actual signature is:
```rust
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement;
```
Because `self` is owned, rendering can move fields directly into the Element tree and its `'static` handlers. The component value is used once; its parent constructs a new value the next time that parent renders. Builder methods may mutate the value while constructing it; the resulting props describe one render rather than a persistent mutable model.
Destructuring first keeps ownership clear when several fields move into different parts of the tree. The complete `MessageRow::render` above does this for its text and action slot.
The slot conversion happens in the builder, before `render`; `.when_some(...)` adds it only when present. A `SharedString` owns reusable text, and `AnyElement` owns the type-erased child, so neither borrows a short-lived local variable.
This does **not** mean the visible UI disappears after one frame, nor that every display refresh constructs a new value. “Once” refers to one component instance: the parent makes another whenever its `render` runs. A normal window redraw may render even unchanged child views, while an explicit [cached view](./view-cache) can skip a clean subtree. GPUI retains Entities and keyed state across these render passes, so this is not simply a traditional immediate-mode loop that rebuilds the whole application on every screen refresh. Do not retain `&mut Window` or `&mut App` beyond this call. Repeated rows should use stable IDs derived from domain data when their children need identity; see [ElementId](./element_id).
## State belongs outside the component value
`RenderOnce` does **not** mean “no state.” The value holds props such as a label, disabled flag, or checked flag for this render. GPUI may retain small interaction details under a stable [ElementId](./element_id), and a `RenderOnce` component may hold an external `Entity` handle. The boundary is that changing application state must have a durable owner outside the consumed value. That owner may be a `Render` View, a model-only Entity, or another application state holder; pass current values, a callback, or a handle into the component.
GPUI Kit's styled `Button` and `Checkbox` both implement `RenderOnce`. The Button takes a label and click handler. Checkbox takes a controlled `checked` bool and reports the requested next value through `on_click`; the owner writes that value and calls `cx.notify()`. Their Base layer supplies focus, keyboard, and accessibility behavior. The parent can recreate them cheaply while keeping the actual state in one place.
```rust
use gpui_kit::component::checkbox::Checkbox;
// Inside the owner's Render::render, with self.show_hidden and cx available:
Checkbox::new("show-hidden")
.checked(self.show_hidden)
.label("Show hidden files")
.on_click(cx.listener(|this, checked, _, cx| {
this.show_hidden = *checked;
cx.notify();
}))
```
```rust
use gpui_kit::component::button::{Button, ButtonVariants};
struct Editor {
saved: bool,
}
impl Render for Editor {
fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement {
div()
.child(if self.saved { "Saved" } else { "Unsaved" })
.child(
Button::new("save")
.label("Save")
.primary()
.on_click(cx.listener(|this, _, _, cx| {
this.saved = true;
cx.notify();
})),
)
}
}
```
This example assumes the first example's `use gpui_kit::*;` import. The `Editor` is mounted in an `Entity`; `Button::new` establishes a stable element ID. The button's label, focus, keyboard activation, and accessible role come from the component and Base layers. Element handlers are `'static`, so a child that captures a callback or `Entity` needs an owned value. Clone a handle before moving it when the caller still needs it.
Text editing shows the other side of the boundary. `Input::new(&state)` returns a `RenderOnce` visual component, but `state` is an `Entity` retained by the owning View. `InputState` implements `Render` and keeps text, selection, focus, editing history, and input behavior across parent renders. Recreating `Input::new(&state)` does not recreate the editor state:
```rust
use gpui_kit::component::input::{Input, InputState};
struct SearchView {
query: Entity,
}
impl Render for SearchView {
fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement {
Input::new(&self.query).id("search-query")
}
}
```
Create `query` once when constructing `SearchView`, for example with `cx.new(|cx| InputState::new(window, cx))`; do not create it inside `render`. A collection-heavy View follows the same ownership rule: keep records, filters, selected IDs, and subscriptions in the retained owner, then render value-like rows and controls from them.
Capturing an `Entity` keeps that entity alive as long as the rendered handler is retained. This is often correct for a child acting on its owner. Use `WeakEntity` when the handler must not extend the target's lifetime, and handle the case where `weak.update(...)` can no longer reach it.
`RenderOnce::render` receives `&mut App`, not `&mut Context`. A `RenderOnce` component therefore has no entity [Context](./context) of its own: it cannot use `cx.listener` for itself, retain its own subscriptions or tasks, or call `cx.notify()` to schedule itself. Pass a handler, dispatch an [Action](./action), or update the state-owning `Entity` instead. Keyed element state can retain local interaction details, but it does not replace an owner for durable application data.
## Common compile errors and a quick check
| Symptom | Check |
| --- | --- |
| `MessageRow` does not implement `IntoElement` at `.child(...)` | Add `#[derive(IntoElement)]` as well as `impl RenderOnce`; keep `use gpui_kit::*;` in scope. |
| `no method named when_some` (or a fluent style method) | Import `gpui_kit::prelude::*;` for `FluentBuilder`, and check whether the method belongs to the returned `div()` rather than your component type. |
| A borrowed local value “does not live long enough” in a slot or handler | Move owned text (`SharedString`), an owned `AnyElement`, or a cloned `Entity` handle into the component. GPUI handlers must be `'static`. |
| `cx.listener` or `cx.notify()` is unavailable in `RenderOnce::render` | That method gets `&mut App`, not an entity `Context`. Create the listener in the owner View's `Render::render` and pass it in, as above. |
| Calling `.child(...)` on `MessageRow` fails | The derive provides `IntoElement`, not `ParentElement`. Add a named builder such as `.action(...)`, or implement `ParentElement` and store children explicitly. |
To check a copied example, first put the type, builders, and `RenderOnce` implementation in the same module with both imports shown above. Use it as a child of a retained View and run `cargo check -p your-app` in that app's workspace. The `Editor` snippets are separate illustrations; add the mentioned fields and imports before compiling them together. For the repository's real API, compare [`Empty`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/empty.rs), [`Checkbox`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/checkbox.rs), and [`Button`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/button/button.rs).
## Builder-style components
Owned, private fields make `RenderOnce` work naturally with builder APIs. GPUI Kit uses this pattern in its components. A builder takes and returns `Self`, preserving a valid component while callers refine it:
```rust
use gpui_kit::component::ActiveTheme;
#[derive(IntoElement)]
struct StatusBadge {
label: SharedString,
muted: bool,
}
impl StatusBadge {
fn new(label: impl Into) -> Self {
Self {
label: label.into(),
muted: false,
}
}
fn muted(mut self, muted: bool) -> Self {
self.muted = muted;
self
}
}
impl RenderOnce for StatusBadge {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
div()
.rounded_full()
.px_2()
.text_color(cx.theme().muted_foreground)
.when(!self.muted, |this| this.text_color(cx.theme().foreground))
.child(self.label)
}
}
```
This example also uses `use gpui_kit::*;`. The returned `div()` implements [`Styled`](./style) and `ParentElement`, enabling `.rounded_full()`, `.text_color()`, and `.child()`. If callers need those methods **on `StatusBadge` itself**, implement `Styled` and/or `ParentElement` for the type, store the styles or children, and apply them in `render`. `IntoElement` derive does not forward the returned element's fluent traits. GPUI Kit's `Button` explicitly implements both. Use `.when(...)` and `.when_some(...)` for small refinements; use ordinary Rust branches when the UI structure differs substantially.
## Choose the right layer
| Use | When |
| --- | --- |
| [`RenderOnce`](./render-once) + `IntoElement` | A reusable component consumes caller-supplied props and handlers for a render. It may use keyed state or an external Entity; rendering receives `&mut Window` and `&mut App`. |
| [`Render`](./render) | A retained `Entity` owns changing data, collections, subscriptions, tasks, or a lifecycle; rendering receives `&mut Context` to update and notify it. |
| [`Element`](./element) | Built-in elements cannot express required layout, prepaint, paint, hit testing, or other low-level phases. |
A useful composition is: a `Render` view owns state, it creates `RenderOnce` components to describe reusable UI, and those components return built-in Elements. This gives most GPUI Kit components a small, explicit API while keeping complex state in a few meaningful owners. Implement `Element` only when the standard Element APIs cannot express the rendering behavior.
If a component starts accumulating mutable state, subscriptions, or background tasks, move that lifecycle into an `Entity` and implement `Render` for it. Keeping such state inside a value that is consumed on every render loses the ownership model that makes `RenderOnce` simple.
---
# Event
Source: /docs/event
GPUI provides **Event** as a typed notification mechanism between [Entities](./entity). An Event reports something that already happened; unlike an [**Action**](./action), it does not use Focus, Key Contexts, KeyBindings, or the Dispatch Path. GPUI also calls raw mouse and keyboard input values “events”; those follow different rules, covered below.
## Action in, Event out
An Action can cause the state change, but Event delivery starts after that change:
```text
Chat changes state → emit(MessageSent) → subscribers receive Event → Workspace updates
```
- [**Action**](./action) carries intent inward: “send this message.”
- **Event** reports the result outward: “this message was sent.”
The command owner handles the Action and changes its state. It then emits an Event so owners or services can react without being coupled to the command's UI entry point. See [Action](./action) for Focus and command dispatch, and [KeyBinding](./keybinding) for shortcut matching.
## A complete first Event
This small application has two Entities. `Chat` owns the count and emits a typed fact. `Workspace` owns the `Chat` handle, subscribes to that exact Entity, and renders the latest count. Clicking the button updates `Chat`; the subscription then updates `Workspace`.
Use the project from [Getting Started](./getting-started), which already depends on `gpui-kit`. Replace its `src/main.rs` with this complete program and run `cargo run` from that project directory. The window should start at **Messages sent: 0**; each click on **Send** should increase the count by one.
```rust
use gpui_kit::*;
use gpui_kit::assets::Assets;
use gpui_kit::component::button::Button;
#[derive(Clone, Debug)]
enum ChatEvent {
MessageSent { total: usize },
}
struct Chat {
sent: usize,
}
impl EventEmitter for Chat {}
impl Chat {
fn send(&mut self, cx: &mut Context) {
self.sent += 1;
cx.emit(ChatEvent::MessageSent { total: self.sent });
}
}
struct Workspace {
chat: Entity,
shown_total: usize,
_subscriptions: Vec,
}
impl Workspace {
fn new(cx: &mut Context) -> Self {
let chat = cx.new(|_| Chat { sent: 0 });
let subscription = cx.subscribe(&chat, |workspace, _chat, event, cx| {
match event {
ChatEvent::MessageSent { total } => workspace.shown_total = *total,
}
cx.notify(); // Workspace's visible count changed.
});
Self {
chat,
shown_total: 0,
_subscriptions: vec![subscription],
}
}
}
impl Render for Workspace {
fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement {
div()
.p_4()
.child(format!("Messages sent: {}", self.shown_total))
.child(Button::new("send").label("Send").on_click(cx.listener(
|workspace, _, _, cx| {
workspace.chat.update(cx, |chat, cx| chat.send(cx));
},
)))
}
}
fn main() {
gpui_kit::application().with_assets(Assets).run(|cx| {
gpui_kit::init(cx);
gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| {
cx.new(Workspace::new)
})
.expect("failed to open window");
});
}
```
The bootstrap calls `gpui_kit::init` before opening the window; `open_window` supplies the application window. The button's callback updates `Chat`, where the state mutation and `cx.emit` happen together. The subscriber reads the Event payload and calls `cx.notify()` because the value rendered by `Workspace` changed.
`cx.emit(...)` queues an effect. GPUI delivers it after the current entity update can finish; the subscription callback is not a direct call inside `Chat::send`. Emit only after the operation succeeds. If the emitting Entity also renders changed state, call `cx.notify()` in that Entity as well: `emit` does not request a render. A model Entity such as `Chat` above has no view to redraw. Name Event variants as facts, such as `MessageSent`, `Saved`, and `Dismissed`; `SendMessage` names a command.
### Connect the Action to this Event
The complete example above begins at a button callback. To exercise the full [Action](./action) → state → Event → subscription route, edit the **same** `src/main.rs` in four places. Keep `Chat`, `ChatEvent`, `Chat::send`, and the existing subscription callback.
1. After the imports, add `actions!(chat, [SendMessage]);`. Add `focus: FocusHandle,` to `Workspace` beside its `chat` field.
2. Change `Workspace::new` to accept `window: &mut Window` before `cx`. Before the existing `let chat = ...` line, create and focus the handle, then add `focus,` to the returned `Self`:
```rust
let focus = cx.focus_handle().tab_stop(true);
focus.focus(window, cx);
```
3. Add this method to `impl Workspace`:
```rust
fn on_send_message(&mut self, _: &SendMessage, _: &mut Window, cx: &mut Context) {
self.chat.update(cx, |chat, cx| chat.send(cx));
}
```
In `render`, add the following three calls to the **outer** `div()` before its children, then replace only the button's `.on_click(...)` with the callback below:
```rust
.track_focus(&self.focus)
.key_context("Chat")
.on_action(cx.listener(Self::on_send_message))
.on_click(cx.listener(|workspace, _, window, cx| {
workspace.focus.dispatch_action(&SendMessage, window, cx);
}))
```
4. In `main`, after `gpui_kit::init(cx)`, bind Enter. Change the `open_window` closure to pass its window to the constructor:
```rust
cx.bind_keys([KeyBinding::new("enter", SendMessage, Some("Chat"))]);
gpui_kit::open_window(WindowOptions::default(), cx, |window, cx| {
cx.new(|cx| Workspace::new(window, cx))
})
.expect("failed to open window");
```
Run `cargo run` again. Press **Enter** while the Workspace region has focus, then click **Send**. Each input dispatches the same `SendMessage` Action to `Workspace::on_send_message`. That handler updates `Chat`; `Chat::send` increments `sent` and emits `MessageSent`; the retained subscription updates `Workspace::shown_total`. The visible count should rise once per input. As a check, temporarily remove `cx.emit(...)` from `Chat::send`: the internal `sent` value still rises, but the displayed count stops changing because `Workspace` listens for the Event. Restore the emit call before the next exercise. If Enter does nothing, check that the focus handle is attached to the rendered container and its `Chat` Key Context is present; if the button does nothing, inspect the Action handler and dispatch path.
## Subscription lifetime and ownership
`cx.subscribe(&chat, callback)` returns a `Subscription`. Keep it in the subscribing owner, as `Workspace` does above. Dropping the handle disconnects the callback, so a local handle that disappears at the end of `new` silently stops delivery. Dropping `Workspace` also drops its handles. An `Entity` clone only identifies the source; retaining it does not retain a subscription. Keep view-specific subscriptions with the view instead of placing them in long-lived global state.
The callback arguments are `&mut Workspace`, the emitting `Entity`, `&ChatEvent`, and `&mut Context`. The payload is borrowed for the callback; copy or clone data that must outlive it. The subscription is tied to one source Entity and one Event type, not every `Chat` or every event in the application. Multiple owners may subscribe independently to the same source. To stop one subscription early, remove or drop its handle from the owner's collection.
Use `cx.subscribe_in(&chat, window, callback)` when the handler also needs `&mut Window`. Its callback receives five arguments: owner, `&Entity`, `&ChatEvent`, window, and context; keep its returned `Subscription` just as above. Use `cx.observe(&chat, ...)` for a generic entity-change notification when no typed Event payload is needed. An Event is deliberate semantic information; `cx.notify()` reports that an Entity needs an update and does not produce a `ChatEvent`.
If a subscription seems silent, check that the source is the same Entity, the `EventEmitter` implementation matches the emitted type, the handle is still stored, and `cx.emit` is reached after a successful state change. If the callback runs but the screen stays stale, check which Entity renders the changed value and call `cx.notify()` on its context.
### Try `observe` without an Event payload
Restore the original complete `Chat`/`Workspace` example above if you made the Action continuation, then run it: each click raises the visible count by one. Now make these three changes to that application:
1. Remove the `ChatEvent` enum and `impl EventEmitter for Chat {}`. Replace `Chat::send` with the following method. This version changes state and calls `notify`; it emits no Event.
```rust
fn send(&mut self, cx: &mut Context) {
self.sent += 1;
cx.notify();
}
```
2. In `Workspace::new`, replace the `cx.subscribe(...)` block with this observer. Keep the returned handle in the existing `_subscriptions` field.
```rust
let subscription = cx.observe(&chat, |workspace, chat, cx| {
workspace.shown_total = chat.read(cx).sent;
cx.notify();
});
```
3. Run the application again and click **Send**. The count still rises. The observer receives the changed `Entity` and reads its state; it receives no `ChatEvent` or payload. Remove `cx.notify()` from `Chat::send` once and rerun: the observer no longer fires, so the count stays at zero even though `Chat.sent` changes. Restore `cx.notify()` afterward.
`observe` is useful when any notification from a particular Entity is enough and the observer can read current state. `subscribe` is useful when the producer deliberately reports a typed fact. Both return a `Subscription` that the owner must retain; `cx.emit` alone does not notify `observe` callbacks, and `cx.notify()` alone does not emit a typed Event.
**INFO — Event delivery does not follow Focus**
An Event goes to subscribers of its source Entity. Moving Focus or changing a Key Context does not change who receives it. Do not use Events as a global command bus to bypass Action routing.
## Action or Event?
| Question | Use | Examples |
| --- | --- | --- |
| Is this an instruction a user or caller wants performed? | **Action** | Save, Delete, Open Search |
| Should it be bindable to a key or shown in a menu? | **Action** | Copy, Toggle Sidebar, Rename |
| Is this a fact reported after state or lifecycle changed? | **Event** | ValueChanged, Saved, Dismissed |
| Should an owner observe a child independently of its UI tree? | **Event** | Input changed, row selected, dialog submitted |
| Is it only a pointer gesture with no other command entry point? | callback | hover, drag delta, pointer position |
Use both when a command produces a fact other parts of the application need to observe: handle the Action first, commit the state change, then emit the Event.
## Pointer and keyboard input are also events
`MouseDownEvent`, `MouseUpEvent`, `MouseMoveEvent`, `ScrollWheelEvent`, `KeyDownEvent`, and `KeyUpEvent` describe raw input. They are distinct from the typed `EventEmitter` notifications above. A normal `div()` can register `.on_mouse_down(MouseButton::Left, ...)` or `.on_key_down(...)`; its `InteractiveElement` implementation handles the underlying hitbox and dispatch registration. Use [Action](./action) for an operation that needs a shortcut or menu entry, and raw events when positions, buttons, modifiers, or gesture deltas matter. Raw input callbacks do not create a typed entity Event unless the owning entity calls `cx.emit(...)`.
For example, [GPUI Kit's TimeField](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/src/time_field.rs) binds arrow-key **Actions** inside its own Key Context, handles a typed `KeyDownEvent` for digit input, and emits `TimeFieldEvent::Change` only after the time value changes. Its owner can subscribe to that event without knowing whether the change came from a key or another control. A matching KeyBinding can consume a key before a raw `on_key_down` handler receives it, so commands belong in Actions rather than duplicate raw key handlers. A digit handler follows this shape:
```rust
fn on_key_down(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context) {
let stroke = &event.keystroke;
if stroke.modifiers.modified() || stroke.key.chars().count() != 1 {
return;
}
let Some(digit) = stroke.key.chars().next().and_then(|c| c.to_digit(10)) else {
return; // Leave unrelated keys to the rest of the UI.
};
window.prevent_default();
cx.stop_propagation();
if self.editor.input_digit(digit) {
cx.emit(TimeFieldEvent::Change(self.editor.time));
}
cx.notify();
}
```
### Capture and bubble
Raw input has two dispatch phases. **Keyboard** listeners follow the focused element's path: capture walks from root to focused node; bubble returns from focused node to root. **Mouse** listeners are registered in paint order rather than on that ancestry path: capture runs back to front, and bubble runs front to back. The dispatcher calls matching mouse listeners in that order; a low-level listener must check its own hitbox before acting. Normal `.on_mouse_down(...)` and `.on_key_down(...)` callbacks run in bubble; `.capture_any_mouse_down(...)` is an element-level capture hook.
### Try a nested pointer handler
Return to the original Event version of the complete example above. Add `parent_hits: usize` and `child_hits: usize` to `Workspace`, initialize both to `0` in `Workspace::new`, and replace its `Render` implementation with this one:
```rust
impl Render for Workspace {
fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement {
div()
.p_4()
.child(format!("Messages sent: {}", self.shown_total))
.child(Button::new("send").label("Send").on_click(cx.listener(
|workspace, _, _, cx| {
workspace.chat.update(cx, |chat, cx| chat.send(cx));
},
)))
.child(format!(
"Parent: {} | Child: {}",
self.parent_hits, self.child_hits
))
.child(
div()
.p_4()
.bg(rgb(0xd0d7de))
.on_mouse_down(MouseButton::Left, cx.listener(|workspace, _, _, cx| {
workspace.parent_hits += 1;
cx.notify();
}))
.child("Parent surface")
.child(
div()
.p_4()
.bg(rgb(0x8ecae6))
.on_mouse_down(MouseButton::Left, cx.listener(
|workspace, _, _, cx| {
workspace.child_hits += 1;
cx.notify();
// Uncomment to stop the parent handler:
// cx.stop_propagation();
},
))
.child("Child surface"),
),
)
}
}
```
Click the blue **Child surface** once: both counters increase. The child listener is registered later in paint order and runs first in mouse bubble; the parent hitbox also contains that point, so its listener runs next. Click the gray area outside the child: only the parent counter increases. Uncomment `cx.stop_propagation()` in the child callback and rerun. Clicking the child now increases only its counter; clicking the gray area still increases only the parent counter. A click on **Send** changes the message count without changing either hit counter.
The two surfaces are nested for this exercise, but GPUI mouse dispatch uses the window frame's ordered listener list and each listener's hitbox. It does **not** route mouse input along a DOM-style ancestor chain. An overlapping surface painted later can be the first bubble listener even when it is not a child in the element tree. Stop propagation only when the child interaction must keep later listeners from acting on the same input.
Custom [`Element`](./element#the-three-phases) code can register a listener during `paint` with `window.on_mouse_event` and inspect `DispatchPhase`:
```rust
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
if phase.capture() && hitbox.is_hovered(window) {
// Decide whether this surface owns the gesture.
if event.button == MouseButton::Left {
cx.stop_propagation();
}
}
});
```
This listener is registered during `paint` and is replaced when the next frame is rendered. A `Hitbox` should have been inserted during `prepaint`. GPUI Kit's [Carousel scroll mask](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/carousel/scroll_mask.rs) uses capture for pointer and wheel gestures: it consumes movement on the carousel's axis while letting movement on the other axis reach an outer scroller. `Hitbox::is_hovered` tests pointer location; `should_handle_scroll` also accounts for scroll occlusion. Prefer fluent element handlers for ordinary controls; use `window.on_mouse_event` when building a custom Element that needs its own hitbox or phase handling.
### `stop_propagation` versus `prevent_default`
| Call | Meaning | Typical use |
| --- | --- | --- |
| `cx.stop_propagation()` | Stop delivery to later listeners in the **current dispatch**. In mouse bubble this blocks surfaces behind the current one; in keyboard bubble it blocks ancestors. In capture it also prevents the remaining capture listeners and bubble phase. | A nested control consumed a drag or key. |
| `window.prevent_default()` | Mark the current input event's default behavior as prevented. GPUI uses this for built-in behavior such as parent focus acquisition on mouse down. | A child handles mouse down but should keep the parent's focus from moving. |
They are independent. Stopping propagation does not itself cancel the focus default; preventing the default does not itself stop another handler. GPUI resets both flags for each input dispatch. `prevent_default` controls GPUI behavior that checks this flag; do not treat it as a general browser-style or operating-system event cancellation. Call either only after deciding that this input belongs to the control. An Action has the reverse initial propagation policy from raw input: its handler stops bubbling by default, and `cx.propagate()` explicitly lets an ancestor try it. Input callbacks receive `&mut Window`; call `window.prevent_default()` there.
When debugging an input handler, check the event phase, focused path, content mask, hitbox behavior, and z order. A handler may be registered correctly but never see a point because a higher surface occludes it or because the event follows a different focus path.
---
# Coding Guides
Source: /docs/coding-guides
This guide describes the application architecture and code patterns that have
proved durable in GPUI Kit. It is written for both engineers and coding
agents. Read [Design Guides](/docs/design-guides) first: code structure should
preserve product intent, not replace it.
This is a normative guide. **Must** marks lifecycle, correctness, or ecosystem
constraints; **should** is the default architecture and requires a concrete
reason to depart from it. Current source and API docs remain authoritative for
exact signatures.
## Make the common path inexpensive
**Design principle: choose framework types and ownership so ordinary component use is inexpensive by default.** Developers should be able to build and reuse UI without adding a cache at every call site. [SharedString](./shared-string) for retained UI text and [RenderOnce](./render-once) for lightweight component values are examples: the API places ownership where it belongs and avoids repeated work in the common path. Small costs still multiply across elements and renders; measure exceptional hot paths before adding caches. Neither type promises zero cost. The [text ownership rule](#own-persistent-ui-text-with-sharedstring) and later rendering sections explain the mechanics.
## Architecture at a glance
Dependencies point downward. Higher layers own domain meaning and orchestration;
lower layers own reusable presentation or behavior. Do not make a reusable
component depend on an application screen, or make `gpui-base` depend on a
theme from GPUI Component.
Use these boundaries:
- **app shell:** compose windows and feature crates while keeping feature logic out;
- **feature crate:** keep one capability's model, services, views, commands, dialogs,
and workflow behind one public boundary;
- **app component:** a repeated domain-aware pattern;
- **gpui-component:** themed, general-purpose UI;
- **gpui-base:** reusable behavior and geometry without product presentation.
### Organize large applications by capability
In a large Rust application, a complex capability often merits its own crate
once its state, lifecycle, and public boundary are worth maintaining
independently. Keep its model [Entity](./entity), services, [Render](./render)
Views, commands, dialogs, and workflow together. A dialog that edits a
workspace belongs to the workspace feature; only the reusable dialog
primitive belongs to the UI library. A small capability can remain a module
until a crate boundary has a concrete benefit.
```text
crates/
├── app/
│ └── src/main.rs # Compose windows and features
├── workspace/
│ └── src/
│ ├── lib.rs # The feature's public boundary
│ ├── model.rs
│ ├── commands.rs
│ ├── workspace_view.rs
│ └── rename_dialog.rs
├── search/
│ └── src/
│ ├── lib.rs
│ ├── model.rs
│ ├── commands.rs
│ ├── search_view.rs
│ └── filters.rs
├── settings/
│ └── src/
│ ├── lib.rs
│ ├── model.rs
│ ├── settings_view.rs
│ └── account_dialog.rs
└── shared/
└── src/
├── lib.rs
└── recent_items.rs # A stable capability with multiple owners
```
Do not invert this into global `models/`, `views/`, `modals/`, and `commands/`
directories. Those folders classify files by implementation role while
scattering every feature across the application.
The application shell composes feature crates but contains little feature
logic. Each feature owns its model Entities and the Views that render them. Keep a
feature's [`Global`](./global) private if it is needed for a genuinely application-wide
service or registry; a model used by one feature is not automatically a
Global. Within a feature, pass cloned `Entity` handles to cooperating
owners instead of repeatedly copying a large `Vec` or collection. Across a
feature boundary, expose only a deliberately public Entity type or a small
handle, domain ID, command, event, or interface. Keep model fields and
implementation modules private.
For example, the workspace crate can export a cloneable handle while retaining
the mutable model behind its public API:
```rust
// Condensed workspace/src/lib.rs; move the private model to model.rs as it grows.
use gpui_kit::{App, Entity, SharedString};
struct WorkspaceModel {
name: SharedString,
}
#[derive(Clone)]
pub struct WorkspaceHandle {
model: Entity,
}
impl WorkspaceHandle {
pub fn rename(&self, name: SharedString, cx: &mut App) {
self.model.update(cx, |model, cx| {
model.name = name;
cx.notify();
});
}
}
```
An application may hold this handle and pass a workspace ID to search; search
does not need the workspace's entire collection or its private View. A simple
acyclic dependency shape is:
```text
app ──▶ workspace ──▶ shared contracts
└────▶ search ─────▶ shared contracts
workspace, search ──▶ gpui-kit
```
Features may depend on stable shared capabilities and UI foundations, but not
on the shell or a sibling's private modules. If one feature must call another,
make that dependency explicit and one-way, or move a genuinely shared contract
below both. Extract a shared crate only after it has a coherent purpose and
real owners; avoid cycles and a catch-all `shared` crate.
These boundaries let different engineers or AI agents work on separate
capabilities in parallel with fewer file conflicts and less shared-state
coupling. Shared contracts still require coordination. Cargo may also reuse
cached dependencies and rebuild a smaller subgraph after a local change, but
incremental build speed depends on the dependency graph, public API changes,
features, and build configuration; adding crates has overhead and does not
guarantee a faster edit-build cycle. Split where ownership and collaboration
justify the boundary, not for every screen or helper.
## Bootstrap and root ownership
Initialize GPUI Component once, before creating component-backed views, and put
`Root` at the first level of each window:
```rust
app.run(move |cx| {
gpui_kit::init(cx);
gpui_kit::open_window(WindowOptions::default(), cx, |window, cx| {
cx.new(|cx| Workspace::new(window, cx))
})
.expect("failed to open window");
});
```
`Root` coordinates window-level component facilities such as overlays and
notifications. Do not create a separate root for each page inside one window.
It also coordinates modal focus restoration, focus traps, tooltip/menu layers,
and window-scoped text selection. Bypassing it can produce behavior that looks
correct at rest but fails when overlays nest or focus changes quickly.
## Understand GPUI's phases and contexts
GPUI is retained state with declarative rendering. An [Entity](./entity) survives across
frames; the [Element](./element) tree returned by `render` is a fresh description of the
current frame. Keep that distinction explicit. See [Context](./context) for the
scopes of `Context`, `App`, and `Window`.
- `Context` mutates the current entity, creates listeners tied to it,
emits its events, and notifies its observers.
- `App` gives access to application globals and entity reads/updates without
implying ownership by the rendered element.
- `Window` owns focus, actions, input dispatch, element-keyed state,
measurement, and animation-frame requests for that window.
- layout, prepaint, and paint are later phases; use their hooks only when
resolved geometry is genuinely required.
Never retain `&mut Window`, `&mut App`, or `&mut Context<_>` beyond the call in
which it is provided. Retain typed handles—`Entity`, `WeakEntity`,
`FocusHandle`, scroll handles, or domain IDs—instead.
## Choose the right unit
### Use `RenderOnce` for value-like elements
**Default to [`RenderOnce`](./render-once) for lightweight, reusable component values.**
The caller supplies current props and handlers, GPUI consumes the value to
produce elements, and the value can then be discarded. GPUI Kit's `Button`
and `Checkbox` follow this pattern: their displayed label, checked or selected
value, and callbacks come from the caller's current state. A `RenderOnce`
component may still use small keyed interaction state, such as a focus handle;
it does not own an independent View lifecycle. Do not infer that its render
method runs on every displayed frame: a displayed frame need not rebuild that
component's element tree.
```rust
#[derive(IntoElement)]
struct EmptyState {
title: SharedString,
}
impl RenderOnce for EmptyState {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
div()
.v_flex()
.gap_2()
.items_center()
.text_color(cx.theme().muted_foreground)
.child(self.title)
}
}
```
### Use `Entity` for retained behavior
Use an entity-backed [`Render`](./render) view when a component owns persistent,
complex state or an independent lifecycle: subscriptions, async work, history,
child entities, or incremental updates. Store entities in an owning view rather
than recreating them in `render`.
```rust
struct SearchView {
query: Entity,
}
impl SearchView {
fn new(window: &mut Window, cx: &mut Context) -> Self {
let query = cx.new(|cx| InputState::new(window, cx).placeholder("Search…"));
Self { query }
}
}
```
Do not turn every visual fragment into an entity. Entity boundaries have
lifecycle and coordination costs; use them where retained identity matters.
### Elements, views, and behavior systems are different
Do not force every component into one template. The ecosystem contains:
- semantic elements such as Button, Checkbox, Link, and Tabs;
- compound behavior roots such as Dialog, Popover, Select, and Combobox;
- entity-backed systems such as Input, Table, Tree, Dock, and notifications;
- infrastructure such as positioning, virtualization, scrolling, focus traps,
motion, history, and measurement.
An element may be internally complex and still be value-like to its caller. A
stateful system may expose render callbacks so applications own presentation
without reimplementing behavior. Choose the public seam from the behavior,
not from how many `div`s appear in its renderer.
## State ownership
### Own persistent UI text with `SharedString`
**Must prefer [`SharedString`](./shared-string) for UI text retained across frames or callback closures**, including component labels, titles, and placeholders. Store a stable value in the owning View or Entity and clone that handle when building elements; do not repeatedly clone a `String` on every render. For a mutable editing buffer or text being assembled with formatting, use `String` and convert when the result is ready. For temporary read-only access during one call, use `&str`.
For immutable API or JSON response text that the UI will retain, make the response field a `SharedString` at the deserialization boundary. Keep the received transport value intact; put derived or formatted presentation text in a separate domain or View model instead of rewriting the response or repeatedly cloning a `String` into UI. This rule does not require `SharedString` for a truly mutable buffer or a narrow, non-UI parsing step. See the [API response example](./shared-string#from-an-api-response-to-a-view).
This is an ownership and performance rule, not a claim that text is always allocation-free. Short values can live inline; longer dynamic values use shared storage, so cloning them does not copy the text bytes. Constructing a value can still allocate, and cloning a heap-backed value updates a reference count. See [SharedString](./shared-string) for the storage details and tradeoffs.
Put each state in the narrowest owner that can keep it correct:
- domain state belongs to a model or feature view;
- transient view state belongs to the view that renders it;
- reusable behavioral state belongs to the component state designed for it;
- tiny element-local state may use GPUI keyed element state;
- shared application services may be stored as GPUI globals.
Prefer controlled values for ordinary selection and toggles: pass the current
value into the component, receive a requested change, update the owner, and
render again. A callback reports intent; it should not create a second hidden
source of truth.
```rust
Checkbox::new("show-hidden")
.checked(self.show_hidden)
.label("Show hidden files")
.on_click(cx.listener(|this, checked, _, cx| {
this.show_hidden = *checked;
cx.notify();
}))
```
Call `cx.notify()` after a mutation that changes rendering. Use `cx.emit(...)`
for a semantic event that an owner should handle, and `cx.subscribe(...)` or
`cx.observe(...)` when the lifetime should follow an entity. Keep returned
subscriptions in an explicit lifetime owner.
### Keep ownership links and subscriptions bounded
When a parent strongly owns a child `Entity`, a child reference back to
that parent must be a `WeakEntity`; a strong back-reference would close an
ownership cycle. `WeakEntity::upgrade()` returns `Option`, and its `update`
returns `Result`, so treat a missing parent as a normal lifecycle outcome.
See [Entity](./entity) for the ownership and weak-access patterns.
Store returned `Subscription` handles on the View or Entity that needs the
callbacks, commonly in `_subscriptions: Vec`. Dropping that
owner then drops the handles and unsubscribes. A local handle dropped at the
end of setup stops the subscription early; `Subscription::detach()` instead
discards the unsubscribe handle and lets the callback continue until its
subscribed Entity goes away. Do not detach a recurring View subscription by
default; repeated detaches against a long-lived emitter can accumulate
callbacks. GPUI's `observe` and `subscribe` use weak subscriber handles
internally, but a callback that additionally captures a strong handle to its
own View can still close a cycle.
When a View remains alive unexpectedly, first inspect strong Entity cycles,
callbacks that capture strong View handles, and detached subscriptions or
[Task](./task)s that outlive their intended owner. A dropped Task handle cancels
unfinished work; it does not itself keep the View alive. These are common places
to start, not an exhaustive list of leak causes.
Do not notify merely because a value was read or derived. Avoid unconditional
notification from `render`; it schedules another render and can create a
permanent redraw loop. When several fields form one invariant, update them
together and notify once. A reusable state type that cannot receive a context
should make that limitation explicit and require its owner to emit/notify.
### Avoid state feedback loops
Text input, selection, filters, and controlled popups commonly have two paths:
an external owner updates the value, and user interaction requests a new value.
Do not send an owner-supplied value back through the user callback during sync.
Track the origin or compare coherent snapshots so each logical change is
reported once. Make callbacks re-entrancy-safe when a callback can synchronously
close, replace, or update the component that invoked it.
## Stable identity
An [`ElementId`](./element_id) is part of behavior. It gives an element stable identity and keys
element-local or component state. A component may also use it as one input to
its own focus, measurement, or animation identity; focus and scrolling are
otherwise owned by their dedicated handles.
- Use stable domain IDs for rows, tabs, tree nodes, and repeated controls.
- Namespace child IDs with their owning object when the same control repeats.
- Never derive identity from a translated label or a mutable list index when
items can be inserted or reordered.
- Do not generate a fresh random ID during `render`.
```rust
Button::new(("delete-project", project.id))
.danger()
.label("Delete")
```
A changed ID means a changed UI identity. Treat that reset as deliberate.
The same rule applies to transition channels, overlay tokens, scroll handles,
and persistence IDs. If two independently retained behaviors share a key, they
can overwrite each other's state; if one behavior changes keys every frame, it
never accumulates state.
## Rendering and composition
Keep `render` declarative: read current state, derive presentation values, and
compose elements. Move domain operations, parsing, and non-trivial mutation to
named methods or services.
```rust
impl Render for ProjectView {
fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement {
div()
.v_flex()
.size_full()
.child(self.render_toolbar(cx))
.child(self.render_content(cx))
}
}
```
Extract a render helper when it names a meaningful region and reduces the
amount of state a reader must hold at once. Extract a new component when the
region has its own reusable contract or retained lifecycle—not merely because
a builder chain is long.
Use GPUI Component's fluent traits consistently (`Sizable`, `Disableable`,
`Selectable`, and component-specific builders). Prefer `.when(...)` and
`.when_some(...)` for small conditional refinements; use ordinary Rust control
flow when branches represent substantially different interfaces.
Compose from the standard semantic component before building a custom surface.
Do not reproduce a menu, select, dropdown, or command palette from generic
`div`s merely to match one screenshot. Reusing the component preserves its
item geometry, focus transfer, keyboard navigation, selection, disabled state,
dismissal, and accessibility contract. If the standard component cannot
express a recurring valid pattern, improve its explicit API instead of styling
arbitrary descendants at each call site.
Render callbacks supplied by application code should be side-effect-free. A
list item renderer, menu builder, or dock panel renderer may run whenever its
owner needs to measure or redraw. It must not perform a business operation,
append data, or register an unbounded subscription.
## Behavior and presentation boundary
The durable Base rule is:
> Base owns reusable behavior and the geometry required to implement it. The
> presentation layer owns the product's visual language.
“Headless” does not mean “one empty `div`.” Popup collision, keyboard
navigation, editing, virtualization, resize arithmetic, focus trapping, and
dock reconciliation require internal structure and state. Moving that work to
every caller would not create flexibility; it would duplicate fragile
behavior.
Conversely, Base must not choose brand colors, typography, density, final
icons, component variants, or application composition. Expose presentation
through `Styled`, typed semantic-state styles, explicit parts, child slots, and
item renderers. Do not inspect arbitrary descendants to discover titles,
descriptions, or close buttons—make semantic parts explicit.
## Theme and styling
Read semantic values from the active theme and apply layout with GPUI's
[`Styled` methods](./style):
```rust
div()
.bg(cx.theme().background)
.text_color(cx.theme().foreground)
.border_1()
.border_color(cx.theme().border)
.rounded(cx.theme().radius)
```
Rules:
- do not hard-code product colors, corner radii, spacing, or control geometry;
- application code must not introduce raw hex, `rgb`/`rgba`, or `hsla`; read a
semantic color from `cx.theme()` or add the missing role to the product theme;
- application layout should use GPUI's rem-based scale helpers (`p_2()`,
`gap_3()`, `w_64()`, `text_sm()`) instead of direct `px(...)` values;
- use semantic tokens for meaning, not palette position;
- keep state-independent geometry in the ordinary builder chain;
- use GPUI `hover`, `active`, `focus`, and `focus_visible` modifiers for
runtime interaction states;
- use a component's semantic state styles for checked, selected, pressed, or
disabled appearance;
- keep popup ownership in explicit state so its trigger can render the open or
pressed appearance until dismissal;
- guard hover/active refinements when a disabled control must not react;
- keep component variants few and meaningful rather than adding a variant for
every call site.
- bind primary styling to the decision area's real default commit and Enter
action; do not derive it from action count, frequency, or toolbar position.
- keep Badge and Alert variants semantic and scarce. Ordinary metadata stays
neutral; do not map every enum case or section to a different color merely
because a variant exists.
The effective precedence is instance style, active semantic value states,
disabled state, then GPUI runtime interaction refinements. Later layers only
replace fields they set.
Prefer `Theme::semantic_tokens()` for new application-owned presentation. The
semantic token surface contains generic color roles plus radius, spacing,
typography, and shadow scales; it deliberately avoids component names. Legacy
component-specific theme values still exist for compatibility but should not
become the extension point for every application widget.
There is one current ownership caveat: `Theme::spacing_tokens()` projects the
default scale, and `Theme::apply_semantic_tokens(...)` does not store custom
spacing or elevation scales. An application that customizes those scales must
retain its own `SemanticThemeTokens` (or narrower design-system state) and make
that state available to its components. Do not write a custom spacing snapshot
into the global theme and expect a later `cx.theme().semantic_tokens()` call to
return it.
Edit the global GPUI Component theme through `Theme::update(cx, |theme| ...)`.
The theme keeps the same colors twice (`colors` as solid colors, `tokens` as
renderable backgrounds that may carry a gradient) and the Base layer holds a
projection for its scrollbars and resize handles; `update` brings all three
back in step after the closure and refreshes every window. An edit through
`Theme::global_mut(cx)` updates only the field you touched — a sidebar can then
paint its text from the new colors and its background from the old tokens —
so it owns the rest: derive `tokens` from `colors`, call `Theme::sync_base(cx)`,
refresh the windows. `Theme::change(...)` performs the projection as part of a
complete theme change.
An outward focus ring needs physical room. An ancestor with
`overflow_hidden()` clips it. Prefer layouts that leave room; if a product must
clip heavily, use the theme's focus-ring policy and retain the focused border
instead of silently hiding all keyboard focus.
### Base font is the application zoom control
The Component Root plugin calls `window.set_rem_size(cx.theme().font_size)` from
its `prepare` hook before the Root surface is rendered. Therefore the theme's
base font is not only body typography; it is the reference length for the
application's rem-based design scale. This deliberately follows the useful part
of Tailwind's model: named type, spacing, and size steps share one relative base
instead of becoming unrelated pixel constants. See the [Tailwind CSS theme
reference](https://tailwindcss.com/docs/theme) for that scale, and the
[Style guide](./style) for GPUI's fluent method names and layout behavior.
Change zoom by updating the base font and refreshing the window:
```rust
Theme::update(cx, |theme| theme.font_size = px(18.));
```
The base font itself is a pixel value because it anchors the scale. Descendant
application UI should normally use relative helpers—`text_sm()`, `gap_2()`,
`px_3()`, `h_8()`, `size_4()`—so type, whitespace, controls, and icons respond
together. A custom component that combines rem-based text with fixed-pixel
padding or icon geometry must document why that part should not zoom.
Treat every direct `px(...)` and raw color constructor in application UI as a
review finding. Accept it only for a documented physical/platform boundary,
measured runtime geometry, raster/data color, or the theme/token definition
itself. Convenience and matching a screenshot are not valid exceptions.
Anything cached from resolved layout must include `window.rem_size()` in its
invalidation key, directly or through a revision that changes with it. This
includes wrapped row heights, text shaping/layout, virtual-list measurement,
popup and dialog geometry, icon sizing derived from text, and custom canvas
metrics. The Command component's variable-height rows are an ecosystem example:
they remeasure when rem changes because the same fixed width wraps differently
at a larger base font.
Do not confuse this application zoom with Dock panel zoom. Dock zoom is a
stateful layout operation that makes one tab group fill the DockArea
while keeping the container chrome and the way back out. It must not modify the
window rem size.
## Events, actions, and focus
Use pointer callbacks for pointer-specific behavior. Use GPUI [Actions](./action) for
commands that should support key bindings, menus, or dispatch from multiple
inputs. Keep action handlers close to the view that owns the command.
Model one logical desktop command once. A toolbar Button, `DropdownMenu` item,
`ContextMenu` item, menu-bar item, and key binding should dispatch the same
Action or call the same owner method instead of copying five mutations. Derive
their label, icon, shortcut, and enabled state from one command policy where
practical, so the entry points cannot disagree. The menu owns navigation and
dismissal; the feature owner still owns whether the command is allowed and
what it does.
Preserve semantic roles in the element choice. Use `Button` for commands even
when the desired treatment is quiet—select `outline`, `ghost`, or an icon
presentation instead of replacing it with `Link`. GPUI Kit applications
reserve `Link` for targets opened by a browser or mail client, such as a URL,
web document, or email address. Use the relevant navigation component for an
in-app destination and `Button`/`Action` for a command. This is a product
convention, not a limitation of `gpui_kit::base::Link`, whose `open_with` seam can
route a destination elsewhere.
Only stop propagation when a nested interaction must prevent its parent from
handling the same event. Blanket propagation stops break menus, selection,
dragging, and window-level commands in ways that are difficult to diagnose.
Bind keys before building the menu bar. `cx.set_menus` reads the keymap at
the moment it is called and bakes each item's shortcut into the native menu,
so a binding registered afterwards never shows next to its menu item and the
item does not react to the key. Call `cx.bind_keys` first, then
`cx.set_menus`; if the keymap changes later (a user keymap file, a locale
switch that rebuilds the menus), call `cx.set_menus` again.
Make focus ownership explicit:
- retain a `FocusHandle` in the entity that owns keyboard interaction;
- register key contexts and actions on the appropriate focused region;
- transfer focus when opening an overlay and restore it on dismissal;
- render a visible `focus_visible` state;
- do not request focus unconditionally from `render`.
A tracked handle is a Tab stop only when it says so: build it with
`cx.focus_handle().tab_stop(true)` (or `.tab_index(n)`), because the element's
own `tab_index`/`tab_stop` settings do not apply to a handle passed to
`track_focus`. A stateless component may create that handle in `render`
through `window.use_keyed_state`;
the keyed state survives re-renders, so the Tab order is stable — this is
what `Button` does.
Attach a `key_context` and its `on_action` handlers to the same focused region.
Bindings are contextual: a registered Action without the intended focus path
is not a working keyboard interaction. Composite widgets should implement the
complete navigation model—arrow movement, Home/End or page movement where
appropriate, confirmation, cancellation, and Tab behavior—rather than a few
isolated shortcuts.
Modal surfaces must trap focus and restore the previous valid focus target on
dismissal. Nested overlays dismiss from the top. Handle rapid close/open
sequences without restoring focus through an intermediate, already-closing
surface.
## Async work and side effects
Start asynchronous work through a [Task](./task) from an event, lifecycle hook, or named method—not as an
unconditional side effect of `render`. Capture weak entities when work should
not keep a closed view alive. When the task completes, update state through the
GPUI context, handle the case where the entity or window no longer exists, and
notify once after the coherent state change.
Keep a recurring task's handle on its owner so dropping the owner cancels it;
do not detach recurring work by default. A one-shot task may call
`Task::detach()` when it must finish independently, but that gives up its
cancellation handle. Handle errors and failed weak-Entity updates explicitly.
Represent async operations with explicit states such as idle, loading, loaded,
and failed. Preserve usable previous data during refresh when possible. Prevent
duplicate destructive submissions and surface recoverable errors in the UI;
do not rely on logs as user feedback.
Use background executors for expensive parsing or computation, but keep GPUI
entity mutation on the appropriate application context. Results can arrive
after the request, document, view, or selection has changed; attach a revision
or identity and reject stale work rather than applying it to new state.
## Layout, measurement, and scrolling
`h_flex` centres its children on the cross axis; `v_flex` leaves flexbox's
default, `stretch`. This is what a row of
controls wants, so a row of icon and label says nothing. It is not what a row
of full-height columns wants: a column placed in a bare `h_flex` does not fill
the row's height, so a column taller than the row is centred and its top —
commonly a header — is clipped off the top of the window, with nothing near the
column to say why. A row whose children are columns says `items_stretch()`:
```rust
h_flex()
.items_stretch()
.size_full()
.child(sidebar)
.child(content)
```
Most UI should use GPUI layout rather than measuring itself. Measurement is a
deep behavior tool for popups, virtualization, editors, resize handles, charts,
and similar components whose correctness depends on resolved geometry.
- Put measurement and geometry in the layer that owns the behavior.
- Observe bounds in prepaint only when ordinary layout cannot express the
relationship.
- Never mutate unrelated application state every prepaint.
- Treat measured data as frame- or revision-scoped; it can become stale after
typography, rem size, width, theme, or content changes.
- Centralize shared geometry such as popup flipping and viewport clamping so
every overlay follows the same edge policy.
For alignment invariants, prefer construction over correction: sibling regions
should consume the same spacing token or shared inset instead of repeating
equivalent literals. Add geometry assertions or visual regression coverage for
critical repeated edges, columns, and gaps. Exercise more than the default
window: rem zoom and display scaling can turn fractional coordinates into a
one-physical-pixel drift even when the default screenshot looks aligned.
Measure the resolved result when reviewing precision, but do not encode a
measured correction as a raw `px(...)` nudge. Trace the mismatch to duplicated
padding, nested insets, border ownership, font metrics, or rounding, then fix
the structural owner.
`h_flex()` and `v_flex()` are not mirror images: `h_flex()` centers its
children on the cross axis, `v_flex()` leaves them stretching. A column placed
in a row therefore takes its content's height, not the row's, and a column
taller than the row is centered — its header is pushed off the top edge and
clipped. Give a full-height column `h_full()`, or give the row `items_start()`
or `items_stretch()`, whenever the child owns a header, a footer, or a scroll
region that must resolve against the row's height.
Every scrollable region must have one owner. In flex layouts, apply
`min_w_0()` or `min_h_0()` to the flexible child that is allowed to shrink.
A flex item only drops its content-based automatic minimum size when its own
overflow is not visible, so an ordinary flexible child refuses to shrink around
long content until you say so. `Scrollable` handles this for its own wrapper,
but a plain `div` between it and the flex container still needs the minimum
released. Avoid accidental nested scrolling; route wheel input to the intended
axis and preserve platform/wasm differences when an API is not portable.
Attach `Scrollable` to the element that owns the full panel, editor, or window
viewport so its scrollbar resolves against the region edge. Put content inset
inside that scroll owner rather than wrapping the scroll owner in a padded
container. A scrollbar floating between content and the panel boundary usually
reveals the wrong scroll owner or padding on the wrong layer.
## Lists, tables, and large data
Use virtualization when data can grow beyond a small, bounded collection. Keep
row identity separate from visible position and avoid cloning the full data set
on every render. Let a stateful list or table own navigation, selection, scroll
coordination, and visible-range calculation while item renderers own row
presentation.
Separate:
- source data and domain IDs;
- filtering/sorting state;
- selection state;
- viewport/scroll state;
- row rendering.
This keeps updates local and prevents the view tree from becoming the data
model.
Virtualization is a behavioral contract, not just a performance switch. Item
measurement must be invalidated when width, typography, rem size, or row
content changes. Keyboard selection and scroll-to-item must operate in model
coordinates even when most elements do not exist in the current frame.
## Public API design
For reusable components:
- constructors should establish valid defaults;
- builders take and return `Self` and use domain language;
- callbacks describe requested changes and include pointer events only when
modifiers or pointer details are meaningful;
- evolvable behavioral seams use private fields, builders for construction,
and readers for inspection;
- boolean readers use `is_` or `has_` where a same-named builder exists;
- non-boolean setters use `with_` when readers need the plain field name;
- explicit compound parts are preferable to inspecting arbitrary descendants;
- adding reusable behavior must not force a product-level visual choice.
Private fields are the default for behavioral state that must evolve without
breaking callers. Public fields are appropriate for deliberately record-like
configuration, theme tokens, geometry, and serialized schemas. Every public
struct with public fields must carry `#[non_exhaustive]`. Provide constructors,
`Default`, or builders so callers can create values without exhaustive struct
literals. This preserves the ability to add fields without breaking callers.
Apply this rule to new types and public API changes; unrelated existing types
can be migrated separately.
Keep public module paths stable while reorganizing internals: use a module seam
with deliberate re-exports so folders can change without forcing downstream
imports to change. Prefer platform control terminology and established project
naming over web-framework vocabulary.
## Platform and capability boundaries
Do not assume every native or web target supports the same facility. Window
decorations, accessibility bridges, system notifications, clipboard behavior,
scroll gestures, fonts, and timing can differ. Put platform-specific code
behind a narrow capability seam and define the fallback behavior.
A platform branch must preserve the semantic contract even if presentation
differs. For example, a system notification may have different retraction
support, but the application still needs a coherent delivery state. Test both
the shared state machine and the platform adapter where possible.
## File and naming conventions
- Name views and entities after product concepts: `ProjectList`,
`ProjectEditor`, `SettingsState`.
- Name event handlers after intent: `confirm_delete`, `open_project`,
`on_query_changed`.
- Keep one main responsibility per module; split a file when state ownership or
lifecycle can no longer be understood without reading unrelated behavior.
- Keep component module, state, events, and focused tests together when they
change together.
- Document invariants and surprising lifecycle constraints; do not narrate
obvious builder calls.
- Use `rustfmt` and satisfy the workspace's Clippy rules. Avoid broad `allow`
attributes that conceal unrelated warnings.
### Vocabulary is part of the API
Use the same word for the same concept across components. Before naming a new
method, search GPUI, `gpui-base`, and GPUI Component for the established term;
prefer macOS/Windows control terminology where the ecosystem has no precedent.
Localized documentation preserves exact API identifiers and established UI
framework terms when translation would reduce precision. Format identifiers as
code, explain retained terms when needed, and do not mix languages merely to
make ordinary prose sound technical.
| Concept | Naming pattern | Example |
| --- | --- | --- |
| Value-like rendered control | noun | `Button`, `Checkbox`, `Tab` |
| Retained behavioral model | `State` | `InputState`, `TableState` |
| Imperative shared reference | `Handle` | `DialogHandle`, scroll handle |
| Semantic notification | `Event` | `TableEvent`, `SelectEvent` |
| Keyboard command | verb or intent noun | `Confirm`, `Cancel`, `SelectNext` |
| Pluggable data/behavior owner | `Delegate` / `Provider` | `TableDelegate`, `CompletionProvider` |
| Application-supplied presentation | `render_` or `_renderer` | `render_item` |
| Construction | `new`, or a semantic constructor | `new`, `horizontal`, `vertical` |
| Fluent property | noun/adjective | `label`, `disabled`, `selected`, `placement` |
| General non-boolean replacement builder | `with_` | `with_size`, `with_mode` |
| In-place mutation | `set_` | `set_items`, `set_selected_index` |
| Boolean reader | `is_` / `has_` | `is_open`, `is_closable`, `has_selection` |
| Plain value reader | field noun | `placement`, `selected_value` |
| Callback registration | `on_` | `on_click`, `on_open_change` |
| Rendering a named region | `render_` | `render_toolbar`, `render_content` |
For new APIs, fluent builders omit `set_` because they consume and return
`Self`; mutation through `&mut self` uses `set_`. Preserve established public
names when changing them would cause needless churn. Existing builder names
such as `set_position` are compatibility exceptions, not patterns for new APIs.
A boolean reader is either `has_`, when the value holds something, or
`is_`, when it describes a state or a permission. Reach for the
adjective whenever the action has one: `is_closable` over `can_close`,
`is_zoomable` over `can_zoom`, `is_copyable` over `can_copy`. When the action
is a verb phrase with no adjective form, name the thing it needs instead:
`has_definition`, not `can_go_to_definition`. Do not add new `can_` readers.
Boolean builders may use the field name (`disabled(bool)`) while their readers
use `is_disabled()`. For a public seam struct containing non-boolean fields,
use `with_item_ix(...)` for construction and `item_ix()` for reading so setter
and getter names never collide. Prefer `_ix` for new local or internal
zero-based indices, preserve established public terms such as `selected_index`,
and do not introduce `_idx`. If callers never construct the seam value, do not
publish a builder merely for symmetry.
### Let the enclosing name carry the context
A name is read inside something. A field is read inside its type and a parameter
inside its method, so neither repeats what encloses it: `with_item_ix(ix)`, not
`with_item_ix(item_ix)`.
Keep one type's fields at the same level of abbreviation. A single field spelled
out in full becomes the odd one out, and a reader goes looking for the
distinction that made it different. Because a builder is named `with_`,
shortening a field shortens its builder with it and the pair stays matched.
Shorten only where the enclosing name really does disambiguate. When a short
form is also the established term for a *different* quantity elsewhere in the
ecosystem, say which one you mean in the doc comment rather than lengthening the
identifier — the doc is read at the call, and it can explain what a longer name
could only hint at.
### Use precise domain words
- **selected** is persistent membership or the active item; **focused** is the
current keyboard target; **hovered** is pointer presence; **confirmed** is an
activation result. Never use them interchangeably.
- **open/close** describes an overlay or disclosure state; **show/hide** is for
transient presentation requests; **expand/collapse** describes structure.
- **disabled** prevents interaction; **readonly** permits navigation and
selection but prevents editing; **loading** prevents duplicate work while an
operation is pending. Spell the state `readonly` — one word, as the
`readonly(bool)` builder and `is_readonly()` reader do — in identifiers,
interface labels, and documentation alike; never `read-only` or `read only`.
- **index** is a current positional coordinate; **id** is stable identity;
`IndexPath` represents hierarchical position. Do not persist or key
reorderable data by index.
- **value** is controlled domain data; **presentation** is a read-only snapshot
prepared for rendering; **state** is retained behavior.
- **placement** is a side or anchor policy; **position** is resolved geometry.
- **size** is a semantic control tier; **width/height/bounds** are geometry.
- **child/children** follows GPUI composition; named slots such as `header`,
`footer`, `trigger`, and `content` carry additional semantics.
Avoid vague public names such as `data`, `item2`, `handle_action`, `update_ui`,
`process`, `manager`, or `config` when a narrower domain term exists. `Manager`
is appropriate only when a type truly coordinates a collection or lifecycle,
as `ToastManager` does.
### Type and module style
- Rust types and Actions use `UpperCamelCase`; modules, functions, methods,
fields, and local variables use `snake_case`; constants use
`SCREAMING_SNAKE_CASE`.
- A module named after a component owns its public seam. Internal folders may
split state, element, geometry, platform adapter, and tests without leaking
those folder names into imports.
- Use singular module names for one component concept and established
ecosystem names for families (`input`, `table`, `dock`).
- Suffix type-erased wrappers with `Any` only when they erase a real type
boundary, such as `AnyInputState` or `AnyElement`.
- Suffix identifiers with `Id`, zero-based indices with `ix`, and collections
with meaningful plurals. Do not alternate `idx`, `index`, and `ix` in one
subsystem.
- Name predicates positively when possible. A positive `enabled`/`visible`
contract is easier to compose than multiple negatives, but preserve
established API terms such as `disabled` where they match control semantics.
### Callback and event wording
Use `on_click` only for a genuine click-level contract. A controlled semantic
primitive in Base should prefer `on_change(next_value, ...)`; a styled
compatibility component may retain `on_click` when pointer details or existing
API expectations matter. Do not invent a `ClickEvent` for a model-driven
change.
Name before/after lifecycle hooks precisely. `on_will_change` can veto or
prepare; `on_change` observes a requested/current value contract; `on_confirm`
commits a choice; `on_dismiss` closes a transient surface. Document whether a
callback runs before internal state changes, after them, or instead of them,
and whether it may synchronously re-enter the component.
### Documentation and copy style
Public docs should begin with what a type does and who owns its state. Examples
must use current, compilable APIs and show stable IDs. Document defaults,
platform limitations, focus behavior, callback ordering, and any requirement
to call `notify`, `emit`, or a theme synchronization method.
Follow the [interface-language rules](/docs/design-guides#interface-language) for
labels, commands, confirmation dialogs, capitalization, and ellipses. Keep one
canonical term for each domain object, command, and state.
Translation keys describe stable intent (`dialog.delete_project.title`), not a
source-language sentence or a screen coordinate. Never assemble a sentence
from translated fragments or reuse one key for meanings that happen to share
the same English text.
Localize intent, not syntax. Give every locale control over word order,
pluralization, punctuation, and the amount of context it needs. Review strings
inside the component and with realistic data. Tests or linting should catch
missing keys, unintended CJK text in English resources, three-dot ellipses,
unreviewed ALL CAPS, and inconsistent fixed terms; human review still decides
whether repetition is justified by context. Verify every string inside its
component with realistic content, text expansion, and application zoom.
## Testing strategy
Test at the lowest layer that can prove the behavior:
1. pure tests for state transitions, geometry, parsing, and ordering;
2. GPUI context tests for entities, events, and subscriptions;
3. `VisualTestContext` interaction tests for focus, keyboard, pointer, layout,
and rendered state;
4. example or application smoke tests for complete workflows.
For an interactive component, cover the semantic contract rather than its
implementation details: pointer and keyboard activation, controlled value
changes, disabled behavior, focus movement, event count/order, stable identity,
and important empty or failure states. Add a regression test before fixing a
bug whenever the failure can be reproduced deterministically.
For UI behavior that depends on the real window system, test through the
accessibility tree by role, label, value, enabled state, focus, and selection.
Re-read the tree after every state-changing action because element indexes are
snapshots. Use screenshots for visual facts the semantic tree cannot express;
use coordinate input only as a fallback. Report automated and manual evidence
separately.
## Performance rules
- Do not mutate state or notify unconditionally in `render`.
- Avoid rebuilding entities, subscriptions, focus handles, and expensive data
structures per frame.
- Notify the narrowest owning entity after a coherent state change.
- Virtualize long collections and render only the visible range.
- Avoid cloning large strings or collections solely to satisfy a closure;
capture stable handles or shared data.
- Measure before adding caches. A cache must have a clear invalidation owner.
- Keep animation work bounded and honor reduced motion.
## Common failure modes
Avoid these patterns:
- one entity containing the entire application's unrelated state;
- business logic and network requests embedded in a long `render` method;
- random or index-based `ElementId` values for reorderable content;
- literal colors and radii that break custom themes;
- custom clickable `div`s where a semantic component already supplies focus,
keyboard, disabled, and accessibility behavior;
- duplicated local state that drifts from a controlled model value;
- `cx.notify()` loops caused by mutation during every render;
- nested scroll containers without explicit ownership;
- a new component variant for a one-off screen;
- confirmation dialogs for reversible, low-risk actions;
- tests that call internal methods but never exercise keyboard or pointer
behavior.
## Rules for coding agents
Before editing, an agent must read the nearest implementation, its tests, the
re-export seam, and the relevant component documentation. It must search the
current source for signatures instead of translating a React, CSS, or old GPUI
example by analogy.
For each change, the agent should be able to name:
1. the behavior owner and presentation owner;
2. the retained identity and state lifecycle;
3. the pointer, keyboard, focus, and accessibility contract;
4. the layout and overflow owner;
5. the theme tokens and intentional exceptions;
6. the test that would fail if the behavior regressed.
Generated code must be reviewed and tested by a person. “Compiles” is not a UI
quality bar, and a broad refactor that merely makes generated code look tidy is
not a substitute for matching the repository's architecture.
## Implementation checklist
Before opening a change for review, confirm that:
- state and side-effect ownership are explicit;
- `RenderOnce` versus `Entity` is chosen deliberately;
- repeated elements have stable domain-based IDs;
- theme tokens and component sizes replace isolated visual literals;
- keyboard actions, focus, disabled state, and overlays work together;
- loading, empty, error, and cancellation paths are represented;
- long data sets use an appropriate virtualized component;
- public API additions preserve dependency direction and encapsulation;
- tests prove behavior at the appropriate layer;
- formatting, Clippy, targeted tests, and relevant examples pass.
See [Getting Started](/docs/getting-started) for application setup and the
component pages for current API details.
---
# Entity
Source: /docs/entity
When several Views, handlers, or async tasks need the same state, put that state in GPUI's [`Entity`][Entity]. A Chat, for example, can keep its messages in an `Entity`; any code holding a clone can access the same Chat through a GPUI [Context](./context).
Create the Entity with `cx.new`, read it with `read`, and change it with `update`. If `Chat` implements [`Render`](./render), its `Entity` can also render directly as a View. Otherwise, it works as a shared state model.
```text
Entity
├── read(cx) → &Chat
├── update(cx, …) → &mut Chat + Context
└── downgrade() → WeakEntity
```
Cloning an Entity copies its handle, not the state inside it. GPUI increments a synchronized strong-handle count; it does not clone `T` or collections inside it. A handle clone is much smaller than a deep model clone, but it is not free, and every retained clone extends the Entity's lifetime. Entity access always goes through a GPUI context, allowing GPUI to coordinate updates, rendering, subscriptions, and the Entity lifecycle.
## Data model or persistent View
`Entity` is a state and identity handle; `T` does **not** need to implement `Render`. A data-only Entity can hold a model or component state, and several owners can read or update it. When `T` **does** implement `Render`, its `Entity` can also be placed in the element tree as a persistent View. The View's Entity survives while the elements returned by `render` are rebuilt for drawing.
```text
Entity (model; no Render)
↑ read/observe
Entity (Render View)
↓ render
element tree
```
```rust
use gpui_kit::*;
struct MessageStore {
messages: Vec,
}
struct MessagePanel {
store: Entity,
_subscription: Subscription,
}
impl Render for MessagePanel {
fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement {
let count = self.store.read(cx).messages.len();
div().child(format!("{count} messages"))
}
}
// In a GPUI context, outside render:
let store = cx.new(|_| MessageStore { messages: vec![] });
let panel = cx.new(|cx| {
let _subscription = cx.observe(&store, |_, _, cx| cx.notify());
MessagePanel { store: store.clone(), _subscription }
});
store.update(cx, |store, cx| {
store.messages.push("Hello".into());
cx.notify();
});
// Add `panel` as a child View; `store` is data, not an element.
```
The model's `notify()` reaches `MessagePanel` through the stored observation, which notifies the View in turn. This explicit connection also keeps the View current if it is later placed behind a cache boundary. `RenderOnce` is another route to elements: it describes a value-like component consumed during rendering and does not, by itself, create or require an Entity. See [RenderOnce](./render-once) for that lifecycle.
### Try it: one model, one observing View
From the repository root, replace `examples/hello_world/src/main.rs` with this complete example, then run `cargo run -p hello_world`. It uses the existing example package and requires no new dependency. Save the original file first if you want to restore the Hello World example afterward.
```rust
use gpui_kit::base::StyledExt;
use gpui_kit::component::button::Button;
use gpui_kit::*;
struct MessageStore {
count: usize,
}
struct MessagePanel {
store: Entity,
_subscription: Subscription,
}
impl MessagePanel {
fn new(cx: &mut Context) -> Self {
let store = cx.new(|_| MessageStore { count: 0 });
let _subscription = cx.observe(&store, |_, _, cx| cx.notify());
Self {
store,
_subscription,
}
}
}
impl Render for MessagePanel {
fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement {
let count = self.store.read(cx).count;
div()
.v_flex()
.gap_2()
.size_full()
.items_center()
.justify_center()
.child(format!("Messages: {count}"))
.child(
Button::new("add-message")
.label("Add message")
.on_click(cx.listener(|this, _, _, cx| {
this.store.update(cx, |store, cx| {
store.count += 1;
cx.notify();
});
})),
)
}
}
fn main() {
gpui_kit::application().run(|cx| {
gpui_kit::init(cx);
gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| {
cx.new(MessagePanel::new)
})
.expect("failed to open window");
});
}
```
The window starts at **Messages: 0**. Each click changes the model Entity through `update`; its `notify()` schedules the stored observer, which calls `notify()` on the panel. The next render reads the same model handle, so the label becomes **Messages: 1**, then **Messages: 2**. The model has no `Render` implementation and is never added to the element tree. The panel retains both the model handle and the `Subscription`; recreating either in `render` would lose the stable relationship.
If the count stays at zero, check both notification calls and the `_subscription` field. Removing the model's `notify()` leaves the observer without a signal; dropping the subscription after `new` cancels the observation. When changing the example, keep `store.update(...)` in the click handler and `store.read(cx)` in `render`, outside any active update of that same store.
## Create an Entity
Use `cx.new` in any GPUI context:
```rs
use gpui_kit::*;
struct Chat {
messages: Vec,
}
let chat: Entity = cx.new(|_cx| Chat {
messages: Vec::new(),
});
let same_chat = chat.clone();
assert_eq!(chat.entity_id(), same_chat.entity_id());
same_chat.update(cx, |chat, cx| {
chat.messages.push("Hello".into());
cx.notify();
});
assert_eq!(chat.read(cx).messages.len(), 1);
```
The closure receives [`Context`](./context), so initialization can also create child entities or register subscriptions. Both handles in the example refer to the same `Chat`: an update through one is visible through the other. Cloning the handle performs synchronized ownership bookkeeping, but avoids copying the model and its messages. Reuse a handle when ownership or a callback needs it; avoid cloning it repeatedly inside a hot loop without a reason. [`SharedString`](./shared-string) also makes sharing text inexpensive, through a different storage mechanism.
An owner keeps a strong `Entity` when the child should live as long as the owner:
```rs
struct Workspace {
chat: Entity,
}
impl Workspace {
fn new(cx: &mut Context) -> Self {
let chat = cx.new(|_cx| Chat {
messages: Vec::new(),
});
Self { chat }
}
}
```
This strong ownership pattern appears throughout GPUI Kit: a parent View owns the child Views or models it renders and coordinates.
### Pass a handle across a feature
A feature can keep its business data in one model Entity and pass cloned `Entity` handles to its panels, commands, and tasks. Those owners refer to **the same state**; they do not each receive a copy of a large message list or document. This makes a typed Entity handle a useful boundary between modules of one feature. A clone still updates GPUI's strong-handle count and extends the model's lifetime, so use `WeakEntity` for back references or work that should not retain it.
Keep document, workspace, and other feature-specific state owned by that feature's Entity. Use a [`Global`](./global) for a value or service whose scope really is the whole application, such as a shared preference read by multiple features or windows. A Global is one app-wide slot for its type; it is not a shortcut for passing a per-document Entity. Views also need an explicit global observation when changes must update their output.
Across crates, expose the feature's intended handle, command, or event through its public boundary rather than making sibling features depend on its internal model fields. Move a capability into a shared crate only when it has a coherent contract and more than one real owner. The [Coding Guides](./coding-guides) cover the full feature and crate layout.
### Retain component state outside `render`
GPUI Kit's stateful components follow the same rule. Construct an input's state once, store its Entity, and pass the handle to the value-like `Input` element on each render:
```rust
use gpui_kit::*;
use gpui_kit::component::input::{Input, InputState};
struct SearchPane {
query: Entity,
}
impl SearchPane {
fn new(window: &mut Window, cx: &mut Context) -> Self {
let query = cx.new(|cx| InputState::new(window, cx));
Self { query }
}
}
impl Render for SearchPane {
fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement {
div().child(Input::new(&self.query))
}
}
```
Creating `InputState` inside `render` would reset the draft, selection, and focus-related behavior as the View redraws. The `Input` value can be rebuilt; its state Entity should survive.
## Read state
Use `read` for direct, synchronous access:
```rs
let message_count = chat.read(cx).messages.len();
```
The returned reference is tied to `cx`; copy or clone the value you need instead of trying to store the reference.
Use `read_with` when code has a generic `AppContext`, or when a closure makes the read boundary clearer:
```rs
let last_message = chat.read_with(cx, |chat, _cx| {
chat.messages.last().cloned()
});
```
`read` takes `&App` (a `Context` dereferences to `App`). `read_with` accepts any `AppContext` and returns what its closure produces. Neither creates a second copy of the Entity. Keep either read short: an Entity already leased for an update or render cannot be read again until that access ends. Do not keep an `&T` across a later update or an `await`; extract an owned value before leaving the read.
## Update state
Use `update` to obtain mutable state and its `Context`:
```rs
chat.update(cx, |chat, cx| {
chat.messages.push("Hello".into());
cx.notify();
});
```
`cx.notify()` reports that this Entity changed. Views that rendered or observed it can then update. Mutation alone does not imply a notification, so call it when the new state should be reflected by observers or rendering.
An `update` closure may return a result. This lets a handler change one Entity, finish that borrow, and then use the result to update another without creating a callback cycle:
```rust
let new_count = chat.update(cx, |chat, cx| {
chat.messages.push("Hello".into());
cx.notify();
chat.messages.len()
});
// The Chat update has ended here; `new_count` is an ordinary usize.
```
Always use the inner `cx` passed to the update closure. It is the `Context` for the Entity currently being updated.
Do not call `read` or `update` on an Entity while that **same** Entity is already being updated or rendered. GPUI prevents re-entrant access and will panic. Updating a different Entity inside an update can work, but an observer or callback that reaches back to the first Entity creates the same problem indirectly. Use the `&mut T` already provided by the callback, copy out a small result, or finish the first update before starting the next. This also applies to deferred callbacks that run with their owning Entity already borrowed.
## Use a WeakEntity for back references and callbacks
Cloning `Entity` creates another strong handle and keeps the Entity alive. That matters when a parent View owns a child Entity and passes its **strong** `Entity` to the child. If the child stores it, both sides own each other:
```text
outside owner → ParentView ──strong──→ ChildView
↑ │
└──────strong────────┘
```
Dropping the outside owner does not remove either remaining strong handle, so neither Entity is released. Passing a parent handle to a child is safe when the child only uses it temporarily; the cycle appears when the child retains that strong handle while the parent retains the child. Store a [`WeakEntity`][WeakEntity] for the back reference instead:
```rust
use gpui_kit::*;
struct ParentView {
child: Option>,
clicks: usize,
}
struct ChildView {
parent: WeakEntity,
}
impl Render for ParentView {
fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement {
div()
.child(format!("Clicks: {}", self.clicks))
.children(self.child.clone())
}
}
impl Render for ChildView {
fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement {
div().child("Notify parent").on_click(cx.listener(|this, _, _, cx| {
let Some(parent) = this.parent.upgrade() else { return; };
parent.update(cx, |parent, cx| {
parent.clicks += 1;
cx.notify();
});
}))
}
}
// In a GPUI context, outside render:
let parent = cx.new(|_| ParentView { child: None, clicks: 0 });
let child = cx.new(|_| ChildView { parent: parent.downgrade() });
parent.update(cx, |parent, cx| {
parent.child = Some(child);
cx.notify();
});
```
Now the parent owns the child, but the child's link back does not keep the parent alive: `ParentView ──strong──→ ChildView ──weak──→ ParentView`. `upgrade()` returns `Option>`; when the parent has gone away, the click handler simply returns. The handler runs after rendering, so it can update the parent without re-entering the parent's `render` borrow. GPUI Kit's nested popup menus use the same strong child, weak parent ownership direction.
A weak handle may outlive its target. Upgrade it, or use its fallible access methods:
```rs
let workspace = cx.weak_entity();
cx.spawn(async move |_, cx| {
let conversations = load_conversations().await;
workspace
.update(cx, |workspace, cx| {
workspace.set_conversations(conversations);
cx.notify();
})
.ok();
})
.detach();
```
`WeakEntity::upgrade` returns `Option>`; `read_with` and `update` return a `Result` because the Entity may already have been released. GPUI Kit uses this pattern for async tasks, callbacks, delegates, and parent references so those relationships do not accidentally keep a View alive.
`Context::spawn` already supplies a `WeakEntity` as the first async argument. Hold the returned [`Task`](./task) in the owning Entity when dropping that owner should cancel the work; call `.detach()` when it should continue independently. In either case, handle a failed weak update after an `await` as normal cancellation, since the View may have closed while the work was running.
Retained closures can close an ownership cycle too, but only when their owner chain leads back to a strongly captured Entity. GPUI's `cx.observe`, `cx.subscribe`, and `cx.listener` use weak subscriber/View handles internally; storing one of their `Subscription`s does not by itself make a strong Entity cycle. An additional `move` capture of a strong Entity can still complete that cycle. `cx.processor` differs from `cx.listener`: it captures a strong handle to its View, so do not store its closure back in that same View without breaking the ownership loop. When investigating retained state, check both strong Entity cycles and subscriptions or tasks whose lifetime was detached from their intended owner; these are common ownership paths to inspect, not an exhaustive list of causes.
## Observe changes and subscribe to Events
An Entity can coordinate with another Entity in two related ways:
- `cx.observe(&entity, ...)` runs when that Entity calls `cx.notify()`. Use it when only “this state changed” matters.
- `cx.subscribe(&entity, ...)` receives a typed [Event]. Use it when the meaning and payload of the change matter.
They are separate signals: `notify()` does not emit an Event, and `emit(event)` does not by itself notify renderers. A state change that needs both a redraw and a semantic event can do both deliberately, usually once each. An observer can inspect the observed Entity with the handle it receives; it must still avoid re-entering an Entity already borrowed by the callback chain. GPUI delivers these callbacks through its effect cycle, after the current update's borrow has ended; do not rely on the callback having run inside the `update` closure.
Store the [`Subscription`](https://docs.rs/gpui-pre/{{gpui_pre_version}}/gpui/struct.Subscription.html) returned by `observe` or `subscribe` on the subscribing Entity, in a `_subscription` field or a `_subscriptions: Vec` field:
```rs
enum ChatEvent {
MessageSent,
}
impl EventEmitter for Chat {}
impl Chat {
fn send_message(&mut self, cx: &mut Context) {
self.messages.push("Hello".into());
cx.notify();
cx.emit(ChatEvent::MessageSent);
}
}
struct Workspace {
chat: Entity,
sent_count: usize,
_subscriptions: Vec,
}
impl Workspace {
fn new(cx: &mut Context) -> Self {
let chat = cx.new(|_cx| Chat {
messages: Vec::new(),
});
let _subscriptions = vec![
cx.observe(&chat, |_workspace, _chat, cx| {
cx.notify();
}),
cx.subscribe(&chat, Self::on_chat_event),
];
Self {
chat,
sent_count: 0,
_subscriptions,
}
}
fn on_chat_event(
&mut self,
_chat: Entity,
_event: &ChatEvent,
cx: &mut Context,
) {
self.sent_count += 1;
cx.notify();
}
}
```
Calling `chat.update(cx, |chat, cx| chat.send_message(cx))` now sends both signals: the observer invalidates `Workspace` for the model change, while the Event subscriber increments `sent_count`. This is the pattern used by GPUI Kit Views. Dropping `Workspace` also drops `_subscriptions` and cancels its callbacks. Dropping a local `let subscription = ...` at the end of the function **cancels too early**; it is not a leak. `Subscription::detach()` has the opposite effect: it consumes the handle without canceling the registration, which can remain until the subscribed Entity is dropped. Repeatedly detaching subscriptions to a long-lived Entity can accumulate callbacks and captured resources. Detach only when that longer lifetime is intended; it differs from `Task::detach()`, which lets asynchronous work continue independently. A callback's strong Entity capture becomes a cycle only if the ownership chain leads back to the owner.
`Context::observe` and `subscribe` use a weak handle to the subscriber, so the registration does not itself own that View. Keep the `Subscription` in the subscriber for an explicit lifetime. If both callbacks above call `cx.notify()` for one operation, GPUI can coalesce invalidations, but decide whether both responses are actually needed.
See [Event] for `EventEmitter`, `emit`, and typed subscription design.
## Lifecycle
An Entity remains alive while at least one strong `Entity` handle exists. Dropping the final strong handle makes `WeakEntity::upgrade()` fail. GPUI then runs release callbacks and drops the state during its effect cycle; do not depend on the state's destructor or a release callback running synchronously at the `drop(handle)` statement.
Most cleanup should follow normal ownership:
- own child entities with `Entity`;
- use `WeakEntity` for non-owning links;
- keep View-level subscriptions in the same View's `_subscription` or `_subscriptions` field;
- let dropping the View release its subscriptions and captured resources.
For integration code that needs access to state before GPUI drops it, [Context](./context) also provides `cx.on_release(...)` for the current Entity and `cx.observe_release(...)` for another Entity. Both callbacks run when GPUI processes the release; `observe_release` runs only while its subscriber still exists. Store the returned subscriptions for exactly as long as the release callback is needed.
### Try it: shared handles and a weak lifetime
Add this test to a package that enables `gpui-kit`'s `test-support` feature (see [Testing](./test)). It needs no window. The assertions check that a clone shares state, one remaining strong handle keeps it alive, and the final drop invalidates the weak handle:
```rust
use gpui_kit::{AppContext, TestAppContext};
struct Counter {
value: usize,
}
#[gpui_kit::test]
fn handles_share_state_and_control_lifetime(cx: &mut TestAppContext) {
let counter = cx.new(|_| Counter { value: 0 });
let another_owner = counter.clone();
let weak = counter.downgrade();
another_owner.update(cx, |counter, cx| {
counter.value += 1;
cx.notify();
});
assert_eq!(counter.read(cx).value, 1);
drop(counter);
assert!(weak.upgrade().is_some());
drop(another_owner);
assert!(weak.upgrade().is_none());
}
```
As a second check, use the `Chat`/`Workspace` example above: update Chat once with `notify()` alone and once with `emit(ChatEvent::MessageSent)` alone. Predict which call changes `sent_count`, then assert it in a `#[gpui_kit::test]`. Retain `_subscriptions` on `Workspace`; otherwise the test checks an already canceled listener.
## Entity identity and view caching
An `Entity` can be embedded directly as a child View. Its `EntityId` gives that View a stable identity across frames; `cx.notify()` invalidates the views that display it. The state persists in the Entity, while the ordinary element tree is rebuilt for a draw. Keeping an Entity is therefore different from caching its rendered subtree.
For an expensive child that often stays unchanged while its parent redraws, GPUI also exposes `child.clone().cached(style)` and the equivalent `AnyView::cached(style)`. The parent must retain the same child Entity, and `style` must provide a definite outer size because GPUI can skip rendering the contents during layout. A clean cached child may replay its previous subtree; notifications, changed bounds or inherited drawing context cause a rebuild. See [View Cache](./view-cache) for the exact boundary and how it differs from element state and virtualization.
[Entity]: https://docs.rs/gpui-pre/{{gpui_pre_version}}/gpui/struct.Entity.html
[WeakEntity]: https://docs.rs/gpui-pre/{{gpui_pre_version}}/gpui/struct.WeakEntity.html
[Event]: ./event.md
---
# I18N
Source: /docs/i18n
# I18n
GPUI Component ships component strings in `crates/component/locales/ui.yml`, including `en`, `zh-HK`, `zh-TW`, and `it` entries. Coverage varies by key. Your application owns its own copy, locale policy, and formatting; it can add or override component translations without copying that file.
This feature requires [rust-i18n](https://github.com/longbridge/rust-i18n) 4.2 or later.
## Add the dependency
Add `rust-i18n` to the application crate that owns your locale files:
```toml
[dependencies]
gpui-kit = "{{gpui_kit_version}}"
rust-i18n = "4.2"
```
## Create an application locale file
Create `locales/ui.yml` in your application crate. Put component translations under the `gpui_component` namespace:
```yaml
_version: 2
app:
save:
en: Save
fr: Enregistrer
gpui_component:
Calendar:
week.0:
fr: Di
month.January:
fr: Janvier
DatePicker:
placeholder:
fr: Sélectionner une date
```
Keep your own keys outside `gpui_component`. That namespace must match the Rust crate name `gpui-component` with hyphens converted to underscores. Copy key names exactly from [GPUI Component's built-in locale file](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/locales/ui.yml); for example, `month.January` is one nested key under `Calendar`.
## Register the extension
Initialize the application's locales at the crate root:
```rust
rust_i18n::i18n!("locales", fallback = "en");
```
Register them before initializing GPUI Component:
```rust
app.run(move |cx| {
use gpui_kit::component as gpui_component;
rust_i18n::extend!(gpui_component);
gpui_kit::init(cx);
// Open windows and initialize the rest of the application.
});
```
Call `extend!` only once during application startup. The local story app uses this alias and ordering in [`crates/story/src/lib.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story/src/lib.rs).
## Lookup priority
The application's translations take priority over the component's built-in translations:
```text
Application locales (locales/ui.yml)
│
│ key not found
▼
GPUI Component built-in locales
```
This deep-merge behavior means that:
- A new locale, such as `fr`, can contain only the keys your application needs.
- A translation with the same locale and key overrides the built-in value.
- Keys not supplied by the application continue to use the built-in value.
- Future GPUI Component translations remain available without being copied into the application.
For example, defining only `gpui_component.Calendar.month.January.en` changes January's English label while all other English calendar labels still come from GPUI Component.
### The namespace applies to component lookups only
`extend!` changes how GPUI Component resolves _its_ keys. It does not give the application's own `t!` calls access to the built-in translations:
```rust
// Inside a GPUI Component component: application value first, built-in second.
t!("Calendar.month.February") // -> "February"
// In application code: reads the application's locale files only.
t!("gpui_component.Calendar.month.February") // -> the key, unless you defined it
```
Read component strings by rendering the component, not by looking the key up yourself.
For application copy, call `rust_i18n::t!("app.save")` from the application crate. Keep the value in a complete message key so each language can choose its own word order and punctuation. A component's translated placeholder does not translate the surrounding application title, label, or menu item.
## Select a locale
`gpui-component` re-exports the locale accessors, so an application does not have to reach for `rust-i18n` to switch languages:
```rust
use gpui_kit::component::{locale, set_locale};
set_locale("fr");
let current = locale();
```
Components then resolve their translated labels using the selected locale and the priority described above.
The active locale is global state outside GPUI's change tracking. Changing it alone does not schedule a repaint. If a view handles its own language picker, notify that view after switching:
```rust
use gpui_kit::component::set_locale;
set_locale("fr");
cx.notify();
```
See [Context](./context) for how `cx.notify()` schedules a View update.
If an application-wide action changes the locale from `&mut App`, refresh the windows that show translations:
```rust
use gpui_kit::{App, component::set_locale};
fn select_locale(language: &str, cx: &mut App) {
set_locale(language);
cx.refresh_windows();
}
```
Rebuild other cached presentation explicitly. For example, the story app's [`SelectLocale` handler](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story/src/app_menus.rs) rebuilds its native and in-window menus after changing the language; their checked state is not a live translation lookup. The story's menu labels themselves are currently fixed strings, so the language picker demonstrates component translations and selection state, not a fully localized gallery.
## Text and layout across scripts
Translation changes the string; the text system then resolves fonts, shapes glyphs, and measures the result. CJK or mixed Latin/CJK text needs a font with the actual glyphs. A font family that loads can still lack them, and a fallback face can change line breaks or control height. See [Fonts](./fonts) and [TextSystem](./text-system). Check long translations, narrow buttons, input placeholders, and text at application scale on each target platform. Measure custom text with shaped layout rather than character counts.
GPUI Kit does not expose an application-wide RTL layout switch through this locale API. Setting an Arabic or Hebrew locale will select strings, but it does not automatically mirror navigation, icon direction, alignment, focus order, or pointer and keyboard behavior. Review those parts of each screen for the target language, and test bidirectional text and editing on the target platform. Treat text shaping and full interface mirroring as separate checks.
## Format data in the application
`rust-i18n` selects message strings here; `set_locale` does not format dates, numbers, currencies, units, or plurals for your domain data. Choose a locale-aware formatter appropriate to your application and pass its finished output into your views. Keep variable placement in a whole translated message so a translator can reorder it. Do not assemble a sentence from separately translated fragments or assume `format!("{value}")` applies locale-specific separators. A date picker can translate its own month names while a date shown elsewhere in the app still needs your formatting policy.
## Verify the integration
The repository's story crate provides a working extension and French overrides. From the repository root, run `cargo run -p gpui-component-story`, open the **Language** menu, and select **Français**. Open a calendar or date picker story: its month names and date placeholder use the `fr` values in [`crates/story/locales/ui.yml`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story/locales/ui.yml). Switch back to English and check the same controls. The menu checkmark should follow the choice. You can also run `cargo test -p gpui-component-story --features test-support extends_component_translations_with_story_locales` to exercise the story's translation layering test.
## Common mistakes
| Symptom | Check |
| --- | --- |
| Component still shows an old language | Call `set_locale`, then notify the owning view or refresh windows; rebuild cached menus or labels. |
| Override does not appear | Put the key under `gpui_component`, match the built-in spelling and locale code, and register `extend!` before `gpui_kit::init`. |
| Application `t!` returns a key | Define that key in your application locale file; component built-ins are available to component lookups, not application lookups. |
| CJK characters are boxes or layout changes | Check font glyph coverage and fallback on the target platform; see [Fonts](./fonts). |
| RTL string appears but the screen is awkward | Review layout and interactions explicitly; the locale setter does not mirror the interface. |
---
# Native Extensions
Source: /docs/native-extension
A native extension attaches an OS control or view to a GPUI window. The two concrete integrations in this repository show different paths: [NativeMenu](https://github.com/MohsenDastaran/uni-kit/tree/main/crates/component/src/native_menu) uses the operating system's menu API, while [gpui-wry](https://github.com/MohsenDastaran/uni-kit/tree/main/crates/webview) embeds a native WebView. Both cross the GPUI/OS boundary; neither is implemented by launching another application.
Use `NativeMenu` when a popup must escape a small window's bounds or follow the system menu appearance. Use the GPUI `PopupMenu` when the menu needs to participate in GPUI's own overlay composition. Use `gpui-wry` only when the screen needs an actual browser engine and can reserve a rectangle for a native child view; [TextView HTML](/component/text-view#html) handles document display, and [`cx.open_url`](./context#open-a-url-in-the-default-browser) opens the user's browser. `gpui-wry` is experimental.
| Platform | NativeMenu | Embedded WebView in this repository |
| --- | --- | --- |
| macOS | AppKit `NSMenu` attached to an `NSView` | Wry child view; current example works |
| Windows | Win32 `TrackPopupMenuEx` with an `HWND` | Wry child view; current example works with its renderer setting |
| Linux | GPUI-drawn `PopupMenu` fallback, clipped to the window | GTK hosting code is unfinished; no supported path yet |
The Linux menu fallback preserves the `NativeMenu` API, but it is GPUI content rather than an OS popup. Linux native-view hosting would need its own GTK/Wayland/X11 integration and tests. See [WebView](./webview) for the current platform requirements.
## Source and build map
The workspace pins `gpui-pre` to `={{gpui_pre_version}}` in the root `Cargo.toml`. Check the source in this checkout before copying an adapter to a different GPUI version. The relevant paths are:
| Concern | Source in this repository | Build or exercise from the repository root |
| --- | --- | --- |
| Shared menu API and platform selection | `crates/component/src/native_menu/mod.rs` in the `gpui-component` crate | `cargo check -p gpui-component`; `cargo test -p gpui-component native_menu` checks its builder and icon tests |
| AppKit and Win32 menu adapters; GPUI fallback | `crates/component/src/native_menu/{macos,windows,fallback}.rs`; fallback overlay ownership is in `crates/component/src/root.rs` | `cargo run -p gpui-component-story`, then open the **NativeMenu** story on the target OS |
| Wry wrapper and custom element | `crates/webview/src/lib.rs` in the `gpui-wry` crate | `cargo check -p gpui-wry` checks the wrapper for the current target |
| Child-view creation and renderer setup | `examples/webview/src/main.rs` with dependencies in `examples/webview/Cargo.toml` | `cargo run -p webview` on macOS or Windows |
Cargo checks only the current target's `#[cfg]` branch; a Linux build does not validate the AppKit or Win32 adapter. The story and WebView example require a graphical desktop and native platform libraries. The Linux WebView branch may compile but is not a working GPUI host.
## 1. Choose the integration boundary
For **platform menu integration**, keep the shared GPUI-facing API separate from platform adapters. `NativeMenu::show(position, window, cx)` chooses its macOS, Windows, or Linux fallback implementation in [the shared module](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/native_menu/mod.rs). Its native adapters create and operate the system menu, then send the selected action back through GPUI.
For an **embedded child view**, native content occupies a GPUI layout slot across frames. Own it in an `Entity`, as [`gpui-wry::WebView`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/webview/src/lib.rs) does. The owner must control show/hide, focus, bounds, and destruction before the parent window closes.
## 2. Obtain the right window handle
`Window::window_handle(window)` gives a GPUI `AnyWindowHandle` for updating that same window later. To call an OS API, use the `raw-window-handle` trait instead. These methods have similar names but different purposes:
```rust
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
let gpui_handle = Window::window_handle(window); // Later GPUI update
let native_handle = HasWindowHandle::window_handle(window)?; // OS adapter
match native_handle.as_raw() {
RawWindowHandle::AppKit(handle) => { /* macOS: handle.ns_view */ }
RawWindowHandle::Win32(handle) => { /* Windows: handle.hwnd */ }
RawWindowHandle::Xcb(handle) => { /* Linux X11: handle.window */ }
RawWindowHandle::Wayland(handle) => { /* Linux Wayland: handle.surface */ }
_ => { /* unsupported backend */ }
}
```
This is an adapter excerpt inside a function returning `Result`; it is not a complete cross-platform function. Match the actual handle variant under the corresponding target `#[cfg]`. The `HasWindowHandle` result borrows the live window, and the raw pointer or ID does not own the native object. The menu adapters copy an `NSView` pointer or `HWND` into a foreground task and rely on the parent window staying alive during the synchronous tracking call. Do not cache or use such a value after window destruction. GPUI's pinned Linux platform implementation exposes XCB window IDs under X11 and a Wayland surface under Wayland. A raw surface pointer alone does not provide the GTK widget hierarchy or compositor integration needed to embed a GTK control. The repository's [AppKit](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/native_menu/macos.rs) and [Win32](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/native_menu/windows.rs) adapters show handle extraction.
## 3. Platform menu example: NativeMenu
The adapter calls the platform's UI framework directly, using target-specific Rust crates:
| Backend | Rust crates and APIs used here | Native object or operation |
| --- | --- | --- |
| macOS | `objc2`, `objc2-app-kit`, `objc2-foundation`; `MainThreadMarker`, `NSMenu`, `NSMenuItem`, `define_class` | Construct AppKit menu items, receive Objective-C selection, show the menu from an `NSView` |
| Windows | `windows` crate with Win32 UI features; `CreatePopupMenu`, `TrackPopupMenuEx`, `DestroyMenu` | Build an `HMENU`, track the selection against an `HWND`, release native resources |
| Linux | GPUI's X11 backend uses `x11rb`; its Wayland backend uses `wayland-client`. Wry's unfinished path uses `gtk` and `WebViewBuilderExtUnix`. | NativeMenu currently renders a GPUI `PopupMenu` fallback in `Root`'s overlay; native widget hosting is not implemented |
These are the calls inside the actual adapters, with surrounding item construction and error handling omitted:
```rust
// macOS: objc2-app-kit, on the AppKit main thread.
let mtm = MainThreadMarker::new()?;
let menu = NSMenu::new(mtm);
menu.popUpMenuPositioningItem_atLocation_inView(None, point, Some(view));
// Windows: windows::Win32::UI::WindowsAndMessaging.
let menu = unsafe { CreatePopupMenu() }.ok()?;
let selected = unsafe { TrackPopupMenuEx(menu, flags.0, x, y, hwnd, None) };
let _ = unsafe { DestroyMenu(menu) };
```
The macOS adapter turns the AppKit `NSView` into an `NSMenu` anchor, converts GPUI's top-left logical position to AppKit view coordinates, and runs the menu tracking loop. The Windows adapter obtains an `HWND`, converts logical pixels to physical client coordinates and then screen coordinates, and calls `TrackPopupMenuEx`. Both run the blocking OS tracking loop **outside GPUI's mutable borrow**, then use a foreground task, `cx.update(...)`, and the retained `AnyWindowHandle::update(...)` to dispatch the selected `Action` through `Window::dispatch_action`. Closing the window or cancelling the menu produces no action.
A caller only supplies the semantic items and position; `Copy` and `Paste` below are application-defined GPUI Actions:
```rust
NativeMenu::new()
.menu("Copy", Box::new(Copy))
.menu("Paste", Box::new(Paste))
.show(position, window, cx);
```
Import `NativeMenu` from `gpui_kit::component::native_menu::NativeMenu`. `position` is a window-relative `Point` in logical pixels, such as `MouseDownEvent::position`. The builder also accepts separators, submenus, disabled and checked items, and icons; `NativeMenu::from(gpui_kit::Menu)` reuses an existing GPUI menu definition. `show` consumes the menu and returns immediately. It does nothing for an empty menu. Focus the intended action handler before opening the menu: the selected action is dispatched to the window's active focus context. On Linux, the fallback asks `Root` for its overlay; an application without that window root has no fallback menu to show.
The [story](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story/src/stories/native_menu_story.rs) shows focus placement and action handling. On Linux, [the fallback](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/native_menu/fallback.rs) builds a GPUI `PopupMenu` in `Root`'s overlay; it is still subject to the GPUI window boundary.
## 4. Embedded view example: WebView
The [WebView example](https://github.com/MohsenDastaran/uni-kit/blob/main/examples/webview/src/main.rs) creates a Wry native child view from a live GPUI window and stores the wrapper in an `Entity`:
```rust
let webview = cx.new(|cx| {
use raw_window_handle::HasWindowHandle;
let handle = window.window_handle().expect("No window handle");
let native = wry::WebViewBuilder::new()
.build_as_child(&handle)
.expect("Failed to create WebView");
gpui_wry::WebView::new(native, window, cx)
});
```
The wrapper renders a custom `Element` in the normal GPUI tree. `request_layout` reserves the slot; `prepaint` receives its resolved `Bounds` and calls Wry's `set_bounds` with logical coordinates. It also inserts a GPUI hitbox. `paint` registers outside-click focus behavior. **GPUI does not paint the WebView pixels**: the OS owns that child view. A GPUI `ContentMask` or hitbox does not change its compositor order. The owner hides the native view on drop; cloned `WebViewHandle`s must be released before the parent window is destroyed.
The current WebView sits above GPUI content in the same rectangle, so GPUI popovers, dialogs, and tooltips cannot reliably cover it. On Windows, the repository example sets `GPUI_DISABLE_DIRECT_COMPOSITION=true` before `gpui_kit::application()` for this child-view route. The Linux GTK path in the example is marked unfinished. Details and usage are in [WebView](./webview).
On Linux, the example imports `gtk` and Wry's `WebViewBuilderExtUnix`, creates a `gtk::Fixed`, and calls `build_gtk(&fixed)`. The code itself says the host initialization is unfinished: creating a GTK widget does **not** attach it to GPUI's XCB or Wayland window. GPUI's pinned Linux backend uses `x11rb` calls such as `configure_window` on X11 and `wayland-client`'s `WlSurface::commit` on Wayland. A Linux adapter must use the matching backend's connection and event loop, own its surface, and arrange input and compositor order; the raw `XcbWindowHandle` or `WaylandWindowHandle` alone cannot supply that integration. The current `gpui-wry` example is not a working template for this final step.
## 5. Build another native control
1. Define one GPUI-facing operation and the same result semantics for each platform. State whether the Linux implementation is native, GPUI-drawn, or unsupported.
2. Put AppKit, Win32, and Linux backend code behind target-specific adapters. Keep raw handles and OS types inside those adapters.
3. For an embedded view, own its lifetime in an `Entity`; map GPUI layout bounds to native bounds in `prepaint`, and test scale changes, focus, input, and window close. For system UI such as NativeMenu, run its tracking loop without holding a GPUI mutable borrow, then return the result to GPUI.
4. Test each actual backend. A headless GPUI test can check state and actions; it cannot prove OS positioning, native focus, or compositor stacking.
## Debug and verify the native boundary
Start with `cargo check -p gpui-component` or `cargo check -p gpui-wry` on the target machine. Run the NativeMenu story or WebView example above, then inspect the behavior at normal and high display scale. A successful Cargo check establishes type checking for that target; it does not link or run the application and cannot establish correct native input or stacking.
| Symptom | Check in this repository |
| --- | --- |
| Menu does not appear | Confirm the menu is nonempty, the pointer position is in window-relative logical pixels, and the actual `RawWindowHandle` variant matches the adapter. On Linux, confirm the window uses GPUI Kit's `Root` so `WindowState::native_menu_overlay` exists. |
| Menu appears but its action has no effect | Focus the intended view before `show`, register its `on_action` handler, and check whether selection was cancelled or the window closed before the saved `AnyWindowHandle` update. |
| Menu is offset on a scaled display | Check AppKit's flipped Y coordinate or Win32's logical-to-physical conversion followed by `ClientToScreen`; test after moving the window between displays. |
| WebView is blank on Windows | Check that the example's DirectComposition setting was applied before GPUI initialization and that the WebView was built as a child of the live window. |
| WebView has stale bounds or captures unexpected input | Inspect the `prepaint` bounds update, visibility state, focus return on `hide`, and native compositor order. A GPUI hitbox or content mask does not clip or raise the native pixels. |
When adding an adapter, test opening, cancelling, selecting, resizing, scaling, hiding, and closing its parent window on every supported backend. Keep the native object and any cloned handles within that window's lifetime. Record unsupported behavior explicitly rather than treating a successful Rust build as platform support.
### Composition boundary
The `gpui-wry` wrapper in this checkout does not provide an API for putting GPUI overlays above its native child view. Keep overlay interactions outside the WebView bounds or use a separate window. See [WebView](./webview) for the current behavior; verify any experimental GPUI composition branch separately before relying on it.
---
# FPS Monitor
Source: /docs/fps
`gpui-fps` overlays a performance HUD on a [Window](./window): a headline rate, a rolling
frame time trace, and this process' CPU, GPU and memory. It depends only on
`gpui`, so any GPUI application can use it.
```rs
use gpui_fps::fps_monitor;
fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
div()
.relative()
.size_full()
.child(self.content.clone())
.when(self.show_fps, |this| this.child(fps_monitor(window, cx)))
}
```
The parent must be `relative()`, the HUD positions itself absolutely, and
whether it is on screen is the caller's to decide.
## 120 Hz is a frame budget, not a refresh promise
“120 FPS” is often shorthand for a **target on a 120 Hz display**: one refresh
period is `1 / 120 s`, about **8.33 ms**. It says nothing by itself about how
often an idle application asks for a frame, or whether a particular screen can
finish and present every frame within that budget. A 60 Hz display gives about
16.67 ms per refresh. Variable-refresh displays and platform scheduling add
further context; neither number is an application's measured FPS.
GPUI responds to invalidation: an input or state change can make a view dirty;
the window later draws and presents the updated scene. Multiple changes may be
coalesced. An animation can request another frame while it is progressing, and
should stop requesting animation frames when it settles. The GPUI render
pipeline does not require an application to rebuild its element tree 120 times
per second merely because the display is 120 Hz. Platform frame callbacks may
still occur, and some backends may present an unchanged scene without rebuilding
its elements. An FPS HUD with its own timer, an input device, a platform
compositor, or application code can also cause activity while the app looks
idle. Measure idle CPU and GPU use on the target
platform rather than inferring zero work or continuous full redraws from the
refresh rate.
When GPUI needs a new drawing pass
IdleNo new element tree required
Input
Event → invalidation → draw
Animation
Requested frames while active
Conceptual frame requests, not measured FPS or a claim about platform presentation of cached scenes.
For a sustained **120 displayed FPS** claim, measure a representative *whole
window* in a release build, at the target size and hardware. Include realistic
data, scrolling or animation, and report a distribution of frame times and
present intervals after warmup. One small component completing `render` in
less than 8.33 ms only measures part of one frame. Layout, prepaint, paint,
submission, GPU work, the compositor, and scheduling can consume the rest.
Likewise, a low average can hide slow `P95` frames and visible stutter.
GPUI does work on both sides of the CPU/GPU boundary: application state,
element construction, layout, and paint preparation run on the CPU; the
platform renderer and compositor use the GPU where supported. Text, scene
complexity, cache behavior, graphics backend, and hardware determine the
bottleneck for a particular workload. “CPU-bound” or “GPU-bound” is a
profiling result for that workload, not a fixed property of every GPUI app.
## Immediate, retained, and hybrid describe different layers
These terms answer different questions, so a single label is easy to
misread:
| Layer | What GPUI does | What persists |
| --- | --- | --- |
| View state | An [`Entity`](./entity) owns application state across updates and frames. | The Entity and its subscriptions, tasks, and child handles while owned. |
| UI description | On a needed [render pass](./render), `Render` or `RenderOnce` constructs elements from current state. This resembles immediate-style declaration. | The element description is specific to that pass; `RenderOnce` does **not** mean once per display frame. |
| Reuse and drawing | GPUI can reuse eligible [cached views](./view-cache) and keyed element state, and submits paint work for the window. | Cache and platform scene resources can outlive an individual element description. |
“Retained” accurately describes the Entity state and selected caches;
“immediate-style” describes construction of a current element description;
“hybrid” is also the term in [Zed's GPUI overview](https://github.com/zed-industries/zed/blob/bcf6582ce3500df93a8a39366640173e6786cea6/crates/gpui/README.md#the-big-picture). None of those terms defines the
idle present rate or proves a performance number. The [Render](./render),
[Element](./element), and [View Cache](./view-cache) guides show the ownership
and invalidation boundaries in code.
Add `gpui-fps` from the same GPUI dependency family as the application. Render
`fps_monitor(window, cx)` at most once per window; repeated calls reuse the
same monitor and would draw it twice. The repository's runnable example is
`cargo run --release -p fps_monitor`. Use `cargo run -p fps_monitor` only when
comparing Debug builds: unoptimized framework code can change the measured
frame cost substantially. See [Installation](./installation#improve-development-runtime-performance)
for the repository's development profile.
## From an update to a displayed frame
1. A state change followed by `cx.notify()`, an animation request, or a window
refresh invalidates the window. Several invalidations can become one draw.
2. GPUI runs `Window::draw`, building dirty views' elements and doing layout,
prepaint, and paint work. The profiler records `draw_start`, `draw_end`, and
the invalidation count for this window. `FRAME` uses **only**
`draw_end - draw_start`.
3. GPUI submits the drawn scene to the platform in a separate present step.
Its `present_end` timestamp feeds `FPS` and `INTERVAL`.
This distinction matters: a 5 ms `FRAME` says that GPUI's draw completed in
5 ms. It does not say the request waited only 5 ms, that platform submission
took 5 ms, or that the GPU and compositor displayed it within 5 ms. The
profiler also exposes `dirty_to_draw_duration()` and `PresentTiming` for
custom instrumentation, but those are separate measurements from this HUD's
`FRAME`. See GPUI's [frame timing definitions](https://docs.rs/gpui-pre/{{gpui_pre_version}}/gpui/profiler/struct.FrameTiming.html)
and the [sampler implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/fps/src/sampler.rs).
## The headline
The big figure answers one of two questions, and the `MAX` marker says which.
**Right-click** to switch; **click** to collapse the HUD to a tag.
| | Reads | Means |
| --- | --- | --- |
| `MAX FPS` (default) | `1 / mean FRAME`, capped by the display when its rate is known | Estimated draw throughput for the sampled workload; it omits presentation and GPU completion |
| `FPS` | Rate inferred from recent present timestamps | The observed present cadence while the window has enough samples |
They are different questions. For example, this HUD requests a readout update
twice a second even if the application has no visible changes. That can give
the window a low observed `FPS` while `MAX FPS` remains high; neither figure
alone proves continuous idle rendering or sustained performance in a busier
screen.
### Why MAX is derived rather than counted
The obvious way to make a frame counter read "as fast as this UI can go" is to
keep asking for frames, the way an in-game counter does. That is not free here.
Marking any view dirty schedules a **window** draw, and GPUI re-renders every
view in that window outside an [`Entity::cached`] boundary (see [View Cache](./view-cache)) — so each frame the
HUD asked for would add window layout and [paint](./paint) work, and the CPU
row underneath would include work the HUD itself was causing.
The measured draw cost offers an estimate of CPU-side headroom for the sampled
window. Its reciprocal is `MAX FPS`, but it does not measure a sustained
animation or the complete present pipeline. The HUD does not request
continuous frames to calculate it; its twice-second readout clock is
described below.
### Why MAX is capped by asking, not by measuring
A frame drawn in 3ms reads as 333, and no panel will ever show that. Counting
presents had the ceiling for free — frames go to the compositor on vsync — and
a figure derived from frame cost has no such bound, so the cap is applied
explicitly.
It cannot be inferred. The gaps between a window's presents are whole multiples
of the panel's period, so they bound it **from below and never from above**:
41.7ms is six refreshes at 144Hz and one at 24Hz, and nothing in the timing
distinguishes them. Every estimate tried read a real window wrong — 169 and 149
from the shortest and the densest gaps, 75 from a window drawing every other
refresh, and 24 from an application whose own timer happened to fire every
41.7ms.
So the platform is asked instead. GPUI hands out the platform's own display
handle through `DisplayId`, and the HUD takes it from there:
- **macOS** — `CGDisplayCopyDisplayMode` on the `CGDirectDisplayID`. A built-in
panel reports no fixed rate, which is the truth on ProMotion, and is read as
no cap.
- **Windows** — `EnumDisplaySettingsW` on the monitor's device name.
- **Wayland** — the outputs are enumerated on a second connection and matched
to GPUI's displays by the identity it derives from their names, because
object ids are per-connection and mean nothing across one.
- **X11 and everything else** — no query, so no cap.
The answer is re-asked when the window moves to another display and not
otherwise. Where nobody will say, the reading is left uncapped rather than held
to a guess: a ceiling under the truth hides the figure the reader came for.
## The rows
| Row | Measures |
| --- | --- |
| `INTERVAL` | Mean time between presents. The same figure a platform overlay calls its frame interval, and the reciprocal of `FPS`. A wide gap between it and `MAX` is an idle window, not a slow one. |
| `FRAME` | Mean `Window::draw` cost. Graded against the frame budget: this is the row to read when something feels slow. |
| `P95` | The slow tail of the same frames, graded the same way. |
| `DROP` | Share of frames that overran the budget. |
| `INV` | Invalidations coalesced into one frame. Well above one means the window was asked to redraw more often than it could. |
| `GPU` | This process' GPU use where the platform exposes a per-process counter; otherwise the row is absent. |
| `CPU` | This process, on the scale `top` and Activity Monitor use: 100 is one saturated core, so a process spread across one and a half cores reads about 150. |
| `MEM` | Process-attributed memory: macOS physical footprint, Windows private usage, or Linux resident anonymous memory, with an RSS fallback. It is not the same counter on every platform. |
`FRAME`, `P95` and `DROP` are graded against the frame budget: one refresh of
the display the window is on, where the platform reports the rate above, and
one 60Hz frame where it does not. `frame_budget()` pins a budget of your own
instead, and the display then no longer replaces it.
`FRAME`, `P95`, `DROP`, and `INV` use the latest **120 retained draw samples**
by default, not a fixed number of seconds; `capacity()` changes that length.
`P95` is the nearest observed 95th-percentile frame, while `DROP` is the
fraction whose draw time is *strictly greater* than the budget. The trace
plots these draw costs, newest at the right. `FPS` and `INTERVAL` use present
timestamps in a rolling one-second window. The displayed numbers are
republished every 500 ms; CPU, GPU, and memory are background samples averaged
over the trailing three seconds. Resource sampling is unavailable on the web.
These windows can describe different moments. After an expensive interaction,
spikes may remain in the frame trace, and repeated slow frames in `P95`, after
`FPS` has already returned to idle. The headline is not graded because a low observed
rate can simply mean that the application has stopped asking for frames.
## Reproduce and diagnose a slowdown
1. Run `cargo run --release -p fps_monitor` from this repository. Let the
window and HUD settle, then note the display, window size, build profile,
and baseline `FRAME`, `P95`, `DROP`, `INV`, and resource rows. The example's
`+ load` and `− load` controls change its curve count; compare the same
count and window size between runs. Its scene calls
`window.request_animation_frame()`, so it provides a sustained draw load.
2. Increase the load and watch `FRAME` against the display's budget. For a
60 Hz target one refresh is about 16.7 ms; for 120 Hz it is about 8.3 ms.
If `FRAME` is low but `P95` or `DROP` rises, investigate intermittent
work. If `INV` rises while draw costs stay low, inspect redundant update
or animation requests. If only `INTERVAL` grows, first check whether the
window is idle, occluded, or waiting for presentation.
3. Repeat the same interaction several times after warmup. Capture a CPU
profile for expensive view construction, layout, or paint submission; use
the platform's GPU/compositor tools when `FRAME` is low but visible output
still stutters. Narrow the workload, change one cause, and compare the
same build profile and conditions again. The HUD locates a symptom; it
does not attribute time to a particular view or GPU command.
For application-specific records, GPUI's `profiler` feature exposes
`FrameTimingCollector::new()` and `collect_unseen()`. They yield `FrameEvent::Draw`
and `FrameEvent::Present`; filter each by `window_id`, and keep the collector
alive while sampling. The trace is process-wide and is enabled by
`gpui::profiler::set_trace_enabled(true)`; disabling it clears its buffers.
The HUD manages this switch while visible. These raw records are useful when
you need to correlate a frame with an app event, including the first
invalidation timestamp (`dirty_at`) and present submission. They do not by
themselves measure GPU completion or photons on screen. See GPUI's
[collector API](https://docs.rs/gpui-pre/{{gpui_pre_version}}/gpui/profiler/struct.FrameTimingCollector.html)
and [GPUI Kit's monitor](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/fps/src/monitor.rs).
## The first frames are not measured
A window's first frames are its most expensive — shaders, the glyph atlas, the
icons, every cache still cold — and they are not what the application costs to
run. One of them is a hundred milliseconds against a budget of sixteen, and a
HUD that has seen eight frames would report it as a twelfth of the window's
work, in amber, before the reader has done anything at all.
So the sampler discards two things: everything GPUI recorded before the HUD was
mounted, which is either somebody else's history or the cold start, and the
first few frames after it. The default reading of a window that just opened is
a healthy one.
## What the HUD itself costs
One frame every 500ms. It does not drive the frame loop, but it does need a
clock — nothing else would wake a HUD in a window that has stopped drawing, and
the figures would freeze at whatever the application last drew. That clock also
carries the CPU, GPU and memory sample.
Those frames are not measured. To GPUI the clock's `notify` is an invalidation
like any other, answered with a full draw of the window, and left in the
readings it would be a cold frame every 500ms reported as the application's
`FRAME` and `MAX`. So the clock announces each one, and the sampler leaves out
the draw that answered it — unless the application asked for that frame too, in
which case the work was wanted and the cost counts.
Hidden, the HUD costs nothing. Two ticks without being rendered — a second —
and the clock stops, the resource probe with it, and the HUD lets go of GPUI's
frame trace unless something else is holding it. The next render starts it all
again from an empty sampler: the trace buffer was cleared with the switch, and
the frames the window drew meanwhile were nobody's to report.
[`Entity::cached`]: https://docs.rs/gpui-pre/{{gpui_pre_version}}/gpui/struct.Entity.html#method.cached
---
# Action
Source: /docs/action
An **Action** represents an operation the application can perform. A shortcut, menu item, command palette, button, or another Action handler can all dispatch the same typed value. GPUI routes it to the part of the [Element](./element) tree that owns the command. An [Event](./event) serves the other direction: it reports something that happened after state changed.
The [GPUI Action source](https://docs.rs/crate/gpui-pre/{{gpui_pre_version}}/source/src/action.rs) defines the macro, trait, and registry described here.
This page explains command definition and dispatch. Start with [Focus](./focus) if you have not yet created a keyboard target; see [KeyBinding](./keybinding) for key notation, context matching, and keymap setup.
## Run an Action already in this repository
From the repository root, launch the Story Gallery directly on its Tree page:
```sh
cargo run -p gpui-component-story -- Tree
```
Select a file-tree row and press **Enter**. The process prints `Renaming item: ...` in the terminal. Select another row and repeat to see that the handler reads the current selection. If Enter does nothing, click a row first: the binding is scoped to the Tree story's focused path. This example does not rename a file.
Read the implementation in [`crates/story/src/stories/tree_story.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story/src/stories/tree_story.rs):
1. `actions!(story, [Rename, OpenFile, Delete])` defines the typed commands.
2. `init` binds `enter` to `Rename` under the `TreeStory` context. [`stories::init`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story/src/stories/mod.rs) calls it during application setup.
3. `TreeStory::render` attaches `.key_context(CONTEXT)` and `.on_action(cx.listener(Self::on_action_rename))` to the story container. Its child Tree supplies the active focus path.
4. `on_action_rename` reads the selected entry from `tree_state`. Selection belongs to the Tree state; the Action expresses what the user requested.
The rest of this guide builds from that route. A menu or button can later dispatch the same Action without copying the handler.
## Build a runnable Action in `hello_world`
The Tree story is useful for tracing a real application. To build the route yourself, temporarily replace `examples/hello_world/src/main.rs` with the complete program below. It uses the existing `hello_world` package and needs no new crate or dependency.
```rust
use gpui_kit::component::button::Button;
use gpui_kit::*;
actions!(counter, [Increment]);
struct Counter {
count: usize,
focus: FocusHandle,
}
impl Counter {
fn new(window: &mut Window, cx: &mut Context) -> Self {
let focus = cx.focus_handle().tab_stop(true);
focus.focus(window, cx);
Self { count: 0, focus }
}
fn on_increment(&mut self, _: &Increment, _: &mut Window, cx: &mut Context) {
self.count += 1;
cx.notify();
}
}
impl Render for Counter {
fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement {
div()
.flex()
.flex_col()
.gap_2()
.p_4()
.track_focus(&self.focus)
.key_context("Counter")
.on_action(cx.listener(Self::on_increment))
.child(format!("Count: {}", self.count))
.child("Press Enter while this region has focus")
.child(
Button::new("increment")
.label("Increment")
.on_click(cx.listener(|this, _, window, cx| {
this.focus.dispatch_action(&Increment, window, cx);
})),
)
}
}
fn main() {
gpui_kit::application().run(|cx| {
gpui_kit::init(cx);
cx.bind_keys([KeyBinding::new("enter", Increment, Some("Counter"))]);
gpui_kit::open_window(WindowOptions::default(), cx, |window, cx| {
cx.new(|cx| Counter::new(window, cx))
})
.expect("failed to open window");
});
}
```
Run it from the repository root:
```sh
cargo run -p hello_world --bin hello_world
```
The window starts at `Count: 0`. Press **Enter** while the counter region has Focus, or click **Increment**; each operation increases the displayed count by one. The counter's `FocusHandle` is retained in its Entity and attached to the rendered container. `cx.bind_keys` maps Enter to `Increment` only on a path containing `Counter`. The container's `.on_action` calls `on_increment`, which changes retained state and calls `cx.notify()` so the new count renders. The button sends the *same* Action to that container through its saved handle, even if clicking the button moves Focus.
Check each link in the route with two small experiments, then restore the original source. First, remove `.key_context("Counter")`: Enter no longer selects this binding, while the button still works because direct dispatch does not consult Key Context. Next, restore the context and remove `.on_action(...)`: neither input changes the count because the route has no handler. If a handler runs but the label stays stale, check whether it mutates retained state and calls `cx.notify()`. Restore `examples/hello_world/src/main.rs` when finished.
## One command, several entry points
Define a unit Action with a namespace. `actions!` generates the type and registers its stable name, here `chat::SendMessage`:
```rust
use gpui_kit::*;
actions!(chat, [SendMessage]);
```
An element handler receives the typed Action, the window, and the owning entity's [Context](./context). `cx.listener` adapts the method to the element callback:
```rust
impl Chat {
fn on_action_send_message(
&mut self,
_: &SendMessage,
_: &mut Window,
cx: &mut Context,
) {
self.submit_draft();
cx.notify();
}
}
// In Chat::render:
div()
.track_focus(&self.focus_handle)
.key_context("Chat")
.on_action(cx.listener(Self::on_action_send_message))
.child("Chat")
```
Bind a shortcut to `SendMessage`, and use the same Action elsewhere:
```rust
window.dispatch_action(Box::new(SendMessage), cx); // Button or command palette
MenuItem::action("Send Message", SendMessage) // Native application menu
```
The command logic stays in one handler. A direct `window.dispatch_action(...)` does **not** need a KeyBinding or match a Key Context; those participate when a keystroke is translated into an Action. Use [KeyBinding](./keybinding) for the binding itself.
## Define data carrying Actions
An Action can include data. Deriving `Action` requires `Clone` and `PartialEq`. If the Action should be constructible from a named JSON keymap entry, also derive `Deserialize` and `JsonSchema`:
```rust
#[derive(Action, Clone, PartialEq, serde::Deserialize, schemars::JsonSchema)]
#[action(namespace = chat)]
struct InsertPrompt {
text: String,
}
```
The Action registry uses the namespace and type name to build a typed value from an action name and optional JSON payload. This lets a configurable keymap and a command UI refer to the same command. Names must be unique; duplicate registration panics during application creation.
The JSON-capable example requires `serde` with its `derive` feature and `schemars` as application dependencies for those two derives.
Use a unit Action for a command whose handler can read all required state from its owner, as `Rename` does in the Tree story. Use a data Action when the caller must identify a target or supply a value. An Action payload is the command input, not a place to store the owner's changing UI state.
For a runtime command whose payload should never come from JSON, `no_json` retains typed dispatch but opts out of JSON construction:
```rust
#[derive(Action, Clone, PartialEq)]
#[action(namespace = workspace, no_json)]
struct OpenConversation {
conversation_id: String,
}
```
Choose a stable verb based name for each command. Use one Action type for all entry points that mean the same operation; put state changes in its owner rather than in each input callback.
## Focus selects the route
A [FocusHandle](./window) identifies a keyboard target. Keep the handle on the entity that owns the interaction, then attach it to an element each time that entity renders:
```rust
struct Chat {
focus_handle: FocusHandle,
}
impl Chat {
fn new(cx: &mut Context) -> Self {
Self { focus_handle: cx.focus_handle() }
}
}
impl Focusable for Chat {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
// In Chat::render:
div()
.track_focus(&self.focus_handle)
.key_context("Chat")
.on_action(cx.listener(Self::on_action_send_message))
```
`track_focus` registers the handle on the element's dispatch node. Mouse down inside the element focuses that handle by default. If an inner control must retain its own focus, its mouse down handler can call `window.prevent_default()` to suppress the ancestor's default focus transfer. Tracking does not focus the element during render; call `self.focus_handle.focus(window, cx)` when the view opens or the user enters it.
`handle.is_focused(window)` tests the exact target. `handle.contains_focused(window, cx)` also accepts a focused descendant, useful while a child control is active. A tracked handle is not automatically a Tab stop: configure `cx.focus_handle().tab_stop(true)` when creating it. For a stateless component, retain a handle across renders with `window.use_keyed_state(...)`.
GPUI builds a **dispatch path** from the focused element through its ancestors. A `key_context("Chat")` on that path makes contextual bindings eligible; the matching key produces an Action. The Action then travels on the path. A handler on a sibling is not reachable from this route.
### Trace one shortcut
Suppose the focused element is inside `Chat`, itself inside `Workspace`. A binding such as `KeyBinding::new("secondary-enter", SendMessage, Some("Chat"))` is eligible only while `Chat` appears on that focused path. GPUI chooses a matching binding, then dispatches its `SendMessage` value along that path. The `Chat` handler runs before a `Workspace` bubble handler. If no element on the route handles the Action, a global `cx.on_action` handler can receive it. The context chooses a **binding**; it does not select a handler by itself. [KeyBinding](./keybinding) explains competing bindings and predicate syntax.
For a command triggered by a click, `window.dispatch_action(Box::new(SendMessage), cx)` uses the focus captured when called. Ensure the intended route has focus, or dispatch through a retained `FocusHandle`. A button's callback can also call the owning entity's method directly when no shared command route is needed.
## Handler order and propagation
Action dispatch has two phases:
1. **Capture:** global capture listeners, then matching `.capture_action(...)` listeners from the root toward the target.
2. **Bubble:** matching `.on_action(...)` listeners from the target toward the root, then global `cx.on_action(...)` listeners if propagation continues.
The closest bubble handler therefore gets the first chance to handle a command. An Action handler stops bubble propagation by default. Call `cx.propagate()` when this handler declines the Action and a parent or global handler should try it:
```rust
fn on_action_close(
&mut self,
_: &ClosePanel,
_: &mut Window,
cx: &mut Context,
) {
if !self.can_close() {
cx.propagate();
return;
}
self.close();
cx.notify();
}
```
Capture listeners can call `cx.stop_propagation()` to stop dispatch before it reaches the target. Global bubble handlers also stop propagation by default, so a global fallback that declines a command should call `cx.propagate()`. These are Action dispatch controls; `window.prevent_default()` controls a default input behavior such as mouse focus transfer. See [Event](./event) for pointer and keyboard event propagation.
To inspect a command without consuming it, a capture listener can observe it and leave propagation enabled. Capture starts with propagation enabled. In bubble, each listener starts with propagation stopped. Call `cx.propagate()` when the next ancestor or global fallback should also receive that Action. Returning early alone does not pass it on.
`window.dispatch_action(Box::new(action), cx)` captures the current focus target and defers dispatch to the rendered frame. For an explicit owner, `focus_handle.dispatch_action(&action, window, cx)` starts at the element that rendered that handle, if it is present in the current frame. `cx.dispatch_action(&action)` targets the active window, or global handlers when no window is active. These choices matter when a popup or click changes focus before a command runs.
| Call | Target | When to use it |
| --- | --- | --- |
| `window.dispatch_action(Box::new(action), cx)` | Current focus in this window, captured at call time | A menu or button command for the focused region. Dispatch is deferred. |
| `focus_handle.dispatch_action(&action, window, cx)` | The element that rendered this handle in the current frame | A command for a specific rendered region, even if another control now has focus. No rendered handle means no dispatch. |
| `cx.dispatch_action(&action)` | Active window, or global handlers without one | An application-level command when the caller has an `App` context. |
None of these calls evaluates a Key Context predicate. Key Context participates when a **keystroke** selects an Action from the keymap. The dispatched Action still needs a handler reachable from its chosen target.
## Coordinate sibling regions through an owner
Suppose a conversation selected in a Sidebar should open in Chat. The Sidebar describes the intent with `OpenConversation`; the common owner, `Workspace`, handles it and updates the Chat entity:
```rust
impl Workspace {
fn on_action_open_conversation(
&mut self,
action: &OpenConversation,
window: &mut Window,
cx: &mut Context,
) {
self.chat.update(cx, |chat, cx| {
chat.open(action.conversation_id.clone(), window, cx);
});
}
}
// Workspace renders both regions under its handler.
h_flex()
.on_action(cx.listener(Self::on_action_open_conversation))
.child(self.sidebar.clone())
.child(self.chat.clone())
// From a Sidebar interaction, while its focus path is active:
window.dispatch_action(Box::new(OpenConversation { conversation_id }), cx);
```
The route is **Sidebar → Workspace**. `Workspace` then updates Chat through the [Entity](./entity) API. Attaching the handler only to Chat would not work for an Action dispatched from Sidebar because Chat is a sibling, outside Sidebar's dispatch path. For commands that must target a specific rendered region regardless of current focus, retain that region's `FocusHandle` and use its `dispatch_action` method. See [Coding Guides](./coding-guides) for larger feature ownership patterns.
GPUI Kit uses the same pattern in its Command palette and Popup Menu: a selected item supplies a boxed Action, then the window dispatches it. The Command palette also keeps its own focus handle, key context, and navigation Action handlers on the palette element. The framework component owns selection and keyboard mechanics; the application owner handles the command's meaning.
## Diagnose a missing command
When a shortcut works only after clicking a region, inspect the route in order:
1. Which `FocusHandle` is focused, and is it attached with `track_focus` in the rendered tree?
2. Is the required `key_context` on that element or an ancestor? See [KeyBinding](./keybinding) for binding matching.
3. Is the typed `.on_action(...)` handler on the resulting dispatch path?
4. Did a closer handler consume the Action, or did a declining handler forget `cx.propagate()`?
5. If a direct dispatch runs after focus changes, should it use an explicit `FocusHandle` target?
If a key does nothing, first distinguish **no binding match** from **no reachable handler**. Try dispatching the Action directly on the intended `FocusHandle`. If that reaches the handler, inspect the key string and context predicate. If it does not, inspect the rendered handle, dispatch path, and propagation. If the handler runs but the screen stays unchanged, verify that it updates the owning state and calls `cx.notify()` when a redraw is needed.
Keep the handle, context, and handler with the region that owns the command. Use a global handler only for an operation that truly applies across the application.
## Practice with the Tree story
1. **Follow the path.** Run `cargo run -p gpui-component-story -- Tree`, select a row, and press Enter. In `tree_story.rs`, find the binding, context, and handler. Which component owns the selected row, and which entity owns the command handler?
2. **Change the input.** In your own branch, temporarily change the Tree story binding from `"enter"` to `"secondary-r"`. Re-run the Story Gallery. Verify that the new key invokes the same `Rename` handler and that Enter no longer does. Restore the source after the experiment.
3. **Predict propagation.** Place `Rename` handlers on a child and its parent in a small view. Have the child call `cx.propagate()` only when it has no selection. Predict which handler runs in both cases, then test with visible output. A handler that returns without calling `cx.propagate()` consumes the Action.
4. **Add a target.** Model an operation that must carry a row identifier as a data Action with `#[action(namespace = story, no_json)]`. Dispatch it from a row callback; keep the mutation in the owning handler. Compare this with `Rename`, which reads the currently selected row instead.
---
# Window
Source: /docs/window
GPUI provides [`Window`](https://docs.rs/gpui-pre/{{gpui_pre_version}}/gpui/struct.Window.html) as the context for one platform window. It connects the rendered Element tree to [platform input](./event#pointer-and-keyboard-input-are-also-events), Focus, Action dispatch, drawing, and window controls. A View receives it only while GPUI is updating or rendering that window:
```rust
impl Render for Chat {
fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
let active = window.is_window_active();
div()
.track_focus(&self.focus_handle)
.when(!active, |this| this.opacity(0.8))
.child("Chat")
}
}
```
Keep application state in an [Entity](./entity). Use `Window` when an operation belongs to the current window or needs its current interaction state.
`App` gives access to application-wide services, [globals](./global), and entities. [`Context`](./context) adds operations tied to the current Entity, including `cx.notify()`, listeners, events, and tasks. `Window` carries focus, dispatch, input, [keyed element state](./element_id), measurement, and drawing for **one** window. These are temporary callback contexts; store an `Entity`, `FocusHandle`, task, subscription, or window handle for later work, never `&mut Window` or `&mut Context<_>`.
## Open and own a window
Call `gpui_kit::init(cx)` before opening windows. `gpui_kit::open_window` creates a GPUI window with a Base `Root` around the view returned by the builder. It returns both a window handle and the application content Entity, so the app can retain the part it owns:
```rust
use gpui_kit::*;
application().run(|cx| {
init(cx);
let (window_handle, workspace) = open_window(
WindowOptions::default(),
cx,
|window, cx| cx.new(|cx| Workspace::new(window, cx)),
)
.expect("open workspace window");
// Retain the handles in an application owner if later work needs them.
});
```
[`WindowOptions`](https://docs.rs/gpui-pre/{{gpui_pre_version}}/gpui/struct.WindowOptions.html) controls initial bounds, focus, visibility, window kind, minimum size, and other platform-facing choices. The builder receives the `Window` only for construction. A window handle lets later code request an update, but handle-based updates can fail after the window closes. In a multi-window app, use the handle for the particular window whose focus or geometry you mean; an Entity handle alone does not select a window.
`open_window` returns an `AnyWindowHandle` because the actual GPUI root is `gpui_kit::base::Root`, not `Workspace`. From a later callback with `&mut App`, use the handle to enter that window, and check the result before assuming it is still open:
```rust
if window_handle
.update(cx, |_, window, _| window.activate_window())
.is_err()
{
// The window has already closed.
}
```
The first callback argument is the Base `Root` view; keep the `workspace` Entity returned by `open_window` for application content updates. A window handle selects the window, while an Entity selects the state to update.
## Try it: update and close one window
This exercise uses the existing `hello_world` package. Replace `examples/hello_world/src/main.rs` with the following code, then run `cargo run -p hello_world` from the repository root:
```rust
use gpui_kit::component::button::*;
use gpui_kit::component::*;
use gpui_kit::*;
struct WindowPractice {
renamed: bool,
}
impl Render for WindowPractice {
fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement {
let status = if self.renamed {
"Title: Second title"
} else {
"Title: First title"
};
div()
.v_flex()
.gap_2()
.p_4()
.child(status)
.child(
Button::new("rename")
.label("Change title")
.on_click(cx.listener(|this, _, window, cx| {
this.renamed = !this.renamed;
let title = if this.renamed {
"Second title"
} else {
"First title"
};
window.set_window_title(title);
cx.notify();
})),
)
.child(
Button::new("close")
.label("Close window")
.on_click(|_, window, _| window.remove_window()),
)
}
}
fn main() {
application().run(|cx| {
gpui_kit::init(cx);
open_window(WindowOptions::default(), cx, |window, cx| {
window.set_window_title("First title");
cx.new(|_| WindowPractice { renamed: false })
})
.expect("open practice window");
});
}
```
Click **Change title** twice. The native window title and the text inside the View should alternate together. `renamed` is persistent Entity state; `cx.notify()` makes its new text visible. `window.set_window_title(...)` changes the current native window directly and does not need that Entity notification. Click **Close window** to request removal of this window. The button callback's `&mut Window` is valid only during that callback; after removal, a saved window handle may return an error on update.
If the content changes but the title does not, check whether your desktop displays native window titles and whether `set_window_title` runs in the button callback. If neither changes, confirm `init(cx)` ran before `open_window`, the button has its `on_click` listener, and the listener calls `cx.notify()` after changing `renamed`. Restore the original `main.rs` after the exercise. For more than one window and ownership across them, continue with [Multi Window](./multi-window).
## What belongs to Window
Common window-local operations include:
| Need | API |
| --- | --- |
| Inspect geometry and state | `bounds`, `viewport_size`, `scale_factor`, `is_window_active` |
| Manage Focus | `focused`, `focus`, `blur`, `focus_next`, `focus_prev` |
| Send a command from code | `dispatch_action` |
| Redraw or schedule a frame callback | `refresh`, `request_animation_frame`, `on_next_frame` |
| Control the native window | `set_window_title`, `activate_window`, `remove_window` |
| Continue work later | `defer`, `spawn` |
`Window` also carries layout, text, hit testing, input, and drawing state internally. Most Views do not manipulate those systems directly; Elements and GPUI use them during rendering.
## Geometry and scale
`window.bounds()` returns the native window rectangle in **global** coordinates, potentially spanning displays. `window.viewport_size()` returns the drawable content area's size in window-local logical `Pixels`. For a local overlay that must fit inside the content, use the viewport size; for saved placement, use `window.window_bounds()`, which includes the window's restorable state. `window.inner_window_bounds()` excludes platform insets where supported.
`window.scale_factor()` converts logical pixels to physical display pixels: a factor of `2.0` means one logical pixel covers two device pixels along each axis. It may change when the window moves between displays. Do not multiply GPUI layout sizes by it; use it at a boundary that actually needs device pixels, such as a native platform integration. `visual_viewport_bounds()` can shrink or move when a mobile keyboard appears, while `viewport_size()` remains the layout area.
These values have different origins: `bounds().origin` is global display space, while pointer events and `visual_viewport_bounds()` use window-local logical coordinates. Do not compare a pointer position directly with a saved global window origin. For an overlay that must stay clear of system insets or the software keyboard, `window.fully_visible_bounds()` gives a conservative window-local rectangle; it cannot account for obscuring surfaces the platform does not report.
The [Dialog implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/dialog/dialog.rs) uses `window.viewport_size()` and window border padding to keep a surface within available content. The [native menu integration](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/native_menu/windows.rs) reads `scale_factor()` at its platform coordinate boundary. Let standard components perform these calculations when they already own the overlay or native control.
## Focus and Action dispatch
Focus is local to a Window. `window.focus(...)` selects a `FocusHandle`, and `window.focused(cx)` returns the current one. Attach that handle to a rendered Element with `.track_focus(&handle)` so it has a node on the Dispatch Path; a handle alone does not create a keyboard target. A tracked handle is not automatically in Tab order: opt in with `cx.focus_handle().tab_stop(true)` when creating it. Keyboard input then uses the focused Element's Dispatch Path to match a [KeyBinding](./keybinding) and dispatch its Action.
Follow the [Focus tutorial](./focus) to build and verify the target, Tab order, and overlay restoration before adding shortcuts.
```rust
fn focus_composer(&mut self, window: &mut Window, cx: &mut Context) {
window.focus(&self.composer_focus, cx);
}
fn on_action_open_conversation(
&mut self,
action: &OpenConversation,
window: &mut Window,
cx: &mut Context,
) {
self.open(action.id, cx);
window.focus(&self.composer_focus, cx);
}
```
Use `window.dispatch_action(action.boxed_clone(), cx)` when a button, command palette, native menu, or another piece of code should issue the same command as a KeyBinding. GPUI captures the current Focus and defers the actual dispatch until the current effect cycle completes.
```rust
Button::new("open-conversation")
.label("Open")
.on_click(|_, window, cx| {
window.dispatch_action(Box::new(OpenConversation { id }), cx);
})
```
The Action still follows the focused Dispatch Path. Place its `on_action_*` handler on that path, usually on the focused region or a common owner. See [Action](./action) for the complete routing model.
## Run work after the current update
Use `window.defer` when work must wait until entities currently being updated have been released. This is common when closing an overlay changes Focus, or when the next operation updates another part of the same UI tree.
```rust
fn dismiss(&mut self, window: &mut Window, cx: &mut Context) {
let composer = self.composer.clone();
window.defer(cx, move |window, cx| {
let focus_handle = composer.read(cx).focus_handle(cx);
window.focus(&focus_handle, cx);
});
}
```
Inside an Entity, `cx.defer_in(window, ...)` is often more convenient because GPUI supplies that Entity again:
```rust
cx.defer_in(window, |this, window, cx| {
this.rebuild_results(window, cx);
});
```
The callback already receives `&mut Self`. Do not call `update` on the same Entity from inside it; that attempts to update an Entity which is already being updated. Use `defer` and `defer_in` for Focus changes and UI-tree mutations that cannot safely happen in the current callback.
Use `window.on_next_frame(...)` only when the operation specifically belongs to the next frame callback, such as an animation step. `defer` means “after the current effect cycle,” which is a different boundary.
## Redraw and frame lifecycle
An Entity mutation followed by `cx.notify()` marks that Entity for rendering. `window.refresh()` marks the **whole window** dirty for its next draw; use it for window-local changes that lack an Entity notification, such as a platform or overlay state change. Neither belongs in an unconditional render path.
`window.on_next_frame(callback)` runs the callback at the next platform frame tick, before that tick's optional draw. It creates frame demand but does not itself mark the window dirty. `window.request_animation_frame()` captures the currently rendering View and notifies it on the next tick. In the pinned `gpui-pre` {{gpui_pre_version}} implementation, it calls `current_view()` immediately, so use it only while GPUI has a current View; outside that render path, use `on_next_frame` and explicitly notify an Entity or call `window.refresh()`. Call it only while the motion still needs another sample. GPUI's `AnimationExt::with_animation` and [Base Motion](./animation) already manage frame requests and reduced motion for their animations.
For a frame callback that changes external window state, call `window.refresh()` in that callback so the change reaches a draw. For Entity state, update the Entity and call `cx.notify()` in its update callback. A frame tick alone does not redraw an unchanged window.
Rendering builds a fresh Element tree from retained Entity state, then GPUI resolves layout, prepaints input geometry, and paints the scene. `Window` has methods for all these stages, but application views should normally derive elements in `render`; custom Elements need later-stage hooks only when resolved bounds are required. An unconditional `cx.notify()`, `window.refresh()`, or `window.request_animation_frame()` in `render` creates continuous work even when the UI is idle. See [Render](./render) and [Element](./element).
## Async work with a Window
Use `cx.spawn_in(window, ...)` when a [Task](./task) belongs to the current Entity and later needs both Entity and Window access:
```rust
struct Chat {
load_task: Option>,
}
fn load_conversation(&mut self, window: &mut Window, cx: &mut Context) {
self.load_task = Some(cx.spawn_in(window, async move |this, cx| {
let Ok(messages) = fetch_messages().await else { return };
this.update_in(cx, |this, _window, cx| {
this.messages = messages;
cx.notify();
})
.ok();
}));
}
```
`cx.spawn_in` provides a `WeakEntity` and an `AsyncWindowContext`. If the Entity or Window has gone away, `update_in` returns an error; propagate or handle it instead of assuming they still exist.
Use `window.spawn(cx, ...)` when the task needs the Window but does not belong to one Entity. Use `cx.spawn(...)` when no Window access is needed, and `cx.background_spawn(...)` for CPU-heavy work. A `Task` is cancelled when dropped, so store it on the owning View when its lifetime should follow that View, or call `.detach()` only for work that should continue independently.
## Subscribe with Window access
Use `cx.subscribe_in` when an Event callback needs `&mut Window`, for example to restore Focus after a child finishes:
```rust
struct Workspace {
chat: Entity,
_subscriptions: Vec,
}
impl Workspace {
fn new(chat: Entity, window: &mut Window, cx: &mut Context) -> Self {
let _subscriptions = vec![
cx.subscribe_in(&chat, window, |_this, chat, event, window, cx| {
if let ChatEvent::ConversationOpened = event {
window.focus(&chat.read(cx).focus_handle(cx), cx);
}
}),
];
Self { chat, _subscriptions }
}
}
```
Store the returned `Subscription` on the subscribing View. Dropping a local variable immediately cancels the subscription. Storing it on a longer-lived global owner can keep the callback and captured resources alive after the View disappears, causing a memory leak. See [Event](./event) for subscription ownership and multiple subscribers.
## Window lifetime
Do not store `&mut Window`; it is a temporary context supplied by GPUI. For later work, use `defer`, `spawn_in`, or obtain `window.window_handle()` and update it through GPUI. A handle does not keep a closed window alive, so handle-based updates can fail and should be treated accordingly.
`window.remove_window()` requests removal from the current update. To decide whether a platform close request may proceed, register `window.on_window_should_close(cx, callback)` and return `false` to cancel it; the application owns any unsaved-work confirmation flow. To observe a completed close, `cx.on_window_closed(callback)` returns a `Subscription` whose callback takes `&mut App` and `WindowId`, in that order. Retain that subscription on an application owner. The closed `Window` is already inaccessible when this callback runs, so gather any needed window state before closing it.
Register the close guard while that window is available, typically in the `open_window` builder. It applies to the platform's close request; `remove_window()` is an explicit programmatic removal. If the app should exit when its last window closes, use the closed callback to check `cx.windows().is_empty()` and call `cx.quit()`. The [FPS monitor example](https://github.com/MohsenDastaran/uni-kit/blob/main/examples/fps_monitor/src/main.rs) shows this single-window quit pattern and a View requesting animation frames. Run it from this repository with `cargo run -p fps_monitor`; it uses GPUI Kit without the optional Component layer.
If a window action appears to do nothing, first check that the handle still names an open window and that the focused Element's Dispatch Path contains the Action handler. If an Entity mutation is not visible, verify that its update calls `cx.notify()`; if a window-level change is not visible, call `window.refresh()`. If a frame callback fires without a draw, remember that `on_next_frame` creates frame demand but does not dirty the window. If an Event callback stops firing, check that its `Subscription` is retained.
Keep these ownership rules together:
- persistent UI state belongs to an Entity;
- window-specific work receives `&mut Window` only for the duration of a callback;
- `Task` and `Subscription` fields tie background work and observers to the owning View;
- Focus and Action dispatch always use the state of the specific Window.
Continue with [Multi Window](./multi-window) when an application owns several windows. [Native Extensions](./native-extension) covers OS APIs at a window boundary; [WebView](./webview) explains the native child-view integration and its limits. For OS-delivered notifications, see [SystemNotification](./system-notification).
---
# Packaging
Source: /docs/packaging
# Packaging Desktop Apps
`cargo build --release` produces an executable, not an installer. GPUI Kit supplies the application and UI layers; your application owns its package identity, icons, external files, installer, signing, and release tests. Start with [Installation](/docs/installation) and [Getting Started](/docs/getting-started), then build and package **on each target platform**. This guide uses the repository's `hello_world` binary to make paths concrete. Replace `hello_world`, `HelloWorld`, the sample version, and `com.example.helloworld` with your application's values before shipping.
## Choose the artifact
| Platform | Portable artifact | Installed artifact | What changes |
| --- | --- | --- | --- |
| macOS | A `.app` inside a ZIP or tarball | A `.app` distributed in a `.dmg` | Finder recognizes the app bundle; direct distribution needs Developer ID signing and notarization. A DMG is a delivery container, not an installer that writes files automatically. |
| Windows | ZIP containing an `.exe` and its files | An Inno Setup `.exe` installer | The installer registers the app, shortcuts, and uninstall behavior. Renaming a ZIP to `.exe` does not provide that behavior. |
| Linux | `.tar.gz` with the binary and resources | Distribution package such as `.deb` | A DEB declares package identity and dependencies, installs a desktop entry and icon, and supports package manager upgrade/removal. A tarball needs an explicit install process. |
Choose only the formats your users need. A `.deb` does not cover RPM distributions, and a Windows ZIP is useful for portable use even when you also provide an installer. [Apple's distribution guide](https://developer.apple.com/documentation/xcode/packaging-mac-software-for-distribution), [Microsoft's packaging overview](https://learn.microsoft.com/en-us/windows/apps/package-and-deploy/packaging/), and the [Debian binary package guide](https://www.debian.org/doc/debian-policy/ap-pkg-binarypkg.html) describe the platform rules.
## Build and inventory the application
From this repository's root, build the existing example on the platform being packaged:
```sh
cargo build --release --locked -p hello_world
```
The output is `target/release/hello_world` on macOS/Linux or `target\release\hello_world.exe` on Windows. In your own application, run `cargo build --release --locked` from its project directory and use its binary name. Keep the `Cargo.lock` used to build the release, and record the target OS, CPU architecture, app version, and build revision with the artifact. Build separately for each architecture you publish; a successful x86-64 build does not establish an Arm build.
Before packaging, list everything the program opens **at runtime**:
- GPUI Kit's default native icon assets are embedded in the binary. A custom `AssetSource` may instead read files at runtime. Assets addressed through a filesystem `Path` need to be copied into the package and resolved relative to a stable installed location; see [Icons & Assets](/docs/assets).
- Fonts inserted with `include_bytes!` are compiled into the executable. Fonts opened from disk must travel with it, with their licenses; see [Fonts](/docs/fonts).
- Include other required files such as configuration templates, translations, helper executables, and license notices. Keep mutable user data outside the installed application directory.
- Review native dynamic libraries and system services used by *your* feature set. A compile-time development package is not necessarily a runtime dependency. For example, a Linux GPUI window needs a working graphical session and Vulkan driver; a machine with only the Vulkan loader cannot render it. See [Installation](/docs/installation).
Give the app a stable identity before publishing it. The visible name, executable name, platform package identifier, update channel, and data directory policy should agree across releases. If the app uses GPUI's application identity for notifications or related platform integration, set it at startup with `cx.set_app_identity("com.example.helloworld", "HelloWorld")`; see [System Notifications](/docs/system-notification). Package identity and GPUI runtime identity serve different APIs, so check both. A new identifier can create a second installation or separate settings/notification identity rather than upgrading the old one.
## macOS
A macOS application is a directory with a defined bundle layout: `Contents/MacOS` holds the executable, `Contents/Resources` holds resources, and `Contents/Info.plist` declares the executable and bundle identifier. [Apple documents the bundle structure](https://developer.apple.com/documentation/bundleresources/placing-content-in-a-bundle). On a Mac, assemble a minimal local test bundle from this repository's example:
```sh
mkdir -p dist/HelloWorld.app/Contents/MacOS dist/HelloWorld.app/Contents/Resources
cp target/release/hello_world dist/HelloWorld.app/Contents/MacOS/hello_world
cat > dist/HelloWorld.app/Contents/Info.plist <<'PLIST'
CFBundleNameHelloWorldCFBundleDisplayNameHelloWorldCFBundleIdentifiercom.example.helloworldCFBundleExecutablehello_worldCFBundlePackageTypeAPPLCFBundleShortVersionString{{gpui_kit_version}}CFBundleVersion1
PLIST
plutil -lint dist/HelloWorld.app/Contents/Info.plist
open dist/HelloWorld.app
```
That bundle has no custom icon. For your app, add an `.icns` in `Contents/Resources`, declare `CFBundleIconFile`, and copy external files under `Resources`; resolve them from the bundle location rather than the caller's working directory. Update both version fields for each release. The example above is a **local bundle test**, not a signed public release.
For distribution outside the Mac App Store, sign the final app with a Developer ID Application certificate and hardened runtime, then create and sign a DMG. Sign any nested executable code from the inside out before signing the app; do not treat `codesign --deep` as a substitute for that signing order. Verify the signed app before creating the DMG. Apple's [signing guide](https://developer.apple.com/documentation/xcode/creating-distribution-signed-code-for-the-mac/) explains these requirements. A representative final sequence is:
```sh
codesign --force --timestamp --options runtime --sign "Developer ID Application: Your Organization" dist/HelloWorld.app
codesign --verify --deep --strict --verbose=2 dist/HelloWorld.app
mkdir -p dist/dmg-root
cp -R dist/HelloWorld.app dist/dmg-root/HelloWorld.app
hdiutil create -volname HelloWorld -srcfolder dist/dmg-root -ov -format UDZO dist/HelloWorld.dmg
codesign --force --timestamp --sign "Developer ID Application: Your Organization" dist/HelloWorld.dmg
xcrun notarytool submit dist/HelloWorld.dmg --keychain-profile "notary-profile" --wait
xcrun stapler staple dist/HelloWorld.dmg
xcrun stapler validate dist/HelloWorld.dmg
```
Replace the signing identity and `notary-profile` with credentials you provision in your own release process. Check that `notarytool` reports **Accepted**; `--wait` alone is not a success criterion. Investigate its submission log if rejected. Staple and validate the accepted DMG, then download and launch that exact artifact on a clean Mac. An ad hoc signature (`--sign -`) only seals code for local use; it does not replace Developer ID signing and notarization for direct distribution. See Apple's [notarization workflow](https://developer.apple.com/documentation/security/customizing-the-notarization-workflow).
## Windows
Build with the MSVC Rust toolchain and Windows SDK described in [Installation](/docs/installation). Stage the executable and any runtime files first. A ZIP can be enough for a portable app:
```powershell
New-Item -ItemType Directory -Force dist\windows | Out-Null
Copy-Item target\release\hello_world.exe dist\windows\hello_world.exe
Compress-Archive -Path dist\windows\* -DestinationPath dist\hello-world-windows-x64.zip -Force
```
Test by extracting the ZIP into a fresh directory and launching the extracted `.exe`; do not test only `target\release`. A ZIP does not add an uninstall entry or Start menu shortcut. If your app needs those, build an `.exe` installer with [Inno Setup](https://jrsoftware.org/isinfo.php). Save this minimal **unsigned local example** as `packaging.iss` in the project root, after staging `dist\windows\hello_world.exe`:
```ini
[Setup]
AppId=Example.HelloWorld
AppName=HelloWorld
AppVersion={{gpui_kit_version}}
DefaultDirName={autopf}\HelloWorld
DefaultGroupName=HelloWorld
OutputDir=dist
OutputBaseFilename=HelloWorld-Setup
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
[Files]
Source: "dist\windows\hello_world.exe"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
Name: "{group}\HelloWorld"; Filename: "{app}\hello_world.exe"
[Run]
Filename: "{app}\hello_world.exe"; Description: "Launch HelloWorld"; Flags: nowait postinstall skipifsilent
```
```powershell
ISCC.exe packaging.iss
```
Keep `AppId` stable for upgrades. Add your own icon, publisher metadata, license, files, and install scope as needed. For a signed release, sign the application executable **before** placing it in either the ZIP or installer, then sign the finished setup `.exe`. Inno Setup's uninstaller is embedded in that `.exe`, so signing the setup afterward does **not** sign the uninstaller; configure Inno's [signed uninstaller workflow](https://jrsoftware.org/ishelp/topic_setup_signeduninstaller.htm) or an equivalent two-pass build. Check all signatures and install/uninstall behavior on a clean Windows VM. The exact signing mechanism depends on your certificate provider; it is not supplied by GPUI Kit.
## Linux
For a portable tarball, stage the executable, any external files, license notices, and a launcher that resolves files from its own directory. Add your own 256 × 256 PNG at `packaging/hello-world-gpui.png`, then make an archive for the minimal example:
```sh
mkdir -p dist/portable/HelloWorld/bin \
dist/portable/HelloWorld/share/icons/hicolor/256x256/apps
cp target/release/hello_world dist/portable/HelloWorld/bin/hello_world
install -m 0644 packaging/hello-world-gpui.png \
dist/portable/HelloWorld/share/icons/hicolor/256x256/apps/hello-world-gpui.png
tar -czf dist/hello-world-linux-x64.tar.gz -C dist/portable HelloWorld
tar -tzf dist/hello-world-linux-x64.tar.gz
```
Extract the archive somewhere else and launch the executable there. For an app with external files, copy them under `HelloWorld` before archiving and locate them from the executable or launcher, not the shell's current directory. A bare Rust binary may run on your build machine while missing shared libraries or a usable GPU driver on another. Inspect direct ELF requirements with `objdump -p target/release/hello_world` and test on the oldest distribution you support. If you use `ldd`, run it only on binaries you built or trust.
For users who want a desktop menu entry without a system package, publish an `install.sh` alongside the tarball. The following small installer accepts a **local** archive and its expected SHA-256 digest as two arguments. Give users the digest through a trusted release channel; a checksum downloaded from the same untrusted location as a replaced archive does not authenticate that archive. The script verifies the entire archive before changing installed files, rejects unsafe paths and links, requires the binary and icon, then installs into the current user's default XDG locations. It needs Python 3 and no root access:
```sh
#!/bin/sh
set -eu
[ "$#" -eq 2 ] || { echo 'Usage: sh install.sh ARCHIVE EXPECTED_SHA256' >&2; exit 2; }
python3 - "$1" "$2" <<'PY'
from pathlib import Path, PurePosixPath
import hashlib
import io
import os
import re
import shutil
import sys
import tarfile
import tempfile
archive = Path(sys.argv[1])
expected = sys.argv[2].lower()
if not re.fullmatch(r"[0-9a-f]{64}", expected):
raise SystemExit("Expected a 64-character SHA-256 digest")
digest = hashlib.sha256()
verified = tempfile.TemporaryFile()
with archive.open("rb") as archive_stream:
for chunk in iter(lambda: archive_stream.read(1024 * 1024), b""):
digest.update(chunk)
verified.write(chunk)
if digest.hexdigest() != expected:
raise SystemExit("Checksum mismatch; nothing installed")
verified.seek(0)
binary_name = "HelloWorld/bin/hello_world"
icon_name = "HelloWorld/share/icons/hicolor/256x256/apps/hello-world-gpui.png"
with tarfile.open(fileobj=verified, mode="r:gz") as bundle:
members = {}
for member in bundle:
path = PurePosixPath(member.name)
if (not path.parts or path.is_absolute() or ".." in path.parts
or path.parts[0] != "HelloWorld"
or not (member.isfile() or member.isdir())
or str(path) in members):
raise SystemExit("Unsafe or duplicate archive entry")
members[str(path)] = member
if not all(name in members and members[name].isfile()
for name in (binary_name, icon_name)):
raise SystemExit("Archive is missing its executable or icon")
bin_dir = Path.home() / ".local/bin"
data_dir = Path.home() / ".local/share"
app_dir = data_dir / "applications"
icon_dir = data_dir / "icons/hicolor/256x256/apps"
for directory in (bin_dir, app_dir, icon_dir):
directory.mkdir(parents=True, exist_ok=True)
binary = bin_dir / "hello_world"
icon = icon_dir / "hello-world-gpui.png"
desktop = app_dir / "hello-world-gpui.desktop"
# Desktop Entry quoting has two escape layers; this handles a home path with spaces.
exec_path = str(binary).replace("\\", "\\\\\\\\")
for character in ('"', '`', '$'):
exec_path = exec_path.replace(character, "\\\\" + character)
exec_path = exec_path.replace("%", "%%")
desktop_content = (
"[Desktop Entry]\nType=Application\nName=HelloWorld\n"
+ f'Exec="{exec_path}"\n'
+ "Icon=hello-world-gpui\nTerminal=false\nCategories=Utility;\n"
)
staged_files = []
def stage_file(destination, input_stream, mode):
with tempfile.NamedTemporaryFile(
dir=destination.parent, prefix=".hello-world-install-", delete=False
) as staged:
staged_path = Path(staged.name)
staged_files.append(staged_path)
shutil.copyfileobj(input_stream, staged)
staged_path.chmod(mode)
return staged_path
try:
with bundle.extractfile(members[binary_name]) as entry_stream:
staged_binary = stage_file(binary, entry_stream, 0o755)
with bundle.extractfile(members[icon_name]) as entry_stream:
staged_icon = stage_file(icon, entry_stream, 0o644)
staged_desktop = stage_file(
desktop, io.BytesIO(desktop_content.encode("utf-8")), 0o644
)
for staged, destination in (
(staged_binary, binary),
(staged_icon, icon),
(staged_desktop, desktop),
):
os.replace(staged, destination)
finally:
for staged in staged_files:
staged.unlink(missing_ok=True)
verified.close()
print("Installed HelloWorld for the current user")
PY
```
Save the code as `install.sh`. Run `sh install.sh dist/hello-world-linux-x64.tar.gz SHA256_FROM_TRUSTED_RELEASE`, replacing the final argument with the archive's real 64-character digest. The example uses `~/.local/bin` for the executable and the default XDG data directory `~/.local/share` for the desktop file and icon; [the XDG Base Directory specification](https://specifications.freedesktop.org/basedir/latest/) defines these defaults. If your desktop uses a custom XDG data home, adapt `data_dir` to that configured **absolute** directory before using the script. The desktop entry uses an absolute executable path, so the menu does not depend on `~/.local/bin` being on the desktop session's `PATH`. See the [Desktop Entry specification](https://specifications.freedesktop.org/desktop-entry/latest-single/) for `Exec` quoting and icon lookup.
Close the app and rerun the installer with a verified newer archive to upgrade. Replacing the binary within one directory is atomic, while updating the binary, icon, and desktop entry together is **not** one transaction. For this minimal example, uninstall with `rm ~/.local/bin/hello_world ~/.local/share/applications/hello-world-gpui.desktop ~/.local/share/icons/hicolor/256x256/apps/hello-world-gpui.png`; leave user settings and documents in their own directories. An application with additional runtime files needs an installer that stages and updates those files too.
For Debian/Ubuntu installation, stage files under a package root. The following tree shows the key locations; copy your actual binary into `usr/lib/hello-world-gpui/`, your icon into the hicolor icon tree, and your desktop entry into `usr/share/applications/`:
```text
dist/deb-root/
├── DEBIAN/control
└── usr/
├── lib/hello-world-gpui/hello_world
└── share/
├── applications/hello-world-gpui.desktop
└── icons/hicolor/256x256/apps/hello-world-gpui.png
```
Create the staging directories and copy the example binary on a Linux build machine; supply your own application icon:
```sh
mkdir -p dist/deb-root/DEBIAN \
dist/deb-root/usr/lib/hello-world-gpui \
dist/deb-root/usr/share/applications \
dist/deb-root/usr/share/icons/hicolor/256x256/apps
install -m 0755 target/release/hello_world \
dist/deb-root/usr/lib/hello-world-gpui/hello_world
```
Use a stable package name and an architecture/version that match the binary. A minimal `DEBIAN/control` for the sample is:
```text
Package: hello-world-gpui
Version: {{gpui_kit_version}}
Section: utils
Priority: optional
Architecture: amd64
Maintainer: Example Maintainer
Description: A GPUI Kit desktop example
```
Add the runtime `Depends` derived from your **built application** and distribution baseline; `dpkg-shlibdeps` can help derive shared-library dependencies. Do not copy another application's dependency list. A desktop entry for the staged paths is:
```ini
[Desktop Entry]
Type=Application
Name=HelloWorld
Exec=/usr/lib/hello-world-gpui/hello_world
Icon=hello-world-gpui
Terminal=false
Categories=Utility;
```
When all staged files are present, build and inspect the package. The [documented `--root-owner-group` option](https://manpages.debian.org/unstable/dpkg/dpkg-deb.1.en.html) records root ownership for staged package files even when you build as a regular user; use it when all packaged files should be root-owned.
```sh
dpkg-deb --root-owner-group --build dist/deb-root dist/hello-world-gpui_{{gpui_kit_version}}_amd64.deb
dpkg-deb --info dist/hello-world-gpui_{{gpui_kit_version}}_amd64.deb
dpkg-deb --contents dist/hello-world-gpui_{{gpui_kit_version}}_amd64.deb
```
Install a copy on a clean test machine with `sudo apt install ./dist/hello-world-gpui_{{gpui_kit_version}}_amd64.deb`, launch it from the desktop menu, then remove it with `sudo apt remove hello-world-gpui`. [Debian policy](https://www.debian.org/doc/debian-policy/ch-opersys.html) prohibits packages from placing their files in `/usr/local`; the [Desktop Entry specification](https://specifications.freedesktop.org/desktop-entry/latest-single/) defines the launcher keys. Use a separate RPM packaging recipe for RPM distributions.
## Verify the release artifact
Do the final checks against the **downloaded package**, not the original build directory:
1. Confirm filename, embedded app version, architecture, package identity, and checksum match the intended release. Keep one version source of truth where possible.
2. Inspect the archive/package contents for the executable, icon, external assets, required notices, and accidental development files. Extract it to a new location; launch from there without relying on the source tree or working directory.
3. On a clean machine or VM for each supported OS/architecture, check first launch, window creation, text and icons, keyboard and pointer input, notifications or WebView if used, and behavior without a network connection if offline use is promised. Linux tests need a real graphical session and Vulkan driver.
4. Install version N, install N+1 over it, and check both application data and shortcuts. Then uninstall and check which files remain by design. Test portable archives separately because they have no package-managed upgrade or removal.
5. Verify signatures and notarization where applicable, publish SHA-256 checksums, and download the artifact again to compare its checksum before announcing it.
Package format does not guarantee platform support for every optional feature. In particular, check the [WebView](/docs/webview) target limitations before promising a Linux WebView build.
For in-app version checks and upgrades after packaging, continue with [Auto Update](/docs/auto-update).
---
# Design Guides
Source: /docs/design-guides
Use this guide before choosing components or writing layout code. It records
the product judgment accumulated through years of GPUI Kit desktop work:
an interface should feel native, restrained, precise, and understandable
without guesswork.
This is a normative guide. **Must** identifies a correctness or ecosystem
constraint, **should** is the default that needs a concrete reason to override,
and **may** is an optional technique. Component API documentation remains the
authority for individual methods.
The rules build on behavior in `gpui-base`, the GPUI Component theme and
component system, and familiar desktop interaction. shadcn/ui contributes useful
methods—open code, composition, and dependable defaults—but does not determine
how a GPUI application should look. When influences conflict, preserve GPUI's
lifecycle constraints and the interaction people already understand.
## Design thesis
Build interfaces that feel native, quiet, and precise. Let content, hierarchy,
and interaction carry the experience; decoration should support them rather
than compete with them.
1. **Clarity before personality.** Make the primary task and next action clear
before adding brand expression.
2. **Composition before invention.** Start with established components and
compose them into product-specific workflows. Create a new primitive only
when its behavior is genuinely new.
3. **Tokens before values.** Colors, radii, typography, and spacing should form
a system. Avoid isolated literals that cannot respond to themes. See
[Style](./style) for GPUI's styling API.
4. **Desktop before web convention.** Preserve keyboard access, window chrome,
menus, dense data views, resizable regions, and persistent navigation where
the task benefits from them.
5. **State must be visible.** Hover, focus, selection, disabled, loading,
validation, and destructive states need distinct and consistent treatment.
6. **Geometry explains containment.** Radii, insets, and boundaries should
describe one coherent hierarchy, even where surfaces overlap.
7. **Feedback reflects reality.** Loading, tooltips, and motion should clarify
a real state or transition, not add ceremony to an immediate action.
## Learning from Shadcn
Shadcn's most useful contribution is not a particular border color. It is a
way of building a system:
- own the top layer of the interface instead of fighting a sealed abstraction;
- compose small, predictable parts into product-specific components;
- provide defaults that already form one visual language;
- keep the code and composition legible to both people and AI;
- separate behavior primitives from the styled layer.
GPUI Kit applies those ideas through a Rust library and the split between
`gpui-base` and `gpui-component`. Applications normally compose or wrap the
published components; contributors move genuinely reusable behavior into Base
and keep visual policy above it.
Do not copy these web assumptions blindly:
| Web habit | Native GPUI default |
| --- | --- |
| Pointing-hand cursor on every button | Default arrow cursor; pointing hand for links |
| Page navigation as the main structure | Persistent windows, panes, sidebars, tabs, and menus |
| Browser focus and scrolling as a fallback | Explicit focus ownership and region-owned scrolling |
| Mobile-first single column | Resizable desktop shell with a defined minimum window size |
| Hover-revealed critical actions | Keyboard- and pointer-reachable actions that do not depend on hover |
| A row of hover-only icon buttons | A visible primary action plus `DropdownMenu` or `ContextMenu` for secondary commands |
| Link-styled text for application commands | `Button`, `outline`, or `ghost`; Link only for URLs, web resources, or email addresses |
| Large touch density everywhere | Medium density by default; compact only where information work benefits |
| CSS overrides across descendants | Typed builders, semantic parts, and application composition |
## Start from the task
Before drawing a screen, write down:
- the user's primary task;
- the object being viewed or changed;
- the actions that must remain immediately available;
- the information required to make a decision;
- empty, loading, error, offline, read-only, and permission-denied states;
- the keyboard path through the workflow.
Organize the window around those answers. Do not begin with a dashboard grid or
a component catalogue. A good desktop interface exposes the user's mental
model: documents, accounts, projects, messages, settings, or another stable
object—not the internal service architecture.
Design the primary task before distributing controls. Its visual weight,
location, and information depth should match its importance to the product. A
core result must not be reduced to a small count, an icon in a corner, or a
weak footer action while secondary content consumes the page. When a result set
is the product's main value, consider a summary region or card that exposes the
count, representative results, meaningful state, and a clear next action.
For every proposed action, name its visible object, current state, scope, and
result. If the interface does not show or clearly imply those things, the
action is premature. Do not expose a capability merely because the backend has
it; first design how the object enters the user's mental model.
## Visual language
### Hierarchy
Prefer a small number of clear levels:
- **window or page title** identifies the current object or workspace;
- **section title** separates meaningful regions;
- **body text** carries the work;
- **muted text** provides secondary metadata and help;
- **labels** identify controls and values.
Use size, weight, spacing, and separators before adding color or containers.
Avoid nesting cards inside cards: most desktop regions need only a background,
a hairline boundary, and intentional spacing.
Evaluate hierarchy across the whole feature, not component by component. Hide
accent color and decoration during review: the primary task, current selection,
result summary, and next action should still be obvious from structure. A
screen that contains individually plausible controls can still fail when they
do not form one reading order and one decision path.
Treat emphasis as a limited budget. A local surface needs one clear focal point,
not a field of competing highlights. If everything is colored, badged, bold,
boxed, or promoted to an alert, nothing reads as important. Establish priority
with structure and proximity first; spend stronger color and components only
where a distinction changes what the user notices or does.
### Color and themes
Read colors from `cx.theme()` and use them by semantic role:
- `background` and `foreground` for the main surface and text;
- `group_box`, `popover`, `sidebar`, and their foreground tokens for their
named surfaces;
- `muted` and `muted_foreground` for supporting information;
- `primary` for the principal action or selection emphasis;
- `danger`, `warning`, `success`, and `info` only for their meanings;
- `border`, `input`, and focus-ring tokens for structure and interaction.
Do not use a semantic status color as decoration. Do not encode meaning by
color alone. Verify every custom surface in light and dark themes and with
custom theme values; never assume that foreground is black or background is
white.
Use Badge for a short state, count, or classification that benefits from rapid
scanning—not for every label, metadata value, filter, or section title. Keep
most badges neutral; reserve semantic variants for states that truly carry
success, warning, danger, or informational meaning. A row of multicolored
badges is usually a missing hierarchy or grouping decision.
Application UI should not contain raw hex, `rgb`/`rgba`, or `hsla` colors.
Resolve colors from `cx.theme()` by semantic role. If the required role does
not exist, define it in the product's theme/token layer rather than embedding a
palette value at the call site. Raw colors belong only inside theme definitions
or in audited data/raster content whose color is itself the data.
### Radius, spacing, and density
Use a finite radius scale, as with type and control sizes. Choose from the
theme's named tiers (`sm`, `md`, `lg`, `xl`, and larger surface tiers) instead
of inventing a radius for each element. Large containers may use a softer tier
than the controls they contain; compact controls and overlays use tighter
tiers. For an ordinary rectangle, the radius should remain visibly smaller
than half its shorter side. Use `radius_full()` only for intentional circles
and pills; a tooltip must not become a capsule by accident.
Nested surfaces should read as concentric shapes. Use
`inner radius ≈ max(0, outer radius − gap)` to choose the closest suitable
theme tier; the visible band should remain even through the turn. Add a
named application token if a repeated composition needs an intermediate
value. A segmented control is one silhouette: its end segments follow the container's inner
corners, its middle segments remain square, and each boundary has one divider
of consistent thickness.
A radius must govern the entire visible surface, not just its border. Exposed
corners remain transparent to the surface behind them; child backgrounds,
selection fills, and scrolling content must also follow the boundary. In GPUI,
do not assume a rounded parent clips its descendants. Give inner surfaces their
own radius or an appropriate clip, then inspect the composition against a
contrasting background in both themes. Preserve outward focus rings when
choosing a clip.
Use a compact spacing scale and repeat it. Related label/control pairs should
be closer than separate groups; separate groups should be closer than separate
sections. Prefer component sizes (`xsmall`, `small`, default medium, `large`)
over one-off heights. Use compact variants for toolbars and data-dense screens,
not to squeeze an unclear layout into less space.
The shared semantic scale is intentionally small: spacing progresses through
roughly 2, 4, 8, 12, 16, 24, and 32 pixels, while typography stays near 12,
14, 16, 18, and 20 pixels. Treat these as relationships rather than permission
to scatter their current values through feature code. GPUI Component currently
projects a fixed default `SpacingTokens` scale from its global `Theme`; unlike
colors and radii, `Theme::apply_semantic_tokens` does not persist a custom
spacing scale. An application that needs different spacing must own that full
token snapshot and use it consistently in its application components.
### Spatial grammar
Spacing expresses relationship. Choose a gap from the semantic scale by asking
what the two things mean to each other:
| Relationship | Typical token | Current scale | Examples |
| --- | --- | --- | --- |
| Optical correction | `xxs` | 2 px | icon baseline, compact separator |
| Parts of one control | `xs` | 4 px | menu icon/label, title/description |
| Closely related controls | `sm` | 8 px | button icon/label, dialog actions |
| One content group | `md` | 12 px | notification columns, compact form rows |
| Separate groups in one section | `lg` | 16 px | panel padding, form groups |
| Separate sections | `xl` | 24 px | major blocks in a page or inspector |
| Major region boundary | `xxl` | 32 px | empty-state breathing room, page bands |
These values describe the current default scale, not literals to repeat. Use
`cx.theme().spacing_tokens()` or the corresponding GPUI scale helpers for the
ecosystem default. A product-owned scale should preserve the ordering and
relationships and must be passed through the application's own design-system
context rather than assumed to persist in the global GPUI Component theme.
Use these rules when resolving horizontal and vertical space:
1. **Inside before outside.** A component's padding belongs to the component;
the gap between components belongs to their parent.
2. **Vertical rhythm shows grouping.** The gap between a title and its
description is smaller than the gap from that description to the next
section. Equal gaps imply equal relationships.
3. **Horizontal space supports scanning.** Repeated rows keep icons, labels,
values, badges, and trailing actions on stable columns.
4. **Leading and trailing are semantic.** Think in reading-order edges even
when the current API uses left/right; this keeps future RTL adaptation
possible.
5. **Do not double padding.** A card placed in an already padded panel should
not automatically add another full panel inset.
6. **Use optical alignment sparingly.** A one- or two-pixel correction is valid
for icon or glyph geometry, but document why it differs from the scale.
Common compositions in the current system illustrate the relationships:
- button contents use 4 px at small sizes and 8 px at normal sizes;
- dialog headers and footers use an 8 px internal gap;
- compact list and menu rows use 4 px vertical and 8–12 px horizontal padding;
- sheet headers use about 16 px leading and 12 px trailing space, leaving room
for a close affordance, while footers use 16 px horizontal and 12 px vertical
space;
- notifications use 16 px horizontal padding and a 12 px column gap because
icon, message, and action are distinct groups.
Do not treat these as copy-and-paste recipes for every surface. They reveal the
system: controls are tighter internally, rows optimize scanning, and
containers spend more space at their boundary than between their contents.
### Proportion and layer hierarchy
Start with content requirements, then set proportions. Avoid arbitrary halves
when one pane has a clearly different role.
- A navigation sidebar should be wide enough for stable labels but visibly
subordinate to the work area. Give it a minimum, preferred, and maximum
width rather than a percentage alone.
- In master–detail layouts, let the collection remain scannable and give the
detail pane the surplus. A roughly one-third/two-thirds starting point is
often useful, but content constraints are authoritative.
- Inspectors and auxiliary sheets should not cover the primary object by
default. They should be resizable or dismissible when their content grows.
- Dialog width comes from the decision: short confirmation, medium form, or a
dedicated window/page for complex work. Do not enlarge a dialog simply to
create whitespace.
- Reserve the strongest elevation for the topmost decision layer. Within a
layer, use background and hairlines—not successively larger shadows—to show
hierarchy.
Define three size constraints for every major region: the minimum at which its
task still works, a comfortable default, and how it consumes surplus. Persist
user-controlled splits when they represent workflow preference, and clamp
restored values against the current window.
### Alignment details
Alignment is a structural system, not a final polish pass. Establish a small
set of alignment spines for each surface: shared leading and trailing edges,
text baselines, center lines, and fixed functional lanes. Elements at the same
level should attach to the same spine from top to bottom or leading to trailing,
even when they are different component types.
Vertical red lines sit beside shared edges or control centers for content,
status, time, and trailing actions. Horizontal lines sit beneath text baselines
or pass through a row center to show bottom and vertical-center alignment. The
compact comparison isolates a one-rendered-pixel drift that must be corrected
at its structural owner.
- Give sibling regions a shared content inset. A heading, toolbar, list row,
empty state, and footer that describe the same level should not each invent a
slightly different leading edge.
- Repeat column geometry through the whole region. Headers, rows, summaries,
loading states, and inline editors should reserve the same lanes for identity,
metadata, status, numbers, and actions.
- Align related controls across rows and sections. Form labels, fields,
descriptions, and validation messages should reveal a stable vertical grid
when the page is scanned from top to bottom.
- Keep horizontal bands coherent. Items sharing a toolbar, title bar, status
bar, or row should use one baseline or center line instead of individually
tuned offsets.
- Introduce indentation only for real hierarchy, containment, or disclosure.
Decorative indentation makes siblings look subordinate and breaks the
surface's reading line.
- When a nested level ends, return exactly to the parent spine. Do not let
accumulated padding drift across nested containers.
- Preserve the spine through optional content. Missing icons, badges,
descriptions, or trailing actions must not move the remaining labels; use
intentional slots or lanes when cross-row comparison matters.
- Align major regions with one another where their hierarchy matches. Sidebar
headers, content titles, split panes, toolbars, and bottom bars need not share
every coordinate, but coincident levels should form visible continuous lines.
Not every edge should align. A child can indent, a primary value can lead its
supporting metadata, and a destructive decision can gain separation. Such
exceptions must communicate hierarchy or meaning; they must not result from
uncoordinated component padding. Start with the shared spine, then make the
exception explicit.
Treat exact alignment and repeated gaps as quality invariants. When two edges
or spaces are intended to be equal, a one-rendered-pixel difference is a defect,
not an acceptable optical approximation. Inspect resolved bounds with a
measurement tool at representative window sizes, zoom levels, and display scale
factors. Compare coordinates and distances; do not approve alignment only from
a casual screenshot.
The rendered-pixel tolerance is a verification rule, not permission to patch
the code with raw pixel offsets. Equal relationships should resolve from the
same `rem` helper, spacing token, grid definition, or shared component inset.
Fix the common owner when they differ. Account for fractional layout and device
rounding so intended spines land on the same physical pixel instead of drifting
at particular zoom levels.
- Align text by baselines, not by bounding-box centers, when mixed sizes share
a row.
- Center icons in a fixed slot so labels do not move when icons differ in
intrinsic width.
- Right-align comparable numbers; left-align prose and identifiers unless the
locale requires otherwise.
- Keep trailing row actions and disclosure indicators in fixed-width lanes.
- Align form controls by their interactive frame, not by help text below them.
- Use `justify_between` only when the two sides truly own opposite edges; it
should not disguise missing structure in the middle.
- Hairlines belong on the boundary owner. Two adjacent regions must not each
draw the same separator.
- A scrollbar belongs to the region that scrolls and sits against that panel,
editor, or window's trailing edge. Content padding may inset text and rows;
it must not pull the scrollbar into the middle of the surface. Reserve a
deliberate scrollbar gutter when content needs clearance.
### Density tiers
Medium is the ecosystem default. Change density for the whole local context,
not one isolated control:
- **comfortable / large:** onboarding, sparse forms, prominent decisions;
- **standard / medium:** most application chrome and workflows;
- **compact / small:** toolbars, menus, tables, and repeated professional data;
- **extra compact / xsmall:** exceptional high-density utilities, never the
automatic choice for an entire application.
The current controls demonstrate a bounded scale rather than arbitrary sizing:
buttons commonly move through approximately 20, 24, and 32 px frames; input
and data controls may extend to about 44 px at large size; table rows use about
26, 30, 32, and 40 px. Use the component's `Size` API so typography, icon,
padding, and hit target change together. A custom height that changes only the
outer box is usually incomplete.
### Zoom, base font, and `rem`
A well-designed `rem` system preserves hierarchy while the interface zooms.
Zoom is successful when the relationship between title and body, control and
icon, inner and outer spacing, primary and secondary regions still feels the
same at every scale—not merely when every object becomes larger.
GPUI Component adopts the relative-scale idea familiar from Tailwind CSS. The
theme's base `font_size` becomes the window's `rem` through `Root`, and GPUI
scale helpers such as `text_sm()`, `gap_2()`, `p_4()`, `h_8()`, and `size_4()`
resolve against it. This gives typography, spacing, controls, and icons one
shared zoom axis.
Design in ratios:
- type steps keep the same hierarchy around the base body size;
- spacing steps keep the same grouping relationships around the type;
- control frames, icons, and hit targets scale with their labels;
- pane minima and comfortable widths account for the scaled content;
- corner radii and focus treatment remain optically consistent with the
control frame.
Do not implement zoom by changing text size alone. A larger label inside a
fixed-height button, a larger document inside fixed pane minima, or larger rows
inside a stale virtual-list measurement destroys the original rhythm and can
clip content. Conversely, multiplying every physical pixel—including
hairlines—can make the interface visually heavy.
As a rule, application layout should not call `px(...)` directly. Use GPUI's
rem-based scale helpers (`p_2`, `gap_3`, `w_64`, `text_sm`, and related
builders) or semantic component sizes. Use fixed pixels only when the value
represents a physical or raster boundary:
a one-device-pixel hairline, platform window inset, bitmap dimension, minimum
hit-test tolerance, or geometry that must match an external surface. These are
audited, documented exceptions. Product spacing, typography, icon size, and
ordinary control geometry stay on the relative scale.
Test interface zoom at several base-font values, not just the default. Verify
hierarchy, wrapping, truncation, minimum window size, pane resizing, focus-ring
clearance, popup placement, and virtualized row measurement. Also distinguish
interface zoom from Dock's panel zoom: Dock zoom makes one container fill its
area while retaining its chrome; it does not change `rem` or application scale.
### Surfaces and elevation
Use elevation to explain stacking, not importance. The base window surface is
flat; separators and background contrast define its regions. Popovers, menus,
dialogs, and notifications may use progressively stronger shadows because they
sit above other content. Do not put a shadow on every card.
All surfaces of the same kind should share one treatment. GPUI Component, for
example, deliberately gives popup families one themed popover surface so
Popover, Select, Combobox, DatePicker, and menus do not drift apart. When an
application invents another anchored surface, reuse that semantic treatment
instead of approximating it with unrelated border and shadow literals.
### Typography and icons
Use the platform UI font for interface text and monospace only for code,
identifiers, shortcuts, and aligned numeric data. Keep body text readable and
avoid excessive uppercase or letter spacing, especially for CJK text.
Use one icon family in a product. Icons supplement labels; they should not
replace unfamiliar actions with guesswork. Icon-only buttons always need an
accessible name. Their visual explanation follows the tooltip policy below.
Use filled or colored icons to communicate a state, not merely to make a
toolbar lively.
## Layout patterns
### Choose a stable shell
Most applications should use one of these shells:
- **single workspace:** toolbar or title bar above one primary view;
- **sidebar workspace:** persistent navigation beside a changing detail view;
- **master–detail:** resizable collection and detail panes;
- **document workspace:** tabs or a dock area for multiple long-lived objects;
- **utility window:** one focused task with a short, fixed action path.
Keep global navigation stable while content changes. Give the primary work area
the remaining space with `flex_1()` and `min_w_0()` / `min_h_0()` where
overflowing children must shrink. Use `Scrollable`, `VirtualList`, `Table`, or
`DockArea` for their intended behavior instead of rebuilding scrolling or pane
management from nested `div`s.
The title bar is window chrome first. Preserve its drag region and avoid
binding ordinary title clicks to infrequent editing commands. Rename belongs
behind an explicit object command, with a keyboard path where appropriate.
### Responsive desktop windows
Desktop does not mean fixed-size. Decide what happens as a window narrows:
1. preserve the primary task;
2. allow resizable regions to reach a documented minimum;
3. collapse secondary labels or inspectors;
4. move low-frequency actions into a menu;
5. scroll only the region whose content actually overflows.
Do not hide an action without providing another path to it. Avoid making the
entire window scroll when only a list or document body should scroll.
GPUI flex layouts have the same intrinsic-size pressure found in other layout
systems: a `flex_1()` child may still refuse to shrink around long content.
Design and implementation must agree on which panes may shrink, truncate, wrap,
or scroll. A clipped region also clips an outward focus ring; never trade away
keyboard visibility merely to simplify overflow.
### Forms and settings
Use a visible label for each field and place help or validation next to the
field it describes. Align related fields, but do not force long labels into a
narrow fixed column. Use the appropriate control: `Checkbox` for independent
choices, `RadioGroup` for a small visible set, `Select` for a longer set, and
`Switch` for a setting that takes effect immediately.
Disable submission while an operation is in flight, keep the user's input, and
show the result near the action. Reserve dialogs for short, focused decisions;
use a full page or sheet for workflows that need exploration or many fields.
A loading treatment must represent actual waiting. Switching between settings
panels whose content is already available should be immediate; a skeleton is
appropriate only while content is genuinely loading and its shape is known.
## Components and composition
Follow the Shadcn principle that components are building material rather than a
sealed design system. GPUI Component supplies coherent defaults, while the
application owns composition and product semantics.
- Use component variants by meaning. Primary is reserved for the explicit
default commit in a decision area—normally the action invoked by Enter. A
lone, frequent, or desirable action is not automatically primary. An `Add`
command in a management toolbar normally uses a default Button; a form's
default `Create` commit may use primary. Use `danger` for destructive
commitment and `ghost` for quiet toolbar actions.
- Prefer explicit compound parts and render callbacks over styling arbitrary
descendants.
- Keep a repeated pattern consistent across the product. Wrap it in an
application component when it carries domain language or policy.
- Use the standard component for its semantic role. A menu, dropdown menu,
popover, select, and command palette are not interchangeable boxes; each owns
different selection, focus, keyboard, dismissal, and layout contracts.
- Preserve the component family's geometry. Menu rows share vertical and
horizontal padding, height, icon and checkmark slots, separators, radius, and
state treatment. Do not imitate one menu with a custom popup whose spacing
only approximates the system.
- Do not wrap a library component merely to rename every method or freeze all
of its capabilities.
- Move reusable behavior without product styling to `gpui-base`; keep themed,
opinionated presentation in GPUI Component or the application.
## Interaction states
### Make the result understandable before the click
A control should predict its result. Use familiar desktop controls and
placement so people can act without learning the interface first. Its label
names the action and object, its state shows availability, and its feedback
confirms the same outcome.
Do not label a Button `Save` if it opens a configuration flow, or `Delete` if
it only removes an item from a group. Name the scope when context does not make
it clear. Respond immediately to activation, prevent duplicate submission
during longer work, and show the result near the object that changed. Add a
success message only when the result itself is not visible.
Every interactive control should be designed for:
| State | Design requirement |
| --- | --- |
| Rest | Clear affordance without visual noise |
| Hover | Subtle pointer feedback, never the only cue |
| Pressed | Immediate press feedback |
| Open / pressed | Persistent feedback while an attached popup is open |
| Focus visible | High-contrast keyboard focus ring |
| Selected / checked | Persistent state distinct from hover |
| Disabled | Lower emphasis and no misleading hover/pressed response |
| Loading | Preserve context, prevent duplicate action, explain long waits |
| Error | State what happened and how to recover |
Use GPUI's focus system and Actions for commands that should work from the
keyboard. Match familiar desktop shortcuts, expose shortcuts in menus or
tooltips, and keep focus in a logical place after opening or dismissing an
overlay.
Selection is part of the information model, not optional polish. Tabs,
segmented choices, selectable rows, filters, and navigation destinations must
show a persistent selected state. A Button that owns a dropdown must remain
visibly pressed or open until the popup closes; hover alone cannot explain the
relationship between trigger and surface.
In a segmented control, the selected fill and unselected surface must share
the container's silhouette. Neither may square off an end corner or make a
boundary appear heavier than its peers.
Show a selected navigation item, list row, or tab through the item's own
surface: a selected fill, stronger foreground, or heavier weight. Do not add a
leading-edge bar or one-sided border as the selection marker. It is a web
template habit, not a desktop convention; it breaks the item's rounded
silhouette and adds a second, competing edge to a column that already aligns
on its text.
For destructive actions, distinguish between reversible and irreversible work.
Prefer undo or a temporary notification for reversible changes. Use an
`AlertDialog` when the consequence is serious and cannot be undone; name the
specific object and consequence in the confirmation copy.
### Pointer conventions
Use the default arrow cursor for buttons, checkboxes, menu items, tabs, and
other native controls. Use a pointing hand for links and content that behaves
as a link. Use text, resize, grab, and prohibited cursors only when they
describe the active manipulation. A cursor reinforces an affordance; it does
not replace the control's visible state or accessible role.
Keep hover effects modest because keyboard and accessibility interaction has no
hover. Do not reveal the only copy of a destructive or essential action on
hover. Contextual row actions may become quieter at rest if the same commands
remain available through selection, keyboard, or a context menu.
### Prefer desktop command surfaces over hover toolbars
Use command frequency and scope to choose where an action lives:
- keep the primary or frequent action visible as a labeled Button or familiar
toolbar control;
- put secondary actions for the current region behind a visible
`DropdownMenu` trigger;
- put commands that act on the object under the pointer in a `ContextMenu`;
- expose the same important command through an [Action](./action)/[key binding](./keybinding) when it has a
natural keyboard form;
- use a hover-revealed icon only as a shortcut to a command that remains
reachable elsewhere.
This is more than a visual preference. GPUI Component's menu system already
owns directional keyboard navigation, confirmation and cancellation, disabled
items, separators, submenus, shortcut presentation, focus transfer and
restoration, and nested-menu dismissal. A custom strip of hover buttons must
rebuild those behaviors and is invisible to keyboard-only and many assistive
technology workflows.
Choose `DropdownMenu` when users need a visible indication that more commands
exist—for example a toolbar overflow, document actions, or account menu. Choose
`ContextMenu` for selection- or object-scoped commands such as rename,
duplicate, reveal, or remove. The context menu must not be the only way to
perform an essential command; provide a menu-bar, toolbar, keyboard, or detail
view path as appropriate.
Do not put every action into a menu to make a screen look minimal. Discovery
and speed matter: the main action stays visible, dangerous items remain clearly
labeled and separated, and a menu item should use the same verb, icon, shortcut,
enabled state, and result everywhere it appears.
### Button means application action; Link means external resource
Use a Button when activation changes application state, confirms a decision,
opens a tool, submits data, or runs a command. Choose its treatment by local
hierarchy:
- primary Button for the one emphasized commitment in a decision area;
- default Button for ordinary visible actions;
- outline Button when an action needs a clear boundary with less emphasis;
- ghost Button for familiar, low-emphasis toolbar and inline actions;
- icon Button only for a well-known symbol, with an accessible name and a
tooltip when meaning or scope is unclear.
Do not assign primary because a Button is the only action on screen, because it
is placed at the top right, or because the team wants more clicks. Primary
communicates default commitment and keyboard behavior. If activation is merely
an ordinary command such as adding an item, opening a tool, or refreshing a
view, use a default, outline, or ghost Button according to its local hierarchy.
Use an underlined Link only for an external resource target: a URL, web page,
online documentation, or email address. It uses the pointing-hand cursor
because its contract is leaving the current application context for that
resource. Do not use Link styling to make a functional command look quiet. A
link-shaped Delete, Save, Refresh, Add, Open-menu, or in-app navigation action
hides the control's affordance and exposes the wrong accessibility role.
“View” does not make an in-app destination a Link. A full report, analysis,
details panel, or local record still opens through a Button, row, card, tab, or
disclosure control. Use concise context-aware labels such as `Full analysis`
when the containing card already establishes what opens; reserve underlining
for a resource that actually opens in a browser or mail client.
All internal navigation—sidebar rows, tabs, breadcrumbs, list items, opening a
local view, or switching workspaces—must use the corresponding native component
or a Button/Action. Visual emphasis is chosen through Button variant or the
navigation component's selected state, never by lying about semantics.
## Feedback and overlays
Choose the smallest surface that fits the decision:
- tooltip: a short explanation or shortcut;
- popover: contextual controls that do not interrupt the task;
- menu: a compact list of actions;
- notification: asynchronous status that does not require a decision;
- dialog: a focused decision or short form;
- alert dialog: explicit confirmation of a consequential action;
- sheet: supplementary work that benefits from more persistent space.
A tooltip supplies missing meaning; it does not restate an obvious action.
Reserve it for an unfamiliar symbol, ambiguous scope, or a useful shortcut
that is otherwise hidden. Repeated, recognizable commands such as Copy and
Download in a message footer need accessible names and keyboard access, but
their hover labels add interruption rather than clarity.
An Alert interrupts the visual hierarchy even when it does not open a modal.
Use it for important, exceptional information that needs attention in the
current task, not as a decorated container for ordinary descriptions, tips, or
empty space. Prefer inline help, muted text, or a normal section when the
content does not require immediate notice or action.
Avoid stacking overlays. Escape should dismiss the topmost dismissible layer,
and focus should return to the trigger or the next logical target.
An overlay action must refer to an object or state the overlay actually shows.
For example, expose `Clear history` only when a distinct recent-history section
is visible and contains entries. Search results, recent items, and favorites
are different collections; label and separate them instead of merging them
into one unexplained list. Hide an inapplicable action or disable it with a
useful reason—do not park an ambiguous trash icon in a footer.
Footer space is not a catch-all for capabilities that lacked a place in the
design. A footer may present shortcuts, status, or actions that apply to the
whole surface, but each item must answer: what is its object, why is it
available now, what scope does it affect, and what visible state changes after
activation?
## Motion
Motion explains change; it is not ambient decoration. Use short transitions for
appearance, dismissal, expansion, and spatial continuity. Avoid animating large
layout changes when opacity or transform communicates the same relationship.
Honor reduced-motion preferences, never require animation to understand state,
and do not add a default animation to every component.
Motion policy belongs to the styled or application layer. Base may own the
lifecycle mechanism or geometry needed for a transition, but it should not
decide that every product fades or slides. Give independently animated values
stable identity, and make interruption reverse smoothly from the currently
sampled value rather than restarting from an old endpoint.
Motion follows the continuity of the interaction. While a pointer or scroll
position moves among related targets, keep the preview surface stable and
update its content or position in place. Dismiss it only after the interaction
ends; repeated fade-out and re-entry on each update reads as flicker.
## Designing data-heavy interfaces
Dense does not mean cramped. In tables, trees, command palettes, editors, and
docks:
- keep headers and primary row identity visually stable;
- align comparable values and use tabular numerals where appropriate;
- distinguish focus, hover, active row, and multi-selection;
- keep sorting and filtering visible and reversible;
- preserve selection by domain identity across filtering and reordering;
- virtualize large collections without changing keyboard semantics;
- use progressive disclosure for secondary columns and inspectors;
- provide a useful empty state that explains the next action.
Choose a table for comparison across consistent fields, a list for scanning
heterogeneous items, a tree for real hierarchy, and a dock only when users need
to arrange long-lived tools or documents. Do not use a complex data component
as a visual style.
## Interface language
Words are part of the interface architecture. Write the vocabulary for a
feature as a system—destinations, objects, commands, states, and outcomes—not as
isolated translations of implementation features. Prefer the shortest wording
that remains accurate in its actual context.
### Let context carry context
Do not repeat information that the surrounding surface already establishes. A
sidebar destination is usually the object or domain itself: use `Users`, not
`User Management`; `Shortcuts`, not `Shortcut Configuration Management`. A
column whose rows already contain actions can omit a generic `Operation`
heading. A dialog titled `Delete “Roadmap”?` does not need body text that asks
the same question again.
This is context economy, not deletion for its own sake. Add text when it changes
the decision: identify the affected scope, an irreversible consequence, an
unusual prerequisite, or a way to recover. Every extra word should answer a
question the current layout does not already answer.
Use nouns for destinations and objects (`Users`, `Appearance`, `Orders`), verbs
for commands (`Save`, `Duplicate`, `Export`), and adjectives or short phrases
for states (`Offline`, `Up to date`, `Pending review`). Avoid wrappers such as
`Management`, `Module`, `Page`, `Function`, `Operation`, and `System` unless the
word distinguishes a real domain concept.
### Write each language, do not translate its shape
Start from shared intent, hierarchy, and terminology, then compose each locale
as natural interface language. Do not preserve the source language's word
order, number of words, politeness filler, or grammatical category. English
`Users` can express a Chinese feature concept that would literally expand to
“user management”; fidelity means preserving purpose, not preserving tokens.
Remove words supplied by the enclosing information architecture. Inside a
`Settings` surface, a destination is often simply `Account`, not `Account
Settings` and never the unnatural singular `Account Setting`. The correct
English label is chosen from its role and neighbors, not from the standalone
source phrase.
Maintain a small product lexicon for recurring objects, commands, and states.
Use the same term in the toolbar, menu, context menu, dialog, shortcut search,
and documentation unless the context genuinely changes its meaning. Review
copy in the rendered surface: neighboring labels often reveal repetition or
inconsistent scope that a locale file cannot.
In localized technical writing, preserve an established framework term when a
translation would be less precise. Keep API identifiers in their original form
and format them as code. Do not retain ordinary foreign words merely to sound
technical. Explain a retained term on first use when needed, then use the same
form throughout the interface, documentation, and API examples.
In documentation, use the exact English name for a named API, component,
source file, or guide in link text, including localized pages. For example,
write `Font`, `Render`, `Input element`, and `Plot label` rather than translating
their names or adding a redundant `GPUI Kit` prefix. Keep surrounding explanations in the
page's language. Link only the named target; place words such as “source”
outside the link when they describe why it is cited.
In a technical comparison table shared across locales, keep capability names,
framework names, API names, and model names in their established English form.
Translate the explanation around the table, not its technical labels. Common
interface words with an established local name may still be localized outside
that comparison context.
### Buttons and confirmation dialogs
Button labels are short by default—usually one or two words—and describe the
result, not the gesture or the component. Prefer `Save`, `Move`, or `Delete` to
`Click to save`, `Perform move`, or `Confirm deletion`. Use `Cancel` consistently
for the action that leaves without committing. Reserve `OK` for acknowledging
purely informational content.
Short is a default, not a character limit. A deliberately longer label is
better when its words expose a consequence or distinguish choices that users
could otherwise confuse, for example `Delete from this group` versus `Delete
everywhere`, or `Restart without saving`. Length must buy decision-critical
information; it must not restate the dialog title or body.
Use the most specific concise result as the confirmation label when possible:
| Context | Weak | Prefer |
| --- | --- | --- |
| Delete dialog | `Yes`, `Sure`, `Confirm deletion` | `Delete` |
| Unsaved changes | `Confirm`, `Yes` | `Discard changes` |
| Pure acknowledgement | `Confirm operation` | `OK` or `Done` |
| Complex consent whose result has no clear verb | `Yes` | `Confirm` |
`Confirm` is a useful fallback when the surrounding dialog fully names a
complex commitment and no shorter result verb is accurate. It should not
replace a clear command. `Sure` is conversational rather than a stable English
command and is too ambiguous for the standard vocabulary.
A confirmation dialog should form one compact decision:
- title: the decision or condition, such as `Delete “Roadmap”?`;
- body: only new scope, consequence, or recovery information;
- actions: `Cancel` and the result, such as `Delete`;
- destructive styling: applied to the destructive result, not substituted for
precise wording.
Avoid generic titles such as `Notice`, `Warning`, `Error`, and `Confirmation`
when the actual condition can be named. Avoid ritual phrases such as “Are you
sure you want to…”, “Would you like to…”, “Please note that…”, and “successfully”
when the structure or state already communicates them. Courtesy should come
from a calm, respectful tone, not repeated `please`.
### Capitalization, punctuation, and symbols
Use sentence case for English UI by default: `Reset layout`, not `Reset Layout`
or `RESET LAYOUT`. Preserve proper nouns and established acronyms. Follow a
platform convention such as title case for native menu commands only when the
platform integration benefits from it, and apply that convention consistently
within the component class.
ALL CAPS can provide restrained typographic emphasis for very short section
labels, eyebrows, statuses, established acronyms, and code-like identifiers.
Its compact shape and measured tracking can form a level similar to bold type,
but it does not belong on Buttons, long headings, sentences, or dense lists. Do
not combine uppercase, strong color, and bold weight in the same region, and do
not transform every string automatically: product names, acronyms, and localized
content must preserve their intended casing.
Labels, buttons, menu items, tabs, headings, placeholders, and short states do
not take a final period. Complete explanatory, warning, and error sentences do.
Avoid exclamation marks in routine success and failure messages. In Chinese,
use full-width punctuation in sentences and omit terminal punctuation from
short control labels by the same semantic rule.
Use the single ellipsis character (`…`), not three periods. Append it to every
Button or MenuItem that opens a dialog, sheet, or separate window, and to a
command that requires more input or choices before it can complete, such as
`Settings…` or `Export…`. An immediately executed command does not take an
ellipsis. Use an indeterminate progress indicator, not decorative dots, to
communicate ongoing work.
Errors should say what happened and, when useful, the next recovery action.
Success feedback should name the resulting state only when that state is not
already visible. Prefer `Couldn’t save. Check your connection and try again.`
to a technical code or a long apology; omit a `Saved successfully` toast when
the document visibly becomes saved.
## Internationalization and platform fit
Copy must survive expansion, CJK typography, and different shortcut notation.
Do not size a control from one English label. Keep text out of raster assets,
avoid concatenating translated fragments, and let labels wrap or truncate only
where the product defines a recovery path such as a tooltip.
Respect platform differences that carry meaning: Command versus Control,
native window decorations, system appearance, scrollbar behavior, menus, and
notification capabilities. Keep the product's information architecture stable
across platforms, but do not erase familiar platform behavior for superficial
pixel equality.
## Guidance for AI-generated interfaces
An AI changing a GPUI interface should first inspect the nearest feature,
theme tokens, and component documentation. It should state the primary task,
state owner, component composition, and keyboard path before generating code.
It must not infer an API from React/Shadcn examples or invent a GPUI method
because the name seems plausible.
AI output is incomplete until a human can explain why the hierarchy, density,
component choice, and exceptional literal values belong in this product. A
visually plausible screenshot is not proof: keyboard behavior, focus, dynamic
content, themes, resizing, and failure states are part of the design.
## Accessibility checklist
Before considering a screen complete, verify that:
- every action is reachable and operable by keyboard;
- focus order follows visual and task order;
- focus remains visible and is restored after overlays;
- controls have accessible names; icon-only controls use tooltips when their
meaning, scope, or shortcut needs explanation;
- text and meaningful boundaries have sufficient contrast;
- status is not communicated by color alone;
- disabled and read-only states are distinguishable;
- labels, errors, and descriptions remain near their controls;
- content remains usable with longer translations and larger text;
- pointer targets are comfortably sized even in a dense layout.
## Design review checklist
A review does not inventory components; it judges whether the interface made
the right decisions. Ask, in order:
1. **Is the task clear?** Can a new user recognize the purpose, primary action,
and next step without learning, guessing, or experimenting?
2. **Does every action keep its promise?** Do the label, control, state, scope,
feedback, and result describe one consistent outcome?
3. **Is hierarchy decisive and restrained?** Does the core feature receive the
space it deserves while strong color, bold type, badges, alerts, and primary
Buttons remain scarce?
4. **Could the interface do less, better?** Can an entry point, option, or state
be removed, combined, or deferred without weakening the complete task?
5. **Is the structure exact?** Do peers share alignment spines, equal gaps stay
equal to the rendered pixel, and scrollbars sit at the edge of their actual
scrolling region?
6. **Does it follow the component system?** Do standard controls retain their
geometry, states, keyboard behavior, and dismissal model, with appearance
supplied by theme and scale tokens?
7. **Does it remain usable in every state and constraint?** Verify keyboard and
focus behavior, empty/loading/failure/permission states, longer translations,
zoom, minimum window size, and reduced motion.
8. **Has it been tested in a real window?** Complete the task with real
components, copy, and representative content—not only an ideal screenshot.
Continue with [Coding Guides](/docs/coding-guides) to translate these design
decisions into GPUI architecture and code.
---
# Getting Started
Source: /docs/getting-started
This guide builds a small desktop window with a GPUI Kit button. You need Rust and Cargo plus the system libraries for your platform; see [Installation](/docs/installation) for macOS, Windows and Linux requirements. For a browser target, start with [WebAssembly](/docs/webassembly) after learning the view model here.
## Create a project
```sh
cargo new gpui-hello
cd gpui-hello
```
Add GPUI Kit to the generated `Cargo.toml`:
```toml
[dependencies]
gpui-kit = "{{gpui_kit_version}}"
```
This single dependency includes GPUI, GPUI Base, the styled GPUI Component library and its default icon assets. Application code accesses GPUI through `use gpui_kit::*;` and components through `gpui_kit::component`. You can change the feature selection later; see [Icons & Assets](/docs/assets).
## Add a view
Replace `src/main.rs` with:
```rust
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::*;
struct HelloWorld;
impl Render for HelloWorld {
fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement {
div()
.flex()
.flex_col()
.size_full()
.items_center()
.justify_center()
.gap_2()
.child("Hello, World!")
.child(
Button::new("hello")
.primary()
.label("Click me")
.on_click(|_, _, _| println!("Clicked!")),
)
}
}
fn main() {
application()
.with_assets(assets::Assets)
.run(|cx| {
init(cx);
open_window(WindowOptions::default(), cx, |_, cx| {
cx.new(|_| HelloWorld)
})
.expect("Failed to open window");
});
}
```
Run `cargo run` from the project directory. A window shows the label and button; clicking the button prints `Clicked!` in the terminal.
The startup sequence has three parts:
1. `gpui_kit::application()` creates the desktop application; `.with_assets(...)` registers the default icon source.
2. `gpui_kit::init(cx)` initializes the enabled Kit layers, including component themes. Call it once before opening application windows or constructing components.
3. `gpui_kit::open_window(...)` creates an `Entity` from the closure and wraps it in a [`Root`](./window). `Root` owns the window's overlay layers, including dialogs, sheets and notifications. Return your content view from the closure, not another `Root`.
`HelloWorld` implements GPUI's [`Render`](./render) trait. When GPUI renders the view, `render` returns an [element tree](./element): a `div` containing text and a `Button`. The button is a value built for that render; when a control needs lasting state, such as an input's text, the owning view keeps an [Entity](./entity) for that state instead of recreating it in `render`.
## A small mental model
An [Entity](./entity) holds state across frames. It can own a model without drawing anything; when `T` implements `Render` and is mounted, the entity is a persistent **View** that builds a fresh element tree each time it renders. A [RenderOnce](./render-once) component takes its inputs as a value and describes a reusable piece of that tree. Use one where the caller supplies its state and handlers; it can still use small keyed element state. Give complex state, subscriptions, and tasks a lasting owner.
```text
app shell → feature (model, commands, view)
├─ Entity retained state
└─ Entity retained view; View implements Render
└─ element tree rebuilt for each render
└─ RenderOnce values for reusable pieces
```
As an app grows, a feature with its own workflow can keep its model and views together in a feature crate, with a private [Global](./global) only when it needs truly application-wide state. Let features cooperate through small public interfaces, events, or `Entity` handles. This keeps reusable pieces inexpensive to adopt and gives teammates or AI agents a clear boundary for parallel changes. The [Coding Guides](./coding-guides) explain when to make that split and how to keep ownership and dependencies clear.
## Where to go next
Read these in order as your app grows:
1. [Entity](/docs/entity), [Context](/docs/context), and [Render](/docs/render): retain a value, change it from a button callback, and confirm the window shows the new value. Read [Window](/docs/window) to see how `Root` hosts that view and its overlays.
2. [Element](/docs/element) and [RenderOnce](/docs/render-once): distinguish the rebuilt element tree from persistent state. Follow the runnable Brush exercise in [Paint](/docs/paint), and confirm a pointer press changes the drawing.
3. [Focus](/docs/focus), [Action](/docs/action), and [Event](/docs/event): use Tab to reach an interactive target, then trigger one command and observe its state change. Follow [Task](/docs/task) to run the streaming example; press Replay twice and confirm old chunks do not return.
4. [Accessibility](/docs/accessibility) and [Testing](/docs/test): follow the Save flow with a keyboard, check its focus and visible result, then run the documented UI test and confirm both the rendered status and saved model value. Check assistive technology separately on each target platform.
5. [Component catalog](/component): choose controls for your application; then read [Icons & Assets](/docs/assets) and [Fonts](/docs/fonts) as your interface needs them.
For a tested example of retained input state and subscriptions, read the [application recipes](https://github.com/MohsenDastaran/uni-kit/tree/main/examples/ai_recipes). The [Coding Guides](/docs/coding-guides) explain the conventions behind those examples.
## Complete tested view
This settings view is a compiled recipe showing retained input state and subscriptions. The excerpt is synchronized with its [Rust source](https://github.com/MohsenDastaran/uni-kit/blob/main/examples/ai_recipes/src/settings.rs). The repository's default `gpui-kit-recipes` executable opens a small bootstrap view; it does not display this settings view. The [settings interaction test](https://github.com/MohsenDastaran/uni-kit/blob/main/examples/ai_recipes/tests/settings.rs) mounts `Settings` in a GPUI test window. From the repository root, run:
```sh
cargo test -p gpui-kit-recipes --test settings
```
The test passes when typing updates the retained preview to `a`, then to `ab` after an unrelated redraw, with change counts of 1 and 2. This verifies the input subscription and state lifetime in a test window; it does not perform a native visual review.
```rust
use gpui_kit::component::{
ActiveTheme, IconName, WindowExt,
button::Button,
checkbox::Checkbox,
form::{Field, Form},
input::{Input, InputEvent, InputState},
radio::RadioGroup,
switch::Switch,
};
use gpui_kit::{
AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, SharedString,
Styled as _, Subscription, Window, div,
};
pub struct Settings {
name: Entity,
preview: SharedString,
changes: usize,
enabled: bool,
remember: bool,
delivery: Option,
_subscriptions: Vec,
}
impl Settings {
pub fn new(window: &mut Window, cx: &mut Context) -> Self {
let name = cx.new(|cx| InputState::new(window, cx).placeholder("Name"));
let subscription = cx.subscribe_in(&name, window, |this, state, event, _, cx| {
if matches!(event, InputEvent::Change) {
this.preview = state.read(cx).value().to_string().into();
this.changes += 1;
cx.notify();
}
});
Self {
name,
preview: "".into(),
changes: 0,
enabled: false,
remember: false,
delivery: Some(0),
_subscriptions: vec![subscription],
}
}
pub fn input(&self) -> Entity {
self.name.clone()
}
pub fn preview(&self) -> &SharedString {
&self.preview
}
pub fn changes(&self) -> usize {
self.changes
}
}
impl Render for Settings {
fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement {
div()
.flex()
.flex_col()
.size_full()
.p_4()
.gap_3()
.bg(cx.theme().background)
.text_color(cx.theme().foreground)
.child("Profile")
.child(
Form::new()
.child(Field::new().label("Name").child(Input::new(&self.name)))
.child(Field::new().label("Preview").child(self.preview.clone()))
.child(
Field::new().label_indent(false).child(
Checkbox::new("remember")
.label("Remember name")
.checked(self.remember)
.on_change(cx.listener(|this, value, _, cx| {
this.remember = *value;
cx.notify();
})),
),
)
.child(
Field::new().label_indent(false).child(
Switch::new("enabled")
.label("Enable notifications")
.checked(self.enabled)
.on_change(cx.listener(|this, value, _, cx| {
this.enabled = *value;
cx.notify();
})),
),
)
.child(
Field::new().label("Delivery").child(
RadioGroup::new("delivery")
.children(["Immediately", "Daily summary"])
.selected_index(self.delivery)
.on_change(cx.listener(|this, value, _, cx| {
this.delivery = Some(*value);
cx.notify();
})),
),
)
.footer(
Button::new("about")
.label("About…")
.icon(IconName::Info)
.on_click(|_, window, cx| {
window.open_dialog(cx, |dialog, _, _| {
dialog.title("About").child("A complete GPUI Kit window")
});
}),
),
)
}
}
```
---
# Geometry
Source: /docs/geometry
# Geometry and color
GPUI uses types to say what a number *means*. A coordinate is a `Point`, an extent is a `Size`, and a rectangle is a `Bounds`. The `T` says which unit the components use. Layout accepts lengths that may still need a parent size; drawing and hit testing usually use resolved `Pixels`. Colors distinguish a hue based representation (`Hsla`) from direct channels (`Rgba`). GPUI Kit reexports GPUI, so application examples can start with `use gpui_kit::*;`.
## Points, sizes, and bounds
| Type | Fields | Meaning |
| --- | --- | --- |
| `Point` | `x`, `y` | A position in a coordinate space. |
| `Size` | `width`, `height` | An extent, without a position. |
| `Bounds` | `origin: Point`, `size: Size` | An axis aligned rectangle. |
The free functions `point(x, y)`, `size(width, height)`, and `bounds(origin, size)` infer `T` from their arguments. They also work with ordinary numeric types, but UI geometry normally uses `Pixels`:
```rust
use gpui_kit::*;
let frame: Bounds = bounds(
point(px(20.), px(40.)),
size(px(240.), px(80.)),
);
assert_eq!(frame.right(), px(260.));
assert_eq!(frame.bottom(), px(120.));
assert!(frame.contains(&point(px(20.), px(40.))));
assert!(!frame.contains(&point(px(260.), px(40.))));
let midpoint: Point = frame.center();
let local = point(px(35.), px(55.)).relative_to(&frame.origin);
assert_eq!(local, point(px(15.), px(15.)));
```
`right()` and `bottom()` add the extent to the origin. `contains()` includes the top and left edges but excludes the bottom and right edges, so adjacent rectangles do not both claim a point on their shared boundary. `center()`, `intersects()`, `Bounds::from_corners(...)`, and `Bounds::centered_at(...)` help with alignment and placement. A local position and a window position can both be `Point`: the type checks the unit, while your code must still track which origin it uses. Add the bounds origin when painting locally measured geometry in window coordinates; subtract it when interpreting a pointer position within an element.
These values typically appear after layout. A custom [`Element`](./element) receives `Bounds` in `prepaint` and uses the same resolved geometry for hitboxes and later [painting](./paint). A bounds value is geometry, not an interactive region by itself.
## Choose a coordinate origin
`Point` records a unit, not a coordinate system. In a window, the top-left of the window content is the usual origin; an element's own top-left is a different origin. State the space in variable names when both appear in one calculation:
```rust
use gpui_kit::*;
let element_bounds = bounds(point(px(100.), px(60.)), size(px(80.), px(40.));
let pointer_in_window = point(px(125.), px(75.));
let pointer_in_element = pointer_in_window.relative_to(&element_bounds.origin);
assert_eq!(pointer_in_element, point(px(25.), px(15.)));
let marker_in_element = point(px(10.), px(8.));
let marker_in_window = element_bounds.origin + marker_in_element;
assert_eq!(marker_in_window, point(px(110.), px(68.)));
```
The `bounds` passed to a custom element's `prepaint` and `paint` is already positioned in window coordinates. A pointer event's `position` is also in window coordinates. Test `element_bounds.contains(&pointer_in_window)` directly; convert to local coordinates only for work such as locating a character or handle *inside* the element. Do not add `element_bounds.origin` a second time to a rectangle already based on those bounds. Conversely, a local point cannot be compared directly with a window-space hitbox even though both have type `Point`.
## From layout to input and drawing
The phases answer different questions:
| Phase | Available geometry | Responsibility |
| --- | --- | --- |
| `request_layout` | Style lengths and layout nodes; some lengths still depend on the parent. | Return a `LayoutId` for the layout engine to solve. |
| `prepaint` | Resolved `Bounds` for this frame. | Prepare geometry and, when needed, call `window.insert_hitbox(bounds, HitboxBehavior::Normal)`. |
| `paint` | The resolved bounds and prepared state. | Draw with methods such as `window.paint_quad(fill(bounds, color))` and register frame-local input listeners. |
The layout tree, hitboxes in the dispatch tree, and painted scene are separate. Painting a rectangle does not make it clickable; inserting a hitbox does not draw it. The hitbox returned by `insert_hitbox` can be carried as `PrepaintState` into `paint`, where a listener can test `hitbox.is_hovered_at(event.position, window)`. A hitbox records the content mask active when it was inserted, so establish clipping before inserting child hitboxes. See the [custom Element walkthrough](./element) for a complete event handler.
## Scrolling, clipping, and a worked calculation
A scroll viewport and its content have different origins. GPUI Kit's [virtual list implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/src/virtual_list.rs) uses a **negative** scroll offset, adds that offset when positioning items in window space, and prepaints them under a `ContentMask` for the viewport. Its `visible_range` first selects items in content space; the mask then limits drawing and input to the visible region. Selecting a visible item does not itself clip its pixels.
### Run the coordinate calculation
From the repository root, create `examples/hello_world/src/bin/geometry_walkthrough.rs` (create the `bin` directory if needed). This is a second binary in the existing `hello_world` package, not a new crate. Paste the complete program below, then run `cargo run -p hello_world --bin geometry_walkthrough`.
The vertical viewport starts at window `(100, 60)` and has size `80 × 40`. Item A starts at content `y = 20`, has height `20`, and the scroll offset is `-30`. The pointer position is in window coordinates. The program converts it to item-local coordinates and calculates each item's intersection with the viewport:
```rust
use gpui_kit::*;
fn main() {
let viewport = bounds(point(px(100.), px(60.)), size(px(80.), px(40.)));
let scroll_y = px(-30.);
let item_a = bounds(
viewport.origin + point(px(0.), px(20.) + scroll_y),
size(px(80.), px(20.)),
);
assert_eq!(item_a.origin, point(px(100.), px(50.)));
let pointer_in_window = point(px(105.), px(65.));
let pointer_in_item = pointer_in_window.relative_to(&item_a.origin);
assert_eq!(pointer_in_item, point(px(5.), px(15.)));
let visible_a = item_a.intersect(&viewport);
assert_eq!(visible_a.origin, point(px(100.), px(60.)));
assert_eq!(visible_a.size, size(px(80.), px(10.)));
assert!(visible_a.contains(&pointer_in_window));
let item_b = bounds(
viewport.origin + point(px(0.), px(80.) + scroll_y),
size(px(80.), px(20.)),
);
assert_eq!(item_b.origin.y, px(110.));
assert!(!item_b.intersects(&viewport));
println!(
"Item A: local pointer = ({}, {}), visible height = {}; Item B visible = {}",
pointer_in_item.x.as_f32(),
pointer_in_item.y.as_f32(),
visible_a.size.height.as_f32(),
item_b.intersects(&viewport),
);
}
```
The expected output is `Item A: local pointer = (5, 15), visible height = 10; Item B visible = false`. The assertions also fail immediately if the scroll offset sign or coordinate origin is wrong. Item A begins 10 pixels above the viewport; only its bottom 10 pixels intersect it. Item B begins at window `y = 110`, beyond the viewport's bottom edge at `y = 100`. You can remove the exercise file after running it.
`intersect()` computes a rectangle; it does **not** clip drawing or input by itself. In a real custom element, apply the viewport's `ContentMask` while prepainting and painting children, so child hitboxes inherit the mask and painted pixels are clipped. Use the resulting hitbox to resolve pointer handling. Ordinary scroll containers manage this for you. For a custom one, keep content positioning, clipping, and hitboxes in the same coordinate space, and clamp the scroll offset to the content range as the virtual list does.
Common mistakes are treating `relative(0.5)` as 0.5 pixels before layout, comparing local coordinates with window bounds, subtracting a negative scroll offset when positioning content, or assuming `contains()` clips a painted child. For a mismatch, write down the origin and unit of each intermediate value, then inspect the resolved bounds and current content mask.
## Edges, sides, and placement
`Edges` holds four independent values in `top`, `right`, `bottom`, `left` order. Use `Edges` for resolved insets such as padding, borders, or the space reserved around a window. `Edges::all(value)` gives every side the same value; specify fields when they differ:
```rust
use gpui_kit::*;
let padding: Edges = Edges {
top: px(8.),
right: px(12.),
bottom: px(8.),
left: px(12.),
};
let uniform = Edges::all(px(4.));
```
The `Edges` imported by `use gpui_kit::*` is GPUI's type. GPUI Kit also has `gpui_kit::base::Edges` (reexported as `gpui_kit::component::Edges`) for values that need serialization or a JSON schema. They have the same four fields but are different Rust types; use the one required by the API you call.
[`Placement`](https://docs.rs/gpui-base/latest/gpui_base/enum.Placement.html) is GPUI Kit's choice of **one side** of a trigger: `Top`, `Right`, `Bottom`, or `Left`. It is not a rectangle or a set of four insets. For example, `Positioner::side` treats `Placement::Bottom` as a *preferred* side; it may flip to `Top` if the popup does not fit below the trigger, then clamps the result inside the viewport. `ResolvedPosition::placement` reports the side actually chosen:
```rust
use gpui_kit::*;
use gpui_kit::base::{Align, Placement, Positioner};
let trigger_bounds = bounds(point(px(40.), px(40.)), size(px(100.), px(32.)));
let popup = Positioner::side(trigger_bounds)
.placement(Placement::Bottom)
.align(Align::Start)
.offset(px(8.))
.child(div().child("Menu"));
```
Here `trigger_bounds` is a `Bounds` in window coordinates. `Side` is the narrower GPUI Kit enum for `Left` or `Right`, and `Axis` expresses `Horizontal` or `Vertical`. GPUI's `Anchor` identifies a reference point such as `TopLeft` or `BottomCenter`; `Corners` holds four corner values, often radii. Neither is a substitute for `Placement`. See [Window](./window) for window-local coordinates and scale.
## Why `Pixels` instead of `int` or `float`?
`px(12.)` produces `Pixels`, a wrapper around `f32`. Fractions matter for text metrics, animation, and positioning before rasterization. Integers would discard that precision. A bare `f32` could mean a coordinate, a scale factor, an opacity, or a fraction of a parent; it gives the compiler no way to catch a mix-up. For example, adding two pixel distances is meaningful, and multiplying a distance by a scalar stays in pixels:
```rust
let inset: Pixels = px(8.);
let width: Pixels = px(120.) - inset * 2.;
let raw: f32 = width.as_f32(); // Convert only at an API boundary that needs f32.
```
`Pixels` are GPUI's logical UI pixels, not necessarily physical display pixels. `Pixels::scale(factor)` produces `ScaledPixels`; `DevicePixels` represents integer device pixel counts. For example, `px(12.).scale(2.)` is 24 scaled pixels, but it is still a distinct type from `DevicePixels(24)`. Keep those units distinct when crossing a display or raster boundary. The wrapper cannot prove that two `Point` values share the same origin, or that a width is nonnegative; those remain application responsibilities.
## Lengths before layout, pixels after layout
A [style](./style) length can depend on context. GPUI expresses this with nested types:
| Type | Values | Use |
| --- | --- | --- |
| `AbsoluteLength` | `Pixels` or `Rems` | A fixed UI length or one based on the root text scale. |
| `DefiniteLength` | `AbsoluteLength` or a parent fraction | A length with a specified value, possibly relative. |
| `Length` | `DefiniteLength` or `Auto` | A layout value that may be chosen by the layout engine. |
`px(24.)` makes `Pixels`; `rems(1.5)` makes `Rems`; `relative(0.5)` makes `DefiniteLength::Fraction(0.5)`, or half the relevant parent dimension. `auto()` makes `Length::Auto`. Conversions from `Pixels`, `Rems`, and `DefiniteLength` into `Length` are available. Which values a style method accepts depends on that method's signature; let inference handle the conversion when using a builder:
```rust
use gpui_kit::*;
let panel = div()
.w(relative(0.5))
.min_w(px(240.))
.h(rems(3.));
```
The width remains relative until layout knows the parent. A rem needs the root rem size. `auto` asks layout to choose a value under its rules; it is not zero. GPUI passes these values to the layout engine and receives pixel bounds.
`Percentage` is a separate `Percentage(f32)` wrapper made with `percentage(0.25)`. Its helper expects a fraction from `0.0` to `1.0` (asserted in debug builds), and GPUI can convert it to `Radians` as a portion of a full turn. It is **not** the type used by `relative(0.25)` for 25% layout width. For relative layout, use `relative`; for a percentage of a circle, use `percentage`.
## HSLA and RGBA
GPUI's [`Hsla`](https://docs.rs/gpui-pre/{{gpui_pre_version}}/gpui/struct.Hsla.html) stores hue, saturation, lightness, and alpha as values from 0 to 1. Its `hsla(0.6, 0.8, 0.5, 1.)` constructor uses a hue fraction, not degrees, and clamps its four inputs to that range. Use `Hsla` as the default representation for theme colors and their interaction states. [`Rgba`](https://docs.rs/gpui-pre/{{gpui_pre_version}}/gpui/struct.Rgba.html) stores red, green, blue, and alpha channels in the same range. `rgb(0x3366CC)` reads a six digit RGB hex value and sets alpha to 1; `rgba(0x3366CC80)` reads eight digits in **RRGGBBAA** order, with alpha `128 / 255` (about 0.502). Convert to `Rgba` when an RGB channel API or hex color is the natural input.
```rust
use gpui_kit::*;
let tint: Hsla = hsla(0.6, 0.8, 0.5, 1.);
let translucent = tint.opacity(0.5); // Multiply the existing alpha.
let exact_alpha = tint.alpha(0.5); // Replace the alpha.
let again: Rgba = translucent.to_rgb();
let source: Rgba = rgb(0x3366CC);
let from_hex_alpha: Rgba = rgba(0x3366CC80);
let red_channel: f32 = from_hex_alpha.r;
```
HSLA makes it easier to change hue, saturation, or lightness while retaining the other components. RGBA is direct for hex assets, channel values, and compositing. GPUI converts between them; conversion can incur floating point rounding, so it is not a promise of byte exact round trips. HSL lightness is also not perceptual brightness: equal `l` values across different hues need not look equally bright. GPUI Kit provides `Colorize::mix_oklab` when a perceptual color interpolation is useful.
GPUI's `Hsla::blend(other)` places `other` over `self` by converting through RGBA. Its `Rgba::blend` interpolates RGB channels using the overlay alpha and keeps the receiver's alpha; it is useful for the opaque background case, but should not be described as a general alpha compositing equation for two translucent layers.
## Theme colors and interaction states
GPUI Kit stores semantic colors as `Hsla` theme values and exposes resolved tokens for components. A primary button uses `button_primary`, `button_primary_hover`, and `button_primary_active` tokens for its states. Use those tokens for an existing component instead of inventing a local lightness adjustment. Themes may supply each token explicitly, including a background gradient. When a token is absent, GPUI Kit derives a fallback from the theme: primary hover blends the background with primary after multiplying primary's existing alpha by 0.9; primary active multiplies primary's lightness by 0.9 in light mode or 0.8 in dark mode. Button primary state tokens fall back to those primary state tokens. Other variants have their own fallbacks, so there is no universal hover formula.
For a custom solid color, `Colorize` has `lighten`, `darken`, `hue`, `saturation`, and `lightness` on `Hsla`:
```rust
use gpui_kit::*;
use gpui_kit::component::Colorize;
let base: Hsla = hsla(0.6, 0.8, 0.5, 1.);
let darker = base.darken(0.1); // l = base.l * (1 - 0.1)
let quieter = base.opacity(0.6); // a = base.a * 0.6
```
`lighten(f)` multiplies lightness by `1 + f`; `darken(f)` multiplies it by `1 - f`. They do not add or subtract percentage points. Unlike the `hsla(...)` constructor, `Colorize::lighten` does not clamp its result and can produce an `l` above 1, so inspect resulting colors before using them as a theme. `Colorize::opacity` and GPUI's `Hsla::opacity` both multiply alpha. Use semantic theme tokens for control states, and check text contrast and both theme modes when defining new colors.
---
# Render
Source: /docs/render
`Render` is the boundary between a persistent [`Entity`](./entity) and the UI it currently describes. GPUI calls `T::render` when it needs that View's [element tree](./element). The Entity keeps its data and identity; the returned elements describe layout, appearance, and handlers for this rendering pass. Use `Render` for a panel, page, editor, or other View that owns changing state, subscriptions, or child entities.
## Run a stateful View
After [Getting Started](./getting-started), replace that project's `src/main.rs` with this complete example. It uses the same `gpui-kit` dependency and does not need another example crate.
```rust
use gpui_kit::*;
use gpui_kit::component::button::Button;
#[derive(Default)]
struct Chat {
messages: Vec,
}
impl Chat {
fn add(&mut self, cx: &mut Context) {
self.messages
.push(format!("Message {}", self.messages.len() + 1).into());
cx.notify();
}
fn clear(&mut self, cx: &mut Context) {
if !self.messages.is_empty() {
self.messages.clear();
cx.notify();
}
}
}
impl Render for Chat {
fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement {
div()
.flex()
.flex_col()
.size_full()
.gap_2()
.p_4()
.child(format!("Messages: {}", self.messages.len()))
.child(
Button::new("add-message")
.label("Add message")
.on_click(cx.listener(|this, _, _, cx| this.add(cx))),
)
.child(
Button::new("clear-messages")
.label("Clear")
.on_click(cx.listener(|this, _, _, cx| this.clear(cx))),
)
.children(
self.messages
.iter()
.cloned()
.map(|message| div().child(message)),
)
}
}
fn main() {
application()
.with_assets(assets::Assets)
.run(|cx| {
init(cx);
open_window(WindowOptions::default(), cx, |_, cx| {
cx.new(|_| Chat::default())
})
.expect("Failed to open window");
});
}
```
Run `cargo run`. The window initially shows **Messages: 0** and two buttons. **Add message** changes the count to 1 and adds **Message 1**; further clicks append another row. **Clear** removes all rows and returns the count to 0. Clearing an already empty list leaves it unchanged.
`open_window` retains the `Entity` returned by `cx.new`; `Chat::default()` runs once when that Entity is created. `Render::render` receives mutable access to the View, a `Window`, and that View's [Context](./context), then returns `impl IntoElement`; the trait requires `Self: 'static + Sized`. The return type keeps the concrete, often deeply nested element type out of the signature. Here `div()` is a GPUI element and `Button` is a GPUI Kit component. The click closures are *registered* while rendering. They run later on input, with mutable access to `Chat` through `cx.listener`. Neither button mutates `Chat` while the tree is being built.
Follow one click through the ownership boundary:
| Step | What happens |
| --- | --- |
| 1. Input | The button invokes its registered callback. `cx.listener` enters the existing `Chat` Entity and calls `add`. |
| 2. State | `add` appends to `Chat::messages`, then calls that Entity's `cx.notify()`. |
| 3. Window work | GPUI invalidates windows currently showing `Chat`. When it next processes the affected view, `render` describes the new count and rows. |
| 4. Element work | GPUI resolves layout and runs prepaint and paint as needed before the changed result can be presented. |
This is an update path, not a fixed schedule of `render` calls or display frames. If the text stays at **Messages: 0**, check that the callback updates the mounted `Chat` Entity and calls its `Context::notify()`. If clicks trigger network or logging work more than once, check whether that work accidentally lives in `render` instead of the click handler.
## The View owns state; the tree describes this pass
Create the Entity with `cx.new`. A parent can retain and render the same handle on every pass:
```rust
struct ConversationPage {
chat: Entity,
}
impl ConversationPage {
fn new(cx: &mut Context) -> Self {
Self {
chat: cx.new(|_| Chat::default()),
}
}
}
impl Render for ConversationPage {
fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement {
div().size_full().child(self.chat.clone())
}
}
```
`ConversationPage` owns the same `Chat` Entity across rendering passes; it does not recreate the message list whenever the page redraws. Cloning `Entity` copies its handle, not its messages. A file list page can follow the same pattern: keep its file list View as a child Entity, with the list owning selection, loading state, and subscriptions. An `Entity` can be a child because GPUI converts it into a View element. Its Entity ID gives that View a reactive boundary and a distinct element ID space. See [Entity](./entity) for creation, sharing, and lifetime rules.
## Render or RenderOnce?
`Entity` is a state container first: `T` does not need to implement `Render`. An `Entity` can hold data, receive updates, and notify observers without ever appearing in an element tree. When `T: Render`, the same container can be placed in the tree as a persistent View, keyed by its Entity ID. GPUI's `View` machinery also accepts `RenderOnce` values, but those have no Entity ID of their own. `Render` is the Entity-backed UI route, not a requirement for every Entity.
| Trait | Receiver and owner | Appropriate work |
| --- | --- | --- |
| [`Render`](./render) | `render(&mut self, ..., &mut Context)` on a persistent `Entity` View | Own changing or complex state, subscriptions, focus, tasks, and child entities across render passes. |
| [`RenderOnce`](./render-once) | `render(self, ..., &mut App)` consumes a value constructed by its parent | Describe a lightweight component from current inputs and callbacks; the parent supplies a new value when it renders again. |
The `Chat` View above implements `Render` because it owns a changing message collection. GPUI Kit's `SelectState` is another state owner that implements `Render`. Its styled `Button` implements `RenderOnce`: the parent supplies its current props and handles the result of a click. A `RenderOnce` value can still be interactive, and GPUI may retain small keyed element state beneath it; it simply has no independent Entity lifecycle. This is a choice about **who owns persistent state**, not a promise that one trait runs once per display frame or that the other runs every frame.
Returning a tree does not paint it immediately. GPUI converts the values to elements, resolves layout, runs `prepaint`, then [paints](./paint). A View's `render` can be called as part of layout or prepaint, and GPUI can reuse a valid cached subtree. Do not assume exactly one `render` call per frame, or that every parent render calls every child View's `render`. The output is a description for GPUI's element pipeline, not a retained list of pixels. [Element](./element) covers those phases.
For the distinction between a render pass, a display refresh, and a presented frame, including the meaning of “120 FPS” and GPUI's hybrid model, see [FPS Monitor](./fps#120-hz-is-a-frame-budget-not-a-refresh-promise).
## Notify after a meaningful state change
An Entity update grants mutable access, but mutation alone does not report a visible change. Call the *updated Entity's* `Context::notify()` when its rendered output or observers should change:
```rust
chat.update(cx, |chat, cx| {
chat.messages.push("Hello".into());
cx.notify(); // This is Context, not the caller's context.
});
```
`notify` invalidates live windows displaying that Entity and queues notification to observers. GPUI arranges a later rendering pass; `render` is not called synchronously at the `notify` line. A View shown in more than one live window may invalidate in each. GPUI marks the changed View and its rendered ancestors dirty, then may reuse eligible cached subtrees. Do not rely on a particular number of `render` calls. Notifications are also meaningful for observed model entities that do not themselves render.
An [Event](./event) and a notification have different jobs: `cx.emit(event)` delivers a typed fact to subscribers; `cx.notify()` reports that the Entity changed. Emitting an event alone does not mean every View that reads the Entity will redraw. Conversely, a notification does not convey an event payload. Use both when the state and the event each need to be observed.
When one View reads another Entity's state, keep ownership and invalidation explicit. If the other Entity renders as a child, its own `notify` can invalidate that child View. If the parent copies the other's values into its own output, arrange for the parent to observe the source and call the parent's `cx.notify()` when those copied values change. Avoid assuming a parent rerender is the only way a retained child can change.
### Trace one update through the example
1. `ConversationPage::new` creates one `Chat` Entity and retains its handle. Each page render places that same handle in the tree.
2. A caller can append a message with `chat.update(cx, |chat, cx| { chat.messages.push("Hello".into()); cx.notify(); });`. The inner `cx` belongs to `Chat`; the update does not require a separate `ConversationPage` notification.
3. On a later window pass, GPUI builds the affected element tree and runs layout, prepaint, and paint as needed. The new message appears. A click on **Clear** then runs the registered listener, clears `Chat`'s messages, and notifies that same Entity.
4. Clicking **Clear** again leaves the collection unchanged, so `clear` does not notify. Neither step promises a fixed count of `render` calls: window refreshes and cache eligibility also affect that count.
To check the ownership boundary in a running view, append a message through the retained `Chat` handle and confirm it appears. Change unrelated parent state and confirm the child keeps the message; if it resets, look for a new `Chat` Entity being constructed during the parent's `render`. Then click **Clear** and confirm the message disappears.
## Render builds values; handlers perform work
Reading state, choosing children, applying [styles](./style), and attaching handlers are ordinary render work. Rebuilding a tree may happen for reasons unrelated to a user action. Starting a request, registering a subscription, emitting an event, or mutating application state unconditionally in `render` would repeat that work whenever GPUI rebuilds the View. An unconditional `cx.notify()` from `render`, `prepaint`, `paint`, or a canvas callback can keep invalidating the window.
Place lifecycle work in Entity initialization, subscriptions, or an explicit input handler. Hold a `Subscription` on the owning Entity. When asynchronous work completes, update the owning Entity and notify from that update if its state changed. For a click, `cx.listener` turns the later GPUI Kit `Button` callback into an update of this `Render` owner, as in the Chat example. Captured `Entity` handles can similarly update a different owner from a handler; never try to reenter an Entity already being rendered or updated.
GPUI Kit uses `Render` for state owners such as input and selection state, while controls such as `Button` use `RenderOnce` to turn their current props into elements. A custom `Element` is appropriate when layout, hitboxes, or painting need direct control. This composition lets a persistent View own behavior while short lived values describe its current interface.
## Common mistakes
| Symptom | Check |
| --- | --- |
| State changed but the screen stayed the same | Was the correct Entity updated and its context notified? |
| Work runs repeatedly while idle | Is `render` or a paint callback starting work or calling `notify` on every pass? |
| Child state resets when the parent changes | Is the child a newly created Entity inside `render` instead of a handle retained by an owner? |
| A click handler cannot borrow `self` | Register an owned callback with `cx.listener`, or capture an `Entity` handle for later update. |
| Repeated rows lose local UI state | Give repeated elements with keyed state stable IDs based on item identity, not array positions. See [ElementId](./element_id). |
For the context APIs used here, see [Context](./context). For input callback and event details, see [Event](./event).
---
# Icons & Assets
Source: /docs/assets
GPUI Kit exposes [IconName] and [Icon] through `gpui_kit::assets` and `gpui_kit::component`. The default `gpui-kit` features make both available, but the application must register an `AssetSource` to load icons by path. The underlying [gpui-kit-assets] crate keeps the SVG payloads separate from the component code: register the default `Assets`, select extra icons, or provide your own source.
**NOTE — Depending on the crate does not embed every icon**
**The complete catalog does not make existing applications embed every icon.**
`Assets` keeps the original 101 component icons. Applications provide additional
icons through their own `AssetSource`, as before; they do not need to redeclare
the component icons. Only explicitly registering `AllAssets` embeds all 1,830
SVGs on native platforms. Depending on the crate or using the shared `IconName`
alone does not reference every SVG payload.
| Native asset configuration | Embedded SVG data | Binary increase vs. default `Assets` |
| --- | ---: | ---: |
| Default component icons (101) | 44.28 KiB | 0 B (baseline) |
| Default + 2 application icons (103) | 45.04 KiB | +15.19 KiB |
| Default + 10 application icons (111) | 48.09 KiB | +19.19 KiB |
| Explicit `AllAssets` (1,830) | 731.45 KiB | +1.02 MiB |
**In this example, adding 10 application icons costs about 19 KiB, not the full
catalog.** Their SVGs total 3,903 bytes; the measured binary increase is 19,648
bytes, including the extra source's lookup/list-composition code, metadata and
alignment. These are not fixed per-icon costs or whole-application sizes.
Measured with Lucide 1.43.0 on Linux x86_64, Rust 1.98.0, `--release`, and stripped
symbols. Each program uses the same `IconName` lookup and runtime asset path.
The extra source falls back to `Assets`, and merges, sorts and deduplicates both
sources' lists. The 10 extras are `Accessibility`, `AlarmClock`, `Archive`,
`Award`, `Backpack`, `Bike`, `Bird`, `Camera`, `Coffee` and `Compass`; the two-icon
case uses the first two. SVG complexity, toolchain and source implementation
change the result.
Binary size is not RAM usage. Selected sources borrow static bytes without a
copy/cache; actual rendering still allocates for parsing, rasterization and
render caches. Runtime shared-name lookup can retain a name/path table, and
Cargo's downloaded package/build artifacts still contain the complete catalog.
On [WebAssembly](./webassembly), `Assets::new(endpoint)` and `AllAssets::new(endpoint)` use the existing
on-demand CDN loader instead of embedding the complete bundle.
## Shared names and compatibility
`gpui_kit::assets::IconName` provides the complete shared catalog without a
Component dependency. `gpui_kit::component::IconName` remains the original
compatibility enum: existing imports, exhaustive matches and `.view(cx)` calls
continue to work without a new trait import. `Icon::new(...)` accepts either
type. A legacy name converts into the shared name with `.into()`.
For the new shared enum, use `Icon::new(name).view(cx)` when a component [Entity](./entity)
is needed, or import `gpui_kit::component::IconNameExt` to call `name.view(cx)`.
`IconName::ALL` enumerates all 1,830 names; `IconName::Accessibility.path()`
returns `icons/accessibility.svg`. The default source contains only the original
101 component icons. Supply extra icons using the custom source below, or
explicitly register `AllAssets` to use the complete bundle.
## Start with the default source
`gpui_kit::assets::Assets` provides the default resource source with the original 101 component icons listed in [`default-icons.txt`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/assets/default-icons.txt).
Add the umbrella crate to `Cargo.toml`; its default features include `component` and `assets`:
```toml
[dependencies]
gpui-kit = "{{gpui_kit_version}}"
```
For a native desktop app, register the source before opening a window. This complete `src/main.rs` renders a default icon:
```rust
use gpui_kit::*;
use gpui_kit::assets::Assets;
use gpui_kit::component::{Icon, IconName};
struct Example;
impl Render for Example {
fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement {
div().child(Icon::new(IconName::Inbox))
}
}
fn main() {
gpui_kit::application().with_assets(Assets).run(|cx| {
gpui_kit::init(cx);
gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| {
cx.new(|_| Example)
})
.expect("Failed to open window");
});
}
```
`with_assets` installs one application-wide source. `gpui_kit::init(cx)` initializes the component layer before the window is constructed. `IconName::Inbox` resolves to `icons/inbox.svg`; GPUI passes that exact path to the source. Merely adding the crate dependency does not register a source. See [Getting Started](/docs/getting-started) for the full application setup.
## Pick additional catalog icons with `icon_assets!`
If an app already depends on `gpui-kit`, use `gpui_kit::assets::icon_assets!`; its default `assets` feature exposes the macro and the shared catalog. **Do not add a second `gpui-kit-assets` dependency just to use it.** A crate that intentionally uses the asset layer without the umbrella can depend on `gpui-kit-assets` directly and call `gpui_kit_assets::icon_assets!` instead.
Choose the source according to the icons the app renders:
| Need | Register with `with_assets` | Native icon data |
| --- | --- | --- |
| Only the default component icons | `Assets` | 101 default SVGs |
| Default icons plus a few catalog icons | A composite of `Assets` and `icon_assets!` | Defaults plus named selections |
| Every catalog icon | `AllAssets` | All 1,830 SVGs |
| Only a few catalog icons, without Component | The generated source alone | Named selections only |
The macro arguments are **`IconName` variant identifiers**, not strings, paths, or your own SVG filenames. For example, `Accessibility` selects `icons/accessibility.svg`. The generated `ExtraIcons` is a unit struct implementing `AssetSource`: `load` returns borrowed embedded bytes for selected paths and `Ok(None)` otherwise; `list(prefix)` lists selected paths. Selection happens at compile time. A misspelled or nonexistent variant fails compilation. The macro accepts an optional visibility modifier, such as `icon_assets!(pub ExtraIcons, [Accessibility]);`, when the source must be used from another module.
The following complete `src/main.rs` selects two extra Lucide icons while retaining the default component icons. It targets native desktop applications. Use the `gpui-kit` dependency shown above; no SVG copying or new crate is needed.
```rust
use gpui_kit::*;
use gpui_kit::assets::{Assets as ComponentAssets, icon_assets};
use gpui_kit::component::Icon;
use std::borrow::Cow;
icon_assets!(ExtraIcons, [Accessibility, AlarmClock]);
struct AppAssets;
impl AssetSource for AppAssets {
fn load(&self, path: &str) -> Result