Commit Graph
176 Commits
Author SHA1 Message Date
Tommy D. Rossi 410fb56f2e release: @gpuix/native@0.10.0, @gpuix/react@0.10.0, and @gpuix/solid@0.10.0
Consume the pending changesets into a minor release. Users get the official Solid 1 renderer, AnimatePresence exit animations, live image uploads and scrollIntoView on host refs, native window controls, and an OS file picker.

Bump the native Cargo crate, native npm package, React package, Solid package, generated napi loader version checks, and Bun workspace lock to 0.10.0. Keep published adapter dependencies on @gpuix/native as workspace:^. CI is the only publish path for native platform binaries.

Fixes #68

Session: ses_f3b7fae0dffeZjEBH48TZSQVBq
2026-09-21 17:14:54 +02:00
Tommy D. Rossi 9547b07853 Add changeset for empty text taking zero layout height
GPUI reserved a full line for an empty string. Solid's universal
renderer emits an empty placeholder text node for every dynamic hole
(Show, For, conditional). Those placeholders painted as 26px rows and
stretched Select menu items in the Solid chat example.

The native fix already landed. This changeset documents it for the
next changelog.

Session: ses_f3c65bfd5ffegJLklT8domWQ7B
2026-09-21 16:48:15 +02:00
Tommy D. Rossi dc7b106a2d Add live image uploads and scrollIntoView on host refs
`<img>` refs can push image bytes without a data URL or JSON mutation.
`setImage` takes encoded PNG, JPEG, WebP, GIF, SVG, BMP, TIFF, ICO, or
Netpbm. `setImagePixels` takes packed RGBA. Prefer pixels for a live
waveform. There is no density argument. Upload a 2x bitmap into a 1x
layout box on retina.

Every host ref now has `scrollIntoView()`, which scrolls the nearest
overflow parent or `<virtual-list>` until that node is visible.

The waveform example paints a generated 2x RGBA buffer through
`setImagePixels`.

Session: ses_f3c07efdbffeHwbaQhsE7fIz5z
2026-09-21 16:38:49 +02:00
Tommy D. Rossi 1881927e9e Split NativeRenderer into small host contracts
Stop copying napi wire types in `@gpuix/native/host`. Re-export
ElementBounds, HighlightMatch, PathPromptOptions, EdgeInsets, and
overlay stats from the generated addon types.

Helpers now ask only for the methods they use:

- createMutationQueue takes MutationHost (applyBatch)
- window observers take WindowSizeHost / WindowInsetsHost
- selection observation takes SelectionHost

NativeRenderer stays the adapter-facing type: required applyBatch,
plus live renderer methods as Partial<Pick<GpuixRenderer, ...>>.
Fakes can still implement applyBatch alone. Do not subclass the
napi GpuixRenderer.

Session: ses_f3c88f82affeXgsQdmQW4wpizp
2026-09-21 10:40:12 +02:00
Tommy D. Rossiandjuan 919a736e72 Add the first-party Solid renderer
Introduce @gpuix/solid for stable Solid 1 with a small universal host tree, shared native mutation and event infrastructure, native-backed testing and automation, motion, AnimatePresence, Select, Combobox, and Tooltip.

Compile reactive JSX through an OpenTUI-style Bun preload and exported Bun.build plugin, with no Vite or wrapper CLI. Add direct workspace, real native renderer, imported TSX, and installed-tarball end-to-end coverage.

Automatically flush signal mutations outside event boundaries while keeping failed native batches available for retry.

Session: ses_f4102f732ffesSfYYRRHlXA2j9

Co-authored-by: juan <47119916+jhomra21@users.noreply.github.com>
2026-09-21 10:40:12 +02:00
Tommy D. Rossi 58a6b1ab67 Make presence completion target-aware
Tie motion completion to the generation that started each target so queued events cannot complete a replacement animation. Preserve partial target values and finish no-op, invalid, and offscreen exits.

Harden AnimatePresence participant registration, cancellation, and keyed sibling handling with focused native-backed regressions.

Session: ses_f3d2f1310ffeo5K8hcRmsuYLWO
2026-09-21 10:40:01 +02:00
Tommy D. Rossi 896d6598f2 Add AnimatePresence exit animations for motion.div
Keep leaving motion nodes mounted until native motion reaches the exit target, matching Motion for React. AnimatePresence holds the child, motion.div swaps animate to exit while isPresent is false, and motionComplete then unmounts it. Without AnimatePresence, unmount stays immediate.

Native MotionState now reports just_settled once when a track reaches its current target, including zero-duration retargets. GpuixView emits motionComplete after the build, the same way highlight events flush, so a setState in the handler cannot re-enter the frame.

Wire onMotionComplete through host types and the reconciler. Export AnimatePresence, usePresence, and useIsPresent. Document the API, add GPU-backed unmount coverage, and add a minor changeset.

