Commit Graph
277 Commits
Author SHA1 Message Date
Tommy D. Rossi 51876600f9 feat: keyboard focus dim is independent of focusVisible
Before, any `focusVisible` style (even `{}`) returned early from
apply_focus_visible, so one key controlled two states: the look of the
element while it has keyboard focus, and whether it dims while another
control has it. A custom focus ring therefore could not dim.

Now the two are separate:
- `focusVisible` only styles the element while it has keyboard focus
- every other focusable element off the focus path dims to 40%, with or
  without a `focusVisible`
- new style key `keyboardFocusDim: false` keeps one element at full opacity
  (StyleDesc.keyboard_focus_dim, not inherited, excluded from state styles)
- `Select.Content` and `Dialog.Popup` (React and Solid) switch from
  `focusVisible: {}` to `keyboardFocusDim: false`
- the window-wide `render({ keyboardFocusDim: false })` option is unchanged

Test: the focus scene now checks that `keyboardFocusDim: false` opts an
element out and that an element with a focusVisible ring still dims.
README Focus styles and the pending focus changeset describe the new rule.

Session: ses_f231682b0ffe2fr5xaNLPPWQre
2026-09-26 11:16:20 +02:00
Tommy D. Rossi 9362f04263 chore(native): regenerate index.d.ts
Picks up setImagePixels format param and the setKeyboardFocusDim batch op doc
from the two previous commits.

Session: ses_f230bc6fdffe5CqhxBXqCeacz9
2026-09-26 11:14:06 +02:00
Tommy D. Rossi 550f56005b feat: setImagePixels takes a format option (rgba | bgra)
GPUI stores color images as BGRA (RenderImage, Metal and DirectX atlases).
setImagePixels only took RGBA, so every upload swapped bytes 0 and 2 of
every pixel on the UI thread. BGRA sources (ffmpeg -pix_fmt bgra,
VideoToolbox, node-canvas toBuffer('raw')) now skip that pass.

- native: PixelFormat enum in img.rs; napi, test renderer, UiCommand and
  wasm bindings take an optional format. Unknown formats throw
- host types: ImagePixelFormat, ImagePixelsOptions
- react/solid: setImagePixels(w, h, pixels, { format })
- waveform example writes BGRA by hand
- README: format docs, node-canvas raw is BGRA premultiplied (opaque only)
- test: rgba and bgra of the same color paint byte-identical screenshots,
  misread bytes swap red/blue, unknown format throws

Session: ses_f230bc6fdffe5CqhxBXqCeacz9
2026-09-26 11:13:55 +02:00
Tommy D. Rossi b2d69758ca feat: keyboardFocusDim root option turns off the keyboard focus dim
`render(<App />, { keyboardFocusDim: false })` (React and Solid) and
`createTestRoot({ keyboardFocusDim: false })` (React) stop GPUIX from dimming
every other focusable element while a control has keyboard focus. Apps then
draw their own focus look with `focusVisible`.

How it reaches native:
- new batch op `["setKeyboardFocusDim", bool]`, parsed in apply_batch_to_tree,
  so macOS, Windows/Linux, web and the test renderer share one path with no
  per-backend method and no pending state while the web window boots
- stored as RetainedTree.keyboard_focus_dim (default true)
- keyboard_focus_path() returns None when it is off, so apply_focus_visible
  never dims
- each root enqueues the op on creation (React createRoot, Solid attach), so
  a bun --hot remount without the option restores the default

Docs: README Focus styles section documents the option and the 8px pointer
slop from the GPUI bump; the pending focus changeset gets both bullets.
Test: the styles.test.tsx focus scene with keyboardFocusDim: false paints the
same frame as idle after Tab.

Session: ses_f231682b0ffe2fr5xaNLPPWQre
2026-09-26 11:13:05 +02:00
Tommy D. Rossi e1dcc09a95 feat: keyboard focus dims every other focusable element
Instead of marking the focused control, dim the rest: while a control has
keyboard focus (last input keyboard, focused handle is not an input or
textarea), every other focusable element renders at 40% of its own
opacity. The focused one stands out, and every Tab target is visible.

- GpuixView::keyboard_focus_path computes the focused control and its
  ancestors once per render; BuildCtx and CustomRenderContext carry it.
  Ancestors stay undimmed because gpui opacity covers the subtree.
