文档
GPUI
GPUI 后端用 GPUI(GPUI Fast)绘制 NSH 应用。GPUI 是 GPUI Kit 所基于的 GPU 加速 UI 框架,在 macOS、
Windows 和 Linux 上表现一致。它是嵌入 GPUI 和 GPUI Kit 应用的后端:AppView 是一个 GPUI 视图,GPUI 或 GPUI Kit 应用可以在
自己元素树的任意位置显示 NSH 应用:它自己的 Rust 根组件,或它加载的 TSX。独立应用用各桌面系统自己的后端运行和打包:macOS(AppKit)、
Windows(Win32)和 Linux(GTK)。核心、响应式图、渲染器、Taffy 布局以及原生模块与其他所有平台完全相同,因此同一个应用可以在所有
平台上运行。os()(TSX 中的 platform.os)是它所运行的操作系统(macOS、Windows 或 Linux),并会应用 TSX 的 desktop={{…}} 属性。
macOS(AppKit)、Windows(Win32)和 Linux(GTK)后端的元素和模块它都具备; GPUI 用自己方式绘制的地方,见差异。
#工作方式
- 每个界面一个元素。 一个界面(surface)是一个 GPUI 元素,它填满父元素给它的空间,用 GPUI 的图元(矩形、阴影、路径、字形和 图片)绘制整棵视图树。与 Windows 后端一样,NSH 为每个节点保留一个视图,只在提交改变了内容时才重绘。
- 文字在引擎线程上排版,使用 GPUI 的文字系统(应用自己的字体),由 NSH 断行;主线程只绘制字形。
- 每次提交一次跳转。 引擎运行在自己的线程上;每次提交是发往 GPUI 主线程的一个闭包,由它通知视图重绘。
- 输入来自 GPUI 的鼠标、滚动、按键和输入法事件;命中测试、按压、响应者系统、滚动和文本编辑与 Windows 后端是同一套逻辑。
#环境要求
-
macOS、Windows 或 Linux,以及 GPUI 能使用的 GPU(macOS 上是 Metal,Windows 上是 DirectX,Linux 上是 Vulkan)。
-
macOS 上:构建你的 GPUI 应用或从源码构建运行时会用到 Xcode 的 Metal Toolchain(
xcodebuild -downloadComponent MetalToolchain),由它提前编译 GPUI 的着色器。在没有它的 Mac 上,nsh run会打开 GPUI 的runtime-shadersfeature 构建运行时并给出提示: 着色器在应用启动时编译(第一次不到一秒,之后由 macOS 缓存)。 -
Linux 上:Vulkan 驱动、带
libxkbcommon的 Wayland 或 X11、GTK 3 和 WebKitGTK 4.1(Web 视图),以及 GStreamer 1.20 或更新版本及其 base 和 good 插件(视频)。Web 视图需要 X11:在 Wayland 桌面上请让应用运行在 Xwayland 下(把WAYLAND_DISPLAY=设为空),否则显示占位内容。从源码构建运行时还需要开发包;在 Debian 和 Ubuntu 上:sudo apt-get install clang libclang-dev pkg-config libfontconfig-dev libfreetype-dev libwayland-dev libx11-xcb-dev \ libxcb1-dev libxkbcommon-dev libxkbcommon-x11-dev libzstd-dev libvulkan1 libgtk-3-dev libwebkit2gtk-4.1-dev \ libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-good
#运行应用
Rust 应用运行在你的 GPUI 应用里:用 AppView 嵌入它的根组件(嵌入 GPUI 应用),然后 cargo run 这个 GPUI
应用;你的 crate 和这个应用是同一次 Cargo 构建。单独运行时,Rust 应用使用本机操作系统自己的后端(nsh run --target macos、
windows 或 linux)。
TSX 应用在 GPUI 开发宿主中运行:
nsh run App.tsx --target gpui
它在一个 GPUI 窗口(默认 1200×800)中打开应用,显示效果与嵌入它的 GPUI 应用相同,和其他目标一样支持热重载、在终端显示日志并提供错误浮层。开发宿主
(nsh-gpui-host)在每个操作系统上都直接运行,不需要外面包一层应用包。在源码仓库中,nsh run 会先构建它;发行版把它放在
runtime/gpui/ 中。宿主也可以单独运行一个构建好的 bundle:
nsh build my-app --release -o build
dist/nsh/runtime/gpui/nsh-gpui-host --bundle build/bundle.nshc --window-size 390x844 # 源码仓库中:target/debug/nsh-gpui-host
--window-size 是窗口的尺寸,单位为点(<宽>x<高>)。
#打包
GPUI 应用把 NSH 放在自己的可执行文件里:它用 AppView 嵌入应用的 Rust 根组件或 TSX bundle(嵌入 GPUI 应用),并作为
GPUI 应用本身构建和分发。独立的 NSH 应用用本机操作系统自己的目标打包:--target macos、
--target windows 或 --target linux(打包)。
在 Linux 上,带 Web 视图和视频的 GPUI 应用动态链接 WebKitGTK 4.1 和 GStreamer,因此运行它的机器需要安装它们,以及 GTK 3 和
GStreamer 的 base、good 插件(Debian / Ubuntu:libwebkit2gtk-4.1-0、libgstreamer1.0-0、libgstreamer-plugins-base1.0-0、
gstreamer1.0-plugins-good),此外还需要 Vulkan 驱动和 libxkbcommon。
#嵌入 GPUI 应用
nsh-platform-gpui crate(来自 NSH 仓库)为 Rust GPUI 应用提供 AppView:一个以 GPUI 视图(Entity<AppView>)形式存在的
NSH 引擎,填满父元素给它的空间。每个 AppView 相互独立,可以同时运行多个。它运行一个 Rust 根组件,即你的 crate 的根组件:
use gpui::{AppContext, WindowOptions};
use nsh_platform_gpui::{set_ui_sets, App, AppOptions, AppView, Root};
// 在第一个视图之前设置一次:应用导入的 UI 集合(NSH 的和你的 crate 的)。
set_ui_sets(vec![nsh_base::registry(), my_app::registry()]);
gpui_platform::application().run(|cx| {
cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|cx| {
let view = AppView::new(AppOptions::default(), window, cx);
view.load(App::Rust(Root::of::<my_app::App>()));
view
})
})
.unwrap();
});
Root::tokens 为根组件设置主题(nsh_base::Theme::from_json(json)?.into_tokens(),这里也重新导出了
Theme),Root::app_id 设置保存其数据所用的 id。nsh build 写出的 TSX bundle 也加载到同一种视图中:
use nsh_platform_gpui::{App, Bundle};
let code = std::fs::read_to_string("app/bundle.js").expect("bundle");
view.load(App::Js(Bundle::source(code)));
像其他视图一样把这个 entity 放进元素树(GPUI Kit 的 Root、一个面板、一个停靠区)。AppView::load 替换正在运行的内容;
stop(或丢弃视图)结束应用。AppOptions::asset_base 指定 asset:/ URI 的解析位置(AssetBase::Directory 或
AssetBase::Http)。字节码 bundle(bundle.nshc)用 unsafe { Bundle::bytecode(bytes) }(或同样是 unsafe 的
Bundle::read_trusted(path))加载:QuickJS 字节码是受信任的输入,只应从你自己应用的文件中加载。
open_app_window(source, options, cx)在运行中的 GPUI 应用里打开一个带AppView的窗口。run_app(source, options)启动 GPUI,在一个窗口中运行一个应用直到窗口关闭(开发宿主就是这样做的)。AppView::register_component(name, factory)在加载前注册你自己的原生组件(原生组件)。AppOptions::limits(或AppView::set_limits,作用于之后加载的应用)设置 VM 限制:JS 堆、栈、时间预算和节点配额(沙箱与能力)。嵌入的应用默认是Limits::default()(256 MB 堆);开发宿主把应用作为独立应用运行,使用Limits::standalone()。AppView::connect_dev_server(url, cx)(featuredev)从nsh dev加载应用,边改边重新加载,错误浮层显示在视图上;AppView::on_error照样收到应用的错误。AppView::on_app_error(|error| …)接收应用的错误:error.kind(应用加载失败、未捕获的异常、VM 已停止),error.is_fatal()表示该应用不会再执行任何 JS,error.message、error.stack,以及error.package(它来自的签名包的 id 和 build 号)。AppView::on_error以文本形式收到同样的错误。AppView::set_locale(locale, cx)和AppView::set_font_scale(scale, cx)用你的应用的语言和文字大小代替系统的设置(None恢复跟随系统):locale()、Intl、fontScale()和文字立即跟随(设备与环境)。AppView::run_package(&package)运行来自verify_package或load_package的签名包;你的应用判定它健康之后,用accept_package记录它。set_ui_sets(sets)设置每个引擎提供的 UI 套件:NSH 的(nsh_base::registry())和你的 crate 的(my_app::registry(),由#[native_module]生成;用 Rust 开发),也用来提供你的应用自己的套件:应用通过nativeModule按名称使用其中的函数和类,并用标准的nsh build构建。仓库中的examples/embed-gpui就这样在宿主中维护一个交易台,由两个嵌入的下单面板和宿主自己的控件共享。
#差异
与原生桌面后端在绘制或行为上的不同之处:
- 变换:旋转、斜切和透视的视图,其背景、边框和阴影会随之变换,但文字保持直立,图片、视频和画布按其边界绘制。
- 不透明度作用于每种颜色而不是整组,因此半透明视图中相互重叠的子视图会彼此透出;图片的不透明度(淡入、半透明视图)以 十六分之一为级变化。
- 圆角裁剪(带圆角的
overflow: "hidden")会让子视图的背景和图片跟随圆角,但文字、边框、视频和原生组件按矩形裁剪。 - 不绘制:
filter: blur()、backdropFilter和mixBlendMode;阴影会透过半透明填充显示;borderCurve: "continuous"绘制为圆形圆角。 - 画布文字是近似的:旋转时保持直立,
maxWidth下缩小字号而不是压窄,渐变以单一颜色绘制,strokeText绘制为填充,阴影 不模糊,合成操作和clearRect对它不起作用。 - 滚动视图不支持缩放(
zoomTo和缩放相关属性被忽略)。 - 视频在 Linux 上播放(GStreamer);在 macOS 和 Windows 上显示占位内容并报告
onError。 - Web 视图是位于 NSH 在该位置所绘制一切内容之上的原生视图。Linux 上需要 X11(见环境要求);在 Xwayland
下页面一直是黑的(日志中有 “Failed to create GBM buffer”)时,请用
WEBKIT_DISABLE_DMABUF_RENDERER=1运行应用。macOS 和 Windows 上裁剪祖先不会裁剪它们,加载失败也不会报告onError。不支持baseUrl、scrollable和allowsBackForwardNavigationGestures。 - 无障碍通过 AccessKit 实现:读屏软件在所有平台上都能激活、聚焦、设置值并把元素滚动到可见;自定义操作和上下文菜单操作 尚未传递。
可对照各原生后端自己的列表:macOS、Windows、 Linux。