news 2026/9/15 12:42:21

gpui-base Sheet 原语详解:从边缘进入的模态表面、焦点陷阱与关闭流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
gpui-base Sheet 原语详解:从边缘进入的模态表面、焦点陷阱与关闭流程

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

Sheetgpui-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 -- sheet
  • gpui-base-examplesgpui-basecrate 下的示例二进制包名(示例目录位于 crates/base/examples,发布时被排除在正式包外,见 crates/base/Cargo.toml#L6-L8)。
  • 不带参数时默认显示组件总览页("overview")。
  • 同一个 showcase 还会被编译为 WASM 预览:由 showcase/mod.rs 中的run_embeddedrun_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 = trueoverlay_closable = truerequest_closeon_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)——因此无论表面钉在哪条边,遮罩总能覆盖整个视口。
  • 稳定宿主 IDid("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_closeoverlay_closable(false)时点击遮罩不产生任何事件
escape_uses_the_same_close_order_and_registers_focus_trapEscape 走相同关闭顺序,且焦点陷阱已注册(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 稳定:在overlaysurface及内部交互元素上使用稳定 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.rs
  • gpui-base包描述与特性(test-supportinspector):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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 12:42:19

用 Twilio/SendGrid 为 IoT 地理围栏触发函数添加短信与邮件通知

用 Twilio/SendGrid 为 IoT 地理围栏触发函数添加短信与邮件通知 【免费下载链接】IoT-For-Beginners 12 Weeks, 24 Lessons, IoT for All! 项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners 本文围绕 IoT-For-Beginners 运输项目第 4 课&#xff08…

作者头像 李华
网站建设 2026/9/15 12:38:47

30分钟把FiftyOne部署到边缘设备:YOLOv8s计算机视觉应用完整实战

30分钟把FiftyOne部署到边缘设备&#xff1a;YOLOv8s计算机视觉应用完整实战 【免费下载链接】fiftyone Refine high-quality datasets and visual AI models 项目地址: https://gitcode.com/GitHub_Trending/fi/fiftyone FiftyOne 是一个用来构建高质量数据集和视觉 AI…

作者头像 李华