gpui-base Sheet 原语详解:从边缘进入的模态表面、焦点陷阱与关闭流程
【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit
Sheet是gpui-base(GPUI 生态基础行为层,见 crates/base/Cargo.toml)提供的一种模态原语:它从窗口某个边缘滑入(典型如右侧设置面板),同时接管关闭确认与焦点管理。本文以官方文档 website/base/primitives/sheet.md 为主线,结合 Sheet 核心实现 与可运行 showcase 源码,讲清它的构造 API、状态与事件模型、底层渲染结构、遮罩交互策略及测试契约,帮助你直接在自己的 GPUI 应用中原样落地。
Sheet 与gpui-base中所有原语一样,只提供行为与语义结构,不施加任何产品视觉语言——布局、定位、颜色、尺寸与动效全部交由调用方通过 GPUI 标准样式 trait 表达。读完本文,你将能够:运行官方示例、按受控状态驱动 Sheet 开关、利用overlay/surface组合出自有设计系统的侧滑面板,并理解 Escape、遮罩点击与焦点陷阱的完整行为闭环。
快速运行官方示例
文档引用的可运行入口是 crates/base/examples/native/src/bin/components.rs,它读取命令行第一个参数作为组件名,并编译共享的 showcase 实现(crates/base/examples/showcase/mod.rs):
cargo run -p gpui-base-examples -- sheetgpui-base-examples是gpui-basecrate 下的示例二进制包名(示例目录位于 crates/base/examples,发布时被排除在正式包外,见 crates/base/Cargo.toml#L6-L8)。- 不带参数时默认显示组件总览页(
"overview")。 - 同一个 showcase 还会被编译为 WASM 预览:由 showcase/mod.rs 中的
run_embedded与run_native两个入口分别驱动,因此原生与浏览器预览渲染的是同一份源码。
运行后你会看到页面中央一个 "Open settings" 按钮,点击后从右侧滑出设置面板,覆盖半透明遮罩;点击遮罩或按 Escape 可关闭。
导入方式
在应用代码中,推荐通过gpui-kitfacade 导入(facade 的层级映射见 crates/kit/src/lib.rs):
use gpui_kit::base::{Sheet};其中gpui_kit::base对应gpui-basecrate,base层总是可用(无需 feature)。当然你也可以直接use gpui_base::Sheet;,它由 crates/base/src/lib.rs 公开导出(模块声明在 crates/base/src/lib.rs#L56)。
构造与公开 API
Sheet是一个实现了IntoElement的宿主结构,核心定义见 crates/base/src/sheet.rs#L31-L43。构造入口为Sheet::new(cx),随后通过 Builder 风格方法组装内容与行为:
| 方法 | 签名要点 | 默认值 / 说明 |
|---|---|---|
new(cx) | fn new(cx: &mut App) -> Self | 创建宿主,内部生成FocusHandle并绑定到"Sheet"键盘上下文 |
overlay(...) | impl IntoElement | 遮罩元素;未设置则不渲染遮罩层 |
surface(...) | impl IntoElement | 表面(面板主体)元素;未设置则无内容 |
overlay_closable(bool) | 布尔 | 默认true,决定点击遮罩(左键)是否触发关闭流程 |
on_close(handler) | Fn(&ClickEvent, &mut Window, &mut App) | 关闭通知回调,在request_close之后执行 |
request_close(...) | Fn(&mut Window, &mut App)(#[doc(hidden)]) | 关闭请求回调,先于on_close执行 |
focus_handle(FocusHandle) | 覆盖内部焦点句柄(#[doc(hidden)]) | 主要用于测试注入 |
dismiss_before_y(Pixels) | 纵向裁剪阈值(#[doc(hidden)]) | 遮罩点击在阈值线以上时被忽略,用于「吸顶区域不可关闭」场景 |
此外Sheet实现了Styled(crates/base/src/sheet.rs#L109-L113),宿主本身的样式可通过.style()/GPUI 样式链式方法细化。
默认值来自构造器(crates/base/src/sheet.rs#L45-L59):overlay_interactive = true、overlay_closable = true,request_close与on_close初始为空实现。
状态与事件:受控打开与关闭顺序
文档明确:Sheet 的打开与关闭「镜像 Dialog,而 placement(进入边缘)由你决定」。推荐做法是把受控状态放在父级渲染类型或 GPUI Entity 中,在回调里更新状态并调用cx.notify();不要在每次渲染时重建持久 Entity。
关闭流程的调用顺序由内部close辅助函数保证(crates/base/src/sheet.rs#L17-L21):
request_close(window, cx) → on_close(&ClickEvent::default(), window, cx)即先请求关闭,再通知关闭。Escape 键走同一条路径:Sheet::init在应用初始化时注册KeyBinding::new("escape", Cancel, Some(CONTEXT))(crates/base/src/sheet.rs#L23-L25),渲染时宿主设置key_context("Sheet")并挂接Cancel动作(crates/base/src/sheet.rs#L132-L138)。
完整 Rust 示例(showcase 源码)
以下即文档内嵌、由可运行 showcase 直接使用的完整实现,出自 crates/base/examples/showcase/components/sheet.rs。它演示了:受控布尔状态、request_close+on_close双回调、遮罩与表面元素的自定义,以及状态更新后的cx.notify():
use gpui::relative; use super::*; impl BaseShowcase { pub(in super::super) fn sheet(&self, cx: &mut Context<Self>) -> impl IntoElement { let open = self.sheet_open; let entity = cx.entity().downgrade(); let open_sheet = entity.clone(); let trigger = Button::new("open-sheet") .h_7() .px_2() .text_xs() .flex() .items_center() .justify_center() .border_1() .border_color(super::example_rgb(0x171717)) .bg(super::example_rgb(0xffffff)) .child("Open settings") .on_click(move |_, _, cx| { _ = open_sheet.update(cx, |this, cx| { this.sheet_open = true; cx.notify(); }); }); div() .size_full() .min_h_64() .text_xs() .flex() .items_center() .justify_center() .child(trigger) .when(open, |this| { this.child( Sheet::new(cx) .request_close({ let entity = entity.clone(); move |_, cx| { _ = entity.update(cx, |this, cx| { this.sheet_open = false; cx.notify(); }); } }) .overlay( div() .absolute() .inset_0() .bg(super::example_rgb(0x000000)) .opacity(0.15), ) .surface( div() .absolute() .right_0() .top_0() .h_full() .w(px(210.)) .p_3() .bg(super::example_rgb(0xffffff)) .border_1() .border_color(super::example_rgb(0x171717)) .child( div() .font_weight(gpui::FontWeight::SEMIBOLD) .child("Settings"), ) .child( div().mt_4().child("Workspace name").child( div() .mt_1() .h_7() .px_2() .flex() .items_center() .border_1() .border_color(super::example_rgb(0xa3a3a3)) .child("Acme Studio"), ), ) .child( div() .mt_2() .text_color(super::example_rgb(0x525252)) .child("Update the workspace preferences for your team."), ) .child( div() .mt_4() .py_1() .border_t_1() .border_color(super::example_rgb(0xd4d4d4)) .child("Notifications · Enabled"), ) .child( div().mt_3().flex().justify_end().child( Button::new("close-sheet") .h_7() .line_height(relative(1.)) .px_3() .flex() .items_center() .justify_center() .bg(gpui::black()) .text_color(gpui::white()) .child("Done") .on_click({ let entity = entity.clone(); move |_, _, cx| { _ = entity.update(cx, |this, cx| { this.sheet_open = false; cx.notify(); }); } }), ), ), ), ) }) } }要点拆解:
- 受控开关:
self.sheet_open是 showcase 上持久化的布尔字段(定义于 crates/base/examples/showcase/mod.rs#L138),触发按钮与request_close都只更新该字段并cx.notify()。 - 关闭的双入口一致性:遮罩点击 / Escape / 面板内 "Done" 按钮都会将
sheet_open置回false——前两者走request_close,按钮直接写状态,殊途同归。 - placement 由 surface 决定:示例用
absolute().right_0().top_0().h_full().w(px(210.))把表面钉在右侧;改成bottom_0/left_0/居中即可实现下边缘、左边缘或居中弹层。 - 元素 ID 稳定性:
Button::new("open-sheet")、Button::new("close-sheet")使用稳定字符串 ID,便于测试与无障碍。
渲染原理:锚定全屏宿主、焦点陷阱与键盘上下文
RenderOnce实现(crates/base/src/sheet.rs#L115-L167)是理解 Sheet 行为的关键:
- 全视口锚定:宿主被包在
anchored().position(point(px(0.), px(0.)))中,尺寸取window.viewport_size(),并设置absolute().top_0().left_0()、w(viewport.width).h(viewport.height)——因此无论表面钉在哪条边,遮罩总能覆盖整个视口。 - 稳定宿主 ID:
id("sheet-host")并调用test_support()(为 GPUI 测试基础设施提供支持)。 - 焦点管理:
track_focus(&self.focus)+focus_trap("sheet", &self.focus)注册焦点陷阱,打开时焦点被捕获在 Sheet 内,关闭后恢复——这正是文档无障碍部分「trap and restore focus」的底层实现。 - 键盘上下文:
key_context(CONTEXT)("Sheet")使 Escape 的Cancel绑定仅在 Sheet 处于激活上下文时生效;on_action里先cx.propagate()再走统一的close流程。
遮罩交互:可关闭性、可交互性与纵向裁剪线
遮罩行为集中在 crates/base/src/sheet.rs#L139-L162:
overlay_interactive(默认true)控制是否挂接on_any_mouse_down监听;false时遮罩完全不拦截事件。- 任何鼠标按下都会
cx.stop_propagation(),防止穿透到下层内容。 - 仅当
overlay_closable && event.button == MouseButton::Left时触发关闭流程。 dismiss_before_y裁剪线:若点击位置y < top(在裁剪线上方),则忽略该次点击——典型用途是「顶部工具栏区域点击不关闭面板」。
测试与行为契约
Sheet 的行为契约有完整单测背书,全部位于 crates/base/src/sheet.rs#L169-L269:
| 测试 | 验证内容 |
|---|---|
overlay_close_requests_then_notifies | 左键点击遮罩后事件序列严格为["request", "closed"] |
non_closable_overlay_does_not_request_close | overlay_closable(false)时点击遮罩不产生任何事件 |
escape_uses_the_same_close_order_and_registers_focus_trap | Escape 走相同关闭顺序,且焦点陷阱已注册(active_focus_trap非空) |
pointer_above_the_dismiss_cutoff_is_ignored | 裁剪线上方点击被忽略、下方点击正常关闭 |
测试通过gpui::TestAppContext+VisualTestContext模拟点击与键盘动作,使用request_close/on_close记录事件名并断言顺序,是复现行为的最佳参考。
无障碍
文档给出的无障碍要求与 Dialog 一致:为 Sheet 提供标题、捕获并恢复焦点、提供明确的关闭途径。落到实现上:
- 焦点陷阱由
focus_trap("sheet", &self.focus)保证(与 focus_trap.rs 中的FocusTrapElement协作,导出见 crates/base/src/lib.rs#L103-L105)。 - Escape 关闭由
"Sheet"键盘上下文承载。 - 表面元素本身是无样式 div,屏幕阅读器语义(如 role、aria-label)由调用方在
surface/overlay内容中补充。
使用注意事项
- 元素 ID 稳定:在
overlay、surface及内部交互元素上使用稳定 ID(如Button::new("close-sheet")),便于定位、测试与无障碍引用。 - 状态管理:受控状态放在父级 Entity 或渲染类型上,回调中
cx.notify();切勿在每次 render 中重建持久 Entity。 - 不要复用文档内嵌的视觉样式:示例中的黑白配色、
210px宽度仅用于演示;应在消费设计系统中验证 focus、hover、active、selected、disabled、reduced-motion(减弱动效)与 high-contrast(高对比度)下的外观。 - 与 Dialog 的取舍:Sheet 适合「边缘抽屉 + 上下文设置」类场景;居中弹窗需求请参考 dialog 文档(实现见 crates/base/src/dialog.rs)。
- 若使用完整设计语言:
gpui-component层提供带样式的 Sheet 组件(见 crates/component/src/sheet.rs),适合直接落地到成品界面;gpui-base原语则保留最大自由度。
延伸阅读
- 本文档原文:website/base/primitives/sheet.md
- 核心实现与单测:crates/base/src/sheet.rs
- showcase 共享实现(含
BaseShowcase与组件分派):crates/base/examples/showcase/mod.rs - 原生入口(组件名作为 CLI 参数):crates/base/examples/native/src/bin/components.rs
gpui-kitfacade 与层级说明:crates/kit/src/lib.rsgpui-base包描述与特性(test-support、inspector):crates/base/Cargo.toml
【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考