WebView

实验性依赖平台

gpui-webview 是 GPUI Kit 基于 Wry 的实验性集成。需要浏览器行为时可以使用它;TextView HTML 用于渲染文档内容,并不是浏览器。要在默认外部浏览器中打开 URL,使用 cx.open_url。0.7.1 及更早版本以 gpui-wry 发布;升级时把依赖换成 gpui-webview,并把 gpui_wry:: 路径改为 gpui_webview::。它支持 macOS、Windows 和 Linux;各平台使用的引擎和显示服务见平台支持。

#平台支持

平台引擎状态
macOSWKWebView实验性。启用 gpui-fast 后,延后绘制的 GPUI 浮层显示在网页上方。
WindowsWebView2实验性。示例关闭了 GPUI 的 DirectComposition,子视图才能正常渲染。
Linux(X11)WebKitGTK实验性。应用必须以 X11 启动,见 Linux。启用 gpui-fast 后,延后绘制的 GPUI 浮层显示在网页上方。
Linux(Wayland)—不支持。请让应用通过 XWayland 以 X11 运行。

#运行示例

在仓库根目录运行:

cargo run -p webview

完整示例是可运行的起点:它在 open_window 回调中创建原生子视图,把它包装为 Entity<WebView>,再放在地址输入框下方渲染。在输入框按 Enter 会调用 load_url;示例还包含返回上一页的处理函数。从仓库根目录运行上面的命令。其他应用请参照示例的依赖配置:这条集成路径直接依赖 gpui-kit、gpui-webview 和 wry(package 名为 lb-wry)。

use gpui_kit::*;
use gpui_webview::WebView;

let webview = cx.new(|cx| {
    WebView::build(wry::WebViewBuilder::new(), window, cx).expect("Failed to create WebView")
});

webview.update(cx, |view, _| view.load_url("https://gpui-kit.com"));

// In the owning View's render method:
div().flex_1().child(webview.clone())

WebView::build 把 Wry 视图挂为 GPUI 窗口的子视图,并完成 Wry 所需的平台初始化;WebView::new 仍可包装应用自行创建的 wry::WebView。要等 GPUI 提供有效的 Window 后才创建原生视图;所属 View 保存 Entity<WebView>,让它在多次 render 之间持续存在。完整示例先调用 gpui_kit::init(cx) 初始化组件,再通过 gpui_kit::open_window(...) 创建窗口。完整的应用启动代码请以链接的源码为准;此片段不是独立的 main 函数。

#GPUI Fast 原生合成

GPUI 后端由 gpui-kit 决定,gpui-webview 跟随它。要使用 GPUI Fast,两个 crate 的 gpui-fast feature 都要启用:

[dependencies]
gpui-kit = { version = "0.8.0", features = ["gpui-fast"] }
gpui-webview = { version = "0.8.0", features = ["gpui-fast"] }

gpui-webview/gpui-fast 本身不切换后端,只启用合成路径。两个 feature 必须一致,不一致会编译失败:只启用 gpui-kit/gpui-fast 时,gpui-webview 提示启用 gpui-webview/gpui-fast;只启用 gpui-webview/gpui-fast 时,提示启用 gpui-kit/gpui-fast。

启用 gpui-fast 后,macOS 上的 WebView::new() 会自动把 WKWebView 注册到窗口合成树。Linux 上,WebView::build() 会把视图建在合成 surface 中,即一个 X11 子窗口,GPUI 会把位于其上方的浮层区域从中挖空。这样延后绘制的 GPUI 浮层就能显示在网页上方。Windows 在两个后端下都保留原生子视图的行为。

在 gpui-pre 下,原生视图与 Windows 一样直接挂在窗口上,会遮住其 bounds 内的 GPUI 内容,包括 popover、dialog、menu 和 tooltip。原生视图也无法被裁剪:容器只裁掉一部分时(例如位于滚动区域中),它仍保持完整的 bounds,溢出到容器外,盖住相邻的界面;只有完全移出可视区或被应用隐藏时才会隐藏。GPUI Fast 无法创建合成 surface 时也是如此。

