Commit Graph
19 Commits
Author SHA1 Message Date
Tommy D. Rossi c375e5e566 Replace the README hero with the glass dashboard
The top image was the mail.tax screenshot. Swap in the painted-sky
dashboard and match the caption to the glass window, cards, and schedule.

Session: ses_f354b12f6ffecM3jsw9hX59RHp
2026-09-22 22:02:02 +02:00
Tommy D. Rossi cbc3de0a12 Show the mail.tax example as the README hero.
The homepage README now opens on the Superhuman-style mail client
sitting on the desktop wallpaper, not the chat window. Alt text is
`mail.tax example`. The launch snippet is `bun --hot mail.tsx`.

The committed JPEG is a window capture composited onto the Tahoe
wallpaper so the lake shows around the rounded window. Full-screen
capture cannot see other apps on this machine.

Also land the mail example itself: three panes, thread list, reading
pane, and the Framer newsletter assets.

Session: ses_f92fa1015ffeSFgm8TqqfEZnVb
2026-09-04 17:29:25 +02:00
Tommy D. Rossi 4369d55c9e Require atomic renderer mutation batches
Reduce the renderer mutation contract to one applyBatch call per React
commit across napi, wasm-bindgen, and the GPU-backed test renderer.

- replace the dynamic batching Proxy with an explicit commit-phase facade
- keep application commands on the original renderer instance
- remove direct create, append, style, text, prop, and commit entry points
- remove the unused removeChild and setCustomPropValue wire operations
- require raw JSON style and custom-prop values and boolean event flags
- regenerate native declarations and update architecture documentation
- update serialization benchmarks for the smaller protocol
- add a minor changeset for @gpuix/native and @gpuix/react

Validated with 204 Rust tests, 359 React tests, 47 example tests,
6 starter-app tests, release native and Wasm builds, lintcn, and both
serialization benchmark stages.

Session: ses_fb7d2b638ffen9PHc0FVU3DpAv
2026-08-28 14:48:39 +02:00
Tommy D. Rossi 2daf9881ba Decode mutation batches into typed ops, and share styles by content
applyBatch built a serde_json::Value tree, deep-cloned every style payload
out of it, and parsed the clone a second time into a StyleDesc. Each style
was allocated three times. The batch then landed in a Vec<BatchOp> whose
SetStyle variant held a 1.4 KB StyleDesc inline, so a 220k-op mount reserved
over 300 MB before it parsed a single op.

The batch now deserializes straight from its JSON bytes into typed ops
through a hand-written SeqAccess visitor. Strings borrow out of the input, so
an element type or a text run is copied once instead of twice, and the
intermediate tree is gone. JSON is unchanged; only the decode changed.

Styles are hash-consed by raw payload. A 10,000-turn chat sends 59,320
setStyle ops carrying 90 distinct styles, and every element now shares one
Arc. RetainedElement drops from 1624 bytes to 248, and because the element
map stores values inline that shrinks the whole table.

Measured on ChatApp at 10,000 turns, 221,764 ops, 72,010 elements:

| metric              |   before |    after |
|---------------------|---------:|---------:|
| parse and apply     | 127.1 ms |  30.2 ms |
| heap churn          | 900.5 MB | 104.5 MB |
| allocations         |  1476196 |   186091 |
| retained tree       | 224.5 MB |  42.6 MB |
| bytes per element   |   3116 B |    592 B |
| chat mount          | 351.3 ms |  ~200 ms |
| chat wheel p50      |  8.39 ms |  ~3.9 ms |

Atomicity is preserved, and is now structural. resolve_styles runs between
decode and apply and borrows only the style table, so the borrow checker
proves no element was touched when it returns Err. An earlier draft interned
inside the apply loop, which left a malformed style at the end of a batch
half applied; batch_tests covers that.

The style table is swept after a batch and after a direct setStyle, and the
sweep is amortized at max(swept_at * 2, 64) so a one-element commit does not
scan a large table. Anything with strong_count == 1 is released, which is
what keeps a dragged element from growing the table forever.

Interning deliberately lives in Rust, not in the protocol. commitUpdate
resends the full style on every commit and a JSX style literal is a fresh
object each render, so a JS-owned table would grow without bound and the
update path would send a definition plus a reference where it sends one op
today. Measured: JS-side interning is 1.13x on encode and 1.6x on the wire,
which does not pay for that.

