Files
Ely-GPUI-Components/AGENTS.md
T
ZacharyZhang-NY a6f9a7b77b Capture · Roots: the roots round 2 missed
A sweep that read every render found 28 roots still on w_full,
reached by 64 components: the field button under Select and every
picker, the checked and mention fields, the slider track,
NumberInput, Steps, the state views, the agent previews, and every
chart and finance frame. All drop it. LivePreview's size_full becomes
h_full, since its card no longer fills.

Every host of those roots was listed and read, 231 in all. Five
tests that mounted a picker as the window root sit in a size_full
box, the index manager gives its method select a flex_1 box, and the
notification center and the gallery's state frames stack their state
views in a column so they stretch.

Each dropped root carries a debug selector, and six chapter tests
mount 43 components in a padded card whose column is measured by
content, through layout::tests::narrow_width: each root must take the
full 240px. With w_full back on the roots, all six fail.

AGENTS.md names the roots that keep w_full and why: picture frames
that keep a ratio, and rows that hand a text field its share.
2026-09-28 19:34:01 -04:00

43 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_shaders is on by default: machines without Xcode lack the Metal compiler.
  • Assets, embedded with rust-embed: Lucide 1.48.0 icons (ISC), Inter 4.1 (regular, medium, semibold, italic) 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).
  • Codes: qrcode (QR) and barcoders (Code 128, EAN-13), both MIT OR Apache-2.0, default features off. rqrr 0.11 ((MIT OR Apache-2.0) AND ISC, default features off) reads QR codes from the host's frames.
  • macOS extras (tray icon, Dock badge) call AppKit through cocoa 0.26 and objc 0.2, the crates gpui already links.
  • Web views: wry 0.57 (Apache-2.0 OR MIT, default features off, macOS only) lays a child WKWebView over the window.
  • Terminal: alacritty_terminal 0.26 (Apache-2.0, default features off) for the grid, its parser and the pseudo-terminal; futures carries its events.
  • Diffs: similar 3.2 (Apache-2.0) for line and word diffs and three-way merges; its unicode feature splits words at punctuation.
  • Markdown: pulldown-cmark 0.13 (MIT, default features off) for CommonMark with tables, tasks, strikethrough, footnotes and math.
  • Pictures: image 0.25 (MIT OR Apache-2.0), the crate gpui decodes with, default features off; it turns decoded frames and rims map tiles.
  • Maps: Natural Earth's 110m countries (public domain). assets/maps/countries.geojson, trimmed by scripts/countries.py to an ISO code, a name and positions to 0.01°, backs maps::WorldMap; the gallery's tiles are cut from the same file, z0 to z3, by scripts/tiles.py with Pillow.
  • Gallery: examples/gallery. Website: frontend/ (Vite 8, pnpm), built after the components: a home page in motion with three.js and smooth scroll, and a components page with categories on the left and the chosen component on the right, after astryx.atmeta.com. It deploys to Cloudflare through npx wrangler.

Commands

  • cargo run --example gallery opens 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 run --example gallery -- --narrow 280 --capture <dir> lays each page, header and body, at that width in a padded card and shoots it without its script, whose aims are measured at full width. Demos that set their own width run past the card.
  • cargo test --lib --features test-support, cargo clippy --all-targets --features test-support -- -D warnings, cargo fmt --check. scripts/check.sh runs them with the house rules.
  • rm -rf target/debug/incremental and cargo clean -p ely-gpui-component after each task item keep the disk lean. Cargo keeps every old build of the crate and its tests under a new hash; by T25 they had filled the disk.
  • scripts/icons.sh <lucide-name>... adds icons. Then add a line to IconName in src/primitives/icon.rs.
  • python3 scripts/countries.py <ne_110m_admin_0_countries.geojson> writes the bundled world; python3 scripts/tiles.py <same file> writes the gallery's map tiles.

