From 7d900a385d7a4678c0a8a2b157d3c0df30d2493a Mon Sep 17 00:00:00 2001 From: JetSquirrel Date: Fri, 28 Aug 2026 13:22:49 +0800 Subject: [PATCH 1/6] Fix build against gpui-component 0.5.0-preview2 The input API changed: set_placeholder now takes window and cx, and set_default_value was removed. Seed the region defaults with set_value instead. Also gets `cargo clippy -- -D warnings` back to clean: - apply cargo fmt to the budget code merged in #23 - allow(dead_code) on the budget API until the UI is wired up (v0.2.0) - use sort_by_key for the SigV4 canonical header sort fmt, clippy, check and test all pass. Co-Authored-By: Claude Opus 5 (1M context) --- src/cloud/aws.rs | 4 ++-- src/cloud/mod.rs | 4 ++++ src/crypto.rs | 4 ++++ src/db.rs | 18 +++++++++++++----- src/ui/accounts.rs | 40 ++++++++++++++++++++-------------------- 5 files changed, 43 insertions(+), 27 deletions(-) diff --git a/src/cloud/aws.rs b/src/cloud/aws.rs index e28a129..16e311f 100644 --- a/src/cloud/aws.rs +++ b/src/cloud/aws.rs @@ -76,7 +76,7 @@ impl AwsCloudService { all_headers.push(("x-amz-content-sha256".to_string(), payload_hash.clone())); // Sort by lowercase key - all_headers.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase())); + all_headers.sort_by_key(|(name, _)| name.to_lowercase()); let canonical_headers: String = all_headers .iter() @@ -363,7 +363,7 @@ impl AwsCloudService { all_headers.push(("x-amz-content-sha256".to_string(), payload_hash.clone())); // Sort by lowercase key - all_headers.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase())); + all_headers.sort_by_key(|(name, _)| name.to_lowercase()); let canonical_headers: String = all_headers .iter() diff --git a/src/cloud/mod.rs b/src/cloud/mod.rs index f498591..64fe9d4 100644 --- a/src/cloud/mod.rs +++ b/src/cloud/mod.rs @@ -138,6 +138,8 @@ pub struct CostTrend { } /// Budget information +// TODO(v0.2.0): drop this allow once the budget UI is wired up +#[allow(dead_code)] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BudgetInfo { /// Account ID @@ -155,6 +157,8 @@ pub struct BudgetInfo { } /// Budget status (comparison of budget vs actual) +// TODO(v0.2.0): drop this allow once the budget UI is wired up +#[allow(dead_code)] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BudgetStatus { /// Account ID diff --git a/src/crypto.rs b/src/crypto.rs index 8def653..eaa1c84 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -47,6 +47,10 @@ impl CryptoManager { } /// Encrypt data + /// + /// Unused since secrets moved to the OS keyring; kept as the counterpart of + /// `decrypt`, which still reads legacy encrypted blobs during migration. + #[allow(dead_code)] pub fn encrypt(&self, plaintext: &str) -> Result { let mut nonce_bytes = [0u8; NONCE_SIZE]; OsRng.fill_bytes(&mut nonce_bytes); diff --git a/src/db.rs b/src/db.rs index 91a32d8..8a351b0 100644 --- a/src/db.rs +++ b/src/db.rs @@ -625,6 +625,7 @@ pub fn clear_all_cache() -> Result<()> { // ==================== Budget Functions ==================== /// Save or update budget for an account +#[allow(dead_code)] // TODO(v0.2.0): remove once the budget UI calls this pub fn save_budget(budget: &BudgetInfo) -> Result<()> { let db = get_connection()?; let conn = db.as_ref().unwrap(); @@ -650,6 +651,7 @@ pub fn save_budget(budget: &BudgetInfo) -> Result<()> { } /// Get budget for an account +#[allow(dead_code)] // TODO(v0.2.0): remove once the budget UI calls this pub fn get_budget(account_id: &str) -> Result> { let db = get_connection()?; let conn = db.as_ref().unwrap(); @@ -688,6 +690,7 @@ pub fn get_budget(account_id: &str) -> Result> { } /// Get all budgets +#[allow(dead_code)] // TODO(v0.2.0): remove once the budget UI calls this pub fn get_all_budgets() -> Result> { let db = get_connection()?; let conn = db.as_ref().unwrap(); @@ -724,17 +727,22 @@ pub fn get_all_budgets() -> Result> { } /// Delete budget for an account +#[allow(dead_code)] // TODO(v0.2.0): remove once the budget UI calls this pub fn delete_budget(account_id: &str) -> Result<()> { let db = get_connection()?; let conn = db.as_ref().unwrap(); - conn.execute("DELETE FROM budgets WHERE account_id = ?", params![account_id])?; + conn.execute( + "DELETE FROM budgets WHERE account_id = ?", + params![account_id], + )?; tracing::info!("Deleted budget for account {}", account_id); Ok(()) } /// Get budget status (compares budget with current costs) +#[allow(dead_code)] // TODO(v0.2.0): remove once the budget UI calls this pub fn get_budget_status(account_id: &str) -> Result> { // Get budget let budget = match get_budget(account_id)? { @@ -750,11 +758,10 @@ pub fn get_budget_status(account_id: &str) -> Result> { .ok_or_else(|| anyhow::anyhow!("Account not found"))?; // Get cached cost summary - let cost_summary = get_cached_cost_summary_with_account(account_id, &account.name, &account.provider)?; + let cost_summary = + get_cached_cost_summary_with_account(account_id, &account.name, &account.provider)?; - let current_cost = cost_summary - .map(|cs| cs.current_month_cost) - .unwrap_or(0.0); + let current_cost = cost_summary.map(|cs| cs.current_month_cost).unwrap_or(0.0); // Calculate metrics let percentage_used = if budget.monthly_budget > 0.0 { @@ -779,6 +786,7 @@ pub fn get_budget_status(account_id: &str) -> Result> { } /// Get all budget statuses +#[allow(dead_code)] // TODO(v0.2.0): remove once the budget UI calls this pub fn get_all_budget_statuses() -> Result> { let budgets = get_all_budgets()?; let mut statuses = Vec::new(); diff --git a/src/ui/accounts.rs b/src/ui/accounts.rs index e6c4db9..4c04a79 100644 --- a/src/ui/accounts.rs +++ b/src/ui/accounts.rs @@ -104,38 +104,38 @@ impl AccountsView { // Update input placeholders based on cloud provider match provider { CloudProvider::AWS => { - self.ak_input.update(cx, |state, _cx| { - state.set_placeholder("Access Key ID"); + self.ak_input.update(cx, |state, cx| { + state.set_placeholder("Access Key ID", window, cx); }); - self.sk_input.update(cx, |state, _cx| { - state.set_placeholder("Secret Access Key"); + self.sk_input.update(cx, |state, cx| { + state.set_placeholder("Secret Access Key", window, cx); }); - self.region_input.update(cx, |state, _cx| { - state.set_placeholder("Region (optional, default us-east-1)"); - state.set_default_value("us-east-1"); + self.region_input.update(cx, |state, cx| { + state.set_placeholder("Region (optional, default us-east-1)", window, cx); + state.set_value("us-east-1", window, cx); }); } CloudProvider::Aliyun => { - self.ak_input.update(cx, |state, _cx| { - state.set_placeholder("AccessKey ID"); + self.ak_input.update(cx, |state, cx| { + state.set_placeholder("AccessKey ID", window, cx); }); - self.sk_input.update(cx, |state, _cx| { - state.set_placeholder("AccessKey Secret"); + self.sk_input.update(cx, |state, cx| { + state.set_placeholder("AccessKey Secret", window, cx); }); - self.region_input.update(cx, |state, _cx| { - state.set_placeholder("Region (optional, default cn-hangzhou)"); - state.set_default_value("cn-hangzhou"); + self.region_input.update(cx, |state, cx| { + state.set_placeholder("Region (optional, default cn-hangzhou)", window, cx); + state.set_value("cn-hangzhou", window, cx); }); } CloudProvider::DeepSeek => { - self.ak_input.update(cx, |state, _cx| { - state.set_placeholder("API Key"); + self.ak_input.update(cx, |state, cx| { + state.set_placeholder("API Key", window, cx); }); - self.sk_input.update(cx, |state, _cx| { - state.set_placeholder("(Not required, leave empty)"); + self.sk_input.update(cx, |state, cx| { + state.set_placeholder("(Not required, leave empty)", window, cx); }); - self.region_input.update(cx, |state, _cx| { - state.set_placeholder("(Not required)"); + self.region_input.update(cx, |state, cx| { + state.set_placeholder("(Not required)", window, cx); }); } From 36155454f181ba494479499556f47ff98c435a1b Mon Sep 17 00:00:00 2001 From: JetSquirrel Date: Fri, 28 Aug 2026 13:22:56 +0800 Subject: [PATCH 2/6] Add upstream gpui-component skill Vendored from longbridge/gpui-component (skills/gpui-component on main): SKILL.md plus the design, coding, usage and style reference guides for the component library this project builds its UI on. Refresh it from the source repo rather than editing in place. Co-Authored-By: Claude Opus 5 (1M context) --- .claude/skills/README.md | 19 + .claude/skills/gpui-component/SKILL.md | 168 ++++ .../references/coding-guides.md | 863 +++++++++++++++++ .../references/design-guides.md | 890 ++++++++++++++++++ .../gpui-component/references/style-guide.md | 364 +++++++ .../skills/gpui-component/references/usage.md | 402 ++++++++ 6 files changed, 2706 insertions(+) create mode 100644 .claude/skills/gpui-component/SKILL.md create mode 100644 .claude/skills/gpui-component/references/coding-guides.md create mode 100644 .claude/skills/gpui-component/references/design-guides.md create mode 100644 .claude/skills/gpui-component/references/style-guide.md create mode 100644 .claude/skills/gpui-component/references/usage.md diff --git a/.claude/skills/README.md b/.claude/skills/README.md index 84540b1..888c7e4 100644 --- a/.claude/skills/README.md +++ b/.claude/skills/README.md @@ -130,6 +130,25 @@ Helps debug common GPUI framework issues. - Performance optimization - Memory leak prevention +### 8. GPUI Component Library (`gpui-component/`) +Upstream skill vendored from [longbridge/gpui-component](https://github.com/longbridge/gpui-component/tree/main/skills/gpui-component). Covers the component library CloudBridge builds its UI on (`gpui-component 0.5.0-preview1`). + +**Use when:** +- Building or changing UI with library components (Button, Input, Select, Dialog, Tabs, Sidebar, Table, ...) +- Choosing the right component for a UI need +- Wiring component state or theming +- Making layout, spacing, hierarchy, interaction-state, or interface-copy decisions +- Making architecture, state-ownership, or public API decisions + +**Key Topics:** +- Component catalog with imports and stateless/stateful notes +- Setup patterns (`gpui_component::init`, `Root::new`) +- [design-guides.md](gpui-component/references/design-guides.md) — normative design rules +- [coding-guides.md](gpui-component/references/coding-guides.md) — crate layering, `RenderOnce` vs `Entity`, focus, async +- [usage.md](gpui-component/references/usage.md), [style-guide.md](gpui-component/references/style-guide.md) + +**Note:** This is a copy of upstream content — refresh it from the source repo rather than editing in place. + ## How to Use Skills Skills are reference documents that provide: diff --git a/.claude/skills/gpui-component/SKILL.md b/.claude/skills/gpui-component/SKILL.md new file mode 100644 index 0000000..36f442f --- /dev/null +++ b/.claude/skills/gpui-component/SKILL.md @@ -0,0 +1,168 @@ +--- +name: gpui-component +description: How to use the gpui-component UI library in GPUI applications, and the normative Design and Coding Guides that govern it. Use when building UIs with gpui-component components (Button, Input, Select, Dialog, Tabs, Sidebar, List, Table, etc.), setting up the library, handling component state or theming, finding the right component for a UI need, and also when designing layouts, spacing, visual hierarchy, or interaction states, writing interface copy, or making application architecture, state-ownership, or public API decisions. +--- + +## Read This First + +Before changing UI, interaction, interface language, layout, styling, +components, or application architecture, **read the relevant guide**: + +| Guide | Read before | +| --- | --- | +| [Design Guides](references/design-guides.md) | Choosing components, layout, spacing, hierarchy, color, density, interaction states, overlays, interface copy | +| [Coding Guides](references/coding-guides.md) | Crate layering, `RenderOnce` vs `Entity`, state ownership, `ElementId`, focus, async, public API, testing | + +These guides are requirements, not optional inspiration. Do not copy generic +web conventions, infer a design system from one existing screen, or add a +control merely because the underlying feature exists. Review the finished work +against both guides before considering it complete. + +Read the guide file itself. Do not answer from what this page summarizes, from +an existing screen in the codebase, or from training data — those are the three +ways the guides get quietly ignored. + +### Non-negotiables + +These are a floor, not a substitute. Read the guide for anything past this list. + +- **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 name that does not exist is the most common + failure mode here. +- **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 — use `ghost` or + `outline` when it should read quietly. `Link` only for external URLs and + email addresses. +- **Tokens before values.** No raw hex or `rgb(...)` in application UI; use + `cx.theme()` semantic tokens. Use rem-based helpers (`p_2()`, `gap_3()`, + `text_sm()`) so window zoom works. Any spacing number you see quoted 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. +- **Stable identity.** Repeated elements need domain-derived `ElementId`s, not + list indexes. +- **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`. + +## Documentation + +- **Full reference**: fetch `https://longbridge.github.io/gpui-component/llms-full.txt` +- **Per-component API**: fetch `https://longbridge.github.io/gpui-component/docs/components/{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** — always required: +```rust +gpui_component::init(cx); // in app.run(), must be first +Root::new(view, window, cx) // first-level view in every window +``` + +**Stateless** — use directly in render: +```rust +Button::new("id").primary().label("OK").on_click(|_, _, _| {}) +``` + +**Stateful** — hold `Entity` in struct, pass ref in render: +```rust +// in new(): let input = cx.new(|cx| InputState::new(window, cx)); +// in render: Input::new(&self.input) +``` + +**Sizes**: `.xsmall()` `.small()` `.medium()` (default) `.large()` + +**Theme**: `cx.theme().primary` · `.background` · `.foreground` · `.border` · `.muted` + +## Component Catalog + +When you need a component, find it here. For full API, fetch its `.md` doc. + +### Input & Form +| Component | Import | Notes | +|-----------|--------|-------| +| `Input` | `input::{Input, InputState}` | Stateful. Text, password, mask, validation | +| `NumberInput` | `input::{NumberInput, NumberInputEvent}` | Stateful. Numeric with step | +| `OtpInput` | `input::OtpInput` | Stateful. One-time password | +| `Select` | `select::{Select, SelectState}` | Stateful. Dropdown picker | +| `Combobox` | `combobox::{Combobox, ComboboxState}` | Stateful. Searchable select | +| `Checkbox` | `checkbox::Checkbox` | Stateless. `on_click(|&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. Increment/decrement | +| `ColorPicker` | `color_picker::{ColorPicker, ColorPickerState}` | Stateful. | +| `DatePicker` | `date_picker::{DatePicker, DatePickerState}` | Stateful. | +| `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 | +| `Progress` | `progress::{Progress, ProgressCircle}` | Stateless. | +| `Tooltip` | `tooltip::Tooltip` | Via `.tooltip()` on elements | +| `HoverCard` | `hover_card::{HoverCard, HoverCardState}` | Stateful. | +| `Clipboard` | `clipboard::Clipboard` | Stateless. Copy button | + +### 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 | + +### Navigation & Layout +| Component | Import | Notes | +|-----------|--------|-------| +| `Tabs` / `TabBar` | `tab::{Tab, TabBar}` | Tabbed interface | +| `Sidebar` | `sidebar::{Sidebar, SidebarMenu, ...}` | App navigation panel | +| `TitleBar` | `TitleBar` | Window title 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 | +| `Scrollable` | `scroll::Scrollbar` | Custom scrollbar | +| `FocusTrap` | `gpui_base::focus_trap::FocusTrapElement` | Keyboard trap for modals | + +### 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 | + +### Charts +| Component | Import | Notes | +|-----------|--------|-------| +| `Chart` | `chart::{AreaChart, BarChart, LineChart, PieChart, RadarChart}` | Bar, line, area, pie charts | +| `Plot` | `plot::Plot` | `#[derive(IntoPlot)]` for data | + +## Reference Files + +- [usage.md](references/usage.md) — setup patterns, component types, common examples +- [style-guide.md](references/style-guide.md) — code style for contributors diff --git a/.claude/skills/gpui-component/references/coding-guides.md b/.claude/skills/gpui-component/references/coding-guides.md new file mode 100644 index 0000000..46c4022 --- /dev/null +++ b/.claude/skills/gpui-component/references/coding-guides.md @@ -0,0 +1,863 @@ +--- +title: Coding Guides +description: Architecture and coding conventions for maintainable GPUI Component applications +order: -2.2 +--- + +# Coding Guides + +This guide describes the application architecture and code patterns that have +proved durable in GPUI Component. 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_component::init(cx); + + cx.spawn(async move |cx| { + cx.open_window(WindowOptions::default(), |window, cx| { + let workspace = cx.new(|cx| Workspace::new(window, cx)); + cx.new(|cx| Root::new(workspace, window, cx)) + }) + .expect("failed to open window"); + }) + .detach(); +}); +``` + +`Root` coordinates window-level component facilities such as overlays and +notifications. Do not create a separate root for each page inside one window. +It also coordinates modal focus restoration, focus traps, tooltip/menu layers, +and window-scoped text selection. Bypassing it can produce behavior that looks +correct at rest but fails when overlays nest or focus changes quickly. + +## Understand GPUI's phases and contexts + +GPUI is retained state with declarative rendering. An entity survives across +frames; the element tree returned by `render` is a fresh description of the +current frame. Keep that distinction explicit. + +- `Context` 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. + +If code mutates the global GPUI Component theme directly, call +`Theme::sync_base(cx)` afterward so Base-owned scrollbars and resize handles +receive the new projection. `Theme::change(...)` performs this projection as +part of a complete theme change. + +An outward focus ring needs physical room. An ancestor with +`overflow_hidden()` clips it. Prefer layouts that leave room; if a product must +clip heavily, use the theme's focus-ring policy and retain the focused border +instead of silently hiding all keyboard focus. + +### Base font is the application zoom control + +`Root::render` calls `window.set_rem_size(cx.theme().font_size)`. Therefore the +theme's base font is not only body typography; it is the reference length for +the application's rem-based design scale. This deliberately follows the useful +part of Tailwind's model: named type, spacing, and size steps share one relative +base instead of becoming unrelated pixel constants. + +Change zoom by updating the base font and refreshing the window: + +```rust +Theme::global_mut(cx).font_size = px(18.); +Theme::sync_base(cx); +window.refresh(); +``` + +The base font itself is a pixel value because it anchors the scale. Descendant +application UI should normally use relative helpers—`text_sm()`, `gap_2()`, +`px_3()`, `h_8()`, `size_4()`—so type, whitespace, controls, and icons respond +together. A custom component that combines rem-based text with fixed-pixel +padding or icon geometry must document why that part should not zoom. + +Treat every direct `px(...)` and raw color constructor in application UI as a +review finding. Accept it only for a documented physical/platform boundary, +measured runtime geometry, raster/data color, or the theme/token definition +itself. Convenience and matching a screenshot are not valid exceptions. + +Anything cached from resolved layout must include `window.rem_size()` in its +invalidation key, directly or through a revision that changes with it. This +includes wrapped row heights, text shaping/layout, virtual-list measurement, +popup and dialog geometry, icon sizing derived from text, and custom canvas +metrics. The Command component's variable-height rows are an ecosystem example: +they remeasure when rem changes because the same fixed width wraps differently +at a larger base font. + +Do not confuse this application zoom with Dock panel zoom. Dock zoom is a +stateful layout operation that makes one tab group or tile 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 Component 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_base::Link`, whose `open_with` seam can +route a destination elsewhere. + +Only stop propagation when a nested interaction must prevent its parent from +handling the same event. Blanket propagation stops break menus, selection, +dragging, and window-level commands in ways that are difficult to diagnose. + +Make focus ownership explicit: + +- retain a `FocusHandle` in the entity that owns keyboard interaction; +- register key contexts and actions on the appropriate focused region; +- transfer focus when opening an overlay and restore it on dismissal; +- render a visible `focus_visible` state; +- do not request focus unconditionally from `render`. + +Attach a `key_context` and its `on_action` handlers to the same focused region. +Bindings are contextual: a registered Action without the intended focus path +is not a working keyboard interaction. Composite widgets should implement the +complete navigation model—arrow movement, Home/End or page movement where +appropriate, confirmation, cancellation, and Tab behavior—rather than a few +isolated shortcuts. + +Modal surfaces must trap focus and restore the previous valid focus target on +dismissal. Nested overlays dismiss from the top. Handle rapid close/open +sequences without restoring focus through an intermediate, already-closing +surface. + +## Async work and side effects + +Start async work from an event, lifecycle hook, or named method—not as an +unconditional side effect of `render`. Capture weak entities when work should +not keep a closed view alive. When the task completes, update state through the +GPUI context, handle the case where the entity or window no longer exists, and +notify once after the coherent state change. + +Represent async operations with explicit states such as idle, loading, loaded, +and failed. Preserve usable previous data during refresh when possible. Prevent +duplicate destructive submissions and surface recoverable errors in the UI; +do not rely on logs as user feedback. + +Use background executors for expensive parsing or computation, but keep GPUI +entity mutation on the appropriate application context. Results can arrive +after the request, document, view, or selection has changed; attach a revision +or identity and reject stale work rather than applying it to new state. + +## Layout, measurement, and scrolling + +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. + +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. +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 when direct +construction is part of the contract and the compatibility cost is accepted. +Use `#[non_exhaustive]` when callers may inspect a record but should not depend +on exhaustive construction or matching. + +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; **read-only** permits navigation and + selection but prevents editing; **loading** prevents duplicate work while an + operation is pending. +- **index** is a current positional coordinate; **id** is stable identity; + `IndexPath` represents hierarchical position. Do not persist or key + reorderable data by index. +- **value** is controlled domain data; **presentation** is a read-only snapshot + prepared for rendering; **state** is retained behavior. +- **placement** is a side or anchor policy; **position** is resolved geometry. +- **size** is a semantic control tier; **width/height/bounds** are geometry. +- **child/children** follows GPUI composition; named slots such as `header`, + `footer`, `trigger`, and `content` carry additional semantics. + +Avoid vague public names such as `data`, `item2`, `handle_action`, `update_ui`, +`process`, `manager`, or `config` when a narrower domain term exists. `Manager` +is appropriate only when a type truly coordinates a collection or lifecycle, +as `ToastManager` does. + +### Type and module style + +- Rust types and Actions use `UpperCamelCase`; modules, functions, methods, + fields, and local variables use `snake_case`; constants use + `SCREAMING_SNAKE_CASE`. +- A module named after a component owns its public seam. Internal folders may + split state, element, geometry, platform adapter, and tests without leaking + those folder names into imports. +- Use singular module names for one component concept and established + ecosystem names for families (`input`, `table`, `dock`). +- Suffix type-erased wrappers with `Any` only when they erase a real type + boundary, such as `AnyInputState` or `AnyElement`. +- Suffix identifiers with `Id`, zero-based indices with `ix`, and collections + with meaningful plurals. Do not alternate `idx`, `index`, and `ix` in one + subsystem. +- Name predicates positively when possible. A positive `enabled`/`visible` + contract is easier to compose than multiple negatives, but preserve + established API terms such as `disabled` where they match control semantics. + +### Callback and event wording + +Use `on_click` only for a genuine click-level contract. A controlled semantic +primitive in Base should prefer `on_change(next_value, ...)`; a styled +compatibility component may retain `on_click` when pointer details or existing +API expectations matter. Do not invent a `ClickEvent` for a model-driven +change. + +Name before/after lifecycle hooks precisely. `on_will_change` can veto or +prepare; `on_change` observes a requested/current value contract; `on_confirm` +commits a choice; `on_dismiss` closes a transient surface. Document whether a +callback runs before internal state changes, after them, or instead of them, +and whether it may synchronously re-enter the component. + +### Documentation and copy style + +Public docs should begin with what a type does and who owns its state. Examples +must use current, compilable APIs and show stable IDs. Document defaults, +platform limitations, focus behavior, callback ordering, and any requirement +to call `notify`, `emit`, or a theme synchronization method. + +Follow the [interface-language rules](./design-guides.md#interface-language) for +labels, commands, confirmation dialogs, capitalization, and ellipses. Keep one +canonical term for each domain object, command, and state. + +Translation keys describe stable intent (`dialog.delete_project.title`), not a +source-language sentence or a screen coordinate. Never assemble a sentence +from translated fragments or reuse one key for meanings that happen to share +the same English text. + +Localize intent, not syntax. Give every locale control over word order, +pluralization, punctuation, and the amount of context it needs. Review strings +inside the component and with realistic data. Tests or linting should catch +missing keys, unintended CJK text in English resources, three-dot ellipses, +unreviewed ALL CAPS, and inconsistent fixed terms; human review still decides +whether repetition is justified by context. Verify every string inside its +component with realistic content, text expansion, and application zoom. + +## Testing strategy + +Test at the lowest layer that can prove the behavior: + +1. pure tests for state transitions, geometry, parsing, and ordering; +2. GPUI context tests for entities, events, and subscriptions; +3. `VisualTestContext` interaction tests for focus, keyboard, pointer, layout, + and rendered state; +4. example or application smoke tests for complete workflows. + +For an interactive component, cover the semantic contract rather than its +implementation details: pointer and keyboard activation, controlled value +changes, disabled behavior, focus movement, event count/order, stable identity, +and important empty or failure states. Add a regression test before fixing a +bug whenever the failure can be reproduced deterministically. + +For UI behavior that depends on the real window system, test through the +accessibility tree by role, label, value, enabled state, focus, and selection. +Re-read the tree after every state-changing action because element indexes are +snapshots. Use screenshots for visual facts the semantic tree cannot express; +use coordinate input only as a fallback. Report automated and manual evidence +separately. + +## Performance rules + +- Do not mutate state or notify unconditionally in `render`. +- Avoid rebuilding entities, subscriptions, focus handles, and expensive data + structures per frame. +- Notify the narrowest owning entity after a coherent state change. +- Virtualize long collections and render only the visible range. +- Avoid cloning large strings or collections solely to satisfy a closure; + capture stable handles or shared data. +- Measure before adding caches. A cache must have a clear invalidation owner. +- Keep animation work bounded and honor reduced motion. + +## Common failure modes + +Avoid these patterns: + +- one entity containing the entire application's unrelated state; +- business logic and network requests embedded in a long `render` method; +- random or index-based `ElementId` values for reorderable content; +- literal colors and radii that break custom themes; +- custom clickable `div`s where a semantic component already supplies focus, + keyboard, disabled, and accessibility behavior; +- duplicated local state that drifts from a controlled model value; +- `cx.notify()` loops caused by mutation during every render; +- nested scroll containers without explicit ownership; +- a new component variant for a one-off screen; +- confirmation dialogs for reversible, low-risk actions; +- tests that call internal methods but never exercise keyboard or pointer + behavior. + +## Rules for coding agents + +Before editing, an agent must read the nearest implementation, its tests, the +re-export seam, and the relevant component documentation. It must search the +current source for signatures instead of translating a React, CSS, or old GPUI +example by analogy. + +For each change, the agent should be able to name: + +1. the behavior owner and presentation owner; +2. the retained identity and state lifecycle; +3. the pointer, keyboard, focus, and accessibility contract; +4. the layout and overflow owner; +5. the theme tokens and intentional exceptions; +6. the test that would fail if the behavior regressed. + +Generated code must be reviewed and tested by a person. “Compiles” is not a UI +quality bar, and a broad refactor that merely makes generated code look tidy is +not a substitute for matching the repository's architecture. + +## Implementation checklist + +Before opening a change for review, confirm that: + +- state and side-effect ownership are explicit; +- `RenderOnce` versus `Entity` 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/.claude/skills/gpui-component/references/design-guides.md b/.claude/skills/gpui-component/references/design-guides.md new file mode 100644 index 0000000..aca4413 --- /dev/null +++ b/.claude/skills/gpui-component/references/design-guides.md @@ -0,0 +1,890 @@ +--- +title: Design Guides +description: Product and interaction design guidance for GPUI Component 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 Component desktop work: +an interface should feel native, restrained, precise, and understandable +without guesswork. + +This is a normative guide. **Must** identifies a correctness or ecosystem +constraint, **should** is the default that needs a concrete reason to override, +and **may** is an optional technique. Component API documentation remains the +authority for individual methods. + +The rules build on behavior in `gpui-base`, the GPUI Component theme and +component system, and familiar desktop interaction. Shadcn contributes useful +methods—open code, composition, and dependable defaults—but does not determine +how a GPUI application should look. When influences conflict, preserve GPUI's +lifecycle constraints and the interaction people already understand. + +## Design thesis + +Build interfaces that feel native, quiet, and precise. Let content, hierarchy, +and interaction carry the experience; decoration should support them rather +than compete with them. + +1. **Clarity before personality.** Make the primary task and next action clear + before adding brand expression. +2. **Composition before invention.** Start with established components and + compose them into product-specific workflows. Create a new primitive only + when its behavior is genuinely new. +3. **Tokens before values.** Colors, radii, typography, and spacing should form + a system. Avoid isolated literals that cannot respond to themes. +4. **Desktop before web convention.** Preserve keyboard access, window chrome, + menus, dense data views, resizable regions, and persistent navigation where + the task benefits from them. +5. **State must be visible.** Hover, focus, selection, disabled, loading, + validation, and destructive states need distinct and consistent treatment. + +## Learning from Shadcn + +Shadcn's most useful contribution is not a particular border color. It is a +way of building a system: + +- own the top layer of the interface instead of fighting a sealed abstraction; +- compose small, predictable parts into product-specific components; +- provide defaults that already form one visual language; +- keep the code and composition legible to both people and AI; +- separate behavior primitives from the styled layer. + +GPUI Component applies those ideas through a Rust library and the split between +`gpui-base` and `gpui-component`. Applications normally compose or wrap the +published components; contributors move genuinely reusable behavior into Base +and keep visual policy above it. + +Do not copy these web assumptions blindly: + +| Web habit | Native GPUI default | +| --- | --- | +| Pointing-hand cursor on every button | Default arrow cursor; pointing hand for links | +| Page navigation as the main structure | Persistent windows, panes, sidebars, tabs, and menus | +| Browser focus and scrolling as a fallback | Explicit focus ownership and region-owned scrolling | +| Mobile-first single column | Resizable desktop shell with a defined minimum window size | +| Hover-revealed critical actions | Keyboard- and pointer-reachable actions that do not depend on hover | +| A row of hover-only icon buttons | A visible primary action plus `DropdownMenu` or `ContextMenu` for secondary commands | +| Link-styled text for application commands | `Button`, `outline`, or `ghost`; Link only for URLs, web resources, or email addresses | +| Large touch density everywhere | Medium density by default; compact only where information work benefits | +| CSS overrides across descendants | Typed builders, semantic parts, and application composition | + +## Start from the task + +Before drawing a screen, write down: + +- the user's primary task; +- the object being viewed or changed; +- the actions that must remain immediately available; +- the information required to make a decision; +- empty, loading, error, offline, read-only, and permission-denied states; +- the keyboard path through the workflow. + +Organize the window around those answers. Do not begin with a dashboard grid or +a component catalogue. A good desktop interface exposes the user's mental +model: documents, accounts, projects, messages, settings, or another stable +object—not the internal service architecture. + +Design the primary task before distributing controls. Its visual weight, +location, and information depth should match its importance to the product. A +core result must not be reduced to a small count, an icon in a corner, or a +weak footer action while secondary content consumes the page. When a result set +is the product's main value, consider a summary region or card that exposes the +count, representative results, meaningful state, and a clear next action. + +For every proposed action, name its visible object, current state, scope, and +result. If the interface does not show or clearly imply those things, the +action is premature. Do not expose a capability merely because the backend has +it; first design how the object enters the user's mental model. + +## Visual language + +### Hierarchy + +Prefer a small number of clear levels: + +- **window or page title** identifies the current object or workspace; +- **section title** separates meaningful regions; +- **body text** carries the work; +- **muted text** provides secondary metadata and help; +- **labels** identify controls and values. + +Use size, weight, spacing, and separators before adding color or containers. +Avoid nesting cards inside cards: most desktop regions need only a background, +a hairline boundary, and intentional spacing. + +Evaluate hierarchy across the whole feature, not component by component. Hide +accent color and decoration during review: the primary task, current selection, +result summary, and next action should still be obvious from structure. A +screen that contains individually plausible controls can still fail when they +do not form one reading order and one decision path. + +Treat emphasis as a limited budget. A local surface needs one clear focal point, +not a field of competing highlights. If everything is colored, badged, bold, +boxed, or promoted to an alert, nothing reads as important. Establish priority +with structure and proximity first; spend stronger color and components only +where a distinction changes what the user notices or does. + +### Color and themes + +Read colors from `cx.theme()` and use them by semantic role: + +- `background` and `foreground` for the main surface and text; +- `group_box`, `popover`, `sidebar`, and their foreground tokens for their + named surfaces; +- `muted` and `muted_foreground` for supporting information; +- `primary` for the principal action or selection emphasis; +- `danger`, `warning`, `success`, and `info` only for their meanings; +- `border`, `input`, and focus-ring tokens for structure and interaction. + +Do not use a semantic status color as decoration. Do not encode meaning by +color alone. Verify every custom surface in light and dark themes and with +custom theme values; never assume that foreground is black or background is +white. + +Use Badge for a short state, count, or classification that benefits from rapid +scanning—not for every label, metadata value, filter, or section title. Keep +most badges neutral; reserve semantic variants for states that truly carry +success, warning, danger, or informational meaning. A row of multicolored +badges is usually a missing hierarchy or grouping decision. + +Application UI should not contain raw hex, `rgb`/`rgba`, or `hsla` colors. +Resolve colors from `cx.theme()` by semantic role. If the required role does +not exist, define it in the product's theme/token layer rather than embedding a +palette value at the call site. Raw colors belong only inside theme definitions +or in audited data/raster content whose color is itself the data. + +### Radius, spacing, and density + +Derive corner radii from the active theme. This preserves a product's ability +to become square or more rounded as one coherent system. Use `radius_full()` +for circles and pills rather than a literal maximum radius. + +Use a compact spacing scale and repeat it. Related label/control pairs should +be closer than separate groups; separate groups should be closer than separate +sections. Prefer component sizes (`xsmall`, `small`, default medium, `large`) +over one-off heights. Use compact variants for toolbars and data-dense screens, +not to squeeze an unclear layout into less space. + +The shared semantic scale is intentionally small: spacing progresses through +roughly 2, 4, 8, 12, 16, 24, and 32 pixels, while typography stays near 12, +14, 16, 18, and 20 pixels. Treat these as relationships rather than permission +to scatter their current values through feature code. GPUI Component currently +projects a fixed default `SpacingTokens` scale from its global `Theme`; unlike +colors and radii, `Theme::apply_semantic_tokens` does not persist a custom +spacing scale. An application that needs different spacing must own that full +token snapshot and use it consistently in its application components. + +### Spatial grammar + +Spacing expresses relationship. Choose a gap from the semantic scale by asking +what the two things mean to each other: + +| Relationship | Typical token | Current scale | Examples | +| --- | --- | --- | --- | +| Optical correction | `xxs` | 2 px | icon baseline, compact separator | +| Parts of one control | `xs` | 4 px | menu icon/label, title/description | +| Closely related controls | `sm` | 8 px | button icon/label, dialog actions | +| One content group | `md` | 12 px | notification columns, compact form rows | +| Separate groups in one section | `lg` | 16 px | panel padding, form groups | +| Separate sections | `xl` | 24 px | major blocks in a page or inspector | +| Major region boundary | `xxl` | 32 px | empty-state breathing room, page bands | + +These values describe the current default scale, not literals to repeat. Use +`cx.theme().spacing_tokens()` or the corresponding GPUI scale helpers for the +ecosystem default. A product-owned scale should preserve the ordering and +relationships and must be passed through the application's own design-system +context rather than assumed to persist in the global GPUI Component theme. + +Use these rules when resolving horizontal and vertical space: + +1. **Inside before outside.** A component's padding belongs to the component; + the gap between components belongs to their parent. +2. **Vertical rhythm shows grouping.** The gap between a title and its + description is smaller than the gap from that description to the next + section. Equal gaps imply equal relationships. +3. **Horizontal space supports scanning.** Repeated rows keep icons, labels, + values, badges, and trailing actions on stable columns. +4. **Leading and trailing are semantic.** Think in reading-order edges even + when the current API uses left/right; this keeps future RTL adaptation + possible. +5. **Do not double padding.** A card placed in an already padded panel should + not automatically add another full panel inset. +6. **Use optical alignment sparingly.** A one- or two-pixel correction is valid + for icon or glyph geometry, but document why it differs from the scale. + +Common compositions in the current system illustrate the relationships: + +- button contents use 4 px at small sizes and 8 px at normal sizes; +- dialog headers and footers use an 8 px internal gap; +- compact list and menu rows use 4 px vertical and 8–12 px horizontal padding; +- sheet headers use about 16 px leading and 12 px trailing space, leaving room + for a close affordance, while footers use 16 px horizontal and 12 px vertical + space; +- notifications use 16 px horizontal padding and a 12 px column gap because + icon, message, and action are distinct groups. + +Do not treat these as copy-and-paste recipes for every surface. They reveal the +system: controls are tighter internally, rows optimize scanning, and +containers spend more space at their boundary than between their contents. + +### Proportion and layer hierarchy + +Start with content requirements, then set proportions. Avoid arbitrary halves +when one pane has a clearly different role. + +- A navigation sidebar should be wide enough for stable labels but visibly + subordinate to the work area. Give it a minimum, preferred, and maximum + width rather than a percentage alone. +- In master–detail layouts, let the collection remain scannable and give the + detail pane the surplus. A roughly one-third/two-thirds starting point is + often useful, but content constraints are authoritative. +- Inspectors and auxiliary sheets should not cover the primary object by + default. They should be resizable or dismissible when their content grows. +- Dialog width comes from the decision: short confirmation, medium form, or a + dedicated window/page for complex work. Do not enlarge a dialog simply to + create whitespace. +- Reserve the strongest elevation for the topmost decision layer. Within a + layer, use background and hairlines—not successively larger shadows—to show + hierarchy. + +Define three size constraints for every major region: the minimum at which its +task still works, a comfortable default, and how it consumes surplus. Persist +user-controlled splits when they represent workflow preference, and clamp +restored values against the current window. + +### Alignment details + +Alignment is a structural system, not a final polish pass. Establish a small +set of alignment spines for each surface: shared leading and trailing edges, +text baselines, center lines, and fixed functional lanes. Elements at the same +level should attach to the same spine from top to bottom or leading to trailing, +even when they are different component types. + +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 require a tooltip +and an accessible name. Use filled or colored icons to communicate a state, +not merely to make a toolbar lively. + +## Layout patterns + +### Choose a stable shell + +Most applications should use one of these shells: + +- **single workspace:** toolbar or title bar above one primary view; +- **sidebar workspace:** persistent navigation beside a changing detail view; +- **master–detail:** resizable collection and detail panes; +- **document workspace:** tabs or a dock area for multiple long-lived objects; +- **utility window:** one focused task with a short, fixed action path. + +Keep global navigation stable while content changes. Give the primary work area +the remaining space with `flex_1()` and `min_w_0()` / `min_h_0()` where +overflowing children must shrink. Use `Scrollable`, `VirtualList`, `Table`, or +`DockArea` for their intended behavior instead of rebuilding scrolling or pane +management from nested `div`s. + +### Responsive desktop windows + +Desktop does not mean fixed-size. Decide what happens as a window narrows: + +1. preserve the primary task; +2. allow resizable regions to reach a documented minimum; +3. collapse secondary labels or inspectors; +4. move low-frequency actions into a menu; +5. scroll only the region whose content actually overflows. + +Do not hide an action without providing another path to it. Avoid making the +entire window scroll when only a list or document body should scroll. + +GPUI flex layouts have the same intrinsic-size pressure found in other layout +systems: a `flex_1()` child may still refuse to shrink around long content. +Design and implementation must agree on which panes may shrink, truncate, wrap, +or scroll. A clipped region also clips an outward focus ring; never trade away +keyboard visibility merely to simplify overflow. + +### Forms and settings + +Use a visible label for each field and place help or validation next to the +field it describes. Align related fields, but do not force long labels into a +narrow fixed column. Use the appropriate control: `Checkbox` for independent +choices, `RadioGroup` for a small visible set, `Select` for a longer set, and +`Switch` for a setting that takes effect immediately. + +Disable submission while an operation is in flight, keep the user's input, and +show the result near the action. Reserve dialogs for short, focused decisions; +use a full page or sheet for workflows that need exploration or many fields. + +## Components and composition + +Follow the Shadcn principle that components are building material rather than a +sealed design system. GPUI Component supplies coherent defaults, while the +application owns composition and product semantics. + +- Use component variants by meaning. Primary is reserved for the explicit + default commit in a decision area—normally the action invoked by Enter. A + lone, frequent, or desirable action is not automatically primary. An `Add` + command in a management toolbar normally uses a default Button; a form's + default `Create` commit may use primary. Use `danger` for destructive + commitment and `ghost` for quiet toolbar actions. +- Prefer explicit compound parts and render callbacks over styling arbitrary + descendants. +- Keep a repeated pattern consistent across the product. Wrap it in an + application component when it carries domain language or policy. +- Use the standard component for its semantic role. A menu, dropdown menu, + popover, select, and command palette are not interchangeable boxes; each owns + different selection, focus, keyboard, dismissal, and layout contracts. +- Preserve the component family's geometry. Menu rows share vertical and + horizontal padding, height, icon and checkmark slots, separators, radius, and + state treatment. Do not imitate one menu with a custom popup whose spacing + only approximates the system. +- Do not wrap a library component merely to rename every method or freeze all + of its capabilities. +- Move reusable behavior without product styling to `gpui-base`; keep themed, + opinionated presentation in GPUI Component or the application. + +## Interaction states + +### Make the result understandable before the click + +A control should predict its result. Use familiar desktop controls and +placement so people can act without learning the interface first. Its label +names the action and object, its state shows availability, and its feedback +confirms the same outcome. + +Do not label a Button `Save` if it opens a configuration flow, or `Delete` if +it only removes an item from a group. Name the scope when context does not make +it clear. Respond immediately to activation, prevent duplicate submission +during longer work, and show the result near the object that changed. Add a +success message only when the result itself is not visible. + +Every interactive control should be designed for: + +| State | Design requirement | +| --- | --- | +| Rest | Clear affordance without visual noise | +| Hover | Subtle pointer feedback, never the only cue | +| Pressed | Immediate press feedback | +| Open / pressed | Persistent feedback while an attached popup is open | +| Focus visible | High-contrast keyboard focus ring | +| Selected / checked | Persistent state distinct from hover | +| Disabled | Lower emphasis and no misleading hover/pressed response | +| Loading | Preserve context, prevent duplicate action, explain long waits | +| Error | State what happened and how to recover | + +Use GPUI's focus system and Actions for commands that should work from the +keyboard. Match familiar desktop shortcuts, expose shortcuts in menus or +tooltips, and keep focus in a logical place after opening or dismissing an +overlay. + +Selection is part of the information model, not optional polish. Tabs, +segmented choices, selectable rows, filters, and navigation destinations must +show a persistent selected state. A Button that owns a dropdown must remain +visibly pressed or open until the popup closes; hover alone cannot explain the +relationship between trigger and surface. + +For destructive actions, distinguish between reversible and irreversible work. +Prefer undo or a temporary notification for reversible changes. Use an +`AlertDialog` when the consequence is serious and cannot be undone; name the +specific object and consequence in the confirmation copy. + +### Pointer conventions + +Use the default arrow cursor for buttons, checkboxes, menu items, tabs, and +other native controls. Use a pointing hand for links and content that behaves +as a link. Use text, resize, grab, and prohibited cursors only when they +describe the active manipulation. A cursor reinforces an affordance; it does +not replace the control's visible state or accessible role. + +Keep hover effects modest because keyboard and accessibility interaction has no +hover. Do not reveal the only copy of a destructive or essential action on +hover. Contextual row actions may become quieter at rest if the same commands +remain available through selection, keyboard, or a context menu. + +### Prefer desktop command surfaces over hover toolbars + +Use command frequency and scope to choose where an action lives: + +- keep the primary or frequent action visible as a labeled Button or familiar + toolbar control; +- put secondary actions for the current region behind a visible + `DropdownMenu` trigger; +- put commands that act on the object under the pointer in a `ContextMenu`; +- expose the same important command through an Action/key binding when it has a + natural keyboard form; +- use a hover-revealed icon only as a shortcut to a command that remains + reachable elsewhere. + +This is more than a visual preference. GPUI Component's menu system already +owns directional keyboard navigation, confirmation and cancellation, disabled +items, separators, submenus, shortcut presentation, focus transfer and +restoration, and nested-menu dismissal. A custom strip of hover buttons must +rebuild those behaviors and is invisible to keyboard-only and many assistive +technology workflows. + +Choose `DropdownMenu` when users need a visible indication that more commands +exist—for example a toolbar overflow, document actions, or account menu. Choose +`ContextMenu` for selection- or object-scoped commands such as rename, +duplicate, reveal, or remove. The context menu must not be the only way to +perform an essential command; provide a menu-bar, toolbar, keyboard, or detail +view path as appropriate. + +Do not put every action into a menu to make a screen look minimal. Discovery +and speed matter: the main action stays visible, dangerous items remain clearly +labeled and separated, and a menu item should use the same verb, icon, shortcut, +enabled state, and result everywhere it appears. + +### Button means application action; Link means external resource + +Use a Button when activation changes application state, confirms a decision, +opens a tool, submits data, or runs a command. Choose its treatment by local +hierarchy: + +- primary Button for the one emphasized commitment in a decision area; +- default Button for ordinary visible actions; +- outline Button when an action needs a clear boundary with less emphasis; +- ghost Button for familiar, low-emphasis toolbar and inline actions; +- icon Button only for a well-known symbol, with an accessible name and tooltip. + +Do not assign primary because a Button is the only action on screen, because it +is placed at the top right, or because the team wants more clicks. Primary +communicates default commitment and keyboard behavior. If activation is merely +an ordinary command such as adding an item, opening a tool, or refreshing a +view, use a default, outline, or ghost Button according to its local hierarchy. + +Use an underlined Link only for an external resource target: a URL, web page, +online documentation, or email address. It uses the pointing-hand cursor +because its contract is leaving the current application context for that +resource. Do not use Link styling to make a functional command look quiet. A +link-shaped Delete, Save, Refresh, Add, Open-menu, or in-app navigation action +hides the control's affordance and exposes the wrong accessibility role. + +“View” does not make an in-app destination a Link. A full report, analysis, +details panel, or local record still opens through a Button, row, card, tab, or +disclosure control. Use concise context-aware labels such as `Full analysis` +when the containing card already establishes what opens; reserve underlining +for a resource that actually opens in a browser or mail client. + +All internal navigation—sidebar rows, tabs, breadcrumbs, list items, opening a +local view, or switching workspaces—must use the corresponding native component +or a Button/Action. Visual emphasis is chosen through Button variant or the +navigation component's selected state, never by lying about semantics. + +## Feedback and overlays + +Choose the smallest surface that fits the decision: + +- tooltip: a short explanation or shortcut; +- popover: contextual controls that do not interrupt the task; +- menu: a compact list of actions; +- notification: asynchronous status that does not require a decision; +- dialog: a focused decision or short form; +- alert dialog: explicit confirmation of a consequential action; +- sheet: supplementary work that benefits from more persistent space. + +An Alert interrupts the visual hierarchy even when it does not open a modal. +Use it for important, exceptional information that needs attention in the +current task, not as a decorated container for ordinary descriptions, tips, or +empty space. Prefer inline help, muted text, or a normal section when the +content does not require immediate notice or action. + +Avoid stacking overlays. Escape should dismiss the topmost dismissible layer, +and focus should return to the trigger or the next logical target. + +An overlay action must refer to an object or state the overlay actually shows. +For example, expose `Clear history` only when a distinct recent-history section +is visible and contains entries. Search results, recent items, and favorites +are different collections; label and separate them instead of merging them +into one unexplained list. Hide an inapplicable action or disable it with a +useful reason—do not park an ambiguous trash icon in a footer. + +Footer space is not a catch-all for capabilities that lacked a place in the +design. A footer may present shortcuts, status, or actions that apply to the +whole surface, but each item must answer: what is its object, why is it +available now, what scope does it affect, and what visible state changes after +activation? + +## Motion + +Motion explains change; it is not ambient decoration. Use short transitions for +appearance, dismissal, expansion, and spatial continuity. Avoid animating large +layout changes when opacity or transform communicates the same relationship. +Honor reduced-motion preferences, never require animation to understand state, +and do not add a default animation to every component. + +Motion policy belongs to the styled or application layer. Base may own the +lifecycle mechanism or geometry needed for a transition, but it should not +decide that every product fades or slides. Give independently animated values +stable identity, and make interruption reverse smoothly from the currently +sampled value rather than restarting from an old endpoint. + +## Designing data-heavy interfaces + +Dense does not mean cramped. In tables, trees, command palettes, editors, and +docks: + +- keep headers and primary row identity visually stable; +- align comparable values and use tabular numerals where appropriate; +- distinguish focus, hover, active row, and multi-selection; +- keep sorting and filtering visible and reversible; +- preserve selection by domain identity across filtering and reordering; +- virtualize large collections without changing keyboard semantics; +- use progressive disclosure for secondary columns and inspectors; +- provide a useful empty state that explains the next action. + +Choose a table for comparison across consistent fields, a list for scanning +heterogeneous items, a tree for real hierarchy, and a dock only when users need +to arrange long-lived tools or documents. Do not use a complex data component +as a visual style. + +## Interface language + +Words are part of the interface architecture. Write the vocabulary for a +feature as a system—destinations, objects, commands, states, and outcomes—not as +isolated translations of implementation features. Prefer the shortest wording +that remains accurate in its actual context. + +### Let context carry context + +Do not repeat information that the surrounding surface already establishes. A +sidebar destination is usually the object or domain itself: use `Users`, not +`User Management`; `Shortcuts`, not `Shortcut Configuration Management`. A +column whose rows already contain actions can omit a generic `Operation` +heading. A dialog titled `Delete “Roadmap”?` does not need body text that asks +the same question again. + +This is context economy, not deletion for its own sake. Add text when it changes +the decision: identify the affected scope, an irreversible consequence, an +unusual prerequisite, or a way to recover. Every extra word should answer a +question the current layout does not already answer. + +Use nouns for destinations and objects (`Users`, `Appearance`, `Orders`), verbs +for commands (`Save`, `Duplicate`, `Export`), and adjectives or short phrases +for states (`Offline`, `Up to date`, `Pending review`). Avoid wrappers such as +`Management`, `Module`, `Page`, `Function`, `Operation`, and `System` unless the +word distinguishes a real domain concept. + +### Write each language, do not translate its shape + +Start from shared intent, hierarchy, and terminology, then compose each locale +as natural interface language. Do not preserve the source language's word +order, number of words, politeness filler, or grammatical category. English +`Users` can express a Chinese feature concept that would literally expand to +“user management”; fidelity means preserving purpose, not preserving tokens. + +Remove words supplied by the enclosing information architecture. Inside a +`Settings` surface, a destination is often simply `Account`, not `Account +Settings` and never the unnatural singular `Account Setting`. The correct +English label is chosen from its role and neighbors, not from the standalone +source phrase. + +Maintain a small product lexicon for recurring objects, commands, and states. +Use the same term in the toolbar, menu, context menu, dialog, shortcut search, +and documentation unless the context genuinely changes its meaning. Review +copy in the rendered surface: neighboring labels often reveal repetition or +inconsistent scope that a locale file cannot. + +In localized technical writing, preserve an established framework term when a +translation would be less precise. Keep API identifiers in their original form +and format them as code. Do not retain ordinary foreign words merely to sound +technical. Explain a retained term on first use when needed, then use the same +form throughout the interface, documentation, and API examples. + +### Buttons and confirmation dialogs + +Button labels are short by default—usually one or two words—and describe the +result, not the gesture or the component. Prefer `Save`, `Move`, or `Delete` to +`Click to save`, `Perform move`, or `Confirm deletion`. Use `Cancel` consistently +for the action that leaves without committing. Reserve `OK` for acknowledging +purely informational content. + +Short is a default, not a character limit. A deliberately longer label is +better when its words expose a consequence or distinguish choices that users +could otherwise confuse, for example `Delete from this group` versus `Delete +everywhere`, or `Restart without saving`. Length must buy decision-critical +information; it must not restate the dialog title or body. + +Use the most specific concise result as the confirmation label when possible: + +| Context | Weak | Prefer | +| --- | --- | --- | +| Delete dialog | `Yes`, `Sure`, `Confirm deletion` | `Delete` | +| Unsaved changes | `Confirm`, `Yes` | `Discard changes` | +| Pure acknowledgement | `Confirm operation` | `OK` or `Done` | +| Complex consent whose result has no clear verb | `Yes` | `Confirm` | + +`Confirm` is a useful fallback when the surrounding dialog fully names a +complex commitment and no shorter result verb is accurate. It should not +replace a clear command. `Sure` is conversational rather than a stable English +command and is too ambiguous for the standard vocabulary. + +A confirmation dialog should form one compact decision: + +- title: the decision or condition, such as `Delete “Roadmap”?`; +- body: only new scope, consequence, or recovery information; +- actions: `Cancel` and the result, such as `Delete`; +- destructive styling: applied to the destructive result, not substituted for + precise wording. + +Avoid generic titles such as `Notice`, `Warning`, `Error`, and `Confirmation` +when the actual condition can be named. Avoid ritual phrases such as “Are you +sure you want to…”, “Would you like to…”, “Please note that…”, and “successfully” +when the structure or state already communicates them. Courtesy should come +from a calm, respectful tone, not repeated `please`. + +### Capitalization, punctuation, and symbols + +Use sentence case for English UI by default: `Reset layout`, not `Reset Layout` +or `RESET LAYOUT`. Preserve proper nouns and established acronyms. Follow a +platform convention such as title case for native menu commands only when the +platform integration benefits from it, and apply that convention consistently +within the component class. + +ALL CAPS can provide restrained typographic emphasis for very short section +labels, eyebrows, statuses, established acronyms, and code-like identifiers. +Its compact shape and measured tracking can form a level similar to bold type, +but it does not belong on Buttons, long headings, sentences, or dense lists. Do +not combine uppercase, strong color, and bold weight in the same region, and do +not transform every string automatically: product names, acronyms, and localized +content must preserve their intended casing. + +Labels, buttons, menu items, tabs, headings, placeholders, and short states do +not take a final period. Complete explanatory, warning, and error sentences do. +Avoid exclamation marks in routine success and failure messages. In Chinese, +use full-width punctuation in sentences and omit terminal punctuation from +short control labels by the same semantic rule. + +Use the single ellipsis character (`…`), not three periods. Append it to every +Button or MenuItem that opens a dialog, sheet, or separate window, and to a +command that requires more input or choices before it can complete, such as +`Settings…` or `Export…`. An immediately executed command does not take an +ellipsis. Use an indeterminate progress indicator, not decorative dots, to +communicate ongoing work. + +Errors should say what happened and, when useful, the next recovery action. +Success feedback should name the resulting state only when that state is not +already visible. Prefer `Couldn’t save. Check your connection and try again.` +to a technical code or a long apology; omit a `Saved successfully` toast when +the document visibly becomes saved. + +## Internationalization and platform fit + +Copy must survive expansion, CJK typography, and different shortcut notation. +Do not size a control from one English label. Keep text out of raster assets, +avoid concatenating translated fragments, and let labels wrap or truncate only +where the product defines a recovery path such as a tooltip. + +Respect platform differences that carry meaning: Command versus Control, +native window decorations, system appearance, scrollbar behavior, menus, and +notification capabilities. Keep the product's information architecture stable +across platforms, but do not erase familiar platform behavior for superficial +pixel equality. + +## Guidance for AI-generated interfaces + +An AI changing a GPUI interface should first inspect the nearest feature, +theme tokens, and component documentation. It should state the primary task, +state owner, component composition, and keyboard path before generating code. +It must not infer an API from React/Shadcn examples or invent a GPUI method +because the name seems plausible. + +AI output is incomplete until a human can explain why the hierarchy, density, +component choice, and exceptional literal values belong in this product. A +visually plausible screenshot is not proof: keyboard behavior, focus, dynamic +content, themes, resizing, and failure states are part of the design. + +## Accessibility checklist + +Before considering a screen complete, verify that: + +- every action is reachable and operable by keyboard; +- focus order follows visual and task order; +- focus remains visible and is restored after overlays; +- controls have names, and icon-only controls have tooltips; +- text and meaningful boundaries have sufficient contrast; +- status is not communicated by color alone; +- disabled and read-only states are distinguishable; +- labels, errors, and descriptions remain near their controls; +- content remains usable with longer translations and larger text; +- pointer targets are comfortably sized even in a dense layout. + +## Design review checklist + +A review does not inventory components; it judges whether the interface made +the right decisions. Ask, in order: + +1. **Is the task clear?** Can a new user recognize the purpose, primary action, + and next step without learning, guessing, or experimenting? +2. **Does every action keep its promise?** Do the label, control, state, scope, + feedback, and result describe one consistent outcome? +3. **Is hierarchy decisive and restrained?** Does the core feature receive the + space it deserves while strong color, bold type, badges, alerts, and primary + Buttons remain scarce? +4. **Could the interface do less, better?** Can an entry point, option, or state + be removed, combined, or deferred without weakening the complete task? +5. **Is the structure exact?** Do peers share alignment spines, equal gaps stay + equal to the rendered pixel, and scrollbars sit at the edge of their actual + scrolling region? +6. **Does it follow the component system?** Do standard controls retain their + geometry, states, keyboard behavior, and dismissal model, with appearance + supplied by theme and scale tokens? +7. **Does it remain usable in every state and constraint?** Verify keyboard and + focus behavior, empty/loading/failure/permission states, longer translations, + zoom, minimum window size, and reduced motion. +8. **Has it been tested in a real window?** Complete the task with real + components, copy, and representative content—not only an ideal screenshot. + +Continue with [Coding Guides](./coding-guides.md) to translate these design +decisions into GPUI architecture and code. diff --git a/.claude/skills/gpui-component/references/style-guide.md b/.claude/skills/gpui-component/references/style-guide.md new file mode 100644 index 0000000..40b797c --- /dev/null +++ b/.claude/skills/gpui-component/references/style-guide.md @@ -0,0 +1,364 @@ +# GPUI Component Code Style Guide + +Based on analysis of `Button`, `Checkbox`, `Input`, `Select`, and other components in `crates/ui/src`. + +**Contents:** [Component Structure](#component-structure) · [Required Traits](#required-trait-implementations) · [Optional Traits](#optional-traits) · [Variants Pattern](#variants-pattern) · [Callback Signatures](#callback-signatures) · [Import Organization](#import-organization) · [Doc Comments](#doc-comments) · [Applying User Style Overrides](#applying-user-style-overrides) · [FluentBuilder Conditionals](#fluentbuilder-for-conditionals) · [Theme Colors](#theme-colors) · [Size Handling](#size-handling) · [Checklist](#checklist-for-new-components) + +## Component Structure + +### Standard Stateless Component + +```rust +use std::rc::Rc; + +use crate::{ActiveTheme, Disableable, Sizable, Size, StyledExt as _, /* ... */}; +use gpui::{ + AnyElement, App, Div, ElementId, InteractiveElement, IntoElement, + ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, + StyleRefinement, Styled, Window, div, prelude::FluentBuilder as _, +}; + +/// A MyComponent element. +#[derive(IntoElement)] +pub struct MyComponent { + // 1. Identity + id: ElementId, + base: Div, + style: StyleRefinement, + + // 2. Configuration + size: Size, + disabled: bool, + selected: bool, + tab_stop: bool, + tab_index: isize, + + // 3. Content + label: Option, + children: Vec, + + // 4. Callbacks (last) + on_click: Option>, +} + +impl MyComponent { + /// Create a new MyComponent with the given id. + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + base: div(), + style: StyleRefinement::default(), + size: Size::default(), + disabled: false, + selected: false, + tab_stop: true, + tab_index: 0, + label: None, + children: Vec::new(), + on_click: None, + } + } + + /// Set the label. + pub fn label(mut self, label: impl Into) -> Self { + self.label = Some(label.into()); + self + } + + /// Set the click handler. + pub fn on_click(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { + self.on_click = Some(Rc::new(handler)); + self + } +} +``` + +### Stateful Component (Interactive, Needs `.id()`) + +Components with mouse interactions (hover, click tracking) use `Stateful
`: + +```rust +use gpui::{Stateful, StatefulInteractiveElement as _, /* ... */}; + +#[derive(IntoElement)] +pub struct Button { + id: ElementId, + base: Stateful
, // Not Div — needs stateful for interaction tracking + // ... +} + +impl Button { + pub fn new(id: impl Into) -> Self { + let id = id.into(); + Self { + id: id.clone(), + base: div().flex_shrink_0().id(id), // .id() makes it Stateful
+ // ... + } + } +} + +impl InteractiveElement for Button { + fn interactivity(&mut self) -> &mut Interactivity { + self.base.interactivity() + } +} +``` + +--- + +## Required Trait Implementations + +```rust +// All components that accept children +impl ParentElement for MyComponent { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements) + } +} + +// All components with styleable outer div +impl Styled for MyComponent { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} + +// For interactive components (mouse events, hover, click) +impl InteractiveElement for MyComponent { + fn interactivity(&mut self) -> &mut Interactivity { + self.base.interactivity() + } +} + +// Required if InteractiveElement is implemented +impl StatefulInteractiveElement for MyComponent {} + +// Rendering +impl RenderOnce for MyComponent { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + self.base + .id(self.id) + // Apply user style overrides last + .refine_style(&self.style) + .children(self.children) + } +} +``` + +--- + +## Optional Traits + +```rust +impl Disableable for MyComponent { + fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} + +impl Selectable for MyComponent { + fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } + fn is_selected(&self) -> bool { + self.selected + } +} + +impl Sizable for MyComponent { + fn with_size(mut self, size: impl Into) -> Self { + self.size = size.into(); + self + } +} +``` + +Implementing `Sizable` gives `.xsmall()`, `.small()`, `.medium()`, `.large()` for free via `StyleSized`. + +--- + +## Variants Pattern + +Use a `Variants` trait with default method impls: + +```rust +#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)] +pub enum AlertVariant { + #[default] + Info, + Success, + Warning, + Error, +} + +pub trait AlertVariants: Sized { + fn with_variant(self, variant: AlertVariant) -> Self; + + fn info(self) -> Self { self.with_variant(AlertVariant::Info) } + fn success(self) -> Self { self.with_variant(AlertVariant::Success) } + fn warning(self) -> Self { self.with_variant(AlertVariant::Warning) } + fn error(self) -> Self { self.with_variant(AlertVariant::Error) } +} + +impl AlertVariants for MyAlert { + fn with_variant(mut self, variant: AlertVariant) -> Self { + self.variant = variant; + self + } +} +``` + +--- + +## Callback Signatures + +```rust +// Click event (ClickEvent first) +on_click: Option> + +// State change (state value first) +on_change: Option> +on_change: Option> +on_change: Option> +``` + +Always `Rc` — components are cloned and called multiple times. + +--- + +## Import Organization + +```rust +// 1. std +use std::rc::Rc; + +// 2. crate imports (project internals) +use crate::{ + ActiveTheme, Disableable, Icon, IconName, + Selectable, Sizable, Size, StyledExt as _, + h_flex, v_flex, +}; + +// 3. gpui imports +use gpui::{ + AnyElement, App, Div, ElementId, InteractiveElement, IntoElement, + ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, + StyleRefinement, Styled, Window, div, + prelude::FluentBuilder as _, + px, rems, relative, +}; +``` + +--- + +## Doc Comments + +```rust +/// A Checkbox element. ← struct: one-line with capital, period +#[derive(IntoElement)] +pub struct Checkbox { ... } + +impl Checkbox { + /// Create a new Checkbox with the given id. ← constructor + pub fn new(id: impl Into) -> Self { ... } + + /// Set the label for the checkbox. ← setter + pub fn label(mut self, label: impl Into) -> Self { ... } + + /// Set the click handler for the checkbox. + /// + /// The `&bool` parameter indicates the new checked state after the click. + pub fn on_click(mut self, ...) -> Self { ... } +} +``` + +- Struct doc: `/// A {Name} element.` +- Constructor: `/// Create a new {Name} with the given id.` +- Setters: `/// Set the {field}.` +- No redundant comments — only document non-obvious behavior + +--- + +## Applying User Style Overrides + +Use `refine_style` to merge user's `Styled` calls onto the root element: + +```rust +impl RenderOnce for MyComponent { + fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { + div() + .flex() + .items_center() + // Apply component defaults first, then user overrides + .refine_style(&self.style) + .children(self.children) + } +} +``` + +--- + +## FluentBuilder for Conditionals + +```rust +div() + .when(self.disabled, |this| this.opacity(0.5).cursor_not_allowed()) + .when(self.selected, |this| this.bg(cx.theme().primary)) + .when_some(self.label.as_ref(), |this, label| { + this.child(div().child(label.clone())) + }) +``` + +Always `use gpui::prelude::FluentBuilder as _;` for `.when()` / `.when_some()`. + +--- + +## Theme Colors + +```rust +// In render, access via cx.theme() (requires ActiveTheme import) +use crate::ActiveTheme; + +div() + .bg(cx.theme().surface) + .text_color(cx.theme().foreground) + .border_color(cx.theme().border) + .when(is_active, |el| el.bg(cx.theme().primary)) +``` + +--- + +## Size Handling + +```rust +// Get pixel values based on Size +let (width, height) = self.size.input_size(); + +// Or use match +let font_size = match self.size { + Size::XSmall => rems(0.75), + Size::Small => rems(0.875), + Size::Medium | Size::Size(_) => rems(1.0), + Size::Large => rems(1.125), +}; +``` + +--- + +## Checklist for New Components + +- [ ] `#[derive(IntoElement)]` +- [ ] Fields: `id: ElementId`, `base: Div` (or `Stateful
`), `style: StyleRefinement` +- [ ] `impl RenderOnce` — calls `.refine_style(&self.style)` on root element +- [ ] `impl Styled` returning `&mut self.style` +- [ ] `impl ParentElement` if accepts children +- [ ] `impl InteractiveElement` + `StatefulInteractiveElement` if interactive +- [ ] `impl Sizable` if has size variants +- [ ] `impl Disableable` if can be disabled +- [ ] `impl Selectable` if can be selected +- [ ] Callbacks as `Option>` +- [ ] Doc comment on struct and public methods +- [ ] Import `prelude::FluentBuilder as _` diff --git a/.claude/skills/gpui-component/references/usage.md b/.claude/skills/gpui-component/references/usage.md new file mode 100644 index 0000000..d09d137 --- /dev/null +++ b/.claude/skills/gpui-component/references/usage.md @@ -0,0 +1,402 @@ +# gpui-component Usage Guide + +**Contents:** [Setup](#setup) · [Component Types](#component-types) · [Common Components](#common-components) (Button, Input, Select, Checkbox, Icon, Dialog, Notification, Tabs, Tooltip, Form, List) · [Theming](#theming) · [Layout Helpers](#layout-helpers) · [Overlay Layers](#overlay-layers-dialogs-sheets-notifications) · [Shared Traits](#shared-traits) + +## Setup + +### 1. Cargo.toml + +```toml +[dependencies] +gpui = { git = "https://github.com/zed-industries/zed" } +gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit"] } +gpui-component = { git = "https://github.com/longbridge/gpui-component" } +gpui-component-assets = { git = "https://github.com/longbridge/gpui-component" } # optional icons +``` + +### 2. Initialization + +```rust +fn main() { + gpui_platform::application() + .with_assets(gpui_component_assets::Assets) + .run(move |cx| { + gpui_component::init(cx); // MUST be first + + cx.spawn(async move |cx| { + cx.open_window(WindowOptions::default(), |window, cx| { + let view = cx.new(|_| MyApp); + cx.new(|cx| Root::new(view, window, cx)) // Root wraps first view + }).expect("Failed to open window"); + }).detach(); + }); +} +``` + +**`Root` is required** as the first-level child of every window — it enables dialogs, sheets, and notifications. + +--- + +## Component Types + +### Stateless (most components) + +Used directly in `render`, no stored state: + +```rust +use gpui_component::button::Button; + +impl Render for MyView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + Button::new("btn").primary().label("Submit") + .on_click(|_, _, _| println!("clicked")) + } +} +``` + +### Stateful (Input, Select, Combobox, etc.) + +Require an `Entity` stored in your view: + +```rust +use gpui_component::input::{Input, InputState}; + +struct MyView { + name: Entity, +} + +impl MyView { + fn new(window: &mut Window, cx: &mut Context) -> Self { + Self { + name: cx.new(|cx| InputState::new(window, cx).placeholder("Your name")), + } + } +} + +impl Render for MyView { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + Input::new(&self.name) + } +} +``` + +--- + +## Common Components + +### Button + +```rust +use gpui_component::button::{Button, ButtonGroup}; + +// Variants +Button::new("btn").label("Default") +Button::new("btn").primary().label("Primary") +Button::new("btn").danger().label("Delete") +Button::new("btn").warning().label("Warning") +Button::new("btn").success().label("Success") +Button::new("btn").ghost().label("Ghost") +Button::new("btn").link().label("Link") + +// States +Button::new("btn").label("Text").disabled(true) +Button::new("btn").label("Text").loading(true) +Button::new("btn").label("Text").selected(true) + +// With icon +Button::new("btn").icon(IconName::Plus).label("Add") + +// Sizes +Button::new("btn").xsmall().label("XS") +Button::new("btn").small().label("S") +Button::new("btn").large().label("L") + +// Group +ButtonGroup::new("group") + .child(Button::new("a").label("A")) + .child(Button::new("b").label("B")) + .on_click(|indices, _, _| { /* selected indices */ }) +``` + +### Input + +```rust +use gpui_component::input::{Input, InputState}; + +// State setup (in new/init) +let input = cx.new(|cx| InputState::new(window, cx) + .placeholder("Enter text...") + .default_value("Hello") +); + +// Render +Input::new(&input) +Input::new(&input).cleanable(true) // clear button +Input::new(&input).disabled(true) +Input::new(&input).prefix(Icon::new(IconName::Search).small()) +Input::new(&input).suffix(Button::new("b").ghost().icon(IconName::X).xsmall()) +Input::new(&input).content_type(InputContentType::Password) +Input::new(&input).mask_toggle() // password reveal toggle +Input::new(&input).appearance(false) // remove default border/bg + +// Reading value +let value = input.read(cx).value(); + +// Events +cx.subscribe_in(&input, window, |view, state, event, window, cx| { + match event { + InputEvent::Change => { let v = state.read(cx).value(); } + InputEvent::PressEnter { .. } => { /* submit */ } + InputEvent::Focus | InputEvent::Blur => {} + } +}); +``` + +### Select + +```rust +use gpui_component::select::{Select, SelectState}; + +// Simple string list +let state = cx.new(|cx| { + SelectState::new(vec!["Apple", "Orange", "Banana"], Some(IndexPath::default()), window, cx) +}); + +// Render +Select::new(&state) +Select::new(&state).placeholder("Pick one") + +// Reading selection +let selected = state.read(cx).selected_item(); +``` + +### Checkbox / Switch / Radio + +```rust +use gpui_component::{Checkbox, Switch}; + +// Stateless (controlled) +Checkbox::new("cb").checked(self.checked) + .on_click(|checked, _, cx| { /* &bool */ }) + +Switch::new("sw").checked(self.enabled) + .on_click(|checked, _, cx| {}) +``` + +### Icon + +```rust +use gpui_component::{Icon, IconName}; + +Icon::new(IconName::Check) +Icon::new(IconName::Search).small() +Icon::new(IconName::Plus).large().text_color(cx.theme().primary) +``` + +### Dialog + +```rust +use gpui_component::dialog::{Dialog, DialogAction, DialogClose, DialogFooter}; + +// Open from window context. `footer` takes an element, not a closure. +// DialogClose dismisses the dialog, so no manual close call is needed. +window.open_dialog(cx, |dialog, _, _| { + dialog + .title("Export Report") + .child("Choose a destination for the exported file.") + .footer( + DialogFooter::new() + .gap_2() + .child(DialogClose::new().child( + Button::new("cancel").label("Cancel").outline(), + )) + .child(DialogAction::new().child( + Button::new("export").label("Export").primary(), + )), + ) +}); +``` + +### AlertDialog + +Use `AlertDialog` — not `Dialog` — to confirm a consequential action. It is not +overlay-closable and has no close button, so the choice must be made. Name the +object in the title and the result on the confirming button; see the Design +Guides for the copy rules. + +```rust +use gpui_component::{button::ButtonVariant, dialog::DialogButtonProps}; + +window.open_alert_dialog(cx, |alert, _, _| { + alert + .title("Remove “Roadmap”?") + .description("Files on disk aren’t deleted.") + .button_props( + DialogButtonProps::default() + .ok_text("Remove") + .ok_variant(ButtonVariant::Danger) + .on_ok(|_, _, _| true), + ) +}); +``` + +### Notification + +```rust +// Simple string message +window.push_notification("Saved successfully!", cx); + +// With type variant +window.push_notification( + Notification::new("Upload complete").info().message("File uploaded"), + cx, +); +``` + +### Tabs + +```rust +use gpui_component::tab::{Tab, TabBar}; + +TabBar::new("tabs") + .child(Tab::new("tab1").child("Overview")) + .child(Tab::new("tab2").child("Settings")) + .child(Tab::new("tab3").child("Logs")) +``` + +### Tooltip + +```rust +// On any element with .id(), add .tooltip(): +div() + .id("my-btn") + .tooltip(|window, cx| Tooltip::new("Delete item").build(window, cx)) + .child("Delete") + +// Or on a Button directly: +Button::new("btn").icon(IconName::Trash).tooltip("Delete") +``` + +### Form + +```rust +use gpui_component::form::{v_form, h_form, field}; + +// Vertical form +v_form() + .child(field().label("Name").child(Input::new(&self.name))) + .child(field().label("Email").child(Input::new(&self.email))) + .child(Button::new("submit").primary().label("Submit")) + +// Horizontal label alignment +h_form() + .child(field().label("Username").child(Input::new(&self.username))) +``` + +### List (searchable, virtualized) + +```rust +use gpui_component::list::{List, ListState, ListDelegate, ListItem, ListEvent}; + +// Implement ListDelegate for your data type, then: +let list_state = cx.new(|cx| ListState::new(MyDelegate::new(), window, cx)); + +// Render +List::new(&list_state) +// Events +cx.subscribe(&list_state, |this, _, event, cx| { + if let ListEvent::Select(index_path) = event { + // handle selection + } +}); +``` + +--- + +## Theming + +```rust +use gpui_component::ActiveTheme as _; + +// Access colors +cx.theme().primary +cx.theme().background +cx.theme().foreground +cx.theme().border +cx.theme().surface +cx.theme().muted +cx.theme().destructive + +// Use in styles +div() + .bg(cx.theme().surface) + .text_color(cx.theme().foreground) + .border_color(cx.theme().border) +``` + +### Switch Theme + +```rust +use gpui_component::Theme; + +// Toggle light/dark +cx.update_global::(|theme, cx| { + theme.toggle_mode(cx); +}); + +// Load a named theme +Theme::global_mut(cx).apply_config(&theme_config); +``` + +--- + +## Layout Helpers + +gpui-component extends GPUI with convenient layout methods: + +```rust +h_flex() // div().flex().flex_row().items_center() +v_flex() // div().flex().flex_col() + +// Common patterns +h_flex().gap_2().items_center() + .child(Icon::new(IconName::User)) + .child(label("Username")) + +v_flex().gap_4().p_4() + .child(Input::new(&self.name)) + .child(Input::new(&self.email)) + .child(Button::new("submit").primary().label("Submit")) +``` + +--- + +## Overlay Layers (Dialogs, Sheets, Notifications) + +To render overlays, add these to your first-level view's render: + +```rust +impl Render for MyApp { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .size_full() + .child(self.main_content(window, cx)) + .children(Root::render_dialog_layer(cx)) + .children(Root::render_sheet_layer(cx)) + .children(Root::render_notification_layer(cx)) + } +} +``` + +--- + +## Shared Traits + +All components follow the builder pattern `Component::new("id").method().method()`: +- `Sizable`: `.xsmall()` / `.small()` / `.medium()` (default) / `.large()` +- `Disableable`: `.disabled(bool)` +- `Selectable`: `.selected(bool)` +- `Styled`: any GPUI style methods (`.w()`, `.bg()`, `.p_2()`, etc.) + +For any component not covered here, fetch its doc from: +`https://longbridge.github.io/gpui-component/docs/components/{name}.md` From 476cd3734a5aac99a95d3b2388182f071d9fb019 Mon Sep 17 00:00:00 2001 From: JetSquirrel Date: Fri, 28 Aug 2026 13:24:16 +0800 Subject: [PATCH 3/6] Bump gpui-component to 0.5.1 Cargo.toml still asked for 0.5.0-preview1 while the lockfile had already moved to preview2, which is what broke the input API. Move both the manifest and the lockfile onto the 0.5.1 release so they agree. No source changes needed; fmt, clippy, check and test all pass. Co-Authored-By: Claude Opus 5 (1M context) --- Cargo.lock | 13 +++++++------ Cargo.toml | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 389b41b..416d176 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2718,13 +2718,14 @@ dependencies = [ [[package]] name = "gpui-component" -version = "0.5.0-preview2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b600febdc128afd24febf1bc4fa3a6b704e6d8521e67aa5e5a245117ff6a1b9" +checksum = "d021d46b4088d3d93a57ccdf443da85695a77272108caca2f6fe5369f584966a" dependencies = [ "aho-corasick", "anyhow", "chrono", + "core-text", "enum-iterator", "gpui", "gpui-component-macros", @@ -2757,9 +2758,9 @@ dependencies = [ [[package]] name = "gpui-component-assets" -version = "0.5.0-preview2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a00977ad46e08cfafb0e500647fce9b2a8de0e04daca9890860f1b9dc889c3" +checksum = "afc6e4c6551a1a12d4e8b69c3e8eba3cef43331c8c87898a0d4d040c78c6865e" dependencies = [ "anyhow", "gpui", @@ -2768,9 +2769,9 @@ dependencies = [ [[package]] name = "gpui-component-macros" -version = "0.5.0-preview2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c73cc07eab9c69be8858f156043f635ba257ce5d28ddfb1b7586350bdeb918c4" +checksum = "86fbc2d84bf91717b171320e6adc600d91ccb3ed259448f3b006787633c1c615" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index ec1d6f4..e0623fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,8 @@ rust-version = "1.75" [dependencies] # GPUI framework gpui = "0.2" -gpui-component = "0.5.0-preview1" -gpui-component-assets = "0.5.0-preview1" +gpui-component = "0.5.1" +gpui-component-assets = "0.5.1" # Sync HTTP client (avoids aws-lc-sys compilation issues) ureq = { version = "3", features = ["json"] } From a2c6cc781defd1f592dee7ca2a42c4b5147d44e2 Mon Sep 17 00:00:00 2001 From: JetSquirrel Date: Fri, 28 Aug 2026 13:30:23 +0800 Subject: [PATCH 4/6] Fix illegible active item in the sidebar `accent` is a surface token, so using it as the label color painted the selected nav item light grey on light grey. Mirror what the library's own SidebarMenuItem does: sidebar_accent for the background, sidebar_accent_foreground for the label, sidebar_foreground when inactive. Also move the sidebar chrome onto sidebar_border / sidebar_foreground so the whole panel uses one token family. Co-Authored-By: Claude Opus 5 (1M context) --- src/app.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/app.rs b/src/app.rs index 7d25291..97e5256 100644 --- a/src/app.rs +++ b/src/app.rs @@ -46,7 +46,7 @@ impl CloudBridgeApp { .w(px(220.0)) .h_full() .border_r_1() - .border_color(cx.theme().border) + .border_color(cx.theme().sidebar_border) .bg(cx.theme().sidebar) .p_4() .v_flex() @@ -55,11 +55,11 @@ impl CloudBridgeApp { div() .text_xl() .font_weight(FontWeight::BOLD) - .text_color(cx.theme().foreground) + .text_color(cx.theme().sidebar_foreground) .child("CloudBridge") .pb_4() .border_b_1() - .border_color(cx.theme().border) + .border_color(cx.theme().sidebar_border) .mb_4(), ) .child(self.nav_item( @@ -92,16 +92,18 @@ impl CloudBridgeApp { is_active: bool, cx: &Context, ) -> impl IntoElement { + // Mirrors gpui-component's own SidebarMenuItem: `accent` is a surface + // token, so the active label has to use `sidebar_accent_foreground`. let bg = if is_active { - cx.theme().accent.opacity(0.1) + cx.theme().sidebar_accent } else { transparent_black() }; let text_color = if is_active { - cx.theme().accent + cx.theme().sidebar_accent_foreground } else { - cx.theme().foreground + cx.theme().sidebar_foreground }; div() @@ -111,7 +113,7 @@ impl CloudBridgeApp { .rounded_md() .cursor_pointer() .bg(bg) - .hover(|s| s.bg(cx.theme().accent.opacity(0.05))) + .hover(|s| s.bg(cx.theme().sidebar_accent.opacity(0.8))) .text_color(text_color) .child(label) .on_click(cx.listener(move |this, _, _, cx| { From ed616d640866c7225d265b052ffb4112c75dd889 Mon Sep 17 00:00:00 2001 From: JetSquirrel Date: Fri, 28 Aug 2026 13:33:27 +0800 Subject: [PATCH 5/6] Fix version display, drop the inert refresh-interval row The About section had 0.1.0 hardcoded; read CARGO_PKG_VERSION instead so it tracks the manifest. AppConfig derived Default, so refresh_interval_minutes came out as 0 and Settings advertised "0 minutes". Give it a real default of 60 and repair configs already written with 0. Nothing schedules a refresh from that value, though, so the Data section only promised behaviour the app does not have. Remove it until the interval is actually wired to something, and say so on the field. Co-Authored-By: Claude Opus 5 (1M context) --- src/config.rs | 27 ++++++++++++++++++++++++--- src/ui/settings.rs | 25 +------------------------ 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/config.rs b/src/config.rs index d19d4e2..34c3256 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,17 +6,33 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; +/// Default data refresh interval, in minutes +pub const DEFAULT_REFRESH_INTERVAL_MINUTES: u32 = 60; + /// Application configuration -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppConfig { /// Encryption key (for encrypting AK/SK) pub encryption_key: Option, /// Theme settings pub theme: ThemeConfig, - /// Data refresh interval (minutes) + /// Data refresh interval (minutes). + /// + /// Persisted but not acted on yet: nothing schedules a refresh from it, + /// so it has no Settings UI either. Wire both up together. pub refresh_interval_minutes: u32, } +impl Default for AppConfig { + fn default() -> Self { + Self { + encryption_key: None, + theme: ThemeConfig::default(), + refresh_interval_minutes: DEFAULT_REFRESH_INTERVAL_MINUTES, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ThemeConfig { /// Whether to use dark mode @@ -64,7 +80,12 @@ pub fn load_config() -> Result { if config_path.exists() { let content = fs::read_to_string(&config_path)?; - let config: AppConfig = serde_json::from_str(&content)?; + let mut config: AppConfig = serde_json::from_str(&content)?; + // Configs written before AppConfig had a real Default stored 0 here, + // which is not a usable interval. + if config.refresh_interval_minutes == 0 { + config.refresh_interval_minutes = DEFAULT_REFRESH_INTERVAL_MINUTES; + } Ok(config) } else { // Return default config diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 7f0b901..3391f39 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -110,29 +110,6 @@ impl Render for SettingsView { cx, ), ) - // Data settings - .child( - self.render_section( - "Data", - div().v_flex().gap_3().child( - div().h_flex().justify_between().items_center().child( - div() - .v_flex() - .child(div().child("Data Refresh Interval")) - .child( - div() - .text_sm() - .text_color(cx.theme().muted_foreground) - .child(format!( - "{} minutes", - self.config.refresh_interval_minutes - )), - ), - ), - ), - cx, - ), - ) // About .child( self.render_section( @@ -149,7 +126,7 @@ impl Render for SettingsView { .text_color(cx.theme().muted_foreground) .child("Version:"), ) - .child(div().child("0.1.0")), + .child(div().child(env!("CARGO_PKG_VERSION"))), ) .child( div() From 91b6822695479cb7d969377adda6a1d420f7e79d Mon Sep 17 00:00:00 2001 From: JetSquirrel Date: Fri, 28 Aug 2026 15:04:21 +0800 Subject: [PATCH 6/6] Make the Dark Mode switch actually change the theme toggle_dark_mode only wrote the flag to disk; nothing ever called into the theme system, so the switch was inert and the stored preference was never applied. Call Theme::change on toggle, passing the window so it repaints, and apply the saved preference in main() before the first window opens. Drive the switch off the checked value the component hands back instead of flipping our own copy, so the two cannot drift apart. Default to the light theme; the previous dark default had never actually taken effect, so turning the preference on would have changed behaviour for everyone on first launch. Co-Authored-By: Claude Opus 5 (1M context) --- src/config.rs | 10 ++-------- src/main.rs | 13 +++++++++++++ src/ui/settings.rs | 17 +++++++++++++---- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/config.rs b/src/config.rs index 34c3256..fa63769 100644 --- a/src/config.rs +++ b/src/config.rs @@ -33,18 +33,12 @@ impl Default for AppConfig { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ThemeConfig { - /// Whether to use dark mode + /// Whether to use dark mode. Defaults to light. pub dark_mode: bool, } -impl Default for ThemeConfig { - fn default() -> Self { - Self { dark_mode: true } - } -} - /// Get application data directory pub fn get_app_data_dir() -> Result { // Use simpler path: AppData/Roaming/CloudBridge/ on Windows diff --git a/src/main.rs b/src/main.rs index 5e61a52..c3a45bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,6 +35,19 @@ fn main() { // Initialize GPUI Component gpui_component::init(cx); + // Apply the persisted theme before the first window opens, so the app + // does not flash the default appearance on startup. + let dark_mode = config::load_config().unwrap_or_default().theme.dark_mode; + Theme::change( + if dark_mode { + ThemeMode::Dark + } else { + ThemeMode::Light + }, + None, + cx, + ); + cx.spawn(async move |cx| { // Initialize database if let Err(e) = db::init_database() { diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 3391f39..49ea5b7 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -24,8 +24,17 @@ impl SettingsView { } } - fn toggle_dark_mode(&mut self, cx: &mut Context) { - self.config.theme.dark_mode = !self.config.theme.dark_mode; + fn set_dark_mode(&mut self, dark: bool, window: &mut Window, cx: &mut Context) { + self.config.theme.dark_mode = dark; + Theme::change( + if dark { + ThemeMode::Dark + } else { + ThemeMode::Light + }, + Some(window), + cx, + ); self.save_config(cx); } @@ -103,8 +112,8 @@ impl Render for SettingsView { .child( Switch::new("dark-mode") .checked(dark_mode) - .on_click(cx.listener(|this, _, _, cx| { - this.toggle_dark_mode(cx); + .on_click(cx.listener(|this, checked: &bool, window, cx| { + this.set_dark_mode(*checked, window, cx); })), ), cx,