Session: ses_f40bbeaeeffebG77zwP6ZNd8m0
2026-09-21 09:21:40 +02:00
Tommy D. Rossi 788223c13e Keep floating overlay fill in sync with visibility and opacity
Move visibility and opacity, including hover and active refinements, onto the outer anchored FloatingLayer surface. Nested opacity no longer multiplies against the deferred fallback fill.

pointerEvents none now disables the anchored occluder so a hidden or non-interactive overlay does not steal hits. Background, border, shadow, overflow, and layout stay on the inner content.

Extend the retained-tree snapshot so hover and active surface styles, content opacity stripping, and occlude false are covered. Document the overlay contract in the README and the pending changeset.

Session: ses_f4fec2f63ffeX4htZahVSoX5f6
2026-09-21 09:21:27 +02:00
Tommy D. Rossi e256ed1345 Add native desktop window controls
Expose minimizeWindow, zoomWindow, and toggleFullscreen through the native renderer and React renderer contract.

Dispatch macOS operations directly through the embedded window and route Windows, Linux, and FreeBSD operations through the GPUI UI command channel.

Document the desktop-only API, regenerate the N-API declarations, and add a production binding regression test plus a minor changeset for issue #68.

Session: ses_f41046982ffe9HBet3NGzDDJXd
2026-09-20 15:51:25 +02:00
Tommy D. Rossi 3a033eafc4 Preserve rounded floating surfaces
Copy uniform and per-corner border radii from FloatingLayer content to the outer anchored surface. This prevents the deferred fallback background from showing square corners behind Select, Combobox, and Tooltip popups.

Add retained-tree snapshot coverage for every supported radius property and document the overlay behavior.

Session: ses_f4fec2f63ffeX4htZahVSoX5f6
2026-09-17 20:21:32 +02:00
Tommy D. Rossi f145960e78 Add native path picker API
Expose promptForPaths through the desktop renderer and React renderer contract. The promise resolves selected absolute paths, returns null for cancellation, validates unsupported option combinations, and rejects on browser builds.

Route Windows and Linux requests through the GPUI UI command channel. Settle all desktop results through napi-rs JsDeferred without holding a libuv worker for the lifetime of the operating-system dialog.

Keep the capability optional for injected mutation-only renderers. Add native option tests, a production deferred rejection test, test-renderer cancellation coverage, generated declarations, public docs, and the Windows GPUI cancellation caveat.

Session: ses_f4fec2f63ffeX4htZahVSoX5f6
2026-09-17 20:21:09 +02:00
Tommy D. Rossi de7f5d1415 Fix native input caret and paste routing
Draw input and textarea carets at the full line height so the bar matches the painted editor row.

Propagate paste shortcuts when the clipboard is empty, contains only an image, contains external paths, or belongs to a read-only editor. Keep ordinary text and mixed text-plus-image clipboard content on the native text insertion path.

Add Rust and React regression coverage for clipboard classification, element and window event propagation, and caret geometry.

Session: ses_f4fec2f63ffeX4htZahVSoX5f6
2026-09-17 20:20:36 +02:00
Tommy D. Rossi 40f25ae601 Draw the input and textarea caret at full font size.
The caret used to be 75% of fontSize so it matched approximate cap height.
That bar looked too short next to the glyphs. Size it to the em square
instead, still capped by lineHeight so extra leading does not stretch it
past the line box.

Chat composer at fontSize 14 / lineHeight 20 now paints a 14px caret
instead of 10.5px. Chrome uses the full line height; Firefox uses font
size after you type. This matches Firefox.

Session: ses_f5601bbdaffevYnfCmDhYOddPC
2026-09-16 13:39:50 +02:00
Tommy D. Rossi 7ac9880abd release: @gpuix/native@0.9.0 and @gpuix/react@0.9.0
Consume the pending changesets into a minor release. Users get a window-level onSelectionChange callback for text selection, a live-window click panic fix, and docs to pin @gpuix/react and @gpuix/native to the same exact version.

Bump the native Cargo crate, native npm package, React package, generated napi loader version checks, and Bun workspace lock to 0.9.0. Keep the published React dependency on @gpuix/native as workspace:^. CI is the only publish path for native platform binaries.

Fixes #38

Session: ses_f56207a65ffeFBg1JxfiwGIPqH
2026-09-16 13:07:10 +02:00
Tommy D. Rossi 48a4039e95 Add a window-level onSelectionChange event for text selection.
React apps can now react when the selected text ranges change, which is the
gap that blocked porting termcast onto gpuix. Selection is app-wide and
rebuilt at paint, so this is a root callback on render() / createRoot(),
the same attachment as onKeyDown, not a per-element prop.

Native emit reads the same SelectionState as getSelectedText and keys on a
span identity hash, so an unchanged frame does not fire. Empty on mount is
not a change. A later clear fires once with value omitted. The payload is
a normal EventPayload; value is the joined selected text.

