Compare commits

..
640 Commits
Author SHA1 Message Date
ZacharyZhang-NY da3fdcaf39 Column shell: a drag that comes back where it set off gives the width the owner had, where the last width it reported stayed; the width a drag sets off from lives with its press, so each press starts afresh
CI / site (push) Successful in 31s
CI / platforms (ubuntu-24.04) (push) Waiting to run
CI / crate (push) Failing after 13m11s
CI / platforms (windows-latest) (push) Successful in 4m50s
2026-10-16 21:17:36 -04:00
ZacharyZhang-NY 0b4848bb2a Column shell: a drag sets off from the wider of the width drawn and the owner's, so a column a short shell squeezes moves with the pointer, where it jumped back against it, and a drag with no sideways move changes no width, where it wrote the squeezed one
CI / platforms (windows-latest) (push) Successful in 5m2s
CI / platforms (ubuntu-24.04) (push) Waiting to run
CI / crate (push) Failing after 13m0s
CI / site (push) Successful in 31s
2026-10-16 20:57:32 -04:00
ZacharyZhang-NY 005faefd5d Resize handles: the line is a filled hairline, since gpui paints a border no wider than half its box: a split's divider, a resizable box's edge and a column's border drew half a pixel, and none at a scale of one
CI / site (push) Successful in 25s
CI / platforms (ubuntu-24.04) (push) Waiting to run
CI / crate (push) Failing after 12m30s
CI / platforms (windows-latest) (push) Successful in 4m36s
2026-10-16 20:10:03 -04:00
ZacharyZhang-NY 5c2822cfb6 Column shell: a resizable column drags by its border toward the filling column, whose handle draws that border and occludes the header under it, a drag setting off from the width drawn; a set width is a flex basis, so a column holds its header's own parts, where Windows' caption buttons ran out of the window
CI / crate (push) Failing after 12m32s
CI / platforms (ubuntu-24.04) (push) Waiting to run
CI / site (push) Successful in 30s
CI / platforms (windows-latest) (push) Successful in 4m47s
2026-10-16 19:56:21 -04:00
ZacharyZhang-NYandClaude Opus 5.5 2e65b05a65 Documents: inline code in a sixth-level heading keeps its own color
CI / platforms (windows-latest) (push) Successful in 5m29s
CI / platforms (ubuntu-24.04) (push) Waiting to run
CI / crate (push) Failing after 12m24s
CI / site (push) Successful in 30s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 14:48:11 -04:00
ZacharyZhang-NYandClaude Opus 5.5 962964e7da Documents: the field highlighter reads front matter as front matter
CI / platforms (windows-latest) (push) Successful in 4m41s
CI / platforms (ubuntu-24.04) (push) Waiting to run
CI / site (push) Successful in 30s
CI / crate (push) Failing after 13m19s
Without the front matter construct, an open front matter block parsed
as a rule and a setext heading, so its keys drew bright and bold. The
highlighter now parses as the file does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 14:34:16 -04:00
ZacharyZhang-NYandClaude Opus 5.5 1ff993dcb1 Documents: an open heading keeps its weight and color; front matter keeps its panel
CI / platforms (ubuntu-24.04) (push) Waiting to run
CI / site (push) Successful in 31s
CI / crate (push) Failing after 12m51s
CI / platforms (windows-latest) (push) Successful in 4m42s
The field's highlighter gave headings and bold semibold over the drawn
bold, and its plain color hid a sixth-level heading's muted color. It
now uses the drawn weight, and runs inside a sixth-level heading stay
muted. Front matter opens in its mono, muted panel. The open text box
is built by one function, so tests read its background directly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 14:27:26 -04:00
ZacharyZhang-NYandClaude Opus 5.5 bff7ebcd64 Documents: an open block keeps its look
CI / platforms (ubuntu-24.04) (push) Waiting to run
CI / crate (push) Failing after 2s
CI / platforms (windows-latest) (push) Failing after 2s
CI / site (push) Successful in 24s
The open field sits in the closed block's frame at its size, line
height and weight; headings keep their step. Code and math keep their
panel. Typewriter centering also keys on the ends' room, so a block
opened at the same height still centers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 14:01:58 -04:00
ZacharyZhang-NY cdc5ce8247 Shell: a control in a drag region occludes it, so it takes its press on Windows, where gpui answers the hit test with the caption over any Drag hitbox left uncovered: window buttons and title bar slots occlude, and a column header's item row does while staying the scroll container, its title a drag region of its own; the mini window drags by its title alone, keeping its hover
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / site (push) Successful in 29s
CI / platforms (windows-latest) (push) Successful in 4m19s
CI / crate (push) Failing after 6m18s
2026-10-15 18:11:10 -04:00
ZacharyZhang-NYandClaude Opus 5.5 90bd37b550 Documents: strong text keeps a heading's weight; sizes are tested
CI / platforms (windows-latest) (push) Successful in 4m33s
CI / crate (push) Failing after 6m26s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / site (push) Successful in 32s
Headings and strong text share one bold weight, so strong text in a
heading no longer draws lighter. A heading's line height is relative
to its own size, and tests check every level's size against the body,
a larger body and a zoomed document.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 14:55:39 -04:00
ZacharyZhang-NYandClaude Opus 5.5 acbbe1dbe5 Documents: a heading test comment fits ten words
CI / site (push) Successful in 27s
CI / crate (push) Failing after 4m33s
CI / platforms (windows-latest) (push) Successful in 4m41s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 14:33:31 -04:00
ZacharyZhang-NYandClaude Opus 5.5 4a8f940ea2 Code editor: two measure comments fit ten words
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / site (push) Successful in 26s
CI / crate (push) Failing after 3m24s
CI / platforms (windows-latest) (push) Successful in 4m47s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 14:19:39 -04:00
ZacharyZhang-NYandClaude Opus 5.5 cd61653ff1 Code editor: measure with the current text; the line end is a stop
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / site (push) Successful in 31s
CI / crate (push) Failing after 4m9s
CI / platforms (windows-latest) (push) Successful in 4m34s
Bracket and match offsets from the last draw belong to that text's
stamp, so an input-method edit before the next draw no longer splits
a character into font runs. A press past the last glyph's start picks
the nearer of that start and the line end.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 14:01:12 -04:00
ZacharyZhang-NYandClaude Opus 5.5 26af4be04f Documents: a CRLF drawn as two chars reads back char for char
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / site (push) Successful in 31s
CI / platforms (windows-latest) (push) Successful in 4m36s
CI / crate (push) Failing after 9m28s
Only a CRLF the drawn text collapses is one end; inline code keeps both.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 13:26:29 -04:00
ZacharyZhang-NYandClaude Opus 5.5 c31d092583 Documents: a drawn CRLF reads back to its start
CI / platforms (windows-latest) (push) Failing after 10s
CI / site (push) Successful in 23s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / crate (push) Failing after 10m55s
A press at a CRLF line's end typed between CR and LF.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 13:04:25 -04:00
ZacharyZhang-NYandClaude Opus 5.5 5954132e76 Documents: headings step up from the body, all bold
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 10s
CI / site (push) Successful in 23s
CI / crate (push) Failing after 10m33s
Sizes follow Typora's steps over the body (2.25, 1.75, 1.5, 1.25, 1, 1)
so levels 3 and 4 stand out; level 6 is muted; line height fits the size.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 12:25:50 -04:00
ZacharyZhang-NYandClaude Opus 5.5 9fb607beb4 Code editor: columns sit where gpui lays them out
CI / crate (push) Failing after 11m53s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 10s
CI / site (push) Successful in 22s
Carets, selections, presses and the input method box measured the line
as columns times one 'm', so a wide character (CJK, emoji) put the caret
mid-glyph. editor::measure lays out the drawn line; rulers and indent
guides stay on the column grid; whitespace marks hide the rest.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 12:21:22 -04:00
ZacharyZhang-NYandClaude Opus 5.5 93efa7f70b Documents: soft breaks draw as lines; Reader::joined keeps GFM spaces
CI / site (push) Successful in 22s
CI / crate (push) Failing after 27s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 8s
The screen keeps a single line break (Typora's default when writing);
outputs read with Reader::joined, which joins them as GFM does. Heading
titles read on one line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 12:08:22 -04:00
ZacharyZhang-NY a2cb531e06 Text input: Copy, Cut, Paste and SelectAll are public, so an app's Edit menu reaches a focused field
CI / site (push) Successful in 22s
CI / crate (push) Failing after 11m28s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 9s
2026-10-15 10:07:08 -04:00
ZacharyZhang-NYandClaude Opus 5.5 5a53c08c84 Menus: a row exit drops its delayed mark; icon rows keep the aim
CI / platforms (windows-latest) (push) Failing after 8s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / site (push) Successful in 22s
CI / crate (push) Failing after 11m4s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 02:54:10 -04:00
ZacharyZhang-NYandClaude Opus 5.5 d4fdb694ef Menus: hover intent for submenus; panels occlude; flip left
CI / site (push) Successful in 22s
CI / platforms (windows-latest) (push) Failing after 9s
CI / crate (push) Failing after 11m21s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Panels occlude the rows under them. A submenu without room on the
right opens on the left of its row. A submenu opens after the pointer
rests 150 ms; a row crossed toward an open submenu waits 300 ms.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 02:25:43 -04:00
ZacharyZhang-NYandClaude Opus 5.5 f1771b3880 Documents: cells keep their row's pipes; long reads finish cell moves
CI / site (push) Successful in 22s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 10s
CI / crate (push) Failing after 11m22s
An emptied bare edge cell writes a pipe so its column stays. The caret
reads the escaped write-back. A right press beside an edited cell keeps
its place. A move that waits on a long read opens once the read lands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 02:09:43 -04:00
ZacharyZhang-NYandClaude Opus 5.5 2b6bd1d369 Documents: untouched cells keep their bytes; open cells take padding presses
CI / site (push) Successful in 22s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 9s
CI / crate (push) Failing after 11m35s
A trailing backslash doubles only before a closing pipe. A write at a
block's start keeps the block, so Tab leaves a first cell without outer
pipes. Padding of the open cell moves its caret to the end, and a right
press there points at the end.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 01:39:17 -04:00
ZacharyZhang-NYandClaude Opus 5.5 6a3f3486fd Documents: open cells keep presses; finds open their cell
CI / platforms (windows-latest) (push) Failing after 9s
CI / site (push) Successful in 23s
CI / crate (push) Failing after 13m33s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
A press inside the open cell stays with its field. Find opens the
cell holding a match. A closing backslash no longer escapes the
next pipe. A right press after typed pipes reads the right byte.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 01:16:08 -04:00
ZacharyZhang-NY 5dc4dd1805 Resize border: glass leaves a see-through window's pane to its content, without ground or shadow
CI / platforms (windows-latest) (push) Failing after 10s
CI / site (push) Successful in 22s
CI / crate (push) Failing after 11m28s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
2026-10-15 01:01:26 -04:00
ZacharyZhang-NYandClaude Opus 5.5 2594b3fd0f Documents: a press in a table opens its cell alone
CI / platforms (windows-latest) (push) Failing after 10s
CI / crate (push) Failing after 11m56s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / site (push) Successful in 23s
Visual mode opened a pressed table whole, as Markdown source. A press
in a table now opens only that cell, in place, as its source in a
one-line field; the rest of the table stays drawn. Tab and Shift-Tab
walk the cells, Up and Down move a row or leave the table, Enter and
Escape write back in one undo step, and a bare pipe typed in a cell is
escaped on write-back. The table drawing moves to render/table.rs and
presses on a cell's padding name the cell.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 00:50:20 -04:00
ZacharyZhang-NY 991ca7b548 Column shell: glass grounds the columns in the palette's glass, so a blurred window shows through them
CI / platforms (windows-latest) (push) Failing after 9s
CI / site (push) Successful in 23s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / crate (push) Failing after 11m45s
2026-10-14 23:23:30 -04:00
ZacharyZhang-NYandClaude Opus 5.5 e07fd34fa3 Documents: time the drawn-lookup test with web_time
CI / site (push) Successful in 25s
CI / crate (push) Failing after 11m23s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 9s
The component's check bans std Instant; the R4.10 test used it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 22:12:51 -04:00
ZacharyZhang-NYandClaude Opus 5.5 2b8bd9fcf3 Right press: one alignment pass, latest press wins
CI / crate (push) Failing after 12m14s
CI / site (push) Successful in 22s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 10s
drawn_at aligns each source piece once instead of re-reading the span
for every drawn byte, so a right press near a long paragraph's start
stays linear. A newer press drops an older pending open-block lookup.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 21:40:55 -04:00
ZacharyZhang-NYandClaude Opus 5.5 a4bf6965f9 i18n: the find box, sort and filter builders and password toggle in Chinese
CI / platforms (windows-latest) (push) Failing after 18s
CI / site (push) Successful in 23s
CI / crate (push) Failing after 13m36s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
The find counts, toggles, replace buttons, the builders' add, join and
direction labels, the filter tests and the password tooltip read the
builtin catalog. Test::label becomes Test::key, the choice's value; its
label is the catalog's. The shipped-copy scan checks button, segment and
match-arm text on every match, and zh-CN render tests cover both.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 20:41:47 -04:00
ZacharyZhang-NYandClaude Opus 5.5 79e94f7e24 documents: a right press in an open block targets the pressed byte
CI / crate (push) Failing after 11m46s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 9s
CI / site (push) Successful in 23s
The open block's target was read at the selection's start and from
the syntax tree only, so a press on the second of two selected links
named the first, and an inline anchor read as its outer link.
point_open now reads the byte under the press and resolves links as
drawing does; delete_image takes that byte.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 20:18:22 -04:00
ZacharyZhang-NYandClaude Opus 5.5 6df9ab0aa6 AGENTS: name the i18n call and the shipped-copy test
CI / site (push) Successful in 22s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 9s
CI / crate (push) Failing after 11m5s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 19:53:15 -04:00
ZacharyZhang-NYandClaude Opus 5.5 d05b1b78ca i18n: shipped components read their copy from the catalog
CI / platforms (windows-latest) (push) Failing after 16s
CI / site (push) Successful in 25s
CI / crate (push) Failing after 10m59s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Select's default placeholder, the clear, close, remove and unpin tooltips,
the completion menu's empty line and the size sample showed English under
zh-CN. A source test now fails on any fixed English literal in them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 19:53:00 -04:00
ZacharyZhang-NYandClaude Opus 5.5 1b5fb07cda documents: Ctrl+V with no block open pastes text at the caret
CI / platforms (windows-latest) (push) Failing after 8s
CI / site (push) Successful in 22s
CI / crate (push) Failing after 11m50s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
It pasted only pictures; text was dropped without a word.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 18:20:11 -04:00
ZacharyZhang-NYandClaude Opus 5.5 013f14216f documents: right presses keep a selection; Delete Image
CI / platforms (windows-latest) (push) Failing after 9s
CI / crate (push) Failing after 12m8s
CI / site (push) Successful in 21s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
A right press in a text field or the code editor keeps a selection it
lands in, else moves the caret. In visual mode a right press in the open
block keeps it open and emits Pointed, read apart from the whole text.
MarkdownDocument::delete_image removes the picture at the caret.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 18:15:38 -04:00
ZacharyZhang-NYandClaude Opus 5.5 6c2d28c56e documents: a dropped block hands its focus to the document
CI / platforms (windows-latest) (push) Failing after 8s
CI / site (push) Successful in 21s
CI / crate (push) Failing after 12m5s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
An edit past an open block wrote it back and dropped its input while
it held focus, so focus fell to the window root and Undo reached
nothing. The next render passes such focus to the document.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 17:39:24 -04:00
ZacharyZhang-NYandClaude Opus 5.5 24ffc45c6b documents: TableEdit::Delete removes the table at the caret
CI / platforms (windows-latest) (push) Failing after 9s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / site (push) Successful in 22s
CI / crate (push) Failing after 12m17s
Typora's Paragraph > Table > Delete Table. It removes the table as the
block command does and refuses outside a table as TableRefusal::Outside.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 17:04:26 -04:00
ZacharyZhang-NYandClaude Opus 5.5 6d90351423 documents: an open block stays open while an overlay borrows focus
CI / crate (push) Failing after 11m41s
CI / platforms (windows-latest) (push) Failing after 12s
CI / site (push) Successful in 21s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
A menu or dialog took focus, the open block wrote back and closed, and
the overlay handed focus back to a closed input: its command reached
nothing and the selection was gone. The block now waits for the overlay
to let focus go, and writes back only when focus lands elsewhere.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 17:01:42 -04:00
ZacharyZhang-NYandClaude Opus 5.5 6b033f3eb0 documents: edits read the caret off a CRLF split
CI / site (push) Successful in 22s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 10s
CI / crate (push) Failing after 11m47s
The editor splits lines on LF only, so End can leave the caret
between CR and LF. The edit runner and table insert now settle the
selection before the CR; the line helpers assert they never see a
split instead of guessing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 15:56:17 -04:00
ZacharyZhang-NYandClaude Opus 5.5 ce015e7b7b documents: a caret inside CRLF reads as its line's end
The code editor splits lines on LF only, so End and Right can leave the
caret between CR and LF. line_start and line_end now read that spot as
the end of its line, so insert_table keeps CRLF around a new table.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 15:44:59 -04:00
ZacharyZhang-NYandClaude Opus 5.5 07fee552c3 documents: tables keep their line ending; tabs count four
Table rewrites find their prefix and rows by CR, LF and CRLF alike and
write the table's own line ending; insert_table takes the text's. A
cell's width counts a tab as 4 and a control character as 0, as
Typora's wcswidth does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 15:33:29 -04:00
ZacharyZhang-NYandClaude Opus 5.5 bcee87c08f documents: table moves and tidy read apart; a code press names its text
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 15:16:42 -04:00
ZacharyZhang-NYandClaude Opus 5.5 0558f0c890 documents: block checks read as drawn; alert, fence and box edges kept
CI / site (push) Successful in 21s
CI / platforms (windows-latest) (push) Failing after 8s
CI / crate (push) Failing after 12m1s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 15:03:36 -04:00
ZacharyZhang-NYandClaude Opus 5.5 892e095025 documents: block commands reread their result
Fixes R4.7 round 2: a heading change or split is parsed again and
refused as Misread when its text, formats or kind would change;
closing hashes and block openers are escaped at split ends; task
boxes below a marker stay; indented code is written from its value;
alerts unwrap past blank and lazy lines; delete counts every gone
opener.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 14:36:49 -04:00
ZacharyZhang-NYandClaude Opus 5.5 b37031b00b documents: block commands keep text, tasks and list items whole
Round-1 fixes for R4.7: a caret in heading marks splits at the text; a
split closes and reopens bold, italic and strike, refusing inside links,
code, math and HTML; a task's first text takes no block; retype keeps
task boxes and code indent and takes siblings past the list; an alert
in an item keeps the item's opener; paragraph-before moves a holder's
first text below its opener; delete keeps neighbors from joining; a
heading's text keeps closing hashes and block openers escaped; a heading
level is idempotent, as Typora's menu calls it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 14:08:40 -04:00
ZacharyZhang-NYandClaude Opus 5.5 58e212d4ec documents: block commands as Typora's Paragraph menu
MarkdownDocument::block runs a BlockCommand at the caret: heading
levels, paragraph, promote and demote; quote and list toggles;
rule, code, math, toc, footnote, link definition and front matter
inserts; empty paragraphs before and after; delete. Formats and
block commands share one runner off the UI thread; FormatRefusal
is now EditRefusal.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 13:44:42 -04:00
ZacharyZhang-NYandClaude Opus 5.5 1d0354592b documents: format always reads apart; read cutoff back to 128 KiB
CI / site (push) Successful in 23s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 9s
CI / crate (push) Failing after 12m9s
The format command never parses on the UI thread; refusals past the
size checks come by event. The document's at-once read returns to
128 KiB, so a visual opening between the cutoffs starts at the top.
Timing test takes web_time::Instant.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 13:14:49 -04:00
ZacharyZhang-NYandClaude Opus 5.5 0289fdaa19 documents: format checks nested marks, reports late refusals, reads small texts at once
Unwrapping a style now runs the crossing check on the found node, so an
end in a nested link address or strike mark refuses. Clear keeps an
autolink's backslashes. Block lookups use a sorted search, so the walk
grows linearly. A long format refuses a second one as Busy and reports
refusals, a changed text and a shown preview through FormatRefused.
markdown-rs grows with the square of its nodes, so texts read at once
only up to 16 KiB.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 12:47:18 -04:00
ZacharyZhang-NYandClaude Opus 5.5 d238e3b4f0 documents: format refuses ends in marks, clears more, reads long texts apart
Unwrap and merge check the original ends against marks and link
addresses. Blocks take text up to their end spaces; a cell between its
pipes. Clear also takes inline math's dollars, footnote brackets and
escape backslashes. Underline finds its <u> pair past other tags. Blank
lines split on CR too; a selection across blocks says Across. Past
128 KiB the toggle runs off the UI thread and lands only if text and
selection are unchanged; past the largest read it refuses (TooLong).
Clear and the document command move into format/.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 12:16:09 -04:00
ZacharyZhang-NYandClaude Opus 5.5 68d33b4c54 documents: inline formats toggle as Typora's
MarkdownDocument::format toggles bold, italic, code, strike, underline,
link and image at the selection, or clears every inline mark from the
blocks it touches, following Typora's toggleStyle: off inside the style,
wrap a selection, take the caret's word or write an empty pair. Code,
math, HTML and front matter refuse, as does a selection crossing a
block, a link or another mark. An open block keeps its field open.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 11:51:24 -04:00
ZacharyZhang-NYandClaude Opus 5.5 c35fdac2e3 documents: read presses as the parser decoded them
CI / site (push) Successful in 22s
CI / platforms (windows-latest) (push) Failing after 8s
CI / crate (push) Failing after 11m39s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Round 3 of R4.5 found five ways the reverse reading drifted from
markdown-rs: kept indents and marks were eaten as joins, autolink text
was decoded, table code kept its escaped pipes, decoded white space
could not join or drop, and C0/C1 numeric references kept their
controls. Matching now tries an equal character first, a drawn space
against any line end next, and drops white space decoded or not;
autolinks read as written, table cell code reads \| as |, and numeric
references follow markdown-rs. The reviewer's probes are regressions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 11:34:50 -04:00
ZacharyZhang-NYandClaude Opus 5.5 d4c614cb7f documents: drawn text reads back from its end, decoded as written
Each piece records how its source decodes: Markdown's escapes and
references for text, HTML's for an HTML block's runs, none for code
spans, cut as CommonMark cuts their end spaces. Matching runs from the
piece's end, so quote marks and indents at a line's start are never
taken for text, and an alert's text needs no moved position: alert()
takes no source again.

Lines split on LF, CRLF and CR alike; LineMap matches each drawn line
to the written line it ends once, so an HTML block maps in linear time
and a tab cut to spaces lands on the tab. A write-back refuses the
press when the pressed block's kind, text or drawing changed.

The hostile HTML test measures growth: a fixed 2 s budget failed under
the full suite's load on the old code too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 11:07:25 -04:00
ZacharyZhang-NYandClaude Opus 5.5 83d747d87e documents: presses read back through quotes, code, HTML and front matter
Each drawn character finds its written one, skipping only white
space, escapes and quote marks, so a press lands on the character
itself. Code, front matter and link definitions read back line for
line. An HTML block keeps each text run's source byte, and an alert
keeps its text past the marker; alert() now takes the source.

A secondary press refuses a target its write-back changed, and its
focus no longer opens an empty document. Run's element stack moves to
tree/stack.rs and the HTML-block reader to tree/html_block.rs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 10:11:00 -04:00
ZacharyZhang-NYandClaude Opus 5.5 5af11fe489 documents: a press opens its block on the pressed character; a secondary press points
Each drawn text piece keeps its written span, so a press maps back to a
source byte. A secondary press opens nothing: it moves the caret there and
emits DocumentEvent::Pointed with the block's kind, link, picture and
whether it lies in a table or code. Block nodes count from their own start.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 09:38:08 -04:00
ZacharyZhang-NYandClaude Opus 5.5 12512217bd documents: exports read through MarkdownReader, one fresh stack per area; reading helpers close again
CI / platforms (windows-latest) (push) Failing after 13s
CI / crate (push) Failing after 28s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / site (push) Successful in 22s
The writer in ElyMark kept its own copy of the inline reading, which drifted
from the drawing. Reader goes public as MarkdownReader with block, inline,
note and label, and its blocks as MarkdownBlock. Shortcodes, InlineTag,
inline_tag and html_block_text close back to the tree; headings(text) goes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 01:36:21 -04:00
ZacharyZhang-NYandClaude Opus 5.5 97fa047d18 documents: the drawing's reading rules are public, for exports to share
CI / platforms (windows-latest) (push) Failing after 8s
CI / site (push) Successful in 22s
CI / crate (push) Failing after 10m31s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Shortcodes, inline_tag, html_block_text, alert, is_toc and headings are what
the drawing itself calls, so an export that calls them shows the same.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 00:59:03 -04:00
ZacharyZhang-NYandClaude Opus 5.5 8e5dad8177 documents: shortcodes read per DOM text node as GitHub reads them; HTML5's p closers end a p; a TOC press follows editor anchors
CI / platforms (windows-latest) (push) Failing after 9s
CI / crate (push) Failing after 10m54s
CI / site (push) Successful in 22s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
- The first `:name:`-shaped run in a text node decides for the whole node:
  blank text or a space before it turns every shortcode, else none. Filtered
  tags are text and do not split the node. Probed on GitHub's markdown API.
- `fieldset`, `menu` and the rest of HTML5's list end an open `p`.
- `CodeEditor` carries owner anchors through each edit; the document anchors
  every heading it reads, and a TOC press goes where its anchor went.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 00:48:26 -04:00
ZacharyZhang-NYandClaude Opus 5.5 a744316274 documents: a TOC press follows the edit since its read; filtered tags keep shortcodes; tag names end as HTML5 ends them; block starts end p, li, dd and dt; owed space shifts only what opened after it
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 00:11:57 -04:00
ZacharyZhang-NYandClaude Opus 5.5 98f19a8aac documents: alerts read their marker from parsed text; GFM's filtered tags show as text; block elements close what opened inside them; a TOC press relocates its heading after a write-back or a pending read and acts on press
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 23:42:55 -04:00
ZacharyZhang-NYandClaude Opus 5.5 2eec25140d documents: GitHub alerts, a [toc] list of headings, front matter in a box, HTML blocks as text, a visible rule and bullets by depth
At the root a quote marked [!NOTE] and the rest, as GitHub's API reads it,
is an alert; a [toc] paragraph lists the headings and a press reveals one;
an HTML block shows its text with the inline tags, white space as a browser
lays it, scripts and styles dropped. The rule is 2px; bullets turn by depth.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 23:08:51 -04:00
ZacharyZhang-NYandClaude Opus 5.5 9ff403df31 Property grid: the group doc fits one line
CI / platforms (windows-latest) (push) Failing after 10s
CI / site (push) Successful in 25s
CI / crate (push) Failing after 11m3s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 23:00:40 -04:00
ZacharyZhang-NYandClaude Opus 5.5 9f39d325a4 documents: Markdown containers are the elements GitHub writes, repaired with tags as HTML5 repairs; shortcodes read from decoded text, kept in autolinks and <code>
CI / site (push) Successful in 27s
CI / crate (push) Failing after 12m13s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 10s
A raw end tag ends the Markdown element of its name; an end tag pops every
element above its target and only formatting elements reopen, so mark, kbd
and del stop where the browser stops them; a new anchor ends the open one.
Emoji read after escapes and references, as GitHub's Markdown API shows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 22:48:52 -04:00
ZacharyZhang-NYandClaude Opus 5.5 1bba544e73 documents: references scan in bounded steps; attribute values decode and split as HTML does; tags cross emphasis as a browser repairs them; GitHub's emoji names
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 22:22:52 -04:00
ZacharyZhang-NYandClaude Opus 5.5 2736a109b2 documents: shortcodes read from the source, so escapes, references and autolinks keep them; whole tag names, decoded attributes, only anchors link
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 22:22:51 -04:00
ZacharyZhang-NYandClaude Opus 5.5 8d27f9088a documents: emoji shortcodes, inline HTML tags and numbered footnote references draw as Typora draws them
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 22:22:51 -04:00
ZacharyZhang-NYandClaude Opus 5.5 b3d6cc023e Property grid: a group's title keys its fold, so a group put first takes no other group's fold
CI / platforms (windows-latest) (push) Failing after 10s
CI / crate (push) Failing after 10m48s
CI / site (push) Successful in 24s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 22:22:50 -04:00
ZacharyZhang-NYandClaude Opus 5.5 1b8d67839a Menus · button rows: a reader presses a button through Click and hears the marked one; the panel is a menu
CI / site (push) Successful in 27s
CI / crate (push) Failing after 10m45s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 10s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 21:25:18 -04:00
ZacharyZhang-NYandClaude Opus 5.5 ed75621a6d Menus: a mark holds its item, so a neighbor shifted into its place is not chosen; a move after the menu closed marks nothing
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 20:47:02 -04:00
ZacharyZhang-NYandClaude Opus 5.5 a1718b6f7c Menus · button rows: a marked button that turns off or leaves clears the mark, a filter field gives its arrows to a marked row, a disabled button says it is unavailable
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 20:31:44 -04:00
ZacharyZhang-NYandClaude Opus 5.5 3c35daa4d8 Menus · button rows: Menu::buttons lays icon buttons in one row, arrows cross it, a press inside a panel stays the menu's
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 20:20:18 -04:00
ZacharyZhang-NY 8fd3e431db documents: a comment within ten words
CI / site (push) Successful in 21s
CI / crate (push) Failing after 11m37s
CI / platforms (windows-latest) (push) Failing after 8s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
2026-10-12 17:09:27 -04:00
ZacharyZhang-NY 60d71f29b1 documents and typography: two comments within ten words
CI / platforms (windows-latest) (push) Failing after 8s
CI / crate (push) Failing after 10m46s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / site (push) Successful in 22s
2026-10-12 17:07:21 -04:00
ZacharyZhang-NY b39097cd3f documents: Shift-Tab takes four columns out of any mix of spaces and tabs
CI / site (push) Successful in 22s
CI / crate (push) Failing after 10m10s
CI / platforms (windows-latest) (push) Failing after 8s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
2026-10-12 16:58:50 -04:00
ZacharyZhang-NY 58227cdc2f documents: measuring and export load gpui's bundled faces and map generic families as it does
CI / platforms (windows-latest) (push) Failing after 11s
CI / site (push) Successful in 25s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / crate (push) Failing after 11m33s
2026-10-12 12:45:21 -04:00
ZacharyZhang-NY e0b4e29ef3 typography: a drawn formula's fallback text stays inside its frame (test)
CI / site (push) Successful in 25s
CI / platforms (windows-latest) (push) Failing after 11s
CI / crate (push) Failing after 11m58s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
2026-10-12 12:27:56 -04:00
ZacharyZhang-NY a0011772ae documents: export and measuring choose fonts as gpui draws: a missing family falls to sans-serif, then any face
CI / platforms (windows-latest) (push) Failing after 11s
CI / site (push) Successful in 26s
CI / crate (push) Failing after 11m39s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
2026-10-12 12:23:55 -04:00
ZacharyZhang-NY 50a59d2620 documents and typography: SVG sizes and frames read with the system's fonts, as drawn
CI / crate (push) Failing after 12m18s
CI / platforms (windows-latest) (push) Failing after 9s
CI / site (push) Successful in 23s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
2026-10-12 11:55:54 -04:00
ZacharyZhang-NY 8234fcdeb2 typography and documents: an exported formula frames its outlined text; rendered modes have no source selection
CI / platforms (windows-latest) (push) Failing after 8s
CI / site (push) Successful in 22s
CI / crate (push) Failing after 11m54s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
2026-10-12 11:15:52 -04:00
ZacharyZhang-NY 3bbbd05f38 typography: an exported formula's text becomes outlines, or its missing letter is an error
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 9s
CI / site (push) Successful in 22s
CI / crate (push) Failing after 12m23s
2026-10-12 10:55:53 -04:00
ZacharyZhang-NY ff9c0f88d3 documents: a diagram letter no font has is an export error
CI / platforms (windows-latest) (push) Failing after 8s
CI / site (push) Successful in 22s
CI / crate (push) Failing after 12m5s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
2026-10-12 10:33:03 -04:00
ZacharyZhang-NY c7a86079fc documents: the selected Markdown, in source or an open block
CI / crate (push) Failing after 11m47s
CI / platforms (windows-latest) (push) Failing after 9s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / site (push) Successful in 21s
2026-10-12 10:06:57 -04:00
ZacharyZhang-NY fd8c98f3b1 documents: the whole-file parse options are public, for export
CI / platforms (windows-latest) (push) Failing after 7s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / site (push) Successful in 21s
CI / crate (push) Failing after 11m35s
2026-10-12 10:01:55 -04:00
ZacharyZhang-NY cec9689f26 typography and documents: formula and diagram SVG for export; a diagram label without a font is an error
CI / platforms (windows-latest) (push) Failing after 8s
CI / site (push) Successful in 22s
CI / crate (push) Failing after 12m32s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
2026-10-12 09:56:38 -04:00
ZacharyZhang-NY 7ed4ea8323 editor: typewriter centering keys on view and line heights; a placement resets the editor's centering too
CI / platforms (windows-latest) (push) Failing after 8s
CI / crate (push) Failing after 12m26s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / site (push) Successful in 22s
2026-10-12 09:28:47 -04:00
ZacharyZhang-NY cf9cc1971c editor: typewriter room as rows, so drawing and text queries share one origin; a placement resets centering
CI / platforms (windows-latest) (push) Failing after 8s
CI / crate (push) Failing after 10m53s
CI / site (push) Successful in 22s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
2026-10-12 09:13:49 -04:00
ZacharyZhang-NYandClaude Opus 5.5 f102ea0b6c documents and editor: typewriter room at both ends, open blocks scaled, a resize re-centers
CI / platforms (windows-latest) (push) Failing after 8s
CI / site (push) Successful in 25s
CI / crate (push) Failing after 11m6s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 08:54:41 -04:00
ZacharyZhang-NYandClaude Opus 5.5 508fee9d15 documents and editor: a writing view - measure, font, scale, focus fading, typewriter centering
CI / platforms (windows-latest) (push) Failing after 9s
CI / site (push) Successful in 22s
CI / crate (push) Failing after 12m26s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 08:25:45 -04:00
ZacharyZhang-NYandClaude Opus 5.5 e89fb32f43 documents: reset keeps the caret and the top line
CI / site (push) Successful in 21s
CI / platforms (windows-latest) (push) Failing after 8s
CI / crate (push) Failing after 11m55s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 07:48:16 -04:00
ZacharyZhang-NYandClaude Opus 5.5 d126a7e7f7 documents: dropped files and pasted pictures go to the owner; insert puts text at the caret
CI / platforms (windows-latest) (push) Failing after 8s
CI / crate (push) Failing after 11m49s
CI / site (push) Successful in 21s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 07:24:15 -04:00
ZacharyZhang-NYandClaude Opus 5.5 cd16b204f0 documents: a comment within ten words
CI / platforms (windows-latest) (push) Failing after 8s
CI / site (push) Successful in 21s
CI / crate (push) Failing after 12m1s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 07:19:04 -04:00
ZacharyZhang-NYandClaude Opus 5.5 4fb59bb8e1 documents: an empty task ends a list; Shift-Tab takes a tab out; a table on a blank indented line is a table
CI / crate (push) Failing after 11m41s
CI / platforms (windows-latest) (push) Failing after 8s
CI / site (push) Successful in 22s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 07:17:24 -04:00
ZacharyZhang-NYandClaude Opus 5.5 e162082f6f documents: lists go on inside quotes and after tabs; a waiting match lapses with its text and yields to a newer one
CI / platforms (windows-latest) (push) Failing after 8s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / site (push) Successful in 21s
CI / crate (push) Failing after 11m32s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 07:02:59 -04:00
ZacharyZhang-NYandClaude Opus 5.5 edc328aadf documents: table edits keep extra cells and quote marks and say why they refuse; preview takes no edits; lists go on only in list items; a match waits for its text
CI / crate (push) Failing after 11m29s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 8s
CI / site (push) Successful in 22s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 06:40:36 -04:00
ZacharyZhang-NYandClaude Opus 5.5 ce4a1e9cb5 documents: lists go on, block lines shift, tables change, matches show, fences color by language
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 9s
CI / site (push) Successful in 21s
CI / crate (push) Failing after 11m41s
The document carries Markdown's editing tools for ElyMark (MARK-06):
Enter continues a list item or ends an empty one, Tab and Shift-Tab
shift an open block's lines, table rows and columns come and go as
one undo step, a match shows in any mode, replacements are one step,
and a fenced block colors by its language.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 06:14:34 -04:00
ZacharyZhang-NYandClaude Opus 5.5 2e196a10a7 documents: a picture shrinks within a narrow table cell; tree tests in their own file; test comments within ten words
CI / platforms (windows-latest) (push) Failing after 8s
CI / site (push) Successful in 22s
CI / crate (push) Failing after 11m42s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 05:23:55 -04:00
ZacharyZhang-NYandClaude Opus 5.5 4801555e0d documents: pictures draw in table cells; Markdown-path comments within ten words
CI / crate (push) Failing after 11m44s
CI / platforms (windows-latest) (push) Failing after 8s
CI / site (push) Successful in 21s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
A cell holds blocks, split at its pictures as a paragraph is. A picture
takes its natural width, capped by its container.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 05:12:16 -04:00
ZacharyZhang-NYandClaude Opus 5.5 bbb607106a documents: link addresses stay out of the log; tests for the link rules and the extended syntax
CI / crate (push) Failing after 11m19s
CI / site (push) Successful in 21s
CI / platforms (windows-latest) (push) Failing after 8s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 04:44:47 -04:00
ZacharyZhang-NYandClaude Opus 5.5 639521e109 documents: one undo history; a block's own never runs
CI / site (push) Successful in 22s
CI / crate (push) Failing after 11m33s
CI / platforms (windows-latest) (push) Failing after 8s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Undo or redo in an open block writes it back as one step and steps the
document, so redo survives mode switches and further undos.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 04:39:09 -04:00
ZacharyZhang-NYandClaude Opus 5.5 a3d9f089a3 documents: undo and redo leave a block only past its own history; typing lands after undo empties the document
CI / crate (push) Failing after 11m46s
CI / platforms (windows-latest) (push) Failing after 8s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / site (push) Successful in 21s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 04:28:33 -04:00
ZacharyZhang-NYandClaude Opus 5.5 362a0fc9fa documents: undo in an untouched block steps the document; visual after preview opens an empty end
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 8s
CI / site (push) Successful in 22s
CI / crate (push) Failing after 11m33s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 04:14:19 -04:00
ZacharyZhang-NYandClaude Opus 5.5 6f0401e967 documents: an empty visual document opens its end on focus
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 9s
CI / site (push) Successful in 22s
CI / crate (push) Failing after 11m48s
Typing in a new file lands with no press.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 03:52:55 -04:00
ZacharyZhang-NYandClaude Opus 5.5 f59c9f7f50 documents: a text past the read limit drops the old read
CI / crate (push) Failing after 11m24s
CI / platforms (windows-latest) (push) Failing after 9s
CI / site (push) Successful in 31s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 02:49:00 -04:00
ZacharyZhang-NYandClaude Opus 5.5 69b11d7852 MarkdownDocument: typed source reads after a pause off the main thread; texts past 2 MiB are never read
CI / crate (push) Failing after 11m23s
CI / site (push) Successful in 25s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 9s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 02:33:49 -04:00
ZacharyZhang-NYandClaude Opus 5.5 abac8b41b3 MarkdownDocument: numbered top-line asks, clamped when placed, reveal waits for its read; tests run the frame callbacks
CI / crate (push) Failing after 11m58s
CI / platforms (windows-latest) (push) Failing after 9s
CI / site (push) Successful in 24s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 02:02:56 -04:00
ZacharyZhang-NYandClaude Opus 5.5 433c38fb2d place: a shorter comment
CI / site (push) Successful in 28s
CI / crate (push) Failing after 11m37s
CI / platforms (windows-latest) (push) Successful in 6m14s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 01:40:47 -04:00
ZacharyZhang-NYandClaude Opus 5.5 33ddb45f31 MarkdownDocument: blocks open on a read of the text as it is, top lines wait for their read and keep their place in tall blocks
CI / site (push) Successful in 46s
CI / platforms (windows-latest) (push) Successful in 6m17s
CI / crate (push) Failing after 26m58s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 01:40:41 -04:00
ZacharyZhang-NYandClaude Opus 5.5 fc181b44c2 MarkdownDocument: one caret for every mode, pressed blocks kept across write-backs, preview read only, keyboard reach, history capped on every path
CI / site (push) Successful in 25s
CI / crate (push) Failing after 26m50s
CI / platforms (windows-latest) (push) Successful in 4m37s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 01:14:06 -04:00
ZacharyZhang-NYandClaude Opus 5.5 2b6f5513aa MarkdownDocument: a Markdown file in one CodeEditor, shown visual, source, split or preview
CI / site (push) Successful in 34s
CI / platforms (windows-latest) (push) Successful in 5m7s
CI / crate (push) Failing after 26m45s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Visual draws the top-level blocks in a gpui list; a pressed block opens as
its source and its typing is the text at once, written back as one undo
step. CodeEditor takes the owner's line colors, resets with a fresh
history and keeps at most 64 MiB of undo text. MarkdownMode gains Preview.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 00:44:17 -04:00
ZacharyZhang-NYandClaude Opus 5.5 9036c92442 held: a shorter comment
CI / site (push) Successful in 30s
CI / crate (push) Failing after 26m5s
CI / platforms (windows-latest) (push) Successful in 4m32s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 00:22:19 -04:00
ZacharyZhang-NYandClaude Opus 5.5 82d76e9b37 Markdown pictures: the painted image lives with the asset record, freed by whichever holder leaves last
CI / crate (push) Failing after 26m43s
CI / platforms (windows-latest) (push) Failing after 9s
CI / site (push) Successful in 23s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 00:21:37 -04:00
ZacharyZhang-NYandClaude Opus 5.5 8b9820be74 Markdown pictures: an asset leaves the cache with its last holder
CI / crate (push) Failing after 27m9s
CI / site (push) Successful in 30s
CI / platforms (windows-latest) (push) Successful in 4m42s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 00:11:26 -04:00
ZacharyZhang-NYandClaude Opus 5.5 ab9a14c692 Markdown pictures: freed with their elements, glyphs whole, failures keep the source, file URLs parsed, Mermaid labels in Ely's ink
CI / site (push) Successful in 23s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / crate (push) Failing after 27m31s
CI / platforms (windows-latest) (push) Failing after 9s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 23:59:56 -04:00
ZacharyZhang-NYandClaude Opus 5.5 d5ecc5a69e Markdown: display math through RaTeX, Mermaid through mermaid-rs-renderer, pictures from the owner's folder
CI / crate (push) Failing after 25m44s
CI / platforms (windows-latest) (push) Successful in 4m47s
CI / site (push) Successful in 32s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Latex typesets with RaTeX to outlined SVG drawn by gpui's SvgRenderer at the
window's scale, loaded as an asset off the main thread. Diagram draws a
mermaid block in Mermaid's theme for the mode. MarkdownRenderer::base names
the folder pictures load from; data addresses decode, web ones stay unloaded.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 23:38:22 -04:00
ZacharyZhang-NYandClaude Opus 5.5 508cc55af6 Markdown: nested definitions resolve, images in styles and links stay images, CR soft breaks, reference links highlight
CI / crate (push) Failing after 26m21s
CI / platforms (windows-latest) (push) Failing after 8s
CI / site (push) Successful in 22s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 23:19:28 -04:00
ZacharyZhang-NYandClaude Opus 5.5 fcad0ff8e9 Markdown: MarkdownRenderer, MarkdownEditor, highlights and block ranges read with markdown-rs
CI / site (push) Successful in 32s
CI / crate (push) Failing after 29m18s
CI / platforms (windows-latest) (push) Successful in 4m40s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
The tree comes from markdown::to_mdast with GitHub's flavor and math.
References resolve against their definitions, an image splits its
paragraph, a one-line $$ formula stands as a block. BlockEditor's
import and export keep pulldown-cmark.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 23:07:14 -04:00
ZacharyZhang-NYandClaude Opus 5.5 bd014707fd ChangesList: a long file name ends in an ellipsis before the row's buttons
CI / crate (push) Canceled after 0s
CI / site (push) Successful in 31s
CI / platforms (windows-latest) (push) Successful in 4m52s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 00:41:50 -04:00
ZacharyZhang-NYandClaude Opus 5.5 3eb4d346fe TitleBar: a press its buttons claim never moves the window
CI / platforms (windows-latest) (push) Failing after 28s
CI / site (push) Successful in 40s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s
CI / crate (push) Canceled after 19m24s
The drag area armed a window move on every press, buttons included, so a
click on Linux's drawn buttons that moved a pixel started a move and the
compositor kept the release. It now arms only on unclaimed presses.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 00:36:15 -04:00
gitea-actions 13642424e7 Site · Showcase: pin Ely-GPUI-Showcases at ca872b7 2026-10-10 15:32:00 +00:00
ZacharyZhang-NYandClaude Opus 5.5 81d0f5f5dd Format: the update dialog, toast and feedback tests pass cargo fmt
CI / platforms (windows-latest) (push) Successful in 6m28s
CI / platforms (ubuntu-24.04) (push) Successful in 4m4s
CI / site (push) Successful in 46s
CI / crate (push) Successful in 26m45s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 02:30:48 -04:00
ZacharyZhang-NYandClaude Opus 5.5 44a7debfb6 CI · the platform legs match the Gitea runners; the Linux leg tests as a plain user
CI / crate (push) Failing after 36s
CI / platforms (ubuntu-24.04) (push) Successful in 5m33s
CI / platforms (windows-latest) (push) Failing after 1s
CI / site (push) Successful in 46s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 02:27:26 -04:00
ZacharyZhang-NY 49ed1f8405 Site and gallery: the repo and the showcase catalog live on Gitea at git.zacharyzhang.com
CI / crate (push) Failing after 2s
CI / site (push) Successful in 30s
CI / platforms (ubuntu-24.04-arm) (push) Canceled after 0s
CI / platforms (windows-11-arm) (push) Canceled after 0s
CI / platforms (ubuntu-24.04) (push) Failing after 1m48s
CI / platforms (macos-15-intel) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 15s
Links, the header's Gitea mark, the catalog's archive download and the showcase pin
workflow (Gitea's API and commit status) move off GitHub; pinned at 6973834, which
restores TokenLedger.
2026-10-09 16:20:32 -04:00
ZacharyZhang-NY 2a578d3238 Merge remote-tracking branch 'gitea/main'
CI / platforms (macos-15-intel) (push) Canceled after 0s
CI / platforms (windows-11-arm) (push) Canceled after 0s
CI / site (push) Failing after 24s
CI / platforms (ubuntu-24.04) (push) Failing after 1m59s
CI / crate (push) Failing after 2s
CI / platforms (ubuntu-24.04-arm) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 10s
2026-10-09 15:51:09 -04:00
ZacharyZhang-NY f893693492 docs: add AGit pull request instructions
CI / platforms (macos-15-intel) (push) Canceled after 0s
CI / platforms (ubuntu-24.04-arm) (push) Canceled after 0s
CI / platforms (windows-11-arm) (push) Canceled after 0s
CI / crate (push) Failing after 3s
CI / platforms (windows-latest) (push) Failing after 13s
CI / site (push) Failing after 24s
CI / platforms (ubuntu-24.04) (push) Failing after 2m2s
2026-10-09 19:46:35 +00:00
ZacharyZhang-NY 5d6efd46ec Update banner and dialog: texts from the built-in English and Chinese catalog (Ely UPD-1) 2026-10-09 12:01:50 -04:00
ZacharyZhang-NY f834f55c16 Toaster::shown: the titles and severities on show, for owners and tests 2026-10-09 10:50:01 -04:00
ZacharyZhang-NY b0dbfd429c Code colors: Rust, JavaScript, Ruby, Haskell, Erlang presets; TOML and CMake comments follow code (Ely R2.12) 2026-10-09 07:29:53 -04:00
ZacharyZhang-NY c77124812d Code colors per language: comments, keywords and constants by file; Shell comments start words (Ely R2.12) 2026-10-09 06:52:16 -04:00
ZacharyZhang-NY 5caf5aaa40 DiffViewer: lines color by their file's syntax; prose reads plain (Ely R2.12) 2026-10-09 05:09:41 -04:00
ZacharyZhang-NY 7f2ac3e4eb DiffViewer: prose files (Markdown, plain text) read plain, not as code 2026-10-09 03:24:12 -04:00
ZacharyZhang-NY 0f468e8661 G15: the sixth review passed the last fix
CI / site (push) Failing after 24s
CI / platforms (ubuntu-24.04) (push) Failing after 1m58s
CI / platforms (ubuntu-24.04-arm) (push) Canceled after 0s
CI / platforms (windows-11-arm) (push) Canceled after 0s
CI / crate (push) Failing after 2s
CI / platforms (windows-latest) (push) Failing after 22s
CI / platforms (macos-15-intel) (push) Canceled after 0s
2026-10-08 22:11:50 -04:00
ZacharyZhang-NY cb5e35f7cb CI: fmt the narrow-layout commits, and Theme::hairline in place of their raw px 2026-10-08 21:55:55 -04:00
ZacharyZhang-NY 694cccbc81 GitHub round 2026-10-08: tasks ticked 2026-10-08 21:47:08 -04:00
ZacharyZhang-NY d24984cc38 BlockEditor selects across blocks: drag or Shift-press, then type, paste, delete, cut, copy as Markdown or break, one undo step (#7) 2026-10-08 21:47:08 -04:00
ZacharyZhang-NY f5eb82ee1b BlockData::to_markdown and from_markdown: a document of blocks as GitHub Markdown and back, each field's inline Markdown kept as written (#7) 2026-10-08 21:47:08 -04:00
ZacharyZhang-NY 5490869f23 BlockEditor fills its box and scrolls with a scrollbar, bringing the caret into view as it moves; a ScrollArea keeps a wheel it can use, so the page behind stays (#7) 2026-10-08 21:47:07 -04:00
ZacharyZhang-NY ca6d2e219e Option lists occlude: a wheel over the / menu, or any floating list, scrolled the page behind it too (#12) 2026-10-08 21:45:52 -04:00
ZacharyZhang-NY 152d881f4f IME · the candidate box clamps to the last layout: asked before the next one, it found nothing and macOS put it in the screen's corner (#7) 2026-10-08 21:45:52 -04:00
ZacharyZhang-NY 33df23c02a Caret · whole device pixels at a device pixel: gpui snapped a 1.5px bar's edges apart, so it painted 1 or 2 pixels by where it stood (#7) 2026-10-08 21:45:52 -04:00
ZacharyZhang-NY cbf63c6945 cargo fmt: the Retired, F6 and appearance commits left four files unformatted, failing CI's crate job 2026-10-08 21:45:51 -04:00
ZacharyZhang-NY 69265b4fd4 ZoomControl: owner-driven mode for content zoom 2026-10-08 21:08:58 -04:00
ZacharyZhang-NY 62dff57b85 Banner text wraps; PropertyGrid label column 104 2026-10-08 20:59:04 -04:00
ZacharyZhang-NY 6d95b24889 Narrow layouts: vertical ScrollArea, wrapping property rows, icon-only segments and tabs, shrinking status items, unfocused fields show their start 2026-10-08 20:42:30 -04:00
ZacharyZhang-NY ed07342039 oss-scanner: Dockerfile and threat model for Anthropic's OSS Scanner 2026-10-08 18:58:19 -04:00
ZacharyZhang-NY a7eb1afc9d Retired::release_all, for an owner that knows nothing draws them 2026-10-08 16:34:22 -04:00
ZacharyZhang-NY bbc37f41c1 Retired::append, to take over a closing owner's images 2026-10-08 16:32:33 -04:00
ZacharyZhang-NY dac2c5a39f Retired frees only images no one else holds
An image shared with something still drawing waits for a later paint.
2026-10-08 16:30:51 -04:00
ZacharyZhang-NY cd50824049 Retired: free images a view stopped drawing
GPUI keeps every painted RenderImage in the window's atlas until
drop_image; views that repaint tiles grew without bound.
2026-10-08 16:27:45 -04:00
ZacharyZhang-NY fd0efc7939 Dark palette: soft greys from #252525, never near-black
The base was #131110 and sunken areas #0e0d0b. Text tiers move up with
the fills so muted and subtle text keep 4.5:1 on hover and active, and
the colour-blind chart's violet and orange are lifted to 3:1.
2026-10-08 15:09:35 -04:00
ZacharyZhang-NY 029632b1f4 F6 and Shift-F6 move focus on from a field that keeps Tab
Documents and web pages take Tab for themselves; without another key a
keyboard user could not leave them.
2026-10-08 14:30:43 -04:00
ZacharyZhang-NY 3fcbd6f139 Toast · an alert or status role named by its title, so screen readers hear every notice 2026-10-08 12:48:47 -04:00
ZacharyZhang-NY ea4ed4cbb7 SettingsRow · a group named by its title, described by its line, so assistive tech says what each control sets 2026-10-08 11:51:39 -04:00
ZacharyZhang-NY fac20ab967 Banner · an alert or status role named by its message, so screen readers hear it 2026-10-08 08:04:16 -04:00
ZacharyZhang-NY 489ef31195 ThemeSelector · light, dark and system through the built-in catalogs, not English only 2026-10-08 07:53:42 -04:00
ZacharyZhang-NY 28a6bba929 Toaster · log a toast's id and severity, never its title: titles may quote user content 2026-10-07 23:34:42 -04:00
ZacharyZhang-NY f756043853 EditorTabs · the close button over a dirty or inactive tab stays pressable: clear, not hidden 2026-10-07 20:26:07 -04:00
ZacharyZhang-NY aa0c99dd70 Toggle · shorter doc comments 2026-10-07 16:14:23 -04:00
ZacharyZhang-NY 8f4729cc9f Toggle · toggles reach assistive tech: a button named by its label or tooltip, with its pressed state 2026-10-07 14:51:31 -04:00
ZacharyZhang-NY 55945bb10b Kit · K06: acceptance table; T54 done 2026-10-07 14:07:06 -04:00
ZacharyZhang-NY 0040dc799a Kit · K04: cargo search without color, so CI reads Kit's newest release 2026-10-07 13:45:36 -04:00
ZacharyZhang-NY b3985b9b5b Kit · K05: the GPUI Kit guide, a README section and a whole app beside Kit 2026-10-07 13:36:55 -04:00
ZacharyZhang-NY 99615e1a0c Kit · K04: compat/kit runs Ely beside the newest GPUI Kit in one window; check.sh holds it to Kit's newest release 2026-10-07 13:23:23 -04:00
ZacharyZhang-NY 98de2a455f Kit · K03: init checks every bundled file through the app's source; Assets::before joins another source 2026-10-07 13:23:23 -04:00
ZacharyZhang-NY dabc1eb4d2 Tree · the tree and its rows reach assistive tech: tree items with their name, level, selection and open state 2026-10-07 13:11:16 -04:00
ZacharyZhang-NY a9b30c7eab Kit · K02: Tab binds under FocusScope's ElyFocus context, so a deeper context's Tab wins 2026-10-07 12:39:20 -04:00
ZacharyZhang-NY c61a5b22b0 Kit · K01: gpui from gpui-pre =0.3.8, Kit's pin; test-support keeps leak detection 2026-10-07 12:27:17 -04:00
ZacharyZhang-NY 083ef85696 Git: clear CI's lints; a row's drawn parts gather in Entry, the history's tuple is Counted, picking tests sit under tests/ 2026-10-07 07:39:01 -04:00
ZacharyZhang-NY 179ea8e276 Panics · P45: recount and result; T53 done 2026-10-07 07:27:32 -04:00
ZacharyZhang-NY 0040e851e2 Panics · P44 Rendering: atlas faults log and skip the draw 2026-10-07 07:26:55 -04:00
ZacharyZhang-NY 05c973abb7 Panics · P43 Tooling: a knob that left sets nothing 2026-10-07 07:22:15 -04:00
ZacharyZhang-NY 3a0a4bb699 Panics · P42 Misc: handlers in new; the web view shows wry's failures; kept state fits lists that changed 2026-10-07 07:17:20 -04:00
ZacharyZhang-NY 770d13575d Panics · P41 Maps: no change; places, zooms and pins are contracts the map's own moves keep 2026-10-07 06:49:23 -04:00
ZacharyZhang-NY 22fedae2b6 Panics · P40 i18n: no change; keys, catalogs and locales are contracts 2026-10-07 06:46:16 -04:00
ZacharyZhang-NY 5c3a096229 Panics · P39 Theme: no change; tokens and hues are contracts over constant tables 2026-10-07 06:41:52 -04:00
ZacharyZhang-NY 513ea50946 Panics · P38 Interaction: handlers in new; resizes fit the limits now, stale drags and leaders do nothing 2026-10-07 06:39:05 -04:00
ZacharyZhang-NY 8db6cecd5a Panics · P37 Onboarding: handlers in new; gone articles, steps and topics hold 2026-10-07 06:18:15 -04:00
ZacharyZhang-NY e7aec1651e Panics · P36 Account: handlers in new; gone accounts, workspaces, providers and roles mark none; expiries refuse overflow 2026-10-07 05:59:27 -04:00
ZacharyZhang-NY ad472490e0 Panics · P35 Settings: handlers in new; gone sections and themes mark none, drifted font sizes peg 2026-10-07 05:38:45 -04:00
ZacharyZhang-NY 45cecebea5 Panics · P34 Dashboard: handlers in new; gone tiles move nothing, unmatched tiles and cards stay out 2026-10-07 05:27:56 -04:00
ZacharyZhang-NY 05e846b62a Panics · P33 Devtools: handlers in new; stale collection moves, environments and diagram tables change nothing 2026-10-07 05:15:49 -04:00
ZacharyZhang-NY ffe4f38103 Panics · P32 Canvas: panels take their handlers in new; stale nodes, shapes, layers and topics change nothing 2026-10-07 05:02:40 -04:00
ZacharyZhang-NY 3741d5d5e8 Git · row actions always shown: refs rows put them by the name, the words below; changes rows keep them in reach 2026-10-07 04:40:26 -04:00
ZacharyZhang-NY bc6f1c2e73 Git · rows draw their focus ring; row actions show while the keyboard is on them 2026-10-07 04:34:38 -04:00
ZacharyZhang-NY 893eb392d0 Panics · P31 Project: stale drops and drag targets move nothing, timelines and roadmaps hold at jiff's ends, gone assignees show nobody 2026-10-07 04:29:32 -04:00
ZacharyZhang-NY 1c96a3ca7b Git · ref rows: hover actions sit on the first line 2026-10-07 04:17:26 -04:00
ZacharyZhang-NY 7fab0725fb Git · ref rows: detail on its own line, name gives way, actions take the trailing words' place on hover 2026-10-07 04:15:44 -04:00
ZacharyZhang-NY c5799cc7e4 Git · StashList: words in i18n, rows pick, only handled actions drawn 2026-10-07 03:57:22 -04:00
ZacharyZhang-NY 8a0344d2c4 Panics · P30 Calendar: views and recurrences stay inside jiff's range, the editor refuses edits past it 2026-10-07 03:54:07 -04:00
ZacharyZhang-NY a284c71e92 Git · merging: markers split at LF only, as Git writes them 2026-10-07 03:51:31 -04:00
ZacharyZhang-NY 7bb53f4779 Git · export MARKER 2026-10-07 03:39:03 -04:00
ZacharyZhang-NY 525085d6f3 Git · merging: a shorter comment 2026-10-07 03:37:37 -04:00
ZacharyZhang-NY af061f5737 Git · conflict markers take their width and whole lines; marked, resolve and conflicts share it 2026-10-07 03:37:31 -04:00
ZacharyZhang-NY 0e64693688 Panics · P29 Mail: gone boxes, labels and mails mark none; a moment no zone holds reads as none 2026-10-07 03:28:36 -04:00
ZacharyZhang-NY bb8732ac64 Panics · P28 Messaging: an open chat or selected member that left marks none 2026-10-07 03:22:11 -04:00
ZacharyZhang-NY c9b4a04a4f Panics · P27 Files: gone owner picks replace local ones and mark none, stale path folders go unmarked, duplicate marks never take a last copy 2026-10-07 03:17:17 -04:00
ZacharyZhang-NY 922cd53a8c Git · rows: trailing words give way before actions, which keep their room; row actions tested by key 2026-10-07 03:17:05 -04:00
ZacharyZhang-NY e01b82bcbd Git · merging: a shorter comment 2026-10-07 03:06:11 -04:00
ZacharyZhang-NY ef4ac5b1b8 Git · marked(): regions as Git grouped them in marked text, base from diff3 2026-10-07 03:06:03 -04:00
ZacharyZhang-NY 63d4a459c7 Git · BranchList: draws only the row actions its owner handles 2026-10-07 03:03:45 -04:00
ZacharyZhang-NY 078a2d19d8 Panics · P26 Media: live positions and trims peg to the media, gone choices clear, rails with no width and huge waveforms hold, subtitle times refuse overflow 2026-10-07 02:50:34 -04:00
ZacharyZhang-NY 829ef681ba Git · ThreeWayMerge draws rows in view with its words in i18n and lines read out; diff3 base sections parse; branch and tag rows pick from the keyboard; shorter comments 2026-10-07 02:36:16 -04:00
ZacharyZhang-NY 949d776d78 Git · DiffViewer: a shorter comment 2026-10-07 02:25:23 -04:00
ZacharyZhang-NY 037cae1bbc Git · DiffViewer: shorter comments 2026-10-07 02:25:16 -04:00
ZacharyZhang-NY 3d0a3dee08 Git, i18n: shorter comments 2026-10-07 02:25:08 -04:00
ZacharyZhang-NY fcc45ccbd8 Git · BlameView: a run's name leaves out empty parts 2026-10-07 02:13:37 -04:00
ZacharyZhang-NY 68a98198b0 Panics · P25 Generative: gone choices mark none, overruns peg; seed fields hold seeds through fits, undo and input methods 2026-10-07 02:12:07 -04:00
ZacharyZhang-NY fb7ee42e3d Git · DiffViewer and BlameView: every line, header and fold reads out to assistive tech 2026-10-07 02:09:30 -04:00
ZacharyZhang-NY d9cbe996a5 Data display · DescriptionList::text rows speak their label and value; Git · commit rows speak their refs; shorter comments 2026-10-07 01:52:19 -04:00
ZacharyZhang-NY 3fc5a46a13 Git · CommitItem: flex_shrink takes its factor 2026-10-07 01:34:33 -04:00
ZacharyZhang-NY e6220ad044 Git · CommitItem: narrow, the details give way before the subject 2026-10-07 01:32:53 -04:00
ZacharyZhang-NY 51d2aea02e Git · CommitList, FileHistory, BlameView draw only rows in view, rows are stops with list semantics; CommitList loads more near its end; CommitInput words read i18n 2026-10-07 01:16:54 -04:00
ZacharyZhang-NY 7b807a9478 Git · rows: a shorter comment 2026-10-07 01:01:08 -04:00
ZacharyZhang-NY e226d7979b Git · DiffViewer: rows move to their own file; shorter comments 2026-10-07 00:45:24 -04:00
ZacharyZhang-NY e69b96c9e3 Panics · P24 Agent: stale checkpoints and frames mark none; progress and memory past their totals fill 2026-10-07 00:39:48 -04:00
ZacharyZhang-NY 2e080b83de Buttons · SegmentedControl: each segment is a radio button with its label and state 2026-10-07 00:31:08 -04:00
ZacharyZhang-NY 2535260f45 Panics · P23: tick the item 2026-10-07 00:29:47 -04:00
ZacharyZhang-NY 0b8b74b968 Panics · P23 Chat: a branch past its total shows none; parameters peg into range; matches outside a passage stay out 2026-10-07 00:29:39 -04:00
ZacharyZhang-NY 30fea9ecec Git · DiffViewer: rows share the hunk button's height, so washes meet 2026-10-07 00:28:26 -04:00
ZacharyZhang-NY edd2c125e5 Git · ChangesList: each press names its section, rows are stops and one shows as current 2026-10-07 00:18:25 -04:00
ZacharyZhang-NY 9eab2019a8 Panics · P22 Collaboration: marks outside their text stay out under the owner's index; accept answers None; zero reactions and stale masked offsets hold 2026-10-07 00:17:15 -04:00
ZacharyZhang-NY 19b0b2997a Git · DiffViewer: stretches made elsewhere, a hunk button, pickable changed lines, words marked on built lines; its words read i18n 2026-10-07 00:14:51 -04:00
ZacharyZhang-NY 10788579c9 Panics · P21 Documents: stale block keys, cells and kinds change nothing; replaced text drops visual presses; nested images, versions and find hits hold 2026-10-06 23:39:51 -04:00
ZacharyZhang-NY 37c51874c8 Git · ChangesList: a shorter comment on Changed::lines 2026-10-06 23:19:52 -04:00
ZacharyZhang-NY 4f8f3bd4c4 Git · ChangesList: line counts are optional, so binary files and folders show none 2026-10-06 22:55:43 -04:00
ZacharyZhang-NY 490b0220f6 Git · ChangesList: conflicted, untracked and ignored files apart, each with its own actions; titles and tooltips read i18n (English and zh-CN built in) 2026-10-06 22:48:10 -04:00
ZacharyZhang-NY 295731d336 Panics · P20 Debug: stale selections, frames and focus paths mark none; the timeline's wheel keeps a drawable range 2026-10-06 22:23:58 -04:00
ZacharyZhang-NY 99980e413c Panics · P19 Git: a lagging blame draws every line; resolve answers None for a conflict gone 2026-10-06 21:52:25 -04:00
ZacharyZhang-NY 0bb3f4e858 Panics · P18 Terminal: paint faults log, a pty side caps at what it holds, a default shell past the list keeps the last 2026-10-06 21:34:44 -04:00
ZacharyZhang-NY b29d66ccc0 Panics · P17 Editor: owner offsets ride the text's edits, stale ranges, rows and globs log instead of panicking 2026-10-06 21:13:15 -04:00
ZacharyZhang-NY f0b7f779f6 Panics · P16 Finance: live market states, fine grids and lagging overlays draw or say why; system-zone times say UTC when unknown 2026-10-06 20:09:15 -04:00
ZacharyZhang-NY a6b1bb0929 Forms · SignaturePad: Sign here and Clear read i18n (English and zh-CN built in) 2026-10-06 19:05:31 -04:00
ZacharyZhang-NY defbec006e Panics · P15 Charts: values within charts::LIMIT; scales, shares and tangents stay finite at the edges; entries past their lists are skipped 2026-10-06 19:05:05 -04:00
ZacharyZhang-NY 2ea16f4541 Panics · P14 Tables: pivots, groupings and merges that name what their data lost log instead of panicking 2026-10-06 17:47:24 -04:00
ZacharyZhang-NY f2fc17118c Panics · P13 Lists: a directory selection gone from its entries picks none 2026-10-06 17:41:53 -04:00
ZacharyZhang-NY 4a05c12889 Panics · P12 Data Display: featured columns, gauges, usage bars and stars meet readings past their scale 2026-10-06 17:36:17 -04:00
ZacharyZhang-NY c9f492d0b4 Panics · P11 Motion: a buffer behind the value draws none 2026-10-06 17:21:29 -04:00
ZacharyZhang-NY 4c86e831bb Panics · P10 Feedback: the one site is a caller contract 2026-10-06 17:17:02 -04:00
ZacharyZhang-NY 8f00d2eabb Panics · P09 Overlays: a lightbox or tour past its last item closes 2026-10-06 17:14:09 -04:00
ZacharyZhang-NY 7af7c409c9 Panics · P08 Menus: every site is a contract or holds by construction 2026-10-06 17:09:27 -04:00
ZacharyZhang-NY 919f26fcc6 Panics · P07 Navigation: Wizard takes its handlers; stale places in tabs, crumbs, history, steps and pages mark none 2026-10-06 17:06:25 -04:00
ZacharyZhang-NY e2d880fb4f Panics · P06 Forms: roots, stale selections, shaping faults and gone units log instead of panicking; cron runs say UTC in an unknown zone 2026-10-06 16:49:35 -04:00
ZacharyZhang-NY 73a8bf2e34 Panics · P05 Buttons: a segmented choice missing from its segments marks none; slide logs and shows no marker 2026-10-06 16:32:12 -04:00
ZacharyZhang-NY 9bb342cb16 Panics · P04 Window & Shell: every site is a contract or holds by construction 2026-10-06 16:25:11 -04:00
ZacharyZhang-NY 2347edeed6 Panics · P03 Layout: panes and splits meet stale ids, lagging sizes and mid-drag changes without a panic 2026-10-06 16:21:39 -04:00
ZacharyZhang-NY f46cc4e7c3 Panics · P02 Typography: Link takes its handler, an unknown zone reads as UTC and says so, a long highlight query logs 2026-10-06 16:02:40 -04:00
ZacharyZhang-NY e5166e9e26 Panics · P01 Primitives: init returns an error, a missing app icon logs and shows a broken picture 2026-10-06 15:25:09 -04:00
ZacharyZhang-NY 52d3d790b1 Panics · P00: the policy in AGENTS.md and a task list by chapter 2026-10-06 15:00:30 -04:00
ZacharyZhang-NY 3d08744d38 Data display · DescriptionList: a value fills its column, so a field inside it is full width 2026-10-06 13:39:06 -04:00
ZacharyZhang-NY bd3772ad54 Charts · Navigation · Forms: band names widen a sideways chart's gutter; Pagination takes a size, speaks the locale and jumps to a typed page
- A horizontal chart's gutter grows to its widest band name, floored at the theme gutter and capped at half the chart; a name past the cap ends in an ellipsis (#10).
- Pagination's previous and next tooltips read i18n (English and zh-CN built in, app catalogs override); `.size(ControlSize)` sizes every control, Sm by default; `.jump()` adds a Go to field (#11).
- NumberInput keys the owner's value as shown: a draft survives a blur, a value set while focused lands when focus leaves, and an echo of its own commit is no new value. A ranged field holds room for its widest bound through `Input::least`.
2026-10-06 13:29:09 -04:00
ZacharyZhang-NY 2ce336895c Menus · ContextMenu: styles size its host, so a list inside can fill it 2026-10-06 09:17:56 -04:00
ZacharyZhang-NY 617a8d68de Editor · FindWidget: Escape from the query closes the box 2026-10-06 04:51:15 -04:00
ZacharyZhang-NY 71bfc93786 Lib · init_for_tests: theme and key bindings without assets, so apps can render Ely components in tests 2026-10-06 00:51:52 -04:00
ZacharyZhang-NY bcf0378ea7 Collab · CommentSidebar: threads are a list; each row is a Tab stop, named, selected, picked with Enter or Space 2026-10-05 20:21:09 -04:00
ZacharyZhang-NY 408f2b0582 Collab · CommentSidebar: localized copy and relative times; one list when the owner does not resolve 2026-10-05 19:26:33 -04:00
ZacharyZhang-NY af54877693 Forms · NumberInput: blank shows a placeholder and commits only what is typed 2026-10-05 17:48:31 -04:00
ZacharyZhang-NY 7df3045a82 Lists · SelectableList: list semantics, row names, selection and place in set 2026-10-05 16:43:30 -04:00
github-actions[bot] 17643893cb Site · Showcase: pin Ely-GPUI-Showcases at 8b5c36a 2026-10-05 18:56:55 +00:00
ZacharyZhang-NY 61562b588c CI · Showcase: a forced deploy keeps the pin while the catalog's main is still checking 2026-10-05 14:47:28 -04:00
ZacharyZhang-NY 94f34c9f8e CI · Showcase: a forced deploy keeps the pin while the catalog's main is still checking 2026-10-05 14:43:12 -04:00
ZacharyZhang-NY f2dd7dc474 CI · Showcase: move the site's pin to the catalog's main once its checks pass, build and deploy 2026-10-05 14:42:03 -04:00
ZacharyZhang-NY 7a5e1de8d1 Site · Showcase: pin Ely-GPUI-Showcases at 291ab4a, with TokenLedger (#1) 2026-10-05 14:06:56 -04:00
ZacharyZhang-NY 80c46b13d4 Documents · TemplatePicker: one Tab stop, arrows and Enter, list semantics, builtin copy 2026-10-05 13:13:09 -04:00
ZacharyZhang-NY 718617939e Forms · NumberInput label: the name assistive technology reads, drawn nowhere 2026-10-05 12:34:19 -04:00
ZacharyZhang-NY 47caebdd36 Forms · Select: rustfmt's wrap for the label's when_some, so CI's fmt check passes 2026-10-05 09:46:42 -04:00
ZacharyZhang-NY 8658999903 Forms · Select and TextInput label: the name assistive technology reads, drawn nowhere 2026-10-05 04:59:41 -04:00
ZacharyZhang-NY e21336fc1b Primitives · Focus: overlays dropped together hand focus back past every one of them 2026-10-05 04:13:35 -04:00
ZacharyZhang-NY 38deadde89 Primitives · Focus: an overlay dropped without Close hands focus back to what it took it from 2026-10-05 03:39:25 -04:00
ZacharyZhang-NY b4716bb09b Gallery · LicenseViewer reads Ely's MIT license from LICENSE 2026-10-05 01:38:24 -04:00
ZacharyZhang-NY d779585d4a Issue template: the version placeholder names a commit that exists after the rewrite 2026-10-05 01:27:15 -04:00
ZacharyZhang-NY ae06da3128 License: MIT alone, in LICENSE; the Apache-2.0 option is gone from the crate, README, CONTRIBUTING, the site's footer and docs 2026-10-05 01:26:01 -04:00
ZacharyZhang-NY 8139c359ce Tasks · G9: pushed, deployed, answered #6 and #7, closed #6 2026-10-05 01:14:58 -04:00
ZacharyZhang-NY 82c5275e7c Shell · ColumnShell's story on the Window & Shell page, the PRD line, stories.json and AGENTS.md (#6) 2026-10-05 01:14:17 -04:00
ZacharyZhang-NY 3935bb3fde 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) 2026-10-05 01:04:34 -04:00
ZacharyZhang-NY 2a18e9d0b3 Forms · An IME composition restarts the caret's blink, as every edit does, and logs its ranges (#7) 2026-10-05 00:35:46 -04:00
ZacharyZhang-NY 1f297c7801 Logo: logo.png, 2048px and transparent, rendered from the mark by scripts/favicon.mjs 2026-10-04 22:27:45 -04:00
ZacharyZhang-NY f80721abed 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.
2026-10-04 22:19:04 -04:00
ZacharyZhang-NY faf0987cf8 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.
2026-10-04 21:39:13 -04:00
ZacharyZhang-NY 78bf5917a5 Tests · NumberInput's commit test selects with the platform's secondary key; Tabs tests move to navigation/tests/tabs.rs, under 500 lines 2026-10-04 17:59:36 -04:00
Amit Verma dfe1e9f42c 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 2026-10-04 17:24:39 -04:00
ZacharyZhang-NY 9977785de6 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 64293a81af Menus · ContextMenu manual: one-line doc 2026-10-04 12:01:34 -04:00
ZacharyZhang-NY 2c62b4f7b9 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 f6f8b20624 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 b56acd2b9d 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 157b6ba761 Tasks · T50g: chapter 44 accepted, item by item 2026-10-04 00:52:22 -04:00
ZacharyZhang-NY 939397dbca 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 0033f1cffc Tasks · T50e: CI green on Linux, Windows and macOS, arm64 and x86_64 2026-10-04 00:20:56 -04:00
ZacharyZhang-NY 59c73f18f2 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 bbe45a4bcd 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 35fb8897a0 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 5267c44adf 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 eddb13f3d7 Tasks · T50a: chapter 44 Rendering, RenderSurface planned, what gpui can and cannot host 2026-10-03 23:14:27 -04:00
ZacharyZhang-NY 3921edebb0 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-NY e7dbcadc89 Tasks · G4: pushed, #2 closed, #3 answered 2026-10-03 01:36:16 -04:00
ZacharyZhang-NY 875cdfdc9b 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.
2026-10-03 01:35:48 -04:00
ZacharyZhang-NY ec586e371b 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.
2026-10-03 01:21:26 -04:00
JetSquirrel 943b7d4f8d 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.
2026-10-03 01:01:17 -04:00
ZacharyZhang-NY 7ba18fbb2f Dialogs · a confirm or alert message names itself to assistive technology 2026-10-02 19:28:01 -04:00
ZacharyZhang-NY 70162a7dbe 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 4580e3b075 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 8fd7b8ab87 Ely 0.1.1 2026-10-01 14:13:54 -04:00
ZacharyZhang-NY f1ba2d74bf T48m Docs · E2E passes review; T48 closes 2026-10-01 14:02:00 -04:00
ZacharyZhang-NY 3cb182410b 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 497dbe5414 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 91e86edac7 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 4067354454 T48l Docs · Testing passes review 2026-10-01 13:40:30 -04:00
ZacharyZhang-NY 66a4128c34 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 7d89aed97f T48k Docs · WebAssembly passes review 2026-10-01 13:33:37 -04:00
ZacharyZhang-NY b0261454a3 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 15d2eb887f 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 ca76311fd3 T48j Docs · Internationalization passes review 2026-10-01 13:22:35 -04:00
ZacharyZhang-NY 2ec8c979b1 T48j Docs · Internationalization: catalogs and messages, locales, right to left; the i18n example compiles 2026-10-01 13:15:07 -04:00
ZacharyZhang-NY 066b83e52d T48i Docs · Overlays passes review 2026-10-01 13:13:59 -04:00
ZacharyZhang-NY dc96946712 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 8c3b6840a3 T48i Docs · Overlays: drawing over the page, dialogs, popovers, toasts; the overlays example compiles 2026-10-01 13:07:03 -04:00
ZacharyZhang-NY 0e491cceb5 T48h Docs · Icons, fonts and assets passes review 2026-10-01 13:04:09 -04:00
ZacharyZhang-NY e5c90fdd40 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 0b6fa5b375 T48g Docs · Motion passes review 2026-10-01 12:55:27 -04:00
ZacharyZhang-NY 06a19fefd8 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 999f7f6d7f T48f Docs · Focus and keyboard passes review 2026-10-01 12:48:48 -04:00
ZacharyZhang-NY c1f67f1da5 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 0eeeb2db15 T48e Docs · Theme passes review 2026-10-01 12:36:39 -04:00
ZacharyZhang-NY b9356cc504 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 3001fe88de T48d Docs · Getting started passes review 2026-10-01 12:30:09 -04:00
ZacharyZhang-NY 3ca27c00d9 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 e36a0c49af T48c Docs · Installation passes review 2026-10-01 12:26:00 -04:00
ZacharyZhang-NY 2ac34ed7ee 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 d266fe7491 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 f245782248 T48b Docs · Introduction passes review 2026-10-01 12:15:07 -04:00
ZacharyZhang-NY 7f082f097a 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 264a9e3232 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 30dc94f44c 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 b2a4633333 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 fba90c22e6 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 157176e2fb CONTRIBUTING.md: setup, where things go, the house rules reviews catch, checks, license 2026-10-01 11:25:34 -04:00
ZacharyZhang-NY a52a3a0f29 Pull request template: what and why, screenshots, the house checklist 2026-10-01 11:14:21 -04:00
ZacharyZhang-NY a2a01c1f43 CI: checkout v7, setup-node v7 and pnpm/action-setup v6, on Node 24 2026-10-01 11:03:09 -04:00
ZacharyZhang-NY c2b70b1455 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 259ee2ab9c 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 465c48ce12 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 c5ad42d154 Remove frontend-reference: the folder, its ignore line and its docs 2026-10-01 10:03:27 -04:00
ZacharyZhang-NY 8d7797eaf2 更新 README.md 2026-10-01 08:51:27 -04:00
ZacharyZhang-NY ed2904ce60 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 293ab76a0f T47 Ship: wrangler leaves macOS folder files out 2026-10-01 08:44:46 -04:00
ZacharyZhang-NY fe1a7f35d6 T47 Ship: the OG image lives at the site's own domain 2026-10-01 08:43:43 -04:00
ZacharyZhang-NY d06a0e9e1a T46a Site · the hero title fits at 768 too 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 28dc0bdb35 T46a Site · Owner's notes pass review; the hero title fits its column 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY ac1a869fb5 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 9079b21bdb T46 E2E passes review; T45 closes 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 033c41c5cc 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 4587d1f251 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 1bf9c632ec T45e Site · Local passes review 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 5d198634e7 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 ffbb37183d 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 4aff314bb5 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 8d647e632f T45d Site · Components: comments one short line 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 8712fd0389 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 9ea811d4e8 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 d7dbcfbb18 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 03f40c4b86 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 b47f2267e4 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 7fd05df4dc T45b Site · Iso passes review 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY d05e73770f 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 02c2bbe0a1 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 ddcc8a4938 T45a Site · Stack passes review 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 49d5c8161d T45a Site · Stack: comments one short line 2026-10-01 08:38:05 -04:00
ZacharyZhang-NY 3ae04f4ead 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 167055fdae 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 e3957f7816 Update README.md 2026-09-30 00:29:08 -04:00
ZacharyZhang-NY 327bdf4b7f 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 587e08b5cb 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 f50a708951 T46 E2E: Playwright walks the built site, desktop and phone, live stories included 2026-09-29 23:40:04 -04:00
ZacharyZhang-NY 6ff43607b7 T45 Website: a home in motion and every story live, chapters on the left 2026-09-29 23:40:04 -04:00
ZacharyZhang-NY 58d3fd7fe2 T44d Web · Stories passes review 2026-09-29 23:39:50 -04:00
ZacharyZhang-NY dcdd46038e 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 81adec5c47 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 2463bb0ac9 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 edab58e91a T44d Web · The gallery's start takes its defaults from one place 2026-09-29 19:16:41 -04:00
ZacharyZhang-NY 2cd2ca8bc1 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 48c7b09d8d 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 a03df0987c web: the library checks for wasm32 (carried) 2026-09-29 19:16:41 -04:00
ZacharyZhang-NY fe467d2bfe T44c Port · Parity passes review 2026-09-29 19:14:18 -04:00
ZacharyZhang-NY 2da09c1947 Port · Parity: the last long comments are short 2026-09-29 18:43:49 -04:00
ZacharyZhang-NY c59d19645d 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-NY 3f1597fa28 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.
2026-09-29 10:39:55 -04:00
ZacharyZhang-NY 3189df013f Tasks: the site deploys to Cloudflare through wrangler
The Dokploy line was a misreading; the plan stays as it was.
2026-09-29 00:28:32 -04:00
ZacharyZhang-NY 48dce3b52b T44b Port · Build passes review 2026-09-28 21:24:24 -04:00
ZacharyZhang-NY 05bd22b7f0 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.
2026-09-28 20:54:25 -04:00
ZacharyZhang-NY 4c3ed1bbb9 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.
2026-09-28 20:30:17 -04:00
ZacharyZhang-NY 25b8a6f22c 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.
2026-09-28 20:13:44 -04:00
ZacharyZhang-NY 5117a5fc1b T44a Capture · Roots passes review 2026-09-28 20:13:05 -04:00
ZacharyZhang-NY a6f9a7b77b Capture · Roots: the roots round 2 missed
A sweep that read every render found 28 roots still on w_full,
reached by 64 components: the field button under Select and every
picker, the checked and mention fields, the slider track,
NumberInput, Steps, the state views, the agent previews, and every
chart and finance frame. All drop it. LivePreview's size_full becomes
h_full, since its card no longer fills.

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

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

AGENTS.md names the roots that keep w_full and why: picture frames
that keep a ratio, and rows that hand a text field its share.
2026-09-28 19:34:01 -04:00
ZacharyZhang-NY ad98ca7746 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.
2026-09-28 16:30:47 -04:00
ZacharyZhang-NY 81e73b79c1 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.
2026-09-28 13:07:22 -04:00
ZacharyZhang-NY 3be809a660 Tasks: split T44 into three 2026-09-28 09:12:25 -04:00
ZacharyZhang-NY 59f57c89c2 T43c Tooling · Catalogs passes review 2026-09-28 09:10:55 -04:00
ZacharyZhang-NY a7e492f28e 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.
2026-09-28 08:57:52 -04:00
ZacharyZhang-NY d5271d03ea 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.
2026-09-28 08:23:39 -04:00
ZacharyZhang-NY bfd1285064 T43b Tooling · Inspection passes review 2026-09-28 08:05:26 -04:00
ZacharyZhang-NY 4d1a17d29a 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.
2026-09-28 07:54:54 -04:00
ZacharyZhang-NY 462e47b854 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.
2026-09-28 07:13:12 -04:00
ZacharyZhang-NY 274089a8f6 T43a Tooling · Workbench passes review 2026-09-28 06:46:24 -04:00
ZacharyZhang-NY 8c038e2c24 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-NY 1dbdafd056 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.
2026-09-28 06:08:40 -04:00
ZacharyZhang-NY c9ff1f8f36 Tasks: split T43 into three 2026-09-28 06:03:22 -04:00
ZacharyZhang-NY 7ed840e02d T42g Misc · Export and import passes review 2026-09-28 06:03:05 -04:00
ZacharyZhang-NY 385faa0581 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.
2026-09-28 05:50:28 -04:00
ZacharyZhang-NY 79fe78cefd 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.
2026-09-28 05:20:03 -04:00
ZacharyZhang-NY cbba4ddeec 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.
2026-09-28 05:07:35 -04:00
ZacharyZhang-NY c7c4c006a7 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.
2026-09-28 04:59:11 -04:00
ZacharyZhang-NY 012a6e3105 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.
2026-09-28 04:32:26 -04:00
ZacharyZhang-NY a978f7a6eb T42e Misc · Terms and notices passes review
A test walks Shift-Tab round the cookie banner past the resting Necessary switch.
2026-09-28 04:25:31 -04:00
ZacharyZhang-NY 8794a42d42 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.
2026-09-28 04:09:49 -04:00
ZacharyZhang-NY 2837c802b5 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.
2026-09-28 03:36:03 -04:00
ZacharyZhang-NY 320ab4ae3a T42d Misc · Asking and learning passes review
The poll's mark and its row's selector share one binding.
2026-09-28 03:24:15 -04:00
ZacharyZhang-NY 46aa381eee 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.
2026-09-28 03:11:52 -04:00
ZacharyZhang-NY 86c9de7d0a 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.
2026-09-28 02:38:33 -04:00
ZacharyZhang-NY a213248910 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.
2026-09-28 02:22:34 -04:00
ZacharyZhang-NY a184b4ef9a 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.
2026-09-28 02:05:44 -04:00
ZacharyZhang-NY fd052086eb Tasks: the website's home page, components page and Cloudflare deploy 2026-09-28 01:40:15 -04:00
ZacharyZhang-NY 692fae6244 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.
2026-09-28 01:40:15 -04:00
ZacharyZhang-NY dc4911a68f T42b Misc · Numbers passes review 2026-09-28 01:14:17 -04:00
ZacharyZhang-NY 5e7fc24db9 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.
2026-09-28 01:06:32 -04:00
ZacharyZhang-NY 73f3399cea 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.
2026-09-28 00:41:49 -04:00
ZacharyZhang-NY d1345cd14e T42a Misc · Time passes review 2026-09-28 00:30:00 -04:00
ZacharyZhang-NY b586300f0e 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.
2026-09-28 00:21:23 -04:00
ZacharyZhang-NY 45ae161c18 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.
2026-09-27 23:51:58 -04:00
ZacharyZhang-NY 4777cd666b Tasks: split T42 into seven 2026-09-27 23:44:24 -04:00
ZacharyZhang-NY e97850163f T41c Maps · World passes review 2026-09-27 23:42:16 -04:00
ZacharyZhang-NY d52ca0e6a9 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.
2026-09-27 23:35:04 -04:00
ZacharyZhang-NY a4ca1f04d4 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.
2026-09-27 23:12:27 -04:00
ZacharyZhang-NY a64b78b673 T41b Maps · Layers passes review 2026-09-27 23:01:43 -04:00
ZacharyZhang-NY 3e5c5f37ba 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.
2026-09-27 22:57:05 -04:00
ZacharyZhang-NY e3a96b04e0 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.
2026-09-27 22:35:40 -04:00
ZacharyZhang-NY 91a0fc2252 T41a Maps · View passes review 2026-09-27 22:20:08 -04:00
ZacharyZhang-NY d1202a88e2 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.
2026-09-27 22:14:42 -04:00
ZacharyZhang-NY 5f1c8ff46a 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.
2026-09-27 21:57:27 -04:00
ZacharyZhang-NY 931e0ed6f5 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.
2026-09-27 21:22:50 -04:00
ZacharyZhang-NY 090520a9f1 Tasks: split T41 into view, layers and world 2026-09-27 20:57:30 -04:00
ZacharyZhang-NY 2863e4fde2 T40b a11y passes review 2026-09-27 20:52:32 -04:00
ZacharyZhang-NY 4aaff497fd 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.
2026-09-27 20:42:19 -04:00
ZacharyZhang-NY 1ac5c04fb0 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.
2026-09-27 20:00:26 -04:00
ZacharyZhang-NY aa39fba87f T40a i18n passes review 2026-09-27 19:40:34 -04:00
ZacharyZhang-NY 451fdcc7c0 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.
2026-09-27 19:36:12 -04:00
ZacharyZhang-NY 83ecacde65 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.
2026-09-27 19:20:13 -04:00
ZacharyZhang-NY ae5f2334f5 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.
2026-09-27 18:59:45 -04:00
ZacharyZhang-NY 17e3e19fc9 Tasks: split T40 into i18n and a11y 2026-09-27 18:26:52 -04:00
ZacharyZhang-NY ba5ec73ecb T39c Theme · Platform passes review 2026-09-27 18:25:40 -04:00
ZacharyZhang-NY 5f5504be38 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.
2026-09-27 18:20:20 -04:00
ZacharyZhang-NY ff1e97132e 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.
2026-09-27 18:04:05 -04:00
ZacharyZhang-NY d3cfc71ead 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.
2026-09-27 17:41:15 -04:00
ZacharyZhang-NY 7f2205c811 T39b Theme · Importing passes review 2026-09-27 17:34:16 -04:00
ZacharyZhang-NY d95ee5a88d 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.
2026-09-27 17:32:21 -04:00
ZacharyZhang-NY 5f61f3e08d 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.
2026-09-27 17:11:55 -04:00
ZacharyZhang-NY d2a5c0d955 T39a Theme · Editing passes review 2026-09-27 17:02:23 -04:00
ZacharyZhang-NY fe6a20d463 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.
2026-09-27 16:58:23 -04:00
ZacharyZhang-NY 84bc45df5f 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.
2026-09-27 16:48:07 -04:00
ZacharyZhang-NY 9160fdc931 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.
2026-09-27 16:31:33 -04:00
ZacharyZhang-NY 5edc937cdf T38b passes review in round 1 2026-09-27 16:18:23 -04:00
ZacharyZhang-NY 9a976b1c00 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.
2026-09-27 16:10:32 -04:00
ZacharyZhang-NY 08171a269d T38a passes review in round 2 2026-09-27 16:01:08 -04:00
ZacharyZhang-NY c19c45ca8a 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.
2026-09-27 15:58:19 -04:00
ZacharyZhang-NY 040fc94f49 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.
2026-09-27 15:42:03 -04:00
ZacharyZhang-NY 8ade21b7ba T37b passes review in round 2 2026-09-27 15:32:18 -04:00
ZacharyZhang-NY 3bd4b63d91 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.
2026-09-27 15:28:49 -04:00
ZacharyZhang-NY fe1b00d414 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.
2026-09-27 15:05:06 -04:00
ZacharyZhang-NY d4ba8f7037 T37a passes review in round 2 2026-09-27 14:56:30 -04:00
ZacharyZhang-NY 1879f6f08b 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.
2026-09-27 14:48:20 -04:00
ZacharyZhang-NY eea99b8e41 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.
2026-09-27 14:20:45 -04:00
ZacharyZhang-NY c68e3229bb T36c passes review in round 2 2026-09-27 14:14:12 -04:00
ZacharyZhang-NY a2f654044e 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.
2026-09-27 14:05:34 -04:00
ZacharyZhang-NY 431aab594f 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.
2026-09-27 13:49:17 -04:00
ZacharyZhang-NY bade1ef0d5 TASKS: T36b passes review 2026-09-27 13:41:13 -04:00
ZacharyZhang-NY 90afc6381c 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.
2026-09-27 13:38:40 -04:00
ZacharyZhang-NY 31f5f91507 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.
2026-09-27 13:29:35 -04:00
ZacharyZhang-NY 0177d9f3c7 TASKS: T36a passes review 2026-09-27 13:23:11 -04:00
ZacharyZhang-NY 2e8f0e79a5 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.
2026-09-27 13:20:37 -04:00
ZacharyZhang-NY c9e3400a71 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.
2026-09-27 13:12:49 -04:00
ZacharyZhang-NY 69befddeb3 TASKS: T35 passes review 2026-09-27 13:06:34 -04:00
ZacharyZhang-NY 8e4d1f7785 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.
2026-09-27 13:03:55 -04:00
ZacharyZhang-NY 195f9964a0 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.
2026-09-27 12:53:08 -04:00
ZacharyZhang-NY 380cdb0676 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.
2026-09-27 12:22:58 -04:00
ZacharyZhang-NY 27c7cd965b TASKS: T34 passes review 2026-09-27 12:10:59 -04:00
ZacharyZhang-NY 15e34fcb35 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.
2026-09-27 12:08:44 -04:00
ZacharyZhang-NY 9fddf4b6bf 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.
2026-09-27 11:51:19 -04:00
ZacharyZhang-NY f3332f935b TASKS: T33d passes review 2026-09-27 11:38:54 -04:00
ZacharyZhang-NY 8b6bf80adf 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.
2026-09-27 11:15:50 -04:00
ZacharyZhang-NY f7013b17ef 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.
2026-09-27 11:02:35 -04:00
ZacharyZhang-NY ec82331ac1 TASKS: T33c passes review 2026-09-27 10:53:02 -04:00
ZacharyZhang-NY da549cae24 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.
2026-09-27 10:51:19 -04:00
ZacharyZhang-NY 658e4e73c6 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.
2026-09-27 10:44:21 -04:00
ZacharyZhang-NY 3c9f52f853 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.
2026-09-27 10:24:44 -04:00
ZacharyZhang-NY 2ec826761e 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.
2026-09-27 10:12:20 -04:00
ZacharyZhang-NY 54328a54ca 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.
2026-09-27 10:03:53 -04:00
ZacharyZhang-NY b87704a7c7 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.
2026-09-27 09:42:03 -04:00
ZacharyZhang-NY 86a8b4a958 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.
2026-09-27 09:22:30 -04:00
ZacharyZhang-NY 08c8ebf386 TASKS: T33a passes review 2026-09-27 09:19:30 -04:00
ZacharyZhang-NY 60f3c4a34e 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.
2026-09-27 09:16:56 -04:00
ZacharyZhang-NY 0c8e0d47ba 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.
2026-09-27 09:05:35 -04:00
ZacharyZhang-NY c137ddb0f7 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.
2026-09-27 08:51:45 -04:00
ZacharyZhang-NY 56ae76664f TASKS: T32d passes review; T33 splits into four 2026-09-27 08:45:02 -04:00
ZacharyZhang-NY 31bd483dc5 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.
2026-09-27 08:41:05 -04:00
ZacharyZhang-NY 538c3ed93f 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.
2026-09-27 08:35:08 -04:00
ZacharyZhang-NY 4498ff09dc 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.
2026-09-27 08:22:54 -04:00
ZacharyZhang-NY ec1988cd6f TASKS: T32c passes review 2026-09-27 08:07:36 -04:00
ZacharyZhang-NY 07b875b0d3 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.
2026-09-27 07:58:27 -04:00
ZacharyZhang-NY c637347a2a TASKS: T32b passes review 2026-09-27 07:50:23 -04:00
ZacharyZhang-NY af93eece28 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.
2026-09-27 07:41:10 -04:00
ZacharyZhang-NY c474a3eb33 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.
2026-09-27 07:27:27 -04:00
ZacharyZhang-NY 50259aff77 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.
2026-09-27 07:15:01 -04:00
ZacharyZhang-NY e5a0c19611 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.
2026-09-27 06:58:59 -04:00
ZacharyZhang-NY 384872cbf0 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.
2026-09-27 06:32:50 -04:00
ZacharyZhang-NY f70726476a 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.
2026-09-27 06:11:38 -04:00
ZacharyZhang-NY 943862caad 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.
2026-09-27 05:53:28 -04:00
ZacharyZhang-NY 207d4b0109 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.
2026-09-27 05:42:03 -04:00
ZacharyZhang-NY e630e61a63 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.
2026-09-27 05:28:35 -04:00
ZacharyZhang-NY 8cbd389d72 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.
2026-09-27 05:12:20 -04:00
ZacharyZhang-NY 1633bc9293 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.
2026-09-27 04:48:54 -04:00
ZacharyZhang-NY c99465153d 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.
2026-09-27 04:31:14 -04:00
ZacharyZhang-NY 0bcfd82bf3 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.
2026-09-27 04:22:02 -04:00
ZacharyZhang-NY 48ff348357 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.
2026-09-27 03:35:59 -04:00
ZacharyZhang-NY 57dd55320e 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.
2026-09-27 03:23:18 -04:00
ZacharyZhang-NY 7e3f99f278 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.
2026-09-27 03:13:38 -04:00
ZacharyZhang-NY cd5ed088c7 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.
2026-09-27 02:49:37 -04:00
ZacharyZhang-NY 51ade29032 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.
2026-09-27 02:23:05 -04:00
ZacharyZhang-NY 341420d1e4 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.
2026-09-27 01:51:51 -04:00
ZacharyZhang-NY ccbc0970e5 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.
2026-09-27 01:16:24 -04:00
ZacharyZhang-NY 14b5faa80c 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.
2026-09-27 00:38:30 -04:00
ZacharyZhang-NY ccd48f9ba2 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.
2026-09-27 00:27:23 -04:00
ZacharyZhang-NY 1ad72bc7aa 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.
2026-09-27 00:07:27 -04:00
ZacharyZhang-NY 70e18af092 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.
2026-09-26 23:57:06 -04:00
ZacharyZhang-NY d2379bc911 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.
2026-09-26 23:45:22 -04:00
ZacharyZhang-NY 7d20567921 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.
2026-09-26 23:35:04 -04:00
ZacharyZhang-NY 7b38f99dcf 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.
2026-09-26 23:11:46 -04:00
ZacharyZhang-NY 1700a72753 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.
2026-09-26 23:00:15 -04:00
ZacharyZhang-NY dd465fedb7 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.
2026-09-26 22:33:54 -04:00
ZacharyZhang-NY dd5e36cd94 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.
2026-09-26 22:08:25 -04:00
ZacharyZhang-NY 99b8a6081b 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.
2026-09-26 21:48:17 -04:00
ZacharyZhang-NY cd14b32dfe 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.
2026-09-26 21:30:17 -04:00
ZacharyZhang-NY 85df47fd5c 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.
2026-09-26 21:12:38 -04:00
ZacharyZhang-NY 4ef9830def 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.
2026-09-26 21:05:59 -04:00
ZacharyZhang-NY b280929d07 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.
2026-09-26 20:55:21 -04:00
ZacharyZhang-NY d76a489934 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.
2026-09-26 20:42:06 -04:00
ZacharyZhang-NY 4717aad596 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.
2026-09-26 20:22:12 -04:00
ZacharyZhang-NY 14de69fed6 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.
2026-09-26 20:07:33 -04:00
ZacharyZhang-NY 501d1b0f1a 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.
2026-09-26 19:51:43 -04:00
ZacharyZhang-NY a846e10847 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.
2026-09-26 19:40:10 -04:00
ZacharyZhang-NY e75171c995 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.
2026-09-26 19:17:52 -04:00
ZacharyZhang-NY d1b8ad395d 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.
2026-09-26 19:05:23 -04:00
ZacharyZhang-NY cb6c78b90c 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`.
2026-09-26 18:44:38 -04:00
ZacharyZhang-NY 04f88812dc 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.
2026-09-26 18:32:45 -04:00
ZacharyZhang-NY 1ac50e4002 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.
2026-09-26 18:14:06 -04:00
ZacharyZhang-NY f8fcfa2870 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.
2026-09-26 17:52:27 -04:00
ZacharyZhang-NY d125314d39 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.
2026-09-26 17:07:26 -04:00
ZacharyZhang-NY 99b6ccd798 Reviews: T26b passes 2026-09-26 16:46:40 -04:00
ZacharyZhang-NY 1788753f6d 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.
2026-09-26 16:38:22 -04:00
ZacharyZhang-NY 5cd7ee0ea8 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.
2026-09-26 16:16:29 -04:00
ZacharyZhang-NY 4acedc60c0 Reviews: T26a passes 2026-09-26 15:58:02 -04:00
ZacharyZhang-NY 0ccf4c1587 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.
2026-09-26 15:51:03 -04:00
ZacharyZhang-NY 9ed801b67d 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.
2026-09-26 15:34:45 -04:00
ZacharyZhang-NY 14f67f8429 Reviews: T25f passes 2026-09-26 15:22:31 -04:00
ZacharyZhang-NY 3d9cf1008e 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.
2026-09-26 15:17:45 -04:00
ZacharyZhang-NY e3038647c8 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.
2026-09-26 14:57:55 -04:00
ZacharyZhang-NY 2f859eeb03 Reviews: T25e passes 2026-09-26 14:02:23 -04:00
ZacharyZhang-NY 7c6b4c883d 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.
2026-09-26 13:57:14 -04:00
ZacharyZhang-NY ff4e828795 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 5ba76ace99 Reviews: T25d passes
Round 2 of the Claude reviewer found no defects.
2026-09-26 13:37:25 -04:00
ZacharyZhang-NY 396531c075 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 a4f101f7b3 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 eb0634e7a0 Reviews: T25c passes
Round 2 of the Claude reviewer found no defects.
2026-09-26 13:09:53 -04:00
ZacharyZhang-NY 23e65d75b9 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 02f0a8deaf 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 c349a025e5 Reviews: T25b passes
Round 2 of the Claude reviewer found no defects.
2026-09-26 12:47:12 -04:00
ZacharyZhang-NY 03e36ab0ef 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 81d8d52a09 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 f0431411d5 Reviews: T25a passes
Round 2 of the Claude reviewer found no defects.
2026-09-26 12:26:37 -04:00
ZacharyZhang-NY 97637dd5dd 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 66345b7e24 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 173e89ed03 Reviews: T24f passes
Round 2 of the Claude reviewer found no defects.
2026-09-26 12:06:06 -04:00
ZacharyZhang-NY 248abc96de 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 37e53a5ecb 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 061be8a0ff Reviews: T24e passes
Round 2 of the Claude reviewer found no defects.
2026-09-26 11:12:29 -04:00
ZacharyZhang-NY 42f64f50bc 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 cbdcf85cec 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 8550c086b0 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 6274b24f03 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 8b6f18f66c 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 dcc4a39e56 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 8c92651e6a Reviews: T24c passes 2026-09-26 08:55:21 -04:00
ZacharyZhang-NY b9bff970d0 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 0a44a2d905 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 859a9051d8 Reviews: T24b passes 2026-09-26 08:34:31 -04:00
ZacharyZhang-NY 85988b14d4 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 2d78db57a8 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 91a513100c Reviews: T24a passes 2026-09-26 08:21:23 -04:00
ZacharyZhang-NY 055d425a90 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 aac5e4ead2 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 1db0a0db44 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 85b01098a1 Reviews: T23e passes; T24 splits into six in PRD order 2026-09-26 07:56:18 -04:00
ZacharyZhang-NY f64dd5806c 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 b967e7d208 Reviews: T23d passes 2026-09-26 07:46:11 -04:00
ZacharyZhang-NY 4f9e9eb756 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 f3336b18bf 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 247f836073 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 7f4bd8415c Reviews: T23c passes 2026-09-26 07:15:16 -04:00
ZacharyZhang-NY 3f2fe16e47 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 eb4e9422c9 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 d362f08d41 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 fce38dec00 Reviews: T23b passes 2026-09-26 06:24:16 -04:00
ZacharyZhang-NY a8defc99cd 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 af258244c8 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 471c7bc188 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 4cbdd9322c Reviews: T23a passes 2026-09-26 06:02:09 -04:00
ZacharyZhang-NY 8d91554f8b 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 c0afce2507 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 03f6ceb583 Reviews: T22 passes 2026-09-26 05:27:57 -04:00
ZacharyZhang-NY b2211f842e 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 11f3e08294 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 4cc94289ca Reviews: T21b and T21c pass 2026-09-26 05:01:35 -04:00
ZacharyZhang-NY 444a95a854 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 9854cfa460 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 14a8990793 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 187f54253e 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 f3e2c6c27f 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 6a9ce8a2c3 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 0ffa001a61 Reviews: T20 passes 2026-09-26 02:57:34 -04:00
ZacharyZhang-NY 2f3b0ac8bb 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 f8bfa29c17 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 2c961833ac Reviews: T19 passes 2026-09-26 02:32:07 -04:00
ZacharyZhang-NY b950f3e614 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 75656358cf 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-NY 950f17636c 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 2026-09-26 01:59:27 -04:00
ZacharyZhang-NY e79ab866c3 Reviews: T18 passes 2026-09-26 01:53:52 -04:00
ZacharyZhang-NY f668f320b8 T18 round 2 fixes: a folder the terminal cannot enter stops the start, and links read combining clusters whole 2026-09-26 01:50:58 -04:00
ZacharyZhang-NY f6ad8d1e50 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 2026-09-26 01:44:13 -04:00
ZacharyZhang-NY 7da1368cd9 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 2026-09-26 01:27:44 -04:00
ZacharyZhang-NY dd09d2802a Reviews: T17a–e pass 2026-09-26 01:20:19 -04:00
ZacharyZhang-NY 40bce9b945 T17 round 2 fixes: merge keeps the newest caret in the lead by where it was given, and a composition cleared to nothing ends 2026-09-26 01:16:08 -04:00
ZacharyZhang-NY 8ea952c6dc 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 2026-09-26 01:07:37 -04:00
ZacharyZhang-NY 5545ce6458 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.
2026-09-26 00:43:41 -04:00
ZacharyZhang-NY a894e5f973 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 2026-09-26 00:20:08 -04:00
551 changed files with 34845 additions and 6684 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: 4f0b372 placeholder: 259ee2a
- type: textarea - type: textarea
id: logs id: logs
attributes: attributes:
+11 -2
View File
@@ -22,7 +22,8 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-24.04, ubuntu-24.04-arm, windows-latest, windows-11-arm, macos-15-intel] # One leg per runner registered with git.zacharyzhang.com; macOS arm64 is the crate job above.
os: [ubuntu-24.04, windows-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
@@ -30,7 +31,15 @@ jobs:
with: with:
toolchain: "1.95" toolchain: "1.95"
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: cargo test --lib --features test-support --quiet - if: runner.os != 'Linux'
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
@@ -0,0 +1,120 @@
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,3 +2,4 @@
node_modules/ node_modules/
/frontend/dist/ /frontend/dist/
.DS_Store .DS_Store
/compat/kit/Cargo.lock
+28
View File
@@ -0,0 +1,28 @@
# 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
@@ -0,0 +1,25 @@
# 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.
+33 -16
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 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. - 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.
- 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,11 +13,12 @@ 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: `pulldown-cmark` 0.13 (MIT, default features off) for CommonMark with tables, tasks, strikethrough, footnotes and math. - 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
- 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`: 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). - 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).
## Commands ## Commands
@@ -29,7 +30,8 @@ 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.
- `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. - `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 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.
@@ -38,22 +40,24 @@ 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. - 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.
- 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`. 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` 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.
- 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.
@@ -62,11 +66,12 @@ 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 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.
- 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.
@@ -79,12 +84,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` 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. - 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`).
- 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 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.
- 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.
@@ -148,12 +153,13 @@ 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()`. - 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.
- 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.
@@ -173,12 +179,13 @@ 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. 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. 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.
- 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.
@@ -189,7 +196,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`, which fails loud; jiff's `TimeZone::system()` falls back to UTC in silence. - 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.
- 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`).
@@ -198,12 +205,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 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`).
- 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`, 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`. - 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`.
- 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.
@@ -214,13 +221,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 yields before a header's own parts; the host's header items keep one control's width and scroll when short. - 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.
- 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. 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. 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.
- 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`).
@@ -242,6 +249,7 @@ 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.
@@ -258,4 +266,13 @@ 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.
+15 -1
View File
@@ -46,4 +46,18 @@ CI runs the same on every pull request. Fill in the pull request template: what
## License ## License
Ely is MIT or Apache-2.0, at your option. Unless you say otherwise, what you contribute is licensed the same way. Ely is MIT. 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
+593 -229
View File
File diff suppressed because it is too large. Load diff
+15 -6
View File
@@ -3,31 +3,38 @@ 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 OR Apache-2.0" license = "MIT"
description = "A restrained component library for GPUI, in light and dark." description = "A restrained component library for GPUI, in light and dark."
repository = "https://github.com/ZacharyZhang-NY/Ely-GPUI-Components" repository = "https://git.zacharyzhang.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"] test-support = ["gpui/test-support", "gpui/leak-detection"]
[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 = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b", default-features = false, features = ["stacker"] } gpui = { package = "gpui-pre", version = "=0.3.8", default-features = false, features = ["stacker"] }
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b", features = ["font-kit"] } gpui_platform = { package = "gpui-pre-platform", version = "=0.3.8", 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"
@@ -36,6 +43,8 @@ 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"
@@ -61,7 +70,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 = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b" } gpui_web = { package = "gpui-pre-web", version = "=0.3.8" }
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"] }
View File
File renamed without changes.
-176
View File
@@ -1,176 +0,0 @@
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
+10 -6
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://github.com/ZacharyZhang-NY/Ely-GPUI-Components" } ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components" }
gpui = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b" } gpui = { package = "gpui-pre", version = "=0.3.8" }
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b", features = ["font-kit"] } gpui_platform = { package = "gpui-pre-platform", version = "=0.3.8", features = ["font-kit"] }
``` ```
```rust ```rust
@@ -22,13 +22,17 @@ 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); ely_gpui_component::init(cx).expect("Ely failed to start");
Theme::set_mode(Mode::Dark, cx); Theme::set_mode(Mode::Dark, cx);
}); });
} }
``` ```
`init` registers the fonts and the theme. It panics if `Assets` is not wired into the application. `init` registers the fonts and the theme. It returns an error if the application's asset source cannot load one of Ely's files.
## 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
@@ -61,5 +65,5 @@ pnpm deploy # Cloudflare, through wrangler
## License ## License
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. MIT. 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,6 +187,8 @@ 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
@@ -0,0 +1,17 @@
<!-- @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>