Also here:

- RetainedTree::elements uses FxHashMap. It is probed once per ancestor hop
  in mark_changed and twice per child per frame in build_virtual_list
- set_style compares with Arc::ptr_eq and falls back to PartialEq on a
  pointer miss, so a reordered but equal style is still not a change
- apply_batch_to_tree is pub, so examples/bench_serde.rs times the real
  function instead of a replica that would drift
- the dead Value-era helpers batch_id, batch_payload, batch_decode and
  batch_str are gone
- docs/serialization-benchmark.md records what was measured and what was
  rejected, including MessagePack at 1.24x and a hand-packed compact style
  that measured slower than Box<StyleDesc> once styles are interned

Tests: 196 Rust (11 new protocol tests), 340 react, 36 examples,
6 example-app, 9 perf.

Session: ses_fc1e82b7bffebLmTFbNQFyGIGP
2026-08-26 19:41:40 +02:00
Tommy D. Rossi 230400e79a Measure the mutation wire format before changing it
The `applyBatch` path looked like the obvious place to speed up a large
mount, so measure both halves on the real `ChatApp` queue instead of
guessing. `examples/bench-serialization.ts` times the JS encode, and
`packages/native/examples/bench_serde.rs` times the Rust decode and the
resident tree.

The answer is that the codec is the **smallest** lever:

| Finding | Number |
|---|---|
| Allocations during decode | 1.76M for 221k ops |
| `StyleDesc` size | 1392 B, so a typed `Vec<Op>` gets 1408 B wide |
| `RetainedElement` resident | 3102 B, 5.4x smaller behind an `Arc` |
| Ops that actually parse a style | 90 of 221k |

`batch_payload` deep-clones every style payload and then `from_value`
parses it again. Interning styles in the protocol beats any codec swap,
and a hand-packed compact style buys nothing once they are interned.

No mainstream JS-Rust codec deduplicates repeated string *values*.
msgpackr `useRecords` deduplicates keys and emits a format `rmp_serde`
cannot read, so value dedup has to happen in the protocol.

`retained_tree` and `style` become `pub` so the bench measures the real
types instead of a copy that silently drifts. `rmp-serde` is a
dev-dependency only; the shipped addon still speaks JSON.

The same `Cargo.toml` and `lib.rs` hunks also carry the
`gpui_platform/test-support` feature and the Windows `cfg` for the test
renderer, because those edits sit inside the same hunks.

Session: ses_fc1c6df4bffewVGSCAfO0JZ4nT
2026-08-26 16:37:44 +02:00
Tommy D. Rossi 7d45d3681a Make GPUIX installable: quickstart, todo example app, and a key that typechecks
Installing the published packages and writing an app was not possible by
following the README. Three things blocked it.

**1. `key` failed on every GPUIX element**

    error TS2322: Type '{ key: string; ... }' is not assignable to type 'Props'.
      Property 'key' does not exist on type 'Props'.

Any list built with `.map()` broke on the first `tsc` run. `key` was expected
to arrive through `JSX.IntrinsicAttributes`, but TypeScript 5 ignores that
member for intrinsic elements. React's DOM types work only because
`DetailedHTMLProps` already carries `key`.

`key` now sits on `Props`, next to `ref`, and on `VirtualListShared`, the one
element prop type that does not extend `Props`. Verified with a probe over all
12 intrinsic elements plus `motion.div`, `VirtualList`, Select, Combobox and
Tooltip.

`jsx-dev-runtime.d.ts` also re-exported `jsx` and `jsxs` from
`react/jsx-dev-runtime`, which exports only `jsxDEV`. The declarations now
match `jsx-dev-runtime.js`.

Nothing caught this because Bun never typechecks and the build tsconfig
excludes `src/__tests__`. CI now runs `bun run typecheck` in `example-app`,
whose `app.tsx` maps a keyed list inside a `<virtual-list>`.

**2. A hello world installed 254 MB**