```
  drag / clear  ►  SelectionState.identity()  ►  emit selectionChange
                                              ►  handleGpuixEvent
                                              ►  onSelectionChange(event)
```

Session: ses_f59f89012ffecnPBB1A40P5don
2026-09-15 19:32:52 +02:00
Tommy D. Rossi dc2409a30f Defer selection drag updates so live clicks do not panic
A physical left click still aborted 0.8.0 with
`cannot update GpuixView while it is already being updated`.
0.8.0 already dispatched automation mouse through AnyWindowHandle,
so that path no longer leased the root view. AppKit still does.
The text-selection mouse-up listener then nested-updated the same
GpuixView while GPUI held the lease.

A tap never started a drag, so skip drag-end cleanup there.
Real selection-drag move and end work now run through App::defer,
after the current GPUI effect cycle returns the root lease.

The test renderer now leases GpuixView the same way AppKit does
for mouse down, move, up, and click. The old AnyWindowHandle path
hid this panic. Cover it with a click that must not abort.

Fixes #38

Session: ses_f5bf1a57bffekyaF7gpwuhWzPr
2026-09-15 12:38:06 +02:00
Tommy D. Rossi 89b67995b2 release: @gpuix/native@0.8.0 and @gpuix/react@0.8.0
Consume the pending changesets into a minor release. Users get GPUI accessibility through role and aria-* props, Base UI Select wrapping with asChild, Finder file drops, GitHub Releases auto-update, Wayland layer-shell windows, http(s) img sources, textDecoration, textarea newlines, font-sized input rows, a runtime error overlay that keeps the macOS window alive, and several desktop and browser reliability fixes.

Bump the native Cargo crate, native npm package, React package, generated napi loader version checks, and Bun workspace lock to 0.8.0. Keep the published React dependency on @gpuix/native as workspace:^. CI is the only publish path for native platform binaries.

Fixes #37
Fixes #38
Fixes #39
Fixes #41
Fixes #46
Fixes #47
Fixes #59
Fixes #63

Session: ses_f5effe0ccffeXHPxxwP7cFDI4h
2026-09-14 19:46:44 +02:00
Tommy D. Rossi ef9b670b3f Add asChild support to Select and Combobox items
GPUI uses a flat painted hit list and does not bubble clicks. A custom row
with its own background can therefore cover SelectItem or ComboboxItem and
prevent selection, hover state, and popup close behavior.

Add asChild to both item primitives through the existing renderSlot path.
The custom row becomes the host item itself, so it owns one native hit target,
one event id, the item ref, composed handlers, state styles, and disabled
selection behavior.

Migrate every chat picker to asChild. MenuRow now forwards its ref and host
props, while keeping its own row padding. Add end-to-end tests for filled
Select and Combobox rows and a chat regression assertion for menu row height.

Document when to use asChild, including the ref and host-prop forwarding
contract. Add a React patch changeset with the new public API.

Session: ses_f61007e8fffe0tRyMx0uRPPeCZ
2026-09-14 12:22:27 +02:00
Tommy D. Rossi ca1a01dd17 Queue focus requests while the browser window opens
Browser WebGPU creates its public window handle before the internal GPUI window can accept updates. Calling focusElement from a React mount effect therefore threw GPUIX web window is not ready even though WEB_WINDOW was already present.

Store the latest browser focus target outside the window update path. Consume it during GpuixView render, after the window is active, and apply it after focus handles are synchronized. Explicit focus keeps precedence over autoFocus.

Use the same deferred focus state on desktop when a requested host element does not have a focus handle yet. Clear an older pending request whenever a newer request focuses an existing handle, so stale state cannot steal focus on a later render.

Document startup queuing and the latest-request-wins rule. Add a native patch changeset for the browser API behavior.

Session: ses_f61007e8fffe0tRyMx0uRPPeCZ
2026-09-14 12:22:04 +02:00
Tommy D. Rossi af9c8bbeb9 Stop the browser chat example from dying before WebGPU opens.
https://gpuix.dev/chat-example/ stayed on the loader for two reasons.

The error overlay read process.platform at module load. A browser has
no process, so that threw before Wasm started. OVERLAY_MONO is now a
CSS font stack. readMacCpuThrottle() treats a missing process as no
THROTTLE. render() only calls process.exit when process exists.

createRoot() also called setWindowKeyEvents before GPUI finished
opening the web window, which threw GPUIX web window is not ready.
The wasm renderer now queues those listeners the same way it already
queues the debug overlay, then applies them when start_web_app builds
GpuixView.

The production chat bundle copies examples/web.css, and the website
serves it as /chat-example/web.css so the loader styles load.