- A focused text field dims nothing: typing is keyboard input too.
- An element with its own focusVisible (even {}) is never dimmed.
- GPUI refreshes the window on focus and modality change, so the path is
  recomputed without any extra wiring.

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 18:49:14 +02:00
Tommy D. Rossi 3448a76224 style: keyboard-focused controls dim to 50% opacity
70% was too subtle on dark controls.

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 18:45:09 +02:00
Tommy D. Rossi 8aefc164c5 refactor: no default focus ring; keyboard-focused controls dim
Remove the default focusVisible ring and the inherited focusRingColor
style. Without its own focusVisible:

- a focusable control (Button, tabIndex div) renders at 70% of its own
  opacity while keyboard-focused, which shows where Tab went without a
  ring (FOCUS_VISIBLE_DIM, paint only)
- <input> / <textarea> draw nothing; the caret already shows focus

apply_focus_visible takes a FocusVisibleDefault (Dim | None) instead of
an inherited ring. outlineWidth/Color/Offset stay for apps that want a
ring via focusVisible.

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 18:43:20 +02:00
Tommy D. Rossi 9a02694dc1 fix: Shift+Tab leaves input and textarea
<input>/<textarea> track the same focus handle on the editor wrapper and
on the inner text element. GPUI's TabStopMap inserted a second order node
for the repeat, so prev() from the field returned the field itself and
Shift+Tab did nothing (Tab worked, next() skips both).

Fixed in the fork (upstream tab_stop.rs has the same code): a repeated
handle is recorded for replay but adds no second node, with a unit test.

- zed: c0d597f -> 672c259 (gpui: count a focus handle tracked twice as one tab stop)
- regression test: Shift+Tab / Tab around a textarea

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 18:39:05 +02:00
Tommy D. Rossi 3cb85a64a5 fix: focusVisible is keyboard-only on text fields too
A ring around the composer textarea after a click looked bad. Browsers
ring a clicked <input>, but GPUIX now shows focusVisible and the default
ring only after keyboard input, for every element. FocusVisibleRule is
gone; apply_focus_visible always uses gpui's focus_visible.

Test: a clicked input matches a clicked input with the ring off (the
caret differs from idle); Tab into the input still shows the ring.

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 18:34:23 +02:00
Tommy D. Rossi ae10f8b149 refactor: one focusVisible key with the browser text-field rule
Drop the `focus` style key. `focusVisible` now matches CSS
:focus-visible exactly, including the rule browsers apply to text fields:

- controls (div with tabIndex, Button): GPUI .focus_visible(), so only
  after keyboard input
- <input> / <textarea>: GPUI .focus(), so also after a mouse press,
  because a field in use must look active

apply_focus_visible(el, style, ring, FocusVisibleRule) applies either the
declared style or the inherited default ring, right after track_focus,
and replaces apply_default_focus_ring. apply_interactive_styles is back to
hover + active only. The refinement borrows the style (gpui runs it
eagerly), so nothing is cloned per frame.

Test: a clicked input now shows the ring; a clicked div still does not.

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 18:15:03 +02:00
Tommy D. Rossi 1032e257c0 feat: examples use Button and Dialog; popups draw no focus ring
- examples/chat.tsx and examples/solid/chat.tsx: every div + onClick
  control (IconButton, SidebarAction, ConversationRow, thread filter,
  WorkedFor, ModeToggle, GhostButton, search hits, cycle-overlay) is a
  Button, so the chat is fully keyboard reachable. OverlayCard is a Dialog:
  Escape, backdrop press, Tab trap, and focus in (search input via
  initialFocus) and back out to the opener. Overlays stay mounted with
  open={...} instead of conditional rendering.
- example-app/app.tsx: IconButton, SidebarRow, Checkbox, Add are Buttons;
  Add is disabled while the draft is empty.
- Select.Content and Dialog.Popup set focusVisible: {} so the default ring
  does not outline the whole menu or card; they are focused only to take
  keys.
- @gpuix/solid exports the HostElement type for refs.
- chat.test.tsx: Escape closes search and focus returns to its button.

