乐于分享
好东西不私藏

翻了半天 Zed 源码,我才搞懂 GPUI 怎么让元素“脱离”父容器

翻了半天 Zed 源码,我才搞懂 GPUI 怎么让元素“脱离”父容器

最近在折腾一个 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,欢迎在评论区聊聊,大家互相排雷。