Session: ses_f64d18bc8ffeysXOZPWSnfTQWv
2026-09-14 11:04:09 +02:00
dim-ghub bbaa2023e0 Open a window as a wlr-layer-shell surface via WindowOptions.layerShell.
The GPUI fork in zed/ already implements layer-shell at the Rust level
(WindowKind::LayerShell, layer_shell::LayerShellOptions), but nothing
reached JS: WindowOptions had no way to ask for it, so @gpuix/react
could only open normal floating windows. A panel, dock, notification
shade or wallpaper needs an edge-anchored surface with an exclusive
zone, which is exactly what layer-shell is for.

Add to the napi WindowOptions:

- layerShell?: LayerShellOptions - namespace, layer (background |
  bottom | top | overlay), anchor (top / bottom / left / right, two
  opposite edges stretch that axis), exclusiveZone, exclusiveEdge,
  margin ([top, right, bottom, left]), keyboardInteractivity (none |
  on-demand | exclusive). All optional; the default anchor is a top
  bar (top | left | right) and the default layer is top.
- appId?: string - Wayland app_id / X11 WM_CLASS, useful on its own
  and needed for a compositor to target a layer surface by rule.

to_gpui_window_options maps layerShell onto WindowKind::LayerShell and
drops the titlebar. The WindowKind variant only exists on a
wayland-enabled Linux gpui build, so the mapping is
cfg(target_os = "linux"); on macOS and Windows layerShell is ignored
and appId still applies. init_threaded opens a layer surface at the
origin instead of Bounds::centered, since the compositor places it
from its anchor.

index.d.ts is hand-edited rather than regenerated: the @napi-rs/cli
in node_modules emits a much smaller index.d.ts than the committed
one (it drops every doc comment and all of TestGpuixRenderer's
methods), so a full `napi build` regen here would be a large
destructive diff unrelated to this change. The added block matches
the shape napi produces for these structs.

Tests in renderer.rs cover the mapping (anchor union, exclusive zone
and edge, margin, keyboard interactivity, titlebar dropped) and that a
window with no layerShell stays Normal with a titlebar.

Verified on Hyprland (3 monitors): a @gpuix/react app calling
render(<Bar/>, { layerShell: { anchor: ['top','left','right'],
exclusiveZone: 34 } }) opens a 34px top bar, hyprctl reports the
kite-panel layer surface and reserved: [0, 34, 0, 0], and a React
setInterval clock re-renders live on the surface.
2026-09-10 15:00:01 -05:00
Tommy D. Rossi 18e695ed0e Size input and textarea rows from style.fontSize and style.lineHeight.
layout_text used window.line_height() after Taffy measure. Parent
with_text_style is gone by then, so the row was always 16×φ no matter
what React passed. A 28px font still sat in a 26px box. Issue 63.

Use the TextStyle captured during request_layout instead:
style.line_height_in_pixels(rem_size). Without an explicit lineHeight
that follows GPUI default leading, so a larger fontSize grows the box.
Pass lineHeight to set the row in pixels. minRows and maxRows still
multiply that height. An explicit height still wins.

Tests assert:

- textarea height equals lineHeight
- minRows=3 with lineHeight=30 is 90
- fontSize=28 with no lineHeight is 45 (28×φ)
- a single-line input honors lineHeight too

Session: ses_f79437cb5ffennyb8lTdU6KH7Q
2026-09-10 16:48:47 +02:00
Tommy D. Rossi f24d27071d Point @gpuix/react/floating at a public barrel, and drop useFocusTrap from the docs.
The package used to export ./floating from dist/components/floating.js, which is the internal module for Select, Combobox, and Tooltip. That surface listed useControllableState and useFocusTrap as public helpers.

useFocusTrap is already gone from the implementation. It listened for Tab on a panel ref and forwarded it to focusNextWithin / focusPreviousWithin. Those renderer methods are the real scoped-tab API. A thin hook on top of them also fought a window-level Tab handler, because both moved focus and neither could stop the native key.

src/floating.ts is now the published entry. It re-exports FloatingLayer, renderSlot, and the style helpers libraries actually need. package.json points ./floating at dist/floating.js.

Docs and the pending Select changeset now match that:

- trap Tab by owning onKeyDown, then calling focusNextWithin / focusPreviousWithin
- do not also call focusNext from an element onKeyDown when a window Tab handler is already installed
- the floating table lists FloatingLayer and renderSlot only

Session: ses_f7fa3a82effee2tmI4o1tW6YzS
2026-09-10 16:48:37 +02:00
Tommy D. Rossi 833d77f7e3 Add onFileDrop for Finder and OS file drops.
GPUI already turns an OS FileDropEvent into an ExternalPaths drag and
delivers on_drop to the hovered hitbox. Wire that onto host elements the
same way onClick works. Nested targets keep working because GPUI stops
propagation after a matching drop.

```tsx
<div
  onFileDrop={(event) => openFiles(event.paths ?? [])}
  style={{ width: 400, height: 300 }}
/>
```

event.paths is string[] of absolute Unicode filesystem paths. event.x and
event.y are the drop point in window pixels. An empty drop, or a drop that
contains a non-Unicode path, does not fire. to_string_lossy would invent a
path that does not exist on disk, so conversion uses Path::to_str instead.

