最近在折腾一个 AI Agent 的桌面端。为了速度快、体积小,我选了 GPUI。
这玩意儿是 Zed 编辑器背后的 UI 框架,用 Rust 写的,渲染确实快,写起来也有点 Tailwind 的影子,挺爽。但前两天做“技能配置”弹窗时,我被一个遮罩层卡住了:不管我怎么写,遮罩只能遮住局部,挡不住全屏。

分享一下我是怎么从坑里爬出来的。
1. 被“关”在容器里的遮罩
写 Web 的朋友习惯了 position: fixed,或者干脆把 Modal 挂在 body 下面。但在 GPUI 里,这招不灵。
Agent 的界面一般是左边侧边栏,右边对话框。如果我的弹窗组件写在右边对话框的代码里,即便我用了 absolute().size_full(),那个灰色遮罩也只能盖住右边那一块。左边的导航栏还是亮着的,用户甚至还能点。
这种“半吊子”遮罩代码通常长这样(别学):
div().absolute() // 以为能全屏,其实被父容器关了禁闭.size_full().bg(gpui::white().opacity(0.5)).child(content)
2. 翻源码找出来的 anchored
我意识到 GPUI 的布局引擎需要一种明确的指令,让它“跳”出当前的布局树。
翻了翻 Zed 的源码(这是学 GPUI 最快的办法),我发现了一个叫 anchored() 的方法。配合 snap_to_window(),它能强行让元素相对于整个窗口定位,而不是那个该死的父容器。
3. 搞定全局弹窗的正确姿势
实现一个真正的全屏弹窗,得解决这几个事:拿到窗口大小、脱离容器、挡住点击。
直接看代码:
// 先拿窗口的实际尺寸let view_size = window.viewport_size();gpui::deferred(anchored().snap_to_window() // 重点:让它吸附到窗口,而不是父元素.child(div().id("global-mask").w(view_size.width) // 撑满宽.h(view_size.height) // 撑满高.flex().items_center().justify_center().bg(gpui::white().opacity(0.2)) // 磨砂感.on_mouse_down(MouseButton::Left, |_, window, cx| {window.close_dialog(cx); // 点遮罩关闭}).child(div().id("dialog-panel").stop_propagation() // 别让点击穿透到遮罩上.child("配置面板内容")))).with_priority(100) // 确保这层在最上面,别被遮住
4. 几个关键点
deferred():这很重要。它会让弹窗在主界面渲染完后再去计算,避免布局冲突。 with_priority(100):相当于 CSS 的 z-index。GPUI 里没有默认的层级管理,你得手动告诉它谁在上面。viewport_size():因为窗口是可以拉伸的,动态获取尺寸才能保证遮罩永远严丝合缝。
说点心里话
用 Rust 写 UI 确实比 Electron 累,文档碎得像渣子一样,很多用法得去翻 Zed 的仓库。但那种零延迟的响应感,是真的让人上瘾。
搞定了这个弹窗,我的 Agent 终于能像个正经桌面软件了。如果你也在折腾 GPUI 或者 Rust 桌面端,碰到了什么奇怪的 Bug,欢迎在评论区聊聊,大家互相排雷。

夜雨聆风