跳到主要内容

窗口与渲染器约 10 分钟

render() 与窗口选项

render() 创建原生窗口、挂载 React 并启动帧循环,以及它接受的窗口装饰与行为选项。

import React, { useState } from 'react'
import { render } from '@gpuix/react'

function App() {
  const [count, setCount] = useState(0)
  return (
    <div style={{ display: 'flex', gap: 8, padding: 16 }}>
      <div
        style={{ backgroundColor: '#3b82f6', borderRadius: 8, padding: 12, cursor: 'pointer' }}
        onClick={() => setCount(c => c + 1)}
      >
        <div style={{ color: '#ffffff' }}>Count: {count}</div>
      </div>
    </div>
  )
}

render(<App />, {
  title: 'My App',
  width: 800,
  height: 600,
  titlebarTransparent: true,
  windowBackground: 'blurred',
  trafficLightX: 16,
  trafficLightY: 17,
})

render() 会创建原生窗口、挂载 React 并启动帧循环。红色的红绿灯按钮会退出进程。需要从终端再次启动应用。

选项 取值 用途
titlebarTransparent boolean 隐藏原生标题栏,让应用把装饰元素绘制在红绿灯按钮之下
windowBackground "opaque"(默认)、"transparent"、"blurred" 窗口填充。"blurred" 即 macOS 的 vibrancy 背景
trafficLightX / trafficLightY 像素 红绿灯按钮原点。chat 示例使用的是 (16, 17)
transparent boolean 当 windowBackground 未设置时,等同于 windowBackground: "transparent"
appName string macOS Hide X 与 Quit X 条目里的名称。默认为 title
focus boolean,默认 true 设为 false 时窗口在活跃应用之后打开,类似 open -g
show boolean,默认 true 设为 false 时窗口以隐藏方式打开。调用 activateWindow() 来显示它

在保存后再次调用它,它会在同一个窗口上重新挂载组件树。

其它 render 选项

选项 用途
onKeyDown / onKeyUp 窗口级键盘监听器,见焦点与键盘导航
onSelectionChange 窗口级选区变更回调,见文本选区
debugFrameOverlay 帧时间叠加层,见调试帧叠加层
keyboardFocusDim 关闭「其余元素变暗」的默认焦点行为
tabNavigation false 关闭 Tab 的默认焦点移动

macOS 菜单栏

GPUIX 会为你安装好应用菜单栏,因此一个全新的应用就已经响应 ⌘Q、⌘H、⌥⌘H、⌘M 与 ⌘W。若没有它,NSApp.mainMenu 会是 nil,macOS 绘制出的菜单栏是空的,而这些快捷键根本不存在:AppKit 只通过菜单项来提供它们。

Apple    <executable>             Window
         ├ Services               ├ (AppKit window tiling)
         ├ Hide <appName>   ⌘H    ├ Minimize          ⌘M
         ├ Hide Others     ⌥⌘H    ├ Zoom
         ├ Show All               ├ Close Window      ⌘W
         └ Quit <appName>   ⌘Q    └ (open windows)

没有 Edit 菜单,这是有意为之。菜单的按键等效(key equivalent)会被 AppKit 在窗口接收到按键事件之前就消费掉,因此一个带有 ⌘C 的 Edit 菜单会把按键从文本选区以及 <input> 手中抢走。

用 render(),而不是 createRenderer()

在应用入口处请使用 render(),而非 createRenderer()。bun --hot 在保存时会重新运行整个文件。若用 createRenderer() 加 init(),就会再构建一个宿主。render() 是幂等的:第一次调用拥有窗口,后续调用只会重新挂载 React。

createRenderer()、createRoot() 与 startFrameLoop() 仍然公开,供测试和自定义宿主使用。当你已经持有一个 renderer 时,可以把 { renderer } 传入 render()。

一个 renderer 驱动一个 root。 一个 renderer 拥有一个窗口、一个原生 root id 和一张事件映射表,因此若该 renderer 已经有了一个已挂载的 root,createRoot() 会抛出异常。在创建另一个 root 之前,请先对第一个 root 调用 unmount();render() 已经替你做了这件事。

帧循环

在 macOS 上,startFrameLoop 会以固定频率(默认约 125fps)调用 renderer.tick()。每一帧只排空已就绪的 AppKit 事件与 Core Foundation 源,随后便返回,不会等待下一次原生唤醒。Bun 的定时器、socket、promise 以及 PTY 回调都可以在帧之间运行。传入 { frameMs } 可改变频率,并对返回的控制句柄调用 .stop() 来结束它。

在 Windows 与 Linux 上,GPUI 在一个专用的 Rust UI 线程上运行其正常的阻塞式原生事件循环。tick() 并不会驱动该循环。它只报告 UI 线程是否仍处于 Platform::run 内部。startFrameLoop 仍然会创建一个 JavaScript 定时器,以便「最后一个窗口关闭」能够返回 false、而 render() 能够 process.exit,从而与 macOS 保持一致。所有平台都使用 GPUI 原生的平台、窗口、渲染器、输入、滚动、剪贴板、键盘与 IME 实现。内嵌的 macOS 运行循环扩展来自被固定的 GPUIX fork。

运行时抛错不会冻结窗口。 帧循环会捕获来自 tick() 的错误,原生事件回调会捕获来自 React 处理器的抛错,而 render() 会安装 uncaughtException / unhandledRejection 监听器,从而让 bun 保持存活。窗口会显示堆栈,以及一个重新加载按钮,用于重新挂载上一次的 render() 树。在 bun --hot 下保存也会重新挂载。进程不会退出。