在 Linux 上,GPUI Fast 会把阴影和半透明内容(例如 dialog 的遮罩)画进网页上方的透明窗口,由窗口合成器叠加到网页上;XWayland 下始终有合成器。在没有窗口合成器的 X11 桌面上,浮层改为从网页中挖空,网页上方的阴影不会绘制。Hyprland 会把置顶窗口画在所有窗口之上,因此在置顶窗口(Omarchy 的弹出窗口)中,浮层仍会显示,但网页上方没有阴影和遮罩压暗。

窗口坐标下的尺寸和位置使用 WebView::set_bounds(Rect) -> wry::Result<()>;显隐使用 WebView::set_visible(bool) -> wry::Result<()>。这两个方法会同步合成容器和 Wry 子视图,直接调用原始 Wry 对象会绕过同步。show()、hide() 仍是忽略错误的便捷方法。合成注册失败时,构造方法记录错误并保留原来的挂载方式。浮层点击和反复调整尺寸必须在真实窗口中验证。

#Linux

Wry 使用 WebKitGTK 渲染,而 GTK 只能嵌入 X11 窗口;Wayland 不允许把一个客户端的 surface 放进另一个客户端的 surface。因此在 Linux 上,使用 gpui-webview 的应用必须运行在 X11 上,在 Wayland 会话中即通过 XWayland 运行。GPUI 整个应用只连接一个显示服务,所以这个选择对所有窗口生效,而不只是承载 WebView 的窗口。

用 gpui_kit::platform::linux(WindowingModes::X11) 代替 gpui_kit::application() 启动应用,示例也是这样做的:

use gpui_kit::*;

fn main() {
    #[cfg(target_os = "linux")]
    let app = gpui_kit::platform::linux(WindowingModes::X11);
    #[cfg(not(target_os = "linux"))]
    let app = gpui_kit::application();

    app.run(|cx| {
        gpui_kit::init(cx);
        // 在这里打开窗口并创建 WebView。
    });
}

WebView::build 在做任何初始化之前,会先检查窗口是否为 X11 窗口。窗口运行在 Wayland 时,它返回说明这一要求的错误,配置不对的应用会在创建时就失败,而不是显示一块空白。窗口是 X11 时,它以 X11 后端初始化 GTK,由一个 GPUI task 分发 GTK 事件,再把视图挂到窗口上。桌面环境需要提供 XWayland,GNOME、KDE Plasma 和 Hyprland 默认都已启用。GTK 只支持整数缩放(GDK_SCALE),因此 wrapper 把 Wry 的页面缩放设为 GPUI 缩放除以 GDK 缩放:页面按 GDK 的分辨率渲染,但大小与 GPUI 一致。窗口缩放变化时会重新设置,并覆盖应用通过 zoom() 设置的缩放。在分数缩放下,XWayland 窗口可能比原生 Wayland 窗口略模糊,具体取决于 compositor。

构建前需安装 WebKitGTK 4.1 和 GTK 3 的开发包,例如 Debian 和 Ubuntu 的 libwebkit2gtk-4.1-dev、Arch Linux 的 webkit2gtk-4.1。

#视图归属与布局

WebView::new 起初把原生 bounds 设为空矩形。渲染 Entity 会安装 GPUI layout element;在 prepaint 阶段,该 element 把算出的 bounds 传给 Wry(macOS 和 Windows 使用逻辑坐标,Linux 使用设备像素),并插入 GPUI hitbox。容器必须有实际尺寸。示例使用 div().flex_1().h(px(400.)).child(self.webview.clone())。浏览器像素由操作系统绘制,仅靠 GPUI clipping、hitbox 和 content mask 不能让 GPUI 内容盖过子视图;macOS 的 GPUI Fast 后端通过合成树安排浮层顺序。

每个原生视图保存一个 Entity<WebView>,不要在每次 render 时重新构建 Wry 视图。包装层的 visible() 和 bounds() 分别返回保存的可见状态及上次 layout 的 bounds;show() 和 hide() 改变原生可见性。如果页面或标签不再渲染该 Entity,应在需要消失时明确调用 hide(),恢复时调用 show()。隐藏期间 prepaint 不更新 bounds。