Solid chat port done by a subagent; verified with chat.automation.ts
(6/6) and a throwaway test of open, Escape, close, and focus return.

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 18:10:10 +02:00
Tommy D. Rossi dced20dbbc feat: focus / focusVisible styles, default focus ring, headless Button
Focus styles work like hover: nested style objects that map straight to
GPUI's .focus() and .focus_visible() refinements (focus_visible already
means keyboard-only, via window.last_input_was_keyboard()).

A ring needs to appear without moving layout. A border changes the box
and a zero-blur box-shadow fills the whole rect, so the zed fork gains
Style::outline (width, color, offset), painted after the border as a
border-only quad grown by offset + width that follows corner radii.
GPUIX exposes outlineWidth / outlineColor / outlineOffset.

Default ring, like a browser: every element that tracks a focus handle
(div, input, textarea) gets a 2px accent focusVisible outline unless its
style sets focusVisible. focusRingColor is an inherited style prop
(transparent turns it off), resolved through Inherited like selectionColor.

Button (React + Solid), Base UI shape: tab stop, role=button, onClick on
press, Enter keydown (not isHeld), Space keyup; disabled leaves the Tab
order, focusableWhenDisabled keeps it; state style function. buttonProps()
is shared; Dialog.Trigger / Dialog.Close now use it. GPUI has no
aria_disabled, so none is emitted.

- zed: c6531c8 -> c0d597f (gpui: add Style::outline)
- tests: default ring on keyboard only, no layout change, transparent off,
  custom focusVisible; Button activation in both adapters

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 18:02:45 +02:00
Tommy D. Rossi 7a200d9733 feat: anchored fill="window" sizes a layer to the viewport natively
Completes the fill="window" change from a parallel session. Its Dialog
Portal switch, test, README and changeset lines were swept into ef1c12e
because both sessions edited the same files; this commit adds the native
side so every commit on main builds.

- anchored.rs: fill="window" places the layer at (0, 0), skips offset and
  snapping, and sets width/height from window.viewport_size() during
  render, so a resize is followed in the same frame with no JS polling
- host.ts: AnchoredProps.fill?: "window"

Session: ses_f26e34c00ffeJpfkxJeCdK6hY8
2026-09-25 17:51:23 +02:00
Tommy D. Rossi ef1c12e1df fix: layer stack owns dialog focus, drop stale pending focus
Second oracle review of 6382a80 found five issues:

1. A pending focusElement request survived Tab / clicks and stole focus
   when its element appeared later. PendingFocus now records the focus at
   request time and is dropped if focus moved any other way first. The
   check runs before the frame's autoFocus, so an explicit request still
   beats same-frame autoFocus (live and test renderer now agree).
2. React read previous focus in a layout effect; in the test renderer
   that read flushes and an autoFocus could already hold focus. The
   DismissableLayer now reads it during render (React) / component body
   (Solid), before its elements are committed, and passes it to
   pushDismissLayer({ previousFocus }).
3+4. Each Popup moved focus on its own in effect order, so nested
   dialogs opened together focused the outer one and closed together
   lost focus. DismissLayer gains initialFocus() / finalFocus(previous);
   the host calls them in stack order: only a layer landing on top takes
   focus, a parent inserted under same-commit children rewrites their
   return target to itself, and a child whose parent already closed skips
   its restore.
5. FocusTarget function results now accept refs.

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 17:50:44 +02:00
Tommy D. Rossi 6382a80c34 feat: Dialog.Popup initialFocus / finalFocus, keep pending focus requests
Dialog.Popup takes Base UI's initialFocus and finalFocus: true (default),
false (stay), a ref or element, or a function returning one of these.
The shared resolveFocusTarget lives in @gpuix/native/host.

- initialFocus defaults to the popup itself. Base UI picks the first
  tabbable element, but GPUI's tab map exists only after the popup paints,
  so the first Tab walks that order instead.
- finalFocus defaults to the trigger, else the element focused when the
  popup opened. The Popup records it on open, so a dialog opened from app
  state (no Trigger, e.g. chat.tsx overlays) returns focus on close.
- Popup no longer uses autoFocus; it focuses imperatively after reading the
  previous focus.