`@gpuix/native` packed all six platform binaries into the main tarball through
a `*.node` glob, on top of the six per-platform packages that
`optionalDependencies` already resolves.

    node_modules                       254M
    |- @gpuix/native                   185M  <- all six, never loaded
    |- @gpuix/native-darwin-arm64       23M  <- the one that loads
    |- @gpuix/react                    544K

The glob is gone. `index.js` still resolves the platform package at runtime.

**3. The README never said how to install**

It opened with the architecture and the mutation protocol. There was no
`bun add` anywhere, and no mention of `jsxImportSource`, which is required:
without it TypeScript uses DOM types and `<virtual-list>`, `<markdown>`,
`<code>` and `style.hover` all fail.

A **Quickstart** now sits between the hero and Examples: install, tsconfig,
entry file, `bun --hot`, `bun build --compile`. Nothing was removed. The
Building section says up front that it is for working on GPUIX itself.

**example-app/**

A todo app to copy, in one file, using the Waku palette from `chat.tsx`.

| Script | What it does |
|---|---|
| `bun run dev` | `bun --hot app.tsx` |
| `bun run build` | standalone binary in `dist/todo` |
| `bun run web:build` | browser renderer, needs nightly Rust |
| `bun run web:dev` | browser bundle served with COOP/COEP |
| `bun run screenshot` | drives the app through the automation client |
| `bun run typecheck` | `tsc --noEmit` |

It shows `<virtual-list>`, a native `<input>`, `motion.div`, tinted `<svg>`,
native `hover` and `active`, and `testId` hooks. `scripts/screenshots.ts`
regenerates `docs/images/todo-app.png` from it.

Verified on all four paths: `bun --hot`, the 77 MB binary, the browser build at
`localhost:4188`, and a clean `typecheck`.

Two things worth remembering, both now in comments:

- the browser build embeds only IBM Plex Sans and Lilex, so `Helvetica` falls
  back to a monospace and the family has to differ per target
- `maxWidth` on a `<virtual-list>` row does nothing; the list lays each row out
  at list width, so the column cap goes one level in

`bun.lock` carries a few entries from unrelated in-flight work in `examples/`.
They are inert and prune on the next install.

Session: ses_fc1c928b0ffeeHIfdRpxtE0QaV
2026-08-26 15:55:10 +02:00
Tommy D. Rossi 8210a38aa0 Regenerate the committed README screenshots
`bun scripts/screenshots.ts` after two changes that both move pixels:

- the offscreen test window is sized by `createTestRoot()` now, so every shot
  is captured at the real content size instead of a clipped default
- `<code>` paints no card, so `code.png` and `showcase.png` show the card the
  test builds from `style`, and no language header strip

Only the curated set under `docs/images/` is committed; the per-run
`screenshots/` folders stay gitignored.

Session: ses_fc1eac9e3ffewHyLo8c2O8kvvc
2026-08-26 14:41:20 +02:00
Tommy D. Rossi d057308cd1 Say parent list instead of transcript in scroll docs.
Nested scroll rules apply to any parent scroller, not only a chat
transcript. Rename the virtual-list sample to MessageList and
update the <diff>, overflow-x, and selection-plan wording to match.

Session: ses_fd1333811ffefCKd7t9qN7SLgI
2026-08-23 15:31:06 +02:00
Tommy D. Rossi 574b76058b Let the chat example send a turn and scroll it into view.
Composer submit used to only clear the draft. Enter and the send
button now append a user turn and scroll the virtual-list to that
row. The test types hello, submits, and asserts the last row.

Markdown turns now go through SafeMdxContent. Parse results are
cached by source so a 5k remount does not re-parse the same
blocks. The live window starts at 5,000 turns instead of 10,000.

Also add a wheel-flush profiler for bun --cpu-prof, retitle the
compiled binary as Waku-style, refresh chat-app.png, and drop the
stale chat-diff curated screenshot.

Session: ses_fd1333811ffefCKd7t9qN7SLgI
2026-08-23 15:30:58 +02:00
Tommy D. Rossi 7ac57a78a8 Swap chat example icons to Lucide
Replace the Comet and Solar icon set in the ChatGPT-style example with
Lucide SVGs. The native <svg> tint path stays the same; only the asset
paths and a few action-bar glyphs change.

Map the old Solar names to Lucide equivalents:

- pen-new-square -> square-pen
- magnifer -> search
- folder-with-files -> library
- sidebar-minimalistic-left -> panel-left
- restart -> rotate-ccw
- star / star-bold -> thumbs-up / thumbs-down
- arrow-up-right -> share
- more-horizontal -> ellipsis
- alt-arrow-down -> chevron-down
- paperclip -> plus
- tuning -> sliders-horizontal

The assistant action bar now uses thumbs-up and thumbs-down instead of a
star toggle, matching ChatGPT. The composer attach button becomes a plus.
Update THIRD_PARTY_NOTICES to credit Lucide (ISC) and keep the OpenAI mark
attribution on Comet. Refresh the README chat screenshots so they show the
new glyphs.

Session: ses_fd26ecbddffeHttWjURXRgb1Tv
2026-08-23 09:42:59 +02:00
Tommy D. Rossi d5d3d2677c Render local SVG icons
Add a tintable native SVG element for local icon files, expose its JSX types, and update the chat example to use real vector assets instead of font glyph approximations.

Include source attribution, visual test coverage, refreshed screenshots, and the release changeset.

Session: ses_fd5fc87f4ffefKUayD3HVHAi6L
2026-08-22 17:22:18 +02:00
Tommy D. Rossi fdd6cffb06 Use GPUI native macOS platform
Replace the custom NodePlatform, window, dispatcher, display, clipboard, and winit event translation with the real gpui_macos MacPlatform and MacWindow implementations.

Add remorses/zed as the zed submodule on its gpuix branch. The fork adds a main-thread embedded AppKit mode with non-blocking event pumps, including the after-wait run-loop phase required to start GPUI frame sources and present Metal drawables.

Point Cargo at the submodule GPUI crates, remove the custom-platform dependency stack, invalidate the retained GPUI window through ApplicationHandle after React commits, and keep the paced JavaScript frame loop responsible only for pumping AppKit.

Remove obsolete platform plans, update build and development guidance, and add the native package changeset.

Validated with 82 Rust tests, 145 React tests, 11 example tests, live counter rendering and click updates, native transcript scrolling, and live text selection.

Session: ses_fdb06bf3affe9nMT6DamndI4zV
2026-08-21 22:09:22 +02:00
Tommy D. Rossi c7e8be643b Add a ChatGPT-style example, and screenshots in the README
examples/chat.tsx is a full ChatGPT-like app in ~900 lines: sidebar with
grouped history, scrolling transcript, composer, and <markdown>, <code>
and <diff> inside assistant turns. Data is hardcoded.

    cd examples && bun run chat

Two details it exists to demonstrate:

  Chrome sets userSelect: 'none'. The sidebar, top bar and composer opt
  out, so a drag across the transcript selects message text and never a
  conversation title. A test asserts both halves.

  <diff> needs a bounded viewport, because it virtualizes internally, and
  it carries its own borderRadius. GPUI clips a scroll container to its
  bounds RECTANGLE, never to a rounded path, so a rounded wrapper with
  overflow: hidden still shows square corners underneath.

examples/native-text.tsx is the smaller demo: the three components behind
a tab switcher, with a status line for events.

Screenshots
-----------
Test output lands in gitignored screenshots/ folders and churns on every
run, so only a curated set is committed, under docs/images. They are
regenerated with:

    bun scripts/screenshots.ts

which runs the visual tests and copies eight frames through a 256-colour
palette. That is lossless in practice for flat dark UI (no gradients to
band) and cuts the committed size from 976 KB to 352 KB.

The README now opens with the chat app, gains an Examples table, and shows
each native component next to the snippet that produces it.

AGENTS.md records the two rules that were expensive to learn: all text
must go through crate::text (never div().child(String), or it is invisible
to both selection and getPaintedText), and layout numbers belong on
Theme::metrics rather than in Rust constants.
2026-08-19 22:58:37 +02:00
Tommy D. Rossi 393edaece4 Add selectable text and native <code>, <diff>, <markdown> elements
GPUIX had no text selection at all, and no way to render code, diffs or
markdown without composing thousands of <div> and <text> nodes in JS.
This ports all four from Comet (https://github.com/zeronsh/comet, MIT).

Text selection
--------------
GPUI has no built-in selection, and the usual trick does not apply here:
Zed's markdown selects continuously because its whole document is ONE
element over one text model, while a React renderer produces a TREE of
small text elements. A drag crossing a paragraph has nothing to resolve
against.

So the continuity is rebuilt during paint. Every painted text element
registers into a thread-local per-frame registry; paint order IS document
order, so a drag anchored in one element resolves into per-element byte
spans, partial in the anchor and head, whole for everything between.

  selection_frame_reset()  first child of the root, clears the registry
  selectable_text(..)      content: registers + installs listeners
  chrome_text(..)          gutters and labels: painted, never selected

The registry MUST be built during paint, not during build: a list()
decides at paint time which rows exist, so build order is not document
order. Window-level listeners (not element-level) keep a drag tracking
after the mouse leaves the element.

Native text components
----------------------
  <code>      one row per line at an exact line height, so the block's
              height is known before highlighting runs
  <diff>      unified patches, virtualized with gpui list(), word-level
              intra-line highlights (Comet has none)
  <markdown>  GFM via pulldown-cmark: tables, task lists, autolinks

Syntax highlighting is Tree-sitter with a bounded LRU document cache. The
contract is deliberately colour-free: a HighlightSpan carries a
HighlightKind, never an Hsla, so a theme change recolours existing spans
instead of reparsing. Every run on a line shares one font and differs only
in colour, so a late highlight can never reflow the block.

Virtualization only works inside data-owning elements. gpui's list() takes
a 'static closure that runs during layout, long after render() returned, so
it cannot borrow the renderer's BuildCtx. <diff> virtualizes because it
parses its patch into plain Rust and the closure captures an Rc of it. A
generic <list> over React children is not possible with this architecture.

Theme and metrics
-----------------
Colours are Comet's oklch palette converted to Hsla. Layout numbers are
NOT Rust constants: row heights, gutter widths, paddings and the heading
scale live on Theme::metrics and travel in the `theme` prop, so a design
tweak is a React re-render and not a native rebuild.

Style props that were dropped
-----------------------------
Fixing text rendering surfaced five props that were public and did nothing:

  <text> layout      applied a text-only subset, so padding, width,
                     backgroundColor and every flex prop were discarded
  fontSize           lived only in the text path, ignored on <div>
  textAlign          in StyleDesc, implemented nowhere
  rowGap/columnGap   in StyleDesc, implemented nowhere
  lineHeight         accepted and ignored
  borderWidth: 0     skipped by a `> 0.0` guard, so a border an element
                     drew for itself could never be cleared

Refactors
---------
build_element went from eight positional parameters to a BuildCtx struct
with a save/restore `inherited` field, because the recursion now threads
selection state and the cascade for userSelect/selectionColor.

Testing
-------
getAllText() only sees <text> nodes in the retained tree, and native
elements paint inside gpui. Two new helpers close that gap:

  getPaintedText()   every string painted last frame, in paint order
  dragSelect(..)     flushes between down/move/up, because the listeners
                     are registered during PAINT; calling the three by
                     hand without flushes silently selects nothing

82 Rust tests, 145 React tests. Screenshots land in gitignored
screenshots/ folders so they can be inspected after a run.
2026-08-19 22:57:20 +02:00
Tommy D. Rossi 93db473296 feat: GPU-backed test renderer with screenshot support
Migrate TestGpuixRenderer from headless TestAppContext to GPU-backed
VisualTestAppContext with real Metal rendering on macOS. Windows are
positioned offscreen at (-10000, -10000) — invisible but fully rendered.

- Add captureScreenshot() to TestGpuixRenderer and TestRenderer wrapper
- Add screenshot test validating capture after click interaction
- Update test_renderer.rs to use VisualTestAppContext + TestDispatcher
2026-03-01 14:06:46 +01:00
Tommy D. Rossi c95125bc1b docs: platform improvements plan — aligning NodePlatform with Zed's platform crates
Comprehensive plan comparing our NodePlatform (winit + gpui_wgpu + napi-rs)
against Zed's three production platform implementations:

- gpui_macos (Metal, NSApp, CVDisplayLink, CoreText)
- gpui_linux (wgpu, Wayland/X11, calloop, CosmicText)
- gpui_web (wgpu, canvas, requestAnimationFrame, CosmicText)

16 improvements identified across 3 phases:

Phase 1 — Quick wins:
  - Display bounds + DPI handling (replace hardcoded 1920x1080)
  - Cursor style (map CursorStyle to winit CursorIcon)
  - Frame pacing (throttle to 60fps, stop busy-polling with setImmediate)
  - Window controls + is_maximized (wire minimize/zoom/fullscreen to winit)

Phase 2 — Feature completeness:
  - Clipboard via arboard crate
  - Window appearance detection (dark/light mode)
  - on_moved callback, set_window_title propagation
  - Callback reentrancy safety (take-call-restore pattern from Zed)
  - Dispatcher priority support, spawn_realtime threads

Phase 3 — Advanced:
  - IME/text input, multiple windows, drag-and-drop
  - Window decoration APIs, thermal state

Each item references the specific Zed source files in opensrc/ to use
as implementation baseline. Plan reviewed by Oracle agent for correctness.
2026-03-01 13:27:21 +01:00
Tommy D. Rossi bd80174b72 docs: custom elements plan — polymorphic trait interface for Zed components
Add plan for exposing Zed's Editor and DiffView as GPUIX React custom
elements (<editor>, <diff>) using a trait-based plugin architecture.

Key design decisions:
- CustomElement trait with render/set_prop/get_prop/destroy methods
- CustomElementFactory trait for creating instances by element type
- CustomElementRegistry on GpuixView for polymorphic dispatch in build_element()
- Custom props forwarded from React via setCustomProp() napi method
- Events flow through existing EventCallback (ThreadsafeFunction)
- Single binary for now, trait boundary enables binary split later
- Editor wraps Entity<Editor> with project: None (standalone, no LSP)
- Diff wraps Editor configured in expand_all_diff_hunks mode

Includes concrete Rust trait definitions, renderer integration points,
React-side host-config changes, JSX type definitions, and Zed dependency
initialization requirements.
2026-03-01 12:51:54 +01:00
Tommy D. Rossi a65435afd1 feat: expand event system with keyboard, scroll, hover, focus, and mouseDownOutside support
Expand EventPayload to carry all event types (keyboard, scroll, hover, focus,
mouseDownOutside) through a single napi struct with optional fields per event type.

Changes:
- element_tree.rs: Add fields for button, clickCount, isRightClick, pressedButton,
  key, keyChar, isHeld, deltaX, deltaY, precise, touchPhase, hovered. Add
  EventModifiers with From<gpui::Modifiers> conversion.
- host-config.ts: Wire 12 event types (click, mouseDown, mouseUp, mouseEnter,
  mouseLeave, mouseMove, mouseDownOutside, keyDown, keyUp, focus, blur, scroll)
  through EVENT_PROPS → syncEventListeners/diffEventListeners.
- types/host.ts: Add all event handler props (onClick through onScroll),
  focus props (tabIndex, tabStop, autoFocus), NativeRenderer interface.
- testing.ts: Add mocked TestRenderer with in-memory element tree,
  event simulation (simulateClick, simulateKeyDown, simulateMouseEnter, etc.),
  and tree inspection (findByType, getAllText, toJSON).
- events.test.tsx: 7 tests covering click→re-render, keyboard navigation,
  modifier keys, hover enter/leave, mouseDownOutside (dropdown close pattern),
  arrow key list navigation, and tree structure snapshots.
- Plan doc for migrating to native test renderer (docs/native-test-renderer-plan.md).
2026-03-01 11:10:30 +01:00
Tommy D. Rossi fa3ead2a2e docs: add gpui_node platform implementation plan
Detailed plan for the NodePlatform crate, covering:
- Architecture overview (Node.js ↔ napi-rs ↔ GPUI ↔ GPU)
- Component design (NodePlatform, NodeDispatcher, NodeWindow, NodeDisplay)
- Event flow (click → GPUI → ThreadsafeFunction → React → re-render)
- Future phases: mutation-based protocol, binary SharedArrayBuffer, more GPUI primitives

Based on gpui_web (PR #50228) as reference implementation.
2026-02-28 18:53:39 +01:00