Works on div, text, img, svg, input, textarea, code, markdown, diff, and
anchored. virtual-list is not a host div and does not take this event; wrap
it. Desktop only. Tests simulate Entered then Submit through GPUI.

Session: ses_f7f7c408bffe5d59D85FJmrsKT
2026-09-08 13:48:22 +02:00
Tommy D. RossiandMateo M. 2e7799714d Keep an element's style when React hides it.
hideInstance used to send `{ visibility: hidden }` and drop every other
declaration. Suspense retries and hidden Activity trees then lost their
layout box. The hide now keeps the existing style and only adds
visibility: hidden. Hover and active still drop, so a hover style cannot
paint an element React asked to hide. unhideInstance restores the style
React passed in.

The host-config test drives hideInstance by hand. The pinned
react-reconciler is older than the React that exports Activity, so a
normal render cannot reach that path here.

Co-authored-by: Mateo M. <hello@mateo.sh>
Session: ses_f7fa131f8ffe300STsqQLIywNJ
2026-09-08 12:03:00 +02:00
Tommy D. Rossi eac7181c33 Return getElementBounds as { x, y, width, height }.
The previous array form could not grow. Extra fields such as scroll
offsets would have to sit at magic indexes. Native napi, wasm, the test
renderer, automation, and every call site now use named fields.

Session: ses_f83f90209ffeVmkdjhaBehDTP7
2026-09-07 18:03:17 +02:00
Tommy D. Rossi fa536086ec Parse GitHub Releases in checkUpdate. Drop latest.json.
The feed file was a workaround. GitHub already has tag_name, assets, and
a sibling .sig. checkUpdate now hits api.github.com:

  GET https://api.github.com/repos/OWNER/REPO/releases/latest

Rewrite github.com/OWNER/REPO, /releases/latest, and
/releases/latest/download/... to that API URL. Match the host bundle
(*.app.tar.gz, *_{arch}.AppImage, *_x64-setup.exe / *_arm64-setup.exe)
and GET {name}.sig. Missing sibling signature is an error. Infer format
from the asset name.

Keep packager JSON as a silent fallback when the body already has
version plus url or platforms. Do not document that path.

CI uploads only what packager writes: the signed bundle and its .sig.
README and the changeset now show GitHub Actions plus
checkUpdate against https://github.com/OWNER/REPO/releases/latest.

Session: ses_f837ecfbeffeynBTQx5bKyvN9d
2026-09-07 18:00:55 +02:00
Tommy D. Rossi 246a160fe9 Add checkUpdate() on @gpuix/native using GPUIX HTTP.
Port cargo-packager-updater 0.2.3 into packages/native/src/updater.rs
instead of depending on the crate. The original crate pulled crates.io
reqwest blocking, a second TLS stack next to Zed zed-reqwest. HTTP now
goes through reqwest_client, the same client <img> already uses.

JS:

  import { checkUpdate } from '@gpuix/native'
  const update = await checkUpdate('0.1.0', {
    endpoints: ['https://github.com/OWNER/REPO/releases/latest/download/latest.json'],
    pubkey,
  })
  if (update) await update.downloadAndInstall()

The feed is 204 or a latest.json with platforms keys like macos-aarch64.
downloadAndInstall() replaces packaged files and does not relaunch.
Desktop only. The browser wasm build does not export this.

Original: https://github.com/crabnebula-dev/cargo-packager/blob/v0.2.3/crates/updater/src/lib.rs

Session: ses_f83f922a9ffe9QbIBVQESIXTQM
2026-09-07 17:52:09 +02:00
Tommy D. Rossi 846de94d8d Let Select follow Base UI, and export library APIs for gpuix-ui.
Select no longer walks children by `child.type === SelectItem`. Root `items`
is optional and only a label lookup for SelectValue while the popup is
closed. Keyboard nav and clicks read mounted SelectItem children through
registerItem. Disabled lives on Item. A styled wrapper around Item works.
Without items, SelectValue shows the raw value.

Register items in useLayoutEffect, not a callback ref, so a middle item
can re-render without jumping to the end of the list. selectValue rejects
unmounted and root-disabled items. A late-mounted selected item becomes
the highlight.

Native:
- getFocusedElementId, focusNextWithin, focusPreviousWithin walk GPUI's
  painted TabStopMap via focus_next_among / focus_prev_among
- visibility: hidden maps to GPUI invisible()
- getElementBounds is typed on NativeRenderer

JS:
- export Props, GpuixTheme, and host prop types from @gpuix/react
- export FloatingLayer, useFocusTrap, and friends from @gpuix/react/floating

AGENTS.md: built-in headless controls follow Base UI's Root-data vs
children split.

Fixes #59

Session: ses_f83f90209ffeVmkdjhaBehDTP7
2026-09-07 17:49:32 +02:00
Tommy D. Rossi 1da015e53b Document packing a Hermes GPUIX app, shrinking the .node, and fetch.
The hermes-node guide now covers three extra ship steps:

