Compare commits

..
371 Commits
Author SHA1 Message Date
ZacharyZhang-NYandClaude Opus 5.5 09a52a7662 Tasks · G9: pushed, deployed, answered #6 and #7, closed #6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:14:58 -04:00
ZacharyZhang-NYandClaude Opus 5.5 9f879fd8a1 Shell · ColumnShell's story on the Window & Shell page, the PRD line, stories.json and AGENTS.md (#6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:14:17 -04:00
ZacharyZhang-NYandClaude Opus 5.5 fea012127a Shell · ColumnShell: columns to the window's top, each under a header that drags it; the traffic lights or controls in the first or last (#6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:04:34 -04:00
ZacharyZhang-NYandClaude Opus 5.5 8c2f7438e0 Forms · An IME composition restarts the caret's blink, as every edit does, and logs its ranges (#7)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:35:46 -04:00
ZacharyZhang-NYandClaude Opus 5.5 4c36a5aa02 Logo: logo.png, 2048px and transparent, rendered from the mark by scripts/favicon.mjs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 22:27:45 -04:00
ZacharyZhang-NYandClaude Opus 5.5 20a8e2b0ee Site · Logo: the letter E in iso blocks, in the header, the favicon, the touch icon and the OG image
One geometry in src/iso/mark.ts, drawn by the engine the miniatures use. The favicon follows the system's mode; scripts/favicon.mjs takes its colors from styles.css. Below 768px the header shows the mark alone, its word kept as the link's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 22:19:04 -04:00
ZacharyZhang-NYandClaude Opus 5.5 f281d01f35 Site · Showcase: apps built with Ely, from the public Ely-GPUI-Showcases repo at a pinned commit
The build fetches showcase.lock's commit and runs the catalog's own validator. /showcase/ lists featured apps first, then the rest by newest or stars, with categories and search; /showcase/<id>/ shows previews, links and the README. The header drops its sound switch below 414px so the fourth link fits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:39:13 -04:00
ZacharyZhang-NYandClaude Opus 5.5 7e39a9383d Tests · NumberInput's commit test selects with the platform's secondary key; Tabs tests move to navigation/tests/tabs.rs, under 500 lines
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 17:59:36 -04:00
Amit VermaandClaude Opus 5.5 2fb505ee98 Primitives · Icon::from_path draws an application's SVG with the theme's size and color; a path the asset source lacks fails at render
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 17:24:39 -04:00
ZacharyZhang-NY 2f02c82c6a Navigation · Tabs on_context: a right click on a tab reports that tab and the pointer, not the chosen one 2026-10-04 15:53:30 -04:00
ZacharyZhang-NY aaaf0d58b2 Menus · ContextMenu manual: one-line doc 2026-10-04 12:01:34 -04:00
ZacharyZhang-NY 967d1e4705 Menus · ContextMenu manual: opens on the owner's numbered request at a point, not on a right click, for rows that come after the press 2026-10-04 11:04:32 -04:00
ZacharyZhang-NY c0f17eeb48 Buttons · ToggleButton disabled: shown, not pressable, no tab stop, says it is unavailable; its tooltip can give the reason 2026-10-04 07:13:14 -04:00
ZacharyZhang-NY b6cb94cad4 Forms · NumberInput on_commit runs on Enter, blur, steps and scrubs, never while typing; number tests move to number/tests.rs 2026-10-04 06:33:06 -04:00
ZacharyZhang-NY beea6caa04 Tasks · T50g: chapter 44 accepted, item by item 2026-10-04 00:52:22 -04:00
ZacharyZhang-NY b85955ae06 Docs · Rendering: what GPUI can show, RenderSurface and its revision, a compiled example; AGENTS.md and README follow 2026-10-04 00:30:12 -04:00
ZacharyZhang-NY 36d5559682 Tasks · T50e: CI green on Linux, Windows and macOS, arm64 and x86_64 2026-10-04 00:20:56 -04:00
ZacharyZhang-NY 717d07ee4e CI · the crate's tests and the gallery on Linux, Windows and macOS, arm64 and x86_64; tests type the platform's secondary key 2026-10-04 00:00:43 -04:00
ZacharyZhang-NY 399e3a6b53 Rendering · web: the RenderSurface story runs live; an e2e test holds that the slider redraws its field 2026-10-03 23:53:30 -04:00
ZacharyZhang-NY 87832c768a Rendering · gallery page: a banded field drawn at device pixels, its slider bumps the revision; the gallery builds on Linux 2026-10-03 23:31:51 -04:00
ZacharyZhang-NY a538092e99 Rendering · RenderSurface: host frames at the box's device pixels, redrawn on a new size or revision, freed from the atlas 2026-10-03 23:22:49 -04:00
ZacharyZhang-NY 61d2378ae2 Tasks · T50a: chapter 44 Rendering, RenderSurface planned, what gpui can and cannot host 2026-10-03 23:14:27 -04:00
ZacharyZhang-NY ba0e0127b4 Scripts · stories.py reads components from the chapter task files alone, not the GitHub round's list 2026-10-03 22:54:14 -04:00
ZacharyZhang-NYandClaude Opus 5.5 2f8b2f687c Tasks · G4: pushed, #2 closed, #3 answered
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 01:36:16 -04:00
ZacharyZhang-NYandClaude Opus 5.5 0dbb8c747c Forms, a11y · Checkbox, Switch, Radio, RadioGroup, Slider and Select name their role, label and state (#3)
A slider's thumb reports its value, the bounds it can reach and its orientation; a select's trigger its value, placeholder and whether its list is open; a disabled control says so in its description.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 01:35:48 -04:00
ZacharyZhang-NYandClaude Opus 5.5 5ff7c219ef Lists · tree guides fall from the parent's chevron and run unbroken across rows (#2)
The chevron takes its indent as a margin, so no gap parts it from its guide column; the cursor's ring is an overlay, so the guides fill the row through its border.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 01:21:26 -04:00
JetSquirrelandClaude Opus 5.5 a6119f1e4e Terminal · build on Windows: set alacritty's Windows-only escape_args
alacritty_terminal 0.26's tty::Options has a field that exists only on
Windows, so the crate failed to compile there with E0063. Each launch
argument is one argv entry, so they are escaped by the C runtime rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 01:01:17 -04:00
ZacharyZhang-NY 7380afb419 Dialogs · a confirm or alert message names itself to assistive technology 2026-10-02 19:28:01 -04:00
ZacharyZhang-NY cf9770b57e Dialogs, a11y · Dialog card names its role, title and detail; prompt error reads as a label; Cancel and Close read i18n; a disabled button says so in its description 2026-10-02 18:52:08 -04:00
ZacharyZhang-NY 290939f82f i18n, a11y, IME · palette copy reads i18n with builtin en/zh; dialog and palette Enter run on the field's key-down action, so an IME-kept Enter never submits; Button, IconButton, TextInput, Tabs, EditorTabs, StatusBarItem and palette rows name their role and label 2026-10-02 12:58:43 -04:00
ZacharyZhang-NY b54500b0e4 Ely 0.1.1 2026-10-01 14:13:54 -04:00
ZacharyZhang-NY 5a5cd636de T48m Docs · E2E passes review; T48 closes 2026-10-01 14:02:00 -04:00
ZacharyZhang-NY 9bdba85727 T48m Docs · E2E: a hashless prose link proves routing in place; the section link keeps its own test 2026-10-01 13:57:16 -04:00
ZacharyZhang-NY ae7614cbf5 T48m Docs · E2E: each guide's heading, rail name and browser title checked; a prose link routes in place 2026-10-01 13:50:45 -04:00
ZacharyZhang-NY d5511238fe T48m Docs · E2E: the Docs link, every guide, the rail, previous and next, section links, the compiled sample and Copy, an unknown guide; the rail marks only the open guide 2026-10-01 13:41:18 -04:00
ZacharyZhang-NY 33c4d651b8 T48l Docs · Testing passes review 2026-10-01 13:40:30 -04:00
ZacharyZhang-NY ae5c2a19f3 T48l Docs · Testing: the feature, an app's limit, writing a test inside Ely, tests to read 2026-10-01 13:34:47 -04:00
ZacharyZhang-NY a84ec17b6a T48k Docs · WebAssembly passes review 2026-10-01 13:33:37 -04:00
ZacharyZhang-NY 2b2f284ed9 T48k Docs · WebAssembly: pictures land in <out>/assets/, the HTTP client is the web's; table words stay whole and long titles hyphenate 2026-10-01 13:30:33 -04:00
ZacharyZhang-NY 16e001ec00 T48k Docs · WebAssembly: the check, an app in the browser, the gallery build, embedding, what differs 2026-10-01 13:22:53 -04:00
ZacharyZhang-NY 02c2d9dfcd T48j Docs · Internationalization passes review 2026-10-01 13:22:35 -04:00
ZacharyZhang-NY a1203af405 T48j Docs · Internationalization: catalogs and messages, locales, right to left; the i18n example compiles 2026-10-01 13:15:07 -04:00
ZacharyZhang-NY 287fe63306 T48i Docs · Overlays passes review 2026-10-01 13:13:59 -04:00
ZacharyZhang-NY bf8ce3f254 T48i Docs · Overlays: only dialogs, popovers and menus take focus; the close button belongs to a fullscreen dialog 2026-10-01 13:12:13 -04:00
ZacharyZhang-NY 4c3b7811f1 T48i Docs · Overlays: drawing over the page, dialogs, popovers, toasts; the overlays example compiles 2026-10-01 13:07:03 -04:00
ZacharyZhang-NY d38430f46a T48h Docs · Icons, fonts and assets passes review 2026-10-01 13:04:09 -04:00
ZacharyZhang-NY c16c6b69d2 T48h Docs · Icons, fonts and assets: icons, adding one, file icons, fonts, an app's own assets; the icons example compiles 2026-10-01 12:57:47 -04:00
ZacharyZhang-NY a9fd0b58a7 T48g Docs · Motion passes review 2026-10-01 12:55:27 -04:00
ZacharyZhang-NY c41ce2055c T48g Docs · Motion: durations and easings, reduced motion, components that move; the motion example compiles 2026-10-01 12:51:23 -04:00
ZacharyZhang-NY dbcedfae88 T48f Docs · Focus and keyboard passes review 2026-10-01 12:48:48 -04:00
ZacharyZhang-NY 73c79bc79a T48f Docs · Focus and keyboard: what init binds, the root scope, making a Tab stop, buttons, overlays; the focus example compiles 2026-10-01 12:38:02 -04:00
ZacharyZhang-NY 85e1fc923a T48e Docs · Theme passes review 2026-10-01 12:36:39 -04:00
ZacharyZhang-NY da39d0a914 T48e Docs · Theme: reading tokens, the calls that change them, an owner's palette, chart hues; the theme example compiles 2026-10-01 12:32:53 -04:00
ZacharyZhang-NY b41a9d3565 T48d Docs · Getting started passes review 2026-10-01 12:30:09 -04:00
ZacharyZhang-NY 1303a407fc T48d Docs · Getting started: four steps, a window that follows the theme, changing state from a press; the start example compiles 2026-10-01 12:26:17 -04:00
ZacharyZhang-NY f90182c638 T48c Docs · Installation passes review 2026-10-01 12:26:00 -04:00
ZacharyZhang-NY 17194c3afd T48c Docs · Installation: GPUI compiles the shaders at run time; the panic quoted without a trailing space 2026-10-01 12:22:19 -04:00
ZacharyZhang-NY 6da6ab00dc T48c Docs · Installation: requirements, the git dependencies and why, shaders, a check, failures and their fixes 2026-10-01 12:17:18 -04:00
ZacharyZhang-NY f326b6dab6 T48b Docs · Introduction passes review 2026-10-01 12:15:07 -04:00
ZacharyZhang-NY 23ccc36668 T48b Docs · Introduction: what Ely gives an app, what is stable, a first window, license 2026-10-01 12:11:29 -04:00
ZacharyZhang-NY 484bec30e1 T48a Docs · Frame passes review; an id-less heading fails the build, protocol-relative links stay native 2026-10-01 12:08:02 -04:00
ZacharyZhang-NY 0b300f1f94 T48a Docs · Frame: guides render at build in a Vite plugin, so a bad one fails the build; focus rings whole in the rail and list; plain heading words, unique ids, captions only when named, code under example= refused 2026-10-01 12:01:42 -04:00
ZacharyZhang-NY 128cc5992f T48a Docs · Frame: titles and the list read plain words, links with a section route, long code wraps, the rail stays in flow on narrow screens, check.sh holds every docs example to Cargo.toml 2026-10-01 11:52:29 -04:00
ZacharyZhang-NY a52241667f T48a Docs · Frame: a Docs tab, /docs/ and /docs/$page/, Markdown guides with a rail, an on-this-page list, previous and next and code blocks; full Rust samples are compiled examples shown as text 2026-10-01 11:40:50 -04:00
ZacharyZhang-NY 6253e4f2d2 CONTRIBUTING.md: setup, where things go, the house rules reviews catch, checks, license 2026-10-01 11:25:34 -04:00
ZacharyZhang-NY 700f402997 Pull request template: what and why, screenshots, the house checklist 2026-10-01 11:14:21 -04:00
ZacharyZhang-NY 9e5850fcb9 CI: checkout v7, setup-node v7 and pnpm/action-setup v6, on Node 24 2026-10-01 11:03:09 -04:00
ZacharyZhang-NY 47a4199a97 Issue forms: Bug Report, Component Request and Other, each titled by its kind; blank issues point to the live components 2026-10-01 10:58:09 -04:00
ZacharyZhang-NY 4f0b3726fe T47 Ship: CI runs the crate's checks on macOS and the site's tests and build 2026-10-01 10:38:19 -04:00
ZacharyZhang-NY 56f0e7b2f7 T47 Ship: the site lives at elygpui.com; its share image and the gallery's pictures load from there 2026-10-01 10:38:18 -04:00
ZacharyZhang-NY cb45f5232f Remove frontend-reference: the folder, its ignore line and its docs 2026-10-01 10:03:27 -04:00
ZacharyZhang-NY 567ab6f36f 更新 README.md 2026-10-01 08:51:27 -04:00
ZacharyZhang-NY fcfebb3db4 T47 Ship: a gallery whose wasm fetch fails retries, then tells the host; the host shows it at once and waits a minute for a slow start 2026-10-01 08:49:10 -04:00
ZacharyZhang-NY 3a0274eeab T47 Ship: wrangler leaves macOS folder files out 2026-10-01 08:44:46 -04:00
ZacharyZhang-NY 0fdd3d5e02 T47 Ship: the OG image lives at the site's own domain 2026-10-01 08:43:43 -04:00
ZacharyZhang-NY bc0fafd57f T46a Site · the hero title fits at 768 too 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 48f344091d T46a Site · Owner's notes pass review; the hero title fits its column 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 8441ee60cd T46a Site · Owner's notes: short story names, aligned icons, a whole city title, tighter story tops, hidden scroll bars, a tablet step at 1024 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 908834465d T46 E2E passes review; T45 closes 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY be56983e15 T46 E2E: typing proven by inked pixels, home frames proven mounted before their claims, full screen measured, window captures decoded 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY faa3dddbfe T46 E2E: the suite follows the new site (hero, features, city, search, rail, keys, captures, live frames, focus, skip, full screen), in Chrome, Firefox and a phone 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 37ea96721a T45e Site · Local passes review 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 52d94a79c9 T45e Site · Local: pnpm local builds the gallery for localhost, builds the site and serves both on 4173 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 557c8dd2cd T45d Site · Components passes review; Slash leaves a hidden field alone; the review log moves to tasks/reviews.md 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 5cc9a302fb T45d Site · Components: the rail reveals the current story with room, Shift arrows stay the page's 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 8444dd3c12 T45d Site · Components: comments one short line 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 97f9811356 T45d Site · Components: one search over chapters, stories and components (Slash, Enter, Escape), the index in hairline rows, the story beside a sticky chapter rail, arrows between stories 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 24687a09fb T45c Site · Home: full screen stops the glide at once, a starting gallery leaves the host's focus alone, a focused frame stays mounted 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY c3f9cd533a T45c Site · Home: full screen locks the page's overflow and Lenis halts on it, ending a glide in flight 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY e2b9670043 T45c Site · Home: a route change ends the glide, full screen holds against the wheel, each skip control focuses its own target 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 23ebaba2a1 T45c Site · Home: a full-screen hero, five live features beside their miniatures, counts on the beat, the chapters as a city, a closing call; smooth scroll, reveals from scroll, a fade between routes; the OG image from the new hero 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY f9c20b251c T45b Site · Iso passes review 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY dbce181588 T45b Site · Iso: diagonal cells in order, the pointer meets only grown cells, motion follows the system live, replay by key, the header fits 280px 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 3aa895c24d T45b Site · Iso: isometric line miniatures on a canvas, hidden lines removed, cells on a marimba beat, playable under the pointer, still under reduced motion 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 8a5ce82119 T45a Site · Stack passes review 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 5a29fc780a T45a Site · Stack: comments one short line 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY b22e80ac47 T45a Site · Stack: reloads open in the current mode, the first paint keeps the system's, plain route changes 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY f67e5c0839 T45a Site · Stack: Vite 8, React 19 and TanStack Router under pnpm; routes, theme, fonts, the manifest 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY e17e31a689 Update README.md 2026-09-30 00:29:08 -04:00
ZacharyZhang-NY b4ad854508 T45 Website: native captures stand in where a browser cannot run, an OG image, no web notes 2026-09-30 00:03:23 -04:00
ZacharyZhang-NY 5550552e76 T45 Website: an isometric miniature grows to a marimba; finance, charts and chat beside their models; no bars, no boxes 2026-09-29 23:40:04 -04:00
ZacharyZhang-NY 3153d892f4 T46 E2E: Playwright walks the built site, desktop and phone, live stories included 2026-09-29 23:40:04 -04:00
ZacharyZhang-NY 3ec0ae790c T45 Website: a home in motion and every story live, chapters on the left 2026-09-29 23:40:04 -04:00
ZacharyZhang-NY ab3f6eb98f T44d Web · Stories passes review 2026-09-29 23:39:50 -04:00
ZacharyZhang-NY acc39374fa T44d Web · Hidden builders start nothing, fields blink only in focus, grids paste with Control 2026-09-29 23:02:37 -04:00
ZacharyZhang-NY 752e7cba0c T44d Web · Hidden demos keep still, wide ones scroll whole, pastes read the web's clipboard 2026-09-29 22:13:27 -04:00
ZacharyZhang-NY d2018f8510 T44d Web · Stories stand alone: nested ones surface, hidden ones drop their demos, early themes land, pastes arrive 2026-09-29 19:16:41 -04:00
ZacharyZhang-NY 2a132d7d41 T44d Web · The gallery's start takes its defaults from one place 2026-09-29 19:16:41 -04:00
ZacharyZhang-NY 95db323797 T44d Web · What a browser cannot do says why; fonts resolve; the bundle builds
Demos that open a second window, take files, move the window, read the
clipboard or set native menus show a note on the web in the blocked style.
IBM Plex Sans Regular joins the assets, unmodified, where gpui's svg
renderer loads it: gpui_web answers .SystemUIFont with it. The gallery's
web entry registers a Hebrew cut of Noto Sans Hebrew. scripts/web.sh builds
the bundle with wasm-bindgen and wasm-opt -Oz, 20.0 MB, and fails past 25 MiB.
2026-09-29 19:16:41 -04:00
ZacharyZhang-NY 09dd3fc2cb T44d Web · Gallery builds for wasm32: one story per section by URL, themed by the host
The gallery's entry runs natively or, on wasm32, from a wasm_bindgen start
through gpui_web, single-threaded with WebGL2 fallback. ?page, ?story and
?theme choose what shows; an unknown one fails in the page and the log.
set_theme switches the mode later. Pictures load from ELY_GALLERY_ASSETS.
scripts/stories.py writes examples/gallery/stories.json from the pages.
2026-09-29 19:16:41 -04:00
ZacharyZhang-NY 29f1c8f467 web: the library checks for wasm32 (carried) 2026-09-29 19:16:41 -04:00
ZacharyZhang-NY 822a58810a T44c Port · Parity passes review 2026-09-29 19:14:18 -04:00
ZacharyZhang-NY 26629a8b74 Port · Parity: the last long comments are short 2026-09-29 18:43:49 -04:00
ZacharyZhang-NY 007ed30d4a Port · Parity: dark shots judged; the status shot waits out the tooltip; comments one short line 2026-09-29 18:18:05 -04:00
ZacharyZhang-NYandClaude Opus 5.5 d7272e88bd Port · Parity: every page matches gpui 0.2.2's; the rules re-tested
Every page and scripted state was shot with gpui 0.2.2 and with the
port, light and dark, full width and at 280px, judged pair by pair by
agents with skeptics, and every page's log diffed. What differed is
fixed, each with a test that fails without it where the test platform
can show it:

- The gallery's harness takes posted mouse moves (gpui_macos listens
  to a tracking area), posts each drag move twice (it no longer
  replays a held drag), and waits three frames before each shot.
- Rolling digits: a block column, so taffy 0.13 lends the cell no
  baseline, and a line of whole device pixels, since gpui snaps each
  length on its own. DescriptionList's row aligns ends.
- Margins that taffy 0.13 collapses through a plain block: the
  drawer's pull and snap, the stagger and transition rise, the dialog
  and palette rises, the fullscreen dialog's slide.
- An Input keeps room to type in a squeezed row; the field rows drop
  w_full, whose reason was taffy 0.9's.

AGENTS.md's rules that named gpui 0.2.2 or taffy 0.9 were re-tested
against both sources and rewritten, and new ones say what the port
changed. TASKS.md's tags follow: what the new gpui makes possible is
prove. Captions, doc comments and README say what holds now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 10:39:55 -04:00
ZacharyZhang-NYandClaude Opus 5.5 41368430f5 Tasks: the site deploys to Cloudflare through wrangler
The Dokploy line was a misreading; the plan stays as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:28:32 -04:00
ZacharyZhang-NYandClaude Opus 5.5 dc465d7244 T44b Port · Build passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:24:24 -04:00
ZacharyZhang-NYandClaude Opus 5.5 f76c3c1280 Port · Calendar: a real exit clears the preview after a key
The calendar's grid clears its preview on leave, and ignores a leave
that follows a key. gpui records the exit a key makes before the
handler runs, so the guard swallowed it and a real exit afterwards
had no change left to report: the stale day stayed the preview. The
grid now listens independently of input, while its days keep the
default, so a key is no leave and a real exit is. A test leaves the
grid after an arrow, a letter and a month key.

shaped()'s doc goes back to its one line; AGENTS.md keeps the why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:54:25 -04:00
ZacharyZhang-NYandClaude Opus 5.5 94deeaed52 Port · Build: Zed's gpui at 1a28cff
gpui and gpui_platform now come from Zed's repo by git at 1a28cff,
the code gpui-pre 0.3.7 republishes, so the gallery can later build
for the browser. gpui takes stacker, as 0.2.2 always did, and
gpui_platform takes font-kit, without which macOS draws no text;
runtime_shaders moves to gpui_platform.

The API moved: focus, blur and Tab steps take cx, Corner is Anchor,
shaped lines paint with an alignment, defer_draw takes a content
mask, the inspector registers a factory, async updates return their
value, and wrapped lines are shared, not cloned. Updates through a
window context go through the window, so a closed window still ends
them as it did.

Where the new gpui behaves differently, Ely keeps its old intent.
Holds and reveals under a resting pointer survive typing through
gpui's input-modality-independent hover; previews the keyboard also
moves keep the default, and the calendar's leave ignores a key press.
Enter reaches a button inside the code editor, since gpui now arms a
key click on the press. The event logger skips a lone modifier. A
video and an image grid keep their shape in a flex column under
taffy 0.13, as a picture does.

AGENTS.md records the new stack and the rules this port changes; the
rest of the old gpui and taffy rules are re-tested in T44c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:30:17 -04:00
ZacharyZhang-NYandClaude Opus 5.5 e49c93bb6f Tasks: live components on the site; Zed's gpui; Dokploy
The site shows every component running, as gpui-kit.com does, never
a screenshot. gpui 0.2.2 cannot build for the browser, so T44b moves
gpui to Zed's repo at 1a28cff, T44c re-checks every page and rule on
it, and T44d builds the gallery for wasm32 as one story per
component. The screenshot items give way, and the site deploys to
Dokploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:13:44 -04:00
ZacharyZhang-NYandClaude Opus 5.5 9ef59b01e4 T44a Capture · Roots passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:13:05 -04:00
ZacharyZhang-NYandClaude Opus 5.5 e482bac865 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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 19:34:01 -04:00
ZacharyZhang-NYandClaude Opus 5.5 ca5c967f17 Capture · Roots: the twelve roots round 1 found
Eleven more roots drop w_full: the docked ActionBar, MessageBubble's
three arms, PromptInput, TitleBar, Tabs, the horizontal Divider with
and without a label, DropZone and Playlist. Their hosts in the crate
are blocks or flex columns. The gallery's docked ActionBar sits in an
items_center column, so the demo gives it the width.

AspectRatio keeps w_full, and AGENTS.md says why: taffy 0.9 follows a
ratio from a set width, not from a stretch, and without one the image
gallery's tiles and the ratio demo collapsed.

Every page and scripted state was shot again at full width and at
280px and compared with the shots from before T44a. Nothing collapsed
or shrank. The mail reader's attachments now wrap side by side, as
their row asks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:30:47 -04:00
ZacharyZhang-NYandClaude Opus 5.5 5779daef82 Capture · Roots: roots drop w_full; hosts give the width
145 component roots drop w_full, the early returns included. Three
field rows keep it, Input, InputGroup and TagInput: stretched without
a width, taffy 0.9 laid the field's flex_1 share at no width in the
template picker at 280px, and the placeholder drew nothing. Container
keeps it, since mx_auto needs a width to fill.

Hosts that relied on a root's width now give it: MessageList boxes
each row at the list's width, the splash gives its bar the column's,
and test stages that mount a component as the window's root sit in a
size_full box. Tests hold a narrow row across the list and the splash
bar across its column; each goes red without its fix.

The gallery gains --narrow <px>, which lays each page in a padded
card at that width and shoots it without its script. The page
header wraps: the title takes the room and the theme switcher drops
below, where a long summary used to push it past the edge. A file
panel step waits for the panel to become key instead of a fixed
pause, which lost a race in a capture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:07:22 -04:00
ZacharyZhang-NYandClaude Opus 5.5 486f618ecc Tasks: split T44 into three
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 09:12:25 -04:00
ZacharyZhang-NYandClaude Opus 5.5 d0fbfd1a4e T43c Tooling · Catalogs passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 09:10:55 -04:00
ZacharyZhang-NYandClaude Opus 5.5 ed1014bc34 Tooling · Catalogs: the audit reads active and code; avatar and container sizes
The audit now holds fg_subtle on active, a selected row's fill, and
each syntax color over the editor's surface, a code block and the
current line. What it found is fixed at the source: fg_subtle is
#696764 light and #94928f dark, syntax.comment follows it, and the
light number, constant and attribute darken to #a3591b, #aa5529 and
#9f5d12. The token viewer adds avatar and container sizes, draws a
container at its share of the widest so it stays in its row, and
names what it shows: the shared colors and scales. Tests pin every
variant, the hue each chart row reads and the code rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 08:57:52 -04:00
ZacharyZhang-NYandClaude Opus 5.5 d2ac6c7753 Tooling · Catalogs: token viewer, contrast audit; fg_subtle meets AA
DesignTokenViewer shows every token of the theme in the mode shown,
named as code reads it: colors with their hex, syntax, chart and
terminal colors, the type scale, radii, control and icon sizes,
elevations and spacing. A11yChecker audits each text color over the
surfaces it sits on at 4.5:1 and the focus ring and chart hues at
3:1, failures first. The audit found fg_subtle under 4.5:1 on hover
and overlay; it is #6f6d6a light and #8c8a87 dark now, and a test
holds all four Ely palettes to the audit. IconBrowser points to
forms::IconPicker and the ScreenshotTest Harness to the gallery's
capture, each with a section on the tooling page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 08:23:39 -04:00
ZacharyZhang-NYandClaude Opus 5.5 e5910068e5 T43b Tooling · Inspection passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 08:05:26 -04:00
ZacharyZhang-NYandClaude Opus 5.5 517d23f78f Tooling · Inspection: stalls count whole, the panel owns Tab, bound keys log
FpsMeter keeps the stamp before the last second, so a frame longer
than the span shows as the worst and a bar. The inspector's panel, a
second root out of the app's scope, sits in a FocusScope of its own,
so Tab goes on from Pick. EventLogger takes key downs through an
interceptor, which runs before bindings, and marks its box with a key
context to know focus is inside, so a field's Backspace and Enter log.
Tests hold the stall, Tab both ways through Pick, bound keys in a
field, a key while focus is outside, newest first and a bare border.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 07:54:54 -04:00
ZacharyZhang-NYandClaude Opus 5.5 5f951ea581 Tooling · Inspection: inspector, FPS meter, render counter and event logger
install_inspector draws gpui's debug inspector in Ely's look: a panel
with the picked box's source, ids and box model, and its margin,
border, padding and size drawn over the page through raise, since the
panel scrolls. FpsMeter counts frames on the executor's clock and
colors each by the frames it missed. RenderCounter counts the renders
of the view that draws it. EventLogger lists the input that reaches
what it holds, its canvas laid first so a child's stop hides nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 07:13:12 -04:00
ZacharyZhang-NYandClaude Opus 5.5 a0d93eb35f T43a Tooling · Workbench passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 06:46:24 -04:00
ZacharyZhang-NY f3b328e8f3 Tooling · Workbench: the knobs seed the settings; knobs checked
The knobs join a layout::seeded seed, so a changed starting value,
option list or range starts every setting over, and a renamed knob
no longer fails. new() refuses no knobs, twin names and a number
outside its range. Controls are keyed by knob name. The test presses
keys without a refresh, so only a notify redraws, and walks the
switch twice, the slider twice and a pick the owner takes away.
2026-09-28 06:37:06 -04:00
ZacharyZhang-NYandClaude Opus 5.5 6c21711328 Tooling · Workbench: a playground, and the gallery as the workbench
tooling::Playground lays a preview beside its knobs, each a switch, a
choice or a number that edits one property; the preview redraws from
the settings, which fail on a name or kind the knobs lack. The gallery
app is the ComponentGallery; its page 43 shows both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 06:08:40 -04:00
ZacharyZhang-NYandClaude Opus 5.5 989eaf29b8 Tasks: split T43 into three
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 06:03:22 -04:00
ZacharyZhang-NYandClaude Opus 5.5 264b552fd4 T42g Misc · Export and import passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 06:03:05 -04:00
ZacharyZhang-NYandClaude Opus 5.5 cddbd150e6 Misc · Export and import: reasons wrap; every way out tested
feedback::InlineMessage lays its words in a block that wraps, so the
importer's reason for unread text no longer runs past the dialog. The
reader skips a leading byte order mark, names the line an unclosed
quote opened on, and logs once per reading. Tests press each Cancel,
the scrim, Export with the first format, see focus return to the
opener, read new text under an open importer, and see a field with no
column come empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 05:50:28 -04:00
ZacharyZhang-NYandClaude Opus 5.5 9912638fa7 Misc · Export and import: export, field mapping and a CSV importer
misc::ExportDialog offers the owner's formats as cards and hands the
chosen one over for the owner to write. misc::ImportDialog maps the
file's columns to the app's fields, matched by name to start, over a
preview, and asks for each required field's column. misc::CsvImporter
reads CSV as RFC 4180, lets the first row name the columns or not, and
maps it through the ImportDialog into records; text that does not read
says why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 05:20:03 -04:00
ZacharyZhang-NYandClaude Opus 5.5 b7c8629ea7 T42f Misc · Web passes review
in_view names its types in place and the non-macOS branch reads its fields, so builds off macOS stay free of warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 05:07:35 -04:00
ZacharyZhang-NYandClaude Opus 5.5 1b04dc07a4 Misc · Web: the web view hands the keyboard back
WKWebView takes AppKit's first responder on a press and gpui's view
never asks for it back, so typed text went to the page for good. The
web view now hands it back through wry's focus_parent on every press
gpui gets and when its page drops. The gallery proves it with a native
key after a press on the page. in_view builds only where it is used,
its test covers a box that straddles the view's top, and the embed's
frame takes the card's bottom corners.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 04:59:11 -04:00
ZacharyZhang-NYandClaude Opus 5.5 529394e57b Misc · Web: a web view proven with wry, and an iframe embed
misc::WebView lays wry 0.57's child WKWebView over the window at its
box, following it from a canvas in prepaint and hiding it out of view;
it loads an address or the owner's HTML, which makes it the HTML
preview. misc::IframeEmbed frames a page under its address with Open
in browser. EmbedCard points to chat::LinkPreviewCard, ShareSheet to
buttons::ShareButton; PrintButton is blocked, gpui 0.2.2 has no print
API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 04:32:26 -04:00
ZacharyZhang-NYandClaude Opus 5.5 45e128e069 T42e Misc · Terms and notices passes review
A test walks Shift-Tab round the cookie banner past the resting Necessary switch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 04:25:31 -04:00
ZacharyZhang-NYandClaude Opus 5.5 067ef3f122 Misc · Terms and notices: tests for every way out; switch labels wrap
Tests now press Decline and the scrim, see focus return to the opener
after Accept, see necessary cookies stay on and off the Tab path, and
see the license list mark its row and read each version and license.
lists::ListItem marks its description and selected state for tests.
forms::Switch's label wraps in its own block, as Checkbox's does.
Ely's own row in the gallery shows both of its license texts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 04:09:49 -04:00
ZacharyZhang-NYandClaude Opus 5.5 6c589c3115 Misc · Terms and notices: licenses, consent and a cookie banner
misc::LicenseViewer lists what a product ships with each license beside
the chosen one's text. misc::ConsentDialog asks for a box before Accept
and answers each way out once, accept or decline. misc::CookieBanner
takes Accept all, Reject all, or a switch per kind through Choose, which
turns into Save choices on its focus. ChangeLog Viewer points to
data_display::Changelog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 03:36:03 -04:00
ZacharyZhang-NYandClaude Opus 5.5 4d21b15219 T42d Misc · Asking and learning passes review
The poll's mark and its row's selector share one binding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 03:24:15 -04:00
ZacharyZhang-NYandClaude Opus 5.5 8f85e4fe04 Misc · Asking and learning: Submit never rests; radio labels wrap
Survey's Submit asks for any missing answer instead of resting, so no
untick takes its focus handle out of the tree. Flashcards turn and go
round, and a quiz pick clears its ask, through methods the tests reach;
the poll's rows show which one holds the vote. forms::Radio's label
wraps in its own block, as Checkbox's does, so a long one stays in a
narrow box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 03:11:52 -04:00
ZacharyZhang-NYandClaude Opus 5.5 6e02a0cce3 Misc · Asking and learning: a poll, a survey, a quiz and flashcards
misc::Poll turns to results once the owner holds the vote, with Change
vote on Vote's stop. misc::Survey asks on one page and sends when each
question is answered, thanking in Submit's place and focus. misc::Quiz
checks, goes on, scores and starts again on one button. misc::Flashcards
turn on a press and go round the deck, each face fading in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 02:38:33 -04:00
ZacharyZhang-NYandClaude Opus 5.5 19a5295ed8 T42c Misc · Codes and checks passes review
Tests now hold focus in the field after an answer and a passed captcha with no Tab stop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 02:22:34 -04:00
ZacharyZhang-NYandClaude Opus 5.5 fa72f2521d Misc · Codes and checks: the captcha keeps focus through every verdict
Verify never rests, so no verdict or new ask takes its focus handle out
of the tree; with nothing typed it sends focus to the field. Enter hands
nothing over while the owner checks. A pass moves focus from the resting
field to the Verified mark, so the field shows no ring and no caret.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 02:05:44 -04:00
ZacharyZhang-NYandClaude Opus 5.5 8648f5eed1 Tasks: the website's home page, components page and Cloudflare deploy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 01:40:15 -04:00
ZacharyZhang-NYandClaude Opus 5.5 621c8406bb Misc · Codes and checks: a QR code scanner and a captcha
misc::QrCodeScanner reads QR codes with rqrr 0.11 from the frames the
host hands in, one at a time off the main thread, and hands each code
to the owner once. misc::Captcha shows the owner's picture over an
answer field that empties and keeps focus with each answer; the owner
says how it stood. BarcodeDisplay, Signature and Rating Widget point
to their homes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 01:40:15 -04:00
ZacharyZhang-NYandClaude Opus 5.5 854f44ed6c T42b Misc · Numbers passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 01:14:17 -04:00
ZacharyZhang-NYandClaude Opus 5.5 d0bf6ee5e3 Misc · Numbers: a result goes on whole; long readings wrap
A calculator result stays in the sum as `a`, handed to evaluate with
its whole value, so 1 ÷ 3 = × 3 = gives 1; sign turns it and delete
takes it. Equals waits on a dangling operator. The converter's reading
wraps in its box rather than ending in an ellipsis, and tests hold
Control, the = key, a point after a result and the six digits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 01:06:32 -04:00
ZacharyZhang-NYandClaude Opus 5.5 fc10b64303 Misc · Numbers: a calculator and a unit converter
misc::Calculator works a typed sum in forms::evaluate's precedence,
from a pad or the keyboard; Enter is equals while the calculator
itself has focus, and Escape clears or passes on. misc::UnitConverter
reads an amount in another unit across seven kinds, with a swap.
Both show numbers through format::significant, which rounds through
Rust's exponent form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 00:41:49 -04:00
ZacharyZhang-NYandClaude Opus 5.5 87b0effafb T42a Misc · Time passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 00:30:00 -04:00
ZacharyZhang-NYandClaude Opus 5.5 977ba4c335 Misc · Time: Lap turns Reset, laps and tickers tested
The stopwatch's second button reads Lap while running and Reset once
paused, on one focus handle, and Reset hands focus to Start, so no
press leaves focus stranded. Lap rows carry selectors; tests hold
their order and times, count the tickers' wakes, and a WorldClock
without places fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 00:21:23 -04:00
ZacharyZhang-NYandClaude Opus 5.5 6918370b08 Misc · Time: clocks and a stopwatch
misc::Clock shows the time in a zone, the system's unless told, as an
analog face with hour marks and three hands over its digits and a
label; it wakes its view at each whole second of the wall clock, and
only while the owner passes no moment. misc::WorldClock lays places in
rows: a small face, the place, how its day and hour stand against home,
and its time. misc::Stopwatch runs on the executor's clock in tenths,
holds while paused, marks laps while running and clears once paused;
Start and Pause share one button and its focus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:51:58 -04:00
ZacharyZhang-NYandClaude Opus 5.5 7cce4ab3fc Tasks: split T42 into seven
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:44:24 -04:00
ZacharyZhang-NYandClaude Opus 5.5 52a98bf676 T41c Maps · World passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:42:16 -04:00
ZacharyZhang-NYandClaude Opus 5.5 8a3f8d152e Maps · World: marks in their wash, the legend clear of the credit
A choropleth's points and lines now draw in their wash, as its doc
says, while its areas keep the page's color at their edges. The legend
moves to the top left, so the attribution a license asks for stays clear
at any width. A key given twice fails. WorldMap's keying by ISO code,
its Natural Earth credit and its acceptance of the bundle's codes, as
Kosovo's XK, each have a test; so does an endless value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:35:04 -04:00
ZacharyZhang-NYandClaude Opus 5.5 752266c36d Maps · World: choropleths and the world's countries
ChoroplethLayer washes each area by its value, keyed by its id or a
property's text or number, on one hue's scale from its faintest at the
least value to its strongest at the most; an area without a value reads
as no data. ChoroplethMap lays it on the owner's map, with or without
tiles, and puts a legend of the scale and of no data in the corner; a
value that matches no area is logged once. WorldMap washes Natural
Earth's 110m countries, bundled trimmed by scripts/countries.py, by ISO
3166 alpha-2 code, and fails on a key that is no code. A shape now
paints as one fill and one edge path per world in view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:12:27 -04:00
ZacharyZhang-NYandClaude Opus 5.5 811a187a3e T41b Maps · Layers passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:01:43 -04:00
ZacharyZhang-NYandClaude Opus 5.5 11fbb07ff8 Maps · Layers: tests hold what clusters promise
Tests hold that a pan keeps a focused cluster, that a cluster away from
the view takes no Tab, that a zoom out folding a focused pin hands its
focus to the map, and that a press shows members far apart whole. The
heat test pins the quartic kernel's value halfway out; a ring of fewer
than four positions fails with its own reason, and has a test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:57:05 -04:00
ZacharyZhang-NYandClaude Opus 5.5 a3115b8126 Maps · Layers: routes, GeoJSON, heat and clusters
MapView takes layers over its tiles and under its pins. RouteLine runs
along places the short way across the date line. read_geojson reads RFC
7946 to features, strictly: a position past the globe, a line of one
position or a ring that does not close fails with its reason, and a
feature placed nowhere keeps no geometry. GeoJsonLayer washes areas as
one path per polygon, so holes stay open, outlines them, strokes lines
and dots points. GeoHeatmap spreads weighted places in a quartic kernel
over small cells, scaled to the densest spot in view. Lines and areas
unwrap across the date line and repeat on every world in view.
cluster_markers gathers pins by squares of the world, so a pan keeps
each group; a cluster shows its count, and its press shows its members
whole, never farther out. A zoom that may regroup them hands a focused
pin's or cluster's focus to the map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:35:40 -04:00
ZacharyZhang-NYandClaude Opus 5.5 f5909f6e43 T41a Maps · View passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:20:08 -04:00
ZacharyZhang-NYandClaude Opus 5.5 e84fbb9a61 Maps · View: a focused pin keeps focus off view
Each pin holds a keyed focus handle of its own, a Tab stop and pressable
while its place is in view, so a focused pin that pans out of view keeps
focus and the arrows go on panning, and a popup that scrolls away hands
focus back to its pin wherever it lies. A popup whose place is off view
takes no focus. Tests hold the tall card's cap and scrolling body, the
map staying under its wheel, and markers refuse a repeated key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:14:42 -04:00
ZacharyZhang-NYandClaude Opus 5.5 bbba5ee04b Maps · View: pins in view take presses, the popup hands focus back
A pin whose place lies outside the view takes no press or Tab, as its
popup would not show. The popup takes focus when it shows and, the
frame it stops showing by its button, Escape, the owner or a pan, hands
focus back to what held it, so focus stays in the map. Its body scrolls
past the room on its side and it paints over the zoom buttons. Tiles
load through gpui's logging loader, so a tile that fails says so. A
drag's owner check has a test, and the per-move viewport logs at debug.
The maps tests split into the map's and the popup's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:57:27 -04:00
ZacharyZhang-NYandClaude Opus 5.5 d6a56f3964 Maps · View: host tiles, pins and a popup, a picker, coordinates
maps::MapView lays the host's tile for each zoom, column and row in Web
Mercator, the nearest cut scaled past the zooms it serves, on whole
pixels. gpui packs pictures in its atlas without padding and samples
them linearly, so each tile draws with a one-pixel rim copied from its
own edge, one of its pixels larger each side and clipped, and no seam
shows. A drag, the wheel or the arrows pan; Command-wheel, + and - or
its buttons zoom about the pointer or the center; the owner keeps the
viewport. MapMarker pins take a press and Enter when the map hears
them; MapPopup opens toward the map's larger half and stays inside it,
closing by its button or Escape. LocationPicker picks the place under a
center crosshair; CoordinateDisplay writes degrees or degrees, minutes
and seconds with tabular figures and a copy button. The gallery's tiles
are Natural Earth's countries, cut z0 to z3 by scripts/tiles.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:22:50 -04:00
ZacharyZhang-NYandClaude Opus 5.5 8da3edbeda Tasks: split T41 into view, layers and world
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:57:30 -04:00
ZacharyZhang-NYandClaude Opus 5.5 7e32d2c294 T40b a11y passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:52:32 -04:00
ZacharyZhang-NYandClaude Opus 5.5 bb59ae845f Gallery: a blocked note wraps beside its dot
blocked() laid its text as a flex item beside the dot, and gpui measures
a text item's least width unwrapped, so a long note ran past its column
and the dot shrank to nothing. The text now sits in a flex_1 block with
no least width beside a dot that never shrinks, which rests on the first
line, as Alert holds its icon. The a11y note and the motion page's
Lottie note wrap inside their columns. A failed hue check names both
charts in hex.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:42:19 -04:00
ZacharyZhang-NYandClaude Opus 5.5 6265afbb01 a11y: a skip link, color-blind safe charts
i18n::SkipLink holds a Tab stop in flow at zero size, first where it
sits, and while focused raises a pill over the page; Enter, Space or a
press hands focus to its target. gpui orders stops by tab_index before
paint and registers none for an invisible element, so the stop stays in
flow and only the pill is raised. Theme::set_color_blind_safe swaps 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, and red against green shows the measure
bites. KeyboardNavigationHelper points to interaction::RovingFocus. The
announcer, labels and screen-reader text stay blocked: gpui 0.2.2 has no
accessibility tree, and nothing here is trusted to observe VoiceOver.
The i18n tests move under src/i18n/tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:00:26 -04:00
ZacharyZhang-NYandClaude Opus 5.5 ff8f4ba84f T40a i18n passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 19:40:34 -04:00
ZacharyZhang-NYandClaude Opus 5.5 928da801fb i18n: a right-to-left message leads with its mark
gpui sets no paragraph direction, so CoreText took a Hebrew line whose
first strong character was Latin, or the left-to-right mark of a signed
number, as left to right and swapped its parts. I18n::text now leads a
right-to-left message with U+200F; the number keeps CLDR's U+200E, so
its sign stays at the digits' left. The demo adds a change that opens
with a signed number, in every catalog; under Hebrew it lies at the
line's right end. A test holds the marks on a message that starts with
a number and one that starts with a Latin name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 19:36:12 -04:00
ZacharyZhang-NYandClaude Opus 5.5 219e4744c0 i18n: Hebrew on one line, its sign held left, CLDR's short years
gpui 0.2.2 wraps shaped glyphs in the order they are drawn, so a
right-to-left line that wraps comes out garbled. The demo's Hebrew
lines are Ellipses, which cut by characters, and an Ellipsis now sits
by its text's alignment, so a short line under text_right lies at the
right; the editor's completion details take the side they ask for. A
right-to-left locale leads a minus with a left-to-right mark, as CLDR
does, so the sign stays at the digits' left inside Hebrew. en-US and
de-DE write CLDR's two-digit year. The repeated-key and second-catalog
guards have tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 19:20:13 -04:00
ZacharyZhang-NYandClaude Opus 5.5 2a91218fd8 i18n: a locale per app, catalogs of messages, mirrored stacks
i18n::I18n is a gpui global holding the shown locale and one catalog of
messages per locale. text fills each {name} from its arguments; a
missing key, argument or catalog fails, and every catalog holds the
first one's keys. Locale writes numbers with its separators and dates
in CLDR's short numeric form; Hebrew replaces Arabic, whose Egyptian
digits Ely does not write. Direction::row mirrors a stack and
Direction::align sets text against the reading side. The gallery's
chapter 40 shows the messages in seven locales, every locale's number
and date, and a file list that mirrors under Hebrew.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 18:59:45 -04:00
ZacharyZhang-NYandClaude Opus 5.5 8c3ff2c584 Tasks: split T40 into i18n and a11y
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 18:26:52 -04:00
ZacharyZhang-NYandClaude Opus 5.5 215b1d49ae T39c Theme · Platform passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 18:25:40 -04:00
ZacharyZhang-NYandClaude Opus 5.5 ba865710a7 Theme · Platform: a title bar in three parts, buttons at the window's rem
A title bar lays out a start, a title and an end. In the centered styles
the start and end grow alike from nothing, their padding inside them, so
a short title sits at the bar's center and a long one gives way before
whatever stands at either end; on Windows the title, or a spacer when
there is none, takes the free width before the buttons. Caption buttons
take the bar's height at the window's own rem. Tests keep an untitled
Windows bar's buttons at its end, a long Linux title before the round
buttons, a short title centered, and the buttons inside a bar at rem 8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 18:20:20 -04:00
ZacharyZhang-NYandClaude Opus 5.5 815aed304d Theme · Platform: caption buttons of their own height, titles that give way
Windows caption buttons take the bar's height less its hairline, a new
token, instead of their row's, so they keep their height beside caps
that wrap below them. A title bar's title is an Ellipsis: on Windows it
takes the free width before the buttons and gives way; a centered title
sits inset past the lights on both sides. The controls never shrink.
Tests hold a long Windows title's close button inside a 280px bar and
wrapped caps inside their row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 18:04:05 -04:00
ZacharyZhang-NYandClaude Opus 5.5 f3c8cca4c9 Theme · Platform: one platform for buttons and caps, icon themes, frosted panes
theme::Platform moves out of typography and shell's ControlsStyle folds
into it; the theme's platform starts as the system's. Window buttons Ely
draws, the title bar's layout and every key cap follow it, secondary
included, through typography::keys::keystroke; a title bar whose buttons
the OS draws follows the OS. primitives::IconTheme lays an owner's rules
by file name and extension over Ely's map, for every file icon.
shell::Vibrancy fills a pane with the palette's new glass, translucent
so a window opened Blurred shows its backdrop, solid under high contrast.
gpui 0.2.2 has no Mica; that line stays blocked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 17:41:15 -04:00
ZacharyZhang-NYandClaude Opus 5.5 53578e0525 T39b Theme · Importing passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 17:34:16 -04:00
ZacharyZhang-NYandClaude Opus 5.5 106faca970 Theme · Importing: a long name gives way, four-digit colors read
A read theme's name is an Ellipsis that gives way beside its mode badge,
so a long name keeps the badge inside a 280px box. parse_hex reads #rgba
as VS Code does, each digit doubled, so a theme with a four-digit color
reads. Tests hold the badge inside the box and read #0008 as black at
0x88 alpha.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 17:32:21 -04:00
ZacharyZhang-NYandClaude Opus 5.5 d5cfaeb19c Theme · Importing: VS Code themes read into Ely's palette, code palettes to choose
settings::read_vscode_theme reads a VS Code color theme, comments and
trailing commas and all: its type picks the mode, its colors and token
scopes name what they give, and the rest keep Ely's palette of that mode.
settings::ThemeImporter takes one dropped or browsed for, shows its name,
mode and count, and hands it over on Apply; a file that does not read
says why. theme::syntax_themes offers Ely's code colors, Quiet and Paper,
chosen through settings::SyntaxThemePicker, radio cards with a line of
code in each palette. A card's words now shrink as a plain block, so a
preview wraps inside the card at 280px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 17:11:55 -04:00
ZacharyZhang-NYandClaude Opus 5.5 38ee858b27 T39a Theme · Editing passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 17:02:23 -04:00
ZacharyZhang-NYandClaude Opus 5.5 d49e02ceee Theme · Editing: segments sized by their labels, a thumb that covers its segment
A segmented control's segments now take their labels' widths first and
share the spare room, so a label ends in an ellipsis only when the whole
strip lacks room. The thumb's insets meet the strip's padding again, and
each segment is measured from its borderless wrapper, so the thumb covers
the chosen segment exactly. Tests hold Comfortable whole at 280px and the
thumb to the chosen segment's bounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:58:23 -04:00
ZacharyZhang-NYandClaude Opus 5.5 1860764d95 Theme · Editing: segments that give way, every color named
SegmentedControl's segments now give way when narrow, their labels
ending in an ellipsis, down to twice a control's height each; past
that the strip scrolls as before. The pill's border and padding moved
onto the scrolling strip, and the row inside it takes that floor as its
least width, so the strip scrolls exactly to its last segment. The
palette macro now writes each color's name from its field: Palette and
Syntax gain NAMES and token_mut for every color, the quiet fills too.
Tests press a density segment and each slider, and fit the density
strip in 280px with the text a step and a half larger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:48:07 -04:00
ZacharyZhang-NYandClaude Opus 5.5 3d2bc0f4d6 Theme · Editing: a theme laid open, and an owner's palette per mode
settings::ThemeEditor lays a theme open: density, corners, text size,
high contrast and reduced motion, then twelve colors of the mode shown,
each a swatch that opens a picker beside its hex; each change hands the
owner a whole draft. Theme::set_palette takes an owner's palette per
mode, the colors fading to it when the mode shows; Theme::palette names
the palette the shown mode fades to; Palette::token_mut and
Syntax::token_mut reach a color by its field's name. The gallery's
theme page applies the editor to the whole gallery live, with a way
back to Ely's theme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:31:33 -04:00
ZacharyZhang-NYandClaude Opus 5.5 5f2e6a5cda T38b passes review in round 1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:18:23 -04:00
ZacharyZhang-NYandClaude Opus 5.5 9e3e6c109d Interaction · Keys, scroll and focus: roving focus, scroll sync, type-ahead
RovingFocus keeps a row or column of items behind one Tab stop: the
arrows walk focus, Home and End reach the ends, Enter or Space presses,
and a press moves the stop without taking focus. ScrollSync keeps boxes
at one place along an axis: the pane a wheel turned leads, and a canvas
ahead of the panes copies its place before they draw. SelectableList
moves its cursor to the row that starts with the letters typed, the
letters joining within a pause and a Command letter left to the app.
forms::History becomes public as the undo manager. The gallery shows
gpui's own key bindings, a two-stroke chord waiting on its first
stroke, uniform_list, autofocus and the focus ring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:10:32 -04:00
ZacharyZhang-NYandClaude Opus 5.5 ec4c15b539 T38a passes review in round 2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:01:08 -04:00
ZacharyZhang-NYandClaude Opus 5.5 c3e8b114ce Interaction · Pointer: handles inside the box, drags from the press
Resizable's grip sits inside the box's lower right corner and its edge
handles lie inside the edges, their lines on the edge; the edges use
layout::resize_edge, a resize_handle whose line shows only while
hovered, since the box draws its own border. A drag now measures from
the handle's press, kept in keyed state, so the edge follows the
pointer from the first move. Tests drag the right edge downward and
the bottom edge sideways, turn the dial without Shift, and draw a band
with Shift over a selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:58:19 -04:00
ZacharyZhang-NYandClaude Opus 5.5 f513df6aaa Interaction · Pointer: resize, turn, band selection, and gpui's own drag
Resizable sizes a box from its right and bottom edges and a grip at
its corner, within its least and greatest size; the grip is a Tab stop
whose arrows resize a step. Rotatable turns an svg to its angle inside
a ring whose knob a drag moves, Shift snapping to 15 degrees, with the
arrows turning it a notch. SelectionArea selects items with a band
drawn on empty space, a press, Command or Shift to add, and a cursor
the arrows walk with Space to add or drop; Escape clears and
Command-A takes all. The gallery shows gpui's own drag and drop, drag
preview, double press and wheel zoom, with pointers for Sortable,
LongPress and PanZoom; pinch stays blocked, gpui 0.2.2 forwards no
magnify event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:42:03 -04:00
ZacharyZhang-NYandClaude Opus 5.5 9bde90a492 T37b passes review in round 2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:32:18 -04:00
ZacharyZhang-NYandClaude Opus 5.5 e26b2d1407 Onboarding · Help: articles in a list the keyboard walks, a long address cut
HelpPanel lists its articles in lists::SelectableList: the list is one
stop whose cursor scrolls into view, and Enter or a double press opens
an article, focus going to All articles as before. SelectableList now
activates on Enter's release, armed by its own press, as the palette
and menus pick: an activation that moves focus left the release to
press what took it, here the way back. ContactSupport cuts a long
address in the middle and puts a Copy beside it. The lists tests move
into a folder, the release tests on their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:28:49 -04:00
ZacharyZhang-NYandClaude Opus 5.5 5f5e2f73e1 Onboarding · Help: help panel, cheatsheet, what's new, feedback, support, inline help
HelpPanel searches articles and opens one, its body in Markdown, with a
way back; focus follows to All articles and back to the search, a
link to help:<key> opens another, and Contact support and Keyboard
shortcuts wait at its foot. KeyboardShortcutCheatsheet puts
settings::KeyboardShortcutsList in a dialog with its search focused;
WhatsNewDialog puts data_display::Changelog in one with Got it.
FeedbackWidget asks how it is going with four faces and a note, and
thanks. ContactSupport sends a topic and a message, spins while the
owner sends, and empties the message with focus back in it. InlineHelp
is a quiet line with Learn more. HelpTooltip moves to primitives,
where FormLabel's help mark shows it; ToggleGroup keys its toggles by
its own id. EmptyStateWithAction points to feedback::EmptyState.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:05:06 -04:00
ZacharyZhang-NYandClaude Opus 5.5 a066179699 T37a passes review in round 2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:56:30 -04:00
ZacharyZhang-NYandClaude Opus 5.5 973f10a6dc Onboarding · Flows and highlights: focus that holds as the flow turns, a tip that closes
navigation::Wizard keeps one focus handle for Next and Finish, so a
second press on the step before last finishes; Back that reaches the
first step, and a finished step picked by key, hand focus to Next.
Popover::with_opener hands its content a close that gives focus back:
Got it closes the hotspot's tip before the owner hides it, and the
event details close before Edit and Delete run, so focus never lands
on a panel that left the tree. The wizard's count and the setup
checklist's tally name their selectors after their words, and the bar
takes the count's own value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:48:20 -04:00
ZacharyZhang-NYandClaude Opus 5.5 996daed6e4 Onboarding · Flows and highlights: wizard, hotspot, highlight, setup checklist
OnboardingWizard runs a first run on navigation::Wizard without its
numbered steps: "Step 2 of 4" over a bar in as many parts, the step's
title, line and content, Back and Next once ready, Finish last, and
Skip for now. Hotspot pulses on a dot that opens a tip on a press or
Enter, with Got it. FeatureHighlight names something new under a New
badge, with Try it and Not now, and rises in. SetupChecklist counts
first things to do in words and a bar, offers Start on each left, eases
a check in as one gets done, and offers Hide once all are.

navigation::Wizard now keys its buttons, steps and motion by its own id
and takes on_step and on_finish at render, failing loud without them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:20:45 -04:00
ZacharyZhang-NYandClaude Opus 5.5 ebf8955556 T36c passes review in round 2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:14:12 -04:00
ZacharyZhang-NYandClaude Opus 5.5 2027842769 Account · Billing and team: a meter's name gives way, a month past 12 says so
A Meter's name takes the free width and ends in an ellipsis while its
count stays whole, so a long quota name keeps "9 of 12 seats" inside a
280px box. `expiry` says why it refuses a date, and Expires reads
"There is no month 13." apart from "This date has passed.". Tests now
cover an American Express card's fifteen digits and four-digit code,
the address emptying once an invitation goes, a quota past its limit,
and a prompt with benefits standing as a paywall and one without as a
line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:05:34 -04:00
ZacharyZhang-NYandClaude Opus 5.5 6f3d8036da Account · Billing and team: plans, quotas, cards, bills, prompts, members, invitations
SubscriptionCard shows a plan with its state, price and what comes
next, a way to change it, and Cancel after a second press or Keep plan
once canceled. UsageQuota gives each limit a meter and a line of how
much, or how far past. PaymentMethodForm tells a card's network from
its number, checks it with Luhn, and saves once the number checks out,
the month is ahead, the code is whole and a name is given; it rests
while the owner checks. BillingHistory lists bills with their state and
a download; UpgradePrompt stands as one line or as a paywall.
TeamMemberTable changes a member's role through RoleSelector and
removes after a second press, the owner staying; InvitationList
invites by a reading address with a role and resends or revokes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:49:17 -04:00
ZacharyZhang-NYandClaude Opus 5.5 b3ce50ecab TASKS: T36b passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:41:13 -04:00
ZacharyZhang-NYandClaude Opus 5.5 91d939e21e Account · Profile and access: a key's ends stay whole, keys drawn literally
A key's name gives way before its ends, which keep their width. The
ends and a new secret draw through typography::literal. Tests show this
device listing first and the name field emptying after Create.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:38:40 -04:00
ZacharyZhang-NYandClaude Opus 5.5 24e0366e0e Account · Profile and access: profiles, switchers, a user menu, sessions, keys
ProfileCard shows someone at a glance; ProfileEditor edits a profile
seeded by its owner and saves once it changed, has a name and an email
that reads, with a way to put it back. AccountSwitcher and
WorkspaceSwitcher show the one in use in a row that opens a menu of the
others and a way to add one; UserMenu opens from an avatar with who is
signed in, the owner's rows and Sign out, through the new
menus::menu_under. SessionList puts this device first, signs others
out one by one or all after a second press. ApiKeyManager lists keys by
their ends, makes one by name and shows its secret once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:29:35 -04:00
ZacharyZhang-NYandClaude Opus 5.5 5f26c8ee1e TASKS: T36a passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:23:11 -04:00
ZacharyZhang-NYandClaude Opus 5.5 fa167cab38 Account · Sign in: tests for each rest and wait
New tests: Sign in waits for a password and rests while busy; Create
account waits for a name; while one provider is on its way the other
takes no press; Send again rests while a link is on its way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:20:37 -04:00
ZacharyZhang-NYandClaude Opus 5.5 e36458e44e Account · Sign in: log in, sign up, providers, a link by email, a second factor
LoginForm takes an email and a password, keeps one signed in on a box,
and signs in on Enter once the address reads and a password is typed;
a failed try says why above Sign in. SignupForm ticks a new password's
rules as they hold and waits for all of them and the terms.
OAuthButtons offer each provider, one spinning while the rest rest.
MagicLinkForm and ForgotPassword send a link to an address and, once
sent, say where it went and offer it again. TwoFactorInput takes a
six-digit code at once and, after one fails, clears the boxes through
forms::PinInput's new attempt, keeping its focus. The Account chapter
splits into sign-in, profile and access, and billing and team.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:12:49 -04:00
ZacharyZhang-NYandClaude Opus 5.5 89a177d021 TASKS: T35 passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:06:34 -04:00
ZacharyZhang-NYandClaude Opus 5.5 6e5a742148 Settings: an accent palette folds inside a narrow box
The accent picker's palette takes what the custom well leaves and folds
its swatches; a test puts twelve presets in a 280px box and asserts
they fold under each other inside it. The settings tests split their
appearance cases into a file of their own. AGENTS says editing takes
the owner's handler itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:03:55 -04:00
ZacharyZhang-NYandClaude Opus 5.5 9a8dbcb92e Settings: controls give way, notices fold, and every edit needs its owner
A settings row's control slot shrinks to the box, so a control's text
wraps and a palette's row folds; the font size's sample wraps in a plain
block. A notice's boxes carry their channel's name and fold under the
kind's name when narrow. canvas::panel::editing takes the owner's
handler itself: the design panels, the structure editor, the index
manager and the privacy, notice and startup settings take theirs at
render and fail loud without one. Apply fails loud if pressed while the
proxy does not read. Tests cover Apply resting on a bad port, the way
back resting at the default size and a box pressed off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 12:53:08 -04:00
ZacharyZhang-NYandClaude Opus 5.5 93de2a6bd6 Settings: a settings window, its sections, a search, and what fills them
SettingsLayout lists sections down the side and shows the open one, the
list going above the page in a narrow box; SettingsSection and
SettingsRow title and hold settings, a row's control wrapping under its
name. SettingsSearch finds a setting by its name or words and opens its
section. ThemeSelector, AccentColorPicker, FontSizeControl and
DensitySelector set the look; KeyboardShortcutsList finds shortcuts by
every word; ProxySettings, PrivacySettings, NotificationSettings,
StartupSettings, StorageSettings with its clear, ImportExportSettings,
ResetToDefault, FeatureFlags and DeveloperModeToggle fill the rest.
Every handler a control needs is required and fails loud when missing.
ColorPalette wraps its swatches in a row capped at its columns, so it
keeps their size in a box sized by its content, and its swatches carry
the palette's id. ThemeSwitcher points here: src/theme sits under the
chapters that draw with buttons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 12:22:58 -04:00
ZacharyZhang-NYandClaude Opus 5.5 4ab478c3c3 TASKS: T34 passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 12:10:59 -04:00
ZacharyZhang-NYandClaude Opus 5.5 206d89abde Dashboard: a focused tile's Left and Right, and a control's own arrows, tested
The arrows test also moves a focused tile right and back; a new test
focuses a button inside a card and shows its arrows leave the tiles.
A card given for a key with no tile fails loud, in both layouts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 12:08:44 -04:00
ZacharyZhang-NYandClaude Opus 5.5 d1440ef49a Dashboard: a grid of widgets, what fires, what is up, and the stream
DashboardGrid lays widgets on columns: a tile's strip drags it and its
corner resizes it a cell at a time, others move down and rise to fill
gaps, a focused tile steps with the arrows, and under the theme's
stack_below width the tiles stand in one column. DashboardCard frames a
widget. AlertList acknowledges a firing alert from its row and opens
one on Enter; IncidentCard shows an incident's gravity, phase and
updates. UptimeBar, ServiceStatus and HealthCheck show what is up, a
check's detail wrapping under its name when narrow. EventStream keeps
kinds and holds the list while new events count up.
RefreshIntervalSelector and DashboardFilterBar set the window, the
filters and the refresh. StatCard, MetricsChart, ResourceGauge,
LogStream, TraceWaterfall, SystemMonitor, ProcessTable and NetworkGraph
point to their homes; the maps wait for T41.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 11:51:19 -04:00
ZacharyZhang-NYandClaude Opus 5.5 e59239a7bb TASKS: T33d passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 11:38:54 -04:00
ZacharyZhang-NYandClaude Opus 5.5 57a3265a57 DB & Dev Tools · Tools and machines: washes paint, strict hex, tested levels
Text fields paint each run's wash before selections and glyphs, so a
Highlight's background shows: the regex tester's matches, a mention's
link and markdown's code. A tile's value gives way and wraps in a narrow
box. Hex and percent escapes take two hex digits and no sign. Tests
cover each WCAG level at its edge, a clean exit's quiet badge, a
stopped container's line without load, and a reading past its limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 11:15:50 -04:00
ZacharyZhang-NYandClaude Opus 5.5 e3f2d497f1 DB & Dev Tools · Tools and machines: regex, encoders, contrast, containers, pods
RegexTester tries a pattern with its flags against a sample, washes
each match and lists it with its groups, zero-width ones too; a pattern
that does not compile says why. Encoder turns text into Base64, URL or
hex and back, says why a decode fails (Base64 pads end the text, twice
at most) and copies the result, drawn through typography::literal so no
ligature hides a character. ColorContrastChecker measures text on a
background, translucent text as it blends, and names the WCAG levels
met. ContainerList starts, stops, restarts and opens containers, each
button shown when the owner handles it. PodList sorts pods by any
column, ages by length through tables' new Cell::Duration, and narrows
them by namespace. ResourceMonitor shows each reading's latest value
beside its sparkline and, with a limit, how full it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 11:02:35 -04:00
ZacharyZhang-NYandClaude Opus 5.5 0cf33eea33 TASKS: T33c passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 10:53:02 -04:00
ZacharyZhang-NYandClaude Opus 5.5 a8bc4b169c DB & Dev Tools · APIs: a drop inside a folder lands last, tested
The collection's unit test now drops a request into a folder that holds
two, and asserts it lands after them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 10:51:19 -04:00
ZacharyZhang-NYandClaude Opus 5.5 2a87e6cd30 DB & Dev Tools · APIs: a log held at its newest, secrets per environment, moves
WebSocketConsole draws its log through chat::MessageList, so it holds
at the newest message. EnvironmentSelector opens a secret for its own
environment only. CollectionTree takes on_move and devtools::moved
applies a drop; the gallery opens a saved request into the builder and
moves them. Tests cover the builder's refill, Send at rest, Bearer
sign-in, the indented body, the eye, a field's pick path and the log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 10:44:21 -04:00
ZacharyZhang-NYandClaude Opus 5.5 4550c87bf4 DB & Dev Tools · APIs: requests, answers, environments, sockets and GraphQL
ApiRequestBuilder builds a request: its method and address, then tabs
for the query's pairs, headers, body and sign-in, with the address that
goes out under the field, the environment's {{variables}} filled in or
the missing ones named. Send hands the owner the request. The query's
pairs go through forms::KeyValueInput. ResponseViewer shows an answer's
status in its class's tone with its words, its time and size, then its
body, JSON indented, or its headers in a HeadersTable.
EnvironmentSelector picks a set of variables and masks secrets until
their eye opens them. CollectionTree holds saved requests in folders;
Enter opens one. WebSocketConsole shows a socket's messages, each
marked sent or received with its time, and sends on Enter while the
socket is open and the field holds words. GraphQLExplorer browses a
schema beside a query and its variables; Run waits for variables that
read as JSON and the answer shows as a tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 10:24:44 -04:00
ZacharyZhang-NYandClaude Opus 5.5 54eb947b34 DB & Dev Tools · Data formats: every YAML escape, keys split outside quotes
Double-quoted text reads all of YAML's escapes, \x, \u and \U among
them; a code that is no character, or a lone backslash at the end,
fails with its line. A map entry splits at the first colon outside
quotes, so a quoted key or list item may hold ": ". TASKS records the
third round and closes the item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 10:12:20 -04:00
ZacharyZhang-NYandClaude Opus 5.5 479d902643 DB & Dev Tools · Data formats: quoted YAML reads its escapes
A double-quoted scalar reads \" \\ \/ \n \t \r and \0, a single-quoted
one reads '' as a quote, and a key loses either kind of quotes. An
escape YAML does not know fails with its line, and so does block text
after | or >, which the reader does not take.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 10:03:53 -04:00
ZacharyZhang-NYandClaude Opus 5.5 a0984e80a3 DB & Dev Tools · Data formats: YAML lists at their key's indent, XML entities
YAML reads a list written at its key's own indent, as manifests do, and
a list at a map's indent ends at the next key. A `#` inside a quoted
scalar is no comment. XML reads entity references in text and
attributes, failing on one it does not know, and a text's path counts
text nodes from one. A tree's note takes at most half its row, cut with
an ellipsis, so a long value or attribute leaves the key its name.
Format and Minify fail loud if pressed while the JSON does not read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:42:03 -04:00
ZacharyZhang-NYandClaude Opus 5.5 1c01adc4d6 DB & Dev Tools · Data formats: JSON, YAML and XML as trees, a JSON editor
A document reads into one value, Datum: JSON through serde_json, its
keys now kept in their written order, and YAML's block style through a
small reader that says which line it could not read. JsonViewer and
YamlViewer draw it as one tree: each key with its value, maps and lists
with their counts; a search keeps what holds its words and opens the
way; Enter on a value copies its JSONPath. XmlViewer reads elements,
attributes and text and says where tags cross. JsonEditor writes JSON
in the code editor with Format and Minify and says whether it reads.
DocumentViewer is JsonViewer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:22:30 -04:00
ZacharyZhang-NYandClaude Opus 5.5 8c3811aef9 TASKS: T33a passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:19:30 -04:00
ZacharyZhang-NYandClaude Opus 5.5 9340c8b1b3 DB & Dev Tools · Databases: the structure editor stacks in a narrow box
TableStructureEditor measures its width. With room for its least width
it lines the fields up as a table whose cells take fixed shares of that
width, so what they hold is measured at its final width; in a narrower
box each field stacks on its own, its name and remove button, its type
with its flags, then its default, all inside the box. No sideways
scroll remains, so no Tab stop lies past a fold. Tests cover both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:16:56 -04:00
ZacharyZhang-NYandClaude Opus 5.5 d14117849a DB & Dev Tools · Databases: narrow boxes scroll or wrap; the form's rules tested
QueryPlanViewer's step column takes the label width and the table
scrolls sideways past its least width; TableStructureEditor's rows
stretch to a least width that scrolls sideways; RedisKeyBrowser's
columns wrap below each other inside 280px. The form's rules are pure,
parse_port and ready, with unit tests; gpui tests cover saving, a new
connection refilling the form, another engine bringing its port, and a
press on a row's button leaving the row alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:05:35 -04:00
ZacharyZhang-NYandClaude Opus 5.5 4c9727d67c DB & Dev Tools · Databases: connections, schemas, queries, structure, Redis
A new chapter, src/devtools. ConnectionManager lists saved connections
with their engine and address, or why one failed, and connects on Enter;
ConnectionForm fills a connection with a port checked from 1 to 65535,
tests it and saves it. SchemaTree shows schemas, tables, views and
fields with keys marked; ERDiagram draws tables and their foreign keys
on NodeGraph, whose fixed wires show only the sockets that hold one.
QueryHistory finds past runs and runs one again; QueryPlanViewer shows
each step's share of the cost. TableStructureEditor edits fields in
place; IndexManager lists and draws up indexes; RedisKeyBrowser finds
keys by glob and lays out each type. QueryEditor is editor::CodeEditor,
whose lexer now knows SQL's words, and QueryResultGrid is
tables::DataGrid. lists::row_action keeps a row's button its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 08:51:45 -04:00
ZacharyZhang-NYandClaude Opus 5.5 94ae7e73e7 TASKS: T32d passes review; T33 splits into four
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 08:45:02 -04:00
ZacharyZhang-NYandClaude Opus 5.5 9170d47f8b Canvas · Graphs and boards: a field hands focus back on Enter and Escape
writing::begin records the owner's focus, the mind map's or the tool
layer's, through forms::Editing::returning. The field keeps its words on
Enter's release and drops them on Escape, and both hand focus back, so
the map's arrows and the board's Backspace work at once after a write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 08:41:05 -04:00
ZacharyZhang-NYandClaude Opus 5.5 bd0285bd37 Canvas · Graphs and boards: Tab keeps a topic's words; the untested rules tested
Tab in a mind map topic being rewritten keeps the words and hands focus
back to the map; it asks for no child while the field is open, and the
map's keys wait for the field. Tests cover a new wire into a wired input
replacing the old, a connector released on its own shape linking
nothing, and a deleted whiteboard shape taking its links with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 08:35:08 -04:00
ZacharyZhang-NYandClaude Opus 5.5 f3822a8d98 Canvas · Graphs and boards: node graph, flow chart, mind map, whiteboard
NodeGraph joins Nodes by wires between typed Ports: a title drags a
node, a drag out of an output lands on an input of the same kind, an
input takes one wire, and a press on a wired input lifts its wire.
Links join shapes on the plane and follow them, straight or at right
angles, with an arrow and words; the Connector tool draws them and the
Note tool drops a sticky note. A double press with Select writes on a
shape. FlowChart draws Steps as ellipses, boxes and diamonds with their
words. MindMap lays topics out from a root, half to each side, and asks
the owner to add, remove and rename by key. Whiteboard keeps its own
tool, view and selection over a board it hands back after each change.
Painting moves to canvas::paint; a press on the tool layer goes through
canvas::press, and the layer focuses itself and asks to delete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 08:22:54 -04:00
ZacharyZhang-NYandClaude Opus 5.5 5d58ee4577 TASKS: T32c passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 08:07:36 -04:00
ZacharyZhang-NYandClaude Opus 5.5 c04c67d39a Canvas · Panels: layers, inspector, alignment, assets, style and layout editors
LayerPanel lists the canvas's layers topmost first on lists::Tree: a box
shows or hides each, F2 renames, a drag restacks into a new paint order.
InspectorPanel edits the one selected shape's frame, fill, visibility and
lock. AlignmentToolbar lines up or spreads the selection through
`aligned`. AssetPanel finds shapes by name in a picks grid, each drawn
to fit its tile. ColorPanel, ShadowEditor, BorderEditor and
TypographyPanel edit a look; GradientPanel is forms::GradientEditor.
ConstraintsEditor pins a shape to its frame, with `Constraints::placed`;
AutoLayoutControls sets flow, gap, padding and a place grid, with
`AutoLayout::laid`; ExportPanel names each file once. The panels share
`canvas::panel`: editing, pair, caption, heading and color_well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 07:58:27 -04:00
ZacharyZhang-NYandClaude Opus 5.5 7d493b669b TASKS: T32b passes review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 07:50:23 -04:00
ZacharyZhang-NYandClaude Opus 5.5 b55c81d994 Canvas · Tools: single handles, a wrapping palette, the pen ends with its tool
Handles resize one shape, so a selection of several shows its outline
and no handles; a press there no longer falls through to a marquee.
ToggleGroup wraps its faces when narrow, so the ToolPalette stays in a
280px box. A pen path left open ends when the tool changes: two points
or more go to the owner after the frame, a lone point is dropped, and
the open path paints only while the pen is the tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 07:41:10 -04:00
ZacharyZhang-NYandClaude Opus 5.5 49a205ea35 Canvas · Tools: select, move and snap, resize, draw, pen, text, brush
ToolLayer lies over an InfiniteCanvas and carries its tools. Select
takes the topmost shape a press reaches, Shift adds or drops one, a
drag moves the selection and snaps its nearest edge or middle to
another shape's within reach, a handle resizes it, Shift keeping a
corner's shape, and a drag on empty space draws a SelectionBox that
takes what lies wholly inside. The shape tools draw by a drag toward
any corner, Shift keeping them square; the pen adds a point per press
and a double press ends the path; text drops a line where pressed; the
brush strokes a path its width. A drag may end in the layer or out of
it; each gesture ends once and asks the owner. SelectionBox,
TransformHandles and SnapIndicator draw the marks.

ToolPalette picks a tool from icons named in tips; BrushSettings sets a
brush's width and hue; HistoryPanel lists the steps done, the undone
ones quiet, and takes the drawing to a step on Enter or a double press.

The model changes: lines and arrows end at a named corner, so they run
either way, and paths carry their width. Three Lucide icons: hexagon,
slash and move-up-right.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 07:27:27 -04:00
ZacharyZhang-NYandClaude Opus 5.5 ea99d30e3b Canvas · the wheel scrolls as scroll boxes do; pans hold at the press
A wheel without a modifier now slides the content by its delta, as
gpui's scroll boxes do; it moved the other way. The wheel reads the
live viewport and skips an end with no delta, and a drag pans from the
press by the pointer's whole way with the viewport held at the press,
so events that land in one frame no longer overwrite each other. Tests:
a plain wheel moves the content up with it; three wheels and an empty
end add up, and four moves pan their sum; a small frame fits at a zoom
of one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 07:15:01 -04:00
ZacharyZhang-NYandClaude Opus 5.5 2c099ceffb Canvas · Plane: an endless canvas, its grid, rulers, zoom and map
A new chapter, src/canvas. InfiniteCanvas lays artboards, shapes and
guides on an endless plane over a dot grid whose step, one, two or five
times a power of ten, keeps the dots apart as it zooms out. Rulers run
along the top and left in canvas units. A drag on empty space pans from
the press, a wheel scrolls, and Command or Control with the wheel zooms
about the pointer; the owner keeps the viewport, seeded, so its own
zoom shows at once. Layers lie over the plane in view pixels.

Shapes are rectangles, ellipses, regular polygons, lines, arrows, text,
paths and notes, each in a chart hue, painted to scale; text and notes
scale their type. Artboards carry their names above them.

ZoomControls step out and in through fixed stops, fit on request, and
hold ZoomIndicator, whose press sets 100%. MiniMap draws what lies on
the canvas and the view's box, and centers the view where it is pressed
or dragged. The viewport math, the steps and the map's fit are tested
without gpui.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 06:58:59 -04:00
ZacharyZhang-NYandClaude Opus 5.5 86336ee828 Project · focus that follows: segments, the tracker's action, tests
A segmented control too narrow for its segments now scrolls sideways
in a strip of its own, and a segment that takes focus comes into view:
its focus is a tab_stop handle, and layout::reveal_when_focused, now
shared with the board's cards, measures it from a box that holds its
ring. The DatabaseView's own strip goes.

The time tracker's Start and Stop are one Button that keeps one handle
through the new Button::focus_handle. A start moves focus to it, a stop
hands focus back to the field, and the field starts on Enter's release,
so the release no longer clicks the Stop that took focus.

ProgressRing's percent refuses content inside, as content refuses a
percent. Tests: Enter starts, Enter stops after 90 seconds, and the
field takes the keys again; Skip moves on at once; a focused Timeline
segment at 280px comes into view; the ring's refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 06:32:50 -04:00
ZacharyZhang-NYandClaude Opus 5.5 6377f926c8 Project · Time and databases: a time tracker, a pomodoro, a database
TimeTracker times a task: a field names it, Start or Enter starts the
clock and Stop ends it, the running time counting up by the executor's
clock; the entries sit under it in hours and minutes with their total.
The owner keeps what runs and what was spent, and a blank name starts
nothing.

PomodoroTimer runs focus, a short break, and after the last round a
long one. A ring counts each phase down around the time left, dots
count the rounds, Pause banks the time gone, and a phase that runs out
moves on, waits for Start, and reaches the owner through window.defer.
motion::ProgressRing gains a size and content inside, its stroke kept
at the theme ring's weight, the content outside the per-turn glide.

DatabaseView shows tasks five ways, switched from a strip that scrolls
when narrow: a table, a board by status whose moves hand on the new
status, a gallery of issue cards, a month calendar by due day, and a
timeline from start to due, which counts the records without dates.
Task gains a start day.

Along the way: table rows, groups and details fill with min_w_full, so
they stretch in the table's column and fill a virtual list alike;
before, a table in a measured column drew rows at their words' width.
The gallery script logs each probe it aims at.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 06:11:38 -04:00
ZacharyZhang-NYandClaude Opus 5.5 28973fe1d2 Project · a roadmap bar's end and a workload bar's cap, tested
A roadmap bar ran through its last day, but no test looked at its end:
a bar stopping at the last day's start stayed green. The roadmap test
now asserts October's bar reaches the span's end. The workload bar's
share, capped at the whole track, moves into Load::filled with a unit
test, so a load past capacity cannot draw past its track unseen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 05:53:28 -04:00
ZacharyZhang-NYandClaude Opus 5.5 75f00883ac Project · Plans: a roadmap, a burndown, milestones, workload
Roadmap lays initiatives across months in lanes: each a bar from its
first day through its last, filled as far as it has come, under the
months' names and a line at today; hover a bar for its dates. Bars sit
at fractions of the span, so nothing is measured, and a strip too
narrow for its months scrolls sideways at a least width per month.
GanttChart and TimelineView are the charts GanttChart.

BurndownChart lives with the charts, on their scales, labels, tiles
and paint: the ideal line from the scope down to nothing, what was left
at each day's end so far, and a line at today; hover a day to read it.

MilestoneProgress shows a milestone's issues done as a bar and a count,
when it is due, and whether it keeps pace: on track, at risk when the
work trails the time gone by more than a tenth, late past its day, or
done. WorkloadView sets each person's points against what they can
take, the bar turning red past it, in columns that line up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 05:42:03 -04:00
ZacharyZhang-NYandClaude Opus 5.5 fc0c3e564d Project · a focused card scrolls into view
The board scrolled sideways without a handle, so a card that took
focus past the fold stayed out of sight, ring and all, and Option-Right
could send it into a column nobody saw. The board now tracks its
scroll, and a card that takes focus sets the offset through
layout::bring_into_view from its row's painted box, ring included, once
per focus and again when it lands in another column. The stepping
rules gain tests for left and right keeping a card's place and for the
end of a shorter column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 05:28:35 -04:00
ZacharyZhang-NYandClaude Opus 5.5 62382f7e68 Project · Issues and boards: a board, issue cards, a task's panel, labels
KanbanBoard lays columns of cards side by side. A card drags to a
place in any column: it rides raised at the pointer, a slot opens where
it would land, the other cards glide aside on motion::Flip, and the
drop asks the owner through on_move(card, column, index); a drop in its
own place asks nothing. Option with an arrow moves the focused card,
whose focus the board keys by card so it follows it across columns. A
press, Enter or Space opens it. Sprint Board is this board.

IssueCard shows an issue's status, name, assignee, title, priority,
labels and due day. TaskDetailPanel lays a task out in full: its title
edits in place, its status, priority, assignee and due day change where
they stand, and its notes and steps follow; each edit hands the whole
task on, and a blank title is not taken. LabelManager tends a team's
labels: a hue picked from a popover, a name edited in place, a delete,
and a field that makes a new one; names stay unique in any case, and a
refused name says why.

The fill rule changes: min_w_full, taken up in T30d, resolves against
a block's outer width in taffy 0.9, so a field in a padded card ran
past its padding. A component that fills now takes no width on its
root, which blocks and flex columns stretch; FormField and the new
roots do. The chart's hue names move to theme::HUE_NAMES, which the
event editor and the label manager share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 05:12:20 -04:00
ZacharyZhang-NYandClaude Opus 5.5 9b506e5d54 Project · Tasks: priorities, issues, statuses, assignees, task rows
A new chapter, src/project. PriorityIndicator shows how urgent a task
is in three bars, urgent in red, its words in a tip or beside it.
IssueIdBadge names an issue by its team's key and number, as ENG-123.
StatusSelect picks a status from the workflow, each with its mark, and
AssigneePicker picks a person or no one; DueDatePicker is the forms
DatePicker. TaskItem is a task in a row: its box, priority and issue,
its title, then its due day, red when past and still open, and who has
it. TaskList puts them in a SelectableList: arrows move, Enter opens,
and a box's press or keys stay at the box. SubtaskList is the
documents Checklist, which gains a field that adds a step on Enter.

Along the way: forms::Choice::avatar leads a person's row with their
initials, in lists and in a Select's trigger; ListItem::struck strikes
a closed task's title; typography::Ellipsis now shapes with the text
style's run, so a strike or underline on its box shows; two Lucide
icons, circle-dashed and circle-dot-dashed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 04:48:54 -04:00
ZacharyZhang-NYandClaude Opus 5.5 e2eb1cfc41 Calendar · open times start over on new days
AvailabilityPicker kept the day chosen in keyed state whatever days the
owner passed next, and the slots lookup expected it among them: an
owner paging to the next week crashed the app once a day was chosen.
The choice is now seeded on the owner's days and pick, so a new run of
days starts it over at the picked day or the first open one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 04:31:14 -04:00
ZacharyZhang-NYandClaude Opus 5.5 07a577b574 Calendar · Editing: an event's editor, its repeats, reminders and open times
EventEditor puts an event's parts in a form: its title, whole days or
hours, where, its color, how it repeats, its reminders and notes. Save
waits for a title and an end after the start and hands on an
EventDraft; Cancel and Delete show when the owner handles them. A new
draft from the owner starts the editor over.

RecurrenceEditor sets a rule after RFC 5545: daily, weekly on its days,
monthly or yearly, every so many, until a day or a count. A line reads
the rule back with the next days it falls on. ReminderPicker keeps
reminders from a fixed list, soonest first, and Add brings the nearest
one not yet set. AvailabilityPicker counts the open slots of each day
and lists the chosen day's, around what is busy, inside working hours
and after now.

FormField filled its column with a percent width, which taffy 0.9
leaves at content width when a block measures the column first and the
width comes out the same: at 280px the editor's fields shrank to their
labels. It now fills with min_w_full, as new roots do; older roots move
over in T44.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 04:22:02 -04:00
ZacharyZhang-NYandClaude Opus 5.5 a506ddc98c Calendar · Agenda and people: an agenda, an event's details, its guests
AgendaView lists the days ahead under their names, whole days first,
then by the hour, and leaves out days with nothing; it rides on
lists::Sections, so a press or an arrow selects and Enter opens. An
empty stretch says nothing is planned.

EventPopover is an event's chip that opens its details: its title in
its hue, when and where, who is asked, and Edit and Delete when the
owner handles them. It opens through Popover::with_opener, which now
hands any element the toggle and keeps focus, placement and every way
out in the popover. AttendeeList counts the answers and lists each
guest with their answer, the organizer first. A hue worn in a row is
now data_display::color_mark, for mail labels and events alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 03:35:59 -04:00
ZacharyZhang-NYandClaude Opus 5.5 b68f2bf1d5 Calendar · views on the clock keep moving
The week and day views read the clock once and handed that moment down
as given, which turned off the line's ticker: the line at now and the
lit day stood still. The owner's moment now goes down unchanged, and a
time grid, month view or year view on the clock keeps its own ticker,
so the line moves and today turns over at midnight, even on days that
are not today. A given moment holds them still.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 03:23:18 -04:00
ZacharyZhang-NYandClaude Opus 5.5 a41cb2f9c2 Calendar · Time grid: week and day views, drag to make and to resize
TimeGrid sets days side by side under a column of hours. Each event is
a card by its hours, clipped to each day it touches, and events that
overlap share the width in lanes. A drag on empty time makes an event
by quarter hours through forms::pen; a drag on an event's lower edge,
on the day it ends, moves its end, each drag checked against its
owner. The grid opens at half past seven, or an hour and a half
before now when today is shown and that is later.

CurrentTimeIndicator marks the minute now on today and moves with the
clock. TimezoneOverlay adds another zone's hours beside the grid's.
AllDayRow lays whole days across their columns in lanes.
CalendarWeekView and CalendarDayView are one view of seven days or
one: a header that steps and comes back to today, the days' names
stacked so they fit narrow columns, the whole days, then the hours.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 03:13:38 -04:00
ZacharyZhang-NYandClaude Opus 5.5 f316ab56ba Calendar · Months: a month view, a year view, event chips and cards
CalendarMonthView lays a month in week rows. An event across days lies
over them as a bar in the first free lane, cut at the week's edge;
the rest sit by the hour as chips, and a crowded day keeps the rest
behind "N more". Arrows move a day, Page keys turn the month, and
Enter or a double press opens the day; the cursor holds through the
owner's echo of a turn. Days sit at sevenths of their row, as the bars
do, so text never widens a column.

YearView shows twelve small months; a day with something on it wears a
dot in its first event's hue. EventChip is an event in a line and
EventCard a block with its hours and place. MiniCalendar points to
forms::Calendar. Palette::hue is now the one home for a chart color by
index, for mail labels and events alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 02:49:37 -04:00
ZacharyZhang-NYandClaude Opus 5.5 c1a5603184 Primitives · a raise keeps its state under its owner's id
A raise inside another was namespaced by its place in the layer's
queue, so one that came before it moved its id path and rebuilt its
keyed state and focus: an open date popup would lose its month when a
tip appeared. raise now takes its owner's id and draws under it, and
float and float_height pass the owner's id on from every call site.
A test holds a focused box in a raise while another comes before it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 02:23:05 -04:00
ZacharyZhang-NYandClaude Opus 5.5 9abab5f2ba Primitives · raise: what opens inside a dialog lies over it
gpui 0.2.2 asserts on a deferred draw inside another, and every dialog,
sheet and popover draws inside one, so opening a date field in the
snooze or schedule dialog panicked, as would any list, menu or picker
opened in an overlay. primitives::raise replaces each deferred draw:
at the top it defers as before; inside something raised it leaves a
placeholder in the tree, and its layer lays it out on its own, then
draws it last. check.sh refuses gpui's deferred anywhere else.

The time dialog's rule is now times::ahead, tested for a moment
behind, now itself, a moment ahead and another zone; a gpui test picks
yesterday in the snooze dialog, sees that it has passed and that
nothing is sent, then picks two days ahead and hears that moment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 01:51:51 -04:00
ZacharyZhang-NYandClaude Opus 5.5 5b0a8443a7 Mail · Sorting: a label picker and a snooze menu
LabelPicker puts labels on a message: a field finds them by name,
arrows move and a press or Enter turns one on or off, each row with its
check and its hue. A name no label has can be made into one, and the
field starts over. Its rows are the option rows every list shares;
forms::marked_row leads one with a mark. The cursor shows only while
the field has focus.

SnoozePicker offers later today, tomorrow, this weekend and next week,
each with its day and hour, or a time picked in a dialog. It shares
mail::times with ScheduleSend: the names, the dialog, which now waits
for a time ahead of now, and a rule that a moment comes once, so on a
Sunday tomorrow morning no longer repeats as Monday morning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 01:16:24 -04:00
ZacharyZhang-NYandClaude Opus 5.5 154f7f2b46 Data display · a tag stays inside its container
A Tag is capped at its container's width, and its label ends in an
ellipsis beside a fixed remove mark, so a long name in a narrow tag
field stays inside the field instead of running past it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:38:30 -04:00
ZacharyZhang-NYandClaude Opus 5.5 84c3dc16de Mail · Writing: the composer, recipients that suggest, a signature, send later
MailComposer writes a message: To, and Cc and Bcc once asked for, as
tag fields that suggest contacts and mark what is not an address; the
subject and the words; the files, each with a way off; the signature
under the words. Send wakes once there is someone to send to and every
address is one; it hands over the draft now or at a time, and Discard
lets it go. A new start from the owner begins the draft over.

forms::TagInput now suggests: the rows that hold the typed words float
under it through the list Combobox uses; Up, Down and Enter pick, and
Escape sets them aside. RecipientInput points to it. ScheduleSend
offers tomorrow morning, tomorrow afternoon, Monday morning, or a day
and hour picked in a dialog. SignatureEditor writes the lines and shows
how they sit under a message; it tells the owner's echo from a new
signature, so the caret stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:27:23 -04:00
ZacharyZhang-NYandClaude Opus 5.5 48118499ae Mail · a folded message keeps focus as it opens, and gives way at 280px
Each message in a thread holds one focus handle, a Tab stop while
folded and tracked on the letter once open, so a keyboard open keeps
focus on the message and Tab goes on to its Reply. A press on a fold
leaves focus alone. A fold's name now gives way with its words, so a
long name beside a long date stays inside the row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 00:07:27 -04:00
ZacharyZhang-NYandClaude Opus 5.5 f946af6216 Mail · Reading: a message reader, a conversation, and quoted text
MailReader opens a message under its subject: the sender's picture,
name and address, who it went to, when it came, and Reply, Reply all
when it went to more than one, and Forward, each only with a handler;
then its words and its files, each downloadable. MailThreadView lays a
conversation oldest first: the newest stands open, the others fold to
a line of the sender, their first words and when, which a press, Enter
or Space opens. QuotedText folds words quoted from before behind a
small mark; open, they sit behind a rule. Long names give way at the
label width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:57:06 -04:00
ZacharyZhang-NYandClaude Opus 5.5 0a281d91ab Mail · a focused star keeps Space and Enter; so does Unpin
A button in a SelectableList row now stops Space and Enter at its box
on key down, so the list no longer toggles its selection before the
button acts. The mail star and the pinned messages' Unpin, which had
the same fault, both do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:45:22 -04:00
ZacharyZhang-NYandClaude Opus 5.5 b6b1224c27 Mail · Boxes and lists: mailboxes, folders and labels, the mail list, inbox zero
MailboxList sets mailboxes, folders and labels in sections: a press, an
arrow or Enter opens one, the open one lit; a box with mail unread
stands out with its count, and a label wears one of the theme's chart
hues. MailList holds messages in a multiple-select list: the sender
strong beside a dot while unread, the subject and first words under
it, the time today, the weekday this week, the date this year or with
its year, a paperclip for files, and a star whose press stays off the
row. InboxZeroState points to feedback::EmptyState.

Sections of selectable lists now have one home, lists::Sections, which
ChannelList and MemberList use too; ListItem gains a third line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:35:04 -04:00
ZacharyZhang-NYandClaude Opus 5.5 6f599803ce Messaging · GIF picker: a GIF focused past the fold scrolls into view
Each GIF holds its own focus handle, and a canvas around it, ring and
all, sets the results box's offset once per focus from its painted
bounds. layout::bring_into_view now works along either axis; the data
table's header and the video timeline pass their sideways one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:11:46 -04:00
ZacharyZhang-NYandClaude Opus 5.5 5ade2bed74 Messaging · Stickers and GIFs: a sticker picker and a GIF picker
StickerPicker shows stickers a pack at a time, chosen above with a
segmented control; a search spans every pack and keeps the stickers
whose names hold it. Arrows move, Enter or a press picks. It reuses the
glyph grid, now forms::picks, which takes its columns and cell size and
titles from the host, so emoji, icons and stickers share one searchable
grid; forms::glyphs keeps the emoji and icon pickers.

GifPicker packs host GIFs in two columns, each moving, under a search
it owns; the host hears the trimmed words only when they change, and
categories fill the search while it is empty. Waiting, it shows tiles
in GIF shapes; finding nothing, it says so. Each GIF is a Tab stop that
a press, Enter or Space picks.

The share toggle now shows its state, as the microphone and camera do.
The gallery ships line-drawn stickers and small GIFs made from its own
pictures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:00:15 -04:00
ZacharyZhang-NYandClaude Opus 5.5 48d02c4903 Messaging · Calls: controls, tiles, a grid, a shared screen, a bar, a ringing dialog, a huddle
CallControls holds the microphone, camera and a shared screen as
toggles, then Leave. ParticipantTile shows a camera's frame from the
host or the person's initials, their name with a mark when muted, and a
ring while they speak. VideoCallGrid lays tiles in even rows as far as
they fit across its measured width; a short last row centers at the
full rows' width. ScreenShareView names who presents and offers Stop
presenting for your own share, the call's tiles beside or below.
VoiceCallBar tells where a call is, how long it has run and who is in
it, with the microphone and Leave. IncomingCallDialog rings with the
caller's picture; Accept or Decline answers each ring once, and Escape
declines. HuddleIndicator rings while a huddle is live; a press joins.

The recorder's device toggle and the camera's name chip now have one
home in media. People: member names sort whatever their case, a
status's words save trimmed, and a profile card's local time ticks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 22:33:54 -04:00
ZacharyZhang-NYandClaude Opus 5.5 37e56c7ab7 Messaging · People: members, a profile card, online status and a status setter
MemberList sets members online and offline by name, each group a list
under its count. A press or an arrow chooses one; Enter or a double
press acts on them; offline members go quiet. UserProfileCard shows a
person's picture with presence, name and standing, title, status, the
time in their zone, and Message and Call, each only with a handler.
OnlineStatus draws a presence's dot and word; Presence now names itself
and gives its color, in data_display, its one home.

StatusSetter holds a draft seeded from the owner's status: an emoji
from a picker field, words in its own text field, and when they clear.
Suggestions fill all three; Save hands the status over and Clear status
removes it. The emoji field reuses the date pickers' field, whose
trigger ignores the Enter release after a pick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 22:08:25 -04:00
ZacharyZhang-NYandClaude Opus 5.5 5bbfdcd766 Messaging · Messages: messages in runs, threads, the unread line and receipts
ChatMessage sets a message under its author's picture, name and clock
time. The next by the same author within five minutes, on the same day,
joins the run: only its words, the time in the gutter on hover.
UnreadDivider marks where unread messages start. MessageThread tells who
replied, how many and when the last came; with a handler it opens on a
press, Enter or Space. ThreadPanel holds the first message, a rule
counting the replies, the replies and a composer, its body scrolling.
ReadReceipt says sending, sent, delivered or read, and who saw it in a
group. MessageReactions and TypingIndicator point to collab and motion.

gpui recolors nothing on hover: text takes its color at layout. The
gutter time rests invisible and turns visible through a group hover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:48:17 -04:00
ZacharyZhang-NYandClaude Opus 5.5 3c8a5a026c Messaging · Channels: the chat list, its rows, a channel's head and its pins
ChannelList sets chats down in sections, each a list: a press, an arrow
or Enter opens one, and the open chat is lit. A title names one section,
and a section holds a chat. ChannelItem shows a hash or a lock, its
unread count, a pin when pinned, and goes quiet when muted.
DirectMessageItem shows a person's initials with their presence. Unread
names stand out through ListItem's new strong; muted channels and
offline people go quiet through its quiet. ChannelHeader holds the name
and topic beside members, pins and a call, wrapping by the header rule.
PinnedMessages lists pins newest first; a press, an arrow or Enter opens
one, and Unpin lets one go. With none it shows a line and takes no Tab
stop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:30:17 -04:00
ZacharyZhang-NYandClaude Opus 5.5 eeeca67e55 Files: a press on a download's button stays with the button
gpui runs the click of every pressed element under the pointer, so a
press on a finished download's Show button also opened the file through
the pressable row. The actions' box stops the press on mouse down, so
the row starts no click of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:12:38 -04:00
ZacharyZhang-NYandClaude Opus 5.5 0fcde7dbbe Files · Transfers: file operations, a transfer queue, downloads and storage
A file on its way is a Transfer with a state: waiting, moving at a rate,
paused, done or failed with a reason. One row draws it for the queue and
the downloads: its icon, its way, a bar while it moves or waits paused, a
status with the time left, and only the actions its state allows.
TransferQueue counts what moves and waits. DownloadManager keeps the
finished below under Clear, opening on a press and showing their folder.
FileOperationProgress tells a copy or move with its bar, time left and
current file, its header wrapping by the header rule; a failure speaks in
the danger tone. StorageUsage points to data_display's UsageBar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:05:59 -04:00
ZacharyZhang-NYandClaude Opus 5.5 f324702c89 Files: recent and found files keep the cursor on the file, not its place
RecentFiles and FileSearch keyed their rows by place in the list, so when
an open moved a file to the top, the cursor stayed at the place and the
next Enter opened another file. Rows are keyed by the file's folder and
name, asserted once a list, so the cursor follows the file. A dotfile's
kind reads by its extension, as its icon does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:55:21 -04:00
ZacharyZhang-NYandClaude Opus 5.5 c5c35f90fc Files · Find and look: preview, recent files, search, duplicates and archives
FilePreview shows a file large: its picture in the host's shape, the first
lines of its text, or its kind's icon, over its name, kind, size and
change; the gallery opens it on Space as a quick look. RecentFiles lists
files newest first under a header a day, a list a day. FileSearch sits
over a field the host owns and lists what the host found with its folder.
DuplicateFinder marks copies to remove and keeps the last of each group,
its mark shut, with the bytes the marks free beside Remove. ArchiveViewer
draws an archive's entries as a tree with sizes and savings, from paths
gathered by the folder builder the file tree now shares. A checkbox's
label wraps inside its box instead of running past it. Favorites and the
hex viewer point to documents and debug.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:42:06 -04:00
ZacharyZhang-NYandClaude Opus 5.5 78fc3f90c0 Files: a narrow listing keeps its names; a column starts fresh for a new folder
The directory listing's size and date columns are fixed, so at 280px the
name had no room and rows showed no names. The heading and rows now sit
in a sideways scroll box whose child keeps the least width: the name's
least, the fixed columns and each row's icon, gaps and padding. A column
of the explorer was keyed by its depth, so a new folder there inherited
the old list's cursor; columns and rows are keyed by their folder. The
explorer asserts each folder on its path is a folder in its parent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:22:12 -04:00
ZacharyZhang-NYandClaude Opus 5.5 f694f0d413 Files · Explorer: a folder as a list, tiles or columns, with one path and one order
FileExplorer shows the folder at the end of a path three ways: the
directory listing, a grid of tiles, or a column per folder along the
path with the next folder lit. Its name, its count and a switch of views
sit above one path bar that climbs. A folder opens in place; a file goes
to the host, which lists each folder it goes to. A pick in an earlier
column closes the columns past it, and a new deepest column scrolls into
view. The directory listing takes its pick from its owner and reports
it, so the pick carries across views; its path bar and its first order
are shared. The gallery walks a small file system and shows the tree,
the context menu, renaming in place and a drop zone in use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:07:33 -04:00
ZacharyZhang-NYandClaude Opus 5.5 906e6c8ac1 Files: a tile's name keeps its ends; the ellipses share one line cutter
gpui's line_clamp wraps once and lays the rest of a long name on the
second line, which a centered tile then clipped at both ends. A tile's
name is now one line, cut in the middle so the front and the extension
stay. MiddleEllipsis draws like Ellipsis: in the text style around it,
sized by its text, so a short name centers; the two share one cutter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:51:43 -04:00
ZacharyZhang-NYandClaude Opus 5.5 ebad5328ed Files · Items: one icon map, file icon, file row and file grid
The crate had two maps from a file's name to its icon, in chat and in the
file tree, and they disagreed. One map now lives in primitives, over the
Lucide file-* icons, and chat, agent, the file tree and the directory
listing use it. FileIcon draws it for a file or a folder. FileItem is a
file's row, a ListItem with its size and when it changed. FileList is the
directory listing. FileGrid lays tiles in the order given, with a cursor
seeded from the owner's pick: arrows by tile and by row, Home, End, Enter
and a double press to open. The count of what fits a width has one home,
layout's columns_for, and fit builds on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:40:10 -04:00
ZacharyZhang-NYandClaude Opus 5.5 9872adc5e9 Forms: an option list stays inside the window; media ratios are checked in one place
The floating list under a Select, Combobox or MultiSelect was at least its
trigger's width with no ceiling, so a long choice ran it past the window
and its rows never ellipsized. It now stops at the window's width less the
window margin on each side, so gpui snaps it inside. The camera preview
and the model viewer check their ratio through primitives::checked_ratio.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:17:52 -04:00
ZacharyZhang-NYandClaude Opus 5.5 7f1cfe8c3d Media · Capture and devices: recorder, camera, mic meter, device choice and 3D model viewer
ScreenRecorderControls holds a recording in one pill: Record, then a live
dot and the time it has run beside pause or resume and stop, with mic and
camera toggles. CameraPreview shows host frames in the camera's shape, or
the camera off. MicLevelMeter lights segments up to the level, amber and
red near the top, with tones shared from data_display. DeviceSelector
picks a microphone, camera or speaker through Select, which may now be
empty while shut. ModelViewer shows the host's frame for an orbit that a
drag, the arrows, Command-scroll, + and - and a reset move; the orbit is
seeded from the host so fast drags add up. The gallery draws a stone
torus per orbit and frees each replaced frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:05:23 -04:00
ZacharyZhang-NYandClaude Opus 5.5 235f059c8e Lists: a selectable list's cursor starts on the owner's pick; the waveform's fit is tested
SelectableList keeps the owner's last selection and the one it last
sent. A selection the owner makes, on first paint or later, moves the
cursor to its first row; the echo of the list's own pick leaves the
cursor where the pick put it. The playlist's keys start from the song
under way. The waveform's bar count and inset move into `fit`, tested
beside `resample`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:44:38 -04:00
ZacharyZhang-NYandClaude Opus 5.5 4b86283d69 Media · Audio and controls: player, waveform, spectrum, volume, speed, transport and playlist
AudioPlayer sets a song's cover, title and artist over a scrubber with
the time played and left, the transport and the volume. AudioWaveform
draws peaks gathered to the bars that fit, dark as far as it has played,
and seeks by press, drag and keys, with the time tip the scrubber shares.
AudioSpectrum shows levels by pitch. VolumeControl mutes from its speaker,
and moving its slider while muted unmutes. PlaybackSpeedControl picks from
a menu. MediaControls holds shuffle, back, play, forward and repeat.
Playlist builds on SelectableList: the song under way is lit with moving
bars, and Enter or a double press plays another.

The video player's bar now rests while the stage itself holds focus, a
key it takes wakes the bar, and a press on a resting bar only wakes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:32:45 -04:00
ZacharyZhang-NYandClaude Opus 5.5 fcb1b2d9df Media: the player's bar fades by elapsed time, so its scrubber and buttons keep their state
The fade restarted an animation id at each rest and wake, and gpui hands
that id to every descendant: the scrubber's drag, hover and focus handle
and the buttons' focus were rebuilt under it. The bar's opacity now reads
the time since the last change through motion::since_change. The stage
tracks focus even without key handlers, so a focused bar button keeps
the bar up. The player's tests move to their own file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:14:06 -04:00
ZacharyZhang-NYandClaude Opus 5.5 fbf99ed4f7 Media: the caption outlasts the bar; one Space per press; narrow boxes hold
Only the bar fades now, so the caption stays while the controls rest, and
the caption chip can shrink, so a long line wraps inside a narrow player.
A focused media button keeps Space to itself, since gpui presses it on
the release. The player and the strip let Command and Control letters
pass. The scrubber's tip splits the free width by the pointer's share and
stays over the track at both ends. An inline edit's label gives way with
an ellipsis beside fixed parts.

The player is a Tab stop only when a key does something, logs its
captions, keys its fade by its own id, and logs a failed wake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:52:27 -04:00
ZacharyZhang-NYandClaude Opus 5.5 d0175edb7b T26c Media: video
Scrubber shows what has loaded, what has played and its chapters, names
the time under the pointer, seeks by press and gpui's drag, and steps by
the slider's keys; over_media lights it for a dark layer. VideoPlayer
shows the host's frame in its shape with a caption and a control bar over
it: the scrubber, play, the time, speed, captions, picture in picture and
full screen, each only with its handler; the bar rests after a still
pointer while it plays, on one wake from the executor's timer. Keys:
Space and K, J and L, the arrows, C and F. VideoThumbnailStrip lays frames
along the length under a playhead with a trim whose handles drag and whose
ends I and O set. SubtitleEditor edits cues in place: times as mm:ss.mmm,
kept in order, a time that does not read kept out with the reason shown.

InlineEdit is a Tab stop with the ring; it hands focus back after Enter or
Escape and keeps its text on Enter's release. overlays::media_button is
shared by the lightbox and the player's bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:07:26 -04:00
ZacharyZhang-NYandClaude Opus 5.5 f7f20b5146 Reviews: T26b passes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 16:46:40 -04:00
ZacharyZhang-NYandClaude Opus 5.5 cb28c0e305 Media: a shaped corner follows the pointer up and down; the fitted crop is told once
With an aspect, a corner drag sizes the crop by whichever side the pointer
moved further, so a drag straight up or down resizes it too. The upload
tells the host the fitted crop once per file, whether the host keeps it or
not; a host that dropped it had set off a loop of reports. The annotator's
Escape stops only when it lets a chosen mark go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 16:38:22 -04:00
ZacharyZhang-NYandClaude Opus 5.5 7d86c804d7 T26b Media: crop and annotate
ImageCropper crops a picture by a box in shares of it: a press holds a
corner, a side or the body (crop::grip_at), a drag through gpui's drag
moves them (crop::dragged), kept inside the picture with a least side;
with an aspect only the corners size it and the box keeps its shape; the
arrows move it. The picture sits inset by half a handle, the focus border
outside it, so handles on its edge stay whole and it keeps its shape.
ImageUpload (T06e) takes pictures only, from a drop or the dialog
(forms::taken, DropZone::kinds), decodes the file for its shape, and with
no crop yet reports the largest centered box of its aspect once. The
annotator draws boxes, arrows, lines and numbered pins in chart colors
through forms::pen; Select chooses a mark, Delete removes it, and Cmd-Z
and Shift-Cmd-Z undo and redo through forms::History. theme.media() holds
their sizes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 16:16:29 -04:00
ZacharyZhang-NYandClaude Opus 5.5 0cf1d3679c Reviews: T26a passes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 15:58:02 -04:00
ZacharyZhang-NYandClaude Opus 5.5 12f0baab15 Media: the viewer's picture moves; a viewer with no picture takes no Tab
The stage's picture carries an id, so gpui keeps its frame and asks for
the next: an animated picture plays, turned or not. A viewer whose picture
has not loaded or cannot open is no Tab stop. AGENTS.md: img() moves an
animated picture's frames only with an id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 15:51:03 -04:00
ZacharyZhang-NYandClaude Opus 5.5 f8a51a5352 T26a Media: images
A new chapter, src/media. ImageViewer fits a picture to its stage and
zooms by the document steps, by keys, buttons and Cmd-scroll about the
pointer; a drag through gpui's drag, a scroll or the arrows pan it once it
overhangs; R turns it a quarter at a time. gpui paints pictures without a
transform, so media::turn turns the decoded frames off the main thread,
an asset keyed by the picture and the turn; `image` 0.25, which gpui
already decodes with, is now a dependency. ImageThumbnail shows a picture
in its own shape with a corner label and a ring when chosen, and is a Tab
stop only with a handler. Lightbox / Gallery Viewer points to
overlays::Lightbox, which the gallery opens from the thumbnails.

T26 splits into T26a–e. AGENTS.md: the drag rule says drags that pan,
turn or slide go through gpui's drag; pictures turn by their pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 15:34:45 -04:00
ZacharyZhang-NYandClaude Opus 5.5 4678773383 Reviews: T25f passes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 15:22:31 -04:00
ZacharyZhang-NYandClaude Opus 5.5 54e0384582 Generative: the embedding turns by drag past its edge; version diffs are tested
The turned embedding turns through gpui's drag, marked with its view, so a
drag keeps turning past the plot's edge. What a chosen version is set
against, and its heading, move into versions::compared, with a test. A
hidden legend entry dims its key and not its ring, and an embedding refuses
a group named twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 15:17:45 -04:00
ZacharyZhang-NYandClaude Opus 5.5 98b1a6416c T25f Generative: data and prompts
DatasetViewer is tables::DataTable with row detail; the gallery pages
through a dataset and opens a sample whole. EmbeddingVisualizer scatters
points by group, flat or turned in three dimensions: lens.rs holds the
projection, where a turned view keeps the unit ball's scale and fits any
point past it at every turn. A drag or the arrow keys spin it, the
pointer names a point, and the legend hides a group. PromptPlayground
gives each {name} of a template a field, marks the blanks, and runs once
every value is in. PromptDiff shows word changes inline, a space parting
a word taken out from the word put in; PromptVersionHistory lists
versions to choose from and restore.

ChartLegend entries that toggle are Tab stops with the focus ring. Each
capture pass starts its page fresh, so dark runs the script from the same
start as light.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 14:57:55 -04:00
ZacharyZhang-NYandClaude Opus 5.5 d196aa1e26 Reviews: T25e passes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 14:02:23 -04:00
ZacharyZhang-NYandClaude Opus 5.5 85f13b5455 Description rows wrap; the fine-tune share is tested
A DescriptionList row drops its value under its label once the value has
less than half a label's width beside it, so a 280px model card no longer
breaks its facts mid-word. FineTuneJobCard's bar share and its epoch line
move into tuning::progress, with a test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 13:57:14 -04:00
ZacharyZhang-NY 164d033ad3 T25e Generative: models
ModelCard sets a model's maker, what it does, its facts and tags, and
Use unless it is in use. ModelDownloadManager lists downloads with how
much of how much, speed and time left, and Pause, Resume, Cancel,
Retry and Delete by state. ModelStatus marks a model idle, loading,
loaded with its GPU memory, or failed. HardwareMonitor sets a meter per
part beside its history. FineTuneJobCard shows a job's phase, its
epochs and its loss on training and held-out data.

AGENTS.md: after each item, cargo clean -p ely-gpui-component too;
old builds of the crate had filled the disk. The reveal rule names
layout::bring_into_view.
2026-09-26 13:45:17 -04:00
ZacharyZhang-NY 092d125eda Reviews: T25d passes
Round 2 of the Claude reviewer found no defects.
2026-09-26 13:37:25 -04:00
ZacharyZhang-NY cb1b1ca90a Generative: the video strip scrolls and reveals; tags wrap in their row
The timeline's scroll box is the flex row itself, so its reach is its
shots; a focused shot brings itself whole into view through
layout::bring_into_view, which table headers now share; Extend sits
with the caption under the strip. A voice's tags cap at the row's
width and wrap inside it.
2026-09-26 13:32:44 -04:00
ZacharyZhang-NY 2266b4e367 T25d Generative: sound and motion
AudioGenerationPlayer shows a generated sound's prompt, model and
length, a bar while it is made, then chat::AudioMessage to play it;
Regenerate and Download wait for it. TTSVoicePicker lists voices with a
sample to play, the chosen one washed with a check, tags dropping below
when narrow. VideoGenerationTimeline lays shots along their time under
a ruler, with posters, placeholders, a playhead and Extend.

AudioMessage's waveform is one Tab stop where Left and Right seek, and
it gives way at 280px. debug::ruler serves the timeline's ruler. A mask
shown to read no longer follows the pointer.
2026-09-26 13:16:42 -04:00
ZacharyZhang-NY 6bfd53baee Reviews: T25c passes
Round 2 of the Claude reviewer found no defects.
2026-09-26 13:09:53 -04:00
ZacharyZhang-NY d5129675c9 Generative: a mask shown to read takes no pen; strokes fail loud
InpaintCanvas is a Tab stop with the focus ring only when on_brush
serves its keys, and takes the pen, the crosshair and the ring only
with on_stroke. new() asserts each stroke's radius within REACH and
its points on the picture. The settled strokes' cells stay cached, so
a drag paints only the stroke under the pen over a copy. The canvas
keeps square corners, so the wash fits the picture. The gallery drags
with moves.
2026-09-26 13:05:34 -04:00
ZacharyZhang-NY 997264439b T25c Generative: canvas
InpaintCanvas lays a mask over a picture: brush strokes in shares of
the picture, rastered into a 320-column grid where a later stroke wins
and an erasing one clears, painted as merged rectangles snapped to
device pixels. A ring follows the pointer at the brush's size; [ and ]
size the brush and X swaps it when focused. MaskBrush sets paint or
erase and the size, with Undo and Clear waiting for a stroke.

Pointer strokes move to forms::pen, which SignaturePad and the canvas
share.
2026-09-26 12:54:18 -04:00
ZacharyZhang-NY 8f20af49c3 Reviews: T25b passes
Round 2 of the Claude reviewer found no defects.
2026-09-26 12:47:12 -04:00
ZacharyZhang-NY 2a992f0399 Generative: a picture with nothing to open takes no focus
A done result tracks its focus only beside its press, so a grid with
no open handler leaves focus where it was. A decided comparison drops
its empty verdict row, so no gap is left under the sides.
2026-09-26 12:44:18 -04:00
ZacharyZhang-NY ac949fb1ce T25b Generative: queue and results
GenerationQueue shows each job's picture or where it stands, its
prompt and its progress, with Cancel, Retry and Remove as its state
allows; new rows fold in. GenerationGrid lays a run's results out in
their shape: breathing while they come, the picture once done, why one
failed with Retry. VariationPicker shows the chosen variation large
over thumbnails and ways on from it. ABCompareView sets two to four
models' answers side by side, blind until a verdict. ImageCompare
points to data_display::BeforeAfter.

primitives::framed and checked_ratio now serve previews and generative
alike. AGENTS.md records that gpui reads a &str picture as a web
address, and that grid rows overgrow tiles that keep a ratio.
2026-09-26 12:35:23 -04:00
ZacharyZhang-NY 406fe4cf0e Reviews: T25a passes
Round 2 of the Claude reviewer found no defects.
2026-09-26 12:26:37 -04:00
ZacharyZhang-NY 8be1e36c53 Forms: Cmd-Enter submits from any control in a form
Form sets the ElyForm key context and binds secondary-enter to Submit
under it, so a focused tile, chip or button submits too. The prompt
enhancer's wash trims anything but letters and digits from its runs,
its spacing sits on its parts so nothing gaps at rest, and its tests
check that the field takes focus back.
2026-09-26 12:24:05 -04:00
ZacharyZhang-NY 59b4673240 T25a Generative: prompt and settings
PromptEnhancer offers a fuller prompt with the words it adds washed,
split at punctuation; Use this takes it into the field, Keep mine
lets it go. StylePresetPicker and AspectRatioPicker are rows of Tab
stops, the chosen one in accent; a ratio chip draws its shape.
SeedInput keeps digits up to u32::MAX and rolls one with a die.
NegativePromptInput and ImageGenerationPanel point to forms::TagInput
and forms::Form. theme.generative() holds the chapter's sizes. The
gallery's preset pictures are the atrium photo in five styles.
2026-09-26 12:13:17 -04:00
ZacharyZhang-NY b651c05b1e Reviews: T24f passes
Round 2 of the Claude reviewer found no defects.
2026-09-26 12:06:06 -04:00
ZacharyZhang-NY 1f1166fef0 Tables: focused headers scroll into view; long tables scroll sideways
A sortable header is a tracked Tab stop; once focused it sets its
table's sideways offset whole from its painted bounds and asks one
frame. The virtualized branch scrolls sideways too, and its list
keeps a plain wheel to its rows. layout::on_axis keeps a scroll box's
wheel to its axes; tables, code blocks, the thumbnail strip and the
gallery's page use it. Footer figures move to tables/foot.rs; the
profiler's ruler takes theme.chart().label. HumanInputRequest's
answered state gets a test.
2026-09-26 12:00:27 -04:00
ZacharyZhang-NY a29cc8bc45 T24f Agent: control and cost
HumanInputRequest asks a question with answers to pick and a line to
write one; CostBreakdown totals a session and splits it by part.
InterruptButton, TokenUsageChart, TraceViewer and EvalResultTable
point to chat::stop_button, a stacked BarChart, TimelineProfiler and
DataTable.

UsageBar shows amounts as its owner writes them. MemoryPanel and
HumanInputRequest share agent::line::send_line; the chapter's bool
callbacks take forms::OnFlag.

DataTable scrolls columns wider than its box sideways, pinned or not,
and leaves a plain wheel to the page; sortable headers are Tab stops.
TimelineProfiler fits its ruler to its lane, its spans are Tab stops,
and a picked span takes accent.
2026-09-26 11:39:24 -04:00
ZacharyZhang-NY 96c4fe01a6 Reviews: T24e passes
Round 2 of the Claude reviewer found no defects.
2026-09-26 11:12:29 -04:00
ZacharyZhang-NY bdb5a1093f Agent: fail loud on cpu past full; group tools in grouped()
SandboxStatus::cpu asserts its share lies in 0..=1 and hands it to
Meter unclamped. ToolRegistry gathers kept tools by source through
grouped(), tested for first-seen order and repeated sources.
2026-09-26 11:09:50 -04:00
ZacharyZhang-NY e864c539b0 T24e: Agent, environment
- SandboxStatus: a dot and a word (a ring while it starts or works), what it
  uses of what it has as meters, and how long it has run; the header wraps
- McpServerList over lists::ListItem: a dot per state, what it runs or why it
  failed, its tools once connected, and a switch
- ToolRegistry: tools grouped by source, found by every word of the owner's
  query, each Allow, Ask or Deny; rows wrap at 280px
- MemoryPanel: a line to keep a memory, Enter or Remember, blank words kept
  out; each memory with its source, when, and Forget
- CheckpointList over data_display::Timeline, newest first, the current point
  marked, Rewind on the rest
- gallery: every control wired to state
2026-09-26 11:00:15 -04:00
ZacharyZhang-NY 85781635ff T24d round 3 fix, after the cap
- BrowserPreview marks the shown frame with accent; the focus color stays
  the focus ring's
- BrowserPreview and ComputerUseViewer take the host's ratio, 16:10 unless
  set, so frames keep their shape and a pointer's share lands on the picture;
  test a_screen_takes_its_pictures_shape
- TASKS: the review moves to a Claude Fable 5.1 max session after codex ran
  out of quota; round 3 fixed after the cap, no fourth review
- AGENTS: focus and choice colors part; host pictures keep their shape
2026-09-26 10:54:39 -04:00
ZacharyZhang-NY c013747b7c T24d round 2 fix
- BrowserPreview keeps a ScrollHandle for its strip and a tracked focus
  handle per thumbnail; the focused thumbnail scrolls into view once, through
  forms::options::reveal and the canvas now shared as revealer
- Popup draws its reveal through the same revealer
- test tab_brings_a_hidden_frame_into_view
2026-09-26 09:16:01 -04:00
ZacharyZhang-NY e99a86bed5 T24d round 1 fix
- BrowserPreview's frame strip scrolls sideways, so every frame stays in
  reach in a narrow card
- ArtifactPanel's header wraps: the title keeps label_width, the controls
  ride in one group that wraps inside the box
- tests: a_narrow_strip_scrolls_to_its_last_frame,
  a_narrow_panel_wraps_its_header
- AGENTS: everything works at 280px
2026-09-26 09:07:14 -04:00
ZacharyZhang-NY 26913df98a T24d: Agent, previews
- BrowserPreview: the address with a lock and a spinner while it loads, the
  host's frame shown, and the frames so far along the foot as Tab stops;
  ScreenshotStream is that strip
- ComputerUseViewer: the host's frame, a ring where the agent points that
  rings again at each new point, and what it does there
- LivePreview: the host's page under its address, with reload
- ArtifactPanel: title, versions through chat::BranchNavigator, Preview and
  Source when it has both, copy and download; it fails loud with neither
- gallery: frames cut from the gallery's own pages; one asset! macro in
  main.rs replaces three copies
2026-09-26 08:59:12 -04:00
ZacharyZhang-NY dbda2fd8c2 Reviews: T24c passes 2026-09-26 08:55:21 -04:00
ZacharyZhang-NY 58a5784124 T24c round 1 fix
- FileChangeCard's header wraps: the path keeps label_width, counts and
  verdict drop below as one group; MultiFileDiffReview's header wraps its
  counts and its all-buttons inside the box, parted by justify_between
- git::DiffViewer's fold rows are Tab stops with a focus ring when the
  owner opens folds; every row keeps a transparent border so heights match;
  an inert fold no longer shows a pointer
- tests: a_narrow_review_wraps_its_rows_inside_its_box,
  git::a_fold_opens_from_the_keyboard; the gallery adds a 280px review
- AGENTS: taffy drops gaps beside an auto margin; wrapping headers
2026-09-26 08:52:38 -04:00
ZacharyZhang-NY 62397cbba1 T24c: Agent, changes
- FileChangeCard: a proposed change to one file, its path and counts,
  Reject and Accept in a slot that keeps presses off the header; opened,
  the diff through a headless git::DiffViewer with its folds opening
- MultiFileDiffReview: every file at once, the total counts, how many are
  reviewed, Reject all and Accept all; verdicts as collab::Decision
- git::DiffViewer gains headless, leaving the file's header to its owner
- CommandExecutionCard is terminal::CommandBlock
- gallery: three proposed files and a command's block
2026-09-26 08:37:45 -04:00
ZacharyZhang-NY 882280cb25 Reviews: T24b passes 2026-09-26 08:34:31 -04:00
ZacharyZhang-NY fab0131168 T24b round 1 fix
- AgentProgress splits its bar only for two steps or more; a task of one
  step draws a plain bar
- test a_task_of_one_step_draws_before_and_after renders 0 of 1 and 1 of 1
2026-09-26 08:31:32 -04:00
ZacharyZhang-NY f4cf1ef3d7 T24b: Agent, progress
- AgentStepList: an agent's steps down a data_display::Timeline rail,
  marked by chat::step_mark, with detail and time
- AgentPlan: items waiting, at work or done, with a count; done items go
  quiet and struck through
- chat::step_mark reads motion::changes: a check or cross that arrives pops
  in, still on first paint
- AgentStatus: a pill for planning, working, waiting, done or failed
- AgentProgress: the step at work, done of total, the time so far and a
  bar split by step; progress past its total fails loud
- SubAgentTree is lists::Tree with each agent's state as its note
- gallery: a plan the button advances, steps, statuses and a tree
2026-09-26 08:25:51 -04:00
ZacharyZhang-NY aaf212c53c Reviews: T24a passes 2026-09-26 08:21:23 -04:00
ZacharyZhang-NY f04bc081eb T24a round 2 fix
- a dialog's body scroll box holds one inner column, so the body keeps
  its height and scrolls instead of shrinking to the capped card
- test a_tall_dialog_body_keeps_its_height_and_scrolls: 2000px stays
  2000px and a 300px wheel moves it 300px
2026-09-26 08:17:18 -04:00
ZacharyZhang-NY a47d0b058b T24a round 1 fix
- overlays::Dialog caps its card at the window, a title bar's height clear
  at each end, and scrolls its body while heading and actions stay; the
  fullscreen body scrolls too; test a_tall_dialog_keeps_its_actions_in_the_window
- ToolCallCard's tool name gives way with an Ellipsis, so time and chevron
  stay in a narrow column; the gallery shows one at 280px
- the Agent script shuts the card it opened, so each theme's shot opens it
- AGENTS: dialogs fit the window
2026-09-26 08:09:46 -04:00
ZacharyZhang-NY ecb2eac4a5 T24a: Agent, tool calls
- ToolCallCard: a step mark, the tool in mono, what it touched and how long
  it took; a press opens its arguments and result as code, an error in red
- ToolCallGroup folds several calls under one line
- ToolApprovalDialog over overlays::Dialog: Deny or Run, one answer; Escape,
  the scrim and the close deny
- PermissionPrompt: deny, always allow or allow once, in place
- chat::StepState gains Failed; chat::step_mark draws a step's mark for
  search steps and tool calls alike
- typography::format::took reads ms, seconds or minutes
- gallery: an Agent page with calls in each state, a group, the dialog and
  the prompt
2026-09-26 08:00:21 -04:00
ZacharyZhang-NY 3d3ceee79d Reviews: T23e passes; T24 splits into six in PRD order 2026-09-26 07:56:18 -04:00
ZacharyZhang-NY b9d73e2b30 T23e: AI chat, welcome
- WelcomeScreen: a greeting for the hour over the owner's composer,
  starters and cards, centered while it fits and scrolled from its top
  when it does not
- SuggestionChips, FollowUpSuggestions and CapabilityCards: each press asks
  the owner; rows and cards are Tab stops
- PromptLibrary is documents::TemplatePicker over saved prompts
- gallery: starters and cards fill the welcome composer, a saved prompt
  fills it too, send clears it with a toast, a follow-up asks its question
2026-09-26 07:50:09 -04:00
ZacharyZhang-NY 5b15a324ae Reviews: T23d passes 2026-09-26 07:46:11 -04:00
ZacharyZhang-NY e46974f055 T23d round 1 fix
- the conversation list reads the zone through format::system_zone, which
  fails loud; the gallery expects the system zone
- Combobox opens with its cursor on its value, MultiSelect on the first
  row ticked; tests for all three lists open r25 of thirty in view
- AGENTS: the zone rule; lists open on their choice
2026-09-26 07:42:45 -04:00
ZacharyZhang-NY 280d49a96c T23d: AI chat, conversations
- ConversationList: pinned first, then by day, newest first; a query keeps
  the titles that hold every word
- ConversationItem: a Tab stop when there is something to do; its menu of
  rename, pin and delete shows on hover, while open, or while the row holds
  focus; a press on the menu leaves the row shut
- new_chat_button, ProjectList, ProjectKnowledgePanel with a capacity meter
- SharedConversationView: who shared it and when, and a way to carry it on
- ConversationExport: format and thinking in a Dialog, Cancel or Export
- icons: square-pen, corner-down-right
- gallery: conversations with rename, pin and delete; projects; a shared
  conversation; export with a toast
2026-09-26 07:32:48 -04:00
ZacharyZhang-NY 6478dc4242 Forms: lists reveal their cursor once laid out
- Select, Combobox and MultiSelect opened a long list at its top, the
  choice out of view: gpui skips a scroll asked before the list's first
  frame. Picker and Suggestions share forms::options::reveal, which Popup
  runs from a canvas inside the list
- test: a_long_list_opens_with_its_choice_in_view
- AGENTS: the scroll rule names the reveal
2026-09-26 07:25:13 -04:00
ZacharyZhang-NY 83e2cdeaa2 Reviews: T23c passes 2026-09-26 07:15:16 -04:00
ZacharyZhang-NY 1a00e19591 T23c round 2 fix
- a trigger opens only its word, so a path keeps its slashes in the query
- suggestions reveal the cursor once the list has its bounds: Popup takes a
  row to reveal, and a canvas inside the list asks for it and one more frame
2026-09-26 07:10:53 -04:00
ZacharyZhang-NY 4fafba4b20 T23c round 1 fix
- suggestions: arrows step past disabled rows and scroll the cursor into view;
  the cursor starts on an enabled row; Enter refuses a disabled one
- active_trigger takes the query's word test: one word for mentions, any
  non-space for the composer, so a file name keeps its dot
- DragDropOverlay takes the drop itself, which gives it the hitbox gpui needs
  to style a drag-over
- ParameterPanel shows the places of the step, value and both ends
- gallery: the prompt menu fills the draft; Reset restores the system prompt
- AGENTS: drag-over styles need a hitbox; debug_bounds never clear
2026-09-26 06:59:37 -04:00
ZacharyZhang-NY b2ce2df428 T23c: AI chat, input
- PromptInput: attachments and context above the field, the owner's tools and
  send below; Enter sends, Shift-Enter breaks the line; / offers commands and
  @ offers context over forms::Suggestions; files dropped on it attach behind a
  drop veil
- AttachmentButton, AttachmentChip with thumbnails and uploads, ContextChips
- VoiceInputButton and VoiceWaveform with a red dot and the time
- ParameterPanel, SystemPromptEditor, TokenCounter, CostEstimator
- forms: a Newline action on Shift-Enter keeps one-line fields to one line;
  replace_trigger moves beside Suggestions; choose and dropped shared
- gallery: composer, pickers and tuning
2026-09-26 06:29:20 -04:00
ZacharyZhang-NY fd9bcf486c Reviews: T23b passes 2026-09-26 06:24:16 -04:00
ZacharyZhang-NY 61085ef7a5 T23b round 2 fix
- the gallery binds its passage's range before the vec, as clippy asks
- AGENTS: anything pressable is a Tab stop; text in a row gives way to fixed parts
2026-09-26 06:22:15 -04:00
ZacharyZhang-NY 4c9695a85f T23b round 1 fixes
- source cards and web results with a handler are Tab stops with focus rings
- the sources row and a passage's header shrink with an ellipsis, keeping the
  count, chevron and score in view
- chat tests move into a folder, citations in their own file
- gallery: a narrow column of citations
2026-09-26 06:16:43 -04:00
ZacharyZhang-NY c3e9f14241 T23b: AI chat, citations
- CitationBadge: a numbered pill after a claim, its source on hover, a press
  opens it; a Tab stop
- SourceCard and SourceList: the sources folded under their count and sites,
  each a card numbered as cited; the fold opens from the keyboard
- SearchProgress: steps waiting, working and done
- WebResultCard and DocumentChunkPreview with matched words washed
- gallery: citation and sources states
2026-09-26 06:07:42 -04:00
ZacharyZhang-NY 8889be4f38 Reviews: T23a passes 2026-09-26 06:02:09 -04:00
ZacharyZhang-NY 32135600f2 T23a round 1 fixes
- your messages shrink within their column and wrap there
- pictures, grids and video take their own shape up to their column's width;
  files, voice notes and link cards stay inside it
- a finished stream shows whole
- avatar pictures load from web addresses too
- the list drops its way down once everything fits again
- the reasoning toggle is a Tab stop with a focus ring
- a link's thumbnail keeps its corners
- gallery: a narrow column of media
2026-09-26 05:56:48 -04:00
ZacharyZhang-NY 6151291443 T23a: AI chat, messages
- messages: yours in a quiet bubble, the assistant's as prose beside its mark,
  system notes, errors; avatars, headers with model and time, footers
- MessageList over gpui's bottom-aligned list: drawn near the view, held at the
  newest, counting arrivals while away; a way back down; day separators
- streaming text and markdown reveal toward the text as it grows, with a caret;
  a blinking cursor before the first words
- CodeBlock with language, copy, run, apply, download and folding;
  MarkdownRenderer::code lets messages draw fences with it
- pictures, grids, files, voice notes, video over host frames, link previews
- quotes, actions, an editor that takes focus, versions, regenerate, stop and
  continue
- errors with retry, a rate limit counting down, feedback, thinking blocks
- gallery: the AI Chat page with scripted states
2026-09-26 05:35:09 -04:00
ZacharyZhang-NY 88a1550c14 Reviews: T22 passes 2026-09-26 05:27:57 -04:00
ZacharyZhang-NY f6bbfe61da T22 round 1 fixes
- a remote cursor shows only while its caret sits inside the field, sideways too
- presence avatars keep a transparent border for the focus ring
- the access list shows each role, as words when it cannot change
- status dots take the theme's size
- peer and member pictures load from files and web addresses alike
2026-09-26 05:22:50 -04:00
ZacharyZhang-NY 153cf95b99 T22: Collaboration
- presence: avatars ringed in each peer's color with where they are, press to
  follow; a breathing live dot; follow mode frames the view and stops on Escape
- remote cursors and selections painted over a field from its own layout
- comment threads with replies, reactions and resolve; a marker that opens a
  thread in a bubble; a sidebar of open and resolved threads
- annotations washed in their authors' colors, notes on hover
- suggestion mode: edits become suggestions word by word; track changes shows
  them struck and underlined, to accept or reject
- sharing: invite by email with checked chips, roles, the access list and who
  the link lets in, in one dialog
- primitives::hold_focus gives a mode its own focus, shared with ZenMode
- AvatarGroup's tuck and count shared; Avatar::ring_color; TagInput::check;
  TextInput::bounds_for_range; Popover::size
- icons: smile-plus, user-plus, message-square-diff
- gallery: the Collaboration page with scripted states
2026-09-26 05:10:37 -04:00
ZacharyZhang-NY 0623855f38 Reviews: T21b and T21c pass 2026-09-26 05:01:35 -04:00
ZacharyZhang-NY 9c9e811534 T21b and T21c round 1 fixes
- a page centers only while it fits the view; a wider one starts in reach
- the reveal runs after the pages' prepaint and keys on the hit itself, so a
  new hit at the same index and a hit current from the first frame come into view
- the chapter list scrolls
- zen takes focus when turned on from outside, so Escape leaves it, and hands
  focus back
- the gallery keeps a page's due date and tags
2026-09-26 04:55:58 -04:00
ZacharyZhang-NY cc82acbe20 T21b and T21c: Documents, reading and knowledge
- DocumentViewer: pages the app drew on a desk, the page under the view's middle
  counted, zoom steps and fit, thumbnails, notes pinned where a press lands, and
  the owner's FindWidget over the pages, hits lit and the current one revealed
- PageThumbnailList, EpubReader (chapters, type size, progress), PrintPreview
- MarkdownRenderer feeds TableOfContents through sections and outline
- Footnote, TooltipTerm, Glossary, ReadingProgress, ZenMode
- Backlinks, Favorites, TrashBin, PageCover, PageIcon, TemplatePicker,
  VersionHistory with PageHistoryDiff
- editor::FindWidget shows the replace fold and option toggles only with handlers
- block editor tests move to documents/blocks
- icons: a-arrow-down, a-arrow-up
- gallery: reading and knowledge sections; scripted find, changes and zen
2026-09-26 04:34:32 -04:00
ZacharyZhang-NY fb993ffceb T21a round 3 fix, after the cap
- TextInput records the selection each edit replaces, where every edit passes;
  the block editor's undo goes back to it, for typing, delete, cut and paste
- TextInput's layout geometry moves to forms/text/geometry.rs
- Reviews: T21a done, fixed after the cap, no fourth review
2026-09-26 04:03:43 -04:00
ZacharyZhang-NY d0c7f98269 T21a round 2 fixes
- a field notes its selection when a key goes down, so undo of typing selects
  what the typing replaced
- reading order skips table cells merged away, so Up and Down across blocks
  land on cells that show
2026-09-26 03:57:27 -04:00
ZacharyZhang-NY 82e227cf60 T21a round 1 fixes
- Enter over a selection drops it and splits at its start, one undo step
- undo and redo keep where the caret was and put it back
- a format unwraps only its own markers, so italic inside bold keeps the bold
- keys into a merged table cell land on the cell that shows
- undo rebuilds a block whose kind needs other fields, so styling comes back
- tests activate their window, as focus events need
- undo bookkeeping moves to blocks/edit.rs
2026-09-26 03:49:27 -04:00
ZacharyZhang-NY 9edd0a8612 T21a: Documents, editing
- markdown read with pulldown-cmark: marks for live styling, blocks, links, words
- forms::Highlight gains weight, slant and strike; measure and paint share runs
- Inter Italic embedded, so emphasis reads slanted
- forms::mention's popup and keys become Suggestions, shared with the editors
- RichTextEditor with FixedFormatToolbar, the floating toolbar, LinkEditor,
  @ and : suggestions, WordCount and ReadingTime
- MarkdownEditor: visual (a pressed block opens as source), source, split
- BlockEditor: prose, lists, to-dos, quotes, callouts, toggles, code, math,
  diagrams the app draws, dividers, tables that grow and merge, pictures to
  resize and align, cards for video and pages, columns, synced text; slash menu,
  markdown shortcuts, handles that drag and open a menu, one undo for all
- TodoItem and Checklist
- icons: heading-1..3, workflow, merge
- gallery: the Documents page with scripted states
2026-09-26 03:32:24 -04:00
ZacharyZhang-NY 05e764304d Reviews: T20 passes 2026-09-26 02:57:34 -04:00
ZacharyZhang-NY 7a5d7e2ea6 T20 round 1 fixes
- the hex demo's selection stops at memory's end
- a disassembly gutter fills its row, so an empty one takes a press
- a breakpoint's box keeps its press from the row
- ruler ticks keep the places their step needs
- the toolbar names each usual key for the platform; the app binds them
- tests: gutter press, box press, tick places
2026-09-26 02:54:23 -04:00
ZacharyZhang-NY 74d6bd1771 T20: Debug
- DebugToolbar, BreakpointList, CallStack with folded library frames, ThreadList
- VariablesPanel and WatchPanel, colored by kind, changes washed
- HexViewer at a base address with the byte under the pointer lit on both sides
- Disassembly with source lines, registers and numbers colored, gutter breakpoints
- Flamegraph that zooms by frame, TimelineProfiler that zooms and pans by wheel
- NetworkInspector with a waterfall, a filter and a request's headers and body
- icons: redo-dot, arrow-down-to-dot, arrow-up-from-dot
- gallery: the Debug page
2026-09-26 02:38:50 -04:00
ZacharyZhang-NY 74dfb5130e Reviews: T19 passes 2026-09-26 02:32:07 -04:00
ZacharyZhang-NY b2c587546e T19 round 2 fixes
- regions read their lines through TextMerge's accessors; conflicts and resolve
  split with similar's tokenize_lines, so a lone return ends a line everywhere
- the merged ending and line breaks know a lone return
- taking both sides separates them only when both hold text
- merge views split rows the same way
2026-09-26 02:28:13 -04:00
ZacharyZhang-NY 3abcd5cc87 T19 round 1 fixes
- merges keep each line's ending and the last newline; resolve keeps every other byte
- the split view numbers each side from its own file
- hunk headers come from similar's UnifiedHunkHeader: an empty side starts before its range
- the commit subject counter sits in the input's suffix
- clipped code rows in diffs, merges, blame and review set their line height
- gallery: scripted split view and a long subject
2026-09-26 02:19:11 -04:00
ZacharyZhang-NYandClaude Opus 5.5 d083e87493 T19 Git: diffs unified and split with word marks and folds, three-way merge and conflict resolver, changes list and commit input, commit graph and list, branch selector, branch, tag and stash lists, blame with annotation, file history, pull request card, review thread, diff stat and status badge
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 01:59:27 -04:00
ZacharyZhang-NYandClaude Opus 5.5 de1c03746e Reviews: T18 passes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 01:53:52 -04:00
ZacharyZhang-NYandClaude Opus 5.5 99d9bf45d4 T18 round 2 fixes: a folder the terminal cannot enter stops the start, and links read combining clusters whole
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 01:50:58 -04:00
ZacharyZhang-NYandClaude Opus 5.5 47f3c7d505 T18 review fixes: a missing folder stops the start, combining marks paint on their cell, the terminal is a Tab stop and keeps its wheel, clear drops find matches; the gallery starts a fresh shell per pick and picks the shell tab before typing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 01:44:13 -04:00
ZacharyZhang-NYandClaude Opus 5.5 8374b77906 T18 Terminal: a real shell on a pseudo-terminal through alacritty_terminal 0.26, drawn cell by cell in the theme's ANSI colors, with keys, text input, selection, scrollback, links and regex find; replayed output; tabs, splits and toolbar with a shell selector; command blocks and history; AnsiText, LogViewer and ProcessList
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 01:27:44 -04:00
ZacharyZhang-NYandClaude Opus 5.5 601a29a374 Reviews: T17a–e pass
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 01:20:19 -04:00
ZacharyZhang-NYandClaude Opus 5.5 30ffe7a0b2 T17 round 2 fixes: merge keeps the newest caret in the lead by where it was given, and a composition cleared to nothing ends
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 01:16:08 -04:00
ZacharyZhang-NYandClaude Opus 5.5 a42f8e44a8 T17 review fixes: hidden numbers draw, empty edits and read-only commands change nothing, hints stay in a ghost-cut row, the newest cursor leads, a composition undoes in one step, call keys carry their place, references count matches, trailing-space hits and **/ globs, name-only switcher marks, pin and remove stay on their button, settings show their places; decimals moves to typography::format
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 01:07:37 -04:00
ZacharyZhang-NYandClaude Opus 5.5 d05754ac19 T17 Editor: CodeEditor with gutter, folds, minimap, rulers, guides, brackets, whitespace, cursors, multi-cursor, sticky scroll, inlay hints, lenses, decorations, ghost text, inline chat and diff (T17a); completions, signatures, hover, code actions, rename, references, outline, go to symbol, call and type hierarchy (T17b); find and replace, search panel and filters (T17c); problems, output, debug console, extensions, settings with JSON, keybindings, theme previews, welcome, recent projects, switcher, task runner (T17d); status items (T17e)
Shared: generic undo History, OverflowMenu icon and tooltip, StatusBarItem leading, six Lucide icons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 00:43:41 -04:00
ZacharyZhang-NYandClaude Opus 5.5 c229a096ca Reviews: T16a–e done. After the cap: a lagging synced chart leaves the shared window alone, and a price step keeps its places by magnitude
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 00:20:08 -04:00
551 changed files with 6685 additions and 34846 deletions

No files matched your search

+1 -1
View File
@@ -50,7 +50,7 @@ body:
id: version id: version
attributes: attributes:
label: Commit or version label: Commit or version
placeholder: 259ee2a placeholder: 4f0b372
- type: textarea - type: textarea
id: logs id: logs
attributes: attributes:
+2 -11
View File
@@ -22,8 +22,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
# One leg per runner registered with git.zacharyzhang.com; macOS arm64 is the crate job above. os: [ubuntu-24.04, ubuntu-24.04-arm, windows-latest, windows-11-arm, macos-15-intel]
os: [ubuntu-24.04, windows-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
@@ -31,15 +30,7 @@ jobs:
with: with:
toolchain: "1.95" toolchain: "1.95"
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- if: runner.os != 'Linux' - run: cargo test --lib --features test-support --quiet
run: cargo test --lib --features test-support --quiet
# The Linux job container runs as root, which ignores the file permissions the tests check.
- if: runner.os == 'Linux'
run: |
useradd --create-home ci
chmod o+x "$HOME"
chown -R ci "$GITHUB_WORKSPACE" "$HOME/.cargo" "$HOME/.rustup"
runuser -u ci -- env HOME=/home/ci CARGO_HOME="$HOME/.cargo" RUSTUP_HOME="$HOME/.rustup" PATH="$PATH" cargo test --lib --features test-support --quiet
- run: cargo build --example gallery - run: cargo build --example gallery
site: site:
-120
View File
@@ -1,120 +0,0 @@
name: Showcase
# Moves the site's showcase pin to the Gitea catalog's main once its checks pass, then deploys.
on:
schedule:
- cron: "*/30 * * * *"
workflow_dispatch:
inputs:
deploy:
description: Build and deploy even if the pin is current
type: boolean
default: false
permissions:
contents: read
concurrency:
group: showcase
cancel-in-progress: false
env:
CATALOG: ZacharyZhang-NY/Ely-GPUI-Showcases
jobs:
check:
runs-on: ubuntu-latest
outputs:
commit: ${{ steps.head.outputs.commit }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- id: head
env:
API: ${{ github.server_url }}/api/v1/repos/${{ env.CATALOG }}
FORCE: ${{ inputs.deploy }}
run: |
head=$(curl -fsS "$API/branches/main" | jq -r .commit.id)
pinned=$(tr -d '[:space:]' < frontend/showcase.lock)
if [ "$head" = "$pinned" ]; then
echo "showcase: pinned at ${head:0:7}, the catalog's main"
[ "$FORCE" = "true" ] && echo "commit=$head" >> "$GITHUB_OUTPUT"
exit 0
fi
status=$(curl -fsS "$API/commits/$head/status")
total=$(echo "$status" | jq .total_count)
state=$(echo "$status" | jq -r .state)
if [ "$total" -eq 0 ] || [ "$state" = "pending" ]; then
echo "showcase: ${head:0:7}'s checks have not finished; next run looks again"
[ "$FORCE" = "true" ] && echo "commit=$pinned" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$state" != "success" ]; then
echo "::error::showcase: ${head:0:7} failed its catalog checks ($state); the pin stays at ${pinned:0:7}"
exit 1
fi
echo "showcase: ${pinned:0:7} -> ${head:0:7}"
echo "commit=$head" >> "$GITHUB_OUTPUT"
deploy:
needs: check
if: needs.check.outputs.commit != ''
runs-on: ubuntu-latest
permissions:
contents: write
env:
COMMIT: ${{ needs.check.outputs.commit }}
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.95"
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
key: web
- name: wasm-bindgen at Cargo.lock's version
run: |
version=$(sed -n '/^name = "wasm-bindgen"$/{n;s/^version = "\(.*\)"$/\1/p;}' Cargo.lock)
echo "WASM_BINDGEN=wasm-bindgen@$version" >> "$GITHUB_ENV"
- uses: taiki-e/install-action@v2
with:
tool: ${{ env.WASM_BINDGEN }}
- name: wasm-opt
run: |
curl -fsSL https://github.com/WebAssembly/binaryen/releases/download/version_132/binaryen-version_132-x86_64-linux.tar.gz | tar -xz -C "$RUNNER_TEMP"
echo "$(ls -d "$RUNNER_TEMP"/binaryen-*/)bin" >> "$GITHUB_PATH"
- uses: pnpm/action-setup@v6
with:
version: 11
- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
cache-dependency-path: frontend/pnpm-lock.yaml
- run: echo "$COMMIT" > frontend/showcase.lock
- run: scripts/web.sh frontend/public/gallery
env:
ELY_GALLERY_ASSETS: https://elygpui.com/gallery/assets/
- working-directory: frontend
run: |
pnpm install --frozen-lockfile
pnpm test
pnpm build
pnpm run deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
- name: Commit the pin
run: |
git config user.name 'gitea-actions'
git config user.email 'actions@git.zacharyzhang.com'
git add frontend/showcase.lock
git diff --cached --quiet && exit 0
git commit -m "Site · Showcase: pin Ely-GPUI-Showcases at ${COMMIT:0:7}"
for attempt in 1 2 3; do
git pull --rebase origin main && git push origin HEAD:main && exit 0
sleep 5
done
exit 1
-1
View File
@@ -2,4 +2,3 @@
node_modules/ node_modules/
/frontend/dist/ /frontend/dist/
.DS_Store .DS_Store
/compat/kit/Cargo.lock
-28
View File
@@ -1,28 +0,0 @@
# Builds Ely for Anthropic's OSS Scanner: the library and its tests, all crates fetched while online.
FROM debian:bookworm
ENV DEBIAN_FRONTEND=noninteractive \
RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:$PATH \
CARGO_NET_OFFLINE=false
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl build-essential clang cmake pkg-config git python3 \
libfontconfig-dev libfreetype-dev libxkbcommon-dev libxkbcommon-x11-dev \
libwayland-dev libx11-dev libx11-xcb-dev libxcb1-dev libxi-dev libxcursor-dev libxrandr-dev \
libvulkan-dev libasound2-dev libssl-dev libzstd-dev \
&& rm -rf /var/lib/apt/lists/*
RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.95
# Tests check file permissions, which root ignores, so the build runs as a plain user.
RUN useradd --create-home ely
COPY --chown=ely:ely . /src
RUN chown -R ely:ely /usr/local/cargo /usr/local/rustup
USER ely
WORKDIR /src
# Fetch every crate, then build the library and its tests with debug info; tests run offline later.
RUN cargo fetch --locked
RUN cargo build --locked --lib --features test-support \
&& cargo test --locked --lib --features test-support --no-run
ENV CARGO_NET_OFFLINE=true
RUN cargo test --locked --lib --features test-support --quiet || echo "WARNING: cargo test reported failures"
-25
View File
@@ -1,25 +0,0 @@
# Threat model
## What this project does and where untrusted input enters
Ely is a Rust component library for GPUI desktop and web apps. It runs inside the host app's process, so the host decides what is trusted. Treat as untrusted whatever an app's users or the network can hand a component:
- Markdown rendered by `documents` (pulldown-cmark) and text typed or pasted into fields (`forms::TextInput`, `editor::CodeEditor`).
- JSON and settings parsed with serde: saved layouts (`layout::workspace`), JSON editors and viewers (`devtools`), imported VS Code settings and themes (`settings::vscode`, `editor::settings`), GeoJSON (`maps::geojson`, `maps::choropleth`).
- Pictures decoded with `image` and QR frames read with `rqrr` (`media`, `misc::QrCodeScanner`).
- Terminal output and ANSI escape codes (`terminal`, `typography` `AnsiText` via vte).
- File names and paths from drops, archives and folder listings (`forms::files`, `files`, `lists::Folder::of`).
## Components that matter most / least
- Most: parsers and anything that turns external bytes into layout (markdown, JSON/GeoJSON, ANSI, images, QR, archive paths), and the few `unsafe` blocks that call AppKit (`shell::native`, `buttons::share`, `forms::color::dropper`).
- Less: pure drawing and theme code. `examples/`, `frontend/` (the website), `compat/` and `scripts/` are out of scope.
## How to exercise it
`cargo test --lib --features test-support` runs the suite headless on gpui's test platform. Parsers can be driven directly from unit tests.
## How you rate severity
- Memory unsafety in an `unsafe` block, or a path traversal that reaches outside a chosen folder: high.
- A panic, unbounded allocation or hang reachable from untrusted input: medium (it takes down the host app).
- Wrong rendering with no security effect: out of scope.
## Anything to leave alone
- Panics from asserts on a caller's own arguments are by design (fail fast). Report only panics reachable from runtime data.
- The macOS-only code (AppKit, wry WebView) does not build in this Linux image.
+16 -33
View File
@@ -5,7 +5,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
## Stack ## Stack
- Rust 1.95, edition 2024. One crate: `ely-gpui-component`. - Rust 1.95, edition 2024. One crate: `ely-gpui-component`.
- gpui, gpui_platform and gpui_web from crates.io's `gpui-pre` snapshots at `=0.3.8`, Zed at 279fe07, the pin GPUI Kit 0.7.1 holds; each keeps its crate name through `package`. gpui takes `stacker`, which keeps deep layouts off the stack's end; gpui_platform takes `font-kit`, without which macOS draws no text. Feature `runtime_shaders`, on by default, turns on gpui_platform's: machines without Xcode lack the Metal compiler. - gpui and gpui_platform from Zed's repo by git at 1a28cff, the code gpui-pre 0.3.7 republishes. gpui takes `stacker`, which keeps deep layouts off the stack's end; gpui_platform takes `font-kit`, without which macOS draws no text. Feature `runtime_shaders`, on by default, turns on gpui_platform's: 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), JetBrains Mono 2.304 and IBM Plex Sans Regular (OFL). Plex sits unmodified, its name reserved, where gpui's svg renderer loads it: gpui_web answers `.SystemUIFont` with it, and svg text falls back to it on the web. - Assets, embedded with `rust-embed`: Lucide 1.48.0 icons (ISC), Inter 4.1 (regular, medium, semibold, italic), JetBrains Mono 2.304 and IBM Plex Sans Regular (OFL). Plex sits unmodified, its name reserved, where gpui's svg renderer loads it: gpui_web answers `.SystemUIFont` with it, and svg text falls back to it on the web.
- Catalog data: `emojis` (Unicode emoji), `isolang` (ISO 639, native names), `isocountry` (ISO 3166), `iso_currency` (ISO 4217, also minor units for money). - 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. - 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.
@@ -13,12 +13,11 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- Web views: `wry` 0.57 (Apache-2.0 OR MIT, default features off, macOS only) lays a child WKWebView over the window. - 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, native only) for the grid, its parser and the pseudo-terminal; `futures` carries its events. `vte` 0.15 (Apache-2.0 OR MIT), the parser it re-exports, reads `AnsiText`'s codes alone, so colored output builds for wasm32. - Terminal: `alacritty_terminal` 0.26 (Apache-2.0, default features off, native only) for the grid, its parser and the pseudo-terminal; `futures` carries its events. `vte` 0.15 (Apache-2.0 OR MIT), the parser it re-exports, reads `AnsiText`'s codes alone, so colored output builds for wasm32.
- Diffs: `similar` 3.2 (Apache-2.0) for line and word diffs and three-way merges; its `unicode` feature splits words at punctuation. - Diffs: `similar` 3.2 (Apache-2.0) for line and word diffs and three-way merges; its `unicode` feature splits words at punctuation.
- Markdown: `markdown` 1.0.0 (markdown-rs, MIT) reads GitHub's flavor and math into a syntax tree with byte positions for `MarkdownRenderer`, `MarkdownEditor`, the highlights, block ranges and outline. `MarkdownRenderer` draws display math through `typography::Latex`, a ```` ```mermaid ```` block through `documents::Diagram`, and pictures from the owner's folder (`base`) or `data:` addresses (`base64` 0.22, MIT OR Apache-2.0); web pictures stay unloaded. Inline math shows as TeX: gpui text carries no inline pictures. Inline it reads as Typora's defaults: `:shortcode:` turns to its emoji as GitHub reads it (the `emojis` crate's names, GitHub's today, so `:cricket:` is the insect; read after escapes and references, so `\:smile:` turns too; inside an autolink or `<code>` it stays text; within one DOM text node, which filtered tags do not split, the first `:name:`-shaped run decides for all: blank text or a space before it turns every shortcode, else none, so `x:smile: :smile:` stays), inline HTML's `u`, `b`/`strong`, `i`/`em`, `s`/`del`/`strike`, `code`/`kbd`, `mark`, `br` and `a href` draw as what they mean GFM's filtered tags (`script`, `style`, `title`, `textarea`, `iframe`, `xmp`, `noembed`, `noframes`, `plaintext`) show as text, and other tags, hyphenated names too, vanish, leaving their text; a tag's name runs to white space or `/`, as HTML5 reads it; attribute values split on ASCII whitespace alone and decode as HTML decodes them; each Markdown container is the element GitHub writes (`em`, `strong`, `del`, `a`), on one stack with the tags and repaired as HTML5 repairs: an end tag closes the innermost element of its name and all above it, of which only formatting elements (`a`, `b`, `code`, `em`, `i`, `s`, `strike`, `strong`, `u`) reopen, a new `a` ends the open one and an `a` without `href` links nothing; other tags are not tracked, and `</br>` breaks as `<br>` does; and a footnote reference is its number in superscript, numbered by first reference, as its definition is; one `~` still strikes. At the root, blocks read as GitHub shows them: a quote whose first line is plain text reading `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]` or `[!CAUTION]` in any case, after escapes and references, spaces and tabs around it, with more after it, is an alert under its title (Important in the palette's violet, `syntax.keyword`, read at 4.5:1); a paragraph that is `[toc]` alone, as Typora reads it, lists every heading by depth, and a press on one reveals it as a link press does (`MarkdownDocument`'s `reveal`), the heading found where `CodeEditor` anchors carried it through each edit since the read, gone if an edit or an undo took it, and a secondary press acts on press, since a block writing back moves the list; front matter shows in a box without its fences; an HTML block shows its text with the inline tags above, white space collapsed and owed as a browser lays it, block elements breaking lines and closing what opened inside them, HTML5's start tags ending an open `p`, `fieldset` and `menu` too, and an `li` start an `li`, a `dd` or `dt` start either, as HTML5 ends them, comments dropped, the filtered tags shown as text, and an unclosed tag dropping the rest; a rule is a 2px `border_strong` line; bullets go •, ◦, ▪ by list depth, ordered lists counted. Exports read a tree as the drawing does through `MarkdownReader` (`block` gives its `MarkdownBlock`s, `inline` its text, `note` and `label` its footnotes, `headings` the `[toc]` list), one fresh stack per paragraph, heading or cell. `MarkdownDocument` holds a whole file in one `CodeEditor` (text and undo, colored by `CodeEditor::highlighter` with `markdown_highlights`): visual draws the file's top-level blocks in a gpui `list`, a pressed block opening as its source in a `TextInput` whose typing is the document's text at once and whose write-back is one undo step; a press in a table opens that cell alone, in place, as its source in a one-line field (a press on its text at the pressed character, past it at its end; a table block opened by its edge, the keyboard or a step from a neighbor opens its first or last cell), Tab and Shift-Tab going to the next and previous cell, Up and Down to the cell above and below or out of the table, Enter and Escape writing it back, a bare `|` typed in it escaped as the cell keeps it, a trailing backslash doubled only when a pipe follows at once, an emptied edge cell of a row without that outer pipe written as one so its column stays, the caret read through the same escaping, a padding press on the open cell keeping its typing, and a table edit writing it back before it runs; source is the editor; preview draws without presses. A written-back block, a mode switch or a new text is read at once up to 128 KiB, longer ones on the background executor; typing in source or split reads 300 ms after the last key, off the main thread (markdown-rs grows with the square of its nodes, not its bytes: in release, 128 KiB of one-letter paragraLine truncated - 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. - 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.
- Math and diagrams: RaTeX 0.1.14 (`ratex-parser`, `ratex-layout`, `ratex-svg` with `embed-fonts`, `ratex-types`; MIT, KaTeX's fonts OFL) typesets `Latex` to SVG with outlined glyphs; `mermaid-rs-renderer` 0.3.1 (MIT, default features off) draws `Diagram`. Both go through gpui's `SvgRenderer` at the window's scale, loaded as gpui assets off the main thread; `usvg` 0.46, gpui's own, reads a diagram's size. A RaTeX or Mermaid panic on parsed input becomes the element's error. Each element holds its picture through `primitives::held`: a new source, or the element leaving the window, removes the old asset from gpui's cache and drops its image from the atlas after the frame.
- 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. - 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.
- Web: on wasm32, `web-time` 1.1 (MIT OR Apache-2.0) gives the `Instant` gpui's executor returns, std's own natively; jiff takes `js`, rust-embed `debug-embed`, similar `wasm32_web_time`. The gallery's web entry takes `gpui_web`, `wasm-bindgen`, `js-sys` and `web-sys` as wasm-only dev-dependencies, and a Hebrew cut of Noto Sans Hebrew 2.003 (OFL, `examples/gallery/fonts`): gpui_web asks the browser for emoji and CJK alone. The `wasm-bindgen` CLI at Cargo.lock's version and binaryen's `wasm-opt` finish the bundle. - Web: on wasm32, `web-time` 1.1 (MIT OR Apache-2.0) gives the `Instant` gpui's executor returns, std's own natively; jiff takes `js`, rust-embed `debug-embed`, similar `wasm32_web_time`. The gallery's web entry takes `gpui_web`, `wasm-bindgen`, `js-sys` and `web-sys` as wasm-only dev-dependencies, and a Hebrew cut of Noto Sans Hebrew 2.003 (OFL, `examples/gallery/fonts`): gpui_web asks the browser for emoji and CJK alone. The `wasm-bindgen` CLI at Cargo.lock's version and binaryen's `wasm-opt` finish the bundle.
- Gallery: `examples/gallery`. Website: `frontend/` (Vite 8.3, pnpm 11, TypeScript, React 19.3, TanStack Router 1.170 in code-based routes, Lenis 1.3), in serro.ai's language: heavy titles over thin faint text, hairlines, one accent, full-screen sections, isometric line miniatures on a canvas. Pages name a story by its title's first part (`src/data.ts`'s `name`); scroll bars stay hidden; layouts step at 1024 and 768. Each story runs live from the gallery built for wasm32 through gpui_web, in a same-origin iframe, or, where a browser cannot run it, as a native capture. The Docs tab is Markdown in `frontend/src/docs/`, read in its `ORDER` and rendered by `marked` 18 (MIT) inside `vite.config.ts`'s `docs` plugin, so a bad guide fails `pnpm build` and the browser gets HTML alone; a fenced block tagged `example=NAME` shows `examples/docs/NAME.rs`, an example `scripts/check.sh` compiles as `docs_NAME`, so every full Rust sample on the site builds. The Showcase tab lists apps built with Ely from the public repo `ZacharyZhang-NY/Ely-GPUI-Showcases` on git.zacharyzhang.com (Gitea): manifests, PNG previews and Markdown READMEs, checked by its `scripts/catalog.mjs` (`marked`, `pngjs`, `twitter-text`), its stars refreshed by its CI. The mark is the letter E in iso blocks, its middle arm in the accent: one geometry in `src/iso/mark.ts`, drawn by the engine the miniatures use, worn by the header in the page's iso colors and by the favicon. Below 768px the header shows the mark without its word, which stays the home link's name; below 414px it drops the sound switch. Geist and Geist Mono (OFL, `@fontsource-variable`), Reicon icons (MIT, based on Solar Icons, CC BY 4.0, credited in the footer), simple-icons' Gitea mark (CC0). It deploys to Cloudflare Workers static assets through `npx wrangler` (4.143). - Gallery: `examples/gallery`. Website: `frontend/` (Vite 8.3, pnpm 11, TypeScript, React 19.3, TanStack Router 1.170 in code-based routes, Lenis 1.3), in serro.ai's language: heavy titles over thin faint text, hairlines, one accent, full-screen sections, isometric line miniatures on a canvas. Pages name a story by its title's first part (`src/data.ts`'s `name`); scroll bars stay hidden; layouts step at 1024 and 768. Each story runs live from the gallery built for wasm32 through gpui_web, in a same-origin iframe, or, where a browser cannot run it, as a native capture. The Docs tab is Markdown in `frontend/src/docs/`, read in its `ORDER` and rendered by `marked` 18 (MIT) inside `vite.config.ts`'s `docs` plugin, so a bad guide fails `pnpm build` and the browser gets HTML alone; a fenced block tagged `example=NAME` shows `examples/docs/NAME.rs`, an example `scripts/check.sh` compiles as `docs_NAME`, so every full Rust sample on the site builds. The Showcase tab lists apps built with Ely from the public repo `ZacharyZhang-NY/Ely-GPUI-Showcases`: manifests, PNG previews and Markdown READMEs, checked by its `scripts/catalog.mjs` (`marked`, `pngjs`, `twitter-text`), its stars refreshed by its CI. The mark is the letter E in iso blocks, its middle arm in the accent: one geometry in `src/iso/mark.ts`, drawn by the engine the miniatures use, worn by the header in the page's iso colors and by the favicon. Below 768px the header shows the mark without its word, which stays the home link's name; below 414px it drops the sound switch. Geist and Geist Mono (OFL, `@fontsource-variable`), Reicon icons (MIT, based on Solar Icons, CC BY 4.0, credited in the footer), simple-icons' GitHub mark (CC0). It deploys to Cloudflare Workers static assets through `npx wrangler` (4.143).
## Commands ## Commands
@@ -30,8 +29,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- In `frontend/`: `ELY_GALLERY_ASSETS=<site>/gallery/assets/ pnpm gallery` builds the gallery into `public/gallery`, `pnpm dev` serves the site, `pnpm build` checks types and writes `dist`, `pnpm deploy` runs `wrangler deploy`, `pnpm showcase` writes the showcase catalog into `src/showcase.json` and `public/showcase` (both ignored): it downloads the commit `showcase.lock` pins, installs that commit's packages and runs its own validator, so a bad catalog fails the build; `dev` and `build` run it first, and `ELY_SHOWCASE=<checkout>` reads a local catalog instead. `pnpm test` runs the iso engine's, the mark's and the showcase list's tests, and `pnpm local` builds the gallery for localhost, builds the site and serves both on http://localhost:4173/ (wasm-bindgen and wasm-opt on PATH, as `scripts/web.sh` says). `node scripts/og.mjs`, with `pnpm preview` up, renders `public/og.png` from the hero and the header's mark. `node scripts/favicon.mjs` writes `public/favicon.svg`, which follows the system's mode, `public/apple-touch-icon.png` and the repo's `logo.png` (2048px, transparent) from the mark, in the colors `styles.css` sets. The site reads `examples/gallery/stories.json` for its chapters, stories and search. - In `frontend/`: `ELY_GALLERY_ASSETS=<site>/gallery/assets/ pnpm gallery` builds the gallery into `public/gallery`, `pnpm dev` serves the site, `pnpm build` checks types and writes `dist`, `pnpm deploy` runs `wrangler deploy`, `pnpm showcase` writes the showcase catalog into `src/showcase.json` and `public/showcase` (both ignored): it downloads the commit `showcase.lock` pins, installs that commit's packages and runs its own validator, so a bad catalog fails the build; `dev` and `build` run it first, and `ELY_SHOWCASE=<checkout>` reads a local catalog instead. `pnpm test` runs the iso engine's, the mark's and the showcase list's tests, and `pnpm local` builds the gallery for localhost, builds the site and serves both on http://localhost:4173/ (wasm-bindgen and wasm-opt on PATH, as `scripts/web.sh` says). `node scripts/og.mjs`, with `pnpm preview` up, renders `public/og.png` from the hero and the header's mark. `node scripts/favicon.mjs` writes `public/favicon.svg`, which follows the system's mode, `public/apple-touch-icon.png` and the repo's `logo.png` (2048px, transparent) from the mark, in the colors `styles.css` sets. The site reads `examples/gallery/stories.json` for its chapters, stories and search.
- `python3 scripts/stories.py` writes `examples/gallery/stories.json`, every page's sections in drawing order with their components, for the site: the names in a title, and in its `SHOWN` table what a story draws beyond them. It fails on a section opened inside another and on a ticked entry with a home of its own that no story names. `scripts/check.sh` refuses a stale one. - `python3 scripts/stories.py` writes `examples/gallery/stories.json`, every page's sections in drawing order with their components, for the site: the names in a title, and in its `SHOWN` table what a story draws beyond them. It fails on a section opened inside another and on a ticked entry with a home of its own that no story names. `scripts/check.sh` refuses a stale one.
- `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 and lints the gallery for wasm32. - `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 and lints the gallery for wasm32.
- `cargo test --manifest-path compat/kit/Cargo.toml --target-dir target/kit` runs Ely beside GPUI Kit in one window: Tab through both, each library's dialog over the other's fields, one asset source. `scripts/check.sh` first resolves it afresh and holds `gpui-kit` to the newest release `cargo search` names, so a Kit release that moves its gpui pin fails resolution there, naming `gpui-pre`; Ely's pin then follows. Its lockfile stays out of git. - `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.
- `rm -rf target/debug/incremental target/kit/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`. - `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. - `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.
@@ -40,24 +38,22 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- `src/<chapter>/`: one folder per chapter of `gpui-components.md`. - `src/<chapter>/`: one folder per chapter of `gpui-components.md`.
- `src/theme`: palettes, tokens, `ActiveTheme`. `src/motion`: durations, easings, spring. - `src/theme`: palettes, tokens, `ActiveTheme`. `src/motion`: durations, easings, spring.
- `examples/gallery/pages/<chapter>.rs`: one page per chapter, in PRD order. - `examples/gallery/pages/<chapter>.rs`: one page per chapter, in PRD order.
- `compat/kit`: a crate of its own, with its own workspace, that tests Ely beside GPUI Kit.
- `TASKS.md` and `tasks/`: progress. Every PRD entry has a line and a tag; `tasks/reviews.md` keeps every review round. - `TASKS.md` and `tasks/`: progress. Every PRD entry has a line and a tag; `tasks/reviews.md` keeps every review round.
## Rules ## Rules
- Colors, control and icon sizes, text sizes, radii, shadows come from `cx.theme()`. No raw `px()` literals inside components. - 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. - 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. gpui paints a border no wider than half its box, so a line as thin as its box is a fill, `w_px` or `h_px` with `bg`, as the resize handles draw theirs. - Borders use gpui's fixed-pixel helpers (`border_1` is 1px). Hairlines do not scale.
- One component, one home. Duplicates point to it. - One component, one home. Duplicates point to it.
- Fail fast. No silent fallbacks. Log state changes with `log`. - Fail fast. No silent fallbacks. Log state changes with `log`.
- A handler a component needs is an argument of `new`, so leaving it out fails to compile. A state live data or the outside world can reach, a key gone from a list that changed or a failed load, logs an error naming the owner and draws a visible state, nothing chosen or an error line, and the app runs on. An assert on a caller's argument or on a fact true by construction stays; one that runtime data or input can trip is fixed. Chapters move to this as `tasks/panics.md` ticks them.
- Files stay at or under 500 lines. Comments are one short line, and rare. - 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. - 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. gpui hands an easing's value to the whole animator unclamped, so `motion::spring` goes inside the animator, on only what may overshoot (`forms::Switch` springs its thumb's travel, not its colors). - `motion::duration()` honors reduced motion. Springs overshoot. gpui hands an easing's value to the whole animator unclamped, so `motion::spring` goes inside the animator, on only what may overshoot (`forms::Switch` springs its thumb's travel, not its colors).
- `svg()` paints only with its own `text_color`. `Icon` always sets one. - `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. - 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. - 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` under `ElyFocus`, the key context every `FocusScope` sets. gpui ranks a binding with no context at the deepest level, so a host's Tab in a deeper context, GPUI Kit's code editor among them, lost or won by which `init` ran last; under a context, the deeper one wins. 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. - `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.
- The gallery scripts mouse input by posting `NSEvent`s to its own queue, so a press takes AppKit's path and reaches a native view over the window (`misc::WebView`). `Window::dispatch_event` reaches only gpui's listeners. gpui's normal window takes mouse moves from a tracking area, which only a real cursor feeds, so the harness sets the window's `acceptsMouseMovedEvents` for posted moves. - The gallery scripts mouse input by posting `NSEvent`s to its own queue, so a press takes AppKit's path and reaches a native view over the window (`misc::WebView`). `Window::dispatch_event` reaches only gpui's listeners. gpui's normal window takes mouse moves from a tracking area, which only a real cursor feeds, so the harness sets the window's `acceptsMouseMovedEvents` for posted moves.
- gpui starts a drag on the first move past its threshold, and only the moves after it reach `on_drag_move`. gpui 0.2.2's mac window replayed a held drag every 16 ms; gpui_macos replays only while `pressedMouseButtons` shows a real button, which posted events never set. So `Step::DragTo` posts each move twice, as the replay did: the second post of the first step reaches `on_drag_move`, where a component that anchors on its first drag move sets its anchor (Scrollbar, Knob, Split). - gpui starts a drag on the first move past its threshold, and only the moves after it reach `on_drag_move`. gpui 0.2.2's mac window replayed a held drag every 16 ms; gpui_macos replays only while `pressedMouseButtons` shows a real button, which posted events never set. So `Step::DragTo` posts each move twice, as the replay did: the second post of the first step reaches `on_drag_move`, where a component that anchors on its first drag move sets its anchor (Scrollbar, Knob, Split).
- Every shot of a gpui window, a page's or a script's, waits three of its frames through `on_next_frame`: gpui presents without waiting on the GPU, and a heavy page's draw outlasts a fixed pause, so a shot could show the frame before the last step. A page's pauses stay, for work off the main thread such as pictures decoding. A covered window draws no frames, so its capture fails after 5 s instead of shooting a stale frame. - Every shot of a gpui window, a page's or a script's, waits three of its frames through `on_next_frame`: gpui presents without waiting on the GPU, and a heavy page's draw outlasts a fixed pause, so a shot could show the frame before the last step. A page's pauses stay, for work off the main thread such as pictures decoding. A covered window draws no frames, so its capture fails after 5 s instead of shooting a stale frame.
@@ -66,12 +62,11 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- 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. gpui takes a deferred draw inside another but paints them all by priority alone, so a tooltip (0) deferred inside a fullscreen dialog (1) would draw under it. 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. - 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. gpui takes a deferred draw inside another but paints them all by priority alone, so a tooltip (0) deferred inside a fullscreen dialog (1) would draw under it. 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.
- gpui hides hover after a key press until the mouse moves. A hover that holds or shows something (the toast stack, a carousel, tooltips, hover cards, charts, scroll activity) sets `HoverListenerMode::InputModalityIndependent`, so typing under a resting pointer keeps it. A hover preview the keyboard also moves keeps the default on its items; the box that clears it on leave listens independently, so a real exit still reports, and ignores a leave that follows a key through `last_input_was_keyboard` (`forms::Calendar`). - gpui hides hover after a key press until the mouse moves. A hover that holds or shows something (the toast stack, a carousel, tooltips, hover cards, charts, scroll activity) sets `HoverListenerMode::InputModalityIndependent`, so typing under a resting pointer keeps it. A hover preview the keyboard also moves keeps the default on its items; the box that clears it on leave listens independently, so a real exit still reports, and ignores a leave that follows a key through `last_input_was_keyboard` (`forms::Calendar`).
- `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. - `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 control inside a `drag_region` occludes it. On Windows gpui answers the system's hit test with the caption wherever a `Drag` hitbox lies under the pointer unoccluded, and Windows then keeps the press: no click reaches the control. `block_mouse_except_scroll` is not enough, since that answer reads every hitbox under the pointer. Window buttons occlude, which lets them answer as close, maximize and minimize, and so do a title bar's slots; a column header's item row occludes, staying the scroll container, and its title is a drag region of its own; the mini window drags by its title alone, so its buttons sit outside the region and its hover still shows the bar.
- 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. - 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 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. gpui's loader turns a photo upright by its EXIF orientation first, so `media::ImageViewer` turns from the upright frame and `media::ImageUpload` measures that frame. An svg turns to any angle through its `Transformation`, so a control that turns shows one (`interaction::Rotatable`). - gpui 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. gpui's loader turns a photo upright by its EXIF orientation first, so `media::ImageViewer` turns from the upright frame and `media::ImageUpload` measures that frame. 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`). - 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. - 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`). A number field keys the owner's value as shown: a new one waits while the field is focused and lands before the blur commits, and an echo of its own commit is no new value (`forms::NumberInput`). A ranged number field holds room for its widest bound through `Input::least`, since a text field has no width of its own. - 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. - 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. - 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. - 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.
@@ -84,12 +79,12 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- `TextInput::bounds_for` reads the last layout. Text changed this frame clamps to the laid-out end until the next paint. - `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 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:`. - The macOS open panel runs out of process too. Escape does not reach it; the harness sends it `cancel:`.
- gpui's `test-support` and `leak-detection` stay behind this crate's `test-support` feature and are never on for the gallery: one records every `debug_selector`'s bounds each frame, the other tracks every entity handle, so a test that leaks one fails (`tests::a_leaked_entity_fails_its_test`). - gpui's `test-support` stays behind this crate's `test-support` feature and is never on for the gallery: it tracks every entity handle for leaks and records every `debug_selector`'s bounds each frame.
- 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. - 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. - 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 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 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. A row of icon buttons (`Menu::buttons`) is one entry: Up and Down reach it, Left and Right cross it, a reader presses a button through Click and hears the marked one, and a mark holds its item by label, so a row that changes under it runs nothing unasked. Panels occlude what lies under them. A submenu that does not fit on the right opens on the left of its row, sliding in from that side. A row's submenu opens once the pointer rests on it 150 ms; while a submenu is open, a row crossed inside the triangle from the last pointer place to the submenu's near corners waits 300 ms and switches only if the pointer still rests there (`menus::aim`); a row outside it marks at once. Keys and presses open at once. - 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 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 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 draws a window's first frame inside `open_window`. State the first frame reads, such as `cx.set_menus`, is set before it opens.
@@ -153,13 +148,12 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- Pixels a host draws at a box's size go through `rendering::RenderSurface`: it asks for one BGRA frame at the box's device pixels in prepaint, draws again only for a new size or revision, and frees the old frame and, on leave, the last one. The revision covers all the frame reads, theme colors too. - Pixels a host draws at a box's size go through `rendering::RenderSurface`: it asks for one BGRA frame at the box's device pixels in prepaint, draws again only for a new size or revision, and frees the old frame and, on leave, the last one. The revision covers all the frame reads, theme colors too.
- Tests type a shortcut as it is bound: a `secondary-` binding is Command on macOS and Control elsewhere, so its test types `secondary-`; `cmd-` is the platform key on every OS and reaches only handlers that read that modifier (`lists::SelectableList`). A Tab count runs through `forms::EyeDropper` only on macOS, where it is enabled. - Tests type a shortcut as it is bound: a `secondary-` binding is Command on macOS and Control elsewhere, so its test types `secondary-`; `cmd-` is the platform key on every OS and reaches only handlers that read that modifier (`lists::SelectableList`). A Tab count runs through `forms::EyeDropper` only on macOS, where it is enabled.
- `img()` keeps loading state, and moves an animated picture's frames, only with an id; the frames hold still while its window is inactive or gpui's own `reduce_motion` is on, which the theme's reduced motion does not set. Content masks are rectangles, but `img()` draws only the part of its picture inside its box and rounds that part with its own radius, so a `Cover` crop keeps its corners. - `img()` keeps loading state, and moves an animated picture's frames, only with an id; the frames hold still while its window is inactive or gpui's own `reduce_motion` is on, which the theme's reduced motion does not set. Content masks are rectangles, but `img()` draws only the part of its picture inside its box and rounds that part with its own radius, so a `Cover` crop keeps its corners.
- An app has one asset source: `Assets`, or `Assets::before(other)` when it has files of its own or runs another library such as GPUI Kit; Ely's paths load from Ely, the rest from `other`. `init` loads every file Ely bundles through the app's source and fails naming the first it cannot, since gpui's svg renderer reads IBM Plex there too.
- gpui reads a `&str` picture source with a scheme as a web address, and any other as an embedded asset. A file goes through `Path` (`documents::source` in the crate, `ui::picture` in the gallery). - gpui reads a `&str` picture source with a scheme as a web address, and any other as an embedded asset. A file goes through `Path` (`documents::source` in the crate, `ui::picture` in the gallery).
- Taffy sizes a flex column by the height a ratio box has at its set width, before its cap, so chat's `shaped` pictures sit in a block inside such a column (`chat::ImageMessage`, `VideoMessage`, `ImageGrid`). - Taffy sizes a flex column by the height a ratio box has at its set width, before its cap, so chat's `shaped` pictures sit in a block inside such a column (`chat::ImageMessage`, `VideoMessage`, `ImageGrid`).
- Taffy 0.13 collapses a child's vertical margin through a block parent with no padding or border on that side, and its size cache keeps only that block's size, so a box that measures it again leaves the margin out. A component moves a box with a relative offset, or with padding where what follows moves too, not with such a margin: `layout::Drawer` pulls and snaps its panel, and a fullscreen `overlays::Dialog` slides its card, with `top`; `motion::Stagger`, `Transition`, a centered Dialog and the palette's card rise with padding, so the box a scrim centers grows with them; the gallery's inspector sample is its probe's own child. A margin that stops at its parent is safe: a flex or grid item's own, or a child's of a flex item or of a box with padding or a border on that side. A flex wrapper stops the collapse but gives its items a definite height, so percent heights inside start to resolve (the Drawer's `max_h(relative(0.85))`). - Taffy 0.13 collapses a child's vertical margin through a block parent with no padding or border on that side, and its size cache keeps only that block's size, so a box that measures it again leaves the margin out. A component moves a box with a relative offset, or with padding where what follows moves too, not with such a margin: `layout::Drawer` pulls and snaps its panel, and a fullscreen `overlays::Dialog` slides its card, with `top`; `motion::Stagger`, `Transition`, a centered Dialog and the palette's card rise with padding, so the box a scrim centers grows with them; the gallery's inspector sample is its probe's own child. A margin that stops at its parent is safe: a flex or grid item's own, or a child's of a flex item or of a box with padding or a border on that side. A flex wrapper stops the collapse but gives its items a definite height, so percent heights inside start to resolve (the Drawer's `max_h(relative(0.85))`).
- 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`). - 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. - 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 chart value is finite and within ±`charts::LIMIT` (1e30), so sums of values and their pixels stay inside `f32`; the builders refuse others, and a scale works in halves so a domain across the limit maps. - 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. - 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. - 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. - 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.
@@ -179,13 +173,12 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- A component that fills its container takes no width on its root: a block or a flex column stretches it, in a flex row the host gives it `flex_1`, and as a window's root it fills the window, since gpui sets a window root's auto sides to the window's size. A percent width ignores the root's own margins, so they push it past its container, and in a flex row it takes the whole row as its basis and leaves a `flex_1().min_w_0()` neighbor nothing. A root centered under a cap keeps `w_full`, since a flex column stretches no child with an auto side margin, and `mx_auto` needs a width to fill (`layout::Container`). So does a box that keeps a ratio: a block follows its ratio from a stretch, but a flex or grid box only from a set width, and callers may make one flex (`layout::AspectRatio`, and `primitives::framed` under VideoPlayer, CameraPreview, ModelViewer, ParticipantTile and InpaintCanvas). `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 a flex column in a padded block through `layout::tests::narrow_width`, which reads a root by its `debug_selector`. - A component that fills its container takes no width on its root: a block or a flex column stretches it, in a flex row the host gives it `flex_1`, and as a window's root it fills the window, since gpui sets a window root's auto sides to the window's size. A percent width ignores the root's own margins, so they push it past its container, and in a flex row it takes the whole row as its basis and leaves a `flex_1().min_w_0()` neighbor nothing. A root centered under a cap keeps `w_full`, since a flex column stretches no child with an auto side margin, and `mx_auto` needs a width to fill (`layout::Container`). So does a box that keeps a ratio: a block follows its ratio from a stretch, but a flex or grid box only from a set width, and callers may make one flex (`layout::AspectRatio`, and `primitives::framed` under VideoPlayer, CameraPreview, ModelViewer, ParticipantTile and InpaintCanvas). `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 a flex column in a padded block 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 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. - 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.
- A selection across a BlockEditor's fields lives in the editor (`blocks::span`): a press in a field drags into others, Shift-press reaches from the caret's field. The pressed field selects its own part; the editor sets the other end's and washes blocks between. The editor watches the focused end through `TextInput::edits`, which counts every edit, text changed or not: an edit there, once an input method commits, is rebuilt from the last snapshot as head, typed and tail in one undo step; a caret move or lost focus lets the span go, since gpui runs bindings before key listeners. Backspace, Delete, Cut and Enter take the span before the field acts; copy writes it as Markdown.
- gpui calls focus listeners only while the window is active. A test that needs focus events activates its window with `window.activate_window()`. - 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 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. - A shell in the gallery starts without startup files and with a fixed prompt, so captures show no user or host.
- gpui has no `(ElementId, usize)` id. A child id names its index: `(self.id.clone(), format!("row-{ix}"))`. - gpui 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. - 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. So does `documents::BlockEditor`, which scrolls in a `ScrollArea` through its own handle (`ScrollArea::track_scroll`) and brings the caret into view each time it moves, not each frame, so a wheel moves past a still caret. A fixed-width column beside a `flex_1` one takes `flex_none`, or it shrinks. - 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. - 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 clears `debug_bounds` each frame: a test sees an element leave as well as appear. - gpui clears `debug_bounds` each frame: a test sees an element leave as well as appear.
- Time reads `web_time::Instant`: std's compiles for wasm32 and panics in the browser. `scripts/check.sh` refuses std's in `src` and the gallery. - Time reads `web_time::Instant`: std's compiles for wasm32 and panics in the browser. `scripts/check.sh` refuses std's in `src` and the gallery.
@@ -196,7 +189,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- The gallery's pictures go through `asset!` and `ui::picture` or `ui::resource`: a file natively, on the web an address under `ELY_GALLERY_ASSETS`, since gpui reads no files there. - The gallery's pictures go through `asset!` and `ui::picture` or `ui::resource`: a file natively, on the web an address under `ELY_GALLERY_ASSETS`, since gpui reads no files there.
- On the web a paste reaches the focused field as the browser's paste event, through `EntityInputHandler::paste`, so Paste stays unbound there (`TextInput`, `CodeEditor`). Code that reads the clipboard itself awaits `read_from_clipboard_async`: gpui_web's `read_from_clipboard` answers at once and finds nothing (`tables::DataGrid`'s Cmd-V). - On the web a paste reaches the focused field as the browser's paste event, through `EntityInputHandler::paste`, so Paste stays unbound there (`TextInput`, `CodeEditor`). Code that reads the clipboard itself awaits `read_from_clipboard_async`: gpui_web's `read_from_clipboard` answers at once and finds nothing (`tables::DataGrid`'s Cmd-V).
- The first frame wears the host's mode through `Theme::set_mode_now`, a mode sent before the window opens included; `set_mode` fades from the colors on screen. - The first frame wears the host's mode through `Theme::set_mode_now`, a mode sent before the window opens included; `set_mode` fades from the colors on screen.
- The system time zone comes from `typography::format::system_zone`: an unknown zone logs one error naming the first component that asked and reads as jiff's `TimeZone::unknown()`, which is UTC. A time shown in the system zone goes through `typography::format::datetime`, which adds " UTC" in the unknown zone, so the screen never claims local time. jiff's `TimeZone::system()` falls back to UTC in silence. - 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 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. - 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`). - 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`).
@@ -205,12 +198,12 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- 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. - 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. - 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 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. gpui also hands a wheel to every scroll box under the pointer, so a `ScrollArea` stops a wheel along an axis it has room on and passes one it cannot use, and a floating option list occludes (`forms::options::Popup`). - 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. - 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 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 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`). - 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`; the owner's handler is an argument of `new`. A swatch that opens a picker is `canvas::panel::color_well`. - 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 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 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 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.
@@ -221,13 +214,13 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- `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). - `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). - 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. - 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 window whose sidebar runs to its top is `shell::ColumnShell`: each column's header drags the window; the first leaves the traffic lights room, and drawn controls lead the first header in the macOS manner or end the last. A set width is a flex basis, not a width, so it holds its header's own parts, which a width let run past it, and yields when the shell runs short; the host's header items keep one control's width and scroll when short. A resizable column drags by its border toward the filling column, its right before it and its left after it; the handle draws that border and occludes the header under it, and a drag sets off from the wider of the width drawn and the owner's, since a header's parts may hold a column wider and a short shell squeeze it narrower; a drag that has not moved sideways changes nothing, and one that comes back where it set off gives the width the owner had. - A window whose sidebar runs to its top is `shell::ColumnShell`: each column's header drags the window; the first leaves the traffic lights room, and drawn controls lead the first header in the macOS manner or end the last. A set width yields before a header's own parts; the host's header items keep one control's width and scroll when short.
- Taffy counts a flex item's text in its least width whatever width the item sets. A box that must add none lays its content absolutely inside a filling box (`shell::ColumnShell`'s title and body). - Taffy counts a flex item's text in its least width whatever width the item sets. A box that must add none lays its content absolutely inside a filling box (`shell::ColumnShell`'s title and body).
- 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 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's `MicaBackdrop` and `MicaAltBackdrop` are DWM backdrops from Windows 11 build 22621; older builds get none, in silence, and macOS plain transparency. - 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's `MicaBackdrop` and `MicaAltBackdrop` are DWM backdrops from Windows 11 build 22621; older builds get none, in silence, and macOS plain transparency.
- The chart's hues go by `theme::HUE_NAMES`, for an event's color and a label's alike. - 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`). - 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. Copy a component draws, tooltips and default placeholders too, reads `i18n::text` (app catalog, then the builtin English and Chinese); `i18n/tests/shipped.rs` fails on a fixed English literal in a component Ely ships (labels, tooltips, button and segment text, match arms, capitalized literal arguments); a choice's value stays its key, its label the catalog's (`tables::Test::key`). 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 wraps shaped glyphs in the order they are drawn, on the web as on macOS, 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. - 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 wraps shaped glyphs in the order they are drawn, on the web as on macOS, 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. - 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). - 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 packs pictures in its atlas without padding and samples them with a linear filter, on the web as on macOS, 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`). - gpui packs pictures in its atlas without padding and samples them with a linear filter, on the web as on macOS, 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`).
@@ -249,7 +242,6 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- 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: 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: the gallery photographs its own window with `CGWindowListCreateImage`. No Screen Recording permission needed.
- 2026-09-24: license MIT OR Apache-2.0. - 2026-09-24: license MIT OR Apache-2.0.
- 2026-10-05: license MIT alone, in `LICENSE`, at the owner's word; replaces the 2026-09-24 choice.
- 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: 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: 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: SystemNotification is blocked. An unbundled app has no notification center (probed on macOS 27.2), and gpui 0.2.2 has no API.
@@ -266,13 +258,4 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- 2026-09-29: the ported debug gallery draws its heaviest page about 1.6 times slower than on gpui 0.2.2 (tooling: 9 fps, worst frame 117 ms, against 14 fps and 72 ms), with gpui's accessibility tree on or off (`Application::new_inaccessible` measured the same). Ely's code did not change there; the frame cost is gpui's. - 2026-09-29: the ported debug gallery draws its heaviest page about 1.6 times slower than on gpui 0.2.2 (tooling: 9 fps, worst frame 117 ms, against 14 fps and 72 ms), with gpui's accessibility tree on or off (`Application::new_inaccessible` measured the same). Ely's code did not change there; the frame cost is gpui's.
- 2026-09-29: the web gallery runs on one thread, since a static site sends no cross-origin isolation, and draws through WebGL2 where WebGPU is missing. Its pictures are served beside the wasm, not embedded: the wasm stays under Cloudflare's 25 MiB, and a story fetches only its own. `wasm-opt -Oz` takes the wasm from 23.0 to 20.0 MB; gzip and brotli stay near 7.9 and 5.5 MB. - 2026-09-29: the web gallery runs on one thread, since a static site sends no cross-origin isolation, and draws through WebGL2 where WebGPU is missing. Its pictures are served beside the wasm, not embedded: the wasm stays under Cloudflare's 25 MiB, and a story fetches only its own. `wasm-opt -Oz` takes the wasm from 23.0 to 20.0 MB; gzip and brotli stay near 7.9 and 5.5 MB.
- 2026-10-04: the Showcase follows gpui-kit.com/apps: apps live in their own public repo and arrive by pull request. The site pins a commit of it in `frontend/showcase.lock` and moves only when someone bumps the pin, so stars refresh on the site with each bump. READMEs take Markdown and PNG pictures alone, no HTML or video, so the site needs no sanitizer. The catalog started empty. - 2026-10-04: the Showcase follows gpui-kit.com/apps: apps live in their own public repo and arrive by pull request. The site pins a commit of it in `frontend/showcase.lock` and moves only when someone bumps the pin, so stars refresh on the site with each bump. READMEs take Markdown and PNG pictures alone, no HTML or video, so the site needs no sanitizer. The catalog started empty.
- 2026-10-05: the pin moves by itself: `.github/workflows/showcase.yml` looks at the catalog's main every 30 minutes and, once that commit's checks pass, builds the gallery and the site, deploys through `CLOUDFLARE_API_TOKEN` (secret) and `CLOUDFLARE_ACCOUNT_ID` (variable), then commits the pin; a run with `deploy` set redeploys at the current pin. A catalog commit that fails its checks keeps the pin. The catalog runs its checks on every pull request without approval, save from accounts new to GitHub; they get no secrets and a read-only token.
- 2026-10-09: the repos moved from GitHub to Gitea at git.zacharyzhang.com; GitHub's copies are gone. The site links there, `pnpm showcase` downloads the catalog's archive from there, and `showcase.yml` runs on Gitea Actions, reading the catalog's head and combined commit status through Gitea's API. The catalog's stars still come from GitHub's API, signed in only with its `STARS_GITHUB_TOKEN` secret, since Gitea's own token means nothing there. TokenLedger, merged on GitHub after the copy was taken, was restored from the site's copy.
- 2026-10-04: chapter 44, Rendering, holds `RenderSurface` alone. gpui at 1a28cff paints `RenderImage`s everywhere, `CVPixelBuffer`s (NV12) on macOS's Metal renderer alone, and shares no device or shader hook, so shader, camera and GPU views are the host's, feeding the surface, and zero-copy or Metal, OpenGL, Vulkan and Direct3D surfaces are blocked (`tasks/ch44.md`). CI tests the crate and builds the gallery on Linux, Windows and macOS, arm64 and x86_64. - 2026-10-04: chapter 44, Rendering, holds `RenderSurface` alone. gpui at 1a28cff paints `RenderImage`s everywhere, `CVPixelBuffer`s (NV12) on macOS's Metal renderer alone, and shares no device or shader hook, so shader, camera and GPU views are the host's, feeding the surface, and zero-copy or Metal, OpenGL, Vulkan and Direct3D surfaces are blocked (`tasks/ch44.md`). CI tests the crate and builds the gallery on Linux, Windows and macOS, arm64 and x86_64.
- 2026-10-05: an overlay its owner removes without calling `Close` still hands focus back: `take_focus` records each holder and what it took focus from, `give_back` forgets it, and when the focused element leaves the tree the root scope walks back past every gone holder to the focus before them all, else takes it; nested overlays removed together return to what the outer one took focus from. Before, a dialog whose action set its owner's state to closed left focus on the root, and keys reached nothing until a click.
- 2026-10-05: `Select::label` and `TextInput::label` name the control for assistive technology without drawing anything; a text input with neither label nor placeholder had no name, and a select read only its value, so a field named by a description list beside it was anonymous in the AX tree.
- 2026-10-05: `NumberInput::label` names the field for assistive technology without drawing it, through `TextInput::set_label` on the field's inner input, which lives across frames; a number field named only by a description list beside it was anonymous in the AX tree.
- 2026-10-05: `TemplatePicker`'s list is one Tab stop: up, down, Home and End move the highlight, Enter uses it; rows are list items with name, selection, place and count; its search placeholder, All chip, Use button and empty line read from the builtin catalog in English and Chinese. Before, the rows were reachable by mouse alone and anonymous in the AX tree.
- 2026-10-05: `SelectableList` is a list in the AX tree, named by `label`; each `ListItem` is a list item with its title as name, its description, its selection, and, in a selectable list, its place and count (`ListItem::place`). Before, the rows had no role and were missing from the AX tree.
- 2026-10-05: `NumberInput::blank` shows a placeholder, not a number, as for a mixed selection; Enter or blur on the empty field commits nothing, a typed number commits as usual. Before, a mixed field held a real number the owner had to invent, and Enter sent it. `ScrubInput` moved to `number/scrub.rs`.
- 2026-10-07: gpui comes from crates.io's `gpui-pre` at the exact snapshot GPUI Kit pins, not from Zed's git. Two sources build two gpui crates whose types do not mix, so Ely and Kit could not share a window. The code is still Zed's; the publisher is not. This replaces the 2026-09-28 choice of Zed's git, and the pin moves when Kit's does.
+1 -15
View File
@@ -46,18 +46,4 @@ CI runs the same on every pull request. Fill in the pull request template: what
## License ## License
Ely is MIT. Unless you say otherwise, what you contribute is licensed the same way. Ely is MIT or Apache-2.0, at your option. Unless you say otherwise, what you contribute is licensed the same way.
## Opening a pull request on git.zacharyzhang.com
Forking is disabled on this server. Open pull requests with AGit instead:
1. Register an account at https://git.zacharyzhang.com
2. Clone, commit your changes, then push to a `refs/for/<target-branch>` ref:
```bash
git clone https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git
git push origin HEAD:refs/for/main -o topic=<short-name> -o title="<PR title>" -o description="<PR body>"
```
3. To update the pull request, push again with the same `topic`.
Generated
+227 -591
View File
File diff suppressed because it is too large. Load diff
+6 -15
View File
@@ -3,38 +3,31 @@ name = "ely-gpui-component"
version = "0.1.1" version = "0.1.1"
edition = "2024" edition = "2024"
rust-version = "1.95" rust-version = "1.95"
license = "MIT" license = "MIT OR Apache-2.0"
description = "A restrained component library for GPUI, in light and dark." description = "A restrained component library for GPUI, in light and dark."
repository = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components" repository = "https://github.com/ZacharyZhang-NY/Ely-GPUI-Components"
publish = false publish = false
[features] [features]
default = ["runtime_shaders"] default = ["runtime_shaders"]
runtime_shaders = ["gpui_platform/runtime_shaders"] runtime_shaders = ["gpui_platform/runtime_shaders"]
test-support = ["gpui/test-support", "gpui/leak-detection"] test-support = ["gpui/test-support"]
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
base64 = "0.22"
barcoders = { version = "2", default-features = false, features = ["std"] } barcoders = { version = "2", default-features = false, features = ["std"] }
emojis = "0.9" emojis = "0.9"
futures = "0.3" futures = "0.3"
gpui = { package = "gpui-pre", version = "=0.3.8", default-features = false, features = ["stacker"] } gpui = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b", default-features = false, features = ["stacker"] }
gpui_platform = { package = "gpui-pre-platform", version = "=0.3.8", features = ["font-kit"] } gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b", features = ["font-kit"] }
image = { version = "0.25", default-features = false } image = { version = "0.25", default-features = false }
iso_currency = { version = "0.7", features = ["iterator"] } iso_currency = { version = "0.7", features = ["iterator"] }
isocountry = "0.3" isocountry = "0.3"
isolang = { version = "2.4", features = ["local_names", "list_languages"] } isolang = { version = "2.4", features = ["local_names", "list_languages"] }
jiff = "0.2" jiff = "0.2"
log = "0.4" log = "0.4"
markdown = "=1.0.0"
mermaid-rs-renderer = { version = "=0.3.1", default-features = false }
pulldown-cmark = { version = "0.13", default-features = false } pulldown-cmark = { version = "0.13", default-features = false }
qrcode = { version = "0.14", default-features = false } qrcode = { version = "0.14", default-features = false }
ratex-layout = "=0.1.14"
ratex-parser = "=0.1.14"
ratex-svg = { version = "=0.1.14", features = ["embed-fonts"] }
ratex-types = "=0.1.14"
regex = "1" regex = "1"
rqrr = { version = "0.11", default-features = false } rqrr = { version = "0.11", default-features = false }
rust-embed = "8.12" rust-embed = "8.12"
@@ -43,8 +36,6 @@ serde_json = { version = "1", features = ["preserve_order"] }
similar = { version = "3.2", default-features = false, features = ["std", "text", "inline", "unicode"] } similar = { version = "3.2", default-features = false, features = ["std", "text", "inline", "unicode"] }
smallvec = "1" smallvec = "1"
unicode-segmentation = "1" unicode-segmentation = "1"
unicode-width = "0.2"
usvg = "0.46"
vte = { version = "0.15", default-features = false, features = ["std", "ansi"] } vte = { version = "0.15", default-features = false, features = ["std", "ansi"] }
web-time = "1.1" web-time = "1.1"
@@ -70,7 +61,7 @@ rustix = { version = "1", default-features = false, features = ["std", "fs"] }
env_logger = "0.11" env_logger = "0.11"
[target.'cfg(target_family = "wasm")'.dev-dependencies] [target.'cfg(target_family = "wasm")'.dev-dependencies]
gpui_web = { package = "gpui-pre-web", version = "=0.3.8" } gpui_web = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b" }
js-sys = "0.3" js-sys = "0.3"
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["Location", "UrlSearchParams", "Window"] } web-sys = { version = "0.3", features = ["Location", "UrlSearchParams", "Window"] }
+176
View File
@@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
View File
File renamed without changes.
+6 -10
View File
@@ -11,9 +11,9 @@ Status: early. `TASKS.md` tracks every component, chapter by chapter. To help, s
```toml ```toml
[dependencies] [dependencies]
ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components" } ely-gpui-component = { git = "https://github.com/ZacharyZhang-NY/Ely-GPUI-Components" }
gpui = { package = "gpui-pre", version = "=0.3.8" } gpui = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b" }
gpui_platform = { package = "gpui-pre-platform", version = "=0.3.8", features = ["font-kit"] } gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b", features = ["font-kit"] }
``` ```
```rust ```rust
@@ -22,17 +22,13 @@ use gpui::App;
fn main() { fn main() {
gpui_platform::application().with_assets(Assets).run(|cx: &mut App| { gpui_platform::application().with_assets(Assets).run(|cx: &mut App| {
ely_gpui_component::init(cx).expect("Ely failed to start"); ely_gpui_component::init(cx);
Theme::set_mode(Mode::Dark, cx); Theme::set_mode(Mode::Dark, cx);
}); });
} }
``` ```
`init` registers the fonts and the theme. It returns an error if the application's asset source cannot load one of Ely's files. `init` registers the fonts and the theme. It panics if `Assets` is not wired into the application.
## With GPUI Kit
Ely runs beside Longbridge's [GPUI Kit](https://gpui-kit.com) in one window, on the GPUI snapshot Kit 0.7.1 pins. Pass `Assets::before(gpui_kit::assets::Assets)` as the one asset source, call both `init`s, open the window with `gpui_kit::open_window`, and wrap the view in Ely's root `FocusScope`. The [guide](https://elygpui.com/docs/gpui-kit) covers themes, focus and the inspector; `compat/kit` holds a whole app and the tests behind each claim.
## Gallery ## Gallery
@@ -65,5 +61,5 @@ pnpm deploy # Cloudflare, through wrangler
## License ## License
MIT. Lucide icons: ISC. Inter, JetBrains Mono, IBM Plex Sans and the gallery's Noto Sans Hebrew: SIL Open Font License 1.1. MIT or Apache-2.0, at your option. Lucide icons: ISC. Inter, JetBrains Mono, IBM Plex Sans and the gallery's Noto Sans Hebrew: SIL Open Font License 1.1.
The gallery photos in `examples/gallery/assets` were generated for this project with gpt-image-2. The gallery photos in `examples/gallery/assets` were generated for this project with gpt-image-2.
-2
View File
@@ -187,8 +187,6 @@ Per-component lines live in `tasks/`. Tags there:
- [x] T50g Rendering · Acceptance: the checklist, item by item - [x] T50g Rendering · Acceptance: the checklist, item by item
- [x] T51 Showcase · an apps page fed by a public repo — `tasks/showcase.md` - [x] T51 Showcase · an apps page fed by a public repo — `tasks/showcase.md`
- [x] T52 Logo · an isometric mark for the header, favicon and OG image — `tasks/logo.md` - [x] T52 Logo · an isometric mark for the header, favicon and OG image — `tasks/logo.md`
- [x] T53 Panics · handlers move into `new`, live states log and show, asserts audited — `tasks/panics.md`
- [x] T54 GPUI Kit · Ely and Kit share one app and one window — `tasks/kit.md`
## T00 detail ## T00 detail
-17
View File
@@ -1,17 +0,0 @@
<!-- @license lucide-static v1.48.0 - ISC -->
<svg
class="lucide lucide-message-square-warning"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z" />
<path d="M12 15h.01" />
<path d="M12 7v4" />
</svg>

Before

Width:  |  Height:  |  Size: 481 B

-16
View File
@@ -1,16 +0,0 @@
[package]
name = "ely-kit-compat"
version = "0.0.0"
edition = "2024"
rust-version = "1.95"
publish = false
description = "Ely and the newest GPUI Kit in one app and one window."
[lib]
path = "src/lib.rs"
[dev-dependencies]
ely-gpui-component = { path = "../..", features = ["test-support"] }
gpui-kit = { version = ">=0.7.1", features = ["test-support"] }
[workspace]
-89
View File
@@ -1,89 +0,0 @@
use ely_gpui_component::{
Assets,
buttons::Button,
primitives::FocusScope,
theme::{ActiveTheme, Mode, Theme},
typography::{Heading, Paragraph},
};
use gpui_kit::{
App, AppContext, Context, Entity, FocusHandle, IntoElement, ParentElement, Render, Styled,
Window, WindowOptions,
component::{
WindowExt,
button::Button as KitButton,
input::{Input, InputState},
theme::{Theme as KitTheme, ThemeMode},
},
div, px,
};
struct Start {
focus: FocusHandle,
name: Entity<InputState>,
}
impl Render for Start {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme();
let dark = theme.is_dark();
FocusScope::new(&self.focus)
.root()
.size_full()
.p_8()
.bg(theme.colors.bg)
.child(column(dark, &self.name))
}
}
fn column(dark: bool, name: &Entity<InputState>) -> impl IntoElement {
div()
.w(px(480.))
.flex()
.flex_col()
.gap_4()
.child(Heading::h1("Ely and GPUI Kit"))
.child(Paragraph::new(
"One window, one focus order, one asset source.",
))
.child(Input::new(name))
.child(
KitButton::new("kit-dialog")
.label("Kit dialog")
.on_click(|_, window, cx| {
window.open_dialog(cx, |dialog, _, _| dialog.title("From GPUI Kit"));
}),
)
.child(
Button::new("mode", if dark { "Light mode" } else { "Dark mode" }).on_click(
move |_, window, cx| {
let (ely, kit) = if dark {
(Mode::Light, ThemeMode::Light)
} else {
(Mode::Dark, ThemeMode::Dark)
};
Theme::set_mode(ely, cx);
KitTheme::change(kit, Some(window), cx);
},
),
)
}
fn main() {
gpui_kit::application()
.with_assets(Assets::before(gpui_kit::assets::Assets))
.run(|cx: &mut App| {
gpui_kit::init(cx);
ely_gpui_component::init(cx).expect("Ely failed to start");
gpui_kit::open_window(WindowOptions::default(), cx, |window, cx| {
cx.new(|cx| {
let focus = cx.focus_handle();
window.focus(&focus, cx);
Start {
focus,
name: cx.new(|cx| InputState::new(window, cx).placeholder("Name")),
}
})
})
.expect("window failed to open");
});
}
-26
View File
@@ -1,26 +0,0 @@
use ely_gpui_component::Assets;
use gpui_kit::AssetSource;
#[test]
fn one_source_serves_both_libraries() {
let kit = gpui_kit::assets::Assets;
let both = Assets::before(gpui_kit::assets::Assets);
let ely = Assets.list("").unwrap();
assert!(ely.len() > 300, "Ely bundles {} files", ely.len());
for path in &ely {
let served = both.load(path).unwrap().expect("Ely's file loads");
assert_eq!(
served,
Assets.load(path).unwrap().unwrap(),
"{path} is Ely's"
);
}
let icons = kit.list("icons/").unwrap();
assert!(!icons.is_empty(), "Kit lists its icons");
for path in &icons {
let served = both.load(path).unwrap().expect("Kit's icon loads");
if Assets.load(path).unwrap().is_none() {
assert_eq!(served, kit.load(path).unwrap().unwrap(), "{path} is Kit's");
}
}
}
-112
View File
@@ -1,112 +0,0 @@
use ely_gpui_component::{buttons::Button, overlays::Dialog, primitives::FocusScope};
use gpui_kit::{
AppContext, Bounds, Context, Entity, FocusHandle, IntoElement, KeyUpEvent, Keystroke,
ParentElement, Render, Styled, TestAppContext, VisualTestContext, Window, WindowBounds,
WindowOptions,
component::{
WindowExt,
input::{Editor, EditorState, Input, InputState},
},
div, point,
prelude::*,
px, size,
};
pub const CODE: &str = "fn main() {}";
/// Ely's root scope inside Kit's `Root`.
pub struct Desk {
pub root: FocusHandle,
pub open: FocusHandle,
pub line: Entity<InputState>,
pub code: Entity<EditorState>,
pub ely_dialog: bool,
}
impl Render for Desk {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
FocusScope::new(&self.root)
.root()
.size_full()
.p_4()
.flex()
.flex_col()
.gap_4()
.child(
div().debug_selector(|| "ely-open".into()).child(
Button::new("ely-open", "Open")
.focus_handle(&self.open)
.on_click(|_, window, cx| {
window.open_dialog(cx, |dialog, _, _| dialog.title("Kit dialog"));
}),
),
)
.child(Input::new(&self.line))
.child(div().h(px(120.)).child(Editor::new(&self.code).size_full()))
.when(self.ely_dialog, |desk| {
desk.child(
Dialog::new("ely-dialog", "Ely dialog", move |_, cx| {
view.update(cx, |desk, cx| {
desk.ely_dialog = false;
cx.notify();
})
})
.action(|_| Button::new("ely-first", "First"))
.action(|_| Button::new("ely-second", "Second")),
)
})
}
}
/// Starts both libraries in order; Kit opens the window.
pub fn open(cx: &mut TestAppContext, ely_last: bool) -> (Entity<Desk>, VisualTestContext) {
cx.update(|cx| {
if ely_last {
gpui_kit::init(cx);
ely_gpui_component::init_for_tests(cx);
} else {
ely_gpui_component::init_for_tests(cx);
gpui_kit::init(cx);
}
});
let (window, desk) = cx.update(|cx| {
let options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(Bounds {
origin: point(px(0.), px(0.)),
size: size(px(800.), px(600.)),
})),
..Default::default()
};
gpui_kit::open_window(options, cx, |window, cx| {
cx.new(|cx| Desk {
root: cx.focus_handle(),
open: cx.focus_handle().tab_stop(true),
line: cx.new(|cx| InputState::new(window, cx)),
code: cx.new(|cx| {
EditorState::new(window, cx)
.language("rust")
.default_value(CODE)
}),
ely_dialog: false,
})
})
.expect("Kit opens the window")
});
let cx = VisualTestContext::from_window(window, cx);
cx.run_until_parked();
(desk, cx)
}
/// Whether the handle holds focus.
pub fn focused(cx: &mut VisualTestContext, handle: &FocusHandle) -> bool {
cx.update(|window, _| handle.is_focused(window))
}
/// Key down and up; gpui presses on the release.
pub fn press(cx: &mut VisualTestContext, key: &str) {
cx.simulate_keystrokes(key);
cx.simulate_event(KeyUpEvent {
keystroke: Keystroke::parse(key).expect("a key"),
});
}
-117
View File
@@ -1,117 +0,0 @@
use std::time::Duration;
use gpui_kit::{
FocusHandle, Focusable, Modifiers, TestAppContext, VisualTestContext, test::TestWindowExt,
};
use crate::desk::{focused, open, press};
/// Draws until `done` holds; fails after a second.
fn settle(cx: &mut VisualTestContext, done: impl Fn(&mut VisualTestContext) -> bool) {
for _ in 0..50 {
cx.run_until_parked();
cx.update(|window, cx| window.render_frame(cx));
if done(cx) {
return;
}
std::thread::sleep(Duration::from_millis(20));
}
panic!("the window did not settle");
}
fn kit_dialog_open(cx: &mut VisualTestContext) -> bool {
cx.update(|window, _| window.try_find("dialog").is_some())
}
/// Opens Kit's dialog, tabs inside it, closes it.
fn kit_dialog_round_trip(
cx: &mut TestAppContext,
open_with: impl FnOnce(&mut VisualTestContext, &Handles),
back_to: impl Fn(&Handles) -> FocusHandle,
) {
let (desk, mut cx) = open(cx, true);
let handles = cx.update(|_, cx| {
let desk = desk.read(cx);
Handles {
button: desk.open.clone(),
line: desk.line.read(cx).focus_handle(cx),
code: desk.code.read(cx).focus_handle(cx),
}
});
open_with(&mut cx, &handles);
settle(&mut cx, kit_dialog_open);
for _ in 0..3 {
cx.simulate_keystrokes("tab");
for handle in [&handles.button, &handles.line, &handles.code] {
assert!(!focused(&mut cx, handle), "Tab stays in Kit's dialog");
}
}
cx.simulate_keystrokes("escape");
settle(&mut cx, |cx| !kit_dialog_open(cx));
assert!(
focused(&mut cx, &back_to(&handles)),
"focus returns where it was"
);
}
struct Handles {
button: FocusHandle,
line: FocusHandle,
code: FocusHandle,
}
#[gpui_kit::test]
fn enter_on_an_ely_button_opens_a_kit_dialog_that_hands_focus_back(cx: &mut TestAppContext) {
kit_dialog_round_trip(
cx,
|cx, handles| {
cx.update(|window, cx| window.focus(&handles.button, cx));
press(cx, "enter");
},
|handles| handles.button.clone(),
);
}
#[gpui_kit::test]
fn a_press_on_an_ely_button_leaves_focus_for_the_kit_dialog_to_return(cx: &mut TestAppContext) {
kit_dialog_round_trip(
cx,
|cx, handles| {
cx.update(|window, cx| window.focus(&handles.line, cx));
let bounds = cx.debug_bounds("ely-open").expect("Ely's button drew");
cx.simulate_click(bounds.center(), Modifiers::none());
},
|handles| handles.line.clone(),
);
}
#[gpui_kit::test]
fn an_ely_dialog_over_kit_fields_traps_tab_and_hands_focus_back(cx: &mut TestAppContext) {
let (desk, mut cx) = open(cx, true);
let (button, line, code) = cx.update(|_, cx| {
let desk = desk.read(cx);
(
desk.open.clone(),
desk.line.read(cx).focus_handle(cx),
desk.code.read(cx).focus_handle(cx),
)
});
cx.update(|window, cx| window.focus(&line, cx));
desk.update(&mut cx, |desk, cx| {
desk.ely_dialog = true;
cx.notify();
});
settle(&mut cx, |cx| !focused(cx, &line));
for _ in 0..3 {
cx.simulate_keystrokes("tab");
for handle in [&button, &line, &code] {
assert!(!focused(&mut cx, handle), "Tab stays in Ely's dialog");
}
}
cx.simulate_keystrokes("escape");
settle(&mut cx, |cx| focused(cx, &line));
assert!(
!desk.read_with(&cx, |desk, _| desk.ely_dialog),
"Escape closed it"
);
}
-53
View File
@@ -1,53 +0,0 @@
use gpui_kit::{Focusable, TestAppContext};
use crate::desk::{CODE, focused, open};
fn tab_runs_through_both(cx: &mut TestAppContext, ely_last: bool) {
let (desk, mut cx) = open(cx, ely_last);
let (open_button, line, code) = cx.update(|_, cx| {
let desk = desk.read(cx);
(desk.open.clone(), desk.line.clone(), desk.code.clone())
});
cx.update(|window, cx| window.focus(&open_button, cx));
cx.simulate_keystrokes("tab");
let line_focus = cx.update(|_, cx| line.read(cx).focus_handle(cx));
assert!(
focused(&mut cx, &line_focus),
"Tab leaves Ely's button for Kit's field"
);
cx.simulate_keystrokes("tab");
let code_focus = cx.update(|_, cx| code.read(cx).focus_handle(cx));
assert!(
focused(&mut cx, &code_focus),
"Tab reaches Kit's code editor"
);
cx.simulate_keystrokes("tab");
assert!(
focused(&mut cx, &code_focus),
"Tab stays in the code editor"
);
let text = cx.update(|_, cx| code.read(cx).value().to_string());
assert!(
text.len() > CODE.len() && text.trim() == CODE,
"Tab indents: {text:?}"
);
cx.simulate_keystrokes("shift-tab");
let text = cx.update(|_, cx| code.read(cx).value().to_string());
assert_eq!(text, CODE, "Shift-Tab outdents");
cx.update(|window, cx| window.focus(&line_focus, cx));
cx.simulate_keystrokes("shift-tab");
assert!(
focused(&mut cx, &open_button),
"Shift-Tab returns to Ely's button"
);
}
#[gpui_kit::test]
fn tab_runs_through_both_with_ely_started_last(cx: &mut TestAppContext) {
tab_runs_through_both(cx, true);
}
#[gpui_kit::test]
fn tab_runs_through_both_with_kit_started_last(cx: &mut TestAppContext) {
tab_runs_through_both(cx, false);
}
-10
View File
@@ -1,10 +0,0 @@
//! Ely and GPUI Kit in one app and one window.
#[cfg(test)]
mod assets;
#[cfg(test)]
mod desk;
#[cfg(test)]
mod dialogs;
#[cfg(test)]
mod focus;
+1 -1
View File
@@ -54,7 +54,7 @@ fn main() {
gpui_platform::application() gpui_platform::application()
.with_assets(Assets) .with_assets(Assets)
.run(|cx: &mut App| { .run(|cx: &mut App| {
ely_gpui_component::init(cx).expect("Ely failed to start"); ely_gpui_component::init(cx);
cx.open_window(WindowOptions::default(), |window, cx| { cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|cx| { cx.new(|cx| {
let root = cx.focus_handle(); let root = cx.focus_handle();
+1 -1
View File
@@ -41,7 +41,7 @@ fn main() {
gpui_platform::application() gpui_platform::application()
.with_assets(Assets) .with_assets(Assets)
.run(|cx: &mut App| { .run(|cx: &mut App| {
ely_gpui_component::init(cx).expect("Ely failed to start"); ely_gpui_component::init(cx);
cx.open_window(WindowOptions::default(), |window, cx| { cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|cx| { cx.new(|cx| {
let focus = cx.focus_handle(); let focus = cx.focus_handle();
+1 -1
View File
@@ -53,7 +53,7 @@ fn main() {
gpui_platform::application() gpui_platform::application()
.with_assets(Assets) .with_assets(Assets)
.run(|cx: &mut App| { .run(|cx: &mut App| {
ely_gpui_component::init(cx).expect("Ely failed to start"); ely_gpui_component::init(cx);
cx.set_global( cx.set_global(
I18n::new("en-US") I18n::new("en-US")
.catalog( .catalog(
+1 -1
View File
@@ -87,7 +87,7 @@ fn main() {
gpui_platform::application() gpui_platform::application()
.with_assets(AppAssets) .with_assets(AppAssets)
.run(|cx: &mut App| { .run(|cx: &mut App| {
ely_gpui_component::init(cx).expect("Ely failed to start"); ely_gpui_component::init(cx);
IconTheme::default() IconTheme::default()
.name("Cargo.toml", IconName::Package) .name("Cargo.toml", IconName::Package)
.apply(cx); .apply(cx);
+1 -1
View File
@@ -104,7 +104,7 @@ fn main() {
gpui_platform::application() gpui_platform::application()
.with_assets(Assets) .with_assets(Assets)
.run(|cx: &mut App| { .run(|cx: &mut App| {
ely_gpui_component::init(cx).expect("Ely failed to start"); ely_gpui_component::init(cx);
cx.open_window(WindowOptions::default(), |window, cx| { cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|cx| { cx.new(|cx| {
let focus = cx.focus_handle(); let focus = cx.focus_handle();
+1 -1
View File
@@ -78,7 +78,7 @@ fn main() {
gpui_platform::application() gpui_platform::application()
.with_assets(Assets) .with_assets(Assets)
.run(|cx: &mut App| { .run(|cx: &mut App| {
ely_gpui_component::init(cx).expect("Ely failed to start"); ely_gpui_component::init(cx);
cx.open_window(WindowOptions::default(), |window, cx| { cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|cx| { cx.new(|cx| {
let focus = cx.focus_handle(); let focus = cx.focus_handle();
+1 -1
View File
@@ -56,7 +56,7 @@ fn main() {
gpui_platform::application() gpui_platform::application()
.with_assets(Assets) .with_assets(Assets)
.run(|cx: &mut App| { .run(|cx: &mut App| {
ely_gpui_component::init(cx).expect("Ely failed to start"); ely_gpui_component::init(cx);
cx.open_window(WindowOptions::default(), |window, cx| { cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|cx| { cx.new(|cx| {
let focus = cx.focus_handle(); let focus = cx.focus_handle();
+1 -1
View File
@@ -45,7 +45,7 @@ fn main() {
gpui_platform::application() gpui_platform::application()
.with_assets(Assets) .with_assets(Assets)
.run(|cx: &mut App| { .run(|cx: &mut App| {
ely_gpui_component::init(cx).expect("Ely failed to start"); ely_gpui_component::init(cx);
Theme::set_mode_now(Mode::Dark, cx); Theme::set_mode_now(Mode::Dark, cx);
let bounds = Bounds::centered(None, size(px(640.0), px(400.0)), cx); let bounds = Bounds::centered(None, size(px(640.0), px(400.0)), cx);
let options = WindowOptions { let options = WindowOptions {
+1 -1
View File
@@ -100,7 +100,7 @@ fn main() {
gpui_platform::application() gpui_platform::application()
.with_assets(Assets) .with_assets(Assets)
.run(|cx: &mut App| { .run(|cx: &mut App| {
ely_gpui_component::init(cx).expect("Ely failed to start"); ely_gpui_component::init(cx);
cx.open_window(WindowOptions::default(), |window, cx| { cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|cx| { cx.new(|cx| {
let focus = cx.focus_handle(); let focus = cx.focus_handle();
+6 -13
View File
@@ -41,8 +41,7 @@ mod web;
use std::path::PathBuf; use std::path::PathBuf;
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
use anyhow::bail; use anyhow::{Context as _, Result, bail};
use anyhow::{Context as _, Result};
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
use assets::GalleryAssets; use assets::GalleryAssets;
use ely_gpui_component::theme::{Mode, Theme}; use ely_gpui_component::theme::{Mode, Theme};
@@ -114,8 +113,8 @@ pub struct Start {
} }
/// Sets Ely up and opens the gallery's window. /// Sets Ely up and opens the gallery's window.
fn launch(start: Start, cx: &mut App) -> Result<WindowHandle<shell::Gallery>> { fn launch(start: Start, cx: &mut App) -> WindowHandle<shell::Gallery> {
ely_gpui_component::init(cx)?; ely_gpui_component::init(cx);
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
ely_gpui_component::tooling::install_inspector(cx); ely_gpui_component::tooling::install_inspector(cx);
pages::bind_keys(cx); pages::bind_keys(cx);
@@ -143,10 +142,10 @@ fn launch(start: Start, cx: &mut App) -> Result<WindowHandle<shell::Gallery>> {
.open_window(options, |window, cx| { .open_window(options, |window, cx| {
cx.new(|cx| shell::Gallery::new(start, window, cx)) cx.new(|cx| shell::Gallery::new(start, window, cx))
}) })
.context("gallery window failed to open")?; .expect("gallery window failed to open");
menu_actions(window, cx); menu_actions(window, cx);
cx.activate(true); cx.activate(true);
Ok(window) window
} }
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
@@ -205,13 +204,7 @@ fn main() -> Result<()> {
gpui_platform::application() gpui_platform::application()
.with_assets(GalleryAssets) .with_assets(GalleryAssets)
.run(move |cx: &mut App| { .run(move |cx: &mut App| {
let window = match launch(start, cx) { let window = launch(start, cx);
Ok(window) => window,
Err(error) => {
log::error!("gallery: {error:#}");
return cx.quit();
}
};
if let Some(dir) = capture { if let Some(dir) = capture {
capture::run(window, dir, page, scripted, cx); capture::run(window, dir, page, scripted, cx);
} }
+16 -15
View File
@@ -73,20 +73,23 @@ pub fn switchers(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
.gap_6() .gap_6()
.child( .child(
div().w(px(260.)).flex().flex_col().gap_2().child( div().w(px(260.)).flex().flex_col().gap_2().child(
WorkspaceSwitcher::new("account-workspaces", workspaces, workspace, move |key, _, cx| change(&to_workspace, cx, |places| places.workspace = key.clone())) WorkspaceSwitcher::new("account-workspaces", workspaces, workspace)
.on_select(move |key, _, cx| change(&to_workspace, cx, |places| places.workspace = key.clone()))
.on_create(|_, _| log::info!("gallery: create workspace")), .on_create(|_, _| log::info!("gallery: create workspace")),
) )
.child( .child(
AccountSwitcher::new("account-accounts", accounts, account, move |key, _, cx| change(&to_account, cx, |places| places.account = key.clone())) AccountSwitcher::new("account-accounts", accounts, account)
.on_select(move |key, _, cx| change(&to_account, cx, |places| places.account = key.clone()))
.on_add(|_, _| log::info!("gallery: add account")), .on_add(|_, _| log::info!("gallery: add account")),
), ),
) )
.child(probe( .child(probe(
"account-user", "account-user",
UserMenu::new("account-user", "Ada Lovelace", "ada@atlas.dev", |_, _| log::info!("gallery: sign out")) UserMenu::new("account-user", "Ada Lovelace", "ada@atlas.dev")
.presence(Presence::Online) .presence(Presence::Online)
.item(MenuItem::new("Profile").icon(IconName::User).on_click(|_, _| log::info!("gallery: profile"))) .item(MenuItem::new("Profile").icon(IconName::User).on_click(|_, _| log::info!("gallery: profile")))
.item(MenuItem::new("Settings").icon(IconName::Settings).on_click(|_, _| log::info!("gallery: settings"))), .item(MenuItem::new("Settings").icon(IconName::Settings).on_click(|_, _| log::info!("gallery: settings")))
.on_sign_out(|_, _| log::info!("gallery: sign out")),
)), )),
) )
} }
@@ -130,7 +133,7 @@ pub fn profile(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.action(Button::new("account-card-profile", "View profile")), .action(Button::new("account-card-profile", "View profile")),
), ),
) )
.child(div().w(px(400.)).child(ProfileEditor::new("account-editor", now, move |profile, _, cx| { .child(div().w(px(400.)).child(ProfileEditor::new("account-editor", now).on_save(move |profile, _, cx| {
let profile = profile.clone(); let profile = profile.clone();
change(&saved, cx, move |now| *now = profile) change(&saved, cx, move |now| *now = profile)
}))), }))),
@@ -212,10 +215,8 @@ pub fn security_page(window: &mut Window, cx: &mut App) -> impl IntoElement + us
let now = state.read(cx); let now = state.read(cx);
let (sessions, keys, secret) = (now.sessions.clone(), now.keys.clone(), now.secret.clone()); let (sessions, keys, secret) = (now.sessions.clone(), now.keys.clone(), now.secret.clone());
let [out, others, made, revoked, done] = [(); 5].map(|_| state.clone()); let [out, others, made, revoked, done] = [(); 5].map(|_| state.clone());
let mut manager = ApiKeyManager::new( let mut manager = ApiKeyManager::new("account-keys", keys)
"account-keys", .on_create(move |name, _, cx| {
keys,
move |name, _, cx| {
let name = name.clone(); let name = name.clone();
change(&made, cx, move |security| { change(&made, cx, move |security| {
security.made += 1; security.made += 1;
@@ -232,15 +233,14 @@ pub fn security_page(window: &mut Window, cx: &mut App) -> impl IntoElement + us
); );
security.secret = Some("ely_live_5f1c0a9e2b7d4c8a3e6f7d3e".into()); security.secret = Some("ely_live_5f1c0a9e2b7d4c8a3e6f7d3e".into());
}) })
}, })
move |key, _, cx| { .on_revoke(move |key, _, cx| {
let key = key.clone(); let key = key.clone();
change(&revoked, cx, move |security| { change(&revoked, cx, move |security| {
security.keys.retain(|each| each.key != key) security.keys.retain(|each| each.key != key)
}) })
}, })
move |_, cx| change(&done, cx, |security| security.secret = None), .on_dismiss(move |_, cx| change(&done, cx, |security| security.secret = None));
);
if let Some(secret) = secret { if let Some(secret) = secret {
manager = manager.secret(secret); manager = manager.secret(secret);
} }
@@ -256,7 +256,8 @@ pub fn security_page(window: &mut Window, cx: &mut App) -> impl IntoElement + us
.items_start() .items_start()
.gap_8() .gap_8()
.child(div().w(px(380.)).child( .child(div().w(px(380.)).child(
SessionList::new("account-sessions", sessions, move |key, _, cx| { SessionList::new("account-sessions", sessions)
.on_sign_out(move |key, _, cx| {
let key = key.clone(); let key = key.clone();
change(&out, cx, move |security| security.sessions.retain(|each| each.key != key)) change(&out, cx, move |security| security.sessions.retain(|each| each.key != key))
}) })
+14 -7
View File
@@ -68,7 +68,7 @@ pub fn plan(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
), ),
) )
.child(div().w(px(360.)).flex().flex_col().gap_3().child( .child(div().w(px(360.)).flex().flex_col().gap_3().child(
PaymentMethodForm::new("account-card-form", move |card, _, cx| { PaymentMethodForm::new("account-card-form").on_submit(move |card, _, cx| {
let saved_as: SharedString = format!("{} ending {}", card.brand.name(), &card.number[card.number.len() - 4..]).into(); let saved_as: SharedString = format!("{} ending {}", card.brand.name(), &card.number[card.number.len() - 4..]).into();
change(&saved, cx, move |plan| plan.card = Some(saved_as)) change(&saved, cx, move |plan| plan.card = Some(saved_as))
}), }),
@@ -116,16 +116,18 @@ pub fn history(_: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.flex_col() .flex_col()
.gap_4() .gap_4()
.child( .child(
UpgradePrompt::new("account-nudge", "9 of 10 projects used", |_, _| log::info!("gallery: upgrade")) UpgradePrompt::new("account-nudge", "9 of 10 projects used")
.body("Pro has no limit on projects.") .body("Pro has no limit on projects.")
.on_upgrade(|_, _| log::info!("gallery: upgrade"))
.on_dismiss(|_, _| log::info!("gallery: not now")), .on_dismiss(|_, _| log::info!("gallery: not now")),
) )
.child( .child(
UpgradePrompt::new("account-wall", "Version history is on Pro", |_, _| log::info!("gallery: upgrade")) UpgradePrompt::new("account-wall", "Version history is on Pro")
.body("See and restore every version of a document.") .body("See and restore every version of a document.")
.benefit("Every version, kept a year") .benefit("Every version, kept a year")
.benefit("Restore with one press") .benefit("Restore with one press")
.benefit("Compare any two") .benefit("Compare any two")
.on_upgrade(|_, _| log::info!("gallery: upgrade"))
.on_dismiss(|_, _| log::info!("gallery: not now")), .on_dismiss(|_, _| log::info!("gallery: not now")),
), ),
), ),
@@ -205,22 +207,27 @@ pub fn members(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.gap_6() .gap_6()
.w(px(640.)) .w(px(640.))
.child( .child(
TeamMemberTable::new("account-members", members, roles(), move |key, next, _, cx| { TeamMemberTable::new("account-members", members, roles())
.on_role(move |key, next, _, cx| {
let (key, next) = (key.clone(), next.clone()); let (key, next) = (key.clone(), next.clone());
change(&role, cx, move |team| team.members.iter_mut().filter(|member| member.key == key).for_each(|member| member.role = next.clone())) change(&role, cx, move |team| team.members.iter_mut().filter(|member| member.key == key).for_each(|member| member.role = next.clone()))
}, move |key, _, cx| { })
.on_remove(move |key, _, cx| {
let key = key.clone(); let key = key.clone();
change(&removed, cx, move |team| team.members.retain(|member| member.key != key)) change(&removed, cx, move |team| team.members.retain(|member| member.key != key))
}), }),
) )
.child( .child(
InvitationList::new("account-invites", invitations, roles(), move |email, role, _, cx| { InvitationList::new("account-invites", invitations, roles())
.on_invite(move |email, role, _, cx| {
let (email, role) = (email.clone(), role.clone()); let (email, role) = (email.clone(), role.clone());
change(&invited, cx, move |team| { change(&invited, cx, move |team| {
team.sent += 1; team.sent += 1;
team.invitations.insert(0, Invitation { key: format!("new-{}", team.sent).into(), email, role, sent: "Sent just now".into(), expired: false }); team.invitations.insert(0, Invitation { key: format!("new-{}", team.sent).into(), email, role, sent: "Sent just now".into(), expired: false });
}) })
}, |key, _, _| log::info!("gallery: resend {key}"), move |key, _, cx| { })
.on_resend(|key, _, _| log::info!("gallery: resend {key}"))
.on_revoke(move |key, _, cx| {
let key = key.clone(); let key = key.clone();
change(&revoked, cx, move |team| team.invitations.retain(|each| each.key != key)) change(&revoked, cx, move |team| team.invitations.retain(|each| each.key != key))
}), }),
+23 -18
View File
@@ -30,18 +30,19 @@ pub fn sign_in(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
("google", "Google"), ("google", "Google"),
("apple", "Apple"), ("apple", "Apple"),
], ],
move |key, _, cx| change(&picked, cx, |state| state.provider = Some(key.clone())), )
); .on_pick(move |key, _, cx| change(&picked, cx, |state| state.provider = Some(key.clone())));
if let Some(provider) = provider { if let Some(provider) = provider {
providers = providers.busy(provider); providers = providers.busy(provider);
} }
let mut form = LoginForm::new("account-login", move |_, _, cx| { let mut form = LoginForm::new("account-login")
change(&tried, cx, |state| { .on_submit(move |_, _, cx| {
state.error = Some("No account uses that email and password.".into()) change(&tried, cx, |state| {
state.error = Some("No account uses that email and password.".into())
})
}) })
}) .on_forgot(|_, _| log::info!("gallery: forgot password"))
.on_forgot(|_, _| log::info!("gallery: forgot password")) .on_sign_up(|_, _| log::info!("gallery: sign up"));
.on_sign_up(|_, _| log::info!("gallery: sign up"));
if let Some(error) = error { if let Some(error) = error {
form = form.error(error); form = form.error(error);
} }
@@ -70,12 +71,13 @@ pub fn sign_up(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let state = keep("account-sign-up", SignUp::default, window, cx); let state = keep("account-sign-up", SignUp::default, window, cx);
let error = state.read(cx).error.clone(); let error = state.read(cx).error.clone();
let tried = state.clone(); let tried = state.clone();
let mut form = SignupForm::new("account-signup", move |signup, _, cx| { let mut form = SignupForm::new("account-signup")
let taken = format!("An account already uses {}.", signup.email); .terms("I agree to the terms of service")
change(&tried, cx, |state| state.error = Some(taken.into())) .on_submit(move |signup, _, cx| {
}) let taken = format!("An account already uses {}.", signup.email);
.terms("I agree to the terms of service") change(&tried, cx, |state| state.error = Some(taken.into()))
.on_sign_in(|_, _| log::info!("gallery: sign in")); })
.on_sign_in(|_, _| log::info!("gallery: sign in"));
if let Some(error) = error { if let Some(error) = error {
form = form.error(error); form = form.error(error);
} }
@@ -113,8 +115,9 @@ pub fn by_email(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
"account-magic", "account-magic",
div().w(px(360.)).child( div().w(px(360.)).child(
Card::new().header(CardHeader::new("Sign in by email")).child( Card::new().header(CardHeader::new("Sign in by email")).child(
MagicLinkForm::new("account-magic", move |_, _, cx| change(&sent, cx, |state| state.magic = true)) MagicLinkForm::new("account-magic")
.sent(magic) .sent(magic)
.on_send(move |_, _, cx| change(&sent, cx, |state| state.magic = true))
.on_other(move |_, cx| change(&other, cx, |state| state.magic = false)), .on_other(move |_, cx| change(&other, cx, |state| state.magic = false)),
), ),
), ),
@@ -122,8 +125,9 @@ pub fn by_email(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.child( .child(
div().w(px(360.)).child( div().w(px(360.)).child(
Card::new().header(CardHeader::new("Reset your password")).child( Card::new().header(CardHeader::new("Reset your password")).child(
ForgotPassword::new("account-forgot", move |_, _, cx| change(&asked, cx, |state| state.reset = true)) ForgotPassword::new("account-forgot")
.sent(reset) .sent(reset)
.on_send(move |_, _, cx| change(&asked, cx, |state| state.reset = true))
.on_back(move |_, cx| change(&back, cx, |state| state.reset = false)), .on_back(move |_, cx| change(&back, cx, |state| state.reset = false)),
), ),
), ),
@@ -157,13 +161,14 @@ pub fn second_factor(window: &mut Window, cx: &mut App) -> impl IntoElement + us
Card::new() Card::new()
.header(CardHeader::new("Two-step sign-in")) .header(CardHeader::new("Two-step sign-in"))
.child( .child(
TwoFactorInput::new("account-factor", move |code, _, cx| { TwoFactorInput::new("account-factor")
.failed(failed)
.on_code(move |code, _, cx| {
change(&checked, cx, |state| match code.as_ref() == CODE { change(&checked, cx, |state| match code.as_ref() == CODE {
true => state.passed = true, true => state.passed = true,
false => state.failed += 1, false => state.failed += 1,
}) })
}) })
.failed(failed)
.on_recovery(|_, _| log::info!("gallery: recovery code")), .on_recovery(|_, _| log::info!("gallery: recovery code")),
) )
.children(passed.then(|| InlineMessage::new(Severity::Success, "Signed in."))), .children(passed.then(|| InlineMessage::new(Severity::Success, "Signed in."))),
+1 -1
View File
@@ -114,7 +114,7 @@ pub fn small_buttons(cx: &App) -> impl IntoElement + use<> {
"share-button", "share-button",
ShareButton::new("share-page") ShareButton::new("share-page")
.text("Ely, components for GPUI") .text("Ely, components for GPUI")
.url("https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components"), .url("https://github.com/ZacharyZhang-NY/Ely-GPUI-Components"),
), ),
cx, cx,
)) ))
+5 -13
View File
@@ -221,26 +221,18 @@ pub fn mind(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
change(&added, cx, |mind| { change(&added, cx, |mind| {
mind.made += 1; mind.made += 1;
let key = SharedString::from(format!("idea-{}", mind.made)); let key = SharedString::from(format!("idea-{}", mind.made));
if let Some(root) = mind.root.adding(parent, Topic::new(key.clone(), "New idea")) { mind.root = mind.root.adding(parent, Topic::new(key.clone(), "New idea"));
mind.root = root; mind.selected = Some(key);
mind.selected = Some(key);
}
}) })
}) })
.on_remove(move |key, _, cx| { .on_remove(move |key, _, cx| {
change(&removed, cx, |mind| { change(&removed, cx, |mind| {
if let Some(root) = mind.root.removing(key) { mind.selected = mind.root.parent_of(key);
mind.selected = mind.root.parent_of(key); mind.root = mind.root.removing(key);
mind.root = root;
}
}) })
}) })
.on_rename(move |key, text, _, cx| { .on_rename(move |key, text, _, cx| {
change(&renamed, cx, |mind| { change(&renamed, cx, |mind| mind.root = mind.root.renaming(key, text.clone()))
if let Some(root) = mind.root.renaming(key, text.clone()) {
mind.root = root;
}
})
}) })
.on_viewport(move |next, _, cx| change(&viewed, cx, |mind| mind.view = next)), .on_viewport(move |next, _, cx| change(&viewed, cx, |mind| mind.view = next)),
cx, cx,
+16 -8
View File
@@ -121,8 +121,9 @@ pub fn styles(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.gap_6() .gap_6()
.child( .child(
div().w(px(260.)).child( div().w(px(260.)).child(
ColorPanel::new("canvas-color", color, move |next, _, cx| change(&colored, cx, |look| look.color = next)) ColorPanel::new("canvas-color", color)
.swatches(SWATCHES.map(|(name, hex)| (name, Hsla::from(rgb(hex))))), .swatches(SWATCHES.map(|(name, hex)| (name, Hsla::from(rgb(hex)))))
.on_change(move |next, _, cx| change(&colored, cx, |look| look.color = next)),
), ),
) )
.child( .child(
@@ -136,7 +137,8 @@ pub fn styles(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.on_change(move |next, _, cx| change(&graded, cx, |look| look.stops = next.to_vec())), .on_change(move |next, _, cx| change(&graded, cx, |look| look.stops = next.to_vec())),
) )
.child( .child(
ShadowEditor::new("canvas-shadows", shadows, move |next, _, cx| change(&shaded, cx, |look| look.shadows = next)), ShadowEditor::new("canvas-shadows", shadows)
.on_change(move |next, _, cx| change(&shaded, cx, |look| look.shadows = next)),
), ),
) )
.child( .child(
@@ -146,13 +148,15 @@ pub fn styles(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.flex_col() .flex_col()
.gap_6() .gap_6()
.child( .child(
BorderEditor::new("canvas-border", border, move |next, _, cx| change(&bordered, cx, |look| look.border = next)), BorderEditor::new("canvas-border", border)
.on_change(move |next, _, cx| change(&bordered, cx, |look| look.border = next)),
) )
.child(div().p_6().child(card)), .child(div().p_6().child(card)),
) )
.child( .child(
div().w(px(280.)).child( div().w(px(280.)).child(
TypographyPanel::new("canvas-type", style, &search, move |next, _, cx| change(&typed, cx, |look| look.style = next)), TypographyPanel::new("canvas-type", style, &search)
.on_change(move |next, _, cx| change(&typed, cx, |look| look.style = next)),
), ),
), ),
) )
@@ -278,7 +282,8 @@ pub fn rules(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.flex_col() .flex_col()
.gap_3() .gap_3()
.child( .child(
ConstraintsEditor::new("canvas-constraints", constraints, move |next, _, cx| change(&pinned, cx, |rules| rules.constraints = next)), ConstraintsEditor::new("canvas-constraints", constraints)
.on_change(move |next, _, cx| change(&pinned, cx, |rules| rules.constraints = next)),
) )
.child(pinned_preview), .child(pinned_preview),
) )
@@ -289,7 +294,8 @@ pub fn rules(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.flex_col() .flex_col()
.gap_3() .gap_3()
.child( .child(
AutoLayoutControls::new("canvas-auto-layout", layout, move |next, _, cx| change(&laid, cx, |rules| rules.layout = next)), AutoLayoutControls::new("canvas-auto-layout", layout)
.on_change(move |next, _, cx| change(&laid, cx, |rules| rules.layout = next)),
) )
.child(laid_preview), .child(laid_preview),
) )
@@ -300,7 +306,9 @@ pub fn rules(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.flex_col() .flex_col()
.gap_3() .gap_3()
.child( .child(
ExportPanel::new("canvas-export", "Card", exports, move |next, _, cx| change(&set, cx, |rules| rules.exports = next), move |files, _, cx| change(&sent, cx, |rules| rules.exported = files)), ExportPanel::new("canvas-export", "Card", exports)
.on_change(move |next, _, cx| change(&set, cx, |rules| rules.exports = next))
.on_export(move |files, _, cx| change(&sent, cx, |rules| rules.exported = files)),
) )
.when(!exported.is_empty(), |panel| { .when(!exported.is_empty(), |panel| {
panel.child(note(format!("Exported {}", exported.join(", ")).into())) panel.child(note(format!("Exported {}", exported.join(", ")).into()))
+13 -12
View File
@@ -134,18 +134,19 @@ pub fn arrange(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
studio.record(align.words().into()); studio.record(align.words().into());
}) })
}); });
let inspector = InspectorPanel::new("canvas-inspector", chosen, move |shape, _, cx| { let inspector =
change(&edited, cx, |studio| { InspectorPanel::new("canvas-inspector", chosen).on_change(move |shape, _, cx| {
let at = studio change(&edited, cx, |studio| {
.shapes let at = studio
.iter() .shapes
.position(|each| each.key == shape.key) .iter()
.expect("a listed shape"); .position(|each| each.key == shape.key)
let words = format!("Edit {}", shape.name); .expect("a listed shape");
studio.shapes[at] = shape; let words = format!("Edit {}", shape.name);
studio.record(words); studio.shapes[at] = shape;
}) studio.record(words);
}); })
});
let assets = AssetPanel::new("canvas-assets") let assets = AssetPanel::new("canvas-assets")
.group("Shapes", shapes()) .group("Shapes", shapes())
.group("Notes", notes()) .group("Notes", notes())
+2 -10
View File
@@ -261,10 +261,7 @@ const BASE: &str = "A lift blends color toward white. Half a lift sits midway be
fn applied(base: &str, suggestions: Vec<Suggestion>) -> String { fn applied(base: &str, suggestions: Vec<Suggestion>) -> String {
let (mut text, mut left) = (base.to_string(), suggestions); let (mut text, mut left) = (base.to_string(), suggestions);
while !left.is_empty() { while !left.is_empty() {
let Some(taken) = accept(&text, &left, 0) else { (text, left) = accept(&text, &left, 0);
break;
};
(text, left) = taken;
} }
text text
} }
@@ -309,16 +306,11 @@ pub fn changes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let pending = suggested(&base_text, field.read(cx).text(), &you()); let pending = suggested(&base_text, field.read(cx).text(), &you());
match decision { match decision {
Decision::Accept(ix) => { Decision::Accept(ix) => {
if let Some((text, _)) = accept(&base_text, &pending, ix) { set(&decided, accept(&base_text, &pending, ix).0, cx)
set(&decided, text, cx)
}
} }
Decision::AcceptAll => set(&decided, applied(&base_text, pending), cx), Decision::AcceptAll => set(&decided, applied(&base_text, pending), cx),
Decision::Reject(ix) => { Decision::Reject(ix) => {
let mut rest = pending; let mut rest = pending;
if ix >= rest.len() {
return;
}
rest.remove(ix); rest.remove(ix);
let text = applied(&base_text, rest); let text = applied(&base_text, rest);
field.update(cx, |input, cx| input.set_text(text, cx)); field.update(cx, |input, cx| input.set_text(text, cx));
+50 -47
View File
@@ -82,51 +82,50 @@ pub fn grid(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
picked: service, picked: service,
}, },
]; ];
let grid = DashboardGrid::new("dashboard-grid", 12, tiles, move |next, _, cx| { let grid = DashboardGrid::new("dashboard-grid", 12, tiles)
change(&moved, cx, |wall| wall.tiles = next) .card(
}) "requests",
.card( DashboardCard::new("Requests").subtitle("per minute").body(
"requests", Statistic::new("dashboard-requests", "This hour", 12_400.0)
DashboardCard::new("Requests").subtitle("per minute").body( .trend(TrendIndicator::new(0.082)),
Statistic::new("dashboard-requests", "This hour", 12_400.0) ),
.trend(TrendIndicator::new(0.082)), )
), .card(
) "errors",
.card( DashboardCard::new("Errors").subtitle("5xx share").body(
"errors", Statistic::new("dashboard-errors", "This hour", 0.42)
DashboardCard::new("Errors").subtitle("5xx share").body( .decimals(2)
Statistic::new("dashboard-errors", "This hour", 0.42) .suffix("%")
.decimals(2) .trend(TrendIndicator::new(-0.12)),
.suffix("%") ),
.trend(TrendIndicator::new(-0.12)), )
), .card(
) "cpu",
.card( DashboardCard::new("CPU").body(Gauge::new(
"cpu", "dashboard-cpu",
DashboardCard::new("CPU").body(Gauge::new( "percent busy",
"dashboard-cpu", 62.0,
"percent busy", 0.0,
62.0, 100.0,
0.0, )),
100.0, )
)), .card(
) "latency",
.card( DashboardCard::new("Latency")
"latency", .subtitle("p95, milliseconds")
DashboardCard::new("Latency") .body(LineChart::new("dashboard-latency", hours).series(latency)),
.subtitle("p95, milliseconds") )
.body(LineChart::new("dashboard-latency", hours).series(latency)), .card(
) "saturation",
.card( DashboardCard::new("Saturation").body(Gauge::new(
"saturation", "dashboard-memory",
DashboardCard::new("Saturation").body(Gauge::new( "memory in use",
"dashboard-memory", 81.0,
"memory in use", 0.0,
81.0, 100.0,
0.0, )),
100.0, )
)), .on_change(move |next, _, cx| change(&moved, cx, |wall| wall.tiles = next));
);
section( section(
"Dashboard Grid · Widget / DashboardCard · RefreshIntervalSelector · DashboardFilterBar · StatCard → data_display::Statistic · MetricsChart → charts::LineChart · ResourceGauge → data_display::Gauge", "Dashboard Grid · Widget / DashboardCard · RefreshIntervalSelector · DashboardFilterBar · StatCard → data_display::Statistic · MetricsChart → charts::LineChart · ResourceGauge → data_display::Gauge",
"Widgets on a grid of twelve columns: a tile's top strip drags it and its corner resizes it, a cell at a time, while the others make room and rise to fill gaps; a focused tile steps with the arrows. Over it, the window of time and the filters, and how often it refreshes.", "Widgets on a grid of twelve columns: a tile's top strip drags it and its corner resizes it, a cell at a time, while the others make room and rise to fill gaps; a focused tile steps with the arrows. Over it, the window of time and the filters, and how often it refreshes.",
@@ -144,7 +143,9 @@ pub fn grid(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.justify_between() .justify_between()
.gap_3() .gap_3()
.child( .child(
DashboardFilterBar::new("dashboard-filters", time, filters, move |next, _, cx| change(&windowed, cx, |wall| wall.window = next), move |key, value, _, cx| { DashboardFilterBar::new("dashboard-filters", time, filters)
.on_window(move |next, _, cx| change(&windowed, cx, |wall| wall.window = next))
.on_filter(move |key, value, _, cx| {
change(&filtered, cx, |wall| match key.as_ref() { change(&filtered, cx, |wall| match key.as_ref() {
"environment" => wall.environment = value, "environment" => wall.environment = value,
_ => wall.service = value, _ => wall.service = value,
@@ -152,7 +153,9 @@ pub fn grid(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
}), }),
) )
.child( .child(
RefreshIntervalSelector::new("dashboard-refresh", every, clock - jiff::SignedDuration::from_secs(since), clock, move |next, _, cx| change(&timed, cx, |wall| wall.every = next), move |_, cx| change(&refreshed, cx, |wall| wall.since = 0)), RefreshIntervalSelector::new("dashboard-refresh", every, clock - jiff::SignedDuration::from_secs(since), clock)
.on_change(move |next, _, cx| change(&timed, cx, |wall| wall.every = next))
.on_refresh(move |_, cx| change(&refreshed, cx, |wall| wall.since = 0)),
), ),
) )
.child(div().w(px(900.)).child(grid)), .child(div().w(px(900.)).child(grid)),
+1 -1
View File
@@ -11,7 +11,7 @@ use crate::{
}; };
pub fn codes(cx: &mut App) -> impl IntoElement + use<> { pub fn codes(cx: &mut App) -> impl IntoElement + use<> {
let qr = QrCode::new("https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components") let qr = QrCode::new("https://github.com/ZacharyZhang-NY/Ely-GPUI-Components")
.expect("a short link fits a QR code"); .expect("a short link fits a QR code");
let label = Barcode::code128("ELY-2419").expect("printable ASCII"); let label = Barcode::code128("ELY-2419").expect("printable ASCII");
let retail = Barcode::ean13("400638133393").expect("twelve digits"); let retail = Barcode::ean13("400638133393").expect("twelve digits");
+7 -15
View File
@@ -156,7 +156,8 @@ pub fn requests(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.flex_col() .flex_col()
.gap_4() .gap_4()
.child( .child(
EnvironmentSelector::new("devtools-environments", environments(), environment, move |key, _, cx| change(&chose, cx, |desk| desk.environment = key.clone())), EnvironmentSelector::new("devtools-environments", environments(), environment)
.on_change(move |key, _, cx| change(&chose, cx, |desk| desk.environment = key.clone())),
) )
.child( .child(
div() div()
@@ -177,11 +178,7 @@ pub fn requests(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
}) })
}) })
.on_move(move |key, target, at, _, cx| { .on_move(move |key, target, at, _, cx| {
change(&dragged, cx, |desk| { change(&dragged, cx, |desk| desk.collection = moved(&desk.collection, key, target, at))
if let Some(collection) = moved(&desk.collection, key, target, at) {
desk.collection = collection;
}
})
}), }),
), ),
), ),
@@ -258,7 +255,8 @@ pub fn socket(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
) )
.child( .child(
div().w(px(620.)).child( div().w(px(620.)).child(
WebSocketConsole::new("devtools-socket", "wss://stream.example.com/orders", state, messages, &draft, clock, move |text, _, cx| { WebSocketConsole::new("devtools-socket", "wss://stream.example.com/orders", state, messages, &draft, clock)
.on_send(move |text, _, cx| {
sent.update(cx, |line, cx| { sent.update(cx, |line, cx| {
let at = line.now; let at = line.now;
line.messages.push(SocketMessage { direction: Direction::Sent, text, at }); line.messages.push(SocketMessage { direction: Direction::Sent, text, at });
@@ -335,14 +333,8 @@ pub fn graphql(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
fields: vec![("OPEN".into(), "".into()), ("SHIPPED".into(), "".into())], fields: vec![("OPEN".into(), "".into()), ("SHIPPED".into(), "".into())],
}, },
]; ];
let mut explorer = GraphQLExplorer::new( let mut explorer = GraphQLExplorer::new("devtools-graphql", types, &query, &variables, &search)
"devtools-graphql", .on_run(move |_, _, _, cx| change(&ran, cx, |graph| graph.answer = Some(ANSWER.into())));
types,
&query,
&variables,
&search,
move |_, _, _, cx| change(&ran, cx, |graph| graph.answer = Some(ANSWER.into())),
);
if let Some(answer) = answer { if let Some(answer) = answer {
explorer = explorer.answer(answer); explorer = explorer.answer(answer);
} }
+5 -2
View File
@@ -270,12 +270,15 @@ pub fn structure(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
.flex_wrap() .flex_wrap()
.gap_6() .gap_6()
.child( .child(
div().w(px(660.)).child(TableStructureEditor::new("devtools-structure", fields, move |next, _, cx| change(&edited, cx, |build| build.fields = next)), div().w(px(660.)).child(TableStructureEditor::new("devtools-structure", fields)
.on_change(move |next, _, cx| change(&edited, cx, |build| build.fields = next)),
), ),
) )
.child( .child(
div().w(px(300.)).child( div().w(px(300.)).child(
IndexManager::new("devtools-indexes", indexes, names, move |index, _, cx| change(&created, cx, |build| build.indexes.push(index)), move |name, _, cx| change(&dropped, cx, |build| build.indexes.retain(|index| index.name != *name))), IndexManager::new("devtools-indexes", indexes, names)
.on_drop(move |name, _, cx| change(&dropped, cx, |build| build.indexes.retain(|index| index.name != *name)))
.on_create(move |index, _, cx| change(&created, cx, |build| build.indexes.push(index))),
), ),
), ),
) )
+2 -1
View File
@@ -35,7 +35,8 @@ pub fn text(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.child(div().w(px(380.)).child(Encoder::new("devtools-encoder", "Hello, 世界"))) .child(div().w(px(380.)).child(Encoder::new("devtools-encoder", "Hello, 世界")))
.child( .child(
div().w(px(360.)).child( div().w(px(360.)).child(
ColorContrastChecker::new("devtools-contrast", fore, back, move |fore, back, _, cx| change(&colors, cx, |colors| *colors = (fore, back))), ColorContrastChecker::new("devtools-contrast", fore, back)
.on_change(move |fore, back, _, cx| change(&colors, cx, |colors| *colors = (fore, back))),
), ),
), ),
) )
+2 -5
View File
@@ -202,10 +202,7 @@ pub fn editing(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
"Blocks like a notebook's. Hover one for its handle: drag it to move the block, press it to turn, copy or delete it. / on an empty line lists every kind, and an empty line says so. Undo covers text and blocks alike.", "Blocks like a notebook's. Hover one for its handle: drag it to move the block, press it to turn, copy or delete it. / on an empty line lists every kind, and an empty line says so. Undo covers text and blocks alike.",
cx, cx,
) )
.child(probe( .child(probe("doc-blocks", div().w(px(760.)).child(notes))),
"doc-blocks",
div().w(px(760.)).h(px(360.)).child(notes),
)),
) )
.child( .child(
section( section(
@@ -213,7 +210,7 @@ pub fn editing(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
"Code colored as code, math set as it is typed, diagrams the app draws from their source, dividers, tables that grow and merge cells, pictures to resize and align, pages to open, columns, and synced copies that change together.", "Code colored as code, math set as it is typed, diagrams the app draws from their source, dividers, tables that grow and merge cells, pictures to resize and align, pages to open, columns, and synced copies that change together.",
cx, cx,
) )
.child(div().w(px(760.)).h(px(560.)).child(kinds)), .child(div().w(px(760.)).child(kinds)),
) )
.child( .child(
section( section(
+1 -1
View File
@@ -129,7 +129,7 @@ pub fn merges(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
cx, cx,
) )
.child( .child(
div().w(px(840.)).h(px(320.)).child( div().w(px(840.)).child(
ThreeWayMerge::new("git-merge", ["main", "base", "feature/lift"], Rc::new(regions(BASE, OURS, THEIRS))) ThreeWayMerge::new("git-merge", ["main", "base", "feature/lift"], Rc::new(regions(BASE, OURS, THEIRS)))
.takes(now_takes) .takes(now_takes)
.on_take(move |conflict, side, _, cx| { .on_take(move |conflict, side, _, cx| {
+4 -11
View File
@@ -37,14 +37,7 @@ pub fn blame(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
]; ];
let history_rows: Vec<_> = history() let history_rows: Vec<_> = history()
.into_iter() .into_iter()
.zip([ .zip([(12, 4), (3, 3), (9, 1), (18, 0), (2, 1), (40, 0)])
Some((12, 4)),
Some((3, 3)),
Some((9, 1)),
Some((18, 0)),
None,
Some((40, 0)),
])
.collect(); .collect();
let (pick, select) = (said.clone(), said.clone()); let (pick, select) = (said.clone(), said.clone());
section( section(
@@ -53,7 +46,7 @@ pub fn blame(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
cx, cx,
) )
.child( .child(
div().w(px(840.)).h(px(240.)).child( div().w(px(840.)).child(
BlameView::new("git-blame", CODE, [0, 1, 1, 1, 0, 0, 2, 2, 0, 0], blames) BlameView::new("git-blame", CODE, [0, 1, 1, 1, 0, 0, 2, 2, 0, 0], blames)
.current(2) .current(2)
.on_commit(move |commit, _, cx| set(&pick, Some(format!("Picked {}.", &commit[..7]).into()), cx)), .on_commit(move |commit, _, cx| set(&pick, Some(format!("Picked {}.", &commit[..7]).into()), cx)),
@@ -61,8 +54,8 @@ pub fn blame(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
) )
.children(told.map(Caption::new)) .children(told.map(Caption::new))
.child( .child(
div().w(px(520.)).h(px(360.)).child( div().w(px(520.)).child(
FileHistory::new("git-file-history", std::rc::Rc::new(history_rows)) FileHistory::new("git-file-history", history_rows)
.selected(history()[1].id.clone()) .selected(history()[1].id.clone())
.on_pick(move |commit, _, cx| set(&select, Some(format!("Opened {}.", &commit[..7]).into()), cx)), .on_pick(move |commit, _, cx| set(&select, Some(format!("Opened {}.", &commit[..7]).into()), cx)),
), ),
+7 -23
View File
@@ -3,13 +3,11 @@ use ely_gpui_component::{
forms::TextInput, forms::TextInput,
git::{ git::{
Branch, BranchList, BranchSelector, ChangeAction, Changed, ChangesList, Commit, Branch, BranchList, BranchSelector, ChangeAction, Changed, ChangesList, Commit,
CommitInput, CommitList, GitTag, GraphRow, Stash, StashAction, StashList, TagList, lanes, CommitInput, CommitList, GitTag, Stash, StashAction, StashList, TagList,
}, },
lists::GitStatus, lists::GitStatus,
typography::Caption, typography::Caption,
}; };
use std::rc::Rc;
use gpui::{App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, px}; use gpui::{App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use crate::{ use crate::{
@@ -21,7 +19,8 @@ fn changed(path: &str, status: GitStatus, added: usize, removed: usize) -> Chang
Changed { Changed {
path: path.to_string().into(), path: path.to_string().into(),
status, status,
lines: Some((added, removed)), added,
removed,
} }
} }
@@ -87,7 +86,7 @@ pub fn changes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.child( .child(
div().w(px(420.)).child( div().w(px(420.)).child(
ChangesList::new("git-changes", now_staged, now_unstaged) ChangesList::new("git-changes", now_staged, now_unstaged)
.on_action(move |path, _, action, _, cx| { .on_action(move |path, action, _, cx| {
let (from, to) = match action { let (from, to) = match action {
ChangeAction::Stage => (&act_unstaged, &act_staged), ChangeAction::Stage => (&act_unstaged, &act_staged),
ChangeAction::Unstage => (&act_staged, &act_unstaged), ChangeAction::Unstage => (&act_staged, &act_unstaged),
@@ -212,21 +211,6 @@ pub(super) fn history() -> Vec<Commit> {
] ]
} }
/// The sample history's lanes.
fn graph() -> Vec<GraphRow> {
let commits = history();
let parents: Vec<Vec<&str>> = commits
.iter()
.map(|commit| commit.parents.iter().map(|id| id.as_ref()).collect())
.collect();
lanes(
commits
.iter()
.zip(&parents)
.map(|(commit, parents)| (commit.id.as_ref(), parents.as_slice())),
)
}
pub fn commits(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { pub fn commits(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let picked = keep("git-picked", || history()[0].id.clone(), window, cx); let picked = keep("git-picked", || history()[0].id.clone(), window, cx);
let now = picked.read(cx).clone(); let now = picked.read(cx).clone();
@@ -237,9 +221,9 @@ pub fn commits(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
cx, cx,
) )
.child( .child(
div().w(px(840.)).h(px(320.)).child( div().w(px(840.)).child(
CommitList::new("git-graph", Rc::new(history())) CommitList::new("git-graph", history())
.graph(Rc::new(graph())) .graph()
.selected(now) .selected(now)
.on_pick(move |id, _, cx| set(&pick, id.clone(), cx)), .on_pick(move |id, _, cx| set(&pick, id.clone(), cx)),
), ),
+10 -10
View File
@@ -246,15 +246,12 @@ pub fn focus(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
("strike", IconName::Strikethrough), ("strike", IconName::Strikethrough),
]; ];
let toolbar = tools.into_iter().fold( let toolbar = tools.into_iter().fold(
RovingFocus::new("interaction-tools", Axis::Horizontal, move |key, _, cx| { RovingFocus::new("interaction-tools", Axis::Horizontal)
let key = key.clone(); .gap_0p5()
change(&press, cx, |focusing| focusing.tool = key) .p_0p5()
}) .rounded_lg()
.gap_0p5() .border_1()
.p_0p5() .border_color(theme.colors.border),
.rounded_lg()
.border_1()
.border_color(theme.colors.border),
|bar, (key, icon)| { |bar, (key, icon)| {
let on = tool.as_ref() == key; let on = tool.as_ref() == key;
bar.item( bar.item(
@@ -311,7 +308,10 @@ pub fn focus(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
"RovingFocus", "RovingFocus",
probe( probe(
"interaction-tools", "interaction-tools",
toolbar, toolbar.on_press(move |key, _, cx| {
let key = key.clone();
change(&press, cx, |focusing| focusing.tool = key)
}),
), ),
cx, cx,
)) ))
+17 -15
View File
@@ -162,18 +162,15 @@ pub fn shapes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let theme = cx.theme(); let theme = cx.theme();
let [resize, turn, pick, sort, double, press, long] = [(); 7].map(|_| state.clone()); let [resize, turn, pick, sort, double, press, long] = [(); 7].map(|_| state.clone());
let tiles = (0..12).fold( let tiles = (0..12).fold(
SelectionArea::new("interaction-tiles", move |keys, _, cx| { SelectionArea::new("interaction-tiles")
let keys = keys.to_vec(); .w(px(300.))
change(&pick, cx, |shapes| shapes.picked = keys) .flex()
}) .flex_wrap()
.w(px(300.)) .gap(px(8.))
.flex() .p_3()
.flex_wrap() .rounded_lg()
.gap(px(8.)) .border_1()
.p_3() .border_color(theme.colors.border),
.rounded_lg()
.border_1()
.border_color(theme.colors.border),
|area, ix| { |area, ix| {
area.item( area.item(
SharedString::from(format!("tile-{ix}")), SharedString::from(format!("tile-{ix}")),
@@ -207,7 +204,8 @@ pub fn shapes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
"Resizable", "Resizable",
probe( probe(
"interaction-resizable", "interaction-resizable",
Resizable::new("interaction-resizable", size_now, size(px(160.), px(96.)), size(px(360.), px(220.)), move |next, _, cx| change(&resize, cx, |shapes| shapes.size = next)) Resizable::new("interaction-resizable", size_now, size(px(160.), px(96.)), size(px(360.), px(220.)))
.on_resize(move |next, _, cx| change(&resize, cx, |shapes| shapes.size = next))
.child( .child(
div() div()
.size_full() .size_full()
@@ -227,7 +225,8 @@ pub fn shapes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
"Rotatable", "Rotatable",
probe( probe(
"interaction-rotatable", "interaction-rotatable",
Rotatable::new("interaction-rotatable", "icons/arrow-up.svg", px(120.), angle, move |next, _, cx| change(&turn, cx, |shapes| shapes.angle = next)), Rotatable::new("interaction-rotatable", "icons/arrow-up.svg", px(120.), angle)
.on_turn(move |next, _, cx| change(&turn, cx, |shapes| shapes.angle = next)),
), ),
cx, cx,
)), )),
@@ -238,7 +237,10 @@ pub fn shapes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
"SelectionArea", "SelectionArea",
probe( probe(
"interaction-tiles", "interaction-tiles",
tiles.selected(picked), tiles.selected(picked).on_change(move |keys, _, cx| {
let keys = keys.to_vec();
change(&pick, cx, |shapes| shapes.picked = keys)
}),
), ),
cx, cx,
)) ))
+1 -19
View File
@@ -211,24 +211,6 @@ pub fn hosts(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
pub fn context(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { pub fn context(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let (last, caption) = ran("context-ran", window, cx); let (last, caption) = ran("context-ran", window, cx);
let menu = Menu::new() let menu = Menu::new()
.buttons([
MenuItem::new("Cut")
.icon(IconName::Scissors)
.keys("secondary-x")
.on_click(run(&last, "cut")),
MenuItem::new("Copy")
.icon(IconName::Copy)
.keys("secondary-c")
.on_click(run(&last, "copy")),
MenuItem::new("Paste")
.icon(IconName::Clipboard)
.keys("secondary-v")
.on_click(run(&last, "paste")),
MenuItem::new("Delete")
.icon(IconName::Trash2)
.disabled(true),
])
.separator()
.item( .item(
MenuItem::new("Back") MenuItem::new("Back")
.icon(IconName::ArrowLeft) .icon(IconName::ArrowLeft)
@@ -260,7 +242,7 @@ pub fn context(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let theme = cx.theme(); let theme = cx.theme();
section( section(
"ContextMenu", "ContextMenu",
"A right click opens the menu where it lands; it stays inside the window. A row of icon buttons heads it; arrows cross the row.", "A right click opens the menu where it lands; it stays inside the window.",
cx, cx,
) )
.child(specimen( .child(specimen(
+15 -13
View File
@@ -20,19 +20,19 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
"poll", "poll",
"Which chart should come next?", "Which chart should come next?",
["Sankey", "Sunburst", "Candlestick"], ["Sankey", "Sunburst", "Candlestick"],
move |vote, _, cx| {
tally.update(cx, |tally, cx| {
match (vote, tally.voted) {
(Some(ix), _) => tally.votes[ix] += 1,
(None, Some(ix)) => tally.votes[ix] -= 1,
(None, None) => panic!("gallery poll: no vote to take back"),
}
tally.voted = vote;
cx.notify();
})
},
) )
.votes(votes); .votes(votes)
.on_vote(move |vote, _, cx| {
tally.update(cx, |tally, cx| {
match (vote, tally.voted) {
(Some(ix), _) => tally.votes[ix] += 1,
(None, Some(ix)) => tally.votes[ix] -= 1,
(None, None) => panic!("gallery poll: no vote to take back"),
}
tally.voted = vote;
cx.notify();
})
});
let poll = match voted { let poll = match voted {
Some(ix) => poll.voted(ix), Some(ix) => poll.voted(ix),
None => poll, None => poll,
@@ -62,7 +62,9 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
vec!["Forms".into(), "Charts".into(), "Tables".into()], vec!["Forms".into(), "Charts".into(), "Tables".into()],
), ),
Question::Text("What should we build next?".into()), Question::Text("What should we build next?".into()),
], |answers, _, _| log::info!("gallery: survey {answers:?}")), ],
)
.on_submit(|answers, _, _| log::info!("gallery: survey {answers:?}")),
), ),
cx, cx,
)), )),
+6 -4
View File
@@ -53,8 +53,9 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
) )
.child( .child(
div().w(px(280.0)).child( div().w(px(280.0)).child(
QrCodeScanner::new("scanner", 1.5, |text, _, _| log::info!("gallery: scanned {text}")) QrCodeScanner::new("scanner", 1.5)
.frame(frame.read(cx).clone()), .frame(frame.read(cx).clone())
.on_scan(|text, _, _| log::info!("gallery: scanned {text}")),
), ),
), ),
) )
@@ -67,7 +68,9 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.child(probe( .child(probe(
"captcha", "captcha",
div().w(px(280.0)).child( div().w(px(280.0)).child(
Captcha::new("captcha", picture, 3.0, move |text, _, cx| { Captcha::new("captcha", picture, 3.0)
.state(state)
.on_answer(move |text, _, cx| {
checked.update(cx, |desk, cx| { checked.update(cx, |desk, cx| {
desk.1 = match text.eq_ignore_ascii_case(answer) { desk.1 = match text.eq_ignore_ascii_case(answer) {
true => CaptchaState::Passed, true => CaptchaState::Passed,
@@ -76,7 +79,6 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
cx.notify(); cx.notify();
}) })
}) })
.state(state)
.on_refresh(move |_, cx| { .on_refresh(move |_, cx| {
refreshed.update(cx, |desk, cx| { refreshed.update(cx, |desk, cx| {
*desk = ((desk.0 + 1) % ANSWERS.len(), CaptchaState::Asking); *desk = ((desk.0 + 1) % ANSWERS.len(), CaptchaState::Asking);
+12 -6
View File
@@ -16,8 +16,12 @@ fn packages() -> [Package; 4] {
Package::new( Package::new(
"Ely GPUI Component", "Ely GPUI Component",
"0.1.0", "0.1.0",
"MIT", "MIT OR Apache-2.0",
include_str!("../../../../LICENSE"), concat!(
include_str!("../../../../LICENSE-MIT"),
"\n\n",
include_str!("../../../../LICENSE-APACHE")
),
), ),
Package::new( Package::new(
"Lucide", "Lucide",
@@ -110,7 +114,11 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
div().w(px(360.0)).child( div().w(px(360.0)).child(
CookieBanner::new( CookieBanner::new(
"cookies", "cookies",
"The help pages set cookies. Necessary ones keep them working; the rest are up to you.", move |kinds, _, cx| { "The help pages set cookies. Necessary ones keep them working; the rest are up to you.",
)
.kind("Analytics", "Counts which pages help, without names.")
.kind("Video", "Lets embedded tutorials play and remember where you stopped.")
.on_choose(move |kinds, _, cx| {
let words = match kinds.len() { let words = match kinds.len() {
0 => "Only necessary cookies.".to_string(), 0 => "Only necessary cookies.".to_string(),
_ => format!("Cookies on: {}.", kinds.join(", ")), _ => format!("Cookies on: {}.", kinds.join(", ")),
@@ -119,9 +127,7 @@ pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
*said = words.into(); *said = words.into();
cx.notify(); cx.notify();
}) })
}) }),
.kind("Analytics", "Counts which pages help, without names.")
.kind("Video", "Lets embedded tutorials play and remember where you stopped."),
), ),
)) ))
.child( .child(
+6 -12
View File
@@ -58,16 +58,14 @@ pub fn pagination(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
let now = *page.read(cx); let now = *page.read(cx);
section( section(
"Pagination", "Pagination",
"The first, the last, and the pages around this one. A gap stands for two or more pages. Type a page to go there.", "The first, the last, and the pages around this one. A gap stands for two or more pages.",
cx, cx,
) )
.child(specimen( .child(specimen(
format!("page {now} of 20"), format!("page {now} of 20"),
probe( probe(
"pagination", "pagination",
Pagination::new("pagination", now, 20) Pagination::new("pagination", now, 20).on_change(move |to, _, cx| set(&page, to, cx)),
.jump()
.on_change(move |to, _, cx| set(&page, to, cx)),
), ),
cx, cx,
)) ))
@@ -108,14 +106,10 @@ pub fn steps(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
probe( probe(
"wizard", "wizard",
div().w(px(560.0)).child( div().w(px(560.0)).child(
Wizard::new( Wizard::new("wizard", setup_steps(), now)
"wizard", .content(div().h(px(72.0)).child(Paragraph::new(body)))
setup_steps(), .on_step(move |to, _, cx| set(&walk, to, cx))
now, .on_finish(move |_, cx| set(&finish, true, cx)),
move |to, _, cx| set(&walk, to, cx),
move |_, cx| set(&finish, true, cx),
)
.content(div().h(px(72.0)).child(Paragraph::new(body))),
), ),
), ),
cx, cx,
+24 -30
View File
@@ -88,17 +88,13 @@ pub fn wizard(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
}), }),
) )
.into_any_element(), .into_any_element(),
false => OnboardingWizard::new( false => OnboardingWizard::new("onboarding-wizard", steps, step)
"onboarding-wizard", .content(content)
steps, .ready(ready)
step, .on_step(move |to, _, cx| change(&walk, cx, |welcome| welcome.step = to))
move |to, _, cx| change(&walk, cx, |welcome| welcome.step = to), .on_finish(move |_, cx| change(&finish, cx, |welcome| welcome.finished = true))
move |_, cx| change(&finish, cx, |welcome| welcome.finished = true), .on_skip(move |_, cx| change(&skip, cx, |welcome| welcome.finished = true))
) .into_any_element(),
.content(content)
.ready(ready)
.on_skip(move |_, cx| change(&skip, cx, |welcome| welcome.finished = true))
.into_any_element(),
}; };
section( section(
"OnboardingWizard · WelcomeTour", "OnboardingWizard · WelcomeTour",
@@ -126,15 +122,17 @@ pub fn highlights(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
.relative() .relative()
.child(tool("onboarding-pin", IconName::Pin)) .child(tool("onboarding-pin", IconName::Pin))
.when(!spot_seen, |pin| { .when(!spot_seen, |pin| {
pin.child(div().absolute().top_neg_1p5().right_neg_1p5().child(probe( pin.child(
"onboarding-hotspot", div().absolute().top_neg_1p5().right_neg_1p5().child(probe(
Hotspot::new(
"onboarding-hotspot", "onboarding-hotspot",
"Pin a view", Hotspot::new(
"Keep the views you use most one press away.", "onboarding-hotspot",
move |_, cx| change(&spot, cx, |seen| seen.hotspot = true), "Pin a view",
), "Keep the views you use most one press away.",
))) )
.on_dismiss(move |_, cx| change(&spot, cx, |seen| seen.hotspot = true)),
)),
)
}); });
let toolbar = div() let toolbar = div()
.flex() .flex()
@@ -153,9 +151,9 @@ pub fn highlights(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
IconName::Columns2, IconName::Columns2,
"Split view", "Split view",
"Work on two files side by side, each with its own history.", "Work on two files side by side, each with its own history.",
|_, _| log::info!("gallery: split view tried"),
move |_, cx| change(&card, cx, |seen| seen.highlight = true),
) )
.on_try(|_, _| log::info!("gallery: split view tried"))
.on_dismiss(move |_, cx| change(&card, cx, |seen| seen.highlight = true))
}); });
let menu = List::new() let menu = List::new()
.child( .child(
@@ -249,20 +247,16 @@ pub fn checklist(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
}) })
}) })
.into_any_element(), .into_any_element(),
false => SetupChecklist::new( false => SetupChecklist::new("onboarding-setup", "Get started", tasks)
"onboarding-setup", .on_start(move |key, _, cx| {
"Get started",
tasks,
move |key, _, cx| {
let at = TASKS let at = TASKS
.iter() .iter()
.position(|(each, _, _)| *each == key.as_ref()) .position(|(each, _, _)| *each == key.as_ref())
.expect("a setup task"); .expect("a setup task");
change(&start, cx, |setup| setup.done[at] = true) change(&start, cx, |setup| setup.done[at] = true)
}, })
) .on_dismiss(move |_, cx| change(&hide, cx, |setup| setup.hidden = true))
.on_dismiss(move |_, cx| change(&hide, cx, |setup| setup.hidden = true)) .into_any_element(),
.into_any_element(),
}; };
section( section(
"Checklist (first-run tasks)", "Checklist (first-run tasks)",
+14 -17
View File
@@ -176,21 +176,16 @@ pub fn panel(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.border_color(theme.colors.border) .border_color(theme.colors.border)
.bg(theme.colors.surface) .bg(theme.colors.surface)
.child( .child(
HelpPanel::new( HelpPanel::new("onboarding-help-panel", articles(), &search, open)
"onboarding-help-panel", .on_open(move |key, _, cx| {
articles(),
&search,
open,
move |key, _, cx| {
let key = key.cloned(); let key = key.cloned();
change(&opener, cx, |help| help.open = key) change(&opener, cx, |help| help.open = key)
}, })
) .on_contact(|_, _| log::info!("gallery: contact support"))
.on_contact(|_, _| log::info!("gallery: contact support")) .on_shortcuts(move |_, cx| {
.on_shortcuts(move |_, cx| { change(&keys, cx, |help| help.dialog = Some(Dialog::Shortcuts))
change(&keys, cx, |help| help.dialog = Some(Dialog::Shortcuts)) })
}) .on_close(move |_, cx| change(&close, cx, |help| help.hidden = true)),
.on_close(move |_, cx| change(&close, cx, |help| help.hidden = true)),
) )
.into_any_element(), .into_any_element(),
}; };
@@ -295,7 +290,8 @@ pub fn feedback(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.gap_6() .gap_6()
.child(probe( .child(probe(
"onboarding-feedback", "onboarding-feedback",
FeedbackWidget::new("onboarding-feedback", |sentiment, note, _, _| log::info!("gallery: feedback {} {note}", sentiment.key())), FeedbackWidget::new("onboarding-feedback")
.on_send(|sentiment, note, _, _| log::info!("gallery: feedback {} {note}", sentiment.key())),
)) ))
.child( .child(
div() div()
@@ -304,11 +300,12 @@ pub fn feedback(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.flex_col() .flex_col()
.gap_3() .gap_3()
.child( .child(
ContactSupport::new("onboarding-support", topics, move |topic, _, _, cx| { ContactSupport::new("onboarding-support", topics)
.address("help@example.com")
.on_send(move |topic, _, _, cx| {
log::info!("gallery: support about {topic}"); log::info!("gallery: support about {topic}");
change(&done, cx, |help| help.sent = true) change(&done, cx, |help| help.sent = true)
}) }),
.address("help@example.com"),
) )
.when(sent, |column| column.child(div().text_sm().child("Sent. We answer within a day."))), .when(sent, |column| column.child(div().text_sm().child("Sent. We answer within a day."))),
), ),
+77 -90
View File
@@ -38,36 +38,36 @@ fn appearance(prefs: &Entity<Prefs>, fields: &Fields, cx: &App) -> AnyElement {
let [theme, accent, size, density, font] = [(); 5].map(|_| prefs.clone()); let [theme, accent, size, density, font] = [(); 5].map(|_| prefs.clone());
SettingsSection::new("Appearance") SettingsSection::new("Appearance")
.description("How the app looks.") .description("How the app looks.")
.row(SettingsRow::new("Theme").control(ThemeSelector::new( .row(
"settings-theme", SettingsRow::new("Theme").control(
now.appearance, ThemeSelector::new("settings-theme", now.appearance).on_change(
move |next, _, cx| change(&theme, cx, |prefs| prefs.appearance = next), move |next, _, cx| change(&theme, cx, |prefs| prefs.appearance = next),
))) ),
),
)
.row( .row(
SettingsRow::new("Accent color") SettingsRow::new("Accent color")
.description("Buttons, links and what is chosen.") .description("Buttons, links and what is chosen.")
.control(AccentColorPicker::new( .control(
"settings-accent", AccentColorPicker::new("settings-accent", now.accent, presets).on_change(
now.accent, move |next, _, cx| change(&accent, cx, |prefs| prefs.accent = next),
presets, ),
move |next, _, cx| change(&accent, cx, |prefs| prefs.accent = next), ),
)), )
.row(
SettingsRow::new("Font size").control(
FontSizeControl::new("settings-size", now.size, SIZE, (11.0, 20.0))
.on_change(move |next, _, cx| change(&size, cx, |prefs| prefs.size = next)),
),
) )
.row(SettingsRow::new("Font size").control(FontSizeControl::new(
"settings-size",
now.size,
SIZE,
(11.0, 20.0),
move |next, _, cx| change(&size, cx, |prefs| prefs.size = next),
)))
.row( .row(
SettingsRow::new("Density") SettingsRow::new("Density")
.description("How close things sit.") .description("How close things sit.")
.control(DensitySelector::new( .control(
"settings-density", DensitySelector::new("settings-density", now.density).on_change(
now.density, move |next, _, cx| change(&density, cx, |prefs| prefs.density = next),
move |next, _, cx| change(&density, cx, |prefs| prefs.density = next), ),
)), ),
) )
.row( .row(
SettingsRow::new("Font") SettingsRow::new("Font")
@@ -126,32 +126,25 @@ fn keyboard(prefs: &Entity<Prefs>, fields: &Fields, cx: &App) -> AnyElement {
fn network(prefs: &Entity<Prefs>, cx: &App) -> AnyElement { fn network(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
let proxy = prefs.clone(); let proxy = prefs.clone();
SettingsSection::new("Network") SettingsSection::new("Network")
.row(ProxySettings::new( .row(
"settings-proxy", ProxySettings::new("settings-proxy", prefs.read(cx).proxy.clone())
prefs.read(cx).proxy.clone(), .on_apply(move |next, _, cx| change(&proxy, cx, |prefs| prefs.proxy = next)),
move |next, _, cx| change(&proxy, cx, |prefs| prefs.proxy = next), )
))
.into_any_element() .into_any_element()
} }
fn privacy(prefs: &Entity<Prefs>, cx: &App) -> AnyElement { fn privacy(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
let privacy = prefs.clone(); let privacy = prefs.clone();
PrivacySettings::new( PrivacySettings::new("settings-privacy", prefs.read(cx).privacy)
"settings-privacy", .on_change(move |next, _, cx| change(&privacy, cx, |prefs| prefs.privacy = next))
prefs.read(cx).privacy, .into_any_element()
move |next, _, cx| change(&privacy, cx, |prefs| prefs.privacy = next),
)
.into_any_element()
} }
fn notifications(prefs: &Entity<Prefs>, cx: &App) -> AnyElement { fn notifications(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
let notices = prefs.clone(); let notices = prefs.clone();
NotificationSettings::new( NotificationSettings::new("settings-notices", prefs.read(cx).notices.clone())
"settings-notices", .on_change(move |next, _, cx| change(&notices, cx, |prefs| prefs.notices = next))
prefs.read(cx).notices.clone(), .into_any_element()
move |next, _, cx| change(&notices, cx, |prefs| prefs.notices = next),
)
.into_any_element()
} }
fn startup(prefs: &Entity<Prefs>, cx: &App) -> AnyElement { fn startup(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
@@ -161,13 +154,9 @@ fn startup(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
Choice::new("home", "The home page"), Choice::new("home", "The home page"),
Choice::new("blank", "A blank window"), Choice::new("blank", "A blank window"),
]; ];
StartupSettings::new( StartupSettings::new("settings-startup", prefs.read(cx).startup.clone(), pages)
"settings-startup", .on_change(move |next, _, cx| change(&startup, cx, |prefs| prefs.startup = next))
prefs.read(cx).startup.clone(), .into_any_element()
pages,
move |next, _, cx| change(&startup, cx, |prefs| prefs.startup = next),
)
.into_any_element()
} }
fn storage(prefs: &Entity<Prefs>, cx: &App) -> AnyElement { fn storage(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
@@ -186,36 +175,37 @@ fn storage(prefs: &Entity<Prefs>, cx: &App) -> AnyElement {
.child( .child(
SettingsSection::new("Storage") SettingsSection::new("Storage")
.description("What the app keeps on this device.") .description("What the app keeps on this device.")
.row(StorageSettings::new( .row(
"settings-storage", StorageSettings::new("settings-storage", now.stores.clone()).on_clear(
now.stores.clone(), move |key, _, cx| {
move |key, _, cx| { change(&cleared, cx, |prefs| {
change(&cleared, cx, |prefs| { prefs
prefs .stores
.stores .iter_mut()
.iter_mut() .filter(|store| store.key == *key)
.filter(|store| store.key == *key) .for_each(|store| store.bytes = 0)
.for_each(|store| store.bytes = 0) })
}) },
}, ),
)), ),
) )
.child( .child(
SettingsSection::new("Import and export").row(ImportExportSettings::new( SettingsSection::new("Import and export").row(
"settings-files", ImportExportSettings::new("settings-files")
|_, _| log::info!("gallery: settings exported"), .on_export(|_, _| log::info!("gallery: settings exported"))
|path, _, _| log::info!("gallery: settings imported from {}", path.display()), .on_import(|path, _, _| {
)), log::info!("gallery: settings imported from {}", path.display())
}),
),
) )
.child( .child(
SettingsSection::new("Defaults") SettingsSection::new("Defaults")
.description("A larger font size shows here.") .description("A larger font size shows here.")
.row(ResetToDefault::new( .row(
"settings-reset", ResetToDefault::new("settings-reset", changed)
changed, .on_reset(move |_, _, cx| change(&reset, cx, |prefs| prefs.size = SIZE))
move |_, _, cx| change(&reset, cx, |prefs| prefs.size = SIZE), .on_reset_all(move |_, cx| change(&all, cx, |prefs| prefs.size = SIZE)),
move |_, cx| change(&all, cx, |prefs| prefs.size = SIZE), ),
)),
) )
.into_any_element() .into_any_element()
} }
@@ -230,25 +220,22 @@ fn advanced(prefs: &Entity<Prefs>, fields: &Fields, cx: &App) -> AnyElement {
.child( .child(
SettingsSection::new("Features") SettingsSection::new("Features")
.description("AdvancedSettings / FeatureFlags") .description("AdvancedSettings / FeatureFlags")
.row(FeatureFlags::new( .row(
"settings-flags", FeatureFlags::new("settings-flags", now.flags.clone(), &fields.flags)
now.flags.clone(), .on_toggle(move |key, on, _, cx| {
&fields.flags, change(&toggled, cx, |prefs| {
move |key, on, _, cx| { prefs
change(&toggled, cx, |prefs| { .flags
prefs .iter_mut()
.flags .filter(|flag| flag.key == *key)
.iter_mut() .for_each(|flag| flag.on = on)
.filter(|flag| flag.key == *key) })
.for_each(|flag| flag.on = on) }),
}) ),
}, )
)), .child(
DeveloperModeToggle::new("settings-developer", now.developer)
.on_change(move |on, _, cx| change(&developer, cx, |prefs| prefs.developer = on)),
) )
.child(DeveloperModeToggle::new(
"settings-developer",
now.developer,
move |on, _, cx| change(&developer, cx, |prefs| prefs.developer = on),
))
.into_any_element() .into_any_element()
} }
+3 -2
View File
@@ -259,13 +259,14 @@ pub fn settings(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.flex() .flex()
.flex_col() .flex_col()
.gap_6() .gap_6()
.child(div().w(px(360.)).child(SettingsSearch::new("settings-search", entries, &search, .child(div().w(px(360.)).child(SettingsSearch::new("settings-search", entries, &search).on_pick(
move |key, _, cx| change(&found, cx, |prefs| prefs.section = home(key).into()), move |key, _, cx| change(&found, cx, |prefs| prefs.section = home(key).into()),
))) )))
.child(probe( .child(probe(
"settings-layout", "settings-layout",
div().w(px(880.)).child( div().w(px(880.)).child(
SettingsLayout::new("settings-layout", SECTIONS, selected, move |key, _, cx| change(&picked, cx, |prefs| prefs.section = key.clone())) SettingsLayout::new("settings-layout", SECTIONS, selected)
.on_select(move |key, _, cx| change(&picked, cx, |prefs| prefs.section = key.clone()))
.page(page), .page(page),
), ),
)), )),
+2 -2
View File
@@ -26,11 +26,11 @@ fn about() -> AboutDialog {
.description("A component library for GPUI, in light and dark.") .description("A component library for GPUI, in light and dark.")
.link( .link(
"Website", "Website",
"https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components", "https://github.com/ZacharyZhang-NY/Ely-GPUI-Components",
) )
.link( .link(
"License", "License",
"https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components#license", "https://github.com/ZacharyZhang-NY/Ely-GPUI-Components#license",
) )
.copyright("© 2026 Ely contributors") .copyright("© 2026 Ely contributors")
} }
+1 -1
View File
@@ -49,7 +49,7 @@ pub fn editor(_: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.flex() .flex()
.flex_col() .flex_col()
.gap_4() .gap_4()
.child(ThemeEditor::new("theme-editor", draft, |draft, _, cx| apply(draft, cx))) .child(ThemeEditor::new("theme-editor", draft).on_change(|draft, _, cx| apply(draft, cx)))
.child( .child(
div().flex().child( div().flex().child(
Button::new("theme-restore", "Back to Ely's theme") Button::new("theme-restore", "Back to Ely's theme")
+2 -2
View File
@@ -21,7 +21,7 @@ pub fn importing(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
specimens() specimens()
.child(specimen( .child(specimen(
"ThemeImporter", "ThemeImporter",
div().w(px(360.)).child(ThemeImporter::new("theme-importer", |read, _, cx| { div().w(px(360.)).child(ThemeImporter::new("theme-importer").on_apply(|read, _, cx| {
Theme::set_palette(read.mode, Some(read.colors.clone()), cx); Theme::set_palette(read.mode, Some(read.colors.clone()), cx);
Theme::set_mode(read.mode, cx); Theme::set_mode(read.mode, cx);
})), })),
@@ -29,7 +29,7 @@ pub fn importing(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
)) ))
.child(specimen( .child(specimen(
"SyntaxThemePicker", "SyntaxThemePicker",
div().w(px(360.)).child(SyntaxThemePicker::new("theme-syntax", syntax_themes(), name, div().w(px(360.)).child(SyntaxThemePicker::new("theme-syntax", syntax_themes(), name).on_change(
move |picked, _, cx| { move |picked, _, cx| {
let theme = syntax_themes() let theme = syntax_themes()
.into_iter() .into_iter()
+3 -3
View File
@@ -211,9 +211,9 @@ pub fn link(cx: &App) -> impl IntoElement + use<> {
.child( .child(
row() row()
.child("Read the") .child("Read the")
.child(Link::new("guide", "style guide", |_, _, _| { .child(
log::info!("gallery: link") Link::new("guide", "style guide").on_click(|_, _, _| log::info!("gallery: link")),
})) )
.child("before you start."), .child("before you start."),
) )
} }
+6 -7
View File
@@ -201,27 +201,26 @@ pub fn emoji(cx: &App) -> impl IntoElement + use<> {
} }
pub fn latex(cx: &App) -> impl IntoElement + use<> { pub fn latex(cx: &App) -> impl IntoElement + use<> {
let math = let math = |source: &str| Latex::parse(source).expect("gallery formula parses");
|id: &'static str, source: &str| Latex::parse(id, source).expect("gallery formula parses");
section( section(
"Latex / MathInline", "Latex / MathInline",
"TeX typeset by RaTeX, in the text's color.", "A TeX subset: scripts, fractions, roots, Greek.",
cx, cx,
) )
.child( .child(
specimens() specimens()
.gap_10() .gap_10()
.child(specimen("euler", math("euler", r"e^{i\pi} + 1 = 0"), cx)) .child(specimen("euler", math(r"e^{i\pi} + 1 = 0"), cx))
.child(specimen( .child(specimen(
"sum", "sum",
math("sum", r"\sum_{k=1}^{n} k = \frac{n(n+1)}{2}"), math(r"\sum_{k=1}^{n} k = \frac{n(n+1)}{2}"),
cx, cx,
)) ))
.child(specimen( .child(specimen(
"root", "root",
math("root", r"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"), math(r"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"),
cx, cx,
)), )),
) )
.child(code("Latex::parse(\"euler\", r\"e^{i\\pi} + 1 = 0\")?", cx)) .child(code("Latex::parse(r\"e^{i\\pi} + 1 = 0\")?", cx))
} }
+1 -4
View File
@@ -106,10 +106,7 @@ pub fn start() {
if let Some(choice) = EARLY.take() { if let Some(choice) = EARLY.take() {
start.choice = choice; start.choice = choice;
} }
let window = match launch(start, cx) { let window = launch(start, cx);
Ok(window) => window,
Err(error) => return log::error!("gallery: {error:#}"),
};
HOST.with(|host| *host.borrow_mut() = Some((cx.to_async(), window))); HOST.with(|host| *host.borrow_mut() = Some((cx.to_async(), window)));
}); });
} }
+4 -6
View File
@@ -1,12 +1,12 @@
// Writes the showcase catalog for the site: the pinned commit, or the checkout ELY_SHOWCASE names. // Writes the showcase catalog for the site: the pinned commit, or the checkout ELY_SHOWCASE names.
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join, resolve } from "node:path"; import { join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url"; import { fileURLToPath, pathToFileURL } from "node:url";
import { Marked } from "marked"; import { Marked } from "marked";
const REPO = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Showcases"; const REPO = "ZacharyZhang-NY/Ely-GPUI-Showcases";
const site = fileURLToPath(new URL("..", import.meta.url)); const site = fileURLToPath(new URL("..", import.meta.url));
const pub = join(site, "public", "showcase"); const pub = join(site, "public", "showcase");
const out = join(site, "src", "showcase.json"); const out = join(site, "src", "showcase.json");
@@ -15,14 +15,12 @@ const stamp = join(pub, ".commit");
/** The catalog at `commit`, unpacked with its validator's packages. */ /** The catalog at `commit`, unpacked with its validator's packages. */
async function fetchCatalog(commit) { async function fetchCatalog(commit) {
const work = mkdtempSync(join(tmpdir(), "ely-showcase-")); const work = mkdtempSync(join(tmpdir(), "ely-showcase-"));
const url = `${REPO}/archive/${commit}.tar.gz`; const url = `https://codeload.github.com/${REPO}/tar.gz/${commit}`;
const res = await fetch(url); const res = await fetch(url);
if (!res.ok) throw new Error(`showcase: ${url} answered ${res.status}`); if (!res.ok) throw new Error(`showcase: ${url} answered ${res.status}`);
writeFileSync(join(work, "catalog.tar.gz"), Buffer.from(await res.arrayBuffer())); writeFileSync(join(work, "catalog.tar.gz"), Buffer.from(await res.arrayBuffer()));
execFileSync("tar", ["-xzf", "catalog.tar.gz"], { cwd: work }); execFileSync("tar", ["-xzf", "catalog.tar.gz"], { cwd: work });
const [top] = readdirSync(work, { withFileTypes: true }).filter((e) => e.isDirectory()); const root = join(work, `${REPO.split("/")[1]}-${commit}`);
if (!top) throw new Error(`showcase: ${url} held no folder`);
const root = join(work, top.name);
const pnpm = process.env.npm_execpath; const pnpm = process.env.npm_execpath;
if (!pnpm) throw new Error("showcase: run through pnpm, which installs the catalog's validator"); if (!pnpm) throw new Error("showcase: run through pnpm, which installs the catalog's validator");
execFileSync(process.execPath, [pnpm, "install", "--frozen-lockfile", "--ignore-scripts", "--silent"], { cwd: root, stdio: "inherit" }); execFileSync(process.execPath, [pnpm, "install", "--frozen-lockfile", "--ignore-scripts", "--silent"], { cwd: root, stdio: "inherit" });
+1 -1
View File
@@ -1 +1 @@
ca872b7708d200dafb3027364122aa5895e43299 0f8f5b9cc392d4741c239b1c1c0f6071dfc6faa5
+3 -3
View File
@@ -36,8 +36,8 @@ function Header() {
<button className="tool" aria-label={dark ? "Light mode" : "Dark mode"} onClick={() => setMode(dark ? "light" : "dark")}> <button className="tool" aria-label={dark ? "Light mode" : "Dark mode"} onClick={() => setMode(dark ? "light" : "dark")}>
<Icon name={dark ? "Sun" : "Moon"} /> <Icon name={dark ? "Sun" : "Moon"} />
</button> </button>
<a className="tool" href={REPO} rel="noopener" aria-label="Ely on Gitea"> <a className="tool" href={REPO} rel="noopener" aria-label="Ely on GitHub">
<Icon name="Gitea" /> <Icon name="GitHub" />
</a> </a>
</div> </div>
</header> </header>
@@ -47,7 +47,7 @@ function Header() {
function Footer() { function Footer() {
return ( return (
<footer className="foot"> <footer className="foot">
<span>MIT</span> <span>MIT or Apache-2.0</span>
<a href={REPO} rel="noopener"> <a href={REPO} rel="noopener">
Source Source
</a> </a>
+1 -1
View File
@@ -26,7 +26,7 @@ export interface Catalog {
export type Sort = "newest" | "stars"; export type Sort = "newest" | "stars";
export const SUBMIT = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Showcases#submit-an-app"; export const SUBMIT = "https://github.com/ZacharyZhang-NY/Ely-GPUI-Showcases#submit-an-app";
/** Newest first, then most stars; or most stars, then newest. */ /** Newest first, then most stars; or most stars, then newest. */
export function sorted(apps: App[], sort: Sort): App[] { export function sorted(apps: App[], sort: Sort): App[] {
+1 -1
View File
@@ -37,6 +37,6 @@ export function search(query: string): Chapter[] {
}); });
} }
export const REPO = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components"; export const REPO = "https://github.com/ZacharyZhang-NY/Ely-GPUI-Components";
export const pad = (n: number) => String(n).padStart(2, "0"); export const pad = (n: number) => String(n).padStart(2, "0");
+1 -1
View File
@@ -4,7 +4,7 @@ Every Ely control that takes a press is a Tab stop. Focus moves with Tab and Shi
## What `init` binds ## What `init` binds
`init` binds `tab` to the `FocusNext` action and `shift-tab` to `FocusPrev`, under `ElyFocus`, the key context every `FocusScope` sets. A `FocusScope` turns the two actions into focus moves. A context nested deeper keeps its own Tab, as a code editor that indents does, whichever library's `init` ran last. `init` binds `tab` to the `FocusNext` action and `shift-tab` to `FocusPrev`, with no key context, so they reach every window. A `FocusScope` turns the two actions into focus moves.
## The root scope ## The root scope
+2 -2
View File
@@ -6,8 +6,8 @@ An Ely app is a GPUI app with four more parts: Ely's assets, one call to `init`,
| Part | Why | | Part | Why |
| --- | --- | | --- | --- |
| `gpui_platform::application().with_assets(Assets)` | Ely embeds its icons and fonts. `init` returns an error when the source cannot load one of them. | | `gpui_platform::application().with_assets(Assets)` | Ely embeds its icons and fonts. `init` panics without them. |
| `ely_gpui_component::init(cx)?` | Call once, before any window. It returns an error if the asset source fails. It registers the fonts, sets the theme, and binds Tab, Shift-Tab and the keys of text fields, the code editor, documents and, natively, the terminal. | | `ely_gpui_component::init(cx)` | Call once, before any window. It registers the fonts, sets the theme, and binds Tab, Shift-Tab and the keys of text fields, the code editor, documents and, natively, the terminal. |
| A root `FocusScope` | `FocusScope::new(&handle).root()` owns Tab for the window and takes focus back when the focused element leaves the tree. Focus its handle when the window opens. | | A root `FocusScope` | `FocusScope::new(&handle).root()` owns Tab for the window and takes focus back when the focused element leaves the tree. Focus its handle when the window opens. |
| `cx.theme()` | Colors, sizes and radii come from the theme. Components read it; an app's own boxes read it too. | | `cx.theme()` | Colors, sizes and radii come from the theme. Components read it; an app's own boxes read it too. |
-46
View File
@@ -1,46 +0,0 @@
# GPUI Kit
Ely runs beside Longbridge's [GPUI Kit](https://gpui-kit.com) in one app and one window. Both build on one GPUI: Ely pins `gpui-pre =0.3.8`, the snapshot Kit 0.7.1 pins.
## Add the dependencies
```toml
[dependencies]
gpui-kit = "0.7"
ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components" }
```
Kit brings GPUI and puts it at its root, so the app writes `use gpui_kit::*` and lists no `gpui` line of its own. Ely follows Kit's default GPUI.
In an app, Cargo picks the newest Kit that shares Ely's GPUI. A Kit release on another `gpui-pre` waits until Ely's pin follows. Ely's CI asks for Kit's newest release on every push and fails on such a release, naming `gpui-pre`.
## Start both
| Step | Why |
| --- | --- |
| `gpui_kit::application().with_assets(Assets::before(gpui_kit::assets::Assets))` | GPUI takes one asset source. Ely's files load from Ely, every other path from Kit. `ely_gpui_component::init` returns an error if one of Ely's files does not load. |
| `gpui_kit::init(cx)` and `ely_gpui_component::init(cx)?` | In either order. |
| `gpui_kit::open_window(options, cx, build)` | Kit's `Root` hosts Kit's dialogs, sheets and notifications. |
| `FocusScope::new(&handle).root()` around the view, `handle` focused | Ely's Tab, focus return and overlays live in the scope. |
## Focus
Tab and Shift-Tab run through Ely's and Kit's stops in one order. Ely binds them under `ElyFocus`, the key context of its scopes, so Kit's code editor keeps Tab for indenting whichever `init` ran last. A Kit dialog opened from an Ely button keeps Tab inside and hands focus back when it closes; so does an Ely dialog over Kit's fields.
## Two themes
Each library keeps its own theme. A switch turns both:
| Ely | Kit |
| --- | --- |
| `Theme::set_mode(Mode::Dark, cx)` | `Theme::change(ThemeMode::Dark, Some(window), cx)` |
Locales work the same way: Ely reads its `I18n` global, Kit its `gpui_kit::component::set_locale`.
## Inspector
In debug builds Kit's `init` installs Kit's inspector. Call `ely_gpui_component::tooling::install_inspector(cx)` after it to draw Ely's.
## A whole app
[`compat/kit/examples/start.rs`](https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components/src/branch/main/compat/kit/examples/start.rs) puts Ely's heading and button beside Kit's field and dialog, and one button turns both themes. From a clone, run it with `cargo run --manifest-path compat/kit/Cargo.toml --example start`. The crate's tests hold each claim on this page.
+1 -1
View File
@@ -6,7 +6,7 @@ import { Marked, type Tokens } from "marked";
const at = (relative: string) => fileURLToPath(new URL(relative, import.meta.url)); const at = (relative: string) => fileURLToPath(new URL(relative, import.meta.url));
/** The guides in reading order, by file name. */ /** The guides in reading order, by file name. */
const ORDER = ["introduction", "installation", "getting-started", "gpui-kit", "theme", "focus", "motion", "icons", "overlays", "rendering", "i18n", "webassembly", "testing"]; const ORDER = ["introduction", "installation", "getting-started", "theme", "focus", "motion", "icons", "overlays", "rendering", "i18n", "webassembly", "testing"];
export interface Guide { export interface Guide {
/** The path segment; the introduction has none. */ /** The path segment; the introduction has none. */
+4 -4
View File
@@ -1,6 +1,6 @@
# Icons, fonts and assets # Icons, fonts and assets
`Assets` embeds Ely's icons and fonts in the binary. Pass it to `with_assets`, or `Assets::before(other)` to serve Ely's files first and every other path from another source; `init` checks that each of Ely's files loads through the app's source, then registers the fonts. `Assets` embeds Ely's icons and fonts in the binary. Pass it to `with_assets`; `init` registers the fonts and checks that `icons/check.svg` loads.
## Icons ## Icons
@@ -42,13 +42,13 @@ A name rule beats an extension rule, and both beat Ely's map.
## An app's own assets ## An app's own assets
GPUI takes one asset source. An app with files of its own, or another library's, passes `Assets::before(theirs)`: Ely's paths load from Ely, every other path from `theirs`, whose errors pass through. `init` returns an error naming the first of Ely's files the source cannot load. GPUI takes one asset source. An app with files of its own writes an `AssetSource` that answers its own paths and hands every other path to `Assets`. `init` panics unless `icons/check.svg` loads through it.
Use `Icon::from_path("app-icons/mark.svg")` to draw one of those SVG assets with Ely's themed sizing and color. It also accepts an owned `String` or `SharedString`. Paths are asset keys resolved by `AssetSource`; a path it lacks logs an error on first draw and shows a red broken-picture icon. SVGs render as single-color masks; `IconName` and `IconPicker` continue to list the bundled icons. Use `Icon::from_path("app-icons/mark.svg")` to draw one of those SVG assets with Ely's themed sizing and color. It also accepts an owned `String` or `SharedString`. Paths are asset keys resolved by `AssetSource`; a path it lacks panics on first draw. SVGs render as single-color masks; `IconName` and `IconPicker` continue to list the bundled icons.
Compatible single-color SVGs can come from [Lucide](https://lucide.dev/), [Hugeicons](https://github.com/hugeicons/hugeicons), [Phosphor](https://github.com/phosphor-icons/core), [Remix Icon](https://github.com/Remix-Design/RemixIcon), or [Tabler](https://github.com/tabler/tabler-icons). The app supplies its chosen files and preserves their licenses; Ely does not download or register those catalogs. Compatible single-color SVGs can come from [Lucide](https://lucide.dev/), [Hugeicons](https://github.com/hugeicons/hugeicons), [Phosphor](https://github.com/phosphor-icons/core), [Remix Icon](https://github.com/Remix-Design/RemixIcon), or [Tabler](https://github.com/tabler/tabler-icons). The app supplies its chosen files and preserves their licenses; Ely does not download or register those catalogs.
The example below includes its asset source. Copy [`examples/assets/mark.svg`](https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components/src/branch/main/examples/assets/mark.svg) with it, or supply your own SVG. The example below includes its asset source. Copy [`examples/assets/mark.svg`](https://github.com/ZacharyZhang-NY/Ely-GPUI-Components/blob/main/examples/assets/mark.svg) with it, or supply your own SVG.
## A window of icons ## A window of icons
+9 -8
View File
@@ -8,19 +8,19 @@
## Add the dependencies ## Add the dependencies
Ely is not on crates.io. Depend on it by git, with GPUI at the snapshot Ely pins. `gpui-pre` republishes Zed's GPUI on crates.io, and GPUI Kit pins the same one: Ely is not on crates.io. Depend on it by git, with GPUI from Zed's repository at the revision Ely pins:
```toml ```toml
[dependencies] [dependencies]
ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components" } ely-gpui-component = { git = "https://github.com/ZacharyZhang-NY/Ely-GPUI-Components" }
gpui = { package = "gpui-pre", version = "=0.3.8" } gpui = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b" }
gpui_platform = { package = "gpui-pre-platform", version = "=0.3.8", features = ["font-kit"] } gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b", features = ["font-kit"] }
``` ```
| Line | Why | | Line | Why |
| --- | --- | | --- | --- |
| `ely-gpui-component` | The library. `Cargo.lock` records the commit; add `rev = "<commit>"` to hold it in `Cargo.toml` as well. | | `ely-gpui-component` | The library. `Cargo.lock` records the commit; add `rev = "<commit>"` to hold it in `Cargo.toml` as well. |
| `gpui` | The same snapshot as Ely's, under the name `gpui`. Another builds a second GPUI, and its types do not match the ones Ely takes. | | `gpui` | The same revision as Ely's. Another revision builds a second GPUI, and its types do not match the ones Ely takes. |
| `gpui_platform` | Opens windows on each platform. Without `font-kit`, macOS falls back to GPUI's no-op text system and draws no text. | | `gpui_platform` | Opens windows on each platform. Without `font-kit`, macOS falls back to GPUI's no-op text system and draws no text. |
## Shaders ## Shaders
@@ -41,8 +41,9 @@ A window opens with a counter and a Press button. From a clone of the repository
| Symptom | Cause and fix | | Symptom | Cause and fix |
| --- | --- | | --- | --- |
| `init` error: `ely: the asset source lacks N of Ely's files, first <path>: <reason>` | The app's source cannot load every file Ely bundles. Pass `Assets`, or `Assets::before(yours)` when the app has a source of its own. | | Panic: `` ely: pass `ely_gpui_component::Assets` to `Application::with_assets` `` | `init` found no Ely assets. Start the app with `gpui_platform::application().with_assets(Assets)`. |
| `init` error: `ely: embedded fonts failed to register` | GPUI's text system refused Ely's fonts. File a bug with the log. | | Panic: `ely: asset source failed` | The app's asset source returned an error. The panic carries it. |
| Panic: `ely: embedded fonts failed to register` | GPUI's text system refused Ely's fonts. File a bug with the log. |
| Windows open with no text on macOS | `gpui_platform` lacks the `font-kit` feature. | | Windows open with no text on macOS | `gpui_platform` lacks the `font-kit` feature. |
| `mismatched types`, with the note ``there are multiple different versions of crate `gpui` in the dependency graph`` | `gpui` names another snapshot or a git source. Use `gpui-pre` at `=0.3.8`. | | `mismatched types`, with the note ``there are multiple different versions of crate `gpui` in the dependency graph`` | `gpui` points at a revision other than Ely's. Use `1a28cff4b409169bac058bca40dfbfeb7621d19b`. |
| `metal shader compilation failed` | Default features are off and Xcode's Metal compiler is missing. Keep `runtime_shaders`. | | `metal shader compilation failed` | Default features are off and Xcode's Metal compiler is missing. Keep `runtime_shaders`. |
+1 -1
View File
@@ -33,4 +33,4 @@ The example opens a window with one counter and one Button. `cargo run --example
## License ## License
MIT. The icons are Lucide's, under ISC. Inter, JetBrains Mono and IBM Plex Sans are under the SIL Open Font License 1.1. MIT or Apache-2.0, at your option. The icons are Lucide's, under ISC. Inter, JetBrains Mono and IBM Plex Sans are under the SIL Open Font License 1.1.
+5 -5
View File
@@ -8,7 +8,7 @@ Ely tests itself on GPUI's test platform: every chapter has tests, and `cargo te
## Testing an app that uses Ely ## Testing an app that uses Ely
GPUI's `TestAppContext` serves no assets, so `ely_gpui_component::init` returns an error there. Call `ely_gpui_component::init_for_tests(cx)` instead, under the `test-support` feature: it sets the theme and key bindings without assets or fonts. GPUI's `TestAppContext` serves no assets, so `ely_gpui_component::init` panics there, and Ely's theme has no setup outside `init`. An app cannot draw Ely's components in a GPUI unit test today. Test the app's own state there, and see Ely's components in the gallery and in Ely's own tests.
## Writing a test inside Ely ## Writing a test inside Ely
@@ -25,7 +25,7 @@ GPUI's `TestAppContext` serves no assets, so `ely_gpui_component::init` returns
## Tests to read ## Tests to read
- [Layers](https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components/src/branch/main/src/primitives/tests.rs): what is raised inside a raise lies over it, and keeps its state. - [Layers](https://github.com/ZacharyZhang-NY/Ely-GPUI-Components/blob/main/src/primitives/tests.rs): what is raised inside a raise lies over it, and keeps its state.
- [Feedback](https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components/src/branch/main/src/feedback/tests/mod.rs): toasts and alerts, and focus handed to the root when a focused toast leaves. - [Feedback](https://github.com/ZacharyZhang-NY/Ely-GPUI-Components/blob/main/src/feedback/tests/mod.rs): toasts and alerts, and focus handed to the root when a focused toast leaves.
- [Tables](https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components/src/branch/main/src/tables/tests/mod.rs): clicks, double presses, Tab across a grid. - [Tables](https://github.com/ZacharyZhang-NY/Ely-GPUI-Components/blob/main/src/tables/tests/mod.rs): clicks, double presses, Tab across a grid.
- [Internationalization](https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components/src/branch/main/src/i18n/tests/mod.rs): catalogs, locales and a mirrored row. - [Internationalization](https://github.com/ZacharyZhang-NY/Ely-GPUI-Components/blob/main/src/i18n/tests/mod.rs): catalogs, locales and a mirrored row.
+1 -1
View File
@@ -13,7 +13,7 @@ RUSTC_BOOTSTRAP=1 cargo check --lib --target wasm32-unknown-unknown
## Start an app in the browser ## Start an app in the browser
The gallery's [web entry](https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components/src/branch/main/examples/gallery/web.rs) shows the steps: The gallery's [web entry](https://github.com/ZacharyZhang-NY/Ely-GPUI-Components/blob/main/examples/gallery/web.rs) shows the steps:
| Step | Why | | Step | Why |
| --- | --- | | --- | --- |
+5 -5
View File
@@ -9,14 +9,14 @@ import VolumeCross from "reicon/icons/VolumeCross";
const set = { ArrowRightUp, Maximize, Minimize, Moon, Refresh, Sound, Sun, VolumeCross }; const set = { ArrowRightUp, Maximize, Minimize, Moon, Refresh, Sound, Sun, VolumeCross };
// simple-icons' Gitea mark (CC0), padded to sit like Reicon's. // simple-icons' GitHub mark (CC0), padded to sit like Reicon's.
const GITEA = const GITHUB =
'<svg viewBox="-3 -3 30 30" width="24" height="24" fill="currentColor"><path d="M4.209 4.603c-.247 0-.525.02-.84.088-.333.07-1.28.283-2.054 1.027C-.403 7.25.035 9.685.089 10.052c.065.446.263 1.687 1.21 2.768 1.749 2.141 5.513 2.092 5.513 2.092s.462 1.103 1.168 2.119c.955 1.263 1.936 2.248 2.89 2.367 2.406 0 7.212-.004 7.212-.004s.458.004 1.08-.394c.535-.324 1.013-.893 1.013-.893s.492-.527 1.18-1.73c.21-.37.385-.729.538-1.068 0 0 2.107-4.471 2.107-8.823-.042-1.318-.367-1.55-.443-1.627-.156-.156-.366-.153-.366-.153s-4.475.252-6.792.306c-.508.011-1.012.023-1.512.027v4.474l-.634-.301c0-1.39-.004-4.17-.004-4.17-1.107.016-3.405-.084-3.405-.084s-5.399-.27-5.987-.324c-.187-.011-.401-.032-.648-.032zm.354 1.832h.111s.271 2.269.6 3.597C5.549 11.147 6.22 13 6.22 13s-.996-.119-1.641-.348c-.99-.324-1.409-.714-1.409-.714s-.73-.511-1.096-1.52C1.444 8.73 2.021 7.7 2.021 7.7s.32-.859 1.47-1.145c.395-.106.863-.12 1.072-.12zm8.33 2.554c.26.003.509.127.509.127l.868.422-.529 1.075a.686.686 0 0 0-.614.359.685.685 0 0 0 .072.756l-.939 1.924a.69.69 0 0 0-.66.527.687.687 0 0 0 .347.763.686.686 0 0 0 .867-.206.688.688 0 0 0-.069-.882l.916-1.874a.667.667 0 0 0 .237-.02.657.657 0 0 0 .271-.137 8.826 8.826 0 0 1 1.016.512.761.761 0 0 1 .286.282c.073.21-.073.569-.073.569-.087.29-.702 1.55-.702 1.55a.692.692 0 0 0-.676.477.681.681 0 1 0 1.157-.252c.073-.141.141-.282.214-.431.19-.397.515-1.16.515-1.16.035-.066.218-.394.103-.814-.095-.435-.48-.638-.48-.638-.467-.301-1.116-.58-1.116-.58s0-.156-.042-.27a.688.688 0 0 0-.148-.241l.516-1.062 2.89 1.401s.48.218.583.619c.073.282-.019.534-.069.657-.24.587-2.1 4.317-2.1 4.317s-.232.554-.748.588a1.065 1.065 0 0 1-.393-.045l-.202-.08-4.31-2.1s-.417-.218-.49-.596c-.083-.31.104-.691.104-.691l2.073-4.272s.183-.37.466-.497a.855.855 0 0 1 .35-.077z"/></svg>'; '<svg viewBox="-3 -3 30 30" width="24" height="24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>';
export type IconName = keyof typeof set | "Gitea"; export type IconName = keyof typeof set | "GitHub";
/** An 18px glyph at 1.5px in a 24px box. */ /** An 18px glyph at 1.5px in a 24px box. */
export function Icon({ name }: { name: IconName }) { export function Icon({ name }: { name: IconName }) {
const svg = name === "Gitea" ? GITEA : set[name].toSvg({ size: 24 }); const svg = name === "GitHub" ? GITHUB : set[name].toSvg({ size: 24 });
return <span className="icon" aria-hidden="true" dangerouslySetInnerHTML={{ __html: svg }} />; return <span className="icon" aria-hidden="true" dangerouslySetInnerHTML={{ __html: svg }} />;
} }
-9
View File
@@ -4,15 +4,6 @@ set -eu
cargo fmt --check cargo fmt --check
cargo clippy --all-targets --features test-support -- -D warnings cargo clippy --all-targets --features test-support -- -D warnings
cargo test --lib --features test-support --quiet cargo test --lib --features test-support --quiet
# Ely beside the newest GPUI Kit: a release that moves Kit's gpui pin fails to resolve here.
kit="--manifest-path compat/kit/Cargo.toml"
newest=$(cargo search gpui-kit --limit 1 --color never | sed -n 's/^gpui-kit = "\([^"]*\)".*/\1/p')
[ -n "$newest" ] || { echo "crates.io named no gpui-kit release" >&2; exit 1; }
cargo generate-lockfile $kit
cargo update $kit -p gpui-kit --precise "$newest"
cargo fmt $kit --check
cargo clippy $kit --all-targets --target-dir target/kit -- -D warnings
cargo test $kit --target-dir target/kit --quiet
rustup target list --installed | grep -qx wasm32-unknown-unknown || { rustup target list --installed | grep -qx wasm32-unknown-unknown || {
echo "the web lint needs wasm32: rustup target add wasm32-unknown-unknown" >&2 echo "the web lint needs wasm32: rustup target add wasm32-unknown-unknown" >&2
exit 1 exit 1
+18 -12
View File
@@ -24,20 +24,17 @@ pub struct TwoFactorInput {
id: ElementId, id: ElementId,
failed: usize, failed: usize,
busy: bool, busy: bool,
on_code: OnText, on_code: Option<OnText>,
on_recovery: Option<Run>, on_recovery: Option<Run>,
} }
impl TwoFactorInput { impl TwoFactorInput {
pub fn new( pub fn new(id: impl Into<ElementId>) -> Self {
id: impl Into<ElementId>,
on_code: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
failed: 0, failed: 0,
busy: false, busy: false,
on_code: Rc::new(on_code), on_code: None,
on_recovery: None, on_recovery: None,
} }
} }
@@ -54,6 +51,14 @@ impl TwoFactorInput {
self self
} }
pub fn on_code(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_code = Some(Rc::new(handler));
self
}
/// Shows "Use a recovery code". /// Shows "Use a recovery code".
pub fn on_recovery(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { pub fn on_recovery(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_recovery = Some(Rc::new(handler)); self.on_recovery = Some(Rc::new(handler));
@@ -64,7 +69,9 @@ impl TwoFactorInput {
impl RenderOnce for TwoFactorInput { impl RenderOnce for TwoFactorInput {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id; let id = self.id;
let on_code = self.on_code; let on_code = self
.on_code
.unwrap_or_else(|| panic!("two-factor input {id:?} has no on_code"));
let theme = cx.theme(); let theme = cx.theme();
let note = match (self.busy, self.failed) { let note = match (self.busy, self.failed) {
(true, _) => Some( (true, _) => Some(
@@ -106,11 +113,10 @@ impl RenderOnce for TwoFactorInput {
) )
.children(note) .children(note)
.children(self.on_recovery.map(|run| { .children(self.on_recovery.map(|run| {
div().child(Link::new( div().child(
(id.clone(), "recovery"), Link::new((id.clone(), "recovery"), "Use a recovery code")
"Use a recovery code", .on_click(move |_, window, cx| run(window, cx)),
move |_, window, cx| run(window, cx), )
))
})) }))
} }
} }
+39 -16
View File
@@ -33,26 +33,20 @@ pub struct ApiKeyManager {
id: ElementId, id: ElementId,
keys: Vec<ApiKey>, keys: Vec<ApiKey>,
secret: Option<SharedString>, secret: Option<SharedString>,
on_create: OnKey, on_create: Option<OnKey>,
on_revoke: OnKey, on_revoke: Option<OnKey>,
on_dismiss: Run, on_dismiss: Option<Run>,
} }
impl ApiKeyManager { impl ApiKeyManager {
pub fn new( pub fn new(id: impl Into<ElementId>, keys: impl IntoIterator<Item = ApiKey>) -> Self {
id: impl Into<ElementId>,
keys: impl IntoIterator<Item = ApiKey>,
on_create: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
on_revoke: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
on_dismiss: impl Fn(&mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
keys: keys.into_iter().collect(), keys: keys.into_iter().collect(),
secret: None, secret: None,
on_create: Rc::new(on_create), on_create: None,
on_revoke: Rc::new(on_revoke), on_revoke: None,
on_dismiss: Rc::new(on_dismiss), on_dismiss: None,
} }
} }
@@ -61,13 +55,40 @@ impl ApiKeyManager {
self.secret = Some(secret.into()); self.secret = Some(secret.into());
self self
} }
/// Runs with the new key's name.
pub fn on_create(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_create = Some(Rc::new(handler));
self
}
pub fn on_revoke(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_revoke = Some(Rc::new(handler));
self
}
/// Runs on Done under a secret; the owner clears it.
pub fn on_dismiss(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_dismiss = Some(Rc::new(handler));
self
}
} }
impl RenderOnce for ApiKeyManager { impl RenderOnce for ApiKeyManager {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id; let id = self.id;
let on_create = self.on_create; let on_create = self
let on_revoke = self.on_revoke; .on_create
.unwrap_or_else(|| panic!("api key manager {id:?} has no on_create"));
let on_revoke = self
.on_revoke
.unwrap_or_else(|| panic!("api key manager {id:?} has no on_revoke"));
let name = field(&id, "name", "A name, such as Deploys", false, window, cx); let name = field(&id, "name", "A name, such as Deploys", false, window, cx);
let named: SharedString = name.read(cx).text().trim().to_string().into(); let named: SharedString = name.read(cx).text().trim().to_string().into();
let ready = !named.is_empty(); let ready = !named.is_empty();
@@ -81,7 +102,9 @@ impl RenderOnce for ApiKeyManager {
let theme = cx.theme(); let theme = cx.theme();
let mono = theme.mono_family.clone(); let mono = theme.mono_family.clone();
let secret = self.secret.map(|secret| { let secret = self.secret.map(|secret| {
let dismiss = self.on_dismiss.clone(); let dismiss = self.on_dismiss.clone().unwrap_or_else(|| {
panic!("api key manager {id:?} shows a secret with no on_dismiss")
});
Callout::new(Severity::Warning) Callout::new(Severity::Warning)
.title("Copy this key now. It will not show again.") .title("Copy this key now. It will not show again.")
.child( .child(
+14 -13
View File
@@ -48,7 +48,7 @@ pub(super) fn aside(
.text_size(theme.text_size(TextSize::Sm)) .text_size(theme.text_size(TextSize::Sm))
.text_color(theme.colors.fg_muted) .text_color(theme.colors.fg_muted)
.child(words) .child(words)
.child(Link::new(id, link, move |_, window, cx| run(window, cx))) .child(Link::new(id, link).on_click(move |_, window, cx| run(window, cx)))
} }
/// What a sign-in hands its owner. /// What a sign-in hands its owner.
@@ -67,21 +67,18 @@ pub struct LoginForm {
id: ElementId, id: ElementId,
error: Option<SharedString>, error: Option<SharedString>,
busy: bool, busy: bool,
on_submit: OnLogin, on_submit: Option<OnLogin>,
on_forgot: Option<Run>, on_forgot: Option<Run>,
on_sign_up: Option<Run>, on_sign_up: Option<Run>,
} }
impl LoginForm { impl LoginForm {
pub fn new( pub fn new(id: impl Into<ElementId>) -> Self {
id: impl Into<ElementId>,
on_submit: impl Fn(&Login, &mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
error: None, error: None,
busy: false, busy: false,
on_submit: Rc::new(on_submit), on_submit: None,
on_forgot: None, on_forgot: None,
on_sign_up: None, on_sign_up: None,
} }
@@ -99,6 +96,11 @@ impl LoginForm {
self self
} }
pub fn on_submit(mut self, handler: impl Fn(&Login, &mut Window, &mut App) + 'static) -> Self {
self.on_submit = Some(Rc::new(handler));
self
}
/// Shows "Forgot password?". /// Shows "Forgot password?".
pub fn on_forgot(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { pub fn on_forgot(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_forgot = Some(Rc::new(handler)); self.on_forgot = Some(Rc::new(handler));
@@ -125,7 +127,9 @@ impl RenderOnce for LoginForm {
}; };
let remembered = login.remember; let remembered = login.remember;
let ready = is_email(&login.email) && !login.password.is_empty() && !self.busy; let ready = is_email(&login.email) && !login.password.is_empty() && !self.busy;
let on_submit = self.on_submit; let on_submit = self
.on_submit
.unwrap_or_else(|| panic!("login form {id:?} has no on_submit"));
let submit: Run = Rc::new(move |window, cx| { let submit: Run = Rc::new(move |window, cx| {
log::info!("login form: sign in"); log::info!("login form: sign in");
on_submit(&login, window, cx); on_submit(&login, window, cx);
@@ -166,11 +170,8 @@ impl RenderOnce for LoginForm {
}), }),
) )
.children(self.on_forgot.map(|run| { .children(self.on_forgot.map(|run| {
Link::new( Link::new((id.clone(), "forgot"), "Forgot password?")
(id.clone(), "forgot"), .on_click(move |_, window, cx| run(window, cx))
"Forgot password?",
move |_, window, cx| run(window, cx),
)
})), })),
) )
.children( .children(
+27 -16
View File
@@ -33,7 +33,7 @@ struct Step {
words: Words, words: Words,
sent: bool, sent: bool,
busy: bool, busy: bool,
on_send: OnText, on_send: Option<OnText>,
back: Way, back: Way,
other: Way, other: Way,
} }
@@ -52,6 +52,7 @@ fn email_step(step: Step, window: &mut Window, cx: &mut App) -> AnyElement {
let email = field(&id, "email", "name@example.com", false, window, cx); let email = field(&id, "email", "name@example.com", false, window, cx);
let address: SharedString = email.read(cx).text().trim().to_string().into(); let address: SharedString = email.read(cx).text().trim().to_string().into();
let ready = is_email(&address) && !busy; let ready = is_email(&address) && !busy;
let on_send = on_send.unwrap_or_else(|| panic!("email step {id:?} has no on_send"));
let send: Run = { let send: Run = {
let address = address.clone(); let address = address.clone();
Rc::new(move |window, cx| { Rc::new(move |window, cx| {
@@ -71,9 +72,7 @@ fn email_step(step: Step, window: &mut Window, cx: &mut App) -> AnyElement {
) )
}; };
let back = back.map(|(label, run)| { let back = back.map(|(label, run)| {
Link::new((id.clone(), "back"), label, move |_, window, cx| { Link::new((id.clone(), "back"), label).on_click(move |_, window, cx| run(window, cx))
run(window, cx)
})
}); });
if sent { if sent {
let other = other.map(|(label, run)| { let other = other.map(|(label, run)| {
@@ -150,20 +149,17 @@ pub struct MagicLinkForm {
id: ElementId, id: ElementId,
sent: bool, sent: bool,
busy: bool, busy: bool,
on_send: OnText, on_send: Option<OnText>,
on_other: Option<Run>, on_other: Option<Run>,
} }
impl MagicLinkForm { impl MagicLinkForm {
pub fn new( pub fn new(id: impl Into<ElementId>) -> Self {
id: impl Into<ElementId>,
on_send: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
sent: false, sent: false,
busy: false, busy: false,
on_send: Rc::new(on_send), on_send: None,
on_other: None, on_other: None,
} }
} }
@@ -180,6 +176,15 @@ impl MagicLinkForm {
self self
} }
/// Runs with the address, for the first link and each one after.
pub fn on_send(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_send = Some(Rc::new(handler));
self
}
/// Shows "Use another email" once sent; the owner clears `sent`. /// Shows "Use another email" once sent; the owner clears `sent`.
pub fn on_other(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { pub fn on_other(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_other = Some(Rc::new(handler)); self.on_other = Some(Rc::new(handler));
@@ -217,20 +222,17 @@ pub struct ForgotPassword {
id: ElementId, id: ElementId,
sent: bool, sent: bool,
busy: bool, busy: bool,
on_send: OnText, on_send: Option<OnText>,
on_back: Option<Run>, on_back: Option<Run>,
} }
impl ForgotPassword { impl ForgotPassword {
pub fn new( pub fn new(id: impl Into<ElementId>) -> Self {
id: impl Into<ElementId>,
on_send: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
sent: false, sent: false,
busy: false, busy: false,
on_send: Rc::new(on_send), on_send: None,
on_back: None, on_back: None,
} }
} }
@@ -247,6 +249,15 @@ impl ForgotPassword {
self self
} }
/// Runs with the address, for the first link and each one after.
pub fn on_send(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_send = Some(Rc::new(handler));
self
}
/// Shows "Back to sign in". /// Shows "Back to sign in".
pub fn on_back(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { pub fn on_back(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_back = Some(Rc::new(handler)); self.on_back = Some(Rc::new(handler));
+17 -9
View File
@@ -14,7 +14,7 @@ pub struct OAuthButtons {
id: ElementId, id: ElementId,
providers: Vec<(SharedString, SharedString)>, providers: Vec<(SharedString, SharedString)>,
busy: Option<SharedString>, busy: Option<SharedString>,
on_pick: OnKey, on_pick: Option<OnKey>,
} }
impl OAuthButtons { impl OAuthButtons {
@@ -22,7 +22,6 @@ impl OAuthButtons {
pub fn new( pub fn new(
id: impl Into<ElementId>, id: impl Into<ElementId>,
providers: impl IntoIterator<Item = (impl Into<SharedString>, impl Into<SharedString>)>, providers: impl IntoIterator<Item = (impl Into<SharedString>, impl Into<SharedString>)>,
on_pick: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
let id = id.into(); let id = id.into();
let providers: Vec<_> = providers let providers: Vec<_> = providers
@@ -34,27 +33,36 @@ impl OAuthButtons {
id, id,
providers, providers,
busy: None, busy: None,
on_pick: Rc::new(on_pick), on_pick: None,
} }
} }
/// The provider on its way. /// The provider on its way.
pub fn busy(mut self, key: impl Into<SharedString>) -> Self { pub fn busy(mut self, key: impl Into<SharedString>) -> Self {
let key = key.into(); let key = key.into();
if !self.providers.iter().any(|(each, _)| *each == key) { assert!(
log::error!("oauth buttons: no provider {key}; none busy"); self.providers.iter().any(|(each, _)| *each == key),
self.busy = None; "oauth buttons: no provider {key}"
return self; );
}
self.busy = Some(key); self.busy = Some(key);
self self
} }
pub fn on_pick(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_pick = Some(Rc::new(handler));
self
}
} }
impl RenderOnce for OAuthButtons { impl RenderOnce for OAuthButtons {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let id = self.id; let id = self.id;
let on_pick = self.on_pick; let on_pick = self
.on_pick
.unwrap_or_else(|| panic!("oauth buttons {id:?} have no on_pick"));
let busy = self.busy; let busy = self.busy;
div() div()
.flex() .flex()
+19 -18
View File
@@ -97,12 +97,11 @@ pub fn expiry(text: &str, now: (i32, i8)) -> Result<(i8, i32), ExpiryError> {
.trim() .trim()
.parse::<i8>() .parse::<i8>()
.map_err(|_| ExpiryError::Unread)?; .map_err(|_| ExpiryError::Unread)?;
let year = year let year = 2000
.trim() + year
.parse::<i32>() .trim()
.ok() .parse::<i32>()
.and_then(|year| year.checked_add(2000)) .map_err(|_| ExpiryError::Unread)?;
.ok_or(ExpiryError::Unread)?;
if !(1..=12).contains(&month) { if !(1..=12).contains(&month) {
return Err(ExpiryError::NoMonth(month)); return Err(ExpiryError::NoMonth(month));
} }
@@ -147,19 +146,16 @@ pub struct PaymentMethodForm {
id: ElementId, id: ElementId,
error: Option<SharedString>, error: Option<SharedString>,
busy: bool, busy: bool,
on_submit: OnCard, on_submit: Option<OnCard>,
} }
impl PaymentMethodForm { impl PaymentMethodForm {
pub fn new( pub fn new(id: impl Into<ElementId>) -> Self {
id: impl Into<ElementId>,
on_submit: impl Fn(&CardDetails, &mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
error: None, error: None,
busy: false, busy: false,
on_submit: Rc::new(on_submit), on_submit: None,
} }
} }
@@ -174,12 +170,22 @@ impl PaymentMethodForm {
self.busy = busy; self.busy = busy;
self self
} }
pub fn on_submit(
mut self,
handler: impl Fn(&CardDetails, &mut Window, &mut App) + 'static,
) -> Self {
self.on_submit = Some(Rc::new(handler));
self
}
} }
impl RenderOnce for PaymentMethodForm { impl RenderOnce for PaymentMethodForm {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id; let id = self.id;
let on_submit = self.on_submit; let on_submit = self
.on_submit
.unwrap_or_else(|| panic!("payment method form {id:?} has no on_submit"));
let number = field(&id, "number", "", false, window, cx); let number = field(&id, "number", "", false, window, cx);
let until = field(&id, "expiry", "", false, window, cx); let until = field(&id, "expiry", "", false, window, cx);
let code = field(&id, "cvc", "", false, window, cx); let code = field(&id, "cvc", "", false, window, cx);
@@ -309,11 +315,6 @@ mod tests {
"this month still counts" "this month still counts"
); );
assert_eq!(expiry("12/31", now), Ok((12, 2031))); assert_eq!(expiry("12/31", now), Ok((12, 2031)));
assert_eq!(
expiry("12/2147483647", now),
Err(ExpiryError::Unread),
"a year past any clock"
);
assert_eq!(expiry("08/26", now), Err(ExpiryError::Passed)); assert_eq!(expiry("08/26", now), Err(ExpiryError::Passed));
assert_eq!(expiry("13/30", now), Err(ExpiryError::NoMonth(13))); assert_eq!(expiry("13/30", now), Err(ExpiryError::NoMonth(13)));
assert_eq!(expiry("1230", now), Err(ExpiryError::Unread)); assert_eq!(expiry("1230", now), Err(ExpiryError::Unread));
+11 -8
View File
@@ -212,27 +212,30 @@ fn savable(edited: &Profile, seed: &Profile) -> bool {
pub struct ProfileEditor { pub struct ProfileEditor {
id: ElementId, id: ElementId,
profile: Profile, profile: Profile,
on_save: OnProfile, on_save: Option<OnProfile>,
} }
impl ProfileEditor { impl ProfileEditor {
pub fn new( pub fn new(id: impl Into<ElementId>, profile: Profile) -> Self {
id: impl Into<ElementId>,
profile: Profile,
on_save: impl Fn(&Profile, &mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
profile, profile,
on_save: Rc::new(on_save), on_save: None,
} }
} }
pub fn on_save(mut self, handler: impl Fn(&Profile, &mut Window, &mut App) + 'static) -> Self {
self.on_save = Some(Rc::new(handler));
self
}
} }
impl RenderOnce for ProfileEditor { impl RenderOnce for ProfileEditor {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let (id, profile) = (self.id, self.profile); let (id, profile) = (self.id, self.profile);
let on_save = self.on_save; let on_save = self
.on_save
.unwrap_or_else(|| panic!("profile editor {id:?} has no on_save"));
let fields = window.use_keyed_state((id.clone(), "fields"), cx, |window, cx| { let fields = window.use_keyed_state((id.clone(), "fields"), cx, |window, cx| {
Fields::filled(&profile, window, cx) Fields::filled(&profile, window, cx)
}); });
+14 -8
View File
@@ -33,16 +33,12 @@ pub struct Session {
pub struct SessionList { pub struct SessionList {
id: ElementId, id: ElementId,
sessions: Vec<Session>, sessions: Vec<Session>,
on_sign_out: OnKey, on_sign_out: Option<OnKey>,
on_sign_out_others: Option<Run>, on_sign_out_others: Option<Run>,
} }
impl SessionList { impl SessionList {
pub fn new( pub fn new(id: impl Into<ElementId>, sessions: impl IntoIterator<Item = Session>) -> Self {
id: impl Into<ElementId>,
sessions: impl IntoIterator<Item = Session>,
on_sign_out: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
let mut sessions: Vec<_> = sessions.into_iter().collect(); let mut sessions: Vec<_> = sessions.into_iter().collect();
assert!( assert!(
sessions.iter().filter(|session| session.current).count() <= 1, sessions.iter().filter(|session| session.current).count() <= 1,
@@ -52,11 +48,19 @@ impl SessionList {
Self { Self {
id: id.into(), id: id.into(),
sessions, sessions,
on_sign_out: Rc::new(on_sign_out), on_sign_out: None,
on_sign_out_others: None, on_sign_out_others: None,
} }
} }
pub fn on_sign_out(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_sign_out = Some(Rc::new(handler));
self
}
/// Shows "Sign out of all other devices" while there are others. /// Shows "Sign out of all other devices" while there are others.
pub fn on_sign_out_others(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { pub fn on_sign_out_others(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_sign_out_others = Some(Rc::new(handler)); self.on_sign_out_others = Some(Rc::new(handler));
@@ -67,7 +71,9 @@ impl SessionList {
impl RenderOnce for SessionList { impl RenderOnce for SessionList {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id; let id = self.id;
let on_sign_out = self.on_sign_out; let on_sign_out = self
.on_sign_out
.unwrap_or_else(|| panic!("session list {id:?} has no on_sign_out"));
let others = self.sessions.iter().any(|session| !session.current); let others = self.sessions.iter().any(|session| !session.current);
let theme = cx.theme(); let theme = cx.theme();
let rows = self.sessions.into_iter().enumerate().map(|(ix, session)| { let rows = self.sessions.into_iter().enumerate().map(|(ix, session)| {
+11 -7
View File
@@ -40,21 +40,18 @@ pub struct SignupForm {
terms: Option<SharedString>, terms: Option<SharedString>,
error: Option<SharedString>, error: Option<SharedString>,
busy: bool, busy: bool,
on_submit: OnSignup, on_submit: Option<OnSignup>,
on_sign_in: Option<Run>, on_sign_in: Option<Run>,
} }
impl SignupForm { impl SignupForm {
pub fn new( pub fn new(id: impl Into<ElementId>) -> Self {
id: impl Into<ElementId>,
on_submit: impl Fn(&Signup, &mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
terms: None, terms: None,
error: None, error: None,
busy: false, busy: false,
on_submit: Rc::new(on_submit), on_submit: None,
on_sign_in: None, on_sign_in: None,
} }
} }
@@ -77,6 +74,11 @@ impl SignupForm {
self self
} }
pub fn on_submit(mut self, handler: impl Fn(&Signup, &mut Window, &mut App) + 'static) -> Self {
self.on_submit = Some(Rc::new(handler));
self
}
/// Shows "Have an account? Sign in". /// Shows "Have an account? Sign in".
pub fn on_sign_in(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { pub fn on_sign_in(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_sign_in = Some(Rc::new(handler)); self.on_sign_in = Some(Rc::new(handler));
@@ -103,7 +105,9 @@ impl RenderOnce for SignupForm {
&& rules.iter().all(|(_, kept)| *kept) && rules.iter().all(|(_, kept)| *kept)
&& (self.terms.is_none() || agreed) && (self.terms.is_none() || agreed)
&& !self.busy; && !self.busy;
let on_submit = self.on_submit; let on_submit = self
.on_submit
.unwrap_or_else(|| panic!("sign-up form {id:?} has no on_submit"));
let submit: Run = Rc::new(move |window, cx| { let submit: Run = Rc::new(move |window, cx| {
log::info!("sign-up form: create account"); log::info!("sign-up form: create account");
on_submit(&signup, window, cx); on_submit(&signup, window, cx);
+63 -40
View File
@@ -109,7 +109,7 @@ pub struct AccountSwitcher {
id: ElementId, id: ElementId,
accounts: Vec<Account>, accounts: Vec<Account>,
current: SharedString, current: SharedString,
on_select: OnKey, on_select: Option<OnKey>,
on_add: Option<Run>, on_add: Option<Run>,
} }
@@ -118,19 +118,30 @@ impl AccountSwitcher {
id: impl Into<ElementId>, id: impl Into<ElementId>,
accounts: impl IntoIterator<Item = Account>, accounts: impl IntoIterator<Item = Account>,
current: impl Into<SharedString>, current: impl Into<SharedString>,
on_select: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
let (accounts, current): (Vec<_>, SharedString) = let (accounts, current): (Vec<_>, SharedString) =
(accounts.into_iter().collect(), current.into()); (accounts.into_iter().collect(), current.into());
assert!(
accounts.iter().any(|account| account.key == current),
"account switcher: no account {current}"
);
Self { Self {
id: id.into(), id: id.into(),
accounts, accounts,
current, current,
on_select: Rc::new(on_select), on_select: None,
on_add: None, on_add: None,
} }
} }
pub fn on_select(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_select = Some(Rc::new(handler));
self
}
/// Shows "Add account". /// Shows "Add account".
pub fn on_add(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { pub fn on_add(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_add = Some(Rc::new(handler)); self.on_add = Some(Rc::new(handler));
@@ -141,7 +152,9 @@ impl AccountSwitcher {
impl RenderOnce for AccountSwitcher { impl RenderOnce for AccountSwitcher {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id; let id = self.id;
let on_select = self.on_select; let on_select = self
.on_select
.unwrap_or_else(|| panic!("account switcher {id:?} has no on_select"));
let items = self.accounts.iter().map(|account| { let items = self.accounts.iter().map(|account| {
let (key, pick) = (account.key.clone(), on_select.clone()); let (key, pick) = (account.key.clone(), on_select.clone());
MenuItem::radio(account.email.clone(), account.key == self.current).on_click( MenuItem::radio(account.email.clone(), account.key == self.current).on_click(
@@ -162,22 +175,18 @@ impl RenderOnce for AccountSwitcher {
let now = self let now = self
.accounts .accounts
.into_iter() .into_iter()
.find(|account| account.key == self.current); .find(|account| account.key == self.current)
if now.is_none() { .expect("the current account");
log::error!( let mut avatar = Avatar::new((id.clone(), "avatar"), now.name.clone()).size(AvatarSize::Sm);
"account switcher {id:?}: no account {}; none current", if let Some(image) = now.image {
self.current
);
}
let name = now
.as_ref()
.map_or_else(|| SharedString::from("No account"), |now| now.name.clone());
let mut avatar = Avatar::new((id.clone(), "avatar"), name.clone()).size(AvatarSize::Sm);
if let Some(image) = now.as_ref().and_then(|now| now.image.clone()) {
avatar = avatar.image(image); avatar = avatar.image(image);
} }
let email = now.map(|now| now.email).unwrap_or_default(); let trigger = row(
let trigger = row((id.clone(), "trigger"), avatar, named(name, email, cx), cx); (id.clone(), "trigger"),
avatar,
named(now.name, now.email, cx),
cx,
);
opening(id, &menu, trigger, window, cx) opening(id, &menu, trigger, window, cx)
} }
} }
@@ -188,7 +197,7 @@ pub struct WorkspaceSwitcher {
id: ElementId, id: ElementId,
workspaces: Vec<Workspace>, workspaces: Vec<Workspace>,
current: SharedString, current: SharedString,
on_select: OnKey, on_select: Option<OnKey>,
on_create: Option<Run>, on_create: Option<Run>,
} }
@@ -197,19 +206,30 @@ impl WorkspaceSwitcher {
id: impl Into<ElementId>, id: impl Into<ElementId>,
workspaces: impl IntoIterator<Item = Workspace>, workspaces: impl IntoIterator<Item = Workspace>,
current: impl Into<SharedString>, current: impl Into<SharedString>,
on_select: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
let (workspaces, current): (Vec<_>, SharedString) = let (workspaces, current): (Vec<_>, SharedString) =
(workspaces.into_iter().collect(), current.into()); (workspaces.into_iter().collect(), current.into());
assert!(
workspaces.iter().any(|each| each.key == current),
"workspace switcher: no workspace {current}"
);
Self { Self {
id: id.into(), id: id.into(),
workspaces, workspaces,
current, current,
on_select: Rc::new(on_select), on_select: None,
on_create: None, on_create: None,
} }
} }
pub fn on_select(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_select = Some(Rc::new(handler));
self
}
/// Shows "Create workspace". /// Shows "Create workspace".
pub fn on_create(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { pub fn on_create(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_create = Some(Rc::new(handler)); self.on_create = Some(Rc::new(handler));
@@ -220,7 +240,9 @@ impl WorkspaceSwitcher {
impl RenderOnce for WorkspaceSwitcher { impl RenderOnce for WorkspaceSwitcher {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id; let id = self.id;
let on_select = self.on_select; let on_select = self
.on_select
.unwrap_or_else(|| panic!("workspace switcher {id:?} has no on_select"));
let items: Vec<_> = self let items: Vec<_> = self
.workspaces .workspaces
.iter() .iter()
@@ -245,22 +267,17 @@ impl RenderOnce for WorkspaceSwitcher {
let now = self let now = self
.workspaces .workspaces
.into_iter() .into_iter()
.find(|each| each.key == self.current); .find(|each| each.key == self.current)
if now.is_none() { .expect("the current workspace");
log::error!( let mark = Avatar::new((id.clone(), "mark"), now.name.clone())
"workspace switcher {id:?}: no workspace {}; none current",
self.current
);
}
let name = now.as_ref().map_or_else(
|| SharedString::from("No workspace"),
|now| now.name.clone(),
);
let mark = Avatar::new((id.clone(), "mark"), name.clone())
.square() .square()
.size(AvatarSize::Sm); .size(AvatarSize::Sm);
let detail = now.map(|now| now.detail).unwrap_or_default(); let trigger = row(
let trigger = row((id.clone(), "trigger"), mark, named(name, detail, cx), cx); (id.clone(), "trigger"),
mark,
named(now.name, now.detail, cx),
cx,
);
opening(id, &menu, trigger, window, cx) opening(id, &menu, trigger, window, cx)
} }
} }
@@ -274,7 +291,7 @@ pub struct UserMenu {
image: Option<ImageSource>, image: Option<ImageSource>,
presence: Option<Presence>, presence: Option<Presence>,
items: Vec<MenuItem>, items: Vec<MenuItem>,
on_sign_out: Run, on_sign_out: Option<Run>,
} }
impl UserMenu { impl UserMenu {
@@ -282,7 +299,6 @@ impl UserMenu {
id: impl Into<ElementId>, id: impl Into<ElementId>,
name: impl Into<SharedString>, name: impl Into<SharedString>,
email: impl Into<SharedString>, email: impl Into<SharedString>,
on_sign_out: impl Fn(&mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
Self { Self {
id: id.into(), id: id.into(),
@@ -291,7 +307,7 @@ impl UserMenu {
image: None, image: None,
presence: None, presence: None,
items: Vec::new(), items: Vec::new(),
on_sign_out: Rc::new(on_sign_out), on_sign_out: None,
} }
} }
@@ -310,12 +326,19 @@ impl UserMenu {
self.items.push(item); self.items.push(item);
self self
} }
pub fn on_sign_out(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_sign_out = Some(Rc::new(handler));
self
}
} }
impl RenderOnce for UserMenu { impl RenderOnce for UserMenu {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id; let id = self.id;
let sign_out = self.on_sign_out; let sign_out = self
.on_sign_out
.unwrap_or_else(|| panic!("user menu {id:?} has no on_sign_out"));
let menu = let menu =
Menu::new() Menu::new()
.group(format!("Signed in as {}", self.email), self.items) .group(format!("Signed in as {}", self.email), self.items)
+111 -47
View File
@@ -25,13 +25,11 @@ pub struct Role {
pub description: SharedString, pub description: SharedString,
} }
/// The role of `key`; none, logged for `owner`, when the roles lost it. fn role_named<'a>(roles: &'a [Role], key: &SharedString, owner: &str) -> &'a Role {
fn role_named<'a>(roles: &'a [Role], key: &SharedString, owner: &str) -> Option<&'a Role> { roles
let role = roles.iter().find(|role| role.key == *key); .iter()
if role.is_none() { .find(|role| role.key == *key)
log::error!("{owner}: no role {key}; none shown"); .unwrap_or_else(|| panic!("{owner}: no role {key}"))
}
role
} }
/// A role to pick, each with what it may do beneath its name. /// A role to pick, each with what it may do beneath its name.
@@ -41,7 +39,7 @@ pub struct RoleSelector {
roles: Vec<Role>, roles: Vec<Role>,
selected: SharedString, selected: SharedString,
disabled: bool, disabled: bool,
on_change: OnKey, on_change: Option<OnKey>,
} }
impl RoleSelector { impl RoleSelector {
@@ -49,7 +47,6 @@ impl RoleSelector {
id: impl Into<ElementId>, id: impl Into<ElementId>,
roles: impl IntoIterator<Item = Role>, roles: impl IntoIterator<Item = Role>,
selected: impl Into<SharedString>, selected: impl Into<SharedString>,
on_change: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
let (roles, selected): (Vec<_>, SharedString) = let (roles, selected): (Vec<_>, SharedString) =
(roles.into_iter().collect(), selected.into()); (roles.into_iter().collect(), selected.into());
@@ -59,7 +56,7 @@ impl RoleSelector {
roles, roles,
selected, selected,
disabled: false, disabled: false,
on_change: Rc::new(on_change), on_change: None,
} }
} }
@@ -67,12 +64,22 @@ impl RoleSelector {
self.disabled = disabled; self.disabled = disabled;
self self
} }
pub fn on_change(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_change = Some(Rc::new(handler));
self
}
} }
impl RenderOnce for RoleSelector { impl RenderOnce for RoleSelector {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let id = self.id; let id = self.id;
let on_change = self.on_change; let on_change = self
.on_change
.unwrap_or_else(|| panic!("role selector {id:?} has no on_change"));
let choices = self let choices = self
.roles .roles
.into_iter() .into_iter()
@@ -106,8 +113,8 @@ pub struct TeamMemberTable {
id: ElementId, id: ElementId,
members: Vec<Member>, members: Vec<Member>,
roles: Vec<Role>, roles: Vec<Role>,
on_role: OnPair, on_role: Option<OnPair>,
on_remove: OnKey, on_remove: Option<OnKey>,
} }
impl TeamMemberTable { impl TeamMemberTable {
@@ -115,26 +122,48 @@ impl TeamMemberTable {
id: impl Into<ElementId>, id: impl Into<ElementId>,
members: impl IntoIterator<Item = Member>, members: impl IntoIterator<Item = Member>,
roles: impl IntoIterator<Item = Role>, roles: impl IntoIterator<Item = Role>,
on_role: impl Fn(&SharedString, &SharedString, &mut Window, &mut App) + 'static,
on_remove: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
let (members, roles): (Vec<Member>, Vec<Role>) = let (members, roles): (Vec<Member>, Vec<Role>) =
(members.into_iter().collect(), roles.into_iter().collect()); (members.into_iter().collect(), roles.into_iter().collect());
for member in members.iter().filter(|member| !member.owner) {
role_named(&roles, &member.role, "team member table");
}
Self { Self {
id: id.into(), id: id.into(),
members, members,
roles, roles,
on_role: Rc::new(on_role), on_role: None,
on_remove: Rc::new(on_remove), on_remove: None,
} }
} }
/// Runs with a member's key and their new role's.
pub fn on_role(
mut self,
handler: impl Fn(&SharedString, &SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_role = Some(Rc::new(handler));
self
}
pub fn on_remove(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_remove = Some(Rc::new(handler));
self
}
} }
impl RenderOnce for TeamMemberTable { impl RenderOnce for TeamMemberTable {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id; let id = self.id;
let on_role = self.on_role; let on_role = self
let on_remove = self.on_remove; .on_role
.unwrap_or_else(|| panic!("team member table {id:?} has no on_role"));
let on_remove = self
.on_remove
.unwrap_or_else(|| panic!("team member table {id:?} has no on_remove"));
let theme = cx.theme(); let theme = cx.theme();
let rows = self.members.into_iter().enumerate().map(|(ix, member)| { let rows = self.members.into_iter().enumerate().map(|(ix, member)| {
let mut avatar = Avatar::new( let mut avatar = Avatar::new(
@@ -160,8 +189,8 @@ impl RenderOnce for TeamMemberTable {
(id.clone(), format!("role-{}", member.key)), (id.clone(), format!("role-{}", member.key)),
self.roles.clone(), self.roles.clone(),
member.role.clone(), member.role.clone(),
move |role, window, cx| set(&changed, role, window, cx), )
); .on_change(move |role, window, cx| set(&changed, role, window, cx));
let button = ConfirmButton::new( let button = ConfirmButton::new(
(id.clone(), format!("remove-{}", member.key)), (id.clone(), format!("remove-{}", member.key)),
"Remove", "Remove",
@@ -243,9 +272,9 @@ pub struct InvitationList {
id: ElementId, id: ElementId,
invitations: Vec<Invitation>, invitations: Vec<Invitation>,
roles: Vec<Role>, roles: Vec<Role>,
on_invite: OnPair, on_invite: Option<OnPair>,
on_resend: OnKey, on_resend: Option<OnKey>,
on_revoke: OnKey, on_revoke: Option<OnKey>,
} }
impl InvitationList { impl InvitationList {
@@ -253,32 +282,63 @@ impl InvitationList {
id: impl Into<ElementId>, id: impl Into<ElementId>,
invitations: impl IntoIterator<Item = Invitation>, invitations: impl IntoIterator<Item = Invitation>,
roles: impl IntoIterator<Item = Role>, roles: impl IntoIterator<Item = Role>,
on_invite: impl Fn(&SharedString, &SharedString, &mut Window, &mut App) + 'static,
on_resend: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
on_revoke: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
let (invitations, roles): (Vec<Invitation>, Vec<Role>) = ( let (invitations, roles): (Vec<Invitation>, Vec<Role>) = (
invitations.into_iter().collect(), invitations.into_iter().collect(),
roles.into_iter().collect(), roles.into_iter().collect(),
); );
assert!(!roles.is_empty(), "invitation list: no role to invite as"); assert!(!roles.is_empty(), "invitation list: no role to invite as");
for invitation in &invitations {
role_named(&roles, &invitation.role, "invitation list");
}
Self { Self {
id: id.into(), id: id.into(),
invitations, invitations,
roles, roles,
on_invite: Rc::new(on_invite), on_invite: None,
on_resend: Rc::new(on_resend), on_resend: None,
on_revoke: Rc::new(on_revoke), on_revoke: None,
} }
} }
/// Runs with the address and the role's key.
pub fn on_invite(
mut self,
handler: impl Fn(&SharedString, &SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_invite = Some(Rc::new(handler));
self
}
pub fn on_resend(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_resend = Some(Rc::new(handler));
self
}
pub fn on_revoke(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_revoke = Some(Rc::new(handler));
self
}
} }
impl RenderOnce for InvitationList { impl RenderOnce for InvitationList {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id; let id = self.id;
let on_invite = self.on_invite; let on_invite = self
let on_resend = self.on_resend; .on_invite
let on_revoke = self.on_revoke; .unwrap_or_else(|| panic!("invitation list {id:?} has no on_invite"));
let on_resend = self
.on_resend
.unwrap_or_else(|| panic!("invitation list {id:?} has no on_resend"));
let on_revoke = self
.on_revoke
.unwrap_or_else(|| panic!("invitation list {id:?} has no on_revoke"));
let email = field(&id, "email", "name@example.com", false, window, cx); let email = field(&id, "email", "name@example.com", false, window, cx);
let first = self.roles[0].key.clone(); let first = self.roles[0].key.clone();
let role = window.use_keyed_state((id.clone(), "role"), cx, move |_, _| first); let role = window.use_keyed_state((id.clone(), "role"), cx, move |_, _| first);
@@ -286,8 +346,7 @@ impl RenderOnce for InvitationList {
email.read(cx).text().trim().to_string().into(), email.read(cx).text().trim().to_string().into(),
role.read(cx).clone(), role.read(cx).clone(),
); );
let offered = self.roles.iter().any(|role| role.key == chosen); let ready = is_email(&address);
let ready = is_email(&address) && offered;
let clear = email.clone(); let clear = email.clone();
let invite = Rc::new(move |window: &mut Window, cx: &mut App| { let invite = Rc::new(move |window: &mut Window, cx: &mut App| {
log::info!("invitation list: invite as {chosen}"); log::info!("invitation list: invite as {chosen}");
@@ -299,7 +358,8 @@ impl RenderOnce for InvitationList {
let rows = let rows =
self.invitations.iter().map(|invitation| { self.invitations.iter().map(|invitation| {
let named = role_named(&self.roles, &invitation.role, "invitation list") let named = role_named(&self.roles, &invitation.role, "invitation list")
.map_or_else(|| SharedString::from("No role"), |role| role.name.clone()); .name
.clone();
let [again, gone] = [(); 2].map(|_| invitation.key.clone()); let [again, gone] = [(); 2].map(|_| invitation.key.clone());
let (resend, revoke) = (on_resend.clone(), on_revoke.clone()); let (resend, revoke) = (on_resend.clone(), on_revoke.clone());
div() div()
@@ -388,17 +448,21 @@ impl RenderOnce for InvitationList {
.min_w(theme.label_width()) .min_w(theme.label_width())
.child(EmailInput::new(&email)), .child(EmailInput::new(&email)),
) )
.child(div().w(theme.label_width()).child(RoleSelector::new( .child(
(id.clone(), "invite-role"), div().w(theme.label_width()).child(
self.roles.clone(), RoleSelector::new(
role.read(cx).clone(), (id.clone(), "invite-role"),
move |key, _, cx| { self.roles.clone(),
picked.update(cx, |role, cx| { role.read(cx).clone(),
*role = key.clone(); )
cx.notify(); .on_change(move |key, _, cx| {
}) picked.update(cx, |role, cx| {
}, *role = key.clone();
))) cx.notify();
})
}),
),
)
.child( .child(
Button::new((id.clone(), "invite"), "Invite") Button::new((id.clone(), "invite"), "Invite")
.variant(ButtonVariant::Primary) .variant(ButtonVariant::Primary)
+26 -37
View File
@@ -21,8 +21,8 @@ fn accounts(_: &Bench, owner: Entity<Bench>) -> AnyElement {
account("home", "Ada", "ada@home.com"), account("home", "Ada", "ada@home.com"),
], ],
"ada", "ada",
move |key, _, cx| say(&owner, key.to_string(), cx),
) )
.on_select(move |key, _, cx| say(&owner, key.to_string(), cx))
.into_any_element() .into_any_element()
} }
@@ -48,13 +48,10 @@ fn sessions(_: &Bench, owner: Entity<Bench>) -> AnyElement {
current, current,
}; };
let everyone = owner.clone(); let everyone = owner.clone();
SessionList::new( SessionList::new("sessions", [session("phone", false), session("mac", true)])
"sessions", .on_sign_out(move |key, _, cx| say(&owner, format!("out {key}"), cx))
[session("phone", false), session("mac", true)], .on_sign_out_others(move |_, cx| say(&everyone, "others".into(), cx))
move |key, _, cx| say(&owner, format!("out {key}"), cx), .into_any_element()
)
.on_sign_out_others(move |_, cx| say(&everyone, "others".into(), cx))
.into_any_element()
} }
/// Stops: the phone's Sign out, then Sign out of all other devices; this device has none. /// Stops: the phone's Sign out, then Sign out of all other devices; this device has none.
@@ -84,13 +81,10 @@ fn keys(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
used: "Never used".into(), used: "Never used".into(),
}; };
let [made, revoked, done] = [(); 3].map(|_| owner.clone()); let [made, revoked, done] = [(); 3].map(|_| owner.clone());
let manager = ApiKeyManager::new( let manager = ApiKeyManager::new("keys", [key])
"keys", .on_create(move |name, _, cx| say(&made, format!("make {name}"), cx))
[key], .on_revoke(move |key, _, cx| say(&revoked, format!("revoke {key}"), cx))
move |name, _, cx| say(&made, format!("make {name}"), cx), .on_dismiss(move |_, cx| say(&done, "done".into(), cx));
move |key, _, cx| say(&revoked, format!("revoke {key}"), cx),
move |_, cx| say(&done, "done".into(), cx),
);
match bench.sent { match bench.sent {
true => manager.secret("sk_live_0123456789").into_any_element(), true => manager.secret("sk_live_0123456789").into_any_element(),
false => manager.into_any_element(), false => manager.into_any_element(),
@@ -144,10 +138,11 @@ fn profile() -> Profile {
} }
fn editor(_: &Bench, owner: Entity<Bench>) -> AnyElement { fn editor(_: &Bench, owner: Entity<Bench>) -> AnyElement {
ProfileEditor::new("profile", profile(), move |profile, _, cx| { ProfileEditor::new("profile", profile())
say(&owner, format!("{} {}", profile.name, profile.email), cx) .on_save(move |profile, _, cx| {
}) say(&owner, format!("{} {}", profile.name, profile.email), cx)
.into_any_element() })
.into_any_element()
} }
/// Stops: the picture, name, username, email, about, location; Discard and Save once changed. /// Stops: the picture, name, username, email, about, location; Discard and Save once changed.
@@ -172,11 +167,10 @@ fn a_profile_saves_only_what_changed_and_discard_puts_it_back(cx: &mut TestAppCo
fn menu(_: &Bench, owner: Entity<Bench>) -> AnyElement { fn menu(_: &Bench, owner: Entity<Bench>) -> AnyElement {
let [profile, out] = [(); 2].map(|_| owner.clone()); let [profile, out] = [(); 2].map(|_| owner.clone());
UserMenu::new("user", "Ada", "ada@example.com", move |_, cx| { UserMenu::new("user", "Ada", "ada@example.com")
say(&out, "out".into(), cx) .item(MenuItem::new("Profile").on_click(move |_, cx| say(&profile, "profile".into(), cx)))
}) .on_sign_out(move |_, cx| say(&out, "out".into(), cx))
.item(MenuItem::new("Profile").on_click(move |_, cx| say(&profile, "profile".into(), cx))) .into_any_element()
.into_any_element()
} }
/// Stops: the avatar; the menu's rows are Profile, then Sign out. /// Stops: the avatar; the menu's rows are Profile, then Sign out.
@@ -197,14 +191,10 @@ fn workspaces(_: &Bench, owner: Entity<Bench>) -> AnyElement {
detail: "Pro".into(), detail: "Pro".into(),
}; };
let made = owner.clone(); let made = owner.clone();
WorkspaceSwitcher::new( WorkspaceSwitcher::new("spaces", [space("north"), space("south")], "south")
"spaces", .on_select(move |key, _, cx| say(&owner, key.to_string(), cx))
[space("north"), space("south")], .on_create(move |_, cx| say(&made, "create".into(), cx))
"south", .into_any_element()
move |key, _, cx| say(&owner, key.to_string(), cx),
)
.on_create(move |_, cx| say(&made, "create".into(), cx))
.into_any_element()
} }
/// Stops: the switcher; its rows are north, south, then Create workspace. /// Stops: the switcher; its rows are north, south, then Create workspace.
@@ -232,11 +222,10 @@ fn alone(_: &Bench, owner: Entity<Bench>) -> AnyElement {
current: true, current: true,
}; };
let everyone = owner.clone(); let everyone = owner.clone();
SessionList::new("sessions", [here], move |key, _, cx| { SessionList::new("sessions", [here])
say(&owner, format!("out {key}"), cx) .on_sign_out(move |key, _, cx| say(&owner, format!("out {key}"), cx))
}) .on_sign_out_others(move |_, cx| say(&everyone, "others".into(), cx))
.on_sign_out_others(move |_, cx| say(&everyone, "others".into(), cx)) .into_any_element()
.into_any_element()
} }
/// With this device alone there is nothing to sign out, so the list has no stop. /// With this device alone there is nothing to sign out, so the list has no stop.
+38 -152
View File
@@ -51,22 +51,23 @@ fn a_plan_cancels_on_the_second_press_and_comes_back(cx: &mut TestAppContext) {
} }
fn card(_: &Bench, owner: Entity<Bench>) -> AnyElement { fn card(_: &Bench, owner: Entity<Bench>) -> AnyElement {
PaymentMethodForm::new("card", move |card, _, cx| { PaymentMethodForm::new("card")
say( .on_submit(move |card, _, cx| {
&owner, say(
format!( &owner,
"{} {} {}/{} {} {}", format!(
card.brand.name(), "{} {} {}/{} {} {}",
card.number, card.brand.name(),
card.month, card.number,
card.year, card.month,
card.cvc, card.year,
card.name card.cvc,
), card.name
cx, ),
) cx,
}) )
.into_any_element() })
.into_any_element()
} }
/// Stops: number, expiry, code, name, then Save once the card reads. /// Stops: number, expiry, code, name, then Save once the card reads.
@@ -115,15 +116,11 @@ fn invites(_: &Bench, owner: Entity<Bench>) -> AnyElement {
expired: false, expired: false,
}; };
let [invited, again, gone] = [(); 3].map(|_| owner.clone()); let [invited, again, gone] = [(); 3].map(|_| owner.clone());
InvitationList::new( InvitationList::new("invites", [sent], roles())
"invites", .on_invite(move |email, role, _, cx| say(&invited, format!("invite {email} {role}"), cx))
[sent], .on_resend(move |key, _, cx| say(&again, format!("resend {key}"), cx))
roles(), .on_revoke(move |key, _, cx| say(&gone, format!("revoke {key}"), cx))
move |email, role, _, cx| say(&invited, format!("invite {email} {role}"), cx), .into_any_element()
move |key, _, cx| say(&again, format!("resend {key}"), cx),
move |key, _, cx| say(&gone, format!("revoke {key}"), cx),
)
.into_any_element()
} }
/// Stops: the address, the role, then Resend and Revoke while Invite rests. /// Stops: the address, the role, then Resend and Revoke while Invite rests.
@@ -152,14 +149,10 @@ fn team(_: &Bench, owner: Entity<Bench>) -> AnyElement {
owner, owner,
}; };
let removed = owner.clone(); let removed = owner.clone();
TeamMemberTable::new( TeamMemberTable::new("team", [member("ada", true), member("bo", false)], roles())
"team", .on_role(move |key, role, _, cx| say(&owner, format!("{key} {role}"), cx))
[member("ada", true), member("bo", false)], .on_remove(move |key, _, cx| say(&removed, format!("remove {key}"), cx))
roles(), .into_any_element()
move |key, role, _, cx| say(&owner, format!("{key} {role}"), cx),
move |key, _, cx| say(&removed, format!("remove {key}"), cx),
)
.into_any_element()
} }
/// Stops: bo's role, then bo's Remove; the owner has neither. /// Stops: bo's role, then bo's Remove; the owner has neither.
@@ -174,12 +167,11 @@ fn a_member_leaves_on_the_second_press_and_the_owner_stays(cx: &mut TestAppConte
fn wall(_: &Bench, owner: Entity<Bench>) -> AnyElement { fn wall(_: &Bench, owner: Entity<Bench>) -> AnyElement {
let later = owner.clone(); let later = owner.clone();
UpgradePrompt::new("wall", "Unlock history", move |_, cx| { UpgradePrompt::new("wall", "Unlock history")
say(&owner, "upgrade".into(), cx) .benefit("Every message, kept")
}) .on_upgrade(move |_, cx| say(&owner, "upgrade".into(), cx))
.benefit("Every message, kept") .on_dismiss(move |_, cx| say(&later, "later".into(), cx))
.on_dismiss(move |_, cx| say(&later, "later".into(), cx)) .into_any_element()
.into_any_element()
} }
/// Stops: Upgrade, then Not now. /// Stops: Upgrade, then Not now.
@@ -194,11 +186,10 @@ fn a_paywall_upgrades_or_waits(cx: &mut TestAppContext) {
} }
fn busy_card(_: &Bench, owner: Entity<Bench>) -> AnyElement { fn busy_card(_: &Bench, owner: Entity<Bench>) -> AnyElement {
PaymentMethodForm::new("card", move |card, _, cx| { PaymentMethodForm::new("card")
say(&owner, card.number.to_string(), cx) .busy(true)
}) .on_submit(move |card, _, cx| say(&owner, card.number.to_string(), cx))
.busy(true) .into_any_element()
.into_any_element()
} }
/// Stops: number, expiry, code, name; Save rests while the owner checks the card. /// Stops: number, expiry, code, name; Save rests while the owner checks the card.
@@ -363,10 +354,9 @@ fn a_quota_past_its_limit_draws_full(cx: &mut TestAppContext) {
} }
fn line(_: &Bench, owner: Entity<Bench>) -> AnyElement { fn line(_: &Bench, owner: Entity<Bench>) -> AnyElement {
UpgradePrompt::new("line", "Unlock history", move |_, cx| { UpgradePrompt::new("line", "Unlock history")
say(&owner, "upgrade".into(), cx) .on_upgrade(move |_, cx| say(&owner, "upgrade".into(), cx))
}) .into_any_element()
.into_any_element()
} }
/// With benefits the prompt is a paywall that lists them. /// With benefits the prompt is a paywall that lists them.
@@ -382,107 +372,3 @@ fn a_prompt_without_benefits_stands_as_a_line(cx: &mut TestAppContext) {
let (_, cx) = bench(line, cx); let (_, cx) = bench(line, cx);
assert!(cx.debug_bounds("paywall-benefits").is_none()); assert!(cx.debug_bounds("paywall-benefits").is_none());
} }
/// Accounts, workspaces, a provider and roles that left their lists.
fn departed(_: &Bench, _: Entity<Bench>) -> AnyElement {
let account = crate::account::Account {
key: "ada".into(),
name: "Ada".into(),
email: "ada@work.com".into(),
image: None,
};
let space = crate::account::Workspace {
key: "north".into(),
name: "North".into(),
detail: "Pro".into(),
};
let invitation = Invitation {
key: "bo".into(),
email: "bo@example.com".into(),
role: "gone".into(),
sent: "Sent 2 days ago".into(),
expired: false,
};
let member = Member {
key: "bo".into(),
name: "Bo".into(),
email: "bo@example.com".into(),
role: "gone".into(),
seen: "Active today".into(),
image: None,
owner: false,
};
div()
.w(px(360.0))
.child(crate::account::AccountSwitcher::new(
"accounts",
[account],
"gone",
|_, _, _| {},
))
.child(crate::account::WorkspaceSwitcher::new(
"spaces",
[space],
"gone",
|_, _, _| {},
))
.child(
crate::account::OAuthButtons::new("oauth", [("github", "GitHub")], |_, _, _| {})
.busy("gone"),
)
.child(InvitationList::new(
"invites",
[invitation],
roles(),
|_, _, _, _| {},
|_, _, _| {},
|_, _, _| {},
))
.child(TeamMemberTable::new(
"team",
[member],
roles(),
|_, _, _, _| {},
|_, _, _| {},
))
.into_any_element()
}
#[gpui::test]
fn accounts_workspaces_and_roles_that_left_draw(cx: &mut TestAppContext) {
let (_, cx) = bench(departed, cx);
settle(cx);
}
/// Invitations whose first role leaves once the bench says sent.
fn departing_role(state: &Bench, owner: Entity<Bench>) -> AnyElement {
let offered: Vec<Role> = roles()
.into_iter()
.filter(|role| !state.sent || role.key != "member")
.collect();
InvitationList::new(
"invites",
[],
offered,
move |email, role, _, cx| say(&owner, format!("invite {email} {role}"), cx),
|_, _, _| {},
|_, _, _| {},
)
.into_any_element()
}
#[gpui::test]
fn an_invitation_as_a_role_that_left_sends_nothing(cx: &mut TestAppContext) {
let (host, cx) = bench(departing_role, cx);
tab(1, cx);
write("cy@example.com", cx);
host.update(cx, |state, cx| {
state.sent = true;
cx.notify();
});
settle(cx);
tap("enter", cx);
tab(3, cx);
tap("space", cx);
assert_eq!(said(&host, cx), Vec::<String>::new());
}
+53 -57
View File
@@ -6,15 +6,16 @@ use crate::account::{
}; };
fn login(_: &Bench, owner: Entity<Bench>) -> AnyElement { fn login(_: &Bench, owner: Entity<Bench>) -> AnyElement {
LoginForm::new("login", move |login, _, cx| { LoginForm::new("login")
say( .on_submit(move |login, _, cx| {
&owner, say(
format!("{} {} {}", login.email, login.password, login.remember), &owner,
cx, format!("{} {} {}", login.email, login.password, login.remember),
) cx,
}) )
.on_forgot(|_, _| {}) })
.into_any_element() .on_forgot(|_, _| {})
.into_any_element()
} }
/// Stops: email, password, its eye, the box, the forgotten password, Sign in. /// Stops: email, password, its eye, the box, the forgotten password, Sign in.
@@ -40,15 +41,16 @@ fn sign_in_waits_for_an_address_and_a_password_and_enter_sends(cx: &mut TestAppC
} }
fn signup(_: &Bench, owner: Entity<Bench>) -> AnyElement { fn signup(_: &Bench, owner: Entity<Bench>) -> AnyElement {
SignupForm::new("signup", move |signup, _, cx| { SignupForm::new("signup")
say( .terms("I agree to the terms")
&owner, .on_submit(move |signup, _, cx| {
format!("{} {} {}", signup.name, signup.email, signup.password), say(
cx, &owner,
) format!("{} {} {}", signup.name, signup.email, signup.password),
}) cx,
.terms("I agree to the terms") )
.into_any_element() })
.into_any_element()
} }
/// Stops: name, email, password, its eye, the terms, then Create account once it holds. /// Stops: name, email, password, its eye, the terms, then Create account once it holds.
@@ -78,11 +80,8 @@ fn sign_up_waits_for_every_rule_and_the_terms(cx: &mut TestAppContext) {
} }
fn providers(bench: &Bench, owner: Entity<Bench>) -> AnyElement { fn providers(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
let buttons = OAuthButtons::new( let buttons = OAuthButtons::new("oauth", [("github", "GitHub"), ("google", "Google")])
"oauth", .on_pick(move |key, _, cx| say(&owner, key.to_string(), cx));
[("github", "GitHub"), ("google", "Google")],
move |key, _, cx| say(&owner, key.to_string(), cx),
);
match bench.sent { match bench.sent {
true => buttons.busy("github").into_any_element(), true => buttons.busy("github").into_any_element(),
false => buttons.into_any_element(), false => buttons.into_any_element(),
@@ -114,18 +113,19 @@ fn a_provider_hands_its_key_and_rests_while_one_is_on_its_way(cx: &mut TestAppCo
fn magic(bench: &Bench, owner: Entity<Bench>) -> AnyElement { fn magic(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
let other = owner.clone(); let other = owner.clone();
MagicLinkForm::new("magic", move |address, _, cx| { MagicLinkForm::new("magic")
say(&owner, format!("send {address}"), cx); .sent(bench.sent)
owner.update(cx, |bench, _| bench.sent = true); .on_send(move |address, _, cx| {
}) say(&owner, format!("send {address}"), cx);
.sent(bench.sent) owner.update(cx, |bench, _| bench.sent = true);
.on_other(move |_, cx| {
other.update(cx, |bench, cx| {
bench.sent = false;
cx.notify();
}) })
}) .on_other(move |_, cx| {
.into_any_element() other.update(cx, |bench, cx| {
bench.sent = false;
cx.notify();
})
})
.into_any_element()
} }
/// Stops: email, then the link button; once sent, Send again and Use another email. /// Stops: email, then the link button; once sent, Send again and Use another email.
@@ -151,12 +151,11 @@ fn a_link_goes_to_the_address_and_again_on_request(cx: &mut TestAppContext) {
fn forgot(bench: &Bench, owner: Entity<Bench>) -> AnyElement { fn forgot(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
let back = owner.clone(); let back = owner.clone();
ForgotPassword::new("forgot", move |address, _, cx| { ForgotPassword::new("forgot")
say(&owner, format!("reset {address}"), cx) .sent(bench.sent)
}) .on_send(move |address, _, cx| say(&owner, format!("reset {address}"), cx))
.sent(bench.sent) .on_back(move |_, cx| say(&back, "back".into(), cx))
.on_back(move |_, cx| say(&back, "back".into(), cx)) .into_any_element()
.into_any_element()
} }
/// Stops: email, then Back to sign in while the send rests. /// Stops: email, then Back to sign in while the send rests.
@@ -173,11 +172,10 @@ fn a_reset_waits_for_an_address_and_offers_the_way_back(cx: &mut TestAppContext)
} }
fn factor(bench: &Bench, owner: Entity<Bench>) -> AnyElement { fn factor(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
TwoFactorInput::new("factor", move |code, _, cx| { TwoFactorInput::new("factor")
say(&owner, code.to_string(), cx) .failed(bench.failed)
}) .on_code(move |code, _, cx| say(&owner, code.to_string(), cx))
.failed(bench.failed) .into_any_element()
.into_any_element()
} }
/// Stops: the code. /// Stops: the code.
@@ -209,11 +207,10 @@ fn sign_in_waits_for_a_password(cx: &mut TestAppContext) {
} }
fn busy_login(_: &Bench, owner: Entity<Bench>) -> AnyElement { fn busy_login(_: &Bench, owner: Entity<Bench>) -> AnyElement {
LoginForm::new("login", move |login, _, cx| { LoginForm::new("login")
say(&owner, login.email.to_string(), cx) .busy(true)
}) .on_submit(move |login, _, cx| say(&owner, login.email.to_string(), cx))
.busy(true) .into_any_element()
.into_any_element()
} }
/// Stops: email, password, its eye, the box; Sign in rests while busy, so a fifth Tab wraps to the email. /// Stops: email, password, its eye, the box; Sign in rests while busy, so a fifth Tab wraps to the email.
@@ -251,13 +248,12 @@ fn sign_up_waits_for_a_name(cx: &mut TestAppContext) {
fn busy_sent(_: &Bench, owner: Entity<Bench>) -> AnyElement { fn busy_sent(_: &Bench, owner: Entity<Bench>) -> AnyElement {
let other = owner.clone(); let other = owner.clone();
MagicLinkForm::new("magic", move |address, _, cx| { MagicLinkForm::new("magic")
say(&owner, format!("send {address}"), cx) .sent(true)
}) .busy(true)
.sent(true) .on_send(move |address, _, cx| say(&owner, format!("send {address}"), cx))
.busy(true) .on_other(move |_, cx| say(&other, "other".into(), cx))
.on_other(move |_, cx| say(&other, "other".into(), cx)) .into_any_element()
.into_any_element()
} }
/// While a link is on its way Send again rests, so the first stop is Use another email. /// While a link is on its way Send again rests, so the first stop is Use another email.
+11 -8
View File
@@ -20,22 +20,18 @@ pub struct UpgradePrompt {
title: SharedString, title: SharedString,
body: Option<SharedString>, body: Option<SharedString>,
benefits: Vec<SharedString>, benefits: Vec<SharedString>,
on_upgrade: Run, on_upgrade: Option<Run>,
on_dismiss: Option<Run>, on_dismiss: Option<Run>,
} }
impl UpgradePrompt { impl UpgradePrompt {
pub fn new( pub fn new(id: impl Into<ElementId>, title: impl Into<SharedString>) -> Self {
id: impl Into<ElementId>,
title: impl Into<SharedString>,
on_upgrade: impl Fn(&mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
title: title.into(), title: title.into(),
body: None, body: None,
benefits: Vec::new(), benefits: Vec::new(),
on_upgrade: Rc::new(on_upgrade), on_upgrade: None,
on_dismiss: None, on_dismiss: None,
} }
} }
@@ -51,6 +47,11 @@ impl UpgradePrompt {
self self
} }
pub fn on_upgrade(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_upgrade = Some(Rc::new(handler));
self
}
/// Shows "Not now". /// Shows "Not now".
pub fn on_dismiss(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { pub fn on_dismiss(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_dismiss = Some(Rc::new(handler)); self.on_dismiss = Some(Rc::new(handler));
@@ -61,7 +62,9 @@ impl UpgradePrompt {
impl RenderOnce for UpgradePrompt { impl RenderOnce for UpgradePrompt {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id; let id = self.id;
let upgrade = self.on_upgrade; let upgrade = self
.on_upgrade
.unwrap_or_else(|| panic!("upgrade prompt {id:?} has no on_upgrade"));
let theme = cx.theme(); let theme = cx.theme();
let wall = !self.benefits.is_empty(); let wall = !self.benefits.is_empty();
let actions = div() let actions = div()
+2 -5
View File
@@ -99,10 +99,7 @@ impl SandboxStatus {
/// Memory in use and its limit, in bytes. /// Memory in use and its limit, in bytes.
pub fn memory(mut self, used: u64, limit: u64) -> Self { pub fn memory(mut self, used: u64, limit: u64) -> Self {
assert!(limit > 0, "memory {used} of no limit"); assert!(limit > 0 && used <= limit, "memory {used} of {limit}");
if used > limit {
log::error!("environment: memory {used} past its limit {limit}; the meter fills");
}
self.memory = Some((used, limit)); self.memory = Some((used, limit));
self self
} }
@@ -176,7 +173,7 @@ impl RenderOnce for SandboxStatus {
Meter::new( Meter::new(
(self.id.clone(), "memory"), (self.id.clone(), "memory"),
"Memory", "Memory",
used.min(limit) as f32 / limit as f32, used as f32 / limit as f32,
) )
.detail(format!( .detail(format!(
"{} of {}", "{} of {}",
+10 -12
View File
@@ -166,7 +166,7 @@ pub struct Checkpoint {
pub struct CheckpointList { pub struct CheckpointList {
id: ElementId, id: ElementId,
checkpoints: Vec<Checkpoint>, checkpoints: Vec<Checkpoint>,
current: Option<usize>, current: usize,
on_rewind: Option<Pick>, on_rewind: Option<Pick>,
} }
@@ -178,17 +178,15 @@ impl CheckpointList {
current: usize, current: usize,
) -> Self { ) -> Self {
let checkpoints: Vec<Checkpoint> = checkpoints.into_iter().collect(); let checkpoints: Vec<Checkpoint> = checkpoints.into_iter().collect();
let held = current < checkpoints.len(); assert!(
if !held { current < checkpoints.len(),
log::error!( "checkpoint {current} of {}",
"checkpoints: checkpoint {current} of {}; none current", checkpoints.len()
checkpoints.len() );
);
}
Self { Self {
id: id.into(), id: id.into(),
checkpoints, checkpoints,
current: held.then_some(current), current,
on_rewind: None, on_rewind: None,
} }
} }
@@ -212,7 +210,7 @@ impl RenderOnce for CheckpointList {
let rewind = self let rewind = self
.on_rewind .on_rewind
.clone() .clone()
.filter(|_| current != Some(ix)) .filter(|_| ix != current)
.map(|rewind| { .map(|rewind| {
Button::new((self.id.clone(), format!("rewind-{ix}")), "Rewind to here") Button::new((self.id.clone(), format!("rewind-{ix}")), "Rewind to here")
.variant(ButtonVariant::Ghost) .variant(ButtonVariant::Ghost)
@@ -235,12 +233,12 @@ impl RenderOnce for CheckpointList {
.items_center() .items_center()
.gap_x_2() .gap_x_2()
.child(plural(point.files as u64, "file", "files")) .child(plural(point.files as u64, "file", "files"))
.when(current == Some(ix), |meta| { .when(ix == current, |meta| {
meta.child(div().text_color(colors.accent).child("Here now")) meta.child(div().text_color(colors.accent).child("Here now"))
}) })
.children(rewind), .children(rewind),
); );
timeline.item(if current == Some(ix) { timeline.item(if ix == current {
item.tone(Tone::Accent) item.tone(Tone::Accent)
} else { } else {
item item
+6 -12
View File
@@ -71,7 +71,7 @@ pub struct BrowserPreview {
id: ElementId, id: ElementId,
url: SharedString, url: SharedString,
frames: Vec<SharedString>, frames: Vec<SharedString>,
shown: Option<usize>, shown: usize,
ratio: f32, ratio: f32,
loading: bool, loading: bool,
on_show: Option<Pick>, on_show: Option<Pick>,
@@ -86,18 +86,12 @@ impl BrowserPreview {
shown: usize, shown: usize,
) -> Self { ) -> Self {
let frames: Vec<SharedString> = frames.into_iter().map(Into::into).collect(); let frames: Vec<SharedString> = frames.into_iter().map(Into::into).collect();
let held = shown < frames.len(); assert!(shown < frames.len(), "frame {shown} of {}", frames.len());
if !held {
log::error!(
"browser preview: frame {shown} of {}; none shown",
frames.len()
);
}
Self { Self {
id: id.into(), id: id.into(),
url: url.into(), url: url.into(),
frames, frames,
shown: held.then_some(shown), shown,
ratio: FRAME, ratio: FRAME,
loading: false, loading: false,
on_show: None, on_show: None,
@@ -176,7 +170,7 @@ impl RenderOnce for BrowserPreview {
.h(thumb) .h(thumb)
.rounded(theme.radius(Radius::Sm)) .rounded(theme.radius(Radius::Sm))
.border_1() .border_1()
.border_color(if shown == Some(ix) { .border_color(if ix == shown {
colors.accent colors.accent
} else { } else {
colors.border colors.border
@@ -211,7 +205,7 @@ impl RenderOnce for BrowserPreview {
.when(strip.is_none(), |frame| { .when(strip.is_none(), |frame| {
frame.rounded_b(theme.radius(Radius::Lg)) frame.rounded_b(theme.radius(Radius::Lg))
}) })
.children(shown.map(|shown| { .child({
let image = let image =
Image::new((self.id.clone(), "frame"), source(&self.frames[shown])) Image::new((self.id.clone(), "frame"), source(&self.frames[shown]))
.size_full(); .size_full();
@@ -220,7 +214,7 @@ impl RenderOnce for BrowserPreview {
} else { } else {
image image
} }
})), }),
) )
.children(strip) .children(strip)
} }
Loaded 100 of 551 files, more files were not shown because too many files have changed in this diff. Show more