electron-gpui:在 Electron 应用中运行 GPUI 窗口
Electron-gpui: Run GPUI windows inside Electron apps
electron-gpui 让开发者能在 Electron 应用内运行 Zed 的 GPU 加速 UI 框架 GPUI,用 Rust 编写视图、从 JavaScript 驱动,并支持热重载。
在你的 Electron 应用内运行 GPUI——Zed 背后的 GPU 加速 UI 框架。用 Rust 编写视图,从 JavaScript 驱动它们,并通过热重载进行编辑。
import { app } from "electron"; import { createGpui } from "electron-gpui"; import addon from "virtual:electron-gpui/addon"; // your Rust views, built by the bundler plugin app.whenReady().then(() => { const gpui = createGpui(addon); const hello = gpui.openWindow("Hello", { title: "Hello" }, { name: "Electron" }); hello.on("event", (event) => console.log(event)); // events from Rust hello.send({ name: "GPUI" }); // messages to Rust });
GPUI 窗口与你的 BrowserWindow 一起运行在 Electron 的主进程中。
快速开始
需要 Electron 30+、Node 20.19+ 和 Rust(工具链会自动安装)。Windows 构建需要 Visual Studio 的 C++ Build Tools 和 Windows SDK。Linux 构建需要 C/C++ 编译器、pkg-config,以及 Fontconfig、FreeType、XCB、XKB、Wayland、GLib 和 OpenSSL 开发包;Ubuntu 软件包参见 CI 配置。Linux 使用你当前的 X11 或 Wayland 会话。Windows/Linux ARM64 和 musl 构建不在支持的目标范围内。
1. 安装并搭建 views crate
npx electron-gpui init
此命令会检测你项目的包管理器(pnpm、npm、Yarn 或 Bun),安装 electron-gpui 和 electron-gpui-unplugin 开发依赖,并创建一个带有示例视图的 ./native。传入 --skip-install 可在不安装依赖的情况下进行搭建。
2. 将插件添加到构建主进程的打包工具中
// vite.config.js import electronGpui from "electron-gpui-unplugin/vite"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [electronGpui({ electron: true })], // `electron: true` runs the app in watch mode build: { ssr: "src/main.js", rollupOptions: { external: ["electron"], output: { format: "es" } }, }, });
插件适配器也适用于 Rollup、Rolldown/tsdown、webpack 和 esbuild。
如果 electron-vite 或 Electron Forge 已经在运行你的应用,请省略 electron: true。
对于 TypeScript,在 tsconfig.json 中将 electron-gpui-unplugin/client 添加到 compilerOptions.types:
{
"compilerOptions": {
"types": ["electron-gpui-unplugin/client"]
}
}3. 编写视图,使用 native/src/lib.rs
use electron_gpui::{RootView, WindowBridge, gpui::{prelude::*, *}, serde_json::{Value, json}}; struct Hello { bridge: WindowBridge, name: String } impl RootView for Hello { fn new(props: Value, bridge: WindowBridge, _: &mut Window, _: &mut Context<Self>) -> Self { Hello { bridge, name: props["name"].as_str().unwrap_or("world").into() } } fn on_message(&mut self, message: Value, _: &mut Window, cx: &mut Context<Self>) { if let Some(name) = message["name"].as_str() { self.name = name.into(); cx.notify(); } } } impl Render for Hello { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { div() .id("root") .size_full() .child(format!("Hello, {}!", self.name)) .on_click(cx.listener(|this, _, _, _| { this.bridge.emit(json!({ "type": "clicked" })).ok(); })) } } electron_gpui::export! { "Hello" => Hello }
4. 运行 vite build --watch,并像顶部示例那样从主进程打开视图。
examples/counter 是一个完整的应用,其中 BrowserWindow 和一个 GPUI 窗口相互通信。
热重载
在监听模式下,保存 Rust 文件会在几秒内将更改补丁到正在运行的应用中,无需重启,且视图会保留其状态。当更改无法安全地打补丁时,插件会改为重新构建并重启应用:
| 更改 | 结果 |
|---|---|
函数体、render、事件处理器、on_message、新函数 |
热补丁 |
| 类型定义/别名、布局属性、常量、函数签名、注册 | 重启 |
Crate/工作区清单、锁文件、Cargo/工具链配置、build.rs、添加或删除的文件 |
重启 |
| 你的 views crate 所依赖的其他本地 crate | 重启 |
| 需要依赖中新增内容的代码 | 重启(补丁链接失败) |
| 编译错误 | 显示在终端中;应用继续运行 |
- views crate 中的线程局部变量和
static会在每次补丁时重置。 - 补丁不会再次运行你的 crate 的静态初始化器(
actions!和#[derive(Action)]注册、inventory、ctor)。对注册声明的更改会重新构建并重启应用。 - 每个补丁都是为正在运行的应用进程构建的。该进程未应用的补丁(它崩溃或退出了)会被丢弃,绝不会应用到下一个会话。
- 如果你的打包工具配置设置了监听
include过滤器,它必须持续匹配**/electron-gpui-hot/*/rebuild-trigger,插件会触碰它以重启应用。Vite 插件会为你添加它;使用 Rollup 或 Rolldown 时,请自行添加。 - 被覆盖的窗口在再次可见时会显示更改(GPUI 只重绘可见窗口)。
- 仅限 Rollup、Rolldown 和 Vite;webpack 和 esbuild 会在每次更改时重启。使用
electronGpui({ hot: false })将其关闭。
热补丁使用 Dioxus 团队的 Subsecond。
API
JavaScript(electron-gpui)
createGpui(addon, { app?, panicLog? }) |
在 app.whenReady() 之后启动 GPUI。addon 是 virtual:electron-gpui/addon,或指向已构建插件的绝对路径或 file: URL。返回一个单例,并在 before-quit 时关闭(可通过 app: null 选择退出)。panicLog:参见 须知。 |
gpui.openWindow(view, options?, props?) |
为已注册的视图打开一个窗口。options:title、width、height、x、y、minWidth、minHeight、resizable、focus、titleBarStyle("hidden" 将内容置于透明标题栏下)、trafficLightPosition、background("opaque"、"transparent" 或 "blurred")、alwaysOnTop。props 会传给 RootView::new。 |
window.setAlwaysOnTop(flag, relativeLevel?) |
让窗口保持在普通窗口之上,就像 BrowserWindow 的 setAlwaysOnTop(flag, "floating", relativeLevel) 那样。 |
window.send(message) / window.on("event", fn) |
向 RootView::on_message 发送 JSON 消息;从 bridge.emit(...) 接收事件。 |
window.close()、window.on("closed", fn)、window.isClosed |
生命周期(无论窗口如何关闭,closed 都会触发)。 |
gpui.windows、gpui.windowCount()、gpui.on("all-windows-closed", fn) |
打开 GPUI 窗口。 |
gpui.shutdown()、loadAddon(location) |
关闭所有内容;在不启动 GPUI 的情况下加载插件。 |
openWindow<Message, Event>(...) 接受类型参数,用于指定消息和事件的结构。
Rust(electron-gpui crate):实现 RootView(new、on_message)和 Render,用 WindowBridge::emit 发出事件,并用 export! { "Name" => View } 注册视图。electron_gpui::gpui 和 electron_gpui::serde_json 重新导出 SDK 使用的版本。GPUI 的宏(actions!、#[derive(IntoElement)]、#[derive(Action)]……)会展开为 gpui:: 路径,因此使用它们的文件需要 use electron_gpui::gpui;。electron_gpui::set_always_on_top(window, flag, relative_level) 可跨平台工作。现有的 electron_gpui::macos::set_always_on_top 辅助工具在 macOS 上仍然可用。
CLI:electron-gpui init [dir] [--local <path>] [--skip-install] 安装 JavaScript 包并搭建一个 views crate。electron-gpui build [dir] [--release] [--universal] 手动构建 <dir>/index.node(插件会为你完成此操作)。如果没有打包工具,请将 new URL("./native/index.node", import.meta.url) 传给 createGpui。
窗口选项为尽力而为。红绿灯按钮位置和隐藏透明标题栏适用于 macOS;Windows 和 Linux 使用窗口系统的标题栏。Wayland 标题栏的可用性取决于合成器的装饰支持。模糊背景在 macOS 之外会回退为透明,具体取决于合成器支持。置顶使用 Windows 的 topmost 和 X11 EWMH;普通 Wayland 窗口没有可移植的置顶协议,因此该请求会被忽略。relativeLevel 仅影响 macOS。Wayland 上的定位和焦点也取决于合成器。
在 Linux 上,当 Electron 导出私有实现(包括 Electron 30)时,GPUI 会将其图形库的 Wayland 导入重定向到系统 libwayland,从而保留 Electron 的分配器绑定。这避免了与 Mesa 的 EGL/Vulkan 驱动发生冲突。需要可用的图形驱动;Mesa 的软件图形也可用于开发和 CI。
发布
- 生产构建(
NODE_ENV=production,由vite build设置)使用--release。electronGpui({ universal: true })仅适用于 macOS,会先安装两个 Apple Rust 目标,然后为 Apple 芯片和 Intel 生成一个插件。请在各自平台上构建 Windows 和 Linux 插件。 - 对于应用其余的原生模块,请使用 vite-plugin-native-modules,它以相同方式打包
.node依赖(node-gyp-build、bindings、NAPI-RS)。 - 原生插件无法从 asar 归档内部加载,因此请将其解包(electron-builder:
asarUnpack: ["**/*.node"])。
须知
- 主线程是共享的。 长时间运行的主进程 JavaScript 会冻结 GPUI 窗口,而缓慢的
render会拖慢 Electron。请将繁重工作放在 worker 或 GPUI 的cx.background_spawn中。ELECTRON_GPUI_DEBUG=1会记录每次 JS → GPUI 调用所花费的时间。 - Panic。
on_message中的 panic 会变成 JS 错误,窗口会继续工作。RootView::new中的 panic 会禁用 electron-gpui,直到应用重新启动,因此请改为验证props。render或事件处理器中的 panic 会中止进程,与任何 GPUI 应用一样,且在任何 JS 运行之前就会发生:传入createGpui(addon, { panicLog: path })可将每个 panic 作为 JSON 行追加到该文件(time、message、location、thread、aborts、backtrace),并在下次启动时报告。 - 独立窗口。 GPUI 在自己的原生窗口中绘制,而不是在
BrowserWindow内部。 - 退出。 Electron 的
window-all-closed会忽略 GPUI 窗口;请使用gpui.windowCount()或all-windows-closed。 - 菜单。 菜单栏是 Electron 的。请使用
Menu并用window.send转发操作。 - 磁盘空间。 如果安装了
cargo-sweep,构建会删除 7 天内未使用的 Cargo 输出(使用ELECTRON_GPUI_NO_SWEEP=1可跳过)。
工作原理
GPUI 以嵌入式模式运行,将应用程序的事件循环交给 Electron。macOS 共享 NSApp;Windows 仅挂钩 GPUI 窗口消息,并可回退到 Direct3D WARP;Linux 在 Electron 主线程上每 8 毫秒通过非阻塞的 calloop 轮询分发 X11 或 Wayland 事件。当 GPUI 窗口打开时,Linux 定时器保持进程存活,并在关闭时停止。没有可选轮询导出的旧版插件仍然兼容。
你的视图、GPUI 和 SDK 编译成一个 .node 插件,因为 Rust 没有稳定的 ABI 来在库之间共享 GPUI。对于热重载,该插件保留一个可修补的构建,将更改的代码重新编译为 Mach-O、ELF 或 PE 库,并让 Subsecond 将调用重定向到它。开发插件使用内容寻址的文件名。Windows 加载进程拥有的副本,因此重新构建永远不会覆盖已加载的 DLL;被锁定的文件在其进程退出后清理。已应用的补丁库在应用程序的整个生命周期内保持加载。
许可证
MIT。GPUI 版权归 © Zed Industries, Inc. 所有(Apache-2.0);请参阅 NOTICE。
来源:Hacker News · github.com