Layout

  • src/<chapter>/: one folder per chapter of gpui-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.md and tasks/: 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 raw px() 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_1 is 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.

  • A repeating animation (spinner, breath, sweep) holds still under reduced motion instead of shortening, or it would spin at 1 ms a turn. An effect set off by a change (flash, shake, a bar that fades at rest) reads elapsed time through motion::since_change rather than restarting an animation id, so what it holds keeps its state: an animation's id passes to every descendant, and a new one rebuilds their focus, drags and hover.

  • motion::duration() honors reduced motion. Springs overshoot: use them inside animators, never as gpui easings, because gpui asserts eased values stay in 0..=1.

  • svg() paints only with its own text_color. Icon always sets one.

  • Buttons stay out of focus on click (prevent_default on 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::init binds Tab and Shift-Tab to FocusNext and FocusPrev. Wrap the root in FocusScope with .root() and focus its handle. A root scope takes focus when the focused element leaves the tree, as a toast's or an alert's button does.

  • gpui's Window::dispatch_event returns a private type, so the gallery scripts mouse input by posting NSEvents to its own queue.

  • hover() needs an element id.

  • gpui applies hover and group-hover styles once an element has a hitbox, in prepaint and paint, but text takes its color at layout, so a hover cannot recolor text. What shows on hover rests invisible() and turns visible() through group_hover (chat::ConversationItem, messaging::ChatMessage).

  • gpui 0.2.2 panics on a deferred draw inside another. Whatever draws over the page goes through primitives::raise: at the top it defers; inside something raised it lays out on its own and draws last there, so a list opened in a dialog lies over the dialog. A raise takes its owner's id and draws under it, so its keyed state and focus hold while other raises come and go. scripts/check.sh refuses gpui's deferred anywhere else.

  • occlude() blocks the pointer for everything painted before it, its own ancestors too. Put it on the outermost box that should stop the pointer, as the toast stack does, or the ancestors' hover never fires.

  • A drag that pans, turns or slides goes through gpui's drag, whose moves reach the owner in or out of its box; on_mouse_move stops at the box's edge. gpui sends a drag's moves to every listener of its type, so each payload carries its owner's EntityId and handlers check it.

  • gpui 0.2.2 paints pictures without a transform, so a picture turns by its pixels: media::turn turns decoded frames by quarter turns off the main thread, an asset keyed by the picture and the turn. An svg turns to any angle through its Transformation, so a control that turns shows one (interaction::Rotatable).

  • A box that keeps a cursor over its items rings the item the cursor is on, not the box, as SelectableList marks its row (interaction::SelectionArea).

  • Boxes that scroll together go through interaction::ScrollSync: the pane a wheel turned leads, and a canvas laid before the panes copies its place in prepaint, before they draw, so no frame shows them apart.

  • A component that takes a starting value from its owner keeps it in layout::seeded: a new value from the owner replaces local drags. A field that reports its words as they change keeps what it sent, so the owner's echo leaves the caret alone (mail::SignatureEditor). A cursor inside a seeded view stays while it lies in what is shown, so the owner's echo of a turn leaves it (calendar::CalendarMonthView, YearView).

  • The capture harness turns on reduced motion, so shots are still: repeating motion rests on one frame, and a shimmer shows no band. gpui's mac window stops its display link while occluded, so a capture advances a moving picture only on the draws the harness forces.

  • Each capture pass starts its page fresh (Gallery::fresh): page state lives under the pass, so the dark pass runs its script from the same start as the light one.

  • 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_region follows Zed's title bar.

  • An overlay takes focus with primitives::take_focus and returns it with give_back on every close path. While open, it takes focus back whenever the focused element leaves the tree (gpui's on_focus_lost).

  • A state change made outside render calls cx.notify(), or nothing redraws. A notify made while drawing, as in a canvas's prepaint, marks the view but schedules no frame (gpui's invalidate_view), so it also calls window.request_animation_frame().

  • 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_srgb is tested on a constructed NSColor instead.

  • Every text field is a forms::TextInput entity. Its keys live under the ElyInput context, bound in init. Wrappers take Up, Down, Enter and Backspace first with capture_action.

  • TextInput::bounds_for reads the last layout. Text changed this frame clamps to the laid-out end until the next paint.

  • The harness types with dispatch_keystroke through AnyWindowHandle, 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-support swaps its executor: block fails on a real dispatcher. It stays behind this crate's test-support feature, never on for the gallery.

  • A tracked FocusHandle is a Tab stop only when built with .tab_stop(true). tab_index on the div reaches only handles gpui makes itself. primitives::tab_stop keeps 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. An option list is at least its trigger's width and at most the window's less window_margin each side, so gpui can snap it inside and its rows end in an ellipsis; wider, gpui pins it left and the rest runs off.

  • 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 field that suggests as you type floats its rows through forms::select::Picker and forms::options::Popup, as Combobox and TagInput do.

  • A palette goes through navigation::palette::Palette: query field, grouped rows, cursor, keys and focus live there. CommandPalette, QuickOpen, QuickSwitcher, SearchPalette and QuickLauncher only build rows.

  • A menu goes through menus::menu: rows, submenus, keys, focus and presses outside live there, for DropdownMenu, OverflowMenu, SplitButton, ContextMenu, SearchableMenu and MenuBar. A press on the host's own box is the host's to handle, so a trigger toggles and a right click moves a context menu without a close and reopen in one event.

  • gpui focuses the nearest focusable ancestor on mouse down, and apps wrap everything in a FocusScope. A press that must leave focus alone calls prevent_default, as buttons and context regions do.

  • gpui runs the click of every pressed element under the pointer, a row's with its button's. A pressable row that holds buttons stops their press at their box on mouse down (files::transfer), as overlays::media_button stops its own.

  • gpui draws a window's first frame inside open_window. State the first frame reads, such as cx.set_menus, is set before it opens.

  • gpui clicks a focused element when Enter or Space is released. An overlay that hands focus back picks on release too; a pick on press returns focus first, and the release clicks the opener again. An edit in place that hands focus back keeps its text on Enter's release (forms::InlineEdit). The press itself bubbles, so a container that takes Space leaves a focused button its own: overlays::media_button stops it, and a button in a SelectableList row stops Space and Enter at its box (mail::MailItem's star, messaging::PinnedMessages' Unpin).

  • A focus handle a component hands back lives across all its modes: take it with tab_stop before any early return, or keyed state drops it while the other mode draws and the handed-back focus lands nowhere. A thread's message keeps one handle folded and open, a Tab stop only while folded (mail::MailThreadView).

  • A button whose words and action change with a mode keeps one handle through Button::focus_handle, and the component moves focus to it or back to its field as the mode turns (project::TimeTracker).

  • A button that waits on the owner shows it with loading, which takes no press and stays a Tab stop, so focus holds through the wait; disabled is for what is not ready, and a disabled button is no stop (onboarding::ContactSupport).

  • A help mark, ?, is primitives::HelpTooltip; FormLabel's help shows it.

  • A panel that opens from something other than a button goes through overlays::Popover::with_opener, which hands the opener its toggle; focus, placement and every way out stay in the popover (calendar::EventPopover's chip). Its content gets a close that hands focus back: a press that ends the panel's work closes it before the owner acts, or a panel whose host leaves the tree strands focus (onboarding::Hotspot's Got it, EventPopover's Edit and Delete).

  • A flow in steps goes through navigation::Wizard: Back, Next, Finish and the step's motion live there. Next and Finish share one focus handle, and focus moves to it when Back reaches the first step or a finished step is picked. onboarding::OnboardingWizard leaves out its numbered steps (headless) and shows a count over a bar in as many parts.

  • A dialog goes through overlays::Dialog: scrim, focus trap, Escape and focus return live there. Its close runs on every way out, so a dialog with two answers marks the chosen one before it closes (messaging::IncomingCallDialog). AlertDialog, ConfirmDialog and PromptDialog build on it; a fullscreen dialog fills the window below its title bar. A dialog's card fits the window, a title bar's height clear at each end; its body scrolls and its actions stay.

  • Controls on a dark layer over pictures are overlays::media_button, light icons that dim when they have nowhere to go, for the lightbox and a video player's bar alike.

  • A component's letter keys step aside while Command or Control is held, so the app's shortcuts reach the app (media::ImageAnnotator, VideoPlayer, VideoThumbnailStrip).

  • A tip over a track splits the free width around it by the pointer's share: it stays inside the track and under the pointer, and nothing is measured (media::scrubber::time_tip, for the scrubber and the waveform).

  • primitives::Severity names info, success, warning and danger with their colors and icons, for alerts and feedback alike.

  • Rows that move are keyed. motion::Flip places them absolutely from last frame's heights, so no frame shows a row at its new place before it glides there, and drives the glide from elapsed time: an animation id that changes per move would rebuild the row's keyed state. Reorder holds a row by key, drags with an empty ghost and moves the held row itself.

  • A marker that slides between items (segment thumb, tab line) measures them with motion::slide and eases with glide.

  • 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). Its trigger acts on press and keys, not on click, so a pick made on Enter's press cannot reopen it on the release; messaging::StatusSetter's emoji field reuses it.

  • Motion for a value change keys its animation on motion::changes, so it replays per change and stays still on first paint. Key only the part that moves: a keyed ancestor gives every descendant a new id, and focused buttons lose focus.

  • A spotlight or tour takes its target's box from primitives::Measure; the lit box glides between targets.

  • Thumbs stay inside their component's box: the track is padded by half a thumb (Slider, ColorPicker, GradientEditor, media::ImageCropper).

  • gpui's Hsla::opacity scales alpha; alpha sets it.

  • A rounded box does not clip its children. Each layer inside takes the radius itself; checker takes one.

  • A window keeps one feedback::Toaster and renders one ToastViewport; anything holding the entity pushes to it. Messages share primitives::Severity.

  • Only gpui can build ExternalPaths with paths, so drop rules live in forms::files::dropped, where tests reach them. taken adds file kinds by extension, for drops and the dialog alike (DropZone::kinds).

  • What a dropped file leads to is drawn by a function of what was read, which tests render: gpui's test platform builds no ExternalPaths and leaves prompt_for_paths unimplemented (settings::importer::read_row).

  • A stroke drawn with the pointer goes through forms::pen: a press starts it, a drag extends it, and the lift hands it over, in or out of the pad (SignaturePad, generative::InpaintCanvas, calendar::TimeGrid's drag on empty time).

  • gpui runs key bindings before key listeners. A container takes a child's bound key through the action with capture_action, as Form takes Submit.

  • ScrollHandle::scroll_to_item runs in the container's prepaint against last frame's bounds and overflow, so a call before a list's first frame does nothing. Call it from a child's prepaint once the handle has bounds, then request one frame (navigation::editor_tabs). A floating list reveals its cursor, and a strip its focused item, through forms::options::reveal and revealer. A focused Tab stop deeper in a scroll box sets the box's offset whole from its own painted bounds in prepaint, once per focus, through layout::bring_into_view along the box's axis; the canvas measures the stop with its ring (messaging::GifPicker). layout::reveal_when_focused is that canvas; it lies over a box that holds the stop's ring (buttons::SegmentedControl, project::KanbanBoard). Never poll layout with request_animation_frame.

  • gpui animations run on the wall clock; timers run on the executor's clock, which tests advance by hand. A test that needs an animation settled turns on the theme's reduced motion. Clocks read background_executor().now() and wake only their own view.

  • Machine-read marks, QR codes and barcodes, paint ink on paper: dark on light in both themes, so cameras read them.

  • A layer over pressable content that must still pass scrolls to its ancestors uses block_mouse_except_scroll; occlude hides them from scrolls too.

  • Trackpad momentum arrives as TouchPhase::Moved after Ended, so a gesture acts only between Started and Ended (lists::SwipeableListItem).

  • Quartz scroll events carry no window. The harness hands them to the window itself, placed in window points (Step::Swipe).

  • A chevron that opens and closes is primitives::Disclosure.

  • uniform_list does not stretch its rows; each row sets w_full, or min_w_full when the same row also sits in a flex column, where it stretches (tables::DataTable). gpui's list lays rows at their content's width too; chat::MessageList boxes each row at the list's width.

  • Chapters import only earlier chapters, so a select of tree nodes lives in lists (TreeSelect) and reaches forms::listing.

  • Script clicks aim at a probe's corner, so a scripted target must fit in the window; a tall probe puts its top off screen. The log names each probe's bounds as the script aims, and a probe around content with no width of its own measures smaller than what it draws, so a scripted demo sets its width.

  • scripts/check.sh fails by its exit status; a pipe into tail hides it.

  • Reduced motion shortens animations to 1ms on the wall clock. Tests sleep past it between refreshes to see where one ends.

  • 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.

  • An absolute element with no insets sits where it would flow: after the content in a block, inside the padding in a flex box. A canvas that measures its parent pins itself with .top_0().left_0().

  • Keyed state lives while its element renders in consecutive frames. An overlay rendered only while open starts fresh each time.

  • Long grids scroll in a uniform_list. gpui has no nearest scroll, so a key move up scrolls with Top and down with Bottom (forms::picks). A searchable grid of picks, emoji, icons or stickers, goes through forms::picks's Grid; uniform_list gives every row one height, so a grid of large cells shows one group at a time instead of titles (messaging::StickerPicker).

  • 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. A child keyed by its place inherits the state of what stood there before; key it by what it shows (files::FileExplorer keys a column by its folder).

  • Text wraps at the width its box had when measured. A cross-axis max_w comes too late, and a flex column counts wrapped text as one line. Put prose in a plain block inside a flex row with flex_1().max_w(..) (feedback::states).

  • gpui 0.2.2 keeps a no-wrap line's first measure, taken at full width inside flex and scroll boxes, so .truncate() clips there instead of ending in an ellipsis. A line that may overflow is a typography::Ellipsis.

  • gpui 0.2.2's line_clamp wraps once and lays the rest on the last line, cut hard; centered, it loses both ends. A name whose ends matter is a typography::MiddleEllipsis on one line (files::FileGrid).

  • 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.

  • gpui's img takes its picture's pixel size for any Auto side, and its aspect ratio beats percent heights in flow. Image pins it absolute at full size, so an Image needs a sized box.

  • A file on its way is a files::Transfer drawn by one row in files::transfer: TransferQueue and DownloadManager share it, and its actions follow its state.

  • Paths with / between folders become a tree through lists::Folder::of, for FileTree and files::ArchiveViewer alike.

  • A folder's path over its entries is lists::path_bar, and its first order is lists::listed, folders first by name: DirectoryListing and FileExplorer share both, so every view shows one path and one order.

  • A file's icon comes from primitives::file_icon, one map from name to icon for every chapter, under the owner's primitives::IconTheme, whose rules by name and extension come first; files::FileIcon draws it. What fits across a width comes from layout::columns_for, and layout::fit adds the inset that centers it.

  • Badges, tags and avatars live in data_display; forms and shell draw theirs from there, and data_display imports neither. A hue worn in a row, a label's or an event's, is data_display::color_mark, and Palette::hue gives the chart color by index. Avatars take square pictures. A tag is never wider than its container; a long label ends in an ellipsis beside its remove mark.

  • A picture drawn in code reaches gpui as a RenderImage. A host that replaces one frees the old through App::drop_image, or every frame stays in the sprite atlas (the gallery's model).

  • img() keeps loading state, and moves an animated picture's frames, only with an id. Content masks are rectangles, so rounded corners survive only when the image fills its box without cropping.

  • gpui reads a &str picture source as a web address. A file goes through Path (documents::source in the crate, Path::new in the gallery).

  • Taffy 0.9's grid rows overgrow tiles that keep a ratio. Tiles in a picture's shape sit in flex rows of flex_1 cells (generative::GenerationGrid). A cell's padding counts in its flex base, so a row of fewer cells gives each more width; cells that match across rows keep their inset on a box inside (messaging::VideoCallGrid).

  • Rise reads green and fall red unless a component's red_up swaps them, as markets in East Asia read. finance::quotes::moves picks the pair.

  • Charts lay out in f32 pixels of their own box, tested without gpui, and paint through charts::paint::at: a canvas's paint gets window coordinates, and with_element_offset works only in prepaint. Their sizes come from theme.chart().

  • A label pinned to a point sits in a zero-size absolute box that centers it with flex (charts::axes), so no offset guesses at text size.

  • Tokens live in src/theme; the raw px() scan skips that folder only.

  • An owner's palette, edited or imported, reaches the theme per mode through Theme::set_palette, and Theme::palette names the one the shown mode fades to; high contrast strengthens Ely's own palettes alone. A color goes by its field's name through token and token_mut, on Palette and Syntax alike. Ely's palettes pass tooling::A11yChecker's audit in both modes, with high contrast and without; a test holds them there.

  • ScrollHandle keeps its children's bounds unscrolled: a child paints at its bounds plus the offset. A press on a scrolled child subtracts both, and an offset that brings a child to a place is set whole, not added (documents::find::reveal).

  • A find box is editor::FindWidget, over code, a terminal or a document's pages. It shows the replace fold and the option toggles only when the owner handles them.

  • Taffy 0.9 hands negative free space to auto margins and centering alike, so a centered child wider than its scroll box starts past the scroll's reach. Center only what fits the measured box (documents::DocumentViewer).

  • A mode that leaves on Escape holds its own focus through primitives::hold_focus: turned on while focus is elsewhere, it takes focus so Escape reaches it, and hand_back returns it (documents::ZenMode, collab::FollowMode).

  • A chat holds at its newest message through gpui's list with ListAlignment::Bottom: it sticks while scrolled to the end, and scroll_to one past the last item sticks it again; that call sends no scroll event, so the owner clears its own away state (chat::MessageList).

  • Anything pressable is a Tab stop: a row, card or toggle takes tab_index(0) and focus_ring, with a 1px border that stays transparent where it shows none; gpui presses it on the release of Enter or Space.

  • A list of pressable rows goes through lists::SelectableList, whose cursor reaches them by key: Up and Down move, Enter or a double press activates (media::Playlist). Enter activates on its release, armed by its own press, so an activation that moves focus leaves the release nothing to press (onboarding::HelpPanel). Typing a row's first letters moves the cursor there, the letters joining within a pause on the executor's clock; a letter held with Command, Control or Alt stays the app's. A bare ListItem press is the pointer's only. Titled sections of such lists go through lists::Sections, for ChannelList, MemberList and MailboxList; each owner builds its rows and keeps its own checks.

  • Columns that must line up across rows sit absolutely at fractions of their row, as the bars over them do (calendar::CalendarMonthView): with real text, gpui 0.2.2 lets a flex_1 cell's text widen it, and the test platform's fixed advances hide it.

  • A row that holds text beside fixed parts lets the text give way: the text takes flex_1().min_w_0() and an Ellipsis; counts, icons, dates and chevrons take flex_none. A label that must stay whole wraps in that block instead, as forms::Checkbox's does.

  • A mark over a text field reads the field at paint time, from a canvas laid over it (collab::RemoteCursor): by then TextInput::bounds_for holds this frame's layout, and a still page never renders again to catch up.

  • A callback field holds a named type alias; clippy's type_complexity rejects Option<Rc<dyn Fn(..)>>. A chapter shares one alias per shape.

  • A component that fills its container takes no width on its root: a block or a flex column stretches it. Percents fill badly in taffy 0.9. w_full in a flex column that a block measures by content keeps its content's width: taffy lays the column out once at unknown width and keeps that layout when the width comes out the same. min_w_full resolves against a block's outer width, padding and all (forms::FormField). In a flex row the host gives it flex_1; as a window's root it sits in a size_full box. A root centered under a cap keeps w_full, since mx_auto needs a width to fill (layout::Container); so does one that keeps a ratio, since taffy 0.9 follows a ratio from a set width, not a stretch (layout::AspectRatio, and primitives::framed under VideoPlayer, CameraPreview, ModelViewer, ParticipantTile and InpaintCanvas). A row that hands the text field element itself a flex_1 share keeps w_full too: stretched without a width, taffy 0.9 can keep a pass that laid the share at no width, and the field draws nothing (forms::Input, InputGroup, TagInput); a row around one of those, as NumberInput's is, takes none. Button::full_width is its host asking, since a button takes no style. Each chapter's test holds its filling roots at the full width of such a column through layout::tests::narrow_width, which reads a root by its debug_selector.

  • A field's Highlight sets color, wash, weight, slant and strike per span; measuring and painting share one set of runs, so wrapped heights match.

  • A block editor keeps one undo for the document: text and structure together, as snapshots through forms::History; it takes Undo and Redo before its fields do.

  • gpui calls focus listeners only while the window is active. A test that needs focus events activates its window with window.activate_window().

  • A terminal takes Tab and Shift-Tab through bindings under ElyTerminal, ahead of focus moves; other keys go through terminal::keys in a key listener, and typed text through its input handler.

  • A shell in the gallery starts without startup files and with a fixed prompt, so captures show no user or host.

  • gpui 0.2.2 has no (ElementId, usize) id. A child id names its index: (self.id.clone(), format!("row-{ix}")).

  • Clippy's single_range_in_vec_init rejects [a..b] and vec![a..b]; bind the range first.

  • A component built on lists::Tree fills its box; the host gives it a height. A fixed-width column beside a flex_1 one takes flex_none, or it shrinks.

  • Editor rows read their text through CodeEditor::row_text: a multi-line ghost cuts its line, and the rest follows the ghost's last line.

  • gpui styles a drag-over only on an element with its own hitbox. A veil that shows while files hover takes the drop itself (chat::DragDropOverlay).

  • gpui 0.2.2 never clears debug_bounds: a test sees an element appear, not leave.

  • The system time zone comes from typography::format::system_zone, which fails loud; jiff's TimeZone::system() falls back to UTC in silence.

  • A Select with nothing to pick stays shut: disabled, it shows its placeholder; open, it fails (media::DeviceSelector says no device was found).

  • A list opens with its cursor on the current choice: Select on the chosen row, Combobox on its value, MultiSelect on the first one ticked, SelectableList on its first selected row. A selection the owner makes, not the echo of the list's own pick, moves SelectableList's cursor there.

  • An event repeats as RFC 5545 has it: weeks count from the start's own week, and a month or a year without the start's date is passed over (calendar::Recurrence).

  • A search step, a tool call or an agent's step says where it stands with chat::StepState and draws it with chat::step_mark.

  • A flex line whose free space goes to an auto margin loses its gaps in taffy 0.9. A row that parts left and right groups its sides and uses justify_between; ml_auto is safe only beside a flex_1 item that leaves no free space.

  • A header that holds a name beside counts and actions wraps: flex_wrap, the name flex_1 with min_w(label_width), the rest one flex_none group that drops below.

  • Every header, row and strip works at 280px: a name keeps its minimum width, the rest wraps below or scrolls, and nothing draws past its box.

  • gpui takes a scroll box's reach from its direct children's bounds, and a block child stretches to the box. Content that scrolls sideways sets its least width on that child (tables::body::least).

  • gpui turns a wheel along the other axis onto a box that scrolls one way, and the page around it scrolls too. A box inside a scrolling page keeps its wheel to its axis with layout::on_axis, and so does the page.

  • The focus color belongs to the focus ring. A chosen or shown item takes accent, as forms::picks and lists::ListItem do.

  • A box that shows a host's picture takes the picture's shape from the host (ratio), so nothing is cropped and a point given as a share lands on the picture. A focusable one keeps its focus border on an outer box, so the picture keeps that shape exactly (media::ImageCropper).

  • A default only the component can work out, such as a crop fitted to a picture it decodes, reaches the owner once through window.defer after render (media::ImageUpload).

  • A ScrubInput shows its number in about 140px beside a short label. In a narrower column a pair hides it, so a long label takes a row of its own (canvas::TypographyPanel).

  • Design panels edit a whole value and hand the owner a changed copy through canvas::panel::editing, which takes the owner's handler itself: a component without one fails at render. A swatch that opens a picker is canvas::panel::color_well.

  • A press that stops propagation also stops the ancestors' focus on press. A layer that takes keys focuses itself in its own press handler (canvas::ToolLayer, canvas::MindMap), and a field over it stops its own presses.

  • A button inside a pressable row goes through lists::row_action, which keeps its press and its Space or Enter from the row (devtools::ConnectionManager).

  • A field opened over a component hands focus back to that component on Enter and Escape, through forms::Editing's returning and give_back (canvas::writing); a blur keeps the words and leaves focus where it went.

  • A focus ring on a box whose children measure themselves goes on an overlay child: a border insets the children and shifts what they measure (canvas::ToolLayer).

  • A menu of times goes through mail::times: each row names its day and hour, a moment comes once (tomorrow is next Monday on a Sunday), and "Pick a time…" opens one dialog that waits for a time ahead of now (ScheduleSend, SnoozePicker).

  • A list whose cursor a field drives marks the cursor only while the field has focus; at rest its first row would look hovered (mail::LabelPicker). Its rows are option rows, and forms::marked_row leads one with a mark such as a label's hue.

  • A choice that names a person leads with their initials through forms::Choice::avatar, in the list and in a Select's trigger alike (project::AssigneePicker).

  • typography::Ellipsis shapes its line with the text style's own run, so a strike or an underline set on its box shows (lists::ListItem::struck, a closed task's title).

  • Focus that must follow an item to another parent is taken by the owner and keyed by the item: a handle gpui makes for an element dies with its path (project::KanbanBoard, whose cards keep focus across columns).

  • The theme's platform, the system's to start, decides the window buttons Ely draws, the title bar's layout and key caps, secondary included (typography::keys::keystroke). A title bar whose buttons the OS draws follows the OS.

  • A centered title sits between a start and an end that grow alike from a zero basis, their padding on a box inside them, since taffy counts padding in the flex base; their content is their least width, so the title gives way before either end (shell::TitleBar).

  • A pane the window's blur shows through is shell::Vibrancy, filled with the palette's glass, in a window opened WindowBackgroundAppearance::Blurred: vibrancy on macOS, acrylic on Windows. gpui 0.2.2 has no Mica.

  • The chart's hues go by theme::HUE_NAMES, for an event's color and a label's alike.

  • Text whose every character counts, encoded data and keys, goes through typography::literal: JetBrains Mono draws == and -> as ligatures (devtools::Encoder).

  • Messages go through i18n::I18n, a global the app sets: cx.global::<I18n>().text(key, args) fills each {name}. Every catalog holds the first one's keys; a missing key, argument or catalog fails. A right-to-left locale mirrors a stack with Direction::row and sets its text with Direction::align; CoreText orders mixed Hebrew and Latin runs on macOS, but gpui sets no paragraph direction, so a line takes its first strong character's: text leads a right-to-left message with U+200F, so its words suit labels, not a field's value, and Locale::number a right-to-left minus with U+200E, as CLDR does. gpui 0.2.2 wraps shaped glyphs in the order they are drawn, so a right-to-left line that wraps comes out garbled: right-to-left text stays on one line, an Ellipsis, which cuts by characters and sits by its text's alignment.

  • gpui orders Tab stops by tab_index before paint order, and registers none for an invisible() element. A stop that must come first where it sits, yet draw over what paints after it, stays in flow at zero size and raises what it shows (i18n::SkipLink): a lower tab_index would sort it before the root scope's own handle, so Tab from the root would pass it.

  • Color-blind safe charts swap Ely's chart hues for a set fitted to stay apart under Machado's protanopia, deuteranopia and tritanopia; a test holds every pair 0.10 apart in Oklab and each hue 3:1 on its page (Theme::set_color_blind_safe, in Ely's own palettes alone).

  • gpui 0.2.2's Metal renderer starts its instance buffer at 2 MB; a frame that outgrows it logs "failed to render … retrying with larger instance buffer size" and the buffer doubles, up to 256 MB. A world map's first frame grows it to 16 MB, then draws: the log is gpui's recovery, not a failure.

  • gpui 0.2.2 packs pictures in its atlas without padding and samples them with a linear filter, so a scaled picture's edge takes color from its atlas neighbor, and pictures laid edge to edge show seams. A map tile carries a one-pixel rim copied from its own edge and draws one of its pixels larger each side, clipped to its box (maps::tile).

  • A wall clock reads Timestamp::now and wakes its view at each whole second through a keyed task, only while the owner passes no moment; a stopwatch reads the executor's clock, so tests advance it (misc::Clock, misc::Stopwatch).

  • A number shown to so many significant digits goes through typography::format::significant, which rounds through Rust's exponent form, so no binary noise shows at any size (misc::Calculator, misc::UnitConverter).

  • A component that reads the host's frames reads one at a time off the main thread and passes over frames that come meanwhile; when a read ends it redraws, so the newest frame is read next (misc::QrCodeScanner).

  • A native view laid over the window takes AppKit's first responder on a press, and gpui's view never asks for it back, so typed text would go to the native view for good. The component hands the keyboard back on every press gpui gets and when the view leaves (misc::WebView, through wry's focus_parent); Step::NativeKey proves it, since gpui's own dispatch skips AppKit.

  • A map keeps places in f64: at zoom 19 the world is 134 million pixels across. Its wheel pans and Command or Control with the wheel zooms about the pointer, as the canvas's does. Its tiles stay in gpui's asset cache, as every img() picture does. Its popup is an overlay that is not modal: it takes focus when it shows, and the frame it stops showing, by any way out, hands focus back. Its layers, lines and areas unwrap across the date line and repeat on every world in view. Clustered pins gather by squares of the world, not of the view, so a pan never regroups them; a zoom may, so it hands a focused pin's or cluster's focus to the map.

  • gpui's inspector (debug builds) runs no mouse listener while picking and holds a pick on a press without a redraw, so Ely's panel requests a frame each render while picking (tooling::install_inspector). Its panel scrolls, so the box model it draws over the page goes through primitives::raise, which paints unclipped. The panel is a second root, out of the app's scope, so it owns Tab through a FocusScope of its own.

  • A tool that measures redraws cannot move: any animation redraws the view it counts (tooling::RenderCounter). AnyView::cached renders again only on its own notify or a window refresh. A meter that counts frames keeps its view drawing while shown (tooling::FpsMeter).

  • A logger of input lays its canvas before what it holds, so its capture listeners run first and see what a child stops. Key downs come through App::intercept_keystrokes, which runs before bindings, and a key context on its box tells it focus is inside (tooling::EventLogger).

Decisions

  • 2026-09-24: official gpui 0.2.2 over the gpui-pre snapshot. 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 blocked or prove.
  • 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_cfgs declares feature = "cargo-clippy", which the objc 0.2 macros test.
  • 2026-09-26: Terminal proven with alacritty_terminal 0.26: its grid, parser and pseudo-terminal, drawn by Ely. A replayed grid serves recorded output and tests.
  • 2026-09-27: ThemeSwitcher is settings::ThemeSelector: src/theme sits under every chapter and cannot draw with buttons.
  • 2026-09-28: QRCodeScanner proven with rqrr 0.11: the host hands in frames as RenderImages, read off the main thread; gpui 0.2.2 has no camera.
  • 2026-09-28: WebView proven with wry 0.57: a child WKWebView over the window, placed from a canvas in prepaint at its box each frame and hidden once the box leaves the view. It draws above everything gpui paints and gpui's clipping does not reach it; gpui's test windows have no native handle, so the capture proves it.
  • 2026-09-28: InspectorOverlay is gpui's inspector drawn by Ely, tooling::install_inspector, in debug builds only, as gpui has it.
  • 2026-09-28: every text pair the contrast audit holds meets 4.5:1: fg_subtle is #696764 light and #94928f dark, syntax.comment with it, and the light syntax's number, constant and attribute are #a3591b, #aa5529 and #9f5d12; the contrast audit found them under.