
GPUI 是一个用 Rust 编写图形界面应用的框架，由 Zed 团队开发。Zed 是编辑器，GPUI 是支撑它的界面基础：项目树、标签页、弹窗和编辑区域，都需要这套基础来处理布局、输入和屏幕绘制。[GPUI 官网](https://gpui.rs/)

使用 GPUI 时，开发者写 Rust 代码说明“界面上有什么、排在哪里、用户操作后怎样改变状态”，框架把这些描述变成可交互的窗口。它提供自己的界面元素和绘制系统，并使用 GPU 加速生成屏幕上的像素。

Go 也可以采用自己绘制界面的路线。[MyGo（egoist/mygo）](https://github.com/egoist/mygo) 同时支持系统 WebView 和 Go 编写的原生 UI。下面先解释 GPUI，再沿 MyGo 的源码看同类问题在 Go 中怎样解决。

<!--more-->

## GPUI 替应用做了哪些事

假设要写一个最简单的桌面计数器：窗口中央显示 `0`，下面有一个“加一”按钮，点击后数字变成 `1`。

业务规则只有一句“点击后加一”，但要让它成为一个可用界面，还需要创建操作系统窗口，确定数字和按钮的位置，识别鼠标是否点在按钮内，接收点击，更新文字，再把新画面显示出来。窗口缩放后，布局还要跟着变化。

GPUI 将这些界面工作组织成一套可复用的 API：

| 工作 | 应用代码表达什么 | GPUI 负责什么 |
| --- | --- | --- |
| 窗口 | 初始尺寸、根视图等参数 | 对接平台窗口和事件循环 |
| 内容与布局 | 显示数字、放置按钮、居中排列 | 构建元素树，计算位置与尺寸 |
| 输入与交互 | 点击后将计数加一 | 接收平台输入，按元素区域和事件规则分发 |
| 状态更新 | 保存当前计数，通知它已变化 | 管理实体，安排相关视图更新 |
| 绘制 | 文字、背景、边框等样式 | 组织绘制数据，交给图形后端显示 |

因此，GPUI 处在应用业务逻辑与操作系统、图形接口之间。开发一个普通按钮时，可以组合已有元素、设置样式并注册交互；需要编辑器这样的特殊控件时，也可以下探到自定义布局和绘制。[GPUI 的能力分层](https://github.com/zed-industries/zed/blob/953ac755299df93afba3669968d175a7b25c386a/crates/gpui/README.md#the-big-picture)

这里还要区分三个词：GUI 是图形用户界面这一类交互方式；GPU 是绘制图形的硬件；GPUI 是一个具体的软件框架。用 GPUI 做桌面工具，不需要把业务写成 GPU 程序，通常也不需要自己调用 Metal 等图形接口。

## 为什么 Zed 要自己做界面框架

编辑器的界面会频繁变化：输入字符会改变文字和光标，滚动会改变可见行，搜索会改变高亮区域。Zed 团队希望直接控制这些界面的更新和绘制路径，因此开发了 GPUI。它围绕界面需要的矩形、文字和图片等内容建立绘制能力，同时提供 Rust 中组织状态和处理交互的方式。[Zed 团队对 GPUI 的介绍](https://zed.dev/blog/videogame)

应用开发者可以复用这套框架，做编辑器以外的界面。这里复用的是窗口、元素、输入和绘制能力；文件管理、网络请求、数据处理等业务仍由应用实现。

## GPU 在这里做什么

GPU（Graphics Processing Unit，图形处理器）擅长并行处理图形任务。GPUI 用 GPU 绘制矩形、边框、阴影和图片等图元；文字先由平台文字系统排版，字形在 CPU 上光栅化进图集，再由 GPU 合成到窗口。

例如，编辑器需要画出一排标签页。应用先确定每个标签的文字、位置、大小、颜色和选中状态，再把绘制信息交给渲染器。GPU 根据这些信息生成屏幕上的像素。点击标签后打开哪个文件、怎样修改应用状态，仍由 Rust 代码处理。

Zed 团队在 [Zed Decoded: GPUI](https://zed.dev/blog/videogame) 中解释过这条路线：围绕界面常用的图形原语编写着色器，在 CPU 上描述它们的属性，再交给 GPU 绘制。着色器是运行在 GPU 上、计算图形位置或像素颜色的程序。普通 GPUI 应用开发者通常使用框架提供的元素，无需自己编写着色器。

因此，“GPU 加速”描述的是绘制方式。读取文件、解析数据或在界面线程中执行耗时计算，依然可能让应用卡住。也不能单凭使用 GPU 就断言 GPUI 一定比某个 Electron 应用更快；性能比较需要相同工作负载和实际测量。

## 用 Rust 描述界面是什么样子

用刚才的计数器看 GPUI 的写法。下面是视图片段，省略应用启动和窗口创建；实际创建根视图时，需要把 `Counter { value: 0 }` 注册为实体：

```rust
use gpui::{div, prelude::*, Context, Window};

struct Counter {
    value: usize,
}

impl Render for Counter {
    fn render(
        &mut self,
        _window: &mut Window,
        cx: &mut Context<Self>,
    ) -> impl IntoElement {
        div()
            .flex()
            .flex_col()
            .gap_3()
            .size_full()
            .items_center()
            .justify_center()
            .child(format!("{}", self.value))
            .child(
                div()
                    .id("increment")
                    .p_3()
                    .child("加一")
                    .on_click(cx.listener(|counter, _, _, cx| {
                        counter.value += 1;
                        cx.notify();
                    })),
            )
    }
}
```

这段代码可以分成三部分读。`Counter.value` 保存当前数字；`render` 描述一个居中的纵向容器，里面先放数字，再放可点击区域；`on_click` 指定点击后的操作。

`div()` 创建容器，`.child(...)` 加入子内容，`.flex_col()` 表示纵向排列。这种链式 API 借鉴了 CSS 和 Tailwind 的表达习惯，实际调用的是 Rust 方法，生成的是 GPUI 元素树。它不需要浏览器解析 HTML，也不等于支持完整的 CSS。[官方 Hello World 示例](https://github.com/zed-industries/zed/blob/953ac755299df93afba3669968d175a7b25c386a/crates/gpui/examples/hello_world.rs)

`cx.listener(...)` 把事件回调与当前实体连接起来，让回调可以修改计数器。`cx.notify()` 告诉框架，这个实体的状态发生了变化。代码中的可点击 `div` 只演示鼠标交互；完整按钮还需要键盘操作、焦点和无障碍语义。[listener 实现](https://github.com/zed-industries/zed/blob/953ac755299df93afba3669968d175a7b25c386a/crates/gpui/src/app/context.rs#L240)、[官方点击事件示例](https://github.com/zed-industries/zed/blob/953ac755299df93afba3669968d175a7b25c386a/crates/gpui/examples/tab_stop.rs#L120)

“原生桌面界面”也需要区分含义：GPUI 可以接入操作系统窗口和平台服务，窗口内部的元素由 GPUI 自己绘制。它不意味着每个按钮都是系统提供的按钮控件。

## 状态怎样变成屏幕上的内容

阅读 GPUI 代码时，经常会遇到这几个名字：

| 概念 | 负责什么 |
| --- | --- |
| `App` | 管理应用级状态与服务，创建窗口和实体 |
| `Entity<T>` | 指向由应用管理的 `T` 状态的句柄 |
| `Render` | 根据当前状态生成界面元素树 |
| `Context<T>` | 为当前实体提供访问应用、通知变化等能力 |

在计数器中，`Counter` 是被管理的状态，`Entity<Counter>` 是访问这份状态的句柄；它实现了 `Render`，所以也是一个可以显示的视图。`App` 持有实体，`Context<Counter>` 则让当前计数器访问框架服务。单独持有实体句柄，不等于可以绕开上下文直接修改其字段。

GPUI 这样组织状态，是为了处理 Rust 中的所有权问题。项目列表、编辑区和弹窗可能互相通信，把所有对象简单地嵌套起来，很难表达这些关系。由 `App` 管理实体、通过句柄和上下文访问它们，可以让界面各部分协作，同时保留 Rust 的借用约束。[Ownership and data flow in GPUI](https://zed.dev/blog/gpui-ownership)

回到数字从 `0` 变成 `1` 的过程。这里涉及两个先后发生的动作：事件处理修改状态，随后渲染读取新状态。它们通过 GPUI 的通知和调度连接起来：

1. 平台传来鼠标输入，GPUI 将点击分发到“加一”区域的回调。
2. 回调把 `Counter.value` 从 `0` 改成 `1`，并调用 `cx.notify()`。
3. 框架安排相关视图更新；渲染时，`render` 读取新值，生成显示 `1` 的文本元素。
4. 布局系统确定文本和可点击区域的位置，元素准备绘制内容。
5. 图形后端将内容画到窗口上，用户看到 `1`。

`notify()` 不会在这一行代码里立即把像素改掉。GPUI 的通知涉及更新队列和窗口失效处理，由框架协调后续更新。这也让“先完成状态修改，再处理其影响”有了明确顺序。[状态通知的处理方式](https://zed.dev/blog/gpui-ownership)

这里简化了缓存和调度细节。GPUI 的底层 `Element` 接口将布局请求、绘制前准备和绘制分为 `request_layout`、`prepaint`、`paint`，布局使用 Taffy。需要自定义布局或绘制行为时，可以实现自己的元素。[Element 源码](https://github.com/zed-industries/zed/blob/953ac755299df93afba3669968d175a7b25c386a/crates/gpui/src/element.rs)

## 为什么称为“即时模式与保留模式混合”

GPUI 官方将它描述为 hybrid immediate and retained mode。要理解这个说法，可以先区分两种常见的界面组织方式。

保留模式会让框架长期保存界面对象，应用修改这些对象的属性。例如持有一个文本控件，把它的内容从 `0` 改成 `1`。

即时模式通常由应用在绘制时重新执行界面描述代码，根据当前状态说明这次要显示什么。界面长什么样，直接体现在这次执行的代码中。

GPUI 的实体和视图状态可以跨帧保存；渲染时，`Render` 又根据这些状态构建元素树。底层还支持通过元素标识关联跨帧状态。这就是它混合两种方式的含义：长期存在的是应用状态，界面描述可以随着渲染重新生成。[元素生命周期说明](https://github.com/zed-industries/zed/blob/953ac755299df93afba3669968d175a7b25c386a/crates/gpui/src/element.rs#L7-L16)

对上面的计数器来说，跨帧保存的是 `Counter.value`；显示数字的文本元素由 `render` 根据当前值创建。如果把计数写成 `render` 内的 `let mut value = 0`，下次执行就会重新变成 `0`。这正是应用状态与本次界面描述需要分开的原因。

## 与 Electron、Tauri 有什么区别

三者都能用于桌面应用，但界面技术路线不同。

| 比较项 | Electron | Tauri | GPUI |
| --- | --- | --- | --- |
| 界面描述 | HTML、CSS、JavaScript 等 Web 技术 | Web 前端，可配合 Rust 核心逻辑 | Rust 元素、视图和样式 API |
| 界面渲染 | 随应用分发的 Chromium | 平台提供的 WebView | GPUI 自身的绘制系统与 GPU 后端 |
| 复用已有 Web 界面 | 可沿用 Web 前端代码 | 可沿用 Web 前端代码 | 通常需要重写为 GPUI 界面 |
| 可复用的界面能力 | Web 组件和浏览器能力 | Web 组件和 WebView 能力 | GPUI 元素及适配它的组件库 |

Electron 的结构见[官方进程模型说明](https://www.electronjs.org/docs/latest/tutorial/process-model)，Tauri 的 WebView 与 Rust 分工见[官方架构文档](https://v2.tauri.app/concept/architecture/)。

Tauri 使用 Rust 核心逻辑，并不意味着它与 GPUI 使用相同的界面方案。对已有 React 或 Vue 项目的团队，Tauri 仍可以延续 Web 前端；迁移到 GPUI，则需要重新实现界面和相应交互。

GPUI 提供的是构建界面的基础能力。复杂表单、数据表格等成品控件，可以进一步考察 [Longbridge 的 GPUI Kit](https://github.com/longbridge/gpui-kit)，并核对它与所选 GPUI 版本的兼容关系。

## MyGo 怎样实现原生界面

MyGo 是一个用 Go 编写桌面应用的框架，提供两条窗口内容路径：一条把 HTML、CSS 和 JavaScript 放进系统 WebView，另一条用 Go 的 `ui` 包直接构建原生界面。一个应用或窗口可以混合使用两类内容，本文只分析原生 UI。

| 路径 | 界面来源 | 如何显示 |
| --- | --- | --- |
| 网页 | HTML、CSS、JavaScript，通过生成的 TypeScript 客户端调用 Go | WKWebView、WebKitGTK 或 WebView2 |
| 原生 UI | Go `ui` 视图 | MyGo 构建、布局和绘制，交给 GPU 或软件光栅化 |

原生路径不依赖 HTML、JavaScript 或用于显示界面的 WebView；窗口、输入和平台操作仍由系统后端提供。这一节分析的源码固定在提交 [`ff19a6f9`](https://github.com/egoist/mygo/tree/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02)。它的原生 UI 链路由 Go 的 `ui`、`internal/scene` 和平台渲染器组成，和前面 GPUI 的 Rust 实现分别演进。相似的声明式写法，不代表两者拥有相同的状态管理或渲染实现。

### 窗口先选择 WebView 还是绘制表面

MyGo 的两条路径从窗口参数开始分开：传入 `URL` 显示网页，传入 `Content: ui.View(...)` 显示原生 UI。

入口 `NewWindow` 最终进入 `newWindow`，其中用 `opts.Content != nil` 设置平台参数 `Surface`。macOS 后端的 `createContent` 根据这个字段选择 `createSurface` 或 `createWebView`。前者提供 MyGo 自己绘制内容的表面，后者创建 WKWebView。[窗口入口](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/window.go#L419)、[macOS 分支](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/internal/darwin/window.go#L274)

```text
NewWindow
  ├─ 网页路径：系统 WebView → HTML / CSS / JavaScript
  └─ Content 路径：平台 Surface → MyGo UI 引擎 → 自己绘制
```

网页路径中，`Bind` 注册 Go 服务，生成器依据服务类型生成 TypeScript 客户端；页面通过注入的桥接脚本发送调用，Go 解码、查找方法、执行后返回结果。`handleCall` 会检查页面是否受信任，TypeScript 类型本身不能代替运行时校验。[IPC 实现](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/ipc.go#L381)、[类型生成器](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/internal/tsgen/generate.go)

原生 UI 则直接调用 Go 视图函数。`attachContent` 取得平台表面，组装 `surface.Conn`，将输入、重绘请求和窗口服务连接给 `ui.Content.AttachContent`。后者创建 `windowHost` 和 UI 引擎，并请求首帧。[连接窗口与内容](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/content.go#L29)、[UI 引擎挂接](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/ui/window.go#L76)

这两种窗口共用生命周期和桌面 API。原生界面也可以显式嵌入 WebView，因此描述应用的资源需求时，要看它实际使用了哪些内容。

### 点击发生在构建过程中，为什么文字不会慢一帧

下面从官方计数器中保留最相关的部分。`s.n` 是应用长期保存的 Go 字段，视图函数每次根据它构建界面：

```go
func (s *counter) view(c *ui.Context) {
    ui.Column(c).Fill().Center().Gap(16).Children(func() {
        ui.Textf(c, "%d", s.n).FontSize(56).Bold()
        if ui.PrimaryButton(c.Key("increment"), "+").Clicked() {
            s.n++
        }
    })
}
```

这里先创建数字文本，再查询按钮点击。如果点击时 `s.n` 从 `0` 变成 `1`，前面创建的文本仍然是 `0`，框架怎样处理？

关键在 `engine.runFrame`：它在布局和绘制之前，最多执行三次构建。`Clicked()` 发现点击后，会设置 `rt.consumed = true`；本次构建结束时，`endPass` 清掉已经处理的点击等输入，再次运行视图函数。第二次构建读到 `s.n == 1`，文本随之更新，按钮也不会重复消费同一个点击。[构建循环](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/ui/runtime.go#L418)、[点击查询](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/ui/input.go#L918)

这个实现也约束了视图函数的写法：一次显示帧可能执行多次构建，不能在函数顶层无条件发送请求或保存文件。副作用应由明确的输入动作触发，耗时工作放到后台。官方还提供构建结束后执行的 `OnClick` 等回调，不必把所有动作都写成内联查询。

GPUI 示例通过修改实体并通知变化来驱动界面；MyGo 这里则让控件在构建过程中回答输入查询，再由引擎决定是否重新构建。两者都根据状态描述界面，但更新协议不同。

### 元素可以重建，焦点和编辑状态怎样保留

MyGo 将本次构建的节点与跨帧控件状态分开保存。

私有 `context.newElement` 从节点池取得 `node`，为它计算 ID，再通过 ID 查找引擎中的 `state`。没有显式 key 时，ID 取决于父节点、子元素位置和类型；传入 `c.Key(...)` 时，使用父节点 ID 和 key 计算身份。焦点、编辑等状态由这个身份关联，而非依赖本次分配的节点地址。[节点构造与身份](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/ui/context.go#L217)

这意味着动态列表应使用稳定的业务 key。插入或重排项目后，位置会改变，而文件路径或记录 ID 仍可标识同一个条目。相同父节点下，key 还需要保持唯一。

对外返回的 `ui.Element` 则是带校验的临时句柄，由 owner、节点池槽位和 generation 组成。每次构建递增代数，使上一轮句柄失效，避免它误操作已被复用的节点。这里的生命周期甚至短于“一帧”：前面提到的同帧再次构建，也会让旧句柄过期。长期保存控件身份应使用 `Handle`，而非把 `Element` 放进应用状态。[句柄与代数校验](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/ui/frame.go#L60)

Go 的垃圾回收可以管理对象内存，但不会自动判断一个句柄是否仍然代表当前控件。MyGo 的代数校验解决的正是这种逻辑生命周期问题。

### 从节点树到 GPU，中间有一份绘制列表

构建结束后，`runFrame` 调用 `layoutTree` 计算位置与尺寸，再执行 `paint` 和 `host.present`。MyGo 的 Flexbox、Grid 布局由自己的 Go 代码实现；GPUI 则使用前面提到的 Taffy。[MyGo 布局入口](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/ui/layout.go#L69)

`paint` 并不直接对着 Metal 画按钮。它遍历元素，将填充、阴影、字形、图片和裁剪等操作写进 `scene.Scene`。这份按绘制顺序组织的数据就是 display list（绘制列表）。字形图集也挂在场景上，供渲染器使用。[绘制入口](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/ui/paint.go#L47)、[场景数据结构](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/internal/scene/scene.go)

```text
Go 视图函数
    → 节点树与跨帧状态
    → layoutTree：计算布局
    → paint：生成 scene.Scene
    → windowHost.present：选择渲染路径
    → 平台表面显示结果
```

渲染器只需理解场景里的图形操作，无需知道它们来自按钮、列表还是编辑器。当前平台入口分别连接 Metal、Direct3D 11 和 OpenGL；macOS 的 Metal 渲染器把场景操作组织为实例化四边形，使用着色器绘制。[Metal 渲染器](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/internal/gpu/metal/metal.go)

`windowHost.present` 还包含失败路径：GPU 渲染失败时释放并尝试重建渲染器；无法使用时，通过 `internal/raster` 在内存中绘制，再调用表面的 `PresentPixels` 或 `PresentDamage` 显示结果。失败后的重试带退避，避免每帧持续重建。[渲染与回退](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/ui/window.go#L237)

所以，MyGo 原生 UI 可以使用 GPU，也有软件绘制路径。“自己绘制”与“每次都在物理 GPU 上绘制”是两回事。

### 即时模式不要求空闲时不停重建界面

MyGo 的 `engine.next` 根据动画状态决定是否请求下一帧；窗口被遮挡时，可以暂停相关动画帧。没有输入、状态变化或动画需求时，不必仅为维持即时模式而连续执行视图函数。

它还区分两种更新：布局或应用状态改变时，重新构建、布局并绘制；只有自定义图形随时间变化时，`Painter.AnimationFrame()` 可以请求只重绘。后一条路径进入 `repaintFrame`，复用现有节点树，跳过视图构建和布局。[帧调度与重绘](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/ui/runtime.go#L551)

从这个实现可以看到，即时模式描述的是怎样表达和处理界面，并不要求应用永远以固定频率重新创建所有元素。

### 主线程调度与后台更新

MyGo 不使用 cgo，但仍调用原生库。macOS、Linux 通过 `purego` 动态加载库并调用函数，Windows 路径使用 `syscall` 等平台接口。以 macOS 为例，`internal/darwin/objc.go` 加载 Objective-C runtime 和 AppKit 等框架，并取得 `objc_msgSend` 入口。Metal 渲染器也通过这类机制调用系统 API。[macOS 原生互操作](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/internal/darwin/objc.go#L96)

“无 cgo”降低了构建时对 C 工具链的依赖，不表示运行时没有平台库。例如 Linux 原生 UI 仍依赖 GTK，网页路径还需要 WebKitGTK。[平台依赖说明](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/docs/README.md#requirements)

界面构建和平台操作在主线程执行。`loop.go` 用队列将其他 goroutine 的任务投递到主线程；后台加载完成后，`Window.Update(fn)` 会把状态修改和重绘安排过去。单独调用 `Invalidate()` 只请求更新，不能让任意 Go 变量的并发读写自动变得安全。[主线程队列](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/loop.go)、[状态更新入口](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/content.go#L142)

### 与 GPUI 对照后，差异在哪里

| 问题 | GPUI | MyGo 原生 UI |
| --- | --- | --- |
| 应用状态 | 通过 `App`、`Entity<T>` 和上下文组织 | 普通 Go 结构体，框架不托管实体；视图直接读取和修改 |
| 界面描述 | `Render` 返回元素树 | 视图函数构建节点并查询输入 |
| 跨帧控件状态 | 实体与元素身份等机制 | 节点 ID 对应持久 `state`，临时 `Element` 带代数校验 |
| 布局 | Taffy | Go 编写的 Flexbox、Grid 实现 |
| 平台连接 | Rust 平台后端 | 平台 Surface 加 `purego` / Windows 原生接口 |

两者共同需要处理输入、身份、布局、文字和绘制，但语言改变后，约束也会改变。GPUI 必须让共享状态协作符合 Rust 所有权规则；MyGo 则通过状态 ID、临时句柄检查和主线程调度维护界面生命周期，Go 的垃圾回收不能替它解决这些问题。

为核对上述行为，本次在固定提交下、以 `CGO_ENABLED=0` 运行了 11 个现有定向测试，覆盖同帧动作只执行一次、句柄过期、控件状态保留、只重绘和遮挡调度，以及模拟 GPU 故障后的恢复，全部通过。这些测试使用无窗口测试引擎或替身渲染器，支持相应控制逻辑的判断；真实驱动表现和跨框架性能仍需要另做实验。[构建与句柄测试](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/ui/frame_test.go)、[重绘测试](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/ui/repaint_test.go)、[GPU 故障测试](https://github.com/egoist/mygo/blob/ff19a6f9cb5fdbd6dc659abc230f4b1c1cccbf02/ui/gpu_test.go)

## 什么情况下值得考虑这些方案

如果应用以桌面交互为主，需要较多自定义绘制，而且团队愿意用 Rust 编写界面，GPUI 值得做一个原型。编辑器、终端或数据查看工具这类界面，可以从中获得对布局、输入处理和绘制路径的直接控制。

如果主要目标是把已有 Web 产品做成桌面应用，复用现有页面和组件往往比重写界面更有价值。选型时应把交付速度、组件可用性和团队熟悉程度一起考虑，不能只看渲染技术。

如果核心逻辑已经使用 Go，MyGo 提供了另一种组合：保留 Web 前端，或用 `ui` 编写自己绘制的界面，并共享 Go 桌面 API。选择原生 UI 时，同样要验证组件、平台集成和后台状态更新方式；“Go 编写”和“GPU 绘制”并不能直接保证交付成本更低。

截至 2026 年 10 月 15 日，GPUI 主分支的 README 仍注明项目处于 1.0 之前，版本之间可能出现破坏性变更。文档和代码示例也需要对应版本：官网示例使用 `Application::new()`，本次查阅的主分支示例则通过 `gpui_platform::application()` 选择平台后端。[固定版本的 README](https://github.com/zed-industries/zed/blob/953ac755299df93afba3669968d175a7b25c386a/crates/gpui/README.md)

当前 README 列出了 macOS、Linux / FreeBSD 和 Windows 的平台配置。例如，macOS 使用 Metal 绘制，Linux / FreeBSD 需要选择 Wayland 或 X11 窗口后端。具体应用仍应在目标平台验证中文输入法、字体、高 DPI、键盘导航和无障碍行为，窗口能显示只是最初一步。

准备上手时，可以先选一个真实的小界面：列表、搜索框和详情面板。让它跑通输入、状态更新和后台任务，再评估组件是否够用、平台行为是否符合预期。这个原型比单独跑通 Hello World 更能说明 GPUI 是否适合你的项目。

