8.4 KiB
8.4 KiB
AGENTS.md
Ely GPUI Component. A component library for GPUI, in light and dark.
Stack
- Rust 1.95, edition 2024. One crate:
ely-gpui-component. - gpui 0.2.2 from crates.io. Feature
runtime_shadersis on by default: machines without Xcode lack the Metal compiler. - Assets, embedded with
rust-embed: Lucide 1.48.0 icons (ISC), Inter 4.1 and JetBrains Mono 2.304 (OFL). - Catalog data:
emojis(Unicode emoji),isolang(ISO 639, native names),isocountry(ISO 3166),iso_currency(ISO 4217, also minor units for money). - macOS extras (tray icon, Dock badge) call AppKit through
cocoa0.26 andobjc0.2, the crates gpui already links. - Gallery:
examples/gallery. Website:frontend/(Vite 8, pnpm), built after the components.
Commands
cargo run --example galleryopens the gallery.-- --page <slug>starts on a page.cargo run --example gallery -- --capture <dir>writes PNGs of every page, light and dark, top to bottom, then each page's scripted states, including windows the demos open. macOS only.cargo test --lib --features test-support,cargo clippy --all-targets --features test-support -- -D warnings,cargo fmt --check.scripts/check.shruns them with the house rules.rm -rf target/debug/incrementalafter each task item keeps the disk lean.scripts/icons.sh <lucide-name>...adds icons. Then add a line toIconNameinsrc/primitives/icon.rs.
Layout
src/<chapter>/: one folder per chapter ofgpui-components.md.src/theme: palettes, tokens,ActiveTheme.src/motion: durations, easings, spring.examples/gallery/pages/<chapter>.rs: one page per chapter, in PRD order.TASKS.mdandtasks/: progress. Every PRD entry has a line and a tag.
Rules
- Colors, control and icon sizes, text sizes, radii, shadows come from
cx.theme(). No rawpx()literals inside components. - Gaps and padding use gpui's rem scale helpers (
gap_2,px_3). That scale is the spacing system. - Borders use gpui's fixed-pixel helpers (
border_1is 1px). Hairlines do not scale. - One component, one home. Duplicates point to it.
- Fail fast. No silent fallbacks. Log state changes with
log. - Files stay at or under 500 lines. Comments are one short line, and rare.
motion::duration()honors reduced motion. Springs overshoot: use them inside animators, never as gpui easings, because gpui asserts eased values stay in0..=1.svg()paints only with its owntext_color.Iconalways sets one.- Buttons stay out of focus on click (
prevent_defaulton mouse down). Tab still reaches them. - The focus ring is a 1px focus-colored border (
FocusRing). gpui paints shadows under the element, so a shadow ring floods transparent elements. Focusable elements keep a 1px border, transparent at rest. ely_gpui_component::initbinds Tab and Shift-Tab toFocusNextandFocusPrev. Wrap the root inFocusScopeand focus its handle.- gpui's
Window::dispatch_eventreturns a private type, so the gallery scripts mouse input by postingNSEvents to its own queue. hover()needs an element id.- gpui sends a drag's moves to every listener of its type. Each drag payload carries its owner's
EntityId, and handlers check it. - A component that takes a starting value from its owner keeps it in
layout::seeded: a new value from the owner replaces local drags. - The capture harness finds gpui windows by handle through
raw-window-handle. AppKit popups, which gpui does not own, come from the window list by process and level. - Posted events cannot move a macOS window, so window drags are not scripted.
drag_regionfollows Zed's title bar. - An overlay takes focus with
primitives::take_focusand returns it withgive_backon every close path. - A state change made outside render calls
cx.notify(), or nothing redraws. - The macOS share picker draws its content in another process; a capture shows only its frame.
- NSColorSampler runs in another process too. The harness cannot pick or cancel with it;
read_srgbis tested on a constructedNSColorinstead. - Every text field is a
forms::TextInputentity. Its keys live under theElyInputcontext, bound ininit. Wrappers take Up, Down, Enter and Backspace first withcapture_action. TextInput::bounds_forreads the last layout. Text changed this frame clamps to the laid-out end until the next paint.- The harness types with
dispatch_keystrokethroughAnyWindowHandle, which leaves the root view free to redraw. - The macOS open panel runs out of process too. Escape does not reach it; the harness sends it
cancel:. - gpui's
test-supportswaps its executor:blockfails on a real dispatcher. It stays behind this crate'stest-supportfeature, never on for the gallery. - A tracked
FocusHandleis a Tab stop only when built with.tab_stop(true).tab_indexon the div reaches only handles gpui makes itself.primitives::tab_stopkeeps one per id. - Floating lists go below their anchor, or above when only above has room (
forms::options::float). gpui's own switch keeps the anchor point, so a flipped list would cover its trigger. - A trigger that opens a list goes through
forms::select::listing: toggle, keys, blur and popup live there, for Select, TabOverflowMenu and Breadcrumb alike. - A marker that slides between items (segment thumb, tab line) measures them with
motion::slideand eases withglide. - A picker hands focus to its popup on open and back to its trigger on pick or Escape; it closes once focus leaves both (
forms::date::picker). - Motion for a value change keys its animation on
motion::changes, so it replays per change and stays still on first paint. - Thumbs stay inside their component's box: the track is padded by half a thumb (
Slider,ColorPicker,GradientEditor). - gpui's
Hsla::opacityscales alpha;alphasets it. - A rounded box does not clip its children. Each layer inside takes the radius itself;
checkertakes one. - Only gpui can build
ExternalPathswith paths, so drop rules live informs::files::dropped, where tests reach them. - gpui runs key bindings before key listeners. A container takes a child's bound key through the action with
capture_action, asFormtakesSubmit. ScrollHandle::scroll_to_itemruns in the container's prepaint before its bounds are stored, so a first-frame call misfires. Call it from a child's prepaint once the handle has bounds, then request one frame (navigation::editor_tabs). Never poll layout withrequest_animation_frame.- The test platform never runs next-frame callbacks, and its text metrics are simplified: each character takes a fixed advance. Tests refresh the window in place of the display link, and click where layout does not hang on text, such as padding and slots.
- Long grids scroll in a
uniform_list. gpui has no nearest scroll, so a key move up scrolls withTopand down withBottom(forms::glyphs). - Masks reshape edits inside
TextInput::set_fit, which sees the replaced range and the typed text. A diff after the fact cannot tell typed characters from kept ones. - Ids inside a reusable component carry its owner's id or
EntityId, animation ids too. Twin ids share focus, click and animation state. - Set an explicit line height on any box that clips text. gpui's default leading is taller than a tight box, and the clip eats descenders.
img()keeps loading state only with an id. Content masks are rectangles, so rounded corners survive only when the image fills its box without cropping.
Decisions
- 2026-09-24: official gpui 0.2.2 over the
gpui-presnapshot. Older API, first-party publisher. - 2026-09-24: one crate. Revisit if an incremental check passes 90 seconds.
- 2026-09-24: gpui 0.2.2 has no accessibility tree and no tray, badge, or notification API. Those entries carry
blockedorprove. - 2026-09-24: the gallery photographs its own window with
CGWindowListCreateImage. No Screen Recording permission needed. - 2026-09-24: license MIT OR Apache-2.0.
- 2026-09-25: layouts persist as versioned JSON through serde. Unknown fields are ignored. Newer versions, unknown panels and bad pane trees are refused, and restore is all or nothing.
- 2026-09-25: an entry built from a later chapter's parts lands with that chapter; its line points there. MenuBar goes to Menus, QuickLauncher to Navigation.
- 2026-09-25: SystemNotification is blocked. An unbundled app has no notification center (probed on macOS 27.2), and gpui 0.2.2 has no API.
- 2026-09-25:
unexpected_cfgsdeclaresfeature = "cargo-clippy", which the objc 0.2 macros test.