Native fix found by the Solid test: sync_focus_handles took
pending_focus_element even when the handle did not exist yet, so a frame
rendered before the element's batch dropped the request. That is the
normal order for Solid (microtask flush) and for Windows/Linux (UI thread).
The request now stays until the handle exists. The test renderer's
focusElement uses the same request_focus path as the live renderer.

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 17:30:48 +02:00
Tommy D. Rossi 148c9e82b8 fix: order dismiss layers by nesting, close Combobox on blur
Oracle review found two bugs in the layer stack commit:

1. React runs child layout effects before parent ones, so a Dialog and a
   nested Select opening in one commit registered Select first. The stack
   put the Dialog on top and one Escape closed both. Layers now carry a
   parent (DismissableLayer component + context in React and Solid), and
   pushLayer inserts a parent below any child that registered first.

2. A prevented Tab still closed an open Combobox, because the input closed
   on its own Tab keydown before an ancestor could prevent the default.
   The Combobox now closes when focus leaves the input, like Base UI, so
   it closes only when Tab really moved focus.

onBlur was untestable: the offscreen test window is never active and gpui
reports focus paths only for an active window. The zed fork gains
Window::set_active_for_tests (same body as the platform active-status
callback, extracted into active_status_changed), and the test renderer
calls it. Tooltip open-on-focus is now covered too.

- zed: 81c99f8 -> c6531c8 (gpui: let tests report a window as active)
- useDismissLayer hook replaced by DismissableLayer component
- regression tests: nested defaultOpen layers, ancestor-prevented Tab

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 17:18:49 +02:00
Tommy D. Rossi cff490714e feat: Tab and Escape defaults, dismiss layer stack, headless Dialog
Keyboard defaults run in the shared JS host, after every element onKeyDown
and the window onKeyDown, so any handler can cancel them:

- Tab / Shift+Tab move focus (GPUI focus_next / focus_prev)
- Escape closes the top layer of a per-window dismiss layer stack
- KeyEvent gets preventDefault / stopPropagation / defaultPrevented
- render({ tabNavigation: false }) turns the Tab default off

Why JS and not a GPUI KeyBinding: the event callback is a non-blocking
ThreadsafeFunction, so JS runs after GPUI finished dispatching and could
never veto a native binding.

Native:
- keystrokeId on key events, bumped in a root capture listener, so JS can
  group one keystroke's element events and its trailing window event
- root listener always installed; stops Tab after all element listeners so
  the platform never types "\t" into an input
- anchored: explicit transparent background paints no fallback fill
- bounds_tracker pinned at inset 0; bounds were shifted by padding

Primitives (React + Solid):
- Select / Combobox / Tooltip triggers are tab stops, also with asChild
- Select popup traps Tab; outside press no longer steals focus back
- Escape handling moved to the layer stack
- new Dialog: Root, Trigger, Portal, Backdrop, Popup, Title, Description,
  Close (Base UI shape), modal Tab trap and focus restore
- Solid asChild keeps the child's handlers and children

Session: ses_f2717eddcffeXQkI5JXN8eAnFD
2026-09-25 17:13:47 +02:00
Tommy D. Rossi 9fcd628863 test: resolve Solid consumer build entry with fileURLToPath
URL.pathname yields /C:/... on Windows, which Bun.build rejects as a root directory. fileURLToPath returns a native path on every platform.

Session: ses_f330c012affeeWEt6hY3uyjEvQ
2026-09-23 09:54:57 +02:00
Tommy D. Rossi 989802036e test: prepare browser assets and allow packaged Solid install on Windows
Build the real Wasm browser package before native browser-bundle tests so CI validates the same files that publishing includes. The test job checks out the GPUI submodule and installs the same nightly toolchain and wasm-bindgen version used by the publish job.

Allow the end-to-end tarball consumer test enough time to pack, install, and compile the Solid fixture on Windows without Bun terminating the process at its five-second default.

Session: ses_f330c012affeeWEt6hY3uyjEvQ
2026-09-23 09:39:35 +02:00
Tommy D. Rossi 9b78e7763a test: install unpublished native tarball in Solid consumer
Point the temporary consumer override at the locally packed native tarball. Bun otherwise tries to download the Solid tarball transitive native dependency from npm, which fails while validating a not-yet-published release. Continue exercising the real packaged native and Solid code in both the preload and build-plugin checks.