After

Width:  |  Height:  |  Size: 481 B

+16
View File
@@ -0,0 +1,16 @@
[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
@@ -0,0 +1,89 @@
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
@@ -0,0 +1,26 @@
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
@@ -0,0 +1,112 @@
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
@@ -0,0 +1,117 @@
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
@@ -0,0 +1,53 @@
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
@@ -0,0 +1,10 @@
//! 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); ely_gpui_component::init(cx).expect("Ely failed to start");
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); ely_gpui_component::init(cx).expect("Ely failed to start");
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); ely_gpui_component::init(cx).expect("Ely failed to start");
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); ely_gpui_component::init(cx).expect("Ely failed to start");
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); ely_gpui_component::init(cx).expect("Ely failed to start");
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); ely_gpui_component::init(cx).expect("Ely failed to start");
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); ely_gpui_component::init(cx).expect("Ely failed to start");
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); ely_gpui_component::init(cx).expect("Ely failed to start");
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); ely_gpui_component::init(cx).expect("Ely failed to start");
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();
+13 -6
View File
@@ -41,7 +41,8 @@ mod web;
use std::path::PathBuf; use std::path::PathBuf;
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
use anyhow::{Context as _, Result, bail}; use anyhow::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};
@@ -113,8 +114,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) -> WindowHandle<shell::Gallery> { fn launch(start: Start, cx: &mut App) -> Result<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);
@@ -142,10 +143,10 @@ fn launch(start: Start, cx: &mut App) -> 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))
}) })
.expect("gallery window failed to open"); .context("gallery window failed to open")?;
menu_actions(window, cx); menu_actions(window, cx);
cx.activate(true); cx.activate(true);
window Ok(window)
} }
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
@@ -204,7 +205,13 @@ 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 = launch(start, cx); let window = match 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);
} }
+15 -16
View File
@@ -73,23 +73,20 @@ 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) WorkspaceSwitcher::new("account-workspaces", workspaces, workspace, move |key, _, cx| change(&to_workspace, cx, |places| places.workspace = key.clone()))
.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) AccountSwitcher::new("account-accounts", accounts, account, move |key, _, cx| change(&to_account, cx, |places| places.account = key.clone()))
.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") UserMenu::new("account-user", "Ada Lovelace", "ada@atlas.dev", |_, _| log::info!("gallery: sign out"))
.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")),
)), )),
) )
} }
@@ -133,7 +130,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).on_save(move |profile, _, cx| { .child(div().w(px(400.)).child(ProfileEditor::new("account-editor", now, 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)
}))), }))),
@@ -215,8 +212,10 @@ 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("account-keys", keys) let mut manager = ApiKeyManager::new(
.on_create(move |name, _, cx| { "account-keys",
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;
@@ -233,14 +232,15 @@ 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());
}) })
}) },
.on_revoke(move |key, _, cx| { 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)
}) })
}) },
.on_dismiss(move |_, cx| change(&done, cx, |security| security.secret = None)); 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,8 +256,7 @@ 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) SessionList::new("account-sessions", sessions, move |key, _, cx| {
.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))
}) })
+7 -14
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").on_submit(move |card, _, cx| { PaymentMethodForm::new("account-card-form", 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,18 +116,16 @@ 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") UpgradePrompt::new("account-nudge", "9 of 10 projects used", |_, _| log::info!("gallery: upgrade"))
.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") UpgradePrompt::new("account-wall", "Version history is on Pro", |_, _| log::info!("gallery: upgrade"))
.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")),
), ),
), ),
@@ -207,27 +205,22 @@ 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()) TeamMemberTable::new("account-members", members, roles(), move |key, next, _, cx| {
.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()) InvitationList::new("account-invites", invitations, roles(), move |email, role, _, cx| {
.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))
}), }),
+18 -23
View File
@@ -30,19 +30,18 @@ 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") let mut form = LoginForm::new("account-login", move |_, _, cx| {
.on_submit(move |_, _, cx| { change(&tried, cx, |state| {
change(&tried, cx, |state| { state.error = Some("No account uses that email and password.".into())
state.error = Some("No account uses that email and password.".into())
})
}) })
.on_forgot(|_, _| log::info!("gallery: forgot password")) })
.on_sign_up(|_, _| log::info!("gallery: sign up")); .on_forgot(|_, _| log::info!("gallery: forgot password"))
.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);
} }
@@ -71,13 +70,12 @@ 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") let mut form = SignupForm::new("account-signup", move |signup, _, cx| {
.terms("I agree to the terms of service") let taken = format!("An account already uses {}.", signup.email);
.on_submit(move |signup, _, cx| { change(&tried, cx, |state| state.error = Some(taken.into()))
let taken = format!("An account already uses {}.", signup.email); })
change(&tried, cx, |state| state.error = Some(taken.into())) .terms("I agree to the terms of service")
}) .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);
} }
@@ -115,9 +113,8 @@ 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") MagicLinkForm::new("account-magic", move |_, _, cx| change(&sent, cx, |state| state.magic = true))
.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)),
), ),
), ),
@@ -125,9 +122,8 @@ 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") ForgotPassword::new("account-forgot", move |_, _, cx| change(&asked, cx, |state| state.reset = true))
.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)),
), ),
), ),
@@ -161,14 +157,13 @@ 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") TwoFactorInput::new("account-factor", move |code, _, cx| {
.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://github.com/ZacharyZhang-NY/Ely-GPUI-Components"), .url("https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components"),
), ),
cx, cx,
)) ))
+13 -5
View File
@@ -221,18 +221,26 @@ 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));
mind.root = mind.root.adding(parent, Topic::new(key.clone(), "New idea")); if let Some(root) = mind.root.adding(parent, Topic::new(key.clone(), "New idea")) {
mind.selected = Some(key); mind.root = root;
mind.selected = Some(key);
}
}) })
}) })
.on_remove(move |key, _, cx| { .on_remove(move |key, _, cx| {
change(&removed, cx, |mind| { change(&removed, cx, |mind| {
mind.selected = mind.root.parent_of(key); if let Some(root) = mind.root.removing(key) {
mind.root = mind.root.removing(key); mind.selected = mind.root.parent_of(key);
mind.root = root;
}
}) })
}) })
.on_rename(move |key, text, _, cx| { .on_rename(move |key, text, _, cx| {
change(&renamed, cx, |mind| mind.root = mind.root.renaming(key, text.clone())) change(&renamed, cx, |mind| {
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,
+8 -16
View File
@@ -121,9 +121,8 @@ 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) ColorPanel::new("canvas-color", color, move |next, _, cx| change(&colored, cx, |look| look.color = next))
.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(
@@ -137,8 +136,7 @@ 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) ShadowEditor::new("canvas-shadows", shadows, move |next, _, cx| change(&shaded, cx, |look| look.shadows = next)),
.on_change(move |next, _, cx| change(&shaded, cx, |look| look.shadows = next)),
), ),
) )
.child( .child(
@@ -148,15 +146,13 @@ 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) BorderEditor::new("canvas-border", border, move |next, _, cx| change(&bordered, cx, |look| look.border = next)),
.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) TypographyPanel::new("canvas-type", style, &search, move |next, _, cx| change(&typed, cx, |look| look.style = next)),
.on_change(move |next, _, cx| change(&typed, cx, |look| look.style = next)),
), ),
), ),
) )
@@ -282,8 +278,7 @@ 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) ConstraintsEditor::new("canvas-constraints", constraints, move |next, _, cx| change(&pinned, cx, |rules| rules.constraints = next)),
.on_change(move |next, _, cx| change(&pinned, cx, |rules| rules.constraints = next)),
) )
.child(pinned_preview), .child(pinned_preview),
) )
@@ -294,8 +289,7 @@ 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) AutoLayoutControls::new("canvas-auto-layout", layout, move |next, _, cx| change(&laid, cx, |rules| rules.layout = next)),
.on_change(move |next, _, cx| change(&laid, cx, |rules| rules.layout = next)),
) )
.child(laid_preview), .child(laid_preview),
) )
@@ -306,9 +300,7 @@ 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) 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)),
.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()))
+12 -13
View File
@@ -134,19 +134,18 @@ pub fn arrange(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
studio.record(align.words().into()); studio.record(align.words().into());
}) })
}); });
let inspector = let inspector = InspectorPanel::new("canvas-inspector", chosen, move |shape, _, cx| {
InspectorPanel::new("canvas-inspector", chosen).on_change(move |shape, _, cx| { change(&edited, cx, |studio| {
change(&edited, cx, |studio| { let at = studio
let at = studio .shapes
.shapes .iter()
.iter() .position(|each| each.key == shape.key)
.position(|each| each.key == shape.key) .expect("a listed shape");
.expect("a listed shape"); let words = format!("Edit {}", shape.name);
let words = format!("Edit {}", shape.name); studio.shapes[at] = shape;
studio.shapes[at] = shape; studio.record(words);
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())
+10 -2
View File
@@ -261,7 +261,10 @@ 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() {
(text, left) = accept(&text, &left, 0); let Some(taken) = accept(&text, &left, 0) else {
break;
};
(text, left) = taken;
} }
text text
} }
@@ -306,11 +309,16 @@ 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) => {
set(&decided, accept(&base_text, &pending, ix).0, cx) if let Some((text, _)) = accept(&base_text, &pending, ix) {
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));
+47 -50
View File
@@ -82,50 +82,51 @@ pub fn grid(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
picked: service, picked: service,
}, },
]; ];
let grid = DashboardGrid::new("dashboard-grid", 12, tiles) let grid = DashboardGrid::new("dashboard-grid", 12, tiles, move |next, _, cx| {
.card( change(&moved, cx, |wall| wall.tiles = next)
"requests", })
DashboardCard::new("Requests").subtitle("per minute").body( .card(
Statistic::new("dashboard-requests", "This hour", 12_400.0) "requests",
.trend(TrendIndicator::new(0.082)), DashboardCard::new("Requests").subtitle("per minute").body(
), Statistic::new("dashboard-requests", "This hour", 12_400.0)
) .trend(TrendIndicator::new(0.082)),
.card( ),
"errors", )
DashboardCard::new("Errors").subtitle("5xx share").body( .card(
Statistic::new("dashboard-errors", "This hour", 0.42) "errors",
.decimals(2) DashboardCard::new("Errors").subtitle("5xx share").body(
.suffix("%") Statistic::new("dashboard-errors", "This hour", 0.42)
.trend(TrendIndicator::new(-0.12)), .decimals(2)
), .suffix("%")
) .trend(TrendIndicator::new(-0.12)),
.card( ),
"cpu", )
DashboardCard::new("CPU").body(Gauge::new( .card(
"dashboard-cpu", "cpu",
"percent busy", DashboardCard::new("CPU").body(Gauge::new(
62.0, "dashboard-cpu",
0.0, "percent busy",
100.0, 62.0,
)), 0.0,
) 100.0,
.card( )),
"latency", )
DashboardCard::new("Latency") .card(
.subtitle("p95, milliseconds") "latency",
.body(LineChart::new("dashboard-latency", hours).series(latency)), DashboardCard::new("Latency")
) .subtitle("p95, milliseconds")
.card( .body(LineChart::new("dashboard-latency", hours).series(latency)),
"saturation", )
DashboardCard::new("Saturation").body(Gauge::new( .card(
"dashboard-memory", "saturation",
"memory in use", DashboardCard::new("Saturation").body(Gauge::new(
81.0, "dashboard-memory",
0.0, "memory in use",
100.0, 81.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.",
@@ -143,9 +144,7 @@ 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) DashboardFilterBar::new("dashboard-filters", time, filters, move |next, _, cx| change(&windowed, cx, |wall| wall.window = next), move |key, value, _, cx| {
.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,
@@ -153,9 +152,7 @@ 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) 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)),
.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://github.com/ZacharyZhang-NY/Ely-GPUI-Components") let qr = QrCode::new("https://git.zacharyzhang.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");
+15 -7
View File
@@ -156,8 +156,7 @@ 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) EnvironmentSelector::new("devtools-environments", environments(), environment, move |key, _, cx| change(&chose, cx, |desk| desk.environment = key.clone())),
.on_change(move |key, _, cx| change(&chose, cx, |desk| desk.environment = key.clone())),
) )
.child( .child(
div() div()
@@ -178,7 +177,11 @@ 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| desk.collection = moved(&desk.collection, key, target, at)) change(&dragged, cx, |desk| {
if let Some(collection) = moved(&desk.collection, key, target, at) {
desk.collection = collection;
}
})
}), }),
), ),
), ),
@@ -255,8 +258,7 @@ 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) WebSocketConsole::new("devtools-socket", "wss://stream.example.com/orders", state, messages, &draft, clock, move |text, _, cx| {
.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 });
@@ -333,8 +335,14 @@ 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("devtools-graphql", types, &query, &variables, &search) let mut explorer = GraphQLExplorer::new(
.on_run(move |_, _, _, cx| change(&ran, cx, |graph| graph.answer = Some(ANSWER.into()))); "devtools-graphql",
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);
} }
+2 -5
View File
@@ -270,15 +270,12 @@ 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) div().w(px(660.)).child(TableStructureEditor::new("devtools-structure", fields, move |next, _, cx| change(&edited, cx, |build| build.fields = next)),
.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) 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))),
.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))),
), ),
), ),
) )
+1 -2
View File
@@ -35,8 +35,7 @@ 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) ColorContrastChecker::new("devtools-contrast", fore, back, move |fore, back, _, cx| change(&colors, cx, |colors| *colors = (fore, back))),
.on_change(move |fore, back, _, cx| change(&colors, cx, |colors| *colors = (fore, back))),
), ),
), ),
) )
+5 -2
View File
@@ -202,7 +202,10 @@ 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("doc-blocks", div().w(px(760.)).child(notes))), .child(probe(
"doc-blocks",
div().w(px(760.)).h(px(360.)).child(notes),
)),
) )
.child( .child(
section( section(
@@ -210,7 +213,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.)).child(kinds)), .child(div().w(px(760.)).h(px(560.)).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.)).child( div().w(px(840.)).h(px(320.)).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| {
+11 -4
View File
@@ -37,7 +37,14 @@ 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([(12, 4), (3, 3), (9, 1), (18, 0), (2, 1), (40, 0)]) .zip([
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(
@@ -46,7 +53,7 @@ pub fn blame(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
cx, cx,
) )
.child( .child(
div().w(px(840.)).child( div().w(px(840.)).h(px(240.)).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)),
@@ -54,8 +61,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.)).child( div().w(px(520.)).h(px(360.)).child(
FileHistory::new("git-file-history", history_rows) FileHistory::new("git-file-history", std::rc::Rc::new(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)),
), ),
+23 -7
View File
@@ -3,11 +3,13 @@ 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, Stash, StashAction, StashList, TagList, CommitInput, CommitList, GitTag, GraphRow, Stash, StashAction, StashList, TagList, lanes,
}, },
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::{
@@ -19,8 +21,7 @@ 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,
added, lines: Some((added, removed)),
removed,
} }
} }
@@ -86,7 +87,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),
@@ -211,6 +212,21 @@ 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();
@@ -221,9 +237,9 @@ pub fn commits(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
cx, cx,
) )
.child( .child(
div().w(px(840.)).child( div().w(px(840.)).h(px(320.)).child(
CommitList::new("git-graph", history()) CommitList::new("git-graph", Rc::new(history()))
.graph() .graph(Rc::new(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,12 +246,15 @@ 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) RovingFocus::new("interaction-tools", Axis::Horizontal, move |key, _, cx| {
.gap_0p5() let key = key.clone();
.p_0p5() change(&press, cx, |focusing| focusing.tool = key)
.rounded_lg() })
.border_1() .gap_0p5()
.border_color(theme.colors.border), .p_0p5()
.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(
@@ -308,10 +311,7 @@ pub fn focus(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
"RovingFocus", "RovingFocus",
probe( probe(
"interaction-tools", "interaction-tools",
toolbar.on_press(move |key, _, cx| { toolbar,
let key = key.clone();
change(&press, cx, |focusing| focusing.tool = key)
}),
), ),
cx, cx,
)) ))
+15 -17
View File
@@ -162,15 +162,18 @@ 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") SelectionArea::new("interaction-tiles", move |keys, _, cx| {
.w(px(300.)) let keys = keys.to_vec();
.flex() change(&pick, cx, |shapes| shapes.picked = keys)
.flex_wrap() })
.gap(px(8.)) .w(px(300.))
.p_3() .flex()
.rounded_lg() .flex_wrap()
.border_1() .gap(px(8.))
.border_color(theme.colors.border), .p_3()
.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}")),
@@ -204,8 +207,7 @@ 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.))) 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))
.on_resize(move |next, _, cx| change(&resize, cx, |shapes| shapes.size = next))
.child( .child(
div() div()
.size_full() .size_full()
@@ -225,8 +227,7 @@ 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) Rotatable::new("interaction-rotatable", "icons/arrow-up.svg", px(120.), angle, move |next, _, cx| change(&turn, cx, |shapes| shapes.angle = next)),
.on_turn(move |next, _, cx| change(&turn, cx, |shapes| shapes.angle = next)),
), ),
cx, cx,
)), )),
@@ -237,10 +238,7 @@ pub fn shapes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
"SelectionArea", "SelectionArea",
probe( probe(
"interaction-tiles", "interaction-tiles",
tiles.selected(picked).on_change(move |keys, _, cx| { tiles.selected(picked),
let keys = keys.to_vec();
change(&pick, cx, |shapes| shapes.picked = keys)
}),
), ),
cx, cx,
)) ))
+19 -1
View File
@@ -211,6 +211,24 @@ 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)
@@ -242,7 +260,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 right click opens the menu where it lands; it stays inside the window. A row of icon buttons heads it; arrows cross the row.",
cx, cx,
) )
.child(specimen( .child(specimen(
+13 -15
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,9 +62,7 @@ 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,
)), )),
+4 -6
View File
@@ -53,9 +53,8 @@ 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) QrCodeScanner::new("scanner", 1.5, |text, _, _| log::info!("gallery: scanned {text}"))
.frame(frame.read(cx).clone()) .frame(frame.read(cx).clone()),
.on_scan(|text, _, _| log::info!("gallery: scanned {text}")),
), ),
), ),
) )
@@ -68,9 +67,7 @@ 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) Captcha::new("captcha", picture, 3.0, move |text, _, cx| {
.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,
@@ -79,6 +76,7 @@ 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);
+6 -12
View File
@@ -16,12 +16,8 @@ fn packages() -> [Package; 4] {
Package::new( Package::new(
"Ely GPUI Component", "Ely GPUI Component",
"0.1.0", "0.1.0",
"MIT OR Apache-2.0", "MIT",
concat!( include_str!("../../../../LICENSE"),
include_str!("../../../../LICENSE-MIT"),
"\n\n",
include_str!("../../../../LICENSE-APACHE")
),
), ),
Package::new( Package::new(
"Lucide", "Lucide",
@@ -114,11 +110,7 @@ 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.", "The help pages set cookies. Necessary ones keep them working; the rest are up to you.", move |kinds, _, cx| {
)
.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(", ")),
@@ -127,7 +119,9 @@ 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(
+12 -6
View File
@@ -58,14 +58,16 @@ 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.", "The first, the last, and the pages around this one. A gap stands for two or more pages. Type a page to go there.",
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).on_change(move |to, _, cx| set(&page, to, cx)), Pagination::new("pagination", now, 20)
.jump()
.on_change(move |to, _, cx| set(&page, to, cx)),
), ),
cx, cx,
)) ))
@@ -106,10 +108,14 @@ 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", setup_steps(), now) Wizard::new(
.content(div().h(px(72.0)).child(Paragraph::new(body))) "wizard",
.on_step(move |to, _, cx| set(&walk, to, cx)) setup_steps(),
.on_finish(move |_, cx| set(&finish, true, cx)), now,
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,
+30 -24
View File
@@ -88,13 +88,17 @@ pub fn wizard(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
}), }),
) )
.into_any_element(), .into_any_element(),
false => OnboardingWizard::new("onboarding-wizard", steps, step) false => OnboardingWizard::new(
.content(content) "onboarding-wizard",
.ready(ready) steps,
.on_step(move |to, _, cx| change(&walk, cx, |welcome| welcome.step = to)) step,
.on_finish(move |_, cx| change(&finish, cx, |welcome| welcome.finished = true)) move |to, _, cx| change(&walk, cx, |welcome| welcome.step = to),
.on_skip(move |_, cx| change(&skip, cx, |welcome| welcome.finished = true)) move |_, cx| change(&finish, 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",
@@ -122,17 +126,15 @@ 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( pin.child(div().absolute().top_neg_1p5().right_neg_1p5().child(probe(
div().absolute().top_neg_1p5().right_neg_1p5().child(probe( "onboarding-hotspot",
Hotspot::new(
"onboarding-hotspot", "onboarding-hotspot",
Hotspot::new( "Pin a view",
"onboarding-hotspot", "Keep the views you use most one press away.",
"Pin a view", move |_, cx| change(&spot, cx, |seen| seen.hotspot = true),
"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()
@@ -151,9 +153,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(
@@ -247,16 +249,20 @@ pub fn checklist(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
}) })
}) })
.into_any_element(), .into_any_element(),
false => SetupChecklist::new("onboarding-setup", "Get started", tasks) false => SetupChecklist::new(
.on_start(move |key, _, cx| { "onboarding-setup",
"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)) )
.into_any_element(), .on_dismiss(move |_, cx| change(&hide, cx, |setup| setup.hidden = true))
.into_any_element(),
}; };
section( section(
"Checklist (first-run tasks)", "Checklist (first-run tasks)",
+17 -14
View File
@@ -176,16 +176,21 @@ 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("onboarding-help-panel", articles(), &search, open) HelpPanel::new(
.on_open(move |key, _, cx| { "onboarding-help-panel",
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_shortcuts(move |_, cx| { .on_contact(|_, _| log::info!("gallery: contact support"))
change(&keys, cx, |help| help.dialog = Some(Dialog::Shortcuts)) .on_shortcuts(move |_, cx| {
}) 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(),
}; };
@@ -290,8 +295,7 @@ 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") FeedbackWidget::new("onboarding-feedback", |sentiment, note, _, _| log::info!("gallery: feedback {} {note}", sentiment.key())),
.on_send(|sentiment, note, _, _| log::info!("gallery: feedback {} {note}", sentiment.key())),
)) ))
.child( .child(
div() div()
@@ -300,12 +304,11 @@ 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) ContactSupport::new("onboarding-support", topics, move |topic, _, _, cx| {
.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."))),
), ),
+90 -77
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( .row(SettingsRow::new("Theme").control(ThemeSelector::new(
SettingsRow::new("Theme").control( "settings-theme",
ThemeSelector::new("settings-theme", now.appearance).on_change( now.appearance,
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( .control(AccentColorPicker::new(
AccentColorPicker::new("settings-accent", now.accent, presets).on_change( "settings-accent",
move |next, _, cx| change(&accent, cx, |prefs| prefs.accent = next), now.accent,
), 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( .control(DensitySelector::new(
DensitySelector::new("settings-density", now.density).on_change( "settings-density",
move |next, _, cx| change(&density, cx, |prefs| prefs.density = next), now.density,
), move |next, _, cx| change(&density, cx, |prefs| prefs.density = next),
), )),
) )
.row( .row(
SettingsRow::new("Font") SettingsRow::new("Font")
@@ -126,25 +126,32 @@ 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( .row(ProxySettings::new(
ProxySettings::new("settings-proxy", prefs.read(cx).proxy.clone()) "settings-proxy",
.on_apply(move |next, _, cx| change(&proxy, cx, |prefs| prefs.proxy = next)), prefs.read(cx).proxy.clone(),
) 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("settings-privacy", prefs.read(cx).privacy) PrivacySettings::new(
.on_change(move |next, _, cx| change(&privacy, cx, |prefs| prefs.privacy = next)) "settings-privacy",
.into_any_element() prefs.read(cx).privacy,
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("settings-notices", prefs.read(cx).notices.clone()) NotificationSettings::new(
.on_change(move |next, _, cx| change(&notices, cx, |prefs| prefs.notices = next)) "settings-notices",
.into_any_element() prefs.read(cx).notices.clone(),
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 {
@@ -154,9 +161,13 @@ 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("settings-startup", prefs.read(cx).startup.clone(), pages) StartupSettings::new(
.on_change(move |next, _, cx| change(&startup, cx, |prefs| prefs.startup = next)) "settings-startup",
.into_any_element() prefs.read(cx).startup.clone(),
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 {
@@ -175,37 +186,36 @@ 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( .row(StorageSettings::new(
StorageSettings::new("settings-storage", now.stores.clone()).on_clear( "settings-storage",
move |key, _, cx| { now.stores.clone(),
change(&cleared, cx, |prefs| { move |key, _, cx| {
prefs change(&cleared, cx, |prefs| {
.stores prefs
.iter_mut() .stores
.filter(|store| store.key == *key) .iter_mut()
.for_each(|store| store.bytes = 0) .filter(|store| store.key == *key)
}) .for_each(|store| store.bytes = 0)
}, })
), },
), )),
) )
.child( .child(
SettingsSection::new("Import and export").row( SettingsSection::new("Import and export").row(ImportExportSettings::new(
ImportExportSettings::new("settings-files") "settings-files",
.on_export(|_, _| log::info!("gallery: settings exported")) |_, _| log::info!("gallery: settings exported"),
.on_import(|path, _, _| { |path, _, _| log::info!("gallery: settings imported from {}", path.display()),
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( .row(ResetToDefault::new(
ResetToDefault::new("settings-reset", changed) "settings-reset",
.on_reset(move |_, _, cx| change(&reset, cx, |prefs| prefs.size = SIZE)) changed,
.on_reset_all(move |_, cx| change(&all, cx, |prefs| prefs.size = SIZE)), move |_, _, cx| change(&reset, cx, |prefs| prefs.size = SIZE),
), move |_, cx| change(&all, cx, |prefs| prefs.size = SIZE),
)),
) )
.into_any_element() .into_any_element()
} }
@@ -220,22 +230,25 @@ 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( .row(FeatureFlags::new(
FeatureFlags::new("settings-flags", now.flags.clone(), &fields.flags) "settings-flags",
.on_toggle(move |key, on, _, cx| { now.flags.clone(),
change(&toggled, cx, |prefs| { &fields.flags,
prefs move |key, on, _, cx| {
.flags change(&toggled, cx, |prefs| {
.iter_mut() prefs
.filter(|flag| flag.key == *key) .flags
.for_each(|flag| flag.on = on) .iter_mut()
}) .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()
} }
+2 -3
View File
@@ -259,14 +259,13 @@ 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).on_pick( .child(div().w(px(360.)).child(SettingsSearch::new("settings-search", entries, &search,
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) SettingsLayout::new("settings-layout", SECTIONS, selected, move |key, _, cx| change(&picked, cx, |prefs| prefs.section = key.clone()))
.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://github.com/ZacharyZhang-NY/Ely-GPUI-Components", "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components",
) )
.link( .link(
"License", "License",
"https://github.com/ZacharyZhang-NY/Ely-GPUI-Components#license", "https://git.zacharyzhang.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).on_change(|draft, _, cx| apply(draft, cx))) .child(ThemeEditor::new("theme-editor", draft, |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").on_apply(|read, _, cx| { div().w(px(360.)).child(ThemeImporter::new("theme-importer", |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).on_change( div().w(px(360.)).child(SyntaxThemePicker::new("theme-syntax", syntax_themes(), name,
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( .child(Link::new("guide", "style guide", |_, _, _| {
Link::new("guide", "style guide").on_click(|_, _, _| log::info!("gallery: link")), log::info!("gallery: link")
) }))
.child("before you start."), .child("before you start."),
) )
} }
+7 -6
View File
@@ -201,26 +201,27 @@ 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 = |source: &str| Latex::parse(source).expect("gallery formula parses"); let math =
|id: &'static str, source: &str| Latex::parse(id, source).expect("gallery formula parses");
section( section(
"Latex / MathInline", "Latex / MathInline",
"A TeX subset: scripts, fractions, roots, Greek.", "TeX typeset by RaTeX, in the text's color.",
cx, cx,
) )
.child( .child(
specimens() specimens()
.gap_10() .gap_10()
.child(specimen("euler", math(r"e^{i\pi} + 1 = 0"), cx)) .child(specimen("euler", math("euler", r"e^{i\pi} + 1 = 0"), cx))
.child(specimen( .child(specimen(
"sum", "sum",
math(r"\sum_{k=1}^{n} k = \frac{n(n+1)}{2}"), math("sum", r"\sum_{k=1}^{n} k = \frac{n(n+1)}{2}"),
cx, cx,
)) ))
.child(specimen( .child(specimen(
"root", "root",
math(r"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"), math("root", r"x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"),
cx, cx,
)), )),
) )
.child(code("Latex::parse(r\"e^{i\\pi} + 1 = 0\")?", cx)) .child(code("Latex::parse(\"euler\", r\"e^{i\\pi} + 1 = 0\")?", cx))
} }
+4 -1
View File
@@ -106,7 +106,10 @@ pub fn start() {
if let Some(choice) = EARLY.take() { if let Some(choice) = EARLY.take() {
start.choice = choice; start.choice = choice;
} }
let window = launch(start, cx); let window = match 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)));
}); });
} }
+6 -4
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, readFileSync, rmSync, writeFileSync } from "node:fs"; import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, 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 = "ZacharyZhang-NY/Ely-GPUI-Showcases"; const REPO = "https://git.zacharyzhang.com/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,12 +15,14 @@ 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 = `https://codeload.github.com/${REPO}/tar.gz/${commit}`; const url = `${REPO}/archive/${commit}.tar.gz`;
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 root = join(work, `${REPO.split("/")[1]}-${commit}`); const [top] = readdirSync(work, { withFileTypes: true }).filter((e) => e.isDirectory());
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 @@
0f8f5b9cc392d4741c239b1c1c0f6071dfc6faa5 ca872b7708d200dafb3027364122aa5895e43299
+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 GitHub"> <a className="tool" href={REPO} rel="noopener" aria-label="Ely on Gitea">
<Icon name="GitHub" /> <Icon name="Gitea" />
</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 or Apache-2.0</span> <span>MIT</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://github.com/ZacharyZhang-NY/Ely-GPUI-Showcases#submit-an-app"; export const SUBMIT = "https://git.zacharyzhang.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://github.com/ZacharyZhang-NY/Ely-GPUI-Components"; export const REPO = "https://git.zacharyzhang.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`, with no key context, so they reach every window. A `FocusScope` turns the two actions into focus moves. `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.
## 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` panics without them. | | `gpui_platform::application().with_assets(Assets)` | Ely embeds its icons and fonts. `init` returns an error when the source cannot load one of them. |
| `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. | | `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. |
| 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
@@ -0,0 +1,46 @@
# 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", "theme", "focus", "motion", "icons", "overlays", "rendering", "i18n", "webassembly", "testing"]; const ORDER = ["introduction", "installation", "getting-started", "gpui-kit", "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`; `init` registers the fonts and checks that `icons/check.svg` loads. `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.
## 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 writes an `AssetSource` that answers its own paths and hands every other path to `Assets`. `init` panics unless `icons/check.svg` loads through it. 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.
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. 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.
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://github.com/ZacharyZhang-NY/Ely-GPUI-Components/blob/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://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components/src/branch/main/examples/assets/mark.svg) with it, or supply your own SVG.
## A window of icons ## A window of icons
+8 -9
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 from Zed's repository at the revision Ely pins: 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:
```toml ```toml
[dependencies] [dependencies]
ely-gpui-component = { git = "https://github.com/ZacharyZhang-NY/Ely-GPUI-Components" } ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components" }
gpui = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b" } gpui = { package = "gpui-pre", version = "=0.3.8" }
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "1a28cff4b409169bac058bca40dfbfeb7621d19b", features = ["font-kit"] } gpui_platform = { package = "gpui-pre-platform", version = "=0.3.8", 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 revision as Ely's. Another revision builds a second GPUI, and its types do not match the ones Ely takes. | | `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_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,9 +41,8 @@ A window opens with a counter and a Press button. From a clone of the repository
| Symptom | Cause and fix | | Symptom | Cause and fix |
| --- | --- | | --- | --- |
| 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: 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: asset source failed` | The app's asset source returned an error. The panic carries it. | | `init` error: `ely: embedded fonts failed to register` | GPUI's text system refused Ely's fonts. File a bug with the log. |
| 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` points at a revision other than Ely's. Use `1a28cff4b409169bac058bca40dfbfeb7621d19b`. | | `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`. |
| `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 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. MIT. 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` 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. 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.
## 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` panics t
## Tests to read ## Tests to read
- [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. - [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.
- [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. - [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.
- [Tables](https://github.com/ZacharyZhang-NY/Ely-GPUI-Components/blob/main/src/tables/tests/mod.rs): clicks, double presses, Tab across a grid. - [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.
- [Internationalization](https://github.com/ZacharyZhang-NY/Ely-GPUI-Components/blob/main/src/i18n/tests/mod.rs): catalogs, locales and a mirrored row. - [Internationalization](https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components/src/branch/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://github.com/ZacharyZhang-NY/Ely-GPUI-Components/blob/main/examples/gallery/web.rs) shows the steps: The gallery's [web entry](https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components/src/branch/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' GitHub mark (CC0), padded to sit like Reicon's. // simple-icons' Gitea mark (CC0), padded to sit like Reicon's.
const GITHUB = const GITEA =
'<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>'; '<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>';
export type IconName = keyof typeof set | "GitHub"; export type IconName = keyof typeof set | "Gitea";
/** 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 === "GitHub" ? GITHUB : set[name].toSvg({ size: 24 }); const svg = name === "Gitea" ? GITEA : 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,6 +4,15 @@ 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
+12 -18
View File
@@ -24,17 +24,20 @@ pub struct TwoFactorInput {
id: ElementId, id: ElementId,
failed: usize, failed: usize,
busy: bool, busy: bool,
on_code: Option<OnText>, on_code: OnText,
on_recovery: Option<Run>, on_recovery: Option<Run>,
} }
impl TwoFactorInput { impl TwoFactorInput {
pub fn new(id: impl Into<ElementId>) -> Self { pub fn new(
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: None, on_code: Rc::new(on_code),
on_recovery: None, on_recovery: None,
} }
} }
@@ -51,14 +54,6 @@ 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));
@@ -69,9 +64,7 @@ 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 let on_code = self.on_code;
.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(
@@ -113,10 +106,11 @@ impl RenderOnce for TwoFactorInput {
) )
.children(note) .children(note)
.children(self.on_recovery.map(|run| { .children(self.on_recovery.map(|run| {
div().child( div().child(Link::new(
Link::new((id.clone(), "recovery"), "Use a recovery code") (id.clone(), "recovery"),
.on_click(move |_, window, cx| run(window, cx)), "Use a recovery code",
) move |_, window, cx| run(window, cx),
))
})) }))
} }
} }
+16 -39
View File
@@ -33,20 +33,26 @@ pub struct ApiKeyManager {
id: ElementId, id: ElementId,
keys: Vec<ApiKey>, keys: Vec<ApiKey>,
secret: Option<SharedString>, secret: Option<SharedString>,
on_create: Option<OnKey>, on_create: OnKey,
on_revoke: Option<OnKey>, on_revoke: OnKey,
on_dismiss: Option<Run>, on_dismiss: Run,
} }
impl ApiKeyManager { impl ApiKeyManager {
pub fn new(id: impl Into<ElementId>, keys: impl IntoIterator<Item = ApiKey>) -> Self { pub fn new(
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: None, on_create: Rc::new(on_create),
on_revoke: None, on_revoke: Rc::new(on_revoke),
on_dismiss: None, on_dismiss: Rc::new(on_dismiss),
} }
} }
@@ -55,40 +61,13 @@ 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 let on_create = self.on_create;
.on_create let on_revoke = self.on_revoke;
.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();
@@ -102,9 +81,7 @@ 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().unwrap_or_else(|| { let dismiss = self.on_dismiss.clone();
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(
+13 -14
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).on_click(move |_, window, cx| run(window, cx))) .child(Link::new(id, link, move |_, window, cx| run(window, cx)))
} }
/// What a sign-in hands its owner. /// What a sign-in hands its owner.
@@ -67,18 +67,21 @@ pub struct LoginForm {
id: ElementId, id: ElementId,
error: Option<SharedString>, error: Option<SharedString>,
busy: bool, busy: bool,
on_submit: Option<OnLogin>, on_submit: 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(id: impl Into<ElementId>) -> Self { pub fn new(
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: None, on_submit: Rc::new(on_submit),
on_forgot: None, on_forgot: None,
on_sign_up: None, on_sign_up: None,
} }
@@ -96,11 +99,6 @@ 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));
@@ -127,9 +125,7 @@ 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 let on_submit = self.on_submit;
.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);
@@ -170,8 +166,11 @@ impl RenderOnce for LoginForm {
}), }),
) )
.children(self.on_forgot.map(|run| { .children(self.on_forgot.map(|run| {
Link::new((id.clone(), "forgot"), "Forgot password?") Link::new(
.on_click(move |_, window, cx| run(window, cx)) (id.clone(), "forgot"),
"Forgot password?",
move |_, window, cx| run(window, cx),
)
})), })),
) )
.children( .children(
+16 -27
View File
@@ -33,7 +33,7 @@ struct Step {
words: Words, words: Words,
sent: bool, sent: bool,
busy: bool, busy: bool,
on_send: Option<OnText>, on_send: OnText,
back: Way, back: Way,
other: Way, other: Way,
} }
@@ -52,7 +52,6 @@ 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| {
@@ -72,7 +71,9 @@ 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).on_click(move |_, window, cx| run(window, cx)) Link::new((id.clone(), "back"), label, move |_, window, cx| {
run(window, cx)
})
}); });
if sent { if sent {
let other = other.map(|(label, run)| { let other = other.map(|(label, run)| {
@@ -149,17 +150,20 @@ pub struct MagicLinkForm {
id: ElementId, id: ElementId,
sent: bool, sent: bool,
busy: bool, busy: bool,
on_send: Option<OnText>, on_send: OnText,
on_other: Option<Run>, on_other: Option<Run>,
} }
impl MagicLinkForm { impl MagicLinkForm {
pub fn new(id: impl Into<ElementId>) -> Self { pub fn new(
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: None, on_send: Rc::new(on_send),
on_other: None, on_other: None,
} }
} }
@@ -176,15 +180,6 @@ 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));
@@ -222,17 +217,20 @@ pub struct ForgotPassword {
id: ElementId, id: ElementId,
sent: bool, sent: bool,
busy: bool, busy: bool,
on_send: Option<OnText>, on_send: OnText,
on_back: Option<Run>, on_back: Option<Run>,
} }
impl ForgotPassword { impl ForgotPassword {
pub fn new(id: impl Into<ElementId>) -> Self { pub fn new(
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: None, on_send: Rc::new(on_send),
on_back: None, on_back: None,
} }
} }
@@ -249,15 +247,6 @@ 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));
+9 -17
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: Option<OnKey>, on_pick: OnKey,
} }
impl OAuthButtons { impl OAuthButtons {
@@ -22,6 +22,7 @@ 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
@@ -33,36 +34,27 @@ impl OAuthButtons {
id, id,
providers, providers,
busy: None, busy: None,
on_pick: None, on_pick: Rc::new(on_pick),
} }
} }
/// 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();
assert!( if !self.providers.iter().any(|(each, _)| *each == key) {
self.providers.iter().any(|(each, _)| *each == key), log::error!("oauth buttons: no provider {key}; none busy");
"oauth buttons: no provider {key}" self.busy = None;
); 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 let on_pick = self.on_pick;
.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()
+18 -19
View File
@@ -97,11 +97,12 @@ 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 = 2000 let year = year
+ year .trim()
.trim() .parse::<i32>()
.parse::<i32>() .ok()
.map_err(|_| ExpiryError::Unread)?; .and_then(|year| year.checked_add(2000))
.ok_or(ExpiryError::Unread)?;
if !(1..=12).contains(&month) { if !(1..=12).contains(&month) {
return Err(ExpiryError::NoMonth(month)); return Err(ExpiryError::NoMonth(month));
} }
@@ -146,16 +147,19 @@ pub struct PaymentMethodForm {
id: ElementId, id: ElementId,
error: Option<SharedString>, error: Option<SharedString>,
busy: bool, busy: bool,
on_submit: Option<OnCard>, on_submit: OnCard,
} }
impl PaymentMethodForm { impl PaymentMethodForm {
pub fn new(id: impl Into<ElementId>) -> Self { pub fn new(
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: None, on_submit: Rc::new(on_submit),
} }
} }
@@ -170,22 +174,12 @@ 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 let on_submit = self.on_submit;
.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);
@@ -315,6 +309,11 @@ 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));
+8 -11
View File
@@ -212,30 +212,27 @@ fn savable(edited: &Profile, seed: &Profile) -> bool {
pub struct ProfileEditor { pub struct ProfileEditor {
id: ElementId, id: ElementId,
profile: Profile, profile: Profile,
on_save: Option<OnProfile>, on_save: OnProfile,
} }
impl ProfileEditor { impl ProfileEditor {
pub fn new(id: impl Into<ElementId>, profile: Profile) -> Self { pub fn new(
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: None, on_save: Rc::new(on_save),
} }
} }
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 let on_save = self.on_save;
.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)
}); });
+8 -14
View File
@@ -33,12 +33,16 @@ pub struct Session {
pub struct SessionList { pub struct SessionList {
id: ElementId, id: ElementId,
sessions: Vec<Session>, sessions: Vec<Session>,
on_sign_out: Option<OnKey>, on_sign_out: OnKey,
on_sign_out_others: Option<Run>, on_sign_out_others: Option<Run>,
} }
impl SessionList { impl SessionList {
pub fn new(id: impl Into<ElementId>, sessions: impl IntoIterator<Item = Session>) -> Self { pub fn new(
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,
@@ -48,19 +52,11 @@ impl SessionList {
Self { Self {
id: id.into(), id: id.into(),
sessions, sessions,
on_sign_out: None, on_sign_out: Rc::new(on_sign_out),
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));
@@ -71,9 +67,7 @@ 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 let on_sign_out = self.on_sign_out;
.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)| {
+7 -11
View File
@@ -40,18 +40,21 @@ pub struct SignupForm {
terms: Option<SharedString>, terms: Option<SharedString>,
error: Option<SharedString>, error: Option<SharedString>,
busy: bool, busy: bool,
on_submit: Option<OnSignup>, on_submit: OnSignup,
on_sign_in: Option<Run>, on_sign_in: Option<Run>,
} }
impl SignupForm { impl SignupForm {
pub fn new(id: impl Into<ElementId>) -> Self { pub fn new(
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: None, on_submit: Rc::new(on_submit),
on_sign_in: None, on_sign_in: None,
} }
} }
@@ -74,11 +77,6 @@ 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));
@@ -105,9 +103,7 @@ 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 let on_submit = self.on_submit;
.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);
+40 -63
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: Option<OnKey>, on_select: OnKey,
on_add: Option<Run>, on_add: Option<Run>,
} }
@@ -118,30 +118,19 @@ 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: None, on_select: Rc::new(on_select),
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));
@@ -152,9 +141,7 @@ 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 let on_select = self.on_select;
.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(
@@ -175,18 +162,22 @@ 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);
.expect("the current account"); if now.is_none() {
let mut avatar = Avatar::new((id.clone(), "avatar"), now.name.clone()).size(AvatarSize::Sm); log::error!(
if let Some(image) = now.image { "account switcher {id:?}: no account {}; none current",
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 trigger = row( let email = now.map(|now| now.email).unwrap_or_default();
(id.clone(), "trigger"), let trigger = row((id.clone(), "trigger"), avatar, named(name, email, cx), cx);
avatar,
named(now.name, now.email, cx),
cx,
);
opening(id, &menu, trigger, window, cx) opening(id, &menu, trigger, window, cx)
} }
} }
@@ -197,7 +188,7 @@ pub struct WorkspaceSwitcher {
id: ElementId, id: ElementId,
workspaces: Vec<Workspace>, workspaces: Vec<Workspace>,
current: SharedString, current: SharedString,
on_select: Option<OnKey>, on_select: OnKey,
on_create: Option<Run>, on_create: Option<Run>,
} }
@@ -206,30 +197,19 @@ 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: None, on_select: Rc::new(on_select),
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));
@@ -240,9 +220,7 @@ 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 let on_select = self.on_select;
.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()
@@ -267,17 +245,22 @@ 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);
.expect("the current workspace"); if now.is_none() {
let mark = Avatar::new((id.clone(), "mark"), now.name.clone()) log::error!(
"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 trigger = row( let detail = now.map(|now| now.detail).unwrap_or_default();
(id.clone(), "trigger"), let trigger = row((id.clone(), "trigger"), mark, named(name, detail, cx), cx);
mark,
named(now.name, now.detail, cx),
cx,
);
opening(id, &menu, trigger, window, cx) opening(id, &menu, trigger, window, cx)
} }
} }
@@ -291,7 +274,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: Option<Run>, on_sign_out: Run,
} }
impl UserMenu { impl UserMenu {
@@ -299,6 +282,7 @@ 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(),
@@ -307,7 +291,7 @@ impl UserMenu {
image: None, image: None,
presence: None, presence: None,
items: Vec::new(), items: Vec::new(),
on_sign_out: None, on_sign_out: Rc::new(on_sign_out),
} }
} }
@@ -326,19 +310,12 @@ 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 let sign_out = self.on_sign_out;
.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)
+47 -111
View File
@@ -25,11 +25,13 @@ pub struct Role {
pub description: SharedString, pub description: SharedString,
} }
fn role_named<'a>(roles: &'a [Role], key: &SharedString, owner: &str) -> &'a Role { /// The role of `key`; none, logged for `owner`, when the roles lost it.
roles fn role_named<'a>(roles: &'a [Role], key: &SharedString, owner: &str) -> Option<&'a Role> {
.iter() let role = roles.iter().find(|role| role.key == *key);
.find(|role| role.key == *key) if role.is_none() {
.unwrap_or_else(|| panic!("{owner}: no role {key}")) log::error!("{owner}: no role {key}; none shown");
}
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.
@@ -39,7 +41,7 @@ pub struct RoleSelector {
roles: Vec<Role>, roles: Vec<Role>,
selected: SharedString, selected: SharedString,
disabled: bool, disabled: bool,
on_change: Option<OnKey>, on_change: OnKey,
} }
impl RoleSelector { impl RoleSelector {
@@ -47,6 +49,7 @@ 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());
@@ -56,7 +59,7 @@ impl RoleSelector {
roles, roles,
selected, selected,
disabled: false, disabled: false,
on_change: None, on_change: Rc::new(on_change),
} }
} }
@@ -64,22 +67,12 @@ 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 let on_change = self.on_change;
.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()
@@ -113,8 +106,8 @@ pub struct TeamMemberTable {
id: ElementId, id: ElementId,
members: Vec<Member>, members: Vec<Member>,
roles: Vec<Role>, roles: Vec<Role>,
on_role: Option<OnPair>, on_role: OnPair,
on_remove: Option<OnKey>, on_remove: OnKey,
} }
impl TeamMemberTable { impl TeamMemberTable {
@@ -122,48 +115,26 @@ 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: None, on_role: Rc::new(on_role),
on_remove: None, on_remove: Rc::new(on_remove),
} }
} }
/// 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 let on_role = self.on_role;
.on_role let on_remove = self.on_remove;
.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(
@@ -189,8 +160,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",
@@ -272,9 +243,9 @@ pub struct InvitationList {
id: ElementId, id: ElementId,
invitations: Vec<Invitation>, invitations: Vec<Invitation>,
roles: Vec<Role>, roles: Vec<Role>,
on_invite: Option<OnPair>, on_invite: OnPair,
on_resend: Option<OnKey>, on_resend: OnKey,
on_revoke: Option<OnKey>, on_revoke: OnKey,
} }
impl InvitationList { impl InvitationList {
@@ -282,63 +253,32 @@ 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: None, on_invite: Rc::new(on_invite),
on_resend: None, on_resend: Rc::new(on_resend),
on_revoke: None, on_revoke: Rc::new(on_revoke),
} }
} }
/// 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 let on_invite = self.on_invite;
.on_invite let on_resend = self.on_resend;
.unwrap_or_else(|| panic!("invitation list {id:?} has no on_invite")); let on_revoke = self.on_revoke;
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);
@@ -346,7 +286,8 @@ 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 ready = is_email(&address); let offered = self.roles.iter().any(|role| role.key == chosen);
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}");
@@ -358,8 +299,7 @@ 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")
.name .map_or_else(|| SharedString::from("No role"), |role| role.name.clone());
.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()
@@ -448,21 +388,17 @@ impl RenderOnce for InvitationList {
.min_w(theme.label_width()) .min_w(theme.label_width())
.child(EmailInput::new(&email)), .child(EmailInput::new(&email)),
) )
.child( .child(div().w(theme.label_width()).child(RoleSelector::new(
div().w(theme.label_width()).child( (id.clone(), "invite-role"),
RoleSelector::new( self.roles.clone(),
(id.clone(), "invite-role"), role.read(cx).clone(),
self.roles.clone(), move |key, _, cx| {
role.read(cx).clone(), picked.update(cx, |role, cx| {
) *role = key.clone();
.on_change(move |key, _, cx| { cx.notify();
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)
+37 -26
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,10 +48,13 @@ fn sessions(_: &Bench, owner: Entity<Bench>) -> AnyElement {
current, current,
}; };
let everyone = owner.clone(); let everyone = owner.clone();
SessionList::new("sessions", [session("phone", false), session("mac", true)]) SessionList::new(
.on_sign_out(move |key, _, cx| say(&owner, format!("out {key}"), cx)) "sessions",
.on_sign_out_others(move |_, cx| say(&everyone, "others".into(), cx)) [session("phone", false), session("mac", true)],
.into_any_element() move |key, _, cx| say(&owner, format!("out {key}"), cx),
)
.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.
@@ -81,10 +84,13 @@ 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("keys", [key]) let manager = ApiKeyManager::new(
.on_create(move |name, _, cx| say(&made, format!("make {name}"), cx)) "keys",
.on_revoke(move |key, _, cx| say(&revoked, format!("revoke {key}"), cx)) [key],
.on_dismiss(move |_, cx| say(&done, "done".into(), cx)); move |name, _, cx| say(&made, format!("make {name}"), 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(),
@@ -138,11 +144,10 @@ fn profile() -> Profile {
} }
fn editor(_: &Bench, owner: Entity<Bench>) -> AnyElement { fn editor(_: &Bench, owner: Entity<Bench>) -> AnyElement {
ProfileEditor::new("profile", profile()) ProfileEditor::new("profile", profile(), move |profile, _, cx| {
.on_save(move |profile, _, cx| { say(&owner, format!("{} {}", profile.name, profile.email), 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.
@@ -167,10 +172,11 @@ 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") UserMenu::new("user", "Ada", "ada@example.com", move |_, cx| {
.item(MenuItem::new("Profile").on_click(move |_, cx| say(&profile, "profile".into(), cx))) say(&out, "out".into(), cx)
.on_sign_out(move |_, cx| say(&out, "out".into(), cx)) })
.into_any_element() .item(MenuItem::new("Profile").on_click(move |_, cx| say(&profile, "profile".into(), cx)))
.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.
@@ -191,10 +197,14 @@ fn workspaces(_: &Bench, owner: Entity<Bench>) -> AnyElement {
detail: "Pro".into(), detail: "Pro".into(),
}; };
let made = owner.clone(); let made = owner.clone();
WorkspaceSwitcher::new("spaces", [space("north"), space("south")], "south") WorkspaceSwitcher::new(
.on_select(move |key, _, cx| say(&owner, key.to_string(), cx)) "spaces",
.on_create(move |_, cx| say(&made, "create".into(), cx)) [space("north"), space("south")],
.into_any_element() "south",
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.
@@ -222,10 +232,11 @@ fn alone(_: &Bench, owner: Entity<Bench>) -> AnyElement {
current: true, current: true,
}; };
let everyone = owner.clone(); let everyone = owner.clone();
SessionList::new("sessions", [here]) SessionList::new("sessions", [here], move |key, _, cx| {
.on_sign_out(move |key, _, cx| say(&owner, format!("out {key}"), cx)) say(&owner, format!("out {key}"), cx)
.on_sign_out_others(move |_, cx| say(&everyone, "others".into(), cx)) })
.into_any_element() .on_sign_out_others(move |_, cx| say(&everyone, "others".into(), cx))
.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.
+152 -38
View File
@@ -51,23 +51,22 @@ 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") PaymentMethodForm::new("card", move |card, _, cx| {
.on_submit(move |card, _, cx| { say(
say( &owner,
&owner, format!(
format!( "{} {} {}/{} {} {}",
"{} {} {}/{} {} {}", card.brand.name(),
card.brand.name(), card.number,
card.number, card.month,
card.month, card.year,
card.year, card.cvc,
card.cvc, card.name
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.
@@ -116,11 +115,15 @@ 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("invites", [sent], roles()) InvitationList::new(
.on_invite(move |email, role, _, cx| say(&invited, format!("invite {email} {role}"), cx)) "invites",
.on_resend(move |key, _, cx| say(&again, format!("resend {key}"), cx)) [sent],
.on_revoke(move |key, _, cx| say(&gone, format!("revoke {key}"), cx)) roles(),
.into_any_element() move |email, role, _, cx| say(&invited, format!("invite {email} {role}"), cx),
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.
@@ -149,10 +152,14 @@ fn team(_: &Bench, owner: Entity<Bench>) -> AnyElement {
owner, owner,
}; };
let removed = owner.clone(); let removed = owner.clone();
TeamMemberTable::new("team", [member("ada", true), member("bo", false)], roles()) TeamMemberTable::new(
.on_role(move |key, role, _, cx| say(&owner, format!("{key} {role}"), cx)) "team",
.on_remove(move |key, _, cx| say(&removed, format!("remove {key}"), cx)) [member("ada", true), member("bo", false)],
.into_any_element() roles(),
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.
@@ -167,11 +174,12 @@ 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") UpgradePrompt::new("wall", "Unlock history", move |_, cx| {
.benefit("Every message, kept") say(&owner, "upgrade".into(), cx)
.on_upgrade(move |_, cx| say(&owner, "upgrade".into(), cx)) })
.on_dismiss(move |_, cx| say(&later, "later".into(), cx)) .benefit("Every message, kept")
.into_any_element() .on_dismiss(move |_, cx| say(&later, "later".into(), cx))
.into_any_element()
} }
/// Stops: Upgrade, then Not now. /// Stops: Upgrade, then Not now.
@@ -186,10 +194,11 @@ 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") PaymentMethodForm::new("card", move |card, _, cx| {
.busy(true) say(&owner, card.number.to_string(), cx)
.on_submit(move |card, _, cx| say(&owner, card.number.to_string(), cx)) })
.into_any_element() .busy(true)
.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.
@@ -354,9 +363,10 @@ 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") UpgradePrompt::new("line", "Unlock history", move |_, cx| {
.on_upgrade(move |_, cx| say(&owner, "upgrade".into(), 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.
@@ -372,3 +382,107 @@ 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());
}
+57 -53
View File
@@ -6,16 +6,15 @@ use crate::account::{
}; };
fn login(_: &Bench, owner: Entity<Bench>) -> AnyElement { fn login(_: &Bench, owner: Entity<Bench>) -> AnyElement {
LoginForm::new("login") LoginForm::new("login", move |login, _, cx| {
.on_submit(move |login, _, cx| { say(
say( &owner,
&owner, format!("{} {} {}", login.email, login.password, login.remember),
format!("{} {} {}", login.email, login.password, login.remember), cx,
cx, )
) })
}) .on_forgot(|_, _| {})
.on_forgot(|_, _| {}) .into_any_element()
.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.
@@ -41,16 +40,15 @@ 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") SignupForm::new("signup", move |signup, _, cx| {
.terms("I agree to the terms") say(
.on_submit(move |signup, _, cx| { &owner,
say( format!("{} {} {}", signup.name, signup.email, signup.password),
&owner, cx,
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.
@@ -80,8 +78,11 @@ 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("oauth", [("github", "GitHub"), ("google", "Google")]) let buttons = OAuthButtons::new(
.on_pick(move |key, _, cx| say(&owner, key.to_string(), cx)); "oauth",
[("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(),
@@ -113,19 +114,18 @@ 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") MagicLinkForm::new("magic", move |address, _, cx| {
.sent(bench.sent) say(&owner, format!("send {address}"), cx);
.on_send(move |address, _, cx| { owner.update(cx, |bench, _| bench.sent = true);
say(&owner, format!("send {address}"), cx); })
owner.update(cx, |bench, _| bench.sent = true); .sent(bench.sent)
.on_other(move |_, cx| {
other.update(cx, |bench, cx| {
bench.sent = false;
cx.notify();
}) })
.on_other(move |_, cx| { })
other.update(cx, |bench, cx| { .into_any_element()
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,11 +151,12 @@ 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") ForgotPassword::new("forgot", move |address, _, cx| {
.sent(bench.sent) say(&owner, format!("reset {address}"), cx)
.on_send(move |address, _, cx| say(&owner, format!("reset {address}"), cx)) })
.on_back(move |_, cx| say(&back, "back".into(), cx)) .sent(bench.sent)
.into_any_element() .on_back(move |_, cx| say(&back, "back".into(), cx))
.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.
@@ -172,10 +173,11 @@ 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") TwoFactorInput::new("factor", move |code, _, cx| {
.failed(bench.failed) say(&owner, code.to_string(), cx)
.on_code(move |code, _, cx| say(&owner, code.to_string(), cx)) })
.into_any_element() .failed(bench.failed)
.into_any_element()
} }
/// Stops: the code. /// Stops: the code.
@@ -207,10 +209,11 @@ 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") LoginForm::new("login", move |login, _, cx| {
.busy(true) say(&owner, login.email.to_string(), cx)
.on_submit(move |login, _, cx| say(&owner, login.email.to_string(), cx)) })
.into_any_element() .busy(true)
.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.
@@ -248,12 +251,13 @@ 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") MagicLinkForm::new("magic", move |address, _, cx| {
.sent(true) say(&owner, format!("send {address}"), cx)
.busy(true) })
.on_send(move |address, _, cx| say(&owner, format!("send {address}"), cx)) .sent(true)
.on_other(move |_, cx| say(&other, "other".into(), cx)) .busy(true)
.into_any_element() .on_other(move |_, cx| say(&other, "other".into(), cx))
.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.
+8 -11
View File
@@ -20,18 +20,22 @@ pub struct UpgradePrompt {
title: SharedString, title: SharedString,
body: Option<SharedString>, body: Option<SharedString>,
benefits: Vec<SharedString>, benefits: Vec<SharedString>,
on_upgrade: Option<Run>, on_upgrade: Run,
on_dismiss: Option<Run>, on_dismiss: Option<Run>,
} }
impl UpgradePrompt { impl UpgradePrompt {
pub fn new(id: impl Into<ElementId>, title: impl Into<SharedString>) -> Self { pub fn new(
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: None, on_upgrade: Rc::new(on_upgrade),
on_dismiss: None, on_dismiss: None,
} }
} }
@@ -47,11 +51,6 @@ 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));
@@ -62,9 +61,7 @@ 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 let upgrade = self.on_upgrade;
.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()
+5 -2
View File
@@ -99,7 +99,10 @@ 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 && used <= limit, "memory {used} of {limit}"); assert!(limit > 0, "memory {used} of no 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
} }
@@ -173,7 +176,7 @@ impl RenderOnce for SandboxStatus {
Meter::new( Meter::new(
(self.id.clone(), "memory"), (self.id.clone(), "memory"),
"Memory", "Memory",
used as f32 / limit as f32, used.min(limit) as f32 / limit as f32,
) )
.detail(format!( .detail(format!(
"{} of {}", "{} of {}",
+12 -10
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: usize, current: Option<usize>,
on_rewind: Option<Pick>, on_rewind: Option<Pick>,
} }
@@ -178,15 +178,17 @@ impl CheckpointList {
current: usize, current: usize,
) -> Self { ) -> Self {
let checkpoints: Vec<Checkpoint> = checkpoints.into_iter().collect(); let checkpoints: Vec<Checkpoint> = checkpoints.into_iter().collect();
assert!( let held = current < checkpoints.len();
current < checkpoints.len(), if !held {
"checkpoint {current} of {}", log::error!(
checkpoints.len() "checkpoints: checkpoint {current} of {}; none current",
); checkpoints.len()
);
}
Self { Self {
id: id.into(), id: id.into(),
checkpoints, checkpoints,
current, current: held.then_some(current),
on_rewind: None, on_rewind: None,
} }
} }
@@ -210,7 +212,7 @@ impl RenderOnce for CheckpointList {
let rewind = self let rewind = self
.on_rewind .on_rewind
.clone() .clone()
.filter(|_| ix != current) .filter(|_| current != Some(ix))
.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)
@@ -233,12 +235,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(ix == current, |meta| { .when(current == Some(ix), |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 ix == current { timeline.item(if current == Some(ix) {
item.tone(Tone::Accent) item.tone(Tone::Accent)
} else { } else {
item item
+12 -6
View File
@@ -71,7 +71,7 @@ pub struct BrowserPreview {
id: ElementId, id: ElementId,
url: SharedString, url: SharedString,
frames: Vec<SharedString>, frames: Vec<SharedString>,
shown: usize, shown: Option<usize>,
ratio: f32, ratio: f32,
loading: bool, loading: bool,
on_show: Option<Pick>, on_show: Option<Pick>,
@@ -86,12 +86,18 @@ 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();
assert!(shown < frames.len(), "frame {shown} of {}", frames.len()); let held = shown < 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, shown: held.then_some(shown),
ratio: FRAME, ratio: FRAME,
loading: false, loading: false,
on_show: None, on_show: None,
@@ -170,7 +176,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 ix == shown { .border_color(if shown == Some(ix) {
colors.accent colors.accent
} else { } else {
colors.border colors.border
@@ -205,7 +211,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))
}) })
.child({ .children(shown.map(|shown| {
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();
@@ -214,7 +220,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