diff --git a/.agents/skills/gpui-kit-design-guides/SKILL.md b/.agents/skills/gpui-kit-design-guides/SKILL.md new file mode 100644 index 0000000..c65738f --- /dev/null +++ b/.agents/skills/gpui-kit-design-guides/SKILL.md @@ -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. diff --git a/.agents/skills/gpui-kit-design-guides/references/design-guides.md b/.agents/skills/gpui-kit-design-guides/references/design-guides.md new file mode 100644 index 0000000..fc3dc08 --- /dev/null +++ b/.agents/skills/gpui-kit-design-guides/references/design-guides.md @@ -0,0 +1,950 @@ +--- +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. +6. **Geometry explains containment.** Radii, insets, and boundaries should + describe one coherent hierarchy, even where surfaces overlap. +7. **Feedback reflects reality.** Loading, tooltips, and motion should clarify + a real state or transition, not add ceremony to an immediate action. + +## Learning from Shadcn + +Shadcn's most useful contribution is not a particular border color. It is a +way of building a system: + +- own the top layer of the interface instead of fighting a sealed abstraction; +- compose small, predictable parts into product-specific components; +- provide defaults that already form one visual language; +- keep the code and composition legible to both people and AI; +- separate behavior primitives from the styled layer. + +GPUI Kit applies those ideas through a Rust library and the split between +`gpui-base` and `gpui-component`. Applications normally compose or wrap the +published components; contributors move genuinely reusable behavior into Base +and keep visual policy above it. + +Do not copy these web assumptions blindly: + +| Web habit | Native GPUI default | +| --- | --- | +| Pointing-hand cursor on every button | Default arrow cursor; pointing hand for links | +| Page navigation as the main structure | Persistent windows, panes, sidebars, tabs, and menus | +| Browser focus and scrolling as a fallback | Explicit focus ownership and region-owned scrolling | +| Mobile-first single column | Resizable desktop shell with a defined minimum window size | +| Hover-revealed critical actions | Keyboard- and pointer-reachable actions that do not depend on hover | +| A row of hover-only icon buttons | A visible primary action plus `DropdownMenu` or `ContextMenu` for secondary commands | +| Link-styled text for application commands | `Button`, `outline`, or `ghost`; Link only for URLs, web resources, or email addresses | +| Large touch density everywhere | Medium density by default; compact only where information work benefits | +| CSS overrides across descendants | Typed builders, semantic parts, and application composition | + +## Start from the task + +Before drawing a screen, write down: + +- the user's primary task; +- the object being viewed or changed; +- the actions that must remain immediately available; +- the information required to make a decision; +- empty, loading, error, offline, read-only, and permission-denied states; +- the keyboard path through the workflow. + +Organize the window around those answers. Do not begin with a dashboard grid or +a component catalogue. A good desktop interface exposes the user's mental +model: documents, accounts, projects, messages, settings, or another stable +object—not the internal service architecture. + +Design the primary task before distributing controls. Its visual weight, +location, and information depth should match its importance to the product. A +core result must not be reduced to a small count, an icon in a corner, or a +weak footer action while secondary content consumes the page. When a result set +is the product's main value, consider a summary region or card that exposes the +count, representative results, meaningful state, and a clear next action. + +For every proposed action, name its visible object, current state, scope, and +result. If the interface does not show or clearly imply those things, the +action is premature. Do not expose a capability merely because the backend has +it; first design how the object enters the user's mental model. + +## Visual language + +### Hierarchy + +Prefer a small number of clear levels: + +- **window or page title** identifies the current object or workspace; +- **section title** separates meaningful regions; +- **body text** carries the work; +- **muted text** provides secondary metadata and help; +- **labels** identify controls and values. + +Use size, weight, spacing, and separators before adding color or containers. +Avoid nesting cards inside cards: most desktop regions need only a background, +a hairline boundary, and intentional spacing. + +Evaluate hierarchy across the whole feature, not component by component. Hide +accent color and decoration during review: the primary task, current selection, +result summary, and next action should still be obvious from structure. A +screen that contains individually plausible controls can still fail when they +do not form one reading order and one decision path. + +Treat emphasis as a limited budget. A local surface needs one clear focal point, +not a field of competing highlights. If everything is colored, badged, bold, +boxed, or promoted to an alert, nothing reads as important. Establish priority +with structure and proximity first; spend stronger color and components only +where a distinction changes what the user notices or does. + +### Color and themes + +Read colors from `cx.theme()` and use them by semantic role: + +- `background` and `foreground` for the main surface and text; +- `group_box`, `popover`, `sidebar`, and their foreground tokens for their + named surfaces; +- `muted` and `muted_foreground` for supporting information; +- `primary` for the principal action or selection emphasis; +- `danger`, `warning`, `success`, and `info` only for their meanings; +- `border`, `input`, and focus-ring tokens for structure and interaction. + +Do not use a semantic status color as decoration. Do not encode meaning by +color alone. Verify every custom surface in light and dark themes and with +custom theme values; never assume that foreground is black or background is +white. + +Use Badge for a short state, count, or classification that benefits from rapid +scanning—not for every label, metadata value, filter, or section title. Keep +most badges neutral; reserve semantic variants for states that truly carry +success, warning, danger, or informational meaning. A row of multicolored +badges is usually a missing hierarchy or grouping decision. + +Application UI should not contain raw hex, `rgb`/`rgba`, or `hsla` colors. +Resolve colors from `cx.theme()` by semantic role. If the required role does +not exist, define it in the product's theme/token layer rather than embedding a +palette value at the call site. Raw colors belong only inside theme definitions +or in audited data/raster content whose color is itself the data. + +### Radius, spacing, and density + +Use a finite radius scale, as with type and control sizes. Choose from the +theme's named tiers (`sm`, `md`, `lg`, `xl`, and larger surface tiers) instead +of inventing a radius for each element. Large containers may use a softer tier +than the controls they contain; compact controls and overlays use tighter +tiers. For an ordinary rectangle, the radius should remain visibly smaller +than half its shorter side. Use `radius_full()` only for intentional circles +and pills; a tooltip must not become a capsule by accident. + +Nested surfaces should read as concentric shapes. Use +`inner radius ≈ max(0, outer radius − gap)` to choose the closest suitable +theme tier; the visible band should remain even through the turn. Add a +named application token if a repeated composition needs an intermediate +value. A segmented control is one silhouette: its end segments follow the container's inner +corners, its middle segments remain square, and each boundary has one divider +of consistent thickness. + +Light theme chat composer: the inset surface, both buttons, and placeholder spacing change together; red guide circles show their shared corner center. +Dark theme chat composer: the inset surface, both buttons, and placeholder spacing change together; red guide circles show their shared corner center. + +A radius must govern the entire visible surface, not just its border. Exposed +corners remain transparent to the surface behind them; child backgrounds, +selection fills, and scrolling content must also follow the boundary. In GPUI, +do not assume a rounded parent clips its descendants. Give inner surfaces their +own radius or an appropriate clip, then inspect the composition against a +contrasting background in both themes. Preserve outward focus rings when +choosing a clip. + +Use a compact spacing scale and repeat it. Related label/control pairs should +be closer than separate groups; separate groups should be closer than separate +sections. Prefer component sizes (`xsmall`, `small`, default medium, `large`) +over one-off heights. Use compact variants for toolbars and data-dense screens, +not to squeeze an unclear layout into less space. + +The shared semantic scale is intentionally small: spacing progresses through +roughly 2, 4, 8, 12, 16, 24, and 32 pixels, while typography stays near 12, +14, 16, 18, and 20 pixels. Treat these as relationships rather than permission +to scatter their current values through feature code. GPUI Component currently +projects a fixed default `SpacingTokens` scale from its global `Theme`; unlike +colors and radii, `Theme::apply_semantic_tokens` does not persist a custom +spacing scale. An application that needs different spacing must own that full +token snapshot and use it consistently in its application components. + +### Spatial grammar + +Spacing expresses relationship. Choose a gap from the semantic scale by asking +what the two things mean to each other: + +| Relationship | Typical token | Current scale | Examples | +| --- | --- | --- | --- | +| Optical correction | `xxs` | 2 px | icon baseline, compact separator | +| Parts of one control | `xs` | 4 px | menu icon/label, title/description | +| Closely related controls | `sm` | 8 px | button icon/label, dialog actions | +| One content group | `md` | 12 px | notification columns, compact form rows | +| Separate groups in one section | `lg` | 16 px | panel padding, form groups | +| Separate sections | `xl` | 24 px | major blocks in a page or inspector | +| Major region boundary | `xxl` | 32 px | empty-state breathing room, page bands | + +These values describe the current default scale, not literals to repeat. Use +`cx.theme().spacing_tokens()` or the corresponding GPUI scale helpers for the +ecosystem default. A product-owned scale should preserve the ordering and +relationships and must be passed through the application's own design-system +context rather than assumed to persist in the global GPUI Component theme. + +Use these rules when resolving horizontal and vertical space: + +1. **Inside before outside.** A component's padding belongs to the component; + the gap between components belongs to their parent. +2. **Vertical rhythm shows grouping.** The gap between a title and its + description is smaller than the gap from that description to the next + section. Equal gaps imply equal relationships. +3. **Horizontal space supports scanning.** Repeated rows keep icons, labels, + values, badges, and trailing actions on stable columns. +4. **Leading and trailing are semantic.** Think in reading-order edges even + when the current API uses left/right; this keeps future RTL adaptation + possible. +5. **Do not double padding.** A card placed in an already padded panel should + not automatically add another full panel inset. +6. **Use optical alignment sparingly.** A one- or two-pixel correction is valid + for icon or glyph geometry, but document why it differs from the scale. + +Common compositions in the current system illustrate the relationships: + +- button contents use 4 px at small sizes and 8 px at normal sizes; +- dialog headers and footers use an 8 px internal gap; +- compact list and menu rows use 4 px vertical and 8–12 px horizontal padding; +- sheet headers use about 16 px leading and 12 px trailing space, leaving room + for a close affordance, while footers use 16 px horizontal and 12 px vertical + space; +- notifications use 16 px horizontal padding and a 12 px column gap because + icon, message, and action are distinct groups. + +Do not treat these as copy-and-paste recipes for every surface. They reveal the +system: controls are tighter internally, rows optimize scanning, and +containers spend more space at their boundary than between their contents. + +### Proportion and layer hierarchy + +Start with content requirements, then set proportions. Avoid arbitrary halves +when one pane has a clearly different role. + +- A navigation sidebar should be wide enough for stable labels but visibly + subordinate to the work area. Give it a minimum, preferred, and maximum + width rather than a percentage alone. +- In master–detail layouts, let the collection remain scannable and give the + detail pane the surplus. A roughly one-third/two-thirds starting point is + often useful, but content constraints are authoritative. +- Inspectors and auxiliary sheets should not cover the primary object by + default. They should be resizable or dismissible when their content grows. +- Dialog width comes from the decision: short confirmation, medium form, or a + dedicated window/page for complex work. Do not enlarge a dialog simply to + create whitespace. +- Reserve the strongest elevation for the topmost decision layer. Within a + layer, use background and hairlines—not successively larger shadows—to show + hierarchy. + +Define three size constraints for every major region: the minimum at which its +task still works, a comfortable default, and how it consumes surplus. Persist +user-controlled splits when they represent workflow preference, and clamp +restored values against the current window. + +### Alignment details + +Alignment is a structural system, not a final polish pass. Establish a small +set of alignment spines for each surface: shared leading and trailing edges, +text baselines, center lines, and fixed functional lanes. Elements at the same +level should attach to the same spine from top to bottom or leading to trailing, +even when they are different component types. + +Alignment spines across a desktop surface +Alignment spines across a desktop surface + +Vertical red lines sit beside shared edges or control centers for content, +status, time, and trailing actions. Horizontal lines sit beneath text baselines +or pass through a row center to show bottom and vertical-center alignment. The +compact comparison isolates a one-rendered-pixel drift that must be corrected +at its structural owner. + +- Give sibling regions a shared content inset. A heading, toolbar, list row, + empty state, and footer that describe the same level should not each invent a + slightly different leading edge. +- Repeat column geometry through the whole region. Headers, rows, summaries, + loading states, and inline editors should reserve the same lanes for identity, + metadata, status, numbers, and actions. +- Align related controls across rows and sections. Form labels, fields, + descriptions, and validation messages should reveal a stable vertical grid + when the page is scanned from top to bottom. +- Keep horizontal bands coherent. Items sharing a toolbar, title bar, status + bar, or row should use one baseline or center line instead of individually + tuned offsets. +- Introduce indentation only for real hierarchy, containment, or disclosure. + Decorative indentation makes siblings look subordinate and breaks the + surface's reading line. +- When a nested level ends, return exactly to the parent spine. Do not let + accumulated padding drift across nested containers. +- Preserve the spine through optional content. Missing icons, badges, + descriptions, or trailing actions must not move the remaining labels; use + intentional slots or lanes when cross-row comparison matters. +- Align major regions with one another where their hierarchy matches. Sidebar + headers, content titles, split panes, toolbars, and bottom bars need not share + every coordinate, but coincident levels should form visible continuous lines. + +Not every edge should align. A child can indent, a primary value can lead its +supporting metadata, and a destructive decision can gain separation. Such +exceptions must communicate hierarchy or meaning; they must not result from +uncoordinated component padding. Start with the shared spine, then make the +exception explicit. + +Treat exact alignment and repeated gaps as quality invariants. When two edges +or spaces are intended to be equal, a one-rendered-pixel difference is a defect, +not an acceptable optical approximation. Inspect resolved bounds with a +measurement tool at representative window sizes, zoom levels, and display scale +factors. Compare coordinates and distances; do not approve alignment only from +a casual screenshot. + +The rendered-pixel tolerance is a verification rule, not permission to patch +the code with raw pixel offsets. Equal relationships should resolve from the +same `rem` helper, spacing token, grid definition, or shared component inset. +Fix the common owner when they differ. Account for fractional layout and device +rounding so intended spines land on the same physical pixel instead of drifting +at particular zoom levels. + +- Align text by baselines, not by bounding-box centers, when mixed sizes share + a row. +- Center icons in a fixed slot so labels do not move when icons differ in + intrinsic width. +- Right-align comparable numbers; left-align prose and identifiers unless the + locale requires otherwise. +- Keep trailing row actions and disclosure indicators in fixed-width lanes. +- Align form controls by their interactive frame, not by help text below them. +- Use `justify_between` only when the two sides truly own opposite edges; it + should not disguise missing structure in the middle. +- Hairlines belong on the boundary owner. Two adjacent regions must not each + draw the same separator. +- A scrollbar belongs to the region that scrolls and sits against that panel, + editor, or window's trailing edge. Content padding may inset text and rows; + it must not pull the scrollbar into the middle of the surface. Reserve a + deliberate scrollbar gutter when content needs clearance. + +### Density tiers + +Medium is the ecosystem default. Change density for the whole local context, +not one isolated control: + +- **comfortable / large:** onboarding, sparse forms, prominent decisions; +- **standard / medium:** most application chrome and workflows; +- **compact / small:** toolbars, menus, tables, and repeated professional data; +- **extra compact / xsmall:** exceptional high-density utilities, never the + automatic choice for an entire application. + +The current controls demonstrate a bounded scale rather than arbitrary sizing: +buttons commonly move through approximately 20, 24, and 32 px frames; input +and data controls may extend to about 44 px at large size; table rows use about +26, 30, 32, and 40 px. Use the component's `Size` API so typography, icon, +padding, and hit target change together. A custom height that changes only the +outer box is usually incomplete. + +### Zoom, base font, and `rem` + +A well-designed `rem` system preserves hierarchy while the interface zooms. +Zoom is successful when the relationship between title and body, control and +icon, inner and outer spacing, primary and secondary regions still feels the +same at every scale—not merely when every object becomes larger. + +GPUI Component adopts the relative-scale idea familiar from Tailwind. The +theme's base `font_size` becomes the window's `rem` through `Root`, and GPUI +scale helpers such as `text_sm()`, `gap_2()`, `p_4()`, `h_8()`, and `size_4()` +resolve against it. This gives typography, spacing, controls, and icons one +shared zoom axis. + +Design in ratios: + +- type steps keep the same hierarchy around the base body size; +- spacing steps keep the same grouping relationships around the type; +- control frames, icons, and hit targets scale with their labels; +- pane minima and comfortable widths account for the scaled content; +- corner radii and focus treatment remain optically consistent with the + control frame. + +Do not implement zoom by changing text size alone. A larger label inside a +fixed-height button, a larger document inside fixed pane minima, or larger rows +inside a stale virtual-list measurement destroys the original rhythm and can +clip content. Conversely, multiplying every physical pixel—including +hairlines—can make the interface visually heavy. + +As a rule, application layout should not call `px(...)` directly. Use GPUI's +rem-based scale helpers (`p_2`, `gap_3`, `w_64`, `text_sm`, and related +builders) or semantic component sizes. Use fixed pixels only when the value +represents a physical or raster boundary: +a one-device-pixel hairline, platform window inset, bitmap dimension, minimum +hit-test tolerance, or geometry that must match an external surface. These are +audited, documented exceptions. Product spacing, typography, icon size, and +ordinary control geometry stay on the relative scale. + +Test interface zoom at several base-font values, not just the default. Verify +hierarchy, wrapping, truncation, minimum window size, pane resizing, focus-ring +clearance, popup placement, and virtualized row measurement. Also distinguish +interface zoom from Dock's panel zoom: Dock zoom makes one container fill its +area while retaining its chrome; it does not change `rem` or application scale. + +### Surfaces and elevation + +Use elevation to explain stacking, not importance. The base window surface is +flat; separators and background contrast define its regions. Popovers, menus, +dialogs, and notifications may use progressively stronger shadows because they +sit above other content. Do not put a shadow on every card. + +All surfaces of the same kind should share one treatment. GPUI Component, for +example, deliberately gives popup families one themed popover surface so +Popover, Select, Combobox, DatePicker, and menus do not drift apart. When an +application invents another anchored surface, reuse that semantic treatment +instead of approximating it with unrelated border and shadow literals. + +### Typography and icons + +Use the platform UI font for interface text and monospace only for code, +identifiers, shortcuts, and aligned numeric data. Keep body text readable and +avoid excessive uppercase or letter spacing, especially for CJK text. + +Use one icon family in a product. Icons supplement labels; they should not +replace unfamiliar actions with guesswork. Icon-only buttons always need an +accessible name. Their visual explanation follows the tooltip policy below. +Use filled or colored icons to communicate a state, not merely to make a +toolbar lively. + +## Layout patterns + +### Choose a stable shell + +Most applications should use one of these shells: + +- **single workspace:** toolbar or title bar above one primary view; +- **sidebar workspace:** persistent navigation beside a changing detail view; +- **master–detail:** resizable collection and detail panes; +- **document workspace:** tabs or a dock area for multiple long-lived objects; +- **utility window:** one focused task with a short, fixed action path. + +Keep global navigation stable while content changes. Give the primary work area +the remaining space with `flex_1()` and `min_w_0()` / `min_h_0()` where +overflowing children must shrink. Use `Scrollable`, `VirtualList`, `Table`, or +`DockArea` for their intended behavior instead of rebuilding scrolling or pane +management from nested `div`s. + +The title bar is window chrome first. Preserve its drag region and avoid +binding ordinary title clicks to infrequent editing commands. Rename belongs +behind an explicit object command, with a keyboard path where appropriate. + +### Responsive desktop windows + +Desktop does not mean fixed-size. Decide what happens as a window narrows: + +1. preserve the primary task; +2. allow resizable regions to reach a documented minimum; +3. collapse secondary labels or inspectors; +4. move low-frequency actions into a menu; +5. scroll only the region whose content actually overflows. + +Do not hide an action without providing another path to it. Avoid making the +entire window scroll when only a list or document body should scroll. + +GPUI flex layouts have the same intrinsic-size pressure found in other layout +systems: a `flex_1()` child may still refuse to shrink around long content. +Design and implementation must agree on which panes may shrink, truncate, wrap, +or scroll. A clipped region also clips an outward focus ring; never trade away +keyboard visibility merely to simplify overflow. + +### Forms and settings + +Use a visible label for each field and place help or validation next to the +field it describes. Align related fields, but do not force long labels into a +narrow fixed column. Use the appropriate control: `Checkbox` for independent +choices, `RadioGroup` for a small visible set, `Select` for a longer set, and +`Switch` for a setting that takes effect immediately. + +Disable submission while an operation is in flight, keep the user's input, and +show the result near the action. Reserve dialogs for short, focused decisions; +use a full page or sheet for workflows that need exploration or many fields. + +A loading treatment must represent actual waiting. Switching between settings +panels whose content is already available should be immediate; a skeleton is +appropriate only while content is genuinely loading and its shape is known. + +## Components and composition + +Follow the Shadcn principle that components are building material rather than a +sealed design system. GPUI Component supplies coherent defaults, while the +application owns composition and product semantics. + +- Use component variants by meaning. Primary is reserved for the explicit + default commit in a decision area—normally the action invoked by Enter. A + lone, frequent, or desirable action is not automatically primary. An `Add` + command in a management toolbar normally uses a default Button; a form's + default `Create` commit may use primary. Use `danger` for destructive + commitment and `ghost` for quiet toolbar actions. +- Prefer explicit compound parts and render callbacks over styling arbitrary + descendants. +- Keep a repeated pattern consistent across the product. Wrap it in an + application component when it carries domain language or policy. +- Use the standard component for its semantic role. A menu, dropdown menu, + popover, select, and command palette are not interchangeable boxes; each owns + different selection, focus, keyboard, dismissal, and layout contracts. +- Preserve the component family's geometry. Menu rows share vertical and + horizontal padding, height, icon and checkmark slots, separators, radius, and + state treatment. Do not imitate one menu with a custom popup whose spacing + only approximates the system. +- Do not wrap a library component merely to rename every method or freeze all + of its capabilities. +- Move reusable behavior without product styling to `gpui-base`; keep themed, + opinionated presentation in GPUI Component or the application. + +## Interaction states + +### Make the result understandable before the click + +A control should predict its result. Use familiar desktop controls and +placement so people can act without learning the interface first. Its label +names the action and object, its state shows availability, and its feedback +confirms the same outcome. + +Do not label a Button `Save` if it opens a configuration flow, or `Delete` if +it only removes an item from a group. Name the scope when context does not make +it clear. Respond immediately to activation, prevent duplicate submission +during longer work, and show the result near the object that changed. Add a +success message only when the result itself is not visible. + +Every interactive control should be designed for: + +| State | Design requirement | +| --- | --- | +| Rest | Clear affordance without visual noise | +| Hover | Subtle pointer feedback, never the only cue | +| Pressed | Immediate press feedback | +| Open / pressed | Persistent feedback while an attached popup is open | +| Focus visible | High-contrast keyboard focus ring | +| Selected / checked | Persistent state distinct from hover | +| Disabled | Lower emphasis and no misleading hover/pressed response | +| Loading | Preserve context, prevent duplicate action, explain long waits | +| Error | State what happened and how to recover | + +Use GPUI's focus system and Actions for commands that should work from the +keyboard. Match familiar desktop shortcuts, expose shortcuts in menus or +tooltips, and keep focus in a logical place after opening or dismissing an +overlay. + +Selection is part of the information model, not optional polish. Tabs, +segmented choices, selectable rows, filters, and navigation destinations must +show a persistent selected state. A Button that owns a dropdown must remain +visibly pressed or open until the popup closes; hover alone cannot explain the +relationship between trigger and surface. + +In a segmented control, the selected fill and unselected surface must share +the container's silhouette. Neither may square off an end corner or make a +boundary appear heavier than its peers. + +Show a selected navigation item, list row, or tab through the item's own +surface: a selected fill, stronger foreground, or heavier weight. Do not add a +leading-edge bar or one-sided border as the selection marker. It is a web +template habit, not a desktop convention; it breaks the item's rounded +silhouette and adds a second, competing edge to a column that already aligns +on its text. + +For destructive actions, distinguish between reversible and irreversible work. +Prefer undo or a temporary notification for reversible changes. Use an +`AlertDialog` when the consequence is serious and cannot be undone; name the +specific object and consequence in the confirmation copy. + +### Pointer conventions + +Use the default arrow cursor for buttons, checkboxes, menu items, tabs, and +other native controls. Use a pointing hand for links and content that behaves +as a link. Use text, resize, grab, and prohibited cursors only when they +describe the active manipulation. A cursor reinforces an affordance; it does +not replace the control's visible state or accessible role. + +Keep hover effects modest because keyboard and accessibility interaction has no +hover. Do not reveal the only copy of a destructive or essential action on +hover. Contextual row actions may become quieter at rest if the same commands +remain available through selection, keyboard, or a context menu. + +### Prefer desktop command surfaces over hover toolbars + +Use command frequency and scope to choose where an action lives: + +- keep the primary or frequent action visible as a labeled Button or familiar + toolbar control; +- put secondary actions for the current region behind a visible + `DropdownMenu` trigger; +- put commands that act on the object under the pointer in a `ContextMenu`; +- expose the same important command through an Action/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 a + tooltip when meaning or scope is unclear. + +Do not assign primary because a Button is the only action on screen, because it +is placed at the top right, or because the team wants more clicks. Primary +communicates default commitment and keyboard behavior. If activation is merely +an ordinary command such as adding an item, opening a tool, or refreshing a +view, use a default, outline, or ghost Button according to its local hierarchy. + +Use an underlined Link only for an external resource target: a URL, web page, +online documentation, or email address. It uses the pointing-hand cursor +because its contract is leaving the current application context for that +resource. Do not use Link styling to make a functional command look quiet. A +link-shaped Delete, Save, Refresh, Add, Open-menu, or in-app navigation action +hides the control's affordance and exposes the wrong accessibility role. + +“View” does not make an in-app destination a Link. A full report, analysis, +details panel, or local record still opens through a Button, row, card, tab, or +disclosure control. Use concise context-aware labels such as `Full analysis` +when the containing card already establishes what opens; reserve underlining +for a resource that actually opens in a browser or mail client. + +All internal navigation—sidebar rows, tabs, breadcrumbs, list items, opening a +local view, or switching workspaces—must use the corresponding native component +or a Button/Action. Visual emphasis is chosen through Button variant or the +navigation component's selected state, never by lying about semantics. + +## Feedback and overlays + +Choose the smallest surface that fits the decision: + +- tooltip: a short explanation or shortcut; +- popover: contextual controls that do not interrupt the task; +- menu: a compact list of actions; +- notification: asynchronous status that does not require a decision; +- dialog: a focused decision or short form; +- alert dialog: explicit confirmation of a consequential action; +- sheet: supplementary work that benefits from more persistent space. + +A tooltip supplies missing meaning; it does not restate an obvious action. +Reserve it for an unfamiliar symbol, ambiguous scope, or a useful shortcut +that is otherwise hidden. Repeated, recognizable commands such as Copy and +Download in a message footer need accessible names and keyboard access, but +their hover labels add interruption rather than clarity. + +An Alert interrupts the visual hierarchy even when it does not open a modal. +Use it for important, exceptional information that needs attention in the +current task, not as a decorated container for ordinary descriptions, tips, or +empty space. Prefer inline help, muted text, or a normal section when the +content does not require immediate notice or action. + +Avoid stacking overlays. Escape should dismiss the topmost dismissible layer, +and focus should return to the trigger or the next logical target. + +An overlay action must refer to an object or state the overlay actually shows. +For example, expose `Clear history` only when a distinct recent-history section +is visible and contains entries. Search results, recent items, and favorites +are different collections; label and separate them instead of merging them +into one unexplained list. Hide an inapplicable action or disable it with a +useful reason—do not park an ambiguous trash icon in a footer. + +Footer space is not a catch-all for capabilities that lacked a place in the +design. A footer may present shortcuts, status, or actions that apply to the +whole surface, but each item must answer: what is its object, why is it +available now, what scope does it affect, and what visible state changes after +activation? + +## Motion + +Motion explains change; it is not ambient decoration. Use short transitions for +appearance, dismissal, expansion, and spatial continuity. Avoid animating large +layout changes when opacity or transform communicates the same relationship. +Honor reduced-motion preferences, never require animation to understand state, +and do not add a default animation to every component. + +Motion policy belongs to the styled or application layer. Base may own the +lifecycle mechanism or geometry needed for a transition, but it should not +decide that every product fades or slides. Give independently animated values +stable identity, and make interruption reverse smoothly from the currently +sampled value rather than restarting from an old endpoint. + +Motion follows the continuity of the interaction. While a pointer or scroll +position moves among related targets, keep the preview surface stable and +update its content or position in place. Dismiss it only after the interaction +ends; repeated fade-out and re-entry on each update reads as flicker. + +## Designing data-heavy interfaces + +Dense does not mean cramped. In tables, trees, command palettes, editors, and +docks: + +- keep headers and primary row identity visually stable; +- align comparable values and use tabular numerals where appropriate; +- distinguish focus, hover, active row, and multi-selection; +- keep sorting and filtering visible and reversible; +- preserve selection by domain identity across filtering and reordering; +- virtualize large collections without changing keyboard semantics; +- use progressive disclosure for secondary columns and inspectors; +- provide a useful empty state that explains the next action. + +Choose a table for comparison across consistent fields, a list for scanning +heterogeneous items, a tree for real hierarchy, and a dock only when users need +to arrange long-lived tools or documents. Do not use a complex data component +as a visual style. + +## Interface language + +Words are part of the interface architecture. Write the vocabulary for a +feature as a system—destinations, objects, commands, states, and outcomes—not as +isolated translations of implementation features. Prefer the shortest wording +that remains accurate in its actual context. + +### Let context carry context + +Do not repeat information that the surrounding surface already establishes. A +sidebar destination is usually the object or domain itself: use `Users`, not +`User Management`; `Shortcuts`, not `Shortcut Configuration Management`. A +column whose rows already contain actions can omit a generic `Operation` +heading. A dialog titled `Delete “Roadmap”?` does not need body text that asks +the same question again. + +This is context economy, not deletion for its own sake. Add text when it changes +the decision: identify the affected scope, an irreversible consequence, an +unusual prerequisite, or a way to recover. Every extra word should answer a +question the current layout does not already answer. + +Use nouns for destinations and objects (`Users`, `Appearance`, `Orders`), verbs +for commands (`Save`, `Duplicate`, `Export`), and adjectives or short phrases +for states (`Offline`, `Up to date`, `Pending review`). Avoid wrappers such as +`Management`, `Module`, `Page`, `Function`, `Operation`, and `System` unless the +word distinguishes a real domain concept. + +### Write each language, do not translate its shape + +Start from shared intent, hierarchy, and terminology, then compose each locale +as natural interface language. Do not preserve the source language's word +order, number of words, politeness filler, or grammatical category. English +`Users` can express a Chinese feature concept that would literally expand to +“user management”; fidelity means preserving purpose, not preserving tokens. + +Remove words supplied by the enclosing information architecture. Inside a +`Settings` surface, a destination is often simply `Account`, not `Account +Settings` and never the unnatural singular `Account Setting`. The correct +English label is chosen from its role and neighbors, not from the standalone +source phrase. + +Maintain a small product lexicon for recurring objects, commands, and states. +Use the same term in the toolbar, menu, context menu, dialog, shortcut search, +and documentation unless the context genuinely changes its meaning. Review +copy in the rendered surface: neighboring labels often reveal repetition or +inconsistent scope that a locale file cannot. + +In localized technical writing, preserve an established framework term when a +translation would be less precise. Keep API identifiers in their original form +and format them as code. Do not retain ordinary foreign words merely to sound +technical. Explain a retained term on first use when needed, then use the same +form throughout the interface, documentation, and API examples. + +### Buttons and confirmation dialogs + +Button labels are short by default—usually one or two words—and describe the +result, not the gesture or the component. Prefer `Save`, `Move`, or `Delete` to +`Click to save`, `Perform move`, or `Confirm deletion`. Use `Cancel` consistently +for the action that leaves without committing. Reserve `OK` for acknowledging +purely informational content. + +Short is a default, not a character limit. A deliberately longer label is +better when its words expose a consequence or distinguish choices that users +could otherwise confuse, for example `Delete from this group` versus `Delete +everywhere`, or `Restart without saving`. Length must buy decision-critical +information; it must not restate the dialog title or body. + +Use the most specific concise result as the confirmation label when possible: + +| Context | Weak | Prefer | +| --- | --- | --- | +| Delete dialog | `Yes`, `Sure`, `Confirm deletion` | `Delete` | +| Unsaved changes | `Confirm`, `Yes` | `Discard changes` | +| Pure acknowledgement | `Confirm operation` | `OK` or `Done` | +| Complex consent whose result has no clear verb | `Yes` | `Confirm` | + +`Confirm` is a useful fallback when the surrounding dialog fully names a +complex commitment and no shorter result verb is accurate. It should not +replace a clear command. `Sure` is conversational rather than a stable English +command and is too ambiguous for the standard vocabulary. + +A confirmation dialog should form one compact decision: + +- title: the decision or condition, such as `Delete “Roadmap”?`; +- body: only new scope, consequence, or recovery information; +- actions: `Cancel` and the result, such as `Delete`; +- destructive styling: applied to the destructive result, not substituted for + precise wording. + +Avoid generic titles such as `Notice`, `Warning`, `Error`, and `Confirmation` +when the actual condition can be named. Avoid ritual phrases such as “Are you +sure you want to…”, “Would you like to…”, “Please note that…”, and “successfully” +when the structure or state already communicates them. Courtesy should come +from a calm, respectful tone, not repeated `please`. + +### Capitalization, punctuation, and symbols + +Use sentence case for English UI by default: `Reset layout`, not `Reset Layout` +or `RESET LAYOUT`. Preserve proper nouns and established acronyms. Follow a +platform convention such as title case for native menu commands only when the +platform integration benefits from it, and apply that convention consistently +within the component class. + +ALL CAPS can provide restrained typographic emphasis for very short section +labels, eyebrows, statuses, established acronyms, and code-like identifiers. +Its compact shape and measured tracking can form a level similar to bold type, +but it does not belong on Buttons, long headings, sentences, or dense lists. Do +not combine uppercase, strong color, and bold weight in the same region, and do +not transform every string automatically: product names, acronyms, and localized +content must preserve their intended casing. + +Labels, buttons, menu items, tabs, headings, placeholders, and short states do +not take a final period. Complete explanatory, warning, and error sentences do. +Avoid exclamation marks in routine success and failure messages. In Chinese, +use full-width punctuation in sentences and omit terminal punctuation from +short control labels by the same semantic rule. + +Use the single ellipsis character (`…`), not three periods. Append it to every +Button or MenuItem that opens a dialog, sheet, or separate window, and to a +command that requires more input or choices before it can complete, such as +`Settings…` or `Export…`. An immediately executed command does not take an +ellipsis. Use an indeterminate progress indicator, not decorative dots, to +communicate ongoing work. + +Errors should say what happened and, when useful, the next recovery action. +Success feedback should name the resulting state only when that state is not +already visible. Prefer `Couldn’t save. Check your connection and try again.` +to a technical code or a long apology; omit a `Saved successfully` toast when +the document visibly becomes saved. + +## Internationalization and platform fit + +Copy must survive expansion, CJK typography, and different shortcut notation. +Do not size a control from one English label. Keep text out of raster assets, +avoid concatenating translated fragments, and let labels wrap or truncate only +where the product defines a recovery path such as a tooltip. + +Respect platform differences that carry meaning: Command versus Control, +native window decorations, system appearance, scrollbar behavior, menus, and +notification capabilities. Keep the product's information architecture stable +across platforms, but do not erase familiar platform behavior for superficial +pixel equality. + +## Guidance for AI-generated interfaces + +An AI changing a GPUI interface should first inspect the nearest feature, +theme tokens, and component documentation. It should state the primary task, +state owner, component composition, and keyboard path before generating code. +It must not infer an API from React/Shadcn examples or invent a GPUI method +because the name seems plausible. + +AI output is incomplete until a human can explain why the hierarchy, density, +component choice, and exceptional literal values belong in this product. A +visually plausible screenshot is not proof: keyboard behavior, focus, dynamic +content, themes, resizing, and failure states are part of the design. + +## Accessibility checklist + +Before considering a screen complete, verify that: + +- every action is reachable and operable by keyboard; +- focus order follows visual and task order; +- focus remains visible and is restored after overlays; +- controls have accessible names; icon-only controls use tooltips when their + meaning, scope, or shortcut needs explanation; +- text and meaningful boundaries have sufficient contrast; +- status is not communicated by color alone; +- disabled and read-only states are distinguishable; +- labels, errors, and descriptions remain near their controls; +- content remains usable with longer translations and larger text; +- pointer targets are comfortably sized even in a dense layout. + +## Design review checklist + +A review does not inventory components; it judges whether the interface made +the right decisions. Ask, in order: + +1. **Is the task clear?** Can a new user recognize the purpose, primary action, + and next step without learning, guessing, or experimenting? +2. **Does every action keep its promise?** Do the label, control, state, scope, + feedback, and result describe one consistent outcome? +3. **Is hierarchy decisive and restrained?** Does the core feature receive the + space it deserves while strong color, bold type, badges, alerts, and primary + Buttons remain scarce? +4. **Could the interface do less, better?** Can an entry point, option, or state + be removed, combined, or deferred without weakening the complete task? +5. **Is the structure exact?** Do peers share alignment spines, equal gaps stay + equal to the rendered pixel, and scrollbars sit at the edge of their actual + scrolling region? +6. **Does it follow the component system?** Do standard controls retain their + geometry, states, keyboard behavior, and dismissal model, with appearance + supplied by theme and scale tokens? +7. **Does it remain usable in every state and constraint?** Verify keyboard and + focus behavior, empty/loading/failure/permission states, longer translations, + zoom, minimum window size, and reduced motion. +8. **Has it been tested in a real window?** Complete the task with real + components, copy, and representative content—not only an ideal screenshot. + +Continue with [Coding Guides](./coding-guides.md) to translate these design +decisions into GPUI architecture and code. diff --git a/.agents/skills/gpui-kit/SKILL.md b/.agents/skills/gpui-kit/SKILL.md new file mode 100644 index 0000000..a67265b --- /dev/null +++ b/.agents/skills/gpui-kit/SKILL.md @@ -0,0 +1,277 @@ +--- +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`, 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`, render vs update | +| Choose the right unit | Deciding `RenderOnce` vs `Entity` vs custom `Element` | +| State ownership | Where a piece of state lives, who mutates it, `Entity` 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::*; + +gpui_kit::application() + .with_assets(gpui_kit::assets::Assets) + .run(|cx| { + gpui_kit::init(cx); // first, before anything else + gpui_kit::open_window(options, cx, |window, cx| { + cx.new(|cx| AppView::new(window, cx)) + }) + .expect("failed to open window"); + }); +``` + +- **Stateless** (`RenderOnce`): build in `render`: + `Button::new("save").primary().label("Save").on_click(|_, _, _| {})` +- **Stateful**: hold `Entity` 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 | +| `SpeechButton` | `speech::{SpeechButton, SpeechState}` | Stateful. Dictation, `speech` feature | +| `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. | +| `TimeField` | `time_field::{TimeField, TimeFieldState}` | Stateful. Time of day, 24/12-hour | +| `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`, `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`, `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) diff --git a/.agents/skills/gpui-kit/references/coding-guides.md b/.agents/skills/gpui-kit/references/coding-guides.md new file mode 100644 index 0000000..54ca777 --- /dev/null +++ b/.agents/skills/gpui-kit/references/coding-guides.md @@ -0,0 +1,911 @@ +--- +title: Coding Guides +description: Architecture and coding conventions for maintainable GPUI Kit applications +order: -13 +--- + +# 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 + +GPUI application architecture layers +GPUI application architecture layers + +Dependencies point downward. Higher layers own domain meaning and orchestration; +lower layers own reusable presentation or behavior. Do not make a reusable +component depend on an application screen, or make `gpui-base` depend on a +theme from GPUI Component. + +Use these boundaries: + +- **app shell:** compose windows and feature crates while keeping feature logic out; +- **feature crate:** keep one capability's model, services, views, commands, dialogs, + and workflow behind one public boundary; +- **app component:** a repeated domain-aware pattern; +- **gpui-component:** themed, general-purpose UI; +- **gpui-base:** reusable behavior and geometry without product presentation. + +### Organize large applications by capability + +In a large Rust application, a 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); + + gpui_kit::open_window(WindowOptions::default(), cx, |window, cx| { + cx.new(|cx| Workspace::new(window, cx)) + }) + .expect("failed to open window"); +}); +``` + +`Root` coordinates window-level component facilities such as overlays and +notifications. Do not create a separate root for each page inside one window. +It also coordinates modal focus restoration, focus traps, tooltip/menu layers, +and window-scoped text selection. Bypassing it can produce behavior that looks +correct at rest but fails when overlays nest or focus changes quickly. + +## Understand GPUI's phases and contexts + +GPUI is retained state with declarative rendering. An entity survives across +frames; the element tree returned by `render` is a fresh description of the +current frame. Keep that distinction explicit. + +- `Context` mutates the current entity, creates listeners tied to it, + emits its events, and notifies its observers. +- `App` gives access to application globals and entity reads/updates without + implying ownership by the rendered element. +- `Window` owns focus, actions, input dispatch, element-keyed state, + measurement, and animation-frame requests for that window. +- layout, prepaint, and paint are later phases; use their hooks only when + resolved geometry is genuinely required. + +Never retain `&mut Window`, `&mut App`, or `&mut Context<_>` beyond the call in +which it is provided. Retain typed handles—`Entity`, `WeakEntity`, +`FocusHandle`, scroll handles, or domain IDs—instead. + +## Choose the right unit + +### Use `RenderOnce` for value-like elements + +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` 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, +} + +impl SearchView { + fn new(window: &mut Window, cx: &mut Context) -> Self { + let query = cx.new(|cx| InputState::new(window, cx).placeholder("Search…")); + Self { query } + } +} +``` + +Do not turn every visual fragment into an entity. Entity boundaries have +lifecycle and coordination costs; use them where retained identity matters. + +### Elements, views, and behavior systems are different + +Do not force every component into one template. The ecosystem contains: + +- semantic elements such as Button, Checkbox, Link, and Tabs; +- compound behavior roots such as Dialog, Popover, Select, and Combobox; +- entity-backed systems such as Input, Table, Tree, Dock, and notifications; +- infrastructure such as positioning, virtualization, scrolling, focus traps, + motion, history, and measurement. + +An element may be internally complex and still be value-like to its caller. A +stateful system may expose render callbacks so applications own presentation +without reimplementing behavior. Choose the public seam from the behavior, +not from how many `div`s appear in its renderer. + +## State ownership + +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) -> 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. + +Edit the global GPUI Component theme through `Theme::update(cx, |theme| ...)`. +The theme keeps the same colors twice (`colors` as solid colors, `tokens` as +renderable backgrounds that may carry a gradient) and the Base layer holds a +projection for its scrollbars and resize handles; `update` brings all three +back in step after the closure and refreshes every window. An edit through +`Theme::global_mut(cx)` updates only the field you touched — a sidebar can then +paint its text from the new colors and its background from the old tokens — +so it owns the rest: derive `tokens` from `colors`, call `Theme::sync_base(cx)`, +refresh the windows. `Theme::change(...)` performs the projection as part of a +complete theme change. + +An outward focus ring needs physical room. An ancestor with +`overflow_hidden()` clips it. Prefer layouts that leave room; if a product must +clip heavily, use the theme's focus-ring policy and retain the focused border +instead of silently hiding all keyboard focus. + +### Base font is the application zoom control + +The Component Root plugin calls `window.set_rem_size(cx.theme().font_size)` from +its `prepare` hook before the Root surface is rendered. Therefore the theme's +base font is not only body typography; it is the reference length for the +application's rem-based design scale. This deliberately follows the useful part +of Tailwind's model: named type, spacing, and size steps share one relative base +instead of becoming unrelated pixel constants. + +Change zoom by updating the base font and refreshing the window: + +```rust +Theme::update(cx, |theme| theme.font_size = px(18.)); +``` + +The base font itself is a pixel value because it anchors the scale. Descendant +application UI should normally use relative helpers—`text_sm()`, `gap_2()`, +`px_3()`, `h_8()`, `size_4()`—so type, whitespace, controls, and icons respond +together. A custom component that combines rem-based text with fixed-pixel +padding or icon geometry must document why that part should not zoom. + +Treat every direct `px(...)` and raw color constructor in application UI as a +review finding. Accept it only for a documented physical/platform boundary, +measured runtime geometry, raster/data color, or the theme/token definition +itself. Convenience and matching a screenshot are not valid exceptions. + +Anything cached from resolved layout must include `window.rem_size()` in its +invalidation key, directly or through a revision that changes with it. This +includes wrapped row heights, text shaping/layout, virtual-list measurement, +popup and dialog geometry, icon sizing derived from text, and custom canvas +metrics. The Command component's variable-height rows are an ecosystem example: +they remeasure when rem changes because the same fixed width wraps differently +at a larger base font. + +Do not confuse this application zoom with Dock panel zoom. Dock zoom is a +stateful layout operation that makes one tab group fill the DockArea +while keeping the container chrome and the way back out. It must not modify the +window rem size. + +## Events, actions, and focus + +Use pointer callbacks for pointer-specific behavior. Use GPUI Actions for +commands that should support key bindings, menus, or dispatch from multiple +inputs. Keep action handlers close to the view that owns the command. + +Model one logical desktop command once. A toolbar Button, `DropdownMenu` item, +`ContextMenu` item, menu-bar item, and key binding should dispatch the same +Action or call the same owner method instead of copying five mutations. Derive +their label, icon, shortcut, and enabled state from one command policy where +practical, so the entry points cannot disagree. The menu owns navigation and +dismissal; the feature owner still owns whether the command is allowed and +what it does. + +Preserve semantic roles in the element choice. Use `Button` for commands even +when the desired treatment is quiet—select `outline`, `ghost`, or an icon +presentation instead of replacing it with `Link`. GPUI Kit applications +reserve `Link` for targets opened by a browser or mail client, such as a URL, +web document, or email address. Use the relevant navigation component for an +in-app destination and `Button`/`Action` for a command. This is a product +convention, not a limitation of `gpui_kit::base::Link`, whose `open_with` seam can +route a destination elsewhere. + +Only stop propagation when a nested interaction must prevent its parent from +handling the same event. Blanket propagation stops break menus, selection, +dragging, and window-level commands in ways that are difficult to diagnose. + +Bind keys before building the menu bar. `cx.set_menus` reads the keymap at +the moment it is called and bakes each item's shortcut into the native menu, +so a binding registered afterwards never shows next to its menu item and the +item does not react to the key. Call `cx.bind_keys` first, then +`cx.set_menus`; if the keymap changes later (a user keymap file, a locale +switch that rebuilds the menus), call `cx.set_menus` again. + +Make focus ownership explicit: + +- retain a `FocusHandle` in the entity that owns keyboard interaction; +- register key contexts and actions on the appropriate focused region; +- transfer focus when opening an overlay and restore it on dismissal; +- render a visible `focus_visible` state; +- do not request focus unconditionally from `render`. + +A tracked handle is a Tab stop only when it says so: build it with +`cx.focus_handle().tab_stop(true)` (or `.tab_index(n)`), because the element's +own `tab_index`/`tab_stop` settings do not apply to a handle passed to +`track_focus`. A stateless component may create that handle in `render` +through `window.use_keyed_state(id, cx, |_, cx| cx.focus_handle().tab_stop(true))`; +the keyed state survives re-renders, so the Tab order is stable — this is +what `Button` does. + +Attach a `key_context` and its `on_action` handlers to the same focused region. +Bindings are contextual: a registered Action without the intended focus path +is not a working keyboard interaction. Composite widgets should implement the +complete navigation model—arrow movement, Home/End or page movement where +appropriate, confirmation, cancellation, and Tab behavior—rather than a few +isolated shortcuts. + +Modal surfaces must trap focus and restore the previous valid focus target on +dismissal. Nested overlays dismiss from the top. Handle rapid close/open +sequences without restoring focus through an intermediate, already-closing +surface. + +## Async work and side effects + +Start 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 | `State` | `InputState`, `TableState` | +| Imperative shared reference | `Handle` | `DialogHandle`, scroll handle | +| Semantic notification | `Event` | `TableEvent`, `SelectEvent` | +| Keyboard command | verb or intent noun | `Confirm`, `Cancel`, `SelectNext` | +| Pluggable data/behavior owner | `Delegate` / `Provider` | `TableDelegate`, `CompletionProvider` | +| Application-supplied presentation | `render_` or `_renderer` | `render_item` | +| Construction | `new`, or a semantic constructor | `new`, `horizontal`, `vertical` | +| Fluent property | noun/adjective | `label`, `disabled`, `selected`, `placement` | +| General non-boolean replacement builder | `with_` | `with_size`, `with_mode` | +| In-place mutation | `set_` | `set_items`, `set_selected_index` | +| Boolean reader | `is_` / `has_` | `is_open`, `is_closable`, `has_selection` | +| Plain value reader | field noun | `placement`, `selected_value` | +| Callback registration | `on_` | `on_click`, `on_open_change` | +| Rendering a named region | `render_` | `render_toolbar`, `render_content` | + +For new APIs, fluent builders omit `set_` because they consume and return +`Self`; mutation through `&mut self` uses `set_`. Preserve established public +names when changing them would cause needless churn. Existing builder names +such as `set_position` are compatibility exceptions, not patterns for new APIs. + +A boolean reader is either `has_`, when the value holds something, or +`is_`, when it describes a state or a permission. Reach for the +adjective whenever the action has one: `is_closable` over `can_close`, +`is_zoomable` over `can_zoom`, `is_copyable` over `can_copy`. When the action +is a verb phrase with no adjective form, name the thing it needs instead: +`has_definition`, not `can_go_to_definition`. Do not add new `can_` readers. + +Boolean builders may use the field name (`disabled(bool)`) while their readers +use `is_disabled()`. For a public seam struct containing non-boolean fields, +use `with_item_ix(...)` for construction and `item_ix()` for reading so setter +and getter names never collide. Prefer `_ix` for new local or internal +zero-based indices, preserve established public terms such as `selected_index`, +and do not introduce `_idx`. If callers never construct the seam value, do not +publish a builder merely for symmetry. + +### Let the enclosing name carry the context + +A name is read inside something. A field is read inside its type and a parameter +inside its method, so neither repeats what encloses it: `with_item_ix(ix)`, not +`with_item_ix(item_ix)`. + +Keep one type's fields at the same level of abbreviation. A single field spelled +out in full becomes the odd one out, and a reader goes looking for the +distinction that made it different. Because a builder is named `with_`, +shortening a field shortens its builder with it and the pair stays matched. + +Shorten only where the enclosing name really does disambiguate. When a short +form is also the established term for a *different* quantity elsewhere in the +ecosystem, say which one you mean in the doc comment rather than lengthening the +identifier — the doc is read at the call, and it can explain what a longer name +could only hint at. + +### Use precise domain words + +- **selected** is persistent membership or the active item; **focused** is the + current keyboard target; **hovered** is pointer presence; **confirmed** is an + activation result. Never use them interchangeably. +- **open/close** describes an overlay or disclosure state; **show/hide** is for + transient presentation requests; **expand/collapse** describes structure. +- **disabled** prevents interaction; **readonly** permits navigation and + selection but prevents editing; **loading** prevents duplicate work while an + operation is pending. Spell the state `readonly` — one word, as the + `readonly(bool)` builder and `is_readonly()` reader do — in identifiers, + interface labels, and documentation alike; never `read-only` or `read only`. +- **index** is a current positional coordinate; **id** is stable identity; + `IndexPath` represents hierarchical position. Do not persist or key + reorderable data by index. +- **value** is controlled domain data; **presentation** is a read-only snapshot + prepared for rendering; **state** is retained behavior. +- **placement** is a side or anchor policy; **position** is resolved geometry. +- **size** is a semantic control tier; **width/height/bounds** are geometry. +- **child/children** follows GPUI composition; named slots such as `header`, + `footer`, `trigger`, and `content` carry additional semantics. + +Avoid vague public names such as `data`, `item2`, `handle_action`, `update_ui`, +`process`, `manager`, or `config` when a narrower domain term exists. `Manager` +is appropriate only when a type truly coordinates a collection or lifecycle, +as `ToastManager` does. + +### Type and module style + +- Rust types and Actions use `UpperCamelCase`; modules, functions, methods, + fields, and local variables use `snake_case`; constants use + `SCREAMING_SNAKE_CASE`. +- A module named after a component owns its public seam. Internal folders may + split state, element, geometry, platform adapter, and tests without leaking + those folder names into imports. +- Use singular module names for one component concept and established + ecosystem names for families (`input`, `table`, `dock`). +- Suffix type-erased wrappers with `Any` only when they erase a real type + boundary, such as `AnyInputState` or `AnyElement`. +- Suffix identifiers with `Id`, zero-based indices with `ix`, and collections + with meaningful plurals. Do not alternate `idx`, `index`, and `ix` in one + subsystem. +- Name predicates positively when possible. A positive `enabled`/`visible` + contract is easier to compose than multiple negatives, but preserve + established API terms such as `disabled` where they match control semantics. + +### Callback and event wording + +Use `on_click` only for a genuine click-level contract. A controlled semantic +primitive in Base should prefer `on_change(next_value, ...)`; a styled +compatibility component may retain `on_click` when pointer details or existing +API expectations matter. Do not invent a `ClickEvent` for a model-driven +change. + +Name before/after lifecycle hooks precisely. `on_will_change` can veto or +prepare; `on_change` observes a requested/current value contract; `on_confirm` +commits a choice; `on_dismiss` closes a transient surface. Document whether a +callback runs before internal state changes, after them, or instead of them, +and whether it may synchronously re-enter the component. + +### Documentation and copy style + +Public docs should begin with what a type does and who owns its state. Examples +must use current, compilable APIs and show stable IDs. Document defaults, +platform limitations, focus behavior, callback ordering, and any requirement +to call `notify`, `emit`, or a theme synchronization method. + +Follow the [interface-language rules](./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` 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. diff --git a/.agents/skills/gpui-kit/references/conventions.md b/.agents/skills/gpui-kit/references/conventions.md new file mode 100644 index 0000000..41b2d99 --- /dev/null +++ b/.agents/skills/gpui-kit/references/conventions.md @@ -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)` | `on_change` receives the requested `&usize` | +| Edit retained text | `Input::new(&self.input)` | View retains `Entity` | 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, `cargo test -p gpui-kit-recipes` compiles the external-style consumer and runs its interaction tests, and `script/check-ai rust` runs the control-family event and compatibility tests and Form geometry tests. These are deterministic contract checks, not measured AI first-attempt success rates. diff --git a/.agents/skills/gpui-kit/references/gpui/action.md b/.agents/skills/gpui-kit/references/gpui/action.md new file mode 100644 index 0000000..f57c3ec --- /dev/null +++ b/.agents/skills/gpui-kit/references/gpui/action.md @@ -0,0 +1,208 @@ +# Actions & Keybindings + +**Contents:** [Overview](#overview) · [Quick Start](#quick-start) · [Key Formats](#key-formats) · [Menu Bar Shortcuts](#menu-bar-shortcuts) · [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) -> 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) { + // Handle move up + cx.notify(); + } + + fn move_down(&mut self, _: &MoveDown, cx: &mut Context) { + cx.notify(); + } + + fn save(&mut self, _: &Save, cx: &mut Context) { + // 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.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 +``` + +## Menu Bar Shortcuts + +`cx.set_menus` reads the keymap **at the moment it is called** and bakes each +item's shortcut into the native menu. A key bound afterwards never appears next +to its menu item, and the item does not react to that key. There is no event to +hook: bind first, then build the menus, and rebuild them when the keymap +changes. + +```rust +pub fn init(cx: &mut App) { + // 1. Bindings first — the menu items copy their shortcuts from these. + cx.bind_keys([ + KeyBinding::new("cmd-n", NewChat, None), + KeyBinding::new("cmd-,", OpenSettings, None), + KeyBinding::new("cmd-q", Quit, None), + ]); + + // 2. Then the menu bar. + cx.set_menus(vec![Menu { + name: "File".into(), + items: vec![ + MenuItem::action("New Chat", NewChat), + MenuItem::action("Settings…", OpenSettings), + MenuItem::separator(), + MenuItem::action("Quit", Quit), + ], + }]); +} +``` + +Call `cx.set_menus` again after anything that changes bindings or labels: a +user keymap file loaded later, a locale switch that renames the items. `Undo`, +`Redo`, `Cut`, `Copy`, `Paste` need no `on_action` of your own — give the item +an `OsAction` and the platform forwards it to the focused text control. + +## 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) { + // Handle save + cx.notify(); + } +} + +div().on_action(cx.listener(Self::on_action_save)) +``` + diff --git a/.agents/skills/gpui-kit/references/gpui/async.md b/.agents/skills/gpui-kit/references/gpui/async.md new file mode 100644 index 0000000..16982e5 --- /dev/null +++ b/.agents/skills/gpui-kit/references/gpui/async.md @@ -0,0 +1,225 @@ +# 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`, the closure receives `(WeakEntity, &mut AsyncApp)`: + +```rust +impl MyComponent { + fn fetch_data(&mut self, cx: &mut Context) { + 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) { + 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) { + cx.spawn(async move |this, cx| { + // Runs on background thread, CPU-intensive + let result = cx + .background_spawn(async move { heavy_computation() }) + .await; + + // Back on the foreground to update UI + this.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 { + 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) + +```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.spawn(async move |this, cx: &mut AsyncApp| { + let result = cx.background_spawn(async move { heavy_work() }).await; + 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>) { + 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>) { + 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: Await the background task inside a foreground task + +```rust +// ✅ Correct: the foreground task awaits the background result +cx.spawn(async move |this, cx| { + let data = cx.background_spawn(async move { load_data() }).await; + this.update(cx, |state, cx| { + state.data = data; + cx.notify(); + }).ok(); +}) +.detach(); +``` + +GPUI has no `Task::then`. Await the background task inside `cx.spawn`. The +closure receives `(WeakEntity, &mut AsyncApp)`, not the background result. + diff --git a/.agents/skills/gpui-kit/references/gpui/context.md b/.agents/skills/gpui-kit/references/gpui/context.md new file mode 100644 index 0000000..a1cce79 --- /dev/null +++ b/.agents/skills/gpui-kit/references/gpui/context.md @@ -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`**: Entity-specific context for component `T` +- **`AsyncApp`**: Async context for foreground tasks +- **`AsyncWindowContext`**: Async context with window access + +## Quick Start + +### Context - Component Context + +```rust +impl MyComponent { + fn update_state(&mut self, cx: &mut Context) { + 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() { + gpui_kit::application().run(|cx: &mut App| { + gpui_kit::init(cx); + + // Create entities + let entity = cx.new(|cx| MyState::default()); + + // Open windows (wraps the view in a `Root`) + gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| cx.new(|_| MyView)) + .expect("failed to open window"); + }); +} +``` + +### Window - Window Context + +```rust +impl Render for MyView { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> 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 (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) -> 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) { + 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::(|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) {} // cx = Context +} +cx.spawn(async move |this, cx: &mut AsyncApp| {}) // cx = AsyncApp +``` diff --git a/.agents/skills/gpui-kit/references/gpui/element-advanced.md b/.agents/skills/gpui-kit/references/gpui/element-advanced.md new file mode 100644 index 0000000..80886ab --- /dev/null +++ b/.agents/skills/gpui-kit/references/gpui/element-advanced.md @@ -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, +} + +struct MasonryLayoutState { + column_layouts: Vec>, + column_heights: Vec, +} + +struct MasonryPaintState { + child_bounds: Vec>, +} + +impl Element for MasonryLayout { + type RequestLayoutState = MasonryLayoutState; + type PrepaintState = MasonryPaintState; + + fn id(&self) -> Option { + 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![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, + 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, + _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, +} + +impl Element for CircularLayout { + type RequestLayoutState = Vec; + type PrepaintState = Vec>; + + fn request_layout( + &mut self, + global_id: Option<&GlobalElementId>, + inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App + ) -> (LayoutId, Vec) { + 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, + layout_ids: &mut Vec, + window: &mut Window, + cx: &mut App + ) -> Vec> { + 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, + _layout_ids: &mut Vec, + child_bounds: &mut Vec>, + 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(&mut self, f: F) -> &mut Self + where + F: Fn(&mut Window, &mut App) + 'static; + + fn on_hover_end(&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>, + hover_end_handlers: Vec>, + was_hovered: bool, +} + +impl Hoverable for HoverableElement { + fn on_hover(&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(&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, + _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(&mut self, f: F) -> &mut Self + where + F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static; + + fn on_double_click(&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>, + double_click_handlers: Vec>, + last_click_time: Option, +} + +impl Clickable for ClickableElement { + fn on_click(&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(&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, + loading: bool, + data: Option, +} + +pub struct AsyncState { + loading: bool, + data: Option, +} + +impl Element for AsyncElement { + type RequestLayoutState = (); + type PrepaintState = Hitbox; + + fn paint( + &mut self, + _global_id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, + _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 { + id: ElementId, + value: T, + render_fn: Box AnyElement>, + cached_element: Option, + last_value: Option, +} + +impl MemoizedElement { + pub fn new(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 Element for MemoizedElement { + type RequestLayoutState = LayoutId; + type PrepaintState = (); + + fn id(&self) -> Option { + 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, + _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, + _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) -> 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 AnyElement>, +} + +struct VirtualListState { + visible_range: Range, + visible_item_layouts: Vec, +} + +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, + 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, + 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. diff --git a/.agents/skills/gpui-kit/references/gpui/element-api.md b/.agents/skills/gpui-kit/references/gpui/element-api.md new file mode 100644 index 0000000..bf82a84 --- /dev/null +++ b/.agents/skills/gpui-kit/references/gpui/element-api.md @@ -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; + 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, + 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, + 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); + +// Complex struct +pub struct MyLayoutState { + pub styled_text: StyledText, + pub child_layouts: Vec<(LayoutId, ChildState)>, + pub computed_bounds: Bounds, +} +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; + +// Multiple values +type PrepaintState = (Hitbox, Vec>); + +// Complex struct +pub struct MyPaintState { + pub hitbox: Hitbox, + pub child_bounds: Vec>, + pub visible_range: Range, +} +type PrepaintState = MyPaintState; +``` + +## Methods + +### id() + +Returns optional unique identifier for debugging and inspection. + +```rust +fn id(&self) -> Option { + Some(self.id.clone()) +} + +// Or if no ID needed +fn id(&self) -> Option { + 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, + 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, + 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 + +Represents rectangular region: + +```rust +pub struct Bounds { + pub origin: Point, + pub size: Size, +} + +// 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); +``` diff --git a/.agents/skills/gpui-kit/references/gpui/element-best-practices.md b/.agents/skills/gpui-kit/references/gpui/element-best-practices.md new file mode 100644 index 0000000..0f549ec --- /dev/null +++ b/.agents/skills/gpui-kit/references/gpui/element-best-practices.md @@ -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); +type PrepaintState = (Hitbox, Vec); +``` + +**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, +} + +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) + { + // 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, ..) { + // 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, + last_width: Option, + + // Cached result + cached_layout: Option, +} + +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, 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, container_bounds: &Bounds) -> 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, 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) +{ + // 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) +{ + 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) +{ + // 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, +} +``` + +**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, window: &mut Window, ..) { + // Bad: Creating hitbox in paint + let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); +} +``` + +**Good:** + +```rust +fn prepaint(&mut self, .., bounds: Bounds, 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 diff --git a/.agents/skills/gpui-kit/references/gpui/element-examples.md b/.agents/skills/gpui-kit/references/gpui/element-examples.md new file mode 100644 index 0000000..2644e2f --- /dev/null +++ b/.agents/skills/gpui-kit/references/gpui/element-examples.md @@ -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, 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 { + 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, + 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, + 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, +} + +impl IntoElement for SelectableText { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for SelectableText { + type RequestLayoutState = TextLayout; + type PrepaintState = Option; + + fn id(&self) -> Option { + 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, + _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, + 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, + 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, + bounds: &Bounds, + 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>>, + scrollable: bool, + scroll_offset: Point, +} + +struct ComplexLayoutState { + child_layouts: Vec, + total_height: Pixels, +} + +struct ComplexPaintState { + child_bounds: Vec>, + 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 { + 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, + 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, + 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, container_bounds: &Bounds) -> 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, + 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) -> 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) -> 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) -> impl IntoElement { + let children: Vec>> = 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, + }) +} +``` diff --git a/.agents/skills/gpui-kit/references/gpui/element-id.md b/.agents/skills/gpui-kit/references/gpui/element-id.md new file mode 100644 index 0000000..291d191 --- /dev/null +++ b/.agents/skills/gpui-kit/references/gpui/element-id.md @@ -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
`: + +```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 for &str // "my-id" +impl Into for String // String::from("my-id") +impl Into for usize // 0, 1, 2, ... +impl Into for u64 +impl Into 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
, + // ... +} + +impl Button { + pub fn new(id: impl Into) -> 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") +``` diff --git a/.agents/skills/gpui-kit/references/gpui/element-patterns.md b/.agents/skills/gpui-kit/references/gpui/element-patterns.md new file mode 100644 index 0000000..31c9e2f --- /dev/null +++ b/.agents/skills/gpui-kit/references/gpui/element-patterns.md @@ -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, 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, 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, + direction: FlexDirection, + gap: Pixels, +} + +impl Element for ContainerElement { + type RequestLayoutState = Vec; + type PrepaintState = Vec>; + + fn request_layout(&mut self, .., window: &mut Window, cx: &mut App) + -> (LayoutId, Vec) + { + 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, layout_ids: &mut Vec, + window: &mut Window, cx: &mut App) -> Vec> + { + 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>, + 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>, + hover_style: Option