1. Shrink the sidecar. napi build --platform --release already uses LTO.
   It does not pass --strip. strip -x drops the macOS arm64 addon from
   22 MB to 17 MB. Bare strip fails because NAPI / AppKit imports must
   stay. Dropping test-support and more LTO do not move the needle. The
   remaining 17 MB is GPUI. CI still ships the 22 MB published binary.

2. Wrap gpuix-hermes and the .node in a .app with cargo-packager. List
   the addon as a second binary so it lands in Contents/MacOS, where
   dlopen looks. Measured .app size is 34 MB. A Bun chat .app with no
   sidecar was 82 MB on the same machine.

3. hermes-node has no global fetch. node-fetch@2 over Node http works
   for plain HTTP. HTTPS throws because TLS is not implemented. Do not
   install node-fetch v3. It is ESM and Hermes cannot parse it.

Also link tmikov/hermes-node#11 for the macOS -dead_strip napi_* drop.
Update the docs-hermes-runtime changeset to mention packager, strip,
and the 34 MB .app.

Session: ses_f8427a2d6ffeLODe2UB6Cw8300
2026-09-07 15:25:49 +02:00
Tommy D. Rossi 2d6e599c2b Document shipping a GPUIX app on hermes-node.
Add a Holocron guide at website/src/guides/hermes.mdx and link it from the
README. The page covers rebuilding hermes-node on macOS (the published
tarball dead-strips napi_*), bundling React to CJS with bun build, and the
measured ship set: 12 MB executable plus a 22 MB native sidecar.

A hermes/ prototype sits next to the docs so the same path can be rerun.

Session: ses_f84b88b16ffePB3X7Fqzot47OY
2026-09-07 14:18:43 +02:00
Tommy D. Rossi 8138d4c054 Expose GPUI accessibility through React role and aria-* props
GPUI already talks to the macOS AX tree, Windows UIA, and Linux AT-SPI
through AccessKit. GPUIX never set .role(), so VoiceOver saw an empty
window. A node is in the tree only with both an id (already set) and a
role.

Prop names match React DOM. Role values are ARIA tokens.

```tsx
<div
  role="button"
  aria-label="Delete note"
  aria-id="notes.delete"
  onClick={remove}
>
  Delete
</div>
```

- `none` / `presentation` produce no node
- `<text>` defaults to Label, `<input>` to TextInput, `<textarea>` to
  MultilineTextInput, `<img alt>` to Image
- onClick registers AccessKit Click, so VoiceOver Press fires the same
  JS handler
- Tests dump GPUI debug_a11y_tree_json after paint

The zed submodule picks up two GPUI hooks: tests can activate the a11y
tree without a screen reader, and Img / Svg now report .role() instead
of dropping it.

Browser / wasm has no AccessKit adapter. These props are no-ops there.

Fixes #47

Session: ses_f89a78b8cffe4lrrAfJ8Uz4sH1
2026-09-06 15:23:53 +02:00
Tommy D. Rossi 5f066f3c55 Show a stack overlay after a JavaScript runtime error.
A throw used to keep the window alive but empty. render() now paints the
error message and stack, and Reload remounts the last tree.

Unhandled React render errors, uncaughtException, and unhandledRejection
all take this path. The overlay is scheduled on a microtask keyed to the
failing root, so a later remount does not paint a stale overlay. Process
handlers uninstall on resetRender.

Save under bun --hot still remounts too.

Session: ses_f941afdedffezh2lODSRNO04A8
2026-09-04 17:40:04 +02:00
Tommy D. Rossi 0bb3c94cda Add http(s) src support to img.
Desktop GPUI defaults to NullHttpClient, so a remote URL never loaded.
Install Zed reqwest_client on native apps so img(SharedUri) can fetch in
the background. Wasm already has fetch.

A definite width and height now also set aspect_ratio. GPUI otherwise
fills that from the bitmap after decode and jumps the box. Tests use a
FakeHttpClient that returns the same SVG as the disk fixture, so the
dispatcher never parks on a real network.

Set both width and height. Failed loads still show the existing
fallback. A loading URL paints an empty box of the declared size.

Session: ses_f9810208bffeUeHRs8rtxe7KaD
2026-09-04 17:39:31 +02:00
Tommy D. Rossi fa61d80ed8 Center single-line input text and clip it to borderRadius.
A search pill with extra height used to paint text at the top of the box,
and rounded corners did not clip the glyphs. HTML inputs center and clip.

Single-line inputs now use items_center when given extra height, and
overflow_hidden when borderRadius is set. Document the no-default-padding
gotcha next to the other CSS-looking style traps.

The padding coverage test checks that paddingLeft actually insets painted
text, which is how a pill wrapper is supposed to work.

