chore: gpui-kit skills

This commit is contained in:
since2006 committed 2026-09-12 09:33:59 +08:00
1 parent 5459fe0378
commit 13afe7fc67
32 files changed
+10545

No files matched your search

@@ -0,0 +1,60 @@
---
name: gpui-kit-design-guides
description: The normative Design Guides for GPUI Kit desktop applications. Read in full before designing or changing any screen, layout, spacing, visual hierarchy, color, density, component choice, interaction state, overlay, motion, data-heavy view, or interface copy in a GPUI Kit (gpui-kit / gpui-component) application, and before reviewing UI work for design quality. Also use when asked what the design rules are, or whether a UI decision follows them.
---
# GPUI Kit Design Guides
The guide is [references/design-guides.md](references/design-guides.md). It is
a requirement, not inspiration. Read the guide file itself before doing UI
work. Do not answer from this page, from an existing screen in the codebase, or
from training data.
If the file is missing, fetch `https://gpui-kit.com/docs/design-guides.md`.
The guide is a verbatim copy of that page, so its link to `./coding-guides.md`
means the Coding Guides in the `gpui-kit` skill (or `https://gpui-kit.com/docs/coding-guides.md`).
## How to read it
Read the whole guide for a new screen or a redesign. For a narrow change, read
"Design thesis" and "Start from the task" first, then the section for the
change. Sections, in order (`grep -n '^## ' references/design-guides.md`):
| Section | Read when |
| ------------------------------------ | ---------------------------------------------------------------------- |
| Design thesis | Always |
| Learning from Shadcn | Choosing what to borrow from web component libraries |
| Start from the task | Always; task hierarchy, interaction promise, what to leave out |
| Visual language | Color, typography, spacing, radius, borders, elevation, density, icons |
| Layout patterns | Window structure, sidebars, toolbars, panels, forms, resizable regions |
| Components and composition | Picking a component, composing parts, when to build a new one |
| Interaction states | Hover, focus, pressed, selected, disabled, loading, validation, danger |
| Feedback and overlays | Dialog, sheet, popover, menu, notification, tooltip, dismissal, focus |
| Motion | Any animation or transition |
| Designing data-heavy interfaces | Tables, lists, trees, dashboards, dense inspectors |
| Interface language | Any user-facing text: labels, buttons, titles, errors, empty states |
| Internationalization and platform fit | Multi-locale copy, Chinese terminology, macOS/Windows conventions |
| Guidance for AI-generated interfaces | Always when an agent produces UI |
| Accessibility checklist | Before finishing |
| Design review checklist | Before finishing; run every item against the work |
## Non-negotiables
A floor, not a substitute for the guide.
- **Desktop before web convention.** Keyboard access, window chrome, menus,
dense data views, resizable regions, persistent navigation.
- **`Button` vs `Link`.** `Button` for every in-app command, `ghost` or
`outline` when it should read quietly. `Link` only for external URLs and
email addresses.
- **Tokens before values.** No raw hex or `rgb(...)`; use `cx.theme()`
semantic tokens and rem-based helpers. Any spacing number quoted in the guide
is the current default scale, not a literal to repeat.
- **State must be visible.** Hover, focus, selection, disabled, loading,
validation, and destructive states each need distinct, consistent treatment.
- **Overlays.** Escape dismisses the topmost surface and returns focus to its
trigger.
- **Copy.** Name the object and the verb: `Delete "Roadmap"?` with a `Delete`
button, not `Are you sure?` with `OK`.
Finish by running the Design review checklist against the work.
@@ -0,0 +1,890 @@
---
title: Design Guides
description: Product and interaction design guidance for GPUI Kit applications
order: -2.1
---
# 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 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.
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.
## 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
Derive corner radii from the active theme. This preserves a product's ability
to become square or more rounded as one coherent system. Use `radius_full()`
for circles and pills rather than a literal maximum radius.
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.
<img class="alignment-light" src="/alignment-spines.svg?v=20260822-4" alt="Alignment spines across a desktop surface">
<img class="alignment-dark" src="/alignment-spines-dark.svg?v=20260822-4" alt="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. 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 require a tooltip
and an accessible name. 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.
### 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.
## 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.
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/key binding 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 tooltip.
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.
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.
## 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.
### 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 names, and icon-only controls have tooltips;
- 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](./coding-guides.md) to translate these design
decisions into GPUI architecture and code.
+273
View File
@@ -0,0 +1,273 @@
---
name: gpui-kit
description: 'How to build desktop applications with GPUI Kit, the Rust framework published as the gpui-kit crate (GPUI plus gpui_kit::component, gpui_kit::base, gpui_kit::assets). Use when setting up a gpui-kit app, choosing or using a component (Button, Input, Select, Dialog, Sheet, Tabs, Sidebar, List, DataTable, Tree, Chart, etc.), handling component state, theming, or window overlays, and for GPUI mechanics: actions and keybindings, async tasks, contexts, custom elements, entities, events, focus, global state, layout and styling, ElementId, and tests including UI integration testing. Holds the normative Coding Guides: read them before any architecture, state-ownership, public API, naming, or testing decision. Pairs with the gpui-kit-design-guides skill for the Design Guides.'
---
# GPUI Kit
Applications depend on one crate, `gpui-kit`. GPUI is `use gpui_kit::*;`, and
each layer is reachable by name: `gpui_kit::component` (styled components),
`gpui_kit::base` (unstyled behavior), `gpui_kit::assets` (default icons),
`gpui_kit::platform`.
Start with [component-family conventions](references/conventions.md) to choose the constructor, state owner, event, and layout contract for the task. Learn the family once, then verify the specific component supports the capability.
For a complete compiled view with retained state, subscriptions, and overlay
layers, read [the tested application recipe](references/recipes.md).
## Read the Guides First
Two guides hold the rules this skill assumes. They are requirements, not
inspiration. Read the guide file itself; do not answer from this page, from a
similar file in the codebase, or from training data.
| Guide | Read before |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| Design Guides, skill `gpui-kit-design-guides` | Choosing components, layout, spacing, hierarchy, color, density, interaction states, overlays, motion, interface copy |
| [Coding Guides](references/coding-guides.md) | Crate layering, `RenderOnce` vs `Entity<T>`, state ownership, `ElementId`, events, focus, async, public API, naming, testing |
Read the Design Guides first when the change has a visible surface: code
structure preserves product intent, it does not replace it. If the design
skill is not installed, fetch `https://gpui-kit.com/docs/design-guides.md`.
The coding guide is a verbatim copy of `https://gpui-kit.com/docs/coding-guides.md`,
so its links to `./design-guides.md` and `./getting-started.md` mean the design
skill and `https://gpui-kit.com/docs/getting-started.md`.
### Coding Guides section map
Read the whole guide for a new crate, module, or feature. For a narrow change,
read "Architecture at a glance" and "Rules for coding agents" first, then the
section for the change (`grep -n '^## ' references/coding-guides.md`).
| Section | Read when |
| ------------------------------------ | ---------------------------------------------------------------------- |
| Architecture at a glance | Always; crate layering, ownership boundary |
| Bootstrap and root ownership | `main`, `init`, `Root`, window creation, app-level state |
| Understand GPUI's phases and contexts | Anything touching `App`, `Window`, `Context<T>`, render vs update |
| Choose the right unit | Deciding `RenderOnce` vs `Entity<T>` vs custom `Element` |
| State ownership | Where a piece of state lives, who mutates it, `Entity<State>` handles |
| Stable identity | `ElementId`, lists, repeated elements, keyed state |
| Rendering and composition | `render`, builder chains, `when`/`map`, child composition |
| Behavior and presentation boundary | `gpui-base` vs `gpui-component` vs application code |
| Theme and styling | `cx.theme()`, tokens, `Styled`, sizes, variants |
| Events, actions, and focus | `cx.emit`, `subscribe`, `actions!`, keybindings, `FocusHandle` |
| Async work and side effects | `cx.spawn`, `background_spawn`, `Task`, I/O, timers |
| Layout, measurement, and scrolling | Flex layout, sizing, `overflow`, scroll handles, measuring |
| Lists, tables, and large data | `VirtualList`, `List`, `DataTable`, delegates, large collections |
| Public API design | Anything `pub`: builders, private fields, setter and reader naming |
| Platform and capability boundaries | macOS/Windows/Linux/wasm differences, feature gates |
| File and naming conventions | New files, modules, type and method names, `Kind` suffix, `Context` |
| Testing strategy | What to test, `#[gpui_kit::test]`, `TestAppContext` |
| Performance rules | Render cost, allocation, re-render triggers |
| Common failure modes | Before finishing; invented APIs, state in render, index ids |
| Rules for coding agents | Always when an agent writes code |
| Implementation checklist | Before finishing; run every item against the work |
### Non-negotiables
A floor, not a substitute for the guides.
- **Never invent an API.** Search the current source for the real signature.
Do not translate a React, CSS, or older-GPUI example by analogy; a
plausible-looking method that does not exist is the most common failure.
- **One dependency.** Applications depend on `gpui-kit` alone. GPUI is
`use gpui_kit::*;`; the layers are `gpui_kit::component`, `gpui_kit::base`,
`gpui_kit::assets`, `gpui_kit::platform`.
- **Framework owns behavior, application owns presentation.** Do not put
colors, sizing, or layout in `gpui-base`; do not put interaction behavior in
application styling code.
- **Stable identity.** Repeated elements need domain-derived `ElementId`s, not
list indexes.
- **No `pub` fields across the seam.** Public data types use builders and
reader methods.
- **Spell `Context` out.** `cx` is GPUI's; name anything else after what it
holds.
## Documentation
- **Find a task/component page**: use `https://gpui-kit.com/llms.txt`, then load its Markdown page.
- **Full reference**: `https://gpui-kit.com/llms-full.txt` is available when broad reference is actually needed
- **Per-component API**: fetch `https://gpui-kit.com/component/{name}.md`,
e.g. `button.md`, `input.md`, `select.md`, `dialog.md`, `data-table.md`
- **Any site page** can be fetched as Markdown by appending `.md` to the URL
## Quick Reference
Setup and examples: [references/usage.md](references/usage.md).
```rust
use gpui_kit::*;
use gpui_kit::component::Root;
gpui_kit::application()
.with_assets(gpui_kit::assets::Assets)
.run(|cx| {
gpui_kit::init(cx); // first, before anything else
// ... open_window(..., |window, cx| cx.new(|cx| Root::new(view, window, cx)))
});
```
- **Stateless** (`RenderOnce`): build in `render`:
`Button::new("save").primary().label("Save").on_click(|_, _, _| {})`
- **Stateful**: hold `Entity<State>` in the view, pass a reference in `render`:
`let input = cx.new(|cx| InputState::new(window, cx));` then `Input::new(&self.input)`
- **Sizes**: `.xsmall()` `.small()` `.medium()` (default) `.large()`
- **Theme**: `cx.theme().primary` · `.background` · `.foreground` · `.border` · `.muted`
- **Overlays**: `window.open_dialog(...)`, `open_sheet(...)`, `push_notification(...)`
via `gpui_kit::component::WindowExt`
## Component Catalog
Import paths are relative to `gpui_kit::component::`, so `input::{Input, InputState}`
means `use gpui_kit::component::input::{Input, InputState};`. For the full API
fetch the component's `.md` doc.
### Input & Form
| Component | Import | Notes |
| ------------- | ----------------------------------------------- | -------------------------------------------- |
| `Input` | `input::{Input, InputState}` | Stateful. Text, password, mask, validation |
| `Textarea` | `input::{Textarea, TextareaState}` | Stateful. Multi-line text |
| `Editor` | `input::{Editor, EditorState}` | Stateful. Code editor, `tree-sitter` feature |
| `NumberInput` | `input::{NumberInput, NumberInputEvent}` | Stateful. Numeric with step |
| `OtpInput` | `input::OtpInput` | Stateful. One-time password |
| `Select` | `select::{Select, SelectState}` | Stateful. Dropdown picker |
| `Combobox` | `combobox::{Combobox, ComboboxState}` | Stateful. Searchable select |
| `Checkbox` | `checkbox::Checkbox` | Stateless. `on_click` receives `&bool` |
| `Switch` | `switch::Switch` | Stateless. Toggle |
| `Radio` | `radio::{Radio, RadioGroup}` | Stateless. |
| `Slider` | `slider::{Slider, SliderState}` | Stateful. |
| `Toggle` | `button::Toggle` | Stateless. |
| `Rating` | `rating::Rating` | Stateless. |
| `Stepper` | `stepper::Stepper` | Stateless. Multi-step progress |
| `ColorPicker` | `color_picker::{ColorPicker, ColorPickerState}` | Stateful. |
| `DatePicker` | `date_picker::{DatePicker, DatePickerState}` | Stateful. |
| `Calendar` | `calendar::{Calendar, CalendarState}` | Stateful. Inline month view |
| `Form` | `form::{v_form, h_form, field}` | Layout container for form fields |
### Display & Feedback
| Component | Import | Notes |
| ----------- | ----------------------------------------- | ------------------------------------- |
| `Button` | `button::{Button, ButtonGroup}` | Stateless. Primary UI action |
| `Icon` | `{Icon, IconName}` | Stateless. Lucide icons |
| `Badge` | `badge::Badge` | Stateless. |
| `Tag` | `tag::Tag` | Stateless. Closable tags |
| `Avatar` | `avatar::Avatar` | Stateless. |
| `Label` | `label::Label` | Stateless. Form label |
| `Kbd` | `kbd::Kbd` | Stateless. Keyboard key display |
| `Alert` | `alert::Alert` | Stateless. Info/success/warning/error |
| `Spinner` | `spinner::Spinner` | Stateless. Loading indicator |
| `Skeleton` | `skeleton::Skeleton` | Stateless. Loading placeholder |
| `Shimmer` | `shimmer::{ShimmerText, ShimmerStyle}` | Stateless. Streaming-text shimmer |
| `Marker` | `marker::{Marker, MarkerVariant}` | Stateless. Inline status marker |
| `Progress` | `progress::{Progress, ProgressCircle}` | Stateless. |
| `Tooltip` | `tooltip::Tooltip` | Via `.tooltip()` on elements |
| `HoverCard` | `hover_card::{HoverCard, HoverCardState}` | Stateful. |
| `Clipboard` | `clipboard::Clipboard` | Stateless. Copy button |
| `TextView` | `text::TextView` | `TextView::markdown(id, text)`, HTML too |
| Image | `gpui_kit::{img, ImageSource, ObjectFit}` | GPUI's `img()` element |
### Overlay & Popups
| Component | Import | Notes |
| ---------------- | ------------------------------------------------- | ---------------------------------------- |
| `Dialog` | `dialog::Dialog` + `WindowExt` | Via `window.open_dialog(...)` |
| `AlertDialog` | `WindowExt` | Via `window.open_alert_dialog(...)` |
| `Sheet` | `sheet::Sheet` + `WindowExt` | Side panel, via `window.open_sheet(...)` |
| `Notification` | `notification::Notification` + `WindowExt` | Via `window.push_notification(...)` |
| `Popover` | `popover::Popover` | Floating overlay |
| `Menu` | `menu::{PopupMenu, DropdownMenu}` | Context menus |
| `DropdownButton` | `button::DropdownButton` | Button with dropdown menu |
| `Command` | `command::{Command, CommandState, CommandGroup}` | Stateful. Command palette |
| Focus trap | `FocusTrapElement` | `.focus_trap(id, &handle)` on a container; `Dialog` and `Sheet` have it built in |
### Navigation & Layout
| Component | Import | Notes |
| ----------------- | ------------------------------------------------------------------------ | ------------------------- |
| `Tabs` / `TabBar` | `tab::{Tab, TabBar}` | Tabbed interface |
| `Sidebar` | `sidebar::{Sidebar, SidebarMenu, ...}` | App navigation panel |
| `TitleBar` | `TitleBar` | Window title bar |
| `StatusBar` | `status_bar::StatusBar` | Window status bar |
| `Breadcrumb` | `breadcrumb::Breadcrumb` | Navigation breadcrumb |
| `Pagination` | `pagination::Pagination` | Page navigation |
| `Accordion` | `accordion::Accordion` | Collapsible sections |
| `Collapsible` | `collapsible::Collapsible` | Single collapsible |
| `GroupBox` | `group_box::GroupBox` | Labeled container |
| `Resizable` | `resizable::{h_resizable, v_resizable, resizable_panel, ResizableState}` | Draggable split panes |
| `Scrollbar` | `scroll::Scrollbar` | Custom scrollbar |
### Data Display
| Component | Import | Notes |
| ----------------- | ----------------------------------------------- | ----------------------------- |
| `DataTable` | `table::{DataTable, TableState, TableDelegate}` | Stateful. Full-featured table |
| `Table` | `table::{Table, ...}` | Simpler table |
| `VirtualList` | `{v_virtual_list, h_virtual_list}` | High-perf large lists |
| `List` | `list::{List, ListState, ListDelegate}` | Stateful. Searchable list |
| `Tree` | `tree::{Tree, TreeState, TreeItem, TreeEntry}` | Stateful. Hierarchy |
| `DescriptionList` | `description_list::DescriptionList` | Key-value pairs |
| `Settings` | `setting::Settings` | Settings panel |
### Chat & Messaging
| Component | Import | Notes |
| ----------------- | -------------------------------------------------------- | ------------------------------------- |
| `Message` | `message::{Message, MessageContent, MessageAlignment}` | Stateless. Chat message row |
| `Bubble` | `bubble::{Bubble, BubbleContent, BubbleVariant}` | Stateless. Message bubble |
| `Attachment` | `attachment::{Attachment, AttachmentContent, ...}` | Stateless. File/media attachment card |
| `MessageScroller` | `message_scroller::{MessageScroller, MessageScrollerState}` | Stateful. Auto-scrolling message list |
### Charts
| Component | Import | Notes |
| --------- | --------------------------------------------------------------- | ------------------------------ |
| `Chart` | `chart::{AreaChart, BarChart, LineChart, PieChart, RadarChart}` | Bar, line, area, pie charts |
| `Plot` | `plot::Plot` | `#[derive(IntoPlot)]` for data |
## Testing
**UI integration testing** means rendering real components in headless windows,
simulating input, and checking state, focus, layout and owner callbacks. Use
`#[gpui_kit::test]` to run tests and `gpui_kit::test` to operate and inspect the UI.
When asked to add component interaction coverage, describe it as UI integration testing.
For unit tests, GPUI context tests or UI integration tests, read
[Testing](references/gpui/test.md). It includes dependency setup, a complete
runnable UI flow, scoped native interactions, frame/async handling and the
limits of accessibility and geometry assertions. Use the production view and
verify the action's outcome through normal Rust assertions.
## GPUI References
Load the file for the mechanism the task touches. Each file starts with a
contents line.
| Topic | File | Load when |
| --------------------------- | ------------------------------------------------------ | --------------------------------------------------------------- |
| Actions & keybindings | [action.md](references/gpui/action.md) | `actions!`, `bind_keys`, `on_action`, `key_context` |
| Async & background tasks | [async.md](references/gpui/async.md) | `cx.spawn`, `background_spawn`, `Task`, async I/O |
| Context management | [context.md](references/gpui/context.md) | `App`, `Window`, `Context<T>`, `AsyncApp` |
| Custom elements (low-level) | [element.md](references/gpui/element.md) | `Element` trait, `request_layout`, `prepaint`, `paint` |
| Entity state | [entity.md](references/gpui/entity.md) | `Entity<T>`, `WeakEntity`, state management |
| Events & subscriptions | [event.md](references/gpui/event.md) | `cx.emit`, `cx.subscribe`, `cx.observe` |
| Focus & keyboard nav | [focus-handle.md](references/gpui/focus-handle.md) | `FocusHandle`, `track_focus`, Tab navigation |
| Global state | [global.md](references/gpui/global.md) | `Global` trait, `cx.set_global`, app-wide config |
| Layout & styling | [layout-style.md](references/gpui/layout-style.md) | `div()`, `h_flex()`, `v_flex()`, flexbox, overflow, positioning |
| ElementId | [element-id.md](references/gpui/element-id.md) | `ElementId`, `.id()`, uniqueness rules, stateful elements |
| Testing | [test.md](references/gpui/test.md) | `#[gpui_kit::test]`, `TestAppContext`, `VisualTestContext` |
Deep dives, for when the topic file is not enough:
- **Element trait**: [element-api.md](references/gpui/element-api.md) (complete API, hitbox, events) ·
[element-patterns.md](references/gpui/element-patterns.md) (text, interactive, container, composite) ·
[element-examples.md](references/gpui/element-examples.md) (full examples) ·
[element-best-practices.md](references/gpui/element-best-practices.md) (performance, state, pitfalls) ·
[element-advanced.md](references/gpui/element-advanced.md) (custom layouts, async updates, virtual lists)
- **Entities**: [entity-api.md](references/gpui/entity-api.md) (complete API, lifecycle) ·
[entity-patterns.md](references/gpui/entity-patterns.md) (model-view, cross-entity, observer) ·
[entity-best-practices.md](references/gpui/entity-best-practices.md) (memory, performance) ·
[entity-advanced.md](references/gpui/entity-advanced.md) (collections, registry, debounce, state machines)
- **Testing**: [test-examples.md](references/gpui/test-examples.md) (organization, setup, assertions, running tests) ·
[test-reference.md](references/gpui/test-reference.md) (re-entrancy, property tests, mocking)
@@ -0,0 +1,893 @@
---
title: Coding Guides
description: Architecture and coding conventions for maintainable GPUI Kit applications
order: -2.2
---
# 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](./design-guides.md) 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.
## Architecture at a glance
<img class="architecture-light" src="/application-layers-light.svg?v=20260822-16" alt="GPUI application architecture layers">
<img class="architecture-dark" src="/application-layers-dark.svg?v=20260822-16" alt="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 feature should usually be a crate, not another
file in a global `views`, `models`, or `modals` directory. Keep the model,
views, commands, dialogs, and workflow for one capability together. A dialog
that edits a workspace belongs to the workspace feature; only the reusable
dialog primitive belongs to the UI library.
```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. A feature may depend on stable shared capabilities and UI foundations;
it must not depend on the shell or reach into a sibling feature's internals.
When two features need to communicate, prefer an explicit command, event, data
type, or small shared service over a dependency between their views. Extract a
shared crate only after the capability has a coherent name and more than one
real owner.
Crate boundaries are engineering boundaries. They let Cargo rebuild and test a
smaller dependency subgraph, make ownership visible in `Cargo.toml`, and limit
the review and regression surface of a change. They also make removal honest:
a feature that cannot be detached without searching through global view and
modal directories was never isolated.
Do not create a crate for every screen or helper. Split where a capability has
its own state and lifecycle, a stable public seam, or enough implementation to
benefit from independent compilation and tests. Keep dependencies acyclic and
pointing toward smaller, more stable crates.
## 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);
cx.spawn(async move |cx| {
cx.open_window(WindowOptions::default(), |window, cx| {
let workspace = cx.new(|cx| Workspace::new(window, cx));
cx.new(|cx| Root::new(workspace, window, cx))
})
.expect("failed to open window");
})
.detach();
});
```
`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 survives across
frames; the element tree returned by `render` is a fresh description of the
current frame. Keep that distinction explicit.
- `Context<Self>` 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
Use a `RenderOnce`/`IntoElement` component when all inputs can be supplied by
the caller and the element does not need to retain application state between
frames. This is the normal choice for presentational wrappers and small
controls.
```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<T>` for retained behavior
Use an entity-backed `Render` view when behavior spans frames or needs
observation, subscriptions, focus, async work, history, measurement, or
incremental updates. Store entities in an owning view rather than recreating
them in `render`.
```rust
struct SearchView {
query: Entity<InputState>,
}
impl SearchView {
fn new(window: &mut Window, cx: &mut Context<Self>) -> 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
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 alive when the API requires it.
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` 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<Self>) -> 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:
```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.
If code mutates the global GPUI Component theme directly, call
`Theme::sync_base(cx)` afterward so Base-owned scrollbars and resize handles
receive the new projection. `Theme::change(...)` performs this 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
`Root::render` calls `window.set_rem_size(cx.theme().font_size)`. 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.
Change zoom by updating the base font and refreshing the window:
```rust
Theme::global_mut(cx).font_size = px(18.);
Theme::sync_base(cx);
window.refresh();
```
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 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.
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`.
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 async work 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.
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 matches Zed's `h_flex`, and it 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 | `<Control>State` | `InputState`, `TableState` |
| Imperative shared reference | `<Control>Handle` | `DialogHandle`, scroll handle |
| Semantic notification | `<Control>Event` | `TableEvent`, `SelectEvent` |
| Keyboard command | verb or intent noun | `Confirm`, `Cancel`, `SelectNext` |
| Pluggable data/behavior owner | `<Role>Delegate` / `<Role>Provider` | `TableDelegate`, `CompletionProvider` |
| Application-supplied presentation | `render_<part>` or `<part>_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_<field>` | `with_size`, `with_mode` |
| In-place mutation | `set_<field>` | `set_items`, `set_selected_index` |
| Boolean reader | `is_<adjective>` / `has_<noun>` | `is_open`, `is_closable`, `has_selection` |
| Plain value reader | field noun | `placement`, `selected_value` |
| Callback registration | `on_<event or intent>` | `on_click`, `on_open_change` |
| Rendering a named region | `render_<region>` | `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_<noun>`, when the value holds something, or
`is_<adjective>`, 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_<field>`,
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; **read-only** permits navigation and
selection but prevents editing; **loading** prevents duplicate work while an
operation is pending.
- **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](./design-guides.md#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<T>` 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](./getting-started.md) for application setup and the
component pages for current API details.
@@ -0,0 +1,67 @@
# Component-family conventions
Use this page to predict a component's shape before reading its detailed API. These conventions describe Rust GPUI Component. JavaScript uses its host's generated catalog; a Rust method is not automatically a JavaScript method.
## Choose the family
| Task | Family and constructor | State owner | Event or composition |
| --- | --- | --- | --- |
| Execute a command | `Button::new("save")` | Application owns the operation | `on_click` receives a click event |
| Change a boolean | `Checkbox::new("remember")`, `Switch::new("enabled")`, `Radio::new("choice")` | Application supplies `checked(bool)` | `on_change` receives the requested `&bool` |
| Choose one radio option | `RadioGroup::new("delivery")` | Application supplies `selected_index(Option<usize>)` | `on_change` receives the requested `&usize` |
| Edit retained text | `Input::new(&self.input)` | View retains `Entity<InputState>` | Retain the subscription; handle `InputEvent` |
| Lay out fields | `Form::new()` | Children retain their own state | `child(Field)`, `columns`, `label_layout`, `footer` |
| Supply a compound part | `Field::new()`, `Tab::new()` | Containing component owns placement/selection | Read the parent's accepted child type |
Constructors establish the component family's ownership model. Do not infer that every `new` takes an ID or that every `child` accepts an arbitrary element. Keep domain IDs stable when controls are reordered. Positional selection APIs remain positional; applications map them to domain identity when needed.
## A value request is different from a command
For Checkbox, Switch, Radio, and RadioGroup, use the same owner-update pattern:
```rust
Switch::new("enabled")
.checked(self.enabled)
.on_change(cx.listener(|this, next, _, cx| {
this.enabled = *next;
cx.notify();
}))
```
Replace Switch with Checkbox or Radio without changing the boolean callback shape. Radio represents selecting an option; its activation semantics still differ from toggling a Switch. RadioGroup supplies an index, so its owner stores `Some(*next)` instead.
`on_change` requests a value; it does not mutate your application model. Existing `on_click` calls on these four controls remain valid compatibility aliases. Both names set one handler, and the last call wins. Use `Button::on_click` for commands. Retained controls such as Input keep their existing event/subscription API because the state entity, rather than a transient builder, owns their changes.
## Form layout has independent decisions
| Decision | API | Default |
| --- | --- | --- |
| Label above or beside its control | `label_layout(Axis::Vertical / Horizontal)` | Above |
| Number of field columns | `columns(count)` | One |
| A field spanning several columns | `Field::col_span(count)` | One |
| Commands after the fields | `footer(element)` | No footer; supplied footer spans all columns and aligns to the trailing edge |
`Form::horizontal()` and `h_form()` mean labels beside controls. They do not put all fields into one horizontal row. `Form::vertical()`, `v_form()`, and `layout(Axis)` remain supported.
```rust
Form::new()
.label_layout(Axis::Horizontal)
.columns(2)
.child(Field::new().label("Name").child(Input::new(&self.name)))
.child(Field::new().label("Email").child(Input::new(&self.email)))
.footer(Button::new("save").label("Save"))
```
The footer supplies layout only: attach the save action to the Button. Form does not own submission, validation, responsive breakpoints, or data persistence. Choose column count for the available width. Put multiple action buttons in one supplied action row with a shared gap. Keep field descriptions with their Field so they follow its alignment.
## Import capabilities explicitly
Components live in their component modules. Common capabilities may be extension traits, not inherent methods: import `ButtonVariants` for button variants, `Sizable` for size tiers, `Disableable` for supported disabled builders, `ActiveTheme` for theme access, and `WindowExt` for window overlays. Do not infer that every component supports every trait.
Use the [complete application recipe](recipes.md) for imports, retained ownership, initialization, Root, and overlay layers. For a narrow task, load its component page and the relevant section of the coding/design guides; a component catalog is not a required reading sequence.
## Verify the convention transfer
When transferring a pattern to another family member, check that it compiles, receives the expected requested value, preserves controlled state after redraw, and behaves correctly when disabled. For Form, check label orientation independently of column count and keep footer geometry outside the field grid's individual cells.
In this repository, `script/check-ai rust` compiles the external-style consumer and runs its interaction tests, control-family event and compatibility tests, and Form geometry tests. These are deterministic contract checks, not measured AI first-attempt success rates.
@@ -0,0 +1,173 @@
# Actions & Keybindings
**Contents:** [Overview](#overview) · [Quick Start](#quick-start) · [Key Formats](#key-formats) · [Action Naming](#action-naming) · [Context-Aware Bindings](#context-aware-bindings) · [Best Practices](#best-practices)
## Overview
Actions provide declarative keyboard-driven UI interactions in GPUI.
**Key Concepts:**
- Define actions with `actions!` macro or `#[derive(Action)]`
- Bind keys with `cx.bind_keys()`
- Handle with `.on_action()` on elements
- Context-aware via `key_context()`
## Quick Start
### Simple Actions
```rust
use gpui_kit::actions;
actions!(editor, [MoveUp, MoveDown, Save, Quit]);
const CONTEXT: &str = "Editor";
pub fn init(cx: &mut App) {
cx.bind_keys([
KeyBinding::new("up", MoveUp, Some(CONTEXT)),
KeyBinding::new("down", MoveDown, Some(CONTEXT)),
KeyBinding::new("cmd-s", Save, Some(CONTEXT)),
KeyBinding::new("cmd-q", Quit, Some(CONTEXT)),
]);
}
impl Render for Editor {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.key_context(CONTEXT)
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.on_action(cx.listener(Self::save))
}
}
impl Editor {
fn move_up(&mut self, _: &MoveUp, cx: &mut Context<Self>) {
// Handle move up
cx.notify();
}
fn move_down(&mut self, _: &MoveDown, cx: &mut Context<Self>) {
cx.notify();
}
fn save(&mut self, _: &Save, cx: &mut Context<Self>) {
// Save logic
cx.notify();
}
}
```
### Actions with Parameters
```rust
#[derive(Clone, PartialEq, Action, Deserialize)]
#[action(namespace = editor)]
pub struct InsertText {
pub text: String,
}
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
#[action(namespace = editor, no_json)]
pub struct Digit(pub u8);
cx.bind_keys([
KeyBinding::new("0", Digit(0), Some(CONTEXT)),
KeyBinding::new("1", Digit(1), Some(CONTEXT)),
// ...
]);
impl Editor {
fn on_digit(&mut self, action: &Digit, cx: &mut Context<Self>) {
self.insert_digit(action.0, cx);
}
}
```
## Key Formats
```rust
// Modifiers
"cmd-s" // Command (macOS) / Ctrl (Windows/Linux)
"ctrl-c" // Control
"alt-f" // Alt
"shift-tab" // Shift
"cmd-ctrl-f" // Multiple modifiers
// Keys
"a-z", "0-9" // Letters and numbers
"f1-f12" // Function keys
"up", "down", "left", "right"
"enter", "escape", "space", "tab"
"backspace", "delete"
"-", "=", "[", "]", etc. // Special characters
```
## Action Naming
Prefer verb-noun pattern:
```rust
actions!([
OpenFile, // ✅ Good
CloseWindow, // ✅ Good
ToggleSidebar, // ✅ Good
Save, // ✅ Good (common exception)
]);
```
## Context-Aware Bindings
```rust
const EDITOR_CONTEXT: &str = "Editor";
const MODAL_CONTEXT: &str = "Modal";
// Same key, different contexts
cx.bind_keys([
KeyBinding::new("escape", CloseModal, Some(MODAL_CONTEXT)),
KeyBinding::new("escape", ClearSelection, Some(EDITOR_CONTEXT)),
]);
// Set context on element
div()
.key_context(EDITOR_CONTEXT)
.child(editor_content)
```
## Best Practices
### ✅ Use Contexts
```rust
// ✅ Good: Context-aware
div()
.key_context("MyComponent")
.on_action(cx.listener(Self::handle))
```
### ✅ Name Actions Clearly
```rust
// ✅ Good: Clear intent
actions!([
SaveDocument,
CloseTab,
TogglePreview,
]);
```
### ✅ Handle with Listeners
```rust
// ✅ Good: Proper handler naming
impl MyComponent {
fn on_action_save(&mut self, _: &Save, cx: &mut Context<Self>) {
// Handle save
cx.notify();
}
}
div().on_action(cx.listener(Self::on_action_save))
```
@@ -0,0 +1,226 @@
# Async & Background Tasks
**Contents:** [Overview](#overview) · [Quick Start](#quick-start) · [Core Patterns](#core-patterns) · [Common Pitfalls](#common-pitfalls)
## Overview
GPUI provides integrated async runtime for foreground UI updates and background computation.
**Key Concepts:**
- **Foreground tasks**: UI thread, can update entities (`cx.spawn`)
- **Background tasks**: Worker threads, CPU-intensive work (`cx.background_spawn`)
- All entity updates happen on foreground thread
## Quick Start
### Foreground Tasks (UI Updates)
When spawned from `Context<Self>`, the closure receives `(WeakEntity<Self>, &mut AsyncApp)`:
```rust
impl MyComponent {
fn fetch_data(&mut self, cx: &mut Context<Self>) {
cx.spawn(async move |this, cx: &mut AsyncApp| {
// Runs on UI thread, can await and update entities
let data = fetch_from_api().await;
this.update(cx, |state, cx| {
state.data = Some(data);
cx.notify();
}).ok();
}).detach();
}
}
```
When spawned from `&mut App` (not inside an entity), the closure receives only `(cx: &mut AsyncApp)`:
```rust
cx.spawn(async move |cx: &mut AsyncApp| {
// No entity reference
}).detach();
```
### Spawn with Window Context (spawn_in)
Use `spawn_in` when the task also needs window access (`update_in`):
```rust
impl MyComponent {
fn animate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
cx.spawn_in(window, async move |this, cx| {
// cx here is AsyncWindowContext
this.update_in(cx, |state, window, cx| {
// Can access window here
state.frame += 1;
cx.notify();
}).ok();
}).detach();
}
}
```
### Background Tasks (Heavy Work)
```rust
impl MyComponent {
fn process_file(&mut self, cx: &mut Context<Self>) {
let entity = cx.entity().downgrade();
cx.background_spawn(async move {
// Runs on background thread, CPU-intensive
let result = heavy_computation().await;
result
})
.then(cx.spawn(move |result, cx| {
// Back to foreground to update UI
entity.update(cx, |state, cx| {
state.result = result;
cx.notify();
}).ok();
}))
.detach();
}
}
```
### Task Management
```rust
struct MyView {
_task: Task<()>, // Prefix with _ if stored but not accessed
}
impl MyView {
fn new(cx: &mut Context<Self>) -> Self {
let _task = cx.spawn(async move |this, cx: &mut AsyncApp| {
// Task automatically cancelled when dropped
loop {
cx.background_executor().timer(Duration::from_secs(1)).await;
this.update(cx, |state, cx| {
state.tick();
cx.notify();
}).ok();
}
});
Self { _task }
}
}
```
## Core Patterns
### 1. Async Data Fetching (from Context<Self>)
```rust
cx.spawn(async move |this, cx: &mut AsyncApp| {
let data = fetch_data().await?;
this.update(cx, |state, cx| {
state.data = Some(data);
cx.notify();
})?;
Ok::<_, anyhow::Error>(())
}).detach();
```
### 2. Background Computation + UI Update
```rust
cx.background_spawn(async move {
heavy_work()
})
.then(cx.spawn(move |this, cx: &mut AsyncApp| {
this.update(cx, |state, cx| {
state.result = result;
cx.notify();
}).ok();
}))
.detach();
```
### 3. Periodic Tasks
```rust
cx.spawn(async move |this, cx: &mut AsyncApp| {
loop {
cx.background_executor().timer(Duration::from_secs(5)).await;
this.update(cx, |state, cx| {
state.tick();
cx.notify();
}).ok();
}
}).detach();
```
### 4. Task Cancellation
Tasks are automatically cancelled when dropped. Store in struct to keep alive.
## Common Pitfalls
### ❌ Don't: Use `defer_in` and then update the same entity through its handle
`cx.defer_in(window, callback)` schedules `callback` to run **on the current entity** — GPUI re-acquires that entity's lock to execute it. Calling `entity.update(cx, …)` on the *same* entity from within the deferred callback re-enters the lock and panics:
```
cannot update … while it is already being updated
```
```rust
// ❌ Panic: list entity is locked for the defer_in; calling list.update re-enters
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
cx.defer_in(window, |list_state, window, cx| {
parent.update(cx, |this, cx| {
this.inner_list.update(cx, |_, _| {}); // PANIC if inner_list == the deferred entity
});
});
}
```
```rust
// ✅ Correct: use the direct &mut reference — no lock needed
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
cx.defer_in(window, |list_state, window, cx| {
// Access list data directly through the &mut reference
list_state.delegate_mut().some_method();
// Update a *different* entity — fine, different lock
parent.update(cx, |this, cx| { /* … */ });
// Sync list state directly after parent update — no lock needed
list_state.delegate_mut().update_snapshot(new_val);
});
}
```
The rule: inside a `defer_in` callback, **never call `entity.update(cx, …)` or `entity.read(cx)` on the entity the `defer_in` was scheduled on**. Use the `&mut Entity` direct reference the callback provides instead.
### ❌ Don't: Update entities from background tasks
```rust
// ❌ Wrong: Can't update entities from background thread
cx.background_spawn(async move {
entity.update(cx, |state, cx| { // Compile error!
state.data = data;
});
});
```
### ✅ Do: Use foreground task or chain
```rust
// ✅ Correct: Chain with foreground task
cx.background_spawn(async move { data })
.then(cx.spawn(move |data, cx| {
entity.update(cx, |state, cx| {
state.data = data;
cx.notify();
}).ok();
}))
.detach();
```
@@ -0,0 +1,258 @@
# Context Management
**Contents:** [Overview](#overview) · [Quick Start](#quick-start) · [Common Operations](#common-operations) · [Context Hierarchy](#context-hierarchy) · [cx.listener](#cxlistener--binding-callbacks-to-self) · [subscribe_in](#subscribe_in--subscribe-with-window-access) · [observe_window_activation](#observe_window_activation) · [observe_global](#observe_global) · [defer / defer_in](#defer-and-defer_in) · [Naming Convention](#context-naming-convention)
## Overview
GPUI uses different context types for different scenarios:
**Context Types:**
- **`App`**: Global app state, entity creation
- **`Window`**: Window-specific operations, painting, layout
- **`Context<T>`**: Entity-specific context for component `T`
- **`AsyncApp`**: Async context for foreground tasks
- **`AsyncWindowContext`**: Async context with window access
## Quick Start
### Context<T> - Component Context
```rust
impl MyComponent {
fn update_state(&mut self, cx: &mut Context<Self>) {
self.value = 42;
cx.notify(); // Trigger re-render
// Spawn async task
cx.spawn(async move |cx| {
// Async work
}).detach();
// Get current entity
let entity = cx.entity();
}
}
```
### App - Global Context
```rust
fn main() {
let app = Application::new();
app.run(|cx: &mut App| {
// Create entities
let entity = cx.new(|cx| MyState::default());
// Open windows
cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|cx| Root::new(view, window, cx))
});
});
}
```
### Window - Window Context
```rust
impl Render for MyView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
// Window operations
let is_focused = window.is_window_focused();
let bounds = window.bounds();
div().child("Content")
}
}
```
### AsyncApp - Async Context
```rust
cx.spawn(async move |cx: &mut AsyncApp| {
let data = fetch_data().await;
entity.update(cx, |state, inner_cx| {
state.data = data;
inner_cx.notify();
}).ok();
}).detach();
```
## Common Operations
### Entity Operations
```rust
// Create entity
let entity = cx.new(|cx| MyState::default());
// Update entity
entity.update(cx, |state, cx| {
state.value = 42;
cx.notify();
});
// Read entity
let value = entity.read(cx).value;
```
### Notifications and Events
```rust
// Trigger re-render
cx.notify();
// Emit event
cx.emit(MyEvent::Updated);
// Observe entity
cx.observe(&entity, |this, observed, cx| {
// React to changes
}).detach();
// Subscribe to events
cx.subscribe(&entity, |this, source, event, cx| {
// Handle event
}).detach();
```
### Window Operations
```rust
// Window state
let focused = window.is_window_focused();
let bounds = window.bounds();
let scale = window.scale_factor();
// Close window
window.remove_window();
```
### Async Operations
```rust
// Spawn foreground task
cx.spawn(async move |cx| {
// Async work with entity access
}).detach();
// Spawn background task
cx.background_spawn(async move {
// Heavy computation
}).detach();
```
## Context Hierarchy
```
App (Global)
└─ Window (Per-window)
└─ Context<T> (Per-component)
└─ AsyncApp (In async tasks)
└─ AsyncWindowContext (Async + Window)
```
## cx.listener — Binding Callbacks to Self
`cx.listener` creates a callback that borrows `&mut self` (the current entity). Use it for `on_click`, `on_action`, and other element event handlers:
```rust
impl Render for MyView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.on_action(cx.listener(Self::on_save))
.child(
Button::new("btn")
.on_click(cx.listener(|this, _event, _window, cx| {
this.count += 1;
cx.notify();
}))
)
}
}
impl MyView {
fn on_save(&mut self, _: &Save, _window: &mut Window, cx: &mut Context<Self>) {
cx.notify();
}
}
```
`cx.listener(Self::method)` is equivalent to creating a closure that calls `self.method(...)`.
## subscribe_in — Subscribe with Window Access
Use `subscribe_in` (instead of `subscribe`) when the callback needs `&mut Window`:
```rust
let _subscription = cx.subscribe_in(&input, window, |this, state, event, window, cx| {
match event {
InputEvent::Change => {
let val = state.read(cx).value();
this.on_input_change(val, window, cx);
}
_ => {}
}
});
// Store _subscription in struct to keep it alive
```
`subscribe` vs `subscribe_in`:
- `subscribe(&entity, |this, source, event, cx|)` — no window access
- `subscribe_in(&entity, window, |this, source, event, window, cx|)` — has window access
## observe_window_activation
React when the window gains or loses focus:
```rust
let _sub = cx.observe_window_activation(window, |this, window, cx| {
if window.is_window_active() {
this.resume(cx);
} else {
this.pause(cx);
}
});
```
## observe_global
React when a global value changes:
```rust
cx.observe_global::<Theme>(|cx| {
// Theme changed — react
cx.notify();
});
```
## defer and defer_in
Schedule work after the current update completes:
```rust
// defer: runs after current App update, no window access
cx.defer(|cx| {
// Runs after current entity update is done
});
// defer_in: runs after update, with window access
cx.defer_in(window, |this, window, cx| {
// Can access window here
// CAUTION: never call entity.update(cx) on *this same entity* inside defer_in
// — it re-enters the lock and panics. Use the &mut self reference directly.
this.some_method(window, cx);
});
```
## Context Naming Convention
Always name contexts `cx` regardless of type:
```rust
fn new(window: &mut Window, cx: &mut App) {} // cx = App
impl Render for View {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) {} // cx = Context<Self>
}
cx.spawn(async move |this, cx: &mut AsyncApp| {}) // cx = AsyncApp
```
@@ -0,0 +1,705 @@
# Advanced Element Patterns
**Contents:** [Custom Layout Algorithms](#custom-layout-algorithms) · [Element Composition with Traits](#element-composition-with-traits) · [Async Element Updates](#async-element-updates) · [Element Memoization](#element-memoization) · [Virtual List Pattern](#virtual-list-pattern)
## Custom Layout Algorithms
Implementing custom layout algorithms not supported by GPUI's built-in layouts.
### Masonry Layout (Pinterest-Style)
```rust
pub struct MasonryLayout {
id: ElementId,
columns: usize,
gap: Pixels,
children: Vec<AnyElement>,
}
struct MasonryLayoutState {
column_layouts: Vec<Vec<LayoutId>>,
column_heights: Vec<Pixels>,
}
struct MasonryPaintState {
child_bounds: Vec<Bounds<Pixels>>,
}
impl Element for MasonryLayout {
type RequestLayoutState = MasonryLayoutState;
type PrepaintState = MasonryPaintState;
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, MasonryLayoutState) {
// Initialize columns
let mut columns: Vec<Vec<LayoutId>> = vec![Vec::new(); self.columns];
let mut column_heights = vec![px(0.); self.columns];
// Distribute children across columns
for child in &mut self.children {
let (child_layout_id, _) = child.request_layout(
global_id,
inspector_id,
window,
cx
);
let child_size = window.layout_bounds(child_layout_id).size;
// Find shortest column
let min_column_idx = column_heights
.iter()
.enumerate()
.min_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.unwrap()
.0;
// Add child to shortest column
columns[min_column_idx].push(child_layout_id);
column_heights[min_column_idx] += child_size.height + self.gap;
}
// Calculate total layout size
let column_width = px(200.); // Fixed column width
let total_width = column_width * self.columns as f32
+ self.gap * (self.columns - 1) as f32;
let total_height = column_heights.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap())
.copied()
.unwrap_or(px(0.));
let layout_id = window.request_layout(
Style {
size: size(total_width, total_height),
..default()
},
columns.iter().flatten().copied().collect(),
cx
);
(layout_id, MasonryLayoutState {
column_layouts: columns,
column_heights,
})
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
layout_state: &mut MasonryLayoutState,
window: &mut Window,
cx: &mut App
) -> MasonryPaintState {
let column_width = px(200.);
let mut child_bounds = Vec::new();
// Position children in columns
for (col_idx, column) in layout_state.column_layouts.iter().enumerate() {
let x_offset = bounds.left()
+ (column_width + self.gap) * col_idx as f32;
let mut y_offset = bounds.top();
for (child_idx, layout_id) in column.iter().enumerate() {
let child_size = window.layout_bounds(*layout_id).size;
let child_bound = Bounds::new(
point(x_offset, y_offset),
size(column_width, child_size.height)
);
self.children[child_idx].prepaint(
global_id,
inspector_id,
child_bound,
window,
cx
);
child_bounds.push(child_bound);
y_offset += child_size.height + self.gap;
}
}
MasonryPaintState { child_bounds }
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_layout_state: &mut MasonryLayoutState,
paint_state: &mut MasonryPaintState,
window: &mut Window,
cx: &mut App
) {
for (child, bounds) in self.children.iter_mut().zip(&paint_state.child_bounds) {
child.paint(global_id, inspector_id, *bounds, window, cx);
}
}
}
```
### Circular Layout
```rust
pub struct CircularLayout {
id: ElementId,
radius: Pixels,
children: Vec<AnyElement>,
}
impl Element for CircularLayout {
type RequestLayoutState = Vec<LayoutId>;
type PrepaintState = Vec<Bounds<Pixels>>;
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, Vec<LayoutId>) {
let child_layouts: Vec<_> = self.children
.iter_mut()
.map(|child| child.request_layout(global_id, inspector_id, window, cx).0)
.collect();
let diameter = self.radius * 2.;
let layout_id = window.request_layout(
Style {
size: size(diameter, diameter),
..default()
},
child_layouts.clone(),
cx
);
(layout_id, child_layouts)
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
layout_ids: &mut Vec<LayoutId>,
window: &mut Window,
cx: &mut App
) -> Vec<Bounds<Pixels>> {
let center = bounds.center();
let angle_step = 2.0 * std::f32::consts::PI / self.children.len() as f32;
let mut child_bounds = Vec::new();
for (i, (child, layout_id)) in self.children.iter_mut()
.zip(layout_ids.iter())
.enumerate()
{
let angle = angle_step * i as f32;
let child_size = window.layout_bounds(*layout_id).size;
// Position child on circle
let x = center.x + self.radius * angle.cos() - child_size.width / 2.;
let y = center.y + self.radius * angle.sin() - child_size.height / 2.;
let child_bound = Bounds::new(point(x, y), child_size);
child.prepaint(global_id, inspector_id, child_bound, window, cx);
child_bounds.push(child_bound);
}
child_bounds
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_layout_ids: &mut Vec<LayoutId>,
child_bounds: &mut Vec<Bounds<Pixels>>,
window: &mut Window,
cx: &mut App
) {
for (child, bounds) in self.children.iter_mut().zip(child_bounds) {
child.paint(global_id, inspector_id, *bounds, window, cx);
}
}
}
```
## Element Composition with Traits
Create reusable behaviors via traits for element composition.
### Hoverable Trait
```rust
pub trait Hoverable: Element {
fn on_hover<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&mut Window, &mut App) + 'static;
fn on_hover_end<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&mut Window, &mut App) + 'static;
}
// Implementation for custom element
pub struct HoverableElement {
id: ElementId,
content: AnyElement,
hover_handlers: Vec<Box<dyn Fn(&mut Window, &mut App)>>,
hover_end_handlers: Vec<Box<dyn Fn(&mut Window, &mut App)>>,
was_hovered: bool,
}
impl Hoverable for HoverableElement {
fn on_hover<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&mut Window, &mut App) + 'static
{
self.hover_handlers.push(Box::new(f));
self
}
fn on_hover_end<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&mut Window, &mut App) + 'static
{
self.hover_end_handlers.push(Box::new(f));
self
}
}
impl Element for HoverableElement {
type RequestLayoutState = LayoutId;
type PrepaintState = Hitbox;
fn paint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_layout: &mut LayoutId,
hitbox: &mut Hitbox,
window: &mut Window,
cx: &mut App
) {
let is_hovered = hitbox.is_hovered(window);
// Trigger hover events
if is_hovered && !self.was_hovered {
for handler in &self.hover_handlers {
handler(window, cx);
}
} else if !is_hovered && self.was_hovered {
for handler in &self.hover_end_handlers {
handler(window, cx);
}
}
self.was_hovered = is_hovered;
// Paint content
self.content.paint(bounds, window, cx);
}
// ... other methods
}
```
### Clickable Trait
```rust
pub trait Clickable: Element {
fn on_click<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static;
fn on_double_click<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static;
}
pub struct ClickableElement {
id: ElementId,
content: AnyElement,
click_handlers: Vec<Box<dyn Fn(&MouseUpEvent, &mut Window, &mut App)>>,
double_click_handlers: Vec<Box<dyn Fn(&MouseUpEvent, &mut Window, &mut App)>>,
last_click_time: Option<Instant>,
}
impl Clickable for ClickableElement {
fn on_click<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static
{
self.click_handlers.push(Box::new(f));
self
}
fn on_double_click<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static
{
self.double_click_handlers.push(Box::new(f));
self
}
}
```
## Async Element Updates
Elements that update based on async operations.
```rust
pub struct AsyncElement {
id: ElementId,
state: Entity<AsyncState>,
loading: bool,
data: Option<String>,
}
pub struct AsyncState {
loading: bool,
data: Option<String>,
}
impl Element for AsyncElement {
type RequestLayoutState = ();
type PrepaintState = Hitbox;
fn paint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_layout: &mut (),
hitbox: &mut Hitbox,
window: &mut Window,
cx: &mut App
) {
// Display loading or data
if self.loading {
// Paint loading indicator
self.paint_loading(bounds, window, cx);
} else if let Some(data) = &self.data {
// Paint data
self.paint_data(data, bounds, window, cx);
}
// Trigger async update on click
window.on_mouse_event({
let state = self.state.clone();
let hitbox = hitbox.clone();
move |event: &MouseUpEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Spawn async task
cx.spawn({
let state = state.clone();
async move {
// Perform async operation
let result = fetch_data_async().await;
// Update state on completion
state.update(cx, |state, cx| {
state.loading = false;
state.data = Some(result);
cx.notify();
});
}
}).detach();
// Set loading state immediately
state.update(cx, |state, cx| {
state.loading = true;
cx.notify();
});
cx.stop_propagation();
}
}
});
}
// ... other methods
}
async fn fetch_data_async() -> String {
// Simulate async operation
tokio::time::sleep(Duration::from_secs(1)).await;
"Data loaded!".to_string()
}
```
## Element Memoization
Optimize performance by memoizing expensive element computations.
```rust
pub struct MemoizedElement<T: PartialEq + Clone + 'static> {
id: ElementId,
value: T,
render_fn: Box<dyn Fn(&T) -> AnyElement>,
cached_element: Option<AnyElement>,
last_value: Option<T>,
}
impl<T: PartialEq + Clone + 'static> MemoizedElement<T> {
pub fn new<F>(id: ElementId, value: T, render_fn: F) -> Self
where
F: Fn(&T) -> AnyElement + 'static,
{
Self {
id,
value,
render_fn: Box::new(render_fn),
cached_element: None,
last_value: None,
}
}
}
impl<T: PartialEq + Clone + 'static> Element for MemoizedElement<T> {
type RequestLayoutState = LayoutId;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, LayoutId) {
// Check if value changed
if self.last_value.as_ref() != Some(&self.value) || self.cached_element.is_none() {
// Recompute element
self.cached_element = Some((self.render_fn)(&self.value));
self.last_value = Some(self.value.clone());
}
// Request layout for cached element
let (layout_id, _) = self.cached_element
.as_mut()
.unwrap()
.request_layout(global_id, inspector_id, window, cx);
(layout_id, layout_id)
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_layout_id: &mut LayoutId,
window: &mut Window,
cx: &mut App
) -> () {
self.cached_element
.as_mut()
.unwrap()
.prepaint(global_id, inspector_id, bounds, window, cx);
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_layout_id: &mut LayoutId,
_: &mut (),
window: &mut Window,
cx: &mut App
) {
self.cached_element
.as_mut()
.unwrap()
.paint(global_id, inspector_id, bounds, window, cx);
}
}
// Usage
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
MemoizedElement::new(
ElementId::Name("memoized".into()),
self.expensive_value.clone(),
|value| {
// Expensive rendering function only called when value changes
div().child(format!("Computed: {}", value))
}
)
}
```
## Virtual List Pattern
Efficiently render large lists by only rendering visible items.
```rust
pub struct VirtualList {
id: ElementId,
item_count: usize,
item_height: Pixels,
viewport_height: Pixels,
scroll_offset: Pixels,
render_item: Box<dyn Fn(usize) -> AnyElement>,
}
struct VirtualListState {
visible_range: Range<usize>,
visible_item_layouts: Vec<LayoutId>,
}
impl Element for VirtualList {
type RequestLayoutState = VirtualListState;
type PrepaintState = Hitbox;
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, VirtualListState) {
// Calculate visible range
let start_idx = (self.scroll_offset / self.item_height).floor() as usize;
let end_idx = ((self.scroll_offset + self.viewport_height) / self.item_height)
.ceil() as usize;
let visible_range = start_idx..end_idx.min(self.item_count);
// Request layout only for visible items
let visible_item_layouts: Vec<_> = visible_range.clone()
.map(|i| {
let mut item = (self.render_item)(i);
item.request_layout(global_id, inspector_id, window, cx).0
})
.collect();
let total_height = self.item_height * self.item_count as f32;
let layout_id = window.request_layout(
Style {
size: size(relative(1.0), self.viewport_height),
overflow: Overflow::Hidden,
..default()
},
visible_item_layouts.clone(),
cx
);
(layout_id, VirtualListState {
visible_range,
visible_item_layouts,
})
}
fn prepaint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
state: &mut VirtualListState,
window: &mut Window,
_cx: &mut App
) -> Hitbox {
// Prepaint visible items at correct positions
for (i, layout_id) in state.visible_item_layouts.iter().enumerate() {
let item_idx = state.visible_range.start + i;
let y = item_idx as f32 * self.item_height - self.scroll_offset;
let item_bounds = Bounds::new(
point(bounds.left(), bounds.top() + y),
size(bounds.width(), self.item_height)
);
// Prepaint if visible
if item_bounds.intersects(&bounds) {
// Prepaint item...
}
}
window.insert_hitbox(bounds, HitboxBehavior::Normal)
}
fn paint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
state: &mut VirtualListState,
hitbox: &mut Hitbox,
window: &mut Window,
cx: &mut App
) {
// Paint visible items
for (i, _layout_id) in state.visible_item_layouts.iter().enumerate() {
let item_idx = state.visible_range.start + i;
let y = item_idx as f32 * self.item_height - self.scroll_offset;
let item_bounds = Bounds::new(
point(bounds.left(), bounds.top() + y),
size(bounds.width(), self.item_height)
);
if item_bounds.intersects(&bounds) {
let mut item = (self.render_item)(item_idx);
item.paint(item_bounds, window, cx);
}
}
// Handle scroll
window.on_mouse_event({
let hitbox = hitbox.clone();
let total_height = self.item_height * self.item_count as f32;
move |event: &ScrollWheelEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
self.scroll_offset -= event.delta.y;
self.scroll_offset = self.scroll_offset
.max(px(0.))
.min(total_height - self.viewport_height);
cx.notify();
cx.stop_propagation();
}
}
});
}
}
// Usage: Efficiently render 10,000 items
let virtual_list = VirtualList {
id: ElementId::Name("large-list".into()),
item_count: 10_000,
item_height: px(40.),
viewport_height: px(400.),
scroll_offset: px(0.),
render_item: Box::new(|index| {
div().child(format!("Item {}", index))
}),
};
```
These advanced patterns enable sophisticated element implementations while maintaining performance and code quality.
@@ -0,0 +1,477 @@
# Element API Reference
**Contents:** [Element Trait Structure](#element-trait-structure) · [Associated Types](#associated-types) · [Methods](#methods) · [IntoElement Integration](#intoelement-integration) · [Layout System Integration](#layout-system-integration) · [Hitbox System](#hitbox-system) · [Event Handling](#event-handling) · [Cursor Styles](#cursor-styles)
## Element Trait Structure
The `Element` trait requires implementing three associated types and five methods:
```rust
pub trait Element: 'static + IntoElement {
type RequestLayoutState: 'static;
type PrepaintState: 'static;
fn id(&self) -> Option<ElementId>;
fn source_location(&self) -> Option<&'static std::panic::Location<'static>>;
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState);
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState;
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
);
}
```
## Associated Types
### RequestLayoutState
Data passed from `request_layout` to `prepaint` and `paint` phases.
**Usage:**
- Store layout calculations (styled text, child layout IDs)
- Cache expensive computations
- Pass child state between phases
**Examples:**
```rust
// Simple: no state needed
type RequestLayoutState = ();
// Single value
type RequestLayoutState = StyledText;
// Multiple values
type RequestLayoutState = (StyledText, Vec<ChildLayout>);
// Complex struct
pub struct MyLayoutState {
pub styled_text: StyledText,
pub child_layouts: Vec<(LayoutId, ChildState)>,
pub computed_bounds: Bounds<Pixels>,
}
type RequestLayoutState = MyLayoutState;
```
### PrepaintState
Data passed from `prepaint` to `paint` phase.
**Usage:**
- Store hitboxes for interaction
- Cache visual bounds
- Store prepaint results
**Examples:**
```rust
// Simple: just a hitbox
type PrepaintState = Hitbox;
// Optional hitbox
type PrepaintState = Option<Hitbox>;
// Multiple values
type PrepaintState = (Hitbox, Vec<Bounds<Pixels>>);
// Complex struct
pub struct MyPaintState {
pub hitbox: Hitbox,
pub child_bounds: Vec<Bounds<Pixels>>,
pub visible_range: Range<usize>,
}
type PrepaintState = MyPaintState;
```
## Methods
### id()
Returns optional unique identifier for debugging and inspection.
```rust
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
// Or if no ID needed
fn id(&self) -> Option<ElementId> {
None
}
```
### source_location()
Returns source location for debugging. Usually returns `None` unless debugging is needed.
```rust
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
```
### request_layout()
Calculates sizes and positions for the element tree.
**Parameters:**
- `global_id`: Global element identifier (optional)
- `inspector_id`: Inspector element identifier (optional)
- `window`: Mutable window reference
- `cx`: Mutable app context
**Returns:**
- `(LayoutId, Self::RequestLayoutState)`: Layout ID and state for next phases
**Responsibilities:**
1. Calculate child layouts by calling `child.request_layout()`
2. Create own layout using `window.request_layout()`
3. Return layout ID and state to pass to next phases
**Example:**
```rust
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
// 1. Calculate child layouts
let child_layout_id = self.child.request_layout(
global_id,
inspector_id,
window,
cx
).0;
// 2. Create own layout
let layout_id = window.request_layout(
Style {
size: size(px(200.), px(100.)),
..default()
},
vec![child_layout_id],
cx
);
// 3. Return layout ID and state
(layout_id, MyLayoutState { child_layout_id })
}
```
### prepaint()
Prepares for painting by creating hitboxes and computing final bounds.
**Parameters:**
- `global_id`: Global element identifier (optional)
- `inspector_id`: Inspector element identifier (optional)
- `bounds`: Final bounds calculated by layout engine
- `request_layout`: Mutable reference to layout state
- `window`: Mutable window reference
- `cx`: Mutable app context
**Returns:**
- `Self::PrepaintState`: State for paint phase
**Responsibilities:**
1. Compute final child bounds based on layout bounds
2. Call `child.prepaint()` for all children
3. Create hitboxes using `window.insert_hitbox()`
4. Return state for paint phase
**Example:**
```rust
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
// 1. Compute child bounds
let child_bounds = bounds; // or calculated subset
// 2. Prepaint children
self.child.prepaint(
global_id,
inspector_id,
child_bounds,
&mut request_layout.child_state,
window,
cx
);
// 3. Create hitboxes
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
// 4. Return paint state
MyPaintState { hitbox }
}
```
### paint()
Renders the element and handles interactions.
**Parameters:**
- `global_id`: Global element identifier (optional)
- `inspector_id`: Inspector element identifier (optional)
- `bounds`: Final bounds for rendering
- `request_layout`: Mutable reference to layout state
- `prepaint`: Mutable reference to prepaint state
- `window`: Mutable window reference
- `cx`: Mutable app context
**Responsibilities:**
1. Paint children first (bottom to top)
2. Paint own content (backgrounds, borders, etc.)
3. Set up interactions (mouse events, cursor styles)
**Example:**
```rust
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
// 1. Paint children first
self.child.paint(
global_id,
inspector_id,
child_bounds,
&mut request_layout.child_state,
&mut prepaint.child_paint_state,
window,
cx
);
// 2. Paint own content
window.paint_quad(paint_quad(
bounds,
Anchor::all(px(4.)),
cx.theme().background,
));
// 3. Set up interactions
window.on_mouse_event({
let hitbox = prepaint.hitbox.clone();
move |event: &MouseDownEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Handle click
cx.stop_propagation();
}
}
});
window.set_cursor_style(CursorStyle::PointingHand, &prepaint.hitbox);
}
```
## IntoElement Integration
Elements must also implement `IntoElement` to be used as children:
```rust
impl IntoElement for MyElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
```
This allows your custom element to be used directly in the element tree:
```rust
div()
.child(MyElement::new()) // Works because of IntoElement
```
## Common Parameters
### Global and Inspector IDs
Both are optional identifiers used for debugging and inspection:
- `global_id`: Unique identifier across entire app
- `inspector_id`: Identifier for dev tools/inspector
Usually passed through to children without modification.
### Window and Context
- `window: &mut Window`: Window-specific operations (painting, hitboxes, events)
- `cx: &mut App`: App-wide operations (spawning tasks, accessing globals)
## Layout System Integration
### window.request_layout()
Creates a layout node with specified style and children:
```rust
let layout_id = window.request_layout(
Style {
size: size(px(200.), px(100.)),
flex: Flex::Column,
gap: px(8.),
..default()
},
vec![child1_layout_id, child2_layout_id],
cx
);
```
### Bounds<Pixels>
Represents rectangular region:
```rust
pub struct Bounds<T> {
pub origin: Point<T>,
pub size: Size<T>,
}
// Create bounds
let bounds = Bounds::new(
point(px(10.), px(20.)),
size(px(100.), px(50.))
);
// Access properties
bounds.left() // origin.x
bounds.top() // origin.y
bounds.right() // origin.x + size.width
bounds.bottom() // origin.y + size.height
bounds.center() // center point
```
## Hitbox System
### Creating Hitboxes
```rust
// Normal hitbox (blocks events)
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
// Transparent hitbox (passes events through)
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Transparent);
```
### Using Hitboxes
```rust
// Check if hovered
if hitbox.is_hovered(window) {
// ...
}
// Set cursor style
window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
// Use in event handlers
window.on_mouse_event(move |event, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Handle event
}
});
```
## Event Handling
### Mouse Events
```rust
// Mouse down
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
if phase.bubble() && bounds.contains(&event.position) {
// Handle mouse down
cx.stop_propagation(); // Prevent bubbling
}
});
// Mouse up
window.on_mouse_event(move |event: &MouseUpEvent, phase, window, cx| {
// Handle mouse up
});
// Mouse move
window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
// Handle mouse move
});
// Scroll
window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
// Handle scroll
});
```
### Event Phase
Events go through two phases:
- **Capture**: Top-down (parent → child)
- **Bubble**: Bottom-up (child → parent)
```rust
move |event, phase, window, cx| {
if phase.capture() {
// Handle in capture phase
} else if phase.bubble() {
// Handle in bubble phase
}
cx.stop_propagation(); // Stop event from continuing
}
```
## Cursor Styles
Available cursor styles:
```rust
CursorStyle::Arrow
CursorStyle::IBeam // Text selection
CursorStyle::PointingHand // Clickable
CursorStyle::ResizeLeft
CursorStyle::ResizeRight
CursorStyle::ResizeUp
CursorStyle::ResizeDown
CursorStyle::ResizeLeftRight
CursorStyle::ResizeUpDown
CursorStyle::Crosshair
CursorStyle::OperationNotAllowed
```
Usage:
```rust
window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
```
@@ -0,0 +1,546 @@
# Element Best Practices
**Contents:** [State Management](#state-management) · [Performance Considerations](#performance-considerations) · [Interaction Handling](#interaction-handling) · [Layout Strategies](#layout-strategies) · [Error Handling](#error-handling) · [Testing Element Implementations](#testing-element-implementations) · [Common Pitfalls](#common-pitfalls) · [Performance Checklist](#performance-checklist)
## State Management
### Using Associated Types Effectively
**Good:** Use associated types to pass meaningful data between phases
```rust
// Good: Structured state with type safety
type RequestLayoutState = (StyledText, Vec<ChildLayout>);
type PrepaintState = (Hitbox, Vec<ChildBounds>);
```
**Bad:** Using empty state when you need data
```rust
// Bad: No state when you need to pass data
type RequestLayoutState = ();
type PrepaintState = ();
// Now you can't pass layout info to paint phase!
```
### Managing Complex State
For elements with complex state, create dedicated structs:
```rust
// Good: Dedicated struct for complex state
pub struct TextElementState {
pub styled_text: StyledText,
pub text_layout: TextLayout,
pub child_states: Vec<ChildState>,
}
type RequestLayoutState = TextElementState;
```
**Benefits:**
- Clear documentation of state structure
- Easy to extend
- Type-safe access
### State Lifecycle
**Golden Rule:** State flows in one direction through the phases
```
request_layout → RequestLayoutState →
prepaint → PrepaintState →
paint
```
**Don't:**
- Store state in the element struct that should be in associated types
- Try to mutate element state in paint phase (use `cx.notify()` to schedule re-render)
- Pass mutable references across phase boundaries
## Performance Considerations
### Minimize Allocations in Paint Phase
**Critical:** Paint phase is called every frame during animations. Minimize allocations.
**Good:** Pre-allocate in `request_layout` or `prepaint`
```rust
impl Element for MyElement {
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, Vec<StyledText>)
{
// Allocate once during layout
let styled_texts = self.children
.iter()
.map(|child| StyledText::new(child.text.clone()))
.collect();
(layout_id, styled_texts)
}
fn paint(&mut self, .., styled_texts: &mut Vec<StyledText>, ..) {
// Just use pre-allocated styled_texts
for text in styled_texts {
text.paint(..);
}
}
}
```
**Bad:** Allocate in `paint` phase
```rust
fn paint(&mut self, ..) {
// Bad: Allocation in paint phase!
let styled_texts: Vec<_> = self.children
.iter()
.map(|child| StyledText::new(child.text.clone()))
.collect();
}
```
### Cache Expensive Computations
Use memoization for expensive operations:
```rust
pub struct CachedElement {
// Cache key
last_text: Option<SharedString>,
last_width: Option<Pixels>,
// Cached result
cached_layout: Option<TextLayout>,
}
impl Element for CachedElement {
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, TextLayout)
{
let current_width = window.bounds().width();
// Check if cache is valid
if self.last_text.as_ref() != Some(&self.text)
|| self.last_width != Some(current_width)
|| self.cached_layout.is_none()
{
// Recompute expensive layout
self.cached_layout = Some(self.compute_text_layout(current_width));
self.last_text = Some(self.text.clone());
self.last_width = Some(current_width);
}
// Use cached layout
let layout = self.cached_layout.as_ref().unwrap();
(layout_id, layout.clone())
}
}
```
### Lazy Child Rendering
Only render visible children in scrollable containers:
```rust
fn paint(&mut self, .., bounds: Bounds<Pixels>, paint_state: &mut Self::PrepaintState, ..) {
for (i, child) in self.children.iter_mut().enumerate() {
let child_bounds = paint_state.child_bounds[i];
// Only paint visible children
if self.is_visible(&child_bounds, &bounds) {
child.paint(..);
}
}
}
fn is_visible(&self, child_bounds: &Bounds<Pixels>, container_bounds: &Bounds<Pixels>) -> bool {
child_bounds.bottom() >= container_bounds.top() &&
child_bounds.top() <= container_bounds.bottom()
}
```
## Interaction Handling
### Proper Event Bubbling
Always check phase and bounds before handling events:
```rust
fn paint(&mut self, .., window: &mut Window, cx: &mut App) {
window.on_mouse_event({
let hitbox = self.hitbox.clone();
move |event: &MouseDownEvent, phase, window, cx| {
// Check phase first
if !phase.bubble() {
return;
}
// Check if event is within bounds
if !hitbox.is_hovered(window) {
return;
}
// Handle event
self.handle_click(event);
// Stop propagation if handled
cx.stop_propagation();
}
});
}
```
**Don't forget:**
- Check `phase.bubble()` or `phase.capture()` as appropriate
- Check hitbox hover state or bounds
- Call `cx.stop_propagation()` if you handle the event
### Hitbox Management
Create hitboxes in `prepaint` phase, not `paint`:
**Good:**
```rust
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, window: &mut Window, ..) -> Hitbox {
// Create hitbox in prepaint
window.insert_hitbox(bounds, HitboxBehavior::Normal)
}
fn paint(&mut self, .., hitbox: &mut Hitbox, window: &mut Window, ..) {
// Use hitbox in paint
window.set_cursor_style(CursorStyle::PointingHand, hitbox);
}
```
**Hitbox Behaviors:**
```rust
// Normal: Blocks events from passing through
HitboxBehavior::Normal
// Transparent: Allows events to pass through to elements below
HitboxBehavior::Transparent
```
### Cursor Style Guidelines
Set appropriate cursor styles for interactivity cues:
```rust
// Text selection
window.set_cursor_style(CursorStyle::IBeam, &hitbox);
// Clickable elements (desktop convention: use default, not pointing hand)
window.set_cursor_style(CursorStyle::Arrow, &hitbox);
// Links (web convention: use pointing hand)
window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
// Resizable edges
window.set_cursor_style(CursorStyle::ResizeLeftRight, &hitbox);
```
**Desktop vs Web Convention:**
- Desktop apps: Use `Arrow` for buttons
- Web apps: Use `PointingHand` for links only
## Layout Strategies
### Fixed Size Elements
For elements with known, unchanging size:
```rust
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App) -> (LayoutId, ()) {
let layout_id = window.request_layout(
Style {
size: size(px(200.), px(100.)),
..default()
},
vec![], // No children
cx
);
(layout_id, ())
}
```
### Content-Based Sizing
For elements sized by their content:
```rust
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, Size<Pixels>)
{
// Measure content
let text_bounds = self.measure_text(window);
let padding = px(16.);
let layout_id = window.request_layout(
Style {
size: size(
text_bounds.width() + padding * 2.,
text_bounds.height() + padding * 2.,
),
..default()
},
vec![],
cx
);
(layout_id, text_bounds)
}
```
### Flexible Layouts
For elements that adapt to available space:
```rust
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, Vec<LayoutId>)
{
let mut child_layout_ids = Vec::new();
for child in &mut self.children {
let (layout_id, _) = child.request_layout(window, cx);
child_layout_ids.push(layout_id);
}
let layout_id = window.request_layout(
Style {
flex_direction: FlexDirection::Row,
gap: px(8.),
size: Size {
width: relative(1.0), // Fill parent width
height: auto(), // Auto height
},
..default()
},
child_layout_ids.clone(),
cx
);
(layout_id, child_layout_ids)
}
```
## Error Handling
### Graceful Degradation
Handle errors gracefully, don't panic:
```rust
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, Option<TextLayout>)
{
// Try to create styled text
match StyledText::new(self.text.clone()).request_layout(None, None, window, cx) {
Ok((layout_id, text_layout)) => {
(layout_id, Some(text_layout))
}
Err(e) => {
// Log error
eprintln!("Failed to layout text: {}", e);
// Fallback to simple text
let fallback_text = StyledText::new("(Error loading text)".into());
let (layout_id, _) = fallback_text.request_layout(None, None, window, cx);
(layout_id, None)
}
}
}
```
### Defensive Bounds Checking
Always validate bounds and indices:
```rust
fn paint_selection(&self, selection: &Selection, text_layout: &TextLayout, ..) {
// Validate selection bounds
let start = selection.start.min(self.text.len());
let end = selection.end.min(self.text.len());
if start >= end {
return; // Invalid selection
}
let rects = text_layout.rects_for_range(start..end);
// Paint selection...
}
```
## Testing Element Implementations
### Layout Tests
Test that layout calculations are correct:
```rust
#[cfg(test)]
mod tests {
use super::*;
use gpui_kit::TestAppContext;
#[gpui_kit::test]
fn test_element_layout(cx: &mut TestAppContext) {
cx.update(|cx| {
let mut window = cx.open_window(Default::default(), |_, _| ()).unwrap();
window.update(cx, |window, cx| {
let mut element = MyElement::new();
let (layout_id, layout_state) = element.request_layout(
None,
None,
window,
cx
);
// Assert layout properties
let bounds = window.layout_bounds(layout_id);
assert_eq!(bounds.size.width, px(200.));
assert_eq!(bounds.size.height, px(100.));
});
});
}
}
```
### Interaction Tests
Test that interactions work correctly:
```rust
#[gpui_kit::test]
fn test_element_click(cx: &mut TestAppContext) {
cx.update(|cx| {
let mut window = cx.open_window(Default::default(), |_, cx| {
cx.new(|_| MyElement::new())
}).unwrap();
window.update(cx, |window, cx| {
let view = window.root_view().unwrap();
// Simulate click
let position = point(px(10.), px(10.));
window.dispatch_event(MouseDownEvent {
position,
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
// Assert element responded
view.read(cx).assert_clicked();
});
});
}
```
## Common Pitfalls
### ❌ Storing Layout State in Element Struct
**Bad:**
```rust
pub struct MyElement {
id: ElementId,
// Bad: This should be in RequestLayoutState
cached_layout: Option<TextLayout>,
}
```
**Good:**
```rust
pub struct MyElement {
id: ElementId,
text: SharedString,
}
type RequestLayoutState = TextLayout; // Good: State in associated type
```
### ❌ Mutating Element in Paint Phase
**Bad:**
```rust
fn paint(&mut self, ..) {
self.counter += 1; // Bad: Mutating element in paint
}
```
**Good:**
```rust
fn paint(&mut self, .., window: &mut Window, cx: &mut App) {
window.on_mouse_event(move |event, phase, window, cx| {
if phase.bubble() {
self.counter += 1;
cx.notify(); // Schedule re-render
}
});
}
```
### ❌ Creating Hitboxes in Paint Phase
**Bad:**
```rust
fn paint(&mut self, .., bounds: Bounds<Pixels>, window: &mut Window, ..) {
// Bad: Creating hitbox in paint
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
}
```
**Good:**
```rust
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, window: &mut Window, ..) -> Hitbox {
// Good: Creating hitbox in prepaint
window.insert_hitbox(bounds, HitboxBehavior::Normal)
}
```
### ❌ Ignoring Event Phase
**Bad:**
```rust
window.on_mouse_event(move |event, phase, window, cx| {
// Bad: Not checking phase
self.handle_click(event);
});
```
**Good:**
```rust
window.on_mouse_event(move |event, phase, window, cx| {
// Good: Checking phase
if !phase.bubble() {
return;
}
self.handle_click(event);
});
```
## Performance Checklist
Before shipping an element implementation, verify:
- [ ] No allocations in `paint` phase (except event handlers)
- [ ] Expensive computations are cached/memoized
- [ ] Only visible children are rendered in scrollable containers
- [ ] Hitboxes created in `prepaint`, not `paint`
- [ ] Event handlers check phase and bounds
- [ ] Layout state is passed through associated types, not stored in element
- [ ] Element implements proper error handling with fallbacks
- [ ] Tests cover layout calculations and interactions
@@ -0,0 +1,632 @@
# Element Implementation Examples
Complete examples of implementing custom elements for various scenarios.
## Table of Contents
1. [Simple Text Element](#simple-text-element)
2. [Interactive Element with Selection](#interactive-element-with-selection)
3. [Complex Element with Child Management](#complex-element-with-child-management)
## Simple Text Element
A basic text element with syntax highlighting support.
```rust
pub struct SimpleText {
id: ElementId,
text: SharedString,
highlights: Vec<(Range<usize>, HighlightStyle)>,
}
impl IntoElement for SimpleText {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for SimpleText {
type RequestLayoutState = StyledText;
type PrepaintState = Hitbox;
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, Self::RequestLayoutState) {
// Create styled text with highlights
let mut runs = Vec::new();
let mut ix = 0;
for (range, highlight) in &self.highlights {
// Add unstyled text before highlight
if ix < range.start {
runs.push(window.text_style().to_run(range.start - ix));
}
// Add highlighted text
runs.push(
window.text_style()
.highlight(*highlight)
.to_run(range.len())
);
ix = range.end;
}
// Add remaining unstyled text
if ix < self.text.len() {
runs.push(window.text_style().to_run(self.text.len() - ix));
}
let styled_text = StyledText::new(self.text.clone()).with_runs(runs);
let (layout_id, _) = styled_text.request_layout(
global_id,
inspector_id,
window,
cx
);
(layout_id, styled_text)
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
styled_text: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App
) -> Self::PrepaintState {
// Prepaint the styled text
styled_text.prepaint(
global_id,
inspector_id,
bounds,
&mut (),
window,
cx
);
// Create hitbox for interaction
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
hitbox
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
styled_text: &mut Self::RequestLayoutState,
hitbox: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App
) {
// Paint the styled text
styled_text.paint(
global_id,
inspector_id,
bounds,
&mut (),
&mut (),
window,
cx
);
// Set cursor style for text
window.set_cursor_style(CursorStyle::IBeam, hitbox);
}
}
```
## Interactive Element with Selection
A text element that supports text selection via mouse interaction.
```rust
#[derive(Clone)]
pub struct Selection {
pub start: usize,
pub end: usize,
}
pub struct SelectableText {
id: ElementId,
text: SharedString,
selectable: bool,
selection: Option<Selection>,
}
impl IntoElement for SelectableText {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for SelectableText {
type RequestLayoutState = TextLayout;
type PrepaintState = Option<Hitbox>;
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, Self::RequestLayoutState) {
let styled_text = StyledText::new(self.text.clone());
let (layout_id, _) = styled_text.request_layout(
global_id,
inspector_id,
window,
cx
);
// Extract text layout for selection painting
let text_layout = styled_text.layout().clone();
(layout_id, text_layout)
}
fn prepaint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_text_layout: &mut Self::RequestLayoutState,
window: &mut Window,
_cx: &mut App
) -> Self::PrepaintState {
// Only create hitbox if selectable
if self.selectable {
Some(window.insert_hitbox(bounds, HitboxBehavior::Normal))
} else {
None
}
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
text_layout: &mut Self::RequestLayoutState,
hitbox: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App
) {
// Paint text
let styled_text = StyledText::new(self.text.clone());
styled_text.paint(
global_id,
inspector_id,
bounds,
&mut (),
&mut (),
window,
cx
);
// Paint selection if any
if let Some(selection) = &self.selection {
Self::paint_selection(selection, text_layout, &bounds, window, cx);
}
// Handle mouse events for selection
if let Some(hitbox) = hitbox {
window.set_cursor_style(CursorStyle::IBeam, hitbox);
// Mouse down to start selection
window.on_mouse_event({
let bounds = bounds.clone();
move |event: &MouseDownEvent, phase, window, cx| {
if bounds.contains(&event.position) && phase.bubble() {
// Start selection at mouse position
let char_index = Self::position_to_index(
event.position,
&bounds,
text_layout
);
self.selection = Some(Selection {
start: char_index,
end: char_index,
});
cx.notify();
cx.stop_propagation();
}
}
});
// Mouse drag to extend selection
window.on_mouse_event({
let bounds = bounds.clone();
move |event: &MouseMoveEvent, phase, window, cx| {
if let Some(selection) = &mut self.selection {
if phase.bubble() {
let char_index = Self::position_to_index(
event.position,
&bounds,
text_layout
);
selection.end = char_index;
cx.notify();
}
}
}
});
}
}
}
impl SelectableText {
fn paint_selection(
selection: &Selection,
text_layout: &TextLayout,
bounds: &Bounds<Pixels>,
window: &mut Window,
cx: &mut App
) {
// Calculate selection bounds from text layout
let selection_rects = text_layout.rects_for_range(
selection.start..selection.end
);
// Paint selection background
for rect in selection_rects {
window.paint_quad(paint_quad(
Bounds::new(
point(bounds.left() + rect.origin.x, bounds.top() + rect.origin.y),
rect.size
),
Anchor::default(),
cx.theme().selection_background,
));
}
}
fn position_to_index(
position: Point<Pixels>,
bounds: &Bounds<Pixels>,
text_layout: &TextLayout
) -> usize {
// Convert screen position to character index
let relative_pos = point(
position.x - bounds.left(),
position.y - bounds.top()
);
text_layout.index_for_position(relative_pos)
}
}
```
## Complex Element with Child Management
A container element that manages multiple children with scrolling support.
```rust
pub struct ComplexElement {
id: ElementId,
children: Vec<Box<dyn Element<RequestLayoutState = (), PrepaintState = ()>>>,
scrollable: bool,
scroll_offset: Point<Pixels>,
}
struct ComplexLayoutState {
child_layouts: Vec<LayoutId>,
total_height: Pixels,
}
struct ComplexPaintState {
child_bounds: Vec<Bounds<Pixels>>,
hitbox: Hitbox,
}
impl IntoElement for ComplexElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for ComplexElement {
type RequestLayoutState = ComplexLayoutState;
type PrepaintState = ComplexPaintState;
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, Self::RequestLayoutState) {
let mut child_layouts = Vec::new();
let mut total_height = px(0.);
// Request layout for all children
for child in &mut self.children {
let (child_layout_id, _) = child.request_layout(
global_id,
inspector_id,
window,
cx
);
child_layouts.push(child_layout_id);
// Get child size from layout
let child_size = window.layout_bounds(child_layout_id).size();
total_height += child_size.height;
}
// Create container layout
let layout_id = window.request_layout(
Style {
flex_direction: FlexDirection::Column,
gap: px(8.),
size: Size {
width: relative(1.0),
height: if self.scrollable {
// Fixed height for scrollable
px(400.)
} else {
// Auto height for non-scrollable
total_height
},
},
..default()
},
child_layouts.clone(),
cx
);
(layout_id, ComplexLayoutState {
child_layouts,
total_height,
})
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
layout_state: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App
) -> Self::PrepaintState {
let mut child_bounds = Vec::new();
let mut y_offset = self.scroll_offset.y;
// Calculate child bounds and prepaint children
for (child, layout_id) in self.children.iter_mut()
.zip(&layout_state.child_layouts)
{
let child_size = window.layout_bounds(*layout_id).size();
let child_bound = Bounds::new(
point(bounds.left(), bounds.top() + y_offset),
child_size
);
// Only prepaint visible children
if self.is_visible(&child_bound, &bounds) {
child.prepaint(
global_id,
inspector_id,
child_bound,
&mut (),
window,
cx
);
}
child_bounds.push(child_bound);
y_offset += child_size.height + px(8.); // gap
}
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
ComplexPaintState {
child_bounds,
hitbox,
}
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
layout_state: &mut Self::RequestLayoutState,
paint_state: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App
) {
// Paint background
window.paint_quad(paint_quad(
bounds,
Anchor::all(px(4.)),
cx.theme().background,
));
// Paint visible children only
for (i, child) in self.children.iter_mut().enumerate() {
let child_bounds = paint_state.child_bounds[i];
if self.is_visible(&child_bounds, &bounds) {
child.paint(
global_id,
inspector_id,
child_bounds,
&mut (),
&mut (),
window,
cx
);
}
}
// Paint scrollbar if scrollable
if self.scrollable {
self.paint_scrollbar(bounds, layout_state, window, cx);
}
// Handle scroll events
if self.scrollable {
window.on_mouse_event({
let hitbox = paint_state.hitbox.clone();
let total_height = layout_state.total_height;
let visible_height = bounds.size.height;
move |event: &ScrollWheelEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Update scroll offset
self.scroll_offset.y -= event.delta.y;
// Clamp scroll offset
let max_scroll = (total_height - visible_height).max(px(0.));
self.scroll_offset.y = self.scroll_offset.y
.max(px(0.))
.min(max_scroll);
cx.notify();
cx.stop_propagation();
}
}
});
}
}
}
impl ComplexElement {
fn is_visible(&self, child_bounds: &Bounds<Pixels>, container_bounds: &Bounds<Pixels>) -> bool {
// Check if child is within visible area
child_bounds.bottom() >= container_bounds.top() &&
child_bounds.top() <= container_bounds.bottom()
}
fn paint_scrollbar(
&self,
bounds: Bounds<Pixels>,
layout_state: &ComplexLayoutState,
window: &mut Window,
cx: &mut App
) {
let scrollbar_width = px(8.);
let visible_height = bounds.size.height;
let total_height = layout_state.total_height;
if total_height <= visible_height {
return; // No need for scrollbar
}
// Calculate scrollbar position and size
let scroll_ratio = self.scroll_offset.y / (total_height - visible_height);
let thumb_height = (visible_height / total_height) * visible_height;
let thumb_y = scroll_ratio * (visible_height - thumb_height);
// Paint scrollbar track
let track_bounds = Bounds::new(
point(bounds.right() - scrollbar_width, bounds.top()),
size(scrollbar_width, visible_height)
);
window.paint_quad(paint_quad(
track_bounds,
Anchor::default(),
cx.theme().scrollbar_track,
));
// Paint scrollbar thumb
let thumb_bounds = Bounds::new(
point(bounds.right() - scrollbar_width, bounds.top() + thumb_y),
size(scrollbar_width, thumb_height)
);
window.paint_quad(paint_quad(
thumb_bounds,
Anchor::all(px(4.)),
cx.theme().scrollbar_thumb,
));
}
}
```
## Usage Examples
### Using SimpleText
```rust
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.child(SimpleText {
id: ElementId::Name("code-text".into()),
text: "fn main() { println!(\"Hello\"); }".into(),
highlights: vec![
(0..2, HighlightStyle::keyword()),
(3..7, HighlightStyle::function()),
],
})
}
```
### Using SelectableText
```rust
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.child(SelectableText {
id: ElementId::Name("selectable-text".into()),
text: "Select this text with your mouse".into(),
selectable: true,
selection: self.current_selection.clone(),
})
}
```
### Using ComplexElement
```rust
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let children: Vec<Box<dyn Element<_, _>>> = self.items
.iter()
.map(|item| Box::new(div().child(item.name.clone())) as Box<_>)
.collect();
div()
.child(ComplexElement {
id: ElementId::Name("scrollable-list".into()),
children,
scrollable: true,
scroll_offset: self.scroll_offset,
})
}
```
@@ -0,0 +1,95 @@
# ElementId
`ElementId` is a unique identifier for a GPUI element. It is required for elements that need:
- Mouse event handling (`on_click`, `on_hover`, etc.)
- State storage via `window.use_keyed_state`
- Interaction tracking
## Making an Element Stateful
Call `.id()` on a `div()` to create a `Stateful<Div>`:
```rust
div().id("my-element") // ElementId from &str
div().id(42usize) // ElementId from usize
div().id(ElementId::from(idx)) // Explicit
```
Without `.id()`, a div cannot receive mouse events or store state.
## Accepted Types
```rust
impl Into<ElementId> for &str // "my-id"
impl Into<ElementId> for String // String::from("my-id")
impl Into<ElementId> for usize // 0, 1, 2, ...
impl Into<ElementId> for u64
impl Into<ElementId> for SharedString
```
## Uniqueness Rules
IDs must be unique within the same **stateful parent's scope** — not globally. GPUI builds a `GlobalElementId` by chaining parent IDs:
```rust
div().id("app").child(
div().id("list1").children(vec![
div().id(1usize).child("Item 1"), // GlobalId: ["app", "list1", 1]
div().id(2usize).child("Item 2"), // GlobalId: ["app", "list1", 2]
])
).child(
div().id("list2").children(vec![
div().id(1usize).child("Item 1"), // GlobalId: ["app", "list2", 1] — no conflict
])
)
```
Items in different parent scopes can reuse simple IDs (integers, short strings).
## In Component Structs
Components always store `id: ElementId` and pass it in `new()`:
```rust
#[derive(IntoElement)]
pub struct Button {
id: ElementId,
base: Stateful<Div>,
// ...
}
impl Button {
pub fn new(id: impl Into<ElementId>) -> Self {
let id = id.into();
Self {
id: id.clone(),
base: div().id(id), // id applied to base
// ...
}
}
}
impl RenderOnce for Button {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
self.base // already has .id() applied
.on_click(/* ... */)
}
}
```
## Usage at Call Sites
```rust
// Use unique string IDs for named components
Button::new("save-btn").label("Save")
Button::new("cancel-btn").label("Cancel")
// Use index-based IDs in lists
for (i, item) in items.iter().enumerate() {
div().id(i) // unique within this parent
}
// Use descriptive IDs for debugging
Input::new("search-input")
Select::new("country-select")
```
@@ -0,0 +1,509 @@
# Common Element Patterns
**Contents:** [Text Rendering Elements](#text-rendering-elements) · [Container Elements](#container-elements) · [Interactive Elements](#interactive-elements) · [Composite Elements](#composite-elements) · [Scrollable Elements](#scrollable-elements) · [Pattern Selection Guide](#pattern-selection-guide)
## Text Rendering Elements
Elements that display and manipulate text content.
### Pattern Characteristics
- Use `StyledText` for text layout and rendering
- Handle text selection in `paint` phase with hitbox interaction
- Create hitboxes for text interaction in `prepaint`
- Support text highlighting and custom styling via runs
### Implementation Template
```rust
pub struct TextElement {
id: ElementId,
text: SharedString,
style: TextStyle,
}
impl Element for TextElement {
type RequestLayoutState = StyledText;
type PrepaintState = Hitbox;
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, StyledText)
{
let styled_text = StyledText::new(self.text.clone())
.with_style(self.style);
let (layout_id, _) = styled_text.request_layout(None, None, window, cx);
(layout_id, styled_text)
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, styled_text: &mut StyledText,
window: &mut Window, cx: &mut App) -> Hitbox
{
styled_text.prepaint(None, None, bounds, &mut (), window, cx);
window.insert_hitbox(bounds, HitboxBehavior::Normal)
}
fn paint(&mut self, .., bounds: Bounds<Pixels>, styled_text: &mut StyledText,
hitbox: &mut Hitbox, window: &mut Window, cx: &mut App)
{
styled_text.paint(None, None, bounds, &mut (), &mut (), window, cx);
window.set_cursor_style(CursorStyle::IBeam, hitbox);
}
}
```
### Use Cases
- Code editors with syntax highlighting
- Rich text displays
- Labels with custom formatting
- Selectable text areas
## Container Elements
Elements that manage and layout child elements.
### Pattern Characteristics
- Manage child element layouts and positions
- Handle scrolling and clipping when needed
- Implement flex/grid-like layouts
- Coordinate child interactions and event delegation
### Implementation Template
```rust
pub struct ContainerElement {
id: ElementId,
children: Vec<AnyElement>,
direction: FlexDirection,
gap: Pixels,
}
impl Element for ContainerElement {
type RequestLayoutState = Vec<LayoutId>;
type PrepaintState = Vec<Bounds<Pixels>>;
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, Vec<LayoutId>)
{
let child_layout_ids: Vec<_> = self.children
.iter_mut()
.map(|child| child.request_layout(window, cx).0)
.collect();
let layout_id = window.request_layout(
Style {
flex_direction: self.direction,
gap: self.gap,
..default()
},
child_layout_ids.clone(),
cx
);
(layout_id, child_layout_ids)
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, layout_ids: &mut Vec<LayoutId>,
window: &mut Window, cx: &mut App) -> Vec<Bounds<Pixels>>
{
let mut child_bounds = Vec::new();
for (child, layout_id) in self.children.iter_mut().zip(layout_ids.iter()) {
let child_bound = window.layout_bounds(*layout_id);
child.prepaint(child_bound, window, cx);
child_bounds.push(child_bound);
}
child_bounds
}
fn paint(&mut self, .., child_bounds: &mut Vec<Bounds<Pixels>>,
window: &mut Window, cx: &mut App)
{
for (child, bounds) in self.children.iter_mut().zip(child_bounds.iter()) {
child.paint(*bounds, window, cx);
}
}
}
```
### Use Cases
- Panels and split views
- List containers
- Grid layouts
- Tab containers
## Interactive Elements
Elements that respond to user input (mouse, keyboard, touch).
### Pattern Characteristics
- Create appropriate hitboxes for interaction areas
- Handle mouse/keyboard/touch events properly
- Manage focus and cursor styles
- Support hover, active, and disabled states
### Implementation Template
```rust
pub struct InteractiveElement {
id: ElementId,
content: AnyElement,
on_click: Option<Box<dyn Fn(&MouseUpEvent, &mut Window, &mut App)>>,
hover_style: Option<Style>,
}
impl Element for InteractiveElement {
type RequestLayoutState = LayoutId;
type PrepaintState = (Hitbox, bool); // hitbox and is_hovered
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, LayoutId)
{
let (content_layout, _) = self.content.request_layout(window, cx);
(content_layout, content_layout)
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, content_layout: &mut LayoutId,
window: &mut Window, cx: &mut App) -> (Hitbox, bool)
{
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
let is_hovered = hitbox.is_hovered(window);
self.content.prepaint(bounds, window, cx);
(hitbox, is_hovered)
}
fn paint(&mut self, .., bounds: Bounds<Pixels>, content_layout: &mut LayoutId,
prepaint: &mut (Hitbox, bool), window: &mut Window, cx: &mut App)
{
let (hitbox, is_hovered) = prepaint;
// Paint hover background if hovered
if *is_hovered {
if let Some(hover_style) = &self.hover_style {
window.paint_quad(paint_quad(
bounds,
Anchor::all(px(4.)),
hover_style.background_color.unwrap_or(cx.theme().hover),
));
}
}
// Paint content
self.content.paint(bounds, window, cx);
// Handle click
if let Some(on_click) = self.on_click.as_ref() {
window.on_mouse_event({
let on_click = on_click.clone();
let hitbox = hitbox.clone();
move |event: &MouseUpEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
on_click(event, window, cx);
cx.stop_propagation();
}
}
});
}
// Set cursor style
window.set_cursor_style(CursorStyle::PointingHand, hitbox);
}
}
```
### Use Cases
- Buttons
- Links
- Clickable cards
- Drag handles
- Menu items
## Composite Elements
Elements that combine multiple child elements with complex coordination.
### Pattern Characteristics
- Combine multiple child elements with different types
- Manage complex state across children
- Coordinate animations and transitions
- Handle focus delegation between children
### Implementation Template
```rust
pub struct CompositeElement {
id: ElementId,
header: AnyElement,
content: AnyElement,
footer: Option<AnyElement>,
}
struct CompositeLayoutState {
header_layout: LayoutId,
content_layout: LayoutId,
footer_layout: Option<LayoutId>,
}
struct CompositePaintState {
header_bounds: Bounds<Pixels>,
content_bounds: Bounds<Pixels>,
footer_bounds: Option<Bounds<Pixels>>,
}
impl Element for CompositeElement {
type RequestLayoutState = CompositeLayoutState;
type PrepaintState = CompositePaintState;
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, CompositeLayoutState)
{
let (header_layout, _) = self.header.request_layout(window, cx);
let (content_layout, _) = self.content.request_layout(window, cx);
let footer_layout = self.footer.as_mut()
.map(|f| f.request_layout(window, cx).0);
let mut children = vec![header_layout, content_layout];
if let Some(footer) = footer_layout {
children.push(footer);
}
let layout_id = window.request_layout(
Style {
flex_direction: FlexDirection::Column,
size: Size {
width: relative(1.0),
height: auto(),
},
..default()
},
children,
cx
);
(layout_id, CompositeLayoutState {
header_layout,
content_layout,
footer_layout,
})
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, layout: &mut CompositeLayoutState,
window: &mut Window, cx: &mut App) -> CompositePaintState
{
let header_bounds = window.layout_bounds(layout.header_layout);
let content_bounds = window.layout_bounds(layout.content_layout);
let footer_bounds = layout.footer_layout
.map(|id| window.layout_bounds(id));
self.header.prepaint(header_bounds, window, cx);
self.content.prepaint(content_bounds, window, cx);
if let (Some(footer), Some(bounds)) = (&mut self.footer, footer_bounds) {
footer.prepaint(bounds, window, cx);
}
CompositePaintState {
header_bounds,
content_bounds,
footer_bounds,
}
}
fn paint(&mut self, .., paint_state: &mut CompositePaintState,
window: &mut Window, cx: &mut App)
{
self.header.paint(paint_state.header_bounds, window, cx);
self.content.paint(paint_state.content_bounds, window, cx);
if let (Some(footer), Some(bounds)) = (&mut self.footer, paint_state.footer_bounds) {
footer.paint(bounds, window, cx);
}
}
}
```
### Use Cases
- Dialog boxes (header + content + footer)
- Cards with multiple sections
- Form layouts
- Panels with toolbars
## Scrollable Elements
Elements with scrollable content areas.
### Pattern Characteristics
- Manage scroll state (offset, velocity)
- Handle scroll events (wheel, drag, touch)
- Paint scrollbars (track and thumb)
- Clip content to visible area
### Implementation Template
```rust
pub struct ScrollableElement {
id: ElementId,
content: AnyElement,
scroll_offset: Point<Pixels>,
content_size: Size<Pixels>,
}
struct ScrollPaintState {
hitbox: Hitbox,
visible_bounds: Bounds<Pixels>,
}
impl Element for ScrollableElement {
type RequestLayoutState = (LayoutId, Size<Pixels>);
type PrepaintState = ScrollPaintState;
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, (LayoutId, Size<Pixels>))
{
let (content_layout, _) = self.content.request_layout(window, cx);
let content_size = window.layout_bounds(content_layout).size;
let layout_id = window.request_layout(
Style {
size: Size {
width: relative(1.0),
height: px(400.), // Fixed viewport height
},
overflow: Overflow::Hidden,
..default()
},
vec![content_layout],
cx
);
(layout_id, (content_layout, content_size))
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, layout: &mut (LayoutId, Size<Pixels>),
window: &mut Window, cx: &mut App) -> ScrollPaintState
{
let (content_layout, content_size) = layout;
// Calculate content bounds with scroll offset
let content_bounds = Bounds::new(
point(bounds.left(), bounds.top() - self.scroll_offset.y),
*content_size
);
self.content.prepaint(content_bounds, window, cx);
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
ScrollPaintState {
hitbox,
visible_bounds: bounds,
}
}
fn paint(&mut self, .., layout: &mut (LayoutId, Size<Pixels>),
paint_state: &mut ScrollPaintState, window: &mut Window, cx: &mut App)
{
let (_, content_size) = layout;
// Paint content
self.content.paint(paint_state.visible_bounds, window, cx);
// Paint scrollbar
self.paint_scrollbar(paint_state.visible_bounds, *content_size, window, cx);
// Handle scroll events
window.on_mouse_event({
let hitbox = paint_state.hitbox.clone();
let content_height = content_size.height;
let visible_height = paint_state.visible_bounds.size.height;
move |event: &ScrollWheelEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Update scroll offset
self.scroll_offset.y -= event.delta.y;
// Clamp to valid range
let max_scroll = (content_height - visible_height).max(px(0.));
self.scroll_offset.y = self.scroll_offset.y
.max(px(0.))
.min(max_scroll);
cx.notify();
cx.stop_propagation();
}
}
});
}
}
impl ScrollableElement {
fn paint_scrollbar(
&self,
bounds: Bounds<Pixels>,
content_size: Size<Pixels>,
window: &mut Window,
cx: &mut App
) {
let visible_height = bounds.size.height;
let content_height = content_size.height;
if content_height <= visible_height {
return; // No scrollbar needed
}
let scrollbar_width = px(8.);
// Calculate thumb position and size
let scroll_ratio = self.scroll_offset.y / (content_height - visible_height);
let thumb_height = (visible_height / content_height) * visible_height;
let thumb_y = scroll_ratio * (visible_height - thumb_height);
// Paint track
window.paint_quad(paint_quad(
Bounds::new(
point(bounds.right() - scrollbar_width, bounds.top()),
size(scrollbar_width, visible_height)
),
Anchor::default(),
cx.theme().scrollbar_track,
));
// Paint thumb
window.paint_quad(paint_quad(
Bounds::new(
point(bounds.right() - scrollbar_width, bounds.top() + thumb_y),
size(scrollbar_width, thumb_height)
),
Anchor::all(px(4.)),
cx.theme().scrollbar_thumb,
));
}
}
```
### Use Cases
- Scrollable lists
- Code editors with large files
- Long-form text content
- Image galleries
## Pattern Selection Guide
| Need | Pattern | Complexity |
|------|---------|------------|
| Display styled text | Text Rendering | Low |
| Layout multiple children | Container | Low-Medium |
| Handle clicks/hovers | Interactive | Medium |
| Complex multi-part UI | Composite | Medium-High |
| Large content with scrolling | Scrollable | High |
Choose the simplest pattern that meets your requirements, then extend as needed.
@@ -0,0 +1,107 @@
# Custom Elements
**Contents:** [When to Use](#when-to-use) · [Quick Start](#quick-start) · [Core Concepts](#core-concepts) · [Reference Documentation](#reference-documentation)
## When to Use
Use the low-level `Element` trait when:
- Need fine-grained control over layout calculation
- Building complex, performance-critical components
- Implementing custom layout algorithms (masonry, circular, etc.)
- High-level `Render`/`RenderOnce` APIs are insufficient
**Prefer `Render`/`RenderOnce` for:** Simple components, standard layouts, declarative UI
## Quick Start
The `Element` trait provides direct control over three rendering phases:
```rust
impl Element for MyElement {
type RequestLayoutState = MyLayoutState; // Data passed to later phases
type PrepaintState = MyPaintState; // Data for painting
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
// Phase 1: Calculate sizes and positions
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, Self::RequestLayoutState)
{
let layout_id = window.request_layout(
Style { size: size(px(200.), px(100.)), ..default() },
vec![],
cx
);
(layout_id, MyLayoutState { /* ... */ })
}
// Phase 2: Create hitboxes, prepare for painting
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, layout: &mut Self::RequestLayoutState,
window: &mut Window, cx: &mut App) -> Self::PrepaintState
{
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
MyPaintState { hitbox }
}
// Phase 3: Render and handle interactions
fn paint(&mut self, .., bounds: Bounds<Pixels>, layout: &mut Self::RequestLayoutState,
paint_state: &mut Self::PrepaintState, window: &mut Window, cx: &mut App)
{
window.paint_quad(paint_quad(bounds, Anchor::all(px(4.)), cx.theme().background));
window.on_mouse_event({
let hitbox = paint_state.hitbox.clone();
move |event: &MouseDownEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Handle interaction
cx.stop_propagation();
}
}
});
}
}
// Enable element to be used as child
impl IntoElement for MyElement {
type Element = Self;
fn into_element(self) -> Self::Element { self }
}
```
## Core Concepts
### Three-Phase Rendering
1. **request_layout**: Calculate sizes and positions, return layout ID and state
2. **prepaint**: Create hitboxes, compute final bounds, prepare for painting
3. **paint**: Render element, set up interactions (mouse events, cursor styles)
### State Flow
```
RequestLayoutState → PrepaintState → paint
```
State flows in one direction through associated types, passed as mutable references between phases.
### Key Operations
- **Layout**: `window.request_layout(style, children, cx)` - Create layout node
- **Hitboxes**: `window.insert_hitbox(bounds, behavior)` - Create interaction area
- **Painting**: `window.paint_quad(...)` - Render visual content
- **Events**: `window.on_mouse_event(handler)` - Handle user input
## Reference Documentation
### Complete API Documentation
- **API**: See [element-api.md](element-api.md) — associated types, hitbox system, event handling, cursor styles
- **Examples**: See [element-examples.md](element-examples.md) — text, interactive, complex elements
- **Patterns**: See [element-patterns.md](element-patterns.md) — text, container, interactive, composite, scrollable
- **Best Practices**: See [element-best-practices.md](element-best-practices.md) — performance, state, common pitfalls
- **Advanced**: See [element-advanced.md](element-advanced.md) — masonry/circular layouts, memoization, virtual lists
@@ -0,0 +1,528 @@
# Advanced Entity Patterns
**Contents:** [Entity Collections Management](#entity-collections-management) · [Conditional Update Patterns](#conditional-update-patterns) · [Entity State Machine Pattern](#entity-state-machine-pattern) · [Entity Proxy Pattern](#entity-proxy-pattern) · [Cascading Updates Pattern](#cascading-updates-pattern) · [Entity Snapshot Pattern](#entity-snapshot-pattern) · [Entity Transaction Pattern](#entity-transaction-pattern) · [Entity Pool Pattern](#entity-pool-pattern)
## Entity Collections Management
### Dynamic Collection with Cleanup
```rust
struct EntityCollection<T> {
strong_refs: Vec<Entity<T>>,
weak_refs: Vec<WeakEntity<T>>,
}
impl<T> EntityCollection<T> {
fn new() -> Self {
Self {
strong_refs: Vec::new(),
weak_refs: Vec::new(),
}
}
fn add(&mut self, entity: Entity<T>, cx: &mut App) {
self.strong_refs.push(entity.clone());
self.weak_refs.push(entity.downgrade());
}
fn remove(&mut self, entity_id: EntityId, cx: &mut App) {
self.strong_refs.retain(|e| e.entity_id() != entity_id);
self.weak_refs.retain(|w| {
w.upgrade()
.map(|e| e.entity_id() != entity_id)
.unwrap_or(false)
});
}
fn cleanup_invalid(&mut self, cx: &mut App) {
self.weak_refs.retain(|weak| weak.upgrade().is_some());
}
fn for_each<F>(&self, cx: &mut App, mut f: F)
where
F: FnMut(&Entity<T>, &mut App),
{
for entity in &self.strong_refs {
f(entity, cx);
}
}
fn for_each_weak<F>(&mut self, cx: &mut App, mut f: F)
where
F: FnMut(Entity<T>, &mut App),
{
self.weak_refs.retain(|weak| {
if let Some(entity) = weak.upgrade() {
f(entity, cx);
true
} else {
false // Remove invalid weak references
}
});
}
}
```
### Entity Registry Pattern
```rust
use std::collections::HashMap;
struct EntityRegistry<T> {
entities: HashMap<EntityId, WeakEntity<T>>,
}
impl<T> EntityRegistry<T> {
fn new() -> Self {
Self {
entities: HashMap::new(),
}
}
fn register(&mut self, entity: &Entity<T>) {
self.entities.insert(entity.entity_id(), entity.downgrade());
}
fn unregister(&mut self, entity_id: EntityId) {
self.entities.remove(&entity_id);
}
fn get(&self, entity_id: EntityId) -> Option<Entity<T>> {
self.entities.get(&entity_id)?.upgrade()
}
fn cleanup(&mut self) {
self.entities.retain(|_, weak| weak.upgrade().is_some());
}
fn count(&self) -> usize {
self.entities.len()
}
fn all_entities(&self) -> Vec<Entity<T>> {
self.entities
.values()
.filter_map(|weak| weak.upgrade())
.collect()
}
}
```
## Conditional Update Patterns
### Debounced Updates
```rust
use std::time::{Duration, Instant};
struct DebouncedEntity<T> {
entity: Entity<T>,
last_update: Instant,
debounce_duration: Duration,
pending_update: Option<Box<dyn FnOnce(&mut T, &mut Context<T>)>>,
}
impl<T: 'static> DebouncedEntity<T> {
fn new(entity: Entity<T>, debounce_ms: u64) -> Self {
Self {
entity,
last_update: Instant::now(),
debounce_duration: Duration::from_millis(debounce_ms),
pending_update: None,
}
}
fn update<F>(&mut self, cx: &mut App, update_fn: F)
where
F: FnOnce(&mut T, &mut Context<T>) + 'static,
{
let now = Instant::now();
let elapsed = now.duration_since(self.last_update);
if elapsed >= self.debounce_duration {
// Execute immediately
self.entity.update(cx, update_fn);
self.last_update = now;
self.pending_update = None;
} else {
// Store for later
self.pending_update = Some(Box::new(update_fn));
// Schedule execution
let entity = self.entity.clone();
let delay = self.debounce_duration - elapsed;
cx.spawn(async move |cx| {
tokio::time::sleep(delay).await;
if let Some(update) = self.pending_update.take() {
entity.update(cx, |state, inner_cx| {
update(state, inner_cx);
});
}
}).detach();
}
}
}
```
### Throttled Updates
```rust
struct ThrottledEntity<T> {
entity: Entity<T>,
last_update: Instant,
throttle_duration: Duration,
}
impl<T: 'static> ThrottledEntity<T> {
fn new(entity: Entity<T>, throttle_ms: u64) -> Self {
Self {
entity,
last_update: Instant::now(),
throttle_duration: Duration::from_millis(throttle_ms),
}
}
fn try_update<F>(&mut self, cx: &mut App, update_fn: F) -> bool
where
F: FnOnce(&mut T, &mut Context<T>),
{
let now = Instant::now();
let elapsed = now.duration_since(self.last_update);
if elapsed >= self.throttle_duration {
self.entity.update(cx, update_fn);
self.last_update = now;
true
} else {
false // Update throttled
}
}
}
```
## Entity State Machine Pattern
```rust
enum AppState {
Idle,
Loading,
Loaded(String),
Error(String),
}
struct StateMachine {
state: AppState,
}
impl StateMachine {
fn new() -> Self {
Self {
state: AppState::Idle,
}
}
fn start_loading(&mut self, cx: &mut Context<Self>) {
if matches!(self.state, AppState::Idle | AppState::Error(_)) {
self.state = AppState::Loading;
cx.notify();
let weak_entity = cx.entity().downgrade();
cx.spawn(async move |cx| {
let result = perform_load().await;
let _ = weak_entity.update(cx, |state, cx| {
match result {
Ok(data) => state.on_load_success(data, cx),
Err(e) => state.on_load_error(e.to_string(), cx),
}
});
}).detach();
}
}
fn on_load_success(&mut self, data: String, cx: &mut Context<Self>) {
if matches!(self.state, AppState::Loading) {
self.state = AppState::Loaded(data);
cx.notify();
}
}
fn on_load_error(&mut self, error: String, cx: &mut Context<Self>) {
if matches!(self.state, AppState::Loading) {
self.state = AppState::Error(error);
cx.notify();
}
}
fn reset(&mut self, cx: &mut Context<Self>) {
self.state = AppState::Idle;
cx.notify();
}
}
async fn perform_load() -> Result<String, anyhow::Error> {
// Actual load implementation
Ok("Data".to_string())
}
```
## Entity Proxy Pattern
```rust
struct EntityProxy<T> {
entity: WeakEntity<T>,
}
impl<T> EntityProxy<T> {
fn new(entity: &Entity<T>) -> Self {
Self {
entity: entity.downgrade(),
}
}
fn with<F, R>(&self, cx: &mut App, f: F) -> Result<R, anyhow::Error>
where
F: FnOnce(&T, &App) -> R,
{
self.entity.read_with(cx, f)
}
fn update<F, R>(&self, cx: &mut App, f: F) -> Result<R, anyhow::Error>
where
F: FnOnce(&mut T, &mut Context<T>) -> R,
{
self.entity.update(cx, f)
}
fn is_valid(&self, cx: &App) -> bool {
self.entity.upgrade().is_some()
}
}
```
## Cascading Updates Pattern
```rust
struct CascadingUpdater {
entities: Vec<WeakEntity<UpdateTarget>>,
}
impl CascadingUpdater {
fn new() -> Self {
Self {
entities: Vec::new(),
}
}
fn add_target(&mut self, entity: &Entity<UpdateTarget>) {
self.entities.push(entity.downgrade());
}
fn cascade_update<F>(&mut self, cx: &mut App, update_fn: F)
where
F: Fn(&mut UpdateTarget, &mut Context<UpdateTarget>) + Clone,
{
// Update all entities in sequence
self.entities.retain(|weak| {
if let Ok(_) = weak.update(cx, |state, inner_cx| {
update_fn.clone()(state, inner_cx);
}) {
true // Keep valid entity
} else {
false // Remove invalid entity
}
});
}
}
struct UpdateTarget {
value: i32,
}
```
## Entity Snapshot Pattern
```rust
use serde::{Serialize, Deserialize};
#[derive(Clone, Serialize, Deserialize)]
struct EntitySnapshot {
data: String,
timestamp: u64,
}
struct SnapshotableEntity {
data: String,
snapshots: Vec<EntitySnapshot>,
}
impl SnapshotableEntity {
fn new(data: String) -> Self {
Self {
data,
snapshots: Vec::new(),
}
}
fn take_snapshot(&mut self, cx: &mut Context<Self>) {
let snapshot = EntitySnapshot {
data: self.data.clone(),
timestamp: current_timestamp(),
};
self.snapshots.push(snapshot);
cx.notify();
}
fn restore_snapshot(&mut self, index: usize, cx: &mut Context<Self>) -> Result<(), String> {
if let Some(snapshot) = self.snapshots.get(index) {
self.data = snapshot.data.clone();
cx.notify();
Ok(())
} else {
Err("Invalid snapshot index".to_string())
}
}
fn clear_old_snapshots(&mut self, keep_last: usize, cx: &mut Context<Self>) {
if self.snapshots.len() > keep_last {
self.snapshots.drain(0..self.snapshots.len() - keep_last);
cx.notify();
}
}
}
fn current_timestamp() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
}
```
## Entity Transaction Pattern
```rust
struct Transaction<T> {
entity: Entity<T>,
original_state: Option<T>,
}
impl<T: Clone> Transaction<T> {
fn begin(entity: Entity<T>, cx: &mut App) -> Self {
let original_state = entity.read(cx).clone();
Self {
entity,
original_state: Some(original_state),
}
}
fn update<F>(&mut self, cx: &mut App, update_fn: F)
where
F: FnOnce(&mut T, &mut Context<T>),
{
self.entity.update(cx, update_fn);
}
fn commit(mut self, cx: &mut App) {
self.original_state = None; // Don't rollback
self.entity.update(cx, |_, cx| {
cx.notify();
});
}
fn rollback(mut self, cx: &mut App) {
if let Some(original) = self.original_state.take() {
self.entity.update(cx, |state, cx| {
*state = original;
cx.notify();
});
}
}
}
impl<T> Drop for Transaction<T> {
fn drop(&mut self) {
// Auto-rollback if not committed
if self.original_state.is_some() {
eprintln!("Warning: Transaction dropped without commit");
}
}
}
// Usage
fn perform_transaction(entity: Entity<MyState>, cx: &mut App) -> Result<(), String> {
let mut tx = Transaction::begin(entity, cx);
tx.update(cx, |state, cx| {
state.value = 42;
});
if validate_state(&tx.entity, cx)? {
tx.commit(cx);
Ok(())
} else {
tx.rollback(cx);
Err("Validation failed".to_string())
}
}
```
## Entity Pool Pattern
```rust
struct EntityPool<T> {
available: Vec<Entity<T>>,
in_use: Vec<WeakEntity<T>>,
factory: Box<dyn Fn(&mut App) -> Entity<T>>,
}
impl<T: 'static> EntityPool<T> {
fn new<F>(factory: F) -> Self
where
F: Fn(&mut App) -> Entity<T> + 'static,
{
Self {
available: Vec::new(),
in_use: Vec::new(),
factory: Box::new(factory),
}
}
fn acquire(&mut self, cx: &mut App) -> Entity<T> {
let entity = if let Some(entity) = self.available.pop() {
entity
} else {
(self.factory)(cx)
};
self.in_use.push(entity.downgrade());
entity
}
fn release(&mut self, entity: Entity<T>, cx: &mut App) {
// Reset entity state if needed
entity.update(cx, |state, cx| {
// Reset logic here
cx.notify();
});
self.available.push(entity);
self.cleanup_in_use();
}
fn cleanup_in_use(&mut self) {
self.in_use.retain(|weak| weak.upgrade().is_some());
}
fn pool_size(&self) -> (usize, usize) {
(self.available.len(), self.in_use.len())
}
}
```
These advanced patterns provide powerful abstractions for managing complex entity scenarios while maintaining code quality and performance.
@@ -0,0 +1,382 @@
# Entity API Reference
**Contents:** [Entity Types](#entity-types) · [Entity Creation](#entity-creation) · [Entity Operations](#entity-operations) · [Context Methods](#context-methods-for-entities) · [Async Operations](#async-operations) · [Entity Lifecycle](#entity-lifecycle) · [EntityId](#entityid) · [Error Handling](#error-handling) · [Type Conversions](#type-conversions)
## Entity Types
### Entity<T>
A strong reference to state of type `T`.
**Methods:**
- `entity_id()` → `EntityId` - Returns unique identifier
- `downgrade()` → `WeakEntity<T>` - Creates weak reference
- `read(cx)` → `&T` - Immutable access to state
- `read_with(cx, |state, cx| ...)` → `R` - Read with closure, returns closure result
- `update(cx, |state, cx| ...)` → `R` - Mutable update with `Context<T>`, returns closure result
- `update_in(cx, |state, window, cx| ...)` → `R` - Update with `Window` access (requires `AsyncWindowContext` or `VisualTestContext`)
**Important Notes:**
- Trying to update an entity while it's already being updated will panic
- Within closures, use the inner `cx` provided to avoid multiple borrow issues
- With async contexts, return values are wrapped in `anyhow::Result`
### WeakEntity<T>
A weak reference to state of type `T`.
**Methods:**
- `upgrade()` → `Option<Entity<T>>` - Convert to strong reference if still alive
- `read_with(cx, |state, cx| ...)` → `Result<R>` - Read if entity exists
- `update(cx, |state, cx| ...)` → `Result<R>` - Update if entity exists
- `update_in(cx, |state, window, cx| ...)` → `Result<R>` - Update with window if entity exists
**Use Cases:**
- Avoid circular dependencies between entities
- Store references in closures/callbacks without preventing cleanup
- Optional relationships between components
**Important:** All operations return `Result` since the entity may no longer exist.
### AnyEntity
Dynamically-typed entity handle for storing entities of different types.
### AnyWeakEntity
Dynamically-typed weak entity handle.
## Entity Creation
### cx.new()
Create new entity with initial state.
```rust
let entity = cx.new(|cx| MyState {
count: 0,
name: "Default".to_string(),
});
```
**Parameters:**
- `cx: &mut App` or other context type
- Closure receiving `&mut Context<T>` returning initial state `T`
**Returns:** `Entity<T>`
## Entity Operations
### Reading State
#### read()
Direct read-only access to state.
```rust
let count = my_entity.read(cx).count;
```
**Use when:** Simple field access, no context operations needed.
#### read_with()
Read with context access in closure.
```rust
let count = my_entity.read_with(cx, |state, cx| {
// Can access both state and context
state.count
});
// Return multiple values
let (count, theme) = my_entity.read_with(cx, |state, cx| {
(state.count, cx.theme().clone())
});
```
**Use when:** Need context operations, multiple return values, complex logic.
### Updating State
#### update()
Mutable update with `Context<T>`.
```rust
my_entity.update(cx, |state, cx| {
state.count += 1;
cx.notify(); // Trigger re-render
});
```
**Available Operations:**
- `cx.notify()` - Trigger re-render
- `cx.entity()` - Get current entity
- `cx.emit(event)` - Emit event
- `cx.spawn(task)` - Spawn async task
- Other `Context<T>` methods
#### update_in()
Update with both `Window` and `Context<T>` access.
```rust
my_entity.update_in(cx, |state, window, cx| {
state.focused = window.is_window_focused();
cx.notify();
});
```
**Requires:** `AsyncWindowContext` or `VisualTestContext`
**Use when:** Need window-specific operations like focus state, window bounds, etc.
## Context Methods for Entities
### cx.entity()
Get current entity being updated.
```rust
impl MyComponent {
fn some_method(&mut self, cx: &mut Context<Self>) {
let current_entity = cx.entity(); // Entity<MyComponent>
let weak = current_entity.downgrade();
}
}
```
### cx.observe()
Observe entity for changes.
```rust
cx.observe(&entity, |this, observed_entity, cx| {
// Called when observed_entity.update() calls cx.notify()
println!("Entity changed");
}).detach();
```
**Returns:** `Subscription` - Call `.detach()` to make permanent
### cx.subscribe()
Subscribe to events from entity.
```rust
cx.subscribe(&entity, |this, emitter, event: &SomeEvent, cx| {
// Called when emitter emits SomeEvent
match event {
SomeEvent::DataChanged => {
cx.notify();
}
}
}).detach();
```
**Returns:** `Subscription` - Call `.detach()` to make permanent
### cx.observe_new_entities()
Register callback for new entities of a type.
```rust
cx.observe_new_entities::<MyState>(|entity, cx| {
println!("New entity created: {:?}", entity.entity_id());
}).detach();
```
## Async Operations
### cx.spawn()
Spawn foreground task (UI thread).
```rust
cx.spawn(async move |this, cx| {
// `this`: WeakEntity<T>
// `cx`: &mut AsyncApp
let result = some_async_work().await;
// Update entity safely
let _ = this.update(cx, |state, cx| {
state.data = result;
cx.notify();
});
}).detach();
```
**Note:** Always use weak entity reference in spawned tasks to prevent retain cycles.
### cx.background_spawn()
Spawn background task (background thread).
```rust
cx.background_spawn(async move {
// Long-running computation
let result = heavy_computation().await;
// Cannot directly update entities here
// Use channels or spawn foreground task to update
}).detach();
```
## Entity Lifecycle
### Creation
Entities are created via `cx.new()` and immediately registered in the app.
### Reference Counting
- `Entity<T>` is a strong reference (increases reference count)
- `WeakEntity<T>` is a weak reference (does not increase reference count)
- Cloning `Entity<T>` increases reference count
### Disposal
Entities are automatically disposed when all strong references are dropped.
```rust
{
let entity = cx.new(|cx| MyState::default());
// entity exists
} // entity dropped here if no other strong references exist
```
**Memory Leak Prevention:**
- Use `WeakEntity` in closures/callbacks
- Use `WeakEntity` for parent-child relationships
- Avoid circular strong references
## EntityId
Every entity has a unique identifier.
```rust
let id: EntityId = entity.entity_id();
// EntityIds can be compared
if entity1.entity_id() == entity2.entity_id() {
// Same entity
}
```
**Use Cases:**
- Debugging and logging
- Entity comparison without borrowing
- Hash maps keyed by entity
## Error Handling
### WeakEntity Operations
All `WeakEntity` operations return `Result`:
```rust
let weak = entity.downgrade();
// Handle potential failure
match weak.read_with(cx, |state, cx| state.count) {
Ok(count) => println!("Count: {}", count),
Err(e) => eprintln!("Entity no longer exists: {}", e),
}
// Or use Result combinators
let _ = weak.update(cx, |state, cx| {
state.count += 1;
cx.notify();
}).ok(); // Ignore errors
```
### Update Panics
Nested updates on the same entity will panic:
```rust
// ❌ Will panic
entity.update(cx, |state1, cx| {
entity.update(cx, |state2, cx| {
// Panic: entity already borrowed
});
});
```
**Solution:** Perform updates sequentially or use different entities.
## Type Conversions
### Entity → WeakEntity
```rust
let entity: Entity<T> = cx.new(|cx| T::default());
let weak: WeakEntity<T> = entity.downgrade();
```
### WeakEntity → Entity
```rust
let weak: WeakEntity<T> = entity.downgrade();
let strong: Option<Entity<T>> = weak.upgrade();
```
### AnyEntity
```rust
let any: AnyEntity = entity.into();
let typed: Option<Entity<T>> = any.downcast::<T>();
```
## Best Practice Guidelines
### Always Use Inner cx
```rust
// ✅ Good: Use inner cx
entity.update(cx, |state, inner_cx| {
inner_cx.notify(); // Use inner_cx, not outer cx
});
// ❌ Bad: Use outer cx
entity.update(cx, |state, inner_cx| {
cx.notify(); // Wrong! Multiple borrow error
});
```
### Weak References in Closures
```rust
// ✅ Good: Weak reference
let weak = cx.entity().downgrade();
callback(move || {
let _ = weak.update(cx, |state, cx| {
cx.notify();
});
});
// ❌ Bad: Strong reference (retain cycle)
let strong = cx.entity();
callback(move || {
strong.update(cx, |state, cx| {
// May never be dropped
cx.notify();
});
});
```
### Sequential Updates
```rust
// ✅ Good: Sequential updates
entity1.update(cx, |state, cx| { /* ... */ });
entity2.update(cx, |state, cx| { /* ... */ });
// ❌ Bad: Nested updates
entity1.update(cx, |_, cx| {
entity2.update(cx, |_, cx| {
// May panic if entities are related
});
});
```
@@ -0,0 +1,537 @@
# Entity Best Practices
**Contents:** [Avoiding Common Pitfalls](#avoiding-common-pitfalls) · [Performance Optimization](#performance-optimization) · [Entity Lifecycle Management](#entity-lifecycle-management) · [Entity Observation Best Practices](#entity-observation-best-practices) · [Async Best Practices](#async-best-practices) · [Testing Best Practices](#testing-best-practices) · [Performance Checklist](#performance-checklist)
## Avoiding Common Pitfalls
### Avoid Re-entrant Entity Access (Same Entity)
**Problem:** GPUI locks an entity for the entire duration of its render or update pass. Any attempt to `read` or `update` that *same* entity while the lock is held panics:
```
cannot update … while it is already being updated
```
```rust
// ❌ Panic: entity_a updated from within entity_a's own update
entity_a.update(cx, |_, cx| {
entity_a.update(cx, |_, _| {}); // PANIC
});
// ✅ Fine: updating a *different* entity from within an update
entity_a.update(cx, |_, cx| {
entity_b.update(cx, |_, _| {}); // OK — different lock
});
```
**Note:** Updating two *different* entities in a nested fashion is safe. The restriction is strictly about re-entering the *same* entity's lock.
### `defer_in` Re-locks the Entity — Same Rules Apply
`cx.defer_in(window, callback)` schedules `callback` to run *on the entity the context refers to*. GPUI re-acquires that entity's lock to execute the deferred callback, so the re-entrancy rules apply equally inside it:
```rust
// ❌ Panic: defer_in runs with ListState locked; calling list.update re-enters
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
cx.defer_in(window, |list_state, window, cx| {
// list_state IS locked for this entire callback!
parent.update(cx, |this, cx| {
this.list.update(cx, |_, _| {}); // PANIC — re-enters ListState lock
});
});
}
```
**Fix:** Use the direct `&mut` reference the callback provides instead of going through the entity handle:
```rust
// ✅ Correct: direct mutable access — no lock needed
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
cx.defer_in(window, |list_state, window, cx| {
// Step 1: call hooks directly through list_state (no entity lock)
list_state.delegate_mut().on_will_change(&mut op, &snapshot);
// Step 2: update the parent entity — different lock, safe
let new_sel = parent.update(cx, |this, cx| {
this.state.apply_change(op);
cx.notify();
this.state.selection.clone() // return data needed for step 3
});
// Step 3: sync list state directly — no entity lock
if let Ok(sel) = new_sel {
list_state.delegate_mut().update_snapshot(sel.clone());
list_state.delegate_mut().on_confirm(&sel);
}
});
}
```
### Render Callbacks Must Not Access External Entities
`render_item` and any other rendering hook runs inside the entity's render pass. Calling `entity.read(cx)` or `entity.update(cx, …)` on an external entity (which may itself be in a render/update pass) panics with the same re-entrancy error.
**Fix:** Keep a plain `snapshot` field updated eagerly from outside render after every mutation:
```rust
// ❌ Panic — called during ListState render; external entity access re-enters
fn render_item(&mut self, ix: IndexPath, …) -> … {
let checked = parent.read(cx).selection.contains(&ix); // PANIC
}
// ✅ Read from snapshot field — no entity access at all
fn render_item(&mut self, ix: IndexPath, …) -> … {
let checked = self.selection_snapshot.iter().any(|(sel_ix, _)| sel_ix == &ix);
}
// After every mutation from outside render:
list.update(cx, |l, _| l.delegate_mut().update_snapshot(new_snapshot));
```
### Use Weak References in Closures
**Problem:** Strong references in closures can create retain cycles and memory leaks.
```rust
// ❌ Bad: Strong reference creates retain cycle
impl MyComponent {
fn setup_callback(&mut self, cx: &mut Context<Self>) {
let entity = cx.entity(); // Strong reference
some_callback(move || {
entity.update(cx, |state, cx| {
// This closure holds a strong reference
// If the closure itself is retained by the entity, memory leak!
cx.notify();
});
});
}
}
```
**Solution:** Use weak references in closures.
```rust
// ✅ Good: Weak reference prevents retain cycle
impl MyComponent {
fn setup_callback(&mut self, cx: &mut Context<Self>) {
let weak_entity = cx.entity().downgrade(); // Weak reference
some_callback(move || {
// Safe: weak reference doesn't prevent cleanup
let _ = weak_entity.update(cx, |state, cx| {
cx.notify();
});
});
}
}
```
### Use Inner Context in Closures
**Problem:** Using outer context causes multiple borrow errors.
```rust
// ❌ Bad: Using outer cx causes borrow issues
entity.update(cx, |state, inner_cx| {
cx.notify(); // Wrong! Using outer cx
cx.spawn(...); // Multiple borrow error
});
```
**Solution:** Always use the inner context provided to the closure.
```rust
// ✅ Good: Use inner cx
entity.update(cx, |state, inner_cx| {
inner_cx.notify(); // Correct
inner_cx.spawn(...); // Works fine
});
```
### Entity as Props - Use Weak References
**Problem:** Strong entity references in props can create ownership issues.
```rust
// ❌ Questionable: Strong reference in child
struct ChildComponent {
parent: Entity<ParentComponent>, // Strong reference
}
```
**Better:** Use weak references for parent relationships.
```rust
// ✅ Good: Weak reference prevents issues
struct ChildComponent {
parent: WeakEntity<ParentComponent>, // Weak reference
}
impl ChildComponent {
fn notify_parent(&mut self, cx: &mut Context<Self>) {
// Check if parent still exists
if let Ok(_) = self.parent.update(cx, |parent_state, cx| {
// Update parent
cx.notify();
}) {
// Parent successfully updated
}
}
}
```
## Performance Optimization
### Minimize cx.notify() Calls
Each `cx.notify()` triggers a re-render. Batch updates when possible.
```rust
// ❌ Bad: Multiple notifications
impl MyComponent {
fn update_multiple_fields(&mut self, cx: &mut Context<Self>) {
self.field1 = new_value1;
cx.notify(); // Unnecessary intermediate notification
self.field2 = new_value2;
cx.notify(); // Unnecessary intermediate notification
self.field3 = new_value3;
cx.notify();
}
}
```
```rust
// ✅ Good: Single notification after all updates
impl MyComponent {
fn update_multiple_fields(&mut self, cx: &mut Context<Self>) {
self.field1 = new_value1;
self.field2 = new_value2;
self.field3 = new_value3;
cx.notify(); // Single notification
}
}
```
### Conditional Updates
Only notify when state actually changes.
```rust
impl MyComponent {
fn set_value(&mut self, new_value: i32, cx: &mut Context<Self>) {
if self.value != new_value {
self.value = new_value;
cx.notify(); // Only notify if changed
}
}
}
```
### Use read_with for Complex Operations
Prefer `read_with` over separate `read` calls.
```rust
// ❌ Less efficient: Multiple borrows
let state_ref = entity.read(cx);
let value1 = state_ref.field1;
let value2 = state_ref.field2;
// state_ref borrowed for entire scope
// ✅ More efficient: Single borrow with closure
let (value1, value2) = entity.read_with(cx, |state, cx| {
(state.field1, state.field2)
});
```
### Avoid Excessive Entity Creation
Creating entities has overhead. Reuse when appropriate.
```rust
// ❌ Bad: Creating entity per item in render
impl Render for MyList {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div().children(
self.items.iter().map(|item| {
// Don't create entities in render!
let entity = cx.new(|_| item.clone());
ItemView { entity }
})
)
}
}
```
```rust
// ✅ Good: Create entities once, reuse
struct MyList {
item_entities: Vec<Entity<Item>>,
}
impl MyList {
fn add_item(&mut self, item: Item, cx: &mut Context<Self>) {
let entity = cx.new(|_| item);
self.item_entities.push(entity);
cx.notify();
}
}
```
## Entity Lifecycle Management
### Clean Up Weak References
Periodically clean up invalid weak references from collections.
```rust
struct Container {
weak_children: Vec<WeakEntity<Child>>,
}
impl Container {
fn cleanup_invalid_children(&mut self, cx: &mut Context<Self>) {
// Remove weak references that are no longer valid
let before_count = self.weak_children.len();
self.weak_children.retain(|weak| weak.upgrade().is_some());
let after_count = self.weak_children.len();
if before_count != after_count {
cx.notify(); // Notify if list changed
}
}
}
```
### Entity Cloning and Sharing
Understand that cloning `Entity<T>` increases reference count.
```rust
// Each clone increases the reference count
let entity1: Entity<MyState> = cx.new(|_| MyState::default());
let entity2 = entity1.clone(); // Reference count: 2
let entity3 = entity1.clone(); // Reference count: 3
// Entity is dropped only when all references are dropped
drop(entity1); // Reference count: 2
drop(entity2); // Reference count: 1
drop(entity3); // Reference count: 0, entity is deallocated
```
### Proper Resource Cleanup
Implement cleanup in `Drop` or explicit cleanup methods.
```rust
struct ManagedResource {
handle: Option<FileHandle>,
}
impl ManagedResource {
fn close(&mut self, cx: &mut Context<Self>) {
if let Some(handle) = self.handle.take() {
// Explicit cleanup
handle.close();
cx.notify();
}
}
}
impl Drop for ManagedResource {
fn drop(&mut self) {
// Automatic cleanup when entity is dropped
if let Some(handle) = self.handle.take() {
handle.close();
}
}
}
```
## Entity Observation Best Practices
### Detach Subscriptions Appropriately
Call `.detach()` on subscriptions you want to keep alive.
```rust
impl MyComponent {
fn new(other_entity: Entity<OtherComponent>, cx: &mut App) -> Entity<Self> {
cx.new(|cx| {
// Observer will live as long as both entities exist
cx.observe(&other_entity, |this, observed, cx| {
// Handle changes
cx.notify();
}).detach(); // Important: detach to make permanent
Self { /* fields */ }
})
}
}
```
### Avoid Observation Cycles
Don't create mutual observation between entities.
```rust
// ❌ Bad: Mutual observation can cause infinite loops
entity1.update(cx, |_, cx| {
cx.observe(&entity2, |_, _, cx| {
cx.notify(); // May trigger entity2's observer
}).detach();
});
entity2.update(cx, |_, cx| {
cx.observe(&entity1, |_, _, cx| {
cx.notify(); // May trigger entity1's observer → infinite loop
}).detach();
});
```
## Async Best Practices
### Always Use Weak References in Async Tasks
```rust
// ✅ Good: Weak reference in spawned task
impl MyComponent {
fn fetch_data(&mut self, cx: &mut Context<Self>) {
let weak_entity = cx.entity().downgrade();
cx.spawn(async move |cx| {
let data = fetch_from_api().await;
// Entity may have been dropped during fetch
let _ = weak_entity.update(cx, |state, cx| {
state.data = Some(data);
cx.notify();
});
}).detach();
}
}
```
### Handle Async Errors Gracefully
```rust
impl MyComponent {
fn fetch_data(&mut self, cx: &mut Context<Self>) {
let weak_entity = cx.entity().downgrade();
cx.spawn(async move |cx| {
match fetch_from_api().await {
Ok(data) => {
let _ = weak_entity.update(cx, |state, cx| {
state.data = Some(data);
state.error = None;
cx.notify();
});
}
Err(e) => {
let _ = weak_entity.update(cx, |state, cx| {
state.error = Some(e.to_string());
cx.notify();
});
}
}
}).detach();
}
}
```
### Cancellation Patterns
Implement cancellation for long-running tasks.
```rust
struct DataFetcher {
current_task: Option<Task<()>>,
data: Option<String>,
}
impl DataFetcher {
fn fetch_data(&mut self, url: String, cx: &mut Context<Self>) {
// Cancel previous task
self.current_task = None; // Dropping task cancels it
let weak_entity = cx.entity().downgrade();
let task = cx.spawn(async move |cx| {
let data = fetch_from_url(&url).await?;
let _ = weak_entity.update(cx, |state, cx| {
state.data = Some(data);
cx.notify();
});
Ok::<(), anyhow::Error>(())
});
self.current_task = Some(task);
}
}
```
## Testing Best Practices
### Use TestAppContext for Entity Tests
```rust
#[cfg(test)]
mod tests {
use super::*;
use gpui_kit::TestAppContext;
#[gpui_kit::test]
fn test_entity_update(cx: &mut TestAppContext) {
let entity = cx.new(|_| MyState { count: 0 });
entity.update(cx, |state, cx| {
state.count += 1;
assert_eq!(state.count, 1);
});
let count = entity.read(cx).count;
assert_eq!(count, 1);
}
}
```
### Test Entity Observation
```rust
#[gpui_kit::test]
fn test_entity_observation(cx: &mut TestAppContext) {
let observed = cx.new(|_| MyState { value: 0 });
let observer = cx.new(|cx| Observer::new(observed.clone(), cx));
// Update observed entity
observed.update(cx, |state, cx| {
state.value = 42;
cx.notify();
});
// Verify observer was notified
observer.read(cx).assert_observed();
}
```
## Performance Checklist
Before shipping entity-based code, verify:
- [ ] No strong references in closures/callbacks (use `WeakEntity`)
- [ ] No nested entity updates (use sequential updates)
- [ ] Using inner `cx` in update closures
- [ ] Batching updates before calling `cx.notify()`
- [ ] Cleaning up invalid weak references periodically
- [ ] Using `read_with` for complex read operations
- [ ] Properly detaching subscriptions and observers
- [ ] Using weak references in async tasks
- [ ] No observation cycles between entities
- [ ] Proper error handling in async operations
- [ ] Resource cleanup in `Drop` or explicit methods
- [ ] Tests cover entity lifecycle and interactions
@@ -0,0 +1,579 @@
# Entity Patterns
**Contents:** [Application Scenarios](#application-scenarios) · [Common Patterns](#common-patterns) · [Pattern Selection Guide](#pattern-selection-guide)
## Application Scenarios
### Model-View Separation
Separate business logic (model) from UI (view) using entities.
```rust
struct CounterModel {
count: usize,
listeners: Vec<Box<dyn Fn(usize)>>,
}
struct CounterView {
model: Entity<CounterModel>,
}
impl CounterModel {
fn increment(&mut self, cx: &mut Context<Self>) {
self.count += 1;
// Notify listeners
for listener in &self.listeners {
listener(self.count);
}
cx.notify();
}
fn decrement(&mut self, cx: &mut Context<Self>) {
if self.count > 0 {
self.count -= 1;
cx.notify();
}
}
}
impl CounterView {
fn new(cx: &mut App) -> Entity<Self> {
let model = cx.new(|_cx| CounterModel {
count: 0,
listeners: Vec::new(),
});
cx.new(|cx| Self { model })
}
fn increment_count(&mut self, cx: &mut Context<Self>) {
self.model.update(cx, |model, cx| {
model.increment(cx);
});
}
}
impl Render for CounterView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let count = self.model.read(cx).count;
div()
.child(format!("Count: {}", count))
.child(
Button::new("increment")
.label("Increment")
.on_click(cx.listener(|this, _, cx| {
this.increment_count(cx);
}))
)
}
}
```
### Component State Management
Managing complex component state with entities.
```rust
struct TodoList {
todos: Vec<Todo>,
filter: TodoFilter,
next_id: usize,
}
struct Todo {
id: usize,
text: String,
completed: bool,
}
enum TodoFilter {
All,
Active,
Completed,
}
impl TodoList {
fn new() -> Self {
Self {
todos: Vec::new(),
filter: TodoFilter::All,
next_id: 0,
}
}
fn add_todo(&mut self, text: String, cx: &mut Context<Self>) {
self.todos.push(Todo {
id: self.next_id,
text,
completed: false,
});
self.next_id += 1;
cx.notify();
}
fn toggle_todo(&mut self, id: usize, cx: &mut Context<Self>) {
if let Some(todo) = self.todos.iter_mut().find(|t| t.id == id) {
todo.completed = !todo.completed;
cx.notify();
}
}
fn remove_todo(&mut self, id: usize, cx: &mut Context<Self>) {
self.todos.retain(|t| t.id != id);
cx.notify();
}
fn set_filter(&mut self, filter: TodoFilter, cx: &mut Context<Self>) {
self.filter = filter;
cx.notify();
}
fn visible_todos(&self) -> impl Iterator<Item = &Todo> {
self.todos.iter().filter(move |todo| match self.filter {
TodoFilter::All => true,
TodoFilter::Active => !todo.completed,
TodoFilter::Completed => todo.completed,
})
}
}
```
### Cross-Entity Communication
Coordinating state between parent and child entities.
```rust
struct ParentComponent {
child_entities: Vec<Entity<ChildComponent>>,
global_message: String,
}
struct ChildComponent {
id: usize,
message: String,
parent: WeakEntity<ParentComponent>,
}
impl ParentComponent {
fn new(cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self {
child_entities: Vec::new(),
global_message: String::new(),
})
}
fn add_child(&mut self, cx: &mut Context<Self>) {
let parent_weak = cx.entity().downgrade();
let child_id = self.child_entities.len();
let child = cx.new(|cx| ChildComponent {
id: child_id,
message: String::new(),
parent: parent_weak,
});
self.child_entities.push(child);
cx.notify();
}
fn broadcast_message(&mut self, message: String, cx: &mut Context<Self>) {
self.global_message = message.clone();
// Update all children
for child in &self.child_entities {
child.update(cx, |child_state, cx| {
child_state.message = message.clone();
cx.notify();
});
}
cx.notify();
}
}
impl ChildComponent {
fn notify_parent(&mut self, message: String, cx: &mut Context<Self>) {
if let Ok(_) = self.parent.update(cx, |parent_state, cx| {
parent_state.global_message = format!("Child {}: {}", self.id, message);
cx.notify();
}) {
// Parent successfully notified
}
}
}
```
### Async Operations with Entities
Managing async state updates.
```rust
struct DataLoader {
loading: bool,
data: Option<String>,
error: Option<String>,
}
impl DataLoader {
fn new() -> Self {
Self {
loading: false,
data: None,
error: None,
}
}
fn load_data(&mut self, cx: &mut Context<Self>) {
// Set loading state
self.loading = true;
self.error = None;
cx.notify();
// Get weak reference for async task
let entity = cx.entity().downgrade();
cx.spawn(async move |cx| {
// Simulate async operation
tokio::time::sleep(Duration::from_secs(2)).await;
let result = fetch_data().await;
// Update entity with result
let _ = entity.update(cx, |state, cx| {
state.loading = false;
match result {
Ok(data) => state.data = Some(data),
Err(e) => state.error = Some(e.to_string()),
}
cx.notify();
});
}).detach();
}
}
async fn fetch_data() -> Result<String, anyhow::Error> {
// Actual fetch implementation
Ok("Fetched data".to_string())
}
```
### Background Task Coordination
Using background tasks with entity updates.
```rust
struct ImageProcessor {
images: Vec<ProcessedImage>,
processing: bool,
}
struct ProcessedImage {
path: PathBuf,
thumbnail: Option<Vec<u8>>,
}
impl ImageProcessor {
fn process_images(&mut self, paths: Vec<PathBuf>, cx: &mut Context<Self>) {
self.processing = true;
cx.notify();
let entity = cx.entity().downgrade();
cx.background_spawn({
let paths = paths.clone();
async move {
let mut processed = Vec::new();
for path in paths {
// Process image on background thread
let thumbnail = generate_thumbnail(&path).await;
processed.push((path, thumbnail));
}
// Send results back to foreground
processed
}
})
.then(cx.spawn(move |processed, cx| {
// Update entity on foreground thread
let _ = entity.update(cx, |state, cx| {
for (path, thumbnail) in processed {
state.images.push(ProcessedImage {
path,
thumbnail: Some(thumbnail),
});
}
state.processing = false;
cx.notify();
});
}))
.detach();
}
}
```
## Common Patterns
### 1. Stateful Components
Use entities for components that maintain internal state.
```rust
struct StatefulComponent {
value: i32,
history: Vec<i32>,
}
impl StatefulComponent {
fn update_value(&mut self, new_value: i32, cx: &mut Context<Self>) {
self.history.push(self.value);
self.value = new_value;
cx.notify();
}
fn undo(&mut self, cx: &mut Context<Self>) {
if let Some(prev_value) = self.history.pop() {
self.value = prev_value;
cx.notify();
}
}
}
```
### 2. Shared State
Share state between multiple components using entities.
```rust
struct SharedState {
theme: Theme,
user: Option<User>,
}
struct ComponentA {
shared: Entity<SharedState>,
}
struct ComponentB {
shared: Entity<SharedState>,
}
// Both components can read/update the same shared state
impl ComponentA {
fn update_theme(&mut self, theme: Theme, cx: &mut Context<Self>) {
self.shared.update(cx, |state, cx| {
state.theme = theme;
cx.notify();
});
}
}
```
### 3. Event Coordination
Use entities to coordinate events between components.
```rust
struct EventCoordinator {
listeners: Vec<WeakEntity<dyn EventListener>>,
}
trait EventListener {
fn on_event(&mut self, event: &AppEvent, cx: &mut App);
}
impl EventCoordinator {
fn emit_event(&mut self, event: AppEvent, cx: &mut Context<Self>) {
// Notify all listeners
self.listeners.retain(|weak_listener| {
weak_listener.update(cx, |listener, cx| {
listener.on_event(&event, cx);
}).is_ok()
});
cx.notify();
}
}
```
### 4. Async State Management
Manage state that changes based on async operations.
```rust
struct AsyncState<T> {
state: AsyncValue<T>,
}
enum AsyncValue<T> {
Loading,
Loaded(T),
Error(String),
}
impl<T> AsyncState<T> {
fn is_loading(&self) -> bool {
matches!(self.state, AsyncValue::Loading)
}
fn value(&self) -> Option<&T> {
match &self.state {
AsyncValue::Loaded(v) => Some(v),
_ => None,
}
}
}
```
### 5. Parent-Child Relationships
Manage hierarchical relationships with weak references.
```rust
struct Parent {
children: Vec<Entity<Child>>,
}
struct Child {
parent: WeakEntity<Parent>,
data: String,
}
impl Child {
fn notify_parent_of_change(&mut self, cx: &mut Context<Self>) {
if let Ok(_) = self.parent.update(cx, |parent, cx| {
// Parent can react to child change
cx.notify();
}) {
// Successfully notified
}
}
}
```
### 6. Observer Pattern
React to entity state changes using observers.
```rust
struct Observable {
value: i32,
}
struct Observer {
observed: Entity<Observable>,
}
impl Observer {
fn new(observed: Entity<Observable>, cx: &mut App) -> Entity<Self> {
cx.new(|cx| {
// Observe the entity
cx.observe(&observed, |this, observed_entity, cx| {
// React to changes
let value = observed_entity.read(cx).value;
println!("Value changed to: {}", value);
}).detach();
Self { observed }
})
}
}
```
### 7. Event Subscription
Handle events emitted by other entities.
```rust
#[derive(Clone)]
enum DataEvent {
Updated,
Deleted,
}
struct DataSource {
data: Vec<String>,
}
impl DataSource {
fn update_data(&mut self, cx: &mut Context<Self>) {
// Update data
cx.emit(DataEvent::Updated);
cx.notify();
}
}
struct DataConsumer {
source: Entity<DataSource>,
}
impl DataConsumer {
fn new(source: Entity<DataSource>, cx: &mut App) -> Entity<Self> {
cx.new(|cx| {
// Subscribe to events
cx.subscribe(&source, |this, source, event: &DataEvent, cx| {
match event {
DataEvent::Updated => {
// Handle update
cx.notify();
}
DataEvent::Deleted => {
// Handle deletion
}
}
}).detach();
Self { source }
})
}
}
```
### 8. Resource Management
Manage external resources with proper cleanup.
```rust
struct FileHandle {
path: PathBuf,
file: Option<File>,
}
impl FileHandle {
fn open(&mut self, cx: &mut Context<Self>) -> Result<()> {
self.file = Some(File::open(&self.path)?);
cx.notify();
Ok(())
}
fn close(&mut self, cx: &mut Context<Self>) {
self.file = None;
cx.notify();
}
}
impl Drop for FileHandle {
fn drop(&mut self) {
// Cleanup when entity is dropped
if let Some(file) = self.file.take() {
drop(file);
}
}
}
```
## Pattern Selection Guide
| Need | Pattern | Complexity |
|------|---------|------------|
| Component with internal state | Stateful Components | Low |
| State shared by multiple components | Shared State | Low |
| Coordinate events between components | Event Coordination | Medium |
| Handle async data fetching | Async State Management | Medium |
| Parent-child component hierarchy | Parent-Child Relationships | Medium |
| React to state changes | Observer Pattern | Medium |
| Handle custom events | Event Subscription | Medium-High |
| Manage external resources | Resource Management | High |
Choose the simplest pattern that meets your requirements. Combine patterns as needed for complex scenarios.
@@ -0,0 +1,218 @@
# Entity State Management
**Contents:** [Overview](#overview) · [Quick Start](#quick-start) · [Core Principles](#core-principles) · [Common Use Cases](#common-use-cases) · [Extended References](#reference-documentation)
## Overview
An `Entity<T>` is a handle to state of type `T`, providing safe access and updates.
**Key Methods:**
- `entity.read(cx)` → `&T` - Read-only access
- `entity.read_with(cx, |state, cx| ...)` → `R` - Read with closure
- `entity.update(cx, |state, cx| ...)` → `R` - Mutable update
- `entity.downgrade()` → `WeakEntity<T>` - Create weak reference
- `entity.entity_id()` → `EntityId` - Unique identifier
**Entity Types:**
- **`Entity<T>`**: Strong reference (increases ref count)
- **`WeakEntity<T>`**: Weak reference (doesn't prevent cleanup, returns `Result`)
## Quick Start
### Creating and Using Entities
```rust
// Create entity
let counter = cx.new(|cx| Counter { count: 0 });
// Read state
let count = counter.read(cx).count;
// Update state
counter.update(cx, |state, cx| {
state.count += 1;
cx.notify(); // Trigger re-render
});
// Weak reference (for closures/callbacks)
let weak = counter.downgrade();
let _ = weak.update(cx, |state, cx| {
state.count += 1;
cx.notify();
});
```
### In Components
```rust
struct MyComponent {
shared_state: Entity<SharedData>,
}
impl MyComponent {
fn new(cx: &mut App) -> Entity<Self> {
let shared = cx.new(|_| SharedData::default());
cx.new(|cx| Self {
shared_state: shared,
})
}
fn update_shared(&mut self, cx: &mut Context<Self>) {
self.shared_state.update(cx, |state, cx| {
state.value = 42;
cx.notify();
});
}
}
```
### Async Operations
When calling `cx.spawn` from `Context<Self>`, the closure receives `(WeakEntity<Self>, &mut AsyncApp)`:
```rust
impl MyComponent {
fn fetch_data(&mut self, cx: &mut Context<Self>) {
cx.spawn(async move |this, cx: &mut AsyncApp| {
let data = fetch_from_api().await;
// Update entity safely via the weak reference
let _ = this.update(cx, |state, cx| {
state.data = Some(data);
cx.notify();
});
}).detach();
}
}
```
## Core Principles
### Always Use Weak References in Closures
```rust
// ✅ Good: Weak reference prevents retain cycles
let weak = cx.entity().downgrade();
callback(move || {
let _ = weak.update(cx, |state, cx| cx.notify());
});
// ❌ Bad: Strong reference may cause memory leak
let strong = cx.entity();
callback(move || {
strong.update(cx, |state, cx| cx.notify());
});
```
### Use Inner Context
```rust
// ✅ Good: Use inner cx from closure
entity.update(cx, |state, inner_cx| {
inner_cx.notify(); // Correct
});
// ❌ Bad: Use outer cx (multiple borrow error)
entity.update(cx, |state, inner_cx| {
cx.notify(); // Wrong!
});
```
### Avoid Nested Entity Updates
Nested `entity.update(cx, …)` calls are dangerous. The default posture is: **do not nest them**. The sub-cases below clarify when a panic is guaranteed vs. merely possible.
**Same entity → always panics.**
GPUI locks an entity for the entire duration of its update or render pass. Re-entering that same lock panics immediately:
```
cannot update … while it is already being updated
```
```rust
// ❌ Panic: updating entity_a from inside entity_a's own update
entity_a.update(cx, |state, cx| {
entity_a.update(cx, |_, _| {}); // PANIC — same lock
});
```
**Different entity → generally safe, but indirect cycles still panic.**
Each entity has its own lock, so updating `entity_b` from within `entity_a`'s update normally succeeds. However, if `entity_b`'s callback reaches back into `entity_a` — directly or through a chain — GPUI will attempt to re-acquire `entity_a`'s lock and panic.
```rust
// ✅ Usually fine: different entities, no cycle
entity_a.update(cx, |_, cx| {
entity_b.update(cx, |_, _| {}); // OK — different lock
});
// ❌ Panic: indirect cycle back to entity_a
entity_a.update(cx, |_, cx| {
entity_b.update(cx, |_, cx| {
entity_a.update(cx, |_, _| {}); // PANIC — entity_a is still locked
});
});
```
When in doubt, flatten the call sequence rather than nesting: finish the outer update, then update the second entity from outside.
**`defer_in` does not bypass the lock.** `cx.defer_in(window, callback)` schedules `callback` to run on the current entity — meaning GPUI re-acquires the entity's lock to execute it. The re-entrancy rules apply equally inside the deferred callback:
```rust
// ❌ Panic: defer_in re-locks entity_a; calling entity_a.update inside re-enters
impl SomeDelegate for MyAdapter {
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
cx.defer_in(window, |list_state, window, cx| {
// list_state is locked for this callback!
parent.update(cx, |this, cx| {
this.list.update(cx, |_, _| {}); // PANIC — list is already locked above
});
});
}
}
// ✅ Fix: use the direct &mut reference the callback provides
impl SomeDelegate for MyAdapter {
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
cx.defer_in(window, |list_state, window, cx| {
// Access list data directly — no entity lock needed
list_state.delegate_mut().some_hook();
// Update the *parent* entity — different lock, safe
parent.update(cx, |this, cx| { /* … */ });
// Sync list state directly after parent update
list_state.delegate_mut().update_snapshot(new_val);
});
}
}
```
**Snapshot pattern for render callbacks.** `render_item` (and any other rendering hook) runs inside the entity's render pass. It must never call `entity.read(cx)` or `entity.update(cx, …)` on any external entity. Instead, keep a plain `snapshot` field updated eagerly from *outside* render after every mutation:
```rust
// ❌ Panic in render_item — ListState is already locked
fn render_item(&mut self, ix: IndexPath, window: &mut Window, cx: &mut Context<ListState<Self>>) -> … {
let checked = parent_entity.read(cx).selection.contains(&ix); // PANIC
}
// ✅ Read from a plain snapshot field — no entity access
fn render_item(&mut self, ix: IndexPath, window: &mut Window, cx: &mut Context<ListState<Self>>) -> … {
let checked = self.selection_snapshot.iter().any(|(sel_ix, _)| sel_ix == &ix);
}
```
## Common Use Cases
1. **Component State**: Internal state that needs reactivity
2. **Shared State**: State shared between multiple components
3. **Parent-Child**: Coordinating between related components (use weak refs)
4. **Async State**: Managing state that changes from async operations
5. **Observations**: Reacting to changes in other entities
## Reference Documentation
- **API**: See [entity-api.md](entity-api.md) — entity types, methods, lifecycle, error handling
- **Patterns**: See [entity-patterns.md](entity-patterns.md) — model-view, cross-entity communication, observer
- **Best Practices**: See [entity-best-practices.md](entity-best-practices.md) — pitfalls, memory, performance, async
- **Advanced**: See [entity-advanced.md](entity-advanced.md) — collections, registry, debounce, state machines
@@ -0,0 +1,238 @@
# Events & Subscriptions
**Contents:** [Overview](#overview) · [Quick Start](#quick-start) · [Common Patterns](#common-patterns) · [subscribe_in](#subscribe_in--subscription-with-window-access) · [observe_window_activation](#observe_window_activation) · [observe_global](#observe_global) · [Subscription Lifetime](#subscription-lifetime) · [Best Practices](#best-practices)
## Overview
GPUI provides event system for component coordination:
**Event Mechanisms:**
- **Custom Events**: Define and emit type-safe events
- **Observations**: React to entity state changes
- **Subscriptions**: Listen to events from other entities
- **Global Events**: App-wide event handling
## Quick Start
### Define and Emit Events
```rust
#[derive(Clone)]
enum MyEvent {
DataUpdated(String),
ActionTriggered,
}
impl MyComponent {
fn update_data(&mut self, data: String, cx: &mut Context<Self>) {
self.data = data.clone();
// Emit event
cx.emit(MyEvent::DataUpdated(data));
cx.notify();
}
}
```
### Subscribe to Events
```rust
impl Listener {
fn new(source: Entity<MyComponent>, cx: &mut App) -> Entity<Self> {
cx.new(|cx| {
// Subscribe to events
cx.subscribe(&source, |this, emitter, event: &MyEvent, cx| {
match event {
MyEvent::DataUpdated(data) => {
this.handle_update(data.clone(), cx);
}
MyEvent::ActionTriggered => {
this.handle_action(cx);
}
}
}).detach();
Self { source }
})
}
}
```
### Observe Entity Changes
```rust
impl Observer {
fn new(target: Entity<Target>, cx: &mut App) -> Entity<Self> {
cx.new(|cx| {
// Observe entity for any changes
cx.observe(&target, |this, observed, cx| {
// Called when observed.update() calls cx.notify()
println!("Target changed");
cx.notify();
}).detach();
Self { target }
})
}
}
```
## Common Patterns
### 1. Parent-Child Communication
```rust
// Parent emits events
impl Parent {
fn notify_children(&mut self, cx: &mut Context<Self>) {
cx.emit(ParentEvent::Updated);
cx.notify();
}
}
// Children subscribe
impl Child {
fn new(parent: Entity<Parent>, cx: &mut App) -> Entity<Self> {
cx.new(|cx| {
cx.subscribe(&parent, |this, parent, event, cx| {
this.handle_parent_event(event, cx);
}).detach();
Self { parent }
})
}
}
```
### 2. Global Event Broadcasting
```rust
struct EventBus {
listeners: Vec<WeakEntity<dyn Listener>>,
}
impl EventBus {
fn broadcast(&mut self, event: GlobalEvent, cx: &mut Context<Self>) {
self.listeners.retain(|weak| {
weak.update(cx, |listener, cx| {
listener.on_event(&event, cx);
}).is_ok()
});
}
}
```
### 3. Observer Pattern
```rust
cx.observe(&entity, |this, observed, cx| {
// React to any state change
let state = observed.read(cx);
this.sync_with_state(state, cx);
}).detach();
```
## subscribe_in — Subscription with Window Access
Use when the subscription callback needs `&mut Window`:
```rust
// Store subscriptions to keep them alive
struct MyComponent {
_subscriptions: Vec<Subscription>,
}
impl MyComponent {
fn new(input: &Entity<InputState>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let _subscriptions = vec![
cx.subscribe_in(input, window, |this, state, event, window, cx| {
match event {
InputEvent::PressEnter { .. } => this.on_submit(window, cx),
InputEvent::Change => {
let val = state.read(cx).value();
this.on_change(val, cx);
}
_ => {}
}
}),
];
Self { _subscriptions }
}
}
```
`subscribe` vs `subscribe_in`:
- `cx.subscribe(&entity, |this, source, event, cx|)` — no window
- `cx.subscribe_in(&entity, window, |this, source, event, window, cx|)` — window access
## observe_window_activation
```rust
let _sub = cx.observe_window_activation(window, |this, window, cx| {
if window.is_window_active() {
this.start_polling(cx);
} else {
this.stop_polling(cx);
}
});
```
## observe_global
```rust
cx.observe_global::<Theme>(|cx| {
cx.notify(); // Re-render when theme changes
});
```
## Subscription Lifetime
Subscriptions are cancelled when dropped. Two ways to keep alive:
```rust
// 1. .detach() — lives until entity is dropped
cx.subscribe(&entity, |this, _, event, cx| {
// ...
}).detach();
// 2. Store in struct — cancelled when struct drops
struct MyView {
_subscriptions: Vec<Subscription>,
}
// _subscriptions.push(cx.subscribe(...));
```
Use `.detach()` for permanent subscriptions; store in struct for subscriptions that should stop when the component unmounts.
## Best Practices
### ✅ Detach Subscriptions
```rust
// ✅ Detach to keep alive
cx.subscribe(&entity, |this, source, event, cx| {
// Handle event
}).detach();
```
### ✅ Clean Event Types
```rust
#[derive(Clone)]
enum AppEvent {
DataChanged { id: usize, value: String },
ActionPerformed(ActionType),
Error(String),
}
```
### ❌ Avoid Event Loops
```rust
// ❌ Don't create mutual subscriptions
entity1.subscribe(entity2) → emits event
entity2.subscribe(entity1) → emits event → infinite loop!
```
## Reference Documentation
@@ -0,0 +1,225 @@
# Focus & Keyboard Navigation
**Contents:** [Overview](#overview) · [Quick Start](#quick-start) · [Focus Events](#focus-events) · [Keyboard Navigation](#keyboard-navigation) · [Common Patterns](#common-patterns) · [Best Practices](#best-practices)
## Overview
GPUI's focus system enables keyboard navigation and focus management.
**Key Concepts:**
- **FocusHandle**: Reference to focusable element
- **Focus tracking**: Current focused element
- **Keyboard navigation**: Tab/Shift-Tab between elements
- **Focus events**: on_focus, on_blur
## Quick Start
### Creating Focus Handles
```rust
struct FocusableComponent {
focus_handle: FocusHandle,
}
impl FocusableComponent {
fn new(cx: &mut Context<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
}
}
}
```
### Making Elements Focusable
```rust
impl Render for FocusableComponent {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::on_enter))
.child("Focusable content")
}
fn on_enter(&mut self, _: &Enter, cx: &mut Context<Self>) {
// Handle Enter key when focused
cx.notify();
}
}
```
### Focus Management
```rust
impl MyComponent {
fn focus(&mut self, cx: &mut Context<Self>) {
self.focus_handle.focus(cx);
}
fn is_focused(&self, cx: &App) -> bool {
self.focus_handle.is_focused(cx)
}
fn blur(&mut self, cx: &mut Context<Self>) {
cx.blur();
}
}
```
## Focus Events
### Handling Focus Changes
```rust
impl Render for MyInput {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_focused = self.focus_handle.is_focused(cx);
div()
.track_focus(&self.focus_handle)
.on_focus(cx.listener(|this, _event, cx| {
this.on_focus(cx);
}))
.on_blur(cx.listener(|this, _event, cx| {
this.on_blur(cx);
}))
.when(is_focused, |el| {
el.bg(cx.theme().focused_background)
})
.child(self.render_content())
}
}
impl MyInput {
fn on_focus(&mut self, cx: &mut Context<Self>) {
// Handle focus gained
cx.notify();
}
fn on_blur(&mut self, cx: &mut Context<Self>) {
// Handle focus lost
cx.notify();
}
}
```
## Keyboard Navigation
### Tab Order
Elements with `track_focus()` automatically participate in Tab navigation.
```rust
div()
.child(
input1.track_focus(&focus1) // Tab order: 1
)
.child(
input2.track_focus(&focus2) // Tab order: 2
)
.child(
input3.track_focus(&focus3) // Tab order: 3
)
```
### Focus Within Containers
```rust
impl Container {
fn focus_first(&mut self, cx: &mut Context<Self>) {
if let Some(first) = self.children.first() {
first.update(cx, |child, cx| {
child.focus_handle.focus(cx);
});
}
}
fn focus_next(&mut self, cx: &mut Context<Self>) {
// Custom focus navigation logic
}
}
```
## Common Patterns
### 1. Auto-focus on Mount
```rust
impl MyDialog {
fn new(cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
// Focus when created
focus_handle.focus(cx);
Self { focus_handle }
}
}
```
### 2. Focus Trap (Modal)
```rust
impl Modal {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.on_key_down(cx.listener(|this, event: &KeyDownEvent, cx| {
if event.key == Key::Tab {
// Keep focus within modal
this.focus_next_in_modal(cx);
cx.stop_propagation();
}
}))
.child(self.render_content())
}
}
```
### 3. Conditional Focus
```rust
impl Searchable {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.when(self.search_active, |el| {
el.on_mount(cx.listener(|this, _, cx| {
this.focus_handle.focus(cx);
}))
})
.child(self.search_input())
}
}
```
## Best Practices
### ✅ Track Focus on Interactive Elements
```rust
// ✅ Good: Track focus for keyboard interaction
input()
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::on_enter))
```
### ✅ Provide Visual Focus Indicators
```rust
let is_focused = self.focus_handle.is_focused(cx);
div()
.when(is_focused, |el| {
el.border_color(cx.theme().focused_border)
})
```
### ❌ Don't: Forget to Track Focus
```rust
// ❌ Bad: No track_focus, keyboard navigation won't work
div()
.on_action(cx.listener(Self::on_enter))
```
@@ -0,0 +1,197 @@
# Global State
**Contents:** [Overview](#overview) · [Quick Start](#quick-start) · [Common Use Cases](#common-use-cases) · [Best Practices](#best-practices) · [When to Use](#when-to-use)
## Overview
Global state in GPUI provides app-wide shared data accessible from any context.
**Key Trait**: `Global` - Implement on types to make them globally accessible
## Quick Start
### Define Global State
```rust
use gpui_kit::Global;
#[derive(Clone)]
struct AppSettings {
theme: Theme,
language: String,
}
impl Global for AppSettings {}
```
### Set and Access Globals
```rust
fn main() {
let app = Application::new();
app.run(|cx: &mut App| {
// Set global
cx.set_global(AppSettings {
theme: Theme::Dark,
language: "en".to_string(),
});
// Access global (read-only)
let settings = cx.global::<AppSettings>();
println!("Theme: {:?}", settings.theme);
});
}
```
### Update Globals
```rust
impl MyComponent {
fn change_theme(&mut self, new_theme: Theme, cx: &mut Context<Self>) {
cx.update_global::<AppSettings, _>(|settings, cx| {
settings.theme = new_theme;
// Global updates don't trigger automatic notifications
// Manually notify components that care
});
cx.notify(); // Re-render this component
}
}
```
## Common Use Cases
### 1. App Configuration
```rust
#[derive(Clone)]
struct AppConfig {
api_endpoint: String,
max_retries: u32,
timeout: Duration,
}
impl Global for AppConfig {}
// Set once at startup
cx.set_global(AppConfig {
api_endpoint: "https://api.example.com".to_string(),
max_retries: 3,
timeout: Duration::from_secs(30),
});
// Access anywhere
let config = cx.global::<AppConfig>();
```
### 2. Feature Flags
```rust
#[derive(Clone)]
struct FeatureFlags {
enable_beta_features: bool,
enable_analytics: bool,
}
impl Global for FeatureFlags {}
impl MyComponent {
fn render_beta_feature(&self, cx: &App) -> Option<impl IntoElement> {
let flags = cx.global::<FeatureFlags>();
if flags.enable_beta_features {
Some(div().child("Beta feature"))
} else {
None
}
}
}
```
### 3. Shared Services
```rust
#[derive(Clone)]
struct ServiceRegistry {
http_client: Arc<HttpClient>,
logger: Arc<Logger>,
}
impl Global for ServiceRegistry {}
impl MyComponent {
fn fetch_data(&mut self, cx: &mut Context<Self>) {
let registry = cx.global::<ServiceRegistry>();
let client = registry.http_client.clone();
cx.spawn(async move |cx| {
let data = client.get("api/data").await?;
// Process data...
Ok::<_, anyhow::Error>(())
}).detach();
}
}
```
## Best Practices
### ✅ Use Arc for Shared Resources
```rust
#[derive(Clone)]
struct GlobalState {
database: Arc<Database>, // Cheap to clone
cache: Arc<RwLock<Cache>>,
}
impl Global for GlobalState {}
```
### ✅ Immutable by Default
Globals are read-only by default. Use interior mutability when needed:
```rust
#[derive(Clone)]
struct Counter {
count: Arc<AtomicUsize>,
}
impl Global for Counter {}
impl Counter {
fn increment(&self) {
self.count.fetch_add(1, Ordering::SeqCst);
}
fn get(&self) -> usize {
self.count.load(Ordering::SeqCst)
}
}
```
### ❌ Don't: Overuse Globals
```rust
// ❌ Bad: Too many globals
cx.set_global(UserState { ... });
cx.set_global(CartState { ... });
cx.set_global(CheckoutState { ... });
// ✅ Good: Use entities for component state
let user_entity = cx.new(|_| UserState { ... });
```
## When to Use
**Use Globals for:**
- App-wide configuration
- Feature flags
- Shared services (HTTP client, logger)
- Read-only reference data
**Use Entities for:**
- Component-specific state
- State that changes frequently
- State that needs notifications
@@ -0,0 +1,280 @@
# Layout & Styling
**Contents:** [Overview](#overview) · [Quick Start](#quick-start) · [Common Patterns](#common-patterns) · [Styling Methods](#styling-methods) · [h_flex / v_flex](#h_flex--v_flex-helpers) · [Tailwind Shorthands](#tailwind-style-shorthand) · [Overflow & Scroll](#overflow-and-scroll) · [Absolute Positioning](#absolute-positioning) · [Stacking Order](#stacking-order) · [Theme Integration](#theme-integration) · [Conditional Styling](#conditional-styling) · [Text Styling](#text-styling)
## Overview
GPUI provides CSS-like styling with Rust type safety.
**Key Concepts:**
- Flexbox layout system
- Styled trait for chaining styles
- Size units: `px()`, `rems()`, `relative()`
- Colors, borders, shadows
## Quick Start
### Basic Styling
```rust
use gpui_kit::*;
div()
.w(px(200.))
.h(px(100.))
.bg(rgb(0x2196F3))
.text_color(rgb(0xFFFFFF))
.rounded(px(8.))
.p(px(16.))
.child("Styled content")
```
### Flexbox Layout
```rust
div()
.flex()
.flex_row() // or flex_col() for column
.gap(px(8.))
.items_center()
.justify_between()
.children([
div().child("Item 1"),
div().child("Item 2"),
div().child("Item 3"),
])
```
### Size Units
```rust
div()
.w(px(200.)) // Pixels
.h(rems(10.)) // Relative to font size
.w(relative(0.5)) // 50% of parent
.min_w(px(100.))
.max_w(px(400.))
```
## Common Patterns
### Centered Content
```rust
div()
.flex()
.items_center()
.justify_center()
.size_full()
.child("Centered")
```
### Card Layout
```rust
div()
.w(px(300.))
.bg(cx.theme().surface)
.rounded(px(8.))
.shadow_md()
.p(px(16.))
.gap(px(12.))
.flex()
.flex_col()
.child(heading())
.child(content())
```
### Responsive Spacing
```rust
div()
.p(px(16.)) // Padding all sides
.px(px(20.)) // Padding horizontal
.py(px(12.)) // Padding vertical
.pt(px(8.)) // Padding top
.gap(px(8.)) // Gap between children
```
## Styling Methods
### Dimensions
```rust
.w(px(200.)) // Width
.h(px(100.)) // Height
.size(px(200.)) // Width and height
.min_w(px(100.)) // Min width
.max_w(px(400.)) // Max width
```
### Colors
```rust
.bg(rgb(0x2196F3)) // Background
.text_color(rgb(0xFFFFFF)) // Text color
.border_color(rgb(0x000000)) // Border color
```
### Borders
```rust
.border(px(1.)) // Border width
.rounded(px(8.)) // Border radius
.rounded_t(px(8.)) // Top corners
.border_color(rgb(0x000000))
```
### Spacing
```rust
.p(px(16.)) // Padding
.m(px(8.)) // Margin
.gap(px(8.)) // Gap between flex children
```
### Flexbox
```rust
.flex() // Enable flexbox
.flex_row() // Row direction
.flex_col() // Column direction
.items_center() // Align items center
.justify_between() // Space between items
.flex_grow_1() // Grow to fill space
```
## h_flex / v_flex Helpers
`gpui_kit::component` provides shorthand helpers (import from `gpui_kit::component`):
```rust
use gpui_kit::component::{h_flex, v_flex};
// h_flex() = div().flex().flex_row().items_center()
h_flex()
.gap_2()
.child(icon)
.child(label)
// v_flex() = div().flex().flex_col()
v_flex()
.gap_4()
.p_4()
.child(input1)
.child(input2)
.child(submit_btn)
```
These are the standard layout primitives in `gpui_kit::component`. Prefer them over raw `div().flex()`.
## Tailwind-style Shorthand
GPUI provides Tailwind-style spacing/sizing shorthands:
```rust
// Spacing (0=0, 1=4px, 2=8px, 3=12px, 4=16px, ...)
.p_2() // padding: 8px
.px_4() // padding x: 16px
.py_3() // padding y: 12px
.m_2() // margin: 8px
.gap_3() // gap: 12px
// Size
.size_full() // width: 100%, height: 100%
.size_4() // width: 16px, height: 16px
.w_full() // width: 100%
.h_full() // height: 100%
.flex_1() // flex: 1 1 0 (fill remaining space)
.flex_shrink_0() // prevent shrinking
```
## Overflow and Scroll
```rust
div()
.overflow_hidden() // clip content
.overflow_x_hidden() // clip horizontal
.overflow_y_scrollbar() // show scrollbar on y axis
.overflow_scroll() // scroll both axes
```
## Absolute Positioning
```rust
div()
.relative() // position: relative (container)
.child(
div()
.absolute() // position: absolute
.top_0()
.right_0()
.child("badge")
)
// Inset helpers
div().absolute().inset_0() // top/right/bottom/left: 0 (fill parent)
div().absolute().top(px(8.)).left(px(8.))
```
## Stacking Order
```rust
div()
.relative()
.child(content)
.child(
div()
.absolute()
.top_0()
.right_0()
.child("badge")
) // later children are typically painted above earlier siblings
```
GPUI's general `Styled` API does **not** provide a `z_index(...)` method.
For normal elements, stacking is usually controlled by:
- Parent/child composition
- Absolute positioning
- Render order of siblings (later siblings paint above earlier ones)
If you see a `z_index(...)` method in this repository, make sure it belongs to the specific component you are using: a component's own ordering API is not a general GPUI `Div` styling method.
## Theme Integration
```rust
div()
.bg(cx.theme().surface)
.text_color(cx.theme().foreground)
.border_color(cx.theme().border)
.when(is_hovered, |el| {
el.bg(cx.theme().hover)
})
```
## Conditional Styling
```rust
use gpui_kit::prelude::FluentBuilder as _;
div()
.when(is_active, |el| el.bg(cx.theme().primary))
.when(!is_active, |el| el.opacity(0.5))
.when_some(optional_color.as_ref(), |el, color| el.bg(*color))
```
## Text Styling
```rust
div()
.text_sm() // font-size: small
.text_base() // font-size: base
.text_lg() // font-size: large
.font_bold() // font-weight: bold
.line_height_snug() // tighter line height
.truncate() // overflow: ellipsis, single line
.whitespace_nowrap()
```
@@ -0,0 +1,176 @@
# Test Examples
**Contents:** [Testing Best Practices](#testing-best-practices) · [Test Organization](#test-organization) · [Setup and Teardown](#setup-and-teardown) · [Assertions](#assertions) · [Performance Testing](#performance-testing) · [Running Tests](#running-tests) · [Basic Test Execution](#basic-test-execution) · [Test Configuration](#test-configuration) · [Advanced Test Execution](#advanced-test-execution) · [CI/CD Integration](#cicd-integration)
## Testing Best Practices
### Test Organization
Group related tests in modules:
```rust
#[cfg(test)]
mod tests {
use super::*;
mod entity_tests {
use super::*;
#[gpui_kit::test]
fn test_creation() { /* ... */ }
#[gpui_kit::test]
fn test_updates() { /* ... */ }
}
mod async_tests {
use super::*;
#[gpui_kit::test]
async fn test_async_ops() { /* ... */ }
}
mod distributed_tests {
use super::*;
#[gpui_kit::test]
fn test_multi_app() { /* ... */ }
}
}
```
### Setup and Teardown
Use helper functions for common setup:
```rust
fn create_test_counter(cx: &mut TestAppContext) -> Entity<Counter> {
cx.new(|cx| Counter::new(cx))
}
#[gpui_kit::test]
fn test_counter_operations(cx: &mut TestAppContext) {
let counter = create_test_counter(cx);
// Test operations
}
```
### Assertions
Use descriptive assertions:
```rust
#[gpui_kit::test]
fn test_counter_bounds(cx: &mut TestAppContext) {
let counter = create_test_counter(cx);
// Test upper bound
for _ in 0..100 {
counter.update(cx, |counter, cx| {
counter.increment(cx);
});
}
let count = counter.read_with(cx, |counter, _| counter.count);
assert!(count <= 100, "Counter should not exceed maximum");
// Test lower bound
for _ in 0..200 {
counter.update(cx, |counter, cx| {
counter.decrement(cx);
});
}
let count = counter.read_with(cx, |counter, _| counter.count);
assert!(count >= 0, "Counter should not go below minimum");
}
```
### Performance Testing
Test performance characteristics:
```rust
#[gpui_kit::test]
fn test_operation_performance(cx: &mut TestAppContext) {
let component = cx.new(|cx| MyComponent::new(cx));
let start = std::time::Instant::now();
// Perform many operations
for i in 0..1000 {
component.update(cx, |comp, cx| {
comp.perform_operation(i, cx);
});
}
let elapsed = start.elapsed();
assert!(elapsed < Duration::from_millis(100), "Operations should complete quickly");
}
```
## Running Tests
### Basic Test Execution
```bash
# Run all tests
cargo test
# Run specific test
cargo test test_counter_operations
# Run tests in a specific module
cargo test entity_tests::
# Run with output
cargo test -- --nocapture
```
### Test Configuration
Enable test-support feature for GPUI tests:
```toml
[features]
test-support = ["gpui/test-support"]
```
```bash
cargo test --features test-support
```
### Advanced Test Execution
```bash
# Run tests with iterations for property testing
cargo test -- --test-threads=1
# Run tests matching a pattern
cargo test test_async
# Run tests with backtrace on failure
RUST_BACKTRACE=1 cargo test
```
### CI/CD Integration
For continuous integration:
```yaml
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- name: Run tests
run: cargo test --features test-support
```
GPUI's testing framework provides deterministic, fast, and comprehensive testing capabilities that mirror real application behavior while providing the control needed for thorough testing of complex UI and async scenarios.
@@ -0,0 +1,473 @@
# Test Reference
**Contents:** [Testing Patterns](#testing-patterns) · [Testing Crash-Free State Management (Re-entrancy)](#testing-crash-free-state-management-re-entrancy) · [Property Testing](#property-testing) · [Distributed Systems Testing](#distributed-systems-testing) · [Mocking and Isolation](#mocking-and-isolation)
## Testing Patterns
### Basic Entity Testing
Test entity creation, updates, and reads:
```rust
#[gpui_kit::test]
fn test_counter_entity(cx: &mut TestAppContext) {
let counter = cx.new(|cx| Counter::new(cx));
// Test initial state
let initial_count = counter.read_with(cx, |counter, _| counter.count);
assert_eq!(initial_count, 0);
// Test updates
counter.update(cx, |counter, cx| {
counter.count = 42;
cx.notify();
});
let updated_count = counter.read_with(cx, |counter, _| counter.count);
assert_eq!(updated_count, 42);
}
```
### Event Testing
Test event emission and handling:
```rust
#[derive(Clone)]
struct ValueChanged {
new_value: i32,
}
impl EventEmitter<ValueChanged> for MyComponent {}
#[gpui_kit::test]
fn test_event_emission(cx: &mut TestAppContext) {
let component = cx.new(|cx| {
let mut comp = MyComponent::default();
// Subscribe to self
cx.subscribe_self(|this, event: &ValueChanged, cx| {
this.received_value = event.new_value;
cx.notify();
});
comp
});
// Emit event
component.update(cx, |_, cx| {
cx.emit(ValueChanged { new_value: 123 });
});
// Verify event was handled
let received = component.read_with(cx, |comp, _| comp.received_value);
assert_eq!(received, 123);
}
```
### Action Testing
Test action dispatching and handling:
```rust
actions!(my_app, [Increment, Decrement]);
#[gpui_kit::test]
fn test_action_dispatch(cx: &mut TestAppContext) {
let window = cx.update(|cx| {
cx.open_window(Default::default(), |_, cx| {
cx.new(|cx| MyComponent::new(cx))
}).unwrap()
});
let mut cx = VisualTestContext::from_window(window.into(), cx);
let counter = window.root(&mut cx).unwrap();
// Dispatch action via focus handle
let focus_handle = counter.read_with(&cx, |counter, _| counter.focus_handle.clone());
cx.update(|window, cx| {
focus_handle.dispatch_action(&Increment, window, cx);
});
let count = counter.read_with(&cx, |counter, _| counter.count);
assert_eq!(count, 1);
}
```
### Async Testing
Test async operations and background tasks:
```rust
impl MyComponent {
fn load_data(&self, cx: &mut Context<Self>) -> Task<i32> {
cx.spawn(async move |this, cx| {
// Simulate async work
this.update(cx, |comp, _| comp.loading = true).await;
// Return result
42
})
}
fn background_update(&self, cx: &mut Context<Self>) {
cx.spawn(async move |this, cx| {
// Background work
this.update(cx, |comp, _| {
comp.value += 10;
}).await;
}).detach();
}
}
#[gpui_kit::test]
async fn test_async_operations(cx: &mut TestAppContext) {
let component = cx.new(|cx| MyComponent::new(cx));
// Test awaited task
let result = component.update(cx, |comp, cx| comp.load_data(cx)).await;
assert_eq!(result, 42);
// Test detached task
component.update(cx, |comp, cx| comp.background_update(cx));
// Detached tasks don't run until you yield
let value_before = component.read_with(cx, |comp, _| comp.value);
assert_eq!(value_before, 0);
// Run pending tasks
cx.run_until_parked();
let value_after = component.read_with(cx, |comp, _| comp.value);
assert_eq!(value_after, 10);
}
```
### Timer Testing
Test timer-based operations:
```rust
impl MyComponent {
fn delayed_action(&self, cx: &mut Context<Self>) {
cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(Duration::from_millis(100))
.await;
this.update(cx, |comp, cx| {
comp.action_performed = true;
cx.notify();
}).await;
}).detach();
}
}
#[gpui_kit::test]
async fn test_timers(cx: &mut TestAppContext) {
let component = cx.new(|cx| MyComponent::new(cx));
component.update(cx, |comp, cx| comp.delayed_action(cx));
// Action shouldn't have completed yet
let performed = component.read_with(cx, |comp, _| comp.action_performed);
assert!(!performed);
// Run until parked (timers complete)
cx.run_until_parked();
let performed = component.read_with(cx, |comp, _| comp.action_performed);
assert!(performed);
}
```
### External I/O Testing
For tests involving external systems, use `allow_parking()`:
```rust
#[gpui_kit::test]
async fn test_external_io(cx: &mut TestAppContext) {
// Allow parking for external I/O
cx.executor().allow_parking();
// Simulate external operation
let (tx, rx) = futures::channel::oneshot::channel();
std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(10));
tx.send(42).ok();
});
let result = rx.await.unwrap();
assert_eq!(result, 42);
}
```
## Testing Crash-Free State Management (Re-entrancy)
The most dangerous class of GPUI bugs is **entity re-entrancy**: code that tries to update or read an entity while it is already locked in a render/update pass. These bugs are invisible at compile time — they only panic at runtime, typically when the user clicks something in a list or dropdown.
**Key properties of re-entrancy panics:**
- Triggered by user interaction (click, key press), not during static rendering.
- `#[should_panic]` only confirms the bug exists — tests must pass *without* panicking.
- `cx.run_until_parked()` is required after the triggering action to let deferred callbacks (`defer_in`) execute.
### Pattern: Drive `confirm` / `cancel` through the real delegate
```rust
#[gpui_kit::test]
fn test_confirm_does_not_panic(cx: &mut TestAppContext) {
// Build the component state the same way the real app does.
let state = cx.new(|cx| {
SelectState::new(MyDelegate::default(), None, &mut cx.window_handle(), cx)
});
// Simulate the user clicking the first item — this exercises on_confirm
// and its deferred callback, which is where re-entrancy bugs hide.
state.update(cx, |this, cx| {
this.state.list.update(cx, |list, cx| {
list.delegate_mut().set_selected_index(Some(IndexPath::new(0)), window, cx);
list.delegate_mut().confirm(false, window, cx);
});
});
// Let defer_in callbacks execute — required to trigger the crash.
cx.run_until_parked();
// If we reach here without panicking, the re-entrancy is fixed.
let selected = state.read_with(cx, |s, _| s.selected_value().cloned());
assert!(selected.is_some());
}
```
### Pattern: Verify `on_will_change` and `on_confirm` hooks are called correctly
Use a recording delegate to assert that hooks fire with the right arguments and in the right order:
```rust
#[derive(Default)]
struct RecordingDelegate {
items: Vec<MyItem>,
will_change_calls: Vec<Vec<IndexPath>>,
confirm_calls: Vec<Vec<IndexPath>>,
}
impl SearchableListDelegate for RecordingDelegate {
// … required impls …
fn on_will_change(
&mut self,
change: &mut SearchableListChange<Self>,
_current: &[(IndexPath, Self::Item)],
) {
self.will_change_calls.push(
change.select_queue.iter().map(|(ix, _)| *ix).collect()
);
}
fn on_confirm(&mut self, final_selection: &[(IndexPath, Self::Item)]) {
self.confirm_calls.push(
final_selection.iter().map(|(ix, _)| *ix).collect()
);
}
}
#[gpui_kit::test]
fn test_hooks_fire_in_correct_order(cx: &mut TestAppContext) {
let state = cx.new(|cx| SelectState::new(RecordingDelegate::with_items(3), None, window, cx));
// Simulate confirm on item 0
state.update(cx, |this, cx| {
// … trigger confirm …
});
cx.run_until_parked();
state.read_with(cx, |s, cx| {
let delegate = s.state.list.read(cx).delegate().delegate;
assert_eq!(delegate.will_change_calls.len(), 1);
assert_eq!(delegate.confirm_calls.len(), 1);
assert_eq!(delegate.confirm_calls[0], vec![IndexPath::new(0)]);
});
}
```
### Pattern: Rapid multiple confirms (snapshot consistency)
```rust
#[gpui_kit::test]
fn test_rapid_confirms_keep_consistent_snapshot(cx: &mut TestAppContext) {
let state = cx.new(|cx| SelectState::new(MyDelegate::with_items(5), None, window, cx));
for i in 0..5 {
state.update(cx, |this, cx| {
// trigger confirm on item i
});
cx.run_until_parked();
state.read_with(cx, |s, cx| {
let snapshot = s.state.list.read(cx).delegate().selection_snapshot.clone();
let selection = s.state.selection.clone();
assert_eq!(snapshot, selection, "snapshot out of sync after confirm {i}");
});
}
}
```
### Checklist: What to test for each component that uses `defer_in`
- [ ] `confirm` path: no panic, correct final selection, snapshot matches selection
- [ ] `cancel` path: no panic, selection unchanged, popover closed
- [ ] `on_will_change` veto: selection not changed, `on_confirm` not called
- [ ] `on_will_change` mutating the change: final selection reflects delegate's modification
- [ ] Rapid consecutive confirms: each one leaves snapshot consistent with selection
- [ ] `render_item` never panics even when called immediately after a mutation
## Property Testing
Use random data to test edge cases:
```rust
#[gpui_kit::test(iterations = 10)]
fn test_counter_random_operations(cx: &mut TestAppContext, mut rng: StdRng) {
let counter = cx.new(|cx| Counter::new(cx));
let mut expected = 0i32;
for _ in 0..100 {
let delta = rng.random_range(-10..=10);
expected += delta;
counter.update(cx, |counter, cx| {
counter.count += delta;
cx.notify();
});
}
let actual = counter.read_with(cx, |counter, _| counter.count);
assert_eq!(actual, expected);
}
```
## Distributed Systems Testing
Test multiple app contexts communicating:
```rust
#[derive(Clone)]
struct NetworkMessage {
from: String,
to: String,
data: i32,
}
#[gpui_kit::test]
fn test_distributed_apps(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
// Create components in different app contexts
let comp_a = cx_a.new(|_| MyComponent::new("A".to_string()));
let comp_b = cx_b.new(|_| MyComponent::new("B".to_string()));
// Simulate message passing
comp_a.update(cx_a, |comp, cx| {
comp.send_message("B", 42, cx);
});
// Run async operations
cx_a.run_until_parked();
// Verify message received in other context
comp_b.update(cx_b, |comp, _| {
comp.receive_messages();
});
let messages = comp_b.read_with(cx_b, |comp, _| comp.messages.clone());
assert_eq!(messages.len(), 1);
assert_eq!(messages[0].data, 42);
}
```
### Interleaving Testing
Test concurrent operations with random execution order:
```rust
#[gpui_kit::test(iterations = 10)]
fn test_concurrent_operations(
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
mut rng: StdRng,
) {
let comp_a = cx_a.new(|_| MyComponent::new());
let comp_b = cx_b.new(|_| MyComponent::new());
// Perform random operations across contexts
for i in 0..20 {
if rng.random_bool(0.5) {
comp_a.update(cx_a, |comp, cx| {
comp.perform_operation(i, cx);
});
} else {
comp_b.update(cx_b, |comp, cx| {
comp.perform_operation(i, cx);
});
}
}
// Run all pending operations
cx_a.run_until_parked();
// Verify final state
let state_a = comp_a.read_with(cx_a, |comp, _| comp.state);
let state_b = comp_b.read_with(cx_b, |comp, _| comp.state);
// Assert invariants hold despite execution order
assert!(state_a.is_consistent());
assert!(state_b.is_consistent());
}
```
## Mocking and Isolation
### Network Mocking
Create mock networks for testing distributed features:
```rust
struct MockNetwork {
messages: Arc<Mutex<Vec<NetworkMessage>>>,
}
impl MockNetwork {
fn new() -> Self {
Self {
messages: Arc::new(Mutex::new(Vec::new())),
}
}
fn send(&self, message: NetworkMessage) {
self.messages.lock().unwrap().push(message);
}
fn receive_all(&self) -> Vec<NetworkMessage> {
self.messages.lock().unwrap().drain(..).collect()
}
}
#[gpui_kit::test]
fn test_networked_components(cx: &mut TestAppContext) {
let network = Arc::new(MockNetwork::new());
let sender = cx.new(|_| MessageSender::new(network.clone()));
let receiver = cx.new(|_| MessageReceiver::new(network));
// Send message
sender.update(cx, |sender, _| {
sender.send("Hello");
});
// Receive message
receiver.update(cx, |receiver, _| {
receiver.receive_all();
});
let received = receiver.read_with(cx, |receiver, _| receiver.messages.clone());
assert_eq!(received, vec!["Hello"]);
}
```
@@ -0,0 +1,252 @@
# Testing
**Contents:** [Choose the test level](#choose-the-test-level) · [UI integration workflow](#ui-integration-workflow) · [Complete example](#complete-example) · [Queries and interactions](#queries-and-interactions) · [Frames and async work](#frames-and-async-work) · [Assertion boundaries](#assertion-boundaries) · [Additional resources](#additional-resources)
## UI integration testing
A UI integration test renders real components in a headless window, dispatches
clicks, keyboard input and scrolling, then checks state, focus, layout and owner
callbacks. For example: add a Checkbox UI integration test that proves clicking
toggles the owner's value and disabled controls reject the interaction.
Use this term when describing component interaction coverage.
`#[gpui_kit::test]` runs the test; `gpui_kit::test` operates and inspects its UI.
## Choose the test level
Use ordinary Rust `#[test]` for pure logic. Use `#[gpui_kit::test]` and
`TestAppContext` for entities, subscriptions, actions and async tasks; it can
create headless windows too. `VisualTestContext` is available for existing GPUI
window helpers. For an application UI flow, use `gpui_kit::test::TestWindowExt`
on the real `Window` and assert the behavior produced by native events.
Import the Kit types you use explicitly and write `#[gpui_kit::test]`. Avoid `use gpui_kit::*;` in test modules: it imports GPUI’s `test` macro and can shadow Rust’s built-in `#[test]`. Add
`test-support` to the application's `gpui-kit` development dependency, using
exactly the same source and version as its normal dependency. The helpers
require a Kit revision that includes them; check the installed API before
assuming an older published release supports them. No extra testing crate or
GPUI patch is required.
For a standalone test package beside a Kit checkout:
```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"] }
```
Headless tests still need the platform's native build dependencies. Run
`cargo generate-lockfile` once for the new package, then
`cargo test --test ui --locked`. In the Kit repository the equivalent command
is `cargo test -p gpui-kit --features test-support --test ui --locked`.
## UI integration workflow
1. Import the production view and constructor from the application library.
Keep state entities and subscriptions owned by that view. Define a view
inside a test only for a self-contained example or a deliberate fixture.
2. Initialize with `cx.update(gpui_kit::init)`, open a headless window of an
explicit size, and wrap a Component application in `Root`. Mount the
`Root::render_dialog_layer`, `render_sheet_layer` and
`render_notification_layer` children when the flow uses those overlays;
constructing `Root` alone does not mount them.
3. Render a frame and locate existing control IDs. Custom native elements opt
in with `.id("status").test_support()` through `TestSupportExt`. Place it
before `.track_focus(&handle)`; custom wrappers must forward that method
to their inner element. Registration adds no layout container.
4. Dispatch clicks, input and named keys through `TestWindowExt`. Exercise the
actual handler path; invoking callbacks or directly setting control state
would bypass the behavior being tested.
5. Assert the initial state, the visible outcome after each meaningful action,
and the application result. Include a relevant negative case, such as a
disabled control rejecting the interaction or invalid input preventing save.
6. Run the affected test target and report its result. An uncompiled example or
a test which never checks the action's outcome is not completion evidence.
## Complete example
Copy this into `tests/ui.rs`. It is the exact Kit integration-test example
from `crates/kit/tests/ui.rs`; the inline view makes this reference usable
without a repository checkout. In an application, replace that view definition
with an import of your production view.
```rust
use gpui_kit::test::{TestSupportExt, TestWindowExt};
use gpui_kit::{
AppContext, Context, Entity, SharedString, TestAppContext, Window,
component::{
Root,
button::Button,
input::{Input, InputState},
},
div,
prelude::*,
px, size,
};
struct Profile {
name: Entity<InputState>,
submitted: Option<SharedString>,
}
impl Render for Profile {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let status = self.submitted.as_ref().map_or_else(
|| SharedString::from("Not saved"),
|name| SharedString::from(format!("Saved: {name}")),
);
div()
.size_full()
.flex()
.flex_col()
.p_4()
.gap_4()
.child(Input::new(&self.name).id("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(gpui_kit::Role::Status)
.test_support()
.aria_label(status.clone())
.child(status),
)
}
}
#[gpui_kit::test]
fn saves_a_profile_through_the_ui(cx: &mut TestAppContext) {
cx.update(gpui_kit::init);
let mut profile = None;
let handle = cx.open_window(size(px(640.), px(480.)), |window, cx| {
let view = cx.new(|cx| Profile {
name: cx.new(|cx| InputState::new(window, cx)),
submitted: None,
});
profile = Some(view.clone());
Root::new(view, window, cx)
});
let profile = profile.unwrap();
cx.update_window(handle.into(), |_, window, cx| {
window.render_frame(cx);
assert_eq!(window.find("status").label(), Some("Not saved"));
window.click("name", cx);
window.input("Ada 中文", cx);
let name = window.find("name");
assert_eq!(name.focused(), Some(true));
assert_eq!(name.value(), Some("Ada 中文"));
assert!(name.bounds().size.width > px(0.));
// Named keys share the same Window API and refresh the resulting frame.
window.press("backspace", cx);
assert_eq!(window.find("name").value(), Some("Ada 中"));
window.click("save", cx);
let status = window.find("status");
assert!(status.visible());
assert_eq!(status.label(), Some("Saved: Ada 中"));
assert!(status.bounds().top() >= window.find("save").bounds().bottom());
})
.unwrap();
// Verify the application result as well as the native properties.
cx.update(|cx| {
assert_eq!(profile.read(cx).submitted.as_deref(), Some("Ada 中"));
});
}
```
## Queries and interactions
Import `TestWindowExt` and, for custom registration, `TestSupportExt` from
`gpui_kit::test`. Use normal Rust `assert!` and `assert_eq!` with snapshots.
| API | Behavior |
| --- | --- |
| `window.find(id)` | Requires a unique observed target; errors list registered paths. |
| `window.try_find(id)` | Returns `None` when absent; ambiguous IDs still panic. |
| `window.within(id)` | Resolves a native GPUI identity scope, including an unobserved ancestor. |
| `click`, `right_click`, `double_click`, `hover` | Dispatch real pointer events at the target center. |
| `click_at(id, offset, cx)` | Uses an offset from the target bounds' top-left corner. |
| `scroll(id, delta, cx)` | Dispatches a GPUI `ScrollDelta` wheel event. |
| `drag_to(from_id, to_id, cx)` | Drags between target centers within the current scope. |
| `window.drag(from, to, cx)` | Drags between window-local points; use `bounds()` for precise or cross-scope geometry. |
| `press(key, cx)` | Sends named GPUI keys such as `backspace`, `escape` or `secondary-a`. |
| `input(text, cx)` | Types Unicode characters at current focus; click the input first. |
GPUI IDs need only be unique within their native scope. Use
`window.within("dialog").find("name")` for repeated local IDs. Scoped windows
also provide pointer operations, `press` and `input`; keyboard helpers require
observed focus inside that scope and recheck it between characters. They do
not focus the target or replace its value. For a submenu with another
`"popup-menu"` ID, retain the resolved outer scope before opening the submenu,
then query its descendants.
## Frames and async work
Snapshots are owned facts from the last completed frame, not live elements.
Fetch a new snapshot after an interaction. Interaction helpers refresh frames;
after an external entity update, focus change or resize, call
`window.render_frame(cx)` before querying.
For deferred actions, subscriptions or popup updates, leave `update_window`
so GPUI can process its effects. Use `cx.run_until_parked()` for queued work,
or import `TestAppContextExt` and await a bounded condition:
```rust
cx.wait_for(handle.into(), std::time::Duration::from_secs(1), |window, _| {
window.try_find("dialog").is_none()
}).await;
```
`wait_for` runs outside a borrowed window update and uses GPUI test time.
Process a queued action before changing any input that the action is meant to
validate. Otherwise it can read the later input instead of the value at the
intended test step.
For timers, check the UI before its deadline as well as after expiration.
GPUI's non-synced `Animation` uses wall-clock time; advancing the test clock
does not complete it. Use reduced motion when supported, or a bounded wait
appropriate to that production animation before asserting final geometry.
## Assertion boundaries
- `checked()`, `selected()`, `expanded()`, `value()` and `label()` come from
native accessibility properties. `None` means unavailable; masked input
deliberately omits its value. Do not supply a second set of test-only state.
- `disabled()` currently reports `Some(true)` or `None`. To prove a control is
enabled, perform the interaction and assert the result. Absence of a flag
is not proof that the control accepts input.
- `focused()` measures the observed native focus scope. Missing bindings are
diagnosed when the native element advertises `Action::Focus`; controls
without that action can still return `None`. Register the real binding.
- `bounds()` supports containment, relative position, overlap, scrolling and
drag assertions. `visible()` checks geometry, clipping and observed style;
it does not prove a target is unobstructed. Real hit testing decides whether
a click reaches the control.
- Native properties can be wrong too. Pair them with real interactions,
geometry and the application result. A label is not rendered text, and a
correct accessibility value does not establish correct pixels.
- These tests run in-process. Pixel comparisons need GPUI's separate renderer;
packaged-app behavior and full OS IME composition need other coverage. Kit's
explicit macOS rendering target is
`cargo test -p gpui-kit --features test-support --test rendering --locked`.
## Additional resources
- [Website testing guide](https://gpui-kit.com/docs/test) — bilingual setup,
component coverage, platform requirements and CI.
- [GPUI test examples](test-examples.md) — entity and context patterns.
- [GPUI test reference](test-reference.md) — re-entrancy and property tests.
@@ -0,0 +1,122 @@
# Tested application recipe
This complete view comes from `examples/ai_recipes/src/lib.rs`. Its standalone consumer depends only on `gpui-kit`; the same source is compiled and tested by `script/check-ai-recipes`. Pair it with that example's `src/main.rs`, which installs assets, initializes the library, and wraps the window in `Root`.
The view owns both input state and subscriptions. Rendering creates only elements. Application content renders each overlay layer once; `Root` alone does not render dialog, sheet, or notification content.
<!-- recipe:settings:start -->
```rust
use gpui_kit::component::{
ActiveTheme, IconName, Root, 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 {
pub name: Entity<InputState>,
pub preview: SharedString,
pub changes: usize,
enabled: bool,
remember: bool,
delivery: Option<usize>,
_subscriptions: Vec<Subscription>,
}
impl Settings {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> 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],
}
}
}
impl Render for Settings {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> 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")
});
}),
),
)
.children(Root::render_dialog_layer(window, cx))
.children(Root::render_sheet_layer(window, cx))
.children(Root::render_notification_layer(window, cx))
}
}
```
<!-- recipe:settings:end -->
For changes in this repository, run `script/check-ai docs`, `script/check-ai rust`, or `script/check-ai shell` for the relevant verification profile (or `script/check-ai all` for all three). For a downstream app, compile and test your own consumer, then verify keyboard/focus and visuals in a real window. Automated recipe tests do not establish a model success rate.
+408
View File
@@ -0,0 +1,408 @@
# gpui-kit Usage Guide
**Contents:** [Setup](#setup) · [Component Types](#component-types) · [Common Components](#common-components) (Button, Input, Select, Checkbox, Icon, Dialog, Notification, Tabs, Tooltip, Form, List) · [Theming](#theming) · [Layout Helpers](#layout-helpers) · [Overlay Layers](#overlay-layers-dialogs-sheets-notifications) · [Shared Traits](#shared-traits)
## Setup
Use the [tested application recipe](recipes.md) for complete examples and their verification command. Store subscription handles on the owning view; binding one to a constructor-local variable alone does not keep it alive after construction.
### 1. Cargo.toml
```toml
[dependencies]
gpui-kit = "0.6" # re-exports GPUI, platform, base, component and the default icons; Shell is a separate host dependency
```
### 2. Initialization
```rust
fn main() {
gpui_kit::application()
.with_assets(gpui_kit::assets::Assets)
.run(move |cx| {
gpui_kit::init(cx); // MUST be first
cx.spawn(async move |cx| {
cx.open_window(WindowOptions::default(), |window, cx| {
let view = cx.new(|_| MyApp);
cx.new(|cx| Root::new(view, window, cx)) // Root wraps first view
}).expect("Failed to open window");
}).detach();
});
}
```
**`Root` is required** as the first-level child of every window — it enables dialogs, sheets, and notifications.
---
## Component Types
### Stateless (most components)
Used directly in `render`, no stored state:
```rust
use gpui_kit::component::button::Button;
impl Render for MyView {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
Button::new("btn").primary().label("Submit")
.on_click(|_, _, _| println!("clicked"))
}
}
```
### Stateful (Input, Select, Combobox, etc.)
Require an `Entity<State>` stored in your view:
```rust
use gpui_kit::component::input::{Input, InputState};
struct MyView {
name: Entity<InputState>,
}
impl MyView {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
Self {
name: cx.new(|cx| InputState::new(window, cx).placeholder("Your name")),
}
}
}
impl Render for MyView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
Input::new(&self.name)
}
}
```
---
## Common Components
### Button
```rust
use gpui_kit::component::button::{Button, ButtonGroup};
// Variants
Button::new("btn").label("Default")
Button::new("btn").primary().label("Primary")
Button::new("btn").danger().label("Delete")
Button::new("btn").warning().label("Warning")
Button::new("btn").success().label("Success")
Button::new("btn").ghost().label("Ghost")
Button::new("btn").link().label("Link")
// States
Button::new("btn").label("Text").disabled(true)
Button::new("btn").label("Text").loading(true)
Button::new("btn").label("Text").selected(true)
// With icon
Button::new("btn").icon(IconName::Plus).label("Add")
// Sizes
Button::new("btn").xsmall().label("XS")
Button::new("btn").small().label("S")
Button::new("btn").large().label("L")
// Group
ButtonGroup::new("group")
.child(Button::new("a").label("A"))
.child(Button::new("b").label("B"))
.on_click(|indices, _, _| { /* selected indices */ })
```
### Input
```rust
use gpui_kit::component::input::{Input, InputState};
// State setup (in new/init)
let input = cx.new(|cx| InputState::new(window, cx)
.placeholder("Enter text...")
.default_value("Hello")
);
// Render
Input::new(&input)
Input::new(&input).cleanable(true) // clear button
Input::new(&input).disabled(true)
Input::new(&input).prefix(Icon::new(IconName::Search).small())
Input::new(&input).suffix(Button::new("b").ghost().icon(IconName::X).xsmall())
Input::new(&input).content_type(InputContentType::Password)
Input::new(&input).mask_toggle() // password reveal toggle
Input::new(&input).appearance(false) // remove default border/bg
// Reading value
let value = input.read(cx).value();
// Keep this subscription in the owning view (for example, `_subscriptions`).
self._subscriptions.push(cx.subscribe_in(&input, window, |view, state, event, window, cx| {
match event {
InputEvent::Change => { let v = state.read(cx).value(); }
InputEvent::PressEnter { .. } => { /* submit */ }
InputEvent::Focus | InputEvent::Blur => {}
}
}));
```
### Select
```rust
use gpui_kit::component::select::{Select, SelectState};
// Simple string list
let state = cx.new(|cx| {
SelectState::new(vec!["Apple", "Orange", "Banana"], Some(IndexPath::default()), window, cx)
});
// Render
Select::new(&state)
Select::new(&state).placeholder("Pick one")
// Reading selection
let selected = state.read(cx).selected_item();
```
### Checkbox / Switch / Radio
```rust
use gpui_kit::component::{checkbox::Checkbox, switch::Switch};
// Stateless (controlled)
Checkbox::new("cb").checked(self.checked)
.on_change(cx.listener(|this, checked, _, cx| {
this.checked = *checked;
cx.notify();
}))
Switch::new("sw").checked(self.enabled)
.on_change(cx.listener(|this, checked, _, cx| {
this.enabled = *checked;
cx.notify();
}))
```
### Icon
```rust
use gpui_kit::component::{Icon, IconName};
Icon::new(IconName::Check)
Icon::new(IconName::Search).small()
Icon::new(IconName::Plus).large().text_color(cx.theme().primary)
```
### Dialog
```rust
use gpui_kit::component::dialog::{Dialog, DialogAction, DialogClose, DialogFooter};
// Open from window context. `footer` takes an element, not a closure.
// DialogClose dismisses the dialog, so no manual close call is needed.
window.open_dialog(cx, |dialog, _, _| {
dialog
.title("Export Report")
.child("Choose a destination for the exported file.")
.footer(
DialogFooter::new()
.gap_2()
.child(DialogClose::new().child(
Button::new("cancel").label("Cancel").outline(),
))
.child(DialogAction::new().child(
Button::new("export").label("Export").primary(),
)),
)
});
```
### AlertDialog
Use `AlertDialog` — not `Dialog` — to confirm a consequential action. It is not
overlay-closable and has no close button, so the choice must be made. Name the
object in the title and the result on the confirming button; see the Design
Guides for the copy rules.
```rust
use gpui_kit::component::{button::ButtonVariant, dialog::DialogButtonProps};
window.open_alert_dialog(cx, |alert, _, _| {
alert
.title("Remove “Roadmap”?")
.description("Files on disk aren’t deleted.")
.button_props(
DialogButtonProps::default()
.ok_text("Remove")
.ok_variant(ButtonVariant::Danger)
.on_ok(|_, _, _| true),
)
});
```
### Notification
```rust
// Simple string message
window.push_notification("Saved successfully!", cx);
// With type variant
window.push_notification(
Notification::new("Upload complete").info().message("File uploaded"),
cx,
);
```
### Tabs
```rust
use gpui_kit::component::tab::{Tab, TabBar};
TabBar::new("tabs")
.child(Tab::new("tab1").child("Overview"))
.child(Tab::new("tab2").child("Settings"))
.child(Tab::new("tab3").child("Logs"))
```
### Tooltip
```rust
// On any element with .id(), add .tooltip():
div()
.id("my-btn")
.tooltip(|window, cx| Tooltip::new("Delete item").build(window, cx))
.child("Delete")
// Or on a Button directly:
Button::new("btn").icon(IconName::Trash).tooltip("Delete")
```
### Form
```rust
use gpui_kit::component::form::{v_form, h_form, field};
// Vertical form
v_form()
.child(field().label("Name").child(Input::new(&self.name)))
.child(field().label("Email").child(Input::new(&self.email)))
.footer(Button::new("submit").primary().label("Submit"))
// Horizontal label alignment
h_form()
.child(field().label("Username").child(Input::new(&self.username)))
```
### List (searchable, virtualized)
```rust
use gpui_kit::component::list::{List, ListState, ListDelegate, ListItem, ListEvent};
// Implement ListDelegate for your data type, then:
let list_state = cx.new(|cx| ListState::new(MyDelegate::new(), window, cx));
// Render
List::new(&list_state)
// Keep this subscription in the owning view, alongside list_state.
self._subscriptions.push(cx.subscribe(&list_state, |this, _, event, cx| {
if let ListEvent::Select(index_path) = event {
// handle selection
}
}));
```
---
## Theming
```rust
use gpui_kit::component::ActiveTheme as _;
// Access colors
cx.theme().primary
cx.theme().background
cx.theme().foreground
cx.theme().border
cx.theme().surface
cx.theme().muted
cx.theme().destructive
// Use in styles
div()
.bg(cx.theme().surface)
.text_color(cx.theme().foreground)
.border_color(cx.theme().border)
```
### Switch Theme
```rust
use gpui_kit::component::Theme;
// Toggle light/dark
cx.update_global::<Theme, _>(|theme, cx| {
theme.toggle_mode(cx);
});
// Load a named theme
Theme::global_mut(cx).apply_config(&theme_config);
```
---
## Layout Helpers
`gpui_kit::component` extends GPUI with convenient layout methods:
```rust
h_flex() // div().flex().flex_row().items_center()
v_flex() // div().flex().flex_col()
// Common patterns
h_flex().gap_2().items_center()
.child(Icon::new(IconName::User))
.child(label("Username"))
v_flex().gap_4().p_4()
.child(Input::new(&self.name))
.child(Input::new(&self.email))
.child(Button::new("submit").primary().label("Submit"))
```
---
## Overlay Layers (Dialogs, Sheets, Notifications)
To render overlays, add these to your first-level view's render:
```rust
impl Render for MyApp {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.child(self.main_content(window, cx))
.children(Root::render_dialog_layer(cx))
.children(Root::render_sheet_layer(cx))
.children(Root::render_notification_layer(cx))
}
}
```
---
## Shared Traits
Builders return the component so methods can be chained. Constructors follow component families: controls take a stable ID, retained controls take state, and compound parts may take no arguments. See [component conventions](conventions.md).
- `Sizable`: `.xsmall()` / `.small()` / `.medium()` (default) / `.large()`
- `Disableable`: `.disabled(bool)`
- `Selectable`: `.selected(bool)`
- `Styled`: any GPUI style methods (`.w()`, `.bg()`, `.p_2()`, etc.)
For any component not covered here, fetch its doc from:
`https://gpui-kit.com/component/{name}.md`
+1
View File
@@ -0,0 +1 @@
../../.agents/skills/gpui-kit
+1
View File
@@ -0,0 +1 @@
../../.agents/skills/gpui-kit-design-guides
+17
View File
@@ -0,0 +1,17 @@
{
"version": 1,
"skills": {
"gpui-kit": {
"source": "longbridge/gpui-kit",
"sourceType": "github",
"skillPath": "skills/gpui-kit/SKILL.md",
"computedHash": "017206ed25c2a23fda2d0abc50c2af417e31c47231e08bbdb5891304282fe6c1"
},
"gpui-kit-design-guides": {
"source": "longbridge/gpui-kit",
"sourceType": "github",
"skillPath": "skills/gpui-kit-design-guides/SKILL.md",
"computedHash": "1c8cd7b6fb0dc3448d00748dfccd2c2719ce7efbb97632ac13d015ed2bd74dc6"
}
}
}