[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-96015":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":9,"totalLinesOfCode":9,"stars":12,"forks":13,"watchers":14,"openIssues":14,"contributorsCount":9,"subscribersCount":14,"size":14,"stars1d":14,"stars7d":14,"stars30d":14,"stars90d":14,"forks30d":14,"starsTrendScore":14,"compositeScore":15,"rankGlobal":9,"rankLanguage":9,"license":9,"archived":16,"fork":16,"defaultBranch":17,"hasWiki":16,"hasPages":16,"topics":9,"createdAt":9,"pushedAt":9,"updatedAt":18,"readmeContent":19,"aiSummary":20,"trendingCount":14,"starSnapshotCount":14,"syncStatus":21,"lastSyncTime":9,"discoverSource":22},96015,"gpuix","remorses\u002Fgpuix","remorses","Node.js & React bindings for Zed’s GPUI. Build memory efficient native apps with React and no Electron",null,"https:\u002F\u002Fgithub.com\u002Fremorses\u002Fgpuix","Rust",1627,44,0,49.96,false,"main","2026-09-20 04:01:32","# GPUIX\n\nReact bindings for [GPUI](https:\u002F\u002Fgithub.com\u002Fzed-industries\u002Fzed\u002Ftree\u002Fmain\u002Fcrates\u002Fgpui) - Zed's GPU-accelerated UI framework.\n\nBuild native GPU-accelerated desktop apps with React and TypeScript. Your components render directly to the GPU via Metal, DirectX, or Vulkan. No Electron, no web views.\n\n![mail.tax example](.\u002Fdocs\u002Fimages\u002Fmail-app.jpg)\n\nEverything above is GPUIX: the sidebar, the thread list, the reading pane,\nand native `\u003Cmarkdown>`. Start it with **`bun --hot`** so a save remounts React\non the same window:\n\n```bash\ncd examples && bun --hot mail.tsx\n```\n\n## Quickstart\n\nCreate an app from the official example. The command downloads only\n`example-app\u002F` and installs its dependencies. There is no repository clone,\nnative build, or Rust toolchain.\n\n```bash\nbunx @gpuix\u002Fcli new my-app\ncd my-app\nbun run dev\n```\n\n`@gpuix\u002Freact` pulls the native renderer for your platform. Edit `app.tsx` and\nthe running window remounts on save. Click and keyboard handlers switch to the\nnew tree without recreating the window.\n\n### Build from scratch\n\nInstall the packages directly when you do not want the example app:\n\n```bash\nbun add @gpuix\u002Freact react\nbun add -d @types\u002Freact typescript\n```\n\n### 1. Point TypeScript at the GPUIX JSX types\n\n**`jsxImportSource` is required.** Without it TypeScript uses DOM types, so\n`\u003Cvirtual-list>`, `\u003Cmarkdown>`, `\u003Ccode>` and `style.hover` all fail to\ntypecheck.\n\n```json\n{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"@gpuix\u002Freact\",\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"noEmit\": true\n  }\n}\n```\n\n### 2. Write the entry file\n\nEnd the file with `render()`. That call creates the window, mounts React, and\nstarts the frame loop.\n\n```tsx\nimport { useState } from 'react'\nimport { render } from '@gpuix\u002Freact'\n\nfunction App() {\n  const [count, setCount] = useState(0)\n  return (\n    \u003Cdiv style={{ padding: 24, backgroundColor: '#1a1a1a', height: '100%' }}>\n      \u003Cdiv\n        onClick={() => setCount((c) => c + 1)}\n        style={{\n          padding: 12,\n          borderRadius: 8,\n          cursor: 'pointer',\n          backgroundColor: '#232323',\n          hover: { backgroundColor: '#2c2c2c' },\n        }}\n      >\n        \u003Ctext style={{ color: '#e2e2e2' }}>Count: {count}\u003C\u002Ftext>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  )\n}\n\nrender(\u003CApp \u002F>, { title: 'My App', width: 800, height: 600 })\n```\n\n> [!IMPORTANT]\n> **Give every `\u003Ctext>` a `color`.** GPUI does not inherit `color` from a\n> parent, so text with no color paints **black** and disappears on a dark\n> surface.\n\n### 3. Run it\n\n```bash\nbun --hot app.tsx\n```\n\nUse `bun --hot`, not plain `bun`. A save then remounts React on the same\nwindow instead of opening a second one.\n\n### 4. Ship a binary\n\n```bash\nbun build --compile app.tsx --outfile dist\u002Fapp\n.\u002Fdist\u002Fapp\n```\n\nThe binary carries the renderer, so it runs with no Bun and no Node install.\n\nFor a smaller ship set, run the same React app on\n[hermes-node](.\u002Fwebsite\u002Fsrc\u002Fguides\u002Fhermes.mdx) instead of Bun. That path is\n**12 MB** plus a **22 MB** native sidecar. The steps are in that guide.\n\n### Start from the example app\n\n[`example-app\u002F`](https:\u002F\u002Fgithub.com\u002Fremorses\u002Fgpuix\u002Ftree\u002Fmain\u002Fexample-app) is a complete todo app in one file, with `dev`,\n`build`, `web:dev` and `typecheck` scripts already wired. Create a copy with\n`bunx @gpuix\u002Fcli new my-app`.\n\n![The GPUIX todo example app](.\u002Fdocs\u002Fimages\u002Ftodo-app.png)\n\n### Shell completions\n\nInstall completions for the `gpuix` command:\n\n```bash\nbun add -g @gpuix\u002Fcli\ngpuix completions install\n```\n\n## Examples\n\n| Example | Run | What it shows |\n|---|---|---|\n| **todo** | `bun run dev` in [`example-app\u002F`](https:\u002F\u002Fgithub.com\u002Fremorses\u002Fgpuix\u002Ftree\u002Fmain\u002Fexample-app) | The starting point: one file, a `\u003Cvirtual-list>`, a native `\u003Cinput>`, and an animated sidebar |\n| **blurred window** | `bun run blurred-window` | A macOS frosted-glass surface using GPUI's native vibrancy backdrop and transparent titlebar |\n| **chat** | `bun --hot chat.tsx` | A GPUIX app: transparent titlebar, animated sidebar, message list, composer, `\u003Cmarkdown>` |\n| **timeline** | `bun --hot timeline.tsx` | A video-editor timeline: clip dragging, edge trimming with snapping, playhead scrubbing, marquee selection, zoom under the pointer, and a two-axis pan with a frozen ruler and track column |\n| **mail** | `bun --hot mail.tsx` | A Superhuman-style mail client: three panes, thread list, and a Framer newsletter |\n| **native-text** | `bun --hot native-text.tsx` | The three native text components with a tab switcher |\n| **counter** | `bun --hot counter.tsx` | The smallest possible app: state, events, hover |\n| **diff** | `bun --hot diff.tsx` | A diff viewer composed from `\u003Cdiv>` and `\u003Ctext>` in JS, for comparison |\n| **web** | `bun run web` from the repository root | The ChatGPT example rendered in a browser canvas with WebGPU |\n\nThe todo app lives in [`example-app\u002F`](https:\u002F\u002Fgithub.com\u002Fremorses\u002Fgpuix\u002Ftree\u002Fmain\u002Fexample-app) and is meant to be copied.\nThe rest live in [`examples\u002F`](https:\u002F\u002Fgithub.com\u002Fremorses\u002Fgpuix\u002Ftree\u002Fmain\u002Fexamples). Those use hardcoded data.\n\nOr download a standalone **chat** build from the [GitHub release](https:\u002F\u002Fgithub.com\u002Fremorses\u002Fgpuix\u002Freleases). No Bun or Rust install is required.\n\n```bash\ntar -xzf example-chat-aarch64-apple-darwin.tar.gz\n.\u002Fexample-chat-aarch64-apple-darwin\n```\n\nThe archive keeps the executable bit, so there is no `chmod` step. macOS may still block the unsigned binary the first time. Right-click the file, choose **Open**, and confirm.\n\nOn Windows, download `example-chat-x86_64-pc-windows-msvc.exe` and double-click it. On Linux, the file is `example-chat-x86_64-unknown-linux-gnu.tar.gz`.\n\nThe web example bundles the same React app and reconciler as the desktop chat\nexample. wasm-bindgen exposes mutations and event callbacks to the existing\nretained tree and `GpuixView`, which run through GPUI's browser platform.\n\nThe web build needs nightly Rust and the matching wasm-bindgen CLI:\n\n```bash\nrustup toolchain install nightly --component rust-src --target wasm32-unknown-unknown\ncargo install wasm-bindgen-cli --version 0.2.127 --locked\nbun run web\n```\n\nThe generated Wasm uses shared memory, so the page must be cross-origin\nisolated. Production servers must send these headers on the **top-level\ndocument**:\n\n```http\nCross-Origin-Opener-Policy: same-origin\nCross-Origin-Embedder-Policy: require-corp\n```\n\n`require-corp` then constrains **cross-origin** subresources, which must supply\ntheir own CORS or `Cross-Origin-Resource-Policy`. Serve the JavaScript and the\nWasm from the same origin as the document and nothing else is needed.\n\n`bun run web` rebuilds the Wasm only when `packages\u002Fnative\u002Fwasm` is missing.\nAfter a Rust change, force it:\n\n```bash\nbun scripts\u002Fweb.ts --rebuild\n```\n\n#### Hot reload in the browser\n\n`bun run web` serves the example through Bun's frontend dev server, so an edit\nto `examples\u002Fchat.tsx` arrives as a **React Fast Refresh** update. Components\nswap in place and `useState` survives, which means the composer text, the\nsidebar selection, and the scroll position all stay where they were. The GPUI\ncanvas is never re-created and the ~19 MB Wasm module is never re-fetched.\n\nFast Refresh only applies to a module whose exports are all components. Edit\nanything else, such as the entry file, and Bun reloads the page instead. Both\npaths are correct; the reload is only slower.\n\nThe Wasm half is a **singleton and must never re-evaluate**.\n`WebGpuixRenderer::init` fails with `GPUIX web is already running` once its\nthread-local app exists, and GPUI's browser platform appends its own canvas to\n`\u003Cbody>`. What protects it is not that it lives in `node_modules`; Bun bundles\nit into the same client registry as your app. It is that Bun re-runs only the\n**changed** module and then walks upward through its importers, so an unchanged\ndependency stays evaluated and cached. Two rules follow:\n\n- do not call `import.meta.hot.accept(\".\u002Fyour-app\", ...)` in the entry file.\n  Bun runs an importer's dependency-accept callback **even when the imported\n  module already self-accepted**, so that callback would remount the tree on top\n  of a successful refresh and throw away every `useState`\n- keep the `@gpuix\u002Fnative` import in a module that can never become a Refresh\n  boundary and is never explicitly accepted\n\nThe chat example puts a virtualized `\u003Cdiff>` and a GFM table inside an assistant\nturn, inside a scrolling transcript:\n\n![A diff and a markdown table inside a chat turn](.\u002Fdocs\u002Fimages\u002Fchat-diff.png)\n\nMarkdown, code and a virtualized diff in one frame:\n\n![Markdown, code and diff rendered together](.\u002Fdocs\u002Fimages\u002Fshowcase.png)\n\n## Architecture\n\nGPUIX bridges React to GPUI using a **mutation-based protocol**. Desktop apps use napi-rs; browser apps load the same Rust renderer through wasm-bindgen. React collects changed elements into one atomic mutation batch per commit. Rust applies that batch to a retained element tree that GPUI reads each frame.\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│  React (JavaScript)                                             │\n│                                                                 │\n│  function App() {                                               │\n│    const [count, setCount] = useState(0)                        │\n│    return (                                                     │\n│      \u003Cdiv style={{ display: 'flex', gap: 8 }}>                  │\n│        \u003Cdiv onClick={() => setCount(c => c + 1)}>               │\n│          Count: {count}                                         │\n│        \u003C\u002Fdiv>                                                   │\n│      \u003C\u002Fdiv>                                                     │\n│    )                                                            │\n│  }                                                              │\n└─────────────────────────────────────────────────────────────────┘\n                    │ napi desktop \u002F wasm-bindgen browser\n                    │ applyBatch([\n                    │   [\"createElement\", 1, \"div\"],\n                    │   [\"setStyle\", 1, {...}],\n                    │   [\"setRoot\", 1]\n                    │ ])\n                    ▼\n┌─────────────────────────────────────────────────────────────────┐\n│  Rust host bridge                                               │\n│                                                                 │\n│  RetainedTree ── stores elements, styles, event flags           │\n│       │                                                         │\n│       ▼  each GPUI frame                                        │\n│  GpuixView::render() → build_element() → GPUI elements          │\n└─────────────────────────────────────────────────────────────────┘\n                    │\n                    ▼\n┌─────────────────────────────────────────────────────────────────┐\n│  GPUI                                                           │\n│                                                                 │\n│  Metal, DirectX, Vulkan, or browser WebGPU \u002F WebGL2             │\n│  Flexbox layout via Taffy                                       │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n## Why This Works\n\nGPUI is an **immediate-mode** UI framework — it rebuilds the entire element tree every frame. Instead of fighting this, GPUIX embraces it:\n\n1. React reconciler detects a state change and queues host mutations (`createElement`, `setStyle`, `appendChild`, etc.)\n2. `applyBatch()` validates and applies the complete commit to the Rust **RetainedTree**\n3. On each GPUI frame, `GpuixView::render()` walks the RetainedTree and calls `build_element()` to produce ephemeral GPUI elements\n4. GPUI lays them out (Taffy flexbox) and renders to the GPU\n5. Only **changed elements** cross the FFI boundary — React's reconciler diffs the virtual tree and sends minimal mutations\n\nThis is the same protocol React uses for the DOM (`createElement`, `appendChild`, `removeChild`, `commitUpdate`), but targeting a GPU renderer instead of a browser.\n\n## Mutation API\n\nThe mutation surface between JS and Rust is one atomic method. Desktop uses napi and the browser uses wasm-bindgen:\n\n```ts\ninterface NativeRenderer {\n  applyBatch(json: string): Array\u003Cnumber>\n}\n```\n\nElement IDs are plain numbers generated by an incrementing counter in JS. React may abandon work in concurrent render mode, so GPUIX keeps new host nodes in JS until React places the accepted subtree during commit. Only then are its mutations added to the batch. `applyBatch()` applies that accepted commit atomically and marks the Rust view dirty for the next frame.\n\n## Event Flow\n\nEvents travel from GPUI back to React through a `ThreadsafeFunction` on desktop\nand a wasm-bindgen callback in the browser.\n\n```\nUser clicks element id=3\n       │\n       ▼\nGPUI fires on_click on the element\n       │\n       ▼\nRust closure calls emit_event_full(callback, 3, \"click\", {x, y, ...})\n       │\n       ▼\nDesktop ThreadsafeFunction \u002F browser callback sends EventPayload\n       │\n       ▼\nJS event registry: eventHandlers.get(3)?.get(\"click\")?.(payload)\n       │\n       ▼\nReact handler runs: onClick={() => setCount(c => c + 1)}\n       │\n       ▼\nState update triggers re-render → reconciler sends mutations back to Rust\n```\n\nEvent handlers are stored in a JS-side registry keyed by `(elementId, eventType)`. Rust only knows **whether** an element has a listener (via `setEventListener`), not the closure itself — the actual handler lives in JS.\n\n## Packages\n\n- **`@gpuix\u002Fnative`** — Rust bindings to GPUI. It publishes napi-rs desktop binaries and a wasm-bindgen browser build, both backed by `GpuixRenderer`, `RetainedTree`, `build_element()`, and `apply_styles()`.\n- **`@gpuix\u002Freact`** — React reconciler, event registry, and TypeScript types. Implements the `react-reconciler` host config using the mutation API.\n- **`@gpuix\u002Fcli`** — `gpuix new` downloads `example-app\u002F`, sets its published React dependency, and installs it as a standalone project.\n\n## Building\n\nThis section is for **working on GPUIX itself**. To build an app with it, see\n[Quickstart](#quickstart) instead. Installing the packages needs no Rust\ntoolchain and no submodule.\n\n### Prerequisites\n\n1. Rust toolchain\n2. Node.js 18+\n3. Xcode with Metal Toolchain (macOS)\n\n```bash\n# Install Metal Toolchain if needed\nxcodebuild -downloadComponent MetalToolchain\n\n# Install dependencies\nbun install\n\n# Check out the pinned GPUI fork\ngit submodule update --init --recursive\n\n# Build native package\ncd packages\u002Fnative\nbun run build\n\n# Build React package\ncd ..\u002Freact\nbun run build\n\n# Run example (use tmux for long-running sessions)\ncd ..\u002F..\u002Fexamples\nbun --hot counter.tsx\n```\n\n## Usage\n\n```tsx\nimport React, { useState } from 'react'\nimport { render } from '@gpuix\u002Freact'\n\nfunction App() {\n  const [count, setCount] = useState(0)\n  return (\n    \u003Cdiv style={{ display: 'flex', gap: 8, padding: 16 }}>\n      \u003Cdiv\n        style={{ backgroundColor: '#3b82f6', borderRadius: 8, padding: 12, cursor: 'pointer' }}\n        onClick={() => setCount(c => c + 1)}\n      >\n        \u003Cdiv style={{ color: '#ffffff' }}>Count: {count}\u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  )\n}\n\nrender(\u003CApp \u002F>, {\n  title: 'My App',\n  width: 800,\n  height: 600,\n  titlebarTransparent: true,\n  windowBackground: 'blurred',\n  trafficLightX: 16,\n  trafficLightY: 17,\n})\n```\n\n`render()` creates the native window, mounts React, and starts the frame loop.\nThe red traffic-light button quits the process. Start the app again from the\nterminal.\n\n| Option | Values | Purpose |\n|---|---|---|\n| `titlebarTransparent` | boolean | Hide the native titlebar so the app draws chrome under the traffic lights |\n| `windowBackground` | `\"opaque\"` (default), `\"transparent\"`, `\"blurred\"` | Window fill. `\"blurred\"` is the macOS vibrancy backdrop |\n| `trafficLightX` \u002F `trafficLightY` | pixels | Traffic-light origin. The chat example uses `(16, 17)` |\n| `transparent` | boolean | Same as `windowBackground: \"transparent\"` when that option is unset |\n| `appName` | string | Name inside the macOS `Hide X` and `Quit X` items. Defaults to `title` |\n| `focus` | boolean, default `true` | `false` opens the window behind the active app, like `open -g` |\n| `show` | boolean, default `true` | `false` opens the window hidden. Call `activateWindow()` to reveal it |\n\nCall it again after a save and it remounts the tree on the same window.\n\n### The macOS menu bar\n\nGPUIX installs the application menu bar for you, so a fresh app already answers\n`⌘Q`, `⌘H`, `⌥⌘H`, `⌘M`, and `⌘W`. Without it `NSApp.mainMenu` is nil, macOS\npaints an empty menu bar, and those shortcuts do not exist at all: AppKit only\nprovides them through menu items.\n\n```\nApple    \u003Cexecutable>             Window\n         ├ Services               ├ (AppKit window tiling)\n         ├ Hide \u003CappName>   ⌘H    ├ Minimize          ⌘M\n         ├ Hide Others     ⌥⌘H    ├ Zoom\n         ├ Show All               ├ Close Window      ⌘W\n         └ Quit \u003CappName>   ⌘Q    └ (open windows)\n```\n\n**`appName` does not set the title of the application menu.** macOS takes that\nfrom the executable, so `bun app.tsx` shows `bun` during development and a\n`bun build --compile` binary shows its own file name. Only a real `.app` bundle\nchanges it. `appName` reaches the items inside the menu, and nothing else.\n\nThere is **no Edit menu**, on purpose. A menu key equivalent is consumed by\nAppKit before the window sees the key event, so an Edit menu carrying `⌘C`\nwould take the keystroke away from text selection and from `\u003Cinput>`.\n\nUse **`render()`**, not `createRenderer()`, in the app entry. `bun --hot`\nre-runs the whole file on save. `createRenderer()` plus `init()` would then\nbuild a second host. `render()` is idempotent: the first call owns the window,\nlater calls only remount React.\n\n`createRenderer()`, `createRoot()`, and `startFrameLoop()` stay public for\ntests and custom hosts. Pass `{ renderer }` into `render()` when you already\nhave one.\n\n**One renderer drives one root.** A renderer owns one window, one native root\nid, and one event map, so `createRoot()` throws if that renderer already has a\nmounted root. Call `unmount()` on the first root before you create another;\n`render()` already does that for you.\n\n### Background launch\n\n`focus: false` opens the window **without taking focus**. The app you were\ntyping in keeps the caret and the active titlebar. `show: false` goes further\nand opens no window at all, so the process runs with a live React tree and\nnothing on screen.\n\n```tsx\nrender(\u003CApp \u002F>, { title: 'Notes', focus: false })\n```\n\n**Turn this on whenever a coding agent runs your app.** An agent that starts\nthe app to check its work will otherwise yank the window in front of whatever\nyou are doing, mid-sentence, once per iteration. With `focus: false` the agent\nstill gets a real GPU-rendered window it can screenshot and click, and you keep\nyour editor. See [Let an agent drive the app](#let-an-agent-drive-the-app).\n\n`activateWindow()` brings the window forward and focuses it. It is the only way\nto reveal a `show: false` window. Reach it from any component with\n`useGpuixRequired()`:\n\n```tsx\nimport { useGpuixRequired } from '@gpuix\u002Freact'\n\nfunction Reveal() {\n  const renderer = useGpuixRequired()\n  return \u003Cdiv onClick={() => renderer.activateWindow?.()}>Show\u003C\u002Fdiv>\n}\n```\n\nOutside React, call it on the renderer that `createRenderer()` returned.\n\n| Platform | `focus: false` | `show: false` |\n|---|---|---|\n| macOS | window orders in front without becoming key, like `open -g` | honored |\n| Windows | `SW_SHOWNOACTIVATE` | honored |\n| Linux | **ignored**, the window opens focused | **ignored** |\n\nThe process still gets a **Dock icon** on macOS. GPUI sets the regular\nactivation policy, so there is no menu-bar-agent mode yet. For a real\nbackground daemon, run the app from a `launchd` agent in\n`~\u002FLibrary\u002FLaunchAgents\u002F`; launchd never activates the process.\n\n### Let an agent drive the app\n\nMake focus opt-in through the environment, so a human run behaves normally and\nan agent run stays out of the way:\n\n```tsx\nrender(\u003CApp \u002F>, {\n  title: 'Notes',\n  focus: process.env.GPUIX_BACKGROUND !== '1',\n})\n```\n\n```bash\nbun app.tsx                      # you: window comes to the front\nGPUIX_BACKGROUND=1 bun app.tsx   # agent: window opens behind your editor\n```\n\n`launch()` passes `env` straight through, so an agent script sets it once and\nevery screenshot, click, and assertion runs on a window that never interrupts\nyou:\n\n```ts\nimport { launch } from '@gpuix\u002Freact\u002Fautomation'\n\nconst app = await launch({\n  command: 'bun',\n  args: ['app.tsx'],\n  env: { GPUIX_BACKGROUND: '1' },\n})\n\nawait app.getByTestId('bump').waitFor()\nawait app.getByTestId('bump').click()\nawait app.screenshot({ path: 'tmp\u002Fafter-click.png' })\nawait app.close()\n```\n\nFocus is the only thing that changes. **Automation does not need focus.**\n`click()` hits the last painted bounds and `screenshot()` reads the GPU\nsurface, so both work while the window sits behind your editor, and even on a\n`show: false` window that is not on screen at all.\n\n```\n  agent ──►  launch({ env: { GPUIX_BACKGROUND: '1' } })\n                │\n                ▼\n           GPU window renders and paints without activation\n                │\n                ├──►  getByTestId(..).click()   ✓  hits the last painted bounds\n                ├──►  screenshot({ path })      ✓  reads the GPU surface\n                ├──►  fill() \u002F press()          ✓  uses the live input pipeline\n                └──►  close()\n\n  you   ──►  keep typing, your editor stays frontmost the whole time\n```\n\n`fill()` and `press()` use the live GPUI window input pipeline. They work\nwithout activating the desktop window. **Linux ignores `focus`**, so an agent\nthere still gets a focused window.\n\nPrefer `createTestRoot()` when you can. It opens **no window at all**, so\nnothing can steal focus and keyboard input works. Reach for `launch()` plus\n`focus: false` when the check needs a real window, real GPU paint, or a real\nprocess.\n\n### flushSync\n\nThe root is a **concurrent root**, so React commits in a later microtask.\n`flushSync` forces the render and the commit to finish before it returns, the\nsame as in `react-dom`.\n\n```tsx\nimport { flushSync } from '@gpuix\u002Freact'\n\nflushSync(() => setSidebarOpen(true))\n```\n\nIt flushes **React only**, down to one `applyBatch` call. After it returns the\nnative retained tree is up to date, including styles and text.\n\nIt does **not** wait for GPUI. Layout and paint still happen on the next frame,\nexactly like the browser paints after a DOM mutation. To see pixels, wait a\nframe in the app, or call `renderer.flush()` in a test.\n\nUse it when an ordering bug depends on the commit landing first: an unmount\nbefore a remount, or a state change before you feed the next event.\n\n## Debug frame overlay\n\nGPUI paints frame-time stats into the window after layout. The overlay is not\na React element. A React FPS label would update every frame and cause more work.\n\n```tsx\nrender(\u003CApp \u002F>, { title: 'My App', debugFrameOverlay: 'full' })\n```\n\n| Mode | What you see |\n|---|---|\n| `hidden` | nothing (default) |\n| `minimal` | last draw time, e.g. `8.3 MS` |\n| `full` | `CUR`, `1%`, `10%`, `MAX`, `FRAMES` |\n\nOr call the renderer:\n\n```ts\nrenderer.setDebugFrameOverlay('full')\nrenderer.cycleDebugFrameOverlay()\nrenderer.resetDebugFrameOverlayStats()\nrenderer.getDebugFrameOverlay() \u002F\u002F 'hidden' | 'minimal' | 'full'\nrenderer.getDebugFrameOverlayStats()\n\u002F\u002F { currentMs, p90Ms, p99Ms, maxMs, frames, samples }\n```\n\n`p90Ms` is the overlay **10%** line. `p99Ms` is the **1%** line. Those are the slow tail.\n\nThe overlay shows **draw time**, not FPS. `8.3 MS` is about 120 Hz.\n\nThe chat example has a regression test for this: `examples\u002Fchat.perf.test.tsx`. It times mount, wheel draw, and sidebar clicks. It asserts p95, not every frame.\n\nThe default example suite excludes this hardware-timing test so shared CI runner variance does not fail functional checks. Run it explicitly on the target Mac:\n\nOn macOS, `THROTTLE=utility` restarts the process under `taskpolicy -c utility`. That pins work to E-cores. It is an **M1\u002FM2 Air CPU** proxy, not Chrome 6x. GPU and RAM stay fast. `THROTTLE=background` is slower.\n\n```bash\ncd examples\nTHROTTLE=utility bun run test:perf\nTHROTTLE=utility bun --hot chat.tsx\n```\n\n## Hot reload\n\n### 1. End the file with `render()`\n\n```tsx\nimport { render } from '@gpuix\u002Freact'\n\nfunction App() {\n  return \u003Cdiv style={{ padding: 16 }}>hello\u003C\u002Fdiv>\n}\n\nrender(\u003CApp \u002F>, { title: 'My App', width: 800, height: 600 })\n```\n\nDo **not** call `createRenderer()` or `init()` in this file. `bun --hot` re-runs\nthe whole entry on save. A second `init()` would open a second window.\n\n### 2. Start the app with `bun --hot`\n\nPrefer **`bun --hot`** over a plain `bun` or `tsx` run. Without `--hot`, a\nsave starts a second process. With it, `render()` remounts React on the same\nwindow.\n\n```bash\nbun --hot app.tsx\ncd examples && bun --hot chat.tsx\n```\n\n### 3. Save the file\n\n```\nsave .tsx  ►  bun re-evaluates the entry  ►  render() remounts React\n                     │\n                     ▼\n              GpuixRenderer, window, GPU stay\n```\n\nThe first `render()` creates the native host and stores it on `globalThis`.\nEach save unmounts the React tree and mounts a new one on that same host.\n\n**Stays:** window, GPU device, native `.node` addon, GPUI scroll physics.\n\n**Resets:** `useState`, focus, React event handlers.\n\nThis is a remount, not React Refresh. Keeping hook state needs Bun to inject\n`$RefreshReg$` during `--hot`. That transform exists on\n`bun build --react-fast-refresh` only. Tracked in\n[oven-sh\u002Fbun#40179](https:\u002F\u002Fgithub.com\u002Foven-sh\u002Fbun\u002Fissues\u002F40179).\n\nNative `.node` edits still need a rebuild. See [Developing the Rust side](#developing-the-rust-side).\n\nOn **macOS**, `startFrameLoop` calls `renderer.tick()` at a fixed rate (~125fps by\ndefault). Each tick drains only ready AppKit events and Core Foundation sources,\nthen returns without waiting for the next native wake. Bun timers, sockets, promises,\nand PTY callbacks can run between ticks. Pass `{ frameMs }` to change the rate, and\ncall `.stop()` on the returned handle to end it.\n\nA **runtime throw does not freeze the window.** The frame loop catches errors from\n`tick()`, native event callbacks catch throws from React handlers, and `render()`\ninstalls `uncaughtException` \u002F `unhandledRejection` listeners so bun stays alive.\nThe window shows the stack and a **Reload** button that remounts the last\n`render()` tree. Save under `bun --hot` also remounts. The process does not\nexit.\n\nOn **Windows and Linux**, GPUI runs its normal blocking native event loop on one\ndedicated Rust UI thread. `tick()` does not pump that loop. It only reports\nwhether the UI thread is still inside `Platform::run`. `startFrameLoop` still\ncreates a JavaScript timer so last-window-close can return false and `render()`\ncan `process.exit`, matching macOS. All platforms use GPUI's native platform,\nwindow, renderer, input, scroll, clipboard, keyboard, and IME implementations.\nThe embedded macOS run-loop extension comes from the pinned GPUIX fork. CI runs\nthe full React and example test suites through DirectX on Windows.\n\n> [!IMPORTANT]\n> On macOS, never drive `tick()` from a `setImmediate` loop. That spins at tens of thousands of\n> ticks per second and burns **73% CPU on a completely idle app**, versus **1%** when\n> paced.\n\n## Native animations\n\nUse **`motion.div`** to animate from an initial style to a target style. React\nsends the target once. Rust calculates intermediate values and requests GPUI\nframes until the transition finishes, without a React render or N-API call for\neach frame.\n\n### Animate a target\n\n```tsx\nimport { motion } from '@gpuix\u002Freact'\n\nfunction WelcomeCard() {\n  return (\n    \u003Cmotion.div\n      initial={{ width: 0, opacity: 0 }}\n      animate={{ width: 320, opacity: 1 }}\n      transition={{ duration: 0.25, ease: 'easeOut' }}\n      style={{ overflow: 'hidden' }}\n    >\n      \u003Ctext style={{ color: '#ffffff' }}>Welcome\u003C\u002Ftext>\n    \u003C\u002Fmotion.div>\n  )\n}\n```\n\nSet **`initial={false}`** when the element must mount at its first `animate`\ntarget. Later `animate` changes still transition normally. If a target changes\nwhile motion is active, the next transition starts from the current visible\nvalue, so reversing an animation does not jump.\n\n### Targets and timing\n\nMotion currently accepts these **numeric targets**:\n\n| Target | Range or unit |\n|---|---|\n| `width`, `height` | pixels, zero or greater |\n| `top`, `right`, `bottom`, `left` | pixels |\n| `opacity` | `0` through `1` |\n| `borderRadius` | pixels, zero or greater |\n\nThe **transition** uses seconds, like Motion for React:\n\n| Option | Default | Values |\n|---|---:|---|\n| `duration` | `0.3` | Non-negative seconds |\n| `delay` | `0` | Non-negative seconds |\n| `ease` | `\"easeOut\"` | `\"linear\"`, `\"ease\"`, `\"easeIn\"`, `\"easeOut\"`, `\"easeInOut\"`, or `[x1, y1, x2, y2]` |\n\nSprings, keyframes, variants, exit transitions, and shared layout animations\nare not available yet.\n\n### Animate a sidebar\n\nAnimate an **outer clipping container** and keep the inner sidebar at a fixed\nwidth. This reveals or hides the content without reflowing its text on every\nframe.\n\n```tsx\nimport { motion } from '@gpuix\u002Freact'\nimport type { ReactNode } from 'react'\n\nfunction SidebarFrame({\n  collapsed,\n  children,\n}: {\n  collapsed: boolean\n  children: ReactNode\n}) {\n  const sidebarWidth = 252\n  const dividerWidth = 1\n\n  return (\n    \u003Cmotion.div\n      initial={false}\n      animate={{ width: collapsed ? 0 : sidebarWidth + dividerWidth }}\n      transition={{ duration: 0.2, ease: 'easeOut' }}\n      style={{\n        display: 'flex',\n        flexDirection: 'row',\n        height: '100%',\n        flexShrink: 0,\n        overflow: 'hidden',\n      }}\n    >\n      \u003Cdiv style={{ width: sidebarWidth, height: '100%', flexShrink: 0 }}>\n        {children}\n      \u003C\u002Fdiv>\n      \u003Cdiv style={{ width: dividerWidth, height: '100%', flexShrink: 0 }} \u002F>\n    \u003C\u002Fmotion.div>\n  )\n}\n```\n\nThe **chat example** uses this pattern. The sidebar remains mounted while its\nouter width moves between `253` and `0` pixels.\n\n### Capture exact frames\n\nThe [automation API](#automation) can freeze the native motion clock and render\nspecific timestamps. This avoids timer sleeps and gives CI the same frames on\nevery run.\n\n```tsx\nimport { connectTest } from '@gpuix\u002Freact\u002Fautomation'\nimport { createTestRoot } from '@gpuix\u002Freact\u002Ftesting'\nimport { ChatApp } from '.\u002Fchat'\n\nconst { render, renderer } = createTestRoot()\nrender(\u003CChatApp \u002F>)\nconst app = await connectTest(renderer)\n\nconst startedAt = await app.clock.pause()\nawait app.getByTestId('sidebar-collapse').click()\n\nawait app.captureFrames('review\u002Fsidebar', [\n  startedAt,\n  startedAt + 50,\n  startedAt + 100,\n  startedAt + 150,\n  startedAt + 200,\n])\n\nawait app.clock.resume()\n```\n\n## Scrolling\n\nContainers with `overflow: \"scroll\"` become natively scrollable. GPUI handles scroll physics, clipping, and offset persistence automatically.\n\nPlain scroll containers still build every child. Use `\u003Cvirtual-list>` below when the collection can grow large.\n\n> [!IMPORTANT]\n> **Nested scrolling is not supported.** One parent may scroll. An inner\n> `overflow: \"scroll\"`, `\u003Cvirtual-list>`, or `\u003Cdiff>` must not. GPUI gives both\n> hitboxes the same wheel event, so the inner list steals the gesture.\n>\n> Keep long inner content in that parent. Collapse it behind an **expandable**\n> (preview plus Show more) instead of giving the child its own viewport.\n>\n> Horizontal overflow is the exception. `overflowX: \"scroll\"` on a wide child\n> (a code row, a table) does not steal the vertical wheel. GPUIX lays that\n> scroller out as a flex viewport with `minWidth: 0`. The wide child must not\n> shrink: set `flexShrink: 0` or a definite width. Swipe on **X** to pan.\n> A vertical wheel stays on the parent.\n\n```tsx\nfunction Expandable({\n  preview,\n  children,\n}: {\n  preview: React.ReactNode\n  children: React.ReactNode\n}) {\n  const [open, setOpen] = useState(false)\n  return (\n    \u003Cdiv style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>\n      {open ? children : preview}\n      {!open && \u003Cdiv onClick={() => setOpen(true)}>Show more\u003C\u002Fdiv>}\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n```tsx\nfunction ScrollableList() {\n  return (\n    \u003Cdiv style={{ height: 300, overflow: 'scroll' }}>\n      {items.map((item, i) => (\n        \u003Cdiv key={i} style={{ height: 60, padding: 12 }}>\n          {item.name}\n        \u003C\u002Fdiv>\n      ))}\n    \u003C\u002Fdiv>\n  )\n}\n```\n\nPer-axis scrolling: use `overflowX: \"scroll\"` or `overflowY: \"scroll\"`.\n`overflow: \"scroll\"` scrolls both axes at once from a single diagonal gesture,\nlike a browser.\n\nA flex column stretches its children to the cross axis, so a two-axis container\nneeds its rows to state a width. Without one there is nothing to pan on **X**:\n\n```tsx\n\u003Cdiv style={{ width: 260, height: 220, overflow: 'scroll', display: 'flex', flexDirection: 'column' }}>\n  {rows.map((row) => (\n    \u003Cdiv key={row.id} style={{ display: 'flex', width: 810, flexShrink: 0 }}>\n      {row.cells}\n    \u003C\u002Fdiv>\n  ))}\n\u003C\u002Fdiv>\n```\n\n### Panes that must move together\n\nA native scroll container cannot drive a **frozen header**. GPUI moves the\ncontainer on the wheel frame, and the JavaScript callback that would move the\nheader arrives a frame later, so the header tears away during a fast pan.\n\nWhen two panes must stay locked to the pixel, own the offset in React: put one\n`onScroll` listener on a non-scrolling parent, keep `scrollX` and `scrollY` in\nstate, and translate each pane's content with an absolutely positioned wrapper.\nZed does the same; the editor owns its scroll position and paints the gutter and\nthe text from it.\n\n```tsx\nfunction Pane({ offsetX, children }: { offsetX: number; children: React.ReactNode }) {\n  return (\n    \u003Cdiv style={{ flexGrow: 1, minWidth: 0, overflow: 'hidden', position: 'relative' }}>\n      {\u002F* An empty positioned box still takes hits, so opt it out. *\u002F}\n      \u003Cdiv style={{ position: 'absolute', left: -offsetX, top: 0, pointerEvents: 'none' }}>\n        {children}\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  )\n}\n```\n\nKeep the moving subtree in a `memo` component whose props do not change during a\npan. The wheel then costs a handful of style mutations, not one per row. The\n[timeline example](.\u002Fexamples\u002Ftimeline.tsx) does this for a ruler, a track\ncolumn, and a clip grid.\n\nFor programmatic scroll control, use a React ref to get the element's numeric ID, then call the renderer's scroll methods:\n\n```tsx\nfunction ProgrammaticScroll() {\n  const listRef = useRef\u003Cany>(null)\n\n  const jumpToBottom = () => {\n    if (listRef.current) {\n      renderer.scrollTo(listRef.current.id, 0, -999)\n    }\n  }\n\n  return (\n    \u003C>\n      \u003Cdiv ref={listRef} style={{ height: 200, overflow: 'scroll' }}>\n        {items.map((item, i) => \u003Cdiv key={i}>{item}\u003C\u002Fdiv>)}\n      \u003C\u002Fdiv>\n      \u003Cdiv onClick={jumpToBottom}>Jump to bottom\u003C\u002Fdiv>\n    \u003C\u002F>\n  )\n}\n\n\u002F\u002F Available scroll methods on the renderer:\nrenderer.scrollTo(elementId, x, y)        \u002F\u002F set offset directly\nrenderer.scrollToItem(elementId, index)   \u002F\u002F scroll child into view\nrenderer.getScrollOffset(elementId)       \u002F\u002F returns [x, y] or null\n```\n\n## Virtual lists\n\nUse `\u003Cvirtual-list>` for **long, variable-height collections** such as message lists. React and Rust retain every row, but GPUI only builds, lays out, and paints rows near the viewport.\n\n```tsx\nfunction MessageList({ messages }: { messages: Message[] }) {\n  return (\n    \u003Cvirtual-list\n      alignment=\"bottom\"\n      followTail\n      estimatedItemHeight={180}\n      style={{ flexGrow: 1, minHeight: 0 }}\n    >\n      {messages.map((message) => (\n        \u003CMessage key={message.id} message={message} \u002F>\n      ))}\n    \u003C\u002Fvirtual-list>\n  )\n}\n```\n\nThe list needs a **bounded height** or bounded flex space. Its direct children are rows and can contain any GPUIX host or custom element.\n\n| Prop | Default | Purpose |\n|---|---:|---|\n| `alignment` | `\"top\"` | Use `\"bottom\"` for chat-style initial positioning |\n| `followTail` | `false` | Follow appended rows until the user scrolls away |\n| `overdraw` | `512` | Extra pixels built outside the viewport |\n| `estimatedItemHeight` | none | Height hint for unmeasured rows. **Required** with `itemCount` |\n\n### How virtualization works\n\n**React reconciliation stays normal.** The complete keyed child list crosses the mutation protocol and remains in Rust's retained tree. GPUIX defers only the expensive GPUI element construction, layout, and paint work.\n\n```text\nReact Fiber + Rust RetainedTree    all row IDs, props, text, and events\n                 │\n                 ▼\n          GPUI ListState          row count and measured height cache\n                 │\n                 ▼ visible indexes plus overdraw\n          cx.processor            re-enters GpuixView after root render\n                 │\n                 ▼\n          fresh BuildCtx          builds only the requested React subtree\n                 │\n                 ▼\n       GPUI layout and paint      visible rows only\n```\n\n### Row heights\n\n**Rows do not need equal heights, and you do not need to know them.** GPUI measures a row when it enters the viewport. `estimatedItemHeight` is a **hint for rows nothing has measured yet**, not a size contract.\n\n```text\nindex:     0        1        2        3        4        5        6        7\n       ┌────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┐\n       │  hint  │  hint  │measured│measured│measured│  hint  │  hint  │  hint  │\n       │  220px │  220px │  184px │  512px │   96px │  220px │  220px │  220px │\n       └────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┘\n           ▲                          ▲                          ▲\n           │                          │                          │\n     estimate only         real, variable heights          estimate only\n                          (viewport plus overdraw)\n```\n\nThe sum of that height cache is the scroll length, so a rough estimate only affects **scrollbar accuracy** before a row is visited. The measured height replaces the estimate automatically, and the scrollbar converges as you scroll.\n\nWhen a retained descendant changes, GPUIX marks its direct row for remeasurement, so a streaming row grows correctly. Appending, removing, or reordering keyed rows keeps measurements for rows whose IDs did not change.\n\n`estimatedItemHeight` is optional in children mode, where every row exists and can be measured. It is **required** with `itemCount`, because React never mounts the rows outside the window and native has no element to measure. Those indexes render as an empty box of the estimated height until React mounts the real row.\n\n### Row boundaries\n\nEach **direct host child** is one virtual row. Give every row a stable React key and one host root:\n\n```tsx\n\u003Cvirtual-list style={{ height: 500 }}>\n  {messages.map((message) => (\n    \u003Cdiv key={message.id} style={{ paddingBottom: 24 }}>\n      \u003CMessage message={message} \u002F>\n    \u003C\u002Fdiv>\n  ))}\n\u003C\u002Fvirtual-list>\n```\n\nA row can contain nested `\u003Cdiv>`, `\u003Ctext>`, `\u003Cmarkdown>`, `\u003Ccode>`, `\u003Cdiff>`, `\u003Cinput>`, and `\u003Ctextarea>` elements. Focusable rows stay active when they move offscreen, so keyboard input and native editor state are preserved. Those children must not scroll. Nested scrolling is not supported; see [Scrolling](#scrolling).\n\n### Chat tail behavior\n\nCombine `alignment=\"bottom\"` and `followTail` for a chat thread:\n\n```tsx\n\u003Cvirtual-list\n  alignment=\"bottom\"\n  followTail\n  estimatedItemHeight={220}\n  style={{ flexGrow: 1, minHeight: 0 }}\n>\n  {turns.map((turn) => (\n    \u003CChatTurn key={turn.id} turn={turn} \u002F>\n  ))}\n\u003C\u002Fvirtual-list>\n```\n\nThe list follows new rows while the user is at the bottom. Scrolling upward pauses tail following. Returning to the bottom enables it again. A streaming final row is remeasured as its content grows.\n\n### Scroll anchoring\n\nThe list is anchored on a **row index**, not on a pixel offset. In children mode React reconciles by key, so that index still lands on the same row after a prepend: the rows already on screen stay exactly where they are. A browser does the same, and calls it scroll anchoring.\n\nOne exception, also copied from the browser: a top-aligned list that is scrolled to the **very top** stays at the top, so a prepended row is visible.\n\n```text\nscrolled down                          pinned to the top\n┌──────────────────┐                   ┌──────────────────┐\n│ new row  (above) │  ◄── inserted     │ new row          │  ◄── inserted, visible\n├──────────────────┤                   ├──────────────────┤\n│ ░░ viewport ░░░░ │  stays put        │ ░░ viewport ░░░░ │  follows the insert\n│ ░░░░░░░░░░░░░░░░ │                   │ ░░░░░░░░░░░░░░░░ │\n└──────────────────┘                   └──────────────────┘\n```\n\nThat is what a todo list or a feed wants: `setItems((current) => [fresh, ...current])` puts the new row on screen. A history pane that loads older pages while the user reads should use `alignment=\"bottom\"` instead, so a page load never moves the text.\n\n**With `itemCount`, the app owns the correction.** There is no key to reconcile against, so the index is all there is. Prepending shifts every row down one slot, and the anchor keeps pointing at the old number, so the content slides by exactly the number of rows you inserted. Move `windowStart` by the same amount:\n\n```tsx\nconst prepend = (fresh: Row) => {\n  setRows((current) => [fresh, ...current])\n  \u002F\u002F The anchor is an index. One new row above the window means every existing\n  \u002F\u002F row moved down one, so the window has to move with it.\n  setWindowStart((start) => (start === 0 ? 0 : start + 1))\n}\n```\n\nLeave `windowStart` at `0` alone; the list is pinned to the top there and the new row should be visible.\n\n### Programmatic scrolling\n\nUse a ref to call the same renderer scroll methods as a plain scroll container:\n\n```tsx\nfunction Results({ rows }: { rows: Result[] }) {\n  const renderer = useGpuixRequired()\n  const listRef = useRef\u003C{ id: number } | null>(null)\n\n  const reveal = (index: number) => {\n    if (listRef.current) {\n      renderer.scrollToItem?.(listRef.current.id, index)\n    }\n  }\n\n  return (\n    \u003C>\n      \u003Cvirtual-list ref={listRef} style={{ height: 400 }}>\n        {rows.map((row) => (\n          \u003CResultRow key={row.id} row={row} \u002F>\n        ))}\n      \u003C\u002Fvirtual-list>\n      \u003Cdiv onClick={() => reveal(rows.length - 1)}>Reveal latest\u003C\u002Fdiv>\n    \u003C\u002F>\n  )\n}\n```\n\n`scrollTo`, `scrollToItem`, and `getScrollOffset` all support virtual lists.\n\nOn a virtual list, `scrollToItem` takes an optional **pixel offset** and the\nlist reports its logical anchor:\n\n```tsx\nrenderer.scrollToItem(listId, index, offsetInItem)  \u002F\u002F offset in px, may be negative\nrenderer.getListScrollTop(listId)  \u002F\u002F [itemIndex, offsetInItemPx, viewportHeightPx] or null\n```\n\nA **negative offset anchors the viewport top above the row**, and the next\nlayout resolves it against real measured heights. That is the tool for\ninfinite-scroll history: while the reader waits in a loading row, read\n`getListScrollTop`, commit the fetched page, then re-anchor on the message\nthat was under the loading row with a negative offset. The message stays at\nthe same pixel while the new rows are measured above it —\n`examples\u002Finfinite-chat.tsx` is the worked example.\n\nAn `itemIndex` equal to the item count is gpui's **at-end sentinel**: a\nbottom-aligned list resting at its very end. A reader waiting at a trailing\nloading row usually sits there, and the viewport height in the same tuple is\nwhat converts that into a position relative to the trailing rows\n(`EDGE_HEIGHT - viewportHeight` in the example).\n\nVirtual-list `scrollToItem` calls are applied on the **next render, after\nthat frame's child splice**, so an index computed against a just-committed\nchild list is never shifted twice.\n\n### Performance model\n\n| Work | Plain scroll container | `\u003Cvirtual-list>` children | `\u003Cvirtual-list>` + `itemCount` |\n|---|---|---|---|\n| React Fiber nodes | All rows | All rows | Visible window |\n| Rust retained nodes | All rows | All rows | Visible window |\n| GPUI row construction | All rows | Visible rows plus overdraw | Visible rows plus overdraw |\n| Layout and paint | All rows | Visible rows plus overdraw | Visible rows plus overdraw |\n| Height metadata | None | One lightweight entry per row | One lightweight entry per logical row |\n\nThe children form still creates every React child, so a 10,000-row `turns.map` is slow to mount. Pass `itemCount` and `windowStart` and render only that slice to mount a window too. Collections with millions of rows still need application-level paging or a data-owning native element.\n\n### Keep scroll fast\n\nA wheel event notifies the window view. GPUI then rebuilds the **visible**\nrows and Taffy lays them out again. Draw time is the cost of those rows, not\nthe length of the list.\n\nPut a long list on `\u003Cvirtual-list>`. Keep `overdraw` near one extra\nviewport. Put fat content in one native node (`\u003Cmarkdown>`, `\u003Ccode>`, `\u003Cdiff>`),\nnot a tree of React spans.\n\nThe host `\u003Cvirtual-list>` still retains every React child. Pass `itemCount`,\n`estimatedItemHeight` and `windowStart`, then render only that window, so mount\ndoes not create every row. Native ignores `itemCount` when the estimate is\nmissing, so a jump cannot collapse unmounted rows to height 0.\n\nThere is **no `VirtualList` wrapper component**. The window is app state:\nonly the app knows when it must widen, for example when a filter grows\n`itemCount` without any scroll. Keep `start` in `useState`, move it from\n`onVisibleRange`, and slice around it.\n\n```tsx\nconst WINDOW = 40\n\nconst Transcript = memo(function Transcript({ turns }: { turns: Turn[] }) {\n  const [start, setStart] = useState(0)\n  const end = Math.min(turns.length, start + WINDOW)\n  return (\n    \u003Cvirtual-list\n      itemCount={turns.length}\n      windowStart={start}\n      estimatedItemHeight={220}\n      style={{ flexGrow: 1, minHeight: 0 }}\n      onVisibleRange={(event) =>\n        setStart(Math.max(0, Math.floor(event.startIndex ?? 0) - WINDOW \u002F 4))\n      }\n    >\n      {turns.slice(start, end).map((turn) => (\n        \u003CChatTurn key={turn.id} turn={turn} \u002F>\n      ))}\n    \u003C\u002Fvirtual-list>\n  )\n})\n\nfunction ChatApp() {\n  const [collapsed, setCollapsed] = useState(false)\n  const [turns, setTurns] = useState(initialTurns)\n  return (\n    \u003Cdiv style={{ display: 'flex', flexDirection: 'row', height: '100%' }}>\n      \u003CSidebar collapsed={collapsed} onCollapse={() => setCollapsed(true)} \u002F>\n      \u003CTranscript turns={turns} \u002F>\n      \u003CComposer onSend={(text) => setTurns((current) => [...current, { text }])} \u002F>\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n`turns` is a new array only when a message arrives. Sidebar and draft updates\nleave that reference alone, so `memo` skips the map. The chat example uses\nthis pattern.\n\n`overflowX: \"scroll\"` on a wide child must not steal the vertical wheel.\nGPUIX sets `restrict_scroll_to_axis` on that path. Native\n`overflow_x_scroll()` must call the same method.\n\nTurn on `debugFrameOverlay: 'full'` while you scroll. The overlay is **draw\ntime**. `8.3 MS` is about 120 Hz.\n\n### Pannable surfaces must cull\n\n`\u003Cvirtual-list>` is the only thing that virtualizes. A surface where **you** own\nthe offset — a timeline, a node graph, a map — places its children absolutely,\nso GPUI builds and lays out **every** retained child on every frame. Nothing\nskips them for you.\n\n`memo` and culling fix different halves, and only one of them is the draw:\n\n```\nmemo(Layer)  ►  cuts React work and the applyBatch mutations\ncull in JS   ►  cuts GPUI build, Taffy layout, and paint\n```\n\nYou already know the offset, so the visible window is a `useMemo` away:\n\n```tsx\nconst visible = useMemo(() => {\n  const from = scrollX \u002F pxPerSecond\n  const to = (scrollX + viewportWidth) \u002F pxPerSecond\n  return clips.filter((clip) => clip.start \u003C= to && clip.start + clip.duration >= from)\n}, [clips, scrollX, pxPerSecond, viewportWidth])\n```\n\nThe timeline example measures both, on 3,259 clips across 26 tracks:\n\n| Wheel pan, one full frame | p50 |\n|---|---|\n| Culled | **7.7 ms** |\n| `memo` only, no culling | **92 ms** |\n\n> [!IMPORTANT]\n> A perf sample must include `renderer.flush()`. Without it you time the React\n> update and none of the GPUI build, layout, and paint that follows. The\n> `memo`-only number above looks like **0.6 ms** if you forget.\n\n## Text input\n\n`\u003Cinput>` and `\u003Ctextarea>` use GPUI's platform input handler. They support a\nnative caret, text selection, IME composition, clipboard actions, undo\u002Fredo,\ngrapheme-safe deletion and mouse positioning.\n\n```tsx\n\u003Ctextarea\n  value={draft}\n  placeholder=\"Ask anything\"\n  minRows={1}\n  maxRows={8}\n  onChange={(event) => setDraft(event.value ?? '')}\n\u002F>\n\n\u003Ctextarea\n  value={draft}\n  onChange={(event) => setDraft(event.value ?? '')}\n  onSubmit={send}\n\u002F>\n```\n\n`Enter` inserts a newline in a `\u003Ctextarea>`. Pass **`onSubmit`** to emit that\nevent on Enter instead; `Shift+Enter` still inserts a newline. An `\u003Cinput>`\nalways emits `onSubmit` on Enter. The editor updates natively first, then\nreports the complete value to React.\n`value` changes can replace the native content, but keeping the same prop value\ndoes not reject an edit like a browser-controlled input.\n\nThe focused caret stays solid during edits and then blinks every 500ms while\nidle. It stops scheduling repaint frames on blur or while the window is\ninactive. Override its colour through the shared native theme:\n\n```tsx\n\u003Cinput theme={{ caret: '#22c55e' }} \u002F>\n```\n\n### Input in a search pill\n\n`\u003Cinput>` has **no default inner padding** and paints text at the top of its\nbox. A single-line input vertically centers its text when given extra height.\nSet `padding` on the input style or on a parent wrapper. When the input has\n`borderRadius`, text clips to the rounded shape automatically.\n\n```tsx\n\u003Cdiv style={{\n  display: 'flex',\n  flexDirection: 'row',\n  alignItems: 'center',\n  height: 32,\n  paddingLeft: 10,\n  paddingRight: 4,\n  borderRadius: 16,\n  backgroundColor: '#1a1a22',\n  borderWidth: 1,\n  borderColor: '#ffffff14',\n}}>\n  \u003Cinput\n    value={query}\n    onChange={(e) => setQuery(e.value ?? '')}\n    style={{ flexGrow: 1, minWidth: 0, fontSize: 13, color: '#e8e8ed' }}\n  \u002F>\n\u003C\u002Fdiv>\n```\n\n## Accessibility\n\nGPUI talks to the **macOS AX tree**, Windows UIA, and Linux AT-SPI through\nAccessKit. GPUIX maps React props onto that API. A node is in the tree only\nwhen it has **both** a GPUI id (always set) and a **role**.\n\nProp names match React DOM. Role **values** are ARIA tokens, not AccessKit\nPascalCase. `\"none\"` and `\"presentation\"` produce no node.\n\n```tsx\n\u003Cdiv\n  role=\"button\"\n  aria-label=\"Delete note\"\n  aria-description=\"Removes this note\"\n  aria-id=\"notes.delete\"\n  onClick={remove}\n>\n  Delete\n\u003C\u002Fdiv>\n```\n\n| Prop               | GPUI \u002F AccessKit                          |\n| ------------------ | ----------------------------------------- |\n| `role`             | `.role(Role::…)`                          |\n| `aria-label`       | accessible name                           |\n| `aria-description` | extra description after name, role, value |\n| `aria-id`          | `AXIdentifier` \u002F UIA AutomationId         |\n| `aria-expanded`    | expanded state                            |\n| `aria-selected`    | selected state                            |\n| `aria-valuetext`   | string value                              |\n| `aria-level`       | heading level                             |\n\nNative defaults, so common elements are not silent:\n\n| Element       | Default role            | Name \u002F value                         |\n| ------------- | ----------------------- | ------------------------------------ |\n| `\u003Ctext>`      | `Label`                 | content as `aria-valuetext`          |\n| `\u003Cinput>`     | `TextInput`             | `value` and `placeholder`            |\n| `\u003Ctextarea>`  | `MultilineTextInput`    | `value` and `placeholder`            |\n| `\u003Cimg>`       | `Image`                 | `alt` as `aria-label`                |\n\nAn explicit `role` wins over those defaults. A clickable `div` is **not** a\nbutton until you set `role=\"button\"`. `onClick` registers AccessKit `Click`,\nso VoiceOver Press fires the same JS `click` handler.\n\nThe browser \u002F wasm renderer has no AccessKit adapter. These props are\nno-ops there.\n\n## Focus and keyboard navigation\n\nFocus is a **native GPUI concept**. GPUIX connects stable React element IDs to\npersistent `gpui::FocusHandle` values, so focus survives React rerenders:\n\n```text\nReact \u003Cdiv tabIndex={0}>\n            │\n            ▼\nRetained element ID ► persistent gpui::FocusHandle ► keyboard\u002Faction dispatch\n            ▲\n            │\n      React rerenders\n```\n\nInputs and textareas are tab stops automatically. Add `tabIndex` to a `div` when\nit should participate in explicit focus traversal:\n\n```tsx\n\u003Cdiv\n  tabIndex={0}\n  onFocus={() => setActive(true)}\n  onBlur={() => setActive(false)}\n  onKeyDown={(event) => {\n    if (event.key === 'enter') submit()\n  }}\n>\n  Submit\n\u003C\u002Fdiv>\n```\n\n| Prop | Behavior |\n|---|---|\n| `tabIndex={0}` | Joins the normal focus traversal order |\n| `tabIndex={n}` | Uses `n` as its GPUI tab-order index |\n| `tabIndex={-1}` | Skipped by focus traversal, but focusable by click or renderer API |\n| `autoFocus` | Takes focus once, when its native focus handle is created |\n\n### Element keyboard callbacks\n\n`onKeyDown` fires for the focused element and then for ancestors that declare\n`onKeyDown`, following GPUI's focus dispatch path. `onKeyUp` follows the same\npath when the key is released. Adding either callback creates the element's\nnative focus handle.\n\n```tsx\n\u003Cdiv\n  autoFocus\n  tabIndex={0}\n  onKeyDown={(event) => {\n    console.log(event.key, event.keyChar, event.modifiers, event.isHeld)\n  }}\n  onKeyUp={(event) => {\n    console.log(`${event.key} released`)\n  }}\n>\n  Focused target\n\u003C\u002Fdiv>\n```\n\nGPUI dispatches matching key actions before raw keyboard callbacks. If an\naction consumes the key, `onKeyDown` does not fire. GPUIX does not bind `Tab` or\n`Shift+Tab`, so both reach element callbacks. Editors and terminals can send\nthem directly to their input backend.\n\n### Renderer keyboard callbacks\n\nPass `onKeyDown` or `onKeyUp` to `render()` for an opt-in window-level listener.\nThe renderer callback fires after element callbacks for raw keys that no GPUI\naction consumed. It receives the renderer as its second argument:\n\n```tsx\nrender(\u003CApp \u002F>, {\n  onKeyDown(event, renderer) {\n    if (event.key !== 'tab') return\n    if (event.modifiers?.shift) renderer.focusPrevious?.()\n    else renderer.focusNext?.()\n  },\n})\n```\n\nThese callbacks observe native events. They do not expose GPUI's propagation\ncontrol, so they cannot cancel or stop the native event.\n\n### Imperative focus\n\n`focusNext()` and `focusPrevious()` map directly to GPUI's\n`window.focus_next()` and `window.focus_prev()`.\n\nUse a ref for imperative focus:\n\n```tsx\nconst buttonRef = useRef\u003C{ id: number }>(null)\n\nfunction focusButton() {\n  if (buttonRef.current) renderer.focusElement(buttonRef.current.id)\n}\n\n\u003Cdiv ref={buttonRef} tabIndex={-1}>Focused on demand\u003C\u002Fdiv>\n```\n\nAdding `onKeyDown`, `onKeyUp`, `onFocus`, or `onBlur` creates a persistent focus\nhandle. Add `tabIndex` as well when the element must be reachable through focus\ntraversal. Removing `tabIndex` removes the element from that order.\n\n## Headless controls\n\nThe built-in controls are **unstyled primitives**, not a fixed component\nlibrary. Use them like Radix primitives in shadcn: import a primitive namespace,\nwrap and style it in a local file, then import those local components throughout\nthe app.\n\n```text\n@gpuix\u002Freact\u002Fselect ► components\u002Fui\u002Fselect.tsx ► application screens\n  native behavior       local styles\u002Fvariants       product-specific use\n```\n\nEach primitive has a dedicated namespace entry point:\n\n| Import | Main parts |\n|---|---|\n| `@gpuix\u002Freact\u002Fselect` | `Root`, `Trigger`, `Value`, `Content`, `Item` |\n| `@gpuix\u002Freact\u002Fcombobox` | `Root`, `Input`, `Content`, `List`, `Item`, `Empty` |\n| `@gpuix\u002Freact\u002Ftooltip` | `Provider`, `Root`, `Trigger`, `Content` |\n\n### Build a local Select\n\nCreate `components\u002Fui\u002Fselect.tsx`. This file is application code, so it can be\ncopied and changed without waiting for GPUIX to add a theme option:\n\n```tsx\nimport * as React from 'react'\nimport * as SelectPrimitive from '@gpuix\u002Freact\u002Fselect'\n\nexport const Select = SelectPrimitive.Root\nexport const SelectValue = SelectPrimitive.Value\nexport const SelectGroup = SelectPrimitive.Group\n\nexport const SelectTrigger = React.forwardRef\u003C\n  React.ElementRef\u003Ctypeof SelectPrimitive.Trigger>,\n  SelectPrimitive.SelectTriggerProps\n>(({ style, ...props }, ref) => (\n  \u003CSelectPrimitive.Trigger\n    ref={ref}\n    {...props}\n    style={(state) => ({\n      width: 220,\n      height: 36,\n      padding: 8,\n      backgroundColor: state.open ? '#334155' : '#1e293b',\n      borderRadius: 8,\n      ...(typeof style === 'function' ? style(state) : style),\n    })}\n  \u002F>\n))\n\nexport const SelectContent = React.forwardRef\u003C\n  React.ElementRef\u003Ctypeof SelectPrimitive.Content>,\n  SelectPrimitive.SelectContentProps\n>(({ style, ...props }, ref) => (\n  \u003CSelectPrimitive.Content\n    ref={ref}\n    sideOffset={6}\n    {...props}\n    style={{\n      width: 220,\n      maxHeight: 240,\n      overflowY: 'scroll',\n      padding: 4,\n      backgroundColor: '#0f172a',\n      borderRadius: 8,\n      ...style,\n    }}\n  \u002F>\n))\n\nexport const SelectItem = React.forwardRef\u003C\n  React.ElementRef\u003Ctypeof SelectPrimitive.Item>,\n  SelectPrimitive.SelectItemProps\n>(({ style, ...props }, ref) => (\n  \u003CSelectPrimitive.Item\n    ref={ref}\n    {...props}\n    style={(state) => ({\n      padding: 8,\n      opacity: state.disabled ? 0.4 : 1,\n      backgroundColor: state.highlighted\n        ? '#334155'\n        : state.selected\n          ? '#1e3a5f'\n          : '#0f172a',\n      ...(typeof style === 'function' ? style(state) : style),\n    })}\n  \u002F>\n))\n```\n\nUse the styled local file with the familiar shadcn shape:\n\n```tsx\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '.\u002Fcomponents\u002Fui\u002Fselect'\n\n\u003CSelect value={model} onValueChange={setModel}>\n  \u003CSelectTrigger>\n    \u003CSelectValue placeholder=\"Select a model\" \u002F>\n  \u003C\u002FSelectTrigger>\n  \u003CSelectContent>\n    \u003CSelectGroup>\n      \u003CSelectItem value=\"sonnet\">Sonnet\u003C\u002FSelectItem>\n      \u003CSelectItem value=\"opus\">Opus\u003C\u002FSelectItem>\n    \u003C\u002FSelectGroup>\n  \u003C\u002FSelectContent>\n\u003C\u002FSelect>\n```\n\nThe trigger participates in normal tab navigation. Opening the Select focuses\nits content. `Up`, `Down`, `Ctrl+P`, `Ctrl+N`, `Enter`, and `Escape` control the\nmenu. Closing it restores focus to the trigger. Disabled items are skipped.\n\n### Style Combobox and Tooltip the same way\n\nStart their local files from namespace imports too:\n\n```tsx\n\u002F\u002F components\u002Fui\u002Fcombobox.tsx\nimport * as ComboboxPrimitive from '@gpuix\u002Freact\u002Fcombobox'\n\n\u002F\u002F components\u002Fui\u002Ftooltip.tsx\nimport * as TooltipPrimitive from '@gpuix\u002Freact\u002Ftooltip'\n```\n\nThe application still uses compound components, not one large configuration\nobject:\n\n```tsx\n\u003CComboboxPrimitive.Root items={['Next.js', 'SvelteKit', 'Astro']}>\n  \u003CComboboxPrimitive.Input style={{ width: 220, height: 36, padding: 8 }} \u002F>\n  \u003CComboboxPrimitive.Content style={{ width: 220 }}>\n    \u003CComboboxPrimitive.Empty>No frameworks found.\u003C\u002FComboboxPrimitive.Empty>\n    \u003CComboboxPrimitive.List>\n      {(item) => (\n        \u003CComboboxPrimitive.Item key={item} value={item}>\n          {item}\n        \u003C\u002FComboboxPrimitive.Item>\n      )}\n    \u003C\u002FComboboxPrimitive.List>\n  \u003C\u002FComboboxPrimitive.Content>\n\u003C\u002FComboboxPrimitive.Root>\n```\n\n```tsx\n\u003CTooltipPrimitive.Provider delayDuration={350}>\n  \u003CTooltipPrimitive.Root>\n    \u003CTooltipPrimitive.Trigger asChild>\n      \u003Cdiv tabIndex={0} style={{ padding: 8 }}>Copy\u003C\u002Fdiv>\n    \u003C\u002FTooltipPrimitive.Trigger>\n    \u003CTooltipPrimitive.Content side=\"top\" sideOffset={6}>\n      Copy message\n    \u003C\u002FTooltipPrimitive.Content>\n  \u003C\u002FTooltipPrimitive.Root>\n\u003C\u002FTooltipPrimitive.Provider>\n```\n\nCombobox uses the native input for text editing, IME, clipboard, and focus.\nTooltip `asChild` preserves the child ref and merges trigger behavior into that\nhost element. All floating content uses GPUI's deferred `anchored()` layer,\nsnaps inside the window, and occludes controls behind it.\n\n### Overlay menus\n\nMenus, tooltips, and dialogs must use **`SelectContent`**, **`ComboboxContent`**,\nor `\u003Canchored deferred>`. Those paint in a later pass, on top of\n`\u003Cvirtual-list>` and the rest of the page.\n\nA `position: \"absolute\"` card that overflows out of the composer sits **under**\nthe virtual list. The list paints after the composer, so you still see the\nmarkdown through the menu, and clicks hit the text behind it.\n\n```tsx\n\u003CSelect value={model} onValueChange={setModel}>\n  \u003Cdiv style={{ position: 'relative' }}>\n    \u003CSelectTrigger>\n      \u003CSelectValue \u002F>\n    \u003C\u002FSelectTrigger>\n    \u003CSelectContent side=\"top\" sideOffset={4} style={{ backgroundColor: '#232323' }}>\n      \u003CSelectItem value=\"flash\">DeepSeek V4 Flash\u003C\u002FSelectItem>\n    \u003C\u002FSelectContent>\n  \u003C\u002Fdiv>\n\u003C\u002FSelect>\n```\n\nGive every overlay an **opaque** fill (`#232323`, not `#23232399`).\n`FloatingLayer` defaults to `#1A1A1A`. Item rows should use the same solid\ncolor, or a solid hover color. A `#00000000` child on a blurred window punches\nthrough Metal to the desktop.\n\nA `div` that paints a fill, or that is positioned, blocks clicks and hovers\nbehind it. The **wheel still passes**, so a pannable canvas can place its items\nabsolutely and keep panning.\n\nSet **`pointerEvents: \"auto\"`** on an element that must swallow the wheel too,\nlike a modal backdrop. `\u003Canchored>` occludes by default and has its own\n`occlude` prop, so menus and tooltips need neither.\n\n> [!IMPORTANT]\n> The wheel does not bubble the way DOM events do. GPUI hit-tests one flat list\n> of painted boxes, so the wheel reaches **any** scroller behind the element,\n> not only an ancestor. An absolute card floating over an unrelated scroll pane\n> will scroll that pane. Give a real overlay `pointerEvents: \"auto\"`.\n\n`pointerEvents: \"none\"` means the element inserts **no hitbox**, so it blocks\nnothing behind it. It does not disable the listeners on that same element, and\nit does not inherit, so children keep their own hitboxes.\n\n## Text selection\n\nEvery text GPUIX paints is **selectable and copyable**, including text inside\n`\u003Ccode>`, `\u003Cdiff>` and `\u003Cmarkdown>`. A drag that starts in a heading and ends\ninside a fenced code block selects everything between; Cmd+C copies it joined in\ndocument order.\n\nThere is nothing to opt into. To opt *out* — toolbars, buttons, line-number\ngutters — set `userSelect: \"none\"`, which inherits like the CSS property:\n\n```tsx\n\u003Cdiv style={{ userSelect: 'none' }}>\n  \u003Ctext>toolbar label, never selected\u003C\u002Ftext>\n\u003C\u002Fdiv>\n```\n\n![Text selected across markdown blocks](.\u002Fdocs\u002Fimages\u002Fselection.png)\n\nRead the selection from the renderer:\n\n```tsx\nrenderer.getSelectedText()   \u002F\u002F joined text, or null\nrenderer.clearSelection()\n```\n\nSelection works because each painted text element registers itself into a\nper-frame registry in **paint order**, which is document order. A drag anchored\nin one element resolves against that registry into per-element spans: partial in\nthe anchor and head, whole for everything between.\n\n\u003Cdetails>\n\u003Csummary>Why not one big text element, like Zed?\u003C\u002Fsummary>\n\nZed's markdown selects continuously because its whole document is a single\nelement over one text model. GPUIX renders a *tree* of text elements, so it\nrebuilds that continuity at paint time instead. The mechanism is ported from\n[Comet](https:\u002F\u002Fgithub.com\u002Fzeronsh\u002Fcomet) (MIT), which faced the same problem.\n\u003C\u002Fdetails>\n\n## Text highlighting and search\n\nThe **`highlight` prop** paints a background wash behind matched text. Put it on\nany element and it applies to that element's subtree, so the root searches the\nwindow and a container searches only that container.\n\n```tsx\n\u003Cdiv highlight={{ query: 'fox' }}>\n  \u003Ctext>the quick brown fox\u003C\u002Ftext>\n\u003C\u002Fdiv>\n```\n\nIt reaches `\u003Ctext>`, `\u003Ccode>`, `\u003Cmarkdown>` and `\u003Cdiff>` with no extra props,\nbecause every string GPUIX paints goes through the same funnel.\n\n### A find bar\n\n`useTextSearch` owns the cursor and the count. `next` and `previous` are plain\nevent handlers, so nothing here needs an effect.\n\n```tsx\nimport { useTextSearch } from '@gpuix\u002Freact'\n\nfunction Find() {\n  const [query, setQuery] = useState('')\n  const search = useTextSearch({ query })\n\n  return (\n    \u003Cdiv style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>\n      \u003Cdiv style={{ display: 'flex', gap: 8, alignItems: 'center' }}>\n        \u003Cinput value={query} onChange={(e) => setQuery(e.value ?? '')} \u002F>\n        \u003Ctext>{search.total === 0 ? 'No results' : `${search.active + 1}\u002F${search.total}`}\u003C\u002Ftext>\n        \u003Cdiv onClick={search.previous}>\u003Ctext>↑\u003C\u002Ftext>\u003C\u002Fdiv>\n        \u003Cdiv onClick={search.next}>\u003Ctext>↓\u003C\u002Ftext>\u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n\n      \u003Cdiv {...search.props} style={{ flex: 1 }}>\n        \u003CTranscript \u002F>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n### Explicit ranges\n\nWhen you already have offsets, from an LSP range or your own model, pass them\ninstead of a query. They are `[start, end)` in **UTF-16 code units**, the units\n`indexOf` and `RegExp.exec` return.\n\n```tsx\n\u003Cdiv highlight={{ ranges: [[6, 11]], color: '#f43f5e55' }}>\n  \u003Ctext>Hello {name}!\u003C\u002Ftext>\n\u003C\u002Fdiv>\n```\n\nA pair that splits a surrogate pair is **rejected**, never snapped. Ranges index\nretained text only; native elements build their strings in Rust, so use `query`\nfor those.\n\n### Options\n\n| field | meaning |\n|---|---|\n| `query` | substring to match, case-insensitive by default |\n| `caseSensitive` | exact case only |\n| `wholeWord` | neither neighbour may be alphanumeric","GPUIX 是一个为 Zed 编写的 GPUI 框架提供的 Node.js 与 React 绑定库，用于构建轻量、内存高效且 GPU 加速的原生桌面应用。它绕过 Electron 和 WebView，直接通过 Metal（macOS）、DirectX（Windows）或 Vulkan（Linux）将 React 组件渲染至 GPU；支持热重载、零配置快速启动、TypeScript 类型安全及单二进制分发。适用于需要高性能 UI、低内存占用和原生体验的跨平台桌面应用开发，如代码编辑器、邮件客户端、开发者工具等场景。",2,"trending"]