Session: ses_f330c012affeeWEt6hY3uyjEvQ
2026-09-23 09:30:51 +02:00
Tommy D. Rossi 072edb6897 fix: type test renderer availability across native platforms
The Linux binary exports a constructor-only TestGpuixRenderer stub, while macOS and Windows expose the full implementation. Narrow the constructor through hasTestGpuixRenderer before assigning it to the shared test-renderer API so Linux can build the shared JavaScript package without claiming an unavailable renderer.

Session: ses_f330c012affeeWEt6hY3uyjEvQ
2026-09-23 09:22:18 +02:00
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 95c887f307 Fix live image GPU leaks and virtual-list scrollIntoView
Drop the previous GPU image when src or a live upload replaces it, and when the img node is pruned. Without drop_image, every setImagePixels frame leaked a RenderImage.

scrollIntoView now uses the virtual-list logical index, not the windowed child index. A windowStart of 50 plus child 5 now scrolls to item 55.

Attach setImage, setImagePixels, and scrollIntoView in getPublicInstance instead of createInstance, so concurrent React can abandon a host node before the methods exist. Pass the original buffer through instead of copying it with Buffer.from.

The waveform test compares two live phases. Encoded setImage and windowed scrollIntoView have screenshot and list tests.

Session: ses_f3c07efdbffeHwbaQhsE7fIz5z
2026-09-21 17:11:02 +02:00
Tommy D. Rossi 0c92f6bf77 Clear AnimatePresence exitComplete after a finished exit
When every exiting child reported complete, the map still held those
keys as true. A later exit of the same keyed child then hit the
early return and never called onExitComplete or swapped rendered
children back to the present set.

Clear the map after a completed batch so the next exit can run.

Session: ses_f3d2f1310ffeo5K8hcRmsuYLWO
2026-09-21 16:48:34 +02:00
Tommy D. Rossi 7d40fb6d2c Document the Solid adapter and wrap presence tests in Show
Add website/src/guides/solid.mdx covering install, preload, render,
primitives, motion, Select, testing, and automation. Link it from
the Guides nav and from the README Solid section.

The Solid AnimatePresence test now wraps the exiting motion.div in
Show. A boolean && child is not a tracked hole, so the previous
test never actually unmounted.

Session: ses_f3c074549ffe2jtrJMYbbLhqMx
2026-09-21 16:48:26 +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. Rossi 452ea21eec Keep shared runtime browser-safe
Resolve the native renderer through the package browser condition and expose CPU throttling from a browser-safe subpath so both public adapters bundle for WebGPU without importing node:module.

Add browser bundle coverage to the native package suite and run those package checks in CI after both adapters are built.

Make Solid motion completions generation-aware, reject stale targets, and keep presence removal idempotent. Replace the native motion invariant panic with a safe target fallback.

Session: ses_f4102f732ffesSfYYRRHlXA2j9
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 2f0a298984 Reuse the native host runtime from React
Replace React-owned copies of host types, atomic mutation transport, renderer state, automation, testing, frame-loop helpers, and CPU throttling with @gpuix/native subpaths.

Keep the existing React testing and automation entry points as thin adapters, and preserve the public React API by re-exporting the new imperative window, selection, and text-search primitives.

Session: ses_f4102f732ffesSfYYRRHlXA2j9
2026-09-21 10:39:50 +02:00
Tommy D. Rossi 239acc4376 Move shared adapter runtime into native package
Publish the framework-neutral host contract, mutation batching, renderer ownership, native testing, automation, observers, text search, and frame-loop helpers from @gpuix/native subpaths.

Generate an ESM-first napi-rs loader, retain the documented Hermes CommonJS loader, and update napi-rs to the latest supported CLI and crate releases. Add installed-package resolution tests for the loader and shared subpaths.

Make motion completion generation-aware so stale targets cannot remove current elements, partial targets preserve existing animated values, invalid or no-op exits settle, and offscreen virtual-list motion advances independently of paint.

Session: ses_f4102f732ffesSfYYRRHlXA2j9
2026-09-21 10:39:35 +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 8d3ec09438 Fix Windows bun -e tests that imported files via URL.pathname.
Bun on Windows cannot load `/D:/...` paths. fileURLToPath produces a real Windows path so the uncaught-exception and unhandled-rejection subprocess tests can import the renderer.

Session: ses_f5effe0ccffeXHPxxwP7cFDI4h
2026-09-14 19:56:38 +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