# 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.
let p = |x: f32, y: f32| point(bounds.left() + px(x), bounds.top() + px(y));

let mut outer = PathBuilder::fill();
outer.move_to(p(4., 4.));
outer.line_to(p(28., 4.));
outer.line_to(p(28., 9.));
outer.line_to(p(10., 9.));
outer.line_to(p(10., 23.));
outer.line_to(p(28., 23.));
outer.line_to(p(28., 28.));
outer.line_to(p(4., 28.));
outer.close();
if let Ok(path) = outer.build() {
    window.paint_path(path, foreground);
}

let mut accent = PathBuilder::fill();
accent.move_to(p(16., 13.));
accent.line_to(p(28., 13.));
accent.line_to(p(28., 23.));
accent.line_to(p(23., 23.));
accent.line_to(p(23., 18.));
accent.line_to(p(16., 18.));
accent.close();
if let Ok(path) = accent.build() {
    window.paint_path(path, accent_color);
}
<svg viewBox="0 0 32 32">
  <path fill="currentColor" d="M4 4 L28 4 L28 9 L10 9 L10 23 L28 23 L28 28 L4 28 Z" />
  <path fill="#3B82F6" d="M16 13 L28 13 L28 23 L23 23 L23 18 L16 18 Z" />
</svg>
GPUI Kit mark drawn with two colored paths
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:

sudo apt update
sudo apt install -y gcc g++ clang libfontconfig-dev libwayland-dev \
  libwebkit2gtk-4.1-dev libxkbcommon-x11-dev libx11-xcb-dev \
  libssl-dev libzstd-dev vulkan-validationlayers libvulkan1

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 ``` Chat emits one MessageSent Event to independent Workspace, Activity Log, and Telemetry subscribers Chat emits one MessageSent Event to independent Workspace, Activity Log, and Telemetry subscribers - [**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 GPUI application architecture layers GPUI application architecture layers 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
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 GPUI shortcut dispatch in three steps Focus builds a dispatch path, Key Context selects a binding, and its Action reaches a handler on that path. 1 · FOCUSBuild the Dispatch Path Chat → Workspacefocused Element → ancestors Start at the Element with Focus. 2 · KEY CONTEXTMatch a KeyBinding ⌘ Enter + "Chat"→ SendMessage Use key_context values on the path. 3 · ACTIONDispatch along the path Chat handlerthen parents if propagated The most specific handler runs first. 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' CFBundleNameHelloWorld CFBundleDisplayNameHelloWorld CFBundleIdentifiercom.example.helloworld CFBundleExecutablehello_world CFBundlePackageTypeAPPL CFBundleShortVersionString{{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. Light theme chat composer: the inset surface, both buttons, and placeholder spacing change together; red guide circles show their shared corner center. Dark theme chat composer: the inset surface, both buttons, and placeholder spacing change together; red guide circles show their shared corner center. 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. Alignment spines across a desktop surface Alignment spines across a desktop surface 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>> { if let Some(bytes) = ExtraIcons.load(path)? { return Ok(Some(bytes)); } ComponentAssets.load(path) } fn list(&self, path: &str) -> Result> { let mut paths = ComponentAssets.list(path)?; paths.extend(ExtraIcons.list(path)?); paths.sort(); paths.dedup(); Ok(paths) } } struct Example; impl Render for Example { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div() .child(Icon::new(gpui_kit::assets::IconName::Accessibility)) .child(Icon::new(gpui_kit::assets::IconName::AlarmClock)) } } fn main() { gpui_kit::application().with_assets(AppAssets).run(|cx| { gpui_kit::init(cx); gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| { cx.new(|_| Example) }) .expect("Failed to open window"); }); } ``` `with_assets` takes one source, so `AppAssets` owns both lookups. It checks `ExtraIcons` first because native `ComponentAssets.load` returns an error for an unknown path; that error would prevent a later fallback. `list` merges, sorts, and deduplicates both sources so callers see the same keys that `load` can serve. The generated source alone is suitable only when the app does not need the default component icons. To migrate a hand-written asset lookup for bundled Lucide icons, replace its copied SVG bytes or filename match arms with one `icon_assets!(ExtraIcons, [...]);` declaration. Keep any unrelated app files in their own source, query that source and `ExtraIcons` before the default `Assets` fallback, and merge all three `list` results. Remove the old SVG copies only after verifying that no other code loads those files by path. Your own files such as `icons/brand-mark.svg` are outside the catalog and still need the custom source described below. `IconName::ALL` lists names; it does not make every name loadable from the currently registered source. For a quick check, call `AppAssets.load("icons/accessibility.svg")` and `AppAssets.load("icons/inbox.svg")`; both should return `Some` on native. `AppAssets.list("icons/")` should contain both paths. If the extra icon is blank, check that the rendered `IconName` appears in the macro list and that `AppAssets`, rather than `ComponentAssets`, was registered. On WebAssembly, the macro source can still embed the selection, but the built-in `Assets` is constructed with an endpoint and must be stored as a field in the composite source. ## Add your own asset files `icon_assets!` selects from GPUI Kit's named catalog. For an application's own logo, illustration, photo, or other image, use an application `AssetSource`. The [assets] folder in this repository supplies catalog SVGs; placing a new file in your app does **not** add an `IconName` variant. A key such as `icons/brand-mark.svg` is relative to the embedded folder, not a URL or a path relative to the running process's working directory. The example below embeds one custom SVG and an image folder. Use this layout next to your application's `Cargo.toml`: ```text Cargo.toml src/main.rs assets/icons/brand-mark.svg assets/images/cover.png ``` Add [rust-embed] alongside `gpui-kit`: ```toml [dependencies] gpui-kit = "{{gpui_kit_version}}" rust-embed = { version = "8.7", features = ["include-exclude"] } ``` This complete native `src/main.rs` serves app files first and falls back to GPUI Kit's default component icons. The `images/**/*` pattern embeds **every file** under that folder, so keep only intended assets there; you can replace it with narrower `#[include]` patterns. The example registers one source before opening the window. ```rs use gpui_kit::*; use gpui_kit::assets::Assets as ComponentAssets; use gpui_kit::component::Icon; use rust_embed::RustEmbed; use std::borrow::Cow; #[derive(RustEmbed)] #[folder = "./assets"] #[include = "icons/**/*.svg"] #[include = "images/**/*"] struct AppFiles; struct AppAssets; impl AssetSource for AppAssets { fn load(&self, path: &str) -> Result>> { if path.is_empty() { return Ok(None); } if let Some(file) = AppFiles::get(path) { return Ok(Some(file.data)); } ComponentAssets.load(path) } fn list(&self, path: &str) -> Result> { let mut paths = ComponentAssets.list(path)?; paths.extend(AppFiles::iter().filter_map(|p| p.starts_with(path).then(|| p.into()))); paths.sort(); paths.dedup(); Ok(paths) } } struct Example; impl Render for Example { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div() .flex() .flex_col() .gap_3() .child(Icon::default().path("icons/brand-mark.svg")) .child(img("images/cover.png").w(px(240.)).h(px(160.)).object_fit(ObjectFit::Cover)) } } fn main() { gpui_kit::application().with_assets(AppAssets).run(|cx| { gpui_kit::init(cx); gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| { cx.new(|_| Example) }) .expect("Failed to open window"); }); } ``` `AssetSource::load(path)` supplies bytes for an exact key; `list(prefix)` reports matching keys. In this example app files win on a key collision, and unknown keys reach `ComponentAssets`. On native, that built-in source errors on an unknown nonempty path. If the app also uses `icon_assets!`, check its generated source before this fallback and include its paths in `list` as shown above. On WASM, construct `Assets::new(endpoint)` and store that value in the composite source instead of using the native unit struct. ## Use an asset path Use the path exposed by the registered source. `Icon::path` is for SVG icons; `IconName` maps known names to those paths. ```rust use gpui_kit::component::{Icon, IconName}; let built_in = Icon::new(IconName::Inbox); let custom = Icon::default().path("icons/brand-mark.svg"); let extra = Icon::new(gpui_kit::assets::IconName::Accessibility); ``` `extra` needs the selected-icons source above or `AllAssets`. `custom` needs a source that contains `icons/brand-mark.svg`. Use `Icon::path` or `svg().path(...)` for a **monochrome SVG** that should follow the text color; GPUI paints it as a tinted alpha mask. Set an explicit size and `text_color` when needed. Use `img()` for photographs and other raster graphics, or for a **multicolor SVG** whose original colors should be preserved: ```rust use gpui_kit::{ObjectFit, StyledImage, img, px, svg}; let tinted_mark = svg().path("icons/brand-mark.svg").size(px(24.)); let cover = img("images/cover.png") .w(px(240.)) .h(px(160.)) .object_fit(ObjectFit::Cover); let color_artwork = img("images/illustration.svg").size(px(160.)); ``` The `img()` source supports common PNG, JPEG, WebP, GIF, and SVG files (and other formats listed by GPUI's `Img::extensions()`). It detects raster formats from the bytes; SVG takes a separate decoding path. Use a real image file with a supported format, not only a matching extension. `ObjectFit::Contain` is the default; `Cover` fills the bounds and may crop, while `Fill` can distort. `object_fit` controls `img()`, not the monochrome `svg()` element. `img()` loads and decodes asynchronously through GPUI's image cache; embedded source bytes are copied into its loader, so embedding alone does not eliminate decode or runtime image memory. Animated GIF and WebP can contain multiple frames. For a string like `"images/cover.png"`, GPUI calls the registered `AssetSource` with that key. `img(std::path::Path::new("/absolute/file.png"))` reads a filesystem path, and a URL string uses the HTTP loader. They have different deployment and error behavior. See [Images](/docs/image) for how each source loads, sizes, and caches. ## Embed individual SVG icons For custom icons, `Icon::data` accepts SVG bytes directly without an asset-path registry: ```rust use gpui_kit::component::{Icon, button::Button}; Button::new("search") .icon(Icon::default().data(include_bytes!("search.svg"))) .label("Search") ``` This only removes the asset lookup for that icon. Built-in `IconName` values and other path-based component icons still need an asset source. See [SVG Bytes](/component/icon#svg-bytes) for ownership, source replacement, loading icons, and custom icon types. ## Diagnose a missing asset | Symptom | Check | | --- | --- | | A default component icon is blank | Confirm `.with_assets(Assets)` (or a composed source) runs before opening windows. Check that the path starts with `icons/` and ends in `.svg`. | | A shared catalog name is blank | Confirm that name belongs to the registered source. `Assets` has 101 defaults; register selected icons or `AllAssets` for other names. | | A custom icon is blank | Compare the exact `Icon::path` key with the path below the source's `#[folder]`; check `#[include]` and case. `list("icons/")` can reveal what the source exposes. | | An image is blank | Check whether the argument is an asset key, filesystem `Path`, or URL; verify that the source includes the raster file and that the bytes are a supported image format. | On native, a missing path in the built-in `Assets` and `AllAssets` returns an error; an empty path returns `Ok(None)`. In a composed source, test custom matches before the built-in fallback. `Icon::data` avoids path lookup for that one SVG, but malformed SVG bytes can still fail to render. ## Packaging and WebAssembly Native `Assets` embeds only the default SVGs; `AllAssets` embeds the full catalog. `icon_assets!` embeds selected SVG bytes, and `rust-embed` embeds files matched by your include patterns. Source paths are resolved at build time, so packaged native apps do not need the source `assets/` directory at runtime. A filesystem `Path` passed to `img()` is different: that file must exist where the app runs. Use the size measurements above as examples, then measure your own release binary. On WebAssembly, `Assets::new(endpoint)` and `AllAssets::new(endpoint)` use the same on-demand HTTP source. It requests `endpoint + "/assets/" + path` for `icons/*.svg`, caches successful responses, and reports a temporary loading error while a request is in flight. Host the exact files and use an endpoint without a trailing slash. Check the browser Network and Console panels for status, CORS, or URL problems; the loader does not itself request a repaint after a download. `list()` returns an empty list on this source. The native `rust-embed` example above is a separate choice for explicitly embedding app files, including on WASM. See [WebAssembly](/docs/webassembly) for the gallery's deployment setup. ## Resources - [Lucide Icons](https://lucide.dev/) - GPUI Kit's icon catalog is based on the open-source Lucide collection. [rust-embed]: https://docs.rs/rust-embed/latest/rust_embed/ [IconName]: https://docs.rs/gpui-kit-assets/{{gpui_kit_version}}/gpui_kit_assets/enum.IconName.html [Icon]: https://docs.rs/gpui-component/latest/gpui_component/struct.Icon.html [assets]: https://github.com/MohsenDastaran/uni-kit/tree/main/crates/assets/assets/icons [gpui-kit-assets]: https://docs.rs/crate/gpui-kit-assets/{{gpui_kit_version}} --- # Animation Source: /docs/animation GPUI Kit offers three levels of motion. Choose by **what owns the changing value**, not by the shape of the effect: | Level | Use it for | State and policy | | --- | --- | --- | | GPUI `Animation` and `AnimationExt` | An element entering, pulsing, or running a fixed series while mounted | GPUI retains playback under the wrapper's [`ElementId`](./element_id); the caller chooses duration, easing, and visual property. | | [GPUI Base Motion](/base/motion) | A target that changes during motion, an exit before unmount, keyframes, or measured reveal | Base retains each channel under a stable key and requests frames through [Window](./window) while active; the caller chooses the visual result. | | GPUI Component motion | A styled control whose appearance follows the theme | `cx.theme().motion_tokens()` supplies semantic timing, easing, springs, and distances; components compose these with GPUI or Base. | The application owns the semantic state: whether a dialog is open, which tab is selected, or where a slider points. An animation samples that state for presentation. Keep the result understandable at both endpoints and when motion is disabled. For a first animation, decide whether a new event may change the destination before the motion finishes. If it can, start with a target-driven Base `transition` or `spring`. Use GPUI `with_animation` when one mounted element should play a known sequence from start to finish. The distinction matters when users click twice: a playback clock and a changing target have different interruption behavior. ## Start with a changing selection Run the existing [Motion example](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/motion/mod.rs) from the repository root: ```sh cargo run -p gpui-base-examples --bin motion ``` The example opens on **Sliding time**. Open the **Spring** tab and click **Focus** and **Flow**. The dark indicator moves to the selected half of the track. Switch choices before it settles to see it change direction. The labels remain visible throughout, and the selected label changes color immediately; the motion adds spatial continuity to that state. ### Keep the target in view state `MotionExample` stores `spring_selected: bool`. The button for each option assigns its value and notifies the view: ```rust .on_click(move |_, _, cx| { _ = entity.update(cx, |this, cx| { this.spring_selected = selected; cx.notify(); }); }) ``` The boolean selects **Focus** (`false`) or **Flow** (`true`). It is the durable state; the view does not need to store the indicator's current pixel position. Clicking the already selected option leaves the target unchanged. ### Sample the spring on each render In `spring_demo`, the boolean chooses a target of 0 or 120 pixels. Base's `spring` returns the current value for this frame: ```rust let x = spring( "selector-indicator", if self.spring_selected { 120. } else { 0. }, Spring::new(Duration::from_millis(420)).with_damping(0.68), window, cx, ); ``` `"selector-indicator"` is a stable channel ID, so later renders find the same spring and preserve its position and velocity when the target changes. On the first render, the spring adopts its target immediately; movement begins only after a click changes that target. While it is moving, Base requests more frames. Keep sampling the channel on every render while the indicator is mounted; the click handler's `cx.notify()` announces a state change, but no render loop or timer is needed. ### Apply the sample to the indicator The track is a relatively positioned 240-pixel parent. The indicator uses the sampled value as its left inset: ```rust div() .relative() .w(px(240.)) .h_10() .child( div() .absolute() .left(px(x)) .w(px(119.)) .h_full(), ) ``` The source also styles the track, indicator, and option buttons. The two targets place the 119-pixel indicator under the corresponding option. These fixed widths make the coordinate calculation easy to follow in the example; a product component should use its own sizing and theme policy. ### Follow one click from input to rest | Moment | What happens | Who owns it | | --- | --- | --- | | Click **Flow** | The handler writes `spring_selected = true` and calls `cx.notify()`. | `MotionExample` owns the selection. | | Next render | `spring_demo` passes the new target, `120.`, to the channel named `"selector-indicator"`. | Base retains the channel's sampled position and velocity. | | While moving | `spring` returns a new `x` and requests a later animation frame. The indicator uses that `x` in `.left(px(x))`. | GPUI schedules requested frames; the render code describes each result. | | At rest | `spring` returns the target without asking for another motion frame. | The view still owns `spring_selected`; a later click can retarget it. | Try changing `Duration::from_millis(420)` to `Duration::from_millis(700)` in `spring_demo`, then run the same command again and switch **Focus** and **Flow** before the indicator settles. This changes the spring's response policy, not the click handler or semantic selection. Restore `420` afterward. For a second experiment, change the spring target for **Flow** from `120.` to `60.`. The indicator now stops between the two labels while the selected label still changes correctly. Restore `120.`: the target is presentation geometry, whereas `spring_selected` is the answer to “which choice is selected?” To inspect reduced motion, add `cx.set_reduce_motion(true);` immediately after `gpui_base::init(cx);` in the example's `run` function, then run it again. The selected option still changes, and the spring reaches its target without animated frames. Remove the added line afterward. If the indicator does not move with motion enabled, check that the target changes, the channel ID stays stable, and `spring` is sampled while the indicator remains mounted. Give any additional moving property its own ID; sharing one channel mixes retained values. ### Let an exiting element finish before unmounting Open **Presence** in the same example and click **Remove**. The example changes `present` to `false` immediately, but keeps rendering the notice while its opacity reaches zero. Its render path samples presence *before* deciding whether to include the notice: ```rust let sample = Presence::new("presence-notice", self.present) .transition(Transition::new(Duration::from_millis(360)).easing(Easing::EaseInOut)) .sample(window, cx); // Keep the notice mounted through its exit phase. if sample.should_render() { div().opacity(sample.progress).child("Background task") } else { div() } ``` The excerpt shows the lifecycle decision; the [full example](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/motion/mod.rs) also lays out and styles the notice. If the view instead used `if self.present` to decide whether to render it, **Remove** would unmount the notice on the first changed render and there would be no exit to draw. Click **Insert** during exit to see `Presence` reverse from the sampled value. Under reduced motion it moves directly to the appropriate endpoint. `present` remains the logical state; a visually exiting notice should not remain an active control after its action is no longer available. ## GPUI's element animation `Animation::new(duration)` creates a one-shot, linear animation. `AnimationExt::with_animation(id, animation, animator)` wraps an `IntoElement`; the callback receives that element and an eased progress value. GPUI calls it during layout, applies the returned element's style, and requests another frame until the animation ends. The callback may change any property supported by that element, such as opacity or a transform. See the [GPUI {{gpui_pre_version}} animation source](https://docs.rs/gpui-pre/{{gpui_pre_version}}/src/gpui/elements/animation.rs.html) for the wrapper's playback rules. Here `gpui-pre` is the publication and version-alignment package name for GPUI; it is not an additional application layer. ```rust use std::time::Duration; use gpui_kit::*; let entering = div() .child("Saved") .with_animation( ("saved-notice", generation), Animation::new(Duration::from_millis(180)), |element, progress| element.opacity(progress), ); ``` ### Try one-shot replay beside a retargetable spring Replace `examples/hello_world/src/main.rs` with this temporary exercise, then run `cargo run -p hello_world`. It uses the existing example package and the same `gpui-kit` dependency: ```rust use std::time::Duration; use gpui_kit::*; use gpui_kit::base::{Spring, spring}; use gpui_kit::component::button::*; use gpui_kit::prelude::FluentBuilder as _; struct MotionProbe { generation: usize, show_notice: bool, selected: bool, } impl Render for MotionProbe { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let offset = spring( "motion-probe-indicator", if self.selected { 120. } else { 0. }, Spring::new(Duration::from_millis(420)).with_damping(0.68), window, cx, ); div() .flex() .flex_col() .gap_4() .p_6() .when(self.show_notice, |parent| { parent.child( div().child("Saved").with_animation( ("motion-probe-notice", self.generation), Animation::new(Duration::from_millis(900)), |element, progress| element.opacity(progress), ), ) }) .child( Button::new("replay").label("Replay notice").on_click(cx.listener( |this, _, _, cx| { this.generation += 1; this.show_notice = true; cx.notify(); }, )), ) .child( Button::new("hide").label("Hide notice").on_click(cx.listener( |this, _, _, cx| { this.show_notice = false; cx.notify(); }, )), ) .child( div().relative().w(px(240.)).h_10().child( div() .absolute() .left(px(offset)) .w(px(119.)) .h_full() .bg(rgb(0x3366cc)), ), ) .child( Button::new("retarget").label("Switch target").on_click(cx.listener( |this, _, _, cx| { this.selected = !this.selected; cx.notify(); }, )), ) } } fn main() { gpui_kit::application().run(|cx| { gpui_kit::init(cx); gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| { cx.new(|_| MotionProbe { generation: 0, show_notice: true, selected: false, }) }) .expect("failed to open window"); }); } ``` The **Saved** notice fades in once and then remains visible. Click **Replay notice**: incrementing `generation` gives the wrapper a new ID and starts a fresh fade. Click **Hide notice** before the fade finishes: `show_notice = false` removes the wrapper, so that playback stops. **Replay notice** mounts a fresh wrapper and fades in again. Click **Switch target** again while the blue bar is moving: it changes direction from its current sampled position because the Base spring keeps its channel ID while `selected` changes its target. The spring's 420 ms response is a motion scale, not a fixed completion deadline; settling ends at its configured tolerance. A click on one control does not restart the other animation. For the ID experiment, replace `("motion-probe-notice", self.generation)` with `"motion-probe-notice"` and keep the click handler. While the notice stays mounted, subsequent **Replay notice** clicks still update view state, but the finished one-shot does not fade again. Restore the tuple ID and the original example file after the exercise. To replay the exercise with reduced motion, add `cx.set_reduce_motion(true);` immediately after `gpui_kit::init(cx);` and run it again. **Saved** appears at full opacity without a fade, the spring adopts the selected target, and **Hide notice** still removes the notice. Remove the temporary line afterward. The bar is a visual probe of the spring value; its fixed 240-pixel track and 120-pixel target are local example geometry. In an application, the selected state must also be clear without movement. To compare a complete selector with labels and interaction states, run the existing [Motion example](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/motion/mod.rs) and open its **Spring** tab. Bring `AnimationExt` into scope through `gpui_kit::*` (or import the trait explicitly). The ID identifies the animation wrapper, not the text or the visual property. Rendering the same wrapper at the same place with the same ID continues its playback; recreating `Animation::new(...)` on each render does **not** restart it. Change an application-owned generation in the ID when a fresh appearance should replay. Removing the wrapper ends its lifetime. A one-shot animation stays at its final value while the same wrapper remains mounted. `with_easing(f)` maps normalized time to progress. GPUI supplies functions such as `ease_in_out` and `bounce`; a custom easing function must return a finite value. Overshoot is permitted, so clamp the resulting style property if that property has a narrower valid range. `repeat()` loops locally. `repeat_synced()` loops against an application-wide clock, useful when several indicators should share a phase. `with_max_fps(rate)` limits requests for this animation to at most that rate; invalid nonpositive or nonfinite values are ignored. See [FPS](./fps) for measuring actual frame performance. `with_animations(id, animations, |element, step, progress| ...)` plays a fixed chain and reports the active step index. GPUI also exposes `AnimationExt::with_spring(id, SpringAnimation, animator)` for a spring-driven element. Its stable ID preserves position and velocity across target changes. A newly mounted spring starts at its target unless `SpringAnimation::from(...)` supplies a starting value. Use this when the spring can live naturally on one element; Base's `spring` is useful when a separately keyed value feeds more than one part of a composition. The GPUI spring's `SpringPlayback` controls whether it runs, pauses, stops, completes, or cancels. Reduced motion snaps a **running** spring to its target; paused and stopped playback keeps its playback state. Choose the target and playback state from application state rather than treating the wrapper as the source of truth. ### What interruption means here `with_animation` is a playback of elapsed time, not a transition toward a changing target. Changing the callback's captured endpoint under the same ID continues the existing clock; changing the ID starts a fresh playback. Neither operation automatically samples the current rendered value as the new starting point. For a selection indicator that must reverse smoothly when clicked again, use a target-driven spring or Base `transition` instead. GPUI's `AnimationExt` honors `App::reduce_motion()`: a one-shot renders at its end, a repeating animation renders at its start, and neither schedules animation frames. Keep a loading state visible through text or another static cue; a stopped spinner by itself cannot tell the user what is happening. Use the same sequence of questions for each new effect: What application state changes? Which element or channel ID remains stable between renders? What value is sampled on the first render? What happens on a second input before completion? When does the effect stop requesting frames? What remains understandable with reduced motion? These questions catch most apparent “animation did not run” bugs before changing a timing curve. ## GPUI Base Motion: keyed values and lifecycles Import these APIs from `gpui_kit::base` when an application depends on `gpui-kit`: ```rust use gpui_kit::*; use gpui_kit::base::{Easing, Transition, transition}; use std::time::Duration; let opacity = transition( ("save-panel", "opacity"), if open { 1.0 } else { 0.0 }, Transition::new(Duration::from_millis(180)).easing(Easing::EaseOut), window, cx, ); div().opacity(opacity) ``` `Transition` here is a **timing policy** for a value, not a styled element. `transition` samples and returns the value; `transition_with_status` also reports `Idle`, `Delayed`, `Running`, or `Finished`. On a changed target, the channel starts at its currently sampled value. A direct reversal shortens the return duration to match the remaining distance. Base requests frames only while the channel is delayed or running. Sample the channel on every render while its owner is present, including when the result is visually hidden, so its retained value settles correctly. Every independently moving value needs its own stable channel ID. Namespace channels by domain object and property, for example `(project_element_id.clone(), "opacity")` and `(project_element_id.clone(), "height")`, where `project_element_id` is a stable `ElementId`. Two channels sharing an ID can overwrite retained state; changing IDs every render loses continuity. A reorderable list needs item IDs, not row indexes. GPUI's wrapper ID and Base's channel ID serve different lifecycles even when they describe the same visual element. Use Base `spring(id, target, Spring, window, cx)` when the target may move again before settling. It preserves position **and velocity** on retarget. During direct pointer manipulation, use `Spring::with_travel(false)` so the value tracks the pointer; restore travel on release. A spring's `epsilon` is measured in the target's units, so a pixel offset may need a coarser tolerance than normalized opacity. Base also provides the following choices: | Need | API | Lifecycle detail | | --- | --- | --- | | Authored value stops | `Keyframes`, `Timing`, `animate_keyframes` | Same ID continues playback; include an application generation in the ID to replay. Timing supports delays, iterations, and playback direction. | | Distinct ordered steps | `Sequence` | Each step begins at the previous step's absolute end time; the ID plays once until deliberately changed. Changing the active step's target restarts from its sampled value. A sequence does not reverse automatically. | | Keep content mounted through exit | `Presence` | Sample while logically closed; render until `should_render()` becomes false. Reopening during exit reverses from the current sample. | | Delay repeated items | `Stagger` | Computes a delay by index and origin; it does not own the list or its IDs. | | Expand content of unknown height | `MotionReveal` | Measures the child and clips its visible height by caller-supplied progress. It does not sample or animate progress itself. | See the [Base Motion guide](/base/motion) for the full signatures, validation rules, examples, and benchmark. Its `Transition` is distinct from the older `gpui_kit::base::animation::EffectTransition`, which wraps GPUI `with_animation` to apply predefined fade, slide, width, and height effects. For new target-driven work, use `base::motion` primitives and apply the sampled value yourself. ## GPUI Component: semantic motion policy Styled components use `cx.theme().motion_tokens()` for a shared policy. `MotionTokens` has `duration_instant`, `duration_fast`, `duration_normal`, and `duration_slow`; `easing_enter`, `easing_exit`, and `easing_move`; `spring_control` and `spring_move`; and `distance_short` and `distance_medium`. The defaults are a coherent scale, not a rule that every control must animate. Read tokens from the active theme so a product can tune them in one place. Try the existing styled control with `cargo run -p gpui-component-story -- SwitchStory`. Click an enabled switch twice quickly: its checked state changes with each click, and its thumb changes direction during travel. A disabled switch in the same story does not respond. In [`crates/component/src/switch.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/switch.rs), follow `checked` into the thumb's target offset and `cx.theme().motion_tokens().spring_move` into the Base `spring` call. In [`crates/component/src/theme/motion.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/theme/motion.rs), temporarily change the default `spring_move` duration from 280 ms to 600 ms and rerun the story: the same checked states now settle more slowly. Restore 280 ms afterward. The story owns the checked boolean, the component maps it to a visual target, and the theme supplies motion policy. For example, the [Switch source](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/switch.rs) samples a Base spring on `(self.id.clone(), "thumb")` toward the checked or unchecked thumb offset, using `cx.theme().motion_tokens().spring_move`. The [resizable handle source](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/resizable.rs) samples separate length and opacity channels with `duration_fast` and `easing_move`; the hairline remains present even when the indicator fades. [Collapsible](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/collapsible.rs) opts into measured, reversible reveal through `.motion_id(id)`. Without that ID, it mounts and unmounts immediately. Some components use GPUI's element wrapper for a fixed animation: [Spinner](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/spinner.rs) repeats a rotation, while [Popover](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/popover.rs) animates its entrance. The choice follows whether a component needs continuing target state or a fixed playback, not whether it belongs to the styled layer. ## Reduced motion and frame ownership GPUI stores the preference on `App`; `cx.reduce_motion()` reads it and `cx.set_reduce_motion(...)` can set it. `gpui_kit::init(cx)` initializes Base's system preference handling. Base reads macOS and Windows at initialization, follows the Linux desktop portal preference as it arrives and changes, and leaves the flag alone on other targets. If the application explicitly sets the flag, Base leaves that application choice in control. Call `gpui_kit::base::apply_system_reduce_motion(cx)` to reread macOS or Windows after initialization. GPUI element animation adopts a static endpoint as described above. Base's finite transitions, springs, keyframes, presence, and sequences snap to their appropriate target or final state and stop requesting motion frames. `MotionReveal` only consumes progress: when using it directly, pass an endpoint under reduced motion or drive it with a reduced-motion-aware sampler. It may still request a frame when the child's measured height changes. Infinite activity should remain legible in a static state. If a custom element owns its own clock, check `cx.reduce_motion()` and request frames only while useful motion remains; do not unconditionally call `cx.notify()`, `window.refresh()`, or `window.request_animation_frame()` from render. Use motion to explain appearance, dismissal, expansion, or spatial continuity. Prefer short opacity or transform changes over large layout animation when they communicate the same relationship. Coordinate keyboard focus, hit targets, and semantic state with the transition; painted movement alone does not announce a new state to assistive technology. See [Accessibility](./accessibility) for the semantic side of UI changes. --- # SystemNotification Source: /docs/system-notification `SystemNotification` posts a message to the **operating system's notification center**. It is an application-level platform service, not an Element rendered inside a [Window](./window). GPUI Kit's [Notification component](../component/notification) defaults to an in-app toast and can also post to the system with `.system()` or to both places with `.in_app_and_system()`. | Need | Use | | --- | --- | | Immediate feedback while the user is working in the window | `window.push_notification(Notification::info(...), cx)` | | A completed background task that may matter while the app is inactive | `window.push_notification(Notification::info(...).system(), cx)` | | The same status in the window and the OS notification center | `window.push_notification(Notification::info(...).in_app_and_system(), cx)` | | GPUI payload fields, a chosen tag, or OS action buttons | `cx.show_system_notification(SystemNotification { ... })` | Use an in-app toast for a result the user is already watching; use a system notification when they may have switched away. Keep important state in the application itself: delivery may be disabled, suppressed, or delayed by the OS. For a failure requiring a decision, show an in-app error or dialog when the user returns instead of depending on a transient system message. ## Try the existing story first From this repository, run the notification story and press **System only** or **In-app and system**: ```sh cargo run -p gpui-component-story -- NotificationStory ``` The buttons are in `crates/story/src/stories/notification_story.rs`. The story startup sets an app identity in `crates/story/src/main.rs`. This exercises the existing GPUI Kit path without adding a crate. On macOS, a `cargo run` binary is not a bundled `.app`, so the system post is suppressed; use a bundled build for a real delivery check. The in-app half of **In-app and system** can still appear. Walk through the **System notification** section in this order: | Action | Expected observation | | --- | --- | | Press **System only**. | No toast appears inside the story. On a supported, permitted desktop installation, the OS receives **Build finished** with the body **Delivered straight to the notification center.** | | Press **In-app and system**. | A toast remains in the story because this button sets `autohide(false)`. The OS post uses **Build finished** with the body **Shown as a toast and in the notification center.** | | Switch to another application, then activate the OS notification. | The application and the original story window are requested to activate. The terminal running the story prints `[notification] system notification clicked`; an in-app counterpart, if present, closes. Window activation and notification presentation remain subject to OS policy. | Both buttons use the same `SystemNotificationKind` ID. In-app pushes with that ID replace the existing toast; a later OS post with the same tag replaces an earlier one only where the platform supports tag replacement. The exercise checks the **component route**. It does not exercise raw `SystemNotification` action buttons. On macOS, the `cargo run` exercise can verify only the in-app portion; use a packaged, authorized app to check OS delivery and activation. If your desktop suppresses banners, also inspect its notification center before treating the post as missing. ## Initialize and identify the application Call `gpui_kit::init(cx)` once at startup and set a stable identifier and display name before opening windows or posting. Windows needs an app identity for an unpackaged app; Linux can use its display name. The GPUI test platform also requires an identity, so set one in notification tests. ```rust gpui_kit::application().run(|cx| { gpui_kit::init(cx); cx.set_app_identity("com.example.exporter", "Exporter"); // Open application windows here. }); ``` | Platform | Delivery conditions and behavior | | --- | --- | | macOS | Run from a real `.app` bundle in a trusted location such as `/Applications`. A plain `cargo run` process cannot post. The first post can request notification permission; denial suppresses delivery. | | Windows | Set the identity early, especially for an unpackaged app. Windows toast delivery and visibility also depend on OS notification settings. | | Linux | A working session D-Bus and XDG notification daemon are needed. The adapter logs a warning if it cannot show a notification. Its current implementation neither retracts nor replaces by tag. | GPUI's `show_system_notification` returns `()`, not a delivery result. A successful call therefore means only that the request was submitted to the platform adapter. A missing toast is not evidence that the application's background work failed. Treat permission as part of the product flow: keep the task result in application state, and make it reachable when the user returns. The OS notification is a prompt to revisit that state, not the state itself. A denied permission, muted application, quiet mode, missing daemon, or a closed source window must not erase a completed export or its error. ## Post with the raw GPUI API The project pins `gpui-pre {{gpui_pre_version}}`. Its [`App::show_system_notification`](https://docs.rs/gpui-pre/{{gpui_pre_version}}/gpui/struct.App.html#method.show_system_notification) accepts a `SystemNotification` with `tag`, `title`, `body`, and `actions`: ```rust use gpui_kit::SystemNotification; // In code that has an &mut App named cx: cx.show_system_notification(SystemNotification { tag: "export/report-42".into(), title: "Export complete".into(), body: "report.pdf is ready".into(), actions: Vec::new(), }); ``` Choose a tag for the underlying task or item. Reposting that tag replaces the earlier notification **where the platform supports replacement**. `cx.dismiss_system_notification("export/report-42")` requests removal of a pending or delivered notification, also on a best-effort basis. In particular, the current Linux adapter does not implement either tag replacement or retraction, so duplicate or stale entries may remain until the daemon ages them out. For OS buttons, put `SystemNotificationAction { id, label }` values in `actions`. `id` is returned as `SystemNotificationResponse::action_id` when that button is pressed; activating the body returns `None`. A platform may present the notification without its buttons. Register `cx.on_system_notification_response(|response, cx| { ... })` once to route raw responses by `response.tag` and `response.action_id`. The handler runs only for delivered user activations, not for posting failures or ordinary dismissal. Raw GPUI posting has no originating window to activate or domain callback to run automatically; the application must own that routing and ensure the target window still exists. For a raw response handler, keep the tag tied to a durable task key, look up that task when the response arrives, and handle both a body click (`None`) and each action ID. A response can arrive after the task or window has gone away. Do not treat the handler as a completion callback for the background operation. ## Use GPUI Kit's window integration When a click should return to the originating window, push a `Notification` through that window's `WindowExt`. Open the window through `gpui_kit::open_window` or wrap its view in `Root::new` so the notification overlay exists. ```rust use gpui_kit::component::{notification::Notification, WindowExt}; struct ExportNotice; window.push_notification( Notification::success("report.pdf is ready") .title("Export complete") .id::() .system() .on_click(|_, window, cx| { // Open the completed export in the owning window. }), cx, ); ``` The component maps its ID to a namespaced system tag and remembers the window that posted it. Use `.id1::(task_key)` when separate tasks need separate identities. It sends the title and message as OS text; if only a message is set, that message becomes the OS title. A content-only notification with neither title nor message does not post. Custom content and the component's `.action(...)` button belong to the in-app toast; component system posts have no OS action buttons. On a recognized click, GPUI Kit requests retraction, activates the app, and then activates the originating window, closes its in-app counterpart if present, and calls `on_click`. The callback needs a live originating window. If that window has closed, the app can still activate, but there is no window callback. A response after restart or after the component's bounded routing entries have been pruned can likewise activate the app without invoking `on_click`; keep essential task state outside the notification callback. The component handles a response once, and ignores raw tags outside its namespace. `.system()` creates no in-app toast, so `on_close` does not run. `.in_app_and_system()` creates both; automatic toast timeout does **not** retract the OS copy. Explicit `window.remove_notification::(cx)` or `window.clear_notifications(cx)` requests system retraction for notifications owned by that window, including system-only ones. If another window has since posted the same component ID, the first window's removal does not retract the newer post. OS retraction remains best-effort. ## Keep one response handler GPUI keeps one `App::on_system_notification_response` handler: registering another replaces the previous one. With the default component feature, `gpui_kit::init(cx)` installs GPUI Kit's handler. Do not overwrite it if component system notifications need click behavior. Raw calls can still post, but that handler ignores their tags, so raw action responses will not reach application code. GPUI Kit does not expose a public way to chain a raw handler onto its component handler; choose one response ownership path for the application. ## Test and troubleshoot `TestAppContext` exposes `shown_system_notifications()`, `delivered_system_notifications()`, `dismissed_system_notifications()`, and `simulate_system_notification_response(...)`. Set an identity in the test, inspect the posted title/body/tag, simulate a click, and assert the intended window callback or raw handler result. The test platform models replacement and retraction; it does **not** prove that a real OS daemon, permission prompt, or click activation works. Check those on every target OS. If nothing appears, verify the application identity, bundle and permission status on macOS, Windows notification settings, or the Linux session daemon. If a click does nothing, check whether the notification was posted through the component or raw GPUI path, whether another response handler replaced GPUI Kit's handler, and whether the originating window still exists. For toast styling and lifecycle, see the [Notification component](../component/notification); for platform boundary design, see [Native Extensions](./native-extension). | Symptom | Check next | | --- | --- | | **In-app and system** shows a toast but no OS entry. | The window integration is mounted. Check OS permission, quiet mode, and platform delivery prerequisites above. On macOS under `cargo run`, this is the expected result. | | **System only** appears to do nothing. | This mode deliberately has no in-app toast. Check the notification center and the same platform prerequisites; the API has no delivery-result callback. | | Clicking an OS entry activates the app but not the intended view. | Check that the originating window is still open and the component's in-memory routing entry still exists. Restore the view from application state on return. | | An old entry remains after a newer post or explicit removal. | Check whether the platform supports tag replacement and retraction. The current Linux adapter supports neither. | | A raw notification's action button has no application effect. | Check the single app-global response handler. The handler installed by `gpui_kit::init(cx)` routes GPUI Kit component tags and ignores raw tags. | --- # WebView Source: /docs/webview [`gpui-wry`](https://github.com/MohsenDastaran/uni-kit/tree/main/crates/webview) is GPUI Kit's **experimental** integration with [Wry](https://github.com/tauri-apps/wry). Use it when a screen needs browser behavior; [TextView HTML](/component/text-view#html) renders document content but is not a browser. To open a URL in the user's default external browser, use [`cx.open_url`](./context#open-a-url-in-the-default-browser). The integration currently supports macOS and Windows. The Linux path in the repository's example is unfinished. ## Run the example From the repository root: ```sh cargo run -p webview ``` The [complete example](https://github.com/MohsenDastaran/uni-kit/blob/main/examples/webview/src/main.rs) is the runnable starting point. It creates the child view inside the `open_window` callback, wraps it in an `Entity`, and renders that Entity below an address input. Enter in the input calls `load_url`; the example also contains a back handler. Run it from the repository root with the command above. For another application, match the dependency versions in [the example manifest](https://github.com/MohsenDastaran/uni-kit/blob/main/examples/webview/Cargo.toml): `gpui-kit`, `gpui-wry`, `wry` (package `lb-wry`), and `raw-window-handle` are direct dependencies of this integration. ```rust use gpui_kit::*; use gpui_wry::WebView; 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"); WebView::new(native, window, cx) }); webview.update(cx, |view, _| view.load_url("https://gpui-kit.com")); // In the owning View's render method: div().flex_1().child(webview.clone()) ``` This excerpt shows the macOS and Windows child-view path. Create the native view only after GPUI supplies a live `Window`, and keep the `Entity` in its owning view so it survives renders. The example calls `gpui_kit::init(cx)` before creating component state and uses `gpui_kit::open_window(...)` to create the window. For the complete application setup, use the linked source rather than treating this excerpt as a standalone `main` function. ## Own the view and its layout `WebView::new` initially sets the native bounds to an empty rectangle. Rendering the entity installs a GPUI layout element; during `prepaint`, that element sends its resolved bounds to Wry in **logical coordinates** and inserts a GPUI hitbox. Give the containing region a real size. The example uses `div().flex_1().h(px(400.)).child(self.webview.clone())`. The browser pixels themselves are painted by the operating system, so GPUI clipping, hitboxes, and content masks do not put GPUI UI above the child view. Keep one `Entity` for each native view. Do not build a new Wry view on each `render`. The wrapper's `visible()` and `bounds()` report its stored visibility and last layout bounds; `show()` and `hide()` change native visibility. If a page or tab no longer renders the entity, explicitly call `hide()` when it should disappear, and `show()` when it returns. `prepaint` skips bounds updates while hidden. Call wrapper methods through `webview.update(cx, |view, _| ...)` on the GPUI UI context. `load_url(&str)` requests navigation but discards Wry's `Result`; it does not report load completion. `back()` runs JavaScript `history.back()` and returns a `Result` for script submission, not a history or navigation result. Use `view.raw()` for Wry methods such as `reload()`, `url()`, or `evaluate_script()` when their return values matter. `view.handle().raw()` gives an owned, UI-thread-local Wry handle when a short-lived callback needs one. Avoid keeping a handle merely to avoid updating the entity. ## Loading, navigation, and page messages Configure page policy and callbacks on `wry::WebViewBuilder` **before** `build_as_child`; these are Wry facilities, not `gpui-wry` events. The pinned `lb-wry` version provides: | Need | Wry API | Meaning | | --- | --- | --- | | Initial content | `with_url(...)` or `with_html(...)` | Select content before building; the repository example instead calls wrapper `load_url` after creation. | | Loading status | `with_on_page_load_handler(|event, url| ...)` | Observe `Started` and `Finished`; `Finished` is not a success or HTTP-status report. Treat this separately from whether `load_url` accepted a request. | | Navigation policy | `with_navigation_handler(|url| -> bool { ... })` | Return `true` to allow or `false` to cancel an incoming navigation. Decide policy for redirects and links as well as the initial URL. | | New windows | `with_new_window_req_handler(...)` | Decide what happens to `window.open` requests; its callback has a platform-specific thread contract. | | Rust to page | `evaluate_script(...)` on the raw Wry view | Submits JavaScript and returns a Wry `Result`; use `evaluate_script_with_callback` when a serialized result is needed. | | Page to Rust | `with_ipc_handler(|request| ...)` | Receives strings posted by `window.ipc.postMessage(...)`. Parse and validate the request before acting on it. | For example, a fixed-URL navigation policy can be attached while constructing the builder: ```rust let builder = wry::WebViewBuilder::new() .with_navigation_handler(|url| url == "https://gpui-kit.com/") .with_on_page_load_handler(|event, url| { // Forward a small status message to the owning GPUI view if needed. // Do not capture &mut Window, &mut App, or &mut Context here. let phase = match event { wry::PageLoadEvent::Started => "started", wry::PageLoadEvent::Finished => "finished", }; eprintln!("{phase}: {url}"); }); ``` This exact-match check is only an illustration of the callback shape; use parsed URL origin and an explicit scheme/host policy for a real allowlist. Keep callback work short. Pass status or IPC messages into application-owned state through a safe scheduling/channel path, then update the `Entity` on GPUI's context and call `cx.notify()` if the UI changes. Wry callbacks are not GPUI entity events, and the Windows new-window callback runs on a separate thread. Plan for callbacks arriving after a page has navigated or its owning window has closed. ## Security and failure boundaries Treat page content as untrusted, including content served from a URL you control. IPC is an application capability: allow only expected message types and payload sizes, check the sender origin where the platform supplies it, and require application authorization before file, network, or account actions. Wry's IPC request URL is the main-frame URL for iframes on Linux/Android, so do not use it alone as iframe identity. Initialization scripts can run on each new page; the pinned Wry documentation says Windows also injects them into subframes, even when main-frame-only is requested. Avoid placing secrets in scripts or page globals. Decide how downloads, external links, and `window.open` requests are handled before displaying remote content. Wry's default download-start handler allows downloads, so add a download policy if that is inappropriate for the application. Keep user-visible loading and error state in the owning GPUI view: `gpui-wry::WebView::load_url` ignores immediate errors, and a successful request can still lead to a failed page load. Use raw Wry results and page-load callbacks as separate signals; neither `PageLoadEvent::Finished` nor `load_url` alone proves successful content loading. Show a retry path for failures your application detects. ## Focus and lifetime The WebView is a **native child view**, not a GPUI-painted Element. It occupies the bounds of its GPUI layout node and receives native browser input. `WebView` implements `Focusable`; its wrapper tracks a `FocusHandle`. Calling `hide()` returns focus to the parent before hiding the native view. Clicking outside the view's bounds also requests focus on the parent. Test keyboard and focus behavior for your own screen, especially when it combines GPUI inputs with browser inputs. Keep the WebView and its handles within the parent window's lifetime. Dropping the owning Entity hides the child view, but cloned `WebViewHandle`s or frame-held clones can delay native destruction. Drop those handles before destroying the parent window. See [Entity](./entity) for state ownership and [Window](./window) for window-local handles. ## Debug and verify The example requests Wry devtools in debug builds (or with its `inspector` feature); it does not open them automatically. On macOS release builds, Wry also requires its own `devtools` feature for this request to take effect. Wry's `open_devtools()` is available behind debug or that dependency feature. Inspect the page to distinguish JavaScript/network failures from GPUI layout issues. For a blank child view, first verify the containing GPUI element has nonzero bounds, that `visible()` is true, and that the native view was created from the current live window. On Windows, check the example's DirectComposition setting below. Confirm navigation and IPC in a real native window; GPUI's headless UI tests cannot prove native browser pixels, system focus, or compositor order. For an application smoke test on each supported OS, exercise initial load, links and redirects, back and reload, new-window/download policy, failed/offline load, IPC input validation, resize, show/hide, keyboard focus transfer, and closing the parent window. Keep pure policy parsing tests separate from these native checks. The repository example demonstrates loading and layout but does not implement a complete navigation, bridge, or error-state test suite. ## Current limitations | Area | Current behavior | | --- | --- | | Platforms | Experimental macOS and Windows support. The Linux example contains an unfinished GTK hosting path; do not treat it as supported. | | Overlay order | The native WebView sits above the GPUI surface and covers GPUI content in the same rectangle, including popovers, dialogs, menus, and tooltips. A GPUI overlay cannot reliably appear on top of it. | | Windows renderer | The repository example sets `GPUI_DISABLE_DIRECT_COMPOSITION=true` before starting GPUI so this child-view approach renders. This is an example-specific requirement, not a general GPUI setting recommendation. | When an overlay must be visible, place the WebView in a separate window or arrange the screen so the overlay does not cross its bounds. Do not present a normal GPUI overlay over the WebView as a supported interaction in the current implementation. ## Unmerged composition experiments The following PRs explore overlay composition. **None is part of the current `gpui-wry` behavior described above.** Check their status and implementation before using a branch in an application: - [GPUI Kit #2626](https://github.com/MohsenDastaran/uni-kit/pull/2626) explores drawing GPUI overlays above the native WebView. Its current branch depends on [Zed/GPUI #61945](https://github.com/zed-industries/zed/pull/61945), an opt-in layered scene for deferred GPUI overlays. The GPUI Kit PR validates the macOS path; Windows composition and Linux hosting remain follow-up work in that PR. - [Zed/GPUI #62379](https://github.com/zed-industries/zed/pull/62379) proposes a separate, broader opt-in `CompositionTree` for ordering GPUI and native surfaces, with macOS and Windows examples. It is an alternative to #61945, **not** the dependency of GPUI Kit #2626. Its Linux composition path is outside that PR's scope. These experiments may change before merge. They explain the intended direction; they do not remove today's platform, overlay, or focus limitations. --- # Auto Update Source: /docs/auto-update An update has two separate jobs: the application presents a useful, responsive experience, and the release system delivers a trustworthy replacement. GPUI Kit gives you the UI, state, and task tools for the first job. **Your application and its distribution system own the update policy, release metadata, verification, installation, and recovery.** GPUI Kit does not select a release or replace an installed app. Start with [Packaging Desktop Apps](/docs/packaging): an updater must match the package format and installation scope you ship. ## Choose who owns installation | Distribution | Recommended update owner | Reason | | --- | --- | --- | | A writable, single-binary portable installation | The app can use a binary updater such as `self_update`, with a controlled restart | There is one executable to replace and no package database to keep in sync. | | macOS `.app` delivered in a DMG | A bundle-aware updater or a new signed and notarized app/DMG | Updating only `Contents/MacOS/` changes the signed bundle. Replace and validate the complete `.app`. | | Windows Inno Setup `.exe` installer | The installer | It owns installed files, shortcuts, permissions, version identity, and uninstall records. A running process may lock its executable. | | Linux DEB/RPM or a managed software repository | The package manager | Replacing its binary behind its back leaves its package database and dependencies inconsistent. | | Portable archive with several files | An installer or an app-specific staged update for the **whole** directory | A new binary alone may not match its sidecar libraries or resources. | Do not silently switch an installation from one owner to another. In particular, a binary self-updater does not replace the Inno Setup installation flow, a macOS bundle update, or a Linux package upgrade. A signed update to a portable binary still needs a writable destination and a platform-specific restart strategy. ## Define the release contract Before building the UI, publish enough information to make one unambiguous decision: 1. **Identity and policy:** stable application/package identity, current version, update channel (`stable` or `beta`, for example), minimum supported version, and whether downgrades are allowed. Compare versions with a defined version scheme; do not compare arbitrary version strings lexically. 2. **Exact target:** OS, CPU architecture, and any relevant ABI or installation type. A macOS Arm artifact must not be offered to an Intel build merely because both are called “macOS.” Match the asset name and the binary path inside its archive to the actual release package. 3. **Release metadata:** version, human-readable notes, artifact URL, size if known, and a digest or signature. Serve metadata and assets over HTTPS and treat redirects and release-host credentials as part of your trust policy. Do not embed a private release token in the distributed executable. 4. **Verification:** check the downloaded bytes against the selected release's expected digest **before** installation, then apply the platform's signature or publisher-authenticity checks. A `SHA256SUMS` file hosted beside the artifact catches corruption and mismatches; by itself it does not prove who published either file. Use a trusted signing key, signed metadata, or the applicable platform signature when publisher authenticity matters. 5. **Atomicity and recovery policy:** stage the artifact outside the live installation, reserve space, verify the full payload, and define what happens if extraction, replacement, relaunch, or the new version's startup fails. Do not claim rollback unless your installer actually retains and restores a known-good version. The check may run at launch or on a reasonable timer, but network errors should leave the current app usable. A manual **Check for updates** action is useful even if checks are automatic. Cache a last-check time, avoid simultaneous checks, and do not install in the background without the app's stated policy. ## A simple path for a portable Rust executable [`self_update` 1.3](https://docs.rs/self_update/1.3.0/self_update/) is an optional application dependency, not a GPUI Kit feature. It can discover a release, download an archive, verify a checksum, and replace a single executable. This example uses a public GitHub release with a `SHA256SUMS` asset. Adapt the repository, binary name, archive naming, and target selection to **your** release contract. In your application's `Cargo.toml`: ```toml [dependencies] self_update = { version = "1.3", default-features = false, features = ["ureq", "rustls", "github", "archive-tar", "compression-tar-gz", "archive-zip", "compression-zip-deflate", "checksums"] } ``` After the user accepts an available update, run the **blocking** installation function on a background worker, never inside `render` or a GPUI click callback: ```rust fn install_portable_update() -> Result, Box> { let status = self_update::backends::github::Update::configure() .repo_owner("example") .repo_name("hello-world-releases") .bin_name("hello_world") .current_version(self_update::cargo_crate_version!()) .checksum_from_asset("SHA256SUMS") .no_confirm(true) // The application UI already obtained consent. .show_output(false) .show_download_progress(false) .build()? .update()?; Ok(status.is_updated().then(|| status.version().to_owned())) } ``` `Some(version)` means the executable was replaced; `None` means it was already up to date. For a **check only**, build the same updater and call `is_update_available()?`; it returns `Some(release)` or `None` without installing. The [crate's current API](https://docs.rs/self_update/1.3.0/self_update/) also provides verification hooks and progress callbacks. A separate check and install can observe different “latest” releases if a release is published between them; production code should pin the selected release/version and revalidate its exact asset before replacement. A successful update changes the installed executable, **not** the code already running in memory. Show **Restart to finish**, save state, exit cleanly, and relaunch with a strategy suitable for that platform and install location. The snippet is limited to a writable, single-binary portable app. If an archive also contains required assets, update that payload as a unit. If the app is inside a signed `.app`, an Inno Setup installation directory, DEB, or RPM, use the installation owner from the table instead. `self_update` does not automatically make an arbitrary application update atomic, provide a rollback policy, or preserve package-manager ownership. ## Keep GPUI responsive throughout Represent each visible phase as application-owned state: `Idle`, `Checking`, `Available`, `Downloading`, `Verifying`, `ReadyToRestart`, and `Failed` are a useful starting set. Keep the selected version and an error message in that state. Use a foreground [Task](/docs/task) to coordinate UI updates and a background worker for blocking download, hashing, archive extraction, or installer preparation. Return owned results to the foreground task, update the owning [Entity](./entity), and call `cx.notify()` so a later frame shows the new state. Keep the task handle alive while the operation should continue; cancel or ignore stale results if a newer request supersedes it. Progress is optional when the server does not provide a trustworthy total size. In that case show an indeterminate status and the current phase. Do not drive an animation or redraw loop while the app is idle; GPUI renders in response to work and invalidation, as explained in [120 FPS and Rendering Models](/docs/fps). If you animate progress, honor reduced-motion preferences, and expose phase, percentage when known, errors, and **Restart** through text or accessible status controls. Make a failed download retryable without implying that the existing installation is damaged. ## Install, restart, and recover 1. **Download and stage:** write to a temporary, application-owned location. Reject an unexpected version, target, archive layout, size, or missing required file. Never execute a partially downloaded payload. 2. **Verify:** validate the artifact digest and the publisher/authenticity policy before replacing anything. On macOS, validate the **complete** signed bundle after staging; on Windows, verify the publisher signature or installer according to your release policy. Recheck the exact artifact after any download redirect or cache substitution. 3. **Install through the owner:** let the installer/package manager apply managed updates. For a portable app, arrange replacement after the running process releases files if that platform requires it. Preserve user data outside the installation directory. 4. **Restart deliberately:** tell the user when the new version is ready, save work, and close or relaunch at a safe point. Report the active version only after the new process starts; a completed download is not an active update. 5. **Recover:** retain a known-good package or make the previous installer available, and test the actual failure paths. Rollback may require a platform installer or a separate helper; it cannot be inferred from a successful download or an updater crate call. For macOS direct distribution, stage and verify a signed/notarized replacement `.app` rather than altering one executable inside the installed bundle. For Windows, test an update while the app is running and under the intended per-user or machine-wide install scope; the helper or installer must handle file locks and elevation rather than assuming the app can overwrite itself. For Linux DEB/RPM, direct the user to the configured package source or invoke the package manager through an appropriate installer flow. See [Packaging Desktop Apps](/docs/packaging) for each platform's artifact and identity requirements. ## Test the release path Test with **downloaded release artifacts** on clean machines or VMs for every supported OS, architecture, channel, and installation format: | Scenario | Expected result | | --- | --- | | Up to date, offline, slow network, timeout, or bad metadata | The app remains usable; the status is truthful and retryable. | | Wrong target, missing asset, truncated file, bad digest, or invalid signature | Nothing is installed; the error identifies the failed phase without exposing credentials. | | New version while work is unsaved | The user can defer restart and keep working under the old running version. | | Concurrent checks, repeated clicks, or a release published during check/install | Only the selected, revalidated version is installed once. | | Read-only destination, Windows file lock, macOS bundle signature failure, or package-manager-owned install | The app follows its platform install path or fails safely without partially replacing files. | | Interrupted install, failed relaunch, and downgrade/rollback | The tested recovery path restores a launchable version and preserves user data. | Finally, install version N through each supported distribution format, update to N+1, verify the version **after restart**, and test ordinary uninstall. A working developer build or a successful update check does not establish that the published installer can upgrade a real installation. --- # Mobile Source: /docs/mobile **Current scope** GPUI Kit's primary target remains the desktop. Mobile support exists so that some components can be reused inside iOS and Android applications, for example rendering rich content natively with TextView inside a native screen. GPUI Kit does not currently plan to make mobile a primary target or to become a full mobile application framework in the way Flutter is. Mobile support builds on [gpui-mobile](https://github.com/itsbalamurali/gpui-mobile), created by [itsbalamurali](https://github.com/itsbalamurali) and developed with the community. Credit for the original mobile platform belongs to that project and its contributors. The platform supplies the [Window](./window), touch input, [text system](./text-system), and GPU surface; GPUI and GPUI Kit still own the Rust view tree and components. GPUI Kit currently uses `gpui-pre-mobile`, a temporary compatibility package maintained in a [compatibility fork](https://github.com/longbridge/gpui-mobile). It adapts the original project for crate packaging and publication alongside `gpui-pre`, and tracks newer GPUI versions to keep the integration compatible. Once the community `gpui-mobile` completes the integration and GPUI is published as a crate, we plan to switch this guide and its dependencies to the community `gpui-mobile`. The current integration is experimental. The Swift-hosted iOS example has been built and exercised in the iOS simulator. Beyond this guide, GPUI Kit has been validated on iOS and Android in a limited scope: an AI chat area built from TextView, Button, Menu, Popover, Scrollbar, Input, Textarea and text selection passed functional and performance testing on both platforms, and the fixes from that work are in GPUI Kit. TextView is covered completely in that scenario. Other components and complete application layouts have not been validated on mobile yet. The fork's Android activity example is a different host path that this guide does not cover. Treat the iOS simulator path below as the documented target, not a general mobile support guarantee. Native UI and GPUI can share one screen on both iOS and Android. The native side keeps the parts users expect to behave like the platform, such as the navigation bar and the bottom input field, and GPUI renders as one view between them. Each side keeps its own layout and input; the host places the GPUI view like any other native view. ## Run the iOS example Start with the compatibility fork’s [Swift container example](https://github.com/longbridge/gpui-mobile/tree/0b882efdac7f524e0bb0b1d4c886b2aa752f9f20/example). It includes a conversation UI with `Message`, `Bubble`, `TextView`, `Input`, thought summaries, and copy actions. Its responses are local sample data; it does not connect to an AI service. On an Apple Silicon Mac, install Xcode with an iOS simulator runtime, Rust, and XcodeGen: ```sh brew install xcodegen rustup target add aarch64-apple-ios-sim git clone https://github.com/longbridge/gpui-mobile.git cd gpui-mobile git checkout 0b882efdac7f524e0bb0b1d4c886b2aa752f9f20 cd example ./build.sh ios --simulator ``` The script builds the Rust static library, generates the Xcode project, and installs and launches the app in a simulator. Add `--no-run` to build only, or `--release` for a release build. At this pinned revision the script builds for `iPhone 16 Pro` with iOS 18.6 by name; if that runtime or device is unavailable, change its Xcode destination to one installed on your Mac. The example targets iOS 16 or later; this is a deployment setting, not a claim that every supported OS version has been tested. Check the simulator used for **installation and launch** as a separate step. In the [pinned build script](https://github.com/longbridge/gpui-mobile/blob/0b882efdac7f524e0bb0b1d4c886b2aa752f9f20/example/build.sh), `build_ios` sets the Xcode destination, but `_ios_run_simulator` later takes the first available iPhone from `xcrun simctl list devices available`. On a Mac with multiple simulators, changing only the build destination can launch a different device. Run `xcrun simctl list devices available`, compare its first listed iPhone with your chosen build destination, and, if they differ, update the script's `sim_id` selection to the intended simulator UUID before rerunning `./build.sh ios --simulator`. For device development, install the `aarch64-apple-ios` Rust target and configure your own development team and signing in `example/ios/project.yml`. Re-generate the project after changing that file. Simulator execution does not establish device performance or release readiness. ## Dependencies `gpui-pre-mobile` is the Cargo package name; the Rust library is `gpui_mobile`. Use a Git dependency while evaluating this integration. The package's `0.1.0` manifest version does not imply a crates.io release. ```toml [lib] crate-type = ["staticlib", "rlib"] [dependencies] gpui-mobile = { package = "gpui-pre-mobile", git = "https://github.com/longbridge/gpui-mobile", rev = "0b882efdac7f524e0bb0b1d4c886b2aa752f9f20" } gpui = { package = "gpui-pre", version = "=0.3.4", default-features = false } gpui-kit = { git = "https://github.com/MohsenDastaran/uni-kit", rev = "7d9efcd2069f9eaa6eb3ba6345aac4aa7d87c9f7", default-features = false, features = ["component"] } ``` These revisions reproduce the example's dependency baseline. The Kit revision includes mobile platform gating but predates mobile tooltip suppression. The current GPUI Kit checkout uses `gpui-pre {{gpui_pre_version}}`, while this pinned mobile platform and renderer use `0.3.4`. Cargo can select both versions, producing incompatible GPUI types; replacing the Kit dependency with a local path is **not** a working upgrade by itself. First update the mobile platform and renderer to the same GPUI version as Kit and validate that combination. Only then can you use a path dependency such as: ```toml gpui-kit = { path = "../gpui-kit/crates/kit", default-features = false, features = ["component"] } ``` Adjust the path relative to your application's manifest. Keep the GPUI core, renderer, platform, and Kit on one compatible release. Unlike the desktop [Getting Started](/docs/getting-started) setup, mobile does not use `gpui_kit::application()` or `gpui_kit::platform`. Those desktop platform exports are excluded on iOS and Android. The mobile host initializes GPUI, calls `gpui_kit::init(cx)`, and mounts a single `component::Root` around the application's content. ## Embed a view in UIKit UIKit owns the native window, navigation, safe areas, and keyboard layout. The example's `GPUITextView` is a Swift `UIView` wrapper around the GPUI platform's child `UIViewController`. Despite its name, it hosts a whole Rust conversation view, not just one `TextView` element. Use these files together as the integration reference: | File | Responsibility | | --- | --- | | [App.swift](https://github.com/longbridge/gpui-mobile/blob/0b882efdac7f524e0bb0b1d4c886b2aa752f9f20/example/ios/App.swift) | Native window, view wrapper, child controller containment, layout, and frame scheduling | | [Embedding.h](https://github.com/longbridge/gpui-mobile/blob/0b882efdac7f524e0bb0b1d4c886b2aa752f9f20/example/ios/Embedding.h) | Swift bridging declarations for Rust callbacks | | [src/lib.rs](https://github.com/longbridge/gpui-mobile/blob/0b882efdac7f524e0bb0b1d4c886b2aa752f9f20/example/src/lib.rs) | Application callback, Kit initialization, and Rust root view | | [project.yml](https://github.com/longbridge/gpui-mobile/blob/0b882efdac7f524e0bb0b1d4c886b2aa752f9f20/example/ios/project.yml) | Rust build phase, static library linkage, frameworks, and bridging header | The startup sequence is: 1. Call `gpui_ios_set_embedded()` before creating the GPUI application so the platform does not create a second native window. 2. Call the example-defined `gpui_ios_register_app()`. It registers a Rust callback with `gpui_mobile::ios::ffi::set_app_callback` that initializes Kit and opens the GPUI root. 3. Call `gpui_ios_run_demo()` to start the embedded application, then obtain its window and child controller with `gpui_ios_get_window()` and `gpui_ios_view_controller()`. 4. Attach the controller using UIKit containment: `addChild`, add its view, then `didMove(toParent:)`. `gpui_ios_register_app()` belongs to the example, not the platform library. Adapt its callback to construct your own Rust view. The `run_demo` name is the current bridge entry point; it runs the registered application callback. Once the example's `GPUITextView` wrapper is included in your app, a native controller can constrain it like any other view: ```swift let content = GPUITextView(frame: .zero) content.translatesAutoresizingMaskIntoConstraints = false view.addSubview(content) content.attach(to: self) NSLayoutConstraint.activate([ content.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), content.leadingAnchor.constraint(equalTo: view.leadingAnchor), content.trailingAnchor.constraint(equalTo: view.trailingAnchor), content.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor), ]) ``` This fragment uses the example wrapper; `GPUITextView` is not an SDK-provided UIKit class. Copy its containment and layout behavior along with the declarations and build settings, rather than copying only the constraints. ### Lifetime, resizing, and frames The platform retains the `ApplicationHandle` returned by `Application::run_embedded`. It must outlive callbacks and rendered views. The current bridge supports one GPUI view for the application's lifetime; it does not provide independently destroyable views, multiple instances, or reusable collection-view cells. In `layoutSubviews`, update the child controller's frame only when nonzero bounds change, call `gpui_ios_layout_view`, then request a frame. The example performs those operations inside a Core Animation transaction with implicit animations disabled, keeping the Metal surface and GPUI viewport in sync during layout changes. The host drives `gpui_ios_request_frame` through a `CADisplayLink` while visible and invalidates the link when the controller disappears. Forward application active/inactive callbacks as shown in `App.swift`. Keep UIKit and bridge calls on the main thread. ### Input, fonts, and assets UIKit determines the embedded view's safe area and keyboard avoidance. The GPUI platform translates native touch and text input for the Rust window, while Kit owns the component interaction inside it. Exercise focus, selection, editing, paste, scrolling, and on-screen keyboard changes in the simulator; a successful build does not establish that every input method works. `window.visual_viewport_bounds()` can change as the keyboard appears, while the layout viewport remains the same. Do not manually shrink both the UIKit container and the Rust content for one keyboard transition. The example's embedded callback initializes Kit and changes the theme but does not install application fonts or an icon `AssetSource`. A font family configured for desktop may be absent on iOS, and glyph coverage for CJK or emoji can differ. For fonts your app requires, bundle the font files, register them with `cx.text_system().add_fonts(...)` before opening the GPUI window, then select those families in the theme. Check the actual rendered text and missing glyphs on each target; see [Fonts](/docs/fonts) and [TextSystem](/docs/text-system). The dependency sample enables only Kit's `component` feature. If your app uses named Kit icons, enable its `assets` feature and register a suitable `AssetSource` on the mobile GPUI application before the first window. Native embedded assets and files addressed by runtime paths have different deployment needs: bundle and locate external image files in the app package, or provide an HTTP client for remote images. Do not assume a desktop file path exists in the iOS sandbox. See [Icons & Assets](/docs/assets). ## Platform-specific behavior `gpui_kit::is_mobile()` is an inline `const fn` that returns `true` for iOS and Android targets. It checks the compilation target, not window width or whether a mouse is connected. ```rust if gpui_kit::is_mobile() { // Use touch-friendly interaction. } ``` ## Design for mobile Share component behavior and content with desktop, while adapting the screen to touch and a narrow viewport: - Let the native container handle navigation, safe areas, and keyboard avoidance. Avoid stacking a second title bar or duplicating safe-area padding inside Rust. - Give each conversation one vertical scroll owner. For a `TextView` within that scroller, use `.w_full().min_w_0().scrollable(false)` so text and images fit the available width. - Keep the composer compact when empty. Use a single-line input when multiline composition is unnecessary, and ensure the keyboard does not cover the send action. - HoverCard opens and closes by tapping its trigger on iOS and Android. Tap outside to dismiss it; moving a finger does not open the card. - A long press or double tap in an `Input` or `Textarea` selects a word with touch handles and an edit menu. For selectable, read-only `TextView` content, use a long press; its touch double tap intentionally does not select a word. These are Kit interaction paths that still need validation through the mobile platform's event translation. - Make actions discoverable by touch. Keep copy actions aligned with the reply and use a brief checkmark after copying. Do not rely on hover text to explain an action. - Prefer short paragraphs and purposeful headings. Let code, tables, and images support the conversation rather than presenting every Markdown format in each reply. - Use Kit theme colors, type sizes, and spacing consistently. Check long replies, wide code, image loading, and Chinese or other scripts at the actual device width. GPUI Base disables its tooltip overlay on iOS and Android. This covers Kit tooltips routed through that overlay, not direct GPUI `.tooltip()` calls. The pinned baseline above predates that change. Do not add native GPUI hover tooltips to mobile views. ## Validation and current limits For an application integration, check launch and return from the background, keyboard show/hide, viewport resizing, text selection and copying, scroll behavior, and touch feedback. Inspect the actual rendered screen rather than relying only on a Rust compile check. Measure rendering on a physical device with a release build and Xcode Instruments before making performance claims. Simulator results are useful for layout and interaction, but are not device frame-time measurements. Android uses a separate activity and surface lifecycle. The repository contains an Android example. GPUI can be embedded as an Android `View` in a native layout, as described above, but this guide documents only the iOS steps. Outside the chat scenario validated above, Android Kit compatibility is not established. Validate the Android host path separately before depending on it. ## Troubleshooting | Symptom | Check | | --- | --- | | Xcode cannot find the simulator destination, or the app launches on another simulator | The pinned `build.sh` builds for iOS 18.6 on an iPhone 16 Pro. Install that runtime or edit its Xcode destination to match `xcodebuild -showdestinations`. Then run `xcrun simctl list devices available`: the script's `_ios_run_simulator` installs on the first available iPhone, independently of the build destination. If that is a different device, change its `sim_id` selection to the intended UUID. | | Device build fails signing or install | Replace the example development team in `example/ios/project.yml`, regenerate the Xcode project, and confirm the device appears in Xcode. The default script target is a physical device; pass `--simulator` explicitly for the documented simulator path. | | Rust reports two versions of GPUI or mismatched `App`/`Window` types | Check the resolved `gpui-pre` packages. The pinned mobile fork uses `0.3.4`; this checkout uses `{{gpui_pre_version}}`. Align the entire mobile platform and Kit dependency set before using the local Kit path. | | App launches with a blank or stale GPUI view | Check that the Rust callback opens a window, the child controller is attached, `layoutSubviews` publishes nonzero bounds, and visible frames are requested. Inspect the Xcode console; the example sends Rust logs and panics to `NSLog`. | | Text, icons, or images are missing | Verify the font family and glyph coverage, registered `AssetSource` and exact icon keys, or the image's packaged path and HTTP client. A desktop asset or font configuration does not automatically carry into the mobile host. | --- # Context Source: /docs/context GPUI callbacks often receive `window: &mut Window, cx: &mut Context`. GPUI supplies these parameters for the duration of the call, giving code access to the current window, current Entity, and whole application while keeping mutable access within that call. Start by separating the scopes: | Type | Scope | Common capabilities | | --- | --- | --- | | `Window` | Current system window | Focus, input, window bounds, drawing, Action dispatch | | `Context` | The `Entity` currently being updated | The Entity for `self`, `notify`, subscriptions, Entity tasks | | `App` | The whole application | Globals, creating Entities, opening windows, application Actions and tasks | | `AsyncApp` | A handle for foreground work across `await` | Re-enter `App` or an Entity in a short update | | `AsyncWindowContext` | An async handle for one window | Re-enter an Entity together with its Window | Read the table as a progression: `App` is available first in `application().run`; opening a window supplies `Window`; creating an Entity supplies its `Context` whenever GPUI builds or updates that Entity. A foreground task receives an async handle instead of borrowing either synchronous context across an `await`. | Where code runs | Parameters GPUI supplies | Use it for | | --- | --- | --- | | `application().run`, application callbacks | `&mut App` | Initialize the kit, set Globals, create Entities, open windows | | Window builder, element or component callback | `&mut Window`, `&mut App` | Work with that window and application state; use `cx.listener` when the callback needs its owning View | | `Render` or an Entity update | `&mut Self`, `&mut Context`; `Render` and `update_in` also receive `&mut Window` | Read or change the current View; call `notify` when its rendered state changes | | `App::spawn` / `Context::spawn` task | `&mut AsyncApp`, plus a `WeakEntity` for Entity tasks | Resume short application or Entity operations after `await` | | `Context::spawn_in` task | `&mut AsyncWindowContext` and `WeakEntity` | Resume an operation that also needs the original Window | `Context` dereferences to `App`, so code with `cx: &mut Context` can already call App APIs and does not need a separate `&mut App`. It also knows which Entity is current; plain `App` does not. Window remains separate because the same Entity may appear in different windows, while a data-only update may not belong to any window. Window also owns per-window state keyed by [ElementId](./element_id). Async contexts are handles, not long-lived `&mut App` or `&mut Window` borrows. The [GPUI `Context` source](https://docs.rs/crate/gpui-pre/{{gpui_pre_version}}/source/src/app/context.rs) defines the entity-specific methods used below. GPUI Kit applications depend on `gpui-kit` and import GPUI through `use gpui_kit::*;`. Call `gpui_kit::init(cx)` before creating component-backed Views. An application-wide [Global](./global) lives on `App`; a component or feature View keeps retained state in an [Entity]. ## Open a URL in the default browser Use `cx.open_url(...)` to hand a URL to the platform's default browser. It is an `App` API, so it also works when `cx` is a `Context` or the `&mut App` supplied to a button callback: ```rust use gpui_kit::component::button::Button; Button::new("open-docs") .label("Open docs") .on_click(|_, _, cx| cx.open_url("https://gpui-kit.com/docs")) ``` This opens an external browser; use [WebView](./webview) when browser content must live inside a GPUI window. `open_url` returns no completion result. GPUI Kit's `Link` with an `href` also calls `cx.open_url(...)` when clicked. ## `window, cx` or only `cx` View state belongs to its Entity, while window interaction belongs to Window. A method receives both when it changes View state and operates on the window displaying that View. GPUI style places runtime parameters last, in `window, cx` order: ```rust fn focus_input(&mut self, window: &mut Window, cx: &mut Context) { self.composer_open = true; self.input_focus.focus(window, cx); cx.notify(); } ``` An Action, Event, or pointer callback may put `action`, `event`, or similar arguments first, while keeping runtime parameters last: ```rust fn on_action_send_message( &mut self, action: &SendMessage, window: &mut Window, cx: &mut Context, ) { // ... } ``` If a method only changes data and does not read Focus, input, window bounds, or other window state, keep only the final `cx` argument: ```rust fn clear_messages(&mut self, cx: &mut Context) { self.messages.clear(); cx.notify(); } ``` When there is no current Entity and the work is application-wide, a callback receives `&mut App` directly. Application initialization, registering global state, and opening the first window are common examples. Do not add an unused Window for signature consistency; parameters should expose the scope the logic actually needs. Async code uses the corresponding `AsyncApp` or `AsyncWindowContext` to re-enter GPUI after an `await`. See [Window](./window) for Window-specific capabilities. ## A callback that needs its View A GPUI Kit `Button` click handler receives `(&ClickEvent, &mut Window, &mut App)`. It does not receive the View as `&mut Self`. Build the callback with `cx.listener` while [rendering](./render) a View; GPUI will update that View and pass its `Context` to the inner closure. The component also supplies keyboard and [accessibility](./accessibility) behavior: To try the complete example, replace `examples/hello_world/src/main.rs` in this repository's existing `hello_world` package with the following code. From the repository root, run `cargo run -p hello_world --bin hello_world`. ```rust use gpui_kit::*; use gpui_kit::assets::Assets; use gpui_kit::component::button::Button; struct Counter { count: usize, } impl Render for Counter { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { div().child( Button::new("increment") .label(format!("Count: {}", self.count)) .on_click(cx.listener(|this, _event, _window, cx| { this.count += 1; cx.notify(); })), ) } } fn main() { application() .with_assets(Assets) .run(|cx| { init(cx); open_window(WindowOptions::default(), cx, |_, cx| { cx.new(|_| Counter { count: 0 }) }) .expect("failed to open window"); }); } ``` The window first shows `Count: 0`. Click the button once and its label should become `Count: 1`; each further click increments it by one. This checks that the listener mutates the existing `Counter` Entity and that `cx.notify()` makes the new value visible. Restore your original `main.rs` after the exercise if you want to keep the package's previous example. The outer callback type belongs to the button. The inner closure receives `&mut Counter` and `&mut Context`. `cx.listener` uses a weak handle to the View, so a stored handler does not keep a closed View alive. The count is Entity state, and `cx.notify()` tells GPUI to render its new value. Do not create the View again inside `render`; the owning `Entity` is created when the window opens. ## Create, read, and update `cx.new` creates an [Entity]. Its closure receives the new Entity's own `Context`. A strong `Entity` handle keeps it alive. Use the handle with `read` for a synchronous borrowed view of its data, or `update` to receive `&mut T` and that Entity's own context: ```rs struct Draft { text: String, } fn edit_draft(cx: &mut App) -> Entity { let draft = cx.new(|_| Draft { text: String::new() }); let was_empty = draft.read(cx).text.is_empty(); if was_empty { draft.update(cx, |draft, cx| { draft.text.push_str("Hello"); cx.notify(); }); } draft } ``` The `read` reference cannot outlive the `App` borrow. Complete the read before requesting mutable access with `update`; copy or clone the small amount of data you need later. A strong handle's `update` returns the closure's value directly. A `WeakEntity` can disappear, so its `update` and `update_in` return `Result` instead. Always use the inner `cx` passed to the update closure when notifying or using Entity-specific methods. `cx.new` can be called through `App` or `Context` because both implement GPUI's `AppContext` API. From a `Context`, the construction closure receives a **new** `Context`. It is not the parent's context. Keep the returned strong `Entity` in its owner when the child should survive across renders. `cx.notify()` reports that the current Entity changed. It schedules dependent views and `observe` callbacks; changing a field alone does not. `notify` is unnecessary for a read. Avoid calling it unconditionally from `render`, which can schedule repeated renders. When one action changes several related fields, finish the change and notify once. Do not retain a reference from `read` across an `await`; clone the small piece of data the task needs first. Do not store `&mut App`, `&mut Window`, or `&mut Context` in a View or task. Those are short-lived access granted for the current GPUI call. Do not read or update an Entity again while it is already inside its `render` or `update`. GPUI prevents re-entrant access and will panic. Use the `self` and inner `cx` already provided. The same applies inside `cx.listener`: its `this` argument is already the View. If code must update that Entity *after* the current callback, defer the work instead of trying to access it through its handle immediately. Use `cx.entity()` when another object needs a strong handle to the current Entity. Prefer `cx.weak_entity()` or `downgrade()` in long-lived callbacks that should not keep a View alive. ## Defer until the current update ends `App::defer` runs an app-level closure after the current effect cycle. `window.defer(cx, ...)` supplies that Window later. From an Entity, `cx.defer_in(window, ...)` supplies the same View, its Window, and `Context` after the current update has released its borrow: ```rust fn finish_edit(&mut self, window: &mut Window, cx: &mut Context) { cx.defer_in(window, |this, window, cx| { this.finish_edit_after_update(window, cx); }); } ``` The deferred closure still receives `this: &mut Self`; do not call `update` on that same Entity from inside it. Deferral is for work that needs the current update to finish, such as focus restoration after changing the UI tree. [`window.on_next_frame(...)`](https://docs.rs/crate/gpui-pre/{{gpui_pre_version}}/source/src/window.rs) instead queues a callback for the next platform frame request and wakes the frame source. The callback runs before any drawing for that request; registering it does not mark the window dirty or cause a render. If its work changes visible Entity state, call `cx.notify()` from the callback. Use `window.request_animation_frame()` when the intent is to request a redraw on the next frame. A closed window or released View may prevent deferred work from running, so do not use it as a durable job queue. ## Async work `cx.spawn` starts a foreground task. From `Context`, it supplies the current Entity as a `WeakEntity` and an `AsyncApp`: ```rs self._load_task = cx.spawn(async move |this, cx| { let messages = fetch_messages().await?; this.update(cx, |chat, cx| { chat.messages = messages; cx.notify(); })?; anyhow::Ok(()) }); ``` The weak handle does not keep the View alive. Its `update` returns an error if the View was released, so handle or propagate that result. When `spawn` starts from `App`, there is no current Entity handle. The closure receives only `AsyncApp`; use `cx.update(|cx| ...)` to run a short application-level mutation after an `await`. `AsyncApp` also offers closure-based access to globals, such as `read_global` and `update_global`. It does not give an async task a `&mut App` to hold across awaits. Use `spawn_in` when completion also needs the same Window. Its `AsyncWindowContext` lets `update_in` restore both Window and Entity access: ```rs cx.spawn_in(window, async move |this, cx| { let message = send_to_server().await?; this.update_in(cx, |chat, window, cx| { chat.messages.push(message); chat.input_focus.focus(window, cx); cx.notify(); })?; anyhow::Ok(()) }) .detach(); ``` Use this `spawn_in` → `update_in` pattern for async work that updates a View and its Window. Use `background_spawn` for CPU-heavy work; it cannot update GPUI state directly, so bring its result back to a foreground task first. The `WeakEntity` may have been released, and the Window may have closed while the task waited. `update` and `update_in` therefore return a `Result`; handle it. If a request can finish after a newer request, compare an ID or revision before applying its result. ## Task lifetime A GPUI [Task](./task) is cancelled when its handle is dropped: ```rs struct Chat { _load_task: Task>, } ``` - Store View-owned work on the View, so releasing the View cancels it. - Call `.detach()` only when work should continue independently. - Replacing a stored refresh or debounce task cancels the previous one. ## Observe and subscribe `observe` reacts when another Entity calls `cx.notify()`. `subscribe` reacts to a typed [Event]: ```rs struct Chat { input: Entity, _subscriptions: Vec, } // In Chat::new: let _subscriptions = vec![ cx.observe(&input, |_, _, cx| cx.notify()), cx.subscribe(&input, |chat, input, event: &InputEvent, cx| { if matches!(event, InputEvent::Change) { chat.draft = input.read(cx).value().to_string(); cx.notify(); } }), ]; ``` Both return a `Subscription`. Store it on the subscribing View so it remains active for exactly that View's lifetime. Dropping it cancels the callback immediately. Keeping it in a longer-lived owner may retain its callback and captured resources after the View disappears, causing a memory leak. Use `observe_in` or `subscribe_in` when the callback also needs `&mut Window`. To publish an application event, implement `EventEmitter` on the emitting Entity type, then call `cx.emit(event)` from its `Context`: ```rust struct SaveRequested; struct Draft; impl EventEmitter for Draft {} impl Draft { fn request_save(&mut self, cx: &mut Context) { cx.emit(SaveRequested); } } ``` A parent can use `cx.subscribe(&draft, ...)` to handle `SaveRequested`; the event type is part of that subscription, so an Entity can emit more than one type. An observer is tied to `cx.notify()`, while a subscription is tied to `cx.emit(...)`. Emitting an event does **not** notify observers or redraw the emitter: call `cx.notify()` as well if its rendered state changed. Conversely, `notify` does not emit an event. For an application-wide value, use `observe_global` and keep its `Subscription`; see [Global]. ## Common mistakes - A task stops early because its `Task` was dropped. Store it or intentionally detach it. - An observer never runs because its `Subscription` was only a local variable. - A View stays alive because a task or callback captured a strong Entity handle. Capture a weak handle. - GPUI reports an Entity is already borrowed because code re-entered the same Entity during `render` or `update`. - A callback receives plain `App` but needs its View; create it with `cx.listener` instead of trying to find and re-enter the View manually. - A later operation needs the current Entity after a UI-tree change; use `cx.defer_in` rather than updating the borrowed Entity immediately. - Async code cannot access Window because it used `spawn`; use `spawn_in` and `update_in`. - A borrow is held across `await`; extract owned data first, then reacquire access with `update` or `update_in`. - The UI stays stale because state changed without `cx.notify()`. GPUI convention names every context parameter `cx`, regardless of its concrete type, and names the Window parameter `window`. [Entity]: ./entity.md [Event]: ./event.md [Global]: ./global.md --- # TextSystem Source: /docs/text-system This page follows the `gpui-pre` {{gpui_pre_version}} API pinned by this repository. `gpui-pre` is the published snapshot and version-alignment mechanism for GPUI, not a separate text engine. Start with a text element; use `TextSystem` directly when your element needs the glyph geometry that GPUI normally manages. GPUI's `TextSystem` resolves [fonts](./fonts) and supplies font metrics. Each [Window](./window) has a `WindowTextSystem` that adds a line-layout cache to the shared text system. Ordinary text elements and GPUI Kit controls use these services for you. Reach for `window.text_system()` when writing custom text geometry, a chart label, an editor, or another element that must use shaped glyph positions directly. Text rendering is a pipeline: 1. Resolve a `Font` to a `FontId`, including fallback when the requested family is unavailable. 2. Shape UTF-8 text and styled `TextRun`s into positioned glyph runs and line metrics. 3. Wrap and measure lines against available width where needed. 4. Use the layout for hit testing and paint the shaped glyphs in the window. Shaping is necessary because the width of a string is not generally the sum of independent character widths. Script shaping, ligatures, kerning, font fallback, and emoji can change glyph count, positions, and advances. Measure the **actual shaped text** for a custom label rather than multiplying a character count by an average width. ## Follow a line from text to pixels Use the existing `hello_world` example for a short experiment. Replace `examples/hello_world/src/main.rs` with the code below, then run `cargo run -p hello_world` from the repository root. It uses the normal text element for the visible label and asks the same window text system for that line's shaped advance. The measurement is displayed so you can change the sample and see the result; in an application, keep expensive intrinsic measurements in layout instead of recomputing them merely to print a number in `render`. ```rust use gpui_kit::*; struct TextLab; impl Render for TextLab { fn render(&mut self, window: &mut Window, _: &mut Context) -> impl IntoElement { let label: SharedString = "Office fi café 👋".into(); let font_size = px(18.); let run = TextRun { len: label.len(), // UTF-8 bytes, not character count. font: window.text_style().font(), color: window.text_style().color, ..Default::default() }; let line = window .text_system() .shape_line(label.clone(), font_size, &[run], None); div() .flex() .flex_col() .gap_2() .p_4() .text_size(font_size) .child(label) .child(format!("Shaped advance: {:.1} logical px", line.width().as_f32())) } } fn main() { application().with_assets(assets::Assets).run(|cx| { init(cx); open_window(WindowOptions::default(), cx, |_, cx| cx.new(|_| TextLab)) .expect("Failed to open window"); }); } ``` The window should show the sample on one line and a second line beginning `Shaped advance:`. The number is a **logical pixel width of the shaped first line**, not a frame time or the width of the enclosing `div`; its exact value depends on the resolved font and platform. Change the sample to `"iiii"`, then `"WWWW"`, and rerun: the advances should differ. Change it to `"café 👋"` and observe that `label.len()` counts more bytes than displayed characters. Do not add `\n` to this single-line call; the multi-line API is explained below. ### Paint the shaped line yourself The first exercise measures text in `render` so the result can be printed. To see the low-level drawing path, replace the same `examples/hello_world/src/main.rs` file with this complete program and run `cargo run -p hello_world` again. `canvas` supplies a prepaint callback and a paint callback without requiring a new `Element` implementation: ```rust use gpui_kit::*; struct CanvasTextLab; impl Render for CanvasTextLab { fn render(&mut self, window: &mut Window, _: &mut Context) -> impl IntoElement { let sample: SharedString = "Office fi café 👋".into(); let canvas_text = sample.clone(); let font = window.text_style().font(); let color = window.text_style().color; let font_size = px(18.); let line_height = px(24.); div() .flex() .flex_col() .gap_2() .p_4() .text_size(font_size) .child("Normal text element:") .child(sample) .child("Shaped and painted in a canvas:") .child( canvas( move |_, window, _| { let run = TextRun { len: canvas_text.len(), font, color, ..Default::default() }; window .text_system() .shape_line(canvas_text, font_size, &[run], None) }, move |bounds, line, window, cx| { let advance = line.width(); line.paint(bounds.origin, line_height, TextAlign::Left, None, window, cx) .expect("Failed to paint shaped text"); let marker = Bounds { origin: point(bounds.origin.x + advance, bounds.origin.y), size: size(px(1.), line_height), }; window.paint_quad(fill(marker, color)); }, ) .w(px(320.)) .h(line_height), ) } } fn main() { application().with_assets(assets::Assets).run(|cx| { init(cx); open_window(WindowOptions::default(), cx, |_, cx| cx.new(|_| CanvasTextLab)) .expect("Failed to open window"); }); } ``` The canvas draws the same sample beneath the ordinary text element. Its thin vertical marker sits at the line's **advance**, which is the pen position after shaping, not necessarily the last glyph's visible edge. `canvas` requests the fixed `320 × 24` area during layout. Once bounds are known, its prepaint callback shapes the line and returns a `ShapedLine`; its paint callback uses that same value for both `width()` and `paint(...)`. Change the sample in the code, rerun, and watch the marker move. A very long sample can run past the fixed canvas width; this exercise does not wrap or clip it. If the ordinary line appears but the canvas line does not, check the canvas height, the `TextRun::len` byte count, and the result of `line.paint`. The canvas line has no text-node accessibility identity, selection, hitbox, or keyboard behavior. Use the ordinary text element for interface copy. When text width must determine the custom element's **requested size**, shape in a measured-layout callback instead of this fixed-size canvas; [Element](./element) explains that layout boundary. This is the progression to use in a real view: ordinary label → only if needed, shaped width for intrinsic layout → only if needed, reuse shaped glyphs in a custom element's paint phase. The [Element](./element) tutorial supplies the full lifecycle for an element with its own layout or children. You do not need direct `TextSystem` calls for a label, button, or ordinary paragraph. ## Start with a text element For interface copy, use a normal text child and let GPUI own layout and painting: ```rust use gpui_kit::*; div() .font_family(".SystemUIFont") .text_size(px(14.)) .child(text!("Recent activity")) ``` `text!(...)` creates a `Text` element with an ID based on this macro call's source location. That ID lets GPUI expose a label to the accessibility tree and report content changes under a stable identity. `text!(id = "activity-label", value)` sets an explicit ID when the same call site creates multiple labels; each simultaneously rendered element needs a distinct ID. A plain `.child("Recent activity")` also lays out and paints text, but has no text-node ID. Neither form returns a measured width from `render`: GPUI shapes it during the element's measured-layout callback, places its bounds in prepaint, then paints it. GPUI Kit's [TextView](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/text/compat.rs) renders Markdown or HTML with styled runs, selection, and optional scrolling; it delegates parsing, layout, and selection behavior to Base. Use `TextView::markdown("article", source)` for a rich document. [Input](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/src/input/base/element.rs) shapes visible lines in [`prepaint`](./element#prepaint), where the resolved width and input geometry are known; its caret and pointer mapping use the same shaped layout. A plain label needs neither implementation. ## Font resolution and metrics `Font` names a family and carries weight, style, features, and optional fallbacks. `font("Family")` constructs one; `Font::default()` requests `.SystemUIFont`. `cx.text_system().resolve_font(&font)` returns a `FontId`, trying GPUI's fallback stack if the requested family cannot load. If no fallback resolves, it panics. `all_font_names()` lists available families, including fonts added by `add_fonts(...)`. Add bundled fonts before the first frame so the first layout uses the intended metrics; adding fonts later invalidates cached font resolution and line layouts, while a layout already underway may finish against the earlier set. GPUI exposes `ascent`, `descent`, `cap_height`, `x_height`, `bounding_box`, `advance`, and `typographic_bounds` for a resolved font and `Pixels` size. These answer different questions. `advance(font_id, size, ch)` returns pen movement for one character's glyph in that font; `typographic_bounds(font_id, size, ch)` describes that glyph's typographic rectangle; ascent and descent establish vertical metrics. Neither API shapes a string or applies its font fallback. `WindowTextSystem::layout_width(font_id, size, ch)` shapes one character; use it for a specific cell or space measurement, not for a sentence. The displayed result depends on installed families and platform font fallback. Desktop GPUI resolves against the operating system's font collection; a requested family may resolve to a different available family. A web build cannot assume desktop system families are exposed and must register the fonts it requires. Test representative Latin, CJK, emoji, and mixed-script strings on target platforms when line breaks or alignment are important. ## Shape one line `TextRun::len` is a **UTF-8 byte length**, and all runs together should cover the text they style, including newline bytes for `shape_text`. Split runs only at valid UTF-8 boundaries. A run selects the font, color, background, underline, and strikethrough for its byte range. The text argument is a [SharedString](./shared-string). This example follows [Plot label](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/plot/label.rs): ```rust use gpui_kit::*; fn shape_label(text: SharedString, color: Hsla, window: &mut Window) -> ShapedLine { let run = TextRun { len: text.len(), font: window.text_style().font(), color, background_color: None, underline: None, strikethrough: None, }; window.text_system().shape_line(text, px(14.), &[run], None) } // Inside an existing render, measured-layout, or paint method with `window`: let width = shape_label("Recent activity".into(), color, window).width(); ``` The helper is a copyable pattern inside an existing GPUI view or element, where `window` and `color` are already available; it is not a standalone `main`. The [canvas exercise above](#paint-the-shaped-line-yourself) gives a complete application that shapes and paints a line. For a production example, [`crates/component/src/plot/label.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/plot/label.rs) uses this shape path in `measure_text_width` and calls `ShapedLine::paint(origin, line_height, align, align_width, window, cx)` in `PlotLabel::paint`. `shape_line(text, font_size, runs, force_width)` returns a `ShapedLine`: its `width()` is the shaped advance, and it carries the original text, positioned glyphs, font IDs, ascent, descent, and decoration runs. Pass `None` for `force_width` unless a custom layout intentionally supplies a width. `shape_line` is for **one** line; do not pass text with `\n`. `layout_line(&str, size, runs, force_width)` returns an `Arc` when geometry is enough, but `shape_line` is the direct choice if it will be painted. An advance is the distance to move the text pen after the line; it is not necessarily the rectangle of colored pixels. A glyph can overhang its advance, and `font_size` alone does not determine a row's height. Use the line's ascent/descent and your chosen line height for vertical placement. If you need a box for clipping or hit testing, derive it from the actual placed line and the interaction area, not by treating `width()` as an ink bounding box. `LineLayout::x_for_index(byte_index)`, `index_for_x(x)`, and `closest_index_for_x(x)` support cursor and hit-test calculations. Indices refer to UTF-8 bytes in the original text, not Unicode scalar values or visual columns. `x_for_index` yields a line-local x position; `index_for_x` returns `None` at or beyond the line width, while `closest_index_for_x` chooses an insertion boundary. To draw a caret, add the painted line origin to its local x coordinate. To handle a pointer, subtract that same origin before querying the layout. Their positions use GPUI's [logical pixel geometry](./geometry). Keep selection boundaries on valid text boundaries; a glyph or ligature need not correspond to exactly one character. Use the same shape, font size, line height, and alignment for measurement, caret placement, and painting so they agree. ## Wrap multiple lines Use `shape_text(text, font_size, runs, wrap_width, line_clamp)` for newlines and optional soft wrapping. It returns `Result>`. `wrap_width: Some(width)` sets the available width. Each `WrappedLine` represents one source line between explicit `\n` characters; it can contain several visual rows. `WrappedLine::size(line_height)` includes those rows, and `position_for_index(index, line_height)` or `closest_index_for_position(local_point, line_height)` maps between local geometry and a byte offset **within that source line**. Add the source line's byte start (and the skipped newline byte) to get an offset in the full text. `line_clamp` limits soft-wrap boundaries, but `shape_text` still returns a `WrappedLine` for every explicit newline-separated source line; it does not guarantee at most N lines in total. A width or font change can alter wrap boundaries, so recompute layout when either changes. For ordinary paragraphs, let a GPUI text element or GPUI Kit `TextView` perform this work. A custom element should only shape text directly when it needs glyph-aware placement, drawing, or hit testing that existing elements cannot supply. ## Match GPUI's rendering phases GPUI's [rendering pipeline](./render) separates layout, prepaint, and paint. Place custom text work in the matching phase: | Phase | Text work | Why | | --- | --- | --- | | `request_layout` | Declare style and layout nodes; use a measured-layout callback if intrinsic text size or wrapping determines the requested size. | The callback receives width constraints; final `Bounds` are not available yet. | | `prepaint` | Use resolved bounds to place prepared lines, calculate cursor geometry, and establish hitboxes; shape here if this custom element only now knows its content width. | Input geometry must match this frame. | | `paint` | Paint the prepared `ShapedLine`s or `WrappedLine`s at their origins. | Reusing the shape prepared during measurement or prepaint keeps pixels and hit tests aligned. | The ordinary GPUI text element shapes in its measured-layout callback, records bounds in prepaint, and paints the prepared `WrappedLine`s. The [Input element](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/src/input/base/element.rs) instead shapes visible editor lines in prepaint because it uses its resolved viewport and scroll geometry. `ShapedLine::paint` submits one line; `paint_background(...)` is a separate pass for run backgrounds. Handle both `Result`s. `WrappedLine::paint` takes optional `Bounds` as its alignment width, whereas `ShapedLine::paint` takes `Option`. Text painting does not itself establish a hitbox, keyboard focus, or accessibility name; a custom interactive text element must supply those contracts too. See [Paint](./paint) for custom drawing. For a chart label, ask whether its width changes the chart's requested size. If it does, shape it in the measured-layout callback and carry that result forward; prepaint positions it from the resolved bounds, then paint draws that same result. If the chart already has a fixed rectangle and only the label's location depends on it, shape and position in prepaint. In either case, the pointer mapping and paint origin must use the same prepared line. The `render` measurement in the small exercise above is only an observable probe, not a replacement for these element phases. ## Diagnose a text mismatch | Symptom | First check | Next action | | --- | --- | --- | | First layout panics while resolving a font | Inspect `cx.text_system().all_font_names()` after initialization. On Web, check that required font bytes were registered before opening the window. | Use an available family or register the intended font; see [Fonts](./fonts#diagnose-a-font-problem). | | Text appears in another face, or widths differ by machine | Compare the requested family with the available names and the family embedded in a bundled file. A family name does not prove glyph coverage. | Test on each target platform; bundle the needed faces and check fallback for Latin, CJK, and emoji. | | Measured width does not match the painted label | Check the resolved font, features, size, text, and `force_width` on both paths. A parent `.text_size(...)` does not silently change a `shape_line` call's explicit size. | Shape with the same inputs and reuse the result for placement and painting. | | Caret or click lands at the wrong character | Check whether the index is a UTF-8 byte offset and whether the line origin was added or subtracted exactly once. | Use `x_for_index` and `closest_index_for_x` on the same line layout used to paint; keep indices at valid boundaries. | | Last glyph is cut off or the row is too short | Check whether code used `width()` as ink bounds or `font_size` as full line height. | Allow for glyph overhang and use line metrics plus the chosen line height when allocating and clipping. | | A paragraph wraps differently after fonts load | Check whether registration happened after the first layout and whether the window was refreshed. | Register bundled fonts before the first frame, or call `cx.refresh_windows()` after a later `add_fonts` and measure again. | ## Cache and performance boundaries `WindowTextSystem` caches line layouts in current- and previous-frame caches and shares font resolution and metrics through `TextSystem`. The shared system also caches glyph raster bounds; painting uses glyph rendering parameters that include font, size, scale factor, and subpixel position. A cache key includes text, resolved font runs, size, and width constraints. Repeated identical inputs can reuse layout; changing text, font, features, size, forced width, or wrap width may require a new layout. Paint colors and backgrounds live in decoration runs, separate from glyph positions; changing only those need not reshape glyphs. These frame caches are not a promise that a layout remains cached indefinitely. For a known set of fonts, `TextSystem::prewarm_fonts(&fonts)` can prepare platform font caches on a background executor; ordinary shaping fills missing entries on demand. Reuse the window's text system and do not retain frame-local `&mut Window` references. For large virtualized editors or documents, shape only visible content and avoid measuring every row on every redraw. GPUI Kit's [TextView](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/src/text/text_view.rs) can virtualize scrollable blocks, while [Input](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/src/input/base/element.rs) shapes its visible lines. If a profiler shows repeated materialization of long line text, `try_layout_line_by_hash` probes the cache without building a contiguous string, and `shape_line_by_hash` builds one only on a miss. With the latter API, `ShapedLine.text` is an empty placeholder even on a miss; keep the original text separately if needed. The caller must guarantee that the same hash implies identical text, and pass its UTF-8 byte length as `text_len`. Prefer the ordinary API until that cost is measured. --- # KeyBinding Source: /docs/keybinding A **KeyBinding** maps one or more keystrokes to a typed [Action](./action). GPUI Kit uses GPUI's keymap: register bindings on the application, then put a matching Key Context and Action handler on the focused [Element](./element)'s dispatch path. Begin with [Focus](./focus) to create and track a keyboard target; the Action guide explains command dispatch. This page concentrates on writing and resolving bindings. ## Bind a command `KeyBinding::new(keys, action, context)` takes a keystroke string, an Action value, and an optional context predicate. Call `cx.bind_keys(...)` during initialization. A `None` context is eligible throughout the application; `Some("Editor")` requires an `Editor` context on the focused path. ```rust use gpui_kit::*; actions!(editor, [SaveDocument, MoveSelectionUp]); const EDITOR_CONTEXT: &str = "Editor"; fn init_keys(cx: &mut App) { cx.bind_keys([ KeyBinding::new("secondary-s", SaveDocument, Some(EDITOR_CONTEXT)), KeyBinding::new("up", MoveSelectionUp, Some(EDITOR_CONTEXT)), ]); } ``` Call `gpui_kit::init(cx)` once before `init_keys(cx)` and before opening windows. GPUI Kit registers its component bindings during initialization; the application can then add its own bindings in a deliberate order. Keep a [FocusHandle](./window) in the owning [Entity](./entity) and register it with the context and handler in its [Render](./render) implementation: ```rust impl Render for Editor { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .track_focus(&self.focus_handle) .key_context(EDITOR_CONTEXT) .on_action(cx.listener(Self::on_action_save_document)) .on_action(cx.listener(Self::on_action_move_selection_up)) .child("Editor") } } ``` The handlers have the usual GPUI signature, for example `on_action_save_document`. The `cx` argument is the owner's [Context](./context). A binding does not itself focus the element. Focus it when entering the region, or let pointer interaction focus its tracked handle. See [Action](./action) for focus ownership, dispatch, and propagation. ## Write key strings A keystroke is a key name with optional modifiers separated by hyphens. Separate successive keystrokes with spaces to form a **chord**: | Binding | Meaning | | --- | --- | | `secondary-s` | Primary shortcut modifier plus S: Command on macOS, Control on Linux and Windows. | | `ctrl-enter`, `alt-f4`, `shift-tab` | Explicit modifiers. | | `cmd-shift-p` | Platform modifier plus Shift and P. `cmd`, `super`, and `win` refer to the platform modifier; on Windows this is the Windows key, not Control. | | `up`, `escape`, `space`, `backspace` | Named keys. | | `cmd-k left` | Two keystrokes in sequence: the platform modifier plus K, then Left. | GPUI also parses `fn`, `ctrl`, `alt`, `shift`, `cmd`, `super`, `win`, and `secondary` as modifiers. Use `secondary` for a conventional cross-platform Command/Control shortcut. Use `ctrl` when Control itself is required on every platform. A capital ASCII letter such as `A` implies Shift+A; writing `shift-a` makes that intent clearer. `KeyBinding::new` **panics** if its key string or context predicate cannot be parsed, so keep static definitions reviewable and validate user input before constructing bindings. The operating system or window manager may reserve a shortcut before GPUI receives it. Test the intended combination on each supported platform, especially `cmd`/`super`/`win` combinations and system shortcuts such as `alt-f4`. A binding that parses successfully is not proof that a key event will reach the application. Chords can share a first keystroke. GPUI holds a prefix while a longer matching binding is possible; a following keystroke completes the chord or causes the prefix to be replayed. Avoid making a common text entry key a chord prefix in an editable region. ## Declare Key Contexts The element's `.key_context(...)` declares facts about a node. The binding's third argument is a **predicate** tested against contexts along the current focus path. They use related but different syntax: ```rust // Context attached to one element: identifier plus key/value fact. div().key_context("Editor mode=normal") // Predicates used as KeyBinding::new's third argument. Some("Editor") Some("Editor && mode == normal") Some("Editor && !Modal") Some("Workspace > Editor") ``` `Editor mode=normal` declares two facts on one node; `Editor && mode == normal` tests those facts. Predicates support identifiers, `==`, `!=`, `!`, `&&`, `||`, parentheses, and `>` for an ancestor followed by a descendant context. For example, `Workspace > Editor` requires an `Editor` context below a `Workspace` context. Use parentheses when combining operators so the intended grouping is explicit. `!Modal` excludes a path containing `Modal`; it is useful for a workspace shortcut that should not run while a modal owns focus. Contexts only participate when they lie on the **focused** dispatch path. A sibling's `Editor` context does not activate an editor binding. A binding can match while its handler remains unreachable if that handler is on another branch. Put both context and handler on the focused region or an ancestor that owns the command. ## Understand precedence When several bindings match the same keys, GPUI ranks them by the depth of the matching context on the focused path. A binding for an inner `Editor` ranks ahead of one for its `Workspace` ancestor. At the same depth, the binding added **later** ranks first. A binding with `None` context is treated as matching at the deepest context depth, so it is not automatically a weak fallback: a later `None` binding can rank ahead of an `Editor` binding on the same keys. Prefer an explicit workspace context for a shortcut that should yield to an inner control. GPUI can try more than one matching binding. It dispatches each candidate Action along the focused path until a handler consumes one; Action handlers stop propagation by default. A handler that declines an Action can call `cx.propagate()` to let dispatch continue. Keep competing bindings intentional: order, context depth, and handler availability all affect the result. Select a Focus target to see which `escape` binding ranks first in this example. The tree shows the focused path; the result assumes a reachable handler consumes the first Action.
Focus path
Window
Workspace Escape → ClearWorkspaceSelection
Editor Escape → CloseEditorSearch
Modal No Escape binding
Matching Escape bindings · highest first
1 · WorkspaceClearWorkspaceSelection
1 · EditorCloseEditorSearch
2 · WorkspaceClearWorkspaceSelection

No matching Escape binding on this Focus path.

```rust cx.bind_keys([ KeyBinding::new("escape", ClearWorkspaceSelection, Some("Workspace")), KeyBinding::new("escape", CloseEditorSearch, Some("Editor")), ]); ``` With focus in an Editor inside Workspace, `CloseEditorSearch` has the more specific match. With focus elsewhere in Workspace, only `ClearWorkspaceSelection` matches. This is how GPUI Kit components such as Tree and TimeField keep arrow-key behavior local to their own contexts. ## Look up a shortcut from its Action An Action is also the key for **reverse lookup**: ask the window which binding currently invokes that Action. GPUI Kit's [Kbd component](../component/kbd) displays the result. Use the focus handle of the command's intended target when rendering a button, menu, or command palette. This matters when another control or an overlay currently owns Focus. ```rust use gpui_kit::component::kbd::Kbd; let binding = window.highest_precedence_binding_for_action_in( &SaveDocument, &self.editor_focus, ); // A GPUI Kit hint for the same Action and target. let hint = Kbd::binding_for_action_in( &SaveDocument, &self.editor_focus, window, ); div().children(hint) ``` The first call returns `Option`; the second returns `Option`, ready to render beside a label. Both account for the target's context, shadowing by a higher priority binding, and the active keymap, including user overrides. `None` means there is no visible binding for that Action on that resolved path. These lookups use the **previously rendered frame**, so a focus handle first drawn in the current frame may have no result yet. A binding lookup does not check the command's runtime enabled state or prove that an Action handler is on the target path; the owner must still decide whether the command is allowed. `window.is_action_available_in(&SaveDocument, &self.editor_focus)` checks for an element Action handler on that path. For the current window context, GPUI also offers `window.highest_precedence_binding_for_action(&action)` and `window.bindings_for_action(&action)`; the latter returns all visible bindings. For a known single context, use `window.highest_precedence_binding_for_action_in_context`. GPUI Kit offers `Kbd::binding_for_action(&action, Some("Editor"), window)` for the same simple context lookup, or `None` for the window's current context. Its `Some(...)` argument uses **Key Context declaration syntax**, such as `Editor mode=normal`, not predicate syntax such as `Editor && mode == normal`. An invalid context string silently falls back to the window lookup, so validate dynamic input before passing it. A concrete focus handle is the safer choice when contexts are nested or a menu has moved Focus. GPUI Kit's `Kbd::global_binding_for_action(&action, window)` offers a last-resort lookup against an empty Key Context, without reconstructing a nested focus path. `Kbd::binding_for_action_in` currently displays **only the first keystroke** of a chord. To show the entire chord, format every stroke of the returned `KeyBinding`: ```rust use gpui_kit::AsKeystroke; use gpui_kit::component::kbd::Kbd; let shortcut = binding.map(|binding| { binding.keystrokes().iter() .map(|stroke| Kbd::format(stroke.as_keystroke())) .collect::>() .join(" ") }); ``` `Kbd::format` chooses platform-specific modifier symbols and key names: `secondary-s` displays as `⌘S` on macOS and `Ctrl+S` on Linux and Windows. `AsKeystroke` exposes each binding stroke as a `Keystroke`. Keep `shortcut` optional: an unbound Action should not show a made-up accelerator. ## Keep menus and displayed shortcuts in sync Use one Action for the shortcut, button, command palette, and menu item. A menu can hold the same value with `MenuItem::action("Save Document", SaveDocument)`, while a button can dispatch it with `window.dispatch_action(Box::new(SaveDocument), cx)`. The focused owner then runs the same handler. Register bindings **before** `cx.set_menus(...)`. Native menus read the current keymap when built and retain the displayed shortcut. If an application changes bindings later, rebuild the menus with `cx.set_menus(...)` so their shortcut labels and native accelerators reflect the new keymap. For in-window menus and tooltips, query the Action's current binding as above rather than hard-coding `⌘A` or `Ctrl+A`: the platform, target context, and user keymap can each change the displayed shortcut. GPUI Kit's Popup Menu resolves shortcuts against the command target or trigger Focus and displays them with `Kbd`; a tooltip can use `Tooltip::action` to resolve a simple context when it renders. ## User keymaps and named Actions GPUI's `actions!` macro registers unit Actions under stable names such as `editor::SaveDocument`. For an Action with configuration data, derive `Action` and the required deserialization and schema traits; `#[action(no_json)]` marks an Action that cannot be constructed from JSON. `cx.all_action_names()` lists registered names, and `cx.build_action(name, data)` builds a registered Action from an optional JSON value. GPUI provides this Action registry and keymap machinery, but an application owns its **user keymap file format**, validation, loading, and reload policy. A loader can resolve an Action name and payload with `cx.build_action(...)`, parse the context with `KeyBindingContextPredicate::parse(...)`, and construct a binding with the fallible `KeyBinding::load(...)`. Report parse or unknown-Action errors to the user rather than passing untrusted strings to the panic-on-error `KeyBinding::new(...)`. `cx.bind_keys(...)` appends bindings; `cx.clear_key_bindings()` clears the entire application keymap, including component defaults, so a reload must restore every required binding in the desired order. Rebuild native menus after a keymap change. ## Troubleshoot a shortcut 1. **Keys:** Check the actual modifier on this platform. `secondary-s` means Command+S on macOS and Control+S elsewhere; `cmd-s` does not mean Control+S on Windows. 2. **Focus:** Check which `FocusHandle` is focused and that a rendered element calls `track_focus` with it. A shortcut that works only after a click often has a focus path problem. 3. **Context:** Check that the predicate matches a `key_context` on that path. Use `mode=normal` when declaring a context and `mode == normal` when testing it. 4. **Competition:** Check deeper contexts, later bindings at the same depth, and chord prefixes. A `None` binding can outrank a shallower scoped binding. 5. **Handler:** Check that the matching Action has an `on_action` handler on the focused path and that a more specific handler does not consume it first. 6. **Menus and reloads:** If a menu shows an old shortcut, call `cx.set_menus(...)` after installing the new keymap. If a reload removed a component shortcut, confirm that `clear_key_bindings()` was followed by all component initialization. ## Verify with repository examples The [Tree implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/src/tree.rs) registers arrow-key bindings under `Tree` and places `.key_context(CONTEXT)`, `.track_focus(&focus_handle)`, and the `on_action` handlers on its rendered root. The [Combobox implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/src/combobox.rs) adds Enter, Escape, and `secondary-enter` for a second confirmation mode. The [Popover story](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story/src/stories/popover_story.rs) shows explicit macOS Command versus other-platform Control bindings and a focused action owner. To check an application binding, give the target its focus handle, press the shortcut, and confirm the expected Action handler runs. Then focus a sibling outside the declared context: the contextual shortcut should no longer run. Finally, open an overlay or text input and repeat the test to expose focus changes and key conflicts. Check the displayed shortcut separately with `highest_precedence_binding_for_action_in` for the command's target handle; a correct label alone does not show that the handler is reachable. For the full route from Focus through Action handling, continue with [Action](./action). --- # Element Source: /docs/element An **Element** is a node in the element tree that GPUI builds for one frame. Elements perform layout, prepare hit testing, and [paint](./paint) pixels into a Window. GPUI drops the tree and its frame-local callbacks before the next frame, then builds a new tree from the application's current state. Most application code should compose the elements provided by GPUI and GPUI Kit: ```rust div() .flex() .items_center() .gap_2() .child(Icon::new(IconName::Search)) .child("Search") ``` This code creates an element tree. It does not implement GPUI's low-level `Element` trait. ## `Element`, `IntoElement`, and `AnyElement` These types have different jobs: | Type | Purpose | | --- | --- | | `Element` | Implements the low-level layout and painting lifecycle. | | `IntoElement` | Converts a value into a concrete `Element`, allowing strings, components, Entities, and other values to be passed to `.child(...)`. | | `AnyElement` | Erases the concrete element type, which is useful for heterogeneous collections, conditional branches, slots, and stored children. | Keep the concrete type when every branch has the same type. Erase it only at a boundary that needs different element types: ```rust fn status_icon(online: bool) -> AnyElement { if online { Icon::new(IconName::CircleCheck).into_any_element() } else { div().child("Offline").into_any_element() } } ``` GPUI Kit uses `AnyElement` this way for optional slots, table cells, and functions whose branches return different UI types. `IntoElement` is the usual API boundary when the caller does not need type erasure. ## The three phases GPUI drives an `Element` through three trait methods in order. The layout solver runs between the first and second calls:
Render::render builds this frame's Element tree
  1. 01request_layout

    Register styles and child layouts; return LayoutId and RequestLayoutState.

  2. 02prepaint

    Use resolved bounds to prepare geometry and hitboxes; return PrepaintState.

  3. 03paint

    Draw prepared content and register current-frame input listeners.

Taffy resolves Bounds<Pixels> between request_layout and prepaint.
The tree and frame-local listeners are discarded before the next frame. `RequestLayoutState` and `PrepaintState` move forward within this pass; they are not persistent caches. ### Decide which method owns the work Ask what information the work needs and what it produces: | Work | Put it in | Why | | --- | --- | --- | | Choose width, height, padding, and child layout; measure intrinsic content when its size affects layout | `request_layout` | The layout solver needs these inputs before it can calculate bounds. A measurement closure can use an offered width, but the element's final position is still unknown. | | Turn final bounds into text lines, a path, clipping geometry, or a hitbox; decide which virtualized children are visible | `prepaint` | This is the first phase with the resolved pixel rectangle. Save results needed for drawing or input in `PrepaintState`. | | Submit quads, glyphs, paths, or images; attach pointer, scroll, or keyboard listeners for this frame | `paint` | The scene and dispatch tree are ready to receive drawing commands and listeners. Reuse the prepared geometry. | For example, a clickable chart first requests the chart's size and its child layouts. Once the size is known, it maps data points into pixel coordinates and inserts hitboxes for the interactive regions. Finally, it paints the paths from those coordinates and registers the pointer listeners. If the chart needs a label's width **to determine its own size**, measure that width during `request_layout`; if it only needs the label's final position **inside an already sized chart**, prepare it during `prepaint`. A useful boundary test is: **Would this calculation change the element's requested size?** If yes, it belongs in layout or its measurement closure. If it only needs the resulting bounds, use `prepaint`. If it changes the pixels or current-frame handlers without changing geometry, use `paint`. Long-lived model data and subscriptions are outside all three methods; keep them in an [Entity](./entity). ### `request_layout` Register the element's [Style](./style) and child layout nodes with `window.request_layout`. GPUI's Taffy layout engine resolves their sizes and positions after layout has been requested. Return a `LayoutId` and any `RequestLayoutState` needed by the later phases. Do not assume the final `Bounds` are available here. ### `prepaint` GPUI now provides the resolved `Bounds`. Use this phase to shape text, calculate geometry, insert hitboxes, prepaint children, and prepare data that `paint` needs. Return that data as `PrepaintState`. Hit testing belongs here because GPUI must establish the current frame's spatial and dispatch information before painting. ### `paint` Paint quads, text, paths, or images with the prepared state. Register frame-local input handlers when the custom element requires them. This phase should consume the geometry prepared earlier rather than repeat layout work. `RequestLayoutState` reaches both `prepaint` and `paint`; `PrepaintState` reaches `paint`. Persistent application state belongs in an [Entity](./entity), while small element state that must survive frames can be associated with an [ElementId](./element_id). ### A complete, minimal custom element This invisible event surface fills its parent's bounds. It shows the exact trait signatures and why the hitbox is carried from `prepaint` into `paint`: ```rust use gpui_kit::*; struct EventSurface; impl IntoElement for EventSurface { type Element = Self; fn into_element(self) -> Self::Element { self } } impl Element for EventSurface { type RequestLayoutState = (); type PrepaintState = Hitbox; fn id(&self) -> Option { None } fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None } fn request_layout( &mut self, _: Option<&GlobalElementId>, _: Option<&InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let mut style = Style::default(); style.size.width = relative(1.).into(); style.size.height = relative(1.).into(); (window.request_layout(style, None, cx), ()) } fn prepaint( &mut self, _: Option<&GlobalElementId>, _: Option<&InspectorElementId>, bounds: Bounds, _: &mut Self::RequestLayoutState, window: &mut Window, _: &mut App, ) -> Self::PrepaintState { window.insert_hitbox(bounds, HitboxBehavior::Normal) } fn paint( &mut self, _: Option<&GlobalElementId>, _: Option<&InspectorElementId>, _: Bounds, _: &mut Self::RequestLayoutState, _: &mut Self::PrepaintState, _: &mut Window, _: &mut App, ) {} } ``` The element is intentionally invisible and has no handler. A hitbox is geometry for input routing, not a click callback. [GPUI Kit's `CarouselScrollMask`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/carousel/scroll_mask.rs) starts with this pattern, then retains its `Hitbox` and installs frame-local pointer and wheel listeners in `paint`. It uses `Hitbox::should_handle_scroll` to avoid claiming gestures blocked by a surface in front. When an element has children, request their layouts first and pass their `LayoutId`s to `window.request_layout`; then prepaint and paint them in order. For intrinsic measurement, `window.request_measured_layout` takes a measurement closure instead. `HitboxBehavior::Normal` participates in hit testing without hiding hitboxes behind it. `BlockMouse` occludes both mouse and scroll handling behind the hitbox; `BlockMouseExceptScroll` leaves scrolling available. The hitbox also carries the current content mask. In a custom element, coordinate the hitbox with any clipping and paint order; drawing a shape does not create a hitbox automatically. ### Make the surface visible and respond to a press The first example is a trait skeleton, so adding it as a child produces no visible pixels. To run the continuation, replace the contents of the existing [`examples/hello_world/src/main.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/examples/hello_world/src/main.rs) with the complete file below. The parent gives the surface a definite `240 × 96` pixel area; its relative width and height now have a size to resolve against. This uses the existing workspace example and adds no dependency. ```rust use gpui_kit::component::ActiveTheme as _; use gpui_kit::*; struct SurfaceDemo { presses: usize, } impl Render for SurfaceDemo { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .flex() .flex_col() .gap_2() .child( div() .w(px(240.)) .h(px(96.)) .child(EventSurface { owner: cx.entity().clone(), color: cx.theme().primary, }), ) .child(format!("Pointer presses: {}", self.presses)) } } struct EventSurface { owner: Entity, color: Hsla, } impl IntoElement for EventSurface { type Element = Self; fn into_element(self) -> Self::Element { self } } impl Element for EventSurface { type RequestLayoutState = (); type PrepaintState = Hitbox; fn id(&self) -> Option { None } fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None } fn request_layout( &mut self, _: Option<&GlobalElementId>, _: Option<&InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let mut style = Style::default(); style.size.width = relative(1.).into(); style.size.height = relative(1.).into(); (window.request_layout(style, None, cx), ()) } fn prepaint( &mut self, _: Option<&GlobalElementId>, _: Option<&InspectorElementId>, bounds: Bounds, _: &mut Self::RequestLayoutState, window: &mut Window, _: &mut App, ) -> Self::PrepaintState { window.insert_hitbox(bounds, HitboxBehavior::Normal) } fn paint( &mut self, _: Option<&GlobalElementId>, _: Option<&InspectorElementId>, bounds: Bounds, _: &mut Self::RequestLayoutState, hitbox: &mut Self::PrepaintState, window: &mut Window, _: &mut App, ) { window.paint_quad(fill(bounds, self.color)); let hitbox = hitbox.clone(); let owner = self.owner.clone(); window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| { if phase.bubble() && event.button == MouseButton::Left && hitbox.is_hovered_at(event.position, window) { owner.update(cx, |view, cx| { view.presses += 1; cx.notify(); }); } }); } } fn main() { gpui_kit::application().run(|cx| { gpui_kit::init(cx); gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| { cx.new(|_| SurfaceDemo { presses: 0 }) }) .expect("failed to open window"); }); } ``` From the repository root, run `cargo run -p hello_world`. The window shows a `240 × 96` theme-colored rectangle and `Pointer presses: 0`. Each left press inside the rectangle changes the label to `1`, `2`, and so on; a press outside it leaves the count unchanged. The rectangle is intentionally a pointer-only teaching example; it does not supply keyboard activation, focus, a cursor style, or an accessibility role. Use a standard [Button](../component/button) for an application control unless the lower-level behavior is essential. Read the example in four steps: 1. **Mount and size.** `Render::render` creates a fresh `EventSurface` each time the view renders. The parent fixes the available width and height; the element requests `relative(1.)` in both directions. If its parent has no usable size, the requested percentage may resolve to a zero-sized or otherwise unexpected surface. 2. **Prepare input geometry.** `prepaint` receives the solved `Bounds` and inserts a hitbox for exactly that rectangle. `PrepaintState = Hitbox` carries the hitbox to `paint`; it belongs to this frame's hit-test data. 3. **Paint and register the listener.** The view reads `cx.theme().primary` and passes that `Hsla` into the element; `paint_quad(fill(bounds, self.color))` makes the same rectangle visible using the current theme. `on_mouse_event` adds a frame-local listener to the window's mouse-listener list, in paint registration order. It is not attached to the current dispatch node. The event type is inferred from `&MouseDownEvent`; the bubble phase, left button, and hitbox test prevent unrelated presses from changing the count. The listener closes over cloned handles because it must outlive this `paint` call. 4. **Change retained state.** The `Entity` survives frame rebuilding. `owner.update` changes `presses`, and `cx.notify()` schedules the view to render again. The next `render` builds a new `EventSurface` and label. Neither associated phase state is used as an application counter. To verify each layer while learning, try these changes in the same file and rerun `cargo run -p hello_world` after each one: 1. Comment out `window.paint_quad(...)`. The rectangle becomes invisible, but presses in its original area still increment the count. The hitbox, not the pixels, controls input routing. 2. Restore painting and comment out `window.insert_hitbox(...)` **only after** changing `PrepaintState` to `()` and making `paint` omit its listener. The rectangle remains visible but does not react. Painting alone provides no hitbox or handler. 3. Restore the complete example. Change the parent's `.w(px(240.))` to `.w(px(120.))`. The drawn and clickable areas shrink together because both use the resolved bounds. | What you observe | Check | | --- | --- | | No colored surface | The parent has a definite size, `request_layout` returns its `LayoutId`, and `paint_quad` is still present. | | Surface appears, but presses do nothing | `prepaint` inserts a hitbox for the same bounds, `paint` registers `on_mouse_event`, and the listener checks the bubble phase and left button. Check whether a front hitbox uses `BlockMouse` or `BlockMouseExceptScroll`; an ordinary `Normal` hitbox does not by itself block the hitbox behind it. | | Presses are received, but the label does not change | The listener updates `Entity` and calls `cx.notify()` inside that update. | The first two changes are experiments, not alternate finished implementations: restore the full code before continuing. The example does not need an `ElementId`: the counter belongs to the Entity, while the hitbox and listener are rebuilt each frame. Add a stable ID only when the low-level element itself needs keyed state or accessibility identity. For a surface with children, the next step is to retain their layout handles in `RequestLayoutState`, prepaint them after the parent has bounds, and paint them in order; see [TextView](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/src/text/text_view.rs) for a real implementation. A reusable input primitive also needs a deliberate focus, keyboard, and accessibility contract; attaching a mouse listener alone does not provide one. ### Compose one child in a custom Element The pointer surface above has no child layout to forward. This second complete example adds exactly one child. Replace [`examples/hello_world/src/main.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/examples/hello_world/src/main.rs) with the code below, then run `cargo run -p hello_world` from the repository root. It uses the same package and dependencies as the previous exercise. ```rust use gpui_kit::component::ActiveTheme as _; use gpui_kit::*; struct ChildDemo; impl Render for ChildDemo { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div().flex().flex_col().items_start().gap_2().child( ChildFrame { child: Some(div() .w(px(160.)) .h(px(32.)) .bg(cx.theme().primary) .text_color(cx.theme().primary_foreground) .child("Child: 160 x 32") .into_any_element()), background: cx.theme().secondary, accent: cx.theme().primary, }, ) } } struct ChildFrame { child: Option, background: Hsla, accent: Hsla, } impl IntoElement for ChildFrame { type Element = Self; fn into_element(self) -> Self::Element { self } } impl Element for ChildFrame { type RequestLayoutState = AnyElement; type PrepaintState = Bounds; fn id(&self) -> Option { None } fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None } fn request_layout( &mut self, _: Option<&GlobalElementId>, _: Option<&InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { let mut child = self.child.take().expect("layout requested once per frame"); let child_layout = child.request_layout(window, cx); let mut style = Style::default(); style.padding = Edges::all(px(12.).into()); let layout = window.request_layout(style, [child_layout], cx); (layout, child) } fn prepaint( &mut self, _: Option<&GlobalElementId>, _: Option<&InspectorElementId>, bounds: Bounds, child: &mut Self::RequestLayoutState, window: &mut Window, cx: &mut App, ) -> Self::PrepaintState { child.prepaint(window, cx); Bounds { origin: bounds.origin, size: size(px(4.), bounds.size.height), } } fn paint( &mut self, _: Option<&GlobalElementId>, _: Option<&InspectorElementId>, bounds: Bounds, child: &mut Self::RequestLayoutState, accent_bounds: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { window.paint_quad(fill(bounds, self.background)); window.paint_quad(fill(*accent_bounds, self.accent)); child.paint(window, cx); } } fn main() { gpui_kit::application().run(|cx| { gpui_kit::init(cx); gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| { cx.new(|_| ChildDemo) }) .expect("failed to open window"); }); } ``` The window shows a colored child labeled `Child: 160 x 32`, surrounded by a 12-pixel inset and a narrow accent strip on the frame's left edge. The outer frame has no fixed height: Taffy measures the 32-pixel child and adds the top and bottom padding. In this unconstrained example that yields a 56-pixel-high frame. Change the child's `.h(px(32.))` to `.h(px(64.))`, run again, and the frame grows to 88 pixels high. If an ancestor constrains height, that ancestor can change the result. Follow the ownership path rather than copying the calls mechanically: 1. `Render::render` creates a `ChildFrame` with one `AnyElement`. The `Option` lets `request_layout` move that child into its returned `RequestLayoutState`; the phase is called once for this element in a frame. A new `ChildFrame` is built on the next render. 2. The child requests its own `LayoutId` first. The parent passes that ID to `window.request_layout(style, [child_layout], cx)`. This connects the nodes in the layout tree. Passing an empty child list would leave the colored child out of the parent's measurement and placement. 3. Once Taffy has resolved the tree, `prepaint` calls `child.prepaint(window, cx)` so the child sees its computed position and can prepare its own hitboxes or geometry. The parent also calculates a 4-pixel strip from its resolved bounds and returns that rectangle as `PrepaintState`. 4. `paint` paints the parent's background and strip, then calls `child.paint(window, cx)`. This order keeps the child visible above the background. `AnyElement` carries its internal phase state; the parent must still call each phase in order. There is no hitbox on the frame because it does not handle input. The child's built-in `div` draws its background and text. Neither paint call can make a missing layout child appear: layout, prepaint, and paint all need to include it. | What you observe | Check | | --- | --- | | Frame appears but child is missing | The child is passed to `window.request_layout`, then receives both `prepaint` and `paint`. Check paint order if the frame background covers it. | | Child draws outside the inset | The parent style has `Edges::all(px(12.).into())` and uses the returned child layout ID. Do not hand-offset the child in `paint`; Taffy places it. | | Frame height does not follow child height | Check for a fixed height or clipping on the frame or an ancestor, and verify that the child layout ID is attached to the parent. | The example keeps the layout child in `RequestLayoutState` because it must survive through the next two phases. It keeps only the calculated accent rectangle in `PrepaintState`. Neither state is persistent application data; use an [Entity](./entity) for that. ### Identity and retained state `Element::id()` returns a local `ElementId`. GPUI combines it with keyed ancestors into a `GlobalElementId` and passes that global ID into the three phases. An element can use it with `window.with_element_state` for small state that survives rebuilding the element tree. GPUI Kit's carousel surface uses that mechanism for ongoing scroll state. The ID must be unique among siblings under the nearest keyed ancestor. Use a domain ID for reorderable items; an index changes meaning after insertion or sorting. An element without an ID receives `None` and cannot use this keyed state channel. Entity state remains the right owner for application data and subscriptions. ### Text is a specialized low-level element The [TextSystem](./text-system) owns font lookup, shaping, glyph metrics, and caches. `cx.text_system()` exposes it; a window also has a window-specific text system. Text width depends on font, size, shaping, and available width, so a custom text element may need `request_measured_layout` or a measured line during layout. Its `prepaint` can then compute line geometry, selections, and hitboxes; `paint` draws the prepared text. Keep the same font parameters through measurement and paint, or caret and selection positions will drift. Use GPUI's `text(...)` and GPUI Kit's text components unless you need selection, inline objects, or a specialized editor. [GPUI Base's TextView](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/src/text/text_view.rs) shows measured layout, an element-owned hitbox, and painting tied to the resolved bounds. [GPUI Kit's input element](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/src/input/base/element.rs) shows the same pipeline for editing. ## When to implement `Element` Implement `Element` when existing elements cannot express the work, for example: - a code editor or text input that shapes text and paints selections and cursors; - a chart or canvas with custom geometry; - a custom layout algorithm; - a performance-sensitive primitive that needs direct control over layout, hitboxes, and painting. GPUI Kit's input element uses a custom `Element` because it must shape text, register an input handler, and paint selections and cursors. GPUI's `Svg`, `Img`, lists, and canvas use the same lifecycle. Most application UI can instead compose existing elements and convert differing branches to `AnyElement`. For a reusable UI component, start with [`RenderOnce`](./render-once). For stateful UI owned by an Entity, use [`Render`](./render). Drop down to `Element` only when you need to control the rendering pipeline itself. ### Decisions visible in GPUI Kit source | Example | Why composition alone is insufficient | What the element owns | | --- | --- | --- | | [Input](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/src/input/base/element.rs) | Caret, selection, wrapping, and pointer-to-text mapping must use the same measured text geometry. | Text layout, hitboxes, input handlers, and paint order. | | [TextView](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/src/text/text_view.rs) | Rich text and selectable ranges need measurement and clipping tied to resolved bounds. | Text layout state, hitbox, selection surface, and prepared painting. | | [CarouselScrollMask](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/carousel/scroll_mask.rs) | A gesture surface must occupy the viewport independently of moving carousel content. | Full-size layout, hitbox, and pointer/wheel dispatch; it paints nothing. | | [Plot line](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/plot/shape/line.rs) | Data points require direct path tessellation, but do not require custom layout or input. | A path painted inside a higher-level chart; this shape itself does not need a full `Element` implementation. | These examples show that `Element` is about owning a phase boundary, not merely drawing something custom. The carousel surface demonstrates that an Element may paint nothing. Plot demonstrates the opposite: a custom drawing can use a canvas or a containing Element without making each shape an Element. ### Performance follows phase ownership **Make the efficient path the natural API path.** GPUI divides an Element into layout, prepaint, and paint so authors can measure when dimensions are known, prepare geometry once, and reuse it during painting. GPUI Kit follows the same principle in its components: costs that repeat across a large tree should have an owner and an explicit invalidation rule. The goal is for ordinary component use to avoid repeated work by design, rather than requiring every caller to repair it with a cache. Every rendered frame can rebuild the tree and revisit these methods. Keep `request_layout` focused on styles and layout nodes; do not shape text or tessellate paths there unless intrinsic measurement requires it. In `prepaint`, compute geometry once and pass it through `PrepaintState` to `paint`. Avoid rebuilding unchanged paths on every paint; GPUI Kit's Plot uses keyed window state and a shape key to retain tessellation across frames. Put subscriptions and long-lived application data in an Entity instead of a frame-local associated state. Finally, clip and cull before expensive painting: TextView and Input compute visible content from resolved bounds rather than blindly painting the full document. ## How GPUI drives an Element The low-level contract is enforced by a `Drawable` wrapper. It moves through `Start → RequestLayout → LayoutComputed → Prepaint → Painted`; calling the phases out of order is an error. This explains why GPUI passes two associated state values forward instead of asking the Element to rediscover everything during paint. It also explains why a custom Element should not call drawing methods during `request_layout`: the window is not in its paint phase yet. ### Layout tree, dispatch tree, and scene are different structures `request_layout` contributes nodes to the layout engine and returns a `LayoutId`. After Taffy resolves that layout, `prepaint` obtains pixel bounds. GPUI creates a dispatch node for the Element while prepainting; hitboxes are recorded separately for hit testing. `paint` activates the corresponding dispatch node for Action and keyboard listeners registered on that path. Mouse listeners registered with `window.on_mouse_event` instead enter a frame-local window list, called in registration order for capture and reverse order for bubble; their handlers must check the relevant hitbox. Drawing commands go to the scene. A `paint_path` call changes the scene, but does not add a dispatch node or hitbox. Conversely, the carousel mask contributes input geometry and listeners without adding visible drawing. This separation lets a custom Element do precisely one job. A decoration may only need a canvas and scene commands. A focusable control must coordinate dispatch, hitboxes, focus, and accessibility as well. A virtualized list must decide which children to measure and paint, not merely draw a large rectangle. ### Identity is a path, not a pointer to this frame's value When `Element::id()` returns an `ElementId`, GPUI appends it to the current keyed ancestor path and passes the resulting `GlobalElementId` to the three methods. The Rust Element value itself is recreated for a later frame; its address is not a stable identity. `window.with_element_state(global_id, ...)` can carry a small typed value between consecutive painted frames. `window.use_keyed_state(key, cx, init)` builds an Entity at a key in the current Element namespace and observes it so changes can notify the owning view. These mechanisms require stable keys; an item index is unsafe when items can move. ### Accessibility is established with geometry When accessibility is active, GPUI can create an AccessKit node for an Element that has both an ID and `a11y_role()`. During prepaint it sets that node's bounds from the resolved layout, calls `write_a11y_info`, and can add synthetic children. Painting pixels alone supplies no role, name, value, or action. Standard interactive GPUI Kit components already provide these semantics; a custom low-level control must supply its own contract. Continue with [Accessibility](./accessibility) for those APIs. ## Identity and interactivity These concepts sit on separate boundaries: - [`ElementId`](./element_id) identifies an element within its keyed scope. GPUI uses its global form to connect state and retained work across frames. - Calling `.id(...)` on an `InteractiveElement` returns `Stateful`. That wrapper enables APIs whose state must be associated with a stable element identity. - `InteractiveElement` exposes GPUI's standard interaction machinery, including hitboxes, mouse listeners, Focus tracking, Key Context, and Action handlers. A custom `Element` does not gain those behaviors automatically. `InteractiveElement` is implemented by types that expose an `Interactivity` field. Its methods include `on_mouse_down`, `on_key_down`, `track_focus`, `key_context`, `on_action`, and hover styles. Calling `.id(...)` returns `Stateful`, which implements `StatefulInteractiveElement` and exposes state-dependent methods such as accessibility role and label, tooltip, and click handling. The wrapper is a type-level signal: stateful interaction needs a stable identity. Implementing `IntoElement` alone only makes a value acceptable as a child; it does not implement either interactive trait. `#[derive(IntoElement)]` plus `RenderOnce` is the usual component path, while a hand-written low-level `Element` typically implements `IntoElement` by returning itself. There are two useful ways to obtain those APIs. When composing an ordinary surface, use an existing interactive element: ```rust div().id("result-row") .aria_label("Search result") .on_click(|_, _, _| {}) ``` The `.id(...)` call changes the Rust type from `Div` to `Stateful
`, so identity-dependent methods become available. Give each repeated row a domain-derived ID rather than the same literal ID. `on_click` registers a handler; it does not automatically update an Entity's state. When exposing the same fluent methods on a GPUI Kit component, delegate its interaction state to the underlying primitive. The component's [Radio implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/radio.rs) follows this shape (the excerpt omits unrelated fields): ```rust impl InteractiveElement for Radio { fn interactivity(&mut self) -> &mut Interactivity { self.base.interactivity() } } impl StatefulInteractiveElement for Radio {} ``` `StatefulInteractiveElement` is a marker trait: its default methods write into the returned `Interactivity`; the empty implementation does not create an ID or a hitbox. `Radio::new(id)` passes a stable ID to its Base primitive, which renders the actual interactive surface. If your component cannot guarantee that identity and the underlying interactive element, expose only the methods it can support. A raw custom `Element` such as `EventSurface` above has neither trait until you explicitly provide and drive that interactivity or compose an existing interactive element. ## `canvas`: two phase callbacks without a new trait implementation GPUI's `canvas(prepaint, paint)` is itself an `Element`. It requests layout from its `Styled` properties, then invokes a `FnOnce` prepaint callback with resolved bounds. That callback returns any temporary value `T`, which GPUI passes to the `FnOnce` paint callback. This is a small bridge into the Element lifecycle, not an HTML canvas or a retained drawing surface. ```rust canvas( move |bounds, _, _| { let mut line = PathBuilder::stroke(px(1.)); line.move_to(bounds.origin); line.line_to(point(bounds.right(), bounds.top())); line.build().ok() }, move |_, path, window, _| { if let Some(path) = path { window.paint_path(path, color); } }, ) .w_full() .h(px(1.)) ``` The `prepaint` result here is `Option>`; it exists only for this frame. [GPUI Kit's dashed Separator](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/separator.rs) uses `canvas` to draw a path in its resolved bounds, and the [circular Progress](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/progress/progress_circle.rs) returns measured radii from prepaint to paint. Use this for a focused drawing callback inside `Render` or `RenderOnce`. See [Paint](./paint) for `PathBuilder`, SVG path notation, and path caching examples. It has no built-in children, hitbox, focus tracking, or stable Element ID. When those responsibilities need to work together, implement `Element` or compose a standard interactive element around the canvas. When an existing interactive element such as `div()` already provides the behavior you need, compose it. If a custom primitive needs standard interaction, embed or delegate to GPUI's `Interactivity`, as GPUI's built-in elements do. Implementing raw hitboxes and event registration yourself also makes you responsible for dispatch, clipping, cursor behavior, and accessibility. `Element::id()` returning an `ElementId` does more than label pixels: it creates stable identity across frames. Keep IDs unique within their nearest keyed ancestor, and do not add an ID unless the element or an attached behavior needs identity. [Element]: https://docs.rs/gpui-pre/{{gpui_pre_version}}/gpui/trait.Element.html [IntoElement]: https://docs.rs/gpui-pre/{{gpui_pre_version}}/gpui/trait.IntoElement.html [AnyElement]: https://docs.rs/gpui-pre/{{gpui_pre_version}}/gpui/struct.AnyElement.html --- # SharedString Source: /docs/shared-string Small costs add up. In UI code, a cost built into an ordinary component call is repeated wherever that component appears and redraws. GPUI and GPUI Kit treat that as an API design constraint: retained text should be easy to own and pass around without making every caller copy its full contents. `SharedString` is one concrete choice that gives component authors this default. `SharedString` is GPUI's owned, immutable text type. GPUI Kit uses it for labels, placeholders, titles, and other text that an element or component keeps after the builder call returns. Use it by default for **UI text** held across [render](./render) calls, component boundaries, or task closures. Borrow temporarily with [`&str`](https://doc.rust-lang.org/std/primitive.str.html). Import `SharedString` with `use gpui_kit::*;` or `use gpui_kit::SharedString;`. ## Small costs add up Small copies and allocations are a long-term budget to control when choosing framework types, not a burden to leave at every call site. If a label API required each caller to supply a newly owned buffer, ordinary composition would repeat full text copies across many components. By accepting `SharedString` for retained text, a component can keep an owned value while callers reuse the one they already hold. Authors do not need a hand-written cache around each label for that common path. This default avoids repeated **full text** copies, not all work: cloning inline text copies its small bytes, cloning shared heap text updates a reference count, and `format!(...).into()` still constructs new text each time it runs. Other costs have their own tools, such as [virtualization](../base/virtual-list) for offscreen rows and [view caching](./view-cache) for an unchanged subtree. ## Why use it instead of `String` for UI text? Consider a title passed from a workspace View through a header and tab component to a final label. Each layer that keeps the title after its caller returns needs a valid ownership choice: move the value and give up the caller's copy, borrow it with a lifetime tied to the source, or clone it. Rust's [`String`](https://doc.rust-lang.org/std/string/struct.String.html) owns a mutable buffer, so cloning a nonempty `String` at each boundary creates independent buffers and copies the title's bytes. Rebuilding the title with `format!` at each layer repeats formatting and allocation instead. `SharedString` represents that immutable value in a form that is cheap to clone. The workspace can retain one value while each layer takes an owned clone; for long heap-backed text, the clones share its bytes instead of copying them at every handoff. This is why GPUI and GPUI Kit expose it in many text properties and accept `impl Into` at component boundaries. The tradeoff is immutability: changing the text means constructing a new value. Sharing helps while the value stays unchanged; it is not a promise of zero cost.
Three String owners compared with three SharedString owners For long heap-backed text, three String owners each point to a separate full text buffer. Three SharedString owners hold separate small handles that point to one shared text buffer with a reference count. Animated dashed arrows show the ownership links, not elapsed time. String::clone() Each owned clone copies the full text into its own allocation. View A · handle View B · handle View C · handle Text buffer A · full text Text buffer B · full text Text buffer C · full text 3 text buffers SharedString::clone() Each owner keeps a handle; the long text stays in one allocation. View A · handle View B · handle View C · handle One shared text bufferreference count: 3
Conceptual long-text example with three owners, not a byte-accurate benchmark. SharedString still stores a handle per owner and updates a reference count; short inline values copy their small bytes instead.
## How it stores text GPUI currently backs `SharedString` with `SmolStr`. The representation depends on how the value is created and on its length in **UTF-8 bytes**: | Input | Current storage | What cloning does | | --- | --- | --- | | `SharedString::new_static("Ready")` | References the static literal | Copies the small handle; no heap allocation | | `SharedString::from("Ready")` | Copies this short text inline | Copies the inline bytes; no heap allocation | | Longer dynamic text | Shared heap allocation | Clones a reference-counted handle; no copy of the text bytes | The current inline capacity is 23 bytes. Certain runs of newlines followed by spaces also use a special allocation-free static representation. These are implementation details, not a length limit or a promise that every `SharedString` avoids allocation. In particular, `SharedString::from("a long literal ...")` uses the normal conversion path; use `new_static` when you explicitly want static storage for a literal. Neither construction nor cloning interns strings or deduplicates equal values. For these current representations, `SharedString::clone()` does work bounded independently of the text length: a static clone copies a handle, a short inline clone copies a few bytes, and a heap-backed clone copies a handle and updates a reference count. It does not duplicate the bytes of a long heap-backed value. `String::clone()` instead allocates an independent buffer and copies its bytes. Constructing a fresh long `SharedString` can still allocate; formatting, conversion, reference-count updates, and eventually dropping shared copies still cost time. ## Ownership and access `SharedString` owns a usable value even when its input was a temporary `String` or a borrowed `&str`. Converting a nonstatic borrow copies its content as needed; the result does not borrow from the caller. You can therefore store it in an [Entity](./entity) or move it into a callback without managing the input's lifetime. ```rust use gpui_kit::SharedString; let title: SharedString = "Quarterly report for the product team".into(); let header_title = title.clone(); // Retain it for one UI owner. let tab_title = title.clone(); // Retain it for another owner. let borrowed: &str = title.as_str(); // Read without taking ownership. assert_eq!(borrowed, "Quarterly report for the product team"); assert_eq!(header_title, tab_title); ``` Here `.into()` converts the literal to an owned `SharedString`; for a literal that should use static storage, use `SharedString::new_static(...)` instead. Because this conversion uses the normal path and the title is longer than the current inline capacity, `header_title` and `tab_title` share its heap-backed text while unchanged. Their clones still update reference counts. `as_str()`, `AsRef`, and dereferencing provide a `&str` without creating another owner; that borrow lasts only as long as the `SharedString` it came from. `SharedString` cannot be edited in place. If a task truly needs a mutable construction or editing buffer, use a local `String` and convert it once when the result is ready. Converting an owned `String` to a long `SharedString` can still copy into shared storage; do not assume the `String` buffer is reused. ## Convert at the ownership boundary Choose the operation according to who needs the text next: | Source and next use | Pattern | Ownership result | | --- | --- | --- | | Fixed literal retained by UI | `SharedString::new_static("Ready")` | Owns a value backed by static text | | Temporary `&str` retained by UI | `SharedString::from(text)` | Owns a value independent of the borrowed source | | Finished `String` retained by UI | `let label: SharedString = text.into();` | Moves the `String` into conversion; it may still copy or allocate | | Existing `SharedString`, both owners need it | `let label = title.clone();` | Both keep owned values; long heap text stays shared | | Existing `SharedString`, only the receiver needs it | `Label::new(title)` | Moves the value; no extra clone | | Existing `SharedString`, callee reads only | `read_text(title.as_str())` | Borrows `&str` for the call | For a builder taking `impl Into`, passing `&title` also works because GPUI implements conversion from `&SharedString` by cloning it. Passing `title.as_str()` instead converts borrowed text into a **new** `SharedString`; use `title.clone()` (or `&title`) when you mean to share the existing value. If an API specifically requires `String`, `title.to_string()` creates a separate mutable string; only do this at that API boundary. ```rust use gpui_kit::SharedString; use gpui_kit::component::label::Label; let title = SharedString::new_static("Downloads"); let heading = Label::new(title.clone()); // Keep title for another owner. let tab = Label::new(title); // Last use: move it. ``` The `heading` and `tab` values each own their text. The move makes `title` unavailable afterward; clone first only where another owner still needs it. ## From an API response to a View Design immutable text fields in API response snapshots as `SharedString` by default. Deserialize a JSON `title` directly into that type with Serde, then carry the response into application state. If the UI needs a formatted title, derive a separate presentation value when the response arrives instead of changing the transport response: ```rust use gpui_kit::SharedString; use serde::Deserialize; #[derive(Deserialize)] struct UserResponse { title: SharedString, } struct Workspace { response: UserResponse, display_title: SharedString, } impl From for Workspace { fn from(response: UserResponse) -> Self { let display_title = format!("Profile: {}", response.title).into(); Self { response, display_title } } } ``` Moving `response` into `Workspace` keeps its original `title` without cloning it. The separate `display_title` is computed once here; nested Views and components can clone either value when they need owned text. For long heap-backed text, those later clones share the value's text allocation rather than copying its bytes; short text is stored inline. Initial JSON parsing still costs work: the current `Deserialize` implementation reads a `String` and constructs a `SharedString` from it, which may copy or allocate. `Serialize` writes the text as an ordinary string; sharing is an in-memory property, not part of the JSON value. `format!` also constructs a new value for the derived title, so derive it when data changes rather than on every render. Use a mutable text buffer only when the response text truly needs editing; an ordinary read-only response field need not start as `String`. ## Use it in GPUI Kit GPUI Kit component builders often accept `impl Into`, so callers can pass a literal or an existing `SharedString`. For example, [`Button::label`](../component/button) and [`Label::new`](../component/label) accept that form: ```rust use gpui_kit::*; use gpui_kit::component::{button::Button, label::Label}; let title = SharedString::new_static("Downloads"); let heading = Label::new(title.clone()); let button = Button::new("open-downloads").label(title); ``` For text kept in a view and used on many renders, store one `SharedString` in the view and clone it into each element. The view's [Context](./context) is available alongside `Window` in `Render::render`: ```rust use gpui_kit::*; use gpui_kit::component::label::Label; struct Header { title: SharedString, } impl Render for Header { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { Label::new(self.title.clone()) } } ``` The clone gives the new element its own handle while the view retains its copy. For a long title, it does not copy all the characters on every render. Constructing the long title anew with `format!(...).into()` inside every render would still format and allocate on every render. Keep stable text in the state that owns it, and replace the value when it changes. ## Choose text for its lifetime For text that a UI element, View, callback, or several owners will retain, prefer `SharedString`. For a fixed literal that needs ownership, `SharedString::new_static` explicitly uses static storage. Component builders that accept `impl Into` let callers pass these values without building a fresh `String` first. When an API only reads text during the current call, pass `&str` or borrow from an existing `SharedString` with `.as_str()`. This creates no additional text owner. Avoid introducing `String` for ordinary UI text. Use it when text actually needs **mutable construction or editing**, such as assembling a draft, and convert the finished value at the ownership boundary: ```rust let mut draft = String::from("Quarterly report"); draft.push_str(" for the product team"); let title: SharedString = draft.into(); ``` This `.into()` consumes the `String` and builds a `SharedString`; it may copy or allocate and does not promise to reuse the original buffer. GPUI Kit's [`InputState`](../component/input) shows why an editing buffer and an exposed value need not have the same type: it stores editable text in a `Rope`, while `value()` materializes a `SharedString` snapshot on each call. Avoid repeatedly calling `value()` just to read an unchanged field. Ordinary UI code often needs no `String` at all, but it remains useful for real construction and editing work. **Source snapshot (2026-09-24)** In GPUI Kit's production library source under `crates/{kit,base,component,assets}/src`, **34 named struct storage fields** have a type containing `String`, versus **375** with a type containing `SharedString`. The `String` fields include valid exceptions such as editable text, search state, and theme schema data. This supports the default for retained UI text; it does not mean the repository contains only 34 uses of `String`. To reproduce the count, scan `*.rs` named struct bodies under those four `src` directories, skip test-only files and content after `#[cfg(test)]`, then count field types containing the distinct Rust tokens `String` and `SharedString`. This is a lightweight source scan rather than a Rust semantic parse: it excludes local variables, function signatures, and enum fields, includes both native and WebAssembly `cfg` variants present in source, and says nothing about runtime allocations. ## Common compiler errors and surprises | Symptom | Cause and fix | | --- | --- | | “use of moved value” after `Label::new(title)` | The builder consumes its argument. Pass `title.clone()` when the view or another element still needs the value; move it only on the last use. | | “borrowed data escapes” or a callback must be `'static` | A callback cannot retain `title.as_str()` borrowed from a view. Clone the `SharedString` into the `move` closure, then call `.as_str()` inside the closure if needed. | | A method expects `&str`, but it receives `SharedString` | Pass `title.as_str()` (or `&title` where deref coercion applies). This borrows without copying text. | | `push_str` or another mutation method is unavailable | `SharedString` is immutable. Build or edit in a `String`, then convert the completed text into `SharedString`. | | `.into()` has an ambiguous destination type | Specify it: `let title: SharedString = source.into();` or call `SharedString::from(source)`. | The callback case follows the same ownership rule as an element: the closure must own anything it keeps after `render` returns. For example, `let title_for_click = self.title.clone();` before an `on_click(move |_, _, _| { /* use title_for_click here */ })` gives the handler an independent value. Clone once when building the callback, rather than converting `self.title.as_str()` into a fresh value on each render. ## Related: `Cow` Rust's [`Cow<'a, str>`](https://doc.rust-lang.org/std/borrow/enum.Cow.html) can avoid a copy by borrowing existing text, but its `Borrowed` variant is tied to the source's lifetime. Its `Owned` variant contains a `String`; cloning a nonempty owned value copies those bytes. Calling `to_mut()` on a borrowed value copies it into an owned `String` before editing: ```rust use std::borrow::Cow; let mut text: Cow<'_, str> = Cow::Borrowed("Ready"); text.to_mut().push('!'); // Now owned and editable. ``` `SharedString` is an independently owned, immutable value with static, inline, or shared heap storage. Cloning a long heap-backed value shares its bytes; changing the text requires a new value. It is not an alias for `Cow` and does not offer `Cow`'s write-on-mutation behavior. --- # View Cache Source: /docs/view-cache GPUI keeps application state in [entities](./entity), but normally builds a new [element tree](./element) for each window draw. Keeping an `Entity` alive does not keep its last element tree alive. When a window redraws, an ordinary child view may run `Render::render` again even if its own state did not change. A cached view instead reuses selected records from the last frame, including its input handlers. There is no public type named `ViewCache` to construct. The view-cache API in current GPUI is **`Entity::cached(style)`** (or **`AnyView::cached(style)`**). It creates a cached view boundary for one entity-backed subtree. GPUI Kit also uses other, narrower caches; they solve different costs. | Mechanism | Reuses or avoids | Lifetime and owner | | --- | --- | --- | | `Entity::cached(style)` | A clean view's render, child layout/prepaint, and paint work | GPUI's window cache, keyed by the entity view and its element path | | [`Window::use_keyed_state`](./window) | Small state or computed data used by a rebuilt element | Window element state under a stable [`ElementId`](./element_id) | | A model-owned cache | A derived value, measurement, or drawing resource | An owning `Entity`, with application-defined invalidation | | `VirtualList` | Building offscreen rows | Its visible range; a separate scroll handle keeps scroll position | These mechanisms can be combined. For example, a cached panel may contain a virtual list, and a visible chart row may reuse tessellated paths. A virtual list does **not** cache all of its row views, and `use_keyed_state` does **not** skip a view's `render`. Think of three separate questions: **What requests a redraw?** `cx.notify()` reports changed Entity output, and `window.refresh()` requests a full window refresh. **What survives a rebuilt element?** An Entity owned by the application or state stored under an element key can survive. **What work can be skipped in the new frame?** Only a clean cached View can replay its recorded subtree; a keyed state value or path cache merely supplies reusable data to work that still runs. A cached scene is GPUI's previous-frame paint record, not an application-owned image or a separately addressable texture. ## Cache an entity-backed subtree This example uses the existing `hello_world` package. Replace `examples/hello_world/src/main.rs` with the following code, then run `cargo run -p hello_world`. The two buttons make the cache boundary observable without a profiler: the terminal prints whenever either View's `render` runs. ```rust use gpui_kit::base::StyledExt; use gpui_kit::component::button::Button; use gpui_kit::*; struct Workspace { panel: Entity, parent_clicks: usize, } struct ResultsPanel { panel_clicks: usize, } impl Render for ResultsPanel { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { println!("panel render: {}", self.panel_clicks); div() .v_flex() .gap_2() .size_full() .child(format!("Panel clicks: {}", self.panel_clicks)) .child(Button::new("panel-click").label("Update panel").on_click( cx.listener(|this, _, _, cx| { this.panel_clicks += 1; cx.notify(); }), )) } } impl Render for Workspace { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { println!("workspace render: {}", self.parent_clicks); div() .v_flex() .gap_2() .size_full() .child(format!("Workspace clicks: {}", self.parent_clicks)) .child(Button::new("workspace-click").label("Update workspace").on_click( cx.listener(|this, _, _, cx| { this.parent_clicks += 1; cx.notify(); }), )) .child( div().w_full().h(px(180.)).child( self.panel .clone() .cached(StyleRefinement::default().size_full()), ), ) } } fn main() { gpui_kit::application().run(|cx| { gpui_kit::init(cx); gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| { let panel = cx.new(|_| ResultsPanel { panel_clicks: 0 }); cx.new(|_| Workspace { panel, parent_clicks: 0, }) }) .expect("failed to open window"); }); } ``` After the first frame, click **Update workspace** several times. `Workspace` renders again, while `ResultsPanel` can be replayed without another `panel render` line. **Update panel** still works inside the cached area: it changes the child Entity, calls `cx.notify()`, and produces another `panel render` line with the new count. The exact number of surrounding window frames is platform and input dependent; compare the two render logs rather than expecting one draw per click. `panel` is created once with `cx.new(...)` and retained by `Workspace` (see [Context](./context)). Creating a fresh entity inside `render` gives it a new identity and loses both its state and its warm cache. The `style` argument is the cached view's **outer layout contract**. GPUI lays out that box before deciding whether to reuse its contents; it cannot ask an unrendered subtree for an intrinsic size. The example's fixed-height parent supplies a definite box, while the cached child fills it. For content-sized views, embed the entity normally with `.child(self.panel.clone())`. `AnyView::cached(style)` has the same behavior when a parent stores a type-erased panel, as GPUI Kit's dock does. [`RenderOnce`](./render-once) values and arbitrary `ViewElement`s cannot opt into this API: they have no entity notification contract to invalidate a frozen subtree. The cache belongs to the **window frame**, not to the `Entity` itself. The same entity rendered in two windows has separate frame records. GPUI locates the record by the View's entity-derived element ID within its keyed ancestor path, so moving a View to another path does not carry its old recorded subtree with it. Retaining an Entity preserves its state; retaining its position and a clean cache key makes frame reuse possible. [GPUI Kit's dock panel](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/dock/tab_panel.rs) embeds an `AnyView` with `cached(StyleRefinement::default().absolute().size_full())` in a sized tab area. ## When does GPUI reuse it? On a cache hit, GPUI does not call that child view's `render`. It replays the earlier subtree's prepaint and paint records into the current frame. These include hitboxes, dispatch nodes, focus state, mouse listeners, input handlers, and the drawing scene, so the cached area remains interactive. The original element values are not kept as a permanent tree. Event handlers in a reused subtree run again on input; only the work to rebuild and paint them is skipped. For a hit, the entity must remain clean, and the cached view's **bounds, content mask, and inherited text style** must match the recorded frame. GPUI also bypasses reuse during a forced refresh; inspector picking can disable caching. If any of these conditions fail, it renders and lays out the child again, then records a new cache entry. | Change | What happens at this boundary | | --- | --- | | A sibling or parent redraws, while this child and its box stay unchanged | The parent still builds its tree; the cached child can be replayed. | | The child changes and calls `cx.notify()` | GPUI marks the view dirty, rebuilds it, and updates the cache. | | A descendant view changes and notifies | GPUI marks its ancestor view path dirty so the cached boundary is rebuilt. | | The cached box resizes, clips differently, or inherits a different text style | GPUI misses this cache entry and rebuilds it. | | The child is removed or its identity/path changes | Its old cached subtree cannot be used at the new position. | Keep state mutations in event handlers or [tasks](./task) and call `cx.notify()` on the affected entity when its visible output changes. If the child's output depends on a [Global](./global), observe that global and notify the child; changing a global by itself does not dirty cached readers. If it depends on another entity, use an observation or another explicit invalidation path. Do not assume that a parent re-render alone will refresh a clean cached child. Use `window.refresh()` for an intentional full refresh, not as a normal state-update mechanism. An observation must target the View whose cached output depends on the source. For example, if a panel reads a model Entity during `render`, a model update alone does not tell GPUI that the panel's *View* is dirty. Arrange for the panel to observe the model and call the panel's `cx.notify()` when the displayed value changes. The same rule applies to theme, locale, or other Global values read inside the boundary. A cache hit intentionally skips both the reads and the closures that would have been built by that render, so a fresh value captured by the parent is not sufficient to refresh it. Caching has a scope: it can skip work *inside* the child boundary, but the window still draws a frame and the parent still runs as needed. The first draw and every cache miss pay the ordinary render/layout/paint cost. Use it where a measured subtree is expensive and often stays clean while surrounding content changes. ## Element state is a different cache GPUI recreates value-like elements on subsequent renders. If an element needs a little state across consecutive frames, `Window::use_keyed_state` stores an `Entity` under the current element path plus a supplied key. It also observes that state entity and notifies the current View when the state changes. The state survives while that path is accessed on successive frames, including frames where a cached subtree replays its element-state accesses; it is released when the path disappears and no other strong handle keeps it alive. `Window::use_state` uses a call-site key, which is suitable only where that location uniquely identifies the state. An `ElementId` derived from stable domain data matters for repeated or reorderable items: changing an ID resets the state; reusing one for unrelated siblings risks collision. GPUI Kit's `Plot` path cache is a concrete example. A plot and each `Line` are rebuilt as values, so a path held on a `Line` would disappear with that value. `PathCaches::for_paint("lines", window, cx)` stores caches in keyed window state under the plot's element ID. A `ShapeKey` covers projected points and geometry-affecting stroke settings; `PathCache::get` tessellates only when that key changes. The path is built relative to zero and translated to the current origin for painting, so moving a chart can reuse the geometry. A color change can be applied while painting without rebuilding unchanged path geometry. That cache saves **path construction**, not the plot view's `render` or the current frame's paint submission. Its slots are positional: when series can reorder, map stable series identities to slots or ensure the shape key safely invalidates the changed slot. See [Paint](./paint#plot-a-value-like-element-uses-keyed-window-state) for the source-level walkthrough. The key must include every input that changes the cached **geometry**: projected points, size, stroke width, curve style, and any other tessellation setting used by the builder. It need not include a paint-only color when that color is selected at paint time. A key that omits a geometry input can draw stale paths; a key that includes the absolute origin forfeits reuse while scrolling. GPUI Kit's [`PathCache::get` and `ShapeKey`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/plot/path_cache.rs) make this dependency explicit. Their `PathCaches` owner is the window's keyed element state, so removal of the plot path for a frame ends that window-owned cache even if a later plot uses the same ID. ## Virtualization avoids work instead of replaying it For a long collection, caching a view containing every row still leaves an expensive first render and invalidations. GPUI Kit's [`VirtualList`](../base/virtual-list) accepts item sizes and calls its render closure for the visible range (with a small overdraw); it may also render one representative item for cross-axis measurement. It never constructs most offscreen row elements in that frame. Its `VirtualListScrollHandle` is retained separately in the owner so scrolling survives element rebuilds. ```rust use std::rc::Rc; use gpui_kit::*; use gpui_kit::base::{v_virtual_list, VirtualListScrollHandle}; struct Row { id: u64, name: SharedString, } struct ResultsList { rows: Vec, sizes: Rc>>, scroll: VirtualListScrollHandle, } impl Render for ResultsList { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_virtual_list(cx.entity(), "results", self.sizes.clone(), |this, range, _, _| { range .map(|ix| { div() .id(("result", this.rows[ix].id)) .child(this.rows[ix].name.clone()) }) .collect::>() }) .track_scroll(&self.scroll) .size_full() } } ``` The closure should read prepared data; avoid sorting, loading, or creating one long-lived entity per row in it. Give repeated interactive rows stable IDs from the row data. Virtualization and cached views address different dimensions: how **many** elements are made, and whether an **unchanged subtree** is replayed. ## Choose the smallest useful boundary Start with ordinary entities and declarative rendering. If profiling shows a stable panel being rebuilt because nearby UI changes, place `cached(style)` around that panel and give it a reliable layout box. If a drawing operation remains costly on every visible frame, cache its derived geometry with explicit keys and invalidation. If the cost grows with collection length, virtualize. Do not add a broad cache to hide render work that should have been moved out of `render` or data that should have been retained by an entity. The [Render](./render) guide explains when a View creates a new tree. ## Verify behavior before keeping a cache Count calls to the child View's `render` in a focused test or profiler, and compare these cases after the first draw: 1. Redraw an unrelated sibling while the cached box stays fixed. The child should not render again; its controls should still receive input. 2. Change the child's displayed state, call its `cx.notify()`, and verify that the child renders again and the new content appears. 3. Change a dependency outside the child, such as a model or Global, through its real observation path. Verify that the child rebuilds. If the parent changes but the child does not, the missing dependency notification is a correctness bug. 4. Resize or move the boundary, change its clipping or inherited text style, and check that the expected miss occurs. Moving the box changes its bounds; scrolling a plot may leave its **path geometry** cache warm while the View scene still repaints. GPUI Kit's [cached text-selection test](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/text/window_selection.rs) uses a render counter and checks selection during replayed frames. Use a release/profile build for performance measurements: an extra cache boundary has bookkeeping cost, and a subtree that changes every frame may not benefit. For stale output, inspect Entity notifications and external dependencies first; for unexpected misses, inspect entity identity, keyed ancestor path, bounds, content mask, and inherited text style. --- # Testing Source: /docs/test This guide covers testing GPUI Kit applications and GPUI behavior. Choose the test level from the behavior you need to verify: - Use ordinary Rust `#[test]` for pure data transformations, validation and state transitions. - Use `#[gpui_kit::test]` and `TestAppContext` for [entities](./entity), [actions](./action), [subscriptions](./event) and async [tasks](./task), creating a window when needed. - For UI integration tests, render the production application view, dispatch [events](./event) through `gpui_kit::test`, and check control state, layout and the application result. - Use the separate offscreen renderer for pixel checks, and retain native-window and platform integration tests for those behaviors. GPUI Kit exposes its types and `#[gpui_kit::test]` through the Kit root; applications do not need an additional GPUI dependency. In test modules, import the types you use explicitly: `use gpui_kit::*;` also imports the GPUI `test` macro and can shadow Rust’s ordinary `#[test]`. The complete example below uses explicit imports. ## What is a UI integration test? A **UI integration test** renders real components or an application view in a headless window, simulates clicks, keyboard input and scrolling, then verifies state, focus, layout and application callbacks. For example, a Checkbox test can verify that clicking changes the owner's value and that a disabled Checkbox rejects the same interaction. `#[gpui_kit::test]` runs the test and provides its GPUI context. `gpui_kit::test` supplies the tools to operate and inspect the UI: ```rust use gpui_kit::{TestAppContext, Window}; use gpui_kit::test::TestWindowExt; ``` Use these tests when a behavior depends on components working together, such as entering a value, saving a dialog and checking the result in the parent view. Find controls by [`ElementId`](./element_id), dispatch real GPUI events and assert the outcome with ordinary Rust assertions. This guide covers in-process behavior and layout automation. Element snapshots do not inspect pixels or launch your packaged application. For pixel checks, use GPUI’s separate offscreen renderer as described below. Keep native-window, platform integration and visual checks alongside these tests when those are part of the behavior you need to verify. ## Set up a test project UI testing is part of `gpui-kit`, behind its `test-support` feature. The example below uses a Kit source checkout containing these helpers. There is no additional testing crate, GPUI fork or Cargo patch to install. Prepare the platform dependencies described in [Installation](/docs/installation). Headless tests still compile GPUI's native dependencies. For a standalone test project next to the checkout, use this layout: ```text workspace/ gpui-kit/ ui-tests/ Cargo.toml tests/ui.rs tests/common/mod.rs ``` Put the following in `ui-tests/Cargo.toml`: ```toml [package] name = "ui-tests" version = "0.1.0" edition = "2024" publish = false [dev-dependencies] gpui-kit = { path = "../gpui-kit/crates/kit", features = ["test-support"] } ``` For an existing application, add this development dependency to its package. Its normal `gpui-kit` dependency must resolve to the same source and version; features then unify for tests. Keep `test-support` in development dependencies so ordinary application builds do not enable observation. An application that uses the component crate directly can enable `gpui-component/test-support`. ## A complete test The source below is the repository's compiled `tests/ui.rs`. It initializes the component library and retains the input state on the view, as a real application should. Its `mod common;` line loads the companion `tests/common/mod.rs` fixture. That fixture calls the public `gpui_kit::open_window` with explicit 640 × 480 bounds, wraps the view in `gpui_kit::base::Root`, and returns both the window handle and the view entity. It is test setup, not an additional library dependency. The test enters a Unicode name, edits it with Backspace, clicks Save, checks the status node's AccessKit role and label plus layout, and verifies the saved application value. The same source is compiled and run in GPUI Kit's integration suite. These assertions do not verify what a screen reader actually announces; check that in the running app on each target platform. <<< ../../crates/kit/tests/ui.rs{rust} For the standalone layout above, copy **both** files from the Kit checkout; `ui.rs` alone will fail at `mod common;`. From `ui-tests/`, run: ```sh mkdir -p tests/common cp ../gpui-kit/crates/kit/tests/ui.rs tests/ui.rs cp ../gpui-kit/crates/kit/tests/common/mod.rs tests/common/mod.rs cargo generate-lockfile cargo test --test ui --locked ``` Commit `Cargo.lock` with the test project. In your application, import its production view and constructor instead of copying the example's `Profile`; keep the same window setup and interaction pattern. A separate test view can drift from the application. Inside the GPUI Kit checkout, run this exact test with: ```sh cargo test -p gpui-kit --features test-support --test ui --locked ``` ## Choose stable test targets With `test-support` enabled, these controls register their existing native element; observation adds no layout container: | Control | Native properties beyond geometry and visibility | | --- | --- | | Button | Accessibility label, focus scope | | Input | Non-sensitive accessibility value, label, focus scope | | Checkbox | Checked, indeterminate, label, focus scope | | Switch / Toggle | Checked, label, focus scope | | Radio | Checked, selected, label, focus scope | | Tab | Selected, label | | Command | Native option selected state, root focus scope and row bounds | | Combobox | Native expanded state and focus scope; selection verified through events and retained state | | Select | Accessibility value (including title prefix), expanded, focus scope | | ListItem / SidebarMenuItem | Geometry; additional state only when provided by native accessibility properties | | Accordion | Expanded trigger; header and panel bounds | | Tree | Native tree/item roles, label, selected and expanded; root focus scope | | Table / DataTable | Native table parts; DataTable row selection and root focus scope | | DatePicker / Calendar | DatePicker displayed date value, expanded and focus scope; calendar item labels and bounds | | Slider | Track and thumb bounds; numeric accessibility values are not exposed by `ElementSnapshot::value()` | | Stepper | Step and trigger bounds; verify the resulting application content | | Dialog / Sheet | Host focus scope and surface bounds; child controls retain their own properties | | Menu | Item label and selection, menu focus scope and submenu bounds | | Notification | Alert role and bounds; close button uses normal Button observation | | Dock | Area/group/content bounds and focus scopes; tabs retain native selection | Use constructor IDs where available. Input and Select accept `.id("name")`; their defaults include the state entity ID. Tabs inside a TabBar use their index as ID. Select's existing `"input"` child identifies its trigger: `window.within("language").click("input", cx)`. Native divs opt in without supplying a second description of their state: ```rust use gpui_kit::TestSupportExt as _; let target = div().id("details").test_support().child(content); ``` `TestSupportExt` is available without `test-support`; in normal builds `.test_support()` returns the original native element with its exact type. With the feature enabled, it preserves identity, layout, events and accessibility, without adding a layout container. Repeated observation keeps one registration. Call `.test_support()` before `.track_focus(&handle)` so the wrapper sees the actual binding. Kit controls do this internally. `focused()` checks whether that focus scope contains keyboard focus, including the nested editor inside an Input frame. If GPUI advertises focus support but the binding was not observed, `focused()` panics with a diagnostic instead of silently returning `None`. This catches `.track_focus(&handle).test_support()`; implicit `.focusable()` handles are also unavailable, so use an explicit handle. This diagnostic is best effort: it relies on the native accessibility `Action::Focus`. A custom element that omits this action can still return `None` for a missed binding. `None` means neither a binding nor an advertised focus action was observed; it does not prove that the element cannot receive focus. Debug output marks a detected missed binding as `focused: ` without panicking. Snapshots read native `role`, `aria_toggled`, `aria_selected`, `aria_expanded`, `aria_label` and `aria_value`. There are no `TestProps` or hand-supplied fallback values. Input uses its existing accessibility-value path in tests, with the same masking and sensitive-content restrictions. Select's `value()` is its accessible value, including any title prefix; it is not a selected item ID. `label()` means accessibility label, not visible text. `value()` means accessibility value, not pixels. These properties can still contain component bugs. Do not add `aria_label` or `aria_value` solely to make a visual assertion pass. The example's Status role and label serve the production accessibility announcement. Arbitrary child text is not discovered automatically, and there is no `text()` shortcut that substitutes model strings for rendered text. `disabled()` returns `Some(true)` only when the native node exposes its disabled flag; otherwise it returns `None`. GPUI's div API currently cannot expose a known enabled state this way. Test disabled behavior by attempting the interaction and checking that the application result did not change; do not interpret `None` as enabled. There is no reliable positive enabled-property assertion through this API. To verify that a button accepts activation, exercise it and assert its intended result, for example: ```rust window.click("save", cx); assert_eq!(window.find("status").label(), Some("Saved: Ada")); ``` Use the actual expected application result; `assert_ne!(button.disabled(), Some(true))` or `button.disabled().is_none()` does not establish that activation works. IDs only need to be unique within their GPUI identity scope. Window-wide queries panic on ambiguity. Use existing scopes without adding test containers: ```rust window.within("toolbar").click("save", cx); window.within("dialog").click("save", cx); let save = window.within("dialog").within("footer").find("save"); assert!(save.visible()); ``` A parent scope need not itself be observed: its ID is part of its observed children's GPUI paths. `within` requires a unique painted path. Composite row IDs such as `("row", record_id)` preserve record identity after reordering. ## Interact and assert Import `gpui_kit::test::TestWindowExt` for the following methods: | API | Behavior | | --- | --- | | `window.find(id)` | Requires an `ElementSnapshot` from the last completed frame; missing targets panic with registered paths and troubleshooting hints. | | `window.try_find(id)` | Returns `None` when absent; ambiguity still panics. | | `window.click(id, cx)` | Native mouse move/down/up at the target center. | | `window.click_at(id, offset, cx)` | Click at a pixel offset from the target's top-left corner, useful for partial clipping. | | `window.right_click(id, cx)` / `double_click(id, cx)` | Native right-button or two-click sequences. | | `window.hover(id, cx)` | Move the pointer without pressing a button. | | `window.scroll(id, delta, cx)` | Native wheel event; `ScrollDelta` retains GPUI units and sign. | | `window.drag_to(from_id, to_id, cx)` | Resolve both targets and drag between their centers using native hit testing. | | `window.drag(from, to, cx)` | Left-button drag between window-local points, through GPUI drag creation and drop hit testing. | | `window.press("backspace", cx)` | Native key-down/key-up for a named key or shortcut using GPUI's keystroke parser. | | `window.input(text, cx)` | Per-character text input to the current focus; does not focus or replace the whole value. | Scoped queries support `find`, `try_find`, nested `within`, `click`, `click_at`, `right_click`, `double_click`, `hover`, `scroll`, `drag_to`, `press` and `input`. `drag_to` resolves both IDs within the scope. For cross-scope drags or custom offsets, query the targets and pass window-local points to `window.drag`. ```rust let mut dialog = window.within("dialog"); dialog.click("name", cx); dialog.input("Ada", cx); dialog.press("backspace", cx); dialog.hover("help", cx); ``` Scoped keyboard operations do not move focus. They require an observed focus binding inside the scope; otherwise they panic before dispatch. `input` checks before every character, so a handler moving focus outside the scope cannot redirect the remaining text. Use `window.press` for deliberate window-wide shortcuts. For custom input controls, register the actual focus-bearing element with `.id("editor").test_support().track_focus(&focus_handle)`, using its real focus handle. An unobserved input, or an observed outer container without a tracked handle, cannot satisfy this check even if keyboard focus is physically inside the scope. Window-level `input` and `press` dispatch to the current focus without this scope guarantee. Scoped input shares the window input loop: one initial refresh, then one refresh per character, with scope checks against each completed frame. `ElementSnapshot` is an owned, immutable record of a completed paint. Its readers are `role()`, `path()`, `bounds()`, `visible()`, `focused()`, `disabled()`, `label()`, `value()`, `checked()`, `indeterminate()`, `selected()` and `expanded()`. Focused, disabled, checked, indeterminate, selected and expanded readers return `Option`: `None` means unavailable, not false. Label/value are also optional. Re-query after interactions: ```rust let before = window.find("agree"); window.click("agree", cx); assert_eq!(before.checked(), Some(false)); // The original frame. assert_eq!(window.find("agree").checked(), Some(true)); // The new frame. ``` Assert native properties and application results together. Checking saved model state or an emitted result is a useful part of an integration test; it should not replace verifying the relevant visible control state. Command presses, including Enter, must not inject newline text through an IME callback. Text input does not model complete OS IME composition. Masked inputs report no value; verify sensitive results through application state. ## Complete the frame before querying Call `window.render_frame(cx)` before the first query and after direct external state/focus changes or resizing. Interaction helpers refresh around synchronous dispatch, including `press`. They cannot finish deferred callbacks while the surrounding window update is still borrowed. ```rust cx.update_window(handle.into(), |_, window, cx| { window.render_frame(cx); window.click("name", cx); window.input("Ada", cx); window.press("backspace", cx); assert_eq!(window.find("name").value(), Some("Ad")); }).unwrap(); ``` Use `TestAppContext::update_window`; typed `WindowHandle::update` already borrows the root entity and cannot safely redraw it in the same callback. For asynchronous work or deferred selection commits, use an async `#[gpui_kit::test]` and wait **outside** the window update: ```rust use gpui_kit::test::TestAppContextExt; use std::time::Duration; cx.wait_for(handle.into(), Duration::from_millis(200), |window, _| { window.try_find("result").is_some_and(|snapshot| snapshot.visible()) }).await; ``` `wait_for` refreshes frames and polls every 10 ms using GPUI's test executor clock, with registered paths in timeout errors. This is a bounded condition wait, not an OS event loop or a network-service simulator. Provide controlled responses for external dependencies. A parked executor alone does not imply that timers or deferred work have completed. GPUI `dispatch_action` queues work. Complete the dispatch (for example by leaving `update_window` and running `cx.run_until_parked()`) before editing values that the action will read. Use `wait_for` for the resulting state or timer completion. Legacy non-synced GPUI `Animation` uses wall-clock `Instant`; advancing the test clock does not finish it. The Sheet/Notification geometry tests wait their actual entrance durations before asserting final bounds. Base motion can instead honor the public `cx.set_reduce_motion(true)` preference when testing final disclosure geometry. Snapshots never update in place. Cached views keep their painted facts until invalidated. Unmounted targets disappear after the frame releasing their element state; virtualized rows become queryable when painted after scrolling. ## Coverage and failure cases The repository covers the following component workflows through real input, native properties and resolved bounds. These are concrete regression contracts, not a claim that every option or combination of every component has been exhaustively tested. | Suite | Behavior exercised | | --- | --- | | `test_macro.rs` | Published `#[gpui_kit::test]` sync/async compatibility alongside ordinary Rust tests; the independent Kit-only recipes package runs the same contract | | `input.rs` and `input/` | Input, Textarea and Editor editing, clipboard, selection, history, read-only transitions, Unicode, multiline viewport behavior, search/replace, completion acceptance and retained state across renders | | `input_focus.rs` | Repeated Tab/Shift-Tab traversal with passive addons, addon button focus and activation, and Textarea/Editor body-click focus followed by editing | | `search.rs` | Command disabled-item skipping, wraparound, Unicode keywords, empty results, Action dispatch and original-index callbacks, two-stage Escape; Combobox search, single/multi selection, clearing, empty-result recovery, disabled behavior and exactly one Confirm on close | | `disclosure.rs` | Accordion exclusive expansion/collapse and actual panel geometry; Stepper content navigation; disabled disclosure/steps; Slider track click, thumb drag and disabled behavior | | `collections.rs` | Tree pointer expansion, keyboard collapse/expansion and selection; DataTable row selection, keyboard virtualization and wheel scrolling | | `date_picker.rs` | Opening, exact preset/day selection, month navigation, clearing, Escape and disabled behavior | | `overlays.rs` | Dialog validation → scoped Input → save → Notification; hover-revealed close; auto-dismiss timer; Dialog/Sheet Escape and focus restoration; surface bounds | | `menu.rs` | Disabled items, keyboard confirmation, Escape, focus restoration, submenu hover and nested item activation | | `dock.rs` | Tab selection/reordering, cross-group drag/drop, zoom and restored split geometry | The existing form, Select, HoverCard, virtual-list, pointer, lifecycle and isolation suites remain in place. Pure presentation components need geometry or pixel assertions, not invented interaction state. Custom parts register their existing native elements; unsupported properties remain unavailable, with no manual test-only override. The [Input regression example](https://github.com/MohsenDastaran/uni-kit/tree/main/crates/kit/tests/input) shows how to turn a manual editing sequence into a repeatable UI test. From the repository root, run both editing and focus targets, or select one workflow: ```sh script/test-input # Complete Input gate: Base, Component and Kit workflows (Bash). cargo test -p gpui-kit --features test-support --test input --test input_focus --locked cargo test -p gpui-kit --features test-support --test input --locked -- history::paste_is_atomic_and_separate_from_surrounding_typing --exact cargo test -p gpui-kit --features test-support --test input_focus --locked -- reverse_tab_cycles_three_inputs_with_passive_addons --exact ``` Append `-- --list` to the combined command to list cases without executing them. These commands are reproduction instructions, not recorded passing results. Report the revision, platform, command and observed result for each run. Example workflows include typing → paste → typing → Undo/Redo, Textarea Enter submission versus Shift-Enter insertion, and Editor completion → acceptance → Undo. Each checks fresh snapshots plus public state or owner events where needed. Completion responses come from a deterministic provider, not a live language server. The suite also exercises the public IME handler protocol (preedit, UTF-16 ranges, commit/cancel and history), multi-cursor editing, folding, provider cancellation and failure. Its operation matrix is the review checklist for ordinary input changes; add a regression for the changed interaction and require platform CI. The separate `input_focus` target exercises focus callbacks after window updates. These cases do not establish full OS IME, accessibility action, system clipboard or pixel correctness; use the corresponding platform checks for those boundaries. Views that open dialogs, sheets or notifications through `WindowExt` need a `Root` as the window's root view. `Root` always renders all three overlay layers above application content, including cached views. No manual layer mounting is needed. Use `within` for repeated controls. A Sheet's `"sheet"` host scope contains its `"sheet-content"` surface; Dialog's `"dialog"` scope contains the layer-indexed surface. Nested menus also contain a `"popup-menu"`, so retain the resolved parent scope when opening a submenu, or query under `"submenu"`. Do not assume a previously unique ID remains unique after another layer opens. Missing or invisible click targets panic. Disabled controls receive real events and decide whether to respond. Visibility combines geometry, viewport/content clipping and the target's computed style; it does not detect pixel occlusion. Overlays can intercept clicks. `click_at(id, point(px(10.), px(10.)), cx)` can choose a visible portion of a clipped target without bypassing hit testing. Test instrumentation is feature-gated, so the test build is not byte-identical to a production build. The transparent wrapper adds no layout box, but visibility inspection computes style an additional time; style/drag predicates must not rely on call counts. GPUI does not expose inherited paint opacity from an unobserved ancestor. No GPUI fork or Cargo patch is used to bypass these limitations. On failure, check the reported paths, observation, completed frame, keyboard focus, clipping/overlays and asynchronous completion, in that order as relevant. | Symptom | First check | | --- | --- | | `mod common` cannot be found | Copy `tests/common/mod.rs` beside the included `tests/ui.rs`, or replace the fixture call with your application's window setup. | | `find` lists no matching path | Confirm `test-support`, the control's ID or `.test_support()`, and an initial `render_frame`. | | A query is ambiguous | Resolve an existing parent with `within`, then query its child ID. | | `focused()` reports a missed binding, or scoped `input` panics | Observe the element before `.track_focus(&handle)` and click the intended input before typing. | | An assertion still sees the old state | Query a fresh snapshot after a completed frame; for queued work, leave `update_window` and use `wait_for`. | | A visible target does not receive the click | Inspect clipping and overlay order; pointer helpers use native hit testing. | ## Verify rendering independently A correct value or checked flag does not prove the control was drawn correctly. GPUI exposes `HeadlessAppContext::with_platform`, `Window::render_to_image` and `HeadlessAppContext::capture_screenshot` for real offscreen images. The currently pinned platform crate supplies its headless renderer on macOS (Metal) only. Run this target on a Mac with Metal available: ```sh cargo test -p gpui-kit --features test-support --test rendering --locked ``` The target uses `test = false`, so the default Cargo command does not select it. The macOS CI job explicitly runs `--test rendering` as a required step, alongside the portable interaction suite. Linux and Windows run only the portable suite. Cargo supports this [explicit target selection](https://doc.rust-lang.org/cargo/commands/cargo-test.html#target-selection). It also uses `harness = false` because AppKit initialization requires the main thread; `--test-threads=1` would still run an ordinary Rust test on a worker thread. On other platforms it explicitly reports that pixel verification is skipped. Missing renderer support on macOS fails rather than substituting a fake image. The tests inject two defects into real Kit controls: a missing check-mark asset while `checked()` remains true, and transparent input text while `value()` remains correct. Images must differ from the working control, and repeated working checkbox renders must match. A separate native-event test disconnects a checkbox's change handler and checks that clicking cannot fabricate a checked result. These are sensitivity checks, not a complete golden-image suite. For application visual regression, compare images against reviewed expectations under controlled fonts, dimensions, theme, focus and animation state. State assertions and image assertions detect different defects; neither establishes packaged-app or full IME correctness. The executable rendering examples are in [`crates/kit/tests/rendering.rs`](https://github.com/MohsenDastaran/uni-kit/blob/testing/crates/kit/tests/rendering.rs). ## Run in CI The Kit repository runs the interaction/layout suite on macOS, Linux and Windows. The macOS job additionally runs the two Metal pixel checks; a failure fails the job. A minimal macOS workflow for a Kit checkout is: ```yaml name: UI tests on: [push, pull_request] jobs: test: runs-on: macos-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - run: ./script/bootstrap - run: cargo test -p gpui-kit --features test-support --locked - run: cargo test -p gpui-kit --features test-support --test rendering --locked ``` For an application repository, install its platform dependencies and run `cargo test --test ui --locked` in its test package instead. Make the pinned Kit source available at the paths declared in its manifest. Add Linux and Windows jobs using the same system setup as your normal native builds. The repository suite also covers read-only/disabled inputs, focus changes, cached views, mount/unmount, window isolation, native hit testing, and cleanup when a 1,000-element list shrinks. That large-list case checks correctness; it is not a rendering performance benchmark. --- # Images Source: /docs/image GPUI draws images with two elements. `img()` draws a full-color image: a photo, a screenshot, an avatar, or a multicolor SVG. `svg()` draws a single-color SVG as a mask filled with a text color, which is how icons follow the theme. Both are re-exported from `gpui_kit`. This page explains how each element finds, decodes, sizes, and caches its source, and how to keep remote images from being downloaded again. For ready-made UI patterns see the [Image](/component/image) component page; for bundling files into the binary see [Icons & Assets](/docs/assets). ## Sources `img(source)` takes anything that converts into `ImageSource`. The conversion decides where the bytes come from: | Argument | `ImageSource` | Where the bytes come from | | --- | --- | --- | | `"https://example.com/a.png"`, or any string that parses as a URL; a `SharedUri` | `Resource(Resource::Uri)` | The `HttpClient` installed on the `App` | | `"images/a.png"`, or any other string | `Resource(Resource::Embedded)` | The registered `AssetSource`, looked up by that exact key | | `&Path`, `PathBuf`, `Arc` | `Resource(Resource::Path)` | The file system | | `Arc` | `Image` | Encoded bytes you already hold, with their `ImageFormat` | | `Arc` | `Render` | Frames you already decoded; drawn as is | | `Fn(&mut Window, &mut App) -> Option, ImageCacheError>>` | `Custom` | Your own loader | A string is a URL whenever it parses as one, and an asset key otherwise. A relative-looking string such as `"images/a.png"` is never read from the working directory. Pass a `Path` for a file on disk, so it is never taken for a URL or an asset key. On native platforms the default `HttpClient` fails every request, so URL images load only after the application installs a client with `cx.set_http_client(...)` or `Application::with_http_client(...)`. On the web, `gpui_kit::application()` installs a client backed by the browser's Fetch API. A `Custom` loader runs during layout and paint of every frame. Return `None` while the image is loading and keep the result yourself, for example with `window.use_asset::(...)`, so that each frame does not start a new load. ## Loading, decoding, and failures Loading is asynchronous. While it is pending, `img()` lays out from its own style and draws nothing. When the load finishes, GPUI redraws the view that drew the image. ```rust img("https://example.com/cover.png") .id("cover") .w(px(320.)) .h(px(180.)) .with_loading(|| div().child("Loading image...").into_any_element()) .with_fallback(|| div().child("Image unavailable").into_any_element()) ``` - `with_loading` replaces the image only when the load is still pending 200 ms after it started, so a fast load never flashes a placeholder. GPUI tracks that time in the element's state, so the placeholder appears only on an image with an `.id(...)`. - `with_fallback` replaces the image when loading fails: a missing asset key or file, a network error, a response that is not `2xx`, or bytes that do not decode. - Neither callback retries. To retry, keep the attempt in your view state and draw the image again with a different source, or remove the cached failure (see [Caches for decoded images](#caches-for-decoded-images)). GPUI detects the format from the bytes, not from the file extension. PNG, JPEG, WebP, GIF, BMP, TIFF, ICO and the other formats listed by `Img::extensions()` decode through the `image` crate. Bytes that are not a known raster format are parsed as SVG and rasterized once at twice their intrinsic size, so `img()` of an SVG stays sharp at normal scales but blurs when it is enlarged far beyond its own size. Animated GIF and WebP images play only on an image with an `.id(...)`, because the current frame is kept in element state. They advance while the window is active and stop when the system asks to reduce motion. ## Size and fit Layout decides the image's bounds; `object_fit` decides how the image is drawn inside them. When a dimension is `auto`, GPUI fills it in after the image has decoded. If the other dimension has an absolute length, the image's ratio gives this one; otherwise the image's own size is used. The image's ratio also becomes the element's `aspect_ratio` unless you set one. Before decoding finishes there is nothing to measure, so an image without a size moves the surrounding layout when it arrives. Reserve its box with an explicit size, or with a width and `aspect_ratio(...)`: ```rust img("images/banner.webp") .w_full() .aspect_ratio(16. / 9.) .object_fit(ObjectFit::Cover) ``` | `ObjectFit` | Result | | --- | --- | | `Contain` (default) | The whole image, aspect ratio kept; empty space may remain. | | `Cover` | Fills the bounds, aspect ratio kept; edges may be cropped. | | `Fill` | Stretches to the bounds; may distort. | | `ScaleDown` | Like `Contain`, but never enlarges the image. | | `None` | The image's own size, centered. | `.rounded(...)` on an `img()` rounds the drawn image itself. `.grayscale(true)` draws it without color. Borders, shadows and backgrounds are ordinary element styles. ## svg() `svg()` draws an SVG as a single-color shape. GPUI rasterizes the SVG's alpha channel at the element's size and fills it with a color, so the SVG's own colors are discarded. Choose the source with one of three builders: | Builder | Where the bytes come from | | --- | --- | | `.path("icons/check.svg")` | The registered `AssetSource`, by key | | `.external_path("/path/to/check.svg")` | The file system, read asynchronously and cached by path | | `.data(bytes)` | Bytes you pass in, cached by a hash of the bytes | ```rust svg() .path("icons/check.svg") .size(px(16.)) .text_color(cx.theme().foreground) ``` - **Set the color on the element.** `svg()` paints only when the element itself has a text color. A color inherited from a parent does not count, so an `svg()` without `.text_color(...)` draws nothing. - **Set the size.** Layout never reads the SVG, so `svg()` has no intrinsic size and collapses to zero without one. - **Transform at paint time.** `.with_transformation(Transformation::rotate(percentage(0.25)))`, and the `scale` and `translate` variants, move only the drawing. Layout and the hit area stay where they were. In GPUI Kit, prefer the [Icon](/component/icon) component for icons: it picks the size from the component size scale and the color from the theme. ## img() or svg() | | `img()` | `svg()` | | --- | --- | --- | | Colors | Keeps the source's colors | One color, from `.text_color(...)` | | Formats | Raster formats and SVG | SVG only | | Size | Intrinsic size from the image | Must be set | | Sources | URL, asset key, path, bytes, decoded frames, custom loader | Asset key, path, bytes | | Loading and failure | `with_loading`, `with_fallback` | Draws nothing until ready or on failure | | Use for | Photos, avatars, logos, illustrations | Icons and glyphs that follow the theme or a state | `.text_color(...)` does not recolor an `img()`, and `svg()` cannot keep a multicolor logo's colors. ## Caches for decoded images Decoded images are kept so that drawing the same source again does not load it again. Which cache keeps them decides when they are released. - **Default.** An `img()` of a `Resource` (URL, asset key, or path) uses the App's asset cache, keyed by the source. One entry serves every window and view and stays until you remove it with `ImageSource::remove_asset(cx)`. Failed loads are cached too, so remove the entry before retrying the same source. An `Arc` is cached the same way. - **A scoped cache.** `image_cache(provider)` wraps children in an element whose `img()` descendants use that cache instead. `image_cache(retain_all("preview"))` keeps a `RetainAllImageCache` in element state: it holds everything it loaded and releases it when the element stops being drawn. `img(...).image_cache(&cache)` picks a cache for one image. - **Your own cache.** Implement `ImageCache` to decide what to keep. `ImageCacheItem::new(resource, cx)` starts a load through GPUI's image loader, and `item.use_image(window)` returns the result and redraws the current view when it finishes. When you evict an image, release its GPU texture with `cx.drop_image(image, Some(window))`. The cache below keeps the most recently drawn images and releases the rest. Its capacity must exceed the number of images on screen at once, or visible images will be evicted and reloaded every frame. ```rust use std::{collections::VecDeque, sync::Arc}; use gpui_kit::*; /// Keeps the `capacity` most recently drawn images and releases the rest. pub struct RecentImageCache { capacity: usize, items: VecDeque<(Resource, ImageCacheItem)>, } impl RecentImageCache { pub fn new(capacity: usize, cx: &mut App) -> Entity { let cache = cx.new(|_| Self { capacity, items: VecDeque::new(), }); // Release the GPU textures when the cache itself is dropped. cx.observe_release(&cache, |cache, cx| { for (_, item) in cache.items.drain(..) { if let Some(Ok(image)) = item.get() { cx.drop_image(image, None); } } }) .detach(); cache } } impl ImageCache for RecentImageCache { fn load( &mut self, resource: &Resource, window: &mut Window, cx: &mut App, ) -> Option, ImageCacheError>> { let item = match self.items.iter().position(|(source, _)| source == resource) { Some(ix) => self.items.remove(ix).expect("index is in bounds"), None => (resource.clone(), ImageCacheItem::new(resource, cx)), }; self.items.push_front(item); while self.items.len() > self.capacity { if let Some((_, evicted)) = self.items.pop_back() && let Some(Ok(image)) = evicted.get() { cx.drop_image(image, Some(window)); } } self.items[0].1.use_image(window) } } ``` Create it once, keep the `Entity` in your view, and wrap the images that should use it: ```rust image_cache(self.images.clone()) .flex() .gap_2() .children(self.urls.iter().map(|url| img(url.clone()).size(px(96.)))) ``` These caches hold decoded images in memory. Every one of them still loads a URL through the App's `HttpClient`, and none of them remembers what the server said about the response. That is the job of the next section. ## Cache remote images over HTTP The previous sections cover what `img()` keeps in memory. This section covers the network: how to reuse responses across views and launches on native platforms. On the web the browser's HTTP cache already applies. ### Why GPUI's image cache is not enough GPUI's image cache sits above the App's `HttpClient`. It keeps decoded images in memory, keyed by source, which is enough for repeated sources in a running view but not for network traffic: - It ends with the process, so every launch downloads every image again. - It ignores `Cache-Control`, `ETag`, and `Last-Modified`. It cannot keep a response the server allows to be reused, or ask the server whether an older copy is still current. - It lives only as long as the cache that holds it. An image with its own `.image_cache(...)`, or a loader that keeps a cache per view, requests the image again each time a new view is created. ### Cache at the HTTP layer To reuse responses across views and launches, install an `HttpClient` that applies HTTP caching rules once at startup. Every remote image then benefits, including images loaded by code the application does not own. Follow these rules: - **Cache only a GET without a request body.** Pass every other request through unchanged. - **Treat the cache as shared.** One client serves the whole application: its own views, extensions, and document views. Do not store a `no-store` or `private` response, or a response to a request carrying `Authorization`, unless the server explicitly allows a shared cache to keep it. A layer below the cache that adds cookies or tokens hides them from the cache, so add credentials above the cache, or leave those hosts uncached. - **Revalidate instead of downloading again.** Serve a fresh response without the network. When it is stale, send `If-None-Match` or `If-Modified-Since`; on `304 Not Modified`, return the stored body with the refreshed headers. - **Honor the caller's redirect policy.** `img()` follows redirects, but a loader that authorizes each hop itself requests `RedirectPolicy::NoFollow` and must receive the `3xx` response. Keep a separate cache for each policy, so a followed result never answers that caller. - **Bound memory and disk use.** Cap the cache's size and remove old entries. - **Authorize before the request.** The cache answers any caller that asks for the same URL. A check that decides whether a caller may reach a URL, such as the network grants of gpui-shell scripts, must run before `send`. The cache then never widens what a caller can reach; it only avoids repeating a request that was already allowed. ### Example [http-cache-reqwest](https://crates.io/crates/http-cache-reqwest) implements these rules as [reqwest-middleware](https://crates.io/crates/reqwest-middleware): freshness, `ETag` and `Last-Modified` revalidation, and the restrictions on a shared cache, with a disk store. The application only adapts it to GPUI's `HttpClient`. Add these dependencies: ```toml [dependencies] anyhow = "1" futures = "0.3" http-cache-reqwest = "0.15" reqwest = { version = "0.12", features = ["stream"] } reqwest-middleware = "0.4" tokio = { version = "1", features = ["rt-multi-thread"] } ``` ```rust use std::{path::Path, sync::LazyLock}; use futures::{AsyncReadExt as _, FutureExt as _, TryStreamExt as _, future::BoxFuture}; use gpui_kit::http_client::{ AsyncBody, HttpClient, RedirectPolicy, Request, Response, Url, http::HeaderValue, }; use http_cache_reqwest::{CACacheManager, Cache, CacheMode, HttpCache, HttpCacheOptions}; use reqwest::redirect; use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; /// reqwest needs a tokio runtime; GPUI's executors are not one. static RUNTIME: LazyLock = LazyLock::new(|| { tokio::runtime::Builder::new_multi_thread() .worker_threads(1) .enable_all() .build() .expect("failed to start the HTTP runtime") }); /// A reqwest client with an HTTP cache on disk. pub struct CachedHttpClient { follow: ClientWithMiddleware, no_follow: ClientWithMiddleware, } impl CachedHttpClient { pub fn new(cache_dir: &Path) -> anyhow::Result { let client = |policy, dir| -> anyhow::Result<_> { let client = reqwest::Client::builder().redirect(policy).build()?; Ok(ClientBuilder::new(client) .with(Cache(HttpCache { mode: CacheMode::Default, manager: CACacheManager { path: cache_dir.join(dir), }, options: HttpCacheOptions::default(), })) .build()) }; // A caller that disables redirects must receive the 3xx itself. It // gets a client that never follows them, with a cache of its own. Ok(Self { follow: client(redirect::Policy::default(), "follow")?, no_follow: client(redirect::Policy::none(), "no-follow")?, }) } } impl HttpClient for CachedHttpClient { fn user_agent(&self) -> Option<&HeaderValue> { None } fn proxy(&self) -> Option<&Url> { None } fn send( &self, req: Request, ) -> BoxFuture<'static, anyhow::Result>> { let (head, mut body) = req.into_parts(); let client = match head.extensions.get::() { Some(RedirectPolicy::NoFollow) => self.no_follow.clone(), _ => self.follow.clone(), }; async move { let mut bytes = Vec::new(); body.read_to_end(&mut bytes).await?; let request = client .request(head.method, head.uri.to_string()) .headers(head.headers) .body(bytes); let response = RUNTIME.spawn(request.send()).await??; let mut builder = Response::builder() .status(response.status()) .version(response.version()); *builder.headers_mut().unwrap() = response.headers().clone(); let body = response .bytes_stream() .map_err(std::io::Error::other) .into_async_read(); Ok(builder.body(AsyncBody::from_reader(body))?) } .boxed() } } ``` GPUI's own reqwest client applies the caller's `RedirectPolicy` to each request, but a `reqwest-middleware` client fixes the policy when it is built. The example therefore builds two clients: one that follows redirects, for `img()`, and one that never does, for callers that request `RedirectPolicy::NoFollow`. Each has its own cache directory, because a cache in front of a following client stores the final response under the original URL and would answer the other caller with it. `RedirectPolicy::FollowLimit` uses reqwest's default limit of 10. ### Install the client Install the client once at startup, before any window loads a remote image. Use your platform's cache directory, for example from the [dirs](https://crates.io/crates/dirs) crate, instead of the temporary directory used here: ```rust use std::sync::Arc; gpui_kit::application().run(|cx| { gpui_kit::init(cx); let cache_dir = std::env::temp_dir().join("my-app/http-cache"); let client = CachedHttpClient::new(&cache_dir).expect("failed to create the HTTP client"); cx.set_http_client(Arc::new(client)); // Open windows here. }); ``` ### Extend the example - **Size.** The disk store has no size limit. Remove old entries at startup, or clear the directory when it grows past a limit. - **Proxy and user agent.** Configure them on the `reqwest::Client::builder()`, and return them from `proxy()` and `user_agent()` when other code reads them. - **Request bodies.** The example reads a request body into memory before sending it. That is fine for images and small requests; wrap the stream with `reqwest::Body::wrap_stream` for large uploads. ## Troubleshooting | Symptom | Check | | --- | --- | | An embedded image is blank | The registered `AssetSource` must contain the exact key. `img("images/a.png")` is an asset lookup, not a file read. | | Every URL image shows the fallback on native | Install an `HttpClient`; the default one fails every request. | | `with_loading` never appears, or a GIF does not play | Give the `img()` an `.id(...)`. | | An `svg()` is invisible | Set `.text_color(...)` and a size on the `svg()` element itself. | | A multicolor SVG turns into one color | Draw it with `img()`; `svg()` always draws a single color. | | The layout jumps when an image arrives | Reserve its box with a size, or a width and `aspect_ratio(...)`. | | A fixed image still shows the old failure | The failure is cached; call `ImageSource::remove_asset(cx)` before drawing it again. | | Images download again in every new view or after a restart | Cache at the HTTP layer; see [Cache remote images over HTTP](#cache-remote-images-over-http). | --- # Multi Window Source: /docs/multi-window A GPUI application can own several windows. Each window has its own `Window` context, focus, input dispatch, geometry, and GPUI Kit `Root`. Application data can be shared across them. Read [Window](./window) for the single-window API; this guide builds a second window and explains what each window owns. ## Start from an empty project Install the platform requirements in [Installation](/docs/installation), then create an application: ```sh cargo new gpui-multi-window cd gpui-multi-window ``` In `Cargo.toml`, add the same single dependency used by [Getting Started](/docs/getting-started): ```toml [dependencies] gpui-kit = "{{gpui_kit_version}}" ``` Replace `src/main.rs` with the complete example below. If you already have a single-window application, keep its `application().run(...)` and `init(cx)` calls; create the shared model once in that startup closure, then call `open_window` a second time with a new content view. The loop below performs both calls explicitly, once for each name. ## Open two windows over one model Call `gpui_kit::init` once before either window. Each call to `gpui_kit::open_window` creates a window and wraps the returned view in its own Base `Root`. The function returns `(AnyWindowHandle, Entity)`, where `V` is the application view passed to the builder. Return the content Entity from the builder; do not wrap it in another `Root`. The `Root` supplies window-level overlays, menus, notifications, and focus coordination. This example shares one counter Entity but creates a separate `Workspace` Entity for each window. The observer makes changes to the shared Entity visible in both windows; `local_clicks` remains independent. ```text Application SharedCounter (one Entity) ├── First window → Root → Workspace (first Entity, first observer) └── Second window → Root → Workspace (second Entity, second observer) ``` Each `Workspace` holds a clone of the same Entity handle. Cloning an `Entity` handle does not copy its `T` value. Both observers watch the same model, but each observer belongs to one window's content view. ```rust use gpui_kit::component::button::Button; use gpui_kit::*; struct SharedCounter { count: usize, } struct Workspace { shared: Entity, _shared_observer: Subscription, local_clicks: usize, name: &'static str, } impl Workspace { fn new(shared: Entity, name: &'static str, cx: &mut Context) -> Self { let _shared_observer = cx.observe(&shared, |_, _, cx| cx.notify()); Self { shared, _shared_observer, local_clicks: 0, name } } } impl Render for Workspace { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let count = self.shared.read(cx).count; div() .flex() .flex_col() .gap_2() .p_4() .child(format!("{}: shared count {count}", self.name)) .child(format!("Clicks in this window: {}", self.local_clicks)) .child( Button::new("increment") .label("Increment") .on_click(cx.listener(|this, _, _, cx| { this.local_clicks += 1; this.shared.update(cx, |shared, cx| { shared.count += 1; cx.notify(); }); cx.notify(); })), ) } } fn main() { application().with_assets(assets::Assets).run(|cx| { init(cx); let shared = cx.new(|_| SharedCounter { count: 0 }); for name in ["First window", "Second window"] { let shared = shared.clone(); open_window(WindowOptions::default(), cx, move |_, cx| { cx.new(|cx| Workspace::new(shared, name, cx)) }) .expect("open workspace window"); } }); } ``` Run `cargo run` in the new project. Two windows should open. Click **Increment** in either window: the shared count changes in both, while each window's click count changes only when its own button is clicked. Close one window and use the other; its count and button remain available. The `SharedCounter` was created once, outside the loop, while `Workspace::new` runs once per window. Use this sequence to check which state changed: | Action | First window | Second window | | --- | --- | --- | | Start | Shared 0; local 0 | Shared 0; local 0 | | Click **Increment** in the first window | Shared 1; local 1 | Shared 1; local 0 | | Click **Increment** in the second window | Shared 2; local 1 | Shared 2; local 1 | | Close the first window, then click in the second | Closed | Shared 3; local 2 | Trace the first click through the code: the button listener increments the first `Workspace.local_clicks`; `shared.update` mutates the one `SharedCounter` and calls its `cx.notify()`; both stored observers receive that notification and call `cx.notify()` for their own views. Their next renders read the same count. The first view also calls `cx.notify()` after changing its local count. Reading `self.shared.read(cx)` in `render` supplies a value for that render; the observer is the connection that schedules later renders when the model changes. To open a third independent workspace over the same counter, add `"Third window"` to the `for name in [...]` array and rerun. All three shared labels should advance after one click, while only the clicked window's local label advances. If you instead create `SharedCounter` inside the loop, each window gets its own model and this observable behavior changes. Keep document or session data in a feature-owned Entity shared by the windows that need it. Keep window selection, focus handles, overlay state, and window-scoped tasks in that window's view. Use an application [Global](./global) for genuinely app-wide settings or services, not as a catch-all owner for every window's UI state. A shared Entity notification reaches only views that observe it; reading shared state in `render` alone does not subscribe that view to changes. Store the returned `Subscription` on the observing view: dropping it would stop updates, and retaining it in an application owner would outlive the window unnecessarily. ## Address the intended window `gpui_kit::open_window` returns an `AnyWindowHandle` because its actual root view is GPUI Kit's `Root`, not the content view. Keep the returned content `Entity` when you need its state. Keep the window handle when later work needs that window's focus, Action dispatch, activation, or geometry. `window.window_handle()` obtains the current window's handle inside a callback. The startup example discards both return values because its buttons act on their own views; a document switcher or window registry would retain them. An `AnyWindowHandle` exposes `window_id()` and `update(...)`. `cx.windows()` enumerates open handles; `cx.active_window()` returns the platform-focused one when available. If you already know the target window, retain its handle instead of choosing one by iteration order. A handle can also be downcast to `WindowHandle` when a typed root handle is required; downcasting it to `WindowHandle` fails because `Workspace` is content inside `Root`. ```rust // `target` is the AnyWindowHandle returned by open_window. target.update(cx, |_, window, _cx| { window.activate_window(); window.set_window_title("Document"); })?; ``` The `update` result must be handled because the target may have closed. Focus and Action dispatch run in the selected window's context. For an Entity update that also needs its window, use `cx.update_window(target, |_, window, cx| { ... })` and update the content Entity inside that callback. For an async task bound to one window, use [`cx.spawn_in`](./task) and handle a failed `update_in` after the window or Entity disappears. ## Close and clean up `window.remove_window()` requests removal of the current window; it does not express a whole-app quit policy. Register `window.on_window_should_close(cx, |window, cx| { ... })` on each window that may need to cancel a platform close request; return `false` to prevent that close. A direct `remove_window()` call is an application decision to remove the window, so perform any confirmation before calling it. Capture or persist window-specific data before closing: `cx.on_window_closed(...)` runs after the `Window` is inaccessible and receives its `WindowId` for registry cleanup. For a desktop app that should quit when the last window closes: ```rust cx.on_window_closed(|cx, _closed_id| { if cx.windows().is_empty() { cx.quit(); } }) .detach(); // Deliberately keep this app-wide observer for the app lifetime. ``` Alternatively, retain the returned `Subscription` in an application owner and drop it with that owner. The basic example needs no close observer: closing either window leaves the other usable. If your app can reopen a window from the dock or tray, choose that policy instead of quitting. A registry keyed by `WindowId` can remove the closed handle in this callback. Window-owned `Task` and `Subscription` fields should drop with the corresponding view; do not keep a closed window's view alive through an app-wide collection accidentally. The close policy has two separate decision points. A platform close request reaches `on_window_should_close` while the window is still available; return `false` if the user must resolve unsaved work first. Once a window has actually closed, `on_window_closed` can remove its `WindowId` from a registry or decide whether the app should quit. If a button calls `remove_window()` directly, ask for confirmation before that call; the should-close callback is not a substitute for the button's own confirmation flow. ## Restore placement Before a window closes, read `window.window_bounds()` to obtain its restorable `WindowBounds` (`Windowed`, `Maximized`, or `Fullscreen`). Persist that value in your application settings, then pass it to the next `WindowOptions`: ```rust let options = WindowOptions { window_bounds: Some(saved_bounds), ..Default::default() }; open_window(options, cx, |window, cx| cx.new(|cx| Workspace::new(shared, "Restored", cx)))?; ``` Here `saved_bounds` is a `WindowBounds` captured from a prior window; saving and loading it is application code. Check restored placement against currently attached displays before using it, because display topology and scale may have changed. See [Window geometry](./window#geometry-and-scale) for the difference between global bounds and window-local viewport size. ## Test the boundaries In a [`TestAppContext` test](./test), open two windows through `gpui_kit::open_window`, update the shared Entity, and assert both views change while window-local state stays independent. Close the first with `window.remove_window()`; its handle update should return an error, while the second window should still render and accept input. Run the repository's existing lifecycle coverage with `cargo test -p gpui-kit --features test-support --test lifecycle closing_one_window_preserves_other_window_and_owned_snapshot`; the [test source](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/kit/tests/lifecycle.rs) exercises the close boundary. For this example, also verify the two visible counts and close behavior with `cargo run` in the new project. ## Troubleshoot the example | Observation | Check | | --- | --- | | Only one window opens | Check that `open_window` runs once per loop item and that each call succeeds. A panic from `.expect(...)` identifies an open failure. | | The clicked window changes, but the other shared label stays stale | Keep the `Subscription` returned by `cx.observe` in each `Workspace`, and call `cx.notify()` inside the `SharedCounter` update. Reading the Entity alone does not register an observer. | | Local click counts change in both windows | Create a distinct `Workspace` with `cx.new(...)` for each window; only `SharedCounter` should be constructed outside the loop. | | An action updates the wrong window or fails after close | Keep the `AnyWindowHandle` returned for the intended window, use it for window-scoped work, and handle its `update` error after closure. Do not rely on `cx.windows()` order. | | Closing one window exits the whole app | Inspect the app's last-window quit policy and any `on_window_closed` callback. The two-window sample registers no such callback. | --- # ElementId Source: /docs/element_id An `ElementId` is a **local key** for an element in GPUI's rendered tree. GPUI combines that key with the IDs of its keyed ancestors to form a `GlobalElementId`. This path lets GPUI associate interaction and element state with the same logical element when a [View renders](./render) again. It also preserves node identity in the [accessibility tree](./accessibility) when the element has a role. An ID is not a handle to an [Entity] and is not a way to look up an element like an HTML DOM ID. Use an `Entity` for shared application state. Use an `ElementId` for identity in the element tree, including keyed component state, focus or scroll behavior supplied by a component, and state retained by a custom [Element]. ## Assign an ID Calling `.id(...)` on an interactive element such as `div()` returns a `Stateful
`: ```rust let save = div() .id("save-button") .on_click(|_, window, cx| { // Handle the click. }) .child("Save"); ``` The `Stateful` wrapper exposes GPUI's stateful interaction methods and carries the element's ID. A custom `Element` can return `Some(id)` from its `id()` method without using this wrapper. A plain, unkeyed `div()` can still be a layout parent; it contributes no segment to the ID path. Strings, integers, and name plus integer tuples are common `ElementId` inputs: ```rust use gpui_kit::component::button::Button; div().id("search") div().id(("message", message.id)) Button::new(("delete-project", project.id)).label("Delete") ``` Choose a key from the object's identity, not from the text displayed to the user. A translated label, current selection, or freshly generated random value can change while the object stays the same. ## GlobalElementId and the keyed ancestor path GPUI builds a `GlobalElementId` from the IDs on the path to an element. Only keyed ancestors add path segments: ```text div().id("workspace") ├── div().id("inbox") │ └── div().id(("row", 42)) → ["workspace", "inbox", ("row", 42)] └── div().id("archive") └── div().id(("row", 42)) → ["workspace", "archive", ("row", 42)] ``` The two rows may share a local ID because their keyed ancestor paths differ. This diagram shows only IDs written in the example: an entity-backed View also adds its `EntityId` as a path segment, and a [`RenderOnce`](./render-once) component adds a type-name namespace. The path is scoped to the [Window](./window)'s rendered tree; `GlobalElementId` is GPUI's internal path, not a process-wide string you need to construct at call sites. For a custom drawing API that needs a path for its own key, `window.with_global_id(key, |global_id, window| { … })` creates one within that callback. The practical uniqueness rule is: **within the same nearest keyed ancestor, each keyed descendant branch needs a distinct ID**. An unkeyed container does not open a new namespace: ```rust div().id("workspace") .child(div().child(div().id("item"))) .child(div().child(div().id("item"))) // Same keyed path: collision. ``` Give the branches their own stable IDs, or make the item IDs distinct. Duplicate paths can make retained state and interaction attach to the wrong logical element. ## Stable keys in changing lists For a list that can insert, remove, filter, or reorder rows, derive each row ID from a stable domain value: ```rust div().id("messages").children(messages.iter().map(|message| { div() .id(("message", message.id)) .child(message.preview.clone()) })) ``` `("message", message.id)` separates the row's purpose from other controls that may use the same numeric ID. Reordering changes the drawing position, but each message keeps its keyed path. By contrast, `.id(index)` attaches state to a *position*: after an insertion, the old first row's focus, scroll, animation, or other keyed state may be reused for a different message. An index is appropriate only when the position itself is the identity and cannot shift. If a repeated row contains several controls, key the row and give its children distinct local keys such as `"edit"` and `"delete"`. Moving the row then carries its whole keyed subtree with it. ### Worked example: a row and its controls The following uses the actual `div().id(...)` and `Button::new(id)` APIs. `Message::id` is a stable database ID; `preview` is display data that may change. Each row owns a namespace for its controls: ```rust use gpui_kit::*; use gpui_kit::component::button::Button; struct Message { id: u64, preview: SharedString, } fn message_list(messages: &[Message]) -> impl IntoElement { div().id("messages").children(messages.iter().map(|message| { div() .id(("message", message.id)) .child(message.preview.clone()) .child(Button::new("archive").label("Archive")) })) } ``` For message `42`, the written part of the button's path is `"messages" → ("message", 42) → "archive"` (GPUI may add View and component namespaces). Every row can call its button `"archive"` because the row IDs differ. If the list order changes from `[42, 7]` to `[7, 42]`, those paths stay with their messages. If message `42` is removed for a rendered frame, its element-local state ends; inserting it again later creates fresh state. An application-level selection or draft that must survive removal belongs in an owned `Entity` or model. The row key does not automatically key siblings *inside* the row: two `Button::new("archive")` controls under that same row would still collide. Give them distinct local IDs. Also keep the same domain ID when a message's preview or localized label changes; using that text as the key would reset its UI identity. ### Try it: reorder, hide, and restore rows In the existing `examples/hello_world` package, replace `src/main.rs` with the complete example below and run `cargo run -p hello_world`. Click **Add to 42** twice, then **Swap rows**. Record 42 still shows `2` after moving below record 7. Click **Hide 42**, wait until that row is visibly gone, then click **Show 42**. Its count starts again at `0` because the keyed state was absent from a rendered frame. Keep a persistent count in an application-owned Entity if it must survive hiding. ```rust use gpui_kit::base::StyledExt; use gpui_kit::component::button::Button; use gpui_kit::*; struct Example { reversed: bool, show_42: bool, } impl Render for Example { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let order = if self.reversed { [7_u64, 42] } else { [42, 7] }; let rows = order .into_iter() .filter(|id| *id != 42 || self.show_42) .map(|id| { // This call happens while the parent View renders, before the row div is drawn. // Give the state its own domain-derived key; the row ID below keys its subtree. let count = window.use_keyed_state(("row-count", id), cx, |_, _| 0_u32); let value = *count.read(cx); div() .id(("row", id)) .h_flex() .gap_2() .child(format!("Record {id}: {value}")) .child( Button::new("increment") .label(format!("Add to {id}")) .on_click(move |_, _, cx| { count.update(cx, |value, cx| { *value += 1; cx.notify(); }); }), ) }) .collect::>(); div() .v_flex() .gap_2() .p_4() .child(Button::new("swap").label("Swap rows").on_click(cx.listener( |this, _, _, cx| { this.reversed = !this.reversed; cx.notify(); }, ))) .child( Button::new("toggle-42") .label(if self.show_42 { "Hide 42" } else { "Show 42" }) .on_click(cx.listener(|this, _, _, cx| { this.show_42 = !this.show_42; cx.notify(); })), ) .child(div().id("rows").v_flex().gap_2().children(rows)) } } fn main() { gpui_kit::application().run(|cx| { gpui_kit::init(cx); gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| { cx.new(|_| Example { reversed: false, show_42: true, }) }) .expect("failed to open window"); }); } ``` The row's `("row", id)` path keeps its controls associated with the same record when the order changes. `Button::new("increment")` can use the same local name in both rows because the row paths differ. The counter uses a *separate* `("row-count", id)` key: it is requested while the parent View builds the rows, before the row's element ID is on the path. Both keys must use the stable record ID. Hiding a row is observable only after GPUI renders a frame without that row; clicking Hide and Show before an intervening render may preserve the old state. ## How IDs retain state GPUI reconstructs elements during rendering. The Rust value returned by `div()` or a `RenderOnce` component is temporary; assigning it an ID does not turn it into a persistent `Entity`. The ID gives GPUI a way to reconnect element-local state across consecutive frames. `window.use_keyed_state(key, cx, init)` forms a path from the current keyed ancestors plus `key`. It returns an `Entity` kept for as long as that keyed state is used in consecutive rendered frames. `Window` owns this keyed state; `cx` supplies application access (see [Context](./context) for the different GPUI contexts). `init` runs when that state has no previous entry. GPUI also observes this state entity and notifies the current View when it changes: ```rust let focus_handle = window .use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle()) .read(cx) .clone(); ``` GPUI Kit's `Button` uses this pattern for its focus handle. Its public ID gives the recreated button instance the same state key on later frames. The focus handle still owns focus behavior; the element ID identifies where that handle's state belongs. The window retains keyed state only while its path is accessed in successive frames; a [cached View](./view-cache) replays those accesses when it reuses its subtree. A separate strong `Entity` handle can keep that entity alive after its window state entry disappears, but recreating the path will run `init` again. For a custom `Element`, GPUI passes `Option<&GlobalElementId>` into `request_layout`, `prepaint`, and `paint` when its `id()` returns a value. During drawing, `window.with_element_state(global_id, ...)` can read state from the preceding frame and return the value to store for the next: ```rust let state = window.with_element_state( id.expect("this element always has an ID"), |previous: Option, _window| { let state = previous.unwrap_or_default(); (state.clone(), state) }, ); ``` This is the pattern behind GPUI Kit's `ScrollBounce` element, which retains motion state during `prepaint`. `with_element_state` is a drawing-phase API for element authors; ordinary Views should prefer Entity state or a component's documented API. GPUI keys stored element state by global path **and state type** and drops it when the element no longer participates in the rendered frames. `window.use_state(cx, init)` uses the call site's code location as its local key. It works when the *full path* is unique: a call inside each row is safe if every row has a stable keyed ancestor. If repeated calls share the same keyed ancestor path, use `use_keyed_state` with a stable item key or introduce a keyed namespace around each item. ## Identity changes are state changes - Changing an element's ID or a keyed ancestor's ID gives it a new path and resets its associated element state. - Removing an element ends its consecutive-frame state lifetime. Recreating it later initializes that state again. - An ID does not preserve a View's `Entity` by itself; a strong Entity owner controls that lifetime. - Keyed state belongs to the Window's rendering context. Do not rely on a `GlobalElementId` to transfer state between windows. ## Effects beyond element state An ID can be one input to a component's focus, scroll, measurement, or animation state. For example, Kit's `Button` obtains a focus handle with `window.use_keyed_state(self.id.clone(), ...)`, while `ScrollBounce` uses `window.with_element_state(...)` to retain its motion state. Reusing a path for two live controls can therefore mix behavior; changing a path can restart it. A `FocusHandle` or `ScrollHandle` still owns its respective behavior, and changing an `ElementId` does not by itself reset every handle stored elsewhere. Stable paths also matter to cached Views. A cached Entity View reuses work only while its entity and element path still match; on a cache hit, GPUI replays the subtree's element-state accesses. IDs are not a general render cache: adding `.id(...)` to a `div()` does not make its parent skip `render` (see [View Cache](./view-cache)). For accessibility, a custom element needs both an ID and a role to become a node. A stable ID lets GPUI preserve that node's identity across redraws, but it does not supply a role, label, keyboard behavior, or focusability. Use the component's accessibility API for those properties (see [Accessibility](./accessibility)). ## Find a bad ID path 1. Identify the logical object whose focus, scroll position, animation, or other state moved or reset. Write down the object's stable domain ID and every keyed ancestor above it. Check whether a sibling now has the same **full** path, or whether an ancestor key changes when data is reordered. 2. Look for unkeyed wrappers between repeated controls. They do not distinguish paths. Replace a shared literal with a stable object-derived ID, or key the repeated parent. Do not fix a collision with a new random value on every render: that trades state sharing for state loss. 3. If state resets only after an item disappears, check whether it was absent for a rendered frame. If it was, keep long-lived state in an Entity or model. If it resets while still present, check changing ancestor IDs, a newly created View Entity, and cache invalidation separately. A real Kit example is [`DockSkin::render_resize_handle`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/dock/dock.rs): left, right, and bottom resize handles can live below the same keyed ancestor. Giving every handle the literal `"resize-handle"` would produce one `GlobalElementId`; the dock source notes that a press on one handle could then start another handle's drag. It selects `"resize-handle-left"`, `"resize-handle-right"`, and `"resize-handle-bottom"` from the stable placement instead. In UI integration tests, target IDs are query selectors for observed elements, not a second identity system. Import `gpui_kit::test::TestWindowExt`; `window.find("archive")` requires one matching observed target, so repeated controls need a native scope: ```rust let archive = window.within(("message", 42_u64)).find("archive"); assert!(archive.visible()); ``` `window.within(...)` follows the keyed ancestor path even when the ancestor itself is not observed. An ambiguous `find` or `try_find` asks for a scope; it does not necessarily mean GPUI state collided, since two controls can correctly share a local ID under different row paths. Test the real click or focus outcome as well as the snapshot (see [Testing](./test)). See [Element](./element) for the layout, prepaint, and paint lifecycle, and [Entity](./entity) for state that must outlive an element's presence in the tree. [Element]: ./element.md [Entity]: ./entity.md --- # Focus Source: /docs/focus **Focus** identifies the target for keyboard input in one [Window](./window). GPUI uses the focused element's path through the rendered tree to route [Actions and key bindings](./action). A pointer press may move Focus, but drawing a control or giving it an `ElementId` does not. This guide uses the GPUI API published as `gpui-pre` {{gpui_pre_version}} through `gpui-kit`; `gpui-pre` is the snapshot publishing and version alignment name, not a different rendering engine. ## Try the existing example From the repository root, run: ```sh cargo run -p focus_trap ``` The source is [`examples/focus_trap/src/main.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/examples/focus_trap/src/main.rs). Click an outside button and press Tab: Focus follows the window's ordinary Tab order. Click a button in either numbered area and press Tab or Shift+Tab: Focus cycles among that area's buttons. The example calls `.focus_trap(id, &handle)` on each container; the buttons are real GPUI Kit `Button`s with their own focus handles. It demonstrates containment after entering an area, not a complete modal lifecycle. For a new application, initialize Kit before opening a window with `gpui_kit::init(cx)`, as the example does. `gpui_kit::open_window` supplies the Base `Root` that handles normal Tab and Shift+Tab navigation and participates in Kit's trap handling. The [Getting Started](./getting-started) guide covers the application setup. ## Four separate operations | Operation | What it does | What it does not do | | --- | --- | --- | | `cx.focus_handle()` | Creates a stable target that an owner can retain. | It does not register a rendered element or move Focus. Its default `tab_stop` is `false`. | | `.track_focus(&handle)` | Registers that handle on a rendered interactive element. The element can then participate in Focus dispatch and receive its focused style. | It does not move Focus or automatically opt the handle into Tab order. | | `handle.focus(window, cx)` or `window.focus(&handle, cx)` | Makes the handle the window's current Focus target. | It does not create a rendered node or a keyboard handler. | | `cx.focus_handle().tab_stop(true)` | Includes a tracked handle in Tab navigation. | It does not focus the handle immediately. | The handle belongs to the long lived owner, usually an [`Entity`](./entity). The `Element` is rebuilt for each frame; attach the *same* handle again from [`Render::render`](./render). If you create a fresh handle on every render, Focus identity and Tab behavior can change underneath the user. For a stateless Kit component, retain a handle through `window.use_keyed_state(...)`; Kit's `Button` uses that pattern. An `ElementId` can key retained state, but it is not itself a Focus target. ### A minimal focusable view The following view can be placed in the window builder shown in [Getting Started](./getting-started). It creates one Tab stop and displays whether it has exact Focus: ```rust use gpui_kit::*; struct FocusPanel { focus_handle: FocusHandle, } impl FocusPanel { fn new(cx: &mut Context) -> Self { Self { focus_handle: cx.focus_handle().tab_stop(true), } } } impl Focusable for FocusPanel { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for FocusPanel { fn render(&mut self, window: &mut Window, _: &mut Context) -> impl IntoElement { let label = if self.focus_handle.is_focused(window) { "Focused" } else { "Press Tab or click here" }; div() .track_focus(&self.focus_handle) .p_4() .child(label) } } ``` Create it with `cx.new(FocusPanel::new)` in a window builder. `Focusable` exposes the handle to code that owns the Entity; it does not automatically call `.track_focus(...)`. A view can implement `Focusable` and still fail keyboard dispatch if its rendered tree omits the tracked element. To move Focus from a callback, call `self.focus_handle.focus(window, cx)`. Do that when opening or entering a region, not unconditionally in `render`: repeated requests from rendering can steal Focus from a child or another control. ## Pointer, Tab, and keyboard commands For an element with `.track_focus(&handle)`, GPUI's default mouse down behavior focuses that handle when the pointer hits it. A nested tracked child gets the first opportunity to focus and suppresses an ancestor's default transfer; a custom mouse handler can use `window.prevent_default()` when it deliberately handles Focus itself. A pointer callback that only changes application state is not a keyboard interaction. Tab and Shift+Tab visit *tab stops* registered in the most recently rendered frame. The default `FocusHandle` is not one. Set `.tab_stop(true)` when constructing a handle that users should reach by Tab. GPUI also has `FocusHandle::tab_index(n)` for explicit ordering, but prefer the rendered order for ordinary forms. When passing a handle to `.track_focus(...)`, set Tab configuration on the **handle**; an element's separate `.tab_index(...)` or `.tab_stop(...)` does not change that handle's settings. Once Focus reaches a tracked element, its dispatch path determines which `key_context`, `on_action`, and key bindings apply. For example, an editor's Save binding works when the editor or a descendant owns Focus and the matching handler lies on that path. A sibling's handler is not on the path. See [Action](./action) and [KeyBinding](./keybinding) for a complete command example. A custom control must also provide a visible Focus treatment and the expected keyboard activation or navigation behavior; `track_focus` only supplies the routing target. See [Accessibility](./accessibility) for semantics and testing limits. ## Exact Focus versus Focus within a region Use `handle.is_focused(window)` when only that handle should count as active. Use `handle.contains_focused(window, cx)` when the handle or a tracked descendant should count—for example, keeping a panel visually active while a text field inside it has Focus. Containment comes from the **most recently rendered dispatch tree**; both handles must be attached to elements in the expected parent and child relationship. A stored Entity relationship alone does not establish it. ```rust let panel_itself = self.focus_handle.is_focused(window); let panel_or_child = self.focus_handle.contains_focused(window, cx); ``` `window.focused(cx)` returns an `Option` for the current window. It is useful for diagnostics and for saving the previous target before opening an overlay. The returned handle may later outlive its rendered element, so validate the UI lifecycle before restoring it. ## Observe transitions without putting subscriptions in `render` `Context` provides `on_focus`, `on_blur`, `on_focus_in`, and `on_focus_out`. Register them while constructing the owner, and retain the returned [`Subscription`](./event) on that owner. Dropping a subscription unregisters it. The exact pair fires when the named handle gains or loses Focus; the `in`/`out` pair considers tracked descendants as well. ```rust struct SearchPanel { focus_handle: FocusHandle, _subscriptions: Vec, active: bool, } impl SearchPanel { fn new(window: &mut Window, cx: &mut Context) -> Self { let focus_handle = cx.focus_handle(); let entered = cx.on_focus_in(&focus_handle, window, |this, _, cx| { this.active = true; cx.notify(); }); let left = cx.on_focus_out(&focus_handle, window, |this, _, _, cx| { this.active = false; cx.notify(); }); Self { focus_handle, _subscriptions: vec![entered, left], active: false, } } } ``` Attach `focus_handle` to the panel's rendered root with `.track_focus(&self.focus_handle)`. This pattern is appropriate when another state change must follow Focus; a simple Focus style can often read `is_focused` during rendering instead. Registering listeners during every render would accumulate duplicate callbacks. ## Trap and restore Focus in an overlay GPUI Kit adds `.focus_trap(id, &container_handle)` to an interactive container. The Base `Root` checks whether the current Focus is inside that container when it handles Tab or Shift+Tab. If normal navigation would leave, it searches for another Tab stop inside the container. The trap's `id` must be stable, its handle must survive rendering, and the container must contain usable child Tab stops. ```rust div() .child(first_button) .child(second_button) .focus_trap("settings-dialog", &self.dialog_focus_handle) ``` **A trap alone does not open a modal, enter its first control, block pointer focus outside it, or restore Focus after dismissal.** For a modal flow: 1. Save `window.focused(cx)` when the user opens the overlay. Keep the saved handle with the overlay owner. 2. Render the overlay and move Focus to an enabled control inside it when that control exists. Choose a predictable first target; do not rely on the trap to perform this step. 3. On dismissal, remove the overlay and restore the saved target only if it is still an appropriate rendered target. If it has disappeared, choose a current fallback such as the control that opens the overlay. Coordinate the update and restoration so a closing overlay does not immediately take Focus back. 4. Test both Tab directions, Escape or explicit close, pointer clicks, disabled controls, nested overlays, and the case where the previous target disappears. GPUI Kit's `Dialog` and `Sheet` components provide their own modal focus behavior; use them for ordinary modal UI. The manual trap is useful for a custom surface whose entry, dismissal, and restoration lifecycle you explicitly own. An `on_focus_out` listener on a container can help observe Focus leaving; it is not a substitute for the modal lifecycle. ## Verify and debug Start with the `focus_trap` example to see pointer entry and both Tab directions in a running window. In an application test, render the real view, click its intended control, send Tab or the command key, and assert the resulting owner state. The [Testing](./test) guide covers Kit's UI integration test helpers; focus scopes need an explicit tracked handle for reliable inspection. | Symptom | Check | | --- | --- | | Tab skips the custom view | The retained handle has `.tab_stop(true)` and its element calls `.track_focus(&handle)` in the current frame. | | Click focuses a parent instead of a child | The child has its own tracked handle; inspect mouse handlers that call `prevent_default`. | | A shortcut works only after clicking | The intended handle is focused, the `key_context` matches, and the handler lies on the focused dispatch path. | | Panel appears inactive when a child is focused | Use `contains_focused`, and verify the child's tracked element is nested under the panel's tracked element. | | Tab escapes a custom trap | Focus entered the trap first; the container is rendered through Kit's Base `Root`; children are actual Tab stops. | | Focus disappears after closing an overlay | Save the previous target and restore an existing rendered target after dismissal. | Related guides: [Window](./window), [Action](./action), [KeyBinding](./keybinding), [Accessibility](./accessibility), and [Testing](./test). --- # GPUI Kit Source: /docs GPUI Kit is a Rust desktop application framework built on GPUI. The `gpui-pre` dependency name refers to the published, version-pinned GPUI snapshot used by Kit, not a different rendering framework; see [Installation](./installation#why-the-dependency-is-named-gpui-pre). GPUI Kit's core architecture has five layers: - **`gpui`**: The underlying UI runtime, entity model, windows, elements, layout, and rendering. - **`gpui-kit`**: The application entry point that re-exports GPUI and brings together Base, Component, and default assets through one dependency. - **`gpui-base`**: Unstyled behavior, controlled state, focus, overlays, virtual lists, dock infrastructure, and semantic design tokens. - **`gpui-component`**: GPUI Component, the complete styled component library with 75+ documented components and primitives, themes, data tables, dock layout, and a code editor. - **`gpui-shell`**: Opens a Rust host to JavaScript extensions, one granted capability at a time. An application normally depends on `gpui-kit` for GPUI, Base, Component, and assets. Add `gpui-shell` separately when the application hosts JavaScript extensions; it remains part of the framework's core architecture. Use `gpui-component` for polished controls with one coherent visual language, or build your own design system on the reusable behavior and infrastructure in `gpui-base`. This section covers GPUI Kit setup, shared design and coding guides, and application development. For library APIs, see [GPUI Component](/component), [GPUI Base](/base), and [GPUI Shell](/shell). Read [Focus](./focus) for `FocusHandle`, Tab order, and the keyboard target, then [Action](./action) for command dispatch. [KeyBinding](./keybinding) explains how to bind actions and display the active shortcut. Continue with [Event](./event) for typed notifications and the relationship between Actions and Events. For the core rendering model, start with [Entity](./entity) and [Context](./context), then read [Render](./render), [RenderOnce](./render-once), and [ElementId](./element_id). [Style](./style) covers GPUI's fluent styling methods; [Element](./element) and [Paint](./paint) explain lower-level drawing. [Task](./task) covers work that continues after a callback returns. ## Learn GPUI in a working order Use these stages as a learning path. Each stage has a small application task to try before moving on: 1. **Open a window:** follow [Installation](./installation) and [Getting Started](./getting-started), run the button example, and confirm its click reaches the terminal. 2. **Own and redraw state:** make an [Entity](./entity), update it through [Context](./context), and use [Render](./render) to show the new value. Then read [Window](./window) to understand which window receives the update. 3. **Draw a custom control:** follow [Element](./element), [Geometry](./geometry), and [Paint](./paint) with the existing Brush example. Use [ElementId](./element_id) and [View Cache](./view-cache) when the drawing needs stable state or reuse. 4. **Handle input and ongoing work:** establish a keyboard target with [Focus](./focus), then connect an [Action](./action) or [Event](./event) to the View; use [Task](./task) for asynchronous work and [Animation](./animation) for motion that ends cleanly. 5. **Check the application:** use [Accessibility](./accessibility) and [Testing](./test) for interaction checks. Read [FPS Monitor](./fps) before making frame-rate claims, then choose a target such as [WebAssembly](./webassembly) or [Mobile](./mobile) if the application needs one. Each core page distinguishes a code example from its runtime result and links to the next concept. The [Coding Guides](./coding-guides) collect ownership and architecture conventions once the first window works. ## Features - **75+ Components and Primitives**: Forms, navigation, overlays, data display, editing, feedback, layout, and more. - **Production Ready**: Refined through production desktop applications and continuously tested across GPUI Kit's components and examples. Capabilities outside that desktop path are labeled by [maturity](#maturity). - **WebAssembly**: Applications and component showcases run on the web through `wasm32-unknown-unknown`. - **Accessibility**: AccessKit roles, names, states, relationships, and actions are built into the interaction layer. - **UI Integration Testing**: Headless windows exercise real pointer, keyboard, focus, layout, and accessibility behavior. - **Native Feel**: Modern controls inspired by macOS and Windows. - **High refresh support**: GPUI can target a 120 Hz display when the complete workload fits its roughly 8.3 ms frame budget; actual smoothness depends on the application, device, and presentation path. See [Frames, refresh rates, and rendering modes](./fps#120-hz-is-a-frame-budget-not-a-refresh-promise). - **Data Tables**: Virtual scrolling, fixed and resizable columns, sorting, and cell selection across hundreds of thousands of rows. - **Virtual Lists**: Render only the visible range, including differently sized items. - **Code Editor**: 200K lines, Tree-sitter highlighting, diagnostics, completion, and hover. - **Dock Layout**: Resizable panels, draggable tabs, nested splits, and edge docks. - **Rich Content**: Native Markdown and HTML, syntax highlighting, and charts. - **Design Freedom**: Use the complete visual system or build your own on `gpui-base`. - **Typed Motion**: CSS-aligned easing, timing, keyframes, springs, presence, and measured reveal with allocation-free steady sampling. - **Cross Platform**: Ship one Rust codebase to macOS, Windows, and Linux. ## Maturity GPUI Kit's desktop components run in production applications, including Longbridge's. Other capabilities have a shorter track record, so their pages carry a label under the title. A page without a label is Stable. | Label | Meaning | | --- | --- | | **Stable** | Used by production desktop applications on macOS, Windows, and Linux. | | **Preview** | Usable and documented. The API and edge-case behavior may still change between releases. | | **Experimental** | Works with known gaps. Validate it for your product before depending on it. | | **Showcase only** | Currently used to demonstrate components in a browser, not to ship applications. | | **Platform-dependent** | Availability or behavior differs by operating system or target. The page lists the differences. | A label describes the capability, not the quality of its documentation. WebAssembly, for example, runs the same components as the desktop, but for now it serves the component showcases; shipping an application in a browser is not a supported path yet. ## Quick Example After preparing the platform libraries in [Installation](./installation), create a Rust project with `cargo new gpui-hello` and enter it with `cd gpui-hello`. Add `gpui-kit` to its `Cargo.toml`: ```toml [dependencies] gpui-kit = "{{gpui_kit_version}}" ``` Replace `src/main.rs` with this complete "Hello, World!" application: ```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. The window shows a label and button; clicking the button prints `Clicked!` in the terminal. Continue with [Getting Started](./getting-started) for the initialization sequence, `Render`, and retained state. ## Community & Support Learn how to build interruptible animation in the [GPUI Base Motion guide](/base/motion). - [GitHub Repository](https://github.com/MohsenDastaran/uni-kit) - [Issue Tracker](https://github.com/MohsenDastaran/uni-kit/issues) - [Contributing Guide](https://github.com/MohsenDastaran/uni-kit/blob/main/CONTRIBUTING.md) ## License Apache-2.0 --- # WebAssembly Source: /docs/webassembly **Current scope** GPUI and GPUI Kit WebAssembly support is used primarily to **showcase and try components in a browser**. The gallery and Base showcase build and run, but this repository has not validated WebAssembly as a mature distribution path for full applications. Treat browser support, accessibility, input, startup cost and deployment as work to verify for each product. GPUI Kit can render the same Rust views and components in a browser. The web target is `wasm32-unknown-unknown`: Rust produces a WebAssembly module, `wasm-bindgen` produces its JavaScript bindings, and a web page loads and starts the application. The browser supplies the canvas, input and network environment, so a desktop `main` function alone is not a web entry point. In this workspace, [`gpui_web` is the Cargo alias for `gpui-pre-web` {{gpui_pre_version}}](https://github.com/MohsenDastaran/uni-kit/blob/main/Cargo.toml). [`gpui-kit` includes it as a WASM-only dependency](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/kit/Cargo.toml) and re-exports it as `gpui_kit::web`; [the gallery crate](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story-web/Cargo.toml) depends on `gpui-kit`, not directly on `gpui-pre-web`. Its `cdylib`, exported `run(...)`, web platform initialization and JavaScript loader provide the browser entry path that the desktop `main` cannot provide. The [component gallery](https://gpui-kit.com/gallery/) is the quickest working example. Its [Rust entry point](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story-web/src/lib.rs), [build script](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story-web/scripts/build-wasm.sh) and [JavaScript loader](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story-web/www/src/main.js) show the complete path from a GPUI Kit view to a browser page. ## Run the gallery locally Install Rust, Bun and `make`. Run these commands from the repository root (the directory containing the workspace `Cargo.toml`): ```sh cd crates/story-web rustup target add wasm32-unknown-unknown cargo install wasm-bindgen-cli --version 0.2.121 make dev ``` Keep `make dev` running and open **http://localhost:3000/gallery/**. You should see the gallery's component list and a rendered view; the loading indicator alone does not confirm that graphics started. The first Rust build can take longer than later builds. `make dev` builds the WASM module in debug mode, generates bindings in `www/src/wasm/`, installs the web dependencies and starts Vite. Check the result in this order: 1. The terminal reaches Vite's local URL without a Rust or `wasm-bindgen` error. 2. The browser Network panel shows the generated JavaScript and `.wasm` requests succeeding. Open the `/gallery/` URL above, not the root of the Vite server. 3. The loading indicator gives way to the gallery. Select a story and try a control, such as a button, to check that input reaches the Rust view. A removed loading indicator by itself only proves that the JavaScript loader called `run(...)`. If you edit a Rust view, keep Vite running in one terminal and run `make build-wasm-dev` from `crates/story-web` in another, then reload the page. Editing the loader or other `www/` files is handled by Vite. This distinction matters because Vite does not compile the Rust crate for you. The local [toolchain file](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story-web/rust-toolchain.toml) selects nightly; the `wasm-bindgen-cli` version above matches this checkout's [`Cargo.lock`](https://github.com/MohsenDastaran/uni-kit/blob/main/Cargo.lock). If the lockfile changes, match the CLI to the locked `wasm-bindgen` crate version. If another CLI version is already installed, check `wasm-bindgen --version` and reinstall the pinned version with `cargo install -f wasm-bindgen-cli --version 0.2.121`. The [build script](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story-web/scripts/build-wasm.sh) sets an 8 MiB linker stack for the gallery's large render tree, including in debug builds. For a production gallery build, run `make build-prod` from the same directory. The site bundle lands in `www/dist/` and uses the `/gallery/` base path. Deploy it at that path, or change both the [Vite base](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story-web/www/vite.config.js) and the [Rust asset endpoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story-web/src/lib.rs) for your own host. ## How the web entry point works The gallery's `run(story, dark, theme_name, theme_json)` is exported with `#[wasm_bindgen]`. Its [loader](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story-web/www/src/main.js) imports the generated JS module, awaits its default WASM initialization, reads the optional `?story=` URL parameter and host theme, then calls `run(...)`. On the Rust side, it calls `gpui_kit::platform::web_init()`, creates a web `Application`, registers assets, and passes a launch closure to `run_embedded`. Once graphics initialization succeeds, that closure calls `gpui_component_story::init(cx)` (which calls `gpui_kit::init(cx)`), loads fonts, applies the theme, and calls `gpui_kit::open_window`. The `ApplicationHandle` returned by `run_embedded` is retained in thread-local storage. Graphics initialization is asynchronous, so that return does not mean the launch closure ran or the first frame painted. For your application, initialize Kit before constructing components, as in [Getting Started](/docs/getting-started), and keep the handle alive while the page uses the view. The gallery uses `WebPlatform::new_with_backend_and_font_fallback` and attaches its fetch HTTP client. `Auto` tries WebGPU and then WebGL2 if WebGPU fails. The current web platform uses one document canvas and one top-level window; another top-level window, or reopening a closed one, is unsupported. Render dialogs inside that window through Kit's `Root`. Reuse the [gallery entry point](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story-web/src/lib.rs) when adapting your own app; keep shared views in Rust, with the page responsible for loading the WASM module and hosting it. ### Trace one launch | Stage | File or API | What it establishes | | --- | --- | --- | | Compile | `scripts/build-wasm.sh` | `cargo rustc` builds the gallery's `cdylib` for `wasm32-unknown-unknown`, then `wasm-bindgen --target web` writes JavaScript bindings and the browser-loadable module into `www/src/wasm/`. | | Load | `www/index.html` and `www/src/main.js` | The page shows a loading state; the loader imports the generated bindings and awaits their default initializer. This is the JavaScript-to-Rust boundary. | | Start | `run(...)` in `src/lib.rs` | Rust creates the web platform, supplies assets and keeps the `ApplicationHandle` alive. `run_embedded` starts graphics initialization asynchronously. | | Open | The launch closure in `src/lib.rs` | Once graphics are ready, it initializes Kit, registers fonts, applies a theme and opens the single GPUI window. Only then can the first view be painted. | Use this sequence when moving a desktop app to the browser: retain its GPUI view and state code where the target supports it, then provide a web entry point, a page loader and browser-specific resources. A successful Rust target build does not test the page loader or graphics initialization. ## Fonts and CJK text The GPUI Web platform starts with **no installed font database**. In the gallery, `include_bytes!` embeds four subset TTF files in the WASM module: Inter for UI text, JetBrains Mono for code, Noto Sans SC for the Chinese characters used by the stories, and IBM Plex Sans for GPUI's `.SystemUIFont` alias. The last family must be loaded before the first window, because even initial text measurement can use the default window style and fail when the family is absent. After loading them with `cx.text_system().add_fonts(...)`, the gallery applies its theme and forces its UI and mono family back to bundled fonts; a selected theme can otherwise name a desktop-only family. See the [font setup and theme code](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story-web/src/lib.rs) and [Fonts](/docs/fonts). The [subset script](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/story-web/scripts/subset-fonts.py) scans the story sources, plus the files they pull in with `include_str!` such as the README, and retains their characters. In this checkout, the Noto Sans SC subset is **42,092 bytes** versus **1,213,236 bytes** for its checked-in source TTF. Those are font file sizes, not the change in compressed WASM transfer size. New text entered by a visitor is not guaranteed to be in that subset. For eligible missing emoji and horizontal Han, kana and modern Hangul graphemes, `CanvasFontFallback::EmojiAndCjk` can let the browser measure and draw from its local fonts. GPUI's loaded fonts remain preferred. This fallback works on individual graphemes, so browser font coverage, spacing and typography can differ; it is not a full CJK font replacement. The default policy covers emoji only, while `Disabled` uses loaded fonts alone. The policy is chosen when constructing `WebPlatform` and cannot be changed later. GPUI also supports **loading a font after startup**. In the GPUI version pinned by this repository (`gpui-pre` {{gpui_pre_version}}), `TextSystem::add_fonts` accepts downloaded font bytes through `Cow::Owned`; it clears font resolution and line-layout caches. After an asynchronous fetch has produced a valid raw font file, install it on the application context and redraw: ```rust use std::borrow::Cow; // cx: &mut App; font_bytes: Vec fetched and checked by your app. cx.text_system().add_fonts(vec![Cow::Owned(font_bytes)])?; cx.refresh_windows(); ``` Use a raw supported font file such as the TTF in GPUI Web's `examples/hello_web/dynamic_fonts.rs`, not a font-service CSS response that may refer to WOFF2 subsets. That example uses `cx.on_missing_glyphs(...) -> Subscription` to request a font when needed, retains the subscription and download task, checks for a successful HTTP status and nonempty body, then calls `add_fonts` and `refresh_windows`. Font parsing is checked by `add_fonts`; an HTTP 200 alone does not establish that the response is a usable font. Missing-glyph reports are deduplicated and bounded; a dropped report does not schedule its own retry. The gallery currently **does not** download fonts this way; it embeds subsets and uses Canvas fallback. GPUI Web applies Canvas fallback before reporting missing glyphs, so CJK it draws successfully through Canvas will **not** trigger `on_missing_glyphs`. For high-quality CJK typography, decide which font to fetch from your content or language selection, show a loading or failure state, and test layout after installation. A font downloaded from another origin must also satisfy that host's browser CORS policy. ## A smaller GPUI Base example The [GPUI Base WASM showcase](https://github.com/MohsenDastaran/uni-kit/tree/main/crates/base/examples/wasm) compiles the same [showcase views](https://github.com/MohsenDastaran/uni-kit/tree/main/crates/base/examples/showcase) used by its native example. To run it independently from the repository root, install the matching `wasm-bindgen-cli` if you have not already done so: ```sh cd crates/base/examples/wasm rustup target add wasm32-unknown-unknown --toolchain nightly cargo install wasm-bindgen-cli --version 0.2.121 make dev ``` Keep the server running and open **http://localhost:3001/examples/base/**. It uses `gpui_platform::single_threaded_web()`, generates bindings with `wasm-bindgen`, and serves the examples through Vite. Its `run(component)` export selects a showcase view; the JavaScript loader reads `?component=...` from the URL. The CLI version again follows the workspace `Cargo.lock`. See its [build script](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/wasm/scripts/build.sh) and [loader](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/wasm/www/src/main.js). The gallery is the better starting point for styled GPUI Kit components; this example isolates the Base primitives. ## Troubleshoot a failed load or blank canvas Open the browser's **Console and Network** panels first. The gallery loader catches failures while importing the bindings, initializing WASM or calling `run`, and replaces its loading indicator with “Failed to load the application”. Graphics initialization happens later on the web platform's async task; a failure there is logged and a “Failed to initialize browser graphics” message is appended to the page. The loader can therefore disappear before the first frame appears. | Symptom | Check | | --- | --- | | Rust build cannot find the WASM target, or `wasm-bindgen` is not found | Run `rustup target add wasm32-unknown-unknown` in `crates/story-web` so its selected nightly toolchain receives the target. Install the CLI version in `Cargo.lock`, then rerun `make build-wasm-dev`. The Rust compiler build must finish before a Vite page can load the module. | | JS or `.wasm` request is 404 | Open `/gallery/`, not the site root. Keep Vite's `base: '/gallery/'`, the deployment path and generated files together. Rebuild with `make build-wasm-dev` after Rust changes; ensure the generated `www/src/wasm/` files were included in the web build. | | `wasm-bindgen` import or instantiation fails | Match `wasm-bindgen-cli` to the locked crate version and regenerate bindings. Serve `.wasm` as `application/wasm`; the generated loader can fall back from streaming compilation when MIME is wrong, but that is slower. Check the Network response instead of assuming the requested URL returned a module. | | Loading indicator vanishes but canvas stays empty | Read Console errors after `run` returns. `Auto` tries WebGPU and then WebGL2; if both fail, check browser GPU support, policy and hardware acceleration. The web runtime cannot create a second top-level window. | | Panic or missing text at startup | Register IBM Plex Sans and every initial theme family before opening the window. The gallery's `add_fonts(...).expect(...)` and `open_window(...).expect(...)` turn errors into a panic; its panic hook reports them in Console. | | Squares or wrong CJK appearance | Confirm the text is in the bundled subset, then check the Canvas fallback policy and actual browser font coverage. Download a suitable raw font when exact shaping matters. | | Blank icons | Inspect the exact icon URL, HTTP status and CORS. `Assets::load` concatenates the endpoint and `/assets/icons/...` literally: an endpoint ending in `/`, as in the gallery, produces `//assets/` in the URL. Set an endpoint without a trailing slash in your app. The gallery's fixed endpoint points to the published site even during local development. Its asset loader caches a successful fetch but does not itself request a window refresh, so if the icon remains blank, trigger another render and check the Console for asset errors. | ## Package size and delivery `include_bytes!` puts the gallery's subset fonts **inside the WASM payload**, so visitors download them with the module before they can see a first frame. The gallery also bundles the component stories and rendering stack; icon SVGs are separate on-demand requests. Vite packages the JavaScript loader and WASM for `/gallery/`, but changing only the Vite base will not change the Rust icon endpoint. After `make build-prod`, `ls -lh www/dist/assets/*.wasm` shows the uncompressed module size. Compare the **compressed transfer sizes** of that module and JS in the browser Network panel, and record the first-load time on a throttled connection. Local file size, CDN compression, browser cache state, compilation time and GPU initialization are different costs. Before publishing a copy of this gallery, serve `www/dist/` from the intended `/gallery/` path and repeat the three browser checks above against that served copy. Confirm that the generated `.wasm` and JS URLs resolve on the deployed host, and that icon, theme and font requests reach the hosts you intended. The example's asset endpoint is hard-coded to the published GPUI Kit site, so a successful local gallery does **not** prove that a separately hosted copy serves its own icons. Record a cold load and a warm load separately; browser caching changes the result. The repository's [release workflow](https://github.com/MohsenDastaran/uni-kit/blob/main/.github/workflows/release-website.yml) builds both WASM examples and copies their `dist/` files under the corresponding website paths, but it does not replace browser testing on the final host. For example, **if** a full application such as Longbridge Pro were compiled into one WASM module, including all of its features and font coverage, the initial download and startup cost could grow substantially. That is a distribution risk to measure, not a measured package size or a claim that such an application currently ships on the web. Keeping fonts or features behind later requests can reduce initial transfer, but adds network, caching, CORS and loading-state work. The gallery's font subsets and on-demand icons illustrate those tradeoffs; they do not establish an application-size budget. ## Web capabilities to plan for | Concern | What the examples do | What to check in your app | | --- | --- | --- | | Assets | On WASM, `Assets::new(endpoint)` fetches icon SVGs on demand by concatenating `endpoint` and `/assets/icons/...`. The gallery's endpoint points to its published site, while its Vite build copies icons under `/gallery/assets/`. | Host the requested paths and use an endpoint matching your deployment path, without a trailing slash. Local gallery runs still request icons from the published endpoint unless you change it. A missing asset or failed fetch can leave an icon blank. See [Icons & Assets](/docs/assets). | | Fonts | The gallery embeds Inter, JetBrains Mono, a Noto Sans SC subset and IBM Plex Sans before its first frame, then restores its font choices when applying a theme. | Bundle initial families; consider runtime font downloads for larger scripts and validate fallback and layout. See [Fonts](/docs/fonts). | | Keyboard and IME | The web platform uses a small hidden HTML input for keyboard and composition events. The gallery loader manages focus when embedded and disables mobile text entry on touch-only devices to avoid raising a keyboard for a canvas interaction. | Test focus, Tab order, composition and on-screen keyboards in the browsers and devices you support. The gallery's touch-only policy is specific to a showcase, not a general text-input solution. | | [Accessibility](/docs/accessibility) | Components can express roles and labels in GPUI, while this web example is painted into a canvas. | Verify actual screen-reader and keyboard behavior in the browser. Do not assume that a native accessibility bridge or a GPUI accessibility property produces equivalent web semantics. Provide an accessible HTML alternative for content or actions that your target browser cannot expose. | | Native services | The browser supplies fetch and its own input and rendering APIs; desktop facilities have different availability and permissions. | Put file dialogs, clipboard, notifications and similar features behind target-specific capability code and test the browser path. See [Coding Guides](/docs/coding-guides#platform-and-capability-boundaries). | The release workflow [builds both WASM examples](https://github.com/MohsenDastaran/uni-kit/blob/main/.github/workflows/release-website.yml), so these entry points also serve as maintained build references. A successful WASM build establishes compilation; interaction, font coverage and accessibility still need browser checks. --- # Accessibility Source: /docs/accessibility GPUI sends an accessibility tree to platform assistive technology through [AccessKit](https://accesskit.dev/). A useful control has a **role** (what it is), a **name** (how a person identifies it), **state** (such as value, checked, selected, or expanded), and **actions** (what assistive technology can request). Its stable element ID preserves identity across frames; it is not the name a person hears. The same interface must work with a keyboard and show where focus is. Tree properties alone do not implement keyboard behavior. Applications normally use GPUI Kit's styled components from `gpui_kit::component`. Their interaction comes from the unstyled `gpui_kit::base` layer. Both are available through one `gpui-kit` dependency. Use a standard control before composing a new interactive `div`: it already coordinates pointer input, keyboard input, focus, state, and AccessKit semantics. For a custom keyboard target, follow [Focus](./focus) first: `track_focus`, Tab order, and an accessible role solve different parts of the interaction. Think of accessibility as one path through the application, not a separate description attached at the end: | Step | In the Profile example | What to verify | | --- | --- | --- | | Retained state | `Entity` owns the draft; `Profile::submitted` owns the saved value. | Typing and Save change the intended model, including after a rerender. | | Rendering | `Input`, `Button`, and the status `div` read that state. | The visible label and result match the model. | | Semantics | The controls expose a role, name, relevant state, and available actions through AccessKit. | A painted-frame snapshot contains the expected properties. | | Operation | Pointer, keyboard, and assistive technology reach the same command. | Save works with each input method; focus moves visibly and predictably. | Start with the first two steps, then inspect the semantic tree and operate the real window. A passing headless tree assertion cannot prove what a screen reader announces or whether a focus ring is visible. ## Follow one complete Save flow Run the repository's [Profile UI test](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/kit/tests/ui.rs) from the workspace root: ```sh cargo test -p gpui-kit --features test-support --test ui saves_a_profile_through_the_ui -- --exact ``` The test opens a headless window, enters a name, activates **Save**, and checks both application state and accessibility properties written by the rendered GPUI elements. This gives you a concrete path from the model to a visible control and its AccessKit node. For setup of your own test crate, continue with [Testing](./test). ### 1. Name the control that receives input The view keeps an `Entity` so the text survives rerenders. It renders the input with a stable ID for GPUI and an explicit human-readable name for assistive technology: ```rust Input::new(&self.name) .id("name") .aria_label("Profile name") .w(px(240.)) ``` The ID and label answer different questions. `"name"` lets GPUI and the test identify this particular element; `"Profile name"` tells a person what to enter. The example also renders **Profile name** as visible text directly above the input. Proximity does not create an accessibility relationship by itself, so the input has its own name. A placeholder would disappear as the user types and is a poor replacement for a persistent label. ### 2. Update one model, then render the status from it The **Save** button's listener copies the current input value into the view's `submitted` field and calls `cx.notify()`. The next `render` computes a status string from `submitted` and uses the same string for pixels and the accessibility name: ```rust let status = self.submitted.as_ref().map_or_else( || SharedString::from("Not saved"), |name| SharedString::from(format!("Saved: {name}")), ); div() .id("status") .role(Role::Status) .test_support() .aria_label(status.clone()) .child(status) ``` The status `div` is not a button; it describes the result of an action. Its stable ID plus `Role::Status` gives it a semantic node. `.test_support()` only makes this custom node discoverable to GPUI Kit's test helper when that feature is enabled. A role and label do not change the model; `cx.notify()` is what causes the new model value to reach the next frame. ### 3. Assert the contract after a painted frame The test calls `render_frame` before its first query, then interacts with the native elements and reads fresh snapshots: ```rust window.render_frame(cx); assert_eq!(window.find("status").role(), Some(Role::Status)); assert_eq!(window.find("status").label(), Some("Not saved")); window.click("name", cx); window.input("Ada José", cx); assert_eq!(window.find("name").label(), Some("Profile name")); assert_eq!(window.find("name").value(), Some("Ada José")); window.press("backspace", cx); assert_eq!(window.find("name").value(), Some("Ada Jos")); window.click("save", cx); assert_eq!(window.find("status").label(), Some("Saved: Ada Jos")); ``` The repository test also checks input focus, status geometry, and `profile.read(cx).submitted`. Checking the model prevents a test from passing when only the label changes. `window.click` exercises pointer activation here; the `backspace` step removes the accented `é` after the pointer has focused the input, testing Unicode editing rather than keyboard navigation to **Save**. As an experiment, remove `.aria_label("Profile name")` and rerun the test: the label assertion shows why visible proximity to other text cannot be assumed to name the input. To check the keyboard path in a real window, put this complete version of the same view in the existing `examples/hello_world/src/main.rs`, then run `cargo run -p hello_world` from the workspace root. This is a local exercise; restore the example file when finished. The headless test above does not run this keyboard sequence. ```rust use gpui_kit::{ AppContext, Context, Entity, Role, SharedString, Window, WindowOptions, component::{button::Button, input::{Input, InputState}}, div, prelude::*, px, }; struct Profile { name: Entity, submitted: Option, } impl Render for Profile { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let status = self.submitted.as_ref().map_or_else( || SharedString::from("Not saved"), |name| SharedString::from(format!("Saved: {name}")), ); div() .flex() .flex_col() .p_4() .gap_4() .child( div().flex().flex_col().gap_1().child("Profile name").child( Input::new(&self.name) .id("name") .aria_label("Profile name") .w(px(240.)), ), ) .child(Button::new("save").label("Save").on_click( cx.listener(|this, _, _, cx| { this.submitted = Some(this.name.read(cx).value()); cx.notify(); }), )) .child( div() .id("status") .role(Role::Status) .aria_label(status.clone()) .child(status), ) } } fn main() { gpui_kit::application().run(|cx| { gpui_kit::init(cx); gpui_kit::open_window(WindowOptions::default(), cx, |window, cx| { cx.new(|cx| Profile { name: cx.new(|cx| InputState::new(window, cx)), submitted: None, }) }) .expect("failed to open window"); }); } ``` The window starts with **Not saved**. Starting before the form, Tab to **Profile name** and confirm a visible focus indication; type `Ada José` and use Backspace once, leaving `Ada Jos`. Tab to **Save**, confirm its focus indication, then press Enter. The visible result should become **Saved: Ada Jos**. While **Save** remains focused, press Space to activate it again. Use Shift+Tab to return to the input, then Tab back to **Save** to verify reverse and forward navigation. If either activation key does not work, inspect the button's focus and key handling instead of treating the pointer test as evidence. Finally, activate **Save** through your target platform's assistive technology and check the announced role, name, and result; the status role alone does not guarantee a live announcement. To adapt this exercise to your own form, follow the same order: give each actual input a stable ID and name, make the action update retained state, render any result from that state, then assert both the model and the freshly painted semantics. Keep the visible label beside the control for sighted users; the accessible name is an additional contract. If a field becomes invalid, expose the error in visible text and check that a person using assistive technology can discover the error and recover. A color change by itself does not communicate the reason or correction. ## Start with semantic controls The following controls expose useful semantics from their actual state: | Control | Accessibility contract | | --- | --- | | Button, Link | Button or Link role, accessible name, activation. Use Button for an application command and Link for an external destination. | | Checkbox, Switch, Toggle, Radio | Their respective role and checked or toggled state. Checkbox also reports mixed state. | | Input | Text input role chosen from its content type, name, non-sensitive value, and an accessible `SetValue` action when not disabled. Masked and password values are withheld. | | Select | ComboBox role, name, committed value, expanded state, and an accessible activation path. | | Tab, tab list | Tab and TabList roles; tabs report selection and, when supplied, position in the set. | | Slider, Progress | Numeric value and range; Slider handles accessible Increment and Decrement. Indeterminate Progress omits its value. | | Table | Table, row, header and cell roles with indices and optional counts. Name the Table root. | For a labeled button, the visible label is the accessible name by default. Name an icon-only button explicitly: ```rust use gpui_kit::component::{IconName, button::Button}; Button::new("search-documents") .icon(IconName::Search) .accessibility_label("Search documents") .tooltip("Search documents") .on_click(|_, window, cx| { // Invoke the same application command used by its keyboard route. }) ``` `accessibility_label` names the control for assistive technology; a tooltip is a separate hint and cannot replace that name. Keep the name specific to the action and update it if the action changes. Do not assume that arbitrary text nested inside a custom container becomes its accessible name. A visible form label and a neighboring input likewise do not gain an automatic label relationship merely by being next to each other: name the actual input with `Input::aria_label(...)`, or use a component that provides the relationship. Input can fall back to a placeholder as its name, but an explicit label remains clearer; a generated mask placeholder is deliberately not used as the name. ## Identity and roles GPUI includes an element in its accessibility tree when it has both an [ElementId](./element_id) and a non-empty accessibility role. The global identity also contains IDs of ancestors. Keep IDs stable across frames and derive repeated item IDs from domain keys, so reordering does not look like a series of removals and insertions to assistive technology. An `id` alone is not a role; an unroled `div` is a layout container, not an announced control. A role alone does not make a control focusable or operable. For a semantic status message, a GPUI element can supply both: ```rust use gpui_kit::*; div() .id("save-status") .role(Role::Status) .test_support() .aria_label("Saved") .child("Saved") ``` The explicit label is the announced name. `.test_support()` lets the later integration test find this custom `div` when `test-support` is enabled; it adds no layout container and is inert in normal builds. `Role::GenericContainer` is filtered from the accessibility tree; use an actual role for a meaningful node. `accessibility_id(...)` is a separate, author-provided identifier exposed to platform automation. It maps to identifiers such as UIA `AutomationId` on Windows and `AXIdentifier` on macOS, with Linux AT-SPI support depending on the deployed adapter. It is not a substitute for GPUI's `.id(...)` or for a human-readable name. ## Names, states, and relationships On an identified `div`, GPUI's `StatefulInteractiveElement` provides `.role(...)`, `.aria_label(...)`, `.aria_description(...)`, `.aria_selected(...)`, `.aria_expanded(...)`, `.aria_toggled(...)`, `.aria_value(...)`, `.aria_numeric_value(...)`, and range and collection properties. Numeric controls can also report minimum, maximum, step, and orientation; headings can report level; list and table items can report positions and counts. Update these from the same model that draws the visible UI; the owning view uses its [Context](./context) to notify GPUI after a state change. A description supplements the name; it does not replace it. `.aria_keyshortcuts(...)` announces a shortcut but does not bind the key: register the real GPUI keybinding separately. GPUI's current `div` API has no general `.aria_disabled(...)` builder. Base controls such as Button and Checkbox gate focus and activation when disabled, but that does not guarantee a native disabled property for every node. Verify both the available tree state and the actual disabled behavior. Likewise, `.track_focus(...)` gives a node a focus path and advertises the accessible Focus action; a role or `.focusable()` alone does not implement a useful keyboard command. The element tree establishes parent/child relationships. Composite controls may keep keyboard focus on a parent and mark the current child with `.aria_active_descendant()`. GPUI applies that child-side marker only when an ancestor actually has focus. The child needs its own ID and role. This is specialized composite behavior; use the built-in Select, menu, or list behavior when it fits. Do not infer a web `aria-labelledby`, `aria-describedby`, or `aria-controls` builder from the `aria_` prefix. These are not general builders on GPUI's current `div` API. For a Table with a visible caption, set `.accessibility_label(...)` on the Table root; the caption container does not automatically name it. ## Accessible actions and keyboard input An AccessKit action is distinct from a GPUI [Action](./action) dispatched by a keybinding or menu. GPUI exposes it as `AccessibleAction`. For an identified `div`, `.on_a11y_action(action, handler)` registers one requested action; the handler receives optional `ActionData`, `&mut Window`, and `&mut App`. `.on_click(...)` already advertises accessible Click activation and routes it to the click handler, so a second Click handler can perform the command twice. A custom slider, for example, must offer Increment and Decrement as well as its pointer and keyboard controls, and update its numeric accessibility value after the model changes. GPUI Kit's Slider already does this. Keep the interaction promise consistent: the visible label, accessible name, shortcut, pointer behavior, keyboard behavior, and assistive action should all perform the same command. A clickable painted shape with a hitbox is still missing semantics and keyboard operation until those are implemented. After dialogs or sheets close, restore focus to their trigger. Keep focus visible and ordered according to the task. For the Profile flow above, use the runnable window exercise to verify keyboard operation separately from the pointer test. The visible result changes only after `submitted` changes and `cx.notify()` triggers a rerender. For a composite control, also test its documented arrow keys and Escape behavior. Test these paths on the platforms you ship; a successful pointer test or an announced shortcut does not establish them. ### Build one interactive custom control The earlier `EventSurface` exercise adds a Status node to a painted shape, but it does not make the shape operable without a pointer. For a control whose appearance can be built from normal elements, start with composition. Replace `examples/hello_world/src/main.rs` temporarily with this complete example, then run `cargo run -p hello_world` from the repository root. Restore the example file when finished. ```rust use gpui_kit::{ *, component::ThemeStyled as _, prelude::*, }; struct CounterControl { focus: FocusHandle, activations: usize, } impl CounterControl { fn new(cx: &mut Context) -> Self { Self { focus: cx.focus_handle().tab_stop(true), activations: 0, } } } impl Render for CounterControl { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let focused = self.focus.is_focused(window); let status = format!("Activations: {}", self.activations); div() .flex() .flex_col() .gap_4() .p_4() .child( div() .id("activate-counter") .role(Role::Button) .aria_label("Activate counter") .track_focus(&self.focus) .p_3() .border_1() .when(focused, |control| control.focus_ring_style(window, cx)) .on_click(cx.listener(|this, _, _, cx| { this.activations += 1; cx.notify(); })) .child("Activate counter"), ) .child( div() .id("activation-status") .role(Role::Status) .aria_label(status.clone()) .child(status), ) } } fn main() { gpui_kit::application().run(|cx| { gpui_kit::init(cx); gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| { cx.new(CounterControl::new) }) .expect("failed to open window"); }); } ``` The retained `FocusHandle` survives rerenders. Its `.tab_stop(true)` includes the control in Tab navigation, while `.track_focus(...)` registers the rendered node. The stable ID and `Role::Button` expose a control node, and `.aria_label(...)` names the operation. The focus ring is rendered only while that handle is focused. The single `.on_click(...)` listener changes the retained count and calls `cx.notify()`; GPUI routes primary pointer clicks, Enter/Space keyboard activation on the focused node, and AccessKit Click to that listener. Do not register a second `AccessibleAction::Click` for this node, or the command may run twice. Check each path in the running window. A pointer click should change **Activations: 0** to **Activations: 1**. From outside the control, Tab to it and confirm the ring is visible; Enter should produce **Activations: 2**, and Space should produce **Activations: 3**. Tab away and Shift+Tab back to confirm it remains in the navigation order. With a platform accessibility inspector, find a Button named **Activate counter** and a Status whose label matches the visible count. Invoke the Button's accessible Click operation with the target assistive technology and confirm the count increases exactly once. A headless snapshot can check the role, name, and updated model, but cannot prove the ring is visible, the platform adapter exposes the node, or a screen reader announces the changing Status. If Tab skips the control, check both `.tab_stop(true)` on the retained handle and `.track_focus(...)` on the current rendered node. If its role or name is missing, check that the same node has a stable ID, role, and label. If an activation produces two increments, look for a second Click or key handler calling the same command. This exercise demonstrates one button-like control; for real application commands, prefer the standard Button, which also handles its other component states and styling. ## Custom `Element` implementations When composition with `div()` is insufficient, a low-level `Element` has explicit hooks. Continue the [EventSurface exercise](./element#make-the-surface-visible-and-respond-to-a-press), which already implements `IntoElement`, layout, prepaint, and paint in `examples/hello_world/src/main.rs`. Make these three edits inside that existing example: 1. In `Render for SurfaceDemo`, pass the current counter text into `EventSurface` alongside `owner` and `color`: `status: format!("Pointer presses: {}", self.presses),`. 2. Add `status: String,` to the `EventSurface` struct. 3. Replace its `id()` method and add the two accessibility methods inside `impl Element for EventSurface`: ```rust fn id(&self) -> Option { Some("pointer-status".into()) } fn a11y_role(&self) -> Option { Some(Role::Status) } fn write_a11y_info(&self, node: &mut accesskit::Node) { node.set_label(self.status.clone()); } ``` `use gpui_kit::*;` in the complete EventSurface example already imports `ElementId`, `Role`, and `accesskit`. Run `cargo run -p hello_world` again. With assistive technology or a platform accessibility inspector active, find the rectangle's **Status** node named **Pointer presses: 0**. A left press inside the rectangle updates the visible counter and, after rerender, the node's name to **Pointer presses: 1**; the stable ID keeps its identity across those frames. An inspector can show the tree properties, while only a screen reader check can establish whether the change is announced. The accessibility tree is built when assistive technology is active; an absent tree with no client attached does not by itself prove the methods failed. This is a narrowly scoped semantics exercise. `EventSurface` still responds only to pointer input, and `Role::Status` describes its counter; it does not make the rectangle a keyboard or assistive-technology-activatable control. Use a standard Button for a real command, or separately implement tracked focus, keyboard activation, an appropriate control role, and an accessible action. GPUI only calls `write_a11y_info` for an element that contributes an identified role. GPUI Kit's `window.find(...)` helper observes registered controls and `div().test_support()`, not this raw `Element`; do not expect it to find `pointer-status`. `a11y_synthetic_children(...)` can add AccessKit child nodes after prepaint, for example text runs in a custom editor. `A11ySubtreeBuilder::synthetic_node_id(key)` derives a child ID from its parent and a stable key; `push_child(...)` attaches the node. Keys must be unique among a parent's synthetic children. This is an advanced path: the implementer owns hit testing, event dispatch, focus, keyboard handling, and accessible actions in addition to the tree data. ## Test the contract Enable GPUI Kit's `test-support` feature for UI integration tests and import `gpui_kit::test::TestWindowExt`; import `TestSupportExt` when observing a custom `div`. Query the **painted** element after `window.render_frame(cx)`. `ElementSnapshot` exposes `role()`, `label()`, `value()`, `focused()`, `checked()`, `indeterminate()`, `selected()`, and `expanded()`. Re-query after each interaction because a snapshot describes one completed frame. Use the runnable Profile test above as the assertion example; it checks role, name, value, focus, geometry, and the saved model value. `None` from a snapshot state reader means the property is unavailable, not `false`. In particular, `.disabled()` is `Some(true)` only if the node exposes that flag; test disabled behavior by attempting activation and checking that the result did not change. `ElementSnapshot::value()` reads a string accessibility value, not Slider's numeric value or painted text. Masked and password inputs intentionally expose no accessibility value. The current Input implementation registers `SetValue` when it is not disabled, including in read-only mode; its handler uses a programmatic replacement path. Do not treat `readonly(true)` as protection against this accessible write path without verifying the behavior you need. See [Testing](./test) for test setup, frame, and focus details. Headless snapshots read AccessKit properties produced by GPUI elements; they do not inspect the final platform adapter tree, a screen reader announcement, or pixels. Inspect the running app with assistive technology on each target platform for announcement order, focus movement, editing, and actions. In particular, verify that a changing `Role::Status` is actually announced rather than assuming its role alone guarantees a live announcement. Platform adapters differ, and [WebAssembly support](/docs/webassembly) must be checked independently from native desktop behavior. Pair this with visual checks for focus contrast, readable text, target size, reduced motion, and information that must not depend on color alone; see [Design Guides](./design-guides). ### A practical acceptance pass Use this sequence on every platform you plan to ship. Record the operating system, assistive technology, app build, and whether each result was observed; an AccessKit property in a test is evidence for the GPUI tree, not for every platform bridge. 1. **Keyboard only:** start before the form. Tab to the input and Save button, use Shift+Tab to return, and verify visible focus at each stop. Type, edit, and activate Save with Enter or Space. Confirm the visible result and saved model. For a composite widget, also try its documented arrow keys, Escape, and Tab exit. 2. **Assistive technology:** navigate to the same input and button. Confirm their announced role and name, the input value where disclosure is appropriate, and the state after Save. Activate the button through the assistive technology's control action. Check the resulting status announcement in the actual application; `Role::Status` alone is not proof that it will be spoken. 3. **State changes:** test empty or invalid input, disabled and enabled transitions, and an overlay if the flow opens one. Check that the user can find the error, that disabled commands cannot execute, and that focus reaches a sensible target after the overlay closes. 4. **Presentation:** zoom or enlarge text, inspect focus contrast and clipping, reduce motion where supported, and check that color is not the only way to distinguish success, error, or selection. ### If a check fails | Symptom | Inspect first | | --- | --- | | The custom node is absent from the painted snapshot. | Does it have a stable `.id(...)` and a meaningful `.role(...)`? Has `window.render_frame(cx)` run? For a custom `div` located by GPUI Kit's test helper, did you add `.test_support()` and enable `test-support`? | | The role is present but the name is missing or wrong. | Name the control itself (`Input::aria_label`, `Button::accessibility_label`, or `div().aria_label`); nearby text and tooltips are not a guaranteed naming relationship. | | The status text changes but its snapshot stays old. | Re-query after the interaction; check that the owner updated retained state and called `cx.notify()`. A saved `ElementSnapshot` describes one frame. | | A shortcut is announced but pressing it does nothing. | `.aria_keyshortcuts(...)` only reports a shortcut. Register the GPUI binding and attach its action handler to the intended focus path; see [Actions](./action). | | Pointer activation works but keyboard or assistive activation fails. | Inspect Tab stop, tracked focus, key context, action handler, and accessible action. A role and a hitbox alone do not provide these paths; consider using a built-in control. | | A headless assertion passes but a screen reader behaves differently. | Reproduce on the target platform and inspect the platform adapter's result. The headless helper does not validate announcements, focus visuals, or adapter behavior. | --- # Global Source: /docs/global `Global` marks a Rust type that GPUI can store once per [`App`](./context). It is useful for a setting or service shared by several features and windows. The value is keyed by its concrete Rust type, so `AppSettings` and [`Theme`](../component/theme) occupy different slots. `Global` is an empty marker trait with a `'static` bound; it does not make the value a View or create an event stream by itself. ```rust use gpui_kit::*; struct AppSettings { notifications_enabled: bool, } impl Global for AppSettings {} ``` The slot belongs to this `App`, not to a particular [`Window`](./window) or [`Entity`](./entity). Initialize it once at application startup, before Views read it. A second `set_global` for the same type replaces the previous value; it does not merge fields. ## Choose the owner | Owner | Use it for | How a change reaches a View | | --- | --- | --- | | `Global` in `App` | One setting or service shared by the whole application, including multiple windows. | Observe that global type and notify the dependent View, or use a domain API that refreshes windows. | | `Entity` | A document, feature model, or retained View state with its own lifetime and behavior. | Update the Entity and call `cx.notify()` when its rendered state changes; other owners can observe it or subscribe to its events. | | `Window` | Focus, input dispatch, bounds, and other state or operations belonging to one window. | Use the Window's own invalidation or refresh APIs as appropriate. | The value's *scope* does not determine what redraws. Reading a Global in `render` creates no automatic dependency. Conversely, a View can observe a Global even if it is not the code that changes it. Each window's View must retain its own observer if it renders that setting. For a concrete decision, imagine two editor windows. A notification preference applies to both windows, so one `AppSettings` Global is appropriate. Each window's selected tab and click count belong to its own View Entity. A document opened in both windows can instead have a shared model `Entity` that both Views observe. These are three different lifetimes: app, window View, and document. Moving a value to a Global merely to make it reachable does not establish the right lifetime or make rendering reactive. For a shared model exercise, see [Multi Window](./multi-window). ## Read and change a global | API | Result | Use | | --- | --- | --- | | `cx.has_global::()` | `bool` | Check whether the slot exists. | | `cx.global::()` | `&T` | Read a required value; panics if missing. | | `cx.try_global::()` | `Option<&T>` | Read an optional value. | | `cx.set_global(value)` | `()` | Install or replace a value and notify global observers. | | `cx.global_mut::()` | `&mut T` | Edit an installed value and notify global observers. | | `cx.update_global::(\|value, cx\| …)` | closure result | Edit an installed value while also using its GPUI context; notifies observers when the edit finishes. | | `cx.default_global::()` | `&mut T` | Read or install `T::default()` mutably; requires `T: Default`. | | `cx.update_default_global::(\|value, cx\| …)` | closure result | Update a value, installing its default first if needed. | | `cx.remove_global::()` | `T` | Remove an installed value and notify global observers. | `global_mut`, `update_global`, and `remove_global` require an existing value. A read does not notify observers. The mutable and defaulting APIs schedule a notification even if the code leaves the value unchanged. If redundant work matters, check with `global::()` **before** calling a mutable API; a comparison inside `update_global` is too late to avoid its notification. `remove_global` also notifies: an observer that reads the slot after removal must use `try_global` or handle absence another way. References returned by `global`, `global_mut`, and `default_global` are tied to the current GPUI call; copy or clone data needed later. `update_global` temporarily lends out the global so its closure can hold `&mut T` and `&mut cx` at the same time. Use the `value` passed to the closure. Do not try to read or update that same global again inside the closure. ```rust cx.update_global::(|settings, _cx| { settings.notifications_enabled = false; }); ``` [`Context`](./context) can call these application APIs because it dereferences to `App`. Code outside an Entity, such as application initialization, receives `&mut App` directly. ## App scope and Window scope Choose a global when all windows should see the same value: for example, an application preference, a theme, or a shared service handle. Keep focus, input dispatch, bounds, and other window-specific behavior in [Window](./window). A global is one slot for the entire app; putting a separate selection for each window into one global makes window ownership and cleanup harder. Keep a feature's business state in an Entity owned by that feature's crate or view. Reserve `Global` for services, settings, and coordination genuinely shared across the application; difficulty passing data between modules is not a reason to move a large business collection into an app-wide slot. When features need to collaborate, pass a lightweight Entity handle where the ownership boundary permits it, or use an explicit interface, command, or event. The [Coding Guides](./coding-guides) explain how to keep each feature's model and workflow behind its module boundary. GPUI Kit's `Theme` illustrates application-wide ownership. After `gpui_kit::init(cx)`, components read the active theme through `cx.theme()`. GPUI Kit also keeps derived theme data in sync for its lower layers and refreshes windows when the theme changes. Use `Theme::change(...)` for a mode change or `Theme::update(cx, |theme| { … })` for an edit; a raw `Theme::global_mut(cx)` edit does not perform that synchronization or refresh every window. This is a theme-specific rule on top of GPUI's ordinary `Global` behavior. ## Build a two-window example Changing a global notifies **global observers**. GPUI queues these notifications as effects and coalesces repeated touches of the same type while a notification is pending. The callback sees the current value, not an intermediate snapshot or a field-level change. A global update does not automatically call `cx.notify()` on every Entity that happened to read it. A View whose rendered output depends on a global can register `cx.observe_global::(...)`, then notify itself in the callback. Keep the returned `Subscription` in that View so the observer lives as long as the View. Use the existing `examples/hello_world` workspace package. Replace `examples/hello_world/src/main.rs` with the complete file below and run `cargo run -p hello_world --bin hello_world` from the repository root. No new package or dependency is needed. The example creates one Global before opening either window, then creates one `SettingsView` Entity per window. The Global stores the shared preference; `local_clicks` belongs only to that window's Entity. Both buttons report changes outside `render`. The complete shared-state path is: install the value during startup → create each View and store its observer → read the value in `render` → change it in an input callback → let each observer call its own `cx.notify()` → each View renders again from the current value. The setting is not copied into a second View field. ```rust use gpui_kit::*; use gpui_kit::assets::Assets; use gpui_kit::component::button::Button; struct AppSettings { notifications_enabled: bool, } impl Global for AppSettings {} struct SettingsView { _settings_observer: Subscription, local_clicks: usize, name: &'static str, } impl SettingsView { fn new(name: &'static str, cx: &mut Context) -> Self { let _settings_observer = cx.observe_global::(|_this, cx| { cx.notify(); }); Self { _settings_observer, local_clicks: 0, name, } } } impl Render for SettingsView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let enabled = cx.global::().notifications_enabled; div() .flex() .flex_col() .gap_2() .p_4() .child(format!("{}: notifications {}", self.name, if enabled { "on" } else { "off" })) .child(format!("Clicks in this window: {}", self.local_clicks)) .child( Button::new("toggle-notifications") .label("Toggle shared setting") .on_click(|_, _window, cx| { cx.update_global::(|settings, _cx| { settings.notifications_enabled = !settings.notifications_enabled; }); }), ) .child( Button::new("local-click") .label("Increment local clicks") .on_click(cx.listener(|this, _, _, cx| { this.local_clicks += 1; cx.notify(); })), ) } } fn main() { application() .with_assets(Assets) .run(|cx| { init(cx); cx.set_global(AppSettings { notifications_enabled: true, }); for name in ["First window", "Second window"] { open_window(WindowOptions::default(), cx, move |_, cx| { cx.new(|cx| SettingsView::new(name, cx)) }) .expect("failed to open window"); } }); } ``` Click **Toggle shared setting** in either window. Both windows should change between `notifications on` and `notifications off`. Click **Increment local clicks** in one window; only that window's number should change. Close one window and click either button in the remaining window; its own Entity and the app Global are still alive. The first button edits the Global through `update_global`. Both saved observers notify their owning Entities, so both renders read the new value. The second button mutates only its own Entity and calls that Entity's `cx.notify()`; it does not touch the Global. If you remove the observer field, the subscription drops and the shared label stops following later Global updates. If you remove the local button's `cx.notify()`, its count changes in memory but can remain stale on screen until another render. The observer notification is especially necessary if a View is [cached](./view-cache): a parent redraw alone can replay its old subtree. When one `SettingsView` is dropped, its stored `Subscription` drops and that window's callback disconnects; the other window's observer remains. `observe_global` reports that the type was touched, without a typed change payload. Use an [Event](./event) from an Entity when consumers need a specific operation or payload. For a debugging exercise, temporarily comment out the observer's `cx.notify()`, run the example, and toggle the setting. The Global changes but the two labels need not update immediately. Restore the call before continuing. This isolates the missing invalidation from a missing mutation. If a label remains stale with the call restored, check that each View stores its `Subscription` and that the mutation goes through a GPUI Global API. If a callback also needs its window, use `cx.observe_global_in::(window, ...)` and store its `Subscription`. For a window-level observer without an owning Entity, `window.observe_global::(cx, ...)` provides `&mut Window` and `&mut App` to the callback. Keep that subscription with a suitable owner as well. `window.refresh()` or `cx.refresh_windows()` explicitly requests rendering; neither is required for the View above because its observer calls `cx.notify()`. ## Common mistakes - Calling `cx.global::()` before installing `T`: it panics. Initialize first or use `try_global` for an optional slot. - Assuming `set_global` or `update_global` redraws every reader: register a global observer and notify dependent Views, or use an API such as GPUI Kit's theme update that explicitly refreshes windows. - Dropping the `Subscription` returned by `observe_global` at the end of a constructor: the observer stops immediately. - Putting per-window focus, selection, or document state in a single app global: give it a window or Entity owner, or store explicitly keyed state when application-wide coordination is actually required. - Writing GPUI Kit's theme through `global_mut` and expecting all theme projections and windows to update: use its theme APIs. - Mutating a global during `render`: render can run again for many reasons. Handle input or other effects outside rendering, then let observation invalidate the View. - Changing data *inside* a Global through an `Arc`, lock, atomic, or other interior-mutable handle and expecting Global observers to run: GPUI sees only calls to its Global mutation APIs. Explicitly notify the relevant owner or use an observable Entity for frequently changing data. --- # Task Source: /docs/task In GPUI, a [`Task`](https://docs.rs/gpui-pre/{{gpui_pre_version}}/gpui/struct.Task.html) is the handle to work scheduled by a GPUI executor. Its most important property is **ownership**: dropping the handle cancels unfinished work. A task runs only while its handle is stored, awaited, or explicitly detached. This makes the task's lifetime part of the View's state design, not just a detail of Rust's `Future` trait. The **spawn API** chooses where work runs; the returned `Task` controls its lifetime. A foreground task can re-enter GPUI through an async [Context](./context) and update an [Entity]. A background task runs away from the UI thread and returns owned data; it cannot mutate Entity state there. | Start from | Runs on | Async callback receives | Use for | | --- | --- | --- | --- | | `cx.spawn(...)` in `Context` | Foreground thread | `WeakEntity`, `&mut AsyncApp` | Awaiting I/O, timers, then updating an Entity | | `cx.spawn_in(window, ...)` | Foreground thread | `WeakEntity`, `&mut AsyncWindowContext` | Work whose completion needs the same [Window](./window) | | `cx.spawn(...)` in `App` | Foreground thread | `&mut AsyncApp` | Application-level work without a current Entity | | `cx.background_spawn(...)` | Background executor | No GPUI context | Expensive parsing or computation on owned `Send` data | ## Run the repository example From the repository root, after installing the platform prerequisites in [Installation](/docs/installation), run: ```sh cargo run -p example-stream-markdown ``` The window opens with a **Replay** button and a **Fade in streamed text** switch. Click Replay: the text clears, then appears in chunks. Click it again before the stream finishes to start a new replay; only chunks tagged with the current replay ID are displayed. Closing the window drops the View's task handles, but a producer already running its synchronous loop can finish that loop before stopping. The complete source is [`examples/stream-markdown/src/main.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/examples/stream-markdown/src/main.rs); its package and dependencies are in [`examples/stream-markdown/Cargo.toml`](https://github.com/MohsenDastaran/uni-kit/blob/main/examples/stream-markdown/Cargo.toml). This is a runnable workspace example; for a one-dependency app skeleton, start with [Getting Started](/docs/getting-started). Follow one click through the source: `main` creates the window; `Example::new` creates the channel and saves a foreground receiver `Task`; the button calls `Example::replay`, which increments `replay_id`, clears the text and replaces the background producer `Task`; the receiver checks that ID before updating `TextViewState`. The example uses a worker to simulate incoming chunks. In an actual network stream, await the source's asynchronous read and await a bounded channel send so a slow UI cannot cause unbounded queued data. ## Build a task you can start, cancel, and fail The streaming example shows task ownership but has no failure control. This small app makes all three outcomes visible. In the existing repository checkout, save the following as `examples/hello_world/src/bin/task_lab.rs` (create the `bin` directory if needed), then run `cargo run -p hello_world --bin task_lab` from the repository root. It uses the existing `hello_world` package and adds no dependency or workspace member. ```rust use gpui_kit::component::button::{Button, ButtonVariants}; use gpui_kit::*; use std::time::Duration; struct TaskLab { status: String, request_id: u64, task: Option>, } impl TaskLab { fn start(&mut self, fail: bool, cx: &mut Context) { self.request_id = self.request_id.wrapping_add(1); let request_id = self.request_id; self.task = None; // Drop the previous handle before starting again. self.status = format!("Loading request {request_id}..."); cx.notify(); self.task = Some(cx.spawn(async move |this, cx| { let result: Result = cx .background_spawn(async move { // Stand in for expensive work; this blocks a worker, not the UI thread. std::thread::sleep(Duration::from_millis(800)); if fail { Err("Simulated failure".into()) } else { Ok(format!("Completed request {request_id}")) } }) .await; _ = this.update(cx, |view, cx| { if view.request_id != request_id { return; // A newer start or cancel owns the displayed state. } view.status = match result { Ok(value) => value, Err(error) => format!("Failed: {error}"), }; cx.notify(); }); })); } fn cancel(&mut self, cx: &mut Context) { self.request_id = self.request_id.wrapping_add(1); self.task = None; // Dropping the handle prevents future polls. self.status = "Cancelled".into(); cx.notify(); } } impl Render for TaskLab { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .flex() .flex_col() .size_full() .items_center() .justify_center() .gap_2() .child(self.status.clone()) .child( Button::new("start") .primary() .label("Start") .on_click(cx.listener(|view, _, _, cx| view.start(false, cx))), ) .child( Button::new("fail") .label("Fail") .on_click(cx.listener(|view, _, _, cx| view.start(true, cx))), ) .child( Button::new("cancel") .label("Cancel") .on_click(cx.listener(|view, _, _, cx| view.cancel(cx))), ) } } fn main() { application().with_assets(assets::Assets).run(|cx| { init(cx); open_window(WindowOptions::default(), cx, |_, cx| { cx.new(|_| TaskLab { status: "Idle".into(), request_id: 0, task: None, }) }) .expect("Failed to open window"); }); } ``` Click **Start** and watch **Loading** become **Completed** after about 800 ms; the window should still accept clicks during the wait. Click **Fail** to see a deterministic **Failed** state. Click **Start**, then **Cancel** before it completes: **Cancelled** should remain visible. Click **Start** and immediately **Fail**: only the latest request may update the label. A cancelled worker that is already inside `sleep` may finish that blocking call; the dropped `Task` and request ID prevent its result from changing the View. This sleep is deliberately confined to a background worker for the exercise. Real I/O should use an async API, and long CPU jobs need bounded steps or another cancellation mechanism if prompt stopping matters. After the exercise, delete only `examples/hello_world/src/bin/task_lab.rs`; keep any other files in `src/bin`. In an unmodified checkout, this leaves only the original `hello_world` binary, so the `cargo run -p hello_world` commands in other guides work again. If you already have other binaries in that package, specify `--bin hello_world` when running its original example. The View owns the foreground `Task`. That task awaits a background `Task>`, then updates the View through the weak Entity supplied by `cx.spawn`. `cx.notify()` makes each status transition visible. Keep the completed handle in the field until the next start or cancel; dropping a completed task is harmless. The `Result` branch shows the error in the UI instead of silently discarding it. ## How execution moves between threads On a desktop app, GPUI's **foreground executor** polls `cx.spawn` and `cx.spawn_in` futures on the main/UI thread. Several foreground tasks can be *concurrent*: when one awaits an unfinished operation, its poll returns `Pending`, and the UI thread can handle input, render, or poll another task. They do not run in parallel with each other on separate UI threads. An `await` whose value is already ready may continue in the same poll; an expensive synchronous function inside a foreground task still blocks the UI until it returns. The **background executor** schedules `Send` work through the platform's background dispatch queue or worker pool. On desktop, worker polls may run in parallel with the UI thread and with other background tasks, subject to available workers. GPUI does **not** create an OS thread for every `Task`. The actual worker count and scheduling depend on the platform; test executors may simulate the scheduling without parallel OS threads. A background future may be polled on different workers over its lifetime, so do not rely on worker thread affinity.
GPUI foreground and background task flow Two columns show the main UI thread and the background executor. An event queues a foreground task. It dispatches Send work, yields while awaiting a pending result, and resumes on the UI thread to update an Entity and request a later render. Main / UI thread Background executor 1 · User eventcx.spawn(...) 2 · Foreground pollcx.background_spawn(work) Send future queuedPlatform workers / dispatch queue 3 · Await background TaskIf Pending, UI can handle input/render Worker polls / computesMay run in parallel with UI work 4 · Resume on UI threadWeakEntity::update · cx.notify() Send result readyWake the foreground Task GPUI task flow on a narrow screen The same flow stacked vertically: the UI thread starts and polls a task, background workers compute owned Send data, and the UI thread resumes to update an Entity and request rendering. Main / UI thread 1 · User eventcx.spawn(...) 2 · Foreground pollbackground_spawn(work) 3 · await TaskPending → UI free Background executor Queue Send futurePlatform workers / dispatch queue Worker poll / compute Send result → wake UI Main / UI thread 4 · Resume and update EntityWeakEntity::update · cx.notify()
A typical pending path. The worker returns owned data; only the foreground update mutates GPUI state. Colors and text adapt to the site's light and dark themes.
`cx.spawn` accepts a foreground future that need not be `Send`, so it can hold main-thread-only GPUI handles, but it still must be `'static`: move owned inputs into it instead of borrowing `self`. `background_spawn` requires both its future and its output to be `Send + 'static`. Move owned data into the worker and return a `Send` result; keep `Entity`, `Window`, and `Context` updates on the foreground side. Awaiting the background `Task` from the foreground task schedules the **continuation** back on the foreground executor when the result is ready. `cx.notify()` then invalidates the View for a later render; it does not synchronously draw a frame. An async GPUI context is a way back into GPUI after `await`, not permission to keep a mutable `Context` or `Window` borrowed across suspension. Capture owned inputs before spawning. Use `WeakEntity::update` for a short, synchronous state change after the result arrives; use `update_in` when the same window is required. No UI mutation belongs inside a `background_spawn` future. ## Start work from an owner Start a task in a named method, event handler, or lifecycle hook. Do not start one unconditionally in [`render`](./render): every render could launch another copy. Extract the input before spawning, so no borrow of `self` or `cx` crosses an `await`. ```rust struct SearchView { query: String, results: Vec, _search_task: Option>, } impl SearchView { fn search(&mut self, cx: &mut Context) { let query = self.query.clone(); self._search_task = Some(cx.spawn(async move |this, cx| { let results = search_index(query).await; _ = this.update(cx, |view, cx| { view.results = results; cx.notify(); }); })); } } ``` The callback gets a `WeakEntity` named `this`. It does not keep the View alive. After the `await`, `this.update` reacquires the Entity on the foreground thread and returns an error if the View has gone away. Handle that case with `?`, `if let`, or an intentional `_ =` when disappearance is normal. Call `cx.notify()` after changing View state so dependent UI renders again. Assigning a new `Task` to `_search_task` drops the old handle and cancels the previous search. The field is an `Option` because this View has no task until the user starts one. A View that starts work during construction can store a plain `Task<()>` instead. `search_index` and `SearchResult` above stand for application code; this snippet illustrates ownership, rather than defining a complete application. For a complete buildable workflow, run the repository example above. Cancellation is cooperative with async execution. Dropping a task prevents further polling; it cannot undo an external side effect that already happened or stop a blocking function in the middle of a call. For results that may arrive after a newer request, also check a request ID or revision before applying them. ## Keep, await, or detach Choose the lifetime when you create the task: - **Store it** on the owning Entity/View or a window-scoped owner when the work should end with that owner. Replacing an `Option>` is useful for search, refresh, debounce, and ongoing streams. - **Await it** from another task when the next step needs its result. The awaiting task owns the handle until completion. - **Detach it** with `.detach()` for one-off work that should finish independently of the current owner. This consumes the handle and lets the task run to completion; the owner can no longer cancel it by dropping a field. Give its callback a weak Entity and handle failed updates if the View may close first. Calling `cx.spawn(...);` as a bare statement drops the returned handle at the end of the statement and may cancel the task before it does useful work. Detaching a task that returns `Result` also discards that result unless the task itself reports an error. `Task::detach()` is different from `Subscription::detach()`: the former lets work continue independently until it completes; the latter leaves a callback subscribed until its source Entity is dropped. For user-facing operations, keep loading and failure state in the View and update it on completion. Repeatedly spawning and detaching from a render path or recurring callback can leave many independent tasks running at once; a finite detached task completes normally, so `.detach()` alone is not a leak. Keep recurring or long-lived work under an owner-held `Task`, and replace or drop that handle when the work should stop. If the owner stores the Task while its future captures a strong handle back to that same Entity, they form a retention cycle. Use the `WeakEntity` supplied by `Context::spawn`, or capture `cx.weak_entity()`, when owner release should cancel the work. See [Entity ownership cycles](./entity#use-a-weakentity-for-back-references-and-callbacks). ## Re-enter GPUI after `await` From `Context`, `spawn` supplies `AsyncApp`, which has application access but no current `&mut Window`. Use `spawn_in` when the completion must change Focus, show a prompt, or otherwise use the originating Window: ```rust fn submit(&mut self, window: &mut Window, cx: &mut Context) { let draft = self.draft.clone(); self._submit_task = Some(cx.spawn_in(window, async move |this, cx| { let message = send_message(draft).await; _ = this.update_in(cx, |view, window, cx| { view.messages.push(message); view.input_focus.focus(window, cx); cx.notify(); }); })); } ``` `update_in` restores `&mut Self`, `&mut Window`, and `&mut Context` for one synchronous update. The Window or Entity might be gone by then, so handle its result. Use `update` when the Window is irrelevant. From an application-level `App::spawn`, the callback receives only `AsyncApp`; call `cx.update(|cx| { ... })` for a short application mutation after awaiting. ## Move heavy work off the UI thread A foreground task can await nonblocking I/O without occupying the UI thread while pending, but CPU-intensive work inside a poll still occupies it. Move owned, `Send` input into `background_spawn`, await its `Task` from a foreground task, and apply the result back on the UI thread: ```rust struct DocumentView { source: String, // Mutable editing buffer. revision: u64, parsed: Option, _parse_task: Option>, } impl DocumentView { fn parse(&mut self, cx: &mut Context) { self.revision = self.revision.wrapping_add(1); let revision = self.revision; let source = self.source.clone(); self._parse_task = Some(cx.spawn(async move |this, cx| { let parsed = cx.background_spawn(async move { parse_document(source) }).await; _ = this.update(cx, |view, cx| { if view.revision != revision { return; // An older result must not replace newer content. } view.parsed = Some(parsed); cx.notify(); }); })); } } ``` The worker receives the `String` and returns a `Send` `ParsedDocument`; it has no `App`, `Window`, or `Context`. Clone only the input it needs before leaving the Entity update. Replacing `_parse_task` cancels the previous outer task and its awaited worker task, but cannot interrupt a synchronous parse already executing inside one poll. The revision check also rejects a result that became stale before the foreground update. ## Handle completion, failure, and cancellation For a user-visible operation, keep an explicit state such as idle, loading, loaded, or failed in the owning View. Set loading and notify before spawning. Have the worker return `Result` as owned, `Send` data. After `await`, first reject an outdated request ID, then handle `Ok` by storing data or `Err` by storing a readable error; clear loading and notify once for the complete change. Keep useful previous data visible during a refresh if the UI allows it. A failure that is only logged or dropped by `.detach()` leaves users looking at a perpetual spinner. Treat release of the `WeakEntity` or originating window as an ordinary cancellation path: a failed `update` or `update_in` means there is no UI left to change. Dropping a task handle prevents future polls but does not undo I/O or stop synchronous worker code already running. If the operation has an external side effect, decide separately whether it must finish and how to report its outcome. A request ID still guards against queued messages or a late result after replacement. Never wait for another task with `while !ready {}` or repeated immediate polls. Such a loop occupies the UI thread and can prevent the awaited work from advancing. Await the `Task`, a timer, an I/O future, or a channel receive. For periodic work, await a timer between iterations and keep its handle with the owner. Large CPU work belongs on the background executor; break very long computation into bounded units if prompt cancellation matters. Avoid blocking sleep or blocking I/O in a foreground task. ## Check and diagnose task behavior Run the example above and check these observable cases: Replay streams text, and a second Replay clears it and excludes old chunks. Closing the window drops the View and its task handles; it does not prove that an already running producer stops immediately. To distinguish a frozen UI from a slow worker, inspect whether the foreground future reaches a pending `await`; code before the first pending point runs on the UI thread. If nothing appears, check that the `Task` handle was retained, that the receiver is still alive, and that an update failure is handled. If stale text appears, inspect the request ID at the point of the UI update. If loading never ends, inspect both the success and error branches and whether the awaited operation can complete. For app tests, put pure parsing and request ordering in ordinary Rust tests; use GPUI context tests for the owner lifecycle and stale-result guard. A UI interaction test should trigger the action and observe loading, success, and failure through rendered state. Drive async work with the test executor rather than sleeps or a busy loop. The repository example itself can be checked with `cargo check -p example-stream-markdown` and manually exercised with the run command above. ## A GPUI Kit streaming example GPUI Kit's [streaming Markdown example](https://github.com/MohsenDastaran/uni-kit/blob/main/examples/stream-markdown/src/main.rs) uses two owned tasks and a channel. A background producer generates text chunks. A foreground receiver owns the Entity update, checks a replay ID, and pushes accepted chunks into `TextViewState`. The View keeps both `Task<()>` handles; closing it drops those handles, and another replay replaces the producer handle. The producer's async block contains no `await`, so once its synchronous loop is being polled, dropping its handle cannot interrupt that poll. The replay ID rejects chunks from an older producer, including ones queued before replacement. ```rust // Condensed from Example::new; the returned Task is stored as _task. let _task = cx.spawn(async move |weak_self, cx| { while let Ok((replay_id, chunk)) = rx.recv().await { _ = weak_self.update(cx, |this, cx| { if replay_id != this.replay_id { return; } this.markdown_state.update(cx, |state, cx| { state.push_str(&chunk, cx); }); this.scroll_handle.scroll_to_bottom(); }); } }); // Condensed from Example::replay; replacement drops the prior handle. self._update_task = cx.background_executor().spawn(async move { let chars: Vec = EXAMPLE.chars().collect(); while current < chars.len() { let chunk_size = (5 + rand::random::() % 15).min(chars.len() - current); let chunk: String = chars[current..current + chunk_size].iter().collect(); _ = tx.try_send((replay_id, chunk)); current += chunk_size; std::thread::sleep(std::time::Duration::from_millis(50)); } }); ``` The excerpt shows the ownership and update points; see the linked source for setup and rendering. The example uses `std::thread::sleep` in its background producer to simulate pacing and an **unbounded channel** with `try_send` only for this demonstration. An unbounded channel does not provide backpressure and can grow when a producer outpaces the UI. Its producer can continue sending after replacement until the synchronous loop returns; the replay ID keeps those chunks out of the UI. For a real stream, use asynchronous waiting and a bounded channel with backpressure, handle send and update errors, and end the receiver when its View is gone. `WeakEntity` protects the View lifetime, and the channel crosses executors. See [Entity](./entity) for Entity ownership and updates. [Entity]: ./entity.md --- # Comparison Source: /docs/comparison This guide compares **cross-platform desktop UI frameworks** and their documented ecosystems. Green means the capability is available, yellow means it needs more application work or validation, and red means it is not provided. These marks describe availability, not performance. Check the version, backend, platform, and license for your application. Legend: Yes · Partial · No. Ecosystem packages count where noted below. | Capability | GPUI Kit | Iced | egui | Qt 6 | Slint | | --- | --- | --- | --- | --- | --- | | UI model | Declarative passes + retained state | Declarative view + retained state | Immediate | Retained scene | Reactive tree | | UI authoring | Rust | Rust | Rust | QML / C++ / Python | `.slint` + Rust / C++ / JavaScript / Python | | Visual tooling | Code + component gallery | Code | Code | Qt Quick Designer | Live Preview / SlintPad | | Component count | [75+](/component) | [35](https://docs.rs/iced/0.14.0/iced/widget/#structs) | [16](https://docs.rs/egui/0.36.2/egui/widgets/#structs) | [52](https://doc.qt.io/qt-6/qml-qtquick-controls-control.html) | [24](https://docs.slint.dev/latest/docs/slint/reference/std-widgets/overview/) | | Rendering stack | GPUI | wgpu | eframe: glow / wgpu | Qt RHI / scene graph | FemtoVG / Skia / software | | Documentation | Complete core + component docs; API + gallery | Book + API | API + demos | Guides + API + designer | Guides + API + preview | | Default UI style | Theme-driven components | Styled widgets | egui visuals | Qt Quick styles | Slint widget styles | | Desktop OS | | | | | | | Multiple windows | | | | | | | Shortcuts | | | | | | | Themes | | | | | | | Bundled theme presets | 38 (36 variants + Light/Dark) | 22 built-in variants | Light / Dark | Qt Quick styles | Slint widget styles | | Code editor | | | | | | | CJK font support | System / bundled fonts | Font-dependent | Custom font required | System fallback | Font-dependent | | Text model | Rope | COSMIC Text | TextBuffer | QTextDocument | TextEdit string | | Syntax highlighting | | | | | | | Markdown | | | | | | | Markdown with inline HTML | | | | | | | HTML rendering | | | | | | | Rich text | | | | | | | Text selection | | | | | | | Advanced data table | | | | | | | Large-table virtualization | Rows + columns | No | Rows (`egui_extras`) | Rows + columns (`TableView`) | Rows | | Resizable table columns | | | | | | | Virtual list | | | | | | | Charts | | | | | | | Docking | | | | | | | Animation | | | | | | | Accessibility | | | | | | | I18N | | | | | | | UI testing | | | | | | | Mobile | | | | | | | WebAssembly | | | | | | | License | Apache-2.0 | MIT | MIT / Apache-2.0 | Commercial / LGPLv3 / GPLv3 by module | GPLv3 / commercial / royalty-free | | Minimal Binary Size | ~12 MB | ~11 MB | ~5 MB | ~20 MB | ~21 MB | | WebView | | | | | | **Component count.** GPUI Kit documents 75+ components and primitives. The other numbers count entries in the linked Iced 0.14 widget module (35), egui 0.36 widgets module (16, including `TextEdit`), Qt Quick `Control` descendants (52), and Slint standard widgets catalog (24). These catalogs include different kinds of helpers and exclude different add-ons, so the counts show catalog scale rather than a strict ranking. **Documentation.** GPUI Kit covers its current public surface with [core guides](/docs), [component documentation](/component), [API reference](https://docs.rs/gpui-kit/latest/gpui_kit/), and a [live gallery](/gallery/). Experimental features are documented with their current limits. **Size provenance.** The first four values preserve the original `main` branch's Hello World release estimates; its Qt footnote linked [this binary-size study](https://www.qt.io/blog/reducing-binary-size-of-qt-applications-part-3-more-platforms). Slint's ~21 MB comes from a Slint 1.18.1 Hello World built with `cargo build --release` on Linux x86-64 and stripped to 20,768,216 bytes (19.81 MiB). These are approximate references from different build conditions, not verified minimums or a same-method benchmark. **Frame rate.** This table does not rank frame rates: no shared workload, hardware, or presentation measurement backs a cross-framework FPS figure. A 120 Hz target gives each frame roughly 8.3 ms across the relevant pipeline; it is not a claim that an idle window redraws continuously or that every complex screen sustains 120 displayed frames per second. See [FPS Monitor](./fps#120-hz-is-a-frame-budget-not-a-refresh-promise) for GPUI's draw and present measurements. ## Decision guide The capability matrix shows coverage; these trade-offs show when that coverage matters. They are judgments about the documented APIs, not benchmark results. | If your product needs… | GPUI Kit advantage | GPUI Kit cost or limit | Compare with… | | --- | --- | --- | --- | | A code editor with dense data views | [Editor](/component/editor), [DataTable](/component/data-table), [VirtualList](/component/virtual-list), and [Dock](/component/dock) live in one Rust component stack. | The app still owns data sorting and must validate [accessibility](/docs/accessibility) for its target platforms. | Qt's [model/view](https://doc.qt.io/qt-6/modelview.html) and [QTextDocument](https://doc.qt.io/qt-6/qtextdocument.html) offer a broader established desktop stack. | | Selectable Markdown and article HTML | [TextView](/component/text-view) covers both in the same UI tree. | Its HTML support excludes general CSS layout and scripts. | Qt [QTextDocument](https://doc.qt.io/qt-6/richtext-html-subset.html) supports a broader rich-text subset; use a WebView when browser behavior is required. | | A product on desktop, mobile, and web | The same GPUI Kit component APIs can be explored in [WebAssembly showcases](/docs/webassembly) and experimental [iOS work](/docs/mobile). | Mobile and Web integrations still need validation for the target application. | Qt's [platform matrix](https://doc.qt.io/qt-6/supported-platforms.html) and Slint's [mobile](https://docs.slint.dev/latest/docs/slint/guide/platforms/mobile/general/) and [web](https://docs.slint.dev/latest/docs/slint/guide/platforms/web/) guides document broader targets. | | Visual UI authoring | Rust code and the [component gallery](/component) keep the interface close to application logic. | GPUI Kit does not include a visual designer. | [Qt Quick Designer](https://doc.qt.io/qtcreator/creator-using-qt-quick-designer.html) offers visual editing; Slint provides [Live Preview](https://github.com/slint-ui/slint#tooling). | > AI can edit GPUI Kit's Rust UI code directly; a drag-and-drop editor is unnecessary. ## What the rows mean **Rendering model.** GPUI Kit constructs an element description from current inputs during a [render](./render) pass. It retains [Entities](./entity), keyed element state, and optionally [cached views](./view-cache). The label in the table describes both parts; it is not a promise to rebuild the entire UI on every display refresh. The [rendering model explanation](./fps#immediate-retained-and-hybrid-describe-different-layers) separates these layers. Iced describes a [state/message/update/view architecture](https://book.iced.rs/architecture.html). egui calls itself [immediate mode](https://docs.rs/egui/latest/egui/#understanding-immediate-mode). Qt Quick [retains its scene graph between frames](https://doc.qt.io/qt-6/qtquick-visualcanvas-scenegraph.html), while Slint uses [reactive property bindings](https://docs.slint.dev/latest/docs/slint/guide/language/concepts/reactivity/). **Desktop basics.** All five can open multiple desktop windows, handle keyboard shortcuts, and customize visual themes, though the APIs differ. For example, Iced exposes [window opening](https://docs.rs/iced/latest/iced/window/fn.open.html), eframe supports [native viewports](https://docs.rs/eframe/latest/eframe/trait.App.html), and Slint documents [key bindings](https://docs.slint.dev/latest/docs/slint/reference/keyboard-input/overview/) and [widget styles](https://docs.slint.dev/latest/docs/slint/reference/std-widgets/style/). Test IME, clipboard, and drag-and-drop behavior on each target platform. **Rendering, themes, and CJK.** The “Rendering stack” row names the framework or selectable graphics backends, not a common GPU benchmark: egui's eframe supports [glow and wgpu](https://docs.rs/eframe/latest/eframe/enum.Renderer.html), Qt Quick uses a [scene graph over Qt RHI](https://doc.qt.io/qt-6/qtquick-visualcanvas-scenegraph.html), and Slint lists [FemtoVG, Skia, and software renderers](https://github.com/slint-ui/slint#runtime). GPUI Kit's 38 theme choices are the 36 variants in this repository's `themes/*.json` plus Default Light and Dark; Iced 0.14 documents [22 preset variants plus Custom](https://docs.rs/iced/0.14.0/iced/theme/enum.Theme.html). The original table's “egui CJK: Bad” was too broad: egui [requires a custom font for Asian characters](https://docs.rs/egui/latest/egui/#installing-additional-fonts), while Qt documents [script-aware fallback](https://doc.qt.io/qt-6/qfontdatabase.html). Font coverage still depends on the shipped or installed fonts. **Table column resizing.** GPUI Kit, [egui_extras](https://docs.rs/egui_extras/latest/egui_extras/struct.TableBuilder.html#method.resizable), [Qt](https://doc.qt.io/qt-6/qheaderview.html), and [Slint](https://github.com/slint-ui/slint/blob/master/internal/compiler/widgets/fluent/tableview.slint) support it; [Iced](https://docs.rs/iced/latest/iced/widget/table/struct.Column.html) does not provide built-in drag resizing. **Large-table virtualization.** This retains the original table's distinction between visible rows and visible columns: GPUI Kit's `DataTable` tracks both in its [visible range](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/component/src/table/state.rs); [egui_extras `TableBody::rows`](https://docs.rs/egui_extras/latest/egui_extras/struct.TableBody.html#method.rows) renders visible rows; and Qt Quick [`TableView`](https://doc.qt.io/qt-6/qml-qtquick-tableview.html) reuses delegates as rows and columns leave the viewport. Iced's documented [table API](https://docs.rs/iced/0.14.0/iced/widget/table/fn.table.html) does not promise built-in virtualization. Slint's [`StandardTableView`](https://github.com/slint-ui/slint/blob/master/internal/compiler/widgets/fluent/tableview.slint) places its row repeater inside a `ListView`, which [instantiates only visible rows](https://docs.slint.dev/latest/docs/slint/reference/std-widgets/views/listview/). Its column and cell repeaters do not use the same viewport virtualization. **Editor and highlighting.** GPUI Kit's [Editor](/component/editor) stores text in a [Rope](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/src/input/base/state.rs#L343). It provides code-oriented behavior such as folding, diagnostics, completion, and hover. Syntax highlighting uses [Tree-sitter](https://tree-sitter.github.io/tree-sitter/) when the matching [grammar feature](/component/editor#basic-usage) is enabled; large buffers are parsed in the background, and edits can reuse the previous parse tree. The editor's text model and highlighting engine are separate decisions: a Rope alone does not imply highlighted text. Iced's [TextEditor](https://docs.rs/iced/latest/iced/widget/text_editor/struct.TextEditor.html) has feature-gated highlighting, egui's [TextEdit](https://docs.rs/egui/latest/egui/widgets/text_edit/struct.TextEdit.html) accepts a custom layouter and can use [egui_extras syntax highlighting](https://docs.rs/egui_extras/latest/egui_extras/syntax_highlighting/), while Qt provides [QSyntaxHighlighter](https://doc.qt.io/qt-6/qsyntaxhighlighter.html). This row describes an available API, not comparable performance at the same document size. **Text models.** The table names each editor's storage or editing abstraction: GPUI Kit keeps text in a Rope; Iced uses [COSMIC Text](https://docs.iced.rs/src/iced_graphics/text/editor.rs.html), egui accepts a [TextBuffer](https://docs.rs/egui/latest/egui/widgets/text_edit/trait.TextBuffer.html), and Qt exposes [QTextDocument](https://doc.qt.io/qt-6/qtextdocument.html). This describes an API shape, not a comparative speed result. **Formatted text.** GPUI Kit [TextView](/component/text-view) renders selectable Markdown and HTML; its Markdown parser also passes inline HTML nodes to its HTML parser. Iced's [Markdown widget](https://docs.rs/iced/latest/iced/widget/markdown/) does not expose an HTML item; egui relies on add-ons for Markdown, and Qt's [QTextEdit](https://doc.qt.io/qt-6/qtextedit.html) supports Markdown with only part of embedded HTML. “No” in this row means the documented built-in Markdown path does not handle inline HTML; an application can still add another renderer. **HTML rendering.** [TextView::html](/component/text-view#html) handles article content such as headings, paragraphs, links, images, lists, and tables. It is partial because it does not implement general CSS layout or run scripts; it is not a WebView. Qt's [QTextDocument](https://doc.qt.io/qt-6/richtext-html-subset.html) supports a broader HTML 4 and CSS subset, also without browser behavior. This row compares documented native HTML document rendering, so a separate WebView integration does not count. Iced, egui, and Slint do not provide an equivalent native document renderer in the compared APIs. **Tables and lists.** “Advanced data table” means a component for large datasets with virtualized rendering, sorting, selection, and column management. GPUI Kit's [DataTable](/component/data-table) includes these behaviors; sorting the underlying data is supplied by its delegate. Qt's [QTableView](https://doc.qt.io/qt-6/qtableview.html) and model/view system also cover this use case. [egui_extras TableBuilder](https://docs.rs/egui_extras/latest/egui_extras/struct.TableBuilder.html) virtualizes rows and resizes columns, but leaves more data behavior to the app. Slint's [StandardTableView](https://docs.slint.dev/latest/docs/slint/reference/std-widgets/views/standardtableview/) exposes sorting callbacks and selection; Iced's [Table](https://docs.rs/iced/latest/iced/widget/table/fn.table.html) provides a simpler starting point. Separately, GPUI Kit [VirtualList](/component/virtual-list), egui [ScrollArea::show_rows](https://docs.rs/egui/latest/egui/containers/scroll_area/struct.ScrollArea.html#method.show_rows), Qt [ListView](https://doc.qt.io/qt-6/qml-qtquick-listview.html), and Slint [ListView](https://docs.slint.dev/latest/docs/slint/reference/std-widgets/views/listview/) create visible list items on demand. Iced offers [scrolling and visibility primitives](https://docs.rs/iced/latest/iced/widget/struct.Sensor.html), with virtualization left to the application. **Dense desktop UI.** GPUI Kit includes [VirtualList](/component/virtual-list), [DataTable](/component/data-table), [charts](/component/chart), [TextView](/component/text-view), and [Dock](/component/dock). Iced has [rich text](https://docs.rs/iced/latest/iced/widget/fn.rich_text.html), a [table](https://docs.rs/iced/latest/iced/widget/table/fn.table.html), a feature-gated [Markdown widget](https://docs.rs/iced/latest/iced/widget/markdown/), [Canvas](https://docs.rs/iced/latest/iced/widget/struct.Canvas.html), and [pane grids](https://docs.rs/iced/latest/iced/widget/pane_grid/); assembling a virtualized table or chart remains application work. The egui ecosystem supplies third-party [egui_commonmark](https://docs.rs/egui_commonmark/latest/egui_commonmark/) for Markdown, [egui_dock](https://docs.rs/egui_dock/latest/egui_dock/) for docking, [egui_extras tables](https://docs.rs/egui_extras/latest/egui_extras/struct.TableBuilder.html) and [egui_plot](https://docs.rs/egui_plot/latest/egui_plot/). Qt provides [model/view tables](https://doc.qt.io/qt-6/modelview.html), [dock widgets](https://doc.qt.io/qt-6/qdockwidget.html), [QTextDocument](https://doc.qt.io/qt-6/qtextdocument.html), and the separately licensed [Qt Graphs](https://doc.qt.io/qt-6/qtgraphs-index.html) module. Slint provides [StandardTableView](https://docs.slint.dev/latest/docs/slint/reference/std-widgets/views/standardtableview/) and a [ListView that instantiates visible items](https://docs.slint.dev/latest/docs/slint/reference/std-widgets/views/listview/); its [Path](https://docs.slint.dev/latest/docs/slint/reference/elements/path/) can draw custom graphics, but is not a ready-made chart. **Product readiness.** GPUI Kit's [Accessibility](/docs/accessibility), [Testing](/docs/test), [I18n](/docs/i18n), and [Animation](/docs/animation) guides describe the local implementation and its limits. Its components contain translations, while application strings and locale policy remain the application's responsibility. Iced's [accessibility integration is still tracked as open work](https://github.com/iced-rs/iced/issues/552); its [test harness](https://docs.rs/iced_test/latest/iced_test/) is available. egui documents [AccessKit and egui_kittest](https://github.com/emilk/egui/blob/main/docs/accessibility.md). Qt has mature [accessibility](https://doc.qt.io/qt-6/accessible.html), [internationalization](https://doc.qt.io/qt-6/internationalization.html), and [Qt Test](https://doc.qt.io/qt-6/qtest-overview.html) facilities. Slint documents [accessibility properties](https://docs.slint.dev/latest/docs/slint/reference/common/#accessibility-properties) and [translations](https://docs.slint.dev/latest/docs/slint/guide/development/translations/); its [testing backend](https://docs.slint.dev/latest/docs/rust/i_slint_backend_testing/) is preliminary and belongs to an internal crate. A “Yes” in the accessibility row still requires testing with assistive technology on each target platform. **Platform reach.** GPUI Kit has [experimental iOS integration](/docs/mobile) and working [WebAssembly showcases](/docs/webassembly); the latter have not been validated here as a full application distribution path. Iced has a [web example](https://github.com/iced-rs/iced/blob/master/examples/README.md#tour), while [native mobile support remains under discussion](https://github.com/iced-rs/iced/issues/302). [eframe](https://github.com/emilk/egui/blob/main/README.md#official-integrations) runs egui on web and native platforms, with Android/iOS integration to validate for each app. Qt documents its [supported platforms](https://doc.qt.io/qt-6/supported-platforms.html) and [WebAssembly limits](https://doc.qt.io/qt-6/wasm.html). Slint documents [mobile](https://docs.slint.dev/latest/docs/slint/guide/platforms/mobile/general/) and [web](https://docs.slint.dev/latest/docs/slint/guide/platforms/web/) targets; its web output is a canvas, without browser screen-reader support. None of these canvas-based Wasm paths should be treated as an HTML application by default. **WebView.** GPUI Kit has an experimental [Wry integration](/docs/webview). Its native view covers GPUI elements in the same bounds, so use a separate window or popup when layering matters. The integration currently documents macOS and Windows; its Linux example is unfinished. Qt provides official [WebEngine](https://doc.qt.io/qt-6/qwebengineview.html) and [WebView](https://doc.qt.io/qt-6/qtwebview-index.html) modules, though Qt WebView also limits overlapping QML items. This row counts integrations maintained for the compared framework version: Iced has a third-party [iced_webview](https://docs.rs/iced_webview/latest/iced_webview/) crate targeting Iced 0.13, but no verified integration here for current Iced 0.14; egui has no comparable maintained native WebView, and Slint's [WebView request](https://github.com/slint-ui/slint/issues/3930) remains open. A red dot does not rule out an application-specific bridge. **Licensing and scope.** GPUI Kit is Apache-2.0; Iced is [MIT](https://github.com/iced-rs/iced/blob/master/LICENSE); egui is [MIT or Apache-2.0](https://github.com/emilk/egui/blob/main/LICENSE-MIT); Qt uses [commercial, LGPLv3, or GPLv3 terms by module](https://doc.qt.io/qt-6/licensing.html), with Qt Graphs GPLv3 or commercial; Slint has [GPLv3 and commercial or royalty-free terms](https://slint.dev/pricing). Native single-platform stacks such as SwiftUI and WinUI have a different platform scope. Electron and Tauri use a WebView/JavaScript UI architecture and deserve a separate evaluation rather than a score in this native UI matrix. Bundle size and frame rate depend on build features, renderers, fonts, packaging, and workload; measure a release build of the product you intend to distribute. --- # HoverCard Source: /component/hover-card HoverCard component for displaying rich content that appears when the mouse hovers over a trigger element. Ideal for previewing user profiles, link previews, and other contextual information without requiring a click. Features configurable delays for both opening and closing to prevent flickering during quick mouse movements. This is most like the [Popover] component, but triggered by hover instead of click, and with timing controls for a smoother user experience. On iOS and Android, tap the trigger to open or close the card. Tapping outside closes it; tapping inside keeps it open. Hover delays do not apply. Tooltip hints remain disabled; see [Mobile](/docs/mobile). ## Import ```rust use gpui_kit::component::hover_card::HoverCard; ``` ## Usage ### Hover ```rust use gpui_kit::{ParentElement as _, Styled as _}; use gpui_kit::component::{hover_card::HoverCard, v_flex}; HoverCard::new("basic") .trigger( div() .child("Hover over me") .text_color(cx.theme().primary) .cursor_pointer() .text_sm() ) .child( v_flex() .gap_2() .child( div() .child("This is a hover card") .font_semibold() .text_sm() ) .child( div() .child("You can display rich content when hovering over a trigger element.") .text_color(cx.theme().muted_foreground) .text_sm() ) ) ``` ### User Profile Preview A common use case is showing user profiles when hovering over a username, similar to GitHub or Twitter: ```rust use gpui_kit::{px, relative, Styled as _}; use gpui_kit::component::{ avatar::Avatar, hover_card::HoverCard, h_flex, v_flex, }; h_flex() .child("Hover over ") .text_sm() .child( HoverCard::new("user-profile") .trigger( div() .child("@huacnlee") .cursor_pointer() .text_color(cx.theme().link) ) .child( h_flex() .w(px(320.)) .gap_4() .items_start() .child( Avatar::new() .src("https://avatars.githubusercontent.com/u/5518?s=64") ) .child( v_flex() .gap_1() .line_height(relative(1.)) .child(div().child("Jason Lee").font_semibold()) .child( div() .child("@huacnlee") .text_color(cx.theme().muted_foreground) .text_sm() ) .child("The author of GPUI Kit.") ) ) ) .child(" to see their profile") ``` ### Custom Timing Adjust the opening and closing delays to suit your needs: ```rust use std::time::Duration; use gpui_kit::Styled as _; use gpui_kit::component::{ button::{Button, ButtonVariants as _}, h_flex, }; h_flex() .gap_4() .child( HoverCard::new("fast-open") .open_delay(Duration::from_millis(200)) .close_delay(Duration::from_millis(100)) .trigger(Button::new("fast").label("Fast Open (200ms)").outline()) .child(div().child("This hover card opens after 200ms").text_sm()) ) .child( HoverCard::new("slow-open") .open_delay(Duration::from_secs(1)) .close_delay(Duration::from_secs_f32(0.5)) .trigger(Button::new("slow").label("Slow Open (1000ms)").outline()) .child(div().child("This hover card opens after 1000ms").text_sm()) ) ``` ### Positioning HoverCard supports 6 positioning options using the [Anchor] type: - TopLeft - TopCenter - TopRight - BottomLeft - BottomCenter - BottomRight Imagine the card has a pointer tip (like a speech bubble's tail). The anchor is where that tip sits relative to the trigger — `TopCenter` places it at the trigger's top center, `BottomRight` at the bottom-right, and so on. The card then hangs off that point. For example, `Anchor::TopLeft` places the card just below the trigger, left-aligned to it: ```text [ Trigger ] ┌──────────────┐ │ Hover Card │ └──────────────┘ ``` ### Custom Content Builder For performance optimization, you can provide a content builder function for more complex case, which only calls when the HoverCard is opened: ```rust HoverCard::new("complex") .trigger(Button::new("btn").label("Hover me")) .content(|state, window, cx| { v_flex() .child("Dynamic content") .child(format!("Open: {}", state.is_open())) }) ``` ### Styling HoverCard inherits all `Styled` trait methods: ```rust HoverCard::new("styled") .trigger(Button::new("btn").label("Styled")) .w(px(400.)) .max_h(px(500.)) .text_sm() .gap_2() .child("Styled content") ``` Disable default appearance and apply custom styles: ```rust HoverCard::new("custom-styled") .appearance(false) // Disable default popover styling .trigger(Button::new("btn").label("Custom")) .bg(cx.theme().background) .border_2() .border_color(cx.theme().primary) .rounded(px(12.)) .p_4() .child("Custom styled content") ``` ## Behavior Details ### Hover Timing The HoverCard uses a sophisticated timing system to provide a smooth user experience: 1. **Open Delay (600ms default)**: Prevents the card from flickering when the mouse quickly passes over the trigger 2. **Close Delay (300ms default)**: Gives users time to move their mouse from the trigger to the content area without the card closing 3. **Interactive Content**: Users can move their mouse into the content area, and the card will remain open as long as the mouse is either on the trigger or in the content ### Edge Cases Handled - **Quick Mouse Sweep**: If the mouse quickly moves across the trigger, the card won't open (cancelled by the open delay) - **Trigger to Content Movement**: The card stays open when moving the mouse from the trigger to the content area - **Rapid Hovering**: Multiple rapid hover events are debounced using an epoch-based timer system - **Multiple HoverCards**: Each HoverCard has independent state, so multiple cards can coexist without interfering ## Best Practices 1. **Use appropriate delays**: - Standard content: 600ms open, 300ms close - Quick previews: 500ms open, 200ms close - Tooltips: 300ms open, 100ms close 2. **Keep content concise**: HoverCards should provide preview information, not full content 3. **Make triggers visually distinct**: Use colors, underlines, or cursor changes to indicate hoverable elements 4. **Consider accessibility**: HoverCards are visual-only and don't support keyboard navigation. For keyboard-accessible content, consider using a Popover instead 5. **Avoid nested HoverCards**: They can create confusing user experiences ## Differences from [Popover] | Feature | HoverCard | Popover | | ------------------------ | ---------------- | ------------------ | | Trigger | Mouse hover | Click/right-click | | Keyboard navigation | No | Yes (with focus) | | Dismiss on outside click | No | Yes (configurable) | | Timing delays | Yes (open/close) | No | | Primary use case | Previews | Actions/forms | [Popover]: ./popover.md [Anchor]: https://docs.rs/gpui-component/latest/gpui_component/enum.Anchor.html [Avatar]: ./avatar.md ## API Reference ### HoverCard Methods - `new(id: impl Into)` - Create a new HoverCard with a unique ID - `trigger(trigger: T)` - Set the element that triggers the hover - `content(content: F)` - Set a content builder function that receives `(&mut HoverCardState, &mut Window, &mut Context)` - `open_delay(duration: Duration)` - Set delay before showing (default: 600ms) - `close_delay(duration: Duration)` - Set delay before hiding (default: 300ms) - `anchor(anchor: impl Into)` - Set positioning (default: TopCenter) - `on_open_change(callback: F)` - Callback when open state changes, receives `(&bool, &mut Window, &mut App)` - `appearance(appearance: bool)` - Enable/disable default styling (default: true) ### HoverCardState Methods - `is_open() -> bool` - Check if the hover card is currently open --- # Select Source: /component/select This component was named `Dropdown` in `<= 0.3.x`. It has been renamed to `Select` to better reflect its purpose. A select component that allows users to choose from a list of options. Supports search functionality, grouped items, custom rendering, and various states. Built with keyboard navigation and accessibility in mind. For richer selection UIs with custom trigger rendering or multi-select, see [Combobox](combobox). ## Import ```rust use gpui_kit::component::select::{ Select, SelectState, SelectItem, SelectDelegate, SelectEvent, SearchableVec, SelectGroup }; ``` ## Usage ### Framework You can create a basic select dropdown by initializing a `SelectState` with a list of items. The first type parameter of `SelectState` is the items for the state, which must implement the [SelectItem] trait. The built-in implementations of `SelectItem` include common types like `String`, `SharedString`, and `&'static str`. ```rust let state = cx.new(|cx| { SelectState::new( vec!["Apple", "Orange", "Banana"], Some(IndexPath::default()), // Select first item window, cx, ) }); Select::new(&state) ``` ### Placeholder ```rust let state = cx.new(|cx| { SelectState::new( vec!["Rust", "Go", "JavaScript"], None, // No initial selection window, cx, ) }); Select::new(&state) .placeholder("Select a language...") ``` ### Accessibility Give the control a name that stays the same when the selection changes: ```rust Select::new(&state) .accessibility_label("Programming language") .placeholder("Choose a language") ``` The accessible value uses the committed item's `title()` and any `title_prefix`. A custom `display_title()` remains visual presentation. Searching does not change that committed value. With no selection, the accessible value uses the placeholder. Enabled controls expose accessible activation to open or close the popup. ### Searchable Use `searchable(true)` to enable search functionality within the dropdown. ```rust let fruits = SearchableVec::new(vec![ "Apple", "Orange", "Banana", "Grape", "Pineapple", ]); let state = cx.new(|cx| { SelectState::new(fruits, None, window, cx).searchable(true) }); Select::new(&state) .icon(IconName::Search) // Shows search icon ``` ### Impl SelectItem By default, we have implmemented `SelectItem` for common types like `String`, `SharedString` and `&'static str`. You can also create your own item types by implementing the `SelectItem` trait. This is useful when you want to display complex data structures, and also want get that data type from `select_value` method. You can also customize the search logic by overriding the `matches` method. ```rust #[derive(Debug, Clone)] struct Country { name: SharedString, code: SharedString, } impl SelectItem for Country { type Value = SharedString; fn title(&self) -> SharedString { self.name.clone() } fn display_title(&self) -> Option { // Custom display for selected item Some(format!("{} ({})", self.name, self.code).into_any_element()) } fn value(&self) -> &Self::Value { &self.code } fn matches(&self, query: &str) -> bool { // Custom search logic self.name.to_lowercase().contains(&query.to_lowercase()) || self.code.to_lowercase().contains(&query.to_lowercase()) } } ``` ### Group Items ```rust let mut grouped_items = SearchableVec::new(vec![]); // Group countries by first letter grouped_items.push( SelectGroup::new("A") .items(vec![ Country { name: "Australia".into(), code: "AU".into() }, Country { name: "Austria".into(), code: "AT".into() }, ]) ); grouped_items.push( SelectGroup::new("B") .items(vec![ Country { name: "Brazil".into(), code: "BR".into() }, Country { name: "Belgium".into(), code: "BE".into() }, ]) ); let state = cx.new(|cx| { SelectState::new(grouped_items, None, window, cx) }); Select::new(&state) ``` ### Sizes ```rust Select::new(&state).large() Select::new(&state) // medium (default) Select::new(&state).small() ``` ### Disabled State ```rust Select::new(&state).disabled(true) ``` ### Cleanable ```rust Select::new(&state) .cleanable(true) // Show clear button when item is selected ``` ### Custom Appearance ```rust Select::new(&state) .w(px(320.)) // Set dropdown width .menu_width(px(400.)) // Set menu popup width .menu_max_h(rems(10.)) // Set menu max height (default: 20rem) .appearance(false) // Remove default styling .title_prefix("Country: ") // Add prefix to selected title ``` ### Empty State ```rust let state = cx.new(|cx| { SelectState::new(Vec::::new(), None, window, cx) }); Select::new(&state) .empty( h_flex() .h_24() .justify_center() .text_color(cx.theme().muted_foreground) .child("No options available") ) ``` ### Events ```rust cx.subscribe_in(&state, window, |view, state, event, window, cx| { match event { SelectEvent::Confirm(value) => { if let Some(selected_value) = value { println!("Selected: {:?}", selected_value); } else { println!("Selection cleared"); } } } }); ``` ### Mutating ```rust // Set by index state.update(cx, |state, cx| { state.set_selected_index(Some(IndexPath::default().row(2)), window, cx); }); // Set by value (requires PartialEq on Value type) state.update(cx, |state, cx| { state.set_selected_value(&"US".into(), window, cx); }); // Get current selection let current_value = state.read(cx).selected_value(); ``` Update items: ```rust state.update(cx, |state, cx| { let new_items = vec!["New Option 1".into(), "New Option 2".into()]; state.set_items(new_items, window, cx); }); ``` ### Language Selector ```rust let languages = SearchableVec::new(vec![ "Rust".into(), "TypeScript".into(), "Go".into(), "Python".into(), "JavaScript".into(), ]); let state = cx.new(|cx| { SelectState::new(languages, None, window, cx) }); Select::new(&state) .placeholder("Select language...") .title_prefix("Language: ") ``` ### Country/Region Selector ```rust #[derive(Debug, Clone)] struct Region { name: SharedString, code: SharedString, flag: SharedString, } impl SelectItem for Region { type Value = SharedString; fn title(&self) -> SharedString { self.name.clone() } fn display_title(&self) -> Option { Some( h_flex() .items_center() .gap_2() .child(self.flag.clone()) .child(format!("{} ({})", self.name, self.code)) .into_any_element() ) } fn value(&self) -> &Self::Value { &self.code } } let regions = vec![ Region { name: "United States".into(), code: "US".into(), flag: "🇺🇸".into() }, Region { name: "Canada".into(), code: "CA".into(), flag: "🇨🇦".into() }, ]; let state = cx.new(|cx| { SelectState::new(regions, None, window, cx) }); Select::new(&state) .placeholder("Select country...") ``` ### Integrated with Input Field ```rust // Combined country code + phone input h_flex() .border_1() .border_color(cx.theme().input) .rounded(cx.theme().radius_lg) .w_full() .gap_1() .child( div().w(px(140.)).child( Select::new(&country_state) .appearance(false) // No border/background .py_2() .pl_3() ) ) .child(Separator::vertical()) .child( div().flex_1().child( Input::new(&phone_input) .appearance(false) .placeholder("Phone number") .pr_3() .py_2() ) ) ``` ### Multi-level Grouped Select ```rust let mut grouped_countries = SearchableVec::new(vec![]); for (continent, countries) in countries_by_continent { grouped_countries.push( SelectGroup::new(continent) .items(countries) ); } let state = cx.new(|cx| { SelectState::new(grouped_countries, None, window, cx) }); Select::new(&state) .menu_width(px(350.)) .placeholder("Select country...") ``` ## Keyboard Shortcuts | Key | Action | | --------- | --------------------------------------- | | `Tab` | Focus dropdown | | `Enter` | Open menu or select current item | | `Up/Down` | Navigate options (opens menu if closed) | | `Escape` | Close menu | | `Space` | Open menu | ## Theming The dropdown respects the current theme and uses the following theme tokens: - `background` - Dropdown input background - `input` - Border color - `foreground` - Text color - `muted_foreground` - Placeholder and disabled text - `accent` - Selected item background - `accent_foreground` - Placeholder text color - `border` - Menu border - `radius` - Border radius [SelectItem]: https://docs.rs/gpui-component/latest/gpui_component/select/trait.SelectItem.html --- # Bubble Source: /component/bubble `Bubble` is the surface-level primitive for a conversation. It owns the alignment, the maximum content width, and the position of an optional reaction region. `BubbleContent` owns the visible surface. Keeping those responsibilities separate lets an application replace the content layout without reimplementing message alignment. `Bubble` is a presentational element. It does not own a message record, a collapsed state, a reaction model, or a click action. Compose those behaviors with application state and existing controls such as `Button`, `Link`, `Collapsible`, `Tooltip`, and `Popover`. ## Import ```rust use gpui_kit::{div, ParentElement as _, Styled as _}; use gpui_kit::component::{ ActiveTheme as _, Colorize as _, Sizable as _, bubble::{ Bubble, BubbleContent, BubbleGroup, BubbleReactionSide, BubbleReactions, BubbleVariant, }, button::{Button, ButtonVariants as _}, message::MessageAlignment, }; ``` ## Anatomy and basic usage The shortest form adds children to the typed content slot: ```rust Bubble::new() .alignment(MessageAlignment::Start) .child("Can you review this draft?") ``` Use `content(...)` when the surface needs its own layout or style target: ```rust Bubble::new() .alignment(MessageAlignment::Start) .content( BubbleContent::new().child( gpui_kit::component::h_flex() .gap_2() .child("Can you review this draft?") .child("📎"), ), ) ``` The root is `min_w_0`, grows to the available width only for `Ghost`, and otherwise has a maximum width of 80% of its parent. Long text wraps inside the content slot when its child allows wrapping. An application that needs a different conversation measure can refine the root with `w(...)`, `max_w(...)`, or a child-specific layout. The default state is: | Property | Default | Meaning | | --- | --- | --- | | Alignment | unset | The parent may supply alignment; a standalone bubble does not force an edge. | | Variant | `Filled` | Primary semantic surface. | | Reactions | none | No reaction region is rendered. | | Maximum width | `0.8` of the parent | Applies to regular variants. | | Surface radius | `cx.theme().radius_2xl()` | Follows the active theme. | | Content padding | `px_3()` / `py_2()` | Applied by `BubbleContent` for regular variants. | ## Alignment `MessageAlignment::Start` and `MessageAlignment::End` are shared with `Message`: ```rust Bubble::new() .alignment(MessageAlignment::Start) .with_variant(BubbleVariant::Secondary) .child("Incoming message"); Bubble::new() .alignment(MessageAlignment::End) .child("Outgoing message") ``` When a bubble is placed in `MessageContent::bubble(...)`, `Message` propagates its alignment to the content surface. Leave the bubble alignment unset in that case so the message remains the single owner of horizontal placement. Set it explicitly when a bubble is used on its own or when a custom parent intentionally overrides the message row. ## Variants `BubbleVariant` selects semantic colors and surface treatment. It does not change the content model: ```rust Bubble::new() .with_variant(BubbleVariant::Filled) .child("Primary response"); Bubble::new() .with_variant(BubbleVariant::Secondary) .child("Neutral incoming response"); Bubble::new() .with_variant(BubbleVariant::Muted) .child("Low-emphasis context"); Bubble::new() .with_variant(BubbleVariant::Tinted) .child("Subtle selected or emphasized response"); Bubble::new() .with_variant(BubbleVariant::Outline) .child("A response that needs a visible boundary"); Bubble::new() .with_variant(BubbleVariant::Ghost) .child("A full-width, unframed message surface"); Bubble::new() .with_variant(BubbleVariant::Destructive) .child("The operation failed; explain what the user can do next.") ``` `Filled` is the default. `Ghost` removes the surface padding, border, and radius, can occupy the full row, and does not clip its children: there is no surface to clip against, so the shadows and overhanging controls of rich content stay visible. `Destructive` uses the semantic destructive color with a theme-aware translucent surface; its meaning must also be present in text or another non-color cue. All other variants keep the regular content surface and derive colors from the active theme. ## Rich content and long messages Bubble children are arbitrary GPUI elements. Compose text, code, files, buttons, or custom layouts without a bubble-specific content enum: ```rust use gpui_kit::{div, Styled as _}; use gpui_kit::component::{h_flex, v_flex, Icon, IconName}; Bubble::new() .content( BubbleContent::new().child( h_flex() .gap_3() .items_start() .child(Icon::new(IconName::FileText)) .child( v_flex() .min_w_0() .child("design-notes.pdf") .child(div().text_sm().child("PDF · 2.4 MB")), ), ), ) ``` For a long response, keep the child `min_w_0()` and choose wrapping or truncation at the content boundary. `Bubble` does not truncate arbitrary children. An application layout can expose a `Show more` affordance by wrapping the content in `Collapsible`; the bubble itself has no hidden-text state. ```rust // The state and trigger belong to the application. The same Bubble can be // rendered in the expanded and collapsed states. Bubble::new() .with_variant(BubbleVariant::Ghost) .content(BubbleContent::new().child(long_response_element)) ``` ## Groups `BubbleGroup` is a styleable vertical stack. It does not infer sender identity or remove headers; the application decides which consecutive bubbles belong to one sender: ```rust BubbleGroup::new() .child( Bubble::new() .alignment(MessageAlignment::Start) .with_variant(BubbleVariant::Secondary) .child("The first paragraph belongs to Alice."), ) .child( Bubble::new() .alignment(MessageAlignment::Start) .with_variant(BubbleVariant::Secondary) .child("The second paragraph uses the same group."), ) ``` Use `MessageGroup` when the repeated unit is a complete message with avatar, header, body, and footer. Use `BubbleGroup` when only the surface stack is being repeated. ## Reactions and interactive content `BubbleReactions` positions a region at the top or bottom edge. Put semantic controls inside it. Use the typed `action(Button)` builder for a button that should read as part of the reaction surface: ```rust Bubble::new() .alignment(MessageAlignment::Start) .with_variant(BubbleVariant::Outline) .child("This response has feedback.") .reactions( BubbleReactions::new() .side(BubbleReactionSide::Bottom) .alignment(MessageAlignment::End) .action( Button::new("bubble-like") .ghost() .small() .label("Like · 2"), ) .action( Button::new("bubble-copy") .ghost() .small() .label("Copy"), ), ) ``` The defaults for `BubbleReactions` are `Bottom` and `End`. For a top-attached region aligned to the leading edge: ```rust BubbleReactions::new() .side(BubbleReactionSide::Top) .alignment(MessageAlignment::Start) .action(Button::new("bubble-more").ghost().xsmall().label("More")) ``` `action(Button)` tells `BubbleReactions` that the child is a semantic action. When a reaction region contains any typed action, the container removes its decorative content padding and applies the active theme's full/pill radius to each typed button, so the buttons and reaction surface read as one control group. The supplied `Button` remains customizable: its variant, size, icon, `.on_click(...)` callback, and `.tooltip(...)` are preserved. The typed action owns the pill corner radius so the button stays joined to the reaction surface; use the generic path below when a button needs a different radius. Multiple actions can be added with repeated `.action(...)` calls. Use `.child(...)` for emoji, text, a custom element, or an overlay composition that is not a direct `Button`. This generic path remains backward-compatible and does not opt that child into the compact action treatment. If the same region also contains any `.action(...)`, the whole reaction region still uses the compact surface layout: ```rust BubbleReactions::new() .child("👍 2") .action( Button::new("bubble-reply") .ghost() .xsmall() .label("Reply"), ) ``` Nested interactive wrappers such as `Popover` remain on the generic `.child(...)` path because `action(...)` accepts a direct `Button`. If the wrapper's trigger should share the reaction geometry, opt into that layout explicitly with `p_0()` on the reaction region and a theme-derived full radius on the trigger button. The same escape hatch gives an arbitrary button its own radius or surface treatment. ```rust BubbleReactions::new().p_0().child( gpui_kit::component::popover::Popover::new("bubble-more") .trigger( Button::new("bubble-more-trigger") .ghost() .xsmall() .label("More") .rounded(cx.theme().radius_full()), ) .child(Button::new("bubble-copy").label("Copy")), ) ``` The reaction container supplies the default spacing, rounded semantic surface, and contrast border. Caller `Styled` refinements are applied after those defaults, so an application can customize the reaction surface or the `Button` itself. There is no separate `BubbleAction` component or reaction data model; the application owns counts, selected state, and submitted actions. Button focus, disabled state, and keyboard activation remain the responsibility of `Button`. The current Button accessibility label comes from its visible `.label(...)` value; a tooltip is supplemental. For a URL inside a bubble use `Link`; for an in-app command use `Button`. A tooltip or popover can wrap the relevant child using the existing overlay components. ## Custom styling and theme tokens `Bubble`, `BubbleContent`, `BubbleGroup`, and `BubbleReactions` implement `Styled`. Refinements are applied after the component defaults, so callers can adjust spacing, width, typography, borders, backgrounds, and shadows at the appropriate part boundary: ```rust Bubble::new() .w_full() .content( BubbleContent::new() .rounded(cx.theme().radius_lg) .bg(cx.theme().group_box) .text_color(cx.theme().group_box_foreground) .border_1() .border_color(cx.theme().border) .px_4() .py_3() .child("Application-owned surface treatment"), ) ``` Use semantic theme roles (`primary`, `muted`, `group_box`, `border`, `destructive`, and their foreground colors) instead of raw palette values. Radii come from the active theme, so a custom theme can make all conversation surfaces more square or more rounded consistently. The component uses the shared spacing and typography scale; a product-specific scale should be owned by the surrounding design-system layer and passed through its own builders. Style the group and reaction region independently when the composition needs a different rhythm: ```rust BubbleGroup::new() .gap_3() .child(Bubble::new().child("First")) .child(Bubble::new().child("Second")); BubbleReactions::new() .px_2() .bg(cx.theme().background) .border_color(cx.theme().ring) .action(Button::new("bubble-reaction").ghost().xsmall().label("👍")) ``` ## Accessibility and state guidance - Use visible text, an icon with a label, or an accessible `Button` label to communicate reactions and actions. Color and a bubble variant are not sufficient status announcements. - Keep keyboard actions inside `Button`, `Link`, `Collapsible`, `Tooltip`, or `Popover`. `Bubble` and `BubbleReactions` are layout elements and do not create focus targets themselves. - Preserve readable contrast when overriding a surface. Pair a custom background with the matching semantic foreground token or an explicitly verified theme role. - For loading or generated content, render a meaningful text label and use `ShimmerText` or `Marker` for motion. Respect the application's reduced-motion behavior; the shimmer utility renders static text when reduced motion is requested. - A failed or destructive bubble should include the error and the next action, not only a red surface. ## When to use another component Use `Message` when sender identity, metadata, or a footer belongs to the same row. Use `Marker` for a compact status or timeline boundary. Use `GroupBox` or an application-owned surface for a non-conversational document. Use a plain `div()`/`h_flex()` when the row has no shared bubble behavior; adding a bubble only to obtain padding makes the hierarchy harder to read. ## API reference ### `Bubble` | Method | Default | Purpose | | --- | --- | --- | | `new()` | filled, no alignment, no reactions | Create a bubble. | | `alignment(MessageAlignment)` | unset | Place the bubble at the leading or trailing edge. | | `with_variant(BubbleVariant)` | `Filled` | Select the semantic surface treatment. | | `content(BubbleContent)` | empty typed content | Replace the visible content surface; direct children move into it. | | `reactions(BubbleReactions)` | none | Attach a reaction region. | `Bubble` also implements `ParentElement` for the direct `.child(...)` form and `Styled` for root layout refinements. ### `BubbleContent` | Method | Default | Purpose | | --- | --- | --- | | `new()` | empty | Create the visible surface slot. | | `.child(...)` | — | Add arbitrary GPUI elements. | | `Styled` methods | component defaults | Refine padding, radius, colors, typography, and layout. | The parent `Bubble` supplies its variant and alignment to this slot. A standalone `BubbleContent` therefore has the default `Filled` treatment. ### `BubbleGroup` | Method | Default | Purpose | | --- | --- | --- | | `new()` | empty vertical stack | Create a group. | | `.child(...)` | — | Add consecutive bubbles. | | `Styled` methods | `gap_2()` | Refine group spacing and layout. | ### `BubbleReactions` | Method | Default | Purpose | | --- | --- | --- | | `new()` | bottom, end aligned | Create a reaction region. | | `side(BubbleReactionSide)` | `Bottom` | Attach it above or below the bubble. | | `alignment(MessageAlignment)` | `End` | Align children along the bubble edge. | | `action(Button)` | — | Add a typed action that shares the reaction surface and full/pill radius. | | `.child(...)` | — | Add emoji, text, or arbitrary GPUI elements. | | `Styled` methods | themed reaction surface | Refine spacing, colors, and layout. | ### Related types - [`BubbleVariant`] — `Filled`, `Secondary`, `Muted`, `Tinted`, `Outline`, `Ghost`, and `Destructive`. - [`BubbleReactionSide`] — `Top` or `Bottom`. - [`MessageAlignment`] — `Start` or `End`. [Bubble]: https://docs.rs/gpui-component/latest/gpui_component/bubble/struct.Bubble.html [BubbleContent]: https://docs.rs/gpui-component/latest/gpui_component/bubble/struct.BubbleContent.html [BubbleGroup]: https://docs.rs/gpui-component/latest/gpui_component/bubble/struct.BubbleGroup.html [BubbleReactions]: https://docs.rs/gpui-component/latest/gpui_component/bubble/struct.BubbleReactions.html [BubbleVariant]: https://docs.rs/gpui-component/latest/gpui_component/bubble/enum.BubbleVariant.html [BubbleReactionSide]: https://docs.rs/gpui-component/latest/gpui_component/bubble/enum.BubbleReactionSide.html [MessageAlignment]: https://docs.rs/gpui-component/latest/gpui_component/message/enum.MessageAlignment.html --- # Progress Source: /component/progress Progress components visually represent the completion percentage of a task. The library provides two variants: - **[Progress](#progress)** - A linear horizontal progress bar - **[ProgressCircle](#progresscircle)** - A circular progress indicator Both components feature smooth transition animations when the value changes, a loading (indeterminate) animation mode, customizable colors, and automatic styling that adapts to the current theme. ## Usage ### Progress ```rust struct FileUpload { uploaded: u64, total: u64, } impl FileUpload { fn progress(&self) -> f32 { if self.total == 0 { return 0.0; } (self.uploaded as f32 / self.total as f32) * 100.0 } fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_2() .child( h_flex() .justify_between() .child("Uploading...") .child(format!("{:.0}%", self.progress())), ) .child(Progress::new("upload").value(self.progress())) } } ``` ### Values ```rust struct AppInit { loading: bool, progress: f32, } impl Render for AppInit { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_3() .child( h_flex() .gap_2() .items_center() .child( ProgressCircle::new("init-circle") .loading(self.loading) .value(self.progress) .size_4(), ) .child(if self.loading { "Initializing..." } else { "Ready" }), ) .child( Progress::new("init-bar") .loading(self.loading) .value(self.progress), ) } } ``` ### Sizes and status colors ```rust struct Install { step: usize, // current package index total: usize, // total packages step_progress: f32, } impl Install { fn overall(&self) -> f32 { if self.total == 0 { return 0.0; } (self.step as f32 + self.step_progress / 100.0) / self.total as f32 * 100.0 } fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_2() .child( h_flex() .justify_between() .child(format!("Package {}/{}", self.step + 1, self.total)) .child(format!("{:.0}%", self.overall())), ) .child(Progress::new("overall").value(self.overall())) .child( h_flex() .gap_2() .items_center() .child(Progress::new("package").value(self.step_progress).small()) .child("Current package"), ) } } ``` ### Progress circle The live preview above is this sample. ## Progress ```rust use gpui_kit::component::progress::Progress; ``` ### Usage ```rust Progress::new("my-progress") .value(75.0) // 75% complete ``` ### Different Progress Values ```rust Progress::new("progress-0").value(0.0) Progress::new("progress-25").value(25.0) Progress::new("progress-75").value(75.0) Progress::new("progress-100").value(100.0) ``` ### Loading State Use `.loading(true)` to show an indeterminate animation when the actual progress is unknown. The `value` is ignored while loading is active. ```rust // Indeterminate loading animation Progress::new("loading").loading(true) // Toggle between loading and determinate Progress::new("my-progress") .loading(self.is_loading) .value(self.progress) ``` ### Sizes `Progress` implements the `Sizable` trait: ```rust Progress::new("xs").value(50.0).xsmall() // 4px height Progress::new("sm").value(50.0).small() // 6px height Progress::new("md").value(50.0) // 8px height (default) Progress::new("lg").value(50.0).large() // 10px height ``` ### Custom Style The component implements the `Styled` trait, allowing custom height, border radius, color, and border: ```rust Progress::new("custom") .value(32.0) .h(px(16.)) .rounded(px(2.)) .color(cx.theme().green_light) .border_2() .border_color(cx.theme().green) ``` ### Dynamic Progress Updates ```rust struct MyView { value: f32, is_loading: bool, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_3() .child( h_flex() .gap_2() .child( Button::new("toggle-loading") .label("Loading") .selected(self.is_loading) .on_click(cx.listener(|this, _, _, cx| { this.is_loading = !this.is_loading; cx.notify(); })), ) .child(Button::new("inc").icon(IconName::Plus).on_click( cx.listener(|this, _, _, _| { this.value = (this.value + 10.).min(100.); }), )), ) .child( Progress::new("progress") .value(self.value) .loading(self.is_loading), ) } } ``` ### API Reference | Method | Type | Description | |---|---|---| | `new(id)` | `ElementId` | Create a new progress bar | | `value(v)` | `f32` | Set progress value (0–100), clamped automatically | | `loading(v)` | `bool` | Enable indeterminate loading animation; ignores `value` when `true` | | `color(c)` | `impl Into` | Override the fill color (defaults to `theme.progress_bar`) | | `xsmall()` / `small()` / `large()` | — | Set predefined height via `Sizable` | | `Styled` trait methods | — | Custom height, border radius, border, etc. | ## ProgressCircle A circular progress indicator that displays progress as an arc. Ideal for compact spaces, inline labels, or as a download/upload indicator. ```rust use gpui_kit::component::progress::ProgressCircle; ``` ### Usage ```rust ProgressCircle::new("circle").value(50.0) ``` ### Loading State ```rust // Indeterminate rotating arc animation ProgressCircle::new("loading").loading(true) // Toggle between loading and determinate ProgressCircle::new("circle") .loading(self.is_loading) .value(self.progress) ``` ### Sizes `ProgressCircle` implements the `Sizable` trait. Named sizes map to fixed pixel dimensions; use `.size(px(n))` for custom sizes: ```rust ProgressCircle::new("xs").value(50.0).xsmall() // size_2 ProgressCircle::new("sm").value(50.0).small() // size_3 ProgressCircle::new("md").value(50.0) // size_4 (default) ProgressCircle::new("lg").value(50.0).large() // size_5 ProgressCircle::new("xl").value(50.0).size_20() // 80px ``` ### Custom Color ```rust ProgressCircle::new("green").value(75.0).color(cx.theme().green) ProgressCircle::new("yellow").value(40.0).color(cx.theme().yellow) ProgressCircle::new("primary").value(60.0).color(cx.theme().primary) ``` ### With Inner Content `ProgressCircle` implements `ParentElement`, so you can place content inside the circle: ```rust ProgressCircle::new("circle-with-label") .value(self.value) .size_20() .child( v_flex() .size_full() .items_center() .justify_center() .gap_1() .child( div() .child(format!("{}%", self.value as i32)) .text_color(cx.theme().progress_bar), ) .child(div().child("Loading").text_xs()), ) ``` ### Inline with Label ```rust h_flex() .gap_2() .items_center() .child( ProgressCircle::new("download") .color(cx.theme().primary) .value(self.progress) .size_4(), ) .child("Downloading...") ``` ### API Reference | Method | Type | Description | |---|---|---| | `new(id)` | `ElementId` | Create a new circular progress indicator | | `value(v)` | `f32` | Set progress value (0–100), clamped automatically | | `loading(v)` | `bool` | Enable indeterminate loading animation; ignores `value` when `true` | | `color(c)` | `impl Into` | Override the arc color (defaults to `theme.progress_bar`) | | `xsmall()` / `small()` / `large()` | — | Set predefined size via `Sizable` | | `size(px(n))` | `Pixels` | Set custom size | | `ParentElement` | — | Place content inside the circle | --- # Switch Source: /component/switch A toggle switch component for binary on/off states. Features smooth animations, different sizes, labels, disabled state, and customizable positioning. Use `on_change` for requested values. The owner stores the value and calls `cx.notify()`. The existing `on_click` name remains a compatibility alias; setting either replaces the same handler, so the last call wins. ## Import ```rust use gpui_kit::component::switch::Switch; ``` ## Usage ### Basic ```rust Switch::new("my-switch") .checked(false) .on_change(|checked, _, _| { println!("Switch is now: {}", checked); }) ``` ### Sizes ```rust // Small switch Switch::new("small-switch") .small() .label("Small switch") // Medium switch (default) Switch::new("medium-switch") .label("Medium switch") // Using explicit size Switch::new("custom-switch") .with_size(Size::Small) .label("Custom size") ``` ### Controlled Switch ```rust struct MyView { is_enabled: bool, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Switch::new("switch") .checked(self.is_enabled) .on_change(cx.listener(|view, checked, _, cx| { view.is_enabled = *checked; cx.notify(); })) } } ``` ### With Label ```rust Switch::new("notifications") .label("Enable notifications") .checked(true) .on_change(|checked, _, _| { println!("Notifications: {}", if *checked { "enabled" } else { "disabled" }); }) ``` ### Disabled State ```rust // Disabled unchecked Switch::new("disabled-off") .label("Disabled (off)") .disabled(true) .checked(false) // Disabled checked Switch::new("disabled-on") .label("Disabled (on)") .disabled(true) .checked(true) ``` ### Custom Color Use `.color()` to override the checked-state background color. The disabled alpha is applied automatically on top of the custom color. ```rust // Success color when checked Switch::new("switch") .label("Success") .checked(true) .color(cx.theme().success) // Danger color when checked Switch::new("switch") .label("Danger") .checked(true) .color(cx.theme().danger) // Custom color + disabled: color is shown at 50% opacity Switch::new("switch") .label("Disabled") .checked(true) .color(cx.theme().success) .disabled(true) ``` ### With Tooltip ```rust Switch::new("switch") .label("Airplane mode") .tooltip("Enable airplane mode to disable all wireless connections") .checked(false) ``` ### Keyboard Focus A switch is a tab stop and draws the theme's focus ring around its track when it is focused, the same ring `Checkbox` and `Button` draw. Pass `focus_ring(false)` when the ring is drawn elsewhere, and `tab_stop` / `tab_index` to change its place in the tab order. ```rust Switch::new("switch") .label("Custom tab order") .tab_index(2) .tab_stop(true) // The row around it draws its own focus treatment. Switch::new("switch") .label("Quiet focus") .focus_ring(false) ``` ### Settings Panel ```rust struct SettingsView { marketing_emails: bool, security_emails: bool, push_notifications: bool, } v_flex() .gap_4() .child( // Setting with description v_flex() .gap_2() .child( h_flex() .items_center() .justify_between() .child( v_flex() .child(Label::new("Marketing emails").text_lg()) .child( Label::new("Receive emails about new products and features") .text_color(theme.muted_foreground) ) ) .child( Switch::new("marketing") .checked(self.marketing_emails) .on_change(cx.listener(|view, checked, _, cx| { view.marketing_emails = *checked; cx.notify(); })) ) ) ) .child( // Simple setting h_flex() .items_center() .justify_between() .child(Label::new("Push notifications")) .child( Switch::new("push") .checked(self.push_notifications) .on_change(cx.listener(|view, checked, _, cx| { view.push_notifications = *checked; cx.notify(); })) ) ) ``` ### Compact Settings List ```rust v_flex() .gap_3() .child( Switch::new("wifi") .label("Wi-Fi") .label_side(Side::Left) .checked(true) .small() ) .child( Switch::new("bluetooth") .label("Bluetooth") .label_side(Side::Left) .checked(false) .small() ) .child( Switch::new("airplane") .label("Airplane Mode") .label_side(Side::Left) .checked(false) .disabled(true) .small() ) ``` ### Form Integration ```rust struct FormData { subscribe_newsletter: bool, enable_notifications: bool, remember_me: bool, } v_flex() .gap_4() .p_4() .border_1() .border_color(theme.border) .rounded(theme.radius) .child( Switch::new("newsletter") .label("Subscribe to newsletter") .checked(self.subscribe_newsletter) .tooltip("Receive monthly updates about new features") .on_change(cx.listener(|view, checked, _, cx| { view.subscribe_newsletter = *checked; cx.notify(); })) ) .child( Switch::new("notifications") .label("Enable notifications") .checked(self.enable_notifications) .on_change(cx.listener(|view, checked, _, cx| { view.enable_notifications = *checked; cx.notify(); })) ) .child( Switch::new("remember") .label("Remember me") .checked(self.remember_me) .small() .on_change(cx.listener(|view, checked, _, cx| { view.remember_me = *checked; cx.notify(); })) ) ``` ### Custom Styling ```rust Switch::new("custom") .label("Custom styled switch") .w(px(200.)) .checked(true) .on_change(|checked, _, _| { println!("Custom switch: {}", checked); }) ``` ## Animation The switch features smooth animations: - **Toggle animation**: 150ms duration when switching states - **Background color transition**: Changes from switch color to primary color - **Position animation**: Smooth movement of the toggle indicator - **Disabled state**: Animations are disabled when the switch is disabled ## API Reference ### Switch | Method | Description | | ------------------ | ----------------------------------------------------------- | | `new(id)` | Create a new switch with the given ID | | `checked(bool)` | Set the checked/toggled state | | `label(text)` | Set label text for the switch | | `label_side(side)` | Position label (Side::Left or Side::Right) | | `disabled(bool)` | Set disabled state | | `tooltip(text)` | Add tooltip text | | `color(color)` | Set background color when checked (default: `theme.primary`) | | `on_change(fn)` | Requested checked value, receives `&bool` | | `focus_ring(bool)` | Draw the focus ring around the track when focused (default: `true`) | | `tab_stop(bool)` | Take part in Tab traversal (default: `true`) | | `tab_index(isize)` | Position in the tab order within a tab group (default: `0`) | ### Styling Implements `Sizable` and `Disableable` traits: - `small()` - Small switch size (28x16px toggle area) - `medium()` - Medium switch size (36x20px toggle area, default) - `with_size(size)` - Set explicit size - `disabled(bool)` - Disabled state ### Styling Properties The switch can also be styled using GPUI's styling methods: - `w(width)` - Custom width - `h(height)` - Custom height - Standard margin, padding, and positioning methods --- # Popover Source: /component/popover Popover component for displaying floating content that appears when interacting with a trigger element. Supports multiple positioning options, custom content, different trigger methods, and automatic dismissal behaviors. Perfect for tooltips, menus, forms, and other contextual information. ## Import ```rust use gpui_kit::component::popover::{Popover}; ``` ## Usage ### Click Sometimes you may want to show a popover on right-click, for example, to create a special your ownen context menu. The `mouse_button` method allows you to specify which mouse button triggers the popover. ```rust use gpui_kit::MouseButton; Popover::new("context-menu") .anchor(Anchor::BottomRight) .mouse_button(MouseButton::Right) .trigger(Button::new("right-click").label("Right Click Me").outline()) .child("Context Menu") .child(Separator::horizontal()) .child("This is a custom context menu.") ``` ### Basic Popover Any element that implements [Selectable] can be used as a trigger, for example, a [Button]. Any element that implements [RenderOnce] or [Render] can be used as popover content, use `.child(...)` to add children directly. ```rust use gpui_kit::ParentElement as _; use gpui_kit::component::{button::Button, popover::Popover}; Popover::new("basic-popover") .trigger(Button::new("trigger").label("Click me").outline()) .child("Hello, this is a popover!") .child("It appears when you click the button.") ``` ### Popover with Custom Positioning The `anchor` method names the **popover's own** anchor, not the trigger's corner. `Top*` anchors open below the trigger, `Bottom*` anchors open above it, `LeftCenter` opens to the right, and `RightCenter` opens to the left. The popup clamps to the window without changing its anchor or flipping. ```rust use gpui_kit::{Anchor, px}; use gpui_kit::component::popover::Popover; Popover::new("anchored") .anchor(Anchor::TopCenter) .offset(px(8.)) .arrow(true) .trigger(Button::new("details").label("Details")) .child("Contextual details") ``` | Option | Meaning | Default | | --- | --- | --- | | `anchor(Anchor)` | Popup anchor, including `TopCenter` and `BottomCenter` | `TopLeft` | | `offset(Pixels)` | Gap from trigger to surface, or to arrow tip when enabled | `0.25rem` | | `arrow(bool)` | Show an arrow on the edge selected by the anchor | `false` | The arrow follows the anchor's leading, center, or trailing alignment and is inset as needed to avoid rounded corners. It adds `0.375rem` to the surface distance and uses the surface background, falling back to the theme's popover color. Neither `offset` nor `arrow` changes the positioning strategy. For example, `Anchor::TopLeft` places the popover just below the trigger, left-aligned to it: ```text [ Trigger ] ┌──────────────┐ │ Popover │ └──────────────┘ ``` ```rust use gpui_kit::component::Anchor; // Below the trigger: name the popover's top anchor Popover::new("top-left") .anchor(Anchor::TopLeft) .trigger(Button::new("btn").label("Top Left").outline()) .child("Below the trigger, aligned left") Popover::new("top-center") .anchor(Anchor::TopCenter) .trigger(Button::new("btn").label("Top Center").outline()) .child("Below the trigger, centered") Popover::new("top-right") .anchor(Anchor::TopRight) .trigger(Button::new("btn").label("Top Right").outline()) .child("Below the trigger, aligned right") // Above the trigger: name the popover's bottom anchor Popover::new("bottom-left") .anchor(Anchor::BottomLeft) .trigger(Button::new("btn").label("Bottom Left").outline()) .child("Above the trigger, aligned left") Popover::new("bottom-center") .anchor(Anchor::BottomCenter) .trigger(Button::new("btn").label("Bottom Center").outline()) .child("Above the trigger, centered") Popover::new("bottom-right") .anchor(Anchor::BottomRight) .trigger(Button::new("btn").label("Bottom Right").outline()) .child("Above the trigger, aligned right") ``` ### View in Popover You can add any `Entity` that implemented [Render] as the popover content. ```rust let view = cx.new(|_| MyView::new()); Popover::new("form-popover") .anchor(Anchor::BottomLeft) .trigger(Button::new("show-form").label("Open Form").outline()) .child(view.clone()) ``` ### Add content by `content` method The `content` method allows you to create more complex popover content using a closure. This is useful when you need to build dynamic content or need access to the popover's context. This method will let us to have `&mut PopoverState`, `&mut Window` and `&mut Context` parameters in the closure is to allow you to interact with the popover's state and the overall application context if needed. This `content` callback will called every time on render the popover. So, you should avoid creating new elements or entities in the content closure or other heavy operations that may impact performance. And `content` will works with `child`, `children` methods together. ```rust use gpui_kit::ParentElement as _; use gpui_kit::component::popover::Popover; Popover::new("complex-popover") .anchor(Anchor::BottomLeft) .trigger(Button::new("complex").label("Complex Content").outline()) .content(|_, _, _| { div() .child("This popover has complex content.") .child( Button::new("action-btn") .label("Perform Action") .outline() ) }) ``` ### Dismiss Popover manually If you want to dismiss the popover programmatically from within the content, you can emit a `DismissEvent`. In this case, you should use `content` method to create the popover content so you have access to the `cx: &mut Context`. ```rust use gpui_kit::component::{DismissEvent, popover::Popover}; Popover::new("dismiss-popover") .trigger(Button::new("dismiss").label("Dismiss Popover").outline()) .content(|_, cx| { div() .child("Click the button below to dismiss this popover.") .child( Button::new("close-btn") .label("Close Popover") .on_click(cx.listener(|_, _, _, cx| { // NOTE: Here `cx` is `&mut Context` type, so we can emit DismissEvent. cx.emit(DismissEvent); })) ) }) ``` ### Styling Popover Like the others components in GPUI Component, the `appearance(false)` method can be used to disable the default styling of the popover, allowing you to fully customize its appearance. And the `Popover` has implemented the [Styled] trait, so you can use all the styling methods provided by GPUI to style the popover content as you like. ```rust // For custom styled popovers or when you want full control Popover::new("custom-popover") .appearance(false) .trigger(Button::new("custom").label("Custom Style")) .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .p_6() .rounded_xl() .shadow_2xl() .child("Fully custom styled popover") ``` ### Control Open State There have `open` and `on_open_change` methods to control the open state of the popover programmatically. This is useful when you want to synchronize the popover's open state with other UI elements or application state. When you use `open` to control the popover's open state, that means you have take full control of it, so you need to update the state in `on_open_change` callback to keep the popover working correctly. ```rust use gpui_kit::component::popover::Popover; struct MyView { popover_open: bool, } Popover::new("controlled-popover") .open(self.open) .on_open_change(cx.listener(|this, open: &bool, _, cx| { this.popover_open = *open; cx.notify(); })) .trigger(Button::new("control-btn").label("Control Popover").outline()) .child("This popover's open state is controlled programmatically.") ``` ### Default Open The `default_open` method allows you to set the initial open state of the popover when it is first rendered. Please note that if you use the `open` method to control the popover's open state, the `default_open` setting will be ignored. ```rust use gpui_kit::component::popover::Popover; Popover::new("default-open-popover") .default_open(true) .trigger(Button::new("default-open-btn").label("Default Open").outline()) .child("This popover is open by default when first rendered.") ``` ### Custom Trigger A trigger is any element that implements [Selectable]. While the popover is open, it calls `open(true)` on the trigger — not `selected(true)` — so a trigger can tell "my popover is showing" apart from "I am the selected item". `open` and `is_open` default to `selected` and `is_selected`, so a trigger that only implements the selected state keeps working unchanged, and a [Button] trigger looks the same open as it does selected. Override them when your element already uses `selected` for something else, such as a sidebar row that is selected when it is the current view: ```rust use gpui_kit::component::Selectable; struct SidebarRow { /// This row is the current view. selected: bool, /// This row's account popover is showing. open: bool, } impl Selectable for SidebarRow { fn selected(mut self, selected: bool) -> Self { self.selected = selected; self } fn is_selected(&self) -> bool { self.selected } fn open(mut self, open: bool) -> Self { self.open = open; self } fn is_open(&self) -> bool { self.open } } ``` [Button]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.Button.html [Selectable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Selectable.html [Render]: https://docs.rs/gpui/latest/gpui/trait.Render.html [RenderOnce]: https://docs.rs/gpui/latest/gpui/trait.RenderOnce.html [Styled]: https://docs.rs/gpui/latest/gpui/trait.Styled.html [`Anchor`]: https://docs.rs/gpui-component/latest/gpui_component/enum.Anchor.html --- # Spinner Source: /component/spinner Spinner element displays an animated loading. Perfect for showing loading states, progress spinners, and other visual feedback during asynchronous operations. Features customizable icons, colors, sizes, and rotation animations. ## Import ```rust use gpui_kit::component::spinner::Spinner; ``` ## Usage ### Colors ```rust // Default loader icon Spinner::new() ``` ### Sizes ```rust // Extra small spinner Spinner::new().xsmall() // Small spinner Spinner::new().small() // Medium spinner (default) Spinner::new() // Large spinner Spinner::new().large() // Custom size Spinner::new().with_size(px(64.)) ``` ### In context ```rust use gpui_kit::component::ActiveTheme; // Blue spinner Spinner::new() .color(cx.theme().blue) // Green spinner for success states Spinner::new() .color(cx.theme().green) // Custom color Spinner::new() .color(cx.theme().cyan) ``` ### Spinner with Custom Icon ```rust use gpui_kit::component::IconName; // Loading circle icon Spinner::new() .icon(IconName::LoaderCircle) // Large loading circle with custom color Spinner::new() .icon(IconName::LoaderCircle) .large() .color(cx.theme().cyan) // Different loading icons Spinner::new() .icon(IconName::Loader) .color(cx.theme().primary) ``` ### Loading States ```rust // Simple loading spinner Spinner::new() // Loading with custom color Spinner::new() .color(cx.theme().blue) // Large loading spinner Spinner::new() .large() .color(cx.theme().primary) ``` ### Different Loading Icons ```rust // Default loader (line spinner) Spinner::new() .color(cx.theme().muted_foreground) // Circle loader Spinner::new() .icon(IconName::LoaderCircle) .color(cx.theme().blue) // Large circle loader with custom color Spinner::new() .icon(IconName::LoaderCircle) .large() .color(cx.theme().green) ``` ### Status Spinners ```rust // Loading state Spinner::new() .small() .color(cx.theme().muted_foreground) // Processing state Spinner::new() .icon(IconName::LoaderCircle) .color(cx.theme().blue) // Success processing (still animating) Spinner::new() .icon(IconName::LoaderCircle) .color(cx.theme().green) ``` ### Size Variations ```rust // Extra small for inline text Spinner::new() .xsmall() .color(cx.theme().muted_foreground) // Small for buttons Spinner::new() .small() .color(cx.theme().primary_foreground) // Medium for general use (default) Spinner::new() .color(cx.theme().primary) // Large for prominent loading states Spinner::new() .large() .color(cx.theme().blue) // Custom size for specific requirements Spinner::new() .with_size(px(32.)) .color(cx.theme().orange) ``` ### In UI Components ```rust // In a button Button::new("submit-btn") .loading(true) .icon( Spinner::new() .small() .color(cx.theme().primary_foreground) ) .label("Loading...") // In a card header div() .flex() .items_center() .gap_2() .child("Processing...") .child( Spinner::new() .small() .color(cx.theme().muted_foreground) ) // Full-screen loading div() .flex() .items_center() .justify_center() .h_full() .w_full() .child( Spinner::new() .large() .color(cx.theme().primary) ) ``` ## Available Icons The Spinner component supports various loading and progress icons: ### Loading Icons - `Loader` (default) - Rotating line spinner - `LoaderCircle` - Circular loading spinner ### Other Compatible Icons - Any icon from the `IconName` enum can be used, though loading-specific icons work best with the rotation animation ## Animation The Spinner component features a built-in rotation animation: - **Duration**: 0.8 seconds (configurable via speed parameter) - **Easing**: Ease-in-out transition - **Repeat**: Infinite loop - **Transform**: 360-degree rotation ## Size Reference | Size | Method | Approximate Pixels | | ----------- | ------------------- | ------------------ | | Extra Small | `.xsmall()` | ~12px | | Small | `.small()` | ~14px | | Medium | (default) | ~16px | | Large | `.large()` | ~24px | | Custom | `.with_size(px(n))` | n px | ## Performance Considerations - The animation uses CSS transforms for optimal performance - Multiple spinners on the same page share the same animation timing - The component is lightweight and suitable for frequent updates - Consider using smaller sizes for better performance with many spinners ## Common Patterns ### Conditional Loading ```rust // Show spinner only when loading .when(is_loading, |this| { this.child( Spinner::new() .small() .color(cx.theme().muted_foreground) ) }) ``` ### Loading with Text ```rust // Loading text with spinner h_flex() .items_center() .gap_2() .child( Spinner::new() .small() .color(cx.theme().primary) ) .child("Loading data...") ``` ### Overlay Loading ```rust // Full overlay with spinner div() .absolute() .inset_0() .flex() .items_center() .justify_center() .bg(cx.theme().background.alpha(0.8)) .child( v_flex() .items_center() .gap_3() .child( Spinner::new() .large() .color(cx.theme().primary) ) .child("Loading...") ) ``` --- # Slider Source: /component/slider A slider component for selecting numeric values within a specified range. Supports both single value and range selection modes, horizontal and vertical orientations, custom styling, and step intervals. ## Import ```rust use gpui_kit::component::slider::{Slider, SliderState, SliderEvent, SliderValue}; ``` ## Usage ### Volume ```rust struct VolumeControl { volume_slider: Entity, volume: f32, } impl VolumeControl { fn new(cx: &mut Context) -> Self { let volume_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .step(1.0) .default_value(50.0) }); let subscription = cx.subscribe(&volume_slider, |this, _, event: &SliderEvent, cx| { match event { SliderEvent::Change(value) => { this.volume = value.start(); this.apply_volume_change(); cx.notify(); } } }); Self { volume_slider, volume: 50.0, } } fn apply_volume_change(&self) { // Apply volume change to audio system println!("Volume changed to: {}%", self.volume); } } impl Render for VolumeControl { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { h_flex() .items_center() .gap_3() .child("🔊") .child(Slider::new(&self.volume_slider).flex_1()) .child(format!("{}%", self.volume as i32)) } } ``` ### Basic Slider ```rust let slider_state = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .default_value(50.0) .step(1.0) }); Slider::new(&slider_state) ``` ### Slider with Event Handling ```rust struct MyView { slider_state: Entity, current_value: f32, } impl MyView { fn new(cx: &mut Context) -> Self { let slider_state = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .default_value(25.0) .step(5.0) }); let subscription = cx.subscribe(&slider_state, |this, _, event: &SliderEvent, cx| { match event { SliderEvent::Change(value) => { this.current_value = value.start(); cx.notify(); } } }); Self { slider_state, current_value: 25.0, } } } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_2() .child(Slider::new(&self.slider_state)) .child(format!("Value: {}", self.current_value)) } } ``` ### Range Slider ```rust let range_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .default_value(20.0..80.0) // Range from 20 to 80 .step(1.0) }); Slider::new(&range_slider) ``` ### Vertical Slider ```rust Slider::new(&slider_state) .vertical() .h(px(200.)) ``` ### Custom Step Intervals ```rust // Integer steps let integer_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(10.0) .step(1.0) .default_value(5.0) }); // Decimal steps let decimal_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(1.0) .step(0.01) .default_value(0.5) }); ``` ### Min/Max Configuration ```rust // Temperature slider let temp_slider = cx.new(|_| { SliderState::new() .min(-10.0) .max(40.0) .default_value(20.0) .step(0.5) }); // Percentage slider let percent_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(100.0) .default_value(75.0) .step(5.0) }); ``` ### Disabled State ```rust Slider::new(&slider_state) .disabled(true) ``` ### Custom Styling ```rust Slider::new(&slider_state) .bg(cx.theme().success) .text_color(cx.theme().success_foreground) .rounded(px(4.)) ``` ### Scale There have 2 types of scale for the slider: - `Linear` (default) - `Logarithmic` The logarithmic scale is useful when the range of values is large and you want to give more precision to smaller values. ```rust let log_slider = cx.new(|_| { SliderState::new() .min(1.0) // min must be greater than 0 for log scale .max(1000.0) .default_value(10.0) .step(1.0) .scale(SliderScale::Logarithmic) }); ``` In this case: $$ v = min \times (max/min)^p $$ The value `v` is calculated using the formula above, where `p` is the slider percentage (0 to 1). - If slider at 25%, value will be approximately `5.62`. - If slider at 50%, value will be approximately `31.62`. - If slider at 75%, value will be approximately `177.83`. - If slider at 100%, value will be `1000.0`. #### Conversions ```rust // From f32 let single_value: SliderValue = 42.0.into(); // From tuple let range_value: SliderValue = (10.0, 90.0).into(); // From Range let range_value: SliderValue = (10.0..90.0).into(); ``` ### SliderEvent | Event | Description | | ---------------------- | --------------------------------------------------------------- | | `Change(SliderValue)` | Emitted continuously while the slider value is being changed | | `Release(SliderValue)` | Emitted once when the user releases the slider after interaction | ### Styling The slider component implements `Styled` trait and supports: - Background color for track and thumb - Text color for thumb - Border radius - Size customization ### Color Picker ```rust struct ColorPicker { hue_slider: Entity, saturation_slider: Entity, lightness_slider: Entity, alpha_slider: Entity, current_color: Hsla, } impl ColorPicker { fn new(cx: &mut Context) -> Self { let hue_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(1.0) .step(0.01) .default_value(0.5) }); let saturation_slider = cx.new(|_| { SliderState::new() .min(0.0) .max(1.0) .step(0.01) .default_value(1.0) }); // Subscribe to all sliders to update color let subscriptions = [&hue_slider, &saturation_slider /* ... */] .iter() .map(|slider| { cx.subscribe(slider, |this, _, event: &SliderEvent, cx| { match event { SliderEvent::Change(_) => { this.update_color(cx); } } }) }) .collect::>(); Self { hue_slider, saturation_slider, // ... other fields } } fn update_color(&mut self, cx: &mut Context) { let h = self.hue_slider.read(cx).value().start(); let s = self.saturation_slider.read(cx).value().start(); // ... calculate color self.current_color = hsla(h, s, l, a); cx.notify(); } } impl Render for ColorPicker { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { h_flex() .gap_4() .child( v_flex() .gap_2() .child("Hue") .child(Slider::new(&self.hue_slider).vertical().h(px(120.))) ) .child( v_flex() .gap_2() .child("Saturation") .child(Slider::new(&self.saturation_slider).vertical().h(px(120.))) ) // ... other sliders } } ``` ### Price Range Filter ```rust struct PriceFilter { price_range: Entity, min_price: f32, max_price: f32, } impl PriceFilter { fn new(cx: &mut Context) -> Self { let price_range = cx.new(|_| { SliderState::new() .min(0.0) .max(1000.0) .step(10.0) .default_value(100.0..500.0) // Range slider }); let subscription = cx.subscribe(&price_range, |this, _, event: &SliderEvent, cx| { match event { SliderEvent::Change(value) => { this.min_price = value.start(); this.max_price = value.end(); this.filter_products(); cx.notify(); } } }); Self { price_range, min_price: 100.0, max_price: 500.0, } } fn filter_products(&self) { println!("Filtering products: ${} - ${}", self.min_price, self.max_price); } } impl Render for PriceFilter { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_flex() .gap_2() .child("Price Range") .child(Slider::new(&self.price_range)) .child(format!("${} - ${}", self.min_price as i32, self.max_price as i32)) } } ``` ### Temperature Slider with Custom Styling ```rust struct TemperatureControl { temp_slider: Entity, temperature: f32, } impl Render for TemperatureControl { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let temp_color = if self.temperature < 10.0 { cx.theme().info // Cold - blue } else if self.temperature > 25.0 { cx.theme().destructive // Hot - red } else { cx.theme().success // Comfortable - green }; v_flex() .gap_3() .child("Temperature Control") .child( Slider::new(&self.temp_slider) .bg(temp_color) .text_color(cx.theme().background) .rounded(px(8.)) ) .child(format!("{}°C", self.temperature as i32)) } } ``` ## Keyboard Shortcuts | Key | Action | | ------------- | ------------------------------ | | `←` / `↓` | Decrease value by step | | `→` / `↑` | Increase value by step | | `Page Down` | Decrease by larger amount | | `Page Up` | Increase by larger amount | | `Home` | Set to minimum value | | `End` | Set to maximum value | | `Tab` | Move focus to next element | | `Shift + Tab` | Move focus to previous element | --- # Card Source: /component/card Card is a presentational container for grouping related content: a heading, supporting text, a body, and an optional footer. Use [GroupBox](group-box) when the region is a labeled cluster of controls. Use Card when the region is a content surface with a heading and actions. ## Import ```rust use gpui_kit::component::card::{ Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, }; ``` ## Usage ### Default ```rust Card::new() .child( CardHeader::new() .child(CardTitle::new().child("Team")) .child(CardDescription::new().child("Invite members and set their roles.")), ) .child(CardContent::new().child("Twelve seats remaining on this workspace.")) .child( CardFooter::new() .child(Button::new("cancel").label("Cancel")) .child(Button::new("invite").primary().label("Invite")), ) ``` ### Stacked ```rust h_flex() .gap_4() .items_start() .child( Card::new() .flex_1() .child(CardHeader::new().child(CardTitle::new().child("Usage"))) .child(CardContent::new().child("4.2 GB of 10 GB used.")), ) .child( Card::new() .flex_1() .child(CardHeader::new().child(CardTitle::new().child("Plan"))) .child(CardContent::new().child("Team · billed monthly.")), ) ``` ### Login ### Size ### Spacing ### Image ## Parts - **Card** — bordered surface, padding, and vertical gap. - **CardHeader** — title and description stack. - **CardTitle** — primary heading. - **CardDescription** — supporting copy in the muted foreground. - **CardContent** — main body. - **CardFooter** — action row. All parts implement [`ParentElement`] and [`Styled`], so you can add children and refine layout without a sealed API. ## Copy source This is a GPUI-target registry component. The CLI copies `crates/component/src/card.rs`. There is no `gpui-base` Card primitive; keep `gpui-base` as the dependency for behavior used by other copied components. --- # Alert Source: /component/alert A versatile alert component for displaying important messages to users. Supports multiple variants (info, success, warning, error), custom icons, optional titles, closable functionality, and banner mode. Perfect for notifications, status messages, and user feedback. ## Import ```rust use gpui_kit::component::alert::Alert; ``` ## Usage ### Default ```rust Alert::new("alert-id", "This is a basic alert message.") ``` ### Variants ```rust // Info alert (blue) Alert::info("info-alert", "This is an informational message.") .title("Information") // Success alert (green) Alert::success("success-alert", "Your operation completed successfully.") .title("Success!") // Warning alert (yellow/orange) Alert::warning("warning-alert", "Please review your settings before proceeding.") .title("Warning") // Error alert (red) Alert::error("error-alert", "An error occurred while processing your request.") .title("Error") ``` ### Banner Banner alerts take full width and don't display titles: ```rust Alert::info("banner-alert", "This is a banner alert that spans the full width.") .banner() Alert::success("banner-success", "Operation completed successfully!") .banner() Alert::warning("banner-warning", "System maintenance scheduled for tonight.") .banner() Alert::error("banner-error", "Service temporarily unavailable.") .banner() ``` ### Custom icon ```rust use gpui_kit::component::IconName; Alert::new("custom-icon", "Meeting scheduled for tomorrow at 3 PM.") .title("Calendar Reminder") .icon(IconName::Calendar) ``` ### Alert with Title ```rust Alert::new("alert-with-title", "Your changes have been saved successfully.") .title("Success!") ``` ### Alert Sizes ```rust use gpui_kit::component::{alert::Alert, Sizable as _}; Alert::info("alert", "Message content") .xsmall() .title("XSmall Alert") Alert::info("alert", "Message content") .small() .title("Small Alert") Alert::info("alert", "Message content") .title("Medium Alert") Alert::info("alert", "Message content") .large() .title("Large Alert") ``` ### Closable Alerts When you add an `on_close` handler, a close button appears on the alert: ```rust Alert::info("closable-alert", "This alert can be dismissed.") .title("Dismissible") .on_close(|_event, _window, _cx| { println!("Alert was closed"); // Handle alert dismissal }) ``` ### With Markdown Content We can use `TextView` to render formatted (Markdown or HTML) text within the alert, for displaying lists, bold text, links, etc. ```rust use gpui_kit::component::text::markdown; Alert::error( "error-with-markdown", markdown( "Please verify your billing information and try again.\n\ - Check your card details\n\ - Ensure sufficient funds\n\ - Verify billing address" ), ) .title("Payment Failed") ``` ### Conditional Visibility ```rust Alert::info("conditional-alert", "This alert may be hidden.") .title("Conditional") .visible(should_show_alert) // boolean condition ``` ### Form Validation Errors ```rust Alert::error( "validation-error", "Please correct the following errors before submitting:\n\ - Email address is required\n\ - Password must be at least 8 characters\n\ - Terms of service must be accepted" ) .title("Validation Failed") ``` ### Success Notification ```rust Alert::success("save-success", "Your profile has been updated successfully.") .title("Changes Saved") .on_close(|_, _, _| { // Auto-dismiss after showing }) ``` ### System Status Banner ```rust Alert::warning( "maintenance-banner", "Scheduled maintenance will occur tonight from 2:00 AM to 4:00 AM EST. \ Some services may be temporarily unavailable." ) .banner() .large() ``` ### Interactive Alert with Custom Action ```rust Alert::info("update-available", "A new version of the application is available.") .title("Update Available") .icon(IconName::Download) .on_close(cx.listener(|this, _, _, cx| { // Handle update or dismiss this.handle_update_notification(cx); })) ``` ### Multi-line Content with Formatting ```rust use gpui_kit::component::text::markdown; Alert::warning( "security-alert", markdown( "**Security Notice**: Unusual activity detected on your account.\n\n\ Recent activity:\n\ - Login from new device (Chrome on Windows)\n\ - Location: San Francisco, CA\n\ - Time: Today at 2:30 PM\n\n\ If this wasn't you, please [change your password](/) immediately." ) ) .title("Security Alert") .icon(IconName::Shield) ``` [Alert]: https://docs.rs/gpui-component/latest/gpui_component/alert/struct.Alert.html ## API Reference - [Alert] --- # Input Group Source: /component/input-group Use `InputGroup` to place text, icons, buttons, or toolbars around an input or textarea inside one frame. For a simple prefix or suffix, use [Input](/component/input). The examples below define views for an initialized GPUI Kit application. See [Getting Started](/docs/getting-started) for application setup. ## Input with a clear button Create an `InputState` once in your view and pass it to `InputGroupInput`. Subscribe to `InputEvent::Change` to refresh anything that depends on the text. Keep the returned `Subscription` in the view so the callback stays active. This view shows a character count and lets the user clear the input: ```rust use gpui_kit::{ AppContext as _, ClickEvent, Context, Entity, IntoElement, ParentElement as _, Render, Styled as _, Subscription, Window, rems, }; use gpui_kit::assets::IconName; use gpui_kit::component::{ Disableable as _, Icon, input::{ InputEvent, InputGroup, InputGroupAddon, InputGroupAddonAlignment, InputGroupButton, InputGroupInput, InputGroupText, InputState, }, }; struct SearchField { query: Entity, _change: Subscription, } impl SearchField { fn new(window: &mut Window, cx: &mut Context) -> Self { let query = cx.new(|cx| InputState::new(window, cx).placeholder("Search…")); let change = cx.subscribe(&query, |_, _, event: &InputEvent, cx| { if matches!(event, InputEvent::Change) { cx.notify(); } }); Self { query, _change: change } } fn clear(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { self.query.update(cx, |state, cx| { state.set_value("", window, cx); state.focus(window, cx); }); cx.notify(); } } impl Render for SearchField { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let count = self.query.read(cx).value().chars().count(); InputGroup::new("search") .max_w(rems(24.)) .input(InputGroupInput::new(&self.query).aria_label("Search")) .addon(InputGroupAddon::new("search-icon") .child(Icon::new(IconName::Search).size_4())) .addon(InputGroupAddon::new("search-actions") .align(InputGroupAddonAlignment::InlineEnd) .child(InputGroupText::new().child(format!("{count} characters"))) .child(InputGroupButton::new("clear").label("Clear") .disabled(count == 0) .on_click(cx.listener(Self::clear)))) } } ``` Read or set the value through the same state: ```rust let value = self.query.read(cx).value(); self.query.update(cx, |state, cx| { state.set_value("gpui", window, cx); }); cx.notify(); ``` `InputEvent::Change` reports user edits. Setting a value with `set_value` does not emit this event; call `cx.notify()` when other content in your view must refresh after a programmatic update. ## Parts and alignment | Part | Use | | --- | --- | | `InputGroup` | Combine one input with any number of addons | | `InputGroupInput` | An [Input](/component/input) placed in the group, using `InputState` | | `InputGroupTextarea` | A [Textarea](/component/textarea) placed in the group, using `TextareaState` | | `InputGroupAddon` | Position text, icons, buttons, or custom content | | `InputGroupButton` | A [Button](/component/button) with compact input-group presentation | | `InputGroupText` | Display helper text, a prefix, suffix, or counter | `InputGroupInput` and `InputGroupTextarea` are the ordinary `Input` and `Textarea` types under the names the group uses for them, so every builder those controls have — `aria_label`, `content_type`, `on_paste`, `cleanable`, `mask_toggle`, `Styled` methods — works inside a group. The group removes the control's own border, background, and focus ring and draws them around the whole frame instead. Pass the input to `.input(...)` and each addon to `.addon(...)`. Use `.child(...)` or `.children(...)` inside an addon. A later `.input(...)` replaces the earlier input; repeated `.addon(...)` calls keep all addons. Set an addon's position with `.align(InputGroupAddonAlignment::...)`: | Alignment | Position | | --- | --- | | `InlineStart` (default) | Before the input | | `InlineEnd` | After the input | | `BlockStart` | Above the input row | | `BlockEnd` | Below the input row | You can combine all four positions. Addons on the same side and children within an addon appear in the order you add them. Give each part a stable, distinct ID. Clicking text, icons, or empty space in an addon focuses the input. For example, add a protocol prefix and domain suffix to a single-line input: ```rust InputGroup::new("website") .input(InputGroupInput::new(&self.query).aria_label("Website")) .addon(InputGroupAddon::new("protocol") .child(InputGroupText::new().child("https://"))) .addon(InputGroupAddon::new("domain") .align(InputGroupAddonAlignment::InlineEnd) .child(InputGroupText::new().child(".com"))) ``` ## Buttons, icons, and menus Use `.label(...)` for a text button or `.icon(...)` for an icon button. Give icon-only buttons an `.accessibility_label(...)`; `.tooltip(...)` adds a visible hint. ```rust InputGroupButton::new("clear-icon") .icon(IconName::X) .accessibility_label("Clear search") .tooltip("Clear search") .on_click(cx.listener(Self::clear)) ``` Buttons size through `Sizable` like every other control. `.xsmall()` is the default compact size and `.small()` the larger one; a button with only an icon is square at either size. `.medium()` and `.large()` keep the standard button sizes for a prominent action in a block addon. Buttons default to ghost styling. Import `button::ButtonVariants` to use `.primary()`, `.secondary()`, or `.danger()`. Use `.outline()` for an outline, `.disabled(true)` to disable an action, and `.loading(true)` to show progress and prevent repeated clicks. Clicking a button runs its action without moving focus back to the input afterwards. For an action menu, use `.dropdown_menu(...)` with the [menu API](/component/menu); `.dropdown_caret(true)` draws the caret after the label. For contextual help, pass an `InputGroupButton` to [Popover](/component/popover)'s `.trigger(...)`, then add the Popover to an addon. ## Textarea with a counter and submit action Use `TextareaState` with `InputGroupTextarea`. `.auto_grow(min, max)` grows the input between the given row counts; longer content scrolls. Use `.rows(n)` for a fixed row count or `InputGroupTextarea::h(...)` for a fixed height. This complete view counts characters, disables submission for empty or oversized drafts, and displays the submitted text below the composer. Submitting clears and focuses the textarea. ```rust use gpui_kit::{ AppContext as _, ClickEvent, Context, Entity, IntoElement, ParentElement as _, Render, SharedString, Styled as _, Subscription, Window, rems, }; use gpui_kit::component::{ Disableable as _, button::ButtonVariants as _, v_flex, input::{ InputEvent, InputGroup, InputGroupAddon, InputGroupAddonAlignment, InputGroupButton, InputGroupText, InputGroupTextarea, TextareaState, }, }; struct MessageComposer { message: Entity, submitted: Option, _change: Subscription, } impl MessageComposer { fn new(window: &mut Window, cx: &mut Context) -> Self { let message = cx.new(|cx| { TextareaState::new(window, cx) .placeholder("Write a message…") .auto_grow(2, 6) }); let change = cx.subscribe(&message, |_, _, event: &InputEvent, cx| { if matches!(event, InputEvent::Change) { cx.notify(); } }); Self { message, submitted: None, _change: change } } fn submit(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { let value = self.message.read(cx).value(); if value.trim().is_empty() || value.chars().count() > 280 { return; } self.submitted = Some(value); self.message.update(cx, |state, cx| { state.set_value("", window, cx); state.focus(window, cx); }); cx.notify(); } } impl Render for MessageComposer { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let value = self.message.read(cx).value(); let count = value.chars().count(); v_flex().max_w(rems(28.)).gap_2() .child(InputGroup::new("message") .invalid(count > 280) .input(InputGroupTextarea::new(&self.message).aria_label("Message")) .addon(InputGroupAddon::new("message-footer") .align(InputGroupAddonAlignment::BlockEnd) .child(InputGroupText::new().child(format!("{count}/280"))) .child(InputGroupButton::new("submit").ml_auto().primary().label("Submit") .disabled(value.trim().is_empty() || count > 280) .on_click(cx.listener(Self::submit))))) .children(self.submitted.as_ref().map(|text| format!("Submitted: {text}"))) } } ``` Use `BlockStart` for a heading or toolbar above the textarea. Addons stay in place while the text scrolls. See [Textarea](/component/textarea) for more text options. ## Disabled, read-only, and validation | Method | Effect | | --- | --- | | `.disabled(true)` | Disables the input and direct `InputGroupButton` children | | `.readonly(true)` | Prevents editing while allowing focus, selection, copying, and addon actions | | `.invalid(true)` | Shows an error state while allowing further edits | An input part with `.disabled(true)` also disables its group. Pass the disabled flag to custom interactive addon content and wrapped controls separately. Set `.invalid(...)` from your validation result and show an explanation next to the group. To reject particular edits, use [`InputState::validate`](/component/input). Give each input an `.aria_label(...)`, even if you also name the group. Use `.content_type(...)` on `InputGroupInput` for hints such as a URL or email address. Password masking is configured with `InputState::masked`. Both input parts support `.context_menu(...)` for a custom right-click menu. On touch devices, long-press the text to select a word, drag the selection handles, and use the edit menu to cut, copy, paste, or select all. In Rust, both input parts also support `.on_paste(...)` to handle clipboard images and files before text is inserted. Return `true` to consume the paste, or `false` to allow the default text insertion. The handler is not called while the input is disabled or read-only. See [Paste Hook](/component/input#paste-hook) for an attachment example and web limitations. ## Sizes and custom styles The default group size is Medium. Import `Sizable` to use `.xsmall()`, `.small()`, `.large()`, or `.with_size(Size::Medium)`; the size sets the frame height, the text size, and the insets the addons share with the control. Colors, corners, the focus ring, and the invalid ring follow your [Theme](/component/theme). Use `Styled` methods to set the group's width, spacing, and other appearance. The control keeps its own `Styled` methods for the text it edits, and each addon, button, and text part styles itself the same way: ```rust use gpui_kit::component::{ActiveTheme as _, Sizable as _, StyledExt as _}; InputGroup::new("styled-search") .small() .max_w(rems(24.)) .input(InputGroupInput::new(&self.query) .aria_label("Search") .px_3() .text_base()) .addon(InputGroupAddon::new("styled-actions") .align(InputGroupAddonAlignment::InlineEnd) .child(InputGroupButton::new("styled-clear").label("Clear").icon(IconName::X) .font_semibold() .on_click(cx.listener(Self::clear)))) ``` Placeholder, caret, and selection colors follow the Theme. Import `FocusableExt` and use `.focus_ring(false)` to hide the default ring. ## JavaScript Import the same parts from `gpui-component`. Create text states in `View.init`. Use `.value(...)` and `.on_change(...)` for a controlled input: ```javascript import { View } from "gpui-kit"; import { InputState, InputGroup, InputGroupInput, InputGroupAddon, InputGroupButton, } from "gpui-component"; export default class Search extends View { init() { this.input = InputState("Search…"); this.query = ""; } render() { return new InputGroup("search") .input(new InputGroupInput(this.input) .aria_label("Search").value(this.query) .on_change((value, cx) => { this.query = value; cx.notify(); })) .addon(new InputGroupAddon("actions").align("inline-end") .child(new InputGroupButton("clear").label("Clear") .disabled(this.query.length === 0) .on_click((_event, cx) => { this.query = ""; cx.notify(); }))); } } ``` Programmatic `.value(...)` updates do not call `on_change`. Setting the same value keeps the selection and undo history. Omit `.value(...)` to let the input keep its own value, and use `on_change(value, cx)` when you need to react to edits. `InputGroupTextarea` accepts `TextareaState` and supports `.rows(n)` and `.auto_grow(min, max)`. Both input parts provide `.placeholder(...)`. `InputGroupInput` also provides `.masked(bool)` and `.content_type(...)`, with values such as `email_address`, `url`, and `new_password`. Set group and button size with `.size("small")`; available values are `xsmall`, `small`, `medium`, and `large`. Button icons take an asset path, such as `.icon("icons/search.svg")`. Style methods apply to each part directly, as in Rust: ```javascript new InputGroupInput(this.input).px(12).text_base(); new InputGroupButton("clear").label("Clear").icon("icons/x.svg").font_semibold(); ``` Run `gpui-component-shell types ` to generate editor completion. ## Inline references To combine inline references with attachments or send buttons, pass an Input or Textarea containing tokens to `InputGroup`. You can customize its labels as usual: ```rust use gpui_kit::component::{ IconName, input::{InputToken, InputGroup, Textarea}, }; InputGroup::new("composer") .input(Textarea::new(&state) .token(|token, _, _| InputToken::new(token).icon(IconName::File))) ``` The JavaScript group controls also expose `token` and `on_token_click`. Retain the same input state across redraws; call `set_value` with saved content to restore a draft. See [atomic inline tokens](/component/input#atomic-inline-tokens). --- # Form Source: /component/form Form lays out typed fields and an optional footer. The application owns values, validation, submission, and responsive column choices. ## Import ```rust use gpui_kit::component::form::{field, v_form, h_form, Form, Field}; ``` ## Predictable composition `Form::new()` defaults to one column with labels above controls. `label_layout(Axis::Horizontal)` places labels beside controls; `columns(2)` independently creates two field columns. Existing `horizontal()`, `vertical()`, `layout(Axis)`, `h_form()`, and `v_form()` remain available. ```rust Form::new() .label_layout(Axis::Horizontal) .columns(2) .child(Field::new().label("Name").child(Input::new(&name_input))) .child(Field::new().label("Email").child(Input::new(&email_input))) .footer(Button::new("save").label("Save")) ``` `child` accepts a Field. Put commands in `footer`, which spans all columns and aligns its content to the trailing edge. Attach submission behavior to the supplied Button; Form does not submit automatically. See the [complete application recipe](https://github.com/MohsenDastaran/uni-kit/tree/main/examples/ai_recipes) for retained state, callbacks, and window setup. ## Usage ### Basic Form ```rust v_form() .child( field() .label("Name") .child(Input::new(&name_input)) ) .child( field() .label("Email") .child(Input::new(&email_input)) .required(true) ) ``` ### Horizontal Form Layout ```rust h_form() .label_width(px(120.)) .child( field() .label("First Name") .child(Input::new(&first_name)) ) .child( field() .label("Last Name") .child(Input::new(&last_name)) ) ``` ### Multi-Column Form ```rust v_form() .columns(2) // Two-column layout .child( field() .label("First Name") .child(Input::new(&first_name)) ) .child( field() .label("Last Name") .child(Input::new(&last_name)) ) .child( field() .label("Bio") .col_span(2) // Span across both columns .child(Input::new(&bio_input)) ) ``` ### User Registration Form ```rust struct RegistrationForm { first_name: Entity, last_name: Entity, email: Entity, password: Entity, confirm_password: Entity, terms_accepted: bool, } impl Render for RegistrationForm { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_form() .large() .child( field() .label("Personal Information") .label_indent(false) .child( h_flex() .gap_3() .child( div().flex_1().child( Input::new(&self.first_name) .placeholder("First name") ) ) .child( div().flex_1().child( Input::new(&self.last_name) .placeholder("Last name") ) ) ) ) .child( field() .label("Email") .required(true) .child(Input::new(&self.email)) ) .child( field() .label("Password") .required(true) .description("Must be at least 8 characters") .child(Input::new(&self.password)) ) .child( field() .label("Confirm Password") .required(true) .child(Input::new(&self.confirm_password)) ) .child( field() .label_indent(false) .child( Checkbox::new("terms") .label("I agree to the Terms of Service") .checked(self.terms_accepted) .on_click(cx.listener(|this, checked, _, cx| { this.terms_accepted = *checked; cx.notify(); })) ) ) .child( field() .label_indent(false) .child( Button::new("register") .primary() .large() .w_full() .child("Create Account") ) ) } } ``` ### Settings Form with Sections ```rust v_form() .column(2) .child( field() .label("Profile") .label_indent(false) .col_span(2) .child(Separator::horizontal()) ) .child( field() .label("Display Name") .child(Input::new(&display_name)) ) .child( field() .label("Email") .child(Input::new(&email)) ) .child( field() .label("Bio") .col_span(2) .items_start() .child(Input::new(&bio)) ) .child( field() .label("Preferences") .label_indent(false) .col_span(2) .child(Separator::horizontal()) ) .child( field() .label("Theme") .child(Select::new(&theme_state)) ) .child( field() .label("Language") .child(Select::new(&language_state)) ) .child( field() .label_indent(false) .child(Switch::new("notifications").label("Enable notifications")) ) .child( field() .label_indent(false) .child(Switch::new("marketing").label("Marketing emails")) ) ``` ### Contact Form ```rust v_form() .child( field() .label("Contact Information") .child( h_flex() .gap_2() .child( Select::new(&prefix_state) .w(px(80.)) ) .child( div().flex_1().child( Input::new(&name_input) .placeholder("Your name") ) ) ) ) .child( field() .label("Email") .required(true) .child(Input::new(&email_input)) ) .child( field() .label("Subject") .child(Select::new(&subject_state)) ) .child( field() .label("Message") .required(true) .items_start() .description("Please describe your inquiry in detail") .child(Input::new(&message_input)) ) .child( field() .label_indent(false) .child( h_flex() .gap_2() .justify_between() .child( Checkbox::new("copy") .label("Send me a copy") ) .child( h_flex() .gap_2() .child(Button::new("cancel").child("Cancel")) .child(Button::new("send").primary().child("Send Message")) ) ) ) ``` ## Form Container and Layout ### Vertical Layout (Default) ```rust v_form() .gap(px(12.)) .child(field().label("Name").child(input)) .child(field().label("Email").child(email_input)) ``` ### Horizontal Layout ```rust h_form() .label_width(px(100.)) .child(field().label("Name").child(input)) .child(field().label("Email").child(email_input)) ``` ### Custom Sizing ```rust v_form() .large() // Large form size .label_text_size(rems(1.2)) .child(field().label("Title").child(input)) v_form() .small() // Small form size .child(field().label("Code").child(input)) ``` ## Form Validation ### Required Fields ```rust field() .label("Email") .required(true) // Shows asterisk (*) next to label .child(Input::new(&email_input)) ``` ### Field Descriptions ```rust field() .label("Password") .description("Must be at least 8 characters long") .child(Input::new(&password_input)) ``` ### Dynamic Descriptions ```rust field() .label("Bio") .description_fn(|_, _| { div().child("Use at most 100 words to describe yourself.") }) .child(Input::new(&bio_input)) ``` ### Field Visibility ```rust field() .label("Admin Settings") .visible(user.is_admin()) // Conditionally show field .child(Switch::new("admin-mode")) ``` ## Submit Handling ### Basic Submit Pattern ```rust struct FormView { name_input: Entity, email_input: Entity, } impl FormView { fn submit(&mut self, cx: &mut Context) { let name = self.name_input.read(cx).value(); let email = self.email_input.read(cx).value(); // Validate inputs if name.is_empty() || email.is_empty() { // Show validation error return; } // Submit form data self.handle_submit(name, email, cx); } } // Form with submit button v_form() .child(field().label("Name").child(Input::new(&self.name_input))) .child(field().label("Email").child(Input::new(&self.email_input))) .child( field() .label_indent(false) .child( Button::new("submit") .primary() .child("Submit") .on_click(cx.listener(|this, _, _, cx| this.submit(cx))) ) ) ``` ### Form with Action Buttons ```rust v_form() .child(field().label("Title").child(Input::new(&title))) .child(field().label("Content").child(Input::new(&content))) .child( field() .label_indent(false) .child( h_flex() .gap_2() .child(Button::new("save").primary().child("Save")) .child(Button::new("cancel").child("Cancel")) .child(Button::new("preview").outline().child("Preview")) ) ) ``` ## Field Groups ### Related Fields ```rust v_form() .child( field() .label("Name") .child( h_flex() .gap_2() .child(div().flex_1().child(Input::new(&first_name))) .child(div().flex_1().child(Input::new(&last_name))) ) ) .child( field() .label("Address") .items_start() // Align to start for multi-line content .child( v_flex() .gap_2() .child(Input::new(&street)) .child( h_flex() .gap_2() .child(div().flex_1().child(Input::new(&city))) .child(div().w(px(100.)).child(Input::new(&zip))) ) ) ) ``` ### Custom Field Components ```rust field() .label("Theme Color") .child(ColorPicker::new(&color_state).small()) field() .label("Birth Date") .description("We'll send you a birthday gift!") .child(DatePicker::new(&date_state)) field() .label("Notifications") .child( v_flex() .gap_2() .child(Switch::new("email").label("Email notifications")) .child(Switch::new("push").label("Push notifications")) .child(Switch::new("sms").label("SMS notifications")) ) ``` ### Conditional Fields ```rust v_form() .child( field() .label("Account Type") .child(Select::new(&account_type)) ) .child( field() .label("Company Name") .visible(is_business_account) // Show only for business accounts .child(Input::new(&company_name)) ) .child( field() .label("Tax ID") .visible(is_business_account) .required(is_business_account) .child(Input::new(&tax_id)) ) ``` ## Grid Layout and Positioning ### Column Spanning ```rust v_form() .columns(3) // Three-column grid .child(field().label("First").child(input1)) .child(field().label("Second").child(input2)) .child(field().label("Third").child(input3)) .child( field() .label("Full Width") .col_span(3) // Spans all three columns .child(Input::new(&full_width)) ) ``` ### Column Positioning ```rust v_form() .columns(4) .child(field().label("A").child(input_a)) .child(field().label("B").child(input_b)) .child( field() .label("Positioned") .col_start(1) // Start at column 1 .col_span(2) // Span 2 columns .child(input_positioned) ) ``` ### Responsive Layout ```rust v_form() .columns(if is_mobile { 1 } else { 2 }) .child(field().label("Name").child(name_input)) .child(field().label("Email").child(email_input)) .child( field() .label("Bio") .when(!is_mobile, |field| field.col_span(2)) .child(bio_input) ) ``` --- # Combobox Source: /component/combobox A searchable dropdown for selecting one or multiple values from a list. ## Select vs Combobox | Feature | Select | Combobox | | --- | --- | --- | | Searchable | ✓ (optional) | ✓ (optional) | | Multi-select | — | ✓ (`.multiple(true)`) | | Custom trigger rendering | — | ✓ | | Custom item rendering | — | ✓ | | Footer action slot | — | ✓ | Use `Select` for simple single-value picking. Use `Combobox` when you need multi-select, a fully custom trigger, or custom item rendering. ## Import ```rust use gpui_kit::component::combobox::{ Combobox, ComboboxState, ComboboxEvent, ComboboxTriggerCtx, }; use gpui_kit::component::searchable_list::{ SearchableListItem, SearchableVec, SearchableGroup, }; ``` ## Usage ### Basic Single-Select ```rust let state = cx.new(|cx| { ComboboxState::new( SearchableVec::new(vec!["Next.js", "SvelteKit", "Nuxt.js"]), vec![], // no initial selection window, cx, ) .searchable(true) }); Combobox::new(&state) .placeholder("Select framework...") .search_placeholder("Search...") .w_full() ``` ### Multi-Select Pass `.multiple(true)` to enable multi-select mode. Clicking an item toggles it; the dropdown stays open until the user presses Escape or clicks outside. ```rust let state = cx.new(|cx| { ComboboxState::new( SearchableVec::new(vec!["React", "Vue", "Angular"]), vec![IndexPath::new(0)], // pre-selected window, cx, ) .multiple(true) .searchable(true) }); Combobox::new(&state).placeholder("Select frameworks") ``` ### Pre-selected Item Pass index paths of items to pre-select: ```rust let state = cx.new(|cx| { ComboboxState::new(items, vec![IndexPath::new(0)], window, cx) }); ``` ### Grouped Items Use `SearchableGroup` to group items under a heading: ```rust let grouped = SearchableVec::new(vec![ SearchableGroup::new("Fruits").items(vec![ FoodItem::new("Apples"), FoodItem::new("Bananas"), ]), SearchableGroup::new("Vegetables").items(vec![ FoodItem::new("Carrots"), FoodItem::new("Spinach"), ]), ]); let state = cx.new(|cx| { ComboboxState::new(grouped, vec![], window, cx).searchable(true) }); Combobox::new(&state) ``` ### Implementing `SearchableListItem` Built-in implementations exist for `String`, `SharedString`, and `&'static str`. For custom types implement the trait: ```rust #[derive(Clone)] struct Country { name: SharedString, code: SharedString, } impl SearchableListItem for Country { type Value = SharedString; fn title(&self) -> SharedString { self.name.clone() } fn value(&self) -> &SharedString { &self.code } fn matches(&self, query: &str) -> bool { self.name.to_lowercase().contains(query) || self.code.to_lowercase().contains(query) } } ``` ### Disabled Items Return `true` from `disabled()` on items that should not be selectable: ```rust impl SearchableListItem for MyItem { // ... fn disabled(&self) -> bool { self.is_unavailable } } ``` ### Custom Check Icon ```rust Combobox::new(&state) .check_icon(Icon::new(IconName::CircleCheck)) ``` ### Footer Action Render a persistent action at the bottom of the dropdown (e.g. an "Add new" button): ```rust Combobox::new(&state) .footer(|_, cx| { Button::new("add-new") .ghost() .label("New item") .icon(Icon::new(IconName::Plus)) .w_full() .justify_start() .into_any_element() }) ``` ### Custom Trigger Override the entire trigger element. `ComboboxTriggerCtx` exposes the current selection, open/disabled flags, and size: ```rust Combobox::new(&state) .render_trigger(|ctx, _, cx| { h_flex() .w_full() .items_center() .gap_2() .when(ctx.selection.is_empty(), |this| { this.text_color(cx.theme().muted_foreground) .child("Select...") }) .children(ctx.selection.iter().map(|(_, item)| { div() .bg(cx.theme().accent) .rounded_sm() .px_1p5() .py_0p5() .text_sm() .child(item.title()) })) .into_any_element() }) ``` ### Icons ```rust Combobox::new(&state) .placeholder("Select industry category") .search_placeholder("Search…") ``` ### Badges ```rust Combobox::new(&state) .placeholder("Select frameworks") .search_placeholder("Search…") ``` ### Maximum selections ```rust Combobox::new(&state) .placeholder("Select up to 2 frameworks") .search_placeholder("Search…") ``` ### Pinned items ```rust Combobox::new(&state) .placeholder("Select framework...") .search_placeholder("Search…") ``` ### Rich items ```rust Combobox::new(&state) .placeholder("Select framework...") .search_placeholder("Search…") ``` ### Overflow ```rust Combobox::new(&state) .placeholder("Select frameworks") .search_placeholder("Search…") ``` ### Count ```rust Combobox::new(&state) .placeholder("Select frameworks") .search_placeholder("Search…") ``` ### Values ```rust let basic = state.read(cx).selected_values(); let grouped = grouped.read(cx).selected_values(); ``` ### Sizes ```rust Combobox::new(&state).large() Combobox::new(&state) // medium (default) Combobox::new(&state).small() ``` ### Cleanable ```rust Combobox::new(&state).cleanable(true) // show clear button when a value is selected ``` ### Disabled ```rust Combobox::new(&state).disabled(true) ``` ### Events Both `Change` (fired on every toggle) and `Confirm` (fired when the dropdown closes) carry the full selection as `Vec`. ```rust cx.subscribe_in(&state, window, |view, _, event, window, cx| { match event { ComboboxEvent::Change(values) => { // fired on every toggle } ComboboxEvent::Confirm(values) => { // fired when the dropdown closes } } }); ``` ### Mutating Programmatically Values are resolved through the current delegate. Values that cannot be found are ignored. `set_selected_values` clears the search query first, so an active search never decides which values can be selected. Index paths address the list as it is currently displayed, so `set_selected_indices`, `add_selected_index` and `remove_selected_index` act on the visible rows and leave the query alone. ```rust // Replace the entire selection by value state.update(cx, |s, cx| { s.set_selected_values(&["React", "Angular"], window, cx); }); // Replace the entire selection by index path state.update(cx, |s, cx| { s.set_selected_indices(vec![IndexPath::new(0), IndexPath::new(2)], window, cx); }); // Add / remove individual items state.update(cx, |s, cx| { s.add_selected_index(IndexPath::new(1), cx); s.remove_selected_index(IndexPath::new(0), cx); }); // Clear all selections state.update(cx, |s, cx| { s.clear_selection(cx); }); // Read all selected values (multi-select) let values = state.read(cx).selected_values(); // Vec // Read the first selected value (single-select convenience) let value = state.read(cx).selected_value(); // Option ``` ## Keyboard Shortcuts | Key | Action | | --------- | ---------------------------------------- | | `Tab` | Focus trigger | | `Enter` | Open menu or confirm highlighted item | | `Up/Down` | Navigate options (opens menu if closed) | | `Escape` | Close menu | ## Theming - `background` — Dropdown input background - `input` — Trigger border color - `foreground` — Text color - `muted_foreground` — Placeholder and disabled text - `border` — Menu border - `radius` — Border radius --- # Editor Source: /component/editor `Editor` is the styled source-code control. Use [`Input`](/component/input) for single-line values and [`Textarea`](/component/textarea) for ordinary multi-line text. ## Import ```rust use gpui_kit::component::input::{Editor, EditorState, TabSize}; ``` ## Language editing rules `LanguageConfig` describes a language; `.auto_close(bool)` and `.smart_indent(bool)` are independent editor preferences. Changing languages or replacing rules does not reset either preference. Automatic closing, skip-over, and paired Backspace use `auto_closing_pairs`. Enter uses `brackets` and `indentation_rules`, so it can still split an existing pair when automatic closing is disabled. ```rust use gpui_kit::component::input::{ AutoClosingPair, BracketPair, language_config::LanguageConfig, SyntaxContext, set_language_config, }; let rules = LanguageConfig::default() .brackets([BracketPair::new("{", "}"), BracketPair::new("(", ")")]) .auto_closing_pairs([ AutoClosingPair::new("{", "}") .not_in([SyntaxContext::String, SyntaxContext::Comment]), AutoClosingPair::new("(", ")") .not_in([SyntaxContext::String, SyntaxContext::Comment]), ]) .auto_close_before(";:.,=}])>"); set_language_config("rust", rules, cx); let editor = cx.new(|cx| { EditorState::new(window, cx) .language("rust") .auto_close(true) .smart_indent(true) }); ``` `set_language_config` replaces the configuration for a language in the current application. Existing editors use the replacement on their next edit, including within the same event handler. Aliases share configurations: `python`, `py`, and `pyi` refer to the same language even without its grammar feature. Custom configurations survive component initialization. Exact custom grammar registrations take precedence over built-in aliases and retain their original case. Unknown languages use `LanguageConfig::default()`. Component installs a `LanguageProvider` for language names, editing defaults, and editor-owned syntax providers. Syntax selection follows the language on the first edit and after language changes, independently of rendering. Base clients can install their own service with `set_language_provider`; ordinary Component clients only need `set_language_config`. Grammar resources are available as `highlighter::GrammarConfig`; its existing `highlighter::LanguageConfig` name remains compatible. Pairs use strings, including multi-character delimiters. `auto_closing_pairs` is optional: `None` uses the structural `brackets`, while `Some(vec![])` disables all automatic pairs. Its builder sets `Some`. Whitespace and end-of-document always allow automatic insertion; `auto_close_before` lists other allowed following characters. `not_in` requires a syntax-context provider; without one, the Base editor reports `Code`. The styled editor supplies a provider when the language's Tree-sitter grammar is enabled. `IndentationRules::new(increase, decrease)` accepts two compiled `regex::Regex` patterns. On Enter, the increase pattern tests text before the cursor and the decrease pattern tests text after it. Without an increase pattern, structural opening brackets provide the default indentation. These rules do not reformat existing lines or pasted text. Python's language defaults additionally recognize a trailing colon; unknown languages use structural brackets only. This is the supported subset of Monaco-style language configuration, not a loader for Monaco JSON or Tree-sitter `.scm` files. Selection-surrounding and custom `onEnterRules` are not part of this interface yet. ## Basic usage ```rust let editor = cx.new(|cx| { EditorState::new(window, cx) .language("rust") .line_number(true) .folding(true) .tab_size(TabSize { tab_size: 4, hard_tabs: false, }) .default_value("fn main() {\n println!(\"Hello\");\n}") }); Editor::new(&editor).h(px(320.)) ``` The language set via `.language()` selects syntax highlighting. Enable the matching Cargo feature, such as `tree-sitter-rust` or `tree-sitter-markdown`; use `tree-sitter-languages` to bundle all built-in grammars. ## Editor options ```rust let editor = cx.new(|cx| { EditorState::new(window, cx) .language("json") .line_number(true) .folding(true) .show_whitespaces(true) .default_value(source) }); ``` ## Keyboard shortcuts and column selection These defaults apply while the editor is focused. On macOS, Option is the Alt modifier. Linux uses no Super/Win bindings for these operations. | Operation | macOS | Linux | Windows | | --- | --- | --- | --- | | Add a cursor above / below | Cmd+Option+Up / Down | Alt+Shift+Up / Down | Ctrl+Alt+Up / Down | | Extend every selection by one character | Shift+Left / Right | Shift+Left / Right | Shift+Left / Right | | Extend every selection by one word | Option+Shift+Left / Right | Ctrl+Shift+Left / Right | Ctrl+Shift+Left / Right | | Add a cursor with the mouse | Option+left click | Alt+left click | Alt+left click | | Select a rectangular block | Option+Shift+left drag | Alt+Shift+left drag | Alt+Shift+left drag | | Keep only the active cursor | Escape | Escape | Escape | Linux also accepts Ctrl+Alt+left drag for rectangular selection, matching Ghostty, and Alt+Shift+Left / Right for word selection. Windows additionally accepts Alt+Shift+Left / Right for character selection. Alt/Option+left drag works as a column-selection shortcut on all three platforms: a click adds a cursor, while dragging builds a new block from the mouse-down position. Holding Alt/Option over the editor shows a `+` crosshair. Selection gestures that include Alt take priority over Ctrl/Cmd-click go-to-definition. A block creates one selection per display row, clipped to the available text on short rows. Typing or deleting edits all selections. Releasing the mouse ends the drag; Escape keeps the active cursor (an open context menu handles Escape first). Adding cursors with Up / Down is additive: reversing direction does not shrink the block's height. This is multi-cursor editing with mouse column selection, not a persistent Vim Visual Block mode. During keyboard input, carets remain visible; blinking resumes after 300 ms without input. Linux desktop shortcuts can intercept key combinations before the editor sees them. In particular, Ctrl+Alt+Up / Down is not bound by default on Linux because some desktops use it to switch workspaces. The shortcuts above refer to logical modifiers after any keyboard remapping. ## Search The editor has a built-in search panel. Press `Ctrl-F` (Windows/Linux) or `Cmd-F` (macOS) while the editor is focused to open it. `Enter` jumps to the next match, `Shift+Enter` to the previous one, `Escape` closes the panel. ```rust // Open the find panel programmatically editor.update(cx, |state, cx| { state.open_search(false, cx); }); // Close it editor.update(cx, |state, cx| { state.close_search(cx); }); ``` Search is enabled by default for `Editor`. To disable it: ```rust editor.update(cx, |state, cx| { state.set_searchable(false, cx); }); ``` A read-only editor can still be searched — the replace UI is hidden automatically. ### Custom search UI The search engine is usable without the panel, so an application can draw its own search bar on top of the editor's matching, highlighting, scrolling and replacing. `set_search_query` starts a search; the editor highlights the matches until `close_search`. An editor that is not `searchable` never opens the built-in panel and leaves `Ctrl-F` / `Cmd-F` to its ancestors, so the application can bind the shortcut to its own search field. ```rust let editor = cx.new(|cx| EditorState::new(window, cx).searchable(false)); // Search from the application's own field editor.update(cx, |state, cx| { state.set_search_query("needle", true, cx); }); // Navigate; each call scrolls the match into view editor.update(cx, |state, cx| { state.next_search_match(cx); state.previous_search_match(cx); }); // Describe the matches: "2/5" let matcher = &editor.read(cx).search_session().matcher; let label = matcher.label(); let count = matcher.len(); let current = matcher.current(); // None without matches // Replace, when the editor is editable editor.update(cx, |state, cx| { state.replace_current_search_match("replacement", window, cx); state.replace_all_search_matches("replacement", window, cx); }); // End the search and its highlights editor.update(cx, |state, cx| { state.close_search(cx); }); ``` Take the shortcut on the view that owns the search field: ```rust use gpui_kit::component::input::Search; div() .on_action(cx.listener(|this: &mut Self, _: &Search, window, cx| { this.search.update(cx, |search, cx| search.focus(window, cx)); })) .child(Editor::new(&this.editor)) ``` ## Decorations ```rust let decorations = editor.update(cx, |state, cx| { state.create_decorations_collection(initial_decorations, cx) }); ``` Keep the returned `TextDecorationCollection` to update or clear that owner's text styles. Ranges follow edits; dropping the handle does not remove decorations. ### Geometric range decorations Use a separate `RangeDecorationCollection` for continuous fills or one-logical-pixel frames. These are paint-only annotations: they do not reserve inline space, add widgets, intercept pointer events, or change keyboard focus. ```rust use gpui_kit::component::input::{RangeDecoration, RangeDecorationStyle}; let review_ranges = editor.update(cx, |state, cx| { state.create_range_decorations_collection( vec![ RangeDecoration::new(0..8).with_style(RangeDecorationStyle::Fill), RangeDecoration::new(12..24), // Frame is the default. ], cx, ) }); review_ranges.set(vec![RangeDecoration::new(4..16)], cx); review_ranges.append(vec![RangeDecoration::new(20..28)], cx); let tracked_ranges = review_ranges.get_ranges(cx); review_ranges.clear(cx); // Keep the collection available for reuse. review_ranges.dispose(cx); // Invalidate this handle and all its clones. ``` Each collection owns only its own entries, so separate extensions cannot overwrite one another. Dropping a handle leaves its collection in the editor; `dispose` releases it permanently. Calls on disposed collections or a dropped editor are no-ops. Individual decorations do not require an ID. Ranges are half-open UTF-8 **byte offsets**, not character indices or line numbers. Start/end offsets are clipped outward to valid character boundaries; empty, reversed, and entirely out-of-document ranges are discarded. Both text and geometric decorations share these tracking rules: - Inserting at either edge does not grow the range; inserting inside it does. - Replacements clip overlapping anchors to the replaced span; deleting an entire range removes the decoration. - Undo/redo, `set_value`, and `replace_all` (including formatting) apply the same edit transforms. Annotations are **not** snapshots in undo history: undoing a deletion does not resurrect a removed decoration, and undoing a replacement does not recover its former interior anchors. Reset the collection from your semantic source when that distinction matters. - Folding changes only visual projection. Hidden-only ranges are not painted; visible portions remain clipped to the viewport and follow soft-wrapped glyphs. Fills paint behind frames, and both sit below the selection and glyphs. Within one style, later collections/items paint over earlier ones. Without `with_color`, frames use the editor foreground and fills use that color at 12% opacity, so the fallback follows theme changes. An explicit color is owned by the application. Visible-range queries use an interval index and skip folded buffer spans; they do not scan every decoration per frame. Setting/appending entries rebuilds that collection's index; edits update affected collections linearly without re-sorting. The Editor showcase's **Decorations** tab demonstrates both collection types. ## Value and events ```rust let source = editor.read(cx).value(); editor.update(cx, |state, cx| { state.set_value(new_source, window, cx); }); cx.subscribe(&editor, |this, state, event: &InputEvent, cx| { if matches!(event, InputEvent::Change) { this.source = state.read(cx).value(); cx.notify(); } }); ``` ## Font The editor paints its code in the theme's monospace font — `mono_font_family` at `mono_font_size` — with rows 1.5 times the font size. That is only the default: a text style set on the editor refines over it, and the gutter and row height follow the size. The theme's platform default (`Menlo`, `Consolas`, `DejaVu Sans Mono`) is checked against the installed fonts when the theme loads and swapped for an installed monospace font, or `.SystemUIFont`, when it is missing; a family you set yourself is used as-is. ```rust Editor::new(&editor).text_sm() Editor::new(&editor) .font_family("JetBrains Mono") .text_size(px(15.)) ``` These are the ordinary [`Styled`](https://docs.rs/gpui/latest/gpui/trait.Styled.html) methods every element has, so `font_weight` and `line_height` work the same way. ## Appearance ```rust Editor::new(&editor) .h(px(480.)) .bordered(true) .disabled(false) .readonly(false) .aria_label("Rust source") ``` Use `readonly` to preview a file without allowing changes. Unlike `disabled`, a read-only editor keeps the normal appearance and still can be focused, selected, copied and searched, it only rejects the changes made by the user. The programmatic APIs such as `set_value` keep working. ```rust Editor::new(&editor).readonly(true) ``` Editor focus does not add the single-line Input focus-border treatment. The gutter, current-line background, and scrollbars are painted as one aligned editor surface. Input-only adornments such as `prefix`, `suffix`, mask toggle, and clear button are intentionally absent. Compose toolbars and actions around `Editor`. --- # TitleBar Source: /component/title-bar TitleBar provides a customizable window title bar that can replace the default OS title bar. It includes platform-specific window controls (minimize, maximize, close) and supports custom content and styling. The component automatically adapts to different operating systems (macOS, Windows, Linux) with appropriate behaviors and visual styles. ## Import ```rust use gpui_kit::component::TitleBar; ``` ## Usage ### Basic Title Bar ```rust TitleBar::new() .child(div().child("My Application")) ``` ### Title Bar with Custom Content ```rust TitleBar::new() .child( div() .flex() .items_center() .gap_3() .child("App Name") .child(Badge::new().count(5)) ) .child( div() .flex() .items_center() .gap_2() .child(Button::new("settings").icon(IconName::Settings)) .child(Button::new("profile").icon(IconName::User)) ) ``` ### Title Bar with Menu Bar ```rust TitleBar::new() .child( div() .flex() .items_center() .child(AppMenuBar::new(window, cx)) ) .child( div() .flex() .items_center() .justify_end() .gap_2() .child(Button::new("github").icon(IconName::GitHub)) .child(Button::new("notifications").icon(IconName::Bell)) ) ``` ### Title Bar with Window Controls (Linux only) ```rust TitleBar::new() .on_close_window(|_, window, cx| { // Custom close behavior window.push_notification("Saving before close...", cx); // Perform cleanup window.remove_window(); }) .child(div().child("Custom Close Behavior")) ``` ### Styled Title Bar ```rust TitleBar::new() .bg(cx.theme().primary) .border_color(cx.theme().primary_border) .child( div() .text_color(cx.theme().primary_foreground) .child("Styled Title Bar") ) ``` ### Title Bar Options for Window Use `TitleBar::window_options()` as the base of the window options, it sets up everything the title bar needs, including letting the title bar own dragging and double clicking instead of the system. ```rust use gpui_kit::WindowOptions; WindowOptions { window_bounds: Some(window_bounds), ..TitleBar::window_options() } ``` If you build the [`WindowOptions`] yourself, set both fields: ```rust use gpui_kit::WindowOptions; WindowOptions { titlebar: Some(TitleBar::title_bar_options()), // Required on macOS, otherwise the system also handles title bar double // clicks and delays title bar clicks to disambiguate double clicks. app_owns_titlebar_drag: true, ..Default::default() } ``` ### Application Title Bar ```rust use gpui_kit::component::{TitleBar, button::Button, menu::AppMenuBar}; struct AppTitleBar { app_menu_bar: Entity, } impl Render for AppTitleBar { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { TitleBar::new() .child( div() .flex() .items_center() .child(self.app_menu_bar.clone()) ) .child( div() .flex() .items_center() .justify_end() .gap_2() .child( Button::new("settings") .ghost() .icon(IconName::Settings) ) .child( Button::new("help") .ghost() .icon(IconName::HelpCircle) ) ) } } ``` ### Title Bar with Breadcrumbs ```rust TitleBar::new() .child( div() .flex() .items_center() .gap_2() .child("Home") .child(IconName::ChevronRight) .child("Documents") .child(IconName::ChevronRight) .child("Project") ) .child( div() .flex() .items_center() .gap_1() .child(Button::new("search").icon(IconName::Search).ghost()) .child(Button::new("more").icon(IconName::MoreHorizontal).ghost()) ) ``` ### Custom Themed Title Bar ```rust TitleBar::new() .h(px(40.)) // Custom height .bg(cx.theme().accent) .border_b_2() .border_color(cx.theme().accent_border) .child( div() .flex() .items_center() .text_color(cx.theme().accent_foreground) .font_weight_semibold() .child("Custom Theme App") ) ``` ### Title Bar with Status ```rust TitleBar::new() .child( div() .flex() .items_center() .gap_3() .child("My Editor") .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("● Unsaved changes") ) ) .child( div() .flex() .items_center() .gap_2() .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("Line 42, Col 12") ) .child( Button::new("sync") .small() .ghost() .icon(IconName::RotateCcw) .tooltip("Sync changes") ) ) ``` ### Minimal Title Bar ```rust TitleBar::new() .child( div() .text_center() .flex_1() .child("Document.txt") ) ``` ### Title Bar with Search ```rust TitleBar::new() .child( div() .flex() .items_center() .gap_3() .child("File Explorer") .child( Input::new("search") .placeholder("Search files...") .w(px(200.)) .small() ) ) ``` ## Platform Differences ### macOS - Uses native traffic light buttons (minimize, maximize, close) - Traffic light position is automatically set to `(9px, 9px)` - Double-click behavior calls `window.titlebar_double_click()` - Left padding accounts for traffic light buttons (80px) - Appears transparent by default ### Windows - Custom window control buttons with system integration - Uses `WindowControlArea` for proper window management - Control buttons have hover and active states - Fixed button width of 34px each - Left padding is 12px ### Linux - Custom window control buttons with manual event handling - Supports custom close window callback via `on_close_window()` - Double-click to maximize/restore window - Right-click shows window context menu - Window dragging supported in title bar area ## Notes - The title bar automatically handles platform-specific styling and behavior - Window controls are only rendered on Windows and Linux platforms - The component integrates with GPUI's window management system - Custom styling should consider platform conventions - Window dragging is handled automatically in appropriate areas ## API Reference ### TitleBar | Method | Description | | --------------------- | ---------------------------------------- | | `new()` | Create a new title bar | | `child(element)` | Add child element to the title bar | | `on_close_window(fn)` | Custom close window handler (Linux only) | | `title_bar_options()` | Get default titlebar options for window | | `window_options()` | Get default window options for the title bar | ### Window Configuration | Property | Description | | ------------------------ | -------------------------------------------------------------- | | `appears_transparent` | Make title bar transparent (default: true) | | `traffic_light_position` | Position of macOS traffic lights | | `title` | Window title (optional when using custom title bar) | | `app_owns_titlebar_drag` | Let the title bar own dragging and double clicking (macOS only) | ### Title Bar Element (Internal) The `TitleBarElement` provides window dragging functionality on Linux platforms. ### Constants | Constant | Value | Description | | ------------------------ | ------------------------------- | ------------------------- | | `TITLE_BAR_HEIGHT` | `34px` | Standard title bar height | | `TITLE_BAR_LEFT_PADDING` | `80px` (macOS), `12px` (others) | Left padding for content | --- # Badge Source: /component/badge A versatile badge component that can display counts, dots, or icons on elements. Perfect for indicating notifications, status, or other contextual information on avatars, icons, or other UI elements. ## Import ```rust use gpui_kit::component::badge::Badge; ``` ## Usage ### Icon - Default: Displays a numeric count. - Dot: A small dot indicator, typically used for status. - Icon: Displays an icon instead of a number. ```rust // Number badge (default) Badge::new() .count(5) .child(Avatar::new().src("https://example.com/avatar.jpg")) // Dot badge Badge::new() .dot() .child(Icon::new(IconName::Inbox)) // Icon badge Badge::new() .icon(IconName::Check) .child(Avatar::new().src("https://example.com/avatar.jpg")) ``` ### Count Use `count` to display a numeric badge, if the count is greater than zero (`> 0`) the badge will be shown, otherwise it will be hidden. There is a default maximum count of `99`, any count above this will be displayed as `99+`. You can customize this maximum using the [max](https://docs.rs/gpui-component/latest/gpui_component/badge/struct.Badge.html#method.max) method. ```rust Badge::new() .count(3) .child(Icon::new(IconName::Bell)) ``` ### Badge icon The Badge is also implemented with the [Sizable] trait, allowing you to set small, medium (default), or large sizes. ```rust // Small badge Badge::new() .small() .count(1) .child(Avatar::new().small()) // Medium badge (default) Badge::new() .count(5) .child(Avatar::new()) // Large badge Badge::new() .large() .count(10) .child(Avatar::new().large()) ``` ### Dot ```rust use gpui_kit::component::ActiveTheme; // Custom colors Badge::new() .count(3) .color(cx.theme().blue) .child(Avatar::new()) Badge::new() .icon(IconName::Star) .color(cx.theme().yellow) .child(Avatar::new()) Badge::new() .dot() .color(cx.theme().green) .child(Icon::new(IconName::Bell)) ``` ### Color ```rust use gpui_kit::component::{Icon, IconName}; // Badge with count on icon Badge::new() .count(3) .child(Icon::new(IconName::Bell).large()) // Badge with high count (shows max) Badge::new() .count(103) .child(Icon::new(IconName::Inbox).large()) // Custom max count Badge::new() .count(150) .max(999) .child(Icon::new(IconName::Mail)) ``` ### Sizes ```rust use gpui_kit::component::avatar::Avatar; // Basic count badge Badge::new() .count(5) .child(Avatar::new().src("https://example.com/avatar.jpg")) // Status badge with icon Badge::new() .icon(IconName::Check) .color(cx.theme().green) .child(Avatar::new().src("https://example.com/avatar.jpg")) // Online indicator with dot Badge::new() .dot() .color(cx.theme().green) .child(Avatar::new().src("https://example.com/avatar.jpg")) ``` ### Complex Nested Badges ```rust // Badge on badge for complex status Badge::new() .count(212) .large() .child( Badge::new() .icon(IconName::Check) .large() .color(cx.theme().cyan) .child(Avatar::new().large().src("https://example.com/avatar.jpg")) ) // Multiple status indicators Badge::new() .count(2) .color(cx.theme().green) .large() .child( Badge::new() .icon(IconName::Star) .large() .color(cx.theme().yellow) .child(Avatar::new().large().src("https://example.com/avatar.jpg")) ) ``` ### Notification Indicators ```rust // Unread messages Badge::new() .count(12) .child(Icon::new(IconName::Mail).large()) // New notifications Badge::new() .count(3) .color(cx.theme().red) .child(Icon::new(IconName::Bell).large()) // High priority with custom max Badge::new() .count(1234) .max(999) .color(cx.theme().orange) .child(Icon::new(IconName::AlertTriangle)) ``` ### Status Indicators ```rust // Online status Badge::new() .dot() .color(cx.theme().green) .child(Avatar::new().src("https://example.com/user.jpg")) // Verified status Badge::new() .icon(IconName::CheckCircle) .color(cx.theme().blue) .child(Avatar::new().src("https://example.com/verified-user.jpg")) // Warning status Badge::new() .icon(IconName::AlertTriangle) .color(cx.theme().yellow) .child(Avatar::new().src("https://example.com/user.jpg")) ``` ### Different Badge Positions ```rust // The badge automatically positions itself based on variant: // - Dot: top-right corner (small dot) // - Number: top-right with dynamic sizing // - Icon: bottom-right corner with border ``` ### Count Formatting ```rust // Numbers 1-99 show as-is Badge::new().count(5) // Shows "5" Badge::new().count(99) // Shows "99" // Numbers above max show with "+" Badge::new().count(100) // Shows "99+" (default max) Badge::new().count(1000).max(999) // Shows "999+" // Zero count hides the badge Badge::new().count(0) // Badge not visible ``` [Badge]: https://docs.rs/gpui_component/latest/gpui_component/badge/struct.Badge.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html ## API Reference - [Badge] --- # GroupBox Source: /component/group-box The GroupBox component is a versatile container that groups related content together with optional borders, backgrounds, and titles. It provides visual organization and semantic grouping for form controls, settings panels, and other related UI elements. ## Import ```rust use gpui_kit::component::group_box::{GroupBox, GroupBoxVariant, GroupBoxVariants as _}; ``` ## Usage ### Account Settings ```rust GroupBox::new() .outline() .title("Display Settings") .child( v_flex() .gap_3() .child( h_flex() .justify_between() .child(Label::new("Theme")) .child( RadioGroup::horizontal("theme") .child(Radio::new("light").label("Light")) .child(Radio::new("dark").label("Dark")) .child(Radio::new("auto").label("Auto")) ) ) .child( h_flex() .justify_between() .child(Label::new("Font Size")) .child( Select::new("font-size") .option("small", "Small") .option("medium", "Medium") .option("large", "Large") ) ) ) ``` ### Preferences ```rust GroupBox::new() .child("Subscriptions") .child(Checkbox::new("all").label("All")) .child(Checkbox::new("newsletter").label("Newsletter")) .child(Button::new("save").primary().label("Save")) ``` ### GroupBox Variants ```rust // Normal variant (default) - no background or border GroupBox::new() .child("Content without visual container") // Fill variant - with background color GroupBox::new() .fill() .title("Settings") .child("Content with background") // Outline variant - with border, no background GroupBox::new() .outline() .title("Preferences") .child("Content with border") ``` ### With Title ```rust GroupBox::new() .fill() .title("Account Settings") .child( h_flex() .justify_between() .child("Make profile private") .child(Switch::new("privacy").checked(false)) ) .child(Button::new("save").primary().label("Save Changes")) ``` ### Footer outside the surface Use `footer` for supporting content below the filled background or outline, not inside the content area. It shares the title's leading edge, sits 8 px under the surface, and renders as small muted text like a description, so plain text is enough. `content_style` only changes the body. ```rust GroupBox::new() .fill() .child("Update preferences") .footer("Changes apply to this device only.") ``` ### Custom ID ```rust GroupBox::new() .id("user-preferences") .outline() .title("User Preferences") .child("Preference controls...") ``` ### Custom Title Styling ```rust use gpui_kit::{StyleRefinement, relative}; GroupBox::new() .outline() .title("Custom Title") .title_style( StyleRefinement::default() .font_semibold() .line_height(relative(1.0)) .px_3() .text_color(cx.theme().accent) ) .child("Content with custom title styling") ``` ### Custom Content Styling ```rust GroupBox::new() .fill() .title("Custom Content Area") .content_style( StyleRefinement::default() .rounded_xl() .py_3() .px_4() .border_2() .border_color(cx.theme().accent) ) .child("Content with custom styling") ``` ### Complex Example ```rust GroupBox::new() .id("notification-settings") .outline() .bg(cx.theme().group_box) .rounded_xl() .p_5() .title("Notification Preferences") .title_style( StyleRefinement::default() .font_semibold() .line_height(relative(1.0)) .px_3() ) .content_style( StyleRefinement::default() .rounded_xl() .py_3() .px_4() .border_2() ) .child( v_flex() .gap_3() .child( h_flex() .justify_between() .child("Email notifications") .child(Switch::new("email").checked(true)) ) .child( h_flex() .justify_between() .child("Push notifications") .child(Switch::new("push").checked(false)) ) .child( h_flex() .justify_between() .child("SMS notifications") .child(Switch::new("sms").checked(false)) ) ) .child( h_flex() .justify_end() .gap_2() .child(Button::new("cancel").label("Cancel")) .child(Button::new("save").primary().label("Save Settings")) ) ``` ### Form Section ```rust GroupBox::new() .fill() .title("Personal Information") .child( v_flex() .gap_4() .child( h_flex() .gap_2() .child(Input::new("first-name").placeholder("First Name")) .child(Input::new("last-name").placeholder("Last Name")) ) .child(Input::new("email").placeholder("Email Address")) .child( h_flex() .justify_end() .child(Button::new("update").primary().label("Update Profile")) ) ) ``` ### Subscription Management ```rust GroupBox::new() .title("Email Subscriptions") .child( v_flex() .gap_2() .child(Checkbox::new("newsletter").label("Weekly Newsletter")) .child(Checkbox::new("updates").label("Product Updates")) .child(Checkbox::new("security").label("Security Alerts")) .child(Checkbox::new("marketing").label("Marketing Communications")) ) .child( h_flex() .justify_between() .mt_4() .child(Button::new("unsubscribe-all").link().label("Unsubscribe All")) .child(Button::new("save").primary().label("Update Preferences")) ) ``` ### Without Title ```rust GroupBox::new() .outline() .child( h_flex() .justify_between() .items_center() .child("Enable two-factor authentication") .child(Switch::new("2fa").checked(false)) ) ``` ## Styling The GroupBox component supports extensive customization through both built-in variants and custom styling: ### Theme Integration ```rust // Using theme colors GroupBox::new() .fill() .bg(cx.theme().group_box) .title("Themed Group Box") ``` ### Custom Appearance ```rust GroupBox::new() .outline() .border_2() .border_color(cx.theme().accent) .rounded(cx.theme().radius_lg) .title("Custom Styled Group Box") .title_style( StyleRefinement::default() .text_color(cx.theme().accent) .font_bold() ) ``` ## Best Practices 1. **Use titles for clarity** - Always include a descriptive title when grouping form controls 2. **Choose appropriate variants** - Use `fill()` for primary content groups, `outline()` for secondary groupings 3. **Maintain visual hierarchy** - Use GroupBox to create clear sections without overwhelming the interface 4. **Group related content** - Only group logically related controls and information 5. **Consider spacing** - The component automatically handles internal spacing, but consider external margins 6. **Responsive design** - GroupBox adapts well to different screen sizes and container widths ## Related Components - **Form**: Use GroupBox within forms to organize sections - **Dialog**: GroupBox works well within dialogs for organizing content - **Accordion**: For collapsible grouped content, consider using Accordion instead - **Card**: For elevated content containers with more visual weight --- # Table Source: /component/table A simple, stateless, composable table component for rendering tabular data. Unlike [DataTable], this component does not include virtual scrolling, sorting, or column management — it is designed for straightforward data display using a declarative API. ## Import ```rust use gpui_kit::component::table::{ Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption, }; ``` ## Usage ### Basic table ```rust Table::new() .child(TableHeader::new().child( TableRow::new() .child(TableHead::new().child("Name")) .child(TableHead::new().child("Email")) .child(TableHead::new().text_right().child("Amount")) )) .child(TableBody::new() .child(TableRow::new() .child(TableCell::new().child("John")) .child(TableCell::new().child("john@example.com")) .child(TableCell::new().text_right().child("$100.00"))) .child(TableRow::new() .child(TableCell::new().child("Jane")) .child(TableCell::new().child("jane@example.com")) .child(TableCell::new().text_right().child("$200.00"))) ) .child(TableCaption::new().child("A list of recent invoices.")) ``` ### Striped, bordered and sizes ```rust Table::new() .child(TableHeader::new().child( TableRow::new() .child(TableHead::new().child("Invoice")) .child(TableHead::new().child("Status")) .child(TableHead::new().text_right().child("Amount")) )) .child(TableBody::new() .child(TableRow::new() .child(TableCell::new().child("INV001")) .child(TableCell::new().child("Paid")) .child(TableCell::new().text_right().child("$250.00"))) ) .child(TableFooter::new().child( TableRow::new() .child(TableCell::new().child("Total")) .child(TableCell::new().child("")) .child(TableCell::new().text_right().child("$250.00")) )) ``` ### Column widths Use `.w()` on `TableHead` and `TableCell` to set fixed column widths: ```rust TableRow::new() .child(TableHead::new().w(px(80.)).child("ID")) .child(TableHead::new().child("Name")) // flex-1 .child(TableHead::new().w(px(120.)).child("Date")) ``` ### Text Alignment ```rust // Center-aligned header TableHead::new().text_center().child("Status") // Right-aligned cell (e.g., for numbers) TableCell::new().text_right().child("$1,000.00") ``` ### Without Border (via Styled) All table sub-components implement the `Styled` trait, so you can customize styles directly: ```rust // Remove border and rounded corners Table::new() .border_0() .rounded_none() .child(/* ... */) ``` ### Custom Styling Since all components implement `Styled`, you can apply any GPUI style: ```rust // Custom row hover TableRow::new() .bg(cx.theme().table_even) .child(/* ... */) // Custom cell padding TableCell::new() .px_4() .child("Custom padded content") ``` ## Sub-components | Component | Description | |-----------|-------------| | `Table` | Root container with border, rounded corners, and background | | `TableHeader` | Header section with distinct background and font weight | | `TableBody` | Body section wrapping data rows | | `TableFooter` | Footer section with top border | | `TableRow` | A flex row with bottom border | | `TableHead` | Header cell with alignment and width options | | `TableCell` | Data cell with alignment and width options | | `TableCaption` | Caption text below the table | ## Table vs DataTable | Feature | Table | DataTable | |---------|-------|-----------| | Virtual scrolling | No | Yes | | Column sorting | No | Yes | | Column resizing | No | Yes | | Column moving | No | Yes | | Cell selection | No | Yes | | Row selection | No | Yes | | Infinite loading | No | Yes | | Keyboard navigation | No | Yes | | State management | Stateless | TableState | | Best for | Small, static data | Large, interactive datasets | [DataTable]: ./data-table.md ## API Reference ### Table - `new()` - Create a new table - Implements `Styled`, `ParentElement`, `Sizable`, `RenderOnce` ### TableHead / TableCell - `new()` - Create a new head/cell - `w(width)` - Set fixed width (otherwise flex-1) - `text_center()` - Center-align content - `text_right()` - Right-align content - Implements `Styled`, `ParentElement`, `RenderOnce` ### TableHeader / TableBody / TableFooter / TableRow / TableCaption - `new()` - Create a new instance - Implements `Styled`, `ParentElement`, `RenderOnce` --- # Tabs Source: /component/tabs A tabbed interface component for organizing content into separate sections. Supports multiple variants, sizes, navigation controls, and interactive features like reordering and prefix/suffix elements. ## Import ```rust use gpui_kit::component::tab::{Tab, TabBar}; ``` ## Usage ### Basic Tabs ```rust TabBar::new("tabs") .selected_index(0) .on_click(|selected_index, _, _| { println!("Tab {} selected", selected_index); }) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Settings")) ``` ### Tab Variants #### Default Tabs ```rust TabBar::new("default-tabs") .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) ``` #### Underline Tabs ```rust TabBar::new("underline-tabs") .underline() .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) ``` #### Pill Tabs ```rust TabBar::new("pill-tabs") .pill() .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) ``` #### Outline Tabs ```rust TabBar::new("outline-tabs") .outline() .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) ``` #### Segmented Tabs ```rust use gpui_kit::component::IconName; TabBar::new("segmented-tabs") .segmented() .selected_index(0) .child(IconName::Bot) .child(IconName::Calendar) .child(IconName::Map) .children(vec!["Settings", "About"]) ``` ### Tab Sizes ```rust // Extra Small TabBar::new("tabs").xsmall() .child(Tab::new().label("Small")) // Small TabBar::new("tabs").small() .child(Tab::new().label("Small")) // Medium (default) TabBar::new("tabs") .child(Tab::new().label("Medium")) // Large TabBar::new("tabs").large() .child(Tab::new().label("Large")) ``` ### Tabs with Icons ```rust use gpui_kit::component::{Icon, IconName}; TabBar::new("icon-tabs") .child(Tab::default().icon(IconName::User).with_variant(TabVariant::Tab)) .child(Tab::default().icon(IconName::Settings).with_variant(TabVariant::Tab)) .child(Tab::default().icon(IconName::Mail).with_variant(TabVariant::Tab)) ``` ### Tabs with Prefix and Suffix ```rust use gpui_kit::component::button::Button; use gpui_kit::component::{h_flex, IconName}; TabBar::new("tabs-with-controls") .prefix( h_flex() .gap_1() .child(Button::new("back").ghost().xsmall().icon(IconName::ArrowLeft)) .child(Button::new("forward").ghost().xsmall().icon(IconName::ArrowRight)) ) .suffix( h_flex() .gap_1() .child(Button::new("inbox").ghost().xsmall().icon(IconName::Inbox)) .child(Button::new("more").ghost().xsmall().icon(IconName::Ellipsis)) ) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Settings")) ``` ### Disabled Tabs ```rust TabBar::new("tabs-with-disabled") .child(Tab::new().label("Account")) .child(Tab::new().label("Profile").disabled(true)) .child(Tab::new().label("Settings")) ``` ### Dynamic Tabs ```rust struct TabsView { active_tab: usize, tabs: Vec, } impl Render for TabsView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { TabBar::new("dynamic-tabs") .selected_index(self.active_tab) .on_click(cx.listener(|view, index, _, cx| { view.active_tab = *index; cx.notify(); })) .children( self.tabs .iter() .map(|tab_name| Tab::new().label(tab_name.clone())) ) } } ``` ### Tabs with Menu Use `menu` option to enable a dropdown menu for tab selection when there are many tabs, this is default `false`. If enable, the will have a dropdown button at the end of the tab bar to show all tabs in a menu. ```rust TabBar::new("tabs-with-menu") .menu(true) .selected_index(0) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Documents")) .child(Tab::new().label("Mail")) .child(Tab::new().label("Settings")) ``` ### Maximum Tab Width Use `max_width` to cap the width of each tab. For tabs created with `.label()`, text longer than the limit is truncated with an ellipsis automatically. Icon-only tabs (`.icon()`) are not affected. Prefix and suffix elements (e.g. a close button) are never truncated — the label yields space first. The *more* menu (when enabled) still shows the full label text. Tabs built from custom children (via `.child()`) are only given the width limit; add `truncate()` yourself to the part that should shrink. ```rust use gpui_kit::{div, px}; TabBar::new("tabs-with-max-width") .max_width(px(100.)) .menu(true) .selected_index(0) .child(Tab::new().label("Account Settings & Preferences")) .child(Tab::new().label("Documents & Files")) .child(Tab::new().label("Appearance & Themes")) .child( Tab::new().child( h_flex() .gap_1() .child(Icon::new(IconName::Bot)) .child(div().truncate().child("Custom Child Tab")), ), ) ``` ### Scrollable Tabs ```rust use gpui_kit::ScrollHandle; struct ScrollableTabsView { scroll_handle: ScrollHandle, } impl Render for ScrollableTabsView { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { TabBar::new("scrollable-tabs") .track_scroll(&self.scroll_handle) .child(Tab::new().label("Very Long Tab Name 1")) .child(Tab::new().label("Very Long Tab Name 2")) .child(Tab::new().label("Very Long Tab Name 3")) .child(Tab::new().label("Very Long Tab Name 4")) .child(Tab::new().label("Very Long Tab Name 5")) } } ``` ### Individual Tab Configuration ```rust TabBar::new("custom-tabs") .child( Tab::new().label("Custom Tab") .id("custom-id") .prefix(IconName::Star) .suffix(IconName::X) .on_click(|_, _, _| { println!("Custom tab clicked"); }) ) ``` ## Advanced Examples ### Custom Tab Content ```rust Tab::empty() .child( h_flex() .items_center() .gap_2() .child(IconName::Folder) .child("Documents") .child( div() .px_1() .py_0p5() .text_xs() .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .rounded(cx.theme().radius.half()) .child("12") ) ) ``` ### Tabs with State Management ```rust struct TabsWithContent { active_tab: usize, tab_contents: Vec, } impl TabsWithContent { fn render_tab_content(&self, cx: &mut Context) -> impl IntoElement { match self.active_tab { 0 => div().child("Account content"), 1 => div().child("Profile content"), 2 => div().child("Settings content"), _ => div().child("Unknown content"), } } } impl Render for TabsWithContent { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .child( TabBar::new("content-tabs") .selected_index(self.active_tab) .on_click(cx.listener(|view, index, _, cx| { view.active_tab = *index; cx.notify(); })) .child(Tab::new().label("Account")) .child(Tab::new().label("Profile")) .child(Tab::new().label("Settings")) ) .child( div() .flex_1() .p_4() .child(self.render_tab_content(cx)) ) } } ``` ### Tabs with Close Buttons While the basic Tab component doesn't include closeable functionality, you can create closeable tabs using suffix elements: ```rust struct CloseableTabsView { tabs: Vec, active_tab: usize, } impl CloseableTabsView { fn close_tab(&mut self, index: usize, cx: &mut Context) { if self.tabs.len() > 1 { self.tabs.remove(index); if self.active_tab >= index && self.active_tab > 0 { self.active_tab -= 1; } cx.notify(); } } } impl Render for CloseableTabsView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { TabBar::new("closeable-tabs") .selected_index(self.active_tab) .on_click(cx.listener(|view, index, _, cx| { view.active_tab = *index; cx.notify(); })) .children( self.tabs .iter() .enumerate() .map(|(index, tab_name)| { Tab::new().label(tab_name.clone()) .suffix( Button::new(format!("close-{}", index)) .icon(IconName::X) .ghost() .xsmall() .on_click(cx.listener(move |view, _, _, cx| { view.close_tab(index, cx); })) ) }) ) } } ``` ## Notes - The `TabBar` manages the selection state of all child tabs - Individual tab `on_click` handlers are ignored when `TabBar.on_click` is set - Tabs automatically inherit the variant and size from their parent `TabBar` - The `with_menu` option adds a dropdown for tab selection when there are many tabs - Scrolling is automatically enabled when tabs overflow the container width - The dock system provides advanced closeable tab functionality for complex layouts ## API Reference ### TabBar | Method | Description | | --------------------------- | -------------------------------------------------- | | `new(id)` | Create a new tab bar with the given ID | | `child(tab)` | Add a tab to the bar | | `children(tabs)` | Add multiple tabs to the bar | | `selected_index(index)` | Set the active tab index | | `on_click(fn)` | Callback when a tab is clicked, receives tab index | | `prefix(element)` | Add element before the tabs | | `suffix(element)` | Add element after the tabs | | `last_empty_space(element)` | Custom element for empty space at the end | | `track_scroll(handle)` | Enable scrolling with a scroll handle | | `with_menu(bool)` | Enable dropdown menu for tab selection | | `max_width(width)` | Set maximum width of each tab; truncates | ### TabBar Variants | Method | Description | | ----------------------- | ------------------------------------ | | `with_variant(variant)` | Set the tab variant for all children | | `underline()` | Use underline variant | | `pill()` | Use pill variant | | `outline()` | Use outline variant | | `segmented()` | Use segmented variant | ### Tab | Method | Description | | ----------------------- | ---------------------------------------------- | | `new(label)` | Create a new tab with a label | | `empty()` | Create an empty tab | | `icon(icon)` | Create a tab with only an icon | | `id(id)` | Set custom ID for the tab | | `with_variant(variant)` | Set the tab variant | | `pill()` | Use pill variant | | `outline()` | Use outline variant | | `segmented()` | Use segmented variant | | `underline()` | Use underline variant | | `prefix(element)` | Add element before tab content | | `suffix(element)` | Add element after tab content | | `disabled(bool)` | Set disabled state | | `selected(bool)` | Set selected state (usually handled by TabBar) | | `on_click(fn)` | Custom click handler for individual tab | ### TabVariant ```rust pub enum TabVariant { Tab, // Default bordered tabs Outline, // Rounded outline tabs Pill, // Rounded pill-shaped tabs Segmented, // Segmented control style Underline, // Underline indicator tabs } ``` ### Styling Both `TabBar` and `Tab` implement `Sizable` trait: - `xsmall()` - Extra small size - `small()` - Small size - `medium()` - Medium size (default) - `large()` - Large size --- # Calendar Source: /component/calendar A standalone calendar component that provides a rich interface for date selection and navigation. The Calendar component supports single date selection, date range selection, multiple month views, custom disabled dates, and comprehensive keyboard navigation. - [CalendarState]: For managing calendar state and selection. - [Calendar]: For rendering the calendar UI. ## Import ```rust use gpui_kit::component::{ calendar::{Calendar, CalendarState, CalendarEvent, Date, Matcher}, }; ``` ## Usage ### Calendar ```rust let state = cx.new(|cx| CalendarState::new(window, cx)); Calendar::new(&state) ``` ### Two columns ```rust use chrono::Local; let state = cx.new(|cx| { let mut state = CalendarState::new(window, cx); state.set_date(Local::now().naive_local().date(), window, cx); state }); Calendar::new(&state) ``` ### Three columns ```rust use chrono::{Local, Days}; let state = cx.new(|cx| { let mut state = CalendarState::new(window, cx); let now = Local::now().naive_local().date(); state.set_date( Date::Range(Some(now), now.checked_add_days(Days::new(7))), window, cx ); state }); Calendar::new(&state) ``` ### Multiple Months Display ```rust // Show 2 months side by side Calendar::new(&state) .number_of_months(2) // Show 3 months Calendar::new(&state) .number_of_months(3) ``` ### Calendar Sizes ```rust Calendar::new(&state).large() Calendar::new(&state) // medium (default) Calendar::new(&state).small() ``` ### Event Planning Calendar ```rust let event_calendar = cx.new(|cx| { let mut state = CalendarState::new(window, cx); // Disable past dates and weekends state = state.disabled_matcher(Matcher::custom(|date| { let now = Local::now().naive_local().date(); *date < now || matches!(date.weekday(), Weekday::Sat | Weekday::Sun) })); state }); Calendar::new(&event_calendar) .large() // Easier to see and interact with ``` ### Vacation Booking Calendar ```rust let vacation_calendar = cx.new(|cx| { let mut state = CalendarState::new(window, cx); state.set_date(Date::Range(None, None), window, cx); // Range mode state }); Calendar::new(&vacation_calendar) .number_of_months(2) // Show 2 months for range selection ``` ### Report Date Range Selector ```rust let report_calendar = cx.new(|cx| { let mut state = CalendarState::new(window, cx) .year_range((2020, 2025)); // Limit to business years state.set_date(Date::Range(None, None), window, cx); state }); Calendar::new(&report_calendar) .number_of_months(3) .small() // Compact for dashboard use ``` ### Availability Calendar ```rust use std::collections::HashSet; let unavailable_dates: HashSet = get_unavailable_dates(); let availability_calendar = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(move |date| { unavailable_dates.contains(date) })) }); Calendar::new(&availability_calendar) .number_of_months(2) ``` The Calendar component provides a foundation for any date-related UI requirements, from simple date pickers to complex scheduling interfaces. [Calendar]: https://docs.rs/gpui-component/latest/gpui_component/calendar/struct.Calendar.html [CalendarState]: https://docs.rs/gpui-component/latest/gpui_component/calendar/struct.CalendarState.html [RangeMatcher]: https://docs.rs/gpui-component/latest/gpui_component/calendar/struct.RangeMatcher.html ## Date Restrictions ### Disabled Weekends ```rust let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(vec![0, 6]) // Sunday=0, Saturday=6 }); Calendar::new(&state) ``` ### Disabled Specific Weekdays ```rust // Disable Sundays, Wednesdays, and Saturdays let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(vec![0, 3, 6]) }); Calendar::new(&state) ``` ### Disabled Date Range ```rust use chrono::{Local, Days}; let now = Local::now().naive_local().date(); let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::range( Some(now), now.checked_add_days(Days::new(7)), )) }); Calendar::new(&state) ``` ### Disabled Date Interval ```rust // Disable dates outside the interval (before/after specified dates) let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::interval( Some(now.checked_sub_days(Days::new(30)).unwrap()), now.checked_add_days(Days::new(30)) )) }); Calendar::new(&state) ``` ### Custom Disabled Dates ```rust // Disable first 5 days of each month let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(|date| { date.day0() < 5 // day0() returns 0-based day })) }); Calendar::new(&state) // Disable all Mondays let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(|date| { date.weekday() == chrono::Weekday::Mon })) }); Calendar::new(&state) // Disable past dates let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(|date| { *date < Local::now().naive_local().date() })) }); Calendar::new(&state) ``` ## Month/Year Navigation The Calendar automatically provides navigation controls: - **Previous/Next Month**: Arrow buttons in the header - **Month Selection**: Click on month name to open month picker - **Year Selection**: Click on year to open year picker - **Year Pages**: Navigate through 20-year pages in year view ### Custom Year Range ```rust let state = cx.new(|cx| { CalendarState::new(window, cx) .year_range((2020, 2030)) // Limit to specific year range }); Calendar::new(&state) ``` ## Handle Selection Events ```rust let state = cx.new(|cx| CalendarState::new(window, cx)); cx.subscribe(&state, |view, _, event, _| { match event { CalendarEvent::Selected(date) => { match date { Date::Single(Some(selected_date)) => { println!("Date selected: {}", selected_date); } Date::Range(Some(start), Some(end)) => { println!("Range selected: {} to {}", start, end); } Date::Range(Some(start), None) => { println!("Range start: {}", start); } _ => { println!("Selection cleared"); } } } } }); Calendar::new(&state) ``` ## Advanced Examples ### Business Days Only Calendar ```rust use chrono::Weekday; let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(|date| { matches!(date.weekday(), Weekday::Sat | Weekday::Sun) })) }); Calendar::new(&state) ``` ### Holiday Calendar ```rust use chrono::NaiveDate; use std::collections::HashSet; // Define holidays let holidays: HashSet = [ NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(), // New Year NaiveDate::from_ymd_opt(2024, 7, 4).unwrap(), // Independence Day NaiveDate::from_ymd_opt(2024, 12, 25).unwrap(), // Christmas ].into_iter().collect(); let state = cx.new(|cx| { CalendarState::new(window, cx) .disabled_matcher(Matcher::custom(move |date| { holidays.contains(date) })) }); Calendar::new(&state) ``` ### Multi-Month Range Selector ```rust let state = cx.new(|cx| { let mut state = CalendarState::new(window, cx); state.set_date(Date::Range(None, None), window, cx); // Range mode state }); Calendar::new(&state) .number_of_months(3) // Show 3 months for easier range selection ``` ### Quarterly View Calendar ```rust let state = cx.new(|cx| CalendarState::new(window, cx)); // Update to show current quarter's months Calendar::new(&state) .number_of_months(3) ``` ## Custom Styling ```rust use gpui_kit::{px, relative}; Calendar::new(&calendar) .p_4() // Custom padding .bg(cx.theme().secondary) // Custom background .border_2() // Custom border .border_color(cx.theme().primary) // Custom border color .rounded(px(12.)) // Custom border radius .w(px(400.)) // Custom width .h(px(350.)) // Custom height ``` ## API Reference - [Calendar] - [CalendarState] - [RangeMatcher] --- # List Source: /component/list A powerful List component that provides a virtualized, searchable list interface with support for sections, headers, footers, selection states, and infinite scrolling. The component is built on a delegate pattern that allows for flexible data management and custom item rendering. ## Import ```rust use gpui_kit::component::list::{List, ListState, ListDelegate, ListItem, ListEvent, ListSeparatorItem}; use gpui_kit::component::IndexPath; ``` ## Usage ### Basic List To create a list, you need to implement the `ListDelegate` trait for your data: ```rust struct MyListDelegate { items: Vec, selected_index: Option, } impl ListDelegate for MyListDelegate { type Item = ListItem; fn items_count(&self, _section: usize, _cx: &App) -> usize { self.items.len() } fn render_item( &mut self, ix: IndexPath, _window: &mut Window, _cx: &mut Context>, ) -> Option { self.items.get(ix.row).map(|item| { ListItem::new(ix) .child(Label::new(item.clone())) .selected(Some(ix) == self.selected_index) }) } fn set_selected_index( &mut self, ix: Option, _window: &mut Window, cx: &mut Context>, ) { self.selected_index = ix; cx.notify(); } } // Create the list let delegate = MyListDelegate { items: vec!["Item 1".into(), "Item 2".into(), "Item 3".into()], selected_index: None, }; /// Create a list state. let state = cx.new(|cx| ListState::new(delegate, window, cx)); ``` Now use [List] to render list: ```rs div().child(List::new(&state)) ``` ### List with Sections **Note:** Sections with `items_count` of 0 will be automatically hidden (no header or footer will be rendered for empty sections). ```rust impl ListDelegate for MyListDelegate { type Item = ListItem; fn sections_count(&self, _cx: &App) -> usize { 3 // Number of sections } fn items_count(&self, section: usize, _cx: &App) -> usize { match section { 0 => 5, 1 => 3, 2 => 7, _ => 0, } } fn render_section_header( &mut self, section: usize, _window: &mut Window, cx: &mut Context>, ) -> Option { let title = match section { 0 => "Section 1", 1 => "Section 2", 2 => "Section 3", _ => return None, }; Some( h_flex() .px_2() .py_1() .gap_2() .text_sm() .text_color(cx.theme().muted_foreground) .child(Icon::new(IconName::Folder)) .child(title) ) } fn render_section_footer( &mut self, section: usize, _window: &mut Window, cx: &mut Context>, ) -> Option { Some( div() .px_2() .py_1() .text_xs() .text_color(cx.theme().muted_foreground) .child(format!("End of section {}", section + 1)) ) } } ``` ### List Items with Icons and Actions ```rust fn render_item( &mut self, ix: IndexPath, _window: &mut Window, cx: &mut Context>, ) -> Option { self.items.get(ix.row).map(|item| { ListItem::new(ix) .child( h_flex() .items_center() .gap_2() .child(Icon::new(IconName::File)) .child(Label::new(item.title.clone())) ) .suffix(|_, _| { Button::new("action") .ghost() .small() .icon(IconName::MoreHorizontal) }) .selected(Some(ix) == self.selected_index) .on_click(cx.listener(move |this, _, window, cx| { this.delegate_mut().select_item(ix, window, cx); })) }) } ``` ### List with Search The list automatically includes a search input by default. Implement `perform_search` to handle queries: And you should use `searchable(true)` when creating the `ListState` to show search input. ```rust impl ListDelegate for MyListDelegate { fn perform_search( &mut self, query: &str, _window: &mut Window, _cx: &mut Context>, ) -> Task<()> { // Filter items based on query self.filtered_items = self.all_items .iter() .filter(|item| item.to_lowercase().contains(&query.to_lowercase())) .cloned() .collect(); Task::ready(()) } } let state = cx.new(|cx| ListState::new(delegate, window, cx).searchable(true)); List::new(&state) ``` ### List with Loading State ```rust impl ListDelegate for MyListDelegate { fn loading(&self, _cx: &App) -> bool { self.is_loading } fn render_loading( &mut self, _window: &mut Window, _cx: &mut Context>, ) -> impl IntoElement { // Custom loading view v_flex() .justify_center() .items_center() .py_4() .child(Skeleton::new().h_4().w_full()) .child(Skeleton::new().h_4().w_3_4()) } } ``` ### Infinite Scrolling ```rust impl ListDelegate for MyListDelegate { fn has_more(&self, _cx: &App) -> bool { self.has_more_data } fn load_more_threshold(&self) -> usize { 20 // Trigger when 20 items from bottom } fn load_more(&mut self, window: &mut Window, cx: &mut Context>) { if self.is_loading { return; } self.is_loading = true; cx.spawn_in(window, async move |view, window| { // Simulate API call Timer::after(Duration::from_secs(1)).await; view.update_in(window, |view, _, cx| { // Add more items view.delegate_mut().load_more_items(); view.delegate_mut().is_loading = false; cx.notify(); }); }).detach(); } } ``` ### List Events ```rust // Subscribe to list events let _subscription = cx.subscribe(&state, |_, _, event: &ListEvent, _| { match event { ListEvent::Select(ix) => { println!("Item selected at: {:?}", ix); } ListEvent::Confirm(ix) => { println!("Item confirmed at: {:?}", ix); } ListEvent::Cancel => { println!("Selection cancelled"); } } }); ``` ### Different Item Styles ```rust // Basic item with hover effect ListItem::new(ix) .child(Label::new("Basic Item")) .selected(is_selected) // Item with check icon ListItem::new(ix) .child(Label::new("Checkable Item")) .check_icon(IconName::Check) .confirmed(is_confirmed) // Disabled item ListItem::new(ix) .child(Label::new("Disabled Item")) .disabled(true) // Separator item ListSeparatorItem::new() .child( div() .h_px() .w_full() .bg(cx.theme().border) ) ``` ### Drag and Drop Reordering `ListItem` implements GPUI's `InteractiveElement` and `StatefulInteractiveElement` traits, so all native interaction APIs such as `on_drag`, `on_drop`, `drag_over` and `on_hover` are directly available: ```rust #[derive(Clone)] struct DragItem { ix: IndexPath, name: SharedString, } impl Render for DragItem { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { // The preview element that follows the cursor while dragging. div() .px_2() .py_1() .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .rounded(cx.theme().radius) .child(self.name.clone()) } } // In `render_item` of your `ListDelegate`: ListItem::new(ix) .child(Label::new(item.name.clone())) .on_drag(DragItem { ix, name: item.name.clone() }, |drag, _, _, cx| { cx.new(|_| drag.clone()) }) .drag_over::(|style, _, _, cx| style.bg(cx.theme().drop_target)) .on_drop(cx.listener(move |this, drag: &DragItem, _, cx| { this.delegate_mut().move_item(drag.ix, ix); cx.notify(); })) ``` ### Custom Empty State ```rust impl ListDelegate for MyListDelegate { fn render_empty(&mut self, _window: &mut Window, cx: &mut Context>) -> impl IntoElement { v_flex() .size_full() .justify_center() .items_center() .gap_2() .child(Icon::new(IconName::Search).size_16().text_color(cx.theme().muted_foreground)) .child( Label::new("No items found") .text_color(cx.theme().muted_foreground) ) .child( Label::new("Try adjusting your search terms") .text_sm() .text_color(cx.theme().muted_foreground.opacity(0.7)) ) } } ``` ### File Browser List ```rust struct FileBrowserDelegate { files: Vec, selected: Option, } #[derive(Clone)] struct FileInfo { name: String, is_directory: bool, size: Option, } impl ListDelegate for FileBrowserDelegate { type Item = ListItem; fn render_item(&mut self, ix: IndexPath, window: &mut Window, cx: &mut Context>) -> Option { self.files.get(ix.row).map(|file| { let icon = if file.is_directory { IconName::Folder } else { IconName::File }; ListItem::new(ix) .child( h_flex() .items_center() .justify_between() .w_full() .child( h_flex() .items_center() .gap_2() .child(Icon::new(icon)) .child(Label::new(file.name.clone())) ) .when_some(file.size, |this, size| { this.child( Label::new(format_size(size)) .text_sm() .text_color(cx.theme().muted_foreground) ) }) ) .selected(Some(ix) == self.selected) }) } } ``` ### Contact List with Sections ```rust struct ContactListDelegate { contacts_by_letter: BTreeMap>, selected: Option, } impl ListDelegate for ContactListDelegate { type Item = ListItem; fn sections_count(&self, _cx: &App) -> usize { self.contacts_by_letter.len() } fn render_section_header(&mut self, section: usize, _window: &mut Window, cx: &mut Context>) -> Option { let letter = self.contacts_by_letter.keys().nth(section)?; Some( div() .px_3() .py_2() .bg(cx.theme().background) .border_b_1() .border_color(cx.theme().border) .child( Label::new(letter.to_string()) .text_lg() .text_color(cx.theme().accent_foreground) .font_weight(FontWeight::BOLD) ) ) } } ``` ## Configuration Options ### List Configuration ```rust List::new(&state) .max_h(px(400.)) // Set maximum height .scrollbar_visible(false) // Hide scrollbar .paddings(Edges::all(px(8.))) // Set internal padding ``` ### Scrolling Control ```rust // Scroll to specific item state.update(cx, |state, cx| { state.scroll_to_item( IndexPath::new(0).section(1), // Row 0 of section 1 ScrollStrategy::Center, window, cx, ); }); // Scroll to selected item state.update(cx, |state, cx| { state.scroll_to_selected_item(window, cx); }); // Set selected index without scrolling state.update(cx, |state, cx| { state.set_selected_index(Some(IndexPath::new(5)), window, cx); }); ``` --- # Tree Source: /component/tree A versatile tree component for displaying hierarchical data with expand/collapse functionality, keyboard navigation, and custom item rendering. Perfect for file explorers, navigation menus, or any nested data structure. ## Import ```rust use gpui_kit::component::tree::{tree, TreeState, TreeItem, TreeEntry}; ``` ## Usage ### Basic Tree ```rust // Create tree state let tree_state = cx.new(|cx| { TreeState::new(cx).items(vec![ TreeItem::new("src", "src") .expanded(true) .child(TreeItem::new("src/lib.rs", "lib.rs")) .child(TreeItem::new("src/main.rs", "main.rs")), TreeItem::new("Cargo.toml", "Cargo.toml"), TreeItem::new("README.md", "README.md"), ]) }); // Render tree tree(&tree_state, |ix, entry, selected, window, cx| { ListItem::new(ix) .child( h_flex() .gap_2() .child(entry.item().label.clone()) ) }) ``` ### File Tree with Icons ```rust use gpui_kit::component::{ListItem, IconName, h_flex}; tree(&tree_state, |ix, entry, selected, window, cx| { let item = entry.item(); let icon = if !entry.is_folder() { IconName::File } else if entry.is_expanded() { IconName::FolderOpen } else { IconName::Folder }; ListItem::new(ix) .selected(selected) .pl(px(16.) * entry.depth() + px(12.)) // Indent based on depth .child( h_flex() .gap_2() .child(icon) .child(item.label.clone()) ) .on_click(cx.listener(move |_, _, _, _| { // Handle item click })) }) ``` ### Dynamic Tree Loading ```rust impl MyView { fn load_files(&mut self, path: PathBuf, cx: &mut Context) { let tree_state = self.tree_state.clone(); cx.spawn(async move |cx| { let items = build_file_items(&path).await; tree_state.update(cx, |state, cx| { state.set_items(items, cx); }) }).detach(); } } fn build_file_items(path: &Path) -> Vec { let mut items = Vec::new(); if let Ok(entries) = std::fs::read_dir(path) { for entry in entries.flatten() { let path = entry.path(); let name = path.file_name() .and_then(|n| n.to_str()) .unwrap_or("Unknown") .to_string(); if path.is_dir() { let children = build_file_items(&path); items.push(TreeItem::new(path.to_string_lossy(), name) .children(children)); } else { items.push(TreeItem::new(path.to_string_lossy(), name)); } } } items } ``` ### Tree with Selection Handling ```rust struct MyTreeView { tree_state: Entity, selected_item: Option, } impl MyTreeView { fn handle_selection(&mut self, item: TreeItem, cx: &mut Context) { self.selected_item = Some(item.clone()); println!("Selected: {} ({})", item.label, item.id); cx.notify(); } } // In render method tree(&self.tree_state, { let view = cx.entity(); move |ix, entry, selected, window, cx| { view.update(cx, |this, cx| { ListItem::new(ix) .selected(selected) .child(entry.item().label.clone()) .on_click(cx.listener({ let item = entry.item().clone(); move |this, _, _, cx| { this.handle_selection(item.clone(), cx); } })) }) } }) ``` ### Disabled Items ```rust TreeItem::new("protected", "Protected Folder") .disabled(true) .child(TreeItem::new("secret.txt", "secret.txt")) ``` ### Programmatic Tree Control ```rust // Get current selection if let Some(entry) = tree_state.read(cx).selected_entry() { println!("Current selection: {}", entry.item().label); } // Set selection programmatically (by selected_index) tree_state.update(cx, |state, cx| { state.set_selected_index(Some(2), cx); // Select third item }); // Set selection programmatically (by tree item) tree_state.update(cx, |state, cx| { state.set_selected_item(Some(item), cx); // Select third item }); // Scroll to specific item tree_state.update(cx, |state, _| { state.scroll_to_item(5, gpui_kit::ScrollStrategy::Center); }); // Clear selection (by selected_index) tree_state.update(cx, |state, cx| { state.set_selected_index(None, cx); }); // Clear selection (by tree item) tree_state.update(cx, |state, cx| { state.set_selected_item(None, cx); }); ``` ### Lazy Loading Tree ```rust struct LazyTreeView { tree_state: Entity, loaded_paths: HashSet, } impl LazyTreeView { fn load_children(&mut self, item_id: &str, cx: &mut Context) { if self.loaded_paths.contains(item_id) { return; } let path = PathBuf::from(item_id); if path.is_dir() { let tree_state = self.tree_state.clone(); let item_id = item_id.to_string(); cx.spawn(async move |cx| { let children = load_directory_children(&path).await; tree_state.update(cx, |state, cx| { // Update specific item with loaded children state.update_item_children(&item_id, children, cx); }) }).detach(); self.loaded_paths.insert(item_id.to_string()); } } } ``` ### Search and Filter ```rust struct SearchableTree { tree_state: Entity, original_items: Vec, search_query: String, } impl SearchableTree { fn filter_tree(&mut self, query: &str, cx: &mut Context) { self.search_query = query.to_string(); let filtered_items = if query.is_empty() { self.original_items.clone() } else { filter_tree_items(&self.original_items, query) }; self.tree_state.update(cx, |state, cx| { state.set_items(filtered_items, cx); }); } } fn filter_tree_items(items: &[TreeItem], query: &str) -> Vec { items.iter() .filter_map(|item| { if item.label.to_lowercase().contains(&query.to_lowercase()) { Some(item.clone().expanded(true)) // Auto-expand matches } else { // Check if any children match let filtered_children = filter_tree_items(&item.children, query); if !filtered_children.is_empty() { Some(item.clone() .children(filtered_children) .expanded(true)) } else { None } } }) .collect() } ``` ### Multi-Select Tree ```rust struct MultiSelectTree { tree_state: Entity, selected_items: HashSet, } impl MultiSelectTree { fn toggle_selection(&mut self, item_id: &str, cx: &mut Context) { if self.selected_items.contains(item_id) { self.selected_items.remove(item_id); } else { self.selected_items.insert(item_id.to_string()); } cx.notify(); } fn is_selected(&self, item_id: &str) -> bool { self.selected_items.contains(item_id) } } // In render method tree(&self.tree_state, { let view = cx.entity(); move |ix, entry, _selected, window, cx| { view.update(cx, |this, cx| { let item = entry.item(); let is_multi_selected = this.is_selected(&item.id); ListItem::new(ix) .selected(is_multi_selected) .child( h_flex() .gap_2() .child(checkbox().checked(is_multi_selected)) .child(item.label.clone()) ) .on_click(cx.listener({ let item_id = item.id.clone(); move |this, _, _, cx| { this.toggle_selection(&item_id, cx); } })) }) } }) ``` ## Keyboard Navigation The Tree component supports comprehensive keyboard navigation: | Key | Action | | ------- | ----------------------------------------- | | `↑` | Select previous item | | `↓` | Select next item | | `←` | Collapse current folder or move to parent | | `→` | Expand current folder | | `Enter` | Toggle expand/collapse for folders | | `Space` | Custom action (configurable) | ```rust // Custom keyboard handling tree(&tree_state) .key_context("MyTree") .on_action(cx.listener(|this, action: &MyCustomAction, _, cx| { // Handle custom actions })) ``` ## API Reference ### TreeState | Method | Description | |--------------------------------|----------------------------------| | `new(cx)` | Create a new tree state | | `items(items)` | Set initial tree items | | `set_items(items, cx)` | Update tree items and notify | | `selected_index()` | Get currently selected index | | `set_selected_index(ix, cx)` | Set selected index | | `set_selected_item(item, cx)` | Set selected by tree item | | `selected_item(item, cx)` | Get currently selected tree item | | `selected_entry()` | Get currently selected entry | | `scroll_to_item(ix, strategy)` | Scroll to specific item | ### TreeItem | Method | Description | | ----------------- | -------------------------------------- | | `new(id, label)` | Create new tree item with ID and label | | `child(item)` | Add single child item | | `children(items)` | Add multiple child items | | `expanded(bool)` | Set expanded state | | `disabled(bool)` | Set disabled state | | `is_folder()` | Check if item has children | | `is_expanded()` | Check if item is expanded | | `is_disabled()` | Check if item is disabled | ### TreeEntry | Method | Description | | --------------- | --------------------------- | | `item()` | Get the source TreeItem | | `depth()` | Get item depth in tree | | `is_folder()` | Check if entry has children | | `is_expanded()` | Check if entry is expanded | | `is_disabled()` | Check if entry is disabled | ### tree() Function | Parameter | Description | | ------------- | ------------------------------------- | | `state` | `Entity` for managing tree | | `render_item` | Closure for rendering each item | #### Render Item Closure ```rust Fn(usize, &TreeEntry, bool, &mut Window, &mut App) -> ListItem ``` - `usize`: Item index in flattened tree - `&TreeEntry`: Tree entry with item and metadata - `bool`: Whether item is currently selected - `&mut Window`: Current window context - `&mut App`: Application context - Returns: `ListItem` for rendering --- # DatePicker Source: /component/date-picker A flexible date picker component with calendar interface that supports single date selection, date range selection, custom date formatting, disabled dates, and preset ranges. ## Import ```rust use gpui_kit::component::{ date_picker::{DatePicker, DatePickerState, DateRangePreset, DatePickerEvent, DateTime}, calendar::{Date, Matcher}, time_field::{HourCycle, TimePrecision}, }; ``` ## Usage ### Date ```rust let date_picker = cx.new(|cx| DatePickerState::new(window, cx)); DatePicker::new(&date_picker) ``` ### With Initial Date ```rust use chrono::Local; let date_picker = cx.new(|cx| { let mut picker = DatePickerState::new(window, cx); picker.set_date(Local::now().naive_local().date(), window, cx); picker }); DatePicker::new(&date_picker) ``` ### Date Range Picker ```rust use chrono::{Local, Days}; // Range mode picker let range_picker = cx.new(|cx| DatePickerState::range(window, cx)); DatePicker::new(&range_picker) .number_of_months(2) // Show 2 months for easier range selection // With initial range let range_picker = cx.new(|cx| { let now = Local::now().naive_local().date(); let mut picker = DatePickerState::new(window, cx); picker.set_date( (now, now.checked_add_days(Days::new(7)).unwrap()), window, cx, ); picker }); DatePicker::new(&range_picker) .number_of_months(2) ``` ### Date and Time Set a `time_precision` to edit the time of day as well. The popup then shows a time field below the calendar and stays open after a date is picked, so the time can be adjusted next. Every edit is reported as it happens. Clicking the selected date again — or double-clicking a date — confirms it and closes the popup, as do Enter, Escape and a click outside. ```rust use chrono::NaiveTime; let date_time_picker = cx.new(|cx| { DatePickerState::new(window, cx) .time_precision(TimePrecision::Minute) // or TimePrecision::Second .default_time(NaiveTime::from_hms_opt(9, 0, 0).unwrap()) }); DatePicker::new(&date_time_picker) ``` The time uses a 24-hour clock by default. Use `hour_cycle` for a 12-hour clock with an AM/PM segment: ```rust DatePickerState::new(window, cx) .time_precision(TimePrecision::Minute) .hour_cycle(HourCycle::H12) // 09:30 PM ``` `default_time` is the time a date gets before the user edits it, `00:00` unless configured. The display format follows the precision and hour cycle (for example `%Y/%m/%d %H:%M` or `%Y/%m/%d %I:%M %p`) unless `date_format` is set. Read and write the whole value with `date_time` and `set_date_time`; `date` and `set_date` still address the date part and keep the current time. ```rust use chrono::Local; date_time_picker.update(cx, |state, cx| { state.set_date_time(Local::now().naive_local(), window, cx); }); if let DateTime::Single(Some(at)) = date_time_picker.read(cx).date_time() { println!("Selected {at}"); } ``` A range picker edits dates only, even with a `time_precision`. For a range with times, place two pickers side by side and validate the order in the owner: ```rust h_flex() .gap_2() .child(DatePicker::new(&start_picker)) .child("–") .child(DatePicker::new(&end_picker)) ``` In the time field, Up/Down change the selected segment, Left/Right and Tab/Shift-Tab move between segments, digits type a value and advance to the next segment, `a`/`p` set AM or PM, and Backspace resets the segment. ### With Custom Date Format ```rust let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .date_format("%Y-%m-%d") // ISO format }); DatePicker::new(&date_picker) // Other format examples: // "%m/%d/%Y" -> 12/25/2023 // "%B %d, %Y" -> December 25, 2023 // "%d %b %Y" -> 25 Dec 2023 ``` ### With Placeholder ```rust DatePicker::new(&date_picker) .placeholder("Select a date...") ``` ### Cleanable Date Picker ```rust DatePicker::new(&date_picker) .cleanable(true) // Show clear button when date is selected ``` ### Different Sizes ```rust DatePicker::new(&date_picker).large() DatePicker::new(&date_picker) // medium (default) DatePicker::new(&date_picker).small() ``` ### Disabled State ```rust DatePicker::new(&date_picker).disabled(true) ``` ### Custom Appearance ```rust // Without default styling DatePicker::new(&date_picker).appearance(false) // Use in custom container div() .border_b_2() .px_6() .py_3() .border_color(cx.theme().border) .bg(cx.theme().secondary) .child(DatePicker::new(&date_picker).appearance(false)) ``` ### Event Date Picker ```rust let event_date = cx.new(|cx| { let mut picker = DatePickerState::new(window, cx) .date_format("%B %d, %Y") .disabled_matcher(calendar::Matcher::custom(|date| { // Disable past dates *date < Local::now().naive_local().date() })); picker }); DatePicker::new(&event_date) .placeholder("Choose event date") .cleanable(true) ``` ### Booking System Date Range ```rust let booking_range = cx.new(|cx| DatePickerState::range(window, cx)); let booking_presets = vec![ DateRangePreset::range("This Weekend", /* weekend dates */), DateRangePreset::range("Next Week", /* next week dates */), DateRangePreset::range("This Month", /* this month dates */), ]; DatePicker::new(&booking_range) .number_of_months(2) .presets(booking_presets) .placeholder("Select check-in and check-out dates") ``` ### Financial Period Selector ```rust let financial_period = cx.new(|cx| { DatePickerState::range(window, cx) .date_format("%Y-%m-%d") }); DatePicker::new(&financial_period) .number_of_months(3) .presets(quarterly_presets) .placeholder("Select reporting period") ``` ## Date Restrictions ### Disabled Weekends ```rust use gpui_kit::component::calendar; let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(vec![0, 6]) // Sunday=0, Saturday=6 }); DatePicker::new(&date_picker) ``` ### Disabled Date Range ```rust use chrono::{Local, Days}; let now = Local::now().naive_local().date(); let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::range( Some(now), now.checked_add_days(Days::new(7)), )) }); DatePicker::new(&date_picker) ``` ### Disabled Date Interval ```rust let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::interval( Some(now), now.checked_add_days(Days::new(5)) )) }); DatePicker::new(&date_picker) ``` ### Custom Disabled Dates ```rust // Disable first 5 days of each month let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::custom(|date| { date.day0() < 5 })) }); DatePicker::new(&date_picker) // Disable all Mondays let date_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::custom(|date| { date.weekday() == chrono::Weekday::Mon })) }); ``` ## Custom Year Range By default, the date picker shows 50 years before and after the current year in year selection mode. Use `set_year_range` to configure a different range — for example, a birthday picker that goes back to 1900. The `range` argument uses a **half-open interval** `(start, end)` where `end` is **exclusive**. Pass `(1900, current_year + 1)` to include `current_year`. ```rust use chrono::Datelike; // Birthday picker: allow years from 1900 to the current year (inclusive) let birthday_picker = cx.new(|cx| { let current_year = chrono::Local::now().year(); let mut picker = DatePickerState::new(window, cx) .date_format("%Y-%m-%d"); picker.set_year_range((1900, current_year + 1), window, cx); picker }); DatePicker::new(&birthday_picker) .cleanable(true) .placeholder("Select birthday") ``` `set_year_range` works for both single-date and range-mode pickers. ## Preset Ranges ### Single Date Presets ```rust use chrono::{Utc, Duration}; let presets = vec![ DateRangePreset::single( "Yesterday", (Utc::now() - Duration::days(1)).naive_local().date(), ), DateRangePreset::single( "Last Week", (Utc::now() - Duration::weeks(1)).naive_local().date(), ), DateRangePreset::single( "Last Month", (Utc::now() - Duration::days(30)).naive_local().date(), ), ]; DatePicker::new(&date_picker) .presets(presets) ``` ### Date Range Presets ```rust let range_presets = vec![ DateRangePreset::range( "Last 7 Days", (Utc::now() - Duration::days(7)).naive_local().date(), Utc::now().naive_local().date(), ), DateRangePreset::range( "Last 30 Days", (Utc::now() - Duration::days(30)).naive_local().date(), Utc::now().naive_local().date(), ), DateRangePreset::range( "Last 90 Days", (Utc::now() - Duration::days(90)).naive_local().date(), Utc::now().naive_local().date(), ), ]; DatePicker::new(&date_picker) .number_of_months(2) .presets(range_presets) ``` ## Handle Date Selection Events ```rust let date_picker = cx.new(|cx| DatePickerState::new(window, cx)); cx.subscribe(&date_picker, |view, _, event, _| { match event { DatePickerEvent::Change(value) => { match value.date() { Date::Single(Some(selected_date)) => { println!("Single date selected: {}", selected_date); } Date::Range(Some(start), Some(end)) => { println!("Date range selected: {} to {}", start, end); } Date::Range(Some(start), None) => { println!("Range start selected: {}", start); } _ => { println!("Date cleared"); } } } } }); ``` ## Multiple Months Display ```rust // Show 2 months side by side (useful for date ranges) DatePicker::new(&date_picker) .number_of_months(2) // Show 3 months DatePicker::new(&date_picker) .number_of_months(3) ``` ## Advanced Examples ### Business Days Only ```rust use chrono::Weekday; let business_days_picker = cx.new(|cx| { DatePickerState::new(window, cx) .disabled_matcher(calendar::Matcher::custom(|date| { matches!(date.weekday(), Weekday::Sat | Weekday::Sun) })) }); DatePicker::new(&business_days_picker) .placeholder("Select business day") ``` ### Date Range with Max Duration ```rust use chrono::Days; let max_30_days_picker = cx.new(|cx| DatePickerState::range(window, cx)); cx.subscribe(&max_30_days_picker, |view, picker, event, _| { match event { DatePickerEvent::Change(value) => { let Date::Range(Some(start), Some(end)) = value.date() else { return; }; let duration = end.signed_duration_since(start).num_days(); if duration > 30 { // Reset to start date only if range exceeds 30 days picker.update(cx, |state, cx| { state.set_date(Date::Range(Some(start), None), window, cx); }); } } } }); DatePicker::new(&max_30_days_picker) .number_of_months(2) .placeholder("Select up to 30 days") ``` ### Quarter Presets ```rust use chrono::{NaiveDate, Datelike}; fn quarter_start(year: i32, quarter: u32) -> NaiveDate { let month = (quarter - 1) * 3 + 1; NaiveDate::from_ymd_opt(year, month, 1).unwrap() } fn quarter_end(year: i32, quarter: u32) -> NaiveDate { let month = quarter * 3; let start = NaiveDate::from_ymd_opt(year, month, 1).unwrap(); NaiveDate::from_ymd_opt(year, month, start.days_in_month()).unwrap() } let year = Local::now().year(); let quarterly_presets = vec![ DateRangePreset::range("Q1", quarter_start(year, 1), quarter_end(year, 1)), DateRangePreset::range("Q2", quarter_start(year, 2), quarter_end(year, 2)), DateRangePreset::range("Q3", quarter_start(year, 3), quarter_end(year, 3)), DateRangePreset::range("Q4", quarter_start(year, 4), quarter_end(year, 4)), ]; DatePicker::new(&date_picker) .presets(quarterly_presets) ``` --- # Kbd Source: /component/kbd A component for displaying keyboard shortcuts and key combinations with proper platform-specific formatting. Automatically adapts the display to match the conventions of macOS (using symbols) or Windows/Linux (using text labels). ## Import ```rust use gpui_kit::component::kbd::Kbd; use gpui_kit::Keystroke; ``` ## Usage ### Shortcuts ```rust // Command palette Kbd::new(Keystroke::parse("cmd-shift-p").unwrap()) // New tab Kbd::new(Keystroke::parse("cmd-t").unwrap()) // Zoom controls Kbd::new(Keystroke::parse("cmd--").unwrap()) // Zoom out Kbd::new(Keystroke::parse("cmd-+").unwrap()) // Zoom in // Navigation Kbd::new(Keystroke::parse("escape").unwrap()) Kbd::new(Keystroke::parse("enter").unwrap()) Kbd::new(Keystroke::parse("backspace").unwrap()) ``` ### Basic Keyboard Shortcut ```rust // Create from a keystroke let kbd = Kbd::new(Keystroke::parse("cmd-shift-p").unwrap()); // Or convert directly from keystroke let kbd: Kbd = Keystroke::parse("escape").unwrap().into(); ``` ### Multiple Modifiers ```rust // Complex combinations Kbd::new(Keystroke::parse("cmd-ctrl-shift-a").unwrap()) Kbd::new(Keystroke::parse("cmd-alt-backspace").unwrap()) Kbd::new(Keystroke::parse("ctrl-alt-shift-a").unwrap()) ``` ### Arrow Keys and Function Keys ```rust // Arrow keys Kbd::new(Keystroke::parse("left").unwrap()) Kbd::new(Keystroke::parse("right").unwrap()) Kbd::new(Keystroke::parse("up").unwrap()) Kbd::new(Keystroke::parse("down").unwrap()) // Function keys Kbd::new(Keystroke::parse("f12").unwrap()) Kbd::new(Keystroke::parse("secondary-f12").unwrap()) // Page navigation Kbd::new(Keystroke::parse("pageup").unwrap()) Kbd::new(Keystroke::parse("pagedown").unwrap()) ``` ### Without Visual Styling ```rust // Display only the key text without the styled background Kbd::new(Keystroke::parse("cmd-s").unwrap()) .appearance(false) ``` ### From Action Bindings ```rust use gpui_kit::{Action, Window, FocusHandle}; // Get first keybinding for an action if let Some(kbd) = Kbd::binding_for_action(&MyAction {}, None, window) { // Display the bound shortcut } // Get keybinding for action within a specific context if let Some(kbd) = Kbd::binding_for_action(&MyAction {}, Some("Editor"), window) { // Display context-specific shortcut } // Get keybinding for action within a focus handle if let Some(kbd) = Kbd::binding_for_action_in(&MyAction {}, &focus_handle, window) { // Display shortcut for focused element } ``` ### Keyboard Shortcut Help ```rust use gpui_kit::{div, h_flex, v_flex}; // Display common shortcuts v_flex() .gap_2() .child( h_flex() .gap_2() .items_center() .child("Open command palette:") .child(Kbd::new(Keystroke::parse("cmd-shift-p").unwrap())) ) .child( h_flex() .gap_2() .items_center() .child("Save file:") .child(Kbd::new(Keystroke::parse("cmd-s").unwrap())) ) .child( h_flex() .gap_2() .items_center() .child("Find in files:") .child(Kbd::new(Keystroke::parse("cmd-shift-f").unwrap())) ) ``` ### Menu Item with Shortcut ```rust h_flex() .justify_between() .items_center() .child("New File") .child(Kbd::new(Keystroke::parse("cmd-n").unwrap())) ``` ### Inline Documentation ```rust div() .child("Press ") .child(Kbd::new(Keystroke::parse("escape").unwrap())) .child(" to cancel or ") .child(Kbd::new(Keystroke::parse("enter").unwrap())) .child(" to confirm.") ``` ### Custom Styling ```rust Kbd::new(Keystroke::parse("cmd-k").unwrap()) .text_color(cx.theme().accent) .border_color(cx.theme().accent) .bg(cx.theme().accent.opacity(0.1)) ``` ### Text-Only Format ```rust // Get formatted text without styling let shortcut_text = Kbd::format(&Keystroke::parse("cmd-shift-p").unwrap()); div().child(format!("Shortcut: {}", shortcut_text)) ``` ## Platform Differences The Kbd component automatically formats shortcuts according to platform conventions: ### macOS - Uses symbols: ⌃ ⌥ ⇧ ⌘ - No separators between modifiers - Order: Control, Option, Shift, Command - Special keys: ⌫ (backspace), ⎋ (escape), ⏎ (enter), ← → ↑ ↓ (arrows) ### Windows/Linux - Uses text labels: Ctrl, Alt, Shift, Win - Plus sign (+) separators - Order: Ctrl, Alt, Shift, Win - Special keys: Backspace, Esc, Enter, Left, Right, Up, Down ### Examples by Platform | Input | macOS | Windows/Linux | | ------------------- | ----- | ----------------- | | `cmd-a` | ⌘A | Win+A | | `ctrl-shift-a` | ⌃⇧A | Ctrl+Shift+A | | `cmd-alt-backspace` | ⌥⌘⌫ | Win+Alt+Backspace | | `escape` | ⎋ | Esc | | `enter` | ⏎ | Enter | | `left` | ← | Left | ## Styling The Kbd component uses the following default styles: - Border with theme border color - Muted foreground text color - Background with theme background color - Small rounded corners - Centered text alignment - Extra small font size - Minimal padding (0.5px vertical, 1px horizontal) - Minimum width of 5 units - Flex shrink disabled to maintain size All styles can be customized using the `Styled` trait methods. --- # DataTable Source: /component/data-table # Data Table A comprehensive data table component designed for handling large datasets with high performance. Features virtual scrolling, column configuration, sorting, filtering, row/column/cell selection, and custom cell rendering. Perfect for displaying tabular data with thousands of rows while maintaining smooth performance. ## Key Features - **Multiple Selection Modes**: Row, column, and individual cell selection - **Virtual Scrolling**: Handle thousands of rows with smooth performance - **Column Management**: Resizable, movable, and fixed columns - **Sorting**: Built-in column sorting support - **Keyboard Navigation**: Full keyboard support for all selection modes - **Custom Cell Rendering**: Render any content in table cells - **Context Menus**: Right-click support for rows and cells - **Infinite Loading**: Load more data as user scrolls - **Events**: Comprehensive event system for user interactions ## Import ```rust use gpui_kit::component::table::{ DataTable, TableState, TableDelegate, Column, ColumnSort, ColumnFixed, TableEvent }; ``` ## Usage ### Invoices To create a table, you need to implement the `TableDelegate` trait and provide column definitions, and use `TableState` to manage the table state. ```rust use std::ops::Range; use gpui_kit::{App, Context, Window, IntoElement}; use gpui_kit::component::table::{DataTable, TableDelegate, Column, ColumnSort}; struct MyData { id: usize, name: String, age: u32, email: String, } struct MyTableDelegate { data: Vec, columns: Vec, } impl MyTableDelegate { fn new() -> Self { Self { data: vec![ MyData { id: 1, name: "John".to_string(), age: 30, email: "john@example.com".to_string() }, MyData { id: 2, name: "Jane".to_string(), age: 25, email: "jane@example.com".to_string() }, ], columns: vec![ Column::new("id", "ID").width(60.), Column::new("name", "Name").width(150.).sortable(), Column::new("age", "Age").width(80.).sortable(), Column::new("email", "Email").width(200.), ], } } } impl TableDelegate for MyTableDelegate { fn columns_count(&self, _: &App) -> usize { self.columns.len() } fn rows_count(&self, _: &App) -> usize { self.data.len() } fn column(&self, col_ix: usize, _: &App) -> Column { self.columns[col_ix].clone() } fn render_td(&mut self, row_ix: usize, col_ix: usize, _: &mut Window, _: &mut Context>) -> impl IntoElement { let row = &self.data[row_ix]; let col = &self.columns[col_ix]; match col.key.as_ref() { "id" => row.id.to_string(), "name" => row.name.clone(), "age" => row.age.to_string(), "email" => row.email.clone(), _ => "".to_string(), } } } // Create the table let delegate = MyTableDelegate::new(); let state = cx.new(|cx| TableState::new(delegate, window, cx)); ``` ### Column Configuration Columns provide extensive configuration options: ```rust // Basic column Column::new("id", "ID") // Sortable column Column::new("name", "Name") .sortable() .width(150.) // Right-aligned column Column::new("price", "Price") .text_right() .sortable() // Fixed column (pinned to left) Column::new("actions", "Actions") .fixed(ColumnFixed::Left) .resizable(false) .movable(false) // Column with custom padding Column::new("description", "Description") .width(200.) .paddings(px(8.)) // Non-resizable column Column::new("status", "Status") .width(100.) .resizable(false) // Custom sort orders Column::new("created", "Created") .ascending() // Default ascending // or Column::new("modified", "Modified") .descending() // Default descending ``` ### Virtual Scrolling for Large Datasets The table automatically handles virtual scrolling for optimal performance: ```rust struct LargeDataDelegate { data: Vec, // Could be 10,000+ items columns: Vec, } impl TableDelegate for LargeDataDelegate { fn rows_count(&self, _: &App) -> usize { self.data.len() // No performance impact regardless of size } // Only visible rows are rendered fn render_td(&mut self, row_ix: usize, col_ix: usize, _: &mut Window, _: &mut Context>) -> impl IntoElement { // This is only called for visible rows // Efficiently render cell content let row = &self.data[row_ix]; format_cell_data(row, col_ix) } // Track visible range for optimizations fn visible_rows_changed(&mut self, visible_range: Range, _: &mut Window, _: &mut Context>) { // Only update data for visible rows if needed // This is called when user scrolls } } ``` ### Sorting Implementation Implement sorting in your delegate: ```rust impl TableDelegate for MyTableDelegate { fn perform_sort(&mut self, col_ix: usize, sort: ColumnSort, _: &mut Window, _: &mut Context>) { let col = &self.columns[col_ix]; match col.key.as_ref() { "name" => { match sort { ColumnSort::Ascending => self.data.sort_by(|a, b| a.name.cmp(&b.name)), ColumnSort::Descending => self.data.sort_by(|a, b| b.name.cmp(&a.name)), ColumnSort::Default => { // Reset to original order or default sort self.data.sort_by(|a, b| a.id.cmp(&b.id)); } } } "age" => { match sort { ColumnSort::Ascending => self.data.sort_by(|a, b| a.age.cmp(&b.age)), ColumnSort::Descending => self.data.sort_by(|a, b| b.age.cmp(&a.age)), ColumnSort::Default => self.data.sort_by(|a, b| a.id.cmp(&b.id)), } } _ => {} } } } ``` ### ContextMenu ```rust impl TableDelegate for MyTableDelegate { // Context menu for right-click fn context_menu(&mut self, row_ix: usize, menu: PopupMenu, _: &mut Window, _: &mut Context>) -> PopupMenu { let row = &self.data[row_ix]; menu.menu(format!("Edit {}", row.name), Box::new(EditRowAction(row_ix))) .menu("Delete", Box::new(DeleteRowAction(row_ix))) .separator() .menu("Duplicate", Box::new(DuplicateRowAction(row_ix))) } } ``` ### Cell Rendering Create rich cell content with custom rendering: ```rust impl TableDelegate for MyTableDelegate { fn render_td(&mut self, row_ix: usize, col_ix: usize, _: &mut Window, cx: &mut Context>) -> impl IntoElement { let row = &self.data[row_ix]; let col = &self.columns[col_ix]; match col.key.as_ref() { "status" => { // Custom status badge let (color, text) = match row.status { Status::Active => (cx.theme().green, "Active"), Status::Inactive => (cx.theme().red, "Inactive"), Status::Pending => (cx.theme().yellow, "Pending"), }; div() .px_2() .py_1() .rounded(px(4.)) .bg(color.opacity(0.1)) .text_color(color) .child(text) } "progress" => { // Progress bar div() .w_full() .h(px(8.)) .bg(cx.theme().muted) .rounded(px(4.)) .child( div() .h_full() .w(percentage(row.progress)) .bg(cx.theme().primary) .rounded(px(4.)) ) } "actions" => { // Action buttons h_flex() .gap_1() .child(Button::new(format!("edit-{}", row_ix)).text().icon(IconName::Edit)) .child(Button::new(format!("delete-{}", row_ix)).text().icon(IconName::Trash)) } "avatar" => { // User avatar with image h_flex() .items_center() .gap_2() .child( div() .w(px(32.)) .h(px(32.)) .rounded_full() .bg(cx.theme().accent) .flex() .items_center() .justify_center() .child(row.name.chars().next().unwrap_or('?').to_string()) ) .child(row.name.clone()) } _ => row.get_field_value(col.key.as_ref()).into_any_element(), } } } ``` ### Selection Modes The table supports three distinct selection modes: ```rust // Row selection mode (default) let state = cx.new(|cx| { TableState::new(delegate, window, cx) .row_selectable(true) // Enable row selection .col_selectable(false) .cell_selectable(false) }); // Column selection mode let state = cx.new(|cx| { TableState::new(delegate, window, cx) .row_selectable(false) .col_selectable(true) // Enable column selection .cell_selectable(false) }); // Cell selection mode let state = cx.new(|cx| { TableState::new(delegate, window, cx) .row_selectable(true) // Keep row selection for row selector column .col_selectable(false) .cell_selectable(true) // Enable cell selection }); ``` ### Reading and Writing the Selection A table holds one selection at a time: nothing, a row, a column, or a cell. `selection()` returns it as a single `TableSelection` value, and `set_selection()` writes one back, so persisting and restoring the selection is one read and one write: ```rust use gpui_component::table::TableSelection; match state.read(cx).selection() { TableSelection::None => {} TableSelection::Row(row_ix) => println!("Row {row_ix}"), TableSelection::Column(col_ix) => println!("Column {col_ix}"), TableSelection::Cell(row_ix, col_ix) => println!("Cell ({row_ix}, {col_ix})"), } state.update(cx, |state, cx| { // Scrolls and emits exactly as `set_selected_cell(5, 3, cx)` would. state.set_selection(TableSelection::Cell(5, 3), cx); }); ``` The positional getters `selected_row()`, `selected_col()` and `selected_cell()` each answer only for their own kind of selection. `selected_row()` is `Some` only when a row itself is selected; a selected cell does not surface through it, and selecting a row clears what `selected_cell()` reports. When you need the row a selected cell sits in, map it from the cell: ```rust let row_ix = state.read(cx).selected_cell().map(|(row_ix, _)| row_ix); ``` Keyboard navigation remembers the last row and column position across mode changes. Pressing `Down` after selecting a column continues from the row that was selected before, even though `selected_row()` returned `None` in column mode. ### Column Resizing and Moving Enable dynamic column management: ```rust // Configure table features let state = cx.new(|cx| { TableState::new(delegate, window, cx) .col_resizable(true) // Allow column resizing .col_movable(true) // Allow column reordering .sortable(true) // Enable sorting .col_selectable(true) // Allow column selection .row_selectable(true) // Allow row selection }); // Listen for column changes cx.subscribe_in(&state, window, |view, table, event, _, cx| { match event { TableEvent::ColumnWidthsChanged(widths) => { // Save column widths to user preferences save_column_widths(widths); } TableEvent::MoveColumn(from_ix, to_ix) => { // Save column order save_column_order(from_ix, to_ix); } _ => {} } }).detach(); ``` ### Infinite Loading / Pagination Implement loading more data as user scrolls: ```rust impl TableDelegate for MyTableDelegate { fn has_more(&self, _: &App) -> bool { self.has_more_data } fn load_more_threshold(&self) -> usize { 50 // Load more when 50 rows from bottom } fn load_more(&mut self, _: &mut Window, cx: &mut Context>) { if self.loading { return; // Prevent multiple loads } self.loading = true; // Spawn async task to load data cx.spawn(async move |view, cx| { let new_data = fetch_more_data().await; cx.update(|cx| { view.update(cx, |view, _| { let delegate = view.table.delegate_mut(); delegate.data.extend(new_data); delegate.loading = false; delegate.has_more_data = !new_data.is_empty(); }); }) }).detach(); } fn loading(&self, _: &App) -> bool { self.loading } } ``` ### Table Styling Customize table appearance. `DataTable` implements `Sizable`: use preset sizes such as `.small()` and `.large()` for standard density, or pass a custom pixel size to set a uniform header and body row height. ```rust use gpui_kit::px; use gpui_kit::component::Sizable as _; let state = cx.new(|cx| { TableState::new(delegate, window, cx) }); // In render DataTable::new(&state) .with_size(px(48.)) // Custom uniform row height .stripe(true) // Alternating row colors .bordered(true) // Border around table .scrollbar_visible(true, true) // Vertical, horizontal scrollbars ``` ### Financial Data Table ```rust struct StockData { symbol: String, price: f64, change: f64, change_percent: f64, volume: u64, } impl TableDelegate for StockTableDelegate { fn render_td(&mut self, row_ix: usize, col_ix: usize, _: &mut Window, cx: &mut Context>) -> impl IntoElement { let stock = &self.stocks[row_ix]; let col = &self.columns[col_ix]; match col.key.as_ref() { "symbol" => div().font_weight(FontWeight::BOLD).child(stock.symbol.clone()), "price" => div().text_right().child(format!("${:.2}", stock.price)), "change" => { let color = if stock.change >= 0.0 { cx.theme().green } else { cx.theme().red }; div() .text_right() .text_color(color) .child(format!("{:+.2}", stock.change)) } "change_percent" => { let color = if stock.change_percent >= 0.0 { cx.theme().green } else { cx.theme().red }; div() .text_right() .text_color(color) .child(format!("{:+.1}%", stock.change_percent * 100.0)) } "volume" => div().text_right().child(format!("{:,}", stock.volume)), _ => div(), } } } ``` ### User Management Table ```rust struct UserTableDelegate { users: Vec, columns: Vec, } impl UserTableDelegate { fn new() -> Self { Self { users: Vec::new(), columns: vec![ Column::new("avatar", "").width(50.).resizable(false).movable(false), Column::new("name", "Name").width(150.).sortable().fixed_left(), Column::new("email", "Email").width(200.).sortable(), Column::new("role", "Role").width(100.).sortable(), Column::new("status", "Status").width(100.), Column::new("last_login", "Last Login").width(120.).sortable(), Column::new("actions", "Actions").width(100.).resizable(false), ], } } } ``` ### Cell Selection Enable individual cell selection for more granular control: ```rust let state = cx.new(|cx| { TableState::new(delegate, window, cx) .cell_selectable(true) // Enable cell selection .row_selectable(true) // Also allow row selection }); // Listen for cell events cx.subscribe_in(&state, window, |view, table, event, _, cx| { match event { TableEvent::SelectCell(row_ix, col_ix) => { println!("Selected cell: ({}, {})", row_ix, col_ix); } TableEvent::DoubleClickedCell(row_ix, col_ix) => { // Open editor or detail view open_cell_editor(row_ix, col_ix); } TableEvent::RightClickedCell(row_ix, col_ix) => { // Show cell-specific context menu show_cell_context_menu(row_ix, col_ix); } TableEvent::ClearSelection => { println!("Selection cleared"); } _ => {} } }).detach(); ``` #### Cell Selection Features When cell selection is enabled: - **Click to select**: Click on any cell to select it - **Row selector column**: A dedicated column appears on the left for selecting entire rows - **Keyboard navigation**: Arrow keys navigate between cells (not rows/columns) - **Double-click support**: Trigger actions like editing by double-clicking cells - **Right-click support**: Show context menus specific to cell content - **Visual feedback**: Selected cells show highlight with border #### Programmatic Cell Selection ```rust // Get the currently selected cell. `None` while a row or column is // selected instead; see "Reading and Writing the Selection" above. if let Some((row_ix, col_ix)) = state.read(cx).selected_cell() { println!("Current cell: ({}, {})", row_ix, col_ix); } // Select a specific cell programmatically state.update(cx, |state, cx| { state.set_selected_cell(5, 3, cx); // Select row 5, column 3 }); // Clear all selections state.update(cx, |state, cx| { state.clear_selection(cx); }); ``` #### Non-selectable Columns Prevent specific columns from being selected (useful for action columns): ```rust Column::new("actions", "Actions") .width(100.) .selectable(false) // This column's cells cannot be selected .resizable(false) ``` #### Cell Selection with Custom Rendering ```rust impl TableDelegate for MyTableDelegate { fn render_td(&mut self, row_ix: usize, col_ix: usize, _: &mut Window, cx: &mut Context>) -> impl IntoElement { let row = &self.data[row_ix]; let col = &self.columns[col_ix]; // Render different content based on whether cell is selected let is_selected = cx.entity().read(cx).selected_cell() == Some((row_ix, col_ix)); match col.key.as_ref() { "editable_field" => { if is_selected { // Show input when selected Input::new(format!("cell-{}-{}", row_ix, col_ix)) .value(row.field_value.clone()) .into_any_element() } else { // Show plain text when not selected div().child(row.field_value.clone()).into_any_element() } } _ => div().child(row.get_value(col.key.as_ref())).into_any_element() } } } ``` ## Keyboard Shortcuts ### Row Selection Mode (default) - `↑/↓` - Navigate rows - `←/→` - Navigate columns - `Home` - Jump to first row/column - `End` - Jump to last row/column - `PageUp/PageDown` - Navigate by page - `Escape` - Clear selection ### Cell Selection Mode - `↑/↓` - Navigate up/down within current column - `←/→` - Navigate left/right within current row - `Tab` - Move to next cell (right, then next row) - `Shift+Tab` - Move to previous cell - `Home` - Jump to first cell in current row - `End` - Jump to last cell in current row - `PageUp/PageDown` - Navigate by page within current column - `Escape` - Clear selection ## API Reference ### Core Types - [DataTable] - The data table component - [TableState] - Table state management - [TableDelegate] - Trait for implementing table data source - [Column] - Column configuration - [TableEvent] - Table events (selection, clicks, etc.) - [TableSelection] - The current selection as one value: `None`, `Row`, `Column`, or `Cell` ### Column Types - [ColumnSort] - Column sort direction enum - [ColumnFixed] - Column fixed position enum ### Methods #### TableState - `new(delegate, window, cx)` - Create a new table state - `cell_selectable(bool)` - Enable/disable cell selection - `row_selectable(bool)` - Enable/disable row selection - `col_selectable(bool)` - Enable/disable column selection - `selection()` - Get the current selection as a `TableSelection` - `set_selection(selection, cx)` - Set the selection from a `TableSelection`; `TableSelection::None` clears it - `selected_cell()` - Get the selected cell; `None` unless a cell is selected - `set_selected_cell(row_ix, col_ix, cx)` - Select a specific cell - `selected_row()` - Get the selected row; `None` unless a row itself is selected - `selected_col()` - Get the selected column; `None` unless a column itself is selected - `clear_selection(cx)` - Clear all selections - `scroll_to_row(row_ix, cx)` - Scroll to specific row - `scroll_to_col(col_ix, cx)` - Scroll to specific column #### Column - `new(key, name)` - Create a new column - `width(pixels)` - Set column width - `sortable()` - Make column sortable - `ascending()` - Set default sort to ascending - `descending()` - Set default sort to descending - `text_right()` - Right-align column text - `text_center()` - Center-align column text - `fixed(ColumnFixed)` - Pin column to left - `resizable(bool)` - Enable/disable column resizing - `movable(bool)` - Enable/disable column moving - `selectable(bool)` - Enable/disable column/cell selection - `paddings(edges)` - Set custom padding - `min_width(pixels)` - Set minimum width - `max_width(pixels)` - Set maximum width ### Events - `SelectRow(usize)` - Row selected - `DoubleClickedRow(usize)` - Row double-clicked - `SelectColumn(usize)` - Column selected - `SelectCell(usize, usize)` - Cell selected (row_ix, col_ix) - `DoubleClickedCell(usize, usize)` - Cell double-clicked (row_ix, col_ix) - `RightClickedCell(usize, usize)` - Cell right-clicked (row_ix, col_ix) - `RightClickedRow(Option)` - Row right-clicked - `ColumnWidthsChanged(Vec)` - Column widths changed - `MoveColumn(usize, usize)` - Column moved (from_ix, to_ix) [DataTable]: https://docs.rs/gpui-component/latest/gpui_component/table/struct.DataTable.html [TableState]: https://docs.rs/gpui-component/latest/gpui_component/table/struct.TableState.html [TableDelegate]: https://docs.rs/gpui-component/latest/gpui_component/table/trait.TableDelegate.html [Column]: https://docs.rs/gpui-component/latest/gpui_component/table/struct.Column.html [TableEvent]: https://docs.rs/gpui-component/latest/gpui_component/table/enum.TableEvent.html [TableSelection]: https://docs.rs/gpui-component/latest/gpui_component/table/enum.TableSelection.html [ColumnSort]: https://docs.rs/gpui-component/latest/gpui_component/table/enum.ColumnSort.html [ColumnFixed]: https://docs.rs/gpui-component/latest/gpui_component/table/enum.ColumnFixed.html --- # Pagination Source: /component/pagination The [Pagination] component provides page navigation with next and previous links. It displays page numbers and allows users to navigate through multiple pages of content. ## Import ```rust use gpui_kit::component::pagination::Pagination; ``` ## Usage ### Pages By default, the pagination shows up to 5 visible page buttons. You can customize this with `visible_pages()`: ```rust Pagination::new("my-pagination") .current_page(1) .total_pages(50) .visible_pages(10) .on_click(|page, _, cx| { // Handle page change }) ``` ### Basic Pagination ```rust Pagination::new("my-pagination") .current_page(5) .total_pages(10) .on_click(|page, _, cx| { println!("Navigated to page: {}", page); }) ``` ### Compact Style The compact style only shows the previous and next buttons with icons, without displaying page numbers. Use `compact` method to enable compact style: ```rust Pagination::new("my-pagination") .compact() .current_page(3) .total_pages(10) .on_click(|page, _, cx| { // Handle page change }) ``` ### Different Sizes The Pagination supports the [Sizable] trait for different sizes: ```rust use gpui_kit::component::{Sizable as _, Size}; Pagination::new("my-pagination") .xsmall() .current_page(1) .total_pages(10) Pagination::new("my-pagination") .small() .current_page(1) .total_pages(10) Pagination::new("my-pagination") .current_page(1) .total_pages(10) // Medium (default) Pagination::new("my-pagination") .large() .current_page(1) .total_pages(10) ``` ### Disabled State ```rust Pagination::new("my-pagination") .current_page(4) .total_pages(10) .disabled(true) .on_click(|_, _, _| {}) ``` ### Handle Page Change Events The `on_click` callback receives the new page number when users click on page numbers, previous, or next buttons: ```rust Pagination::new("my-pagination") .current_page(current_page) .total_pages(total_pages) .on_click(|page, _, cx| { // Update your state with the new page // The page number is 1-based }) ``` ### With State Management ```rust let mut current_page = 1; let total_pages = 20; Pagination::new("pagination") .current_page(current_page) .total_pages(total_pages) .on_click({ let entity = entity.clone(); move |page, _, cx| { entity.update(cx, |this, cx| { this.current_page = *page; cx.notify(); }); } }) ``` ### Large Dataset Pagination For large datasets, use `visible_pages()` to show more page options: ```rust Pagination::new("large-pagination") .current_page(25) .total_pages(100) .visible_pages(10) .on_click(|page, _, cx| { // Load data for the new page }) ``` [Pagination]: https://docs.rs/gpui-component/latest/gpui_component/pagination/struct.Pagination.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html ## API Reference - [Pagination] ### Sizing Implements [Sizable] trait: - `xsmall()` - Extra small size - `small()` - Small size - `medium()` - Medium size (default) - `large()` - Large size - `with_size(size)` - Set custom size ### Methods - `current_page(page: usize)` - Set the current page number (1-based). The value will be clamped between 1 and total_pages. - `total_pages(pages: usize)` - Set the total number of pages. - `visible_pages(max: usize)` - Set the maximum number of visible page buttons (default: 5). - `compact()` - Enable compact style (only shows prev/next buttons with icons). - `disabled(bool)` - Set the disabled state. - `on_click(handler)` - Set the handler for page change events. --- # Avatar Source: /component/avatar The Avatar component displays user profile images with intelligent fallbacks. When no image is provided, it shows user initials or a placeholder icon. The component supports various sizes and can be grouped together for team displays. ## Import ```rust use gpui_kit::component::avatar::{Avatar, AvatarGroup}; ``` ## Usage ### Image You can create an [Avatar] by providing an image source URL and a user name: ```rust Avatar::new() .name("John Doe") .src("https://example.com/avatar.jpg") ``` ### Fallback When no image source is provided, the Avatar displays user initials with an automatically generated color background: ```rust // Shows "JD" initials with colored background Avatar::new() .name("John Doe") // Shows "JS" initials Avatar::new() .name("Jane Smith") ``` The color is derived from the initials, so the same person always gets the same one. It comes from a ring of 12 evenly spaced OkLCH hues held at a fixed lightness and chroma, which keeps every avatar at the same visual weight and its text above WCAG AA contrast in both the light and dark themes. The outline follows the same hue; an Avatar showing an image keeps the neutral border. ### Group For anonymous users or when no name is provided: ```rust use gpui_kit::component::IconName; // Default user icon placeholder Avatar::new() // Custom placeholder icon Avatar::new() .placeholder(IconName::Building2) ``` ### Custom shape ```rust Avatar::new() .src("https://example.com/avatar.jpg") .with_size(px(100.)) .border_3() .border_color(cx.theme().foreground) .shadow_sm() .rounded(px(20.)) // Custom border radius ``` ### Custom style ```rust // The avatar automatically generates colors based on the name // Different names will get different colors from the color palette Avatar::new().name("Alice") // Gets one color Avatar::new().name("Bob") // Gets a different color Avatar::new().name("Charlie") // Gets another color ``` [Avatar]: https://docs.rs/gpui-component/latest/gpui_component/avatar/struct.Avatar.html [AvatarGroup]: https://docs.rs/gpui-component/latest/gpui_component/avatar/struct.AvatarGroup.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html ### Avatar Sizes ```rust Avatar::new() .name("John Doe") .xsmall() Avatar::new() .name("John Doe") .small() Avatar::new() .name("John Doe") // 48px (default medium) Avatar::new() .name("John Doe") .large() // Custom size Avatar::new() .name("John Doe") .with_size(px(100.)) ``` ### Team Display ```rust use gpui_kit::component::{h_flex, v_flex}; v_flex() .gap_4() .child("Development Team") .child( AvatarGroup::new() .limit(4) .ellipsis() .child(Avatar::new().name("Alice Johnson").src("https://example.com/alice.jpg")) .child(Avatar::new().name("Bob Smith").src("https://example.com/bob.jpg")) .child(Avatar::new().name("Charlie Brown")) .child(Avatar::new().name("Diana Prince")) .child(Avatar::new().name("Eve Wilson")) ) ``` ### User Profile Header ```rust h_flex() .items_center() .gap_4() .child( Avatar::new() .src("https://example.com/profile.jpg") .name("John Doe") .large() .border_2() .border_color(cx.theme().primary) ) .child( v_flex() .child("John Doe") .child("Software Engineer") ) ``` ### Anonymous User ```rust use gpui_kit::component::IconName; Avatar::new() .placeholder(IconName::UserCircle) .medium() ``` ## AvatarGroup The [AvatarGroup] component allows you to display multiple avatars in a compact, overlapping layout: ### Basic Group ```rust AvatarGroup::new() .child(Avatar::new().src("https://example.com/user1.jpg")) .child(Avatar::new().src("https://example.com/user2.jpg")) .child(Avatar::new().src("https://example.com/user3.jpg")) .child(Avatar::new().name("John Doe")) ``` ### Group with Limit ```rust AvatarGroup::new() .limit(3) // Show maximum 3 avatars .child(Avatar::new().src("https://example.com/user1.jpg")) .child(Avatar::new().src("https://example.com/user2.jpg")) .child(Avatar::new().src("https://example.com/user3.jpg")) .child(Avatar::new().src("https://example.com/user4.jpg")) // Hidden .child(Avatar::new().src("https://example.com/user5.jpg")) // Hidden ``` ### Group with Ellipsis Show an ellipsis indicator when avatars are hidden due to the limit. In this example, only 3 avatars are shown, and "..." indicates there are more: ```rust AvatarGroup::new() .limit(3) .ellipsis() // Shows "..." when limit is exceeded .child(Avatar::new().src("https://example.com/user1.jpg")) .child(Avatar::new().src("https://example.com/user2.jpg")) .child(Avatar::new().src("https://example.com/user3.jpg")) .child(Avatar::new().src("https://example.com/user4.jpg")) .child(Avatar::new().src("https://example.com/user5.jpg")) ``` ### Group Sizes The [Sizeable] trait can also be applied to AvatarGroup, and it will set the size for all contained avatars. ```rust // Extra small group AvatarGroup::new() .xsmall() .child(Avatar::new().name("A")) .child(Avatar::new().name("B")) .child(Avatar::new().name("C")) // Small group AvatarGroup::new() .small() .child(Avatar::new().name("A")) .child(Avatar::new().name("B")) // Medium group (default) AvatarGroup::new() .child(Avatar::new().name("A")) .child(Avatar::new().name("B")) // Large group AvatarGroup::new() .large() .child(Avatar::new().name("A")) .child(Avatar::new().name("B")) ``` ### Adding Multiple Avatars ```rust let avatars = vec![ Avatar::new().src("https://example.com/user1.jpg"), Avatar::new().src("https://example.com/user2.jpg"), Avatar::new().name("John Doe"), ]; AvatarGroup::new() .children(avatars) .limit(5) .ellipsis() ``` ## API Reference - [Avatar] - [AvatarGroup] --- # Radio Source: /component/radio Radio buttons allow users to select a single option from a set of mutually exclusive choices. Use radio buttons when you want to give users a choice between multiple options and only one selection is allowed. Use `on_change` for requested values. The owner stores the value and calls `cx.notify()`. The existing `on_click` name remains a compatibility alias; setting either replaces the same handler, so the last call wins. ## Import ```rust use gpui_kit::component::radio::{Radio, RadioGroup}; ``` ## Usage ### Group ```rust struct MyView { selected_option: Option, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { RadioGroup::horizontal("options") .children(["Option 1", "Option 2", "Option 3"]) .selected_index(self.selected_option) .on_change(cx.listener(|view, selected_index: &usize, _, cx| { view.selected_option = Some(*selected_index); cx.notify(); })) } } ``` ### Standalone ```rust Radio::new("radio-option-1") .label("Option 1") .checked(false) .on_change(|checked, _, _| { println!("Radio is now: {}", checked); }) ``` ### Controlled Radio Button ```rust struct MyView { radio_checked: bool, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Radio::new("radio") .label("Select this option") .checked(self.radio_checked) .on_change(cx.listener(|view, checked, _, cx| { view.radio_checked = *checked; cx.notify(); })) } } ``` ### Different Sizes ```rust Radio::new("small").label("Small").xsmall() Radio::new("medium").label("Medium") // default Radio::new("large").label("Large").large() ``` ### Disabled State ```rust Radio::new("disabled") .label("Disabled option") .disabled(true) .checked(false) Radio::new("disabled-checked") .label("Disabled and checked") .checked(true) .disabled(true) ``` ### Multi-line Label with Custom Content ```rust Radio::new("custom") .label("Primary option") .child( div() .text_color(cx.theme().muted_foreground) .child("This is additional descriptive text that provides more context.") ) .w(px(300.)) ``` ### Custom Tab Order ```rust Radio::new("radio") .label("Custom tab order") .tab_index(2) .tab_stop(true) ``` ### Settings Panel ```rust struct SettingsView { theme: Option, // 0: Light, 1: Dark, 2: Auto language: Option, // 0: English, 1: Spanish, 2: French } impl Render for SettingsView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_6() .child( v_flex() .gap_2() .child(div().text_sm().font_semibold().child("Theme")) .child( RadioGroup::vertical("theme") .child(Radio::new("light").label("Light")) .child(Radio::new("dark").label("Dark")) .child(Radio::new("auto").label("Auto")) .selected_index(self.theme) .on_change(cx.listener(|view, index, _, cx| { view.theme = Some(*index); cx.notify(); })) ) ) .child( v_flex() .gap_2() .child(div().text_sm().font_semibold().child("Language")) .child( RadioGroup::horizontal("language") .children(["English", "Español", "Français"]) .selected_index(self.language) .on_change(cx.listener(|view, index, _, cx| { view.language = Some(*index); cx.notify(); })) ) ) } } ``` ### Survey Form ```rust struct SurveyView { satisfaction: Option, recommendation: Option, } impl Render for SurveyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_8() .child( v_flex() .gap_3() .child( div() .text_base() .font_medium() .child("How satisfied are you with our service?") ) .child( RadioGroup::vertical("satisfaction") .child(Radio::new("very-satisfied").label("Very satisfied")) .child(Radio::new("satisfied").label("Satisfied")) .child(Radio::new("neutral").label("Neutral")) .child(Radio::new("dissatisfied").label("Dissatisfied")) .child(Radio::new("very-dissatisfied").label("Very dissatisfied")) .selected_index(self.satisfaction) .on_change(cx.listener(|view, index, _, cx| { view.satisfaction = Some(*index); cx.notify(); })) ) ) .child( v_flex() .gap_3() .child( div() .text_base() .font_medium() .child("How likely are you to recommend us?") ) .child( RadioGroup::horizontal("recommendation") .children((0..=10).map(|i| i.to_string())) .selected_index(self.recommendation) .on_change(cx.listener(|view, index, _, cx| { view.recommendation = Some(*index); cx.notify(); })) ) ) } } ``` ### Payment Method Selection ```rust struct PaymentView { payment_method: Option, } impl Render for PaymentView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child( div() .text_lg() .font_semibold() .child("Select Payment Method") ) .child( RadioGroup::vertical("payment") .child( Radio::new("credit-card") .label("Credit Card") .child( div() .text_color(cx.theme().muted_foreground) .child("Visa, MasterCard, American Express") ) ) .child( Radio::new("paypal") .label("PayPal") .child( div() .text_color(cx.theme().muted_foreground) .child("Pay with your PayPal account") ) ) .child( Radio::new("bank-transfer") .label("Bank Transfer") .child( div() .text_color(cx.theme().muted_foreground) .child("Direct bank account transfer") ) ) .selected_index(self.payment_method) .on_change(cx.listener(|view, index, _, cx| { view.payment_method = Some(*index); cx.notify(); })) ) } } ``` ## Radio Group Usage ### Horizontal Layout ```rust RadioGroup::horizontal("horizontal-group") .children(["First", "Second", "Third"]) .selected_index(Some(0)) .on_change(cx.listener(|view, index, _, cx| { println!("Selected index: {}", index); cx.notify(); })) ``` ### Vertical Layout ```rust RadioGroup::vertical("vertical-group") .child(Radio::new("option1").label("United States")) .child(Radio::new("option2").label("Canada")) .child(Radio::new("option3").label("Mexico")) .selected_index(Some(1)) .disabled(false) ``` ### Styled Radio Group ```rust RadioGroup::vertical("styled-group") .w(px(220.)) .p_2() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) .child(Radio::new("option1").label("Option 1")) .child(Radio::new("option2").label("Option 2")) .child(Radio::new("option3").label("Option 3")) .selected_index(Some(0)) ``` ### Disabled Radio Group ```rust RadioGroup::vertical("disabled-group") .children(["Option A", "Option B", "Option C"]) .selected_index(Some(1)) .disabled(true) // Disables all radio buttons in the group ``` ## Best Practices 1. **Use RadioGroup**: Always prefer `RadioGroup` over individual `Radio` components for mutually exclusive choices 2. **Clear Labels**: Provide descriptive labels that clearly indicate what each option represents 3. **Default Selection**: Consider providing a sensible default selection, especially for required fields 4. **Logical Order**: Arrange options in a logical order (alphabetical, frequency of use, or importance) 5. **Limit Options**: Keep the number of radio options reasonable (typically 2-7 options) 6. **Group Related Options**: Use visual grouping and clear headings for multiple radio groups 7. **Responsive Design**: Consider using horizontal layout for fewer options and vertical for more options ## API Reference ### Radio | Method | Description | | ------------------ | ----------------------------------------------------------- | | `new(id)` | Create a new radio button with the given ID | | `label(text)` | Set label text | | `checked(bool)` | Set checked state | | `disabled(bool)` | Set disabled state | | `on_change(fn)` | Requested checked value, receives `&bool` | | `tab_stop(bool)` | Enable/disable tab navigation (default: true) | | `tab_index(isize)` | Set tab order index (default: 0) | ### RadioGroup | Method | Description | | ------------------------------- | ------------------------------------------------------------------- | | `new(id)` | Create a vertical radio group with no selection | | `horizontal(id)` | Create a new horizontal radio group | | `vertical(id)` | Create a new vertical radio group | | `layout(Axis)` | Set layout direction (Vertical or Horizontal) | | `child(Radio)` | Add a single radio button to the group | | `children(items)` | Add multiple radio buttons from an iterator | | `selected_index(Option)` | Set the selected option by index | | `disabled(bool)` | Disable all radio buttons in the group | | `on_change(fn)` | Requested selected index, receives `&usize` | ### Styling Both Radio and RadioGroup implement `Styled` trait for custom styling: Radio also implements `Sizable` trait: - `xsmall()` - Extra small size - `small()` - Small size - `medium()` - Medium size (default) - `large()` - Large size --- # Settings Source: /component/settings > Since: v0.5.0 The Settings component provides a UI for managing application settings. It includes grouped setting items and pages. We can search by title, description, and custom keywords to filter the settings to display only relevant settings (Like this macOS, iOS Settings). ## Import ```rust use gpui_kit::component::setting::{Settings, SettingPage, SettingGroup, SettingItem, SettingField}; ``` ## Usage ### Build a settings Here we have components that can be used to build a settings page. - [Settings] - The main settings component that holds multiple setting pages. - [SettingPage] - A page of related setting groups. - [SettingGroup] - A group of related setting items based on [GroupBox] style. - [SettingItem] - A single setting item with title, description, and field. - [SettingField] - Provide different field types like Input, Dropdown, Switch, etc. The layout of the settings is like this: ``` Settings SettingPage SettingGroup SettingItem Title Description (optional) SettingField ``` ### Basic Settings ```rust use gpui_kit::component::setting::{Settings, SettingPage, SettingGroup, SettingItem, SettingField}; Settings::new("my-settings") .pages(vec![ SettingPage::new("General") .group( SettingGroup::new() .title("Basic Options") .item( SettingItem::new( "Enable Feature", SettingField::switch( |cx: &App| true, |val: bool, cx: &mut App| { println!("Feature enabled: {}", val); }, ) ) ) ) ]) ``` ### With Multiple Pages When you want default expland a page, you can use `default_open(true)` on the [SettingPage]. ```rust Settings::new("app-settings") .pages(vec![ SettingPage::new("General") .default_open(true) .group(SettingGroup::new().title("Appearance").items(vec![...])), SettingPage::new("Software Update") .group(SettingGroup::new().title("Updates").items(vec![...])), SettingPage::new("About") .group(SettingGroup::new().items(vec![...])), ]) ``` ### Selection While Searching Search keeps the current page selected while it contains matching settings. If it no longer matches, the first matching page is selected. A matching selected group is preserved; otherwise selection falls back to its page. Clearing the search keeps the current page rather than restoring an earlier selection. When no settings match, no page content is shown and the selection is retained for when results return. ### Group Variants ```rust use gpui_kit::component::group_box::GroupBoxVariant; Settings::new("my-settings") .with_group_variant(GroupBoxVariant::Outline) .pages(vec![...]) Settings::new("my-settings") .with_group_variant(GroupBoxVariant::Fill) .pages(vec![...]) ``` A group can override the settings-level variant, for example to present one page's items directly while the other pages keep the global card surface: ```rust SettingGroup::new() .variant(GroupBoxVariant::Normal) .items(vec![...]) ``` ### Complete Settings Example ```rust use gpui_kit::{App, SharedString}; use gpui_kit::component::{ Settings, SettingPage, SettingGroup, SettingItem, SettingField, setting::NumberFieldOptions, group_box::GroupBoxVariant, Size, }; Settings::new("app-settings") .with_size(Size::Medium) .with_group_variant(GroupBoxVariant::Outline) .pages(vec![ SettingPage::new("General") .resettable(true) .default_open(true) .groups(vec![ SettingGroup::new() .title("Appearance") .items(vec![ SettingItem::new( "Dark Mode", SettingField::switch( |cx: &App| cx.theme().mode.is_dark(), |val: bool, cx: &mut App| { // Handle theme change }, ) ) .description("Switch between light and dark themes."), ]), SettingGroup::new() .title("Font") .items(vec![ SettingItem::new( "Font Family", SettingField::dropdown( vec![ ("Arial".into(), "Arial".into()), ("Helvetica".into(), "Helvetica".into()), ], |cx: &App| "Arial".into(), |val: SharedString, cx: &mut App| { // Handle font change }, ) ), SettingItem::new( "Font Size", SettingField::number_input( NumberFieldOptions { min: 8.0, max: 72.0, ..Default::default() }, |cx: &App| 14.0, |val: f64, cx: &mut App| { // Handle size change }, ) ), ]), ]), SettingPage::new("Software Update") .resettable(true) .group( SettingGroup::new() .title("Updates") .items(vec![ SettingItem::new( "Auto Update", SettingField::switch( |cx: &App| true, |val: bool, cx: &mut App| { // Handle auto update }, ) ) .description("Automatically download and install updates."), ]) ), ]) ``` [Settings]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.Settings.html [SettingPage]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.SettingPage.html [SettingGroup]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.SettingGroup.html [SettingItem]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.SettingItem.html [SettingField]: https://docs.rs/gpui-component/latest/gpui_component/setting/enum.SettingField.html [SettingFieldElement]: https://docs.rs/gpui-component/latest/gpui_component/setting/trait.SettingFieldElement.html [NumberFieldOptions]: https://docs.rs/gpui-component/latest/gpui_component/setting/struct.NumberFieldOptions.html [GroupBox]: ./group-box.md [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html ## Setting Page ### Basic Page ```rust SettingPage::new("General") .group(SettingGroup::new().title("Options").items(vec![...])) ``` ### Multiple Groups ```rust SettingPage::new("General") .groups(vec![ SettingGroup::new().title("Appearance").items(vec![...]), SettingGroup::new().title("Font").items(vec![...]), SettingGroup::new().title("Other").items(vec![...]), ]) ``` ### Icon ```rust SettingPage::new("General") .icon(IconName::Settings) .groups(vec![...]) ``` ### Title Suffix Use `title_suffix` to render a custom element after the title in the page header, for example an info icon button that opens the help documentation: ```rust SettingPage::new("General") .title_suffix(|_, _| { Button::new("help") .icon(IconName::Info) .ghost() .xsmall() .on_click(|_, _, cx| cx.open_url("https://example.com/help")) }) .groups(vec![...]) ``` ### Default Open ```rust SettingPage::new("General") .default_open(true) .groups(vec![...]) ``` ### resettable Enable reset functionality for a page: ```rust SettingPage::new("General") .resettable(true) .groups(vec![...]) ``` ## Setting Group ### Basic Group ```rust SettingGroup::new() .title("Appearance") .items(vec![ SettingItem::new(...), SettingItem::new(...), ]) ``` ### Single Item ```rust SettingGroup::new() .title("Font") .item(SettingItem::new(...)) ``` ### Without Title ```rust SettingGroup::new() .items(vec![...]) ``` ### Footer outside the group surface Use `footer` to render supporting content below the group's background or border. It aligns with the group title and renders as small muted text like a description, so plain text is enough; the callback receives the current window and application context for richer content. It scrolls and is filtered with the group; it is not an independently searchable setting or a sidebar entry, and a group still needs at least one item to be shown. ```rust SettingGroup::new() .item(SettingItem::new( "Update source", SettingField::render(|_, _, _| "GitHub Releases"), )) .footer(|_, _| "Changes apply to this device only.") ``` ## Setting Item ### Basic Item ```rust SettingItem::new("Title", SettingField::switch(...)) .description("Description text") ``` ### Custom Item with a render closure You can create a fully custom setting item using `SettingItem::render`: ```rust SettingItem::render(|options, _, _| { h_flex() .w_full() .justify_between() .child("Custom content") .child( Button::new("action") .label("Action") .with_size(options.size) ) .into_any_element() }) ``` ### Vertical Layout By default, setting items use horizontal layout. Use `layout(Axis::Vertical)` for vertical layout: ```rust SettingItem::new( "CLI Path", SettingField::input(...) ) .layout(Axis::Vertical) .description("This item uses vertical layout.") ``` ### With Markdown Description ```rust use gpui_kit::component::text::markdown; SettingItem::new( "Documentation", SettingField::element(...) ) .description(markdown("Rust doc for the `gpui-component` crate.")) ``` ### Disabled Use `disabled(true)` to render a setting item in a non-interactive state. The whole row is dimmed and the built-in field (Switch, Checkbox, Input, Dropdown, NumberInput) is automatically disabled. ```rust SettingItem::new( "Dark Mode", SettingField::switch(...) ) .description("Switch between light and dark themes.") .disabled(true) ``` For [SettingItem::render] custom items, the row is still dimmed automatically, but the renderer is responsible for honoring the disabled state on any interactive controls inside it via `options.disabled`: ```rust SettingItem::render(|options, _, _| { h_flex() .child("Custom content") .child( Button::new("action") .label("Action") .with_size(options.size) .disabled(options.disabled) ) .into_any_element() }) .disabled(true) ``` ### Search Keywords Use `keywords` to attach additional search terms to an item. They are only used for search matching and are never rendered. For example, an item titled "Enable Two-factor auth" can be made searchable via "MFA": ```rust SettingItem::new( "Enable Two-factor auth", SettingField::switch(...) ) .keywords(["MFA", "2FA"]) ``` This is also useful for [SettingItem::render] custom items that have no title or description but should still appear in search results: ```rust SettingItem::render(|options, _, _| { h_flex().child("Custom content").into_any_element() }) .keywords(["Advanced", "Network"]) ``` ## Setting Fields The [SettingField] enum provides different field types for various input needs. ### Switch The switch field represents a `boolean` on/off state. ```rust SettingItem::new( "Dark Mode", SettingField::switch( |cx: &App| cx.theme().mode.is_dark(), |val: bool, cx: &mut App| { // Handle value change }, ) .default_value(false) ) ``` ### Checkbox Like the switch, but uses a checkbox UI. ```rust SettingItem::new( "Auto Switch Theme", SettingField::checkbox( |cx: &App| AppSettings::global(cx).auto_switch_theme, |val: bool, cx: &mut App| { AppSettings::global_mut(cx).auto_switch_theme = val; }, ) .default_value(false) ) ``` ### Input Display a single line text input. ```rust SettingItem::new( "CLI Path", SettingField::input( |cx: &App| AppSettings::global(cx).cli_path.clone(), |val: SharedString, cx: &mut App| { AppSettings::global_mut(cx).cli_path = val; }, ) .default_value("/usr/local/bin/bash".into()) ) .layout(Axis::Vertical) .description("Path to the CLI executable.") ``` ### Dropdown A dropdown with a list of options. ```rust SettingItem::new( "Font Family", SettingField::dropdown( vec![ ("Arial".into(), "Arial".into()), ("Helvetica".into(), "Helvetica".into()), ("Times New Roman".into(), "Times New Roman".into()), ], |cx: &App| AppSettings::global(cx).font_family.clone(), |val: SharedString, cx: &mut App| { AppSettings::global_mut(cx).font_family = val; }, ) .default_value("Arial".into()) ) ``` ### NumberInput ```rust use gpui_kit::component::setting::NumberFieldOptions; SettingItem::new( "Font Size", SettingField::number_input( NumberFieldOptions { min: 8.0, max: 72.0, ..Default::default() }, |cx: &App| AppSettings::global(cx).font_size, |val: f64, cx: &mut App| { AppSettings::global_mut(cx).font_size = val; }, ) .default_value(14.0) ) ``` ### Custom Field by Render Closure The `SettingField::render` method allows you to create a custom field using a closure that returns an element. ```rust SettingItem::new( "GitHub Repository", SettingField::render(|options, _window, _cx| { Button::new("open-url") .outline() .label("Repository...") .with_size(options.size) .on_click(|_, _window, cx| { cx.open_url("https://github.com/example/repo"); }) }) ) ``` ### Custom Field Element You may have a complex field that you want to reuse, you may want split the element into a separate struct to do the complex logic. In this case, the [SettingFieldElement] trait can help you to create a custom field element. ```rust use gpui_kit::component::setting::{SettingFieldElement, RenderOptions}; struct OpenURLSettingField { label: SharedString, url: SharedString, } impl SettingFieldElement for OpenURLSettingField { type Element = Button; fn render_field(&self, options: &RenderOptions, _: &mut Window, _: &mut App) -> Self::Element { let url = self.url.clone(); Button::new("open-url") .outline() .label(self.label.clone()) .with_size(options.size) .on_click(move |_, _window, cx| { cx.open_url(url.as_str()); }) } } ``` Then use it in the setting item: ```rust SettingItem::new( "GitHub Repository", SettingField::element(OpenURLSettingField { label: "Repository...".into(), url: "https://github.com/MohsenDastaran/uni-kit".into(), }) ) ``` ## API Reference - [Settings] - [SettingPage] - [SettingGroup] - [SettingItem] - [SettingField] - [NumberFieldOptions] ### Sizing Implements [Sizable] trait: - `xsmall()` - Extra small size - `small()` - Small size - `medium()` - Medium size (default) - `large()` - Large size - `with_size(Size)` - Set specific size --- # Tag Source: /component/tag A versatile tag component for categorizing and labeling content. Tags are compact visual indicators that help organize information and display metadata like categories, status, or properties. ## Import ```rust use gpui_kit::component::tag::Tag; ``` ## Usage ### Tags ```rust // Primary tag (default filled style) Tag::primary().child("Primary") // Secondary tag Tag::secondary().child("Secondary") // Status tags Tag::danger().child("Danger") Tag::success().child("Success") Tag::warning().child("Warning") Tag::info().child("Info") ``` ### Tag Variants ```rust // Semantic variants Tag::primary().child("Featured") Tag::secondary().child("Category") Tag::danger().child("Critical") Tag::success().child("Completed") Tag::warning().child("Pending") Tag::info().child("Information") ``` ### Outline Tags ```rust // Outline style variants Tag::primary().outline().child("Primary Outline") Tag::secondary().outline().child("Secondary Outline") Tag::danger().outline().child("Error Outline") Tag::success().outline().child("Success Outline") Tag::warning().outline().child("Warning Outline") Tag::info().outline().child("Info Outline") ``` ### Tag Sizes ```rust // Small size Tag::primary().small().child("Small Tag") // Medium size (default) Tag::primary().child("Medium Tag") ``` ### Custom Colors ```rust use gpui_kit::component::ColorName; // Using predefined color names Tag::color(ColorName::Blue).child("Blue Tag") Tag::color(ColorName::Green).child("Green Tag") Tag::color(ColorName::Purple).child("Purple Tag") Tag::color(ColorName::Pink).child("Pink Tag") Tag::color(ColorName::Indigo).child("Indigo Tag") Tag::color(ColorName::Yellow).child("Yellow Tag") Tag::color(ColorName::Red).child("Red Tag") ``` ### Custom HSLA Colors ```rust use gpui_kit::{hsla, Hsla}; // Custom colors with HSLA values let color = hsla(220.0 / 360.0, 0.8, 0.5, 1.0); let foreground = hsla(0.0, 0.0, 1.0, 1.0); let border = hsla(220.0 / 360.0, 0.8, 0.4, 1.0); Tag::custom(color, foreground, border).child("Custom Color") ``` ### Rounded Corners ```rust use gpui_kit::px; // Fully rounded tags Tag::primary().rounded_full().child("Rounded Full") // Custom border radius Tag::primary().rounded(px(4.0)).child("Custom Radius") // Square corners Tag::primary().rounded(px(0.0)).child("Square Tag") ``` ### Combined Styles ```rust // Small tags with full rounding Tag::primary().small().rounded_full().child("Small Pill") Tag::success().small().rounded_full().child("Success Pill") // Outline tags with custom rounding Tag::warning().outline().rounded(px(2.0)).child("Custom Outline") // Color tags with outline style Tag::color(ColorName::Purple).outline().child("Purple Outline") ``` ### Tag Collections ```rust use gpui_kit::component::{h_flex, v_flex}; // Horizontal tag group h_flex() .gap_2() .child(Tag::primary().child("React")) .child(Tag::success().child("TypeScript")) .child(Tag::info().child("Next.js")) .child(Tag::warning().child("Beta")) // Vertical tag stack v_flex() .gap_1() .child(Tag::danger().small().child("Critical")) .child(Tag::warning().small().child("Important")) .child(Tag::secondary().small().child("Normal")) ``` ### Status Dashboard Tags ```rust // System status indicators h_flex() .gap_3() .child( v_flex() .child("API Status:") .child(Tag::success().child("Operational")) ) .child( v_flex() .child("Database:") .child(Tag::warning().child("Maintenance")) ) .child( v_flex() .child("Cache:") .child(Tag::danger().child("Down")) ) ``` ### Interactive Tag Lists ```rust // Note: Event handling would require additional state management // Tags themselves are display components // Filter tags (would need click handlers) h_flex() .gap_2() .child(Tag::primary().small().child("All")) .child(Tag::secondary().outline().small().child("Active")) .child(Tag::secondary().outline().small().child("Completed")) .child(Tag::secondary().outline().small().child("Archived")) ``` ### Color-Coded Categories ```rust use gpui_kit::component::ColorName; // Content type tags h_flex() .gap_2() .flex_wrap() .child(Tag::color(ColorName::Red).child("Bug")) .child(Tag::color(ColorName::Blue).child("Feature")) .child(Tag::color(ColorName::Green).child("Enhancement")) .child(Tag::color(ColorName::Purple).child("Documentation")) .child(Tag::color(ColorName::Yellow).child("Question")) .child(Tag::color(ColorName::Pink).child("Discussion")) ``` ### Pill-Style Tags ```rust // Skill tags with pill styling h_flex() .gap_2() .flex_wrap() .child(Tag::color(ColorName::Blue).rounded_full().small().child("Rust")) .child(Tag::color(ColorName::Green).rounded_full().small().child("JavaScript")) .child(Tag::color(ColorName::Purple).rounded_full().small().child("Python")) .child(Tag::color(ColorName::Red).rounded_full().small().child("Go")) ``` ## Tag Categories and Use Cases ### Status Tags ```rust // Task or item status Tag::success().child("Completed") Tag::warning().child("In Progress") Tag::danger().child("Failed") Tag::info().child("Pending Review") ``` ### Category Labels ```rust // Content categorization Tag::secondary().child("Technology") Tag::color(ColorName::Blue).child("Design") Tag::color(ColorName::Green).child("Development") Tag::color(ColorName::Purple).child("Marketing") ``` ### Priority Indicators ```rust // Priority levels Tag::danger().child("High Priority") Tag::warning().child("Medium Priority") Tag::secondary().child("Low Priority") ``` ### Feature Tags ```rust // Feature flags or attributes Tag::primary().small().child("New") Tag::success().small().child("Popular") Tag::info().small().child("Beta") Tag::warning().small().child("Limited") ``` ## Behavior Notes - Tags automatically adjust their appearance based on the current theme - Outline tags maintain border visibility across different backgrounds - Small tags use reduced padding and border radius for compact layouts - Custom colors support both light and dark theme adaptations - Tags are display components and don't include built-in interaction handlers - Multiple tags can be combined in flex layouts for tag clouds or lists - Border radius automatically scales based on tag size unless explicitly overridden ## Design Guidelines ### When to Use Tags - **Categorization**: Group content by type, topic, or theme - **Status Indication**: Show state, progress, or health status - **Metadata Display**: Present attributes, properties, or classifications - **Filtering**: Visual indicators for active filters or selections - **Feature Flags**: Highlight new, beta, or special features ### Color Usage - **Semantic Colors**: Use danger (red) for errors, success (green) for completion, warning (yellow) for caution, info (blue) for information - **Category Colors**: Use the ColorName variants for content categorization where color coding helps with recognition - **Custom Colors**: Reserve for brand colors or specific design system requirements ### Size Guidelines - **Small Tags**: Use for compact layouts, metadata, or when space is limited - **Medium Tags**: Default size for most use cases, provides good readability and click targets - **Rounding**: Use `rounded_full()` for pill-style tags, custom `rounded()` for specific design requirements ## API Reference ### Tag Creation Methods | Method | Description | | --------------------------- | ------------------------------------------ | | `primary()` | Create a primary tag (blue theme) | | `secondary()` | Create a secondary tag (gray theme) | | `danger()` | Create a danger tag (red theme) | | `success()` | Create a success tag (green theme) | | `warning()` | Create a warning tag (yellow/orange theme) | | `info()` | Create an info tag (blue theme) | | `color(ColorName)` | Create a tag with predefined color | | `custom(color, fg, border)` | Create a tag with custom HSLA colors | ### Style Methods | Method | Description | | ----------------- | -------------------------------------------- | | `outline()` | Apply outline style (transparent background) | | `rounded(radius)` | Set custom border radius | | `rounded_full()` | Apply full rounding (pill shape) | ### Size Methods (from Sizable trait) | Method | Description | | ----------------- | -------------------------------- | | `small()` | Small tag size (reduced padding) | | `with_size(size)` | Set custom size | ### Content Methods (from ParentElement trait) | Method | Description | | ---------------- | ---------------------------- | | `child(element)` | Add child content to the tag | --- # Marker Source: /component/marker `Marker` is a lightweight row for status text, timeline boundaries, unread labels, and system notices. It deliberately accepts arbitrary children instead of defining an application-specific status enum. `MarkerIcon` and `MarkerContent` are optional typed slots for the common icon-and-label shape; direct children remain available for custom composition. `Marker` is a layout and loading primitive. It does not own a notification record, an unread count, a click action, or a live status store. Compose those with application state and existing `Badge`, `Button`, `Link`, or navigation components. ## Import ```rust use gpui_kit::{ParentElement as _, StyleRefinement, Styled as _}; use gpui_kit::component::{ ActiveTheme as _, Icon, IconName, Sizable as _, badge::Badge, button::{Button, ButtonVariants as _}, marker::{ Marker, MarkerAlignment, MarkerContent, MarkerIcon, MarkerLoadingStyle, MarkerVariant, }, shimmer::{ShimmerStyle, ShimmerText}, spinner::Spinner, }; use std::time::Duration; ``` ## Anatomy and basic usage The typed form keeps icon and content style targets independent: ```rust Marker::new() .icon(MarkerIcon::new().child(Icon::new(IconName::CircleCheck))) .content(MarkerContent::new().text("Online")) ``` Direct children are useful when a marker needs an application-specific layout: ```rust Marker::new() .child(Icon::new(IconName::Info)) .child("Conversation archived") ``` The default state is: | Property | Default | Meaning | | --- | --- | --- | | Variant | `Plain` | A full-width status row without divider decoration. | | Alignment | unset | `Separator` centers its label; every other variant starts at the leading edge. | | Loading | `false` | No automatic loading effect. | | Loading style | `Spinner` | Used when loading is enabled. | | Icon slot | absent | A spinner is inserted only for spinner loading with no icon. | | Content | absent | Add text or arbitrary child content. | | Row minimum height | `rems(1.)` | Follows the shared typography scale. | | Row gap | `gap_2()` | Shared compact spacing. | Use `MarkerContent::text(...)` for text that should receive the loading shimmer. Use `.child(...)` for arbitrary elements or text that should keep its own rendering behavior. ## Variants ### Plain `Plain` is the default compact status row: ```rust Marker::new() .text_color(cx.theme().success) .icon(MarkerIcon::new().child(Icon::new(IconName::CircleCheck))) .content(MarkerContent::new().text("Synced")) ``` The library does not define `Online`, `Read`, `Typing`, or `Synced` values. The application supplies the words, icon, and semantic color so the same primitive can serve different domains. ### Separator `Separator` adds a flexible line on each side of the content: ```rust Marker::new() .with_variant(MarkerVariant::Separator) .content(MarkerContent::new().text("Today")) ``` The line is an internal 1-pixel decorative element. The label remains the semantic content. Use `separator_style(...)` to refine the two lines without having to recreate their layout: ```rust Marker::new() .with_variant(MarkerVariant::Separator) .separator_style( StyleRefinement::default() .bg(cx.theme().ring), ) .content(MarkerContent::new().text("Yesterday")) ``` ### Border `Border` adds a semantic bottom border and compact bottom padding: ```rust Marker::new() .with_variant(MarkerVariant::Border) .icon(MarkerIcon::new().child(Icon::new(IconName::Info))) .content(MarkerContent::new().text("3 unread messages")) ``` The border is a visual boundary. Keep the unread count and meaning in text so the state does not depend on color or a line alone. ## Alignment A marker spans the full row. `alignment(...)` decides where its children sit inside that row and how wrapped text lines align: ```rust Marker::new() .alignment(MarkerAlignment::Center) .icon(MarkerIcon::new().child(Icon::new(IconName::Info))) .content(MarkerContent::new().text("Messages are end-to-end encrypted")); Marker::new() .alignment(MarkerAlignment::End) .content(MarkerContent::new().text("Delivered")) ``` Unset, `Separator` centers its label between the two lines and every other variant starts at the leading edge. An explicit alignment applies to any variant; a separator then keeps only the line on the far side of its label, so `Start` draws the trailing line and `End` the leading one. Centered notices are the usual shape for a transcript's system rows, such as a stopped answer or a failed request with a retry action; an `End` marker trails a delivery state under an outgoing message. ## Loading styles Set `loading(true)` without changing the marker's variant or normal layout: ```rust Marker::new() .loading(true) .with_loading_style(MarkerLoadingStyle::Spinner) .content(MarkerContent::new().text("Loading messages…")); Marker::new() .loading(true) .with_loading_style(MarkerLoadingStyle::Shimmer) .content(MarkerContent::new().text("Thinking…")) ``` Spinner behavior is intentionally predictable: - `Spinner` is the default `MarkerLoadingStyle`. - If loading uses `Spinner` and no `MarkerIcon` was supplied, a compact `Spinner::new().xsmall()` is inserted automatically. - If the application supplies `MarkerIcon`, that icon wins and no automatic spinner is added. - `MarkerVariant::Separator` still renders its divider lines while loading. - `MarkerVariant::Border` still renders its border while loading. Shimmer is text-aware when content was added with `.text(...)`: ```rust Marker::new() .loading(true) .with_loading_style(MarkerLoadingStyle::Shimmer) .content(MarkerContent::new().text("Generating a response…")) ``` Arbitrary `MarkerContent` children are still supported. When there is no typed text child, the content slot receives a gentle opacity animation instead. Icons and separator lines stay static. When reduced motion is enabled, text is rendered without animation and the marker remains readable. ## Shimmer configuration Use one `ShimmerStyle` for a marker's text effect: ```rust Marker::new() .loading(true) .with_loading_style(MarkerLoadingStyle::Shimmer) .with_shimmer_style( ShimmerStyle::new() .duration(Duration::from_secs(3)) .highlight_color(cx.theme().primary) .spread(0.45) .reverse(true) .once(false), ) .content(MarkerContent::new().text("Processing files…")) ``` The `ShimmerStyle` defaults are a two-second repeating sweep, theme-aware highlight color, `0.3` normalized spread, left-to-right direction, and looping. `duration(...)` clamps values below one millisecond. `spread(...)` accepts a relative `f32` (clamped to `0.05..=1.0`) or an absolute `Pixels` half-width; non-finite values leave the current spread unchanged. `reverse(true)` changes the direction, and `once(true)` stops after one sweep. For a marker-independent loading label, use `ShimmerText` directly: ```rust ShimmerText::new("Uploading report.pdf…") .with_shimmer_style(ShimmerStyle::new().spread(0.4)) .text_sm() .text_color(cx.theme().muted_foreground) ``` `ShimmerText` inherits typography and text color through `Styled`, preserves wrapping and truncation, and uses the active theme's background and foreground to keep the highlight readable in light and dark modes. ## Icons, content, and interactive children `MarkerIcon` is a compact `size_4()` slot. `MarkerContent` is a `min_w_0()` slot, so a long label can choose its own wrapping or truncation: ```rust Marker::new() .icon(MarkerIcon::new().child(Icon::new(IconName::Bell))) .content( MarkerContent::new() .child("Unread notifications") .child(Badge::new().count(3)), ) ``` Interactive children are allowed, but `Marker` does not make the row itself a control: ```rust Marker::new() .content( MarkerContent::new() .text("New messages") .child(Button::new("open-messages").ghost().xsmall().label("Open")), ) ``` Use `Button` for an in-app command and `Link` for a URL. Keep focus and action semantics on those controls. If a whole marker should be clickable, compose a semantic control around the content at the application boundary instead of adding a click listener to this layout element. ## Custom styling and theme tokens `Marker`, `MarkerIcon`, and `MarkerContent` implement `Styled`. Refinements are applied after the default layout and theme colors: ```rust Marker::new() .px_3() .py_2() .rounded(cx.theme().radius) .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .icon(MarkerIcon::new().child(Icon::new(IconName::Star))) .content(MarkerContent::new().text("Pinned message")) ``` The separator lines have a separate `StyleRefinement`, so their color and height can be customized without changing the content or marker's own surface: ```rust Marker::new() .with_variant(MarkerVariant::Separator) .separator_style( StyleRefinement::default() .bg(cx.theme().border), ) .content(MarkerContent::new().text("New day")) ``` Prefer semantic theme roles (`muted_foreground`, `border`, `ring`, `accent`, and their foreground tokens) to raw colors. Radius, spacing, typography, and separator geometry follow the shared design scale; typed style refinements can adapt a marker to a denser toolbar or a larger empty-state boundary. ## Accessibility and motion guidance - Include the status, boundary, or unread count in text. Icons, border lines, opacity, and color are supporting cues only. - A marker is presentational by default. Set `.id(...)` and `.role(Role::Status)` on a row that reports streaming or loading progress so assistive technology announces its updates; the role needs the stable identity an id provides. - Keep interactive content in `Button` or `Link` so it receives keyboard focus, activation, and disabled state. For the current `Button` API, use a visible `.label(...)` when the action needs an accessible name; a tooltip is supplemental. - Do not use `Marker` as an unlabeled icon-only status. Add a visible or accessible text label when the icon has meaning. - `MarkerContent::text(...)` remains visible when reduced motion is enabled; only the shimmer frame updates are skipped. Arbitrary children also retain their static content. - Loading text should describe the operation (“Generating…”, “Uploading…”) rather than communicate only through animation. - Keep sufficient contrast after custom styling in both light and dark themes. ## When to use another component - Use `Badge` for only a count, dot, or short classification. - Use `Separator::horizontal().label(...)` when the product needs only a labeled divider and no marker loading or icon composition. - Use `Tag` for a standalone labeled status that is not part of a conversation row. - Use `h_flex()` when the row has no shared marker behavior. - Use `Message` or `Bubble` when the content is a conversational message with sender identity or a message surface. ## API reference ### `Marker` | Method | Default | Purpose | | --- | --- | --- | | `new()` | `Plain`, not loading, spinner style | Create a marker. | | `with_variant(MarkerVariant)` | `Plain` | Choose plain, separator, or border treatment. | | `alignment(MarkerAlignment)` | unset: `Separator` centers, others start | Place the children at the leading edge, the center, or the trailing edge. | | `loading(bool)` | `false` | Enable or disable loading rendering. | | `with_loading_style(MarkerLoadingStyle)` | `Spinner` | Choose spinner or shimmer. | | `with_shimmer_style(ShimmerStyle)` | default style | Configure text shimmer. | | `separator_style(StyleRefinement)` | theme border line | Refine separator lines. | | `id(ElementId)` | none | Give the marker a stable identity for the accessibility tree. | | `role(Role)` | presentational | Announce the row to assistive technology, e.g. `Role::Status` for streaming updates; requires `id(...)`. | | `icon(MarkerIcon)` | none | Add a typed icon slot. | | `content(MarkerContent)` | none | Add a typed content slot. | | `.child(element)` | — | Add arbitrary children. | | `Styled` methods | compact themed row | Refine the marker's layout, colors, and typography. | ### `MarkerIcon` | Method | Default | Purpose | | --- | --- | --- | | `new()` | empty `size_4()` slot | Create an icon slot. | | `.child(element)` | — | Add an icon, badge, spinner, or custom element. | | `Styled` methods | `size_4()` compact slot | Refine icon geometry and layout. | ### `MarkerContent` | Method | Default | Purpose | | --- | --- | --- | | `new()` | empty `min_w_0()` slot | Create content. | | `text(text)` | static text until loading is enabled | Add text that can receive shimmer. | | `.child(element)` | — | Add arbitrary rich content. | | `Styled` methods | inherited text and compact layout | Refine wrapping, colors, spacing, and typography. | ### Related types - [`MarkerVariant`] — `Plain`, `Separator`, and `Border`. - [`MarkerAlignment`] — `Start`, `Center`, and `End`. - [`MarkerLoadingStyle`] — `Spinner` or `Shimmer`. - [`ShimmerStyle`] and [`ShimmerText`] — reusable loading text controls. [Marker]: https://docs.rs/gpui-component/latest/gpui_component/marker/struct.Marker.html [MarkerIcon]: https://docs.rs/gpui-component/latest/gpui_component/marker/struct.MarkerIcon.html [MarkerContent]: https://docs.rs/gpui-component/latest/gpui_component/marker/struct.MarkerContent.html [MarkerVariant]: https://docs.rs/gpui-component/latest/gpui_component/marker/enum.MarkerVariant.html [MarkerAlignment]: https://docs.rs/gpui-component/latest/gpui_component/marker/enum.MarkerAlignment.html [MarkerLoadingStyle]: https://docs.rs/gpui-component/latest/gpui_component/marker/enum.MarkerLoadingStyle.html [ShimmerStyle]: https://docs.rs/gpui-component/latest/gpui_component/shimmer/struct.ShimmerStyle.html [ShimmerText]: https://docs.rs/gpui-component/latest/gpui_component/shimmer/struct.ShimmerText.html --- # Collapsible Source: /component/collapsible An interactive element which expands/collapses. ## Import ```rust use gpui_kit::component::collapsible::Collapsible; ``` ## Usage ### Details ```rust Collapsible::new() .max_w_128() .gap_1() .open(self.open) .child( "This is a collapsible component. \ Click the header to expand or collapse the content.", ) .content( "This is the full content of the Collapsible component. \ It is only visible when the component is expanded. \n\ You can put any content you like here, including text, images, \ or other UI elements.", ) .child( h_flex().justify_center().child( Button::new("toggle1") .icon(IconName::ChevronDown) .label("Show more") .when(open, |this| { this.icon(IconName::ChevronUp).label("Show less") }) .xsmall() .link() .on_click({ cx.listener(move |this, _, _, cx| { this.open = !this.open; cx.notify(); }) }), ), ) ``` We can use `open` method to control the collapsed state. If false, the `content` method added child elements will be hidden. ### Animated reveal Opt into a reversible, measured height reveal with a stable motion ID: ```rust Collapsible::new() .motion_id("advanced-options") .open(self.open) .content(options) ``` The content remains mounted while closed so it can be measured and immediately reverse if toggled mid-animation. Without `motion_id`, the component keeps the immediate mount/unmount behavior. See the [GPUI Base Motion guide](/base/motion) for timing, reduced-motion, and performance details. ### Basic A trigger beside the title, with a summary that stays visible. ```rust Collapsible::new() .open(self.is_open("order")) .child( h_flex() .justify_between() .child("Order #4189") .child( Button::new("order") .ghost() .xsmall() .icon(IconName::ChevronsUpDown) .on_click(cx.listener(|this, _, _, cx| this.toggle("order", cx))), ), ) .child(status_row) .content(order_details) ``` ### Row trigger The whole row is the trigger. ```rust Collapsible::new() .open(self.is_open("faq")) .child( h_flex() .id("faq") .justify_between() .on_click(cx.listener(|this, _, _, cx| this.toggle("faq", cx))) .child("How do I reset my password?") .child(chevron), ) .content("Click the Forgot Password link on the sign in page.") ``` ### Bottom trigger The trigger sits on the bottom edge of the card it opens. ```rust Collapsible::new() .open(self.is_open("usage")) .child(usage_summary) .content(usage_breakdown) ``` ### Settings Holds optional controls, keeping the default view short. ```rust Collapsible::new() .open(self.is_open("settings")) .child( Button::new("settings") .outline() .label("Notification settings") .on_click(cx.listener(|this, _, _, cx| this.toggle("settings", cx))), ) .content(notification_checkboxes) ``` ### Row actions Actions live beside the trigger, in the header and in every row. ```rust Collapsible::new() .open(self.is_open("api-keys")) .child( h_flex() .child(api_keys_trigger) .child(Button::new("add-key").ghost().xsmall().icon(IconName::Plus)), ) .content(api_key_rows) ``` ### Nested Panels nest to any depth. ```rust Collapsible::new() .open(self.is_open("components-dir")) .child(folder_row("components")) .content( Collapsible::new() .open(self.is_open("ui-dir")) .child(folder_row("ui")) .content(v_flex().children(["button.rs", "card.rs", "dialog.rs"])), ) ``` ### Profile Shows who someone is, and their details only on request. ```rust Collapsible::new() .open(self.is_open("profile")) .child( h_flex() .child(Avatar::new().name("Jason Lee").xsmall()) .child("@huacnlee") .child(chevron), ) .content(profile_fields) ``` [Collapsible]: https://docs.rs/gpui-component/latest/gpui_component/collapsible/struct.Collapsible.html --- # Dock Source: /component/dock Dock builds application workspaces from draggable tab groups, nested splits, and collapsible left, right, and bottom docks. It is the layout foundation used by Longbridge in production, not an isolated UI demo. `gpui-base` owns the data model, layout calculation, and drag-and-drop behavior. `gpui-component` supplies the polished controls and visual language. Use `gpui_kit::component::dock` when you want a Dock ready to fit into a real application. For the renderer-independent architecture and custom-renderer API, see [Dock — gpui-base](/base/dock). ## Create a dock area Create the area through `DockSkin`. Keep the returned skin if you want to change its appearance later. ```rust use gpui_kit::component::dock::{DockArea, DockSkin}; struct Workspace { dock_area: Entity, dock_skin: Rc, } impl Workspace { fn new(window: &mut Window, cx: &mut Context) -> Self { let (dock_area, dock_skin) = DockSkin::dock_area("main-dock", Some(1), window, cx); Self { dock_area, dock_skin } } } ``` The optional version belongs to your saved layout schema. Increase it when your application can no longer restore an older layout. ## Define a panel A styled Dock panel implements `BasePanel` for identity and persistence, and `Panel` for its title, tab, and toolbar presentation. ```rust use gpui_kit::component::dock::{BasePanel, Panel, PanelEvent}; struct FilesPanel { focus_handle: FocusHandle, } impl EventEmitter for FilesPanel {} impl Focusable for FilesPanel { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl BasePanel for FilesPanel { fn panel_name(&self) -> &'static str { "FilesPanel" } } impl Panel for FilesPanel { fn title(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { "Files" } } impl Render for FilesPanel { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div().size_full().p_3().child("Project files") } } ``` Wrap styled panels with `panel_handle`. This preserves the `gpui-component` panel chrome when the base Dock stores the panel behind its renderer-independent handle. ## Describe the initial layout `DockLayout` is a value: compose it before a window exists, serialize it, compare it, or generate it from application state. Tabs and splits can be nested freely. ```rust use gpui_kit::component::dock::{DockLayout, panel_handle}; let files = cx.new(|cx| FilesPanel { focus_handle: cx.focus_handle(), }); let editor = cx.new(|cx| EditorPanel::new(cx)); let layout = DockLayout::h_split() .child( DockLayout::tabs().panel_view(panel_handle(files), cx), Some(px(240.)), ) .child( DockLayout::tabs().panel_view(panel_handle(editor), cx), None, ); self.dock_area.update(cx, |area, cx| { area.set_center(layout, window, cx); }); ``` Use `h_split()` and `v_split()` for rows and columns and `tabs()` for a tab group. A `None` split size fills the remaining space. The Dock area also supports left, right, and bottom regions through `DockPlacement`. Panels can be added, removed, activated, zoomed, and moved at runtime; user operations emit `DockEvent`, including `LayoutChanged` for persistence. ## Restore and persist layouts Dump the entire workspace as `DockAreaState`, store it with Serde, then load it on the next launch. ```rust use gpui_kit::component::dock::DockAreaState; // Save. let state = self.dock_area.read(cx).dump(cx); let json = serde_json::to_string_pretty(&state)?; // Restore. let state: DockAreaState = serde_json::from_str(&json)?; self.dock_area.update(cx, |area, cx| { area.load(state, window, cx) })?; ``` Register every restorable panel name during application initialization. The registered factory recreates the styled panel behind a `panel_handle`. ```rust register_panel(cx, "FilesPanel", |state, window, cx| { let panel = cx.new(|cx| FilesPanel::from_state(state, window, cx)); panel_handle(panel) }); ``` Dock state retains compatibility with layouts saved by earlier releases. Keep a sensible fallback layout for removed application panels or deliberate schema changes. ## Move a panel to another group The panel list is the layout's source of truth. Each panel names its title, its body, the dock it starts in, and whether its tab can be closed. The Slint sample below is the workspace the gallery at the top of this page runs. ```rust // Panels start in the left, centre, right, or bottom dock. let workspace = DockLayout::h_split() .child(DockLayout::tabs().panel_view(panel_handle(explorer), cx), Some(px(240.))) .child(DockLayout::tabs().panel_view(panel_handle(editor), cx), None); ``` Dragging a tab rearranges the workspace. A drop on a group's tab strip joins that group; a drop on its left or right edge docks the panel beside it, which splits the centre into two panes. Close a panel with the tab's close button, collapse a dock with the chevron in its tab bar, and zoom the centre with the tab bar's zoom button. ```rust // The same operations the pointer performs are available on the area. area.update(cx, |area, cx| { area.add_panel(panel_handle(outline), DockPlacement::Right, None, window, cx); area.toggle_dock(DockPlacement::Bottom, window, cx); }); ``` ## Style the workspace `DockSkin` keeps rendering decisions outside the layout engine. You can configure the common panel presentation without changing Dock behavior: ```rust self.dock_skin.set_panel_style(PanelStyle::default(), cx); self.dock_skin.set_toggle_button_visible(true, cx); ``` For complete control, implement the renderer traits in `gpui-base`. The same layout data and operations can then drive an entirely different Dock style. ## Runnable example The repository includes a complete workspace with edge docks, runtime panel operations, layout persistence, and keyboard actions: ```sh cargo run -p example-dock ``` See [`examples/dock/src/main.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/examples/dock/src/main.rs) for the full implementation. --- # MessageScroller Source: /component/message-scroller `MessageScroller` coordinates a variable-height virtual list with the behavior conversation screens usually need: follow the live tail, keep the current anchor while older history is inserted, navigate to an unread row, and expose whether the reader has left the tail. The application owns the message collection, stable message IDs, unread meaning, row renderer, composer, and empty/error states. The component owns only virtual-list bookkeeping and the optional jump-to-latest affordance. There is one state entity per scroller. ## Import ```rust use std::{rc::Rc, time::Duration}; use gpui_kit::{ IntoElement as _, ParentElement as _, StyleRefinement, Styled as _, prelude::FluentBuilder as _, }; use gpui_kit::component::{ ActiveTheme as _, button::ButtonVariants as _, message_scroller::{MessageScroller, MessageScrollerState}, Sizable as _, v_flex, }; ``` ## Create state and choose the starting position Create the state beside the application-owned message vector. The constructor receives the entity context because GPUI's list scroll handler notifies the entity after the list releases its internal borrow: ```rust let scroller = cx.new(|cx| MessageScrollerState::new(messages.len(), cx)); cx.observe(&scroller, |_, _, cx| cx.notify()).detach(); ``` `MessageScrollerState::new(...)` starts with tail following enabled. That is the expected starting position for a live conversation. A saved thread or a deep link can choose a row after the initial data has been installed: ```rust let saved_index = messages .iter() .position(|message| message.id == saved_message_id) .unwrap_or(messages.len().saturating_sub(1)); scroller.update(cx, |state, cx| { state.reset(messages.len(), cx); let _ = state.scroll_to_item(saved_index, cx); }); ``` There is no `starting_position` field or persisted scroll-offset API. Keep the application's saved message ID or index, then resolve it to the current index after records are loaded. `reset(...)` replaces the known row count and re-engages tail following; call `scroll_to_item(...)` after it when the product needs a different initial row. ## Render rows and an empty state Pass an indexed renderer. GPUI virtualizes the rows, so the closure is called for the rows needed by the current viewport and overdraw region: ```rust let messages = Rc::new(messages.clone()); MessageScroller::new( "conversation", scroller.clone(), move |index, _window, _cx| { let Some(message) = messages.get(index) else { return gpui_kit::div().into_any_element(); }; gpui_kit::div() .id(("message-row", message.id)) .min_w_0() .child(render_message(message)) .into_any_element() }, ) .w_full() .h_96() ``` The row ID in this example belongs to the application. `MessageScroller` does not retain an index-to-ID map; the ID lets an application-owned row keep its own element-local state when data changes. An empty list is valid (`MessageScrollerState::new(0, cx)`), but the scroller does not invent an empty placeholder. Render the empty, loading, error, or permission-denied state in the surrounding view and mount the scroller once there are rows: ```rust if messages.is_empty() { empty_conversation_view.into_any_element() } else { MessageScroller::new("conversation", scroller.clone(), render_message) .into_any_element() } ``` Keep the empty state separate from the scroll region so it can provide a meaningful action such as “Start a new conversation” without pretending there is a message to scroll to. ## Append, streaming, and follow-tail behavior Update application data and virtual-list count together. Appending while the reader follows the tail keeps the latest row visible. Appending after the reader scrolls up preserves the reader's position and makes the built-in jump button available: ```rust messages.push(new_message); scroller.update(cx, |state, cx| { let _ = state.append(1, cx); }); cx.notify(); ``` Streaming token growth changes a row's height without changing the item count. Update the message body, then remeasure that row: ```rust messages[index].body.push_str(next_token); scroller.update(cx, |state, cx| { let _ = state.remeasure_items(index..index + 1, cx); }); cx.notify(); ``` `remeasure_items(...)` preserves an item anchor while recalculating the selected rows. Use `remeasure(...)` after a global width, typography, or theme change that can affect many row heights. When streaming creates a new message rather than growing an existing one, call `append(1, cx)` first; remeasure the row again if its first render and later content have different heights. The state readers make the follow-tail decision visible to the surrounding view: ```rust let following_tail = scroller.read(cx).is_following_tail(); let show_new_messages = scroller.read(cx).is_scrolled_up(); ``` `is_following_tail()` is true when the list follows appended content. `is_scrolled_up()` is true when there is scrollable content below the current viewport and the reader is away from the end. The component does not decide whether to show a toast, unread count, or “new messages” copy; use these readers to drive an application-owned indicator. Resume following and move to the latest row explicitly: ```rust scroller.update(cx, |state, cx| state.scroll_to_end(cx)); ``` Normal scrolling to the end also allows GPUI's list to resume tail following. ## Prepend earlier history Insert older records at the front and tell the state the number of inserted rows. `prepend(...)` uses GPUI list splicing to preserve the visible item anchor: ```rust let earlier_messages = load_earlier_messages(); let count = earlier_messages.len(); messages.splice(0..0, earlier_messages); scroller.update(cx, |state, cx| { let _ = state.prepend(count, cx); }); cx.notify(); ``` Use `splice(old_range, count, cx)` for replacements or deletions elsewhere in the collection. The range is half-open and must stay within the current item count; invalid ranges return `false` and leave the state unchanged. For a “Load earlier” control, keep the loading state in the application, fetch the records, splice the vector, then call `prepend`. Do not call `reset` for ordinary history pagination because reset intentionally returns to tail following and loses the incremental anchor semantics. ## Unread and arbitrary navigation Unread identity belongs to the application. Resolve a stable message ID to the current vector index, then use `scroll_to_item(...)`: ```rust if let Some(index) = messages.iter().position(|message| message.id == unread_id) { scroller.update(cx, |state, cx| { let _ = state.scroll_to_item(index, cx); }); } ``` `scroll_to_item(...)` returns `false` for an out-of-range index. It is the single navigation primitive: an unread boundary, a search result, a bookmarked message, a reply target, and a deep link all resolve to an index in the application first. The current API does not expose turn anchors, peek previews, visible IDs, or stable-ID navigation. Map domain IDs to the current index in the application; keep an index map if lookup cost matters. The scroller does not know whether a row is a turn, a reply, an unread boundary, or a search result. ## Dynamic row heights and structural updates Rows may contain multiline text, attachments, streamed content, or an application-owned composer and can therefore have different heights. The underlying GPUI list measures rendered rows. Keep the renderer's height-affecting data in the owning view and notify the state after a mutation: | Change | State operation | | --- | --- | | Add rows at the tail | `append(count, cx)` | | Add rows at the front | `prepend(count, cx)` | | Replace/delete a range | `splice(range, count, cx)` | | Token growth in known rows | `remeasure_items(range, cx)` | | Global width/font/theme change | `remeasure(cx)` | | Replace the whole conversation | `reset(item_count, cx)` | Do not mutate the vector length without the matching state operation. The renderer receives an index, so data and virtual-list count must remain aligned for the same render pass. ## Jump-to-latest controls The built-in jump button is enabled by default and appears when `is_scrolled_up()` becomes true. It is a configured `Button` with a secondary variant, icon-button sizing, full radius, arrow-down icon, theme border/background, and a localized tooltip label. It keeps the scroll action owned by the state: ```rust MessageScroller::new("conversation", scroller.clone(), render_message) .with_jump_button_label("Jump to newest") .with_jump_button_transition(Duration::from_millis(250)) ``` Use `Duration::ZERO` to disable the enter/leave transition. Reduced-motion preferences use the final state immediately regardless of the configured duration. Refine its style after the built-in defaults: ```rust MessageScroller::new("conversation", scroller.clone(), render_message) .with_jump_button_style( StyleRefinement::default() .bg(cx.theme().primary) .border_color(cx.theme().primary) .text_color(cx.theme().primary_foreground), ) ``` Use the renderer callback when the application needs a different Button variant, size, icon, or instance style. The callback receives the fully configured button and must return a `Button`; the built-in scroll action stays attached: ```rust MessageScroller::new("conversation", scroller.clone(), render_message) .with_jump_button_renderer(|button| button.outline().small().label("Latest")) ``` During its leave transition the built-in button is rendered disabled while its opacity reaches zero. A renderer should preserve that state rather than force a disabled button to be active. The button's accessible name comes from its `.label(...)` value; `with_jump_button_label(...)` supplies the tooltip label only. Set a visible label in `with_jump_button_renderer(...)` when the jump action needs a named accessible control. The current public renderer callback can change Button styling and content, but it does not expose a separate accessibility-label builder. Disable the built-in affordance when the surrounding view provides its own: ```rust MessageScroller::new("conversation", scroller.clone(), render_message) .jump_button(false) ``` Compose an application-owned button from `is_scrolled_up()` and `scroll_to_end(...)` when its placement, text, or accessibility contract needs to be product-specific. ## Scrollbar and style slots The root implements `Styled`, and the internal regions have separate style refinements: ```rust MessageScroller::new("conversation", scroller.clone(), render_message) .p_2() .bg(cx.theme().group_box) .with_content_style(StyleRefinement::default().bg(cx.theme().background)) .with_list_style(StyleRefinement::default().p_4()) .with_row_style(StyleRefinement::default().pb_6()) .scrollbar(false) ``` The boundaries are: - Root `Styled` methods refine the full-width element that owns the viewport. - `with_content_style(...)` refines the viewport containing the list and optional vertical scrollbar. - `with_list_style(...)` refines the GPUI virtual list after its default `px_3()` / `py_2()` padding. GPUI lists offset rows only vertically, so the horizontal padding component — the default and any refinement — is carried by every row wrapper. - `with_row_style(...)` refines the full-width wrapper around each rendered row; the default wrapper includes `pb_8()` between rows, like a CSS gap. The list's own bottom padding owns the gap between the last row and whatever sits below the transcript. - `scrollbar(false)` hides the built-in vertical scrollbar; it does not disable scrolling or remove keyboard/wheel interaction. - `with_bottom_fade(color)` fades the transcript's bottom edge into the given color, so a partially visible row melts into the surrounding surface instead of clipping mid-line. It shows only while the reader is away from the live edge — at the bottom nothing is clipped. Pass the color of the surface behind the scroller; the fade is off by default. Use theme roles such as `group_box`, `background`, `border`, and `foreground` for custom surfaces. Keep content padding in the surrounding conversation shell when it belongs to the shell's header/composer relationship; use list or row style when it belongs to every transcript row. ## Virtualization, accessibility, and application boundaries `MessageScroller` delegates viewport layout, variable-height measurement, scroll anchoring, and overdraw to GPUI's `ListState`. Only visible rows and the configured overdraw region need rendering. Keep row closures deterministic and avoid doing network work or mutating the message collection during rendering. For keyboard and screen-reader behavior: - Keep the scroller inside a layout with a real height and `min_h_0()` so the scroll region can receive wheel and keyboard navigation. - The transcript viewport announces itself as a log region (`Role::Log`), so assistive technology can treat appended rows as live additions.- Wheel scrolling over the transcript is contained: while the list can move, the event never scrolls an ancestor scroller; at the top or bottom edge it chains to the ancestor, matching platform scroll containers. - Give rows meaningful text and stable application IDs; an index by itself is not a user-facing label. - Give the jump control an explicit visible label when it must be exposed as a named accessible action. Its tooltip is supplemental. - Place “Load earlier”, retry, composer, and unread controls outside the list in semantic `Button` or `Link` controls. - Keep empty, loading, error, and permission states readable without relying on animation or scrollbar position. The component intentionally has no React-style Provider, Viewport, Content, or Item exports. GPUI's list already supplies those layers; an indexed renderer is the item boundary. It also has no turn-anchor, peek, visible-range, or stable-ID API. Those concepts vary by product and belong in the application model around this component. ## API reference ### `MessageScrollerState` | Method | Default/return | Purpose | | --- | --- | --- | | `new(item_count, cx)` | tail following enabled | Create state for the current row count. | | `item_count()` | current count | Read the virtual-list row count. | | `is_scrolled_up()` | `false` until away from tail | Report whether a jump/new-content affordance is useful. | | `is_following_tail()` | `true` initially | Report whether appended rows are followed. | | `reset(item_count, cx)` | re-engages tail | Replace the row count and reset list state. | | `splice(range, count, cx)` | `true` if valid | Replace a half-open range while preserving list bookkeeping. | | `append(count, cx)` | `splice` at tail | Add rows at the end. | | `prepend(count, cx)` | `splice` at index 0 | Add earlier rows while preserving the current anchor. | | `remeasure(cx)` | — | Remeasure all rows after global layout changes. | | `remeasure_items(range, cx)` | `true` if valid | Remeasure selected dynamic rows. | | `scroll_to_item(index, cx)` | `false` if out of range | Navigate to an arbitrary row index. | | `scroll_to_end(cx)` | tail following enabled | Move to the latest row and resume following. | ### `MessageScroller` | Method | Default | Purpose | | --- | --- | --- | | `new(id, state, renderer)` | scrollbar and jump button enabled | Create a virtualized scroller. | | `scrollbar(bool)` | `true` | Show or hide the internal scrollbar. | | `jump_button(bool)` | `true` | Show or hide the built-in jump control. | | `with_jump_button_label(label)` | `Jump to latest` | Set the jump tooltip/localized label. | | `with_content_style(style)` | empty refinement | Style the viewport and scrollbar region. | | `with_list_style(style)` | list `px_3()` / `py_2()` | Style the virtual list. | | `with_row_style(style)` | row `pb_8()` | Style every rendered row wrapper. | | `with_jump_button_style(style)` | themed secondary button | Refine the built-in button. | | `with_jump_button_renderer(callback)` | default Button | Adjust the configured button while keeping its action. | | `with_jump_button_transition(duration)` | 200 ms | Set enter/leave duration; reduced motion skips it. | | `with_bottom_fade(color)` | off | Fade the bottom edge into the surrounding surface color. | | `Styled` methods | full-size, clipped root | Style the outer scroller element. | [MessageScroller]: https://docs.rs/gpui-component/latest/gpui_component/message_scroller/struct.MessageScroller.html [MessageScrollerState]: https://docs.rs/gpui-component/latest/gpui_component/message_scroller/struct.MessageScrollerState.html --- # DescriptionList Source: /component/description-list A versatile component for displaying key-value pairs in a structured, organized layout. Supports both horizontal and vertical layouts, multiple columns, borders, and different sizes. Perfect for showing detailed information like metadata, specifications, or summary data. ## Import ```rust use gpui_kit::component::description_list::{DescriptionList, DescriptionItem, DescriptionText}; ``` ## Usage ### Basic Description List ```rust DescriptionList::new() .item("Name", "GPUI Kit", 1) .item("Version", "0.1.0", 1) .item("License", "Apache-2.0", 1) ``` ### Using DescriptionItem Builder ```rust DescriptionList::new() .children([ DescriptionItem::new("Name").value("GPUI Kit"), DescriptionItem::new("Description").value("UI components for building desktop applications"), DescriptionItem::new("Version").value("0.1.0"), ]) ``` ### Different Layouts ```rust // Horizontal layout (default) DescriptionList::horizontal() .item("Platform", "macOS, Windows, Linux", 1) .item("Repository", "https://github.com/MohsenDastaran/uni-kit", 1) // Vertical layout DescriptionList::vertical() .item("Name", "GPUI Kit", 1) .item("Description", "A comprehensive Rust desktop framework", 1) ``` ### Multiple Columns with Spans ```rust DescriptionList::new() .columns(3) .child(DescriptionItem::new("Name").value("GPUI Kit").span(1)) .children([ DescriptionItem::new("Version").value("0.1.0").span(1), DescriptionItem::new("License").value("Apache-2.0").span(1), DescriptionItem::new("Description") .value("Full-featured UI components for desktop applications") .span(3), // Spans all 3 columns DescriptionItem::new("Repository") .value("https://github.com/MohsenDastaran/uni-kit") .span(2), // Spans 2 columns ]) ``` ### With Separators ```rust DescriptionList::new() .item("Name", "GPUI Kit", 1) .item("Version", "0.1.0", 1) .separator() // Add a visual separator .item("Author", "Longbridge", 1) .item("License", "Apache-2.0", 1) ``` ### Different Sizes ```rust // Large size DescriptionList::new() .large() .item("Title", "Large Description List", 1) // Medium size (default) DescriptionList::new() .item("Title", "Medium Description List", 1) // Small size DescriptionList::new() .small() .item("Title", "Small Description List", 1) ``` ### Without Borders ```rust DescriptionList::new() .bordered(false) // Remove borders for a cleaner look .item("Name", "GPUI Kit", 1) .item("Type", "UI Library", 1) ``` ### Custom Label Width (Horizontal Layout) ```rust use gpui_kit::px; DescriptionList::horizontal() .label_width(px(200.0)) // Set custom label width .item("Very Long Label Name", "Short Value", 1) .item("Short", "Very long value that needs more space", 1) ``` ### Rich Content with Custom Elements ```rust use gpui_kit::component::text::markdown; DescriptionList::new() .columns(2) .children([ DescriptionItem::new("Name").value("GPUI Kit"), DescriptionItem::new("Description").value( markdown( "UI components for building **fantastic** desktop applications.", ).into_any_element() ), ]) ``` ### Complex Example with Mixed Content ```rust DescriptionList::new() .columns(3) .label_width(px(150.0)) .children([ DescriptionItem::new("Project Name").value("GPUI Kit").span(1), DescriptionItem::new("Version").value("0.1.0").span(1), DescriptionItem::new("Status").value("Active").span(1), DescriptionItem::Separator, // Full-width separator DescriptionItem::new("Description").value( "A comprehensive Rust desktop framework built on GPUI" ).span(3), DescriptionItem::new("Repository").value( "https://github.com/MohsenDastaran/uni-kit" ).span(2), DescriptionItem::new("License").value("Apache-2.0").span(1), DescriptionItem::new("Platforms").value("macOS, Windows, Linux").span(2), DescriptionItem::new("Language").value("Rust").span(1), ]) ``` ### User Profile Information ```rust DescriptionList::new() .columns(2) .bordered(true) .children([ DescriptionItem::new("Full Name").value("John Doe"), DescriptionItem::new("Email").value("john@example.com"), DescriptionItem::new("Phone").value("+1 (555) 123-4567"), DescriptionItem::new("Department").value("Engineering"), DescriptionItem::Separator, DescriptionItem::new("Bio").value( "Senior software engineer with 10+ years of experience in Rust and system programming." ).span(2), ]) ``` ### System Information ```rust DescriptionList::vertical() .small() .bordered(false) .children([ DescriptionItem::new("Operating System").value("macOS 14.0"), DescriptionItem::new("Architecture").value("Apple Silicon (M2)"), DescriptionItem::new("Memory").value("16 GB"), DescriptionItem::new("Storage").value("512 GB SSD"), DescriptionItem::new("GPU").value("Apple M2 10-core GPU"), ]) ``` ### Product Specifications ```rust DescriptionList::new() .columns(3) .large() .children([ DescriptionItem::new("Model").value("MacBook Pro").span(1), DescriptionItem::new("Year").value("2023").span(1), DescriptionItem::new("Screen Size").value("14-inch").span(1), DescriptionItem::new("Processor").value("Apple M2 Pro").span(2), DescriptionItem::new("Base Price").value("$1,999").span(1), DescriptionItem::Separator, DescriptionItem::new("Key Features").value( "Liquid Retina XDR display, ProMotion technology, P3 wide color gamut" ).span(3), ]) ``` ### Configuration Settings ```rust DescriptionList::horizontal() .label_width(px(180.0)) .bordered(false) .children([ DescriptionItem::new("Theme").value("Dark Mode"), DescriptionItem::new("Font Size").value("14px"), DescriptionItem::new("Auto Save").value("Enabled"), DescriptionItem::new("Backup Frequency").value("Every 30 minutes"), DescriptionItem::new("Language").value("English (US)"), ]) ``` ## Design Guidelines - Use horizontal layout for simple key-value pairs - Use vertical layout when values are lengthy or complex - Limit columns to 3-4 for optimal readability - Use separators to group related information - Keep labels concise and descriptive - Use consistent spacing with the size prop - Consider removing borders for embedded contexts --- # Shimmer Source: /component/shimmer `ShimmerText` renders readable text with a moving highlight for short-lived loading or generated-content states. `ShimmerStyle` is the reusable appearance and timing value shared by `ShimmerText`, `Marker`, and attachment titles. The utility keeps text as the layout owner, so typography, wrapping, and truncation remain ordinary GPUI text behavior. It does not replace text with a skeleton block, own loading state, or announce progress to assistive technology. Keep a meaningful label in the text and let the surrounding application own the operation state. ## When to use - “Thinking…” or “Generating…” while an AI response is being produced. - File titles in an `Uploading` or `Processing` state. - Lightweight text placeholders for short background work. Use `Skeleton` for placeholder layout blocks, `Spinner` for a rotating indeterminate control, and plain text when the state does not need motion. ## Import ```rust use std::time::Duration; use gpui_kit::{ParentElement as _, Styled as _}; use gpui_kit::component::{ shimmer::{ShimmerStyle, ShimmerText}, ActiveTheme as _, }; ``` ## Basic usage Use the default theme-aware shimmer for a loading label: ```rust ShimmerText::new("Thinking…") ``` `ShimmerText` implements `Styled`, so it inherits the surrounding text style and can be refined like other GPUI elements: ```rust ShimmerText::new("Generating a response…") .text_sm() .text_color(cx.theme().muted_foreground) .max_w_full() ``` The default configuration is: | Property | Default | Meaning | | --- | --- | --- | | Duration | 2 seconds | One complete sweep. | | Highlight color | theme-aware | Derived from the active text/background/theme. | | Spread | relative `0.3` | Highlight half-width as a fraction of text width; a fixed `Pixels` width is also accepted. | | Direction | left to right | `reverse(false)`. | | Repetition | looping | `once(false)`. | | Reduced motion | static text | Animation frames are skipped while text remains visible. | The highlight follows the active theme rather than assuming a white highlight. This keeps the effect legible in both light and dark themes. An explicit color is available when a product's semantic accent requires it. ## Configure `ShimmerStyle` Create a reusable style when multiple labels should share the same motion: ```rust let loading_style = ShimmerStyle::new() .duration(Duration::from_secs(3)) .highlight_color(cx.theme().primary) .spread(0.45) .reverse(true) .once(false); ShimmerText::new("Indexing files…").with_shimmer_style(loading_style); ShimmerText::new("Building response…").with_shimmer_style(loading_style); ``` The individual configuration methods are also available directly on `ShimmerText`: ```rust ShimmerText::new("Uploading…") .duration(Duration::from_secs(4)) .spread(0.5) .reverse(true) .once(true) ``` Use `with_shimmer_style(...)` when the style is shared or built conditionally; use the direct methods when a one-off label is clearer. ### Duration `duration(...)` sets one complete sweep. Values below one millisecond are clamped to one millisecond, so a zero duration does not disable animation. Use `once(true)` for a one-shot effect or render ordinary text when the operation is not loading. ```rust ShimmerText::new("Preparing preview…") .duration(Duration::from_millis(900)) ``` ### Highlight color Leave `highlight_color` unset to use the theme-aware default. Use a semantic theme color when the loading state belongs to a product accent: ```rust ShimmerText::new("Syncing…") .highlight_color(cx.theme().primary) ``` An explicit color is painted over the text and must have enough contrast in both themes. Avoid raw palette values in component call sites; use `cx.theme().primary`, `muted_foreground`, or another semantic token. ### Spread `spread(...)` controls the highlight half-width. A bare `f32` is relative to the text width: finite values are clamped to the inclusive `0.05..=1.0` range. A `Pixels` value is an absolute half-width with a one-pixel minimum, keeping the band the same physical width across labels of different lengths. Non-finite values leave the current spread unchanged: ```rust ShimmerText::new("Loading a narrow label…").spread(0.15); ShimmerText::new("Loading a broad label…").spread(0.7); ShimmerText::new("Fixed-width highlight…").spread(px(48.)); ``` Use a smaller spread for dense status rows and a broader spread for a short assistant label. Prefer the relative form so the text's width remains the scale; use an absolute spread when aligned labels should share one band width. ### Direction and play-once `reverse(true)` sweeps from right to left. `once(true)` completes one sweep and does not loop: ```rust ShimmerText::new("Finalizing…") .reverse(true) .once(true) ``` There is no public angle, pause, progress, or RTL-specific builder. If a product needs those behaviors, keep the loading text static or own a separate animation component until the API is intentionally extended. ## Compose with Marker `Marker` uses `ShimmerStyle` only when it is loading with the `Shimmer` style. Use `MarkerContent::text(...)` to give the component a text run that can receive the highlight: ```rust use gpui_kit::component::marker::{Marker, MarkerContent, MarkerLoadingStyle}; Marker::new() .loading(true) .with_loading_style(MarkerLoadingStyle::Shimmer) .with_shimmer_style( ShimmerStyle::new() .duration(Duration::from_secs(3)) .spread(0.4) .reverse(true), ) .content(MarkerContent::new().text("Searching conversation history…")) ``` If `MarkerContent` contains only arbitrary elements, Marker uses a gentle opacity animation for the content slot instead of trying to repaint those elements as text. Icons and separator lines remain static. The spinner loading style does not use shimmer. ## Compose with Attachment An attachment title automatically shimmers while its inherited or explicit status is `Uploading` or `Processing`. Customize that title without replacing the attachment composition: ```rust use gpui_kit::component::attachment::{ Attachment, AttachmentContent, AttachmentDescription, AttachmentStatus, AttachmentTitle, }; Attachment::new() .status(AttachmentStatus::Processing) .content( AttachmentContent::new() .title( AttachmentTitle::new("transcript.pdf").with_shimmer_style( ShimmerStyle::new() .highlight_color(cx.theme().primary) .spread(0.45), ), ) .description(AttachmentDescription::new("Processing document…")), ) ``` The title's explicit status overrides the parent status. Generic children added with `AttachmentContent::child(...)` do not inherit attachment state because their concrete type is erased; use the typed title builder when the loading behavior matters. ## Use with messages and bubbles `ShimmerText` is an ordinary element and can be placed anywhere a text child is accepted: ```rust use gpui_kit::component::{ bubble::{Bubble, BubbleContent, BubbleVariant}, message::{Message, MessageContent}, }; Message::new() .content( MessageContent::new().bubble( Bubble::new() .with_variant(BubbleVariant::Ghost) .content(BubbleContent::new().child( ShimmerText::new("The assistant is thinking…"), )), ), ) ``` The application should switch from shimmer text to the final message content when generation completes. Do not leave an animated label running after the operation has ended. ## Styling, theme, and reduced motion `ShimmerText` implements `Styled`; style its font, size, color, wrapping, and layout at the call site: ```rust ShimmerText::new("Loading project data…") .text_base() .font_medium() .text_color(cx.theme().foreground) .max_w_full() ``` The animation reads the active theme's foreground, background, and dark/light mode when no explicit highlight color is provided. A custom theme therefore changes the default shimmer without requiring per-label overrides. Explicit colors remain the caller's responsibility for contrast. When `cx.reduce_motion()` is true, `ShimmerText` renders `StyledText` without requesting animation frames. Marker follows the same rule for typed text and keeps arbitrary content static. This is a rendering behavior, not a separate builder option; applications should keep the label meaningful in both modes. ## Accessibility guidance - Keep a meaningful text label visible to assistive technology. “Thinking…” or “Uploading report.pdf…” is more useful than an unlabeled animated band. - Do not rely on the highlight color, direction, or motion to communicate success, failure, or percentage. - Stop or replace the shimmer when the operation completes, fails, or is cancelled. - Respect reduced-motion preferences. The utility leaves static text in place, so no separate motion-only fallback is required. - Use semantic `Button` or `Link` controls for cancel, retry, and navigation; shimmer itself is not interactive. - Verify an explicit highlight color in both light and dark themes and avoid low-contrast combinations. ## When not to use Shimmer Shimmer communicates activity, not progress. - Use `Progress` for a known percentage. - Use `Spinner` for a compact rotating indicator. - Use `Skeleton` for multi-line placeholder layout. - Render ordinary text once a stable, completed, or failed state exists; do not leave the animation running. ## API reference ### `ShimmerStyle` | Method | Default | Purpose | | --- | --- | --- | | `new()` | same as `Default` | Create a theme-aware two-second looping style. | | `duration(Duration)` | 2 seconds | Set one sweep duration; clamps below 1 ms. | | `highlight_color(Hsla)` | theme-aware | Override the highlight color. | | `spread(f32 \| Pixels)` | relative `0.3` | Set half-width: `f32` is relative and clamps to `0.05..=1.0`; `Pixels` is absolute with a 1px minimum. | | `reverse(bool)` | `false` | Reverse the sweep direction. | | `once(bool)` | `false` | Play one sweep instead of looping. | ### `ShimmerText` | Method | Default | Purpose | | --- | --- | --- | | `new(text)` | default style, generated identity | Create loading text. | | `id(ElementId)` | text-based identity | Distinguish identical sibling labels. | | `with_shimmer_style(ShimmerStyle)` | default style | Apply a reusable configuration. | | `duration(Duration)` | 2 seconds | Set duration directly. | | `highlight_color(Hsla)` | theme-aware | Set color directly. | | `spread(f32 \| Pixels)` | relative `0.3` | Set spread directly. | | `reverse(bool)` | `false` | Set direction directly. | | `once(bool)` | `false` | Set repetition directly. | | `Styled` methods | inherited text style | Refine typography, color, wrapping, and layout. | ### Related components - [`Marker`] — status rows with spinner or shimmer loading styles. - [`AttachmentTitle`] — status-aware file title with shimmer customization. - [`Progress`] — determinate progress. - [`Spinner`] — compact indeterminate progress. [ShimmerStyle]: https://docs.rs/gpui-component/latest/gpui_component/shimmer/struct.ShimmerStyle.html [ShimmerText]: https://docs.rs/gpui-component/latest/gpui_component/shimmer/struct.ShimmerText.html [Marker]: https://docs.rs/gpui-component/latest/gpui_component/marker/struct.Marker.html [AttachmentTitle]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentTitle.html [Progress]: https://docs.rs/gpui-component/latest/gpui_component/progress/struct.Progress.html [Spinner]: https://docs.rs/gpui-component/latest/gpui_component/spinner/struct.Spinner.html --- # StatusBar Source: /component/status-bar StatusBar is a horizontal bar split into three regions — `left`, `center`, and `right`. It is usually placed at the bottom of a window or pane to show contextual information and quick actions. The design mirrors the status bars found in native UI frameworks: Windows `StatusStrip`, WPF `StatusBar`, and macOS `NSStatusBar`. ## Import ```rust use gpui_kit::component::status_bar::StatusBar; ``` ## Regions Pass any `impl IntoElement` — a string, an `Icon`, a `Button`, a custom layout, etc. — to a region. `left` and `right` pin items to each end; `child` / `children` add to the center, whose alignment follows the pinned ends — centered with both `left` and `right`, end-aligned with only `left`, start-aligned otherwise (only `right`, or neither, like a plain container). Call a method multiple times to add more. - For a **non-interactive label**, pass a plain string — it inherits the bar's text style and has no hover. - For a **clickable button**, pass a ghost, xsmall `Button` — `Button::new(id).ghost().xsmall()` — so buttons stay a consistent size. Chain `label`, `icon`, `tooltip`, `on_click`, etc. - For a **separator**, pass `Separator::vertical()`. - For anything else, pass the element directly. ## Usage ### Labels ```rust StatusBar::new() .left("Ready") .child("README.md") .right("UTF-8") ``` ### Buttons ```rust StatusBar::new() .left( Button::new("branch").ghost().xsmall() .icon(IconName::Github) .label("main") .on_click(|_, window, cx| { /* ... */ }), ) .right( Button::new("go-to-line").ghost().xsmall() .label("Ln 1, Col 1") .tooltip("Go to Line/Column") .on_click(cx.listener(|this, _, window, cx| { /* ... */ })), ) ``` ### Separators and custom elements ```rust StatusBar::new() .left(Button::new("branch").ghost().xsmall().icon(IconName::Github).label("main")) .left(Separator::vertical()) .left( // Any custom element works. h_flex() .items_center() .gap_1() .child(Icon::new(IconName::CircleCheck).xsmall()) .child("0 problems"), ) .child(Progress::new("indexing").value(60.).w_24()) ``` ### Custom styling `StatusBar` implements `Styled`, so any style method overrides the defaults. ```rust StatusBar::new() .bg(cx.theme().secondary) .border_color(cx.theme().border) .left("Ready") ``` ## Notes - The center (via `child` / `children`) is centered with both `left` and `right`, end-aligned with only `left`, and start-aligned otherwise (only `right`, or neither — like a plain container). - Use a plain string (or any non-interactive element) for read-only items to avoid the button hover effect; use a ghost xsmall `Button` only for clickable items. - Colors come from the `status_bar` (background) and `status_bar_border` theme tokens, which fall back to `background` / `border`. ## API Reference ### StatusBar | Method | Description | | ----------------- | ---------------------------------------------------- | | `new()` | Create a new, empty status bar | | `left(child)` | Append an element to the left region (call to add more) | | `right(child)` | Append an element to the right region | | `child(c)` / `children(cs)` | Add element(s) to the center region | Each region method takes `impl IntoElement`. `StatusBar` also implements `Styled`, so style methods (`bg`, `border_color`, `py`, etc.) can override the defaults. --- # Command Source: /component/command A command palette is a filtered list of commands with groups, Action-derived keybinding hints, and keyboard navigation. Use it inline or compose it into an existing dialog for a `⌘K`-style menu. On invalidation, Command creates and layout-measures every flattened row; `v_virtual_list` then renders and paints only viewport rows. `Command` owns the entries and presentation policy. `CommandState` owns the interaction state: query input, focus, selection, scrolling, and loading. ## Import ```rust use gpui_kit::component::command::{Command, CommandEntry, CommandGroup, CommandItem, CommandState}; ``` ## Composition Build the palette structure directly on `Command`; create an empty state once and reuse it while the palette is shown. ```text Command ├── CommandItem // ungrouped ├── CommandGroup │ ├── CommandItem │ └── CommandItem ├── separator └── CommandGroup ├── CommandItem └── CommandItem CommandState // query, focus, selection, scrolling ``` ## Usage ### Inline Define Actions and bindings in application setup. The default row resolves an Action's active binding in the Command focus scope and then at application scope, rendering a `Kbd` hint only when it finds one. ```rust use gpui_kit::{actions, KeyBinding}; actions!(my_app, [OpenProfile, OpenBilling]); // During application setup: cx.bind_keys([ KeyBinding::new("cmd-p", OpenProfile, Some("Command")), KeyBinding::new("cmd-b", OpenBilling, Some("Command")), ]); let state = cx.new(|cx| CommandState::new(window, cx)); Command::new(&state) .group( CommandGroup::new().label("Suggestions") .item(CommandItem::new().label("Calendar").icon(IconName::Calendar)) .item(CommandItem::new().label("Search Emoji").icon(IconName::Search)) .item(CommandItem::new().label("Calculator").disabled(true)), ) .separator() .group( CommandGroup::new().label("Settings") .item( CommandItem::new().label("Profile") .icon(IconName::User) .action(Box::new(OpenProfile)), ) .item( CommandItem::new().label("Billing") .action(Box::new(OpenBilling)), ), ) .placeholder("Type a command or search...") .empty(|_, _, cx| { v_flex() .items_center() .gap_2() .child(Icon::new(IconName::Search).size_8()) .child("No results found.") }) .w(px(380.)) ``` Do not provide a manually formatted shortcut string. `CommandItem::action` provides both the executable behavior and, for the default row, the displayed binding. A custom row owns its complete presentation, including any key hint. ### Quick Actions Without Search Disable search for a compact action palette. It has no search field, retains all entries, and `state.focus(window, cx)` focuses the Command frame so its arrow, Enter, and Escape actions remain available. ```rust let actions = cx.new(|cx| CommandState::new(window, cx)); Command::new(&actions) .searchable(false) .items([ CommandItem::new().label("New File").icon(IconName::Plus), CommandItem::new().label("Duplicate").icon(IconName::Copy), CommandItem::new().label("Move to Trash").icon(IconName::Delete), ]) .w(px(380.)) ``` With the default `.searchable(true)`, `state.focus(window, cx)` and [`Focusable::focus_handle`] target the search input instead. A non-searchable palette never invokes `on_query`. ### In a Dialog Compose the palette with the existing [`WindowExt::open_dialog`] API. `header` renders above the optional search field and list; `footer` renders below the list. In a searchable palette, Escape clears a non-empty query. Otherwise— including a non-searchable palette with a hidden programmatic query—Command calls `on_cancel` and then propagates Cancel. Let the hosting Dialog perform dismissal—do not close it again in `on_cancel`. ```rust use gpui_kit::component::WindowExt as _; let state = self.command_state.clone(); window.open_dialog(cx, move |dialog, _, _| { let state = state.clone(); dialog.close_button(false).p_0().content(move |content, _, _| { content.child( Command::new(&state) .bordered(false) .placeholder("Type a command or search...") .items([ CommandItem::new().label("Profile"), CommandItem::new().label("Billing"), ]) .on_confirm(|index, window, cx| { window.push_notification(format!("Selected {index}"), cx); }) // Record local cleanup only; Dialog handles the propagated Cancel. .on_cancel(|window, cx| { window.push_notification("Command palette cancelled", cx); }) .header(|state, _, cx| { h_flex() .justify_between() .px_3() .py_2() .border_b_1() .border_color(cx.theme().border) .child("Commands") .child(format!("{} matches", state.matched_count())) }) .footer(|_, _, cx| { h_flex() .gap_3() .px_3() .py_2() .border_t_1() .border_color(cx.theme().border) .child("↑↓ Navigate") .child("Enter Select") .child("Escape Close") }), ) }) }); ``` ### Callbacks and Actions Callbacks are configured on `Command`, not subscribed from `CommandState`. They notify the palette owner directly: ```rust Command::new(&state) .items(entries) .on_query(|query, window, cx| { // Start or update an application-owned search. }) .on_select(|index, window, cx| { // Preview the newly highlighted IndexPath. }) .on_confirm(|index, window, cx| { // Finish with this IndexPath, whether or not it has an Action. }) .on_cancel(|window, cx| { // Clean up local palette state before Cancel propagates. }) ``` An `IndexPath` always addresses the model supplied by the latest `Command` render, before local filtering. Items passed to `.items(...)` are in section 0, with `row` equal to their position in that iterator. Explicit groups use their group and item positions; when both forms are mixed, they follow the implicit ungrouped section. Filtering changes what is visible, not these coordinates. `on_query` runs only when a searchable query actually changes. Refiltering can move the highlight, so its `on_select` runs first when the selected `IndexPath` changes; then `on_query` runs. These callbacks, and `on_confirm`, are delivered after the current `CommandState` update releases its lease. Keyboard and pointer highlight changes run `on_select` but never dispatch an Action. While the source window remains live, confirming an enabled item dispatches its Action first and then invokes `on_confirm`; if that Action closes the window, the callback cannot be delivered. An item without an Action still invokes `on_confirm`. In a searchable palette, Escape clears a non-empty query. Otherwise—including a non-searchable palette with a hidden programmatic query—it invokes `on_cancel`, then propagates Cancel. ### Dynamic Entries Keep asynchronous or changing entries in the owner view, then reconstruct the Command from the owner's current data when that view renders. Do not mutate the state with an entry builder or `set_entries`. ```rust struct StockSearch { state: Entity, results: Vec, } impl StockSearch { fn render_palette(&self, owner: WeakEntity) -> Command { let results = self.results.clone(); Command::new(&self.state) .items(results) .on_query(move |query, window, cx| { _ = owner.update(cx, |this, cx| this.search(query, window, cx)); }) } } ``` The installed model remains in `CommandState` while query, selection, and scrolling change, so those interactions do not need an owner rerender. A later owner render installs the new model, preserves the selected `IndexPath` when it is still present, and remeasures rows. ### Scrollable ```rust Command::new(&state) .max_h(px(220.)) .w(px(380.)) ``` ### Variable-height rows ```rust Command::new(&state) .items(variable_rows()) .w(px(380.)) ``` ### Search panel ```rust Command::new(&state) .placeholder("Search symbols or companies...") .w(px(380.)) ``` ### Last confirmed ```rust let value = self.last_command.clone(); ``` ## Searching Command uses a case-insensitive substring match against each item's label and keywords. Empty queries match every item. A group whose items all filter out hides its heading; a separator left leading, trailing, or adjacent to another separator is omitted. ```rust CommandItem::new().label("Profile") .keywords(["account", "user"]) ``` For custom or remote search, update owner-held entries in `on_query` and call `state.set_loading(true, window, cx)` while waiting so the empty message is suppressed. Render the new entries when the response arrives. ## Custom Rows and Virtualization `CommandItem::child` replaces an item's icon and label content with a lazy child factory. The factory can run more than once for measurement, viewport entry, and typography or width invalidation, so it must be side-effect-free. On invalidation, Command creates and layout-measures every flattened row before supplying independent sizes to `v_virtual_list`. Custom rows may therefore have different intrinsic heights; `v_virtual_list` still renders and paints only viewport rows. Build them for the available list width and keep their rendered content stable until the owner updates the entries. ```rust Command::new(&state) .item(CommandItem::new().label("compact").child(|_, _| { h_flex().w_full().py_1().child("Compact custom row") })) .item(CommandItem::new().label("expanded").child(|_, cx| { v_flex() .w_full() .py_4() .child("Expanded custom row") .child(div().text_xs().text_color(cx.theme().muted_foreground).child("Extra detail")) })) ``` ## Command | Method | Signature and description | | --- | --- | | `new` | `new(&Entity) -> Command` creates a palette for a state. | | `item` / `items` | `item(CommandItem) -> Self` and `items(impl IntoIterator) -> Self` add ungrouped entries. | | `group` / `separator` | `group(CommandGroup) -> Self` adds a group; `separator() -> Self` adds a divider. | | `searchable` | `searchable(bool) -> Self` shows or hides the search field and local filtering. Default: `true`. | | `on_query` | `on_query(F) -> Self`, where `F: Fn(&str, &mut Window, &mut App) + 'static`, runs after a searchable query changes. | | `on_select` | `on_select(F) -> Self`, where `F: Fn(IndexPath, &mut Window, &mut App) + 'static`, runs when the highlighted path changes. | | `on_confirm` | `on_confirm(F) -> Self`, with the same `IndexPath` callback bounds; while the source window remains live, runs after the confirmed Action dispatches. | | `on_cancel` | `on_cancel(F) -> Self`, where `F: Fn(&mut Window, &mut App) + 'static`, runs before Cancel propagates when Escape does not clear a searchable query. | | `placeholder` | `placeholder(impl Into) -> Self` sets the search-field placeholder. | | `empty` | `empty(F) -> Self` renders custom content when there are no matches. | | `max_h` | `max_h(impl Into) -> Self` sets the list maximum. Default: `18.75rem` (300px). | | `bordered` | `bordered(bool) -> Self` draws the surrounding border and rounding. Default: `true`. | | `header` | `header(F) -> Self`, where `F: Fn(&CommandState, &mut Window, &mut App) -> E + 'static` and `E: IntoElement`; renders above search and list. | | `footer` | `footer(F) -> Self`, with the same callback bounds; renders below the list. | `Command` implements [`Styled`], so `w`, `max_w`, `bg`, and other styles apply to the palette frame. ## CommandItem | Method | Description | | --- | --- | | `new` | Creates an item; Command generates its internal rendering identity. | | `label` | Sets the visible label and default search text. | | `icon` | Sets the leading icon for the default row. | | `action` | `action(Box) -> Self` sets the behavior dispatched on click or confirm. The default row displays its resolved binding. | | `checked` | Draws a trailing check. A resolved Action binding uses that position instead. | | `keywords` | Adds default-match terms. | | `disabled` | `Disableable::disabled(bool) -> Self` makes the item non-interactive and skips it during keyboard navigation. | | `child` | `child(F) -> Self`, where `F: Fn(&mut Window, &mut App) -> E + 'static` and `E: IntoElement`; lazily replaces the default row content. | ## CommandGroup | Method | Description | | --- | --- | | `new` | Creates an unlabeled group. | | `label` | Sets the group heading, which hides when all items filter out. | | `item` / `items` | Add one or many `CommandItem`s to the group. | | `heading` | Returns the optional heading. | `CommandEntry` is the public enum for an item, group, or separator. It is useful when an owner stores a mixed dynamic entry collection; replay each variant onto a newly constructed `Command` during rendering. ## CommandState | Method | Signature and description | | --- | --- | | `new` | `new(&mut Window, &mut Context) -> Self` creates empty interaction state. | | `query` / `set_query` | Read the query, or `set_query(query, window, cx)` as if it were typed. | | `selected_index` | Returns the highlighted item's original `IndexPath`; section identifies the top-level entry and row identifies the item within a group. | | `matched_count` | Returns the number of matching items. | | `focus` | `focus(&self, &mut Window, &mut App)` focuses the input when searchable, otherwise the Command frame. | | `set_loading` / `is_loading` | Show or read the search spinner; loading suppresses the empty message. | ## Keyboard Shortcuts | Key | Action | | --- | --- | | `↑` / `↓` | Move the highlight, wrapping around and skipping disabled items. | | `Enter` | Confirm the highlighted item. | | `Escape` | In a searchable palette, clear a non-empty query; otherwise call `on_cancel` and propagate `Cancel`. | ## Best Practices 1. Build static entries, groups, separators, searchability, and filters on `Command`. 2. Keep dynamic entries and asynchronous results in the palette owner; rebuild `Command` from them when rendering. 3. Bind real `Action`s instead of supplying shortcut text, so hints and dispatch stay in sync. 4. Keep `child` factories side-effect-free and use them for rows that need custom presentation or variable heights. 5. Let a hosting Dialog own cancellation after `on_cancel`; use header and footer for application-owned status and hints. 6. Give each independently rendered palette its own [`CommandState`]. [Command]: https://docs.rs/gpui-component/latest/gpui_component/command/struct.Command.html [CommandState]: https://docs.rs/gpui-component/latest/gpui_component/command/struct.CommandState.html [CommandGroup]: https://docs.rs/gpui-component/latest/gpui_component/command/struct.CommandGroup.html [WindowExt::open_dialog]: https://docs.rs/gpui-component/latest/gpui_component/trait.WindowExt.html#tymethod.open_dialog [Focusable::focus_handle]: https://docs.rs/gpui/latest/gpui/trait.Focusable.html#tymethod.focus_handle [Styled]: https://docs.rs/gpui/latest/gpui/trait.Styled.html --- # Empty Source: /component/empty `Empty` presents missing content, empty results, and first-use states. Its named slots provide the layout and visual hierarchy; the application decides when to show it and owns the state and actions of its children. The component is stateless and lives entirely in GPUI Component, using its theme and native controls. ## Import ```rust use gpui_kit::{ParentElement as _, Styled as _, rems}; use gpui_kit::assets::IconName; use gpui_kit::component::{ ActiveTheme as _, Icon, Sizable as _, avatar::{Avatar, AvatarGroup}, button::{Button, ButtonVariants as _}, empty::{ Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyMediaVariant, EmptyTitle, }, input::{Input, InputState}, link::Link, }; ``` Import the component through `gpui_kit::component::empty`; GPUI's own `gpui_kit::Empty` is a separate element that renders nothing. ## Basic usage ```rust Empty::new() .header( EmptyHeader::new() .media( EmptyMedia::new() .with_variant(EmptyMediaVariant::Icon) .child(Icon::new(IconName::Folder)), ) .title(EmptyTitle::new().child("No projects yet")) .description( EmptyDescription::new() .child("Create your first project to get started."), ), ) .content( EmptyContent::new() .flex_row() .flex_wrap() .justify_center() .gap_2() .child(Button::new("create-project").label("Create project…")) .child( Button::new("import-project") .outline() .label("Import project…"), ), ) .child( Link::new("empty-help") .href("https://gpui-kit.com/docs/getting-started") .text_sm() .child("Learn more"), ) ``` Attach normal Button callbacks for application actions. The extra root child appears after `EmptyContent`, so a help link can remain separate from the primary content group. ## Anatomy | Part | Composition | Purpose | | --- | --- | --- | | `Empty` | `.header(EmptyHeader)`, `.content(EmptyContent)`, `.child(...)` | Overall alignment and spacing | | `EmptyHeader` | `.media(EmptyMedia)`, `.title(EmptyTitle)`, `.description(EmptyDescription)` | Media and explanatory content | | `EmptyMedia` | `.with_variant(...)`, `.child(...)` | Icon, image, avatar, or arbitrary media | | `EmptyTitle` | `.child(...)` | Title text or custom content | | `EmptyDescription` | `.child(...)` | Wrapping text or rich supporting content | | `EmptyContent` | `.child(...)` | Actions, inputs, or other controls | All parts have `new()` and `Default` constructors and implement `Styled`. All parts except `EmptyHeader` implement `ParentElement`. Named slots are optional and have replacement semantics: calling `.header(...)` twice keeps the second header. Rendering always places the header before the content, and media before title before description, regardless of the order in which those setters are called. Direct root children are appended after both named slots, in their own insertion order; they are not inserted into the content slot. Replacing a slot leaves the other slots and extra root children intact. ## Outline The default Empty has a transparent background and no visible border. Add a border through `Styled`; its default border style is dashed. ```rust Empty::new() .border_1() .header( EmptyHeader::new() .title(EmptyTitle::new().child("Cloud storage is empty")) .description( EmptyDescription::new() .child("Upload files to access them anywhere."), ), ) ``` Use `.border_color(...)` to refine its semantic color. ## Background Apply a semantic surface directly, without adding a component variant: ```rust Empty::new() .bg(cx.theme().muted.opacity(0.3)) .header( EmptyHeader::new() .title(EmptyTitle::new().child("No notifications")) .description( EmptyDescription::new() .child("New notifications will appear here."), ), ) ``` ## Avatar The default media variant adds no frame, background, or fixed size. An existing Avatar retains its own image, fallback, size, and appearance. ```rust EmptyHeader::new() .media( EmptyMedia::new().child( Avatar::new() .name("Alex Morgan") .src("https://avatars.githubusercontent.com/u/5518?v=4"), ), ) .title(EmptyTitle::new().child("Alex is offline")) .description( EmptyDescription::new() .child("Leave a message for Alex to read when they're back."), ) ``` ## Avatar group Multiple avatars use the same media slot. The group owns avatar overlap and size; Empty does not inspect or modify its children. ```rust EmptyHeader::new() .media( EmptyMedia::new().child( AvatarGroup::new() .child(Avatar::new().name("Alex Morgan")) .child(Avatar::new().name("Taylor Lee")) .child(Avatar::new().name("Sam Chen")), ), ) .title(EmptyTitle::new().child("No team members")) .description( EmptyDescription::new() .child("Invite your team to collaborate on this project."), ) ``` ## Inputs and custom content Retain an `Entity` in the owning view, then compose the existing Input in `EmptyContent`: ```rust EmptyContent::new() .child( Input::new(&self.search) .prefix(Icon::new(IconName::Search).size_4()) .cleanable(true), ) .child( EmptyDescription::new() .child("Search by name or try a different keyword."), ) ``` The application handles input events and switches between results and Empty. Each rendered input retains its own state entity and focus. Empty has no input state, validation, submission, or loading policy. ## Constrained layouts Refine the root and the individual slots together to build a compact, leading-aligned empty state: ```rust Empty::new() .max_w(rems(20.)) .p_4() .items_start() .text_left() .header( EmptyHeader::new() .items_start() .title(EmptyTitle::new().child("No shared files")) .description( EmptyDescription::new() .child("Add files so your team can review and edit them together."), ), ) .content( EmptyContent::new() .items_start() .child(Button::new("add-files").outline().label("Add files…")), ) ``` The root fills the available width and can grow within a flex layout. Header and content use the available width up to 24 rem. Text wraps naturally, and Empty does not clip its children or own a scroll region. The parent supplies the viewport and any required scrolling. Custom media should fit its container; action rows can use `.flex_wrap()` when space is constrained. ## Styling defaults | Part | Default | | --- | --- | | Root | Centered column, `p_6()`, `gap_4()`, theme `radius_tokens().xl` | | Header | `gap_2()`, centered items, maximum width 24 rem | | Media | Centered column sized to its content, `mb_2()`, does not shrink | | Icon media | `size_8()`, muted background, foreground text, theme `radius_tokens().lg` | | Title | `text_sm()`, medium weight | | Description | `text_sm()`, line height 1.625, muted foreground | | Content | Centered column, `gap_2p5()`, `text_sm()`, maximum width 24 rem | Instance styles override defaults and media-variant styles. Icon media supplies a one-rem font size that an unsized GPUI Component `Icon` inherits; an explicit icon size is preserved. Arbitrary SVG/image children keep their own sizing. Typography uses the application's font and rem scale. GPUI's native wrapping and letter spacing apply; CSS `text-balance` and `tracking-tight` are not reimplemented by this component. Empty does not create focus targets or automatically announce itself as an alert or live status. Its Button and Input children retain their normal focus and keyboard behavior. Choose application commands as Buttons and external resources as Links. --- # Accordion Source: /component/accordion An accordion component that allows users to show and hide sections of content. It uses collapse functionality internally to create collapsible panels. ## Import ```rust use gpui_kit::component::accordion::Accordion; ``` ## Usage ### Single ```rust Accordion::new("my-accordion") .item(|item| { item.title("Section 1") .child("Content for section 1") }) .item(|item| { item.title("Section 2") .child("Content for section 2") }) .item(|item| { item.title("Section 3") .child("Content for section 3") }) ``` ### Multiple By default, only one accordion item can be open at a time. Use `multiple()` to allow multiple items to be open: ```rust Accordion::new("my-accordion") .multiple(true) .item(|item| item.title("Section 1").child("Content 1")) .item(|item| item.title("Section 2").child("Content 2")) ``` ### Icons and custom content ```rust Accordion::new("my-accordion") .item(|item| { item.title( h_flex() .gap_2() .child(Icon::new(IconName::Settings)) .child("Settings") ) .child("Settings content here") }) ``` ### Sizes ```rust use gpui_kit::component::{Sizable as _, Size}; Accordion::new("my-accordion") .small() .item(|item| item.title("Small Section").child("Content")) Accordion::new("my-accordion") .large() .item(|item| item.title("Large Section").child("Content")) ``` ### Borderless and disabled ```rust Accordion::new("my-accordion") .disabled(true) .item(|item| item.title("Disabled Section").child("Content")) ``` ### With Borders ```rust Accordion::new("my-accordion") .bordered(true) .item(|item| item.title("Section 1").child("Content 1")) ``` ### Handle Toggle Events ```rust Accordion::new("my-accordion") .on_toggle_click(|open_indices, window, cx| { println!("Open items: {:?}", open_indices); }) .item(|item| item.title("Section 1").child("Content 1")) ``` ### Nested Accordions ```rust Accordion::new("outer") .item(|item| { item.title("Parent Section") .child( Accordion::new("inner") .item(|item| item.title("Child 1").child("Content")) .item(|item| item.title("Child 2").child("Content")) ) }) ``` [Accordion]: https://docs.rs/gpui-component/latest/gpui_component/accordion/struct.Accordion.html [AccordionItem]: https://docs.rs/gpui-component/latest/gpui_component/accordion/struct.AccordionItem.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html ## API Reference - [Accordion] - [AccordionItem] ### Sizing Implements [Sizable] trait: - `small()` - Small size - `medium()` - Medium size (default) - `large()` - Large size - `xsmall()` - Extra small size --- # NumberInput Source: /component/number-input A specialized input component for numeric values with built-in increment/decrement buttons and support for min/max values, step values, and number formatting with thousands separators. ## Import ```rust use gpui_kit::component::input::{InputState, NumberInput, NumberInputEvent, StepAction}; ``` ## Usage ### Quantity ```rust struct QuantitySelector { quantity_input: Entity, } impl QuantitySelector { fn new(window: &mut Window, cx: &mut Context) -> Self { // Step by 1 and clamp to 1..=99, no event handling needed. let quantity_input = cx.new(|cx| InputState::new(window, cx) .default_value("1") .min(1.) .max(99.) ); Self { quantity_input } } } // Usage NumberInput::new(&self.quantity_input).small() ``` ### Basic Number Input ```rust let number_input = cx.new(|cx| InputState::new(window, cx) .placeholder("Enter number") .default_value("1") ); NumberInput::new(&number_input) ``` ### Input Restriction and Normalization By default, the NumberInput only accepts a valid number: an optional leading `+`/`-` sign, digits and a single decimal point (e.g. `-1.5`), other characters are rejected on typing and pasting. Full-width number characters are normalized into their ASCII equivalents automatically, for CJK IME users: - Full-width digits: `123` → `123` - Full-width signs: `+` → `+`, `-` → `-` - Full-width dot and ideographic full stop: `.`, `。` → `.` A bare leading decimal point is kept as-is (e.g. `.5`, parsed as `0.5`), matching the web behavior, so deleting the integer part of `1.2` keeps `.2` and stays editable. To opt out of the default restriction, set an explicit mask: `state.set_mask_pattern(MaskPattern::None, window, cx)`. To further restrict the input (e.g. positive integers only), use `pattern`: ```rust // Integer input with validation let integer_input = cx.new(|cx| InputState::new(window, cx) .placeholder("Integer value") .pattern(Regex::new(r"^\d+$").unwrap()) // Only positive integers ); NumberInput::new(&integer_input) ``` ### With Min/Max/Step By default, the NumberInput updates the value internally with `step(1.)`: the `↑`/`↓` keys and the `+`/`-` buttons step the value by 1 and emit `InputEvent::Change`. Set `min`/`max` to clamp the range, or set a custom step. To fall back to emitting `NumberInputEvent::Step` only (the subscriber is responsible for updating the value), call `state.set_step(None, window, cx)`. A typed out-of-range value is kept while typing, and clamped on blur. Stepping follows the web behavior: a step that cannot move the value in the pressed direction (e.g. `↓` on a value at or below the `min`) does nothing. ```rust let stepper_input = cx.new(|cx| InputState::new(window, cx) .default_value("50") .step(5.) .min(0.) .max(100.) ); NumberInput::new(&stepper_input) ``` ### Dynamic Step Use `step_by` to calculate the step value from the current value and the step direction, e.g. a step size that varies by range. Because the step can differ by direction at a boundary, the closure receives the `StepAction`; here `1.0` steps by `0.1` going down and `0.5` going up. The closure also receives a `Context` for reading or updating other entities: ```rust let price_input = cx.new(|cx| InputState::new(window, cx) .step_by(|value, action, _cx| match action { StepAction::Increment => if value < 1.0 { 0.1 } else { 0.5 }, StepAction::Decrement => if value <= 1.0 { 0.1 } else { 0.5 }, }) .min(0.) ); NumberInput::new(&price_input) ``` The step strategy can also be updated at runtime via `set_step`: ```rust use gpui_kit::component::input::NumberStep; state.set_step(NumberStep::Fixed(0.01), window, cx); state.set_step(NumberStep::by_value(|v, _, _cx| if v < 1. { 0.01 } else { 0.1 }), window, cx); state.set_step(None, window, cx); // Fall back to NumberInputEvent::Step ``` ### With Number Formatting ```rust use gpui_kit::component::input::MaskPattern; // Currency input with thousands separator let currency_input = cx.new(|cx| InputState::new(window, cx) .placeholder("Amount") .mask_pattern(MaskPattern::Number { separator: Some(','), fraction: Some(2), // 2 decimal places }) ); NumberInput::new(¤cy_input) ``` ### Different Sizes ```rust // Large size NumberInput::new(&input).large() // Medium size (default) NumberInput::new(&input) // Small size NumberInput::new(&input).small() ``` ### With Prefix and Suffix ```rust use gpui_kit::component::{button::{Button, ButtonVariants}, IconName}; // With currency prefix NumberInput::new(&input) .prefix(div().child("$")) // With info button suffix NumberInput::new(&input) .suffix( Button::new("info") .ghost() .icon(IconName::Info) .xsmall() ) ``` ### Disabled State ```rust NumberInput::new(&input).disabled(true) ``` ### Without Default Styling ```rust // For custom container styling div() .w_full() .bg(cx.theme().secondary) .rounded(cx.theme().radius) .child(NumberInput::new(&input).appearance(false)) ``` ### Handle Number Input Events By default, the NumberInput updates the value internally. To fall back to `NumberInputEvent::Step` (the subscriber is responsible for updating the value), call `state.set_step(None, window, cx)`: ```rust let number_input = cx.new(|cx| InputState::new(window, cx)); let mut value: i64 = 0; // Subscribe to input changes cx.subscribe_in(&number_input, window, |view, state, event, window, cx| { match event { InputEvent::Change => { let text = state.read(cx).value(); if let Ok(new_value) = text.parse::() { view.value = new_value; } } _ => {} } }); // Subscribe to increment/decrement actions cx.subscribe_in(&number_input, window, |view, state, event, window, cx| { match event { NumberInputEvent::Step(step_action) => { match step_action { StepAction::Increment => { view.value += 1; state.update(cx, |input, cx| { input.set_value(view.value.to_string(), window, cx); }); } StepAction::Decrement => { view.value -= 1; state.update(cx, |input, cx| { input.set_value(view.value.to_string(), window, cx); }); } } } } }); ``` ### Programmatic Control ```rust // Increment programmatically NumberInput::increment(&number_input, window, cx); // Decrement programmatically NumberInput::decrement(&number_input, window, cx); ``` ### Integer Counter ```rust struct CounterView { counter_input: Entity, counter_value: i32, } impl CounterView { fn new(window: &mut Window, cx: &mut Context) -> Self { let counter_input = cx.new(|cx| InputState::new(window, cx) .placeholder("Count") .default_value("0") .pattern(Regex::new(r"^-?\d+$").unwrap()) // Allow negative integers ); let _subscription = cx.subscribe_in(&counter_input, window, Self::on_number_event); Self { counter_input, counter_value: 0, } } fn on_number_event( &mut self, state: &Entity, event: &NumberInputEvent, window: &mut Window, cx: &mut Context, ) { match event { NumberInputEvent::Step(StepAction::Increment) => { self.counter_value += 1; state.update(cx, |input, cx| { input.set_value(self.counter_value.to_string(), window, cx); }); } NumberInputEvent::Step(StepAction::Decrement) => { self.counter_value -= 1; state.update(cx, |input, cx| { input.set_value(self.counter_value.to_string(), window, cx); }); } } } } // Usage NumberInput::new(&self.counter_input) ``` ### Currency Input ```rust struct PriceInput { price_input: Entity, price_value: f64, } impl PriceInput { fn new(window: &mut Window, cx: &mut Context) -> Self { let price_input = cx.new(|cx| InputState::new(window, cx) .placeholder("0.00") .mask_pattern(MaskPattern::Number { separator: Some(','), fraction: Some(2), }) ); Self { price_input, price_value: 0.0, } } } // Usage with currency prefix h_flex() .gap_2() .child(div().child("$")) .child(NumberInput::new(&self.price_input)) ``` ### Floating Point Input ```rust // Step by 0.1, the fraction digits of the value are kept on stepping, // e.g. 0.2 -> 0.3 (not 0.30000000000000004). let float_input = cx.new(|cx| InputState::new(window, cx) .placeholder("0.0") .step(0.1) ); NumberInput::new(&float_input) ``` ## Keyboard Navigation | Key | Action | | ----------- | -------------------------- | | `↑` | Increment value | | `↓` | Decrement value | | `Tab` | Navigate to next field | | `Shift+Tab` | Navigate to previous field | | `Enter` | Submit/confirm value | | `Escape` | Clear input (if enabled) | ## Best Practices 1. **Validation**: Always validate numeric input on both client and server side 2. **Range Limits**: Use `min`/`max` to clamp values for user safety 3. **Step Size**: Choose appropriate `step` values for your use case 4. **Error Handling**: Provide clear feedback for invalid input 5. **Formatting**: Use consistent number formatting across your application 6. **Performance**: Debounce rapid increment/decrement actions if needed 7. **Accessibility**: Always provide proper labels and descriptions ## API Reference ### NumberInput | Method | Description | | ------------------------------ | ------------------------------------------ | | `new(state)` | Create number input with InputState entity | | `placeholder(str)` | Set placeholder text | | `size(size)` | Set input size (small, medium, large) | | `prefix(el)` | Add prefix element | | `suffix(el)` | Add suffix element | | `appearance(bool)` | Enable/disable default styling | | `disabled(bool)` | Set disabled state | | `increment(state, window, cx)` | Increment value programmatically | | `decrement(state, window, cx)` | Decrement value programmatically | ### NumberInputEvent | Event | Description | | ------------------ | ---------------------------------- | | `Step(StepAction)` | Increment/decrement pressed. Only emitted when `step` is `None` (opt out via `set_step(None, ...)`). | ### StepAction | Action | Description | | ----------- | ------------------------- | | `Increment` | Value should be increased | | `Decrement` | Value should be decreased | ### InputState (Number-specific methods) | Method | Description | | ----------------------------------- | ------------------------------------------------------- | | `step(impl Into)` | Set step value for built-in increment/decrement (default: 1) | | `step_by(fn(f64, StepAction, &mut Context) -> f64)` | Calculate step value based on the current value and direction | | `min(f64)` | Set minimum value, clamped on stepping and blur | | `max(f64)` | Set maximum value, clamped on stepping and blur | | `set_step(Option, ...)` | Update step strategy after construction | | `set_min(Option, ...)` | Update minimum value after construction | | `set_max(Option, ...)` | Update maximum value after construction | | `pattern(regex)` | Set regex pattern for validation (e.g., digits only) | | `mask_pattern(MaskPattern::Number)` | Set number formatting with separator and decimal places | | `value()` | Get current display value (formatted) | | `unmask_value()` | Get actual numeric value (unformatted) | ### MaskPattern::Number | Field | Type | Description | | ----------- | --------------- | -------------------------------------- | | `separator` | `Option` | Thousands separator (e.g., ',' or ' ') | | `fraction` | `Option` | Number of decimal places | --- # Scrollable Source: /component/scrollable A comprehensive scrollable container component that provides custom scrollbars, scroll tracking, and virtualization capabilities. Supports both vertical and horizontal scrolling with customizable appearance and behavior. ## Import ```rust use gpui_kit::component::{ scroll::{ScrollableElement, ScrollbarAxis, ScrollbarMode}, StyledExt as _, }; ``` ## Usage ### Basic Scrollable Container The simplest way to make any element scrollable is using the `overflow_scrollbar()` method from `ScrollableElement` trait. This method is almost like the `overflow_scroll()` method, but it adds scrollbars. - `overflow_scrollbar()` - Adds scrollbars for both axes as needed. - `overflow_x_scrollbar()` - Adds horizontal scrollbar as needed. - `overflow_y_scrollbar()` - Adds vertical scrollbar as needed. ```rust use gpui_kit::{div, Axis}; use gpui_kit::component::ScrollableElement; div() .id("scrollable-container") .size_full() .child("Your content here") .overflow_scrollbar() ``` ### Vertical Scrolling ```rust v_flex() .id("scrollable-container") .overflow_y_scrollbar() .gap_2() .p_4() .child("Scrollable Content") .children((0..100).map(|i| { div() .h(px(40.)) .w_full() .bg(cx.theme().secondary) .child(format!("Item {}", i)) })) ``` ### Horizontal Scrolling ```rust h_flex() .id("scrollable-container") .overflow_x_scrollbar() .gap_2() .p_4() .children((0..50).map(|i| { div() .min_w(px(120.)) .h(px(80.)) .bg(cx.theme().accent) .child(format!("Card {}", i)) })) ``` ### Both Directions ```rust div() .id("scrollable-container") .size_full() .overflow_scrollbar() .child( div() .w(px(2000.)) // Wide content .h(px(2000.)) // Tall content .bg(cx.theme().background) .child("Large content area") ) ``` ### File Browser with Scrolling ```rust pub struct FileBrowser { files: Vec, } impl Render for FileBrowser { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .border_1() .border_color(cx.theme().border) .size_full() .child( v_flex() .gap_1() .p_2() .overflow_y_scrollbar() .children(self.files.iter().map(|file| { div() .h(px(32.)) .w_full() .px_2() .flex() .items_center() .hover(|style| style.bg(cx.theme().secondary_hover)) .child(file.clone()) })) ) } } ``` ### Chat Messages with Auto-scroll ```rust pub struct ChatView { messages: Vec, scroll_handle: ScrollHandle, should_auto_scroll: bool, } impl ChatView { fn add_message(&mut self, message: String) { self.messages.push(message); if self.should_auto_scroll { // Scroll to bottom for new messages let max_offset = self.scroll_handle.max_offset(); self.scroll_handle.set_offset(point(px(0.), max_offset.y)); } } } ``` ### Data Table with Virtual Scrolling ```rust pub struct DataTable { data: Vec>, scroll_handle: VirtualListScrollHandle, } impl Render for DataTable { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { VirtualList::new( self.scroll_handle.clone(), self.data.len(), |_ix, _window, _cx| size(px(800.), px(32.)), // Fixed row height |ix, bounds, _selected, _window, cx| { h_flex() .size(bounds.size) .border_b_1() .border_color(cx.theme().border) .children(self.data[ix].iter().map(|cell| { div() .flex_1() .px_2() .flex() .items_center() .child(cell.clone()) })) .into_any_element() }, ) } } ``` ## Custom Scrollbars ### Manual Scrollbar Creation For more control, you can create scrollbars manually: ```rust use gpui_kit::component::scroll::{ScrollableElement}; pub struct ScrollableView { scroll_handle: ScrollHandle, } impl Render for ScrollableView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .relative() .size_full() .child( div() .id("content") .track_scroll(&self.scroll_handle) .overflow_scroll() .size_full() .child("Your scrollable content") ) .vertical_scrollbar(&self.scroll_handle) } } ``` ## Virtualization ### VirtualList for Large Datasets For rendering large lists efficiently, use `VirtualList`: ```rust use gpui_kit::component::{VirtualList, VirtualListScrollHandle}; pub struct LargeListView { items: Vec, scroll_handle: VirtualListScrollHandle, } impl Render for LargeListView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let item_count = self.items.len(); VirtualList::new( self.scroll_handle.clone(), item_count, |ix, window, cx| { // Item sizes - can be different for each item size(px(300.), px(40.)) }, |ix, bounds, selected, window, cx| { // Render each item div() .size(bounds.size) .bg(if selected { cx.theme().accent } else { cx.theme().background }) .child(format!("Item {}: {}", ix, self.items[ix])) .into_any_element() }, ) } } ``` ### Scrolling to Specific Items ```rust impl LargeListView { fn scroll_to_item(&mut self, index: usize) { self.scroll_handle.scroll_to_item(index, ScrollStrategy::Top); } fn scroll_to_item_centered(&mut self, index: usize) { self.scroll_handle.scroll_to_item(index, ScrollStrategy::Center); } } ``` ### Variable Item Sizes ```rust VirtualList::new( scroll_handle, items.len(), |ix, window, cx| { // Different heights based on content let height = if items[ix].len() > 50 { px(80.) // Tall items for long content } else { px(40.) // Normal height }; size(px(300.), height) }, |ix, bounds, selected, window, cx| { // Render logic }, ) ``` ## Theme Customization ### Scrollbar Appearance Customize scrollbar appearance through theme configuration: ```rust // In your theme JSON { "scrollbar.background": "#ffffff20", "scrollbar.thumb.background": "#00000060", "scrollbar.thumb.hover.background": "#000000" } ``` ### Scrollbar Show Modes Control when scrollbars are visible: ```rust use gpui_kit::component::{Theme, scroll::ScrollbarMode}; // In theme initialization Theme::set_scrollbar_mode(ScrollbarMode::Scrolling, cx); // Only while scrolling Theme::set_scrollbar_mode(ScrollbarMode::Hover, cx); // On hover Theme::set_scrollbar_mode(ScrollbarMode::Always, cx); // Always visible ``` ### System Integration Sync scrollbar behavior with system preferences: ```rust // Automatically sync with system settings Theme::sync_scrollbar_appearance(cx); ``` --- # Sidebar Source: /component/sidebar A flexible sidebar component that provides navigation structure for applications. Features collapsible states, nested menu items, header and footer sections, and responsive design. Perfect for creating application navigation panels, admin dashboards, and complex hierarchical interfaces. ## Import ```rust use gpui_kit::component::sidebar::{ Sidebar, SidebarHeader, SidebarFooter, SidebarGroup, SidebarMenu, SidebarMenuItem, SidebarToggleButton }; ``` ## Usage ### Basic Sidebar ```rust use gpui_kit::component::{sidebar::*, Side}; Sidebar::new() .header( SidebarHeader::new() .child("My Application") ) .child( SidebarGroup::new("Navigation") .child( SidebarMenu::new() .child( SidebarMenuItem::new("Dashboard") .icon(IconName::LayoutDashboard) .on_click(|_, _, _| println!("Dashboard clicked")) ) .child( SidebarMenuItem::new("Settings") .icon(IconName::Settings) .on_click(|_, _, _| println!("Settings clicked")) ) ) ) .footer( SidebarFooter::new() .child("User Profile") ) ``` ### Collapsible Sidebar ```rust let mut collapsed = false; Sidebar::new() .collapsed(collapsed) .collapsible(true) .header( SidebarHeader::new() .child( h_flex() .child(Icon::new(IconName::Home)) .when(!collapsed, |this| this.child("Home")) ) ) .child( SidebarGroup::new("Menu") .child( SidebarMenu::new() .child( SidebarMenuItem::new("Files") .icon(IconName::Folder) ) ) ) // Toggle button SidebarToggleButton::new() .collapsed(collapsed) .on_click(|_, _, _| { collapsed = !collapsed; }) ``` ### Nested Menu Items ```rust SidebarMenuItem::new("Projects") .icon(IconName::FolderOpen) .active(true) .children([ SidebarMenuItem::new("Web App") .active(false) .on_click(|_, _, _| println!("Web App selected")), SidebarMenuItem::new("Mobile App") .active(true) .on_click(|_, _, _| println!("Mobile App selected")), SidebarMenuItem::new("Desktop App") .on_click(|_, _, _| println!("Desktop App selected")), ]) .on_click(|_, _, _| { // Toggle project group }) ``` ### Multiple Groups ```rust Sidebar::new() .child( SidebarGroup::new("Main") .child( SidebarMenu::new() .child(SidebarMenuItem::new("Dashboard").icon(IconName::Home)) .child(SidebarMenuItem::new("Analytics").icon(IconName::BarChart)) ) ) .child( SidebarGroup::new("Content") .child( SidebarMenu::new() .child(SidebarMenuItem::new("Posts").icon(IconName::FileText)) .child(SidebarMenuItem::new("Media").icon(IconName::Image)) .child(SidebarMenuItem::new("Comments").icon(IconName::MessageCircle)) ) ) .child( SidebarGroup::new("Settings") .child( SidebarMenu::new() .child(SidebarMenuItem::new("General").icon(IconName::Settings)) .child(SidebarMenuItem::new("Users").icon(IconName::Users)) ) ) ``` ### With Badges and Suffixes ```rust use gpui_kit::component::{Badge, Switch}; SidebarMenuItem::new("Notifications") .icon(IconName::Bell) .suffix( Badge::new() .count(5) .child("5") ) SidebarMenuItem::new("Dark Mode") .icon(IconName::Moon) .suffix( Switch::new("dark-mode") .checked(true) .xsmall() ) SidebarMenuItem::new("Settings") .icon(IconName::Settings) .suffix(IconName::ChevronRight) ``` ### Right-Side Placement ```rust Sidebar::new() .side(Side::Right) .width(300) .header( SidebarHeader::new() .child("Right Panel") ) .child( SidebarGroup::new("Tools") .child( SidebarMenu::new() .child(SidebarMenuItem::new("Inspector").icon(IconName::Search)) .child(SidebarMenuItem::new("Console").icon(IconName::Terminal)) ) ) ``` ### Context Menus Add right-click context menus to sidebar menu items for additional actions: ```rust use gpui_kit::component::menu::PopupMenu; SidebarMenuItem::new("Project Files") .icon(IconName::Folder) .context_menu(|menu, _, _| { menu.link("Open in Editor", "https://editor.example.com") .separator() .menu_with_description("Rename", "Rename this project", Box::new(RenameAction)) .menu_with_description("Delete", "Delete this project", Box::new(DeleteAction)) .separator() .submenu("Share", |submenu| { submenu.menu("Copy Link", Box::new(CopyLinkAction)) .menu("Send via Email", Box::new(EmailAction)) }) }) // Multiple items with context menus SidebarMenu::new() .child( SidebarMenuItem::new("Documentation") .icon(IconName::BookOpen) .context_menu(|menu, _, _| { menu.menu("View Online", Box::new(ViewOnlineAction)) .menu("Download PDF", Box::new(DownloadPdfAction)) }) ) .child( SidebarMenuItem::new("Settings") .icon(IconName::Settings) .children([ SidebarMenuItem::new("General") .context_menu(|menu, _, _| { menu.menu("Reset to Defaults", Box::new(ResetAction)) }), SidebarMenuItem::new("Advanced") .context_menu(|menu, _, _| { menu.menu("Export Settings", Box::new(ExportAction)) .menu("Import Settings", Box::new(ImportAction)) }) ]) ) ``` ### Custom Width and Styling ```rust Sidebar::new() .width(280) // Custom width in pixels .border_width(2) // Custom border width .header( SidebarHeader::new() .p_4() // Custom padding .rounded(cx.theme().radius) .child("Custom Styled Sidebar") ) ``` ### Interactive Header with Popup Menu ```rust use gpui_kit::component::menu::DropdownMenu; SidebarHeader::new() .child( h_flex() .gap_2() .child(Icon::new(IconName::Building)) .child("Company Name") .child(Icon::new(IconName::ChevronsUpDown)) ) .dropdown_menu(|menu, _, _| { menu.menu("Acme Corp", Box::new(SelectCompany("acme"))) .menu("Tech Solutions", Box::new(SelectCompany("tech"))) .separator() .menu("Switch Organization", Box::new(SwitchOrg)) }) ``` ### Footer with User Information ```rust SidebarFooter::new() .justify_between() .child( h_flex() .gap_2() .child(Icon::new(IconName::User)) .when(!collapsed, |this| { this.child( v_flex() .child("John Doe") .child(div().text_xs().text_color(cx.theme().muted_foreground).child("john@example.com")) ) }) ) .when(!collapsed, |this| { this.child(Icon::new(IconName::MoreHorizontal)) }) ``` ### Responsive Sidebar ```rust let is_mobile = window_width < 768; Sidebar::new() .collapsed(is_mobile || manually_collapsed) .width(if is_mobile { 60 } else { 240 }) .header( SidebarHeader::new() .child( div() .when(!is_mobile, |this| this.child("Full App Name")) .when(is_mobile, |this| this.child(Icon::new(IconName::Menu))) ) ) ``` ### File Explorer Sidebar ```rust Sidebar::new() .header( SidebarHeader::new() .child( h_flex() .gap_2() .child(IconName::Folder) .child("Explorer") ) ) .child( SidebarGroup::new("Folders") .child( SidebarMenu::new() .child( SidebarMenuItem::new("src") .icon(IconName::FolderOpen) .active(true) .children([ SidebarMenuItem::new("components") .icon(IconName::Folder), SidebarMenuItem::new("utils") .icon(IconName::Folder), SidebarMenuItem::new("main.rs") .icon(IconName::FileCode) .active(true), ]) ) .child( SidebarMenuItem::new("tests") .icon(IconName::Folder) ) .child( SidebarMenuItem::new("Cargo.toml") .icon(IconName::FileText) ) ) ) ``` ### Admin Dashboard Sidebar ```rust Sidebar::new() .header( SidebarHeader::new() .child( h_flex() .gap_2() .child( div() .size_8() .rounded_full() .bg(cx.theme().primary) .child(Icon::new(IconName::Crown)) ) .child("Admin Panel") ) ) .child( SidebarGroup::new("Overview") .child( SidebarMenu::new() .child( SidebarMenuItem::new("Dashboard") .icon(IconName::LayoutDashboard) .active(true) ) .child( SidebarMenuItem::new("Analytics") .icon(IconName::TrendingUp) .suffix(Badge::new().count(2)) ) ) ) .child( SidebarGroup::new("Management") .child( SidebarMenu::new() .child( SidebarMenuItem::new("Users") .icon(IconName::Users) .suffix("1,234") ) .child( SidebarMenuItem::new("Orders") .icon(IconName::ShoppingCart) .suffix(Badge::new().dot().variant_destructive()) ) .child( SidebarMenuItem::new("Products") .icon(IconName::Package) ) ) ) .footer( SidebarFooter::new() .child( h_flex() .gap_2() .child(IconName::User) .child("Administrator") ) .child(IconName::LogOut) ) ``` ### Settings Sidebar ```rust Sidebar::new() .width(300) .header( SidebarHeader::new() .child("Settings") ) .child( SidebarGroup::new("General") .child( SidebarMenu::new() .child( SidebarMenuItem::new("Appearance") .icon(IconName::Palette) .active(true) ) .child( SidebarMenuItem::new("Notifications") .icon(IconName::Bell) .suffix( Switch::new("notifications") .checked(true) .xsmall() ) ) .child( SidebarMenuItem::new("Privacy") .icon(IconName::Shield) ) ) ) .child( SidebarGroup::new("Advanced") .child( SidebarMenu::new() .child( SidebarMenuItem::new("Developer") .icon(IconName::Code) .children([ SidebarMenuItem::new("Debug Mode") .suffix( Switch::new("debug") .checked(false) .xsmall() ), SidebarMenuItem::new("Console") .on_click(|_, _, _| println!("Open console")), ]) ) .child( SidebarMenuItem::new("Performance") .icon(IconName::Zap) ) ) ) ``` ## Theming The sidebar uses dedicated theme colors: ```rust // Theme colors used by sidebar cx.theme().sidebar // Background cx.theme().sidebar_foreground // Text color cx.theme().sidebar_border // Border color cx.theme().sidebar_accent // Hover/active background cx.theme().sidebar_accent_foreground // Hover/active text cx.theme().sidebar_primary // Primary elements cx.theme().sidebar_primary_foreground // Primary text ``` --- # AlertDialog Source: /component/alert-dialog AlertDialog is a modal dialog component that interrupts the user with important content and expects a response. It is built on top of the [Dialog] component with opinionated defaults and a simplified API. ## Differences from Dialog AlertDialog provides these defaults on top of Dialog: - Not overlay closable by default (can be changed with `overlay_closable(true)`) - No close button by default (can be changed with `close_button(true)`) - Footer buttons are center-aligned (Dialog uses right-alignment) - Simplified API focused on alert and confirmation scenarios ## Import ```rust use gpui_kit::component::dialog::{AlertDialog, DialogAction, DialogClose}; use gpui_kit::component::WindowExt; ``` ## Usage ### Default Like Dialog, you need to set up your application's root view to render the dialog layer. See [Dialog documentation](/component/dialog#setup-application-root-view) for details. ```rust AlertDialog::new(cx) .trigger(Button::new("info-alert").outline().label("Discard Draft")) .on_ok(|_, window, cx| { window.push_notification("Draft discarded", cx); true }) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Discard unsaved changes?")) .child(DialogDescription::new().child( "Your edits since the last save will be permanently lost.", )), ) .child( DialogFooter::new() .child(DialogClose::new().child( Button::new("cancel").outline().label("Cancel"), )) .child(DialogAction::new().child( Button::new("ok").label("Discard").danger(), )), ) }) ``` ### Delete file Using imperative API: ```rust Button::new("delete") .danger() .label("Delete") .on_click(|_, window, cx| { window.open_alert_dialog(cx, |alert, _, _| { alert .title("Delete File?") .description("This action cannot be undone.") .confirm() .ok_text("Delete") .ok_variant(ButtonVariant::Danger) .on_ok(|_, window, cx| { // Perform delete operation window.push_notification("File deleted", cx); true }) }); }) ``` Or using declarative API with DialogAction/DialogClose: ```rust AlertDialog::new(cx) .trigger(Button::new("delete").danger().label("Delete")) .on_ok(|_, window, cx| { window.push_notification("File deleted", cx); true }) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Delete File?")) .child(DialogDescription::new().child("This action cannot be undone.")) ) .child( DialogFooter::new() .child( DialogClose::new().child( Button::new("cancel").outline().label("Cancel") ) ) .child( DialogAction::new().child( Button::new("delete-confirm").danger().label("Delete") ) ) ) }) ``` ### Icon Using icon in declarative API: ```rust use gpui_kit::component::{Icon, IconName, ActiveTheme}; AlertDialog::new(cx) .w(px(320.)) .trigger(Button::new("permission").outline().label("Request Permission")) .on_ok(|_, window, cx| { window.push_notification("Permission granted", cx); true }) .content(|content, _, cx| { content .child( DialogHeader::new() .items_center() .child( Icon::new(IconName::TriangleAlert) .size_10() .text_color(cx.theme().warning) ) .child(DialogTitle::new().child("Network Permission Required")) .child(DialogDescription::new().child( "We need your permission to access the network to provide better services." )) ) .child( DialogFooter::new() .v_flex() .child( DialogAction::new().child( Button::new("allow").w_full().primary().label("Allow") ) ) .child( DialogClose::new().child( Button::new("deny").w_full().outline().label("Don't Allow") ) ) ) }) ``` Using icon in imperative API: ```rust window.open_alert_dialog(cx, |alert, _, cx| { alert .title("Warning") .description("This action requires confirmation.") .icon( Icon::new(IconName::AlertTriangle) .size_8() .text_color(cx.theme().warning) ) }) ``` ### Destructive ```rust AlertDialog::new(cx) .trigger( Button::new("delete-account") .outline() .danger() .label("Delete Account") ) .on_ok(|_, window, cx| { window.push_notification("Account deletion initiated", cx); true }) .content(|content, _, _| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Delete Account")) .child(DialogDescription::new().child( "This will permanently delete your account \ and all associated data. This action cannot be undone." )) ) .child( DialogFooter::new() .child( DialogClose::new().child( Button::new("cancel").flex_1().outline().label("Cancel") ) ) .child( DialogAction::new().child( Button::new("delete") .flex_1() .outline() .danger() .label("Delete Forever") ) ) ) }) ``` ### Without title Create a fully declarative AlertDialog using `trigger` and `content`: ```rust use gpui_kit::component::dialog::{AlertDialog, DialogHeader, DialogTitle, DialogDescription, DialogFooter}; AlertDialog::new(cx) .trigger( Button::new("show-alert") .outline() .label("Show Alert") ) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Are you absolutely sure?")) .child(DialogDescription::new().child( "This action cannot be undone. \ This will permanently delete your account from our servers." )) ) .child( DialogFooter::new() .child( Button::new("cancel") .outline() .label("Cancel") .on_click(|_, window, cx| { window.close_dialog(cx); }) ) .child( Button::new("ok") .primary() .label("Continue") .on_click(|_, window, cx| { window.push_notification("Confirmed", cx); window.close_dialog(cx); }) ) ) }) ``` ### Custom footer Set the button text and variant directly on the dialog: ```rust use gpui_kit::component::button::ButtonVariant; window.open_alert_dialog(cx, |alert, _, _| { alert .title("Delete Account") .description("This will permanently delete your account and all associated data.") .confirm() .ok_text("Delete") .ok_variant(ButtonVariant::Danger) .cancel_text("Keep") .on_ok(|_, window, cx| { window.push_notification("Account deleted", cx); true }) }) ``` `button_props` takes the same properties as one value, for a configuration you want to build up or pass around. It overrides only the fields the value sets, so everything the dialog already carries — the Cancel button `confirm` asked for, a callback an earlier `on_ok` installed — survives, whatever the call order: ```rust use gpui_kit::component::dialog::DialogButtonProps; window.open_alert_dialog(cx, move |alert, _, _| { alert .title("Delete Account") .confirm() .button_props( DialogButtonProps::default() .ok_text("Delete") .ok_variant(ButtonVariant::Danger) ) }) ``` ### Custom content ```rust AlertDialog::new(cx) .width(px(500.)) .trigger(Button::new("custom-width").label("Custom Width")) .content(|content, _, _| { // ... dialog content }) ``` ### Notice `DialogAction` and `DialogClose` are wrapper components that simplify button click handling by automatically triggering the appropriate actions: - **DialogClose**: Wraps a button to trigger the `Cancel` action, invoking `on_cancel` callback - **DialogAction**: Wraps a button to trigger the `Confirm` action, invoking `on_ok` callback These components eliminate the need to manually call `window.close_dialog(cx)`: ```rust AlertDialog::new(cx) .trigger(Button::new("show-alert").outline().label("Show Alert")) .on_ok(|_, window, cx| { window.push_notification("You confirmed!", cx); true // Return true to close dialog }) .on_cancel(|_, window, cx| { window.push_notification("You cancelled!", cx); true }) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Confirm Action")) .child(DialogDescription::new().child("Do you want to proceed?")) ) .child( DialogFooter::new() .child( DialogClose::new().child( Button::new("cancel").outline().label("Cancel") ) ) .child( DialogAction::new().child( Button::new("ok").primary().label("Confirm") ) ) ) }) ``` **Benefits:** - No need to manually close the dialog - Automatically connects to `on_ok` and `on_cancel` callbacks - Cleaner, more declarative code - Supports returning `false` from callbacks to prevent closing ### Confirm Open a dialog imperatively using `WindowExt::open_alert_dialog`: ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Delete File") .description("Are you sure you want to delete this file? This action cannot be undone.") .show_cancel(true) .on_ok(|_, window, cx| { window.push_notification("File deleted", cx); true // Return true to close dialog }) }) ``` ### Prevent close #### Allow Overlay Click to Close ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Notice") .description("Click outside this dialog or press ESC to close it.") .overlay_closable(true) }) ``` #### Disable Keyboard ESC to Close ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Important Notice") .description("Please read this carefully before proceeding.") .keyboard(false) }) ``` #### Show Close Button ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Information") .description("Some information...") .close_button(true) }) ``` ### Prevent Dialog from Closing Return `false` from `on_ok` or `on_cancel` callbacks to prevent the dialog from closing: ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Processing") .description("A process is running. Click Continue to stop it or Cancel to keep waiting.") .confirm() .ok_text("Continue") .on_ok(|_, window, cx| { // Return false to prevent closing window.push_notification("Cannot close: Process still running", cx); false }) .on_cancel(|_, window, cx| { window.push_notification("Waiting...", cx); false }) }) ``` ### Dialog Close Callback Use `on_close` to execute actions after the dialog closes (called after `on_ok` or `on_cancel`): ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .title("Confirm") .description("Are you sure?") .on_close(|_, window, cx| { window.push_notification("Dialog closed", cx); }) }) ``` ### Session Timeout ```rust window.open_alert_dialog(cx, |alert, _, _| { alert .content(|content, _, _| { content .child( DialogHeader::new() .items_center() .child(DialogTitle::new().child("Session Expired")) .child(DialogDescription::new().child( "Your session has expired due to inactivity. \ Please log in again to continue." )) ) .child( DialogFooter::new() .child( Button::new("sign-in") .label("Sign in") .primary() .flex_1() .on_click(|_, window, cx| { window.push_notification("Redirecting to login...", cx); window.close_dialog(cx); }) ) ) }) }) ``` ### Update Available ```rust AlertDialog::new(cx) .trigger(Button::new("update").outline().label("Update Available")) .on_cancel(|_, window, cx| { window.push_notification("Update postponed", cx); true }) .on_ok(|_, window, cx| { window.push_notification("Starting update...", cx); true }) .content(|content, _, _| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Update Available")) .child(DialogDescription::new().child( "A new version (v2.0.0) is available. \ This update includes new features and bug fixes." )) ) .child( DialogFooter::new() .child( DialogClose::new().child( Button::new("later").flex_1().outline().label("Later") ) ) .child( DialogAction::new().child( Button::new("update-now").flex_1().primary().label("Update Now") ) ) ) }) ``` ## Best Practices 1. **Choose the Right API**: Use imperative API (`open_alert_dialog`) for simple confirmations; use declarative API (`trigger` + `content`) for complex layouts or integration with other components 2. **Use DialogAction and DialogClose**: Prefer wrapping buttons with `DialogAction` and `DialogClose` over manual `window.close_dialog()` calls for cleaner, more declarative code 3. **Clarify Intent**: Use appropriate button variants (e.g., `ButtonVariant::Danger` for delete operations) to communicate the importance of actions 4. **Provide Clear Descriptions**: Ensure users understand the consequences of their actions, especially for destructive operations 5. **Use Icons Wisely**: Icons can enhance attention for warnings and errors, but use them appropriately 6. **Prevent Closing Carefully**: Only prevent dialog closing when user confirmation is truly necessary (e.g., a process is running) 7. **Maintain Consistency**: Keep dialog button order and styles consistent throughout your application ## Related Components - [Dialog] - More flexible dialog component - [DialogHeader] - Dialog header component - [DialogTitle] - Dialog title component - [DialogDescription] - Dialog description component - [DialogFooter] - Dialog footer component - [DialogAction] - Wrapper component for confirm/OK buttons - [DialogClose] - Wrapper component for cancel/close buttons [AlertDialog]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.AlertDialog.html [Dialog]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.Dialog.html [DialogHeader]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogHeader.html [DialogTitle]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogTitle.html [DialogDescription]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogDescription.html [DialogFooter]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogFooter.html [DialogAction]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogAction.html [DialogClose]: https://docs.rs/gpui-component/latest/gpui_component/dialog/struct.DialogClose.html ## API Reference ### AlertDialog | Method | Description | | ------------------------ | ------------------------------------------------------------- | | `new(cx)` | Create a new AlertDialog | | `trigger(element)` | Set trigger element that opens the dialog when clicked | | `content(builder)` | Set dialog content using a builder function (declarative API) | | `title(title)` | Set dialog title (imperative API) | | `description(desc)` | Set dialog description (imperative API) | | `icon(icon)` | Set dialog icon (imperative API) | | `confirm()` | Show OK and Cancel buttons | | `ok_text(text)` | Set OK button text, default "OK" | | `ok_variant(variant)` | Set OK button variant, default `Primary` | | `cancel_text(text)` | Set cancel button text, default "Cancel" | | `cancel_variant(variant)`| Set cancel button variant | | `button_props(props)` | Override the button properties the value sets, keep the rest | | `show_cancel(bool)` | Show/hide cancel button, default `false` | | `width(px)` | Set dialog width, default `420px` | | `overlay_closable(bool)` | Allow clicking overlay to close, default `false` | | `close_button(bool)` | Show/hide close button, default `false` | | `keyboard(bool)` | Support ESC key to close, default `true` | | `on_ok(callback)` | Set OK button callback, return `true` to close dialog | | `on_cancel(callback)` | Set cancel button callback, return `true` to close dialog | | `on_close(callback)` | Set callback after dialog closes | ### DialogButtonProps Every property is unset until a builder sets it, and an unset property keeps whatever the dialog already carries. | Method | Description | | ------------------------- | ---------------------------------------- | | `ok_text(text)` | Set OK button text, default "OK" | | `cancel_text(text)` | Set cancel button text, default "Cancel" | | `ok_variant(variant)` | Set OK button variant | | `cancel_variant(variant)` | Set cancel button variant | | `show_cancel(bool)` | Show/hide cancel button | | `on_ok(callback)` | Set OK callback | | `on_cancel(callback)` | Set cancel callback | ### DialogAction A wrapper component that automatically triggers the `Confirm` action when its child element is clicked. This invokes the `on_ok` callback set on the AlertDialog. **Usage:** ```rust DialogAction::new().child( Button::new("ok").primary().label("Confirm") ) ``` **Behavior:** - Dispatches `Confirm` action on click - Invokes the `on_ok` callback - Dialog closes if callback returns `true` - Dialog stays open if callback returns `false` ### DialogClose A wrapper component that automatically triggers the `Cancel` action when its child element is clicked. This invokes the `on_cancel` callback set on the AlertDialog. **Usage:** ```rust DialogClose::new().child( Button::new("cancel").outline().label("Cancel") ) ``` **Behavior:** - Dispatches `Cancel` action on click - Invokes the `on_cancel` callback - Dialog closes if callback returns `true` (or if no callback is set) - Dialog stays open if callback returns `false` --- # TimeField Source: /component/time-field A segmented input for a time of day. Each part — hour, minute, optional second and optional AM/PM — is edited on its own with the keyboard, and the field keeps its width while digits are typed. [DatePicker](date-picker) uses it to edit the time of a date. ## Import ```rust use gpui_kit::component::time_field::{ HourCycle, TimeField, TimeFieldEvent, TimeFieldState, TimePrecision, }; ``` ## Usage ### Basic Time Field ```rust let time = cx.new(|cx| TimeFieldState::new(window, cx)); TimeField::new(&time) ``` The field starts at `00:00`. Set a value with `set_time`, which does not emit an event: ```rust use chrono::NaiveTime; time.update(cx, |state, cx| { state.set_time(NaiveTime::from_hms_opt(9, 30, 0).unwrap(), window, cx); }); ``` ### With Seconds ```rust let time = cx.new(|cx| { TimeFieldState::new(window, cx).precision(TimePrecision::Second) }); TimeField::new(&time) // 09:30:15 ``` ### 12-Hour Clock The field uses a 24-hour clock by default. `HourCycle::H12` shows hours from `12` to `11` followed by an AM/PM segment: ```rust let time = cx.new(|cx| { TimeFieldState::new(window, cx).hour_cycle(HourCycle::H12) }); TimeField::new(&time) // 09:30 PM ``` ### Sizes, Disabled and Invalid ```rust TimeField::new(&time).small() TimeField::new(&time).large() TimeField::new(&time).disabled(true) // Show the owner's validation result; edits are not rejected. TimeField::new(&time).invalid(true) ``` ## Handle Changes User edits emit `TimeFieldEvent::Change` with the new time: ```rust cx.subscribe(&time, |this, _, event, cx| match event { TimeFieldEvent::Change(time) => { this.reminder = *time; cx.notify(); } }); ``` ## Keyboard | Key | Action | | --- | --- | | `Up` / `Down` | Step the selected segment; it wraps without changing the next unit | | `Left` / `Right` | Move between segments | | `Tab` / `Shift-Tab` | Move between segments, then leave the field | | `0`–`9` | Type the selected segment and move on once it is complete | | `a` / `p` | Set AM or PM on a 12-hour clock | | `Backspace` / `Delete` | Reset the selected segment | --- # Questionnaire Source: /component/questionnaire `Questionnaire` guides a user through an ordered set of questions. It owns the active item, answer state, validation, progress, and navigation. A containing page, `GroupBox`, `Dialog`, or `Sheet` remains responsible for closing, cancelling, persistence, transport, and application-specific branching. ## Import ```rust use gpui_kit::component::questionnaire::{ Questionnaire, QuestionnaireActions, QuestionnaireChoice, QuestionnaireChoiceDescription, QuestionnaireChoices, QuestionnaireDescription, QuestionnaireError, QuestionnaireInput, QuestionnaireItem, QuestionnaireNext, QuestionnairePrevious, QuestionnaireProgress, QuestionnaireSkip, QuestionnaireState, QuestionnaireSubmit, QuestionnaireTitle, }; ``` ## Usage Create the item collection once and use one `QuestionnaireState` entity as the source of truth for all parts. ```rust use gpui_kit::component::input::InputState; use gpui_kit::component::questionnaire::{ QuestionnaireChoiceDefinition, QuestionnaireInputDefinition, QuestionnaireItemDefinition, QuestionnaireState, }; let direction_input = cx.new(|cx| { InputState::new(window, cx).placeholder("Type another answer…") }); let items = vec![ QuestionnaireItemDefinition::new("direction", "What should we prototype next?") .with_required(true) .with_description("Choose a direction or write your own.") .with_choices([ QuestionnaireChoiceDefinition::new("delegation", "Delegation") .with_description("Show how work moves to a specialist."), QuestionnaireChoiceDefinition::new("questions", "Question prompts"), QuestionnaireChoiceDefinition::new("both", "Both together"), ]) .with_input(QuestionnaireInputDefinition::new( direction_input, "Another answer", )), QuestionnaireItemDefinition::new("detail", "How much detail should it include?") .with_description("You can skip this question if you are not sure yet.") .with_choices([ QuestionnaireChoiceDefinition::new("focused", "Focused"), QuestionnaireChoiceDefinition::new("complete", "Complete flow"), ]), ]; let state = cx.new(|cx| { QuestionnaireState::new(items, cx) .expect("valid questionnaire schema") }); ``` Map every definition in the collection into the compound parts. The active `QuestionnaireItem` is the only item rendered, so omitting an item from this composition leaves the UI empty when navigation reaches that item. ```rust Questionnaire::new(&state) .child(QuestionnaireProgress::new(&state)) .child( QuestionnaireItem::new(&state, "direction") .child(QuestionnaireTitle::new(&state, "direction")) .child(QuestionnaireDescription::new(&state, "direction")) .child( QuestionnaireChoices::new(&state, "direction") .child(QuestionnaireChoice::new(&state, "direction", "delegation")) .child(QuestionnaireChoice::new(&state, "direction", "questions")) .child(QuestionnaireChoice::new(&state, "direction", "both")) .child(QuestionnaireInput::new(&state, "direction")), ) .child(QuestionnaireError::new(&state, "direction")), ) .child( QuestionnaireItem::new(&state, "detail") .child(QuestionnaireTitle::new(&state, "detail")) .child(QuestionnaireDescription::new(&state, "detail")) .child( QuestionnaireChoices::new(&state, "detail") .child(QuestionnaireChoice::new(&state, "detail", "focused")) .child(QuestionnaireChoice::new(&state, "detail", "complete")), ) .child(QuestionnaireError::new(&state, "detail")), ) .child( QuestionnaireActions::new(&state) .child(QuestionnairePrevious::new(&state)) .child(QuestionnaireSkip::new(&state)) .child(QuestionnaireNext::new(&state)) .child(QuestionnaireSubmit::new(&state)), ) ``` ## Composition ```text Questionnaire ├── QuestionnaireProgress ├── QuestionnaireItem │ ├── QuestionnaireTitle │ ├── QuestionnaireDescription │ ├── QuestionnaireChoices │ │ ├── QuestionnaireChoice │ │ │ └── QuestionnaireChoiceDescription (custom child) │ │ └── QuestionnaireInput │ └── QuestionnaireError └── QuestionnaireActions ├── QuestionnairePrevious ├── QuestionnaireSkip ├── QuestionnaireNext └── QuestionnaireSubmit ``` Every part accepts ordinary GPUI styling and can be composed with existing `Button`, `Input`, `Radio`, `Checkbox`, `Progress`, `Stepper`, `GroupBox`, and `Dialog` elements. Pass the same state entity to each part. A custom part should read its corresponding state and call state methods for user actions; it should not create a second answer store. `QuestionnaireChoice` supplies the default indicator, content, and shortcut. Adding children replaces the fallback label and description while preserving choice activation, focus, state, and accessibility behavior. Use `QuestionnaireChoiceDescription::new()` for secondary text in a custom choice body. The following seams customize only the corresponding region: ```rust use gpui_kit::{IntoElement as _, ParentElement as _, StyleRefinement, Styled as _, div}; use gpui_kit::component::{ActiveTheme as _, StyledExt as _}; use gpui_kit::component::questionnaire::{ QuestionnaireChoice, QuestionnaireChoiceDescription, }; let _styled_choice = QuestionnaireChoice::new(&state, "direction", "questions") .indicator_style(StyleRefinement::default().opacity(0.9)) .content_style(StyleRefinement::default().opacity(0.95)) .shortcut_style(StyleRefinement::default().opacity(0.8)); let _rendered_choice = QuestionnaireChoice::new(&state, "direction", "delegation") .render_indicator(|choice, _, cx| { div() .size_4() .rounded_full() .bg(if choice.is_selected() { cx.theme().primary } else { cx.theme().muted }) .into_any_element() }) .child( div() .child("Delegation") .child(QuestionnaireChoiceDescription::new().child( "Show how work moves to a specialist.", )), ); ``` `render_shortcut` has the same renderer signature and receives the `QuestionnaireChoiceState`; use it when an application wants to replace the default `Kbd` hint. A renderer replaces that region completely, so its matching style seam is not applied; style the custom renderer directly. The state snapshot exposes `is_selected`, `is_disabled`, `is_invalid`, and `shortcut` for custom rendering. ## Choices An item is single-selection by default: activating a choice answers it and makes `Next` available. `with_multiple` keeps every selected choice instead. The answer reader preserves schema order, and a choice disabled later leaves the effective answer. Definition builders carry the initial snapshot: a choice can start selected, an item, a choice, or an input can start disabled, and a single-choice item may carry at most one default. ```rust let tools_input = cx.new(|cx| InputState::new(window, cx)); let items = vec![ QuestionnaireItemDefinition::new("plan", "Which plan fits your team?") .with_required(true) .with_choices([ QuestionnaireChoiceDefinition::new("plus", "Plus").with_default_selected(true), QuestionnaireChoiceDefinition::new("pro", "Pro"), ]), QuestionnaireItemDefinition::new("tools", "Which tools do you use?") .with_multiple(true) .with_choices([ QuestionnaireChoiceDefinition::new("editor", "Editor"), QuestionnaireChoiceDefinition::new("terminal", "Terminal"), QuestionnaireChoiceDefinition::new("browser", "Browser").with_disabled(true), ]) .with_input(QuestionnaireInputDefinition::new(tools_input, "Something else")), QuestionnaireItemDefinition::new("advanced", "Advanced preferences").with_disabled(true), ]; ``` `QuestionnaireState::new` rejects duplicate item names, duplicate choice values within an item, and multiple defaults on a single-choice item. Setters for unknown items or choices return `QuestionnaireSchemaError`. ## Freeform answer Add `QuestionnaireInputDefinition` to allow a user to enter an answer that is not in the fixed choices. Give the input an accessible label; a placeholder is not a label. Whitespace-only input is unanswered. The input draft is kept when a fixed choice is selected, but it is submitted only when the freeform answer is active. In a multiple item, a non-empty freeform answer can accompany fixed choices. ## Validation Required status validation is built in. Add a synchronous validator to an item for domain-specific checks. The validator receives the current item, its answer, and the complete enabled answer snapshot through `QuestionnaireValidationContext`. `Next` validates the current item; `Submit` validates all enabled items and focuses the first invalid item. ```rust let item = QuestionnaireItemDefinition::new("handle", "Choose a handle") .with_required(true) .with_validator(|context| { if context .answer() .freeform() .is_some_and(|value| value.as_ref().len() >= 3) { Ok(()) } else { Err("Use at least three characters.".into()) } }); ``` An optional unanswered item is invalid until the user explicitly skips it; `Skipped` is intentionally valid. Disabled items and disabled controls do not participate in validation. The first invalid item is selected on submit, and focus goes to its filled input or selected choice before falling back to the first enabled control. Use external errors for schema or server responses. External errors belong to the host and remain until the host clears them. ```rust state.update(cx, |state, cx| { state .set_external_error("handle", "This handle is already taken.", cx) .expect("known questionnaire item"); }); // After the owner accepts a corrected answer or a new server response: state.update(cx, |state, cx| { state .clear_external_error("handle", cx) .expect("known questionnaire item"); }); ``` `reset` clears internal validation attempts and errors, but preserves owner-managed external errors. ## Navigation and submission `QuestionnaireState` exposes the current item, ordered item states, and navigation state for custom action layouts. ```rust let state = state.read(cx); let progress = state.progress(); let status = state.item_state("direction").map(|item| item.status()); let navigation = state.navigation_state(); let show_skip = navigation.is_skip_visible(); ``` `QuestionnaireNavigationState` answers the same question for `Previous`, `Next`, `Submit`, and `is_confirmable`; `current_item` and `current_ix` locate the active item. The default action layout shows `Previous` at the beginning, `Next` between items, `Skip` only for the active optional item, and `Submit` at the end. Hidden actions are not rendered and do not enter keyboard navigation. Disabled items are removed from the navigation and progress totals. The three item statuses are `Unanswered`, `Answered`, and `Skipped`. ### Skipping Optional items can expose `QuestionnaireSkip`. A skip is an intentional valid state, clears the item answer, and allows `Next` to continue. Required items do not allow skipping. Re-entering an item and choosing an answer clears its skipped state. Skipping the final enabled item requests submission after the skip has been recorded. ### Events and submission Subscribe to `QuestionnaireEvent` for active-item changes, answer changes, completion, and successful submit. `Completed` is emitted on the transition into a complete state; `Submit` is emitted for each successful explicit submit. On the first successful submit, the order is `Completed` followed by `Submit`. Changing answers or enabled conditions clears completion, so the next successful submit can emit `Completed` again. ```rust use gpui_kit::component::questionnaire::QuestionnaireEvent; cx.subscribe(&state, |_, _, event, _| match event { QuestionnaireEvent::CurrentItemChanged { current, .. } => { println!("Current item: {:?}", current); } QuestionnaireEvent::AnswerChanged(change) => { println!("Changed: {:?} ({:?})", change.item(), change.status()); } QuestionnaireEvent::Completed(submission) | QuestionnaireEvent::Submit(submission) => { println!("Answers: {:?}", submission.items()); } _ => {} }) .detach(); ``` Detaching keeps the callback alive until the subscribed entities are dropped. Store the returned `Subscription` in the host instead when it needs to cancel the listener earlier. The submission is ordered by the item schema and contains only enabled items. Each item includes its name, `Unanswered`/`Answered`/`Skipped` status, and effective answer. It represents a validated local submission request; saving it remotely remains the host application's responsibility. ## Controlling the state When a page owns the active item or needs to apply a saved answer after state creation, use the silent setters. They update the UI and focus as needed but do not emit user-interaction events. ```rust use gpui_kit::component::questionnaire::QuestionnaireAnswer; state.update(cx, |state, cx| { state .set_current_item("detail", window, cx) .expect("known enabled questionnaire item"); state .set_answer( "direction", QuestionnaireAnswer::new().with_choices(["delegation"]), window, cx, ) .expect("known questionnaire item"); state .set_input_value("direction", "A controlled draft", window, cx) .expect("item has an input"); }); ``` Use `activate_choice`, `confirm_current`, `go_previous`, `go_next`, `skip_current`, and `submit` for user intent. Those paths emit the relevant `QuestionnaireEvent` values. A host can also use `set_item_disabled` and `set_choice_disabled`; disabling the current item moves focus to the next enabled item, or to the previous one when there is no next item. ### Reset Reset restores the initial choices and input drafts, clears intentional skips, validation attempts, and completion, and returns to the initial current item. It also focuses the restored current item. ```rust state.update(cx, |state, cx| { state.reset(window, cx); }); ``` External errors remain owner-managed across reset. If a reset should also remove a server error, clear it explicitly with `clear_external_error`. `reset` returns to the snapshot the schema was built with, so a saved draft belongs in the definitions: `InputState::default_value`, `with_default_selected`, and `with_current_item` establish that baseline. Values applied later with `set_answer`, `set_input_value`, or `set_current_item` change the current state without moving the reset baseline. ### Conditional items Questionnaire does not contain a branching engine. The host can derive an item's disabled state from an earlier answer and synchronize it with `set_item_disabled`. This keeps conditional policy in the page while the Questionnaire continues to own ordering, focus, progress, validation, and submission. ```rust fn sync_advanced_item( state: &Entity, window: &mut Window, cx: &mut App, ) { let enabled = state.read(cx).answer("direction").is_some_and(|answer| { answer .choices() .iter() .any(|choice| choice.as_ref() == "delegation") }); state.update(cx, |state, cx| { let _ = state.set_item_disabled("advanced", !enabled, window, cx); }); } ``` Call this helper from the host's answer-change handling or from the UI action that changes the earlier answer. A disabled conditional item is excluded from progress, navigation, validation, focus, shortcuts, and submission. ## Keyboard shortcuts Enable letter or number shortcuts on the state. Shortcuts apply only to the active item's enabled choices. Repeated key events, text input, IME composition, and modified key presses are left untouched. ```rust use gpui_kit::component::questionnaire::QuestionnaireShortcutMode; let state = cx.new(|cx| { QuestionnaireState::new(items, cx) .expect("valid questionnaire schema") .with_shortcuts(QuestionnaireShortcutMode::Letters) }); ``` Questionnaire handles radio movement according to the native single-choice interaction. Up and Down otherwise move through enabled choices and the freeform input in schema order; the input remains in that order when present. When a non-empty text input has focus, its normal text-editing behavior is preserved. Left and Right move between items only outside text inputs and single-choice radio controls; Right requires a confirmable current item. Enter confirms a filled answer. Command/Ctrl+Enter confirms the current item. An empty answer does not implicitly submit. Shortcut labels are assigned in enabled-choice order (`A`–`Z` or `1`–`9`), and disabled choices receive no label. ## Progress `QuestionnaireProgress` follows the default presentation: “Question 2 of 4”. The same snapshot can drive an existing indicator instead. ```rust QuestionnaireProgress::new(&state); let progress = state.read(cx).progress(); let percent = if progress.total() == 0 { 0. } else { progress.current() as f32 / progress.total() as f32 * 100. }; Progress::new("questionnaire-progress").value(percent); ``` `current` and `total` count only the enabled items, and both move when the host disables or re-enables a question. An indicator with one fixed label per step — a `Stepper`, for example — has to derive its steps from the same enabled set, or its labels and its selected step drift apart from the questionnaire. ## Sizes and theming `Questionnaire` takes the scale for the whole questionnaire, and every part of that questionnaire follows it — the root publishes the size under its state, so the compound parts do not have to be told individually. A part that names its own size keeps it. ```rust use gpui_kit::component::{Sizable as _, Size}; Questionnaire::new(&state) .with_size(Size::Small) .child(QuestionnaireProgress::new(&state)) .child( QuestionnaireItem::new(&state, "direction") .child(QuestionnaireTitle::new(&state, "direction")) .child( QuestionnaireChoices::new(&state, "direction") // Follows the root; pass `with_size` here only to differ. .child(QuestionnaireChoice::new(&state, "direction", "delegation")), ), ); ``` The supported sizes are `XSmall`, `Small`, `Medium` (the default) and `Large`, plus `Size::Size(value)` for a custom scale. Answer text matches the Checkbox and Radio family's label at the same size. Spacing, typography, radius, border, input, primary, muted, destructive, and focus-ring values all come from the active theme's semantic tokens, so an application changes the questionnaire's shape by changing the theme. Use `Styled` methods or `StyleRefinement` for local adjustments; local style refinement is applied after the component defaults. `QuestionnaireChoiceDescription` is the one part with no state of its own — it is a plain text slot for a custom choice body — so it defaults to `Medium` and takes `with_size` when a custom composition needs another scale. ## Card and Dialog composition The questionnaire owns the question flow; the container owns its surface and its close or cancel behavior. Put the whole composition — progress, every item, and the actions — inside the container, so moving to the next question stays visible. ```rust use gpui_kit::component::group_box::{GroupBox, GroupBoxVariants as _}; GroupBox::new() .outline() .title("Set up your workspace") .child(questionnaire); ``` In a dialog, the footer carries the container's own `Cancel` next to the questionnaire's actions, and the host closes the dialog when the questionnaire reports a successful submit. ```rust use gpui_kit::component::dialog::{ Dialog, DialogClose, DialogFooter, DialogHeader, DialogTitle, }; use gpui_kit::component::{WindowExt as _, questionnaire::QuestionnaireEvent}; let dialog_state = state.clone(); cx.subscribe_in( &dialog_state, window, |_, _, event: &QuestionnaireEvent, window, cx| { if matches!(event, QuestionnaireEvent::Submit(_)) { window.close_dialog(cx); } }, ) .detach(); Dialog::new(cx) .trigger(Button::new("open-questionnaire").outline().label("Open questionnaire")) .content(move |content, _, _| { content .child(DialogHeader::new().child(DialogTitle::new().child("Workspace setup"))) .child( Questionnaire::new(&dialog_state) // …progress and every item, as in Usage above .child( DialogFooter::new() .child(DialogClose::new().child( Button::new("cancel-questionnaire").outline().label("Cancel"), )) .child( QuestionnaireActions::new(&dialog_state) .child(QuestionnairePrevious::new(&dialog_state)) .child(QuestionnaireNext::new(&dialog_state)) .child(QuestionnaireSubmit::new(&dialog_state)), ), ), ) }); ``` `Cancel` always closes. `Submit` closes only after the questionnaire has validated every enabled item, and the same event hands the validated `QuestionnaireSubmission` to application transport. ## Accessibility `Questionnaire` uses the GPUI `Form` role for the root. `QuestionnaireItem` is an accessible group with its item label and description. The definition's `accessibility_label` and `description` remain the semantic source for the item and choice, even when a custom child replaces the visible fallback content. `QuestionnaireError` is announced as an alert only while the item is invalid. Choice parts preserve radio and checkbox semantics, progress exposes current and total values, and navigation uses real buttons. Inactive items and hidden actions are removed from keyboard navigation. On a successful transition focus moves to the new item; on validation failure focus moves to the selected or filled answer control, then to the first available control. Always provide an accessible label for a freeform input with its definition's `accessibility_label`; a visible label or equivalent custom composition can supplement it. The GPUI accessibility layer does not expose a direct `aria-invalid` builder. Questionnaire still exposes invalid state through its error alert, semantic group state, focus behavior, and destructive styling. ## Current scope The questionnaire asks one question at a time: parts belonging to any question other than the current one render nothing, so a single page of several questions is not what this component builds. The schema is fixed at construction — questions and choices cannot be inserted or reordered at runtime, though any of them can be disabled — and validators run synchronously. Persistence, transport, and submission side effects belong to the containing page, which subscribes to `QuestionnaireEvent`. ## API reference ### Compound parts - [Questionnaire] - [QuestionnaireProgress] - [QuestionnaireItem] - [QuestionnaireTitle] - [QuestionnaireDescription] - [QuestionnaireChoices] - [QuestionnaireChoice] - [QuestionnaireChoiceDescription] - [QuestionnaireInput] - [QuestionnaireError] - [QuestionnaireActions] - [QuestionnairePrevious] - [QuestionnaireSkip] - [QuestionnaireNext] - [QuestionnaireSubmit] ### State, answers, and events - [QuestionnaireState] - [QuestionnaireItemDefinition] - [QuestionnaireChoiceDefinition] - [QuestionnaireInputDefinition] - [QuestionnaireAnswer] - [QuestionnaireAnswers] - [QuestionnaireItemStatus] - [QuestionnaireShortcutMode] - [QuestionnaireProgressState] - [QuestionnaireItemState] - [QuestionnaireChoiceState] - [QuestionnaireNavigationState] - [QuestionnaireValidationContext] - [QuestionnaireValidator] - [QuestionnaireAnswerChange] - [QuestionnaireSubmission] - [QuestionnaireSubmissionItem] - [QuestionnaireEvent] - [QuestionnaireSchemaError] - [Sizable] [Questionnaire]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.Questionnaire.html [QuestionnaireProgress]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireProgress.html [QuestionnaireItem]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireItem.html [QuestionnaireTitle]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireTitle.html [QuestionnaireDescription]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireDescription.html [QuestionnaireChoices]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireChoices.html [QuestionnaireChoice]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireChoice.html [QuestionnaireChoiceDescription]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireChoiceDescription.html [QuestionnaireInput]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireInput.html [QuestionnaireError]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireError.html [QuestionnaireActions]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireActions.html [QuestionnairePrevious]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnairePrevious.html [QuestionnaireSkip]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireSkip.html [QuestionnaireNext]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireNext.html [QuestionnaireSubmit]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireSubmit.html [QuestionnaireState]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireState.html [QuestionnaireItemDefinition]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireItemDefinition.html [QuestionnaireChoiceDefinition]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireChoiceDefinition.html [QuestionnaireInputDefinition]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireInputDefinition.html [QuestionnaireAnswer]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireAnswer.html [QuestionnaireAnswers]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireAnswers.html [QuestionnaireItemStatus]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/enum.QuestionnaireItemStatus.html [QuestionnaireShortcutMode]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/enum.QuestionnaireShortcutMode.html [QuestionnaireProgressState]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireProgressState.html [QuestionnaireItemState]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireItemState.html [QuestionnaireChoiceState]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireChoiceState.html [QuestionnaireNavigationState]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireNavigationState.html [QuestionnaireValidationContext]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireValidationContext.html [QuestionnaireValidator]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/type.QuestionnaireValidator.html [QuestionnaireAnswerChange]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireAnswerChange.html [QuestionnaireSubmission]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireSubmission.html [QuestionnaireSubmissionItem]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/struct.QuestionnaireSubmissionItem.html [QuestionnaireEvent]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/enum.QuestionnaireEvent.html [QuestionnaireSchemaError]: https://docs.rs/gpui-component/latest/gpui_component/questionnaire/enum.QuestionnaireSchemaError.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html --- # VirtualList Source: /component/virtual-list VirtualList is a high-performance component designed for efficiently rendering large datasets by only rendering visible items. Unlike uniform lists, VirtualList supports variable item sizes, making it perfect for complex layouts like tables with different row heights or dynamic content. ## Import ```rust use gpui_kit::component::{ v_virtual_list, h_virtual_list, VirtualListScrollHandle, scroll::{Scrollbar, ScrollbarState, ScrollbarAxis}, }; use std::rc::Rc; use gpui_kit::{px, size, ScrollStrategy, Size, Pixels}; ``` ## Usage ### Basic Vertical Virtual List ```rust use std::rc::Rc; use gpui_kit::{px, size, Size, Pixels}; pub struct ListViewExample { items: Vec, item_sizes: Rc>>, scroll_handle: VirtualListScrollHandle, } impl ListViewExample { fn new(cx: &mut Context) -> Self { let items = (0..5000).map(|i| format!("Item {}", i)).collect::>(); let item_sizes = Rc::new(items.iter().map(|_| size(px(200.), px(30.))).collect()); Self { items, item_sizes, scroll_handle: VirtualListScrollHandle::new(), } } } impl Render for ListViewExample { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_virtual_list( cx.entity().clone(), "my-list", self.item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { div() .h(px(30.)) .w_full() .bg(cx.theme().secondary) .child(format!("Item {}", ix)) }) .collect() }, ) .track_scroll(&self.scroll_handle) } } ``` ### Horizontal Virtual List ```rust h_virtual_list( cx.entity().clone(), "horizontal-list", item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { div() .w(px(120.)) // Width is used for horizontal lists .h_full() .bg(cx.theme().accent) .child(format!("Card {}", ix)) }) .collect() }, ) .track_scroll(&scroll_handle) ``` ### Variable Item Sizes VirtualList excels at handling items with different sizes: ```rust let item_sizes = Rc::new( (0..1000) .map(|i| { // Different heights based on index let height = if i % 5 == 0 { px(60.) // Header items are taller } else if i % 3 == 0 { px(45.) // Some items are medium } else { px(30.) // Regular items }; size(px(300.), height) }) .collect::>() ); v_virtual_list( cx.entity().clone(), "variable-list", item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { let content = if ix % 5 == 0 { format!("Header {}", ix / 5) } else { format!("Item {}", ix) }; let bg_color = if ix % 5 == 0 { cx.theme().accent } else { cx.theme().secondary }; div() .w_full() .h(item_sizes[ix].height) .bg(bg_color) .flex() .items_center() .px_4() .child(content) }) .collect() }, ) ``` ### Table-like Layout with Multiple Columns VirtualList can render complex layouts like tables: ```rust v_virtual_list( cx.entity().clone(), "table-list", item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|row_ix| { h_flex() .w_full() .h(px(40.)) .border_b_1() .border_color(cx.theme().border) .children( // Multiple columns per row (0..5).map(|col_ix| { div() .flex_1() .h_full() .px_3() .flex() .items_center() .child(format!("R{}C{}", row_ix, col_ix)) }) ) }) .collect() }, ) ``` ### File Explorer with Virtual Scrolling ```rust pub struct FileExplorer { files: Vec, item_sizes: Rc>>, scroll_handle: VirtualListScrollHandle, selected_index: Option, } impl FileExplorer { fn calculate_item_heights(&mut self) { let sizes = self.files.iter().map(|file| { // Different heights for different file types let height = match file.file_type { FileType::Directory => px(40.), FileType::Image => px(60.), // Larger for thumbnails FileType::Document => px(35.), _ => px(30.), }; size(px(400.), height) }).collect(); self.item_sizes = Rc::new(sizes); } } impl Render for FileExplorer { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_virtual_list( cx.entity().clone(), "file-list", self.item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { let file = &view.files[ix]; let is_selected = view.selected_index == Some(ix); div() .w_full() .h(view.item_sizes[ix].height) .px_3() .py_1() .flex() .items_center() .gap_2() .bg(if is_selected { cx.theme().accent } else { Color::transparent() }) .hover(|style| style.bg(cx.theme().secondary_hover)) .child(file_icon(&file.file_type)) .child(file.name.clone()) .child( div() .flex_1() .text_right() .text_xs() .text_color(cx.theme().muted_foreground) .child(format_file_size(file.size)) ) .on_click(cx.listener(move |view, _, _, cx| { view.selected_index = Some(ix); cx.notify(); })) }) .collect() }, ) .track_scroll(&self.scroll_handle) } } ``` ### Chat Messages with Auto-scroll ```rust pub struct ChatWindow { messages: Vec, scroll_handle: VirtualListScrollHandle, auto_scroll: bool, } impl ChatWindow { fn add_message(&mut self, message: ChatMessage, cx: &mut Context) { self.messages.push(message); // Recalculate item sizes self.update_item_sizes(); if self.auto_scroll { // Scroll to bottom for new messages self.scroll_handle.scroll_to_bottom(); } cx.notify(); } fn update_item_sizes(&mut self) { let sizes = self.messages.iter().map(|msg| { // Calculate height based on message content let lines = msg.content.lines().count().max(1); let height = px(40. + (lines.saturating_sub(1)) as f32 * 16.); size(px(350.), height) }).collect(); self.item_sizes = Rc::new(sizes); } } impl Render for ChatWindow { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .child( v_virtual_list( cx.entity().clone(), "chat-messages", self.item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { let msg = &view.messages[ix]; div() .w_full() .px_4() .py_2() .child( v_flex() .gap_1() .child( h_flex() .justify_between() .child( div() .text_sm() .font_weight(FontWeight::SEMIBOLD) .child(msg.author.clone()) ) .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child(format_timestamp(msg.timestamp)) ) ) .child( div() .text_sm() .child(msg.content.clone()) ) ) }) .collect() }, ) .track_scroll(&self.scroll_handle) .flex_1() ) .child( // Chat input at bottom div() .w_full() .h(px(60.)) .border_t_1() .border_color(cx.theme().border) .child("Chat input here...") ) } } ``` ### Data Grid with Fixed Headers ```rust pub struct DataGrid { headers: Vec, data: Vec>, column_widths: Vec, scroll_handle: VirtualListScrollHandle, } impl Render for DataGrid { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .child( // Fixed header h_flex() .w_full() .h(px(40.)) .bg(cx.theme().secondary) .border_b_1() .border_color(cx.theme().border) .children( self.headers.iter().zip(&self.column_widths).map(|(header, &width)| { div() .w(width) .h_full() .px_3() .flex() .items_center() .font_weight(FontWeight::SEMIBOLD) .child(header.clone()) }) ) ) .child( // Virtual list for data rows v_virtual_list( cx.entity().clone(), "data-rows", Rc::new(vec![size(px(800.), px(32.)); self.data.len()]), |view, visible_range, _, cx| { visible_range .map(|row_ix| { h_flex() .w_full() .h(px(32.)) .border_b_1() .border_color(cx.theme().border.opacity(0.5)) .children( view.data[row_ix].iter().zip(&view.column_widths).map(|(cell, &width)| { div() .w(width) .h_full() .px_3() .flex() .items_center() .child(cell.clone()) }) ) }) .collect() }, ) .track_scroll(&self.scroll_handle) .flex_1() ) } } ``` ## Scroll Handling ### Basic Scroll Control ```rust pub struct ScrollableList { scroll_handle: VirtualListScrollHandle, scroll_state: ScrollbarState, } impl Render for ScrollableList { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .relative() .size_full() .child( v_virtual_list(/* ... */) .track_scroll(&self.scroll_handle) .p_4() .border_1() .border_color(cx.theme().border) ) .child( // Add scrollbars div() .absolute() .top_0() .left_0() .right_0() .bottom_0() .child( Scrollbar::both(&self.scroll_state, &self.scroll_handle) .axis(ScrollbarAxis::Vertical) ) ) } } ``` ### Programmatic Scrolling ```rust impl ScrollableList { // Scroll to specific item fn scroll_to_item(&self, index: usize) { self.scroll_handle.scroll_to_item(index, ScrollStrategy::Top); } // Center item in view fn center_item(&self, index: usize) { self.scroll_handle.scroll_to_item(index, ScrollStrategy::Center); } // Scroll to bottom fn scroll_to_bottom(&self) { self.scroll_handle.scroll_to_bottom(); } // Get current scroll position fn get_scroll_offset(&self) -> Point { self.scroll_handle.offset() } // Set scroll position manually fn set_scroll_position(&self, offset: Point) { self.scroll_handle.set_offset(offset); } } ``` ### Both Axis Scrolling For content that scrolls in both directions: ```rust v_virtual_list( cx.entity().clone(), "both-axis", item_sizes.clone(), |view, visible_range, _, cx| { visible_range .map(|ix| { // Wide content that requires horizontal scrolling h_flex() .gap_2() .children((0..20).map(|col| { div() .min_w(px(100.)) .h(px(30.)) .bg(cx.theme().secondary) .child(format!("R{}C{}", ix, col)) })) }) .collect() }, ) .track_scroll(&scroll_handle) .child( Scrollbar::both(&scroll_state, &scroll_handle) .axis(ScrollbarAxis::Both) ) ``` ## Performance Optimization ### Efficient Item Rendering Only visible items are rendered, making VirtualList highly performant: ```rust // The render function is only called for visible items v_virtual_list( cx.entity().clone(), "efficient-list", item_sizes.clone(), |view, visible_range, _, cx| { // visible_range contains only the items currently visible // This typically contains 10-20 items, not all 10,000 println!("Rendering {} items out of {}", visible_range.len(), view.total_items); visible_range .map(|ix| { // Complex rendering logic here // Only executed for visible items expensive_item_renderer(ix, cx) }) .collect() }, ) ``` ### Memory Management VirtualList automatically manages memory by: - Only rendering visible items - Reusing rendered elements when scrolling - Calculating precise visible ranges ```rust // Large dataset - only visible items use memory let large_dataset = (0..1_000_000).map(|i| format!("Item {}", i)).collect(); // Memory usage remains constant regardless of dataset size v_virtual_list(/* render only visible items */) ``` ### Variable Heights with Caching For dynamic content with calculated heights: ```rust struct DynamicItem { content: String, calculated_height: Option, } impl MyView { fn calculate_item_size(&mut self, ix: usize) -> Size { if let Some(height) = self.items[ix].calculated_height { return size(px(300.), height); } // Calculate height based on content let content_lines = self.items[ix].content.lines().count(); let height = px(20. + content_lines as f32 * 16.); // Cache the calculated height self.items[ix].calculated_height = Some(height); size(px(300.), height) } } ``` ## Best Practices 1. **Item Sizing**: Pre-calculate item sizes when possible for best performance 2. **Memory Management**: Use VirtualList for any list with >50 items 3. **Scroll Performance**: Avoid heavy computations in render functions 4. **State Management**: Keep item state separate from rendering logic 5. **Error Handling**: Handle edge cases like empty lists gracefully 6. **Testing**: Test with various data sizes and scroll positions ## Performance Tips 1. **Pre-calculate Sizes**: Calculate item sizes upfront rather than during render 2. **Minimize Re-renders**: Use stable item keys and avoid recreating render functions 3. **Batch Updates**: Group multiple data changes together 4. **Efficient Rendering**: Keep item render functions lightweight 5. **Memory Monitoring**: Monitor memory usage with very large datasets --- # Button Source: /component/button The [Button] element with multiple variants, sizes, and states. Supports icons, loading states, and can be grouped together. ## Import ```rust use gpui_kit::component::{ Sizable as _, button::{Button, ButtonGroup, ButtonVariants as _}, }; ``` ## Usage The marked recipe below is a complete, **Tested consumer recipe**. The remaining examples are contextual fragments; keep the imports above when using variant or size builders. ```rust use gpui_kit::IntoElement; use gpui_kit::component::{ Sizable as _, button::{Button, ButtonVariants as _}, }; pub fn primary_command() -> impl IntoElement { Button::new("save").primary().small().label("Save changes") } ``` ### Variants ```rust use gpui_kit::component::button::ButtonVariants as _; // Primary button Button::new("btn-primary").primary().label("Primary") // Secondary button (default) Button::new("btn-secondary").label("Secondary") // Danger button Button::new("btn-danger").danger().label("Delete") // Warning button Button::new("btn-warning").warning().label("Warning") // Success button Button::new("btn-success").success().label("Success") // Info button Button::new("btn-info").info().label("Info") // Ghost button Button::new("btn-ghost").ghost().label("Ghost") // Link button Button::new("btn-link").link().label("Link") // Text button Button::new("btn-text").text().label("Text") ``` ### Outline Outline style is not a variant itself, but can be combined with other variants. ```rust use gpui_kit::component::button::ButtonVariants as _; Button::new("btn").primary().outline().label("Primary Outline") Button::new("btn").danger().outline().label("Danger Outline") ``` ### Sizes The `compact` method reduces the padding of the button for a more condensed appearance. ```rust // Compact (reduced padding) Button::new("btn") .label("Compact") .compact() ``` ### Icons and states The `icon` method supports multiple types, allowing you to use different visual indicators: - **[Icon] / [IconName]** - Static icons for actions and visual cues - **[Spinner]** - Animated loading indicator for async operations - **[ProgressCircle]** - Circular progress indicator showing completion percentage All icon types automatically adapt to the button's size and can be customized with colors and other properties. #### Icon Types ```rust use gpui_kit::component::{Icon, IconName}; // Using IconName (simplest) Button::new("btn") .icon(IconName::Check) .label("Confirm") // Using Icon with custom size Button::new("btn") .icon(Icon::new(IconName::Heart)) .label("Like") // Icon only (no label) Button::new("btn") .icon(IconName::Search) ``` #### Spinner Icon Use a [Spinner] to indicate loading or processing state: ```rust use gpui_kit::component::{ActiveTheme as _, spinner::Spinner}; // Basic spinner Button::new("btn") .icon(Spinner::new()) .label("Loading...") // Spinner with custom color Button::new("btn") .icon(Spinner::new().color(cx.theme().blue)) .label("Processing") // Spinner with icon Button::new("btn") .icon(Spinner::new().icon(IconName::LoaderCircle)) .label("Syncing") ``` #### ProgressCircle Icon Use a [ProgressCircle] to show progress percentage: ```rust use gpui_kit::component::{ ActiveTheme as _, Sizable as _, button::ButtonVariants as _, progress::ProgressCircle, }; // Basic progress circle Button::new("btn") .icon(ProgressCircle::new("install-progress").value(45.0)) .label("Installing...") // Progress circle with custom color Button::new("btn") .primary() .icon( ProgressCircle::new("download-progress") .value(75.0) .color(cx.theme().primary_foreground) ) .label("Downloading") // Different sizes Button::new("btn") .small() .icon(ProgressCircle::new("progress-1").value(60.0)) .label("Installing...") Button::new("btn") .large() .icon(ProgressCircle::new("progress-2").value(80.0)) .label("Installing...") ``` #### Dynamic Icon Updates Icons can be updated dynamically based on component state: ```rust struct InstallButton { progress: f32, is_installing: bool, } impl InstallButton { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let button = Button::new("install-btn") .label(if self.is_installing { "Installing..." } else { "Install" }); if self.is_installing { button.icon( ProgressCircle::new("install-progress") .value(self.progress) ) } else { button.icon(IconName::Download) } } } ``` #### Loading State with Icons When a button is in loading state, it automatically handles icon transitions: ```rust // If icon is already a Spinner or ProgressCircle, it will be shown during loading Button::new("btn") .icon(Spinner::new()) .label("Processing") .loading(true) // Spinner will continue to show // If icon is a regular Icon, it will be replaced with a Spinner during loading Button::new("btn") .icon(IconName::Save) .label("Saving") .loading(true) // Icon will be replaced with Spinner ``` ### Button group ```rust Button::new("my-button") .label("Click me") .on_click(|_, _, _| { println!("Button clicked!"); }) ``` ### Icon only The `.dropdown_caret` method can allows adding a dropdown caret icon to end of the button. ```rust Button::new("btn") .label("Options") .dropdown_caret(true) ``` ### Sizeable The Button supports the [Sizable] trait for different sizes. ```rust use gpui_kit::component::Sizable as _; Button::new("btn").xsmall().label("Extra Small") Button::new("btn").small().label("Small") Button::new("btn").label("Medium") // default Button::new("btn").large().label("Large") ``` ### Button States There have `disabled`, `loading`, `selected` state for buttons to indicate different statuses. ```rust use gpui_kit::component::{Disableable as _, Selectable as _}; // Disabled Button::new("btn") .label("Disabled") .disabled(true) // Loading Button::new("btn") .label("Loading") .loading(true) // Selected Button::new("btn") .label("Selected") .selected(true) ``` ### With Tooltip ```rust Button::new("btn") .label("Hover me") .tooltip("This is a helpful tooltip") .tooltip_placement(Placement::Bottom) ``` Use `.tooltip_placement(...)` to prefer a side for either `.tooltip(...)` or `.tooltip_with_action(...)`. The tooltip still flips when that side does not fit. Omit placement to keep automatic positioning. ### Custom Children ```rust Button::new("btn") .child( h_flex() .items_center() .gap_2() .child("Custom Content") .child(IconName::ChevronDown) .child(IconName::Eye) ) ``` [Button]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.Button.html [ButtonGroup]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.ButtonGroup.html [ButtonCustomVariant]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.ButtonCustomVariant.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html [Spinner]: https://docs.rs/gpui-component/latest/gpui_component/spinner/struct.Spinner.html [ProgressCircle]: https://docs.rs/gpui-component/latest/gpui_component/progress/struct.ProgressCircle.html [Icon]: https://docs.rs/gpui-component/latest/gpui_component/icon/struct.Icon.html [IconName]: https://docs.rs/gpui-component/latest/gpui_component/icon/enum.IconName.html ## Button Group ```rust ButtonGroup::new("btn-group") .child(Button::new("btn1").label("One")) .child(Button::new("btn2").label("Two")) .child(Button::new("btn3").label("Three")) ``` ### Toggle Button Group ```rust use gpui_kit::component::Selectable as _; ButtonGroup::new("toggle-group") .multiple(true) // Allow multiple selections .child(Button::new("btn1").label("Option 1").selected(true)) .child(Button::new("btn2").label("Option 2")) .child(Button::new("btn3").label("Option 3")) .on_click(|selected_indices, _, _| { println!("Selected: {:?}", selected_indices); }) ``` ## Custom Variant ```rust use gpui_kit::component::{ ActiveTheme as _, Colorize as _, button::{ButtonCustomVariant, ButtonVariants as _}, }; let custom = ButtonCustomVariant::new(cx) .color(cx.theme().magenta) .foreground(cx.theme().primary_foreground) .hover(cx.theme().magenta.opacity(0.1)) .active(cx.theme().magenta); Button::new("custom-btn") .custom(custom) .label("Custom Button") ``` ## API Reference - [Button] - [ButtonGroup] - [ButtonCustomVariant] --- # Toggle Source: /component/toggle A button-style toggle component that represents on/off or selected states. Unlike a traditional switch, toggles appear as buttons that can be pressed in or out. They're perfect for toolbar buttons, filter options, or any binary choice that benefits from a button-like appearance. ## Import ```rust use gpui_kit::component::button::{Toggle, ToggleGroup}; ``` ## Usage ### Toggle ```rust Toggle::new("toggle1"). .label("Toggle me") .checked(false) .on_click(|checked, _, _| { println!("Toggle is now: {}", checked); }) ``` Here, we can use `on_click` to handle toggle state changes. The callback receives the **new checked state** as a `bool`. ### Group ```rust use gpui_kit::component::IconName; Toggle::new("toggle2") .icon(IconName::Eye) .checked(true) .on_click(|checked, _, _| { println!("Visibility: {}", if *checked { "shown" } else { "hidden" }); }) ``` ### Controlled Toggle ```rust struct MyView { is_active: bool, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Toggle::new("active") .label("Active") .checked(self.is_active) .on_click(cx.listener(|view, checked, _, cx| { view.is_active = *checked; cx.notify(); })) } } ``` ### Toggle Variants ```rust // Ghost toggle (default) Toggle::new("ghost-toggle") .ghost() .label("Ghost") // Outline toggle Toggle::new("outline-toggle") .outline() .label("Outline") ``` ### Different Sizes ```rust // Extra small Toggle::new("xs-toggle") .icon(IconName::Star) .xsmall() // Small Toggle::new("small-toggle") .label("Small") .small() // Medium (default) Toggle::new("medium-toggle") .label("Medium") // Large Toggle::new("large-toggle") .label("Large") .large() ``` ### Disabled State ```rust // Disabled unchecked Toggle::new("disabled-toggle") .label("Disabled") .disabled(true) .checked(false) // Disabled checked Toggle::new("disabled-checked-toggle") .label("Selected (Disabled)") .disabled(true) .checked(true) ``` ### Toolbar with Toggle Buttons ```rust struct EditorToolbar { bold: bool, italic: bool, underline: bool, strikethrough: bool, } h_flex() .gap_1() .p_2() .bg(cx.theme().background) .border_1() .border_color(cx.theme().border) .child( ToggleGroup::new("formatting") .small() .child(Toggle::new(0).icon(IconName::Bold).checked(self.bold)) .child(Toggle::new(1).icon(IconName::Italic).checked(self.italic)) .child(Toggle::new(2).icon(IconName::Underline).checked(self.underline)) .child(Toggle::new(3).icon(IconName::Strikethrough).checked(self.strikethrough)) .on_click(cx.listener(|view, states, _, cx| { view.bold = states[0]; view.italic = states[1]; view.underline = states[2]; view.strikethrough = states[3]; cx.notify(); })) ) ``` ### Filter Interface ```rust struct FilterPanel { show_completed: bool, show_pending: bool, show_cancelled: bool, show_urgent: bool, } v_flex() .gap_3() .p_4() .child(Label::new("Filter by status")) .child( ToggleGroup::new("status-filters") .outline() .child(Toggle::new(0).label("Completed").checked(self.show_completed)) .child(Toggle::new(1).label("Pending").checked(self.show_pending)) .child(Toggle::new(2).label("Cancelled").checked(self.show_cancelled)) .on_click(cx.listener(|view, states, _, cx| { view.show_completed = states[0]; view.show_pending = states[1]; view.show_cancelled = states[2]; cx.notify(); })) ) .child( Toggle::new("urgent-filter") .label("Show urgent only") .checked(self.show_urgent) .on_click(cx.listener(|view, checked, _, cx| { view.show_urgent = *checked; cx.notify(); })) ) ``` ### Settings with Individual Toggles ```rust struct NotificationSettings { email_notifications: bool, push_notifications: bool, marketing_emails: bool, } v_flex() .gap_4() .child( h_flex() .items_center() .justify_between() .child( v_flex() .child(Label::new("Email notifications")) .child( Label::new("Receive notifications via email") .text_color(cx.theme().muted_foreground) .text_sm() ) ) .child( Toggle::new("email-notifications") .icon(IconName::Mail) .checked(self.email_notifications) .on_click(cx.listener(|view, checked, _, cx| { view.email_notifications = *checked; cx.notify(); })) ) ) .child( h_flex() .items_center() .justify_between() .child(Label::new("Push notifications")) .child( Toggle::new("push-notifications") .icon(IconName::Bell) .checked(self.push_notifications) .on_click(cx.listener(|view, checked, _, cx| { view.push_notifications = *checked; cx.notify(); })) ) ) ``` ### Multi-select Options ```rust struct SelectionView { selected_categories: Vec, } impl SelectionView { fn categories() -> Vec<&'static str> { vec!["Technology", "Design", "Business", "Science", "Art"] } } v_flex() .gap_3() .child(Label::new("Select categories of interest")) .child( ToggleGroup::new("categories") .children( Self::categories() .into_iter() .enumerate() .map(|(i, category)| { Toggle::new(i) .label(category) .checked(self.selected_categories.get(i).copied().unwrap_or(false)) }) ) .on_click(cx.listener(|view, states, _, cx| { view.selected_categories = states.clone(); cx.notify(); })) ) ``` ## Toggle vs Switch | Feature | Toggle | Switch | | ---------------------- | ------------------------------------------- | ----------------------------------------- | | **Appearance** | Button-like, can be pressed in/out | Traditional switch with sliding indicator | | **Use Cases** | Toolbar buttons, filters, binary options | Settings, preferences, on/off states | | **Visual Style** | Rectangular button shape | Rounded switch track with thumb | | **State Indication** | Background color change, pressed appearance | Position of sliding thumb | | **Multiple Selection** | Supports groups with multiple selection | Individual switches only | **Use Toggle when you want:** - Button-like appearance for binary states - Grouping multiple related options - Toolbar or filter interfaces - Options that feel like "selections" rather than "settings" **Use Switch when you want:** - Traditional on/off control appearance - Settings or preferences interface - Clear visual indication of state with sliding animation - Individual boolean controls ## Integration with ToggleGroup Toggle buttons can be grouped together using `ToggleGroup` for related options: ### Basic Toggle Group ```rust ToggleGroup::new("filter-group") .child(Toggle::new(0).icon(IconName::Bell)) .child(Toggle::new(1).icon(IconName::Bot)) .child(Toggle::new(2).icon(IconName::Inbox)) .child(Toggle::new(3).label("Other")) .on_click(|checkeds, _, _| { println!("Selected toggles: {:?}", checkeds); }) ``` The `on_click` callback receives a `Vec` representing the **new checked state** of each toggle in the group. ### Toggle Group with Controlled State ```rust struct FilterView { notifications: bool, bots: bool, inbox: bool, other: bool, } impl Render for FilterView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { ToggleGroup::new("filters") .child(Toggle::new(0).icon(IconName::Bell).checked(self.notifications)) .child(Toggle::new(1).icon(IconName::Bot).checked(self.bots)) .child(Toggle::new(2).icon(IconName::Inbox).checked(self.inbox)) .child(Toggle::new(3).label("Other").checked(self.other)) .on_click(cx.listener(|view, checkeds, _, cx| { view.notifications = checkeds[0]; view.bots = checkeds[1]; view.inbox = checkeds[2]; view.other = checkeds[3]; cx.notify(); })) } } ``` ### Toggle Group Variants and Sizes ```rust // Outline variant, small size ToggleGroup::new("compact-filters") .outline() .small() .child(Toggle::new(0).icon(IconName::Filter)) .child(Toggle::new(1).icon(IconName::Sort)) .child(Toggle::new(2).icon(IconName::Search)) // Ghost variant (default), extra small ToggleGroup::new("mini-toolbar") .xsmall() .child(Toggle::new(0).icon(IconName::Bold)) .child(Toggle::new(1).icon(IconName::Italic)) .child(Toggle::new(2).icon(IconName::Underline)) ``` ### Segmented Toggle Group Use `segmented()` when a group should render as a connected segmented control. The group still uses the same multi-toggle behavior: `on_click` receives a `Vec` with the new checked state for each item. ```rust ToggleGroup::new("formatting") .segmented() .outline() .child(Toggle::new(0).label("Bold").checked(self.bold)) .child(Toggle::new(1).label("Italic").checked(self.italic)) .child(Toggle::new(2).label("Code").checked(self.code)) .on_click(cx.listener(|view, states, _, cx| { view.bold = states[0]; view.italic = states[1]; view.code = states[2]; cx.notify(); })) ``` By default, segmented groups use a zero gap so adjacent items share one outline. Pass a non-zero gap when you want the segmented sizing and variants but separated items: ```rust use gpui_kit::px; ToggleGroup::new("quick-actions") .segmented() .outline() .gap(px(8.)) .small() .child(Toggle::new(0).label("Star")) .child(Toggle::new(1).label("Watch")) .child(Toggle::new(2).label("Pin")) ``` If you need mutually exclusive behavior, keep that state in your view model and set only one child to `checked(true)` until a dedicated single-selection API is available. ## Event Handling ### Individual Toggle Events ```rust Toggle::new("subscribe-toggle") .label("Subscribe") .on_click(|checked, window, cx| { if *checked { // Handle subscription logic println!("Subscribed!"); } else { // Handle unsubscription logic println!("Unsubscribed!"); } }) ``` ## Best Practices 1. **Use meaningful labels**: Choose clear, descriptive text for toggle labels 2. **Group related options**: Use ToggleGroup for logically related binary choices 3. **Provide visual feedback**: The checked state should be clearly distinguishable 4. **Consider context**: Use toggles for options that feel like "selections" rather than "settings" 5. **Maintain state consistency**: Ensure toggle state reflects the actual application state 6. **Accessible labels**: Provide tooltips or ARIA labels for icon-only toggles --- # Carousel Source: /component/carousel Carousel displays one or more related items in a snapping viewport. It supports horizontal and vertical layouts, keyboard navigation, pointer and trackpad gestures, looping, and controlled selection. ## Import ```rust use gpui_kit::Axis; use gpui_kit::component::carousel::{ Carousel, CarouselContent, CarouselEvent, CarouselItem, CarouselNext, CarouselPagination, CarouselPaginationItem, CarouselPrevious, CarouselState, }; ``` ## Usage Create one `CarouselState` for the content and pass it to every carousel part. ```rust let state = cx.new(|_| CarouselState::new(3)); Carousel::new("projects-carousel", &state) .child( CarouselContent::new(&state) .child(CarouselItem::new("project-1", 0, &state).child("Project one")) .child(CarouselItem::new("project-2", 1, &state).child("Project two")) .child(CarouselItem::new("project-3", 2, &state).child("Project three")), ) .child(CarouselPrevious::new(&state)) .child(CarouselNext::new(&state)) ``` `CarouselContent` owns the viewport and snap layout. `CarouselItem` identifies one logical slide. The previous and next controls automatically become disabled at the corresponding boundary. Keep the state's item count equal to the number of direct `CarouselItem` children. A state and its scroll handle belong to one viewport. ## Composition Build a Carousel from one content viewport, its items, and optional controls: ```text Carousel ├── CarouselContent │ ├── CarouselItem │ └── CarouselItem ├── CarouselPrevious └── CarouselNext ``` Constrain the Carousel with `.w_full().max_w_96()` on its root, or style `CarouselContent` when the viewport itself needs a custom width or height. Use `track_style` only for inner-track adjustments such as spacing. The root lays out its flow children as a column with a 16px gap, so a `CarouselPagination` placed after the content keeps its distance; restyle the root for another arrangement. ## Multiple items `CarouselItem` implements `Styled`. Set its flex basis to show more than one item in the viewport, and pair a negative leading margin on the content track with matching leading padding on every item to tune the gap between them. This is the same paired spacing model shadcn/ui uses. ```rust use gpui_kit::{ParentElement as _, StyleRefinement, Styled as _, relative}; let state = cx.new(|_| CarouselState::new(6)); CarouselContent::new(&state) .track_style(StyleRefinement::default().ml_neg_1()) .children((0..6).map(|index| { CarouselItem::new(("project", index), index, &state) .flex_basis(relative(1. / 3.)) .pl_1() .child(format!("Project {}", index + 1)) })) ``` The flex basis controls item geometry; it is separate from the semantic `Size` used by buttons and other controls. Horizontal carousels default to `.ml_neg_4()` on the content track and `.pl_4()` on items. Vertical carousels use the corresponding `.mt_neg_4()` and `.pt_4()` pair. Override both sides with the same spacing scale so the first item stays aligned with the viewport while the visual gap changes. ## Orientation Use `with_axis` when creating the state: ```rust let state = cx.new(|_| { CarouselState::new(3).with_axis(Axis::Vertical) }); ``` Horizontal carousels use Left and Right. Vertical carousels use Up and Down. Give vertical `CarouselContent` an explicit height so each full-height item has a viewport to snap within. The Carousel root is a tab stop, so keyboard navigation also works when optional controls are omitted. Home and End select the first and last items. Clicking inside the carousel or on one of its controls focuses it for keyboard navigation without drawing the focus ring; the ring appears only when focus arrives from the keyboard. ## Looping Enable looping to wrap navigation from the last item to the first: ```rust let state = cx.new(|_| CarouselState::new(5).with_looping(true)); ``` ## Controlled selection `CarouselState` can be controlled by application state. Use `with_selected_index` for the initial selection and `set_selected_index` for programmatic changes. ```rust let state = cx.new(|_| CarouselState::new(4).with_selected_index(1)); state.update(cx, |state, cx| { state.set_selected_index(3, cx); }); ``` Subscribe to `CarouselEvent::Change` when the application needs to mirror the selected item: ```rust cx.subscribe(&state, |this, _, event: &CarouselEvent, cx| { let CarouselEvent::Change(index) = event; this.selected_index = *index; cx.notify(); }); ``` ## Events | Event | Description | | --- | --- | | `CarouselEvent::Change(index)` | Emitted when user navigation selects a new item. | Keyboard navigation and previous/next controls use the same state transition and emit the same event. Pointer and trackpad gestures select the nearest snap point when the gesture ends. A mouse-wheel notch moves one item, and a gesture that begins at an edge scrolls the surrounding container instead. ## Pagination indicators Pagination is optional and does not impose one visual treatment. Compose indicators with `CarouselPaginationItem`, then style or fill each item as needed: ```rust CarouselPagination::new().children((0..3).map(|index| { CarouselPaginationItem::new(("project-page", index), index, &state) .child((index + 1).to_string()) })) ``` `CarouselPaginationItem` uses the same selection transition as pointer, keyboard, and previous/next navigation. ## Control size `CarouselPrevious`, `CarouselNext`, and `CarouselPaginationItem` implement `Sizable`. Apply the same semantic size to the controls when they should scale together: ```rust use gpui_kit::component::{Sizable as _, Size}; CarouselPrevious::new(&state).with_size(Size::Large); CarouselNext::new(&state).with_size(Size::Large); ``` Previous and next controls default to `Size::Medium`. Pagination items default to `Size::XSmall`. ## Custom controls `CarouselPrevious` and `CarouselNext` implement `ParentElement` and `Styled`. Without children they display the direction-appropriate chevron. Add a child to replace that visible content while preserving automatic navigation and disabled boundary states. `accessibility_label` also replaces the control's tooltip. ```rust use gpui_kit::ParentElement as _; CarouselPrevious::new(&state) .accessibility_label("Previous project") .child("Back"); CarouselNext::new(&state) .accessibility_label("Next project") .child("Forward"); ``` For a completely custom control, omit the corresponding Carousel part and compose any control with the public state API: ```rust use gpui_kit::ParentElement as _; use gpui_kit::component::{Disableable as _, button::Button}; let previous_state = state.clone(); let previous_disabled = !state.read(cx).has_previous(); Button::new("projects-previous") .label("Back") .disabled(previous_disabled) .on_click(move |_, _, cx| { previous_state.update(cx, |state, cx| { state.select_previous(cx); }); }) ``` ## Accessibility The carousel exposes a labelled region and each item reports its position within the set. Use `accessibility_label` when the default "Carousel" label does not describe the content. Carousel animation follows the application's reduced-motion preference. --- # TextView Source: /component/text-view `TextView` renders formatted text in GPUI. It supports Markdown and simple HTML, text selection, code block actions, and custom Markdown plugins for project-specific syntax. The canonical implementation now lives in `gpui-base`; this module remains a compatibility re-export and provides component-theme adaptation. Base-only setup, complete default styling, and opt-in syntax highlighting are documented on [GPUI Base TextView](/base/text-view). TextView is selectable by default and uses the shared window selection engine from `gpui-base`. Use `.selectable(false)` only when selection must be disabled. See [GPUI Base Text Selection](/base/text-selection) when integrating plain text or a custom renderer with the same selection. ## Import ```rust use gpui_kit::component::text::{markdown, TextView}; ``` ## Usage ### Markdown Use the `markdown` helper when you only need to render Markdown text: ```rust use gpui_kit::component::text::markdown; markdown("# Hello\n\nThis is **Markdown**.") .scrollable(true) ``` You can also construct a `TextView` directly when you need a stable id: ```rust use gpui_kit::component::text::TextView; TextView::markdown("preview", markdown_source) ``` ### HTML ```rust TextView::html("html-preview", "Hello") ``` ### Clamp to a number of lines Use `max_lines` to render a bounded preview of rich content — for example a collapsed "show more" section. The view's height is capped at `n` × the base line height, and a line of glyphs is never cut in half: a line that would straddle the bottom of the box is left out whole, across paragraphs, lists, headings, code blocks and tables: ```rust TextView::markdown("preview", markdown_source).max_lines(5) ``` Nothing is shown with less than a line of itself to show, so the border and padding a table row leads with never strands at the bottom. Whatever has more than that is cut on the box edge and keeps the part that fits, so an image crossing the edge shows instead of disappearing and leaving blank space behind. `TextViewState::is_clamped()` reports whether the previous painted frame actually clipped content, so the caller can decide whether to render an "expand" affordance. `n` counts lines of body text, so paragraph spacing and taller lines mean fewer of them fit inside the capped height, and a line taller than the whole budget keeps the part that fits rather than emptying the box. `max_lines` only applies to the fit-content mode and is ignored when `scrollable` is set. ### Fade in streamed text A chat reply arrives in chunks. `stream_fade(true)` fades each chunk in where it lands instead of popping it onto the screen, the way Claude reveals a response: ```rust TextView::new(&self.reply).stream_fade(true) ``` The fade follows the rendered text. Whatever a `push_str`, or a `set_text` whose text extends the current one, adds starts transparent and reaches full color over 350 ms on an ease-out curve, the timing measured from Claude: longer than the 50–300 ms a model's chunks arrive at, so consecutive chunks overlap into one gradient tail rather than the newest chunk blinking in. Code in fenced blocks and text in table cells fade the same way. Markdown that completes as it streams (`**bo` becoming bold `bold`) fades the changed glyphs rather than the whole paragraph. Text that replaces the current content shows at once, and so does everything when the system asks for reduced motion. Nothing animates unless the view opts in. Pass a `TextViewMotion` through `.motion(...)` to choose the duration or easing yourself, or to reveal each chunk word by word; see [GPUI Base TextView](/base/text-view#retained-state-and-streaming-updates). ### Highlight ranges An application that searches a document, or points at a citation inside it, paints its ranges with `set_range_highlights`. The application owns the search: it finds its ranges in `rendered_text()`, the text the view shows, and hands them back with the colors to paint them in, a stronger one for the current result: ```rust use gpui_kit::component::{ ActiveTheme as _, text::{RangeHighlight, RangeHighlightError, TextViewState}, }; fn highlight_matches( state: &mut TextViewState, query: &str, current_match: usize, cx: &mut Context, ) -> Result<(), RangeHighlightError> { let (color, current_color) = (cx.theme().warning.opacity(0.3), cx.theme().warning); let text = state.rendered_text(); let matches = if query.is_empty() { Vec::new() } else { text.as_str().match_indices(query).collect() }; let highlights = matches.into_iter().enumerate().map(|(ix, (start, found))| { RangeHighlight::new( start..start + found.len(), if ix == current_match { current_color } else { color }, ) }); state.set_range_highlights(highlights, cx) } ``` `rendered_text()` is the text plain copy produces: `hello **world**` reads `hello world`, escapes are resolved, and heading and list markers are left out. Offsets are UTF-8 byte offsets, so the ranges `str` search returns can be passed as they are, and a repeated phrase is addressed by where it occurs. The text is built the first time it is read. A highlight is painted behind the text and under the selection, so wrapping, alignment, syntax colors, links, selection and copy stay as they were. Where highlights overlap, the later one paints over the earlier. A range that crosses from one block into the next paints in both. Text that belongs to no block is left unpainted: the line breaks between blocks, the spaces between table cells, custom blocks, HTML blocks and inline plugin objects. Only a range that is reversed, out of bounds or not on a character boundary is rejected, and the whole set with it. When the content changes, a highlight follows its block and stays as far as the block's text is unchanged. Text appended while streaming, through `push_str` or `set_text`, keeps the highlights before it, and an edit keeps those before and after it. After an edit inside a table, the cells in and after the edited row lose theirs, since a cell is only known by its place in the table. The view notifies when its text changes: observe the state and search the new `rendered_text()` again. Compute ranges and call `set_range_highlights` in the same state update so the ranges address the current text. Backgrounds that are part of the text, such as `` and syntax highlighting, paint over a range highlight (inline code's background is painted under it), and highlights do not fade in with streamed text. HTML views do not support range highlights. ### Scroll to a range `reveal_range` scrolls to a range of the same text, such as the current result when the user steps to the next one: ```rust state.reveal_range(current_range, cx)?; ``` It scrolls the line the range starts on into view, down to a line in the middle of a long paragraph, and leaves the view where it is when that line, or a whole block revealed, is already visible. An empty range reveals the line of its position. A `scrollable` view scrolls itself. A fit-content view scrolls the nearest enclosing `gpui::list`, as a chat transcript is, as long as the row that holds the view is laid out: scroll to that row first when it may be off screen. Any other scroll container, such as a `div` with `overflow_y_scroll`, scrolls through `on_reveal`, which receives the line's bounds in window coordinates: ```rust let scroll = scroll_handle.clone(); TextView::new(&state).on_reveal(move |line, _, _| { let viewport = scroll.bounds(); let mut offset = scroll.offset(); if line.bottom() > viewport.bottom() { offset.y -= line.bottom() - viewport.bottom(); } else if line.top() < viewport.top() { offset.y += viewport.top() - line.top(); } scroll.set_offset(offset); }) ``` A range that covers no block's text, such as a custom block's, scrolls its whole block into a scrollable view. Only the latest reveal is carried out. It follows the content the way highlights do, and it is dropped when its text changes, when the view clamps its lines with `max_lines`, or when it cannot be shown within a second, so it never scrolls long after it was asked for. Text scrolled sideways inside a table stays where it is, a block revealed whole and taller than the view shows its end when it comes from below, a scrollable view inside an application list scrolls only itself, and views sharing one state share one reveal. Revealing is best effort. `Ok(())` means the range is valid for the current text and the request was taken, not that the view has scrolled, and a dropped request is not reported. ## Touch Selection On a touch screen, a long press selects the word under the finger and keeps following the finger while it stays down. Lifting it opens an edit menu with `Copy` and `Select All` over the selection and puts a grab handle at each end. Dragging a handle moves that end while the other stays put; `Select All` selects the view that was pressed, and its handles keep working on the result. The handles and the menu are drawn by [`Root`](/component/root) for the whole window selection, so they cover a selection that spans several views. A tap elsewhere clears them, and the menu steps aside while the content scrolls under a finger. ## Link Click Handling Use `on_link_click` when links should be routed by the application instead of being opened directly by `App::open_url`. The callback receives the resolved URL and the original GPUI `ClickEvent`, so it can distinguish mouse buttons, keyboard activation, touch, and modifier keys: ```rust use gpui_kit::ClickEvent; use gpui_kit::component::text::markdown; markdown("[Open the project](https://github.com/MohsenDastaran/uni-kit)") .on_link_click(|url, event, _window, cx| { if event.is_right_click() { println!("Show a context menu for {url}"); return; } match event { ClickEvent::Mouse(click) if click.up.modifiers.control => { println!("Open {url} in an internal view"); } _ => cx.open_url(url), } }) ``` Installing a handler consumes the link event and disables the default URL opening behavior. If no handler is installed, links continue to use `App::open_url` as usual. The callback is used for both text links and linked images. ## Images A Markdown `![alt](src)` or HTML `` renders through GPUI's `img` element, and `src` decides where the bytes come from: - `http://` and `https://` URLs are fetched with the application's HTTP client. - `data:` URLs are decoded in place, so a document can embed its own images (`data:image/png;base64,…`, or a percent-encoded `data:image/svg+xml,…`). Any image format GPUI can decode is accepted; a `data:` URL with another media type is left to the loader and reports an error like any other unreachable image. - Every other value — a relative path, `file://`, a custom scheme — is passed through as a URI. `TextView` never reads the filesystem or the asset bundle on a document's behalf. To resolve those other sources, or to change how any image is loaded, wrap the `TextView` in an element that installs a GPUI `ImageCache`. Every `img` inside it, including the ones the document produces, asks that cache for its `Resource` before falling back to the default loader: ```rust use gpui_kit::{ImageCache, ImageCacheProvider}; div() .image_cache(app_image_cache.clone()) .child(markdown("![diagram](app://diagrams/pipeline.svg)")) ``` `ImageCache::load` receives the `Resource::Uri` and decides how to turn it into a `RenderImage`, so the application owns the loading policy while the document stays plain Markdown. ## Markdown Plugins Use `.plugin(...)` to support custom Markdown formats. A plugin owns both parsing and rendering, so callers only need to attach it to the `TextView`: ```rust markdown(source) .plugin(TickerPlugin::new()) ``` A Markdown plugin implements `MarkdownPlugin`: ```rust use gpui_kit::{App, IntoElement, ParentElement as _, Window}; use gpui_kit::component::text::{ markdown_ast, MarkdownNode, MarkdownParseContext, MarkdownPlugin, }; struct TickerNode { symbol: String, } struct TickerPlugin; impl TickerPlugin { fn new() -> Self { Self } } impl MarkdownPlugin for TickerPlugin { fn is_block(&self) -> bool { true } fn name(&self) -> &str { "ticker" } fn parse( &self, node: &markdown_ast::Node, cx: &MarkdownParseContext<'_>, ) -> Option { let markdown_ast::Node::Paragraph(paragraph) = node else { return None; }; let [markdown_ast::Node::Text(text)] = paragraph.children.as_slice() else { return None; }; let symbol = text.value.strip_prefix('$')?; Some( MarkdownNode::new( "ticker", TickerNode { symbol: symbol.to_string(), }, ) .text(format!("${symbol}")) .markdown(cx.node_source(node).unwrap_or(text.value.as_str())), ) } fn render( &self, node: &MarkdownNode, _window: &mut Window, _cx: &mut App, ) -> impl IntoElement { let ticker = node.data::().expect("ticker node data"); gpui_kit::div().child(format!("${}", ticker.symbol)) } } ``` Then attach it to a Markdown `TextView`: ```rust markdown("$AAPL.US") .plugin(TickerPlugin::new()) ``` ## MarkdownNode `MarkdownNode` is the neutral data passed between `parse` and `render`. ```rust MarkdownNode::new("ticker", TickerNode { symbol }) .text("$AAPL.US") .markdown("$AAPL.US") ``` - `name` is the stable node name used to match the renderer. - `data` is typed parser output read with `node.data::()`. - `text` is the plain text representation used by selection and fallback rendering. - `markdown` is the Markdown representation used when the document is serialized back to Markdown. ## Block Plugins Return `true` from `is_block()` to use the block parser and renderer: ```rust fn is_block(&self) -> bool { true } ``` Inline plugins use the default `is_block() == false` and return `Option` from `render_inline`. Wrap any GPUI element with `InlineElement::new(...)`, use native styles and events, and set an optional baseline. TextView measures and selects the whole element as one atom, with plain/Markdown copying, text fallback, and explicit asynchronous layout invalidation. See [Inline plugin](/base/text-view#inline-plugin) for the contract and `.plugin(...)` registration example. The component facade exports the same `InlineElement` and `InlineRenderContext` types. ## YAML Frontmatter YAML frontmatter is opt-in because it is not part of CommonMark or GFM. Enable the parser construct and attach `FrontmatterPlugin` to render top-level mappings as a `DescriptionList`: ```rust use gpui_component::text::{markdown, FrontmatterPlugin, MarkdownExtensions}; let extensions = MarkdownExtensions::default().frontmatter(); markdown("---\nname: example\ndescription: Example metadata.\n---") .markdown_extensions(extensions) .plugin(FrontmatterPlugin::new()) ``` Values are rendered as plain text. Simple unquoted values and block scalars using `|-` or `>-` are supported; literal scalars preserve content indentation. Quoted values, inline comments, collections, aliases, other block headers, and more-indented folded lines fall back to a YAML code block, preserving the source instead of displaying an incorrectly interpreted value. ## Code Block Actions You can render controls for Markdown code blocks: ```rust markdown(source) .code_block_actions(|code_block, _window, _cx| { gpui_kit::div().child(format!("Run {}", code_block.lang().unwrap_or_default())) }) ``` --- # Dialog Source: /component/dialog Dialog component for creating dialogs, confirmations, and alerts. Supports overlay, keyboard shortcuts, and various customizations. ## Import ```rust use gpui_kit::component::dialog::DialogButtonProps; use gpui_kit::component::WindowExt; ``` ## Usage ### Where dialogs render The window's [Root](/component/root) automatically mounts and renders dialogs. Open the window with `gpui_kit::open_window`, or wrap the application view in `Root::new`. Application views do not render overlay layers themselves. ### Basic Dialog ```rust window.open_dialog(cx, |dialog, _, _| { dialog .title("Welcome") .child("This is a dialog dialog.") }) ``` ### Form Dialog ```rust let input = cx.new(|cx| InputState::new(window, cx)); window.open_dialog(cx, |dialog, _, _| { dialog .title("User Information") .child( v_flex() .gap_3() .child("Please enter your details:") .child(Input::new(&input)) ) .footer(|_, _, _, _| { vec![ Button::new("ok") .primary() .label("Submit") .on_click(|_, window, cx| { window.close_dialog(cx); }), Button::new("cancel") .label("Cancel") .on_click(|_, window, cx| { window.close_dialog(cx); }), ] }) }) ``` ### Dialog with Icon ```rust window.open_dialog(cx, |dialog, _, cx| { dialog .child( h_flex() .gap_3() .child(Icon::new(IconName::TriangleAlert) .size_6() .text_color(cx.theme().warning)) .child("This action cannot be undone.") ) }) ``` ### Scrollable Dialog ```rust use gpui_kit::component::text::markdown; window.open_dialog(cx, |dialog, window, cx| { dialog .h(px(450.)) .title("Long Content") .child(markdown(long_markdown_text)) }) ``` A dialog never extends past the window. Its width is capped at the viewport minus a 16px margin on each side, and its height at the space between its top offset and a 16px bottom margin, so the title and footer stay visible while the body scrolls. `w`, `max_w`, `h`, and `margin_top` apply within those limits; a dialog that already fits keeps its requested size and default position. ### Dialog Options ```rust window.open_dialog(cx, |dialog, _, _| { dialog .title("Custom Dialog") .overlay(true) // Show overlay (default: true) .overlay_closable(true) // Click overlay to close (default: true) .keyboard(true) // ESC to close (default: true) .close_button(false) // Show close button (default: true) .child("Dialog content") }) ``` ### Action Buttons A `Dialog` puts its own buttons in the [`footer`](#dialogfooter) and has them dispatch `Confirm` or `Cancel`; `on_ok` and `on_cancel` decide what Enter and Esc do. For a confirmation with default buttons, use [AlertDialog](/component/alert-dialog). ### Nested Dialogs ```rust window.open_dialog(cx, |dialog, _, _| { dialog .title("First Dialog") .child("This is the first dialog") .footer(|_, _, _, _| { vec![ Button::new("open-another") .label("Open Another Dialog") .on_click(|_, window, cx| { window.open_dialog(cx, |dialog, _, _| { dialog .title("Second Dialog") .child("This is nested") }); }), ] }) }) ``` ### Custom Styling ```rust window.open_dialog(cx, |dialog, _, cx| { dialog .rounded(cx.theme().radius_lg) .bg(cx.theme().cyan) .text_color(cx.theme().info_foreground) .title("Custom Style") .child("Styled dialog content") }) ``` ### Custom Padding ```rust window.open_dialog(cx, |dialog, _, _| { dialog .p_3() // Custom padding .title("Custom Padding") .child("Dialog with custom spacing") }) ``` ### Close Dialog Programmatically The `close_dialog` method can be used to close the active dialog from anywhere within the window context. ```rust // Close top level active dialog. window.close_dialog(cx); // Close and perform action Button::new("submit") .primary() .label("Submit") .on_click(|_, window, cx| { // Do something window.close_dialog(cx); }) ``` ## Declarative API The Dialog component now supports a declarative API that provides a more React-like component composition pattern using dedicated header, title, description, and footer components. ### Import ```rust use gpui_kit::component::dialog::{ Dialog, DialogHeader, DialogTitle, DialogDescription, DialogFooter, }; ``` ### Trigger-based Dialog The trigger-based approach allows you to create a dialog that opens when a trigger element is clicked. The dialog is defined inline with the trigger. ```rust Dialog::new(cx) .trigger( Button::new("open-dialog") .outline() .label("Open Dialog") ) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Account Created")) .child(DialogDescription::new().child( "Your account has been created successfully!", )) ) .child( DialogFooter::new() .border_t_1() .border_color(cx.theme().border) .bg(cx.theme().muted) .child( Button::new("cancel") .outline() .label("Cancel") .on_click(|_, window, cx| { window.close_dialog(cx); }) ) .child( Button::new("ok") .primary() .label("Save Changes") ) ) }) ``` ### Content Builder Pattern Use the content builder pattern with `window.open_dialog` for more control over dialog creation: ```rust window.open_dialog(cx, |dialog, _, _| { dialog .w(px(400.)) .content(|content, _, _| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Custom Width")) .child(DialogDescription::new().child( "This dialog has a custom width of 400px.", )) ) .child(div().child( "Content area with custom width configuration." )) .child( DialogFooter::new() .justify_center() .child( Button::new("cancel") .flex_1() .outline() .label("Cancel") .on_click(|_, window, cx| { window.close_dialog(cx); }) ) .child( Button::new("done") .flex_1() .primary() .label("Done") .on_click(|_, window, cx| { window.close_dialog(cx); }) ) ) }) }) ``` ### Declarative Components #### DialogHeader Container for the dialog's title and description section. ```rust DialogHeader::new() .child(DialogTitle::new().child("Title")) .child(DialogDescription::new().child("Description")) ``` #### DialogTitle Displays the main title of the dialog with semantic styling. ```rust DialogTitle::new() .child("Account Settings") ``` #### DialogDescription Displays descriptive text below the title with muted styling. ```rust DialogDescription::new() .child("Update your account settings and preferences here.") ``` #### DialogFooter Container for action buttons and footer content. Automatically applies proper spacing and alignment. ```rust DialogFooter::new() .bg(cx.theme().muted) .border_t_1() .border_color(cx.theme().border) .child(Button::new("cancel").outline().label("Cancel")) .child(Button::new("save").primary().label("Save")) ``` ### Form Dialog with Declarative API ```rust let name_input = cx.new(|cx| InputState::new(window, cx)); let email_input = cx.new(|cx| InputState::new(window, cx)); Dialog::new(cx) .trigger(Button::new("edit-profile").label("Edit Profile")) .content(|content, _, cx| { content .child( DialogHeader::new() .child(DialogTitle::new().child("Edit Profile")) .child(DialogDescription::new().child( "Make changes to your profile here. Click save when done." )) ) .child( v_flex() .gap_4() .py_4() .child( v_flex() .gap_2() .child("Name") .child(Input::new(&name_input).placeholder("Enter your name")) ) .child( v_flex() .gap_2() .child("Email") .child(Input::new(&email_input).placeholder("Enter your email")) ) ) .child( DialogFooter::new() .child(Button::new("cancel").outline().label("Cancel")) .child(Button::new("save").primary().label("Save Changes")) ) }) ``` ### Styled Footer Customize the footer appearance with background colors, borders, and alignment: ```rust DialogFooter::new() .justify_center() // Center align buttons .bg(cx.theme().muted) // Background color .border_t_1() // Top border .border_color(cx.theme().border) .child(Button::new("btn1").flex_1().label("Cancel")) .child(Button::new("btn2").flex_1().primary().label("Confirm")) ``` ### DialogContent Container The `DialogContent` component provides a flexible container for dialog body content: ```rust use gpui_kit::component::dialog::DialogContent; window.open_dialog(cx, |dialog, _, _| { dialog.content(|content, _, cx| { content .child(DialogHeader::new() .child(DialogTitle::new().child("Settings")) .child(DialogDescription::new().child("Configure your preferences")) ) .child( div() .py_4() .child("Main content area") ) .child(DialogFooter::new() .child(Button::new("close").label("Close")) ) }) }) ``` ## API Reference - Declarative Components ### Dialog | Method | Description | | ------------------------ | ----------------------------------------------------- | | `new(cx)` | Create a new Dialog (no longer requires window param) | | `trigger(element)` | Set trigger element that opens the dialog | | `content(builder)` | Set content using a builder function | | `w(px)` / `width(px)` | Set dialog width | | `max_w(px)` | Set maximum width | | `margin_top(px)` | Set top margin | | `overlay(bool)` | Show/hide overlay (default: true) | | `overlay_closable(bool)` | Allow closing by clicking overlay (default: true) | | `keyboard(bool)` | Allow closing with ESC key (default: true) | | `close_button(bool)` | Show/hide close button (default: true) | ### DialogContent Container for dialog body content. Automatically applies padding and flex layout. ```rust DialogContent::new() .child(DialogHeader::new()...) .child(/* your content */) .child(DialogFooter::new()...) ``` ### DialogHeader Container for title and description. Automatically applies vertical flex layout with proper gap. ```rust DialogHeader::new() .child(DialogTitle::new().child("Title")) .child(DialogDescription::new().child("Description")) ``` ### DialogTitle Displays the dialog title with semantic styling (font-semibold, proper line-height). ```rust DialogTitle::new() .child("Dialog Title") ``` ### DialogDescription Displays descriptive text with muted foreground color and proper text sizing. ```rust DialogDescription::new() .child("This is a description text that provides more context.") ``` ### DialogFooter Container for footer buttons with automatic spacing and alignment. ```rust DialogFooter::new() .justify_end() // Right align (default) .child(Button::new("btn1").label("Cancel")) .child(Button::new("btn2").primary().label("OK")) ``` ## Breaking Changes ### Dialog::new() Signature Change The `Dialog::new()` constructor no longer requires a `window` parameter: ```rust // Old API (deprecated) Dialog::new(window, cx) // New API Dialog::new(cx) ``` ### Content Builder Function The `.content()` method now accepts a builder function instead of a pre-built `DialogContent`: ```rust // Old approach (still works) dialog.child(DialogHeader::new()...) // New declarative API dialog.content(|content, window, cx| { content .child(DialogHeader::new()...) .child(DialogFooter::new()...) }) ``` ## Best Practices 1. **Use Declarative Components**: Prefer `DialogHeader`, `DialogTitle`, `DialogDescription`, and `DialogFooter` for consistent styling 2. **Trigger-based for Simple Cases**: Use the trigger pattern for straightforward dialogs that open from a button 3. **Builder Pattern for Complex Dialogs**: Use `window.open_dialog` with content builder for dialogs requiring complex logic or state 4. **Semantic Structure**: Always include `DialogHeader` with title and description for accessibility 5. **Consistent Footer**: Use `DialogFooter` for all action buttons to maintain visual consistency 6. **Proper Sizing**: Explicitly set dialog width when content requires specific dimensions --- # Notification Source: /component/notification A toast notification system for displaying temporary messages to users. Notifications appear at the top right of the window and can auto-dismiss after a timeout. Supports multiple variants (info, success, warning, error), custom content, titles, and action buttons. Perfect for status updates, confirmations, and user feedback. ## Import ```rust use gpui_kit::component::{ notification::{Notification, NotificationType}, WindowExt }; ``` ## Usage ### Where notifications render The window's [Root](/component/root) automatically mounts and renders notifications. Open the window with `gpui_kit::open_window`, or wrap the application view in `Root::new`. Application views do not render overlay layers themselves. ### Basic Notification ```rust // Simple string notification window.push_notification("This is a notification.", cx); // Using Notification builder Notification::new() .message("Your changes have been saved.") ``` ### Notification Types ```rust // Info notification (blue) window.push_notification( (NotificationType::Info, "File saved successfully."), cx, ); // Success notification (green) window.push_notification( (NotificationType::Success, "Payment processed successfully."), cx, ); // Warning notification (yellow/orange) window.push_notification( (NotificationType::Warning, "Network connection is unstable."), cx, ); // Error notification (red) window.push_notification( (NotificationType::Error, "Failed to save file. Please try again."), cx, ); ``` ### Notification with Title ```rust Notification::new() .title("Update Available") .message("A new version of the application is ready to install.") .with_type(NotificationType::Info) ``` ### Auto-hide Control ```rust // Disable auto-hide (manual dismiss only) Notification::new() .message("This notification stays until manually closed.") .autohide(false) // Default auto-hide after 5 seconds Notification::new() .message("This will disappear automatically.") .autohide(true) // default ``` The countdown pauses while the pointer is over the notifications or one of them has keyboard focus, and resumes when the pointer leaves or focus moves on. It keeps running while the window is inactive, so a message that must not be missed should disable auto-hide or use system delivery. ### Placement Notifications appear at the top right of the window by default. Set a global default for all notifications, or override it for a single notification. Notifications are stacked separately for each placement. ```rust use gpui_kit::Anchor; // Global default (default: Anchor::TopRight) Theme::update(cx, |theme| theme.notification.placement = Anchor::BottomRight); // Per-notification override Notification::info("Download complete.") .placement(Anchor::BottomLeft) ``` Supported values are `Anchor::TopLeft`, `Anchor::TopCenter`, `Anchor::TopRight`, `Anchor::LeftCenter`, `Anchor::RightCenter`, `Anchor::BottomLeft`, `Anchor::BottomCenter`, and `Anchor::BottomRight`. ### With Action Button ```rust Notification::new() .title("Connection Lost") .message("Unable to connect to server.") .with_type(NotificationType::Error) .autohide(false) .action(|_, cx| { Button::new("retry") .primary() .label("Retry") .on_click(cx.listener(|this, _, window, cx| { // Perform retry action println!("Retrying connection..."); this.dismiss(window, cx); })) }) ``` ### Clickable Notifications ```rust Notification::new() .message("Click to view details") .on_click(cx.listener(|_, _, _, cx| { println!("Notification clicked"); // Handle notification click cx.notify(); })) ``` ### Custom Content ```rust use gpui_kit::component::text::markdown; let markdown_content = r#" ### Form Validation Error ```rust Notification::error("Please correct the following errors before submitting.") .title("Validation Failed") .autohide(false) .action(|_, _, cx| { Button::new("review") .outline() .label("Review Form") .on_click(cx.listener(|this, _, window, cx| { // Navigate to form this.dismiss(window, cx); })) }) ``` ### File Upload Progress ```rust struct UploadNotification; // Start upload notification window.push_notification( Notification::info("Uploading file...") .id::() .title("File Upload") .autohide(false), cx, ); // Update to success when complete window.push_notification( Notification::success("File uploaded successfully!") .id::() .title("Upload Complete"), cx, ); ``` ### System Status Updates ```rust // Warning about maintenance Notification::warning("System maintenance will begin in 30 minutes.") .title("Scheduled Maintenance") .autohide(false) .action(|_, cx| { Button::new("details") .link() .label("View Details") .on_click(cx.listener(|this, _, window, cx| { // Show maintenance details this.dismiss(window, cx); })) }) ``` ### Batch Operation Results ```rust use gpui_kit::component::text::markdown; let results_content = r#" ## Custom Notification - **Feature**: New dashboard available - **Status**: Ready to use - [Learn more](https://example.com) "#; Notification::new() .content(|_, window, cx| { markdown(markdown_content).into_any_element() }) ``` ### Unique Notifications When you need to manage notifications manually, such as for long-running processes or persistent alerts, you can use unique IDs to push and remove notifications as needed. In this case, you can create a special `struct` in local scope, and use `id` methods with this struct to identify the notification. Then you can push the notification when needed, and later remove it using the same ID. Like this: ```rust // Using type-based ID for uniqueness struct UpdateNotification; Notification::new() .id::() .message("System update available") .autohide(false) // Using type + element ID for multiple unique notifications struct TaskNotification; Notification::warning("Task failed to complete") .id1::("task-123") .title("Task Failed") ``` Then remove the notification with `window.remove_notification::`, like this: ```rust // Later, dismiss the notification window.remove_notification::(cx); ``` ### System Notification A notification can also be delivered to the operating system's notification center. Use `NotificationDelivery` to choose where a notification goes: as an in-app toast (`InApp`, the default), in the OS notification center (`System`), or both (`InAppAndSystem`). ```rust use gpui_kit::component::notification::{Notification, NotificationDelivery}; // Per-notification override; `.system()` and `.in_app_and_system()` are // shorthands for `.delivery(NotificationDelivery::...)`. Notification::info("Your download is ready.") .title("Download complete") .system() // Or set a global default for all notifications Theme::update(cx, |theme| { theme.notification.delivery = NotificationDelivery::InAppAndSystem }); ``` The notification's title and message become the system notification's title and body; a notification with neither is not posted. Pushing again with the same `.id::()` replaces the previous system notification, and `window.remove_notification::(cx)` / `window.clear_notifications(cx)` retract it. When the toast auto-hides, the system notification stays in the notification center. Clicking the system notification activates the application and its window, closes the in-app toast (if any), and fires `on_click` with a default `ClickEvent`. With `NotificationDelivery::System` no toast exists, so `on_close` is never called. `gpui_kit::component::init` registers the app-global `on_system_notification_response` handler, so do not register your own after it — gpui keeps only one. Notifications your application posts directly via `cx.show_system_notification` are left untouched. Platform requirements: | Platform | Requirement | Retraction | | --- | --- | --- | | macOS | Must run from a bundled `.app` in a trusted location (e.g. `/Applications`); silently disabled under plain `cargo run`. The first post triggers the system authorization prompt; a denial is remembered and later posts fail silently | Supported | | Windows | Call `cx.set_app_identity(identifier, name)` early in startup | Supported | | Linux | An XDG notification daemon must be present | Unsupported (ages out) | ## Batch Operation Complete **Processed**: 150 items **Success**: 147 items **Failed**: 3 items [View failed items](/) "#; Notification::success("Batch operation completed with some failures.") .title("Operation Results") .content(|window, cx| { markdown(results_content).into_any_element() }) .autohide(false) ``` ### Interactive Confirmation ```rust struct SaveConfirmation; Notification::new() .id::() .title("Unsaved Changes") .message("You have unsaved changes. Save before leaving?") .autohide(false) .action(|_, cx| { Button::new("save") .primary() .label("Save") .on_click(cx.listener(|this, _, window, cx| { // Perform save println!("Saving changes..."); this.dismiss(window, cx); })) }) .on_click(cx.listener(|_, _, _, cx| { println!("Save reminder clicked"); cx.notify(); })) ``` --- # Textarea Source: /component/textarea `Textarea` is the styled control for ordinary multi-line text. Use [`Input`](/component/input) for a single line and [`Editor`](/component/editor) for source code. ## Import ```rust use gpui_kit::component::input::{Textarea, TextareaState}; ``` ## Basic usage ```rust let notes = cx.new(|cx| { TextareaState::new(window, cx) .rows(5) .placeholder("Notes") }); Textarea::new(¬es) ``` ## Auto-grow ```rust let message = cx.new(|cx| { TextareaState::new(window, cx) .auto_grow(2, 8) .placeholder("Write a message") }); Textarea::new(&message) ``` The control grows until `max_rows`; overflowing content then scrolls. ## Value and events ```rust let value = notes.read(cx).value(); notes.update(cx, |state, cx| { state.set_value("Updated notes", window, cx); }); cx.subscribe(¬es, |this, state, event: &InputEvent, cx| { if matches!(event, InputEvent::Change) { this.notes = state.read(cx).value(); cx.notify(); } }); ``` `insert`, `replace`, `cursor_position`, `soft_wrap`, `searchable`, and `submit_on_enter` are available on `TextareaState`. ## Sizes ```rust Textarea::new(¬es).large() Textarea::new(¬es) // medium (default) Textarea::new(¬es).small() ``` The size changes the text size and the padding around the text together. It does not set the height: use `h` for a fixed height, and let `rows` or `auto_grow` decide the height of a growing textarea. ## Appearance ```rust Textarea::new(¬es) .h(px(160.)) .bordered(true) .disabled(false) .readonly(false) .aria_label("Notes") ``` Unlike `disabled`, a read-only textarea keeps the normal appearance and still can be focused, selected and copied, it only rejects the changes made by the user. `Textarea` deliberately does not expose Input-only adornments such as `prefix`, `suffix`, mask toggle, or the clear button. Compose related actions beside the textarea. ## Atomic inline tokens Use tokens to include mentions, file references or commands in a multi-line message. Insert a token with `TextareaState::replace_with_token`, or restore a saved draft with `set_value`. The default label is ready to use; add a renderer when you want an icon or other custom content: ```rust Textarea::new(&state) .token(|token, _, _| InputToken::new(token).icon(IconName::File)) ``` Import `InputToken` from `gpui_kit::component::input` and `IconName` from `gpui_kit::component`. A token wraps onto the next line as a whole, and auto-grow adjusts the textarea height to fit. Put line breaks in the text between tokens. See [Input: atomic inline tokens](/component/input#atomic-inline-tokens) for editing, activation, draft persistence, mode restrictions and JavaScript APIs. --- # Rating Source: /component/rating A star rating component that allows users to select a rating value. Supports different sizes, custom colors, disabled state, and click handlers. ## Import ```rust use gpui_kit::component::rating::Rating; ``` ## Usage ### Rate this ```rust Rating::new("my-rating") .value(3) .max(5) .on_click(|value, _, _| { println!("Rating changed to: {}", value); }) ``` ### Controlled Rating ```rust struct MyView { rating: usize, } impl Render for MyView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Rating::new("rating") .value(self.rating) .max(5) .on_click(cx.listener(|view, value: &usize, _, cx| { view.rating = *value; cx.notify(); })) } } ``` ### Different Sizes The Rating component supports the [Sizable] trait for different sizes. ```rust Rating::new("rating").xsmall().value(3).max(5) Rating::new("rating").small().value(3).max(5) Rating::new("rating").value(3).max(5) // default (Medium) Rating::new("rating").large().value(3).max(5) ``` ### Custom Color By default, the rating uses the theme's `yellow` color. You can customize it with the `color` method. ```rust Rating::new("rating") .value(4) .max(5) .color(cx.theme().green) ``` ### Disabled State ```rust Rating::new("rating") .value(2) .max(5) .disabled(true) ``` ### Custom Maximum The default maximum is 5 stars, but you can set a different maximum value. ```rust Rating::new("rating") .value(7) .max(10) ``` ### Click Behavior The rating component has special click behavior: - Clicking on a star that's already filled will reduce the rating by 1 - Clicking on an unfilled star will set the rating to that star's value The `on_click` callback receives the new rating value as `&usize`. ```rust Rating::new("rating") .value(3) .max(5) .on_click(|new_value, _, _| { println!("New rating: {}", new_value); }) ``` ### Read-only Display ```rust Rating::new("rating") .value(4) .max(5) .disabled(true) ``` ### Interactive Rating with State ```rust struct ProductView { user_rating: usize, } impl Render for ProductView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_3() .child( Rating::new("product-rating") .value(self.user_rating) .max(5) .on_click(cx.listener(|view, value: &usize, _, cx| { view.user_rating = *value; // Save rating to backend, etc. cx.notify(); })) ) .child(format!("Your rating: {}/5", self.user_rating)) } } ``` ### Large Rating with Custom Color ```rust Rating::new("rating") .large() .value(5) .max(5) .color(cx.theme().orange) ``` [Rating]: https://docs.rs/gpui-component/latest/gpui_component/rating/struct.Rating.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html [Disableable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Disableable.html ## API Reference - [Rating] ### Methods - `new(id: impl Into)` - Create a new Rating component - `with_size(size: impl Into)` - Set the star size (implements [Sizable]) - `value(value: usize)` - Set the initial rating value (0..=max) - `max(max: usize)` - Set the maximum number of stars (default: 5) - `color(color: impl Into)` - Set the active color (default: theme yellow) - `disabled(disabled: bool)` - Disable interaction (implements [Disableable]) - `on_click(handler: Fn(&usize, &mut Window, &mut App))` - Set click handler --- # Input Source: /component/input For multiple addons, shared frames, and textarea toolbars, see [Input Group](/component/input-group). A single-line text input with validation, masking, prefix/suffix elements, and different visual states. Use [Textarea](/component/textarea) for ordinary multi-line text and [Editor](/component/editor) for source code. ## Import ```rust use gpui_kit::component::input::{Input, InputState}; ``` ## Usage ### Basic ```rust let input = cx.new(|cx| InputState::new(window, cx)); Input::new(&input) ``` ### Prefix, suffix and clear button ```rust use gpui_kit::component::{Icon, IconName}; // With prefix icon Input::new(&input) .prefix(Icon::new(IconName::Search).small()) // With suffix button Input::new(&input) .suffix( Button::new("info") .ghost() .icon(IconName::Info) .xsmall() ) // With both Input::new(&input) .prefix(Icon::new(IconName::Search).small()) .suffix(Button::new("btn").ghost().icon(IconName::Info).xsmall()) ``` ### Password ```rust let input = cx.new(|cx| InputState::new(window, cx) .masked(true) .default_value("password123") ); Input::new(&input) .content_type(InputContentType::Password) .mask_toggle() // Shows toggle button to reveal password ``` While the value is masked, the input keeps it out of the clipboard and out of the selection: Copy and Cut do nothing (and are disabled in the context menu), a word-wise delete takes everything before the caret, and a double click selects the whole value instead of one word. Paste and Select All keep working, and revealing the value with `mask_toggle` restores all of them. ### Sizes ```rust Input::new(&input).large() Input::new(&input) // medium (default) Input::new(&input).small() ``` ### States ```rust let input = cx.new(|cx| InputState::new(window, cx) .placeholder("Enter your name...") ); Input::new(&input) ``` ### With Default Value ```rust let input = cx.new(|cx| InputState::new(window, cx) .default_value("John Doe") ); Input::new(&input) ``` ### Cleanable Input ```rust Input::new(&input) .cleanable(true) // Show clear button when input has value ``` ### Disabled Input ```rust Input::new(&input).disabled(true) ``` ### Read-only Input Unlike `disabled`, a read-only input keeps the normal appearance and still can be focused, selected and copied, it only rejects the changes made by the user. ```rust Input::new(&input).readonly(true) ``` ### Clean on ESC ```rust let input = cx.new(|cx| InputState::new(window, cx) .clean_on_escape() // Clear input when ESC is pressed ); Input::new(&input) ``` ### Input Validation ```rust // Validate float numbers let input = cx.new(|cx| InputState::new(window, cx) .validate(|s, _| s.parse::().is_ok()) ); // Regex pattern validation let input = cx.new(|cx| InputState::new(window, cx) .pattern(regex::Regex::new(r"^[a-zA-Z0-9]*$").unwrap()) ); ``` ### Input Masking ```rust // Phone number mask let input = cx.new(|cx| InputState::new(window, cx) .mask_pattern("(999)-999-9999") ); // Custom pattern: AAA-###-AAA (A=letter, #=digit, 9=digit optional) let input = cx.new(|cx| InputState::new(window, cx) .mask_pattern("AAA-###-AAA") ); // Number with thousands separator use gpui_kit::component::input::MaskPattern; let input = cx.new(|cx| InputState::new(window, cx) .mask_pattern(MaskPattern::Number { separator: Some(','), fraction: Some(3), }) ); ``` ### Handle Input Events ```rust let input = cx.new(|cx| InputState::new(window, cx)); cx.subscribe_in(&input, window, |view, state, event, window, cx| { match event { InputEvent::Change => { let text = state.read(cx).value(); println!("Input changed: {}", text); } InputEvent::PressEnter { secondary } => { println!("Enter pressed, secondary: {}", secondary); } InputEvent::Focus => println!("Input focused"), InputEvent::Blur => println!("Input blurred"), } }); ``` ### Custom Appearance ```rust // Without default styling Input::new(&input).appearance(false) // Use in custom container div() .border_b_2() .px_6() .py_3() .border_color(cx.theme().border) .bg(cx.theme().secondary) .child(Input::new(&input).appearance(false)) ``` ### Context Menu ```rust // Turn off the right-click menu entirely, including a custom one. let input = cx.new(|cx| InputState::new(window, cx).context_menu(false)); // Or replace the built-in menu with your own. The state's context menu must // stay enabled, which is the default. Input::new(&input).context_menu(|menu, window, cx| { // You can define your own actions and even utilize // built-in actions (cut, copy, paste, etc.) // to avoid having to re-implement that functionality. menu.menu("Custom Action", Box::new(CustomAction)) .separator() .menu("Cut", Box::new(input::Cut)) .menu("Copy", Box::new(input::Copy)) .menu("Paste", Box::new(input::Paste)) }) ``` ### Touch Selection On a touch screen, a long press selects the word under the finger and keeps following the finger while it stays down. Lifting it opens an edit menu over the selection with the commands that apply — `Cut`, `Copy`, `Paste`, and `Select All` — and puts a grab handle at each end of the selection. Dragging a handle moves that end; the other end stays put, and a multi-line input scrolls when the finger reaches its edge. A long press on whitespace or in an empty field places the caret and offers `Paste` and `Select All`. The handles and the menu belong to the selection the gesture made. They disappear as soon as anything else moves the selection — a tap, typing, an arrow key, `Escape` — and the menu steps aside while the content scrolls under a finger. Tapping the selected text brings the menu back. Cut, Copy, and Paste go through the input's own actions, so a custom key binding or an open completion menu sees them the same way. A read-only input offers only `Copy` and `Select All`; a masked input keeps its value out of the clipboard. ### Paste Hook `on_paste` intercepts the clipboard before the default text insertion, so pasted images and copied files can live in app-owned state instead of being silently dropped. It is available on `Input`, `Textarea` and `Editor`. ```rust use gpui_kit::ClipboardEntry; let view = cx.entity().downgrade(); Textarea::new(&self.composer).on_paste(move |item, _, cx| { let images: Vec<_> = item.entries().iter().filter_map(|entry| match entry { ClipboardEntry::Image(image) => Some(image.clone()), _ => None, }).collect(); if images.is_empty() { return false; // fall through to the default text insertion } view.update(cx, |this, cx| { // Store the images beside the input, e.g. as `Attachment`s. this.attachments.extend(images); cx.notify(); }).ok(); true // consumed, the input inserts nothing }) ``` Return `true` when the handler took the paste: the `input::Paste` action stops there and the input inserts nothing. Return `false` to let the action reach the engine, which inserts `clipboard.text()` as before. Copied files arrive as `ClipboardEntry::ExternalPaths` through the same hook. Known limit: on web `read_from_clipboard()` is `None` (text arrives through the platform input handler); image paste there needs async clipboard access and permission, and is out of scope. ### Search Input ```rust let search = cx.new(|cx| InputState::new(window, cx) .placeholder("Search...") ); Input::new(&search) .prefix(Icon::new(IconName::Search).small()) ``` ### Currency Input ```rust let amount = cx.new(|cx| InputState::new(window, cx) .mask_pattern(MaskPattern::Number { separator: Some(','), fraction: Some(2), }) ); div() .child(Input::new(&amount)) .child(format!("Value: {}", amount.read(cx).value())) ``` ### Form with Multiple Inputs ```rust struct FormView { name_input: Entity, email_input: Entity, } v_flex() .gap_3() .child(Input::new(&self.name_input)) .child(Input::new(&self.email_input)) ``` ## Atomic inline tokens Use inline tokens for mentions, file references or commands that should be selected and deleted as a whole. For example, an input can display “Alice” as a token while `value()` and Copy return its text, `@alice`. ### Insert a reference Create the input state once, then insert a token when the user picks a reference: ```rust use gpui_kit::component::input::{InlineToken, Input, InputState}; let input = cx.new(|cx| InputState::new(window, cx)); input.update(cx, |state, cx| { state.replace_with_token( InlineToken::new("person-1", "@alice").with_label("Alice"), window, cx, ).expect("valid reference"); }); Input::new(&input) ``` `replace_with_token` replaces the selection, or inserts at the caret. It does not add a space. To replace a completion query such as `@ali`, use `replace_range_with_token(range, token, window, cx)`. Rust ranges are half-open UTF-8 byte ranges; use byte offsets such as those returned by `str::find`. The ID names the referenced resource, so two mentions of the same person carry the same ID. Use `text` for the value to copy or submit and `with_label` for its displayed name. Omit `with_label` to display the text itself. Users can move the caret to either side of a token, click it to select it, or delete it with Backspace/Delete. A selection that crosses part of a token includes the whole token. Undo/Redo restores both its text and reference. Pasting inserts plain text. ### Customize appearance and opening a reference Tokens render as an `InputToken` by default. The `token` slot supplies the element for each token; return one with an icon from it, and use `on_token_click` to open the reference: ```rust use gpui_kit::component::{ IconName, input::{InputToken, InlineTokenClickEvent}, }; Input::new(&input) .token(|token, _, _| { InputToken::new(token).icon(IconName::File) }) .on_token_click(|event: &InlineTokenClickEvent, _, _| { // Look up event.token().id() and open its resource. }); ``` You can also return your own single-row element. Keep it within the input's line height; content wider than the available row is clipped. Read selection and readonly/disabled state from the renderer's context. Do not edit the input from the renderer; event callbacks may update it. Keep hover and selection styles the same size. Tokens are measured whenever they render, so an element that grows once its data arrives reflows on the next frame. A click selects the token and then opens it; dragging or Shift-selecting a token does not open it. Readonly inputs allow opening references; disabled inputs do not. To offer a keyboard shortcut for opening an exactly selected token, bind `ActivateToken` to a key of your choice; assistive technology reaches the same listener through the token's click action. Add a menu item for it through `context_menu` when your application has a name for the reference, such as "Open file". If your token includes a button, consume its mouse-down and click events so that it does not also open the reference. Apply `token.is_disabled()` to every child action, including accessibility actions, and `token.is_readonly()` to actions that change the content. ### Save, restore and submit Use `content()` to keep the text and references together when saving a draft: ```rust let draft = input.read(cx).content(); // Restore the saved draft later: `set_value` takes plain text or content. input.update(cx, |state, cx| { state.set_value(draft, window, cx); }); ``` To restore data from your own storage, build an `InputContent` from the text and attach each token to its byte range. `with_token` validates the range against the text as you go, so a content value is always consistent by the time it is set: ```rust use gpui_kit::component::input::InputContent; let draft = InputContent::new("Ask @alice") .with_token(4..10, InlineToken::new("person-1", "@alice").with_label("Alice"))?; ``` At submission time, read a fresh `content()`: `text()` is the message, and `tokens()` contains the references still present in it. Use each token's ID to look up its resource and handle missing resources before sending. `set_value` clears undo history and does not emit `InputEvent::Change`. Passing plain text removes every token, even if the text is unchanged; passing content restores its tokens, except in modes that cannot show them. Use `replace_all` for an undoable plain-text replacement. Token edits, including adding a reference to existing text, emit `InputEvent::Change`. Programmatic setters can update readonly or disabled inputs, so check these states in application commands that should be unavailable to users. ### Validation and supported inputs Tokens work with Input and Textarea. They are not available in Editor, NumberInput, formatted masks or password fields. A token's ID must not be blank; its text and label must be nonempty, single-line strings without control characters. Ranges cannot overlap or split a Unicode grapheme (such as an emoji or a character with a combining accent), and each token's text must match its range when restoring a draft. Token operations return `Result<_, InlineTokenError>`. A rejected operation leaves the input unchanged. If insertion returns `CompositionActive`, wait until the user finishes composing with their input method before inserting the token. ### JavaScript Create and keep an `InputState` in `init()`, then render an Input with that state. JavaScript ranges use **UTF-16 string offsets**, matching `slice()` and `indexOf()`: ```javascript import { Input, InputState } from "gpui-component"; // In init(): this.input = InputState(); this.input.set_value({ text: "🙂 @alice", tokens: [{ range: { start: 3, end: 9 }, token: { id: "person-1", text: "@alice", label: "Alice" }, }], }); // In render(): new Input(this.input) .on_token_click((event, cx) => { // Look up event.token.id and open its resource. }); ``` Use `replace_with_token` or `replace_range_with_token` to insert references, `content()` and `set_value(content)` to save and restore drafts, and `tokens()` to read the current references. To remove a reference, pass its current range to `set_selected_range`, then call `replace("")`. Returned snapshots are independent objects; changing one does not update the input. Make edits from initialization, event or task callbacks, not from renderers. Token validation errors expose an `error.code`, such as `InvalidBoundary` or `CompositionActive`; invalid argument shapes also throw. Textarea offers the same methods on `TextareaState()`. If you use `gpui-base`, construct these states with `InputState.new()` or `TextareaState.new()` instead. --- # OtpInput Source: /component/otp-input A specialized input component for one-time passwords (OTP) that displays multiple input fields in a grid layout. Perfect for SMS verification codes, authenticator app codes, and other numeric verification scenarios. ## Import ```rust use gpui_kit::component::input::{OtpInput, OtpState}; ``` ## Usage ### Verification ```rust struct SmsVerification { otp_state: Entity, phone_number: String, is_verifying: bool, } impl SmsVerification { fn new(window: &mut Window, cx: &mut Context) -> Self { let otp_state = cx.new(|cx| OtpState::new(6, window, cx)); cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| { if let InputEvent::Change = event { let code = state.read(cx).value(); this.verify_sms_code(&code, cx); } }); Self { otp_state, phone_number: "+1234567890".to_string(), is_verifying: false, } } fn verify_sms_code(&mut self, code: &str, cx: &mut Context) { self.is_verifying = true; // API call to verify SMS code println!("Verifying SMS code: {}", code); cx.notify(); } } impl Render for SmsVerification { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child(format!("Enter the 6-digit code sent to {}", self.phone_number)) .child(OtpInput::new(&self.otp_state)) .when(self.is_verifying, |this| { this.child("Verifying...") }) } } ``` ### Basic OTP Input ```rust let otp_state = cx.new(|cx| OtpState::new(6, window, cx)); OtpInput::new(&otp_state) ``` ### With Default Value ```rust let otp_state = cx.new(|cx| OtpState::new(6, window, cx) .default_value("123456") ); OtpInput::new(&otp_state) ``` ### Masked OTP Input ```rust let otp_state = cx.new(|cx| OtpState::new(6, window, cx) .masked(true) .default_value("123456") ); OtpInput::new(&otp_state) ``` ### Different Sizes ```rust // Small size OtpInput::new(&otp_state).small() // Medium size (default) OtpInput::new(&otp_state) // Large size OtpInput::new(&otp_state).large() // Custom size OtpInput::new(&otp_state).with_size(px(55.)) ``` ### Grouped Layout ```rust // Single group (all fields together) OtpInput::new(&otp_state).groups(1) // Two groups (default) - splits fields in half OtpInput::new(&otp_state).groups(2) // Three groups - splits fields into thirds OtpInput::new(&otp_state).groups(3) ``` ### Disabled State ```rust OtpInput::new(&otp_state).disabled(true) ``` ### Different Length Codes ```rust // 4-digit PIN let pin_state = cx.new(|cx| OtpState::new(4, window, cx)); OtpInput::new(&pin_state).groups(1) // 6-digit SMS code (most common) let sms_state = cx.new(|cx| OtpState::new(6, window, cx)); OtpInput::new(&sms_state) // 8-digit authenticator code let auth_state = cx.new(|cx| OtpState::new(8, window, cx)); OtpInput::new(&auth_state).groups(2) ``` ### Handle OTP Events ```rust let otp_state = cx.new(|cx| OtpState::new(6, window, cx)); cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| { match event { InputEvent::Change => { let code = state.read(cx).value(); if code.len() == 6 { println!("Complete OTP: {}", code); // Automatically submit when complete this.verify_otp(&code, cx); } } InputEvent::Focus => println!("OTP input focused"), InputEvent::Blur => println!("OTP input lost focus"), _ => {} } }); ``` ### Programmatic Control ```rust // Set value programmatically otp_state.update(cx, |state, cx| { state.set_value("123456", window, cx); }); // Toggle masking otp_state.update(cx, |state, cx| { state.set_masked(true, window, cx); }); // Focus the input otp_state.update(cx, |state, cx| { state.focus(window, cx); }); // Get current value let current_value = otp_state.read(cx).value(); ``` ### Two-Factor Authentication ```rust struct TwoFactorAuth { otp_state: Entity, is_masked: bool, } impl TwoFactorAuth { fn new(window: &mut Window, cx: &mut Context) -> Self { let otp_state = cx.new(|cx| OtpState::new(6, window, cx) .masked(true) ); Self { otp_state, is_masked: true, } } fn toggle_visibility(&mut self, window: &mut Window, cx: &mut Context) { self.is_masked = !self.is_masked; self.otp_state.update(cx, |state, cx| { state.set_masked(self.is_masked, window, cx); }); } } impl Render for TwoFactorAuth { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child("Enter your authenticator code") .child(OtpInput::new(&self.otp_state)) .child( Button::new("toggle-visibility") .label(if self.is_masked { "Show" } else { "Hide" }) .on_click(cx.listener(Self::toggle_visibility)) ) } } ``` ### PIN Entry ```rust struct PinEntry { pin_state: Entity, attempts: usize, max_attempts: usize, } impl PinEntry { fn new(window: &mut Window, cx: &mut Context) -> Self { let pin_state = cx.new(|cx| OtpState::new(4, window, cx) .masked(true) ); cx.subscribe(&pin_state, |this, state, event: &InputEvent, cx| { if let InputEvent::Change = event { let pin = state.read(cx).value(); this.verify_pin(&pin, cx); } }); Self { pin_state, attempts: 0, max_attempts: 3, } } fn verify_pin(&mut self, pin: &str, cx: &mut Context) { self.attempts += 1; // Simulate PIN verification if pin == "1234" { println!("PIN verified successfully!"); } else { println!("Incorrect PIN. Attempts: {}/{}", self.attempts, self.max_attempts); // Clear PIN on incorrect attempt self.pin_state.update(cx, |state, cx| { state.set_value("", window, cx); }); } cx.notify(); } } impl Render for PinEntry { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let is_locked = self.attempts >= self.max_attempts; v_flex() .gap_4() .child("Enter your 4-digit PIN") .child( OtpInput::new(&self.pin_state) .groups(1) .disabled(is_locked) ) .when(is_locked, |this| { this.child("Too many attempts. Please try again later.") }) .when(self.attempts > 0 && !is_locked, |this| { this.child(format!( "Incorrect PIN. {} attempts remaining.", self.max_attempts - self.attempts )) }) } } ``` ## Behavior ### Input Handling - **Numeric Only**: Accepts only digits (0-9) - **Auto-Focus**: Automatically moves to next field when digit is entered - **Backspace**: Removes current digit and moves to previous field - **Length Limit**: Prevents input beyond specified length - **Auto-Complete**: Emits `Change` event when all fields are filled ### Visual Feedback - **Focus Indicator**: Blue border and blinking cursor on active field - **Masking**: Shows asterisk icons instead of numbers when enabled - **Grouping**: Visual separation of fields into groups for better readability - **Disabled State**: Grayed out appearance when disabled ### Keyboard Navigation - **Arrow Keys**: Navigate between fields - **Tab**: Move to next focusable element - **Shift+Tab**: Move to previous focusable element - **Backspace**: Delete current digit and move backward - **Delete**: Clear current field ## Common Patterns ### Auto-Submit on Complete ```rust cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| { if let InputEvent::Change = event { let code = state.read(cx).value(); if code.len() == 6 { // Auto-submit when complete this.submit_verification_code(&code, cx); } } }); ``` ### Clear on Focus ```rust cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| { if let InputEvent::Focus = event { // Clear previous value when user starts entering new code state.update(cx, |state, cx| { state.set_value("", window, cx); }); } }); ``` ### Resend Code Timer ```rust struct OtpWithResend { otp_state: Entity, resend_timer: Option, can_resend: bool, } // Implementation would include timer logic for resend functionality ``` ## API Reference ### OtpState | Method | Description | | ------------------------------ | -------------------------------------------- | | `new(length, window, cx)` | Create a new OTP state with specified length | | `default_value(str)` | Set initial value | | `masked(bool)` | Enable masked display (shows asterisks) | | `set_value(str, window, cx)` | Set OTP value programmatically | | `value()` | Get current OTP value | | `set_masked(bool, window, cx)` | Toggle masked display | | `focus(window, cx)` | Focus the OTP input | | `focus_handle(cx)` | Get focus handle | ### OtpInput | Method | Description | | ---------------- | ---------------------------------------- | | `new(state)` | Create OTP input with state entity | | `groups(n)` | Set number of visual groups (default: 2) | | `disabled(bool)` | Set disabled state | | `small()` | Small size (6x6 px fields) | | `large()` | Large size (11x11 px fields) | | `with_size(px)` | Custom field size | ### InputEvent | Event | Description | | -------- | ------------------------------------------------- | | `Change` | Emitted when OTP is complete (all digits entered) | | `Focus` | Input received focus | | `Blur` | Input lost focus | --- # Icon Source: /component/icon A flexible icon component that renders SVG icons from asset paths or in-memory bytes, with customizable size, color, and transformations. The built-in Lucide icons use the assets bundle; custom SVG bytes can be supplied directly with `Icon::data`. Before you start, please make sure you have read: [Icons & Assets](/docs/assets) to understand how use SVG in GPUI & GPUI Component application. `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 is needed, or import `gpui_kit::component::IconNameExt` to call `name.view(cx)`. **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 WASM, `Assets::new(endpoint)` and `AllAssets::new(endpoint)` use the existing on-demand CDN loader instead of embedding the complete bundle. ## Additional application icons Keep the default `Assets` registration. For extra catalog icons, follow the [selected-icons recipe](/docs/assets#pick-additional-catalog-icons-with-icon_assets): `icon_assets!` creates a source for exactly the named SVGs, and the application registers it together with the default source. For your own SVG files, see [custom assets](/docs/assets#add-your-own-asset-files). ## Import ```rust use gpui_kit::component::{Icon, IconName}; ``` ## Usage ### Icons ```rust use gpui_kit::{Transformation, radians}; // Rotate by radians Icon::new(IconName::ArrowUp) .rotate(radians(std::f32::consts::FRAC_PI_2)) // Transform with custom transformation Icon::new(IconName::ChevronRight) .transform(Transformation::rotate(radians(std::f32::consts::PI))) ``` ### Basic Icon ```rust // Using IconName enum directly IconName::Heart // Or creating an Icon explicitly Icon::new(IconName::Heart) ``` ### Icon with Custom Size ```rust // Predefined sizes Icon::new(IconName::Search).xsmall() // size_3() Icon::new(IconName::Search).small() // size_3p5() Icon::new(IconName::Search).medium() // size_4() (default) Icon::new(IconName::Search).large() // size_6() // Custom pixel size Icon::new(IconName::Search).with_size(px(20.)) ``` ### Icon with Custom Color ```rust // Using theme colors Icon::new(IconName::Heart) .text_color(cx.theme().red) // Using custom colors Icon::new(IconName::Star) .text_color(gpui_kit::red()) ``` ### Custom SVG Path ```rust // Using a custom SVG file from assets Icon::new(Icon::empty()) .path("icons/my-custom-icon.svg") ``` ### SVG Bytes Use `data(&[u8])` to supply SVG bytes without registering an `AssetSource` path: ```rust use gpui_kit::component::{Icon, button::Button, menu::PopupMenuItem}; let icon = Icon::default().data(include_bytes!("search.svg")); Button::new("search").icon(icon.clone()).label("Search"); PopupMenuItem::new("Search").icon(icon); ``` `data` copies its input into shared storage, so the input need not be `'static`. Cloning an `Icon` shares those bytes and preserves its style and transformation. Both direct rendering and `Icon::view(cx)` retain the data source. GPUI's renderer may copy the bytes again; this API does not promise zero-copy rendering. The last source builder wins, including when the new source is empty: ```rust let bytes = include_bytes!("search.svg"); Icon::default().path("icons/old.svg").data(bytes); // Uses SVG bytes Icon::default().data(bytes).path("icons/search.svg"); // Uses the asset path ``` Bytes go through the same SVG renderer as path-based icons. They retain component sizing, foreground colors, and button loading behavior. Use `loading_icon` to choose a custom loading symbol: ```rust Button::new("search") .icon(Icon::default().data(include_bytes!("search.svg"))) .loading_icon(Icon::default().data(include_bytes!("loader.svg"))) .loading(true) .label("Searching") ``` `NativeMenu::menu_with_icon` also accepts data-backed icons. Native menus keep their existing platform sizing and tinting rules. Other path-based icons used by your application or components still need an asset source. ### Custom Icon Types with SVG Bytes An icon crate can export individual types that implement `From for Icon`: ```rust use gpui_kit::component::{Icon, button::Button}; pub struct Search; impl From for Icon { fn from(_: Search) -> Self { Icon::default().data(include_bytes!("search.svg")) } } Button::new("search").icon(Search); ``` Existing `IconNamed` implementations continue to provide asset paths. A data-backed type uses the conversion above without also implementing `IconNamed`. Binary-size savings depend on which resources are referenced and on build settings. ### Icon in Button ```rust use gpui_kit::component::button::Button; Button::new("like-btn") .icon( Icon::new(IconName::Heart) .text_color(cx.theme().red) .large() ) .label("Like") ``` ### Animated Loading Icon ```rust Icon::new(IconName::LoaderCircle) .text_color(cx.theme().muted_foreground) .medium() // Add rotation animation in your render logic ``` ### Status Icons ```rust // Success Icon::new(IconName::CircleCheck) .text_color(cx.theme().green) // Error Icon::new(IconName::CircleX) .text_color(cx.theme().red) // Warning Icon::new(IconName::TriangleAlert) .text_color(cx.theme().yellow) ``` ### Navigation Icons ```rust // Back button Icon::new(IconName::ArrowLeft) .medium() .text_color(cx.theme().foreground) // Dropdown indicator Icon::new(IconName::ChevronDown) .small() .text_color(cx.theme().muted_foreground) ``` ### Custom Icon from Assets ```rust // Using a custom SVG file Icon::empty() .path("icons/my-brand-logo.svg") .large() .text_color(cx.theme().primary) ``` ## Available Icons The `IconName` enum provides access to a curated set of icons. Here are some commonly used ones: ### Navigation - `ArrowUp`, `ArrowDown`, `ArrowLeft`, `ArrowRight` - `ChevronUp`, `ChevronDown`, `ChevronLeft`, `ChevronRight` - `ChevronsUpDown` ### Actions - `Check`, `Close`, `Plus`, `Minus` - `Copy`, `Delete`, `Search`, `Replace` - `Maximize`, `Minimize`, `WindowRestore` ### Files & Folders - `File`, `Folder`, `FolderOpen`, `FolderClosed` - `BookOpen`, `Inbox` ### UI Elements - `Menu`, `Settings`, `Settings2`, `Ellipsis`, `EllipsisVertical` - `Eye`, `EyeOff`, `Bell`, `Info` ### Social & External - `GitHub`, `Globe`, `ExternalLink` - `Heart`, `HeartOff`, `Star`, `StarOff` - `ThumbsUp`, `ThumbsDown` ### Status & Alerts - `CircleCheck`, `CircleX`, `TriangleAlert` - `Loader`, `LoaderCircle` ### Panels & Layout - `PanelLeft`, `PanelRight`, `PanelBottom` - `PanelLeftOpen`, `PanelRightOpen`, `PanelBottomOpen` - `LayoutDashboard`, `Frame` ### Users & Profile - `User`, `CircleUser`, `Bot` ### Other - `Calendar`, `Map`, `Palette`, `Inspector` - `Sun`, `Moon`, `Building2` ## Icon Sizes The Icon component supports several predefined sizes: | Size | Method | CSS Class | Pixels | | ----------- | --------------------- | ------------ | ------ | | Extra Small | `.xsmall()` | `size_3()` | 12px | | Small | `.small()` | `size_3p5()` | 14px | | Medium | `.medium()` (default) | `size_4()` | 16px | | Large | `.large()` | `size_6()` | 24px | | Custom | `.with_size(px(n))` | - | n px | ## Build you own `IconName`. You can define your own `IconName` to have more specific icons for your application. We have `IconNamed` trait for you to implement for your. ```rust use gpui_kit::component::IconNamed; pub enum IconName { Encounters, Monsters, Spells, } impl IconNamed for IconName { fn path(self) -> gpui_kit::SharedString { match self { IconName::Encounters => "icons/encounters.svg", IconName::Monsters => "icons/monsters.svg", IconName::Spells => "icons/spells.svg", } .into() } } // This allows for the following interactions (works with anything that has the `.icon(icon)` method. Button::new("my-button").icon(IconName::Spells); Icon::new(IconName::Monsters); ``` If you want to directly `render` a custom `IconName` you must implement the `RenderOnce` trait and derive `IntoElement` on the `IconName`. ```rust impl RenderOnce for IconName { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { Icon::empty().path(self.path()) } } // Now you can use it directly in your element tree: div() .child(IconName::Monsters) ``` ## Notes - Icons are rendered as SVG elements and support full CSS styling - The default size matches the current text size if no explicit size is set - Icons are flex-shrink-0 by default to prevent unwanted shrinking in flex layouts - All icon paths are relative to the assets bundle root - Icons from Lucide.dev are designed to work well at 16px and scale nicely to other sizes --- # Attachment Source: /component/attachment `Attachment` presents one file or media item. It provides stable layout for a media preview, metadata, and optional actions, draws the lifecycle status, and offers the two controls every composer needs — remove and retry — while leaving upload state, selection, and navigation in the application. Each public slot is styleable and accepts arbitrary GPUI children. The component is intentionally a composition primitive. `AttachmentActions` does not invent an attachment-specific action model; put `Button`, `Link`, or another semantic control inside it. `AttachmentGroup` only owns horizontal spacing and scrolling. Selection and preview behavior remain application concerns. ## Import ```rust use gpui_kit::{Axis, ParentElement as _, Styled as _}; use gpui_kit::component::{ ActiveTheme as _, Colorize as _, Icon, IconName, Sizable as _, Size, attachment::{ Attachment, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentGroup, AttachmentMedia, AttachmentStatus, AttachmentTitle, }, button::{Button, ButtonVariants as _}, badge::Badge, progress::Progress, shimmer::ShimmerStyle, spinner::Spinner, }; ``` ## Anatomy and basic usage The typed builders make the common file shape explicit: ```rust Attachment::new() .media(AttachmentMedia::new().child(Icon::new(IconName::FileText))) .content( AttachmentContent::new() .title(AttachmentTitle::new("quarterly-report.pdf")) .description(AttachmentDescription::new("PDF · 2.4 MB")), ) .actions( AttachmentActions::new().child( Button::new("remove-report") .ghost() .xsmall() .icon(IconName::Close) .label("Remove"), ), ) ``` The slots are optional. A media-only attachment, metadata-only attachment, or action-only attachment is valid when the product needs it: ```rust Attachment::new() .media(AttachmentMedia::new().child(Icon::new(IconName::FileText))); Attachment::new().content( AttachmentContent::new() .title(AttachmentTitle::new("notes.txt")) .description(AttachmentDescription::new("TXT · 12 KB")), ) ``` The default state is: | Property | Default | Meaning | | --- | --- | --- | | Status | `Complete` | The item is ready. | | Size | `Medium` | Uses the standard conversation density. | | Axis | `Horizontal` | Media, metadata, and actions share one row. | | Media/content/actions | absent | Add only the slots the item needs. | | Surface | `background` and `foreground` | The card surface, separated by the border like shadcn's `bg-card`. | | Radius | `radius_tokens().lg` (`md` for `XSmall`) | Shared semantic radius. | | Geometry | 56 px tall, 232 px wide with content, 38 px media (`Medium`) | The composer chip; see [Sizes and axes](#sizes-and-axes). | `Attachment` never owns a product-level file model. Keep the file ID and state in the parent view, then render the current record into this element. ## Media and image previews Use children for an icon-style media slot and `src(...)` for an image preview: ```rust Attachment::new() .media( AttachmentMedia::new() .src("https://example.com/previews/sdk.svg") .overlay(Icon::new(IconName::Download)), ) .content( AttachmentContent::new() .title(AttachmentTitle::new("sdk-preview.svg")) .description(AttachmentDescription::new("SVG · 1280 × 720")), ) ``` The image is rendered with `ObjectFit::Cover` inside the media bounds. Children and `overlay(...)` are painted above the image. `overlay(...)` centers an element over the whole media area, which is useful for a spinner, play icon, or preview action: ```rust Attachment::new() .status(AttachmentStatus::Uploading) .axis(Axis::Vertical) .media( AttachmentMedia::new() .src(preview_url) .overlay(Spinner::new().small()), ) ``` The slot draws the lifecycle status itself. A source image keeps its colors and takes a scrim: a translucent dark layer with a white spinner while `Uploading` or `Processing`, a darker one with the retry control (see [Remove and retry controls](#remove-and-retry-controls)) or an alert glyph once `Failed`. Custom overlays are painted above the scrim. With no source, the media slot is a themed muted area that shows a spinner in the primary color while in progress and, once failed, the destructive semantic surface and foreground with an alert glyph; its children come back with `Complete`. An image tile — a vertical attachment without content — is a square the media fills edge to edge, its corners one border width tighter than the card's so the two stay concentric. `AttachmentMedia` is independently styleable. Use `with_size(...)` to override the inherited media size, or use normal GPUI refinements for a custom preview ratio and surface: ```rust AttachmentMedia::new() .with_size(Size::Large) .aspect_ratio(16. / 9.) .rounded(cx.theme().radius_lg) .child(Icon::new(IconName::Image)) ``` An explicit media size takes precedence over the attachment size. A vertical attachment makes the media full width and square by default; the media's own style can replace that geometry when the application has a different preview design. ## Lifecycle states `AttachmentStatus` has five explicit states. The parent status is passed to the typed title, description, media, and action layout during rendering: | State | Surface/layout behavior | Recommended content | | --- | --- | --- | | `Pending` | Dashed border; preview is not dimmed. | “Ready to upload” and a start action. | | `Uploading` | Media shows a spinner, or a progress ring with `progress(...)` (over a scrim on an image); a chip draws a bar along its bottom edge; typed title shimmers. | A description such as “Uploading”; the percentage is appended for you. | | `Processing` | Media shows a spinner (over a scrim on an image); typed title shimmers. | “Processing…” and a non-destructive wait state. | | `Failed` | Destructive border and description; media shows the retry control or alert glyph with `on_retry`, the ban glyph without one (a rejection). | Error reason plus `on_retry`, or `tooltip(...)` with the reason and `on_remove`. | | `Complete` | Ready surface; preview is full opacity. | File metadata and normal actions. | ```rust Attachment::new() .status(AttachmentStatus::Uploading) .media(AttachmentMedia::new().child(Icon::new(IconName::FileText))) .content( AttachmentContent::new() .title(AttachmentTitle::new("design-assets.zip")) .description(AttachmentDescription::new("Uploading · 68%")) .child(Progress::new("attachment-progress").value(68.)), ) .actions( AttachmentActions::new() .child(Button::new("cancel-upload").ghost().xsmall().label("Cancel")), ) ``` The status helpers are useful when application state maps to presentation: ```rust match status { AttachmentStatus::Pending => "Ready to upload", AttachmentStatus::Uploading => "Uploading…", AttachmentStatus::Processing => "Processing…", AttachmentStatus::Failed => "Upload failed", AttachmentStatus::Complete => "Ready", } ``` `is_pending()`, `is_uploading()`, `is_processing()`, `is_failed()`, `is_complete()`, and `is_in_progress()` are pure readers. They do not update the attachment or the application upload task. ## Status inheritance and overrides Titles and descriptions added through their typed builders inherit the parent status. An explicit child status wins over the inherited value: ```rust Attachment::new() .status(AttachmentStatus::Failed) .content( AttachmentContent::new() .title(AttachmentTitle::new("archive.zip")) .description( AttachmentDescription::new("Previous upload completed") .status(AttachmentStatus::Complete), ), ) ``` Use typed `.title(...)` and `.description(...)` whenever loading shimmer or failure coloring should follow the attachment. The generic `.child(...)` form still accepts arbitrary elements, but it cannot inspect the erased child's status and therefore does not inherit automatically: ```rust AttachmentContent::new() .title(AttachmentTitle::new("status-aware-title")) .description(AttachmentDescription::new("status-aware-description")) .child(custom_metadata_element) ``` Customize an in-progress title with a reusable shimmer style: ```rust AttachmentTitle::new("transcript.pdf") .with_shimmer_style( ShimmerStyle::new() .duration(std::time::Duration::from_secs(3)) .spread(0.45) .reverse(true) .once(false), ) ``` `AttachmentDescription` uses the destructive semantic color only for an explicit or inherited `Failed` status. The words in the description should still state what happened; color is a supporting cue. ## Sizes and axes `Attachment` implements `Sizable`. The convenience builders map to `Size`: ```rust Attachment::new().xsmall(); Attachment::new().small(); Attachment::new(); // medium (default) Attachment::new().large(); Attachment::new().w_auto() // let the chip hug its content instead of the fixed width ``` The named sizes set the whole geometry as one scale, in rems so it follows the root font size: | Size | Chip height | Chip width | Media | Title | | --- | --- | --- | --- | --- | | `XSmall` | 40 px | 176 px | 28 px | 11 px | | `Small` | 48 px | 200 px | 32 px | 12 px | | `Medium` | 56 px | 232 px | 38 px | 13 px | | `Large` | 64 px | 272 px | 44 px | 14 px | A horizontal card takes the fixed width only when it carries content, so a row of chips lines up and long names truncate instead of stretching the card. An image tile is a square with the chip's height. `Size::Size(...)` scales the `Medium` geometry from a custom base value. Refine with the normal GPUI width methods (`w_auto()`, `w_full()`, `w(...)`) when the product needs another measure; named sizes are preferable for a coherent theme. Horizontal is the default and keeps the media, metadata, and actions in one row. Vertical moves the preview above the metadata and places actions over the preview's upper trailing corner: ```rust Attachment::new() .axis(Axis::Vertical) .large() .media(AttachmentMedia::new().src(preview_url)) .content( AttachmentContent::new() .title(AttachmentTitle::new("presentation.png")) .description(AttachmentDescription::new("PNG · 1920 × 1080")), ) .actions( AttachmentActions::new() .child(Button::new("remove-presentation").ghost().xsmall().label("Remove")), ) ``` The vertical default is square media. Set a media aspect ratio or size when the content needs a landscape preview. `AttachmentContent` and `AttachmentActions` remain independent slots, so an application can omit one or place additional controls in either. ## Content and actions `AttachmentContent` keeps titles and descriptions in a vertical metadata stack. It also accepts custom children for progress, badges, or a second line: ```rust AttachmentContent::new() .title(AttachmentTitle::new("report.pdf")) .description(AttachmentDescription::new("PDF · 2.4 MB")) .child(Badge::new().count(3)) ``` Use `AttachmentActions` for one or more existing semantic controls: ```rust AttachmentActions::new() .child(Button::new("download").ghost().xsmall().label("Download")) .child(Button::new("remove").danger().xsmall().label("Remove")) ``` `AttachmentActions` only supplies layout and does not make its children focusable, clickable, or disabled. A tooltip is supplemental; the current `Button` implementation derives its accessibility label from `.label(...)`, so use a visible label when an action must have a named accessible control. An icon-only button with only `.tooltip(...)` is not a substitute for that label. ## Whole-card click Set `.id(...)` and `.on_click(...)` to make the whole card activate, e.g. to open a preview. The click layer is painted below `AttachmentActions`, so action buttons stay independently clickable: ```rust Attachment::new() .id("design-attachment") .on_click(|_, window, cx| { // Open the preview. }) .content( AttachmentContent::new() .title(AttachmentTitle::new("design-mockups.png")) .description(AttachmentDescription::new("PNG · 1.8 MB")), ) .actions( AttachmentActions::new() .child(Button::new("remove").ghost().xsmall().icon(IconName::Close)), ) ``` The handler takes effect only together with `.id(...)`; click state needs that stable identity. A clickable card shows a muted hover surface so it reads as interactive. What activation means — a dialog, a browser, a file viewer, or a selection — stays with the application. Keep destructive and secondary commands in `AttachmentActions` so they never depend on the card's primary activation, and offer the card's primary action as a `Button` or `Link` somewhere reachable from the keyboard: the click layer itself is a pointer convenience and takes no focus. ## Remove and retry controls A composer removes attachments and retries failed uploads. Both controls are built in, so they look the same in every product and need no wrapper: ```rust Attachment::new() .id(("attachment", item.id)) .status(item.status) .on_remove(cx.listener(move |this, _, _, cx| this.remove(item.id, cx))) .on_retry(cx.listener(move |this, _, _, cx| this.retry(item.id, cx))) .axis(Axis::Vertical) .media(AttachmentMedia::new().src(thumbnail)) ``` `on_remove` rides a small surface-colored disc with a hairline border on the card's upper trailing corner, the way a card's close control usually looks. It appears on hover on desktop and stays visible on touch platforms; the card reserves the overhang, so a row of cards keeps its alignment. `on_retry` takes effect only while the status is `Failed`: an image preview gets a round button in its scrim, and a typed description gets a localized “Retry” link after its text. A failed attachment without `on_retry` reads as a rejection and shows the ban glyph instead. All of these key their element state on `.id(...)`, so they take effect only together with it. What removing or retrying means stays with the application. Two more builders complete the composer picture: ```rust Attachment::new() .id(("attachment", item.id)) .status(AttachmentStatus::Uploading) .progress(item.percent) // 0..=100 .content( AttachmentContent::new() .title(AttachmentTitle::new("Q3 statement.pdf")) .description(AttachmentDescription::new("Uploading")), ); Attachment::new() .id(("attachment", item.id)) .status(AttachmentStatus::Failed) .tooltip("Image exceeds 20 MB limit · Remove to send") .on_remove(cx.listener(move |this, _, _, cx| this.remove(item.id, cx))) .axis(Axis::Vertical) .media(AttachmentMedia::new().src(thumbnail)) ``` `progress(percent)` turns the uploading spinner into a determinate ring, draws a thin primary bar along a horizontal card's bottom edge, and appends “· 62%” to a typed description; it is ignored in every other status. `tooltip(text)` shows the text while the card is hovered, which is where the reason for a failure or a rejection belongs. ## Groups `AttachmentGroup` provides a horizontally scrollable row with the shared group gap. Its ID is required because it owns GPUI's element-local scroll state: ```rust AttachmentGroup::new("message-attachments") .child(first_attachment) .child(second_attachment) .child(third_attachment) ``` The group is `w_full()`, `min_w_0()`, and uses horizontal scrolling. It does not provide selection, snapping, reorder handles, a “+N more” overflow label, or a preview dialog. Compose those behaviors in an application-owned wrapper. Keep the ID stable for the lifetime of the conversation row. Two builders help a composer or message row that overflows: ```rust AttachmentGroup::new("composer-attachments") // Fade each edge into the surface behind the row while it hides content. .with_edge_fade(cx.theme().background) // Drive the scrolling yourself, e.g. from paging buttons. .track_scroll(&self.attachments_scroll) .children(attachments) ``` `with_edge_fade(color)` draws a short gradient at an edge only while more attachments continue past it; a row that fits shows none. The fades sit above the attachments and take no pointer events. `track_scroll(&handle)` replaces the group's own scroll state with the caller's `ScrollHandle`, so the application can move the row and read its offset. ## Custom styling and theme tokens `Attachment`, `AttachmentGroup`, and every named slot implement `Styled`. Refinements are applied after component defaults, which gives developers control over the surface, spacing, media geometry, typography, and action layout: ```rust Attachment::new() .w_full() .rounded(cx.theme().radius_lg) .bg(cx.theme().group_box) .border_color(cx.theme().ring) .media( AttachmentMedia::new() .rounded(cx.theme().radius_lg) .bg(cx.theme().primary.opacity(0.12)) .text_color(cx.theme().primary) .child(Icon::new(IconName::FileText)), ) .content( AttachmentContent::new() .title(AttachmentTitle::new("custom-theme.json").text_color(cx.theme().primary)) .description(AttachmentDescription::new("JSON · 16 KB")), ) ``` Prefer semantic roles from `cx.theme()` (`background`, `muted`, `border`, `destructive`, `foreground`, and their foreground counterparts) to raw colors. The component's default radii, spacing, and typography follow the shared design scale; application-specific density can be expressed with `Size` and typed style refinements at the composition boundary. Use `AttachmentContent::title(...)` and `.description(...)` for status-aware metadata, `.child(...)` for arbitrary custom content, child `.status(...)` for an explicit override, `AttachmentTitle::with_shimmer_style(...)` for loading motion, and `AttachmentMedia::overlay(...)` for controls above an image. ## Accessibility and state guidance - Include the file name and useful type/size information in text. An icon-only media preview is not enough to identify the attachment. - Put upload, retry, remove, download, and preview actions in semantic `Button` or `Link` controls. A tooltip is supplemental; for the current `Button` API, use `.label(...)` when the action needs an accessible name. - Describe `Pending`, `Uploading`, `Processing`, and `Failed` in text or a control state. The dashed border, opacity, shimmer, and destructive color are supporting cues. - Keep progress determinate when the application knows a byte or item count; use `Progress` as a child rather than duplicating progress semantics in `Attachment`. - Loading shimmer is disabled by `ShimmerText` when reduced motion is enabled. Keep a readable title and description visible in that mode. - Ensure a vertical overlay action remains reachable from the keyboard; it must not be available only through image hover. ## Component boundaries These boundaries are deliberate: - Use `Button` directly instead of an attachment-specific action component. This preserves Button variants, sizes, loading, disabled behavior, focus, and event handling. - Use `Progress` directly instead of an attachment-specific progress wrapper. - Use `.id(...)` with `.on_click(...)` for whole-card activation. The card only reports the click; whether that opens a dialog, a browser, a file viewer, or toggles a selection stays with the application. - Use `AttachmentGroup` only for the shared horizontal row and overflow. Use an application-owned container for selection, reordering, snapping, or custom scroll controls. ## API reference ### `Attachment` | Method | Default | Purpose | | --- | --- | --- | | `new()` | `Complete`, `Medium`, `Horizontal`, no slots | Create an attachment. | | `id(ElementId)` | none | Stable identity for the built-in controls: click layer, remove, retry. | | `on_click(handler)` | none | Whole-card activation; requires `id(...)` and stays below the actions. | | `on_remove(handler)` | none | Corner remove control; requires `id(...)`. | | `on_retry(handler)` | none | Retry control while `Failed`; requires `id(...)`. | | `progress(percent)` | none | Determinate ring, bottom bar and “· 62%” while `Uploading`. | | `tooltip(text)` | none | Hover tooltip, e.g. the failure reason; requires `id(...)`. | | `status(AttachmentStatus)` | `Complete` | Set lifecycle styling. | | `axis(Axis)` | `Horizontal` | Choose horizontal or vertical layout. | | `with_size(Size)` | `Medium` | Set a named or custom size. | | `xsmall()` / `small()` / `large()` | — | Sizable shortcuts. | | `media(AttachmentMedia)` | none | Add a preview slot. | | `content(AttachmentContent)` | none | Add metadata. | | `actions(AttachmentActions)` | none | Add action controls. | ### `AttachmentMedia` | Method | Default | Purpose | | --- | --- | --- | | `new()` | no source, no children | Create a media slot. | | `src(ImageSource)` | none | Render an image preview. | | `with_size(Size)` | inherited attachment size | Override media density. | | `overlay(element)` | none | Center an element over the media, above the status treatment. | | `child(element)` | — | Add an icon or custom content above the preview. | | `Styled` methods | themed muted media | Refine geometry, radius, background, and typography. | ### `AttachmentContent`, `AttachmentTitle`, and `AttachmentDescription` | Method | Default | Purpose | | --- | --- | --- | | `AttachmentContent::new()` | empty vertical metadata stack | Create content. | | `.title(AttachmentTitle)` | — | Add a status-aware single-line title. | | `.description(AttachmentDescription)` | — | Add a status-aware single-line description. | | `AttachmentTitle::new(text)` | no explicit child status | Create a title. | | `AttachmentTitle::status(status)` | inherits parent | Override title loading state. | | `AttachmentTitle::with_shimmer_style(style)` | default shimmer | Customize title animation. | | `AttachmentDescription::new(text)` | no explicit child status | Create a description. | | `AttachmentDescription::status(status)` | inherits parent | Override description color state. | | `.child(element)` | — | Add progress, badges, or custom metadata. | ### `AttachmentActions` and `AttachmentGroup` | Method | Default | Purpose | | --- | --- | --- | | `AttachmentActions::new()` | empty action layout | Create the action slot. | | `.child(element)` | — | Add Button, Link, or another control. | | `AttachmentGroup::new(id)` | stable ID required | Create a horizontal scrolling group. | | `AttachmentGroup::track_scroll(&ScrollHandle)` | own scroll state | Scroll the row through the caller's handle. | | `AttachmentGroup::with_edge_fade(color)` | none | Fade an edge into `color` while it hides attachments. | | `AttachmentGroup::child(element)` | — | Add attachments to the group. | ### Related types - [`AttachmentStatus`] — `Pending`, `Uploading`, `Processing`, `Failed`, and `Complete`. - [`Size`] — `XSmall`, `Small`, `Medium`, `Large`, or a custom `Pixels` value. - [`Axis`] — `Horizontal` or `Vertical` from GPUI. - [`ShimmerStyle`] — shared loading animation configuration. [Attachment]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.Attachment.html [AttachmentMedia]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentMedia.html [AttachmentContent]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentContent.html [AttachmentTitle]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentTitle.html [AttachmentDescription]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentDescription.html [AttachmentActions]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentActions.html [AttachmentGroup]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.AttachmentGroup.html [AttachmentStatus]: https://docs.rs/gpui-component/latest/gpui_component/attachment/enum.AttachmentStatus.html [Size]: https://docs.rs/gpui-component/latest/gpui_component/enum.Size.html [Axis]: https://docs.rs/gpui/latest/gpui/enum.Axis.html [ShimmerStyle]: https://docs.rs/gpui-component/latest/gpui_component/shimmer/struct.ShimmerStyle.html --- # ColorPicker Source: /component/color-picker A versatile color picker component that provides an intuitive interface for color selection. Features include color palettes, hex input, featured colors, and support for various color formats including RGB, HSL, and hex values with alpha channel support. ## Import ```rust use gpui_kit::component::color_picker::{ ColorPicker, ColorPickerEvent, ColorPickerState, ColorSelect, }; ``` ## Usage ### Basic Color Picker ```rust use gpui_kit::{Entity, Window, Context}; // Create color picker state let color_picker = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().primary) ); // Create the color picker component ColorPicker::new(&color_picker) ``` ### With Event Handling ```rust use gpui_kit::{Subscription, Entity}; let color_picker = cx.new(|cx| ColorPickerState::new(window, cx)); let _subscription = cx.subscribe(&color_picker, |this, _, ev, _| match ev { ColorPickerEvent::Change(color) => { if let Some(color) = color { println!("Selected color: {}", color.to_hex()); // Handle color change } } }); ColorPicker::new(&color_picker) ``` ### Setting Default Color ```rust use gpui_kit::Hsla; let color_picker = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().blue) // Set default color ); ``` ### Different Sizes ```rust // Small color picker ColorPicker::new(&color_picker).small() // Medium color picker (default) ColorPicker::new(&color_picker) // Large color picker ColorPicker::new(&color_picker).large() // Extra small color picker ColorPicker::new(&color_picker).xsmall() ``` ### With Custom Featured Colors ```rust use gpui_kit::Hsla; let featured_colors = vec![ cx.theme().red, cx.theme().green, cx.theme().blue, cx.theme().yellow, // Add your custom colors ]; ColorPicker::new(&color_picker) .featured_colors(featured_colors) ``` ### With Icon Instead of Color Square ```rust use gpui_kit::component::IconName; ColorPicker::new(&color_picker) .icon(IconName::Palette) ``` ### With Label ```rust ColorPicker::new(&color_picker) .label("Background Color") ``` ### Custom Anchor Position ```rust use gpui_kit::Anchor; ColorPicker::new(&color_picker) .anchor(Anchor::TopRight) // Dropdown opens to top-right ``` ### Theme Color Select a color and preview the resulting value. The gallery opens on indigo. ```rust ColorPicker::new(&self.color).with_size(self.size) ``` ### Color Select `ColorSelect` draws the picker as a framed field, like a `Select`: a swatch of the current color, its hex value and a caret. Clicking anywhere on the field opens the same popover. Use it in forms, where the control should share the height and frame of the inputs around it; keep `ColorPicker` for a compact swatch in a toolbar. ```rust use gpui_kit::component::{Sizable as _, form::field}; field() .label("Theme color") .child(ColorSelect::new(&color_picker)) // Follows the same sizes as Input and Select. ColorSelect::new(&color_picker).large() // Shown while no color is selected. ColorSelect::new(&color_picker).placeholder("Pick a color") ``` ### Color Theme Editor ```rust struct ThemeEditor { primary_color: Entity, secondary_color: Entity, accent_color: Entity, } impl ThemeEditor { fn new(window: &mut Window, cx: &mut Context) -> Self { let primary_color = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().primary) ); let secondary_color = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().secondary) ); let accent_color = cx.new(|cx| ColorPickerState::new(window, cx) .default_value(cx.theme().accent) ); Self { primary_color, secondary_color, accent_color, } } fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .gap_4() .child( h_flex() .gap_2() .items_center() .child("Primary Color:") .child(ColorPicker::new(&self.primary_color)) ) .child( h_flex() .gap_2() .items_center() .child("Secondary Color:") .child(ColorPicker::new(&self.secondary_color)) ) .child( h_flex() .gap_2() .items_center() .child("Accent Color:") .child(ColorPicker::new(&self.accent_color)) ) } } ``` ### Brand Color Selector ```rust use gpui_kit::component::{Sizable as _}; let brand_colors = vec![ Hsla::parse_hex("#FF6B6B").unwrap(), // Brand Red Hsla::parse_hex("#4ECDC4").unwrap(), // Brand Teal Hsla::parse_hex("#45B7D1").unwrap(), // Brand Blue Hsla::parse_hex("#96CEB4").unwrap(), // Brand Green Hsla::parse_hex("#FFEAA7").unwrap(), // Brand Yellow ]; ColorPicker::new(&color_picker) .featured_colors(brand_colors) .label("Brand Color") .large() ``` ### Toolbar Color Picker ```rust use gpui_kit::component::{Sizable as _, IconName); ColorPicker::new(&text_color_picker) .icon(IconName::Type) .small() .anchor(Anchor::BottomLeft) ``` ### Color Palette Builder ```rust struct ColorPalette { colors: Vec>, } impl ColorPalette { fn add_color(&mut self, window: &mut Window, cx: &mut Context) { let color_picker = cx.new(|cx| ColorPickerState::new(window, cx)); // Subscribe to color changes cx.subscribe(&color_picker, |this, _, ev, _| match ev { ColorPickerEvent::Change(color) => { if let Some(color) = color { this.update_palette_preview(); } } }); self.colors.push(color_picker); cx.notify(); } fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { h_flex() .gap_2() .children( self.colors.iter().map(|color_picker| { ColorPicker::new(color_picker).small() }) ) .child( Button::new("add-color") .icon(IconName::Plus) .ghost() .on_click(cx.listener(|this, _, window, cx| { this.add_color(window, cx); })) ) } } ``` ### With Color Validation ```rust let color_picker = cx.new(|cx| ColorPickerState::new(window, cx)); let _subscription = cx.subscribe(&color_picker, |this, _, ev, _| match ev { ColorPickerEvent::Change(color) => { if let Some(color) = color { // Validate color accessibility if this.validate_contrast(color) { this.apply_color(color); } else { this.show_contrast_warning(); } } } }); ``` [ColorPicker]: https://docs.rs/gpui-component/latest/gpui_component/color_picker/struct.ColorPicker.html [ColorSelect]: https://docs.rs/gpui-component/latest/gpui_component/color_picker/struct.ColorSelect.html [ColorPickerState]: https://docs.rs/gpui-component/latest/gpui_component/color_picker/struct.ColorPickerState.html [ColorPickerEvent]: https://docs.rs/gpui-component/latest/gpui_component/color_picker/enum.ColorPickerEvent.html ## Color Selection Interface ### Color Palettes The color picker includes predefined color palettes organized by color family: - **Stone**: Neutral grays and stone colors - **Red**: Red color variations from light to dark - **Orange**: Orange color variations - **Yellow**: Yellow color variations - **Green**: Green color variations - **Cyan**: Cyan color variations - **Blue**: Blue color variations - **Purple**: Purple color variations - **Pink**: Pink color variations Each palette provides multiple shades and tints of the base color, allowing for precise color selection. ### Featured Colors Section A customizable section at the top of the picker that displays frequently used or brand colors. If not specified, defaults to theme colors: - Primary colors from the current theme - Light variants of theme colors - Essential UI colors (red, blue, green, yellow, cyan, magenta) ### Hex Input Field A text input field that allows direct entry of hex color values: - Supports standard 6-digit hex format (#RRGGBB) - Real-time validation and preview - Updates color picker state automatically - Press Enter to confirm selection ## Color Formats ### RGB (Red, Green, Blue) Colors are internally represented using GPUI's `Hsla` format but can be converted to RGB: ```rust let color = cx.theme().blue; // Access RGB components through Hsla methods ``` ### HSL (Hue, Saturation, Lightness) Native format used by the color picker: ```rust use gpui_kit::Hsla; // Create HSL color let color = Hsla::hsl(240.0, 100.0, 50.0); // Blue color // Access components let hue = color.h; let saturation = color.s; let lightness = color.l; ``` ### Hex Format Standard web hex format with # prefix: ```rust // Convert color to hex let hex_string = color.to_hex(); // Returns "#3366FF" // Parse hex string to color if let Ok(color) = Hsla::parse_hex("#3366FF") { // Use parsed color } ``` ## Alpha Channel Full alpha channel support for transparency: ```rust use gpui_kit::hsla; // Create color with alpha let semi_transparent = hsla(0.5, 0.8, 0.6, 0.7); // 70% opacity // Modify existing color opacity let transparent_blue = cx.theme().blue.opacity(0.5); ``` The color picker preserves alpha values when selecting colors and allows modification through the alpha component of HSLA colors. ## API Reference - [ColorPicker] - [ColorSelect] - [ColorPickerState] - [ColorPickerEvent] --- # Image Source: /component/image GPUI's `img()` draws an image, and `svg()` draws a single-color icon. GPUI Kit re-exports both from `gpui_kit`. This page shows the patterns an application uses most; [Images](/docs/image) explains sources, loading, sizing, `svg()`, caching, and HTTP caching in detail. ## Start with a working image This complete native `src/main.rs` uses an icon already bundled by GPUI Kit, so it needs no extra asset file. Add `gpui-kit = "{{gpui_kit_version}}"` to `Cargo.toml`. The same asset is shown as a color-preserving image and as a monochrome SVG. This complete native `src/main.rs` uses an icon already bundled by GPUI Kit, so it needs no extra asset file. Add `gpui-kit = "{{gpui_kit_version}}"` to `Cargo.toml`. The same asset is shown as a color-preserving image and as a theme-colored monochrome SVG; the difference is explained below. ```rust use gpui_kit::*; use gpui_kit::assets::Assets; struct ImageExample; impl Render for ImageExample { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div() .flex() .gap_4() .child( img("icons/inbox.svg") .id("inbox-image") .size(px(64.)) .object_fit(ObjectFit::Contain) .with_loading(|| div().child("Loading image...").into_any_element()) .with_fallback(|| div().child("Image unavailable").into_any_element()), ) .child(svg().path("icons/inbox.svg").size(px(64.))) } } fn main() { gpui_kit::application().with_assets(Assets).run(|cx| { gpui_kit::init(cx); gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| { cx.new(|_| ImageExample) }) .expect("Failed to open window"); }); } ``` `with_assets(Assets)` registers the default component icons; register your own `AssetSource` to embed your images (see [Icons & Assets](/docs/assets)). A string such as `"images/cover.png"` is an asset key, a `Path` reads a file, and a URL is fetched through the App's `HttpClient`; see [Sources](/docs/image#sources). ## Common patterns A thumbnail that fills a fixed box and crops its edges: ```rust img("images/cover.png") .w(px(320.)) .h(px(180.)) .object_fit(ObjectFit::Cover) ``` A banner that follows the parent width and reserves its height before the image arrives: ```rust img("images/banner.webp") .w_full() .aspect_ratio(16. / 9.) .object_fit(ObjectFit::Cover) ``` A remote image with loading and failure states. The `.id(...)` is required for the loading state to appear: ```rust img("https://example.com/avatar.png") .id("avatar") .size(px(48.)) .rounded_full() .with_loading(|| div().child("Loading image...").into_any_element()) .with_fallback(|| div().child("Image unavailable").into_any_element()) ``` Use `ObjectFit::Contain` (the default) for logos and diagrams that must stay fully visible. To keep a multicolor SVG's colors, draw it with `img()`; for an icon that follows the theme, use [Icon](/component/icon). See [Size and fit](/docs/image#size-and-fit) and [img() or svg()](/docs/image#img-or-svg). ## Gallery with selection A thumbnail that changes the main image is a control. Give it a real `Button` so keyboard activation and an accessible name work, and keep the selected index in the view's retained state. This complete `src/main.rs` uses three icons already shipped with GPUI Kit; replace the source array with your own registered image keys for a photo gallery. Run it with the same `gpui-kit = "{{gpui_kit_version}}"` dependency as the first example. ```rust use gpui_kit::*; use gpui_kit::assets::Assets; use gpui_kit::base::Selectable; use gpui_kit::component::button::Button; const PICTURES: [(&str, &str); 3] = [ ("icons/inbox.svg", "Inbox"), ("icons/book-open.svg", "Book"), ("icons/gallery-vertical-end.svg", "Gallery"), ]; struct Gallery { selected: usize, } impl Render for Gallery { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let (source, name) = PICTURES[self.selected]; div() .flex() .flex_col() .gap_3() .p_4() .child(format!("Selected: {name}")) .child( img(source) .id("gallery-main") .w(px(360.)) .h(px(240.)) .object_fit(ObjectFit::Contain) .with_fallback(|| div().child("Image unavailable").into_any_element()), ) .child( div().flex().gap_2().children( PICTURES.iter().enumerate().map(|(index, (source, name))| { Button::new(format!("gallery-thumbnail-{index}")) .accessibility_label(format!("Show {name}")) .selected(index == self.selected) .child( img(*source) .size(px(40.)) .object_fit(ObjectFit::Contain), ) .on_click(cx.listener(move |this, _, _, cx| { this.selected = index; cx.notify(); })) }), ), ) } } fn main() { gpui_kit::application().with_assets(Assets).run(|cx| { gpui_kit::init(cx); gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| { cx.new(|_| Gallery { selected: 0 }) }) .expect("Failed to open window"); }); } ``` The selected button has an explicit selected state, and the visible `Selected: ...` text reports the current choice. In a product gallery, use meaningful labels such as `Show front view` rather than a file name. If the collection can be empty, render an empty-state message before indexing it. If image sources can be removed or reordered, store a stable domain ID instead of an index and resolve it during render. ## Accessibility - Pair an informative image with visible text or an accessible description in the surrounding UI. A file name is not a description. Decorative images need no narration. - Make an image that triggers a command a real control, such as a `Button`, so it has a name, focus, and keyboard activation. - Keep the loading and failure views in the same box as the image, so nearby content does not move. ## Learn more - [Images](/docs/image): every `img()` source, loading and decoding, `svg()`, and troubleshooting. - [Caches for decoded images](/docs/image#caches-for-decoded-images): scoped and custom `ImageCache`s. - [Cache remote images over HTTP](/docs/image#cache-remote-images-over-http): reuse downloads across views and launches. --- # Resizable Source: /component/resizable The resizable component system provides a flexible way to create layouts with resizable panels. It supports both horizontal and vertical resizing, nested layouts, size constraints, and drag handles. Perfect for creating paned interfaces, split views, and adjustable dashboards. ## Import ```rust use gpui_kit::component::resizable::{ h_resizable, v_resizable, resizable_panel, ResizablePanelGroup, ResizablePanel, ResizableState, ResizablePanelEvent }; ``` ## Usage Use `h_resizable` to create a horizontal layout, `v_resizable` to create a vertical layout. The first argument is the `id` for this [ResizablePanelGroup]. In GPUI, the `id` must be unique within the layout scope (The nearest parent has presents `id`). ```rust h_resizable("my-layout") .on_resize(|state, window, cx| { // Handle resize event // You can read the panel sizes from the state. let state = state.read(cx); let sizes = state.sizes(); }) .child( // Use resizable_panel() to create a sized panel. resizable_panel() .size(px(200.)) .child("Left Panel") ) .child( // Or you can just add AnyElement without a size. div() .child("Right Panel") .into_any_element() ) ``` The `v_resizable` component is used to create a vertical layout. ```rust v_resizable("vertical-layout") .child( resizable_panel() .size(px(100.)) .child("Top Panel") ) .child( div() .child("Bottom Panel") .into_any_element() ) ``` ### Resize Handle Appearance A divider rests as a hairline. As the pointer engages it, an indicator grows and solidifies on top of that line through three levels — hovered, pressed, dragging — so a drag stays readable after the pointer has left the handle's own band, which happens within a pixel or two of the drag starting. `h_resizable`, `v_resizable` and the Dock install this appearance. `resize_handle_appearance()` is exported for a handle you build yourself, or to pass explicitly to `with_handle_appearance`: ```rust gpui_kit::base::h_resizable("my-layout") .with_handle_appearance(resize_handle_appearance()) ``` The indicator's duration and easing come from the theme's motion tokens, and a system reduced-motion preference takes it straight to its target. ### Panel Size Constraints ```rust resizable_panel() .size(px(200.)) // Initial size .size_range(px(150.)..px(400.)) // Min and max size .child("Constrained Panel") ``` ### Multiple Panels ```rust h_resizable("multi-panel", state) .child( resizable_panel() .size(px(200.)) .size_range(px(150.)..px(300.)) .child("Left Panel") ) .child( resizable_panel() .child("Center Panel") ) .child( resizable_panel() .size(px(250.)) .child("Right Panel") ) ``` ### Nested Layouts ```rust v_resizable("main-layout", window, cx) .child( resizable_panel() .size(px(300.)) .child( h_resizable("nested-layout", window, cx) .child( resizable_panel() .size(px(200.)) .child("Top Left") ) .child( resizable_panel() .child("Top Right") ) ) ) .child( resizable_panel() .child("Bottom Panel") ) ``` ### Nested Panel Groups ```rust h_resizable("outer", window, cx) .child( resizable_panel() .size(px(200.)) .child("Left Panel") ) .group( v_resizable("inner", window, cx) .child( resizable_panel() .size(px(150.)) .child("Top Right") ) .child( resizable_panel() .child("Bottom Right") ) ) ``` ### Conditional Panel Visibility ```rust resizable_panel() .visible(self.show_sidebar) .size(px(250.)) .child("Sidebar Content") ``` ### Panel with Size Limits ```rust // Panel with minimum size only resizable_panel() .size_range(px(100.)..Pixels::MAX) .child("Flexible Panel") // Panel with both min and max resizable_panel() .size_range(px(200.)..px(500.)) .child("Constrained Panel") // Panel with exact constraints resizable_panel() .size(px(300.)) .size_range(px(300.)..px(300.)) // Fixed size .child("Fixed Panel") ``` ### File Explorer Layout ```rust struct FileExplorer { show_sidebar: bool, } impl Render for FileExplorer { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { h_resizable("file-explorer", window, cx) .child( resizable_panel() .visible(self.show_sidebar) .size(px(250.)) .size_range(px(200.)..px(400.)) .child( v_flex() .p_4() .child("📁 Folders") .child("• Documents") .child("• Pictures") .child("• Downloads") ) ) .child( v_flex() .p_4() .child("📄 Files") .child("file1.txt") .child("file2.pdf") .child("image.png") .into_any_element() ) } } ``` ### IDE Layout ```rust struct IDELayout { main_state: Entity, sidebar_state: Entity, bottom_state: Entity, } impl Render for IDELayout { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { h_resizable("ide-main", self.main_state.clone()) .child( resizable_panel() .size(px(300.)) .size_range(px(200.)..px(500.)) .child( v_resizable("sidebar", self.sidebar_state.clone()) .child( resizable_panel() .size(px(200.)) .child("File Explorer") ) .child( resizable_panel() .child("Outline") ) ) ) .child( resizable_panel() .child( v_resizable("editor-area", self.bottom_state.clone()) .child( resizable_panel() .child("Code Editor") ) .child( resizable_panel() .size(px(150.)) .size_range(px(100.)..px(300.)) .child("Terminal / Output") ) ) ) } } ``` ### Dashboard with Widgets ```rust struct Dashboard { layout_state: Entity, widget_state: Entity, } impl Render for Dashboard { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { v_resizable("dashboard", self.layout_state.clone()) .child( resizable_panel() .size(px(120.)) .child("Header / Navigation") ) .child( resizable_panel() .child( h_resizable("widgets", self.widget_state.clone()) .child( resizable_panel() .size(px(300.)) .child("Chart Widget") ) .child( resizable_panel() .child("Data Table") ) .child( resizable_panel() .size(px(250.)) .child("Stats Panel") ) ) ) .child( resizable_panel() .size(px(60.)) .child("Footer") ) } } ``` ### Settings Panel ```rust struct SettingsPanel { settings_state: Entity, } impl SettingsPanel { fn new(cx: &mut Context) -> Self { let settings_state = ResizableState::new(cx); // Listen for resize events to save layout preferences cx.subscribe(&settings_state, |this, _, event: &ResizablePanelEvent, cx| { match event { ResizablePanelEvent::Resized => { this.save_layout_preferences(cx); } } }); Self { settings_state } } fn save_layout_preferences(&self, cx: &mut Context) { let sizes = self.settings_state.read(cx).sizes(); // Save to preferences println!("Saving layout: {:?}", sizes); } } impl Render for SettingsPanel { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { h_resizable("settings", self.settings_state.clone()) .child( resizable_panel() .size(px(200.)) .size_range(px(150.)..px(300.)) .child( v_flex() .gap_2() .p_4() .child("Categories") .child("• General") .child("• Appearance") .child("• Advanced") ) ) .child( resizable_panel() .child( div() .p_6() .child("Settings Content Area") ) ) } } ``` ## Best Practices 1. **State Management**: Use separate ResizableState for independent layouts 2. **Size Constraints**: Always set reasonable min/max sizes for panels 3. **Event Handling**: Subscribe to ResizablePanelEvent for layout persistence 4. **Nested Layouts**: Use `.group()` method for clean nested structures 5. **Performance**: Avoid excessive nesting for better performance 6. **User Experience**: Provide adequate handle padding for easier interaction --- # Root View Source: /component/root [Root] is the Base-owned root view of every GPUI Kit window. `gpui_kit::open_window` is the application entry point and always creates this type. Base does not expose a separate window helper; `component::Root` re-exports the Base type. Base owns the content and overlay host, keyboard traversal and selection copying. Calling `gpui_component::init` explicitly registers the styled window extension: dialogs, sheets, notifications, tooltips, menus, touch selection, theme and window chrome. Initialize it before creating windows. A Base-only application calls `gpui_base::init` and needs no Component or Kit dependency. Cargo feature unification does not change the root type. This complete **Tested consumer recipe** is compiled from the isolated `gpui-kit` consumer workspace. It initializes GPUI Kit, then opens a window whose root is a `Root` wrapping the application view. ```rust use gpui_kit::{ AppContext as _, Context, IntoElement, ParentElement as _, Render, Styled as _, Window, WindowOptions, div, }; pub fn run() { gpui_kit::application() .with_assets(gpui_kit::assets::Assets) .run(|cx| { gpui_kit::init(cx); // The window's root view is a `Root` wrapping the view, which // renders dialogs, sheets and notifications above it. gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| { cx.new(|_| BootstrapView) }) .expect("failed to open window"); }); } struct BootstrapView; impl Render for BootstrapView { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div().size_full().child("My application") } } ``` `gpui_kit::open_window` is `cx.open_window` plus the `Root` wrapper. `Root` must be the window's root view so window-level facilities such as dialogs, sheets, notifications, focus traversal, and text selection remain available. Client-side window borders are selected from the window's decoration mode; server-decorated and layer-shell windows do not require Root configuration. `open_window` returns the window and the view, so a view that must be built inside the window (it owns an `InputState`, say) can still be kept: ```rust let (window, editor) = gpui_kit::open_window(WindowOptions::default(), cx, |window, cx| { cx.new(|cx| Editor::new(window, cx)) })?; ``` ## Closing windows and quitting Applications define their own quit and close-window actions and key bindings. `gpui_kit::init` does not install them. Handle unsaved changes and any confirmation in the application before closing a window or quitting. ## Overlays `Root` always mounts the dialog, sheet and notification layers above application content. Applications only call `window.open_dialog`, `window.open_sheet` or `window.push_notification`; no manual mounting or configuration is needed. Child view caching does not affect overlay rendering. ### Migrating to 0.7.0 `Root::render_dialog_layer`, `Root::render_sheet_layer` and `Root::render_notification_layer` have been removed. Delete their calls and the corresponding `.children(...)` expressions from application views. Previously customized layer positions now use the window-level Root's overlay placement. [Root]: https://docs.rs/gpui-base/latest/gpui_base/struct.Root.html --- # Focus Trap Source: /component/focus-trap Focus trap utility for constraining keyboard focus within a specific container. Essential for modal dialogs, sheets, and overlay components to provide proper keyboard navigation accessibility. **Note:** [Dialog](/component/dialog) and [Sheet](/component/sheet) components have focus trap built-in. You only need to manually use `focus_trap()` for custom modal-like components. ## Import ```rust use gpui_kit::component::FocusTrapElement; ``` ## Usage ### Basic Focus Trap ```rust let container_handle = cx.focus_handle(); v_flex() .child(Button::new("btn1").label("Button 1")) .child(Button::new("btn2").label("Button 2")) .child(Button::new("btn3").label("Button 3")) .focus_trap("trap1", &container_handle) // Pressing Tab will cycle: btn1 -> btn2 -> btn3 -> btn1 // Focus will not escape to elements outside this container ``` ### Multiple Focus Traps You can have multiple independent focus trap areas in your application. Each trap operates independently: ```rust let trap1_handle = cx.focus_handle(); let trap2_handle = cx.focus_handle(); v_flex() .gap_4() // First focus trap area .child( h_flex() .gap_2() .child(Button::new("trap1-1").label("Area 1 - Button 1")) .child(Button::new("trap1-2").label("Area 1 - Button 2")) .child(Button::new("trap1-3").label("Area 1 - Button 3")) .focus_trap("trap1", &trap1_handle) ) // Second focus trap area .child( h_flex() .gap_2() .child(Button::new("trap2-1").label("Area 2 - Button 1")) .child(Button::new("trap2-2").label("Area 2 - Button 2")) .focus_trap("trap2", &trap2_handle) ) ``` ### Focus Trap with Dialog [Dialog] components have focus trap built-in automatically. You don't need to manually add `focus_trap()`: ```rust window.open_dialog(cx, |dialog, _, _| { dialog .title("Settings") .child( v_flex() .gap_3() .child(Button::new("save").label("Save")) .child(Button::new("cancel").label("Cancel")) .child(Button::new("reset").label("Reset")) ) // Dialog internally uses focus_trap() // Tab navigation automatically cycles: save -> cancel -> reset -> save }) ``` ### Focus Trap with Sheet [Sheet] components also have focus trap built-in automatically: ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Filter Options") .child( v_flex() .gap_2() .child(Checkbox::new("option1").label("Option 1")) .child(Checkbox::new("option2").label("Option 2")) .child(Button::new("apply").label("Apply Filters")) ) // Sheet internally uses focus_trap() // Focus automatically cycles within the sheet panel }) ``` ### Custom Modal with Focus Trap ```rust struct CustomModal { container_handle: FocusHandle, } impl CustomModal { fn new(cx: &mut App) -> Self { Self { container_handle: cx.focus_handle(), } } } impl Render for CustomModal { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { div() .absolute() .inset_0() .flex() .items_center() .justify_center() .child( v_flex() .gap_4() .p_6() .bg(cx.theme().background) .rounded(cx.theme().radius_lg) .shadow_lg() .border_1() .border_color(cx.theme().border) .child("This is a modal dialog") .child( h_flex() .gap_2() .child(Button::new("ok").primary().label("OK")) .child(Button::new("cancel").label("Cancel")) ) .focus_trap("modal", &self.container_handle) ) } } ``` ### Nested Focus Traps Focus traps support nesting. When multiple traps are active, the innermost trap takes precedence: ```rust let outer_handle = cx.focus_handle(); let inner_handle = cx.focus_handle(); div() .child( v_flex() .gap_4() .p_4() .border_1() .border_color(cx.theme().border) .child(Button::new("outer-1").label("Outer Button 1")) .child( // Inner trap takes precedence when focused h_flex() .gap_2() .p_4() .bg(cx.theme().accent.opacity(0.1)) .child(Button::new("inner-1").label("Inner Button 1")) .child(Button::new("inner-2").label("Inner Button 2")) .focus_trap("inner", &inner_handle) ) .child(Button::new("outer-2").label("Outer Button 2")) .focus_trap("outer", &outer_handle) ) ``` ### Conditional Focus Trap You can conditionally apply focus trapping based on application state: ```rust struct ModalView { is_modal: bool, container_handle: FocusHandle, } impl Render for ModalView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let content = v_flex() .gap_2() .child(Button::new("btn1").label("Button 1")) .child(Button::new("btn2").label("Button 2")) .child(Button::new("btn3").label("Button 3")); if self.is_modal { // Apply focus trap when in modal mode content.focus_trap("conditional", &self.container_handle) .into_any_element() } else { // Normal behavior without focus trap content.into_any_element() } } } ``` ## How It Works The focus trap system consists of three key components: 1. **FocusTrapContainer**: Wraps any container element and registers it as a focus trap area 2. **FocusTrapManager**: Global state manager that tracks all active focus traps 3. **Root Integration**: The [Root] view intercepts Tab/Shift-Tab events and enforces focus cycling When Tab or Shift-Tab is pressed: 1. [Root] detects if the currently focused element is inside a focus trap 2. If yes, it calculates the next focusable element within the same trap 3. If focus would escape the trap, it cycles back to the beginning (Tab) or end (Shift-Tab) 4. This prevents focus from leaving the trapped container ### Built-in Focus Trap Components The following components have focus trap functionality built-in and don't require manual `focus_trap()` calls: - **[Dialog]** - Modal dialogs automatically trap focus (see `dialog.rs:437`) - **[Sheet]** - Side panels automatically trap focus (see `sheet.rs:197`) ## Accessibility Notes - Focus trapping is essential for modal dialogs and overlays to meet WCAG accessibility guidelines - Always provide a way to close or dismiss trapped focus areas (ESC key, close button) - The first focusable element in the trap should receive focus when the trap is activated - Use focus traps sparingly - only for truly modal interactions - Ensure keyboard navigation order is logical within the trapped area ## See Also - [Root View System](/component/root) - Manages focus trap behavior at the window level - [Dialog](/component/dialog) - Uses focus trap automatically - [Sheet](/component/sheet) - Uses focus trap automatically - [focus-trap-react](https://github.com/focus-trap/focus-trap-react) - Similar concept for React applications [Root]: https://docs.rs/gpui-component/latest/gpui_component/struct.Root.html [FocusTrapElement]: https://docs.rs/gpui-component/latest/gpui_component/trait.FocusTrapElement.html [Dialog]: ./dialog.md [Sheet]: ./sheet.md ## API Reference - [FocusTrapElement](https://docs.rs/gpui-component/latest/gpui_component/trait.FocusTrapElement.html) - [FocusTrapContainer](https://docs.rs/gpui-component/latest/gpui_component/struct.FocusTrapContainer.html) --- # DropdownButton Source: /component/dropdown_button A [DropdownButton] is a combination of a button and a trigger button. It allows us to display a dropdown menu when the trigger is clicked, but the left Button can still respond to independent events. Shared variant and size can be set on the DropdownButton. Action-specific options such as its label, icon, tooltip, loading state and click handler belong to the inner [Button]. ## Import ```rust use gpui_kit::component::button::{Button, DropdownButton}; ``` ## Usage ```rust use gpui_kit::Anchor; DropdownButton::new("dropdown") .button(Button::new("btn").label("Click Me")) .dropdown_menu(|menu, _, _| { menu.menu("Option 1", Box::new(MyAction)) .menu("Option 2", Box::new(MyAction)) .separator() .menu("Option 3", Box::new(MyAction)) }) ``` ## Basic split The control is two buttons sharing one outline. The leading half runs its own action; the trailing half opens the menu, so a click on the label never opens the menu by accident. ```rust DropdownButton::new("dropdown") .primary() .button(Button::new("btn").label("Export").on_click(|_, _, _| println!("Export"))) .dropdown_menu(|menu, _, _| { menu.menu("Export all rows (.csv)", Box::new(MyAction)) .menu("Download report (.pdf)", Box::new(MyAction)) }) ``` ### Variants Same as [Button], DropdownButton supports different variants. ```rust DropdownButton::new("dropdown") .primary() .button(Button::new("btn").label("Primary")) .dropdown_menu(|menu, _, _| { menu.menu("Option 1", Box::new(MyAction)) }) ``` Leaving the variant or size unset on the DropdownButton uses the inner button's value for both halves. ### Inner button options ```rust DropdownButton::new("dropdown") .button( Button::new("btn") .label("Save") .compact() .loading(is_saving) .tooltip("Save the current view") .on_click(|_, _, _| println!("Saved")), ) .dropdown_menu(|menu, _, _| { menu.menu("Save as…", Box::new(MyAction)) }) ``` ## With custom anchor The menu's anchor and width belong to the menu, not to the button. Set them on the DropdownButton when the default placement does not fit. ```rust DropdownButton::new("dropdown") .button(Button::new("btn").label("Click Me")) .dropdown_menu_with_anchor(Anchor::BottomRight, |menu, _, _| { menu.menu("Option 1", Box::new(MyAction)) }) ``` [Button]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.Button.html [DropdownButton]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.DropdownButton.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html --- # Label Source: /component/label A versatile label component for displaying text with support for secondary text, highlighting, masking, and customizable styling. Perfect for form labels, captions, and general text display with optional/required indicators. ## Import ```rust use gpui_kit::component::label::{Label, HighlightsMatch}; ``` ## Usage ### Form labels ```rust // Required field Label::new("Email Address") .secondary("*") .text_color(cx.theme().destructive) // Optional field Label::new("Phone Number") .secondary("(optional)") // Field with description Label::new("Password") .secondary("(minimum 8 characters)") ``` ### Basic Label ```rust Label::new("This is a label") ``` ### Label with Secondary Text ```rust // Label with optional indicator Label::new("Company Address") .secondary("(optional)") // Label with required indicator Label::new("Email Address") .secondary("(required)") ``` ### Text Alignment ```rust // Left aligned (default) Label::new("Text align left") // Center aligned Label::new("Text align center") .text_center() // Right aligned Label::new("Text align right") .text_right() ``` ### Text Highlighting ```rust // Full text highlighting (finds all matches) Label::new("Hello World Hello") .highlights("Hello") // Prefix highlighting (only matches at start) Label::new("Hello World") .highlights(HighlightsMatch::Prefix("Hello".into())) // Highlight with secondary text Label::new("Company Name") .secondary("(optional)") .highlights("Company") ``` ### Color and Styling ```rust use gpui_kit::component::green_500; // Custom text color Label::new("Color Label") .text_color(green_500()) // Font styling Label::new("Font Size Label") .text_size(px(20.)) .font_semibold() .line_height(rems(1.8)) ``` ### Masked Labels ```rust // For sensitive information Label::new("9,182,1 USD") .text_2xl() .masked(true) // Shows as "•••••••••••" // Toggle masking programmatically Label::new("500 USD") .text_xl() .masked(self.masked) ``` ### Multi-line Text ```rust // Text wrapping with line height div().w(px(200.)).child( Label::new( "Label should support text wrap in default, \ if the text is too long, it should wrap to the next line." ) .line_height(rems(1.8)) ) ``` ### Different Sizes ```rust // Using text size utilities Label::new("Extra Large").text_2xl() Label::new("Large").text_xl() Label::new("Medium").text_base() // default Label::new("Small").text_sm() Label::new("Extra Small").text_xs() ``` ### Search Highlighting ```rust // Interactive search highlighting let search_term = "Hello"; Label::new("Hello World Hello Universe") .highlights(search_term) // Highlights all "Hello" occurrences ``` ### Sensitive Information ```rust // Financial data with toggle h_flex() .child( Label::new("$9,182.50 USD") .text_2xl() .masked(self.is_masked) ) .child( Button::new("toggle-mask") .ghost() .icon(if self.is_masked { IconName::EyeOff } else { IconName::Eye }) .on_click(|this, _, _, _| { this.is_masked = !this.is_masked; }) ) ``` ### Multi-language Support ```rust // Supports Unicode text Label::new("这是一个标签") // Chinese text Label::new("こんにちは世界") // Japanese text Label::new("🌍 Hello World 🚀") // Emojis ``` ### Status Indicators ```rust // Success status Label::new("✓ Verified") .text_color(cx.theme().success) // Warning status Label::new("⚠ Pending Review") .text_color(cx.theme().warning) // Error status Label::new("✗ Failed") .text_color(cx.theme().destructive) ``` ### Custom Layouts ```rust // Flex layout with labels h_flex() .justify_between() .child(Label::new("Total Amount")) .child(Label::new("$1,234.56").font_semibold()) // Grid layout v_flex() .gap_2() .child(Label::new("Name:").font_semibold()) .child(Label::new("John Doe")) .child(Label::new("Email:").font_semibold()) .child(Label::new("john@example.com")) ``` ## API Reference ### Label | Method | Description | | ------------------- | ------------------------------------------------------------- | | `new(text)` | Create a new label with text | | `secondary(text)` | Add secondary text (usually for optional/required indicators) | | `masked(bool)` | Show/hide text with bullet characters | | `highlights(match)` | Highlight matching text | ### HighlightsMatch | Variant | Description | | -------------- | ------------------------------------------------ | | `Full(text)` | Highlights all occurrences of the text | | `Prefix(text)` | Highlights only if text appears at the beginning | | Method | Description | | ------------- | ------------------------------- | | `as_str()` | Get the search text as string | | `is_prefix()` | Check if this is a prefix match | ### Styling Methods (via Styled trait) | Method | Description | | --------------------- | --------------------------- | | `text_color(color)` | Set text color | | `text_size(size)` | Set font size | | `text_center()` | Center align text | | `text_right()` | Right align text | | `font_semibold()` | Set font weight to semibold | | `font_bold()` | Set font weight to bold | | `line_height(height)` | Set line height | | `text_xs()` | Extra small text size | | `text_sm()` | Small text size | | `text_base()` | Base text size (default) | | `text_lg()` | Large text size | | `text_xl()` | Extra large text size | | `text_2xl()` | 2x large text size | --- # Toolbar Source: /component/toolbar Toolbar is a transparent horizontal container for actions — buttons, separators, and short labels — used inside panel headers, tab strips, and custom command surfaces. The surrounding container owns its background and border. The design mirrors the toolbars found in native UI frameworks: macOS `NSToolbar` and Windows `ToolStrip`. ## Import ```rust use gpui_kit::component::toolbar::Toolbar; ``` ## Composition Use `child` for `Sizable` controls. Toolbar applies its final size to those controls even when `.small()` or another size method appears after them in the builder chain. Use `content` for strings, separators, flexible spacers, and custom layout that should keep its own dimensions. Items render in source order. - For a **command**, pass a `Button`; Toolbar applies its size and forces the quiet `ghost + compact` presentation. Chain `label`, `icon`, `tooltip`, `on_click`, etc. as needed. - For an **icon-only button**, always add a `tooltip`; it is the accessible name as well. - For a **separator**, use `content` with `Separator::vertical()` and an explicit height. - For a **non-interactive label**, use one of the content methods with a plain string. ## Usage ### Commands ```rust Toolbar::new("toolbar") .child( Button::new("new") .icon(IconName::Plus) .label("New") .on_click(|_, window, cx| { /* ... */ }), ) .content(Separator::vertical().h_5()) .child( Button::new("undo") .icon(IconName::Undo2) .tooltip("Undo") .on_click(|_, window, cx| { /* ... */ }), ) .content(div().flex_1()) .child( Button::new("more") .icon(IconName::Ellipsis) .tooltip("More options") .on_click(|_, window, cx| { /* ... */ }), ) ``` ### Sizes Use `Sizable` to change the container height, spacing, text size, and hosted controls together: `xsmall` (28px), `small` (32px, default), and `medium` (48px). Builder order does not matter. ```rust Toolbar::new("toolbar") .child(Button::new("new").icon(IconName::Plus).label("New")) .child(Button::new("find").icon(IconName::Search).tooltip("Find")) .small() ``` ### Labels and custom elements ```rust Toolbar::new("toolbar") .content("Dashboard") .content(Separator::vertical().h_5()) .content( h_flex() .items_center() .gap_1() .child(Icon::new(IconName::CircleCheck).xsmall()) .child("Saved"), ) .content(div().flex_1()) .child(Button::new("settings").icon(IconName::Settings2).tooltip("Settings")) ``` ### Custom styling `Toolbar` is transparent and borderless by default. It implements `Styled`, so a standalone command surface can add its own appearance. ```rust Toolbar::new("toolbar") .bg(cx.theme().secondary) .border_color(cx.theme().border) .content("Ready") ``` ## Groups Wrap related controls in `ToolbarGroup` to give them an accessible name, so assistive technology reads a run of controls as one unit. The group implements `Sizable`, and a parent Toolbar propagates its final size through the group to every control: ```rust use gpui_kit::component::toolbar::ToolbarGroup; Toolbar::new("document-toolbar") .child( ToolbarGroup::new("history-group") .label("History") .gap_2() // match the bar's own item spacing .child(Button::new("undo").icon(IconName::Undo2).tooltip("Undo")) .child(Button::new("redo").icon(IconName::Redo2).tooltip("Redo")), ) ``` Unlike Base UI's `Toolbar.Group`, a group cannot disable its children: that API propagates through React context into Base UI's own button primitives, which has no equivalent for arbitrary GPUI children. Disabling the hosted controls is the caller's job. Separators and other non-sized elements use `content`. Sized controls use `child` so the toolbar can propagate its size. ## Keyboard The toolbar exposes `Toolbar` semantics to assistive technology and owns roving keyboard focus, matching the ARIA toolbar pattern and Base UI's `Toolbar`: | Key | Behavior | | --- | --- | | `←` / `→` | Move focus to the previous / next control (horizontal toolbar) | | `↑` / `↓` | Move focus to the previous / next control (vertical toolbar) | | `Tab` | Enter or leave the toolbar; the bar itself is not a tab stop | Focus wraps around at the ends. Hosted inputs keep their own arrow-key caret behavior; place inputs at the trailing end of the bar. This behavior comes from the unstyled `gpui_base::Toolbar` primitive, so applications building custom toolbars on the base layer get the same contract. ## Notes - Use `content(div().flex_1())` when later items need to align to the trailing edge. - Keep the primary command visible; move low-frequency actions into a dropdown or overflow menu rather than hiding them behind hover. - Toolbar has no default background or border; its host surface supplies them. ## API Reference ### Toolbar | Method | Description | | ----------------- | ---------------------------------------------------- | | `new()` | Create a new, empty toolbar (small size) | | `child(c)` / `children(cs)` | Add sized control(s) in source order | | `content(c)` / `contents(cs)` | Add non-sized content in source order | | `with_size(size)` | Set the bar size — `xsmall`, `small`, or `medium` | | `disabled(value)` | Disable roving navigation; the owner also disables hosted controls | Control methods require `Sizable + IntoElement`; content methods accept general elements. `Toolbar` also implements `Styled` and `Sizable`. --- # Chart Source: /component/chart A comprehensive charting library providing Line, Bar, Area, Pie, Radar, Candlestick, and Sankey charts for data visualization. The charts feature smooth animations, customizable styling, tooltips, legends, and automatic theming that adapts to your application's theme. ## Import ```rust use gpui_kit::component::chart::{ LineChart, BarChart, AreaChart, PieChart, RadarChart, CandlestickChart, SankeyChart, }; ``` ## Usage Each sample under Chart Types is a chart the gallery is already running. The heading is the chart type and the subtitle on that card. The code is the source of that card, so the sample is the example on screen. ## Chart Types ### LineChart A line chart displays data points connected by straight line segments, perfect for showing trends over time. #### Tick Control `LineChart` also takes `y_domain` and `point_count`; see Pinned Axis and Unfinished Series under AreaChart. #### Axes and Guides `LineChart` and `AreaChart` share these. `y_axis` shows tick labels at the y ticks, in a gutter left of the plot by default, which widens to fit the widest label, or over the plot with `y_axis_label_placement(AxisLabelPlacement::Inside)`. `y_tick_count` sets how many ticks there are, evenly spaced from the baseline to the top edge with both ends included; they place the horizontal grid lines too, and each label reads the value the scale puts at its height. The default of 5 is the grid the charts have always drawn. `y_tick_format` writes the label text from that value. `x_tick_count` labels only that many x values, spread evenly from the first to the last, instead of every `tick_margin`-th; with `point_count` set they spread over every point the axis is laid out for, so they stay put as the data grows. `grid_columns` adds vertical grid lines, `grid_dashed(false)` draws the grid solid, `reference_line` marks a value with a dashed line across the plot, drawn darker than the grid, and `y_padding` sets the space kept above the highest value and below the lowest, 10px and 0 by default. ### BarChart A bar chart uses rectangular bars to show comparisons among categories. Bars can be oriented vertically or horizontally via the `alignment` option. #### Bar Chart Gradient Fills For gradient fills aligned to the bar's orientation, use `fill_gradient`. The closure receives the datum, the chart's full data range, and a `chart_to_bar` helper that maps a chart-value coordinate to a bar-local gradient position (`0.0` is the bar's base, `1.0` is its tip). The gradient angle is derived from the bar's `BarAlignment` so stop-0 sits at the base and stop-1 at the tip. `fill` and `fill_gradient` are mutually exclusive — setting one clears the other. #### Bar Chart Alignment `BarAlignment` controls the bar orientation and the side where the baseline sits. Import it from `gpui_kit::component::plot::shape`. #### Bar Chart Corner Radii Round the bar rectangles. Pass any value convertible into `Corners` — use a single `px(..)` for uniform rounding, or construct `Corners` manually to round only specific corners (e.g. just the tip end of each bar). #### Bar Chart Negative Values Bars grow from zero rather than from the edge of the plot, so negative values extend to the opposite side of the zero line. The band-axis line follows zero, and each category label moves to whichever side its own bar leaves empty. No configuration is needed — a data set containing negative values renders this way. #### Bar Chart Value Axis Show tick labels for the value scale with `value_axis`, and set how many ticks it carries with `value_tick_count`. The ticks are evenly spaced from the baseline to the far edge with both ends included, and drive both the grid lines and the tick labels, so the two always agree. `tick_margin`, by contrast, is a stride over the band-axis categories: `tick_margin(2)` keeps every second category label. `value_axis_label_placement(AxisLabelPlacement::Inside)` draws the labels over the plot beside their grid lines, so the bars keep the room a gutter would take, and `value_tick_format` writes their text. `band_count` lays the band axis out for more bands than there is data, so a short series keeps each bar's width and fills only the leading bands. `band_tick_count` labels only that many bands, spread evenly from the first to the last (over every band when `band_count` is set), and `grid_dashed(false)` draws the grid solid. #### Bar Chart Labels and Spacing `label_color` colors each bar's `label` text, so a count can take its bar's color instead of the foreground. `padding_inner` and `padding_outer` set the gap between bars and before the first and after the last, as shares of a band; they default to 0.4 and 0.2. `min_length` draws every bar at least that many pixels long, so an empty bucket still shows a stub on the baseline. A stub grows the way its bar's value would: away from the zero line, to the negative side for a negative value and to the positive side for zero. Vertical bars with a `label` keep a line of text clear above the tallest bar, so its label stays inside the chart. ### AreaChart An area chart displays quantitative data visually, similar to a line chart but with the area below the line filled. #### Pinned Axis and Unfinished Series By default the y axis fits the data from zero. `y_domain` pins it to a range instead, so a price or a balance that never nears zero is not pressed flat against the top. `point_count` lays the x axis out for more points than the data has, so a series still in progress, such as today's intraday prices, fills only the leading part. `LineChart` takes both as well. A pinned range keeps the 10px of headroom the default leaves above the highest value, and the series are clipped to the plot, so a value outside the range stops at its edge. Nothing is drawn when `min` equals `max`, so widen a flat series before passing it in. A natural curve can swing past its highest and lowest points; prefer `linear` when the range is fitted tightly to the data. The i-th item of data sits on the i-th point, so the data has to be contiguous from the first point: a missing item shifts every later one a point to the left. ### PieChart A pie chart displays data as slices of a circular chart, ideal for showing proportions. ### RadarChart A radar chart displays multivariate data as closed polygons around a center, ideal for comparing multiple series across several dimensions. #### Element Labels `label` accepts either a string or a custom element. Return `element.into_any_element()` to render anything you like around the outer ring — an icon, several lines, per-dimension colors. Each label is measured at its natural size and pushed radially outward from its dimension, so even a tall one clears the outer ring. Element labels style themselves, so `.label_color()` does not apply to them, and they supply no tooltip title (a string label does). The ring is not shrunk to make room: the default outer radius is 40% of the chart's height, so a label much taller than a line of text needs a smaller `.outer_radius()` to keep it inside the chart's bounds. ### CandlestickChart A candlestick chart displays financial data using OHLC (Open, High, Low, Close) values, perfect for visualizing stock prices and market trends. #### Candlestick Chart Colors A candle that closed above its open is drawn in the theme's `chart.bullish` color and one that closed at or below it in `chart.bearish`. Markets that read a rise as red swap them: ### SankeyChart A sankey diagram visualizes flows between nodes, ideal for financial statements, energy flows, and traffic analysis. The layout algorithm mirrors [d3-sankey](https://github.com/d3/d3-sankey). #### Basic Sankey Chart The value label is drawn above the name label. Its closure receives the node's computed throughput (the larger of incoming and outgoing flow). #### Sankey Chart Styling Link ribbons are filled with a horizontal gradient from the source node color to the target node color. #### Custom Labels For full control over the label lines, use `labels` — one `SankeyLabel` per line, top to bottom, each with its own color and font size. It takes precedence over `node_label`/`value_label` when set. For example, a financial-statement label with a year-over-year change line: Line color defaults to the theme foreground and font size to 10; the chart keeps handling placement, alignment and margin reservation. A first/last-column label wider than its reserved margin is truncated with a trailing ellipsis rather than drawn outside the plot, so break or shorten long labels yourself if you want the full text on multiple lines. #### Compressing Large Value Ranges Node heights are linear in flow value by default, so a large value range (e.g. 200:1) leaves the small flows nearly invisible and the dominant flow oversized. Set `value_scale(SankeyValueScale::Sqrt)` to compress the range — the component sizes nodes by the square root of the value, so small flows stay visible without pre-transforming the data, and labels still receive the raw values: Every node stays exactly filled by its ribbons under either scale, so children always match their parent's height. ## Hover and Tooltips Every chart hit-tests the cursor, shows a tooltip for the datum under it, and emphasizes that datum the way the chart's kind calls for. Nothing opts in: ```rust LineChart::new(data) .x(|d| d.date.clone()) .y(|d| d.value) .name("Desktop") // The series name in the tooltip row ``` | Chart | On hover | | --- | --- | | `LineChart`, `AreaChart` | A crosshair and a dot per series glide along the line to the hovered point; the dot grows a halo. | | `BarChart` | A highlight band the width of a bar slides to the hovered bar, and the other bars fade behind it. | | `PieChart` | The hovered slice lifts out of the ring and the others fade; the tooltip shows the value and its share, via `tooltip_value` if one is set. | | `RadarChart` | A dot per series glides along the polygon to the hovered spoke. | | `CandlestickChart` | A highlight band slides to the hovered candle; the tooltip lists open, high, low and close. | | `SankeyChart` | The links of the hovered node keep their color while the rest fade; the tooltip shows the node's name and throughput, via `tooltip_name` / `tooltip_value` if set. | The tooltip box follows the cursor, flipping toward the center of the plot near each edge. `AreaChart` and `RadarChart` take one `.name()` per series, called after the matching `.y()` / `.value()`. A tooltip row reads as a swatch, a name and a value. `PieChart::tooltip_name` and `SankeyChart::tooltip_name` set that name from the datum under the cursor — the slice's name, the node's name — which for a chart showing one number per datum is what the row wants. Unset, a pie falls back to `name`, the single name the whole series carries, and a sankey's row carries no name at all: a swatch and a number with a gap between them. `name` cannot stand in for it. It says what the numbers measure, the same for every slice, so it can never say which slice the tooltip is about. Titling the tooltip from `label` cannot either, since `label` also draws the leader lines around the ring, and a sankey's `node_label` likewise writes beside the node. `PieChart::tooltip_value` replaces the text of its row, which the default writes as the raw value and its share. Set it wherever the raw number is not what a reader should see — a value that is already a ratio reads as `0.35 (35.0%)` otherwise, and a chart drawn from adjusted values, such as a floor that keeps a hairline slice visible, would report the adjustment as though it were the datum: ```rust PieChart::new(holdings) .value(|d| d.ratio.max(MIN_VISIBLE)) // Drawn with a floor .tooltip_name(|d| d.name.clone()) // Named without leader lines .tooltip_value(|d, _, _| pct(d.ratio)) // Read as it truly is ``` ### Tooltip Content `LineChart`, `AreaChart`, `BarChart`, `RadarChart` and `CandlestickChart` title their tooltip with the hovered x, band or dimension value and write each row's value as the raw number. `tooltip_title` and `tooltip_value` replace that text from the datum under the cursor, and `tooltip_value_color` colors each row's value, such as green or red by its sign. Both closures receive the datum and the value the row reads; on `AreaChart`, `RadarChart` and `CandlestickChart`, which show several rows, they also receive the row's index between the two — the series in the order they were added, or open, high, low and close for a candlestick: ```rust BarChart::new(flows) .band(|d| d.month.clone()) .value(|d| d.net) .tooltip_title(|d| format!("{} 2025", d.month).into()) .tooltip_value(|_, value| format!("${value:.2}").into()) .tooltip_value_color(move |_, value| if value >= 0. { gain } else { loss }) ``` For a layout the title and rows cannot express, such as a table, `tooltip_content` draws the box's content from the datum. The chart's hover marks — crosshair, dots, highlight band — and where the box sits stay the chart's, and the three text options no longer apply: ```rust AreaChart::new(data) .x(|d| d.month.clone()) .y(|d| d.last_year) .y(|d| d.revenue) .tooltip_content(|d, _, _| { v_flex() .child(d.month.clone()) .child(format!("2025: {}", d.revenue)) .child(format!("2024: {}", d.last_year)) }) ``` ### Identity All of it is keyed on an `ElementId`, which a chart takes from the source location it was constructed at — unique for a chart written out once, which is nearly every chart. Where one construction site renders several charts as siblings, name them apart, or they share one hover state and one cache: ```rust shares.iter().enumerate().map(|(i, share)| PieChart::new(share.clone()).id(("share", i))) ``` A `GlobalElementId` is the whole id stack, so siblings that already carry an id of their own — rows drawn by `List` or `uniform_list`, say — separate the charts beneath them without help. ### Turning it off `interactive(false)` takes the whole layer away, hitbox included, the way Highcharts' `enableMouseTracking` or ECharts' `silent` does. Reach for it in two places: a chart that only decorates, and a chart something else is drawn over: ```rust AreaChart::new(placeholder).interactive(false) // A skeleton, a thumbnail AreaChart::new(range).interactive(false) // A backdrop under drag handles ``` The second matters because a plain hitbox does not block the one behind it: an element painted over an interactive chart is hovered *and so is the chart*, so the crosshair keeps tracking under it. The chart has to stand down. A chart that is off keeps its id, so it still draws in and keeps its cache. ### Motion The emphasis is animated with the styled layer's motion tokens (`cx.theme().motion_tokens()`), which the theme projects onto gpui-base as its `PlotMotion`: pointers — crosshair, band, dots — follow the hovered datum on a fast spring, a pie slice lifts on the control spring, and the whole overlay fades in when the cursor lands on a datum and out after it leaves. The motion honors the operating system's reduced-motion preference, under which every value adopts its target at once. ### Appear The data draws in the first time a chart is painted, over 1000 ms on `easeOutQuart`, Chart.js' default curve: lines, areas, candlesticks and a sankey diagram are revealed from the left the way ECharts, Highcharts and Recharts draw them, bars grow out of the zero line together, a pie sweeps clockwise from its first slice, and a radar grows out of its center. Axes, grid lines and tick labels are there from the first frame, and the tooltip waits until the data is whole. The appear runs once per id. New data paints in place, so a chart fed live quotes does not draw in again on every tick. To replay it when the chart starts showing something else, such as another symbol or period, hand it a key: ```rust LineChart::new(candles).appear_key((&symbol, period)) ``` A chart that stops being painted forgets its appear, so one in a virtual list draws in again every time it scrolls back into view. Wrap the list in a `PlotAppearScope` to have each chart draw in once and show whole when it returns; name the scope after what the list shows, such as a conversation or a watchlist, so moving to another one, or closing the view, draws its charts in afresh: ```rust PlotAppearScope::new(("rows", list_id), list(state, render_row).flex_1()) ``` Where a chart should never draw in, turn the appear off: ```rust LineChart::new(intraday).interactive(false).appear(false) ``` Reduced motion skips the appear. ### Caching A chart also keeps its heavy geometry across frames, since it repaints on every frame it is on screen: line and area strokes and pie slices stay tessellated while their projected points are unchanged, and a sankey diagram keeps its placement while its data, settings and size are unchanged. This cache hangs off the same id, so charts sharing one share the cache and thrash it — another reason to name siblings apart. A pie or radar being drawn in is a new shape on every frame, so it tessellates afresh until the appear ends. ### Custom Plots A custom [`Plot`] opts in by hand — `Plot::id` defaults to `None` there. The trait, `PlotElement` and hover tracking come from `gpui_kit::base::plot`, so a plot written against that path works here unchanged. Return an id from `Plot::id`, resolve the datum under the cursor in `Plot::tooltip_state`, and build the overlay in `Plot::tooltip`. To draw in, keep the `PlotAppear` that `Plot::appear` hands over on every frame and paint with its progress. The `Tooltip` returned there animates the hover on its own, the same way the built-in charts do: the whole overlay fades with the hover, the crosshair and dots glide to each hovered datum on the pointer spring, adopting it on the frame the cursor lands, and a dot's `halo` grows as the hover fades in. A crosshair glides along the axis it marks only, so a line that also follows the cursor keeps up with it. Pass the data point itself; the tooltip does the rest: ```rust fn tooltip(&self, state: &TooltipState, cursor: Point, bounds: Bounds, _: &mut Window, cx: &mut App) -> Option { Some( Tooltip::new(cursor, bounds.size) .cross_line(CrossLine::new(state.cross_line).band(px(24.))) .title("Title") .row(cx.theme().chart_1, "Series", "42") .into_any_element(), ) } ``` A row's value takes a color with `value_color`, which colors the row added last, such as a change by its sign; call it right after that row. `plain_row` adds a row without a swatch, for a figure no series on the plot draws, such as a total or a ratio; beside series rows its label lines up with theirs: ```rust Tooltip::new(cursor, bounds.size) .title("Apr 5") .row(desktop, "Desktop", "373") .row(mobile, "Mobile", "187") .plain_row("Total", "560") .plain_row("Change", "+12%") .value_color(gain) ``` To emphasize the plot's own graphics as well — fade the bars around the hovered one, lift a slice — implement `Plot::hover`, which runs each frame before `tooltip` and `paint` with the hovered [`PlotHover`]. It carries the `TooltipState` and lingers after the cursor leaves while `hover.progress()` eases back to zero, so sample the motion there and keep the result on `self`. `hover.glide` follows a position on the same spring the tooltip uses; hand the result to the crosshair and turn the tooltip's own glide off with `Tooltip::glide(false)`, so it springs once: ```rust fn hover(&mut self, hover: Option<&PlotHover>, window: &mut Window, cx: &mut App) { self.band_center = hover.map(|hover| hover.glide(("my-plot", "band"), hover.state().cross_line.x, window, cx)); } ``` ## Data Structures ### Example Data Types ```rust // Time series data #[derive(Clone)] struct DailyDevice { pub date: String, pub desktop: f64, pub mobile: f64, } // Category data with styling #[derive(Clone)] struct MonthlyDevice { pub month: String, pub desktop: f64, pub color_alpha: f32, } impl MonthlyDevice { pub fn color(&self, base_color: Hsla) -> Hsla { base_color.alpha(self.color_alpha) } } // Financial data #[derive(Clone)] struct StockPrice { pub date: String, pub open: f64, pub high: f64, pub low: f64, pub close: f64, pub volume: u64, } // Sankey flow: nodes are referenced by index (from gpui_kit::component::plot::shape) pub struct SankeyLink { pub source: usize, pub target: usize, pub value: f64, } ``` ## Chart Configuration ### Container Setup ```rust fn chart_container( title: &str, chart: impl IntoElement, center: bool, cx: &mut Context, ) -> impl IntoElement { v_flex() .flex_1() .h_full() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius_lg) .p_4() .child( div() .when(center, |this| this.text_center()) .font_semibold() .child(title.to_string()), ) .child( div() .when(center, |this| this.text_center()) .text_color(cx.theme().muted_foreground) .text_sm() .child("Data period label"), ) .child(div().flex_1().py_4().child(chart)) .child( div() .when(center, |this| this.text_center()) .font_semibold() .text_sm() .child("Summary statistic"), ) .child( div() .when(center, |this| this.text_center()) .text_color(cx.theme().muted_foreground) .text_sm() .child("Additional context"), ) } ``` ### Theme Integration ```rust // Charts automatically use theme colors let chart = LineChart::new(data) .x(|d| d.date.clone()) .y(|d| d.value) .stroke(cx.theme().chart_1); // Uses theme chart colors // Available theme chart colors (`chart.1` … `chart.5` in the theme file): // cx.theme().chart_1 … cx.theme().chart_5 ``` ## Customization Options ### Color Schemes ```rust // Theme-based colors (recommended) LineChart::new(data) .x(|d| d.x.clone()) .y(|d| d.y) .stroke(cx.theme().chart_1) // Custom color palette let colors = [ cx.theme().success, cx.theme().warning, cx.theme().destructive, cx.theme().info, cx.theme().chart_1, ]; BarChart::new(data) .band(|d| d.category.clone()) .value(|d| d.value) .fill(|d, _, _, _| colors[d.category_index % colors.len()]) ``` ### Responsive Design ```rust // Container with responsive sizing div() .flex_1() .min_h(px(300.)) .max_h(px(600.)) .w_full() .child( LineChart::new(data) .x(|d| d.x.clone()) .y(|d| d.y) ) ``` ### Grid and Axis Styling Charts automatically include: - Grid lines with dashed appearance, in the theme's `chart.grid` color (a translucent `border` when a theme leaves it unset) - X-axis labels with smart positioning - Y-axis scaling starting from zero - Responsive tick spacing based on `tick_margin` ## Performance Considerations ### Large Datasets ```rust // For large datasets, consider data sampling let sampled_data: Vec<_> = data .iter() .step_by(5) // Show every 5th point .cloned() .collect(); LineChart::new(sampled_data) .x(|d| d.date.clone()) .y(|d| d.value) .tick_margin(3) // Reduce tick density ``` ### Memory Optimization ```rust // Use efficient data accessors LineChart::new(data) .x(|d| d.date.clone()) // Clone only when necessary .y(|d| d.value) // Direct field access ``` ## Integration Examples ### With State Management ```rust struct ChartComponent { data: Vec, chart_type: ChartType, time_range: TimeRange, } impl ChartComponent { fn render_chart(&self, cx: &mut Context) -> impl IntoElement { match self.chart_type { ChartType::Line => LineChart::new(self.filtered_data()) .x(|d| d.date.clone()) .y(|d| d.value) .into_any_element(), ChartType::Bar => BarChart::new(self.filtered_data()) .band(|d| d.date.clone()) .value(|d| d.value) .into_any_element(), ChartType::Area => AreaChart::new(self.filtered_data()) .x(|d| d.date.clone()) .y(|d| d.value) .into_any_element(), } } fn filtered_data(&self) -> Vec { self.data .iter() .filter(|d| self.time_range.contains(&d.date)) .cloned() .collect() } } ``` ### Real-time Updates ```rust struct LiveChart { data: Vec, max_points: usize, } impl LiveChart { fn add_data_point(&mut self, point: DataPoint) { self.data.push(point); if self.data.len() > self.max_points { self.data.remove(0); // Remove oldest point } } fn render(&self, cx: &mut Context) -> impl IntoElement { LineChart::new(self.data.clone()) .x(|d| d.timestamp.clone()) .y(|d| d.value) .linear() .dot() } } ``` [LineChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.LineChart.html [BarChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.BarChart.html [AreaChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.AreaChart.html [PieChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.PieChart.html [RadarChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.RadarChart.html [CandlestickChart]: https://docs.rs/gpui-component/latest/gpui_component/chart/struct.CandlestickChart.html ## API Reference - [LineChart] - [BarChart] - [AreaChart] - [PieChart] - [RadarChart] - [CandlestickChart] - [SankeyChart] --- # Tooltip Source: /component/tooltip A versatile tooltip component that displays helpful information when hovering over or focusing on elements. Supports text content, custom elements, keyboard shortcuts, different trigger methods, and positioning options. ## Mobile behavior On iOS and Android, tooltips managed by the GPUI Base overlay are disabled. Shared components may keep their tooltip configuration, but mobile actions still need visible or accessible labels. Direct GPUI `.tooltip()` calls, including the basic `div()` example below, bypass this overlay and are not disabled by this policy. See [Mobile](/docs/mobile) for integration guidance. ## Import ```rust use gpui_kit::component::tooltip::Tooltip; ``` ## Usage ### Hover the button ```rust Button::new("save-btn") .label("Save") .tooltip("Save the current document") ``` ### Basic Tooltip with Text ```rust // Simple text tooltip div() .child("Hover me") .id("basic-tooltip") .tooltip(|window, cx| { Tooltip::new("This is a helpful tooltip").build(window, cx) }) ``` ### Tooltip with Action/Keybinding ```rust actions!(my_actions, [SaveDocument]); Button::new("save-btn") .label("Save") .tooltip_with_action( "Save the current document", &SaveDocument, Some("MyContext") ) ``` ### Custom Element Tooltip ```rust div() .child("Hover for rich content") .id("rich-tooltip") .tooltip(|window, cx| { Tooltip::element(|_, cx| { h_flex() .gap_x_1() .child(IconName::Info) .child( div() .child("Muted Text") .text_color(cx.theme().muted_foreground) ) .child( div() .child("Danger Text") .text_color(cx.theme().danger) ) .child(IconName::ArrowUp) }) .build(window, cx) }) ``` ### Tooltip with Manual Keybinding ```rust div() .child("Custom keybinding") .id("custom-kb") .tooltip(|window, cx| { Tooltip::new("Delete item") .key_binding(Some(Kbd::new("Delete"))) .build(window, cx) }) ``` ### Toolbar with Tooltips ```rust h_flex() .gap_1() .child( Button::new("new") .icon(IconName::Plus) .tooltip_with_action("Create new file", &NewFile, Some("Editor")) ) .child( Button::new("open") .icon(IconName::FolderOpen) .tooltip_with_action("Open file", &OpenFile, Some("Editor")) ) .child( Button::new("save") .icon(IconName::Save) .tooltip_with_action("Save file", &SaveFile, Some("Editor")) ) ``` ### Status Indicators with Tooltips ```rust h_flex() .gap_2() .child( div() .size_3() .rounded_full() .bg(cx.theme().success) .tooltip(|window, cx| { Tooltip::new("Connected to server").build(window, cx) }) ) .child( div() .size_3() .rounded_full() .bg(cx.theme().warning) .tooltip(|window, cx| { Tooltip::new("Limited connectivity").build(window, cx) }) ) ``` ### Interactive Elements with Rich Tooltips ```rust v_flex() .gap_3() .child( div() .p_2() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) .child("File: document.txt") .id("file-item") .tooltip(|window, cx| { Tooltip::element(|_, cx| { v_flex() .gap_1() .child( h_flex() .gap_2() .child(IconName::File) .child("document.txt") .text_sm() .font_medium() ) .child( div() .child("Size: 2.4 KB") .text_xs() .text_color(cx.theme().muted_foreground) ) .child( div() .child("Modified: 2 hours ago") .text_xs() .text_color(cx.theme().muted_foreground) ) .child( h_flex() .gap_1() .child(Kbd::new("Enter")) .child("to open") .text_xs() .text_color(cx.theme().muted_foreground) ) }) .build(window, cx) }) ) ``` ### Form Validation with Tooltips ```rust struct FormView { email_error: Option, password_error: Option, } v_flex() .gap_4() .child( Input::new("email") .placeholder("Email address") .when_some(self.email_error.clone(), |this, error| { this.tooltip(move |window, cx| { Tooltip::element(|_, cx| { h_flex() .gap_1() .child(IconName::AlertCircle) .child(error.clone()) .text_color(cx.theme().destructive) }) .build(window, cx) }) }) ) ``` ## Advanced Usage ### Components with Built-in Tooltip Support Many components have built-in tooltip methods: ```rust // Button Button::new("btn") .label("Click me") .tooltip("This button performs an action") // Switch Switch::new("toggle") .label("Enable notifications") .tooltip("Toggle push notifications on/off") // Checkbox Checkbox::new("check") .label("Remember me") .tooltip("Keep me logged in for 30 days") // Radio Radio::new("option") .label("Option 1") .tooltip("Select this option to enable feature X") ``` ### Complex Tooltip Content ```rust div() .child("Hover for details") .id("complex-tooltip") .tooltip(|window, cx| { Tooltip::element(|_, cx| { v_flex() .gap_2() .child( h_flex() .gap_1() .child(IconName::User) .child("User Information") .text_sm() .font_semibold() ) .child( div() .child("Last login: 2 hours ago") .text_xs() .text_color(cx.theme().muted_foreground) ) .child( div() .child("Status: Active") .text_xs() .text_color(cx.theme().success) ) }) .build(window, cx) }) ``` ### Tooltip in Form Elements ```rust v_flex() .gap_4() .child( Input::new("email") .placeholder("Enter your email") .tooltip("We'll never share your email address") ) .child( Input::new("password") .input_type(InputType::Password) .placeholder("Password") .tooltip("Must be at least 8 characters with special characters") ) ``` ## Best Practices ### Content Guidelines - **Be concise**: Keep tooltip text short and to the point - **Be helpful**: Provide additional context, not redundant information - **Use proper tone**: Match your application's voice and tone - **Avoid critical info**: Don't put essential information only in tooltips ### Usage Guidelines - **Progressive disclosure**: Use tooltips for additional context, not primary information - **Consistency**: Use consistent tooltip patterns throughout your application - **Performance**: Avoid complex content in frequently triggered tooltips - **Testing**: Test tooltips with both mouse and keyboard interaction ### Examples of Good Tooltip Content ```rust // Good: Provides helpful context Button::new("delete") .icon(IconName::Trash) .tooltip("Delete this item permanently") // Good: Explains abbreviation div() .child("CPU: 45%") .tooltip("Central Processing Unit usage") // Good: Describes action with keybinding Button::new("undo") .icon(IconName::Undo) .tooltip_with_action("Undo last action", &Undo, Some("Editor")) ``` ### Examples to Avoid ```rust // Avoid: Redundant information Button::new("save") .label("Save") .tooltip("Save") // Doesn't add value // Avoid: Critical information Button::new("delete") .tooltip("This will permanently delete all your files") // Too important for tooltip only ``` ## API Reference ### Tooltip | Method | Description | | ------------------------- | -------------------------------------------- | | `new(text)` | Create a tooltip with text content | | `element(builder)` | Create a tooltip with custom element content | | `action(action, context)` | Set action to display keybinding information | | `key_binding(kbd)` | Set manual keybinding information | | `build(window, cx)` | Build and return the tooltip as AnyView | ### Built-in Tooltip Methods Components with tooltip support typically provide these methods: | Method | Description | | -------------------------------------------- | --------------------------------------- | | `tooltip(text)` | Add simple text tooltip | | `tooltip_with_action(text, action, context)` | Add tooltip with action keybinding | | `tooltip(closure)` | Add custom tooltip with builder closure | ### Tooltip Styling The tooltip automatically applies theme-appropriate styling: - Background: `theme.popover` - Text color: `theme.popover_foreground` - Border: `theme.border` - Shadow: Medium drop shadow - Border radius: 6px - Font: System UI font You can apply additional styling using the `Styled` trait: ```rust Tooltip::new("Custom styled tooltip") .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .build(window, cx) ``` --- # Components Source: /component ### Basic Components - [Accordion](accordion) - Collapsible content panels - [Alert](alert) - Alert messages with different variants - [Attachment](attachment) - File and media attachment surfaces - [Avatar](avatar) - User avatars with fallback text - [Badge](badge) - Count badges and indicators - [Bubble](bubble) - Chat message surface with alignment and reactions - [Button](button) - Interactive buttons with multiple variants - [Checkbox](checkbox) - Binary selection control - [Collapsible](collapsible) - Expandable/collapsible content - [DropdownButton](dropdown_button) - Button with dropdown menu - [Icon](icon) - Icon display component - [Image](image) - Image display with fallbacks - [Kbd](kbd) - Keyboard shortcut display - [Label](label) - Text labels for form elements - [Marker](marker) - Conversation status and separator marker - [Message](message) - Composable chat message structure - [MessageScroller](message-scroller) - Tail-following virtualized message list - [Pagination](pagination) - Page navigation controls - [Progress](progress) - Progress bars - [Questionnaire](questionnaire) - Composable multi-step questions and answers - [Radio](radio) - Single selection from multiple options - [Rating](rating) - Interactive star rating component - [Skeleton](skeleton) - Loading placeholders - [Slider](slider) - Value selection from a range - [Spinner](spinner) - Loading and status spinners - [Stepper](stepper) - Step-by-step progress indicator - [Switch](switch) - Toggle on/off control - [Tag](tag) - Labels and categories - [TextView](text-view) - Markdown and HTML text rendering - [Toggle](toggle) - Toggle button states - [Tooltip](tooltip) - Helpful hints on hover ### Form Components - [Input](input) - An input field or a component that looks like an input field. - [Textarea](textarea) - Multi-line text with fixed rows or auto-grow. - [Editor](editor) - Source-code editing with highlighting, gutter, and folding. - [Select](select) - A list of options for the user to pick. - [Combobox](combobox) - Searchable single-select or multi-select dropdown. - [NumberInput](number-input) - Numeric input with increment/decrement - [DatePicker](date-picker) - Date selection with calendar - [TimeField](time-field) - Segmented time-of-day input - [OtpInput](otp-input) - One-time password input - [ColorPicker](color-picker) - Color selection interface - [Form](form) - Form container and layout ### Layout Components - [DescriptionList](description-list) - Key-value pair display - [GroupBox](group-box) - Grouped content with borders - [Root](root) - Window-level provider for themes, dialogs, and notifications - [Theme](theme) - Customize colors, typography, and light/dark appearance - [Dialog](dialog) - Dialog and modal windows - [Notification](notification) - Toast notifications - [Popover](popover) - Floating content display - [Resizable](resizable) - Resizable panels and containers - [Scrollable](scrollable) - Scrollable containers - [Sheet](sheet) - Slide-in panel from edges - [Sidebar](sidebar) - Navigation sidebar - [StatusBar](status-bar) - Bottom status bar with left/center/right regions - [Toolbar](toolbar) - Top toolbar with left/right regions and sizes ### Advanced Components - [Calendar](calendar) - Calendar display and navigation - [Carousel](carousel) - Browse through a set of related items - [Command](command) - Command palette for search and quick actions - [Chart](chart) - Data visualization charts (Line, Bar, Area, Pie, Candlestick) - [List](list) - List display with items - [Menu](menu) - Menu and context menu and dropdown menu. - [Settings](settings) - Settings UI - [DataTable](data-table) - High-performance data tables - [Dock](dock) - Production-ready dock layouts with tabs, splits, and persistent state - [Tabs](tabs) - Tabbed interface - [Tree](tree) - Hierarchical tree data display - [VirtualList](virtual-list) - Virtualized list for large datasets --- # Message Source: /component/message `Message` is the row-level composition primitive for a conversation. It owns the horizontal alignment and the vertical stack that contains optional sender identity, metadata, content, and footer slots. It does not own a sender model, timestamp formatting, delivery state, reaction state, or message actions. Applications provide those values and compose existing components inside the slots. This keeps the message layout reusable across direct messages, group chat, assistant responses, system notices, and generated content. ## Import ```rust use gpui_kit::{ParentElement as _, StyleRefinement, Styled as _}; use gpui_kit::component::{ ActiveTheme as _, Colorize as _, Sizable as _, attachment::{Attachment, AttachmentContent, AttachmentTitle}, avatar::Avatar, bubble::{Bubble, BubbleVariant}, button::{Button, ButtonVariants as _}, message::{ Message, MessageAlignment, MessageAvatar, MessageContent, MessageFooter, MessageGroup, MessageHeader, }, }; ``` ## Anatomy and basic usage All named slots are optional, so a minimal message can contain only a body: ```rust Message::new().content( MessageContent::new().bubble(Bubble::new().child("Can you review this?")), ) ``` A complete message commonly combines sender identity, metadata, a bubble, and a delivery footer: ```rust Message::new() .avatar_slot( MessageAvatar::new() .child(Avatar::new().name("Alice").size_8()), ) .header( MessageHeader::new() .child("Alice") .child("10:24 AM"), ) .content( MessageContent::new().bubble( Bubble::new() .with_variant(BubbleVariant::Secondary) .child("Can you review this draft?"), ), ) .footer(MessageFooter::new().child("Read")) ``` The default state is: | Property | Default | Meaning | | --- | --- | --- | | Alignment | `MessageAlignment::Start` | Place the message at the leading edge. | | Avatar/header/content/footer | absent | Add only the slots needed by the product. | | Outer layout | full width, `min_w_0()`, `gap_2()` | Keeps rows usable in a virtual list. | | Inner stack gap | `rems(0.625)` | Separates metadata, body, and footer. | | Header/footer inset | enabled, `px_3()` | Aligns metadata with a regular bubble surface. | | Typography | inherited | The header and footer set their own; content typography belongs to the bubble or the caller. | | Identity and role | none, presentational | `id(...)` gives the row a stable identity; `role(...)` announces it. | | Avatar baseline | `min_w_8()`, circular muted surface | Gives sender identity a stable column. | `Message` applies its alignment to the complete row and to the named content stack. It reverses the outer row for `End`, so the avatar and message stack remain a single aligned unit. ## Alignment Use `Start` for incoming content and `End` for outgoing content: ```rust Message::new() .alignment(MessageAlignment::Start) .avatar(Avatar::new().name("Alice").size_8()) .header(MessageHeader::new().child("Alice").child("10:24 AM")) .content(MessageContent::new().bubble( Bubble::new() .with_variant(BubbleVariant::Secondary) .child("Incoming message"), )); Message::new() .alignment(MessageAlignment::End) .avatar(Avatar::new().name("You").size_8()) .header(MessageHeader::new().child("You").child("10:25 AM")) .content(MessageContent::new().bubble(Bubble::new().child("Outgoing message"))) .footer(MessageFooter::new().child("Delivered")) ``` `MessageAlignment` is also accepted by `Bubble`. When the body is a typed `MessageContent::bubble(...)`, the message propagates its alignment to that bubble's surface. Leave the bubble's own alignment unset in this composition so the row has one clear owner of placement. ## Avatar, header, content, and footer ### Avatar `.avatar(...)` wraps any element in `MessageAvatar`. Use `.avatar_slot(...)` when the slot itself needs styling or multiple children: ```rust Message::new() .avatar_slot( MessageAvatar::new() .bg(cx.theme().transparent) .child(Avatar::new().name("System").size_8()), ) .content(MessageContent::new().child("A system update")) ``` The avatar reserves the shared `size-8` baseline and always sits flush with the bottom edge of the message content; the footer renders below the avatar row, indented to the content column. The message does not require an avatar; omit it for assistant messages, compact group chat, or system rows where identity is already present elsewhere. ### Header `MessageHeader` is an arbitrary horizontal metadata row. It defaults to an extra-small, medium-weight, muted style with a `px_3()` inset: ```rust MessageHeader::new() .child("Alice") .child("·") .child("10:24 AM") ``` The header does not format dates or infer sender names. Use application-owned formatting and compose a `Tooltip` around timestamps when the full date is useful. ### Content `MessageContent` is a full-width, minimum-width-safe vertical stack. It accepts arbitrary elements and has a typed `.bubble(...)` convenience that records whether a ghost bubble is present: ```rust MessageContent::new() .bubble(Bubble::new().child("First paragraph")) .bubble(Bubble::new().child("Second paragraph")) ``` Use `.child(...)` for attachments, code blocks, images, or custom rich content: ```rust MessageContent::new() .bubble(Bubble::new().child("Here is the file:")) .child( Attachment::new().content( AttachmentContent::new() .title(AttachmentTitle::new("quarterly-report.pdf")), ), ) ``` Typed bubbles are useful when the surrounding header and footer should respond to the `Ghost` variant. Arbitrary `.child(...)` values are still fully composable, but their concrete type is erased and they do not set that ghost-surface metadata. ### Footer `MessageFooter` is another arbitrary horizontal metadata row. Use it for delivery state, reactions, or actions composed from existing controls: ```rust MessageFooter::new() .child("Delivered") .child(Button::new("reply").ghost().xsmall().label("Reply")) .child(Button::new("copy").ghost().xsmall().label("Copy")) ``` Footer uses the same extra-small muted default and `px_3()` inset as the header. The footer does not own a delivery-state enum or action semantics. ## Rich content and actions Compose the existing component that owns each behavior: ```rust Message::new() .content( MessageContent::new() .bubble(Bubble::new().child("The export is ready.")) .child( Attachment::new() .content(AttachmentContent::new().title(AttachmentTitle::new("export.zip"))), ), ) .footer( MessageFooter::new() .child(Button::new("download-export").label("Download")) .child(Button::new("share-export").ghost().label("Share")), ) ``` Use `Button` for commands, `Link` for URLs, `Attachment` for files, and `Bubble` for conversational surfaces. This keeps disabled, loading, focus, keyboard, and accessible-name behavior on the control that owns it. A message does not become clickable merely because it contains a button. Long or multiline content remains the responsibility of the child element. Keep custom children `min_w_0()` when they contain long text or horizontal layouts; `Message` already applies `w_full()` and `min_w_0()` to its own row and stack. ## Grouping `MessageGroup` is a styleable vertical stack for consecutive messages. It does not decide which sender owns a message or automatically remove metadata: ```rust MessageGroup::new() .child( Message::new() .avatar(Avatar::new().name("Alice").size_8()) .header(MessageHeader::new().child("Alice")) .content(MessageContent::new().bubble( Bubble::new() .with_variant(BubbleVariant::Secondary) .child("The first message."), )), ) .child( Message::new() .avatar_slot(MessageAvatar::new().bg(cx.theme().transparent)) .content(MessageContent::new().bubble( Bubble::new() .with_variant(BubbleVariant::Secondary) .child("The follow-up keeps the same sender context."), )), ) ``` Use `BubbleGroup` when only the bubbles are grouped and there is no message header, avatar, or footer. Use `MessageGroup` when each item is a full row. ## Ghost surfaces and content insets The typed `MessageContent::bubble(...)` builder records a ghost bubble. In that case, `Message` removes the default header and footer insets so metadata lines up with the unframed content: ```rust Message::new() .header(MessageHeader::new().child("System").child("Just now")) .content(MessageContent::new().bubble( Bubble::new() .with_variant(BubbleVariant::Ghost) .child("The conversation has been archived."), )) .footer(MessageFooter::new().child("No further action required")) ``` Override this behavior explicitly on either named metadata slot: ```rust MessageHeader::new() .content_inset(true) .child("Keep the regular header inset"); MessageFooter::new() .content_inset(false) .child("Align the footer with a custom surface") ``` `content_inset(...)` takes precedence over inherited ghost behavior. A typed ghost bubble is required for automatic inheritance; an arbitrary child that happens to look like a ghost surface cannot be inspected by `Message`. The inner slot stack can also be refined independently: ```rust Message::new() .with_stack_style(StyleRefinement::default().gap_3()) .content(MessageContent::new().child("A wider message rhythm")) ``` ## Custom styling and theme tokens `Message`, `MessageGroup`, `MessageAvatar`, `MessageHeader`, `MessageContent`, and `MessageFooter` implement `Styled`. Style the part that owns the visual decision: ```rust Message::new() .p_3() .rounded(cx.theme().radius_lg) .bg(cx.theme().muted.opacity(0.35)) .header(MessageHeader::new().px_0().child("System")) .content(MessageContent::new().child("Archived")) .footer(MessageFooter::new().px_0().child("Just now")) ``` Use `with_stack_style(...)` for the vertical stack, slot refinements for header/content/footer typography and spacing, and the child component's own API for bubble, attachment, or button surfaces. `Message` itself sets no text size or line height, so Markdown or other rich content placed in a `Ghost` bubble keeps the typography of its renderer. Radius, spacing, typography, and colors should come from the active semantic theme or shared scale. Avoid raw colors at message call sites so the same composition works in light and dark themes. ## Accessibility and state guidance - Keep sender identity and message content in readable text. An avatar alone should not be the only indication of who sent a message. - A message is presentational by default. Give transcript rows `.id(...)` and `.role(Role::ListItem)` so assistive technology can move between them; the role needs the stable identity an id provides. - Put commands in semantic `Button` or `Link` controls. For the current `Button` API, use a visible `.label(...)` when a footer action needs an accessible name; a tooltip is supplemental. - Delivery, failure, streaming, and unread states belong in text or semantic controls. Do not communicate them with alignment, color, or opacity alone. - Preserve the header/footer inset when it is the visual relationship that aligns metadata with the surface. If a custom surface removes it, verify the reading order and keyboard order still match the visual order. - Keep multiline content readable at the application's minimum window width; use `min_w_0()` on nested horizontal content and avoid hover-only actions. - Motion for generated content belongs to `ShimmerText` or another motion-aware component. Reduced-motion behavior should leave the message text present and understandable. ## Component boundaries The GPUI component intentionally does not add provider or domain layers: - `Message` owns row alignment and slot layout. - The application owns sender records, timestamps, delivery state, reactions, permissions, message IDs, and persistence. - `Bubble`, `Attachment`, `Button`, `Link`, and `Marker` own their own visual or behavioral primitives and are composed through message slots. - `MessageGroup` only supplies a vertical stack. It does not infer sender changes or collapse headers. If a product needs a specific “assistant message” or “group chat message” with fixed metadata policy, wrap `Message` in an application component. Keep that domain policy out of the general-purpose primitive. ## API reference ### `Message` | Method | Default | Purpose | | --- | --- | --- | | `new()` | `Start`, no slots | Create a message row. | | `alignment(MessageAlignment)` | `Start` | Set leading or trailing alignment. | | `id(ElementId)` | none | Give the row a stable identity for element state and the accessibility tree. | | `role(Role)` | presentational | Announce the row to assistive technology, e.g. `Role::ListItem`; requires `id(...)`. | | `with_stack_style(StyleRefinement)` | component stack defaults | Refine the inner vertical stack. | | `avatar(element)` | none | Wrap an element in `MessageAvatar`. | | `avatar_slot(MessageAvatar)` | none | Set a fully configured avatar slot. | | `header(MessageHeader)` | none | Set sender and metadata content. | | `content(MessageContent)` | none | Set the message body. | | `footer(MessageFooter)` | none | Set delivery, reactions, or actions. | `Message` also implements `Styled` for the outer row. ### `MessageGroup` | Method | Default | Purpose | | --- | --- | --- | | `new()` | empty vertical stack | Create a message group. | | `.child(element)` | — | Add complete messages. | | `Styled` methods | `gap_2()` | Refine group spacing and layout. | ### `MessageAvatar` | Method | Default | Purpose | | --- | --- | --- | | `new()` | empty circular `size_8` baseline | Create an identity slot. | | `.child(element)` | — | Add Avatar or another identity element. | | `Styled` methods | muted surface and full radius | Refine size, background, and alignment. | ### `MessageHeader` and `MessageFooter` | Method | Default | Purpose | | --- | --- | --- | | `new()` | empty extra-small metadata row | Create the slot. | | `content_inset(bool)` | inherited or `true` | Keep or remove the default `px_3()` inset. | | `.child(element)` | — | Add text, metadata, reactions, or controls. | | `Styled` methods | muted, medium-weight, `text_xs()` | Refine the slot. | ### `MessageContent` | Method | Default | Purpose | | --- | --- | --- | | `new()` | empty full-width vertical stack | Create the body slot. | | `bubble(Bubble)` | — | Add a typed bubble and propagate ghost metadata. | | `.child(element)` | — | Add arbitrary rich content. | | `Styled` methods | `min_w_0()`, `gap(rems(0.625))` | Refine body layout. | ### Related types - [`MessageAlignment`] — `Start` or `End`. - [`Bubble`] — conversational surface content. - [`Attachment`] — files and media. - [`MessageScroller`] — virtualized conversation rows and tail following. [Message]: https://docs.rs/gpui-component/latest/gpui_component/message/struct.Message.html [MessageGroup]: https://docs.rs/gpui-component/latest/gpui_component/message/struct.MessageGroup.html [MessageAvatar]: https://docs.rs/gpui-component/latest/gpui_component/message/struct.MessageAvatar.html [MessageHeader]: https://docs.rs/gpui-component/latest/gpui_component/message/struct.MessageHeader.html [MessageContent]: https://docs.rs/gpui-component/latest/gpui_component/message/struct.MessageContent.html [MessageFooter]: https://docs.rs/gpui-component/latest/gpui_component/message/struct.MessageFooter.html [MessageAlignment]: https://docs.rs/gpui-component/latest/gpui_component/message/enum.MessageAlignment.html [Bubble]: https://docs.rs/gpui-component/latest/gpui_component/bubble/struct.Bubble.html [Attachment]: https://docs.rs/gpui-component/latest/gpui_component/attachment/struct.Attachment.html [MessageScroller]: https://docs.rs/gpui-component/latest/gpui_component/message_scroller/struct.MessageScroller.html --- # Menu Source: /component/menu # PopupMenu The Menu component provides both context menus (right-click menus) and popup menus with comprehensive features including icons, keyboard shortcuts, submenus, separators, checkable items, and custom elements. Built with accessibility and keyboard navigation in mind. ## Import ```rust use gpui_kit::component::{ menu::{PopupMenu, PopupMenuItem, ContextMenuExt, DropdownMenu}, Button }; use gpui_kit::{actions, Action}; ``` ## Usage ### ContextMenu Context menus appear when right-clicking on an element: ```rust use gpui_kit::component::menu::ContextMenuExt; div() .id("my-element") .child("Right click me") .context_menu(|menu, window, cx| { menu.menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) .separator() .menu("Delete", Box::new(Delete)) }) ``` ### DropdownMenu Dropdown menus are triggered by buttons or other interactive elements: ```rust use gpui_kit::component::popup_menu::{PopupMenuExt as _, PopupMenuItem}; let view = cx.entity(); Button::new("menu-btn") .label("Open Menu") .dropdown_menu(|menu, window, cx| { menu.menu("New File", Box::new(NewFile)) .menu("Open File", Box::new(OpenFile)) .link("Documentation", "https://gpui-kit.com/") .separator() .item(PopupMenuItem::new("Custom Action") .on_click(window.listener_for(&view, |this, _, window, cx| { // Custom action logic here this. }) ) .separator() .menu("Exit", Box::new(Exit)) }) ``` As you see, the each menu item is associated with an [Action], we choice this design to better integrate with GPUI's action and key binding system, allowing menu items to automatically display keyboard shortcuts when applicable. So, the [Action] is the recommended way to define menu item behaviors. However, if you prefer not to use [Action]s, you can create custom menu items using the `item` method along with [PopupMenuItem]. There have a `on_click` callback to handle the click event directly. ### Anchor Position Control where the dropdown menu appears relative to the trigger: ```rust use gpui_kit::Anchor; Button::new("menu-btn") .label("Options") .dropdown_menu_with_anchor(Anchor::TopRight, |menu, window, cx| { menu.menu("Option 1", Box::new(Action1)) .menu("Option 2", Box::new(Action2)) }) ``` ### Icons Add icons to menu items for better visual clarity: ```rust use gpui_kit::component::IconName; menu.menu_with_icon("Search", IconName::Search, Box::new(Search)) .menu_with_icon("Settings", IconName::Settings, Box::new(OpenSettings)) .separator() .menu_with_icon("Help", IconName::Help, Box::new(ShowHelp)) ``` ### Disabled State Create disabled menu items that cannot be activated: ```rust menu.menu("Available Action", Box::new(Action1)) .menu_with_disabled("Disabled Action", Box::new(Action2), true) .menu_with_icon_and_disabled( "Unavailable", IconName::Lock, Box::new(Action3), true ) ``` ### Check state Create menu items that show a check state: ```rust let is_enabled = true; menu.menu_with_check("Enable Feature", is_enabled, Box::new(ToggleFeature)) .menu_with_check("Show Sidebar", sidebar_visible, Box::new(ToggleSidebar)) ``` By default, the check icon will be shown on the left side of the menu item, if this menu item has an icon, the check icon will replace the icon on the left side. There also have a `check_side` option for you to config the check icon to be shown on the right side: ```rust menu.check_size(Side::Right) .menu_with_check("Enable Feature", is_enabled, Box::new(ToggleFeature)) ``` ### Separators Use separators to group related menu items: ```rust menu.menu("New", Box::new(NewFile)) .menu("Open", Box::new(OpenFile)) .separator() // Groups file operations .menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) .separator() // Groups edit operations .menu("Exit", Box::new(Exit)) ``` ### Labels Add non-interactive labels to organize menu sections: ```rust menu.label("File Operations") .menu("New", Box::new(NewFile)) .menu("Open", Box::new(OpenFile)) .separator() .label("Edit Operations") .menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) ``` ### Link MenuItem Create menu items that open external links: ```rust menu.link("Documentation", "https://docs.example.com") .link_with_icon( "GitHub", IconName::GitHub, "https://github.com/example/repo" ) .separator() .external_link_icon(false) // Hide external link icons .link("Support", "https://support.example.com") ``` ### Custom Element Create custom menu items with complex content: ```rust use gpui_kit::component::{h_flex, v_flex}; menu.menu_element(Box::new(CustomAction), |window, cx| { v_flex() .child("Custom Element") .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("This is a subtitle") ) }) .menu_element_with_icon( IconName::Info, Box::new(InfoAction), |window, cx| { h_flex() .gap_1() .child("Status") .child( div() .text_sm() .text_color(cx.theme().success) .child("✓ Connected") ) } ) ``` ### Keyboard Shortcuts Menu items automatically display keyboard shortcuts if they're bound to actions: ```rust // First define your actions and key bindings actions!(my_app, [Copy, Paste, Cut]); // In your app initialization cx.bind_keys([ KeyBinding::new("ctrl-c", Copy, Some("editor")), KeyBinding::new("ctrl-v", Paste, Some("editor")), KeyBinding::new("ctrl-x", Cut, Some("editor")), ]); // The menu will automatically show shortcuts menu.action_context(focus_handle) // Set context for shortcuts .menu("Copy", Box::new(Copy)) // Will show "Ctrl+C" .menu("Paste", Box::new(Paste)) // Will show "Ctrl+V" .menu("Cut", Box::new(Cut)) // Will show "Ctrl+X" ``` A shortcut is shown where the item's action will be dispatched: the `action_context` when one is set, otherwise the key contexts the menu's trigger sits in. The hints appear on the same frame as the menu items. ### Submenus Create nested menus with submenu support: ```rust menu.submenu("File", window, cx, |submenu, window, cx| { submenu.menu("New", Box::new(NewFile)) .menu("Open", Box::new(OpenFile)) .separator() .menu("Recent", Box::new(ShowRecent)) }) .submenu("Edit", window, cx, |submenu, window, cx| { submenu.menu("Undo", Box::new(Undo)) .menu("Redo", Box::new(Redo)) }) ``` ### Submenus with Icons Add icons to submenu headers: ```rust menu.submenu_with_icon( Some(IconName::Folder.into()), "Project", window, cx, |submenu, window, cx| { submenu.menu("Open Project", Box::new(OpenProject)) .menu("Close Project", Box::new(CloseProject)) } ) ``` ### Scrollable Menus For menus with many items, enable scrolling. Submenus open from a scrollable menu the same way as from any other menu: ```rust Button::new("large-menu") .label("Many Options") .dropdown_menu(|menu, window, cx| { let mut menu = menu .scrollable(true) .max_h(px(300.)) .label("Select an option"); for i in 0..100 { menu = menu.menu( format!("Option {}", i), Box::new(SelectOption(i)) ); } menu }) ``` ### Menu Sizing Control menu dimensions: ```rust menu.min_w(px(200.)) // Minimum width .max_w(px(400.)) // Maximum width .max_h(px(300.)) // Maximum height .scrollable(true) // Enable scrolling when content exceeds max height ``` ### Action Context Set the focus context for handling menu actions: ```rust let focus_handle = cx.focus_handle(); menu.action_context(focus_handle) .menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) ``` ### File Manager Context Menu ```rust div() .id("file-manager") .child("Right-click for options") .context_menu(|menu, window, cx| { menu.menu_with_icon("Open", IconName::FolderOpen, Box::new(Open)) .separator() .menu_with_icon("Copy", IconName::Copy, Box::new(Copy)) .menu_with_icon("Cut", IconName::Scissors, Box::new(Cut)) .menu_with_icon("Paste", IconName::Clipboard, Box::new(Paste)) .separator() .submenu("New", window, cx, |submenu, window, cx| { submenu.menu_with_icon("File", IconName::File, Box::new(NewFile)) .menu_with_icon("Folder", IconName::Folder, Box::new(NewFolder)) }) .separator() .menu_with_icon("Delete", IconName::Trash, Box::new(Delete)) .separator() .menu("Properties", Box::new(ShowProperties)) }) ``` ### Add MenuItem without action Sometimes you may not like to define an action for a menu item, you just want add a `on_click` handler, in this case, the `item` and [PopupMenuItem] can help you: ```rust use gpui_kit::component::{menu::PopupMenuItem, Button}; Button::new("custom-item-menu") .label("Options") .dropdown_menu(|menu, window, cx| { menu.item( PopupMenuItem::new("Custom Action") .disabled(false) .icon(IconName::Star) .on_click(|window, cx| { // Custom click handler logic println!("Custom Action Clicked!"); }) ) .separator() .menu("Standard Action", Box::new(StandardAction)) }) ``` ### Editor Menu with Shortcuts ```rust // Define actions with keyboard shortcuts actions!(editor, [Save, SaveAs, Find, Replace, ToggleWordWrap]); // Set up key bindings cx.bind_keys([ KeyBinding::new("ctrl-s", Save, Some("editor")), KeyBinding::new("ctrl-shift-s", SaveAs, Some("editor")), KeyBinding::new("ctrl-f", Find, Some("editor")), KeyBinding::new("ctrl-h", Replace, Some("editor")), ]); // Create menu with automatic shortcuts let editor_focus = cx.focus_handle(); Button::new("editor-menu") .label("Edit") .dropdown_menu(|menu, window, cx| { menu.action_context(editor_focus) .menu("Save", Box::new(Save)) // Shows "Ctrl+S" .menu("Save As...", Box::new(SaveAs)) // Shows "Ctrl+Shift+S" .separator() .menu("Find", Box::new(Find)) // Shows "Ctrl+F" .menu("Replace", Box::new(Replace)) // Shows "Ctrl+H" .separator() .menu_with_check("Word Wrap", true, Box::new(ToggleWordWrap)) }) ``` ### Settings Menu with Custom Elements ```rust Button::new("settings") .label("Settings") .dropdown_menu(|menu, window, cx| { menu.label("Display") .menu_element_with_check(dark_mode, Box::new(ToggleDarkMode), |window, cx| { h_flex() .gap_2() .child("Dark Mode") .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child(if dark_mode { "On" } else { "Off" }) ) }) .separator() .label("Account") .menu_element_with_icon( IconName::User, Box::new(ShowProfile), |window, cx| { v_flex() .child("John Doe") .child( div() .text_xs() .text_color(cx.theme().muted_foreground) .child("john@example.com") ) } ) .separator() .link_with_icon("Help Center", IconName::Help, "https://help.example.com") .menu("Sign Out", Box::new(SignOut)) }) ``` ## Keyboard Shortcuts | Key | Action | | ----------------- | --------------------------------- | | `↑` / `↓` | Navigate menu items | | `←` / `→` | Navigate submenus | | `Enter` / `Space` | Activate menu item | | `Escape` | Close menu | | `Tab` | Close menu and focus next element | ## Best Practices 1. **Group Related Items**: Use separators to group related functionality 2. **Consistent Icons**: Use consistent iconography across your application 3. **Logical Order**: Place most common actions at the top 4. **Keyboard Shortcuts**: Provide shortcuts for frequently used actions 5. **Context Awareness**: Show only relevant items for the current context 6. **Progressive Disclosure**: Use submenus for complex hierarchies 7. **Clear Labels**: Use descriptive, action-oriented labels 8. **Reasonable Limits**: Use scrollable menus for more than 10-15 items [PopupMenu]: https://docs.rs/gpui-component/latest/gpui_component/menu/struct.PopupMenu.html [PopupMenuItem]: https://docs.rs/gpui-component/latest/gpui_component/menu/struct.PopupMenuItem.html [context_menu]: https://docs.rs/gpui-component/latest/gpui_component/menu/trait.ContextMenuExt.html#method.context_menu [Action]: https://docs.rs/gpui/latest/gpui/trait.Action.html ## API Reference - [PopupMenu] - [context_menu] - [PopupMenuItem] --- # Sheet Source: /component/sheet A Sheet (also known as a sidebar or slide-out panel) is a navigation component that slides in from the edges of the screen. It provides additional space for content without taking up the main view, and can be used for navigation menus, forms, or any supplementary content. ## Import ```rust use gpui_kit::component::WindowExt; use gpui_kit::component::Placement; ``` ## Usage ### Where sheets render The window's [Root](/component/root) automatically mounts and renders sheets. Open the window with `gpui_kit::open_window`, or wrap the application view in `Root::new`. Application views do not render overlay layers themselves. ### Basic Sheet ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Navigation") .child("Sheet content goes here") }) ``` ### Sheet with Placement ```rust // Left sheet (default) window.open_sheet_at(Placement::Left, cx, |sheet, _, _| { sheet.title("Left Sheet") }) // Right sheet window.open_sheet_at(Placement::Right, cx, |sheet, _, _| { sheet.title("Right Sheet") }) // Top sheet window.open_sheet_at(Placement::Top, cx, |sheet, _, _| { sheet.title("Top Sheet") }) // Bottom sheet window.open_sheet_at(Placement::Bottom, cx, |sheet, _, _| { sheet.title("Bottom Sheet") }) ``` ### Sheet with Custom Size ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Wide Sheet") .size(px(500.)) // Custom width for left/right, height for top/bottom .child("This sheet is 500px wide") }) ``` ### Sheet with Form Content ```rust let input = cx.new(|cx| InputState::new(window, cx)); let date = cx.new(|cx| DatePickerState::new(window, cx)); window.open_sheet(cx, |sheet, _, _| { sheet .title("User Profile") .child( v_flex() .gap_4() .child("Enter your information:") .child(Input::new(&input).placeholder("Full Name")) .child(DatePicker::new(&date).placeholder("Date of Birth")) ) .footer( h_flex() .gap_3() .child(Button::new("save").primary().label("Save")) .child(Button::new("cancel").label("Cancel")) ) }) ``` ### Overlay Options ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Settings") .overlay(true) // Show overlay background (default: true) .overlay_closable(true) // Click overlay to close (default: true) .child("Sheet settings content") }) // No overlay window.open_sheet(cx, |sheet, _, _| { sheet .title("Side Panel") .overlay(false) // No overlay background .child("This sheet has no overlay") }) ``` ### Resizable Sheet ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Resizable Panel") .resizable(true) // Allow user to resize (default: true) .size(px(300.)) .child("You can resize this sheet by dragging the edge") }) ``` ### Custom Margin and Positioning ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Below Title Bar") .margin_top(px(32.)) // Space for window title bar .child("This sheet appears below the title bar") }) ``` ### Sheet with List ```rust let delegate = ListDelegate::new(items); let list = cx.new(|cx| List::new(delegate, window, cx)); window.open_sheet_at(Placement::Left, cx, |sheet, _, _| { sheet .title("File Explorer") .size(px(400.)) .child( div() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) .size_full() .child(list.clone()) ) }) ``` ### Close Event Handling ```rust window.open_sheet(cx, |sheet, _, _| { sheet .title("Sheet with Handler") .child("This sheet has a custom close handler") .on_close(|_, window, cx| { window.push_notification("Sheet was closed", cx); }) }) ``` ### Navigation Sheet ```rust window.open_sheet_at(Placement::Left, cx, |sheet, _, _| { sheet .title("Navigation") .size(px(280.)) .child( v_flex() .gap_2() .child(Button::new("home").ghost().label("Home").w_full()) .child(Button::new("profile").ghost().label("Profile").w_full()) .child(Button::new("settings").ghost().label("Settings").w_full()) .child(Button::new("logout").ghost().label("Logout").w_full()) ) }) ``` ### Custom Styling ```rust window.open_sheet(cx, |sheet, _, cx| { sheet .title("Styled Sheet") .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .border_color(cx.theme().primary) .child("Custom styled sheet content") }) ``` ### Programmatic Close ```rust // Close sheet from inside Button::new("close") .label("Close Sheet") .on_click(|_, window, cx| { window.close_sheet(cx); }) // Close sheet from outside window.close_sheet(cx); ``` ### Settings Panel ```rust window.open_sheet_at(Placement::Right, cx, |sheet, _, _| { sheet .title("Settings") .size(px(350.)) .child( v_flex() .gap_4() .child("Appearance") .child(Checkbox::new("dark-mode").label("Dark Mode")) .child(Checkbox::new("animations").label("Enable Animations")) .child("Notifications") .child(Checkbox::new("push-notifications").label("Push Notifications")) ) .footer( h_flex() .justify_end() .gap_2() .child(Button::new("apply").primary().label("Apply")) .child(Button::new("cancel").label("Cancel")) ) }) ``` ### File Browser ```rust window.open_sheet_at(Placement::Left, cx, |sheet, _, _| { sheet .title("Files") .size(px(300.)) .child( v_flex() .size_full() .child( h_flex() .gap_2() .p_2() .child(Button::new("new-folder").small().icon(IconName::FolderPlus)) .child(Button::new("upload").small().icon(IconName::Upload)) ) .child( div() .flex_1() .overflow_hidden() .child(file_tree_list) ) ) }) ``` ### Help Panel ```rust window.open_sheet_at(Placement::Bottom, cx, |sheet, _, _| { sheet .title("Help & Documentation") .size(px(200.)) .child( h_flex() .gap_4() .child("Keyboard Shortcuts") .child(Kbd::new("⌘").child("K")) .child("Search") .child(Kbd::new("⌘").child("P")) .child("Command Palette") ) }) ``` ## Best Practices 1. **Appropriate Placement**: Use left/right for navigation, top/bottom for temporary content 2. **Consistent Sizing**: Maintain consistent sheet sizes across your application 3. **Clear Headers**: Always provide descriptive titles 4. **Close Options**: Provide multiple ways to close (ESC, overlay click, close button) 5. **Content Organization**: Use proper spacing and grouping for sheet content 6. **Responsive Design**: Consider sheet behavior on smaller screens 7. **Performance**: Lazy load sheet content when possible for better performance ## API Reference ### Window Extensions | Method | Description | | ---------------------------------- | ----------------------------------------- | | `open_sheet(cx, fn)` | Open sheet with default placement (Right) | | `open_sheet_at(placement, cx, fn)` | Open sheet at specific placement | | `close_sheet(cx)` | Close current sheet | ### Sheet Builder | Method | Description | | ------------------------ | --------------------------------------- | | `title(str)` | Set sheet title | | `child(el)` | Add content to sheet body | | `footer(el)` | Set footer content | | `size(px)` | Set sheet size (width or height) | | `margin_top(px)` | Set top margin (for title bars) | | `resizable(bool)` | Allow resizing (default: true) | | `overlay(bool)` | Show overlay background (default: true) | | `overlay_closable(bool)` | Click overlay to close (default: true) | | `on_close(fn)` | Close event callback | ### Placement Options | Value | Description | | ------------------- | ----------------------------------- | | `Placement::Left` | Slides in from left edge | | `Placement::Right` | Slides in from right edge (default) | | `Placement::Top` | Slides in from top edge | | `Placement::Bottom` | Slides in from bottom edge | ### Styling Methods | Method | Description | | --------------------- | ------------------------ | | `bg(color)` | Set background color | | `text_color(color)` | Set text color | | `border_color(color)` | Set border color | | `px_*()/py_*()` | Custom padding | | `gap_*()` | Spacing between children | --- # Checkbox Source: /component/checkbox A checkbox component for binary choices. Supports labels, disabled state, and different sizes. Use `on_change` for requested values. The owner stores the value and calls `cx.notify()`. The existing `on_click` name remains a compatibility alias; setting either replaces the same handler, so the last call wins. ## Import ```rust use gpui_kit::component::checkbox::Checkbox; ``` ## Usage ### Basic ```rust Checkbox::new("my-checkbox") .label("Accept terms and conditions") .checked(false) .on_change(|checked, _, _| { println!("Checkbox is now: {}", checked); }) ``` The `on_change` callback is triggered when the user toggles the checkbox, receiving the **new checked state**. ### Default Checked and unchecked options can be mixed freely. ```rust Checkbox::new("updates") .label("Product updates") .checked(false) Checkbox::new("remember") .label("Remember this device") .checked(true) ``` ### Sizes ```rust use gpui_kit::component::Sizable as _; Checkbox::new("cb").text_xs().label("Extra Small") Checkbox::new("cb").text_sm().label("Small") Checkbox::new("cb").label("Medium") // default Checkbox::new("cb").text_lg().label("Large") ``` ### Controlled Checkbox This complete **Tested consumer recipe** keeps the value on the rendering owner and applies the requested value from `on_change` before notifying: ```rust use gpui_kit::component::checkbox::Checkbox; use gpui_kit::{Context, IntoElement, Render, Window}; pub struct ControlledCheckbox { checked: bool, } impl ControlledCheckbox { pub fn new() -> Self { Self { checked: false } } pub fn is_checked(&self) -> bool { self.checked } } impl Render for ControlledCheckbox { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { Checkbox::new("marketing-emails") .label("Receive product updates") .checked(self.checked) .on_change(cx.listener(|this, checked, _, cx| { this.checked = *checked; cx.notify(); })) } } ``` ### Disabled State ```rust use gpui_kit::component::Disableable as _; Checkbox::new("disabled-checked") .label("Checked") .checked(true) .disabled(true) Checkbox::new("disabled-unchecked") .label("Unchecked") .checked(false) .disabled(true) ``` ### Without Label ```rust Checkbox::new("checkbox") .checked(true) ``` ### Labels Labels can wrap and include supporting content. ```rust Checkbox::new("description") .label("Automatic updates") .child( div() .text_xs() .child("Download updates when the application is idle."), ) Checkbox::new("wrapping") .label("Notify me when a new device signs in to my account") Checkbox::new("markdown") .label("Accept the terms") .child(markdown( "Read the [terms of service](https://github.com) before continuing.", )) ``` ### Custom Tab Order ```rust Checkbox::new("checkbox") .label("Custom tab order") .tab_index(2) .tab_stop(true) ``` ### Checkbox List ```rust v_flex() .gap_2() .child(Checkbox::new("cb1").label("Option 1").checked(true)) .child(Checkbox::new("cb2").label("Option 2").checked(false)) .child(Checkbox::new("cb3").label("Option 3").checked(false)) ``` ### Form Integration ```rust struct FormView { agree_terms: bool, subscribe: bool, } v_flex() .gap_3() .child( Checkbox::new("terms") .label("I agree to the terms and conditions") .checked(self.agree_terms) .on_change(cx.listener(|view, checked, _, cx| { view.agree_terms = *checked; cx.notify(); })) ) .child( Checkbox::new("subscribe") .label("Subscribe to newsletter") .checked(self.subscribe) .on_change(cx.listener(|view, checked, _, cx| { view.subscribe = *checked; cx.notify(); })) ) ``` [Checkbox]: https://docs.rs/gpui-component/latest/gpui_component/checkbox/struct.Checkbox.html ## API Reference - [Checkbox] ### Styling Implements `Sizable` and `Disableable` traits: - `text_xs()` - Extra small text - `text_sm()` - Small text - `text_base()` - Base text (default) - `text_lg()` - Large text - `disabled(bool)` - Disabled state --- # Skeleton Source: /component/skeleton The Skeleton component displays animated placeholder content while actual content is loading. It provides visual feedback to users that content is being loaded and helps maintain layout structure during loading states. ## Import ```rust use gpui_kit::component::skeleton::Skeleton; ``` ## Usage ### Loading ```rust v_flex() .gap_4() .p_4() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius_lg) .child( h_flex() .gap_3() .items_center() .child(Skeleton::new().size_12().rounded_full()) // Avatar .child( v_flex() .gap_2() .child(Skeleton::new().w(px(120.)).h_4().rounded_md()) // Name .child(Skeleton::new().w(px(100.)).h_3().rounded_md()) // Email ) ) .child( v_flex() .gap_2() .child(Skeleton::new().w_full().h_4().rounded_md()) // Bio line 1 .child(Skeleton::new().w(px(200.)).h_4().rounded_md()) // Bio line 2 ) ``` ### Basic Skeleton ```rust Skeleton::new() ``` ### Text Line Skeleton ```rust // Single line of text Skeleton::new() .w(px(250.)) .h_4() .rounded_md() // Multiple text lines v_flex() .gap_2() .child(Skeleton::new().w(px(250.)).h_4().rounded_md()) .child(Skeleton::new().w(px(200.)).h_4().rounded_md()) .child(Skeleton::new().w(px(180.)).h_4().rounded_md()) ``` ### Circle Skeleton ```rust // Avatar placeholder Skeleton::new() .size_12() .rounded_full() // Profile picture placeholder Skeleton::new() .w(px(64.)) .h(px(64.)) .rounded_full() ``` ### Rectangle Skeleton ```rust // Card image placeholder Skeleton::new() .w(px(250.)) .h(px(125.)) .rounded_md() // Button placeholder Skeleton::new() .w(px(120.)) .h(px(40.)) .rounded_md() ``` ### Different Shapes ```rust // Text content Skeleton::new().w(px(200.)).h_4().rounded_sm() // Square image Skeleton::new().size_20().rounded_md() // Wide banner Skeleton::new().w_full().h(px(200.)).rounded_lg() // Small icon Skeleton::new().size_6().rounded_md() ``` ### Secondary Variant ```rust // Use secondary color (more subtle) Skeleton::new() .secondary() .w(px(200.)) .h_4() .rounded_md() ``` ### Loading Article List ```rust v_flex() .gap_6() .children((0..3).map(|_| { h_flex() .gap_4() .child(Skeleton::new().w(px(120.)).h(px(80.)).rounded_md()) // Thumbnail .child( v_flex() .gap_2() .flex_1() .child(Skeleton::new().w_full().h_5().rounded_md()) // Title .child(Skeleton::new().w(px(300.)).h_4().rounded_md()) // Excerpt line 1 .child(Skeleton::new().w(px(250.)).h_4().rounded_md()) // Excerpt line 2 .child(Skeleton::new().w(px(100.)).h_3().rounded_md()) // Date ) })) ``` ### Loading Table Rows ```rust v_flex() .gap_2() .children((0..5).map(|_| { h_flex() .gap_4() .p_3() .border_b_1() .border_color(cx.theme().border) .child(Skeleton::new().size_8().rounded_full()) // Status indicator .child(Skeleton::new().w(px(150.)).h_4().rounded_md()) // Name .child(Skeleton::new().w(px(200.)).h_4().rounded_md()) // Email .child(Skeleton::new().w(px(80.)).h_4().rounded_md()) // Role .child(Skeleton::new().w(px(60.)).h_4().rounded_md()) // Actions })) ``` ### Loading Button States ```rust h_flex() .gap_3() .child(Skeleton::new().w(px(80.)).h(px(36.)).rounded_md()) // Primary button .child(Skeleton::new().w(px(70.)).h(px(36.)).rounded_md()) // Secondary button .child(Skeleton::new().size_9().rounded_md()) // Icon button ``` ### Loading Form Fields ```rust v_flex() .gap_4() .child( v_flex() .gap_1() .child(Skeleton::new().w(px(60.)).h_4().rounded_md()) // Label .child(Skeleton::new().w_full().h(px(40.)).rounded_md()) // Input ) .child( v_flex() .gap_1() .child(Skeleton::new().w(px(80.)).h_4().rounded_md()) // Label .child(Skeleton::new().w_full().h(px(120.)).rounded_md()) // Textarea ) ``` ### Conditional Loading ```rust if loading { Skeleton::new().w(px(200.)).h_4().rounded_md() } else { div().child("Actual content here") } ``` ## Animation The Skeleton component includes a built-in pulse animation that: - Runs continuously with a 2-second duration - Uses a bounce easing function with ease-in-out - Animates opacity from 100% to 50% and back - Automatically repeats to indicate loading state The animation cannot be disabled as it's essential for indicating loading state. ## Sizes The Skeleton component doesn't have predefined size variants. Instead, use gpui's sizing utilities: ```rust // Height utilities Skeleton::new().h_3() // 12px height Skeleton::new().h_4() // 16px height Skeleton::new().h_5() // 20px height Skeleton::new().h_6() // 24px height // Width utilities Skeleton::new().w(px(100.)) // 100px width Skeleton::new().w(px(200.)) // 200px width Skeleton::new().w_full() // Full width Skeleton::new().w_1_2() // 50% width // Square sizes Skeleton::new().size_4() // 16x16px Skeleton::new().size_8() // 32x32px Skeleton::new().size_12() // 48x48px Skeleton::new().size_16() // 64x64px ``` ## Theming The Skeleton component uses the theme's `skeleton` color, which defaults to the `secondary` color if not specified. You can customize it in your theme: ```json { "skeleton.background": "#e2e8f0" } ``` The `secondary(true)` variant applies 50% opacity to the skeleton color for more subtle loading indicators. --- # Theme Source: /component/theme All components support theming through the built-in Theme system, the [ActiveTheme] trait provides access to the current theme colors: ```rs use gpui_kit::component::{ActiveTheme as _}; // Access theme colors in your components cx.theme().primary cx.theme().background cx.theme().foreground ``` So if you want use the colors from the current theme, you should keep your component or view have [App] context. ## Gradient Backgrounds Theme color values remain backward compatible with the existing string format: ```json { "colors": { "button.primary.background": "#4F46E5" } } ``` Background tokens that opt in to gradient rendering can also use CSS-style two-stop linear gradients: ```json { "colors": { "button.primary.background": "linear-gradient(135deg, #4F46E5, #06B6D4)", "button.primary.hover.background": "linear-gradient(to right, red-500 25%, blue-600 75%)" } } ``` Top-level theme fields, such as `cx.theme().button_primary`, remain solid `Hsla` values for compatibility. Code that needs the full resolved token can use `cx.theme().tokens.button_primary`; its `.color` field is the solid representative color, and its `.background` field contains the configured `Background`, including gradients. ## Theme Registry There have more than 20 built-in themes available in [themes](https://github.com/MohsenDastaran/uni-kit/tree/main/themes) folder. https://github.com/MohsenDastaran/uni-kit/tree/main/themes And we have a [ThemeRegistry] to help us to load themes. Use the `name` of an entry in the `themes` array, such as `Ayu Light`, when looking up a theme from the registry. ```rs use std::path::PathBuf; use gpui_kit::{App, SharedString}; use gpui_kit::component::{Theme, ThemeRegistry}; pub fn init(cx: &mut App) { let theme_name = SharedString::from("Ayu Light"); // Load and watch themes from ./themes directory if let Err(err) = 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)); } }) { tracing::error!("Failed to watch themes directory: {}", err); } } ``` [ActiveTheme]: https://docs.rs/gpui-component/latest/gpui_component/theme/trait.ActiveTheme.html [ThemeRegistry]: https://docs.rs/gpui-component/latest/gpui_component/theme/struct.ThemeRegistry.html [App]: https://docs.rs/gpui/latest/gpui/struct.App.html --- # Stepper Source: /component/stepper A step-by-step progress component that guides users through a series of steps or stages. Supports horizontal and vertical layouts, custom icons, and different sizes. ## Import ```rust use gpui_kit::component::stepper::{Stepper, StepperItem}; ``` ## Usage ### With icons ```rust use gpui_kit::component::IconName; Stepper::new("icon-stepper") .selected_index(0) .items([ StepperItem::new() .icon(IconName::Calendar) .child("Order Details"), StepperItem::new() .icon(IconName::Inbox) .child("Shipping"), StepperItem::new() .icon(IconName::Frame) .child("Preview"), StepperItem::new() .icon(IconName::Info) .child("Finish"), ]) ``` ### Text center The `text_center` method centers the text within each step item. ```rust Stepper::new("center-stepper") .selected_index(0) .text_center(true) .items([ StepperItem::new().child( v_flex() .items_center() .child("Step 1") .child("Desc for step 1."), ), StepperItem::new().child( v_flex() .items_center() .child("Step 2") .child("Desc for step 2."), ), StepperItem::new().child( v_flex() .items_center() .child("Step 3") .child("Desc for step 3."), ), ]) ``` ### Vertical ```rust Stepper::new("vertical-stepper") .vertical() .selected_index(2) .items_center() .items([ StepperItem::new() .pb_8() .icon(IconName::Building2) .child(v_flex().child("Step 1").child("Description for step 1.")), StepperItem::new() .pb_8() .icon(IconName::Asterisk) .child(v_flex().child("Step 2").child("Description for step 2.")), StepperItem::new() .pb_8() .icon(IconName::Folder) .child(v_flex().child("Step 3").child("Description for step 3.")), StepperItem::new() .icon(IconName::CircleCheck) .child(v_flex().child("Step 4").child("Description for step 4.")), ]) ``` ### Sizes and disabled ```rust use gpui_kit::component::{Sizable as _, Size}; Stepper::new("stepper") .xsmall() .items([...]) Stepper::new("stepper") .small() .items([...]) Stepper::new("stepper") .large() .items([...]) ``` ### Basic Stepper Use `selected_index` method to set current active step by index (0-based), default is `0`. ```rust Stepper::new("my-stepper") .selected_index(0) .items([ StepperItem::new().child("Step 1"), StepperItem::new().child("Step 2"), StepperItem::new().child("Step 3"), ]) .on_click(|step, _, _| { println!("Clicked step: {}", step); }) ``` ### Disabled State ```rust Stepper::new("disabled-stepper") .disabled(true) .items([ StepperItem::new().child("Step 1"), StepperItem::new().child("Step 2"), ]) ``` ### Handle Click Events ```rust Stepper::new("my-stepper") .selected_index(current_step) .items([ StepperItem::new().child("Step 1"), StepperItem::new().child("Step 2"), StepperItem::new().child("Step 3"), ]) .on_click(cx.listener(|this, step, _, cx| { this.current_step = *step; cx.notify(); })) ``` ### Multi-step Form ```rust Stepper::new("form-stepper") .w_full() .selected_index(form_step) .items([ StepperItem::new() .icon(IconName::User) .child("Personal Info"), StepperItem::new() .icon(IconName::CreditCard) .child("Payment"), StepperItem::new() .icon(IconName::CircleCheck) .child("Confirmation"), ]) .on_click(cx.listener(|this, step, _, cx| { this.form_step = *step; cx.notify(); })) ``` ### Disabled Individual Steps ```rust Stepper::new("stepper") .selected_index(0) .items([ StepperItem::new().child("Available"), StepperItem::new().disabled(true).child("Locked"), StepperItem::new().child("Available"), ]) ``` [Stepper]: https://docs.rs/gpui-component/latest/gpui_component/stepper/struct.Stepper.html [StepperItem]: https://docs.rs/gpui-component/latest/gpui_component/stepper/struct.StepperItem.html [Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html ## API Reference - [Stepper] - [StepperItem] ### Sizing Implements [Sizable] trait: - `xsmall()` - Extra small size - `small()` - Small size - `medium()` - Medium size (default) - `large()` - Large size --- # Clipboard Source: /component/clipboard The Clipboard component provides an easy way to copy text or other data to the user's clipboard. It renders as a button with a copy icon that changes to a checkmark when content is successfully copied. The component supports both static values and dynamic content through callback functions. ## Import ```rust use gpui_kit::component::clipboard::Clipboard; ``` ## Usage ### Copy ```rust Clipboard::new("simple") .value("Hello, World!") ``` ### Basic Clipboard ```rust Clipboard::new("my-clipboard") .value("Text to copy") .on_copied(|value, window, cx| { window.push_notification(format!("Copied: {}", value), cx) }) ``` ### Using Dynamic Values The `value_fn` method allows you to provide a closure that generates the content to be copied at the time of the copy action. - This is useful when the content to be copied depends on the current state of the application. - And in some cases, it may have a larger overhead to compute, so you only want to do it when the user actually clicks the copy button. ```rust let state = some_state.clone(); Clipboard::new("dynamic-clipboard") .value_fn(move |_, cx| { state.read(cx).get_current_value() }) .on_copied(|value, window, cx| { window.push_notification(format!("Copied: {}", value), cx) }) ``` ### With Custom Content ```rust use gpui_kit::component::label::Label; h_flex() .gap_2() .child(Label::new("Share URL")) .child(Icon::new(IconName::Share)) .child( Clipboard::new("custom-clipboard") .value("https://example.com") ) ``` ### In Input Fields The Clipboard component is commonly used as a suffix in input fields: ```rust use gpui_kit::component::input::{InputState, Input}; let url_state = cx.new(|cx| InputState::new(window, cx).default_value("https://github.com")); Input::new(&url_state) .suffix( Clipboard::new("url-clipboard") .value_fn({ let state = url_state.clone(); move |_, cx| state.read(cx).value() }) .on_copied(|value, window, cx| { window.push_notification(format!("URL copied: {}", value), cx) }) ) ``` ### With User Feedback ```rust h_flex() .gap_2() .child(Label::new("Your API Key:")) .child( Clipboard::new("feedback") .value("sk-1234567890abcdef") .on_copied(|_, window, cx| { window.push_notification("API key copied to clipboard", cx) }) ) ``` ### Form Field Integration ```rust use gpui_kit::component::{ input::{InputState, Input}, h_flex, label::Label }; let api_key = "sk-1234567890abcdef"; h_flex() .gap_2() .items_center() .child(Label::new("API Key:")) .child( Input::new(&input_state) .value(api_key) .readonly(true) .suffix( Clipboard::new("api-key-copy") .value(api_key) .on_copied(|_, window, cx| { window.push_notification("API key copied!", cx) }) ) ) ``` ### Dynamic Content Copy ```rust struct AppState { current_url: String, } let app_state = cx.new(|_| AppState { current_url: "https://example.com".to_string() }); Clipboard::new("current-url") .value_fn({ let state = app_state.clone(); move |_, cx| { SharedString::from(state.read(cx).current_url.clone()) } }) .on_copied(|url, window, cx| { window.push_notification(format!("Shared: {}", url), cx) }) ``` ## Data Types The Clipboard component currently supports copying text strings to the clipboard. It uses GPUI's `ClipboardItem::new_string()` method, which handles: - Plain text strings - UTF-8 encoded content - Cross-platform clipboard integration [Clipboard]: https://docs.rs/gpui-component/latest/gpui_component/clipboard/struct.Clipboard.html ## API Reference - [Clipboard] --- # Dock Source: /base/dock A dockable workspace: nested splits, tab groups with draggable tabs, and left/right/bottom docks that fold away. `gpui-base` owns all of the behavior and draws none of it. The layout is not a tree of views. It is a value — a `PaneTree` — that you can build, compare, serialize, and edit without a `Window` or an `App` in sight. `DockArea` reconciles that value into live entities, and two renderer traits supply every pixel. This page is long because Dock is the largest system in `gpui-base`. If you only need to stand one up, [Get started](#get-started) and [Supply the appearance](#supply-the-appearance) are enough. ## The model Two container shapes, and nothing else: | Container | Holds | Notes | | --------- | -------------------------------- | ------------------------------------------ | | `Split` | Other containers, along one axis | Each child slot has an optional fixed size | | `Tabs` | Panels, one displayed at a time | Carries the displayed index | There is no leaf variant, so **a panel can only ever live inside a `Tabs`**. A region whose center is a single panel is still a `Tabs` holding one panel. Four regions exist: the center, plus an optional left, right and bottom dock. Each is one independent `PaneTree`. Two identities, both stable: - **`NodeId`** addresses a container. It survives every edit and every normalization rule, so a container still present after a drag carries the id it had before. Ids are allocated globally, so a node id is unambiguous across all four regions. - **`PanelId`** addresses a panel. It wraps the panel entity's `EntityId`, so it identifies that panel for as long as the entity lives — across any number of moves between groups and regions. Neither the tree nor any node stores a GPUI entity handle. ### Key types | Type | Role | | --------------------------------------------------------- | --------------------------------------------------------------------------- | | `PaneTree` | One region's layout, as pure data | | `PaneNode` / `PaneRef` | A node, and the borrowed projection you `match` on | | `NodeId` / `PanelId` | Stable container and panel identity | | `DockArea` | Owns the trees, reconciles them into entities, routes drags and persistence | | `DockLayout` | Describes a layout without constructing anything | | `Panel` | What a dockable view implements — behavior only | | `PanelView` | Object-safe panel handle, `Arc` | | `TabGroup` | The entity behind a `Tabs` node | | `DockAreaRenderer` / `TabGroupRenderer` | Where every visual decision goes | | `DockContext` / `TabGroupContext` | Resolved state and callbacks handed to a renderer | | `DockAreaState` | The serializable form of a whole area | ## Get started ```rust use std::rc::Rc; use gpui_kit::base::dock::{DockArea, DockLayout, DockPlacement}; let area = cx.new(|cx| { DockArea::new("workspace", Some(1), window, cx).with_renderer(Rc::new(MySkin)) }); area.update(cx, |area, cx| { area.set_center( DockLayout::h_split() .child(DockLayout::tabs().panel(files.clone()), Some(px(240.))) .child(DockLayout::tabs().panel(editor.clone()), None), window, cx, ); area.set_dock( DockPlacement::Bottom, DockLayout::tabs().panel(terminal.clone()), window, cx, ); }); ``` `DockArea::new` takes an id (yours, for your own persistence) and an optional schema version. An area built without `.with_renderer(...)` still docks, drags, resizes and persists — it simply draws nothing but the panels themselves. ## Describing a layout `DockLayout` builds a tree without touching `window` or `cx`, because building a tree constructs no entities. ```rust DockLayout::h_split() .child(DockLayout::tabs().panel(explorer.clone()), Some(px(240.))) .child( DockLayout::v_split() .child( DockLayout::tabs() .panel(editor.clone()) .panel(diff.clone()) .active_index(1), None, ) .child(DockLayout::tabs().panel(console.clone()), Some(px(180.))), None, ) ``` | Builder | Produces | | ------------------------- | --------------------------------- | | `h_split()` / `v_split()` | A split along that axis | | `child(layout, size)` | Adds a child container to a split | | `tabs()` | A tab group | | `panel(entity)` | Adds a panel to a tab group | | `active_index(ix)` | Which tab starts displayed | Misuse — a panel added to a split, a child added to a tab group — trips a `debug_assert!` and is otherwise ignored. ### Slot sizes The `size` in `child(layout, size)` is the slot's extent **along the split's axis**: width in an `h_split`, height in a `v_split`. - `Some(px(240.))` fixes it. - `None` leaves it unconstrained — the slot shares what is left with its other unconstrained siblings. A layout with every slot `None` divides the space evenly. When a panel is later dropped beside an existing one with no size in mind, it takes half of what it lands next to. ### Normalization Every edit runs one collapse pass to a fixpoint before returning. The rules, applied bottom up: 1. An empty `Tabs` or `Split` is removed from its parent. 2. A `Split` with one child is replaced by that child, which keeps its own `NodeId` and inherits the slot size. 3. A `Split` whose child is a `Split` of the same axis splices that child's children into itself, scaling their sizes to fill the slot. 4. `active_ix` is clamped to the panel count. 5. The center's root stays a `Split` even when empty; a dock's root is unconstrained. Two consequences worth designing around. **You never need to avoid redundant nesting** — wrapping a node in a same-axis split is harmless, because rule 3 flattens it, which is why `split_at` needs no "reuse the parent" special case. And **there is no window in which a caller can observe a malformed tree**: no empty container, no one-child split, no out-of-range active index. Normalization is idempotent, so `normalize(normalize(t)) == normalize(t)`. ## Panels The whole of a panel's obligation to base is a stable name. ```rust struct FilesPanel { focus_handle: FocusHandle } impl Panel for FilesPanel { fn panel_name(&self) -> &'static str { "FilesPanel" } } impl EventEmitter for FilesPanel {} impl Focusable for FilesPanel { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for FilesPanel { /* ... */ } ``` `panel_name` identifies the panel in persisted layouts. **Once chosen, never change it** — it is the key a saved file is read back through. ### Every hook | Method | Default | When it runs | | ------------------------ | ---------- | ---------------------------------------------------------- | | `panel_name()` | _required_ | Any time the panel is identified or written out | | `visible(cx)` | `true` | Every render pass | | `closable(cx)` | `true` | Before a close is offered or applied | | `zoomable(cx)` | `true` | Before a zoom is applied | | `on_added_to(group, ..)` | no-op | When the panel joins a tab group, with a weak handle on it | | `set_active(active, ..)` | no-op | On each real edge of "is the displayed tab" | | `set_zoomed(zoomed, ..)` | no-op | When the group displaying it zooms in or out | | `on_removed(..)` | no-op | When the panel leaves the dock for good | | `dump(cx)` | name only | On `DockArea::dump` | ### Lifecycle contracts These are precise, and worth reading once: **`set_active` fires on edges only.** It is called with the frame-end net state: exactly one notification per real change, delivered on the next tick. Never same-value repeats, never a false-then-true flip within one frame. A panel that is hidden but occupies the active slot still receives `true`, even though rendering falls back to the first visible panel. **A removed panel is not told `false`.** `on_removed` is the deactivation signal. If you release resources in `set_active(false)`, release them in `on_removed` too. **A moved panel never hears `on_removed`.** Dragging a panel from one group to another does not take it out of the dock, so it is told `on_added_to` again with the new group and nothing else. `on_removed` means gone: closed, or displaced by a wholesale `set_center`, `set_dock`, `remove_dock` or `load`. **`on_added_to` precedes any `set_active`,** so a panel can store the handle and act on its first activation. **`set_zoomed` reaches only the displayed panel.** A group has one zoom state and it is the visible panel that fills the dock. Panels in the group's other tabs hear nothing, and a panel that was not displayed when the zoom changed is never told retroactively. **`closable` is permission, not a guarantee.** A container can still refuse — the last group of a dock does, so a dock cannot be emptied by closing. **A hidden panel keeps its place.** `visible` returning `false` leaves the panel in the tree and in its group; it reappears where it was. A container whose panels are _all_ hidden gives up its slot, recursively — a nested split whose every leaf is hidden takes no space. ## The dock area ### Installing layouts ```rust area.set_center(layout, window, cx); area.set_dock(DockPlacement::Left, layout, window, cx); area.remove_dock(DockPlacement::Left, window, cx); ``` Each replaces whatever was there. Panels that were displaced — and are not part of the new layout — receive `on_removed`. ### Adding and moving panels ```rust area.add_panel(panel, DockPlacement::Left, Some(px(240.)), window, cx); area.remove_panel(panel, window, cx); area.move_panel(panel_id, target, window, cx); area.split_at(node, panel_id, Placement::Right, window, cx); ``` `add_panel` lands the panel in the region's first tab group, creating the region if it has none. It has an `add_panel_view` variant taking an `Arc` for callers holding an erased handle. ### Docks ```rust area.has_dock(DockPlacement::Left); area.is_dock_open(DockPlacement::Left); area.toggle_dock(DockPlacement::Left, window, cx); area.dock_size(DockPlacement::Left); area.set_dock_size(DockPlacement::Left, px(280.), window, cx); area.set_dock_collapsible(DockPlacement::Left, true, window, cx); ``` A closed dock keeps its tree and its size; reopening restores both. ### Zoom A zoom names a **container**, not a panel — a group survives its displayed panel closing, and the next tab takes over still zoomed. ```rust area.set_zoomed_in(node, window, cx); area.set_zoomed_out(window, cx); area.is_zoomed(); area.zoomed_group(); // Option ``` The usual entry point is not these but `TabGroupContext::toggle_zoom`, which a skin already has wherever it draws a zoom control. Zoom ends when the zoomed container leaves the dock, or when the container clears it — not when some unrelated panel is removed. ### Locking `area.set_locked(true, window, cx)` freezes rearrangement: no drags, no drops, no closes. Reads and rendering are unaffected. ### Queries ```rust area.layout(DockPlacement::Center); // Option<&PaneTree> area.panel(panel_id); // Option<&Arc> area.is_empty(DockPlacement::Left, cx); area.is_locked(); area.bounds(); ``` ## Editing the tree directly Everything above ultimately goes through `PaneTree`. You can drive it yourself — for a command palette, a keyboard shortcut, a restored session: ```rust tree.insert_panel(panel, InsertTarget::Tabs { node, ix: None, activate: true }); tree.remove_panel(panel); tree.move_panel(panel, target); tree.split(node, panel, Placement::Right, Some(px(320.))); tree.set_active(node, 2); tree.set_sizes(node, vec![Some(px(200.)), None]); ``` `InsertTarget` says where a panel lands: | Variant | Meaning | | --------------------------------- | -------------------------------------------------- | | `Tabs { node, ix, activate }` | Into an existing tab group, optionally at an index | | `Split { node, placement, size }` | Beside a node, in a new tab group | Every edit returns an `EditResult`: `changed()`, plus `created_nodes()`, `removed_nodes()`, `removed_panels()`, `activated()`, `deactivated()`. **`removed_panels` excludes moves** — a moved panel's entity survives, so it must not receive `on_removed`. Reading a tree: ```rust tree.root(); // &PaneNode tree.node_ids(); // Vec, pre-order tree.panels(); // impl Iterator tree.find_node(node_id); // Option<&PaneNode> tree.find_panel_node(panel_id); // Option match node.kind() { PaneRef::Split { axis, children, sizes } => { /* ... */ } PaneRef::Tabs { panels, active_ix } => { /* ... */ } } ``` ## Supply the appearance Nothing in `gpui_kit::base::dock` paints a color, a border, or a size. Two traits carry appearance in. ### `DockAreaRenderer` | Method | Supplies | Default | | --------------------- | ------------------------------------------------------------------ | ------------------------------ | | `frame` | The area's outermost element | Bare `div` | | `center_frame` | The column holding the center and bottom dock | Bare `div` | | `split_frame` | One split's frame | Bare `div` | | `render_split_handle` | The divider between two slots | `None` → base's one-pixel line | | `render_dock` | One dock's chrome: title strip, collapse affordance, resize handle | The content, unwrapped | | `build_placeholder` | The stand-in for a panel this build cannot construct | `None` → draws nothing | | `tab_group_renderer` | _required_ | — | ### `TabGroupRenderer` | Method | Supplies | Default | | ----------------------- | ---------------------------------------- | ---------------------------- | | `frame` | The group's outer element | Bare `div` | | `content_frame` | The element the displayed panel sits in | Bare `div` | | `render_tab_bar` | The tab strip | Nothing | | `render_active_panel` | How the displayed panel is placed | The panel, filling the frame | | `render_drop_indicator` | The highlight showing where a drop lands | Nothing | | `render_empty` | What an empty group shows | Nothing | ### Contexts A renderer never sees a drag event or a mouse position. Base attaches drag sources, drop hit-testing, focus and keyboard handling to the very elements the renderer returns, and hands it resolved state plus callbacks: **`TabGroupContext`** — `node()`, `panels()`, `active_ix()`, `active_panel()`, `drop_indicator()`, `is_zoomed()`, `is_collapsed()`, `can_close()`, `is_locked()`, `is_draggable()`, `is_droppable()`; and the actions `select_tab()`, `close()`, `toggle_zoom()`, `drag_panel()`, `drop_panel()`, `drop_item()`. **`DockContext`** — `placement()`, `size()`, `is_open()`, `is_collapsible()`; and `toggle()`, `resize_to()`. ```rust impl TabGroupRenderer for MySkin { fn render_tab_bar(&self, group: &TabGroupContext, _: &mut Window, cx: &mut App) -> AnyElement { h_flex() .children(group.panels().iter().enumerate().map(|(ix, panel)| { div() .child(my_title(panel, cx)) .when(ix == group.active_ix(), |this| this.font_semibold()) .on_click({ let group = group.clone(); move |_, window, cx| group.select_tab(ix, window, cx) }) })) .into_any_element() } } ``` Every hook is optional in the same way: decline one and you get base's minimum for it. `render_split_handle` is the clearest case — return `None` and the divider falls back to a one-pixel line colored from `Theme::resizable`, so a skin with no opinion about dividers implements nothing, while one that has an opinion replaces the paint without touching the hit area, the cursor, or the drag. ## Drag and drop A tab drag has three parts, and a skin supplies only the middle one. **Starting.** `TabGroupContext::drag_panel(ix, cx)` returns a `DragPanel` if that tab may be dragged (it declines when the group is locked, or when the panel is the last one holding a dock open). Hand it to GPUI's `on_drag`. The preview view you return is yours; base's own `DragPanel` renders nothing, because a preview is appearance. **Landing.** While a drag hovers, base resolves where it would land and exposes it as `TabGroupContext::drop_indicator()` — a `DropIndicator` carrying the rectangle the panel would occupy. Paint it in `render_drop_indicator`; the coordinates are relative to the content frame, so that frame must be positioned. **Applying.** `drop_panel()` on release turns the hover into a `TabGroupEvent::Drop { panel, source, target }`, which the area applies as a single `PaneTree::move_panel`. Dropping onto the middle merges into the group; dropping towards an edge splits there; dropping onto the tab strip inserts at that index. **Host-owned drags.** Anything of your own can be dropped into the dock. Wrap it in `AnyDrag`, and `drop_item()` reports it as `DockEvent::DragDrop { item, target }` where `target` names the tab group it landed on and the edge it resolved. The dock does not interpret the payload. ## Events | Emitter | Event | Meaning | | ------------ | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `DockArea` | `LayoutChanged` | Something changed. Fires on **every** edit — debounce before writing to disk | | `DockArea` | `DragDrop { item, target }` | A host-owned drag landed | | `TabGroup` | `Drop` / `DragDrop` / `ClosePanel` / `ActiveChanged` / `ZoomIn` / `ZoomOut` | A group's intent, applied by the area | | `Panel` | `ZoomIn` / `ZoomOut` / `LayoutChanged` | A panel's own signal | Container events are the container asking the area for something; the area is what actually edits the tree. A host normally subscribes only to `DockEvent`. ## Persistence ```rust let state: DockAreaState = area.read(cx).dump(cx); let json = serde_json::to_string(&state)?; let state: DockAreaState = serde_json::from_str(&json)?; area.update(cx, |area, cx| area.load(state, window, cx))?; ``` `DockAreaState` carries the version you passed to `DockArea::new`, the center, and each dock with its placement, size and open state. Every node writes its shape and every panel writes whatever its `dump` returned. Panels are rebuilt through a global registry, keyed by `panel_name`: ```rust register_panel(cx, "FilesPanel", |context, window, cx| { let state = context.state(); // the PanelState this panel dumped Arc::new(FilesPanel::restore(state, cx)) as Arc }); ``` Three behaviors worth knowing: - **An unregistered panel is not dropped.** It becomes a placeholder carrying the original state forward, so a layout saved by a build that had a panel yours does not know still round-trips intact instead of losing it. - **Slot sizes are resolved on the way out.** `dump` writes the sizes the split is actually drawn at, not the ones the tree was built from, and never writes a zero. - **`LayoutChanged` fires far more often than you want to save.** Debounce, or save on a timer or on window close. ## How this compares Docking layouts are well-trodden. Where implementations differ is how far the layout engine is separated from what draws it — and that choice has consequences you can observe. ### Architecture | Project | Stack | Engine and rendering | What a consumer can change | | ---------------------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | [Qt](https://doc.qt.io/qt-6/qdockwidget.html) | C++, retained | `QMainWindow` owns four fixed dock areas; a `QDockWidget` _is_ a widget | Subclass the widget; styling via QSS | | [AvalonDock](https://github.com/Dirkster99/AvalonDock) | C#/WPF, retained | `LayoutRoot` tree of layout elements | XAML templates and themes | | [Dear ImGui](https://github.com/ocornut/imgui/wiki/Docking) | C++, immediate | `DockSpace()` is a region any window may dock into; nodes are engine-internal | Style vars and colors | | [egui_dock](https://docs.rs/egui_dock/) | Rust, immediate | `DockState` holds surfaces, each with a `Tree` of `Node`s | `TabViewer` renders tab bodies; a `Style` struct tunes the chrome | | [dockview](https://dockview.dev/) | TypeScript, web | Framework-agnostic engine behind thin adapters | CSS variables, theme object, replace the tab component | | [FlexLayout](https://github.com/caplin/FlexLayout) | TypeScript, web | JSON model beside a React renderer | `onRenderTab` callbacks and CSS | | [golden-layout](https://golden-layout.com/) | TypeScript, web | Engine owns its DOM outright | CSS overrides | | [rc-dock](https://github.com/ticlo/rc-dock) | TypeScript, web | `BoxData` / `PanelData` / `TabData` model | Custom tab rendering and CSS | | [VS Code](https://code.visualstudio.com/api/ux-guidelines/panel) | TypeScript, app | Workbench owns the layout | Contributed views, themed via CSS | | [Zed](https://zed.dev/) | Rust, app | `PaneGroup` built into the application | Not reusable outside its host | | **`gpui-base`** | Rust, retained | Pure-data `PaneTree`; the engine paints nothing | Renderer traits return elements — there is no default look to override | Three families are visible in that table. **Application-owned** engines (VS Code, Zed) are the most capable and the least reusable — you cannot lift them out of their host. **Widget-tree** engines (Qt, AvalonDock, golden-layout) make the dockable thing a widget, so the layout _is_ the view hierarchy. **Model-and-renderer** engines (FlexLayout, rc-dock, dockview, egui_dock, and this one) keep a separate description of the layout and hand rendering to something else. `gpui-base` sits at the far end of the third family: the engine paints nothing at all. In a library that draws its own chrome, customization is a set of overrides layered onto a default appearance, and you are limited to the seams it chose to expose. Here a renderer returns elements and base attaches behavior to _those_ elements, so two unrelated appearances can sit over one behavior — `crates/component/src/dock` and the example below are exactly that. ### The closest relative [egui_dock](https://docs.rs/egui_dock/) is worth a paragraph of its own, because it arrives at nearly the same shape from the other side of the retained/immediate divide. It keeps a `DockState` holding surfaces, each surface a `Tree`, each tree a hierarchy of `Node`s split into leaf and split variants — which is this design, down to the vocabulary, and it even calls its render entry point `DockArea`. Two differences matter. Its `TabViewer` renders **tab bodies**, while the crate itself draws the tab bars and splitters through a `Style` struct; here the split is the other way round — panels render themselves, and the skin draws all the chrome, with no `Style` struct because there is nothing built in to configure. And because egui is immediate-mode, its tree is walked and re-emitted every frame by construction; here the tree is a value that changes only when edited, and reconciliation against a stable `NodeId` cache is what keeps entities alive across edits. egui_dock also has something this does not: **undocking a tab into a floating OS window**, modeled as additional surfaces. That is a real capability gap, not a design difference. ### What the data model buys The layout being a value rather than a widget tree is not an aesthetic preference. Three properties follow: **A drag does not reset what it did not touch.** When containers _are_ views — the Qt and AvalonDock model — rearranging the layout means creating and dropping views, so a drag can reset state (scroll offsets, focus, in-progress input) in panels that merely shared a parent with the one being moved. Here identity is a `NodeId` that survives every edit and every normalization rule, so reconciliation is a diff against the entity cache: a steady-state pass creates and drops nothing. **Collapse is a pure function, not a deferred cascade.** When the last panel leaves a group, the group must remove itself from its parent, which may empty the parent in turn. With containers as views this is mutual recursion between two types reaching upward through parent handles — and those handles must be installed after construction, which in GPUI means a deferred pass, which means a window in which the tree disagrees with itself. `normalize` is one post-order pass to a fixpoint: no parent pointers, no deferred work, and the tree is self-consistent the instant an edit returns. **Editing costs no rendering.** `insert_panel`, `move_panel`, `split` and the rest operate on a value. They allocate no entities and request no layout, so a sequence of edits can run and be inspected before anything is drawn. The same property is why the whole layout algebra is tested as plain `#[test]` with no `TestAppContext` — which is why the collapse rules have the coverage they do. The cost, stated plainly: an edit clones the tree once to diff it, and normalization walks it until it reaches a fixpoint (two passes on realistic layouts, with a hard ceiling well above that). For dock-sized trees — tens of nodes — both are negligible against a frame, and they buy the three properties above. This would be the wrong shape for a structure with thousands of nodes. ### Naming The vocabulary follows the neighborhood where it can, which matters if you already know one of these systems: | Concept | Qt | egui_dock | dockview | VS Code | Zed | `gpui-base` | | ------------------------- | -------------------- | ----------- | ------------- | --------------- | ----------- | ----------- | | Window-level container | `QMainWindow` | `DockState` | `DockviewApi` | Workbench | `Workspace` | `DockArea` | | Tree arranging containers | — | `Tree` | Gridview | — | `PaneGroup` | `PaneTree` | | Tree node | — | `Node` | — | — | `Member` | `PaneNode` | | Tab group | (stacked docks) | `LeafNode` | Group | View Container | `Pane` | `TabGroup` | | Content in a tab | `QDockWidget` | `Tab` | `Panel` | `View` | `Item` | `Panel` | | Edge region | `Qt::DockWidgetArea` | — | — | Panel / Sidebar | `Dock` | `Dock` | One caution, because the field uses the word inconsistently: here a **`Panel` is the dockable content**, matching dockview. VS Code calls the _bottom region_ a panel; rc-dock calls the _tab container_ one. Translate the word before porting concepts from either. Qt is the outlier worth noting: it has no separate node type at all, because `QDockWidget` is both the content and the thing the layout arranges. That is the design this one is furthest from. ## Runnable example Everything above, on `gpui-base` alone — panels, a layout, and a skin implementing all three renderer traits. Nothing in it depends on `gpui-component`, which is the point: base is usable on its own, and a host that wants a different look writes a different skin. ```bash cargo run -p gpui-base dock ``` Source: [`showcase/components/dock.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/dock.rs). It is the same file the preview at the top of this page compiles to WebAssembly. ## Integration checklist - Give every panel a `panel_name` you will never change, and register a builder for it before calling `load`. - Install a renderer, or accept that nothing but the panels themselves is drawn. - Release resources in `on_removed`, not only in `set_active(false)` — a departing panel is never told `false`. - Debounce `DockEvent::LayoutChanged` before persisting; it fires on every step of a drag. - Prefer `visible` over removal when a panel should come back in the same place. - Position the element you return from `content_frame`, or the drop indicator will have nothing to anchor to. --- # Getting Started Source: /base/getting-started Applications use `gpui_kit::open_window(options, cx, build)`, which creates a Base `Root` and returns the window handle and content entity. Base owns window structure and overlay hosting without depending on Component. Initialize Component explicitly before opening windows when styled overlays are needed. ## Install Use the repository revision of GPUI that matches `gpui-base`: ```toml [dependencies] gpui-base = { git = "https://github.com/MohsenDastaran/uni-kit" } gpui = { git = "https://github.com/zed-industries/zed" } gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit"] } ``` ## Initialize Call `gpui_kit::base::init` once before opening windows. If the application already calls `gpui_kit::component::init`, base initialization is included. ```rust use gpui_kit::AppContext as _; fn main() { gpui_platform::application().run(|cx| { gpui_kit::base::init(cx); // Open your application window here. }); } ``` ## Render and style a control Base controls intentionally have no product-specific padding, colors, or radius. Style them with ordinary GPUI methods: ```rust use gpui_kit::prelude::*; use gpui_kit::{px, rgb}; use gpui_kit::base::Button; Button::new("save") .px_3() .py_2() .rounded(px(6.)) .bg(rgb(0x2563eb)) .text_color(rgb(0xffffff)) .on_click(|_, _, _| println!("save")) .child("Save") ``` Keep each `ElementId` stable across renders so GPUI can preserve element and focus state. Controlled components such as Checkbox, Switch, Radio, and Toggle report the next value through callbacks; store that value in your view and pass it back on the next render. ## Default color tokens `gpui-base` provides readable light and dark semantic palettes through `ColorTokens::light()` and `ColorTokens::dark()`. `ColorTokens::default()` uses the light palette. Both palettes use `Hsla` values and match the semantic roles of the default `gpui-component` themes. ```rust use gpui_kit::base::{ColorTokens, SemanticThemeTokens, Theme}; // Pick the palette that matches the application's current appearance. let colors = if is_dark { ColorTokens::dark() } else { ColorTokens::light() }; Theme::global_mut(cx).tokens = SemanticThemeTokens { colors, ..Default::default() }; ``` The palette contains semantic roles rather than component-specific colors: `background` and `foreground`, `surface` and `surface_foreground`, `primary`, `secondary`, `muted`, `accent`, `destructive`, `border`, `input`, `ring`, and `selection`, including the corresponding foreground roles. Base components derive what they can from these roles — a link takes `primary`, for instance — rather than adding a component-specific token for it. `selection` is its own role because no other one can stand in for it: it is painted under the glyphs and has to stay legible there, which neither `accent` nor `ring` guarantees. Calling `gpui_kit::component::init` projects its active light or dark theme into the same Base tokens automatically. Applications that use only `gpui-base` should install the matching palette when their appearance mode changes. ## Run the shared examples The examples used by this website also run as a native GPUI application: ```sh cargo run -p gpui-base-examples -- button ``` Replace `button` with a primitive slug from the [primitive catalog](/base/primitives). The website compiles the same showcase for `wasm32-unknown-unknown` and loads it on each primitive page. --- # Hover Card Source: /base/primitives/hover-card A delayed floating card associated with a pointer or keyboard trigger. Like every `gpui-base` primitive, Hover Card supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. On iOS and Android, the trigger toggles the card on click and an outside click dismisses it. Hover and its open/close delays are ignored. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- hover-card ``` ## Import ```rust use gpui_kit::base::{HoverCard}; ``` ## Anatomy and API The example composes `HoverCard`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/hover_card.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/hover_card.rs). Native and browser previews compile this same file. ## State and events Pointer or focus entry schedules opening and exit schedules dismissal. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/hover_card.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Expose it from keyboard focus and duplicate essential information outside hover-only content. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Popup Source: /base/primitives/popup `Popup` owns trigger measurement, anchor positioning, deferred rendering, and window-edge snapping. The application owns open state, content, appearance, and motion. Higher-level primitives such as Popover build on the same floating-surface ideas. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- popup ``` ## Import ```rust use gpui_kit::base::Popup; ``` ## Anatomy and API The example composes `Popup`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/popup.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/popup.rs). Native and browser previews compile this same file. ## State and events The caller owns trigger, anchor, open state, content, and dismissal policy. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/popup.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility The caller must supply suitable menu, listbox, or dialog semantics and focus policy. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Select Source: /base/primitives/select A button-like selection control backed by an anchored, keyboard-navigable popup. Like every `gpui-base` primitive, Select supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- select ``` ## Import ```rust use gpui_kit::base::{Select}; ``` ## Anatomy and API The example composes `Select`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/select.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/select.rs). Native and browser previews compile this same file. ## State and events The delegate/state owns items and selection; activation opens the list and selection closes it. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/select.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Set `.accessibility_label(...)` on the controlled root and `.accessibility_value(...)` to its committed selection, not a temporary search cursor. The root exposes its expanded state and accessible activation. Activation requests an open-state change and moves focus between the trigger and content. Disabled controls do not expose activation. The styled `Select` supplies its committed value automatically, falling back to its placeholder when unselected. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Progress Source: /base/primitives/progress Composable track and indicator parts for reporting task completion. Like every `gpui-base` primitive, Progress supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- progress ``` ## Import ```rust use gpui_kit::base::{Progress, ProgressIndicator, ProgressTrack}; ``` ## Anatomy and API The example composes `Progress`, `ProgressIndicator`, `ProgressTrack`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/progress.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/progress.rs). Native and browser previews compile this same file. ## State and events Set the value on `Progress`; size and position `ProgressIndicator` inside `ProgressTrack`. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/progress.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Expose task label and numeric value; use indeterminate state only when progress is unknown. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Switch Source: /base/primitives/switch A controlled on/off control with separately styleable track and thumb. Like every `gpui-base` primitive, Switch supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- switch ``` ## Import ```rust use gpui_kit::base::{Switch, SwitchThumb, SwitchTrack}; ``` ## Anatomy and API The example composes `Switch`, `SwitchThumb`, `SwitchTrack`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/switch.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/switch.rs). Native and browser previews compile this same file. ## State and events Pass the controlled boolean to `checked`; `on_change` emits the requested next value. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/switch.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Label the setting, expose checked state, and keep the accessible name stable. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Popover Source: /base/primitives/popover An anchored floating surface with controlled or internally managed open state. Like every `gpui-base` primitive, Popover supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- popover ``` ## Import ```rust use gpui_kit::base::{Popover}; ``` ## Anatomy and API The example composes `Popover`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/popover.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/popover.rs). Native and browser previews compile this same file. ## State and events Use `.anchor(Anchor::TopCenter).offset(px(8.))` to open below the trigger, centered, with an eight-pixel gap. The Base offset defaults to zero. `Top*` anchors open below, `Bottom*` above, `LeftCenter` to the right, and `RightCenter` to the left. The anchor names the popup's own point. Window-edge clamping does not flip the popup or change its anchor. `on_position` observes resolved popup and trigger bounds before content prepaint for custom presentation. Base does not draw an arrow; styled Component Popover provides `.arrow(true)` directly (default `false`), aligned to its anchor. Open state can be parent-controlled; activation, outside click, and Escape request lifecycle changes. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/popover.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Support Escape/outside dismissal and return focus; move focus only when its content requires it. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Slider Source: /base/primitives/slider A state-driven range input with independently styleable track, indicator, and thumb. Like every `gpui-base` primitive, Slider supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- slider ``` ## Import ```rust use gpui_kit::base::{Slider, SliderState}; ``` ## Anatomy and API The example composes `Slider`, `SliderState`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/slider.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/slider.rs). Native and browser previews compile this same file. ## State and events `SliderState` owns bounds and value; track, indicator, and thumb are separate visual parts. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/slider.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Expose label, current value, and bounds; support keyboard increments. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Combobox Source: /base/primitives/combobox A text input paired with keyboard-navigable suggestions and selection behavior. Like every `gpui-base` primitive, Combobox supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- combobox ``` ## Import ```rust use gpui_kit::base::{Combobox}; ``` ## Anatomy and API The example composes `Combobox`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/combobox.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/combobox.rs). Native and browser previews compile this same file. ## State and events The input state owns query text while the delegate supplies choices, filtering, and selection. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/combobox.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Synchronize input, popup, active option, and selected value; make every option keyboard reachable. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Editor Source: /base/primitives/editor `Editor` is the source-code editing control. It builds on the shared text engine and adds a language, line-number gutter, folding, whitespace display, text decorations, highlighting, search infrastructure, diagnostics, and LSP hooks. Use [Input](/base/primitives/input) for single-line values and [Textarea](/base/primitives/textarea) for ordinary multi-line text. ## Language editing rules The Base editor accepts `LanguageConfig` and independent `auto_close` / `smart_indent` preferences. It loads registered language configurations without a parser; Component installs a `LanguageProvider` for built-in names, defaults, and syntax providers during initialization. Base clients can install their own service with `set_language_provider`; configurations are set with `set_language_config`. See [Language editing rules](/component/editor#language-editing-rules) for the configuration fields and language registration; import the same types from `gpui_kit::base::input` when using Base directly. ## Keyboard shortcuts The base and styled editors share keyboard and mouse behavior. See [Keyboard shortcuts and column selection](/component/editor#keyboard-shortcuts-and-column-selection) for the macOS, Linux, and Windows bindings, multi-cursor editing, and column-selection details. ## Search The editor has a built-in search panel. Press `Ctrl-F` (Windows/Linux) or `Cmd-F` (macOS) while the editor is focused to open it. See [Search](/component/editor#search) for the programmatic API (`open_search`, `close_search`, `set_searchable`) and read-only behavior. ## Import ```rust use gpui_kit::base::input::{Editor, EditorState, TabSize}; ``` ## Basic usage ```rust let editor = cx.new(|cx| { EditorState::new(window, cx) .language("rust") .line_number(true) .folding(true) .tab_size(TabSize { tab_size: 4, hard_tabs: false, }) .default_value("fn main() {\n println!(\"Hello\");\n}") }); Editor::new(&editor) ``` ## Whitespace and decorations ```rust let editor = cx.new(|cx| { EditorState::new(window, cx) .language("rust") .show_whitespaces(true) .default_value(source) }); let decorations = editor.update(cx, |state, cx| { state.create_decorations_collection(initial_decorations, cx) }); ``` Decoration collections track ranges as the text changes. Keep the returned handle to update or clear its entries; dropping it does not remove decorations. `create_range_decorations_collection` creates an independent collection of paint-only `RangeDecoration` fills or frames. Text and geometric collections share UTF-8 normalization and edit tracking. See [Geometric range decorations](/component/editor#geometric-range-decorations) for ownership, boundary affinity, deletion, undo/redo, folding, layering, and indexing semantics; import the same types from `gpui_kit::base::input`. ## Highlighting and language features `InputHighlighterFactory`, `InputHighlighter`, diagnostic types, and the LSP provider traits are low-level extension seams for design-system authors. They operate on the shared `InputBaseState`; applications using the styled component normally configure these through their editor integration rather than through ordinary text fields. The runnable showcase demonstrates this seam with `syntect`. It selects the WASM-compatible `fancy-regex` backend rather than the native Oniguruma backend, so the same Rust highlighting adapter runs in the desktop example and the Base WASM example. Syntect only identifies syntax scopes: the adapter maps those to semantic names and resolves their styles through `HighlightStyleResolver`, so the application theme remains the source of colors and font styles. The adapter is intentionally simple and reparses the short sample after each edit; production integrations can keep incremental parser state in their `InputHighlighter` implementation. ## Font The editor has no font setting of its own: it paints with the ambient text style, so the family, size, weight, and line height come from the element the application wraps it in. ```rust div() .font_family("JetBrains Mono") .text_size(px(13.)) .child(Editor::new(&editor)) ``` A relative `line_height` keeps the rows in step with the glyphs at any size; an absolute one stays put. For a ready-made monospace treatment, see the [`gpui-component` Editor](/component/editor). ## Presentation The application owns editor colors, gutter appearance, fold icons, and overlay content. Use `InputEditorStyle`, `FoldIconRenderer`, and the provider traits to connect those adapters. For the repository's ready-made visual treatment, see the [`gpui-component` Editor](/component/editor). ## Runnable example ```bash cargo run -p gpui-base-examples -- editor ``` --- # Table Source: /base/primitives/table Semantic table primitives for composing headers, bodies, rows, and cells. Like every `gpui-base` primitive, Table supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- table ``` ## Import ```rust use gpui_kit::base::{Table, TableBody, TableCell, TableHead, TableHeader, TableRow}; ``` ## Anatomy and API The example composes `Table`, `TableBody`, `TableCell`, `TableHead`, `TableHeader`, `TableRow`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/table.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/table.rs). Native and browser previews compile this same file. ## State and events Rows and cells are stateless composition; sorting, selection, and mutations remain in the parent. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/table.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Use headers, preserve reading order, and separately expose sort and selection controls. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Tabs Source: /base/primitives/tabs A tab list and accessible tab controls with controlled selection. Like every `gpui-base` primitive, Tabs supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- tabs ``` ## Import ```rust use gpui_kit::base::{Tab, Tabs}; ``` ## Anatomy and API The example composes `Tab`, `Tabs`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/tabs.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/tabs.rs). Native and browser previews compile this same file. ## State and events The parent owns selected index/value; each tab reflects it and click handlers update the parent. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/tabs.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Associate tabs with panels, expose selection, and support keyboard traversal. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Calendar Source: /base/primitives/calendar A state-driven date grid with selection matchers and custom item rendering. Like every `gpui-base` primitive, Calendar supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- calendar ``` ## Import ```rust use gpui_kit::base::{Calendar, CalendarState}; ``` ## Anatomy and API The example composes `Calendar`, `CalendarState`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/calendar.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/calendar.rs). Native and browser previews compile this same file. ## State and events Selection lives in `CalendarState`; configure matching and update the state from calendar item interaction. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/calendar.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Label dates and selected, disabled, and today states; retain arrow-key navigation. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Tree Source: /base/primitives/tree A virtualized hierarchical list with explicit expansion and selection state. Like every `gpui-base` primitive, Tree supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- tree ``` ## Import ```rust use gpui_kit::base::{Tree, TreeItem, TreeState}; ``` ## Anatomy and API The example composes `Tree`, `TreeItem`, `TreeState`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/tree.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/tree.rs). Native and browser previews compile this same file. ## State and events `TreeState` owns items, expansion, and selection; tree actions update that entity. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/tree.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Expose hierarchy, level, expansion, and selection; preserve keyboard movement and visible focus. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Link Source: /base/primitives/link An accessible link-like control with application-defined styling. Like every `gpui-base` primitive, Link supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- link ``` ## Import ```rust use gpui_kit::base::{Link}; ``` ## Anatomy and API The example composes `Link`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/link.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/link.rs). Native and browser previews compile this same file. ## State and events The link emits activation while the application defines URL or in-app navigation. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/link.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Use links for navigation, meaningful text, and a visible focus style. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Date Picker Source: /base/primitives/date-picker A focus-aware date input that composes calendar behavior with a popup. Like every `gpui-base` primitive, Date Picker supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- date-picker ``` ## Import ```rust use gpui_kit::base::{DatePicker}; ``` ## Anatomy and API The example composes `DatePicker`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/date_picker.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/date_picker.rs). Native and browser previews compile this same file. ## State and events The picker combines focus/input state with calendar selection. Retain its entities on the parent view. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/date_picker.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Label the input, announce locale-appropriate dates, and make the calendar keyboard operable. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Pagination Source: /base/primitives/pagination A controlled page navigator with explicit current and total page state. Like every `gpui-base` primitive, Pagination supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- pagination ``` ## Import ```rust use gpui_kit::base::{Pagination, PaginationState}; ``` ## Anatomy and API The example composes `Pagination`, `PaginationState`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/pagination.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/pagination.rs). Native and browser previews compile this same file. ## State and events `PaginationState` owns current and total pages; `on_change` reports valid requested pages. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/pagination.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Identify current page, label previous/next, and disable unavailable boundary actions. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Avatar Source: /base/primitives/avatar An image with composable fallback content for a person or entity. Like every `gpui-base` primitive, Avatar supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- avatar ``` ## Import ```rust use gpui_kit::base::{Avatar, AvatarFallback, AvatarImage}; ``` ## Anatomy and API The example composes `Avatar`, `AvatarFallback`, `AvatarImage`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/avatar.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/avatar.rs). Native and browser previews compile this same file. ## State and events `Avatar` is presentational. Supply fallback content for the image-loading and image-error paths. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/avatar.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Fallback text should identify the entity; decorative avatars should not duplicate nearby labels. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Radio Source: /base/primitives/radio A controlled single-choice item with selectable and disabled semantics. Like every `gpui-base` primitive, Radio supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- radio ``` ## Import ```rust use gpui_kit::base::{Radio}; ``` ## Anatomy and API The example composes `Radio`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/radio.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/radio.rs). Native and browser previews compile this same file. ## State and events Pass a controlled checked value; `on_change` reports selection and the parent clears peers. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/radio.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Give each option a label and place mutually exclusive options in a named group. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Collapsible Source: /base/primitives/collapsible A composable region that shows or hides content without prescribing its trigger styling. Like every `gpui-base` primitive, Collapsible supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- collapsible ``` ## Import ```rust use gpui_kit::base::{Collapsible}; ``` ## Anatomy and API The example composes `Collapsible`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/collapsible.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/collapsible.rs). Native and browser previews compile this same file. ## State and events Pass the controlled expanded value to `open`; update it from the trigger callback. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/collapsible.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Name the trigger, expose expanded state, and remove hidden content from focus order. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Accordion Source: /base/primitives/accordion A disclosure group composed from independently styleable header, trigger, and panel parts. Like every `gpui-base` primitive, Accordion supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- accordion ``` ## Import ```rust use gpui_kit::base::{Accordion, AccordionHeader, AccordionItem, AccordionPanel, AccordionTrigger}; ``` ## Anatomy and API The example composes `Accordion`, `AccordionHeader`, `AccordionItem`, `AccordionPanel`, `AccordionTrigger`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/accordion.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/accordion.rs). Native and browser previews compile this same file. ## State and events Controlled by `AccordionItem::open`; `AccordionTrigger::on_change` reports the next expanded state. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/accordion.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Name every trigger, expose expanded state, and remove collapsed panel content from the focus order. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Number Input Source: /base/primitives/number-input A numeric input with reusable increment, decrement, and step behavior. Like every `gpui-base` primitive, Number Input supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- number-input ``` ## Import ```rust use gpui_kit::base::{Decrement, Increment, NumberInput, NumberInputText}; ``` ## Anatomy and API The example composes `Decrement`, `Increment`, `NumberInput`, `NumberInputText`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/number_input.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/number_input.rs). Native and browser previews compile this same file. ## State and events The backing input state owns numeric text/value; step actions apply the configured limits. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/number_input.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Expose label, value, bounds, and keyboard-accessible step actions. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Scrollbar Source: /base/primitives/scrollbar `Scrollbar` is a custom-painted scrollbar connected to a GPUI scroll handle. It supports vertical, horizontal, and two-axis viewports; track clicks; thumb dragging; configurable visibility modes; typed paint styles; reduced motion; and reversible visibility and width transitions. `gpui-base` owns the interaction and transition lifecycle. Your application or design-system layer owns colors, geometry, timing, and entrance choreography. ## Run the example The native showcase and WASM preview use the same implementation: ```bash cargo run -p gpui-base-examples -- scrollbar ``` The source is available in [`components/scrollbar.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/scrollbar.rs). ## Imports ```rust use std::time::Duration; use gpui_kit::{div, px, rgb, ScrollHandle, Styled as _}; use gpui_kit::base::{ Scrollbar, ScrollbarAxis, ScrollbarEntrance, ScrollbarMode, ScrollbarMotion, ScrollbarStyles, ScrollbarTheme, Theme, }; ``` ## Basic usage Keep the `ScrollHandle` on persistent view state. Attach it to the scrollable content with `track_scroll`, then overlay a `Scrollbar` in the same relative container. ```rust pub struct ActivityList { scroll_handle: ScrollHandle, } impl ActivityList { pub fn new() -> Self { Self { scroll_handle: ScrollHandle::new(), } } fn render_list(&self) -> impl gpui_kit::IntoElement { div() .relative() .size_full() .overflow_scroll() .track_scroll(&self.scroll_handle) .child(div().children((1..=100).map(|row| { div().h_8().px_2().child(format!("Activity {row}")) }))) .child(Scrollbar::new(&self.scroll_handle)) } } ``` `Scrollbar::new` enables both axes. Use an axis-specific constructor when the container scrolls in only one direction: ```rust Scrollbar::vertical(&scroll_handle); Scrollbar::horizontal(&scroll_handle); Scrollbar::new(&scroll_handle).axis(ScrollbarAxis::Vertical); ``` The scrollbar is an absolute overlay. Its layout and hitboxes stay fixed while the painted track and thumb animate, so entrance motion does not move content or change the interaction geometry. ## Visibility modes Set a mode on one scrollbar, or omit `.mode(...)` to use the global `ScrollbarTheme` mode. ```rust Scrollbar::vertical(&scroll_handle).mode(ScrollbarMode::Scrolling); Scrollbar::vertical(&scroll_handle).mode(ScrollbarMode::Hover); Scrollbar::vertical(&scroll_handle).mode(ScrollbarMode::Always); ``` | Mode | Behavior | | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Scrolling` | Appears after scrolling or dragging. A visible scrollbar stays visible while hovered; leaving starts a fresh idle hold. Hover cannot reveal a fully hidden scrollbar. | | `Hover` | Appears when the pointer enters the scrollbar track. | | `Always` | Remains visible and skips visibility transitions. | All modes use a 6 px resting thumb by default. Track hover keeps that width. Thumb hover and active dragging target the 8 px active width. Width changes use the configured `expand` duration. Hidden track and thumb clicks are ignored. In `Scrolling` mode, a hidden thumb also does not retain a latent hover state that could expand it on the next scroll. ## Configure the global theme `ScrollbarTheme` uses private fields with consuming builders and readers. Set it during application initialization or when your design-system theme changes. ```rust fn install_scrollbar_theme(cx: &mut gpui_kit::App) { let styles = ScrollbarStyles::default() .track(|style| { style .width(px(16.)) .bg(rgb(0x000000).alpha(0.08)) }) .track_hover(|style| { style.bg(rgb(0x000000).alpha(0.12)) }) .track_active(|style| { style.bg(rgb(0x000000).alpha(0.16)) }) .thumb(|style| { style .width(px(6.)) .inset(px(4.)) .radius(px(3.)) .min_length(px(48.)) .bg(rgb(0x737373)) }) .thumb_hover(|style| { style.width(px(8.)).bg(rgb(0x525252)) }) .thumb_active(|style| { style.width(px(8.)).bg(rgb(0x404040)) }); let motion = ScrollbarMotion::default() .with_idle(Duration::from_secs(2)) .with_enter(Duration::from_millis(300)) .with_exit(Duration::from_millis(500)) .with_expand(Duration::from_millis(300)) .with_entrance(ScrollbarEntrance::Fade) .with_thumb_hover_entrance(ScrollbarEntrance::SlideAndFade); Theme::global_mut(cx).scrollbar = ScrollbarTheme::new() .with_mode(ScrollbarMode::Scrolling) .with_motion(motion) .with_styles(styles); } ``` The same values can be inspected without exposing the theme's fields: ```rust let scrollbar = &Theme::global(cx).scrollbar; let mode = scrollbar.mode(); let motion = scrollbar.motion(); let styles = scrollbar.styles(); ``` ## Motion behavior Base ships without product motion. `ScrollbarMotion::default()` uses a 2-second behavioral idle hold, but its `enter`, `exit`, and `expand` durations are zero. An application that does not install motion therefore gets immediate visibility and width changes. The example theme above produces this choreography: | Trigger | Entrance | | ------------------------------------- | ---------------------------------------------------------------- | | Scroll in `Scrolling` or `Hover` mode | `entrance`: fade in place | | Track hover in `Hover` mode | `entrance`: fade in place | | Thumb hover in `Hover` mode | `thumb_hover_entrance`: slide from the nearest edge while fading | | `Always` mode | Immediate; visibility motion is skipped | For `SlideAndFade`, a vertical scrollbar enters from the right and a horizontal scrollbar enters from the bottom. Opacity uses linear entrance progress; position uses cubic ease-out. Exit opacity and position use cubic ease-in. An interrupted transition samples its current opacity and position before changing direction. A zero duration adopts the target immediately, including when a transition is already running. GPUI's reduced-motion preference also sets visibility and width durations to zero. You do not need a separate reduced-motion theme. ## Per-instance styles Use `.styles(...)` to override the global styles for one scrollbar. Instance styles take precedence over theme defaults. ```rust Scrollbar::vertical(&scroll_handle).styles(|styles| { styles .track(|style| style.width(px(14.)).bg(rgb(0xf5f5f5))) .track_hover(|style| style.bg(rgb(0xe5e5e5))) .thumb(|style| { style .width(px(6.)) .inset(px(3.)) .radius(px(3.)) .min_length(px(40.)) .bg(rgb(0x737373)) }) .thumb_hover(|style| style.width(px(8.)).bg(rgb(0x525252))) .thumb_active(|style| style.width(px(8.)).bg(rgb(0x404040))) }) ``` `ScrollbarTrackStyle` supports `bg`, `border_color`, and `width`. `ScrollbarThumbStyle` supports `bg`, `width`, `inset`, `radius`, and `min_length`. ## Custom viewport geometry The viewport normally comes from `ScrollbarHandle::viewport_bounds`. Two overrides support composite or custom-painted controls: ```rust Scrollbar::vertical(&scroll_handle) .viewport_bounds(editor_content_bounds); Scrollbar::vertical(&scroll_handle) .viewport_from_layout(); ``` Use `viewport_bounds` when your painted viewport differs from the handle's layout bounds. Use `viewport_from_layout` when a positioned overlay container already represents the exact viewport, such as a table body below a fixed header. Override the content size only when the handle cannot report the complete scrollable extent: ```rust Scrollbar::vertical(&scroll_handle) .scroll_size(gpui_kit::size(px(800.), px(4_000.))); ``` ## Custom scroll handles `ScrollHandle`, `UniformListScrollHandle`, and `ListState` implement `ScrollbarHandle`. Custom scroll containers can implement the same trait: ```rust use gpui_kit::{Bounds, Pixels, Point, Size}; use gpui_kit::base::ScrollbarHandle; impl ScrollbarHandle for MyScrollState { fn viewport_bounds(&self) -> Bounds { self.viewport_bounds() } fn offset(&self) -> Point { self.offset() } fn set_offset(&self, offset: Point) { self.set_offset(offset); } fn content_size(&self) -> Size { self.content_size() } fn start_drag(&self) { self.set_scrollbar_dragging(true); } fn end_drag(&self) { self.set_scrollbar_dragging(false); } } ``` `start_drag` and `end_drag` are optional. Use them when the scroll container needs to suspend snapping, selection, or another behavior during thumb drag. Only the actively dragged axis receives `end_drag` on mouse-up. ## Stable identity `Scrollbar::new`, `vertical`, and `horizontal` derive an element ID from their call site. Set an explicit stable ID when the same call site produces multiple independent scrollbars: ```rust Scrollbar::vertical(&scroll_handle).id(("activity-list", panel_id)); ``` A stable identity preserves retained visibility and width animation state across renders. ## Complete showcase source The runnable example is embedded directly from the shared Rust source: <<< ../../../crates/base/examples/showcase/components/scrollbar.rs{rust} ## Accessibility and interaction checklist - Keep wheel, trackpad, and keyboard scrolling available on the underlying viewport. - Preserve the default full-track interaction hitbox even when the painted thumb is narrow. - Give the thumb adequate contrast in normal, hover, and active states. - Do not move layout or hitboxes to implement entrance animation. - Test `Scrolling`, `Hover`, and `Always` with reduced motion enabled. - Test vertical, horizontal, and two-axis overflow independently. --- # Alert Dialog Source: /base/primitives/alert-dialog A modal confirmation surface for actions that need an explicit decision. Like every `gpui-base` primitive, Alert Dialog supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- alert-dialog ``` ## Import ```rust use gpui_kit::base::{AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogDescription, AlertDialogPopup, AlertDialogTitle, AlertDialogTrigger}; ``` ## Anatomy and API The example composes `AlertDialog`, `AlertDialogAction`, `AlertDialogCancel`, `AlertDialogDescription`, `AlertDialogPopup`, `AlertDialogTitle`, `AlertDialogTrigger`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/alert_dialog.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/alert_dialog.rs). Native and browser previews compile this same file. ## State and events Opening and dismissal are managed by `AlertDialog`; application action buttons decide when destructive work is committed. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/alert_dialog.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Provide title and description, trap focus, offer cancel, and restore focus to the opener. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Time Field Source: /base/primitives/time-field A segmented time-of-day editor with a complete keyboard model and 24- or 12-hour clocks. Like every `gpui-base` primitive, Time Field supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- time-field ``` ## Import ```rust use gpui_kit::base::{HourCycle, TimeField, TimeFieldEvent, TimeFieldState, TimePrecision}; ``` ## Anatomy and API The example composes `TimeField` over a `TimeFieldState`. The field renders one `TimeFieldSegment` per hour, minute, optional second and optional AM/PM part, separated by `:`. Lay out and style the root with `Styled`, and decorate each segment through `TimeField::render_segment`; the slot receives a `TimeFieldSegmentState` with the segment, its value and whether it is selected. The authoritative module is [`components/time_field.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/time_field.rs). Native and browser previews compile this same file. ## State and events `TimeFieldState` owns the time, its precision (`TimePrecision::Minute` or `Second`) and hour cycle (`HourCycle::H23` by default, or `H12`). `set_time` replaces the value without emitting; user edits emit `TimeFieldEvent::Change`. The field is one Tab stop. Up/Down step the selected segment and wrap within it without carrying into the next unit, Left/Right and Tab/Shift-Tab move between segments, digits type a value with a two-digit buffer and advance once no further digit fits, `a`/`p` set AM or PM, and Backspace/Delete reset the segment. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/time_field.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility The root exposes `Role::TimeInput` with the formatted time as its value. Label the field, and keep the selected segment visibly distinct from the others. ## Notes Use tabular figures or fixed segment widths so the field does not change width while digits are typed. Verify focus, selected, disabled, and high-contrast appearances in the consuming design system. --- # Toggle Group Source: /base/primitives/toggle-group Coordinates a set of toggle controls as a single- or multiple-selection group. Like every `gpui-base` primitive, Toggle Group supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- toggle-group ``` ## Import ```rust use gpui_kit::base::{Toggle, ToggleGroup}; ``` ## Anatomy and API The example composes `Toggle`, `ToggleGroup`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/toggle_group.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/toggle_group.rs). Native and browser previews compile this same file. ## State and events The group coordinates single or multiple selection while children reflect group state. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/toggle_group.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Label the group, expose each selection state, and keep focus order predictable. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Button Source: /base/primitives/button An unstyled, accessible pressable with semantic state and keyboard activation. Like every `gpui-base` primitive, Button supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- button ``` ## Import ```rust use gpui_kit::base::{Button}; ``` ## Anatomy and API The example composes `Button`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/button.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/button.rs). Native and browser previews compile this same file. ## State and events Activation uses GPUI click handling. Styling for hover, active, focus, and disabled states remains application-owned. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/button.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Provide an accessible name, preserve keyboard activation, and expose disabled state. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Toggle Source: /base/primitives/toggle A controlled two-state pressable for persistent choices such as formatting. Like every `gpui-base` primitive, Toggle supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- toggle ``` ## Import ```rust use gpui_kit::base::{Toggle}; ``` ## Anatomy and API The example composes `Toggle`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/toggle.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/toggle.rs). Native and browser previews compile this same file. ## State and events Pass the controlled pressed value; `on_change` emits the requested next value. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/toggle.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Expose pressed state and keep a stable accessible name. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Dialog Source: /base/primitives/dialog A composable modal surface with focus management, backdrop, title, and close parts. Like every `gpui-base` primitive, Dialog supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- dialog ``` ## Import ```rust use gpui_kit::base::{Dialog, DialogBackdrop, DialogClose, DialogDescription, DialogPopup, DialogTitle, DialogTrigger}; ``` ## Anatomy and API The example composes `Dialog`, `DialogBackdrop`, `DialogClose`, `DialogDescription`, `DialogPopup`, `DialogTitle`, `DialogTrigger`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/dialog.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/dialog.rs). Native and browser previews compile this same file. ## State and events `Dialog` manages modal presentation and dismissal; application callbacks own submitted work. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/dialog.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Provide title, initial and return focus, a focus trap, Escape policy, and explicit close action. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Textarea Source: /base/primitives/textarea `Textarea` is for ordinary multi-line text. Its interface stays focused on text entry: rows, wrapping, auto-grow, value updates, insertion, replacement, and cursor position. Code-editor concepts are intentionally kept on [`Editor`](/base/primitives/editor). ## Import ```rust use gpui_kit::base::input::{InputEvent, Textarea, TextareaState}; ``` ## Fixed rows ```rust let notes = cx.new(|cx| { TextareaState::new(window, cx) .rows(5) .placeholder("Notes") .default_value("First line\nSecond line") }); Textarea::new(¬es) ``` ## Auto-grow The textarea grows between the supplied minimum and maximum row counts. Once it reaches the maximum, its content scrolls. ```rust let message = cx.new(|cx| { TextareaState::new(window, cx) .auto_grow(2, 8) .placeholder("Write a message") }); Textarea::new(&message) ``` ## Editing the value ```rust notes.update(cx, |state, cx| { state.insert("Appended text", window, cx); }); let cursor = notes.read(cx).cursor_position(cx); let value = notes.read(cx).value(); ``` Use `soft_wrap(false)` when visual wrapping is undesirable. Set `submit_on_enter(true)` only when Enter should submit instead of inserting a line break. `TextareaState` emits the same `InputEvent` variants as `InputState`. ## Presentation The control is unstyled. Your design system supplies the frame, height, colors, padding, and `InputEditorStyle`. For a styled control, see the [`gpui-component` Textarea](/component/textarea). ## Runnable example ```bash cargo run -p gpui-base-examples -- textarea ``` ## Atomic inline tokens Use tokens for mentions or references that users select and delete as a whole. Insert one through the TextareaState you already use for this control: ```rust use gpui_kit::base::input::InlineToken; notes.update(cx, |state, cx| { state.replace_with_token( InlineToken::new("person-1", "@alice").with_label("Alice"), window, cx, ).expect("valid reference"); }); ``` Tokens display as unstyled labels. Use the `token` slot to supply your own single-row element and `on_token_click` to open a reference. Copy and `value()` return the real text, such as `@alice`. Save drafts with `content()` and restore them with `set_value(content)` to keep their references. See [Input's token examples](/component/input#atomic-inline-tokens) for custom rendering, draft restoration and range units. Import the data types from `gpui_kit::base::input`. In JavaScript, use `TextareaState.new()` from `gpui-base`; it provides the same token methods. --- # Input Source: /base/primitives/input `Input` is the single-line text control in `gpui-base`. It owns editing behavior, focus, selection, keyboard input, IME, masking, validation, and events while the application supplies presentation. Use [Textarea](/base/primitives/textarea) for ordinary multi-line text and [Editor](/base/primitives/editor) for source code. ## Import ```rust use gpui_kit::base::input::{Input, InputEvent, InputState}; ``` ## Basic usage Create the persistent state once, then render `Input` with that entity: ```rust let input = cx.new(|cx| { InputState::new(window, cx) .placeholder("Account name") .default_value("Ada") }); Input::new(&input) ``` Read and update the value through the state: ```rust let value = input.read(cx).value(); input.update(cx, |state, cx| { state.set_value("Grace", window, cx); }); ``` ## Masking and validation ```rust let password = cx.new(|cx| { InputState::new(window, cx) .placeholder("Password") .masked(true) .validate(|value, _| value.chars().count() >= 8) }); ``` For formatted values, combine `mask_pattern`, `pattern`, `min`, `max`, `step`, or `step_by` as appropriate. `unmask_value()` returns the underlying value of a masked input. ## Events `InputState` emits `InputEvent::Change`, `PressEnter`, `Focus`, and `Blur`. ```rust cx.subscribe(&input, |this, state, event: &InputEvent, cx| { if matches!(event, InputEvent::Change) { this.value = state.read(cx).value(); cx.notify(); } }); ``` ## Presentation `gpui-base` does not install product styling. Supply `InputEditorStyle` to the state and compose the control inside your own frame. If you want the ready-made theme, sizing, borders, prefix/suffix slots, and clear button, use the styled [`gpui-component` Input](/component/input). ## Runnable example ```bash cargo run -p gpui-base-examples -- input ``` The implementation is in [`crates/base/examples/showcase/components/input.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/input.rs). ## Atomic inline tokens Use tokens for mentions or references that users select and delete as a whole. Insert one through the InputState you already use for this control: ```rust use gpui_kit::base::input::InlineToken; input.update(cx, |state, cx| { state.replace_with_token( InlineToken::new("person-1", "@alice").with_label("Alice"), window, cx, ).expect("valid reference"); }); ``` Tokens display as unstyled labels. Use the `token` slot to supply your own single-row element and `on_token_click` to open a reference. Copy and `value()` return the real text, such as `@alice`. Save drafts with `content()` and restore them with `set_value(content)` to keep their references. See [Input's token examples](/component/input#atomic-inline-tokens) for custom rendering, draft restoration and range units. Import the data types from `gpui_kit::base::input`. In JavaScript, use `InputState.new()` from `gpui-base`; it provides the same token methods. --- # OTP Input Source: /base/primitives/otp-input A multi-cell one-time-code input driven by a shared text state. Like every `gpui-base` primitive, OTP Input supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- otp-input ``` ## Import ```rust use gpui_kit::base::{OtpInput, OtpState}; ``` ## Anatomy and API The example composes `OtpInput`, `OtpState`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/otp_input.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/otp_input.rs). Native and browser previews compile this same file. ## State and events `OtpState` owns the complete code and active cell; visual cells share that state. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/otp_input.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Label the whole code, announce length/errors, and support paste. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Radio Group Source: /base/primitives/radio-group Groups radio items and provides keyboard navigation for a single selection. Like every `gpui-base` primitive, Radio Group supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- radio-group ``` ## Import ```rust use gpui_kit::base::{Radio, RadioGroup}; ``` ## Anatomy and API The example composes `Radio`, `RadioGroup`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/radio_group.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/radio_group.rs). Native and browser previews compile this same file. ## State and events The group coordinates one selected value and keyboard movement; `Radio` renders each option. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/radio_group.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Label the group, expose one checked item, and support arrow keys among enabled choices. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Color Picker Source: /base/primitives/color-picker State and interaction foundations for selecting colors in a custom picker UI. Like every `gpui-base` primitive, Color Picker supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- color-picker ``` ## Import ```rust use gpui_kit::base::{ColorPicker, ColorPickerEvent, ColorPickerState, ColorSwatch}; ``` ## Anatomy and API The example composes `ColorPicker`, `ColorSwatch`, and `ColorPickerState`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. `ColorPicker` is the controlled root: it carries the trigger's accessibility semantics and focus, opens on Confirm, and dismisses on Cancel. `ColorSwatch` is one selectable color in a palette, carrying radio semantics, an accessible hex name, and the hover and activation callbacks a picker previews and commits with. The authoritative module is [`components/color_picker.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/color_picker.rs). Native and browser previews compile this same file. ## State and events `ColorPickerState` owns the committed color, the transient preview shown while the user hovers or edits, the controlled open state, and the active panel. It also owns a hex `InputState` and four component `SliderState`s and keeps all of them in sync, so an application renders those with its own input and slider presentation rather than reconciling them itself. Committing a color emits `ColorPickerEvent::Change`. A color supplied to `default_value` cannot reach the hex field and sliders without a window, so call `sync_pending_value` from render; it is a no-op once nothing is pending. Retain the state's entity on the parent view. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/color_picker.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Provide a textual color value and keyboard controls; never communicate selection by color alone. The root exposes the trigger's expanded state, and each swatch exposes its hex value as its accessible name plus its selected state, so a palette never depends on color alone. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Toast Source: /base/primitives/toast A managed, animated stack of temporary status messages. Like every `gpui-base` primitive, Toast supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- toast ``` ## Import ```rust use gpui_kit::base::{Toast, ToastManager, ToastOptions, ToastStack}; ``` ## Anatomy and API The example composes `Toast`, `ToastManager`, `ToastOptions`, `ToastStack`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/toast.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/toast.rs). Native and browser previews compile this same file. ## State and events Push messages through toast state; transition status retains an item during entry and exit. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/toast.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Choose live-region priority carefully and avoid essential actions only in expiring content. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Resizable Source: /base/primitives/resizable Panel groups and resize handles for user-adjustable split layouts. Like every `gpui-base` primitive, Resizable supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- resizable ``` ## Import ```rust use gpui_kit::base::{ResizablePanel, ResizablePanelGroup, ResizableState, h_resizable, resizable_panel}; ``` ## Anatomy and API The example composes `ResizablePanel`, `ResizablePanelGroup`, `ResizableState`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/resizable.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/resizable.rs). Native and browser previews compile this same file. ## State and events Panel sizes live in resizable state; dragging handles updates adjacent panels subject to minimums. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Handle appearance Base owns a handle's hit band, its cursor and the drag; what is painted inside it is the consumer's. `ResizeHandleRenderer` is handed a `ResizeHandleContext` carrying the axis and a `ResizeHandleState` — `Idle`, `Hovered`, `Pressed` or `Dragging`. The last two are tracked by base because a drag takes the pointer out of the nine-pixel band almost at once, so GPUI's hover reads false for most of a drag. Returning `None` keeps base's own one-pixel line, so a renderer can override some handles and leave the rest alone. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/resizable.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Provide keyboard alternatives for handles and preserve usable minimum panel sizes. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Tooltip Source: /base/primitives/tooltip A delayed, positioned description associated with a trigger element. Like every `gpui-base` primitive, Tooltip supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- tooltip ``` ## Import ```rust use gpui_kit::base::{Tooltip}; ``` ## Anatomy and API The example composes `Tooltip`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/tooltip.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/tooltip.rs). Native and browser previews compile this same file. ## State and events Hover or focus schedules it and exit or blur dismisses it; content is descriptive. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/tooltip.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Show on focus as well as hover; tooltips supplement names and contain no required controls. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Primitives Source: /base/primitives GPUI Base primitives provide behavior without prescribing presentation. Each page documents the public import and the smallest useful composition. The live example above the page is built from `crates/base/examples` and can also run as a native GPUI application. ## Primitive catalog - [Accordion](/base/accordion) — A disclosure group composed from independently styleable header, trigger, and panel parts. - [Alert Dialog](/base/alert-dialog) — A modal confirmation surface for actions that need an explicit decision. - [Avatar](/base/avatar) — An image with composable fallback content for a person or entity. - [Button](/base/button) — An unstyled, accessible pressable with semantic state and keyboard activation. - [Calendar](/base/calendar) — A state-driven date grid with selection matchers and custom item rendering. - [Checkbox](/base/checkbox) — A controlled tri-state check control with a separately styled indicator. - [Collapsible](/base/collapsible) — A composable region that shows or hides content without prescribing its trigger styling. - [Color Picker](/base/color-picker) — State and interaction foundations for selecting colors in a custom picker UI. - [Combobox](/base/combobox) — A text input paired with keyboard-navigable suggestions and selection behavior. - [Date Picker](/base/date-picker) — A focus-aware date input that composes calendar behavior with a popup. - [Dialog](/base/dialog) — A composable modal surface with focus management, backdrop, title, and close parts. - [Hover Card](/base/hover-card) — A delayed floating card associated with a pointer or keyboard trigger. - [Input](/base/input) — A single-line text input with selection, masking, validation, and number stepping. - [Textarea](/base/textarea) — A multi-line text field with fixed rows, wrapping, and auto-grow behavior. - [Editor](/base/editor) — A source-code editor foundation with highlighting, gutter, folding, decorations, and LSP hooks. - [Link](/base/link) — An accessible link-like control with application-defined styling. - [Nav Stack](/base/nav-stack) — A navigation stack of views with push, pop, forward, and replace, and an animatable transition lifecycle. - [Number Input](/base/number-input) — A numeric input with reusable increment, decrement, and step behavior. - [OTP Input](/base/otp-input) — A multi-cell one-time-code input driven by a shared text state. - [Pagination](/base/pagination) — A controlled page navigator with explicit current and total page state. - [Popover](/base/popover) — An anchored floating surface with controlled or internally managed open state. - [Popup](/base/popup) — A low-level trigger and anchored floating-content host. - [Progress](/base/progress) — Composable track and indicator parts for reporting task completion. - [Radio](/base/radio) — A controlled single-choice item with selectable and disabled semantics. - [Radio Group](/base/radio-group) — Groups radio items and provides keyboard navigation for a single selection. - [Resizable](/base/resizable) — Panel groups and resize handles for user-adjustable split layouts. - [Scrollbar](/base/scrollbar) — An unstyled scrollbar connected to GPUI scroll or uniform-list handles. - [Select](/base/select) — A button-like selection control backed by an anchored, keyboard-navigable popup. - [Sheet](/base/sheet) — A modal surface that enters from an edge while managing dismissal and focus. - [Slider](/base/slider) — A state-driven range input with independently styleable track, indicator, and thumb. - [Switch](/base/switch) — A controlled on/off control with separately styleable track and thumb. - [Table](/base/table) — Semantic table primitives for composing headers, bodies, rows, and cells. - [Tabs](/base/tabs) — A tab list and accessible tab controls with controlled selection. - [Time Field](/base/time-field) — A segmented time-of-day editor with a complete keyboard model and 24- or 12-hour clocks. - [Toast](/base/toast) — A managed, animated stack of temporary status messages. - [Toggle](/base/toggle) — A controlled two-state pressable for persistent choices such as formatting. - [Toggle Group](/base/toggle-group) — Coordinates a set of toggle controls as a single- or multiple-selection group. - [Tooltip](/base/tooltip) — A delayed, positioned description associated with a trigger element. - [Tree](/base/tree) — A virtualized hierarchical list with explicit expansion and selection state. --- # Sheet Source: /base/primitives/sheet A modal surface that enters from an edge while managing dismissal and focus. Like every `gpui-base` primitive, Sheet supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- sheet ``` ## Import ```rust use gpui_kit::base::{Sheet}; ``` ## Anatomy and API The example composes `Sheet`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/sheet.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/sheet.rs). Native and browser previews compile this same file. ## State and events Open and dismissal mirror a dialog while placement chooses the entering edge. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/sheet.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Apply dialog semantics: title it, trap and restore focus, and provide close. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Checkbox Source: /base/primitives/checkbox A controlled tri-state check control with a separately styled indicator. Like every `gpui-base` primitive, Checkbox supplies behavior and semantic structure without imposing a product visual language. Apply GPUI styles and compose the exported parts to match your design system. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- checkbox ``` ## Import ```rust use gpui_kit::base::{Checkbox, CheckboxIndicator}; ``` ## Anatomy and API The example composes `Checkbox`, `CheckboxIndicator`. GPUI's standard styling and event traits provide presentation; these base types provide the interaction structure. The authoritative module is [`components/checkbox.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/checkbox.rs). Native and browser previews compile this same file. ## State and events Pass the controlled value to `checked`; `on_change` emits `CheckboxState`, including indeterminate. Keep controlled state on the parent render type or in a GPUI entity. Update it in callbacks and call `cx.notify()`; do not recreate persistent entities during every render. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/checkbox.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Keep label and control associated and expose checked, unchecked, indeterminate, and disabled states. ## Notes Use stable element IDs where accepted. Verify focus, hover, active, selected, disabled, reduced-motion, and high-contrast appearances in the consuming design system. --- # Nav Stack Source: /base/primitives/nav-stack A last-in-first-out stack of views, one visible at a time: push a view over the current one, pop back to the one below, or replace the top. It is SwiftUI's `NavigationStack`, Qt's `StackView`, and WinUI's `Frame`. Underneath it is a [History](/base/history) whose active entries run from the root through the current page. A popped page becomes a forward entry until the next push discards that forward branch, so `forward` brings it back the way WinUI's `GoForward` does. Like every `gpui-base` primitive, Nav Stack supplies behavior and semantic structure without imposing a product visual language. The pages are views you create, and how a change between them moves is decided by your item renderer. ## Example The [single native Cargo entrypoint](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/native/src/bin/components.rs) selects this primitive from the [shared showcase implementation](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/mod.rs). The same showcase is compiled once for the WASM preview above. ```bash cargo run -p gpui-base-examples -- nav-stack ``` ## Import ```rust use gpui_kit::base::{NavMotion, NavOperation, NavPage, NavStack, NavStackState}; use gpui_kit::base::motion::{PresencePhase, Transition}; ``` ## Anatomy and API `NavStackState` is the stack. It lives in a GPUI entity, holds `AnyView`s root first, and emits `NavStackEvent` after every change. | Method | Does | | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | | `push(view, motion, cx)` | Pushes over the current top. Into an empty stack it is immediate, like Qt's `initialItem`. | | `pop(motion, cx)` | Pops the top and returns it. The root is never popped, so this returns `None` at a depth of one. | | `pop_to_root(motion, cx)` | Pops everything above the root in one transition and returns those views. | | `forward(motion, cx)` | Brings back the most recently popped view over the current top and returns it. `None` when nothing has been popped since the last push. | | `replace(view, motion, cx)` | Swaps the top for `view` and returns the one replaced, keeping the forward views. On an empty stack it pushes. | | `clear(cx)` | Empties the stack and the forward views immediately. | | `depth()`, `is_empty()`, `current()`, `views()`, `forward_views()` | Read the stack. Show a back button when `depth() > 1`, a forward button when `forward_views()` is not empty. | `NavStack` is the element. It holds the entity, takes a `transition` to run each change under, and hands every mounted view to the `item` renderer as a `NavPage`. Style the element for size, background and clipping; it is positioned so that the two pages of a change can overlap. `NavPage` is what the renderer receives. It already fills the container. Read `phase()` (`Entering`, `Present` or `Exiting`), `operation()` (`Push`, `Pop` or `Replace`, or `None` once settled) and `progress()` (eased, `0.0` to `1.0`, shared by both pages of one change), refine the page with GPUI styles, and return it. The authoritative module is [`components/nav_stack.rs`](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/nav_stack.rs). Native and browser previews compile this same file. ## Animation Animation is decided at two levels, and both default to none: - **The stack.** `NavStack` without a `transition` never animates; every change switches on the spot. Give it a `Transition` to animate changes, and an `item` renderer to say how. - **The change.** Each `push`, `pop`, `pop_to_root` and `replace` takes a `NavMotion`, as UIKit's `animated:` and Qt's `StackView.Immediate` do per call. `NavMotion::Animated` runs the stack's transition; `NavMotion::Immediate` switches on the spot even on an animated stack, which is what restoring a stack at launch or jumping to a page from a command wants. ```rust stack.update(cx, |stack, cx| stack.push(detail, NavMotion::Animated, cx)); stack.update(cx, |stack, cx| stack.push(restored, NavMotion::Immediate, cx)); ``` ## Transitions After a push, pop or replace, the outgoing view stays mounted until the element's `Transition` finishes. Paint order follows the operation: a pushed or replacing page paints over the page it covers, and a popped page paints over the page it reveals, so a slide reads correctly in both directions. ```rust NavStack::new(&self.stack) .size_full() .overflow_hidden() .transition(Transition::new(Duration::from_millis(220))) .item(|page, _, _| { let offset = match (page.phase(), page.operation()) { (PresencePhase::Entering, Some(NavOperation::Push)) => 1.0 - page.progress(), (PresencePhase::Exiting, Some(NavOperation::Pop)) => page.progress(), _ => 0.0, }; page.left(relative(offset)).into_any_element() }) ``` The stack also switches immediately when the platform asks for reduced motion, whatever the renderer would have drawn. A new operation while a transition is running supersedes it, and the pages reverse from where they are rather than jumping. While a change runs, neither page takes pointer input. ## State and events Keep the `NavStackState` entity on the view that renders the stack and observe it, so a push from anywhere re-renders the host. A page that needs to navigate holds a `WeakEntity` of the stack, as the showcase page does. `views()` and `forward_views()` are enough for a history menu: list both, and pop or forward until the chosen page is current. The showcase page draws that list as a trail of page numbers, the pages ahead greyed out. Focus is not moved by the stack. `AnyView` carries no focus handle; a page that wants focus takes it when it is pushed, as it would anywhere else. ## Complete Rust example The complete implementation used by the runnable showcase is embedded directly from Rust source: <<< ../../../crates/base/examples/showcase/components/nav_stack.rs{rust} The command above supplies application initialization, window creation, and shared `BaseShowcase` state. ## Accessibility Announce the page change in the page itself: a heading at the top of each page gives assistive technology a landmark to land on after a push. The stack keeps only the current page interactive once a transition has finished. ## Notes Pages are entities. The stack retains the ones on it and the ones popped since the last push, which `forward` can bring back, so a page's own subscriptions and timers live until a push discards it or the stack is cleared. Verify reduced-motion behavior in the consuming design system. --- # VirtualList Source: /base/virtual-list # Virtual List Render a list of any length by drawing only the items currently on screen. Unlike `gpui_kit::uniform_list`, **each item may have a different size** — which is what makes it usable for tables with variable row heights, chat transcripts, and outline trees. Virtual List is infrastructure rather than a component: it has no appearance of its own, contributes no chrome, and imposes nothing on the items you return. You give it the sizes up front and a closure that renders a range. ## Why sizes up front Virtualization needs to know the total extent of the list and which items intersect the viewport **without rendering anything**. Two designs solve this: | Approach | How | Cost | | ------------------------ | ---------------------------------------------------------- | ----------------------------------------------------------------------------- | | `gpui_kit::uniform_list` | Every item is the same size, so offsets are multiplication | No per-item data, but no variable sizes | | Measure as you scroll | Render, measure, correct | Scrollbar jumps; scroll position drifts | | **`VirtualList`** | You supply every item's size | Exact offsets and a stable scrollbar, at the cost of knowing sizes in advance | The third is why `item_sizes` is a required argument rather than a callback. If your items are genuinely unmeasurable until drawn, compute a good estimate, or use a fixed row height and let content clip. ## Get started ```rust use std::rc::Rc; use gpui_kit::base::{v_virtual_list, VirtualListScrollHandle}; use gpui_kit::{px, size}; let sizes = Rc::new(vec![size(px(280.), px(32.)); 100_000]); v_virtual_list( cx.entity(), "customers", sizes, |_this, range, _window, _cx| { range .map(|ix| div().h_8().px_2().child(format!("Customer {ix}"))) .collect() }, ) .track_scroll(&self.scroll_handle) .size_full() ``` The closure is handed a `Range` — only the visible slice, plus a small overdraw — and returns one element per index in that range. It receives `&mut V` for the entity you passed, so it can read your data without cloning it into the closure. Use `h_virtual_list` for a horizontal list; everything else is identical. ## The size contract Three rules, and breaking any of them shows up as misplaced items rather than a panic. **Only one dimension is read.** A vertical list uses each `Size`'s `height` and ignores its `width`; a horizontal list uses `width` and ignores `height`. The value you pass for the unused axis is free. **The cross axis is measured, not declared.** A vertical list gets its width by laying out one item and measuring it — by default item 0. If your first item is not representative (an unusually short label, say), point it at one that is: ```rust v_virtual_list(entity, "rows", sizes, render) .with_item_to_measure_index(3) ``` **`item_sizes.len()` is the item count.** The list renders exactly that many items; there is no separate count argument. If the vector disagrees with your data, the extra indices are still requested from your closure. `Rc>>` is shared rather than owned so that rebuilding the element every frame does not reallocate the size table. Keep the `Rc` in your entity and clone the handle, rather than constructing the vector inside `render`. ## Scrolling `VirtualListScrollHandle` owns the scroll position and survives re-renders, so it belongs in your entity, not in `render`. ```rust struct CustomerList { scroll: VirtualListScrollHandle, } // Jump to an item self.scroll.scroll_to_item(4_200, ScrollStrategy::Top); // Follow a growing list self.scroll.scroll_to_bottom(); ``` `scroll_to_item` takes a `ScrollStrategy` — `Top`, `Center`, or `Bottom` — and works on indices that have never been rendered, because the offset comes from the size table rather than from measurement. `base_handle()` exposes the underlying GPUI `ScrollHandle` when you need it. Attach the handle with `.track_scroll(&handle)`. ## With a scrollbar `VirtualListScrollHandle` implements `ScrollbarHandle`, so the base `Scrollbar` reads it directly. The list draws no scrollbar of its own. ```rust div() .relative() .child( v_virtual_list(cx.entity(), "rows", sizes, render) .track_scroll(&self.scroll) .size_full(), ) .child(Scrollbar::vertical(&self.scroll)) ``` The container needs `relative()` because the scrollbar positions itself against it. ## Sizing behavior `with_sizing_behavior` controls whether the list computes a size of its own: - `ListSizingBehavior::Auto` (default) — the list does not calculate a fixed size, and takes the space its parent gives it. - `ListSizingBehavior::Infer` — the list calculates its size from its items. The default plus a bounded parent is what almost every layout wants. A virtual list inside an unbounded parent has nothing to virtualize against, so it would try to lay out every item. ## The render closure The closure runs on **every frame that the visible range changes**, so treat it as a hot path: - Do no I/O, sorting, or filtering inside it. Keep the prepared data in your entity and index into it. - Return elements, not entities. Creating a GPUI entity per row defeats virtualization — the entity outlives the frame, so a hundred thousand rows would create a hundred thousand entities. - Element ids, if you set them, should derive from the item index or a stable key, not from position within the returned vector. State lives outside the closure. Update it in your own callbacks and call `cx.notify()`; do not mutate it during render. ## What it costs Work per frame is proportional to the number of **visible** items, not total items — the example on this page holds 100,000 rows and draws about a dozen. The size table is the one part that scales with the total: it is one `Size` per item, held once behind an `Rc`. The tradeoff is that the size table must exist before the first frame. For a million rows of uniform height that is a megabyte of sizes for information a single number could carry — that is the case `gpui_kit::uniform_list` exists for, and it is the better choice there. ## Complete Rust example ```bash cargo run -p gpui-base-examples -- virtual-list ``` <<< ../../crates/base/examples/showcase/components/virtual_list.rs{rust} ## Checklist - Hold `item_sizes` and the scroll handle in your entity; rebuild neither during render. - Keep `item_sizes.len()` equal to your data length. - Point `with_item_to_measure_index` at a representative item if item 0 is not one. - Give the list a bounded parent, and `relative()` on that parent if you add a scrollbar. - Preserve logical order, item counts, and stable identity so assistive technology sees a coherent list across virtualization. --- # TextView Source: /base/text-view `gpui-base` owns the complete `TextView` implementation for rendering Markdown and common HTML. It includes document parsing, links, images, lists, tables, code blocks, scrolling, line clamping, plugins, selection, and copying without depending on `gpui-component`. The live example above uses only `gpui-base`. Its fenced Rust block is intentionally unhighlighted: syntax highlighting is opt-in. ## Set up the window Call `gpui_kit::base::init` once during application startup and render one `TextSelectionLayer` per window. The layer coordinates selection across `TextView`, [`SelectableText`](/base/text-selection), and custom text renderers. ```rust use gpui_kit::prelude::*; use gpui_kit::{Context, Render, Window}; use gpui_kit::base::{TextSelectionLayer, TextView}; impl Render for AppView { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { div() .size_full() .child(TextSelectionLayer) .child(TextView::markdown( "readme", "# Hello\n\nSelect and copy this **Markdown**.", )) } } ``` If the application already calls `gpui_kit::component::init`, Base initialization is included. A window using `gpui_base::Root`—including one opened by `gpui_kit::open_window`—installs the selection layer automatically; do not render a second layer in its content. TextView is selectable by default. While dragging a selection near a viewport edge, the shared selection layer scrolls the related `overflow_*_scroll` region automatically; no TextView scroll or selection parameter is required. Use `.selectable(false)` only to disable selection explicitly. ## Markdown and HTML Use the helpers for call-site-derived IDs, or constructors when an explicit stable ID is useful: ```rust use gpui_kit::base::{html, markdown, TextView}; let short_markdown = markdown("A **short** message."); let short_html = html("

A short message.

"); let preview = TextView::markdown("document-preview", markdown_source).scrollable(true); let article = TextView::html("article", html_source); ``` `scrollable(true)` makes the view fill its container and scroll vertically. Without it, the view grows to fit its content. `max_lines(n)` clamps a non-scrollable preview to at most `n` body-text lines. ## Complete default styling Every constructor starts with `TextViewStyle::default()`. The default contains readable neutral foreground, muted, link, selection, code-background, border, heading, paragraph, inline-code, and table styles. A Base-only application does not need to construct a style before rendering text. Override only the values owned by your design system: ```rust use gpui_kit::base::TextViewStyle; let style = TextViewStyle::default() .with_foreground(app_colors.foreground) .with_muted_foreground(app_colors.muted_foreground) .with_link(app_colors.link) .with_selection(app_colors.selection); TextView::markdown("themed", source).style(style) ``` Heading refinements receive the Markdown heading level (1-6) and are applied on top of the built-in size, weight, and spacing for that level: ```rust use gpui_kit::{StyleRefinement, Styled as _, rems}; let style = TextViewStyle::default().with_heading(|level| match level { 1 => StyleRefinement::default().pt(rems(1.)).pb(rems(0.75)), _ => StyleRefinement::default(), }); ``` `TextViewStyle::from_theme(&theme)` maps the semantic colors from a `gpui_kit::base::Theme`. Applications using the higher-level component theme can use `gpui_kit::component::text::text_view_style(cx.theme())`. ## Syntax highlighting is opt-in `gpui-base` does not enable syntax highlighting and has no tree-sitter language dependency. Fenced code blocks use the neutral code surface and plain foreground until the application supplies `code_block_highlighter`. The callback receives a `CodeBlock` and returns byte ranges paired with GPUI `HighlightStyle` values: ```rust use gpui_kit::HighlightStyle; use gpui_kit::base::TextView; TextView::markdown("highlighted", source).code_block_highlighter(|block| { my_highlighter(block.lang(), block.code()) .into_iter() .map(|(range, color)| { ( range, HighlightStyle { color: Some(color), ..Default::default() }, ) }) .collect() }) ``` Ranges are UTF-8 byte ranges relative to `CodeBlock::code()`. Invalid ranges are discarded. The highlighter implementation and its language registrations remain entirely application-owned. ## Markdown extensions `MarkdownExtensions` starts with CommonMark/GFM-compatible parsing. YAML frontmatter is disabled by default because it is not part of either standard. Enable the construct explicitly when a block parser or plugin handles `markdown_ast::Node::Yaml`: ```rust use gpui_base::{MarkdownExtensions, TextView}; let extensions = MarkdownExtensions::default().frontmatter(); TextView::markdown("metadata", source) .markdown_extensions(extensions) ``` Without a matching plugin, enabled YAML frontmatter uses the existing YAML code-block fallback. A custom plugin can be attached with `.plugin(...)`; `gpui-component` provides a themed `FrontmatterPlugin`; Base remains independent of that presentation. ## Inline plugin Implement `MarkdownPlugin` and register it with `.plugin(...)`, just like a Block plugin. A `MarkdownPlugin` with the default `is_block() == false` uses `render_inline`; block plugins keep `render`. ```rust use gpui::{App, Styled, Window, div}; use gpui_base::{ InlineElement, InlineRenderContext, MarkdownNode, MarkdownParseContext, MarkdownPlugin, TextView, markdown_ast, }; struct FormulaPlugin; impl MarkdownPlugin for FormulaPlugin { fn name(&self) -> &str { "formula" } fn parse( &self, node: &markdown_ast::Node, _: &MarkdownParseContext<'_>, ) -> Option { let markdown_ast::Node::InlineMath(math) = node else { return None; }; Some( MarkdownNode::new("formula", math.value.clone()) .text(math.value.clone()) .accessibility_label(format!("Formula: {}", math.value)), ) } fn render_inline( &self, node: &MarkdownNode, _: &InlineRenderContext, _: &mut Window, _: &mut App, ) -> Option { Some(InlineElement::new(div().italic().child(node.as_text().to_string()))) } } TextView::markdown("inline-formulas", "Formulas $x^2$ and $y^2$") .plugin(FormulaPlugin) ``` `render_inline` returns `Some(InlineElement::new(element))` for any GPUI `IntoElement`, including styled text, images, and composed elements. Use native GPUI styling, hover handlers, and child events. The renderer receives `InlineRenderContext` with the effective text style, font size, line height, and rem size. These rendering types are independent of Markdown; parsing and registration in this example remain Markdown-specific. TextView measures the element's intrinsic size and lays it out as one atom. Set `.with_baseline(px(...))` on `InlineElement` when the content needs an explicit baseline, measured from its top edge in logical pixels. Objects wrap only before or after the whole element. Fixed-size elements retain their dimensions even when wider than a line; constrain their size with GPUI styles where needed. TextView does not scale the entire element subtree. Use `MarkdownExtensions::parser_revision(config_version)` when parser captures or plugin configuration change without changing the registered names. Keep the revision stable for equivalent registrations rebuilt during rendering; changing it reparses the existing source. Compose a native `HoverCard` around the trigger to show a profile card. The Markdown example uses a `StyledText` label with a muted `@`, an underlined username, and a `HoverCard` anchored at `Anchor::TopCenter`. Plain copy of `[@huacnlee](mention:huacnlee)` emits the handle; Markdown copy retains the original link syntax. Selection treats the rendered element as a whole. Double-click selects an object; triple-click selects its mixed text line. Drag selection can cross text and consecutive objects in either direction. Child events remain native GPUI events, so plugin authors should coordinate interactive controls with TextView's selection gestures. `source_range()` exposes full-document UTF-8 byte offsets including delimiters. `.text(...)` supplies plain copy and fallback text; `.markdown(...)` supplies Markdown copy, defaulting to the original node source. Missing plain text falls back to source. `.accessibility_label(...)` supplies the accessible name, defaulting to the plain text. Returning `None` from `render_inline` uses atomic text fallback. For images, the plugin supplies loading and failure content through `img(...).with_loading(...).with_fallback(...)`. For asynchronous resources, retain a `TextViewState`, update the application-owned cache, then call `state.invalidate_inline_layout(cx)` through the view's weak entity. This remeasures inline content and virtual-list heights without reparsing or dropping the current logical selection. Associate results with source/font/theme keys and discard obsolete completions. Render callbacks should read prepared resources; do not run an equation engine synchronously during layout. `examples/markdown` contains the formula implementation and a preview zoom control. Inline math syntax is parsed by default. Register a plugin to customize its rendering; no separate syntax switch is needed. Inline code continues to protect dollar signs from math parsing. When no plugin claims a math node, TextView renders its original `$...$` source as literal text, so prose that merely contains dollar signs — `spent $5 and $10` — reads and copies back unchanged. Block math is parsed too: a `$$` fence becomes a block node, which a block plugin (`is_block() == true`) renders, and which falls back to a code block when no plugin claims it. ## Retained state and streaming updates Use `TextViewState` when content changes without replacing the view: ```rust use gpui_kit::base::{TextView, TextViewState}; let document = cx.new(|cx| TextViewState::markdown(initial_source, cx)); // Render TextView::new(&document) // Later document.update(cx, |state, cx| state.set_text(updated_source, cx)); ``` `TextViewMotion` is the view's motion policy. Base plays it but ships no timing: every duration defaults to zero, so an unstyled view adopts streamed text at once. Give `stream_fade` a duration to fade the text an update appends in where it lands, and optionally `stream_fade_stagger` to start each further word of one update a little after the one before it: ```rust use std::time::Duration; use gpui_kit::base::{Easing, TextView, TextViewMotion}; TextView::new(&document).motion( TextViewMotion::default() .with_stream_fade(Duration::from_millis(350)) .with_stream_fade_stagger(Duration::from_millis(30)) .with_stream_fade_easing(Easing::EaseOut), ) ``` Without a stagger each update fades as one chunk. With one, appended text is split into words with their trailing whitespace, and CJK text into characters; a long update compresses its stagger so the last word starts within one fade. The tracker compares rendered text rather than source bytes, so a `set_text` whose text extends the current one counts as an append, and Markdown that completes as it streams (`**bo` becoming bold `bold`) fades the changed glyphs rather than the whole paragraph. Only the blocks the update reaches are compared, and frames are requested only while something is still fading. Reduced motion skips the fade. `TextViewState::set_range_highlights` paints backgrounds behind ranges of `rendered_text()`, the text plain copy produces, so an application can show its search results or citations without reparsing or restyling the document. The ranges are painted, not shaped, so they never change layout. `reveal_range` scrolls the line a range starts on into view, through the view's own list, an enclosing `gpui::list`, or `TextView::on_reveal` for any other container; see [Highlight ranges](/component/text-view#highlight-ranges) and [Scroll to a range](/component/text-view#scroll-to-a-range). Selection can copy rendered text or Markdown source through `SelectionFormat`. Link routing, code-block actions, table actions, images, and custom Markdown plugins use the same builders as the compatibility API documented on the [gpui-component TextView page](/component/text-view). ## Runnable source The live preview and native command use the same Base-only source: <<< ../../crates/base/examples/showcase/components/text_view.rs{rust} ```bash cargo run -p gpui-base-examples -- text-view ``` --- # Motion Source: /base/motion `gpui-base` owns deterministic motion sampling and lifecycle while leaving every visual choice to the application. It provides stable keyed state, interruption, reversal, animation-frame requests, and reduced-motion behavior without imposing product timing or styling. Run the interactive companion for this guide: ```bash cargo run -p gpui-base-examples --bin motion ``` The example contains five separate demos. Use the tabs at the top to inspect one capability at a time. ## Capability map | Demo | API | What it demonstrates | | --- | --- | --- | | Sliding time | `transition` | Four independently rolling digits, 08:00–20:00, with targets changing faster than the transition settles | | Spring | `spring` | A segmented-control indicator that preserves velocity when rapidly retargeted | | Keyframes | `Keyframes`, `Timing`, `animate_keyframes` | A repeating multi-stop activity signal | | Stagger | `Stagger` | Allocation-free timing offsets across a list | | Presence | `Presence` | Exit animation that keeps content mounted until it becomes absent | | Sequence | `Sequence` | Three chained steps — slide in, fill, rest then fade — each starting when the last one ends | The library also exposes `Easing`, `Discrete`, `MotionTransform`, and `MotionReveal`. They compose with the same primitives rather than requiring separate animation runtimes. ## Target transitions Use `transition` for a value moving toward a target over a known duration. Every independently animated value needs a stable ID. ```rust let opacity = transition( ("save-dialog", "opacity"), if open { 1.0 } else { 0.0 }, Transition::new(Duration::from_millis(180)).easing(Easing::EaseOut), window, cx, ); ``` Retargeting starts at the currently sampled value. Direct reversal shortens the return duration, so reversing early does not spend a full duration retracing a short distance. `transition_with_status` additionally returns `Idle`, `Delayed`, `Running`, or `Finished`. `Easing` includes CSS keyword curves, cubic Bézier curves, all CSS step positions, and piecewise `linear()` stops. Invalid parameters return typed errors. ## Springs Use `spring` when the target may change while moving. It preserves both position and velocity, which makes it suitable for selection indicators and settling spatial values. ```rust let x = spring( "selected-indicator", selected_x, Spring::new(Duration::from_millis(420)).with_damping(0.72), window, cx, ); ``` Do not make a pointer-controlled value chase the pointer through a spring. Set `with_travel(false)` during direct manipulation and restore travel after release. `with_damping` requires a finite, non-negative ratio; `with_epsilon` requires a finite value greater than zero and interprets it in the target's own units. The builders panic for invalid trusted constants. Use `try_with_damping` and `try_with_epsilon` for configuration or user-provided values. Normalized values normally keep the `0.001` default; pixel motion can use a coarser tolerance such as `0.1`. ## Keyframes and timing `Keyframes` describes validated value stops. `Timing` uses absolute elapsed time and supports signed delays, finite or infinite iterations, and normal, reverse, or alternating playback. ```rust let frames = Keyframes::try_new([ Keyframe::new(0.0, 0.25), Keyframe::new(0.45, 1.0).ease(Easing::EaseOut), Keyframe::new(1.0, 0.25), ])?; let opacity = animate_keyframes( "activity", &frames, Timing::new(Duration::from_millis(1400)) .iterations(IterationCount::Infinite), window, cx, ).value; ``` Offsets must start at `0`, end at `1`, and be monotonic. Use `Discrete` when a value cannot be interpolated. `animate_keyframes` retains its playback start time under the supplied stable ID. Re-rendering with the same ID continues the current sequence. To replay it, include an application-owned generation in the ID, such as `("notification-enter", generation)`, and increment that generation for each replay. ## Presence and stagger `Presence` separates logical visibility from physical mounting. Its phases are entering, present, exiting, and absent. Render while `should_render()` is true and use `progress` for the chosen visual properties. Reopening during exit reverses from the current sample. `Stagger` calculates a delay for an index from the first, last, center, or a chosen origin. It does not allocate a schedule or own list identity: ```rust let stagger = Stagger::new(Duration::from_millis(80), StaggerOrigin::First); let delay = stagger.delay(index, item_count); ``` ## Sequences `Sequence` chains transitions so each step starts when the previous one ends. It begins at `from` on the first frame it is sampled and plays once per ID; its sample reports the value, the step being played, and a `MotionStatus` that reads `Finished` only after the last step. ```rust let opacity = Sequence::new(("toast", "opacity"), 0.0) .with_step(1.0, Transition::new(Duration::from_millis(160))) .with_step(0.0, Transition::new(Duration::from_millis(200)).delay(Duration::from_secs(3))) .sample(window, cx); div().opacity(*opacity.value()) ``` A step ends at an absolute instant and the next one starts there, not on the frame that noticed it, so a skipped frame does not start a step late. Zero-duration steps complete within one frame. Changing the target of the step being played restarts the sequence from its first step at the value sampled at that instant; steps not yet reached are read when the sequence gets to them. To replay, put an application-owned generation in the ID. Reduced motion adopts the last target at once with no pending frame. `Stagger` composes with a sequence as a delay on its first step: ```rust Sequence::new(("row", index), px(12.)) .with_step(px(0.), Transition::new(Duration::from_millis(120)).delay(stagger.delay(index, count))) .sample(window, cx) ``` ## Measured reveal `MotionReveal` measures a child at its natural size and clips its visible height by progress. `Collapsible::motion_id(...)` is the convenient control-level facade. Without a motion ID, the control keeps immediate mount/unmount behavior. ## Reduced motion and performance Transitions, springs, keyframes, presence, and reveal-compatible controls honor GPUI's reduced-motion preference. Finite motion snaps to the target, synchronizes retained state, and leaves no pending animation frame. Motion must never be the only way state is communicated. The preference is the operating system's. `gpui_base::init` (and so `gpui_component::init`) reads the system setting into `App::set_reduce_motion` — macOS's "Reduce motion" (`NSWorkspace.accessibilityDisplayShouldReduceMotion`), Windows' "Animation effects" (`SPI_GETCLIENTAREAANIMATION`, off means reduce), and on Linux the XDG desktop portal's `org.freedesktop.appearance` `reduced-motion` key, which arrives over D-Bus a moment after `init` and is then followed as it changes. Other targets, wasm included, leave the flag alone. An application that calls `cx.set_reduce_motion(...)` itself owns the flag from then on: Base only writes it while it still holds what Base last wrote. macOS and Windows are read once, at `init`; call `gpui_base::apply_system_reduce_motion(cx)` to read them again. The pure steady sampling paths measured by the benchmark—timing/easing, keyframe lookup, analytic spring integration, and stagger delay calculation—are allocation-free. Keyed transition, spring, presence, and reveal lifecycles are covered by GPUI retained-state and frame-request tests because those updates belong to the framework lifecycle rather than the pure sampler. Sampling uses absolute elapsed time, and keyframe lookup uses binary search. Run the release benchmark with: ```bash cargo bench -p gpui-base --bench motion ``` Choose the smallest suitable primitive: `transition` for duration-based targets, `spring` for changing spatial targets, keyframes for authored sequences, `Presence` for exit-before-unmount, `Sequence` for steps that follow one another, and `Stagger` for list choreography. ## Benchmark results Measured on Linux x86_64 with a release build, 31 batches, and 200 iterations per batch: | Workload | Median | P95 | Worst | Allocations | | --- | ---: | ---: | ---: | ---: | | 1,000 scalar timing + easing samples | 26.490 µs | 26.567 µs | 27.290 µs | 0 | | 1,000 keyframe samples, 2 frames | 21.656 µs | 21.707 µs | 21.729 µs | 0 | | 1,000 keyframe samples, 8 frames | 25.197 µs | 25.251 µs | 25.269 µs | 0 | | 1,000 keyframe samples, 32 frames | 27.932 µs | 27.969 µs | 27.971 µs | 0 | | 1,000 analytic spring integration samples | 6.042 µs | 6.106 µs | 6.216 µs | 0 | | 1,000 stagger delay calculations | 0.574 µs | 0.583 µs | 0.587 µs | 0 | The scalar timing/easing workload remains below its 100 µs median budget. These figures are a reproducible development baseline rather than a cross-platform guarantee; run the benchmark on each target platform when platform-specific performance matters. --- # GPUI Base Source: /base `gpui-base` is the unstyled foundation of GPUI Kit, the Rust desktop application framework. It provides interaction behavior, controlled state, focus management, accessibility semantics, animation, virtual lists, and theme tokens while leaving layout and visual design to your application. ## Choose the right layer | Use | When | | --- | --- | | `gpui-base` | You are building a design system and want to own every visual choice. | | `gpui-component` | You want a complete set of styled, ready-to-use desktop components. | The dependency points one way: `gpui-component` builds on `gpui-base`. Applications can use either layer directly. ## Principles - **Behavior is built in.** Controls provide consistent pointer, keyboard, focus, and state behavior. - **Presentation is yours.** Compose GPUI style methods and children without fighting default visuals. - **Parts stay composable.** Primitives expose their meaningful subparts instead of hiding markup behind a monolith. - **State stays explicit.** Controlled inputs report changes and your view owns the resulting state. ## Start building Follow [Getting started](/getting-started), render selectable Markdown and HTML with [TextView](/text-view), learn how to add [window-level text selection](/text-selection) to custom renderers, then explore the [primitive catalog](/primitives). Each page includes Rust snippets and a live WASM example backed by the same example crate that can run natively. Three systems are larger than a primitive and have pages of their own. [Motion](/motion) provides typed transitions, springs, keyframes, presence, and sequencing. [Virtual List](/virtual-list) renders lists of any length by drawing only what is on screen, with per-item sizes rather than a uniform row height. [Dock](/dock) is a full workspace shell — nested splits, tab groups and edge docks — whose layout is pure data you can build and serialize without a window, and whose every pixel comes from renderer traits you implement. [History](/history) covers two smaller, deliberately distinct structures: `History` is a root/current/back/forward navigation trail, while `UndoHistory` records grouped undo and redo transactions. --- # Text Selection Source: /base/text-selection `gpui-base` provides window-level text selection for ordinary GPUI participants. It coordinates pointer gestures, Shift-click extension, selection across multiple text elements, copying, scrolling, scopes, and multi-window lifetime without prescribing how text is laid out or highlighted. Use it when you render text with `StyledText`, `TextLayout`, a virtualized document, or another custom GPUI `Element`. ## Get started To add text selection to a custom GPUI participant, connect its layout and paint lifecycle to the window selection state as shown below. A selectable window has three roles: 1. One `TextSelectionLayer` element owns the selection state and window pointer handlers. 2. Each independently selectable text participant owns a stable `TextSelectionHandle`. 3. During rendering, the participant registers current geometry and projects the resulting snapshot onto laid-out `TextSelectionRun`s. Pointer gestures flow through the TextSelectionLayer element into window state. A participant registers a TextSelectionHandle and geometry, receives a snapshot, projects text runs into byte ranges, paints highlights, and contributes copied text. ### Key parts | API | Lifetime | Purpose | | --------------------------- | ------------------------------- | ----------------------------------------------------------------------------- | | `TextSelectionLayer` | Once per window | Installs window-level pointer handling and selection state. | | `TextSelection` | Static API | Queries and controls the window selection. | | `TextSelectionHandle` | Once per selectable participant | Identifies the participant and stores its callbacks and projected selection. | | `TextSelectionRegistration` | Recreated each rendered frame | Reports the current hitbox, bounds, scroll offset, scope, and document order. | | `TextSelectionRun` | Recreated during paint | Describes laid-out text for projection to a UTF-8 byte range. | | `TextSelectionProjection` | Returned by `update_runs` | Pairs each submitted run with its selected byte range. | | `TextSelectionSnapshot` | Produced when selection changes | Describes the participant's endpoints and coverage. | | `TextSelectionEvent` | Emitted to subscribers | Reports selection changes, clearing, and auto-scroll requests. | | `TextSelectionContentKey` | Stable content identity | Identifies virtualized content at a selection endpoint. | The complete flow is: 1. Retain one `TextSelectionLayer` element at the window root. 2. Create one `TextSelectionHandle` for each independently selectable participant. 3. During prepaint, call `TextSelectionHandle::register` with a `TextSelectionRegistration`. 4. During paint, pass laid-out `TextSelectionRun`s to `TextSelectionHandle::update_runs`. 5. Paint each returned byte range behind its glyphs. 6. Read or clear the window selection through `TextSelection`. The installed layer also provides familiar multi-click behavior: double-click selects a word using the same boundary rules as `Input`, while triple-click and later clicks select the newline-delimited logical line. The window state belongs to the retained `TextSelectionLayer` element. Handles and callbacks never receive or own that internal state. ## How it works `gpui-base` owns gesture coordination and range projection. The application remains responsible for layout and painting across the participant seam: GPUI Base owns gestures, window selection state, snapshots, and range projection. The application owns the selection handle, geometry, text runs, painting, and copying. ## Install the window element Add one `TextSelectionLayer` as the first child of the window root: ```rust use gpui_kit::prelude::*; use gpui_kit::{Context, Render, Window}; use gpui_kit::base::TextSelectionLayer; impl Render for AppView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { div() .size_full() .child(TextSelectionLayer) .child(self.content.clone()) } } ``` `TextSelectionLayer` is a zero-sized element. Keep it first and mount only one per window. Calling `TextSelection::activate_scope` before the first prepaint stores the scope until the layer binds its window state. ## Create a stable handle Create one handle for the semantic lifetime of the participant. Do not create a new handle every frame. ```rust use gpui_kit::{Context, Subscription, Window}; use gpui_kit::base::TextSelectionHandle; struct DocumentView { selection: TextSelectionHandle, _selection_refresh: Subscription, } impl DocumentView { fn new(window: &Window, cx: &mut Context) -> Self { let selection = TextSelectionHandle::new("", cx); let selection_refresh = selection.refresh_window_on_change(window, cx); Self { selection, _selection_refresh: selection_refresh, } } } ``` `refresh_window_on_change` redraws only the owning window when this handle's selection changes. Retain the returned subscription for as long as the participant is rendered, or explicitly call `.detach()` when the subscription should live for the rest of the participant entity's lifetime. Use `subscribe` instead when the participant needs events or more targeted invalidation. The `fallback_copy_text` passed to `TextSelectionHandle::new` is used until the participant projects laid-out runs or supplies custom copy behavior. Use `set_fallback_copy_text` to replace it. ## Register geometry during prepaint Call `TextSelectionHandle::register(registration, window, cx)` once per rendered frame, after the handle's bounds and hitbox are known: ```rust use gpui_kit::{Bounds, Hitbox, Pixels, Window}; use gpui_kit::base::TextSelectionRegistration; fn register_selection( handle: &TextSelectionHandle, hitbox: Hitbox, bounds: Bounds, window: &mut Window, cx: &mut gpui_kit::App, ) { handle.register( TextSelectionRegistration::new(hitbox, bounds) .with_document_order(0) .with_text_bounds(vec![bounds]), window, cx, ); } ``` - `bounds` is the participant's content viewport in window coordinates. - `text_bounds` contains the visible glyph-bearing areas. Blank-only drags do not start a text selection. - `document_order` provides stable ordering between participants for cross-participant selection and copy. Do not derive semantic order from a `HashMap` or accidental paint order. - `with_scroll_offset` maps window points into scrolled content coordinates. - `with_scope` assigns an explicit opaque scope. A surrounding `.text_selection_scope(scope)` builder overrides it while that subtree renders. Handles not registered in the current frame stop participating automatically. ## Project selection onto text runs In paint, call `TextSelectionHandle::update_runs` with laid-out runs containing the exact text used to create each `TextLayout`. It returns a `TextSelectionProjection` containing UTF-8-safe byte ranges: ```rust use gpui_kit::{Bounds, Pixels, SharedString, TextLayout}; use gpui_kit::base::TextSelectionRun; fn selected_range( handle: &TextSelectionHandle, text: SharedString, layout: TextLayout, bounds: Bounds, cx: &mut gpui_kit::App, ) -> Option> { handle .update_runs( &[TextSelectionRun::new(text, layout, bounds) .with_document_order(0)], cx, ) .ranges() .iter() .next() .and_then(|range| range.clone()) } ``` Paint the returned range behind the glyphs, then paint the text normally. Wrapped selections need three kinds of highlight geometry: the remainder of the first line, full-width middle lines, and the prefix of the last line. For multiple runs, give each run a stable `document_order`. Input order is preserved in `projection.ranges()` so each range can be paired with its original layout; document order is used when composing copied text. The [shared Text Selection showcase](https://github.com/MohsenDastaran/uni-kit/blob/main/crates/base/examples/showcase/components/text_selection.rs) is the complete runnable example used by both the native command and the live Rust/WASM preview above: ```bash cargo run -p gpui-base-examples -- text-selection ``` ## Complete Rust example <<< ../../crates/base/examples/showcase/components/text_selection.rs{rust} ## Query and control the window selection Use `TextSelection` associated functions to read or mutate the window selection. No extension trait import is required: ```rust use gpui_kit::base::TextSelection; let has_selection = TextSelection::has_selection(window, cx); let text = TextSelection::selected_text(window, cx); TextSelection::end(window, cx); // End a drag, preserving its range. TextSelection::clear(window, cx); // Clear window and participant-local ranges. ``` `selected_text` invokes participant copy callbacks only after the window and handle state leases have been released, so a callback may safely read or update selection state. ### Touch selection A long press on a participant selects the word under the finger, and lifting the finger keeps that selection as a *touch selection*: one that carries a grab handle at each end and an edit menu. Base owns the gesture and the drag; a presentation layer draws the handles and the menu from `TouchSelectionSnapshot`, which holds the caret line box at each end in window coordinates. ```rust use gpui_kit::base::{SelectionEdge, TextSelection}; // Re-render whoever draws the handles when the touch selection changes. let subscription = TextSelection::observe_touch_selection(window, cx, |cx| { /* notify */ }); if let Some(snapshot) = TextSelection::touch_selection(window, cx) { let start = snapshot.start(); // the caret box before the first selected character let end = snapshot.end(); // the caret box after the last one let menu = snapshot.is_menu_open(); } // Drag one end from the finger's position; the other end stays. TextSelection::begin_edge_drag(SelectionEdge::End, finger, window, cx); TextSelection::update_edge_drag(finger, window, cx); TextSelection::end_edge_drag(window, cx); TextSelection::select_all(window, cx); // the participant that was pressed TextSelection::close_edit_menu(window, cx); // after the menu's own action ran ``` A participant paints its own handles, where it is in the paint order, so that whatever covers the text covers them too: call `TextSelectionHandle::prepaint_touch_handles` in prepaint (it inserts the hitboxes the finger takes) and `TextSelectionHandle::paint_touch_handles` at the end of paint, after `register`, with the selection color. `TextView` does both. Participants report where their selection ends were painted through `TextSelectionRegistration::with_selection_edges`. Whatever draws the menu must call `TextSelection::register_touch_ui(bounds, window, cx)` with its bounds as it paints, every frame; a press inside a registered surface is then left to that surface instead of clearing the selection it belongs to. `Root` in GPUI Component draws the menu. ## Advanced participant adapters Plain text usually needs only `refresh_window_on_change` and `update_runs`. Rich or virtualized participants can configure additional behavior directly on the handle: | Method | Use | | -------------------------- | ------------------------------------------------------------------------------------- | | `refresh_window_on_change` | Redraw only the owning window when this handle's selection changes. | | `subscribe` | Receive `TextSelectionEvent` values for selection changes, clearing, and auto-scroll. | | `copy_with` | Export source text or include virtualized content that is not currently painted. | | `set_fallback_copy_text` | Replace the participant's fallback copy text. | | `resolve_content_key_with` | Attach a stable `TextSelectionContentKey` to an endpoint. | | `focus_with` | Focus the participant when a drag begins inside it. | | `clear_with` | Synchronously clear participant-local state when the window selection clears. | | `set_local_selection` | Report participant-local selection such as select-all. | Callbacks are invoked outside selection-state leases. They may update the participant or query `TextSelection` without causing a reentrant entity borrow. When `subscribe` receives `TextSelectionEvent::AutoScroll(Some(delta))`, feed that delta into the participant's scrolling loop; `None` stops it. Positive deltas move toward the bottom. The shared showcase demonstrates this with content taller than its viewport. For a virtualized document, inspect `TextSelectionEvent::SelectionChanged` and use `TextSelectionSnapshot::coverage()`, `window_points()`, and each endpoint's `content_point()` and `content_key()`. Coverage distinguishes a bounded participant from one selected from its start, to its end, or in full, allowing `copy_with` to include unpainted content. ## Isolate modal content with scopes Only handles in the active `TextSelectionScopeId` participate. Set the active window scope, then mark the corresponding rendered subtree: ```rust use gpui_kit::base::{ElementExt as _, TextSelection, TextSelectionScopeId}; let dialog_scope = TextSelectionScopeId::new(); TextSelection::activate_scope(dialog_scope, window, cx); let dialog = dialog_content.text_selection_scope(dialog_scope); ``` Scope stacks are isolated per window and are cleaned up even if a scoped subtree panics while rendering. Changing the active scope clears the previous selection atomically. ## Integration checklist - Retain one `TextSelectionLayer` element as the first child of each custom window root. - Keep each `TextSelectionHandle` stable across renders. - Register current geometry every rendered frame. - Use explicit document order and window-local scopes. - Pass the exact UTF-8 text used by each `TextLayout`. - Paint highlights before glyphs. - Keep parser, source export, and virtual-document knowledge in the participant. --- # History Source: /base/history `History` and `UndoHistory` keep two different kinds of application state. Both are independent of GPUI and leave applying a returned value to the caller, but their operations intentionally have different meanings: - `History` is a browser-style linear trail of locations, with back and forward navigation. - `UndoHistory` records changes as undo transactions, including changes grouped into one user action. ## Import ```rust use gpui_kit::base::{History, UndoHistory}; ``` ## Which one to use Choose the type from the meaning of the state, not from the names of the UI commands that operate on it: - Use `History` when each entry is a location and moving backward or forward returns the location reached. The current root must remain available. - Use `UndoHistory` when each entry is a reversible change and undo or redo must return every change in one user transaction. - Use a domain-specific manager when grouping depends on richer semantics than time or explicit boundaries. The Input component, for example, has a private transaction manager that understands typing, deletion, selection, and IME composition. Within gpui-component, `NavStack` uses `History` for page navigation, and `UndoHistory` is available to any state that wants grouped undo and redo. Input deliberately keeps its specialized private undo manager. ## `History`: a navigation trail Push every location the user visits. The current entry is the last value in the trail. For example, after visiting `A -> B -> C`, `C` is current and going back returns the new current entry, `B`: ```rust let mut history = History::new(); history.push("A"); history.push("B"); history.push("C"); assert_eq!(history.back(), Some("B")); assert_eq!(history.current(), Some(&"B")); ``` `back()` never moves past the root entry; it returns `None` there. `forward()` restores the nearest entry that was left behind. Pushing a new entry after going back drops that forward branch, just as a browser does after opening a new page. `max_entries` bounds the root-to-current entries: lowering it removes the oldest active entries immediately, and moving forward at the limit removes the oldest active entry before restoring the next one. `entries()` iterates from the root to the current entry. With the full `A -> B -> C` trail, it yields `A`, `B`, then `C`; `entries().rev()` yields `C`, `B`, then `A`. `forward_entries()` iterates from the nearest forward entry to the furthest. Use `retain` to remove invalid locations, `replace_current` to update the current location in place, and `remove_current` to remove it without discarding the forward branch. | Method | Does | | -------------------------------------------- | ------------------------------------------------------------------------------- | | `new()` | Creates an empty trail. `max_entries` defaults to 1000. | | `max_entries(n)` | Caps root-to-current entries and immediately removes the oldest excess entries. | | `push(entry)` | Makes `entry` current and drops the forward branch. | | `back()`, `forward()` | Move through the trail and return the resulting current entry. | | `current()` | Returns the current entry. | | `can_back()`, `can_forward()` | Report whether movement in that direction is available. | | `entries()`, `forward_entries()` | Iterate the current trail and forward branch in navigation order. | | `replace_current(entry)`, `remove_current()` | Update or remove the current entry. | | `retain(keep)`, `clear()` | Remove rejected entries from both sides, or empty the trail. | ## `UndoHistory`: grouped undo and redo Push a value for each change your application must reverse. To make a drag one undoable action, explicitly group all of its updates. `undo()` returns the group's changes newest first so the most recent change is reverted first; `redo()` returns the same group oldest first so it is applied in its original order: ```rust let mut history = UndoHistory::new(); history.start_grouping(); history.push("move from x=0 to x=10"); history.push("move from x=10 to x=20"); history.end_grouping(); assert_eq!( history.undo(), Some(vec!["move from x=10 to x=20", "move from x=0 to x=10"]), ); assert_eq!( history.redo(), Some(vec!["move from x=0 to x=10", "move from x=10 to x=20"]), ); ``` For changes whose boundary is not explicit, `group_interval` combines consecutive pushes close enough in time. A successful undo or redo ends that timed grouping window, so the next push starts a new transaction. Explicit grouping is separate: while it is active, a push still appends to the current transaction, including after an undo. A new push clears redo transactions. While replaying changes, use `set_ignoring(true)` to prevent the replay itself from being recorded. | Method | Does | | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `new()` | Creates an empty undo history. `max_undos` defaults to 1000. | | `max_undos(n)` | Caps undo transactions and immediately removes the oldest excess transactions. Redo preserves this cap. | | `group_interval(duration)` | Groups consecutive nearby pushes into one transaction. | | `start_grouping()`, `end_grouping()` | Make subsequent pushes append to the current transaction; ending grouping stops that explicit append behavior. On an empty history, as in the example above, the first push starts the transaction. | | `push(change)` | Records a change in the current or a new transaction and clears redo. | | `undo()`, `redo()` | Return the latest transaction newest-first for undo, oldest-first for redo. | | `can_undo()`, `can_redo()` | Report whether a transaction is available. | | `set_ignoring(bool)`, `is_ignoring()` | Control whether pushes are recorded. | | `clear()` | Empties undo and redo transactions. |