在 GPUI UI context 中通过 webview.update(cx, |view, _| ...) 调用包装层方法。load_url(&str) 请求导航,但丢弃 Wry 的 Result,也不表示加载完成。back() 执行 JavaScript history.back(),其 Result 只反映脚本提交,不表示历史导航成功。需要返回值时,通过 view.raw() 使用 Wry 的 reload()、url() 或 evaluate_script() 等方法。短期回调需要持有 Wry 对象时可使用 view.handle().raw(),但这只适用于 UI 线程;不要仅为了避免更新 Entity 而长期保留 handle。

#加载、导航与页面消息

页面策略与回调应在 build_as_child 之前配置到 wry::WebViewBuilder 上。它们属于 Wry,不是 gpui-webview 事件。仓库固定的 lb-wry 版本提供:

需求Wry API含义
初始内容with_url(...) 或 with_html(...)构建前选择内容;仓库示例则在创建后调用包装层的 load_url。
加载状态`with_on_page_load_handler(event, url
导航策略`with_navigation_handler(url
新窗口请求with_new_window_req_handler(...)决定如何处理 window.open;其回调有平台相关的线程约束。
Rust 向页面发送在原始 Wry 视图上调用 evaluate_script(...)提交 JavaScript 并返回 Wry Result;需要序列化返回值时使用 evaluate_script_with_callback。
页面向 Rust 发送`with_ipc_handler(request

例如,在构建 builder 时可附加一个只允许固定 URL 的导航策略:

let builder = wry::WebViewBuilder::new()
    .with_navigation_handler(|url| url == "https://gpui-kit.com/")
    .with_on_page_load_handler(|event, url| {
        // Send a brief status message to the owning GPUI View if the UI must update.
        // Do not capture &mut Window, &mut App, or &mut Context here.
        let phase = match event {
            wry::PageLoadEvent::Started => "started",
            wry::PageLoadEvent::Finished => "finished",
        };
        eprintln!("{phase}: {url}");
    });

此精确匹配仅用于展示回调形式;实际白名单应解析 URL,并明确校验 scheme 与 host。回调中的工作应保持简短。通过安全的调度或 channel 把状态和 IPC 消息送到应用持有的状态,再在 GPUI context 中更新 Entity;界面变化时调用 cx.notify()。Wry 回调不是 GPUI Entity 事件;Windows 的新窗口回调还会在另一线程运行。应处理回调到达时页面已导航或所属窗口已关闭的情况。

#安全与失败边界

把页面内容视为不可信输入,包括来自自己控制的 URL 的内容。IPC 是应用能力入口:仅接受预期的消息类型与长度;平台提供发送方 origin 时应检查;执行文件、网络或账户操作前还要检查应用层授权。Wry 在 Linux/Android 上对 iframe 的 IPC request URL 使用主 frame URL,因此不能仅凭它确认 iframe 身份。初始化脚本会在新页面执行;固定版本的 Wry 文档指出,即使请求只注入主 frame,Windows 仍会注入子 frame。不要把秘密放进脚本或页面全局变量。

展示远程内容前,应决定下载、外部链接和 window.open 的处理策略。Wry 默认的下载开始回调允许下载;如果这不符合应用要求,应配置下载策略。在所属 GPUI View 中维护用户可见的加载和错误状态:gpui-webview::WebView::load_url 忽略即时错误,而成功提交请求也可能最终加载失败。分别使用原始 Wry 返回值与页面加载回调作为信号;单靠 PageLoadEvent::Finished 或 load_url 都不能证明内容成功加载。应为应用能够检测到的失败提供重试入口。

#Focus 与生命周期

WebView 是原生子视图,不是 GPUI 绘制的 Element。它占据 GPUI layout 节点的 bounds,并接收浏览器原生输入。WebView 实现了 Focusable,包装层跟踪一个 FocusHandle。调用 hide() 时会先把 Focus 交回父视图;点击其 bounds 之外也会请求父视图取得 Focus。在 Linux 上,点击网页也会传给 GPUI,因此点击外部即关闭的 popover 和 menu 在点击网页时会关闭。界面同时使用 GPUI input 和浏览器 input 时,应验证实际键盘与 Focus 行为。

WebView 及其 handle 应在父窗口销毁前结束生命周期。销毁所属 Entity 会隐藏子视图,但克隆的 WebViewHandle 或帧内持有的克隆可能推迟原生视图销毁。销毁父窗口前应释放这些 handle。状态归属参见 Entity,窗口 handle 参见 Window。

#调试与验证

示例在 debug 构建或启用其 inspector feature 时请求启用 Wry devtools,但不会自动打开。在 macOS release 构建中,Wry 还要求启用它自己的 devtools feature,这项请求才会生效。Wry 的 open_devtools() 仅在 debug 或该依赖 feature 下可用。通过页面检查工具区分 JavaScript/网络问题与 GPUI layout 问题。遇到空白子视图,先确认 GPUI 容器的 bounds 非零、visible() 为 true、原生视图来自当前有效窗口。在 Windows 上还应核对下文示例的 DirectComposition 设置。导航与 IPC 需在真实原生窗口中验证;GPUI 的 headless UI 测试不能证明原生浏览器像素、系统 Focus 或 compositor 层级。

在每个受支持的操作系统上做应用级 smoke test:初次加载、链接与重定向、返回与刷新、新窗口和下载策略、加载失败/离线、IPC 输入验证、窗口尺寸变化、显示/隐藏、键盘 Focus 转移,以及关闭父窗口。纯策略解析可以单独测试。仓库示例展示加载与布局,但没有完整的导航、bridge 或错误状态测试套件。

#当前限制

范围当前行为
平台macOS、Windows 和 X11 上的 Linux 为实验性支持。不支持 Wayland;Linux 应用必须以 X11 或 XWayland 启动。
Overlay 层级原生 WebView 位于 GPUI surface 上方,会遮住同一矩形范围内的 GPUI 内容,包括 popover、dialog、menu 和 tooltip。在 Windows 上以及 gpui-pre 下,GPUI overlay 无法可靠地显示在它上方。启用 gpui-fast 后,macOS 和 Linux 上延后绘制的浮层通过原生合成显示在网页上方。
裁剪启用 gpui-fast 后,macOS 和 Linux 会按容器裁剪原生视图,且不改变页面尺寸。在 gpui-pre 下和 Windows 上,被部分裁剪的原生视图会溢出到容器外。
Windows renderer仓库示例在启动 GPUI 前设置 GPUI_DISABLE_DIRECT_COMPOSITION=true,以便当前子视图方案正常渲染。这是此示例的要求,不是 GPUI 的通用设置建议。

需要显示 overlay 时,可以将 WebView 放在单独窗口,或安排布局使 overlay 不跨过 WebView 的 bounds。这一限制适用于 Windows 和 gpui-pre;启用 gpui-fast 后,macOS 和 Linux 通过原生合成支持延后绘制的 GPUI 浮层。

#尚未合并的 Composition 尝试

以下 PR 探索 overlay composition。**它们都不属于上文所述的当前 gpui-webview 行为。**应用采用实验分支前,应重新核对 PR 状态和实现:

  • GPUI Kit #2626 尝试将 GPUI overlay 绘制在原生 WebView 上方。当前分支依赖 Zed/GPUI #61945,后者为 deferred GPUI overlay 提供可选的分层 scene。GPUI Kit 这项 PR 验证了 macOS 路径;Windows composition 和 Linux hosting 在该 PR 中仍属于后续工作。
  • Zed/GPUI #62379 提出另一套范围更广、可选启用的 CompositionTree,用于编排 GPUI 与原生 surface,并带有 macOS 和 Windows 示例。它是 #61945 的替代方案,不是 GPUI Kit #2626 的依赖。Linux composition 不在这项 PR 的范围内。

这些实验在合并前仍可能变化。它们说明了探索方向,并未消除当前的平台、overlay 和 Focus 限制。

CC BY 4.0 · GPUI Kit 有权授权的原创文档正文与图示可在注明署名后复制或改编。 许可条款。请署名 GPUI Kit,链接到 本页原文及 CC BY 4.0 许可,并注明是否修改。 代码示例与软件源码仍适用 Apache-2.0。既有 Apache 权利不受影响;第三方材料保留各自许可。