Session: ses_f9922154effe0jqPuJbXGeDNke
2026-09-04 17:38:32 +02:00
Tommy D. Rossi 4bea250745 Draw the input caret at about 75 percent of fontSize.
A 13px composer used to paint a bar as tall as the line box, so it stuck
out above and below the glyphs. Default leading is phi, and cap height is
about 0.75em.

The caret now uses that ratio, clamps to line height, and sits in the
middle of the line. IME bounds use the same geometry.

Session: ses_f97dbaec6ffehVfUKGx9R6Qia8
2026-09-04 17:38:03 +02:00
Tommy D. Rossi ff3c1a2bb7 Make Enter insert a newline in textarea unless onSubmit is set.
Plain Enter used to always fire onSubmit on both input and textarea, so a
normal multiline editor could not insert a newline, and Enter did nothing
when no onSubmit listener was set.

Textarea now uses a separate key context. Enter and Shift+Enter insert a
newline. When onSubmit is present, Enter submits and Shift+Enter still
inserts a newline. Input still submits on Enter.

Adding or removing onSubmit after mount swaps the key context, so a
composer that only submits after the user types still gets the right
Enter binding.

Fixes #46

Session: ses_f998cca76ffexsRUJHhI2AdZeA
2026-09-04 17:37:25 +02:00
Tommy D. Rossi 95757bb10b Add a textDecoration style for underline and strikethrough.
GPUI already paints those decorations. This wires `"underline"`,
`"line-through"`, and `"none"` through StyleDesc, apply_styles, and the
React StyleDesc type so a React tree can request them.

Visual coverage compares decorated text against the same glyphs with no
decoration so a missing native apply cannot hide behind a type-only
change.

Session: ses_f9922154effe0jqPuJbXGeDNke
2026-09-04 17:34:41 +02:00
Tommy D. Rossi 248752177e Keep the macOS window alive after a JavaScript runtime error.
On macOS, JS owns the event loop. startFrameLoop calls renderer.tick()
about every 8ms, and that tick pumps AppKit. A throw that escaped the
timer stopped the next setTimeout, so the window froze. bun then often
exited, which looked like a crash.

startFrameLoop now catches errors from tick() and still schedules the
next pump. Native event callbacks catch throws from React handlers so
they do not become napi_fatal_exception. render() installs
uncaughtException and unhandledRejection listeners so bun stays alive,
and it starts the frame loop before the first React flush so a throw
during mount still leaves AppKit pumping. The loop is not restarted on
remount.

The error is logged. There is no auto-reload. Save under bun --hot to
remount.

Session: ses_f97da32b5ffejnj7pvF2xzbkXL
2026-09-03 20:50:12 +02:00
Tommy D. Rossi 028f70af6d Merge remote-tracking branch 'origin/main' 2026-09-02 20:41:55 +02:00
Tommy D. Rossi 5700c9674a Document nonblocking macOS AppKit ticks and add a regression test.
GpuixRenderer.tick() already drains only ready AppKit events. The README
now says that, and events.test.tsx checks that 30 idle ticks return in
under 200ms so Bun timers and sockets can run between frames.

Fixes #39

Session: ses_fa1101bf5ffexVlY2PD1MZjRSg
2026-09-02 20:36:19 +02:00
Tom X Nguyen bf98e0760c fix(native): deliver clicks from primary mouse-up 2026-09-02 21:33:09 +07:00
Tommy D. Rossi e948b20ddd Prevent live automation mouse input from leasing the root view
Dispatch click, hover, wheel, and drag input through AnyWindowHandle on desktop and browser hosts. Mouse listeners can now update GpuixView during normal GPUI event delivery without triggering a nested entity lease and aborting the process.

Cover the production renderer with a child-process automation test that clicks the live chat sidebar, waits for the state change, then drives keyboard input through the composer.

Build and verify both the release native addon and the WebAssembly renderer so the ownership fix stays consistent across every live host.

Issue: #38

Session: ses_fa1aaf89dffezIsneAEezbytzc
2026-09-01 22:58:09 +02:00
Tommy D. Rossi 1c4c67b409 Keep events live across Bun hot reloads
Persist renderer-owned containers, element ID allocators, and window event IDs across JavaScript module evaluation. The native renderer callback can now route into the active React root after bun --hot replaces the module graph.

Bind render-level onEvent callbacks to the owning root. Stale element and window events are rejected before they can reach either React handlers or the replacement root observer.

Extend the real bun --hot child-process regression to click both before and after reload and confirm that remounted roots keep unique IDs.

Issue: #37

Session: ses_fa1aaf89dffezIsneAEezbytzc
2026-09-01 22:57:49 +02:00
Tommy D. Rossi a24b4a42eb release: @gpuix/native@0.7.0 and @gpuix/react@0.7.0
Consume the seven pending changesets into a minor release. Users get native two-stop gradients, in-memory image data URLs, application-owned Tab handling, renderer-level keyboard callbacks, consistent test-renderer exports, correct Windows DPI scaling, clean last-window exit on Windows and Linux, and a frosted-glass window example.

Bump the native Cargo crate, native npm package, React package, generated napi loader version checks, and Bun workspace lock to 0.7.0. Keep the published React dependency on @gpuix/native as ^0.7.0 through workspace:^ resolution.

The release validation covers 221 Rust tests, 370 React renderer tests, 47 example tests, the starter app typecheck, CLI tests and build, package lint, release native and React builds, and both native and React tarball contents.

Fixes #30
Fixes #31
Fixes #32
Fixes #35
Fixes #36

Session: ses_fa2dcb95bffediF7Xu1SKakouC
2026-09-01 15:31:06 +02:00
Tommy D. Rossi 10e1bb0fb2 Let applications own Tab key behavior
Remove GPUIX's process-wide Tab and Shift+Tab bindings so focused editors and terminals receive the native key events directly.

Add renderer-level onKeyDown and onKeyUp callbacks, plus focusNext and focusPrevious wrappers for applications that want to install their own traversal policy. Keep browser focus on GPUI's hidden keyboard element after Tab without turning that platform safeguard into focus traversal.

Give each mounted root a key-event generation so queued callbacks cannot cross a remount. Disable native root listeners on unmount and preserve the active root when a second root is rejected.

Update the focus documentation, release note, native declarations, and end-to-end tests for element delivery, renderer delivery, explicit traversal, remount safety, and root ownership.

Session: ses_fa3b8830cffeGdTgarIef6r2Kn
2026-09-01 15:11:22 +02:00
Tommy D. Rossi b20e98a88b native: support image data URLs
Decode base64 and percent-encoded image data URLs when the src prop changes, then retain the decoded bytes through gpui::Image while preserving filesystem path loading and image element identity.

Reuse the data URL decoder for SVG sources, document the API, and cover the native render path with a screenshot comparison against the same filesystem image. Add a release changeset that references issue #35.

Keep a TODO at the JSON boundary for replacing base64 transport with binary mutations so large image payloads do not decode during paint.

Session: ses_fa3e66794ffeXNYejd15WllDnE
2026-09-01 11:32:47 +02:00
Tommy D. Rossi aacb07020c native: fix Windows DPI scaling and last-window quit
Two Windows host bugs in the napi `.node` path, which does not get
Zed's executable manifest or AppKit last-window-close wiring.

DPI (#31): GPUI declares Per-Monitor V2 only in the host exe
manifest (zed#8936). node.exe and bun.exe have no such manifest, so
Windows bitmap-stretched the window at >100% scaling. Call
SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2) on the gpuix-ui
thread only, before WindowsPlatform creates an HWND. Never on the
Node/bun thread: the fallback is SetThreadDpiAwarenessContext, which
would change the host. If process awareness is already locked, the
UI thread still requests V2.

Last window (#32): GPUI already quits the UI thread on last window
close (QuitMode::LastWindowClosed is the Windows default). Node/bun
stayed alive because tick() always returned true and requiresTick()
was macOS-only, so JS never process.exit(0). Make tick() observe an
atomic that clears when Platform::run returns, and start the same JS
frame loop on Windows/Linux. Stop run_ui_commands after the last
window is gone so later window.update does not log `window not found`.

Fixes #31
Fixes #32

Session: ses_fa9254252ffe4soIYSYl5CI1dp
2026-08-31 11:14:31 +02:00
Tommy D. Rossi 5937978b96 native: export TestGpuixRenderer on Linux as a throwing stub
index.d.ts already declared the class on every platform, so Linux
type-checked `new TestGpuixRenderer()` and then threw
`TypeError: TestGpuixRenderer is not a constructor`.

Keep the class defined in every napi binary. On macOS and Windows with
test-support, construction still opens the GPU test window. Everywhere
else, including Linux `--no-default-features`, the constructor throws
and explains that wgpu cannot read a rendered image back yet.

Export `hasTestGpuixRenderer()` so `@gpuix/react` can skip GPU tests.
`hasNativeTestRenderer` must not become true just because the class
exists.

Fixes #30

Session: ses_fa9254252ffe4soIYSYl5CI1dp
2026-08-31 10:56:04 +02:00
Tommy D. Rossi 09e0caeb18 native: add linear gradient backgrounds
Expose two-stop linear gradients through the React style.background API with typed stops, CSS angle direction, and sRGB or Oklab interpolation.

Translate the structured value through the retained style pipeline into GPUI backgrounds. Reuse the same resolution for painting, mouse occlusion, and anchored overlay fill detection so transparent gradients keep the existing interaction rules.

Add native translation and transparency tests, an end-to-end GPU screenshot test for gradient direction, public documentation, and a minor changeset for @gpuix/native and @gpuix/react.

Session: ses_fae3bda32ffe3CrFWAX0OEwcI6
2026-08-30 10:36:03 +02:00