
gpui-kit 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-kit Base 层提供的模态交互原语它以从窗口边缘滑入的方式呈现内容同时负责背景遮罩、Esc/点击关闭、焦点陷阱与关闭回调的完整管理。本篇指南以 Base 层Sheet为骨架结合 crates/base/src/sheet.rs 与 crates/component/src/sheet.rs 的源码实现讲解它的结构、状态与事件约定、可访问性要求并给出可直接运行的全量示例代码。读完你将掌握在 GPUI 应用中正确装配 Sheet、组织关闭回调顺序以及处理焦点回归的完整方案。概述行为原语而非视觉组件与 gpui-kit Base 层的所有原语一样Sheet只提供行为与语义结构不规定任何产品视觉语言。它的职责边界在源码文档注释中写得很明确An unstyled modal sheet host. Applications provide the overlay and surface. The host owns focus trapping, Escape handling, overlay dismissal, and close callback ordering.即应用负责提供遮罩overlay与面板surface的外观宿主负责焦点陷阱、Esc 处理、遮罩点击关闭以及关闭回调的顺序编排。样式与事件表现交给 GPUI 标准的Styled、InteractiveElement等 traitBase 类型只负责交互结构。这样设计的结果是你可以用任意设计系统间距、圆角、动效、配色自由组合导出的部件而无需重写交互逻辑。运行示例原生示例与页面上的 WASM 预览共用同一份实现用 Cargo 直接运行cargo run -p gpui-base-examples -- sheet该命令会完成应用初始化、窗口创建并载入共享的BaseShowcase状态。权威实现位于 crates/base/examples/showcase/components/sheet.rs原生与浏览器预览编译的是同一文件。导入use gpui_kit::base::{Sheet};Sheet在 Base 层通过 crates/base/src/lib.rs 中的pub use sheet::Sheet;导出见 lib.rs 第 154 行属于公开 API 的一部分。若使用 gpui-kit 的 component 层封装也可以从gpui_kit::component导入带主题样式的高层Sheet见下文「进阶」。结构与 API结构分解Sheet由四个可组合的部分构成组成说明宿主host铺满视口的锚定容器负责焦点跟踪与焦点陷阱注册overlay遮罩由应用提供的背景元素可交互、可点击关闭surface面板由应用提供的滑动面板承载实际内容关闭回调request_close与on_close两个按序触发的钩子宿主在渲染时通过anchored()定位在视口原点0,0并撑满整个 viewport 宽高anchored().position(point(px(0.), px(0.))).child( self.base .id(sheet-host) .test_support() .absolute() .top_0() .left_0() .w(viewport.width) .h(viewport.height) .key_context(CONTEXT) // CONTEXT Sheet .track_focus(self.focus) .focus_trap(sheet, self.focus) .on_action(move |_: Cancel, window, cx| { /* Esc 关闭 */ }), ... )宿主元素带有key_context(CONTEXT)并注册了 Esc 键绑定。在 crates/base/src/lib.rs 的init中sheet::init(cx)会执行pub fn init(cx: mut App) { cx.bind_keys([KeyBinding::new(escape, Cancel, Some(CONTEXT))]); }这意味着在 Sheet 打开期间Esc 键只会派发Cancelaction 到 Sheet 上下文由宿主统一处理关闭。公开方法Sheet::new(cx)创建的默认实例overlay(element)设置遮罩元素surface(element)设置面板元素overlay_closable(bool)是否允许点击遮罩关闭默认trueon_close(handler)关闭后的通知回调签名为Fn(ClickEvent, mut Window, mut App)。以下方法标注了#[doc(hidden)]属于内部/测试支持 API常规使用不需要overlay_interactive(bool)遮罩是否可交互默认truefocus_handle(handle)注入外部焦点句柄dismiss_before_y(px)设置忽略点击的纵向分界线用于顶部非关闭区域request_close(handler)实际执行关闭的回调。此外Sheet实现了Styled可以在宿主上直接链式调用 GPUI 样式方法。焦点陷阱Sheet通过 crates/base/src/focus_trap.rs 的FocusTrapElementtrait 注册焦点陷阱。其机制是元素在request_layout阶段将自身注册到全局FocusTrapManager当 Tab / Shift-Tab 会让焦点离开容器时Root拦截事件并让焦点循环回容器首尾测试中可通过active_focus_trap(window, cx)断言当前活动的焦点陷阱存在见 base/src/sheet.rs 的测试。这样 Tab 遍历就被限制在面板内部不会逃逸到背后的应用界面。状态与事件Sheet 的打开与关闭行为与 Dialog 对称打开状态由应用或触发器控制关闭dismissal由宿主处理而关闭结束前内容仍保持挂载便于做退出动画或状态清理。受控状态管理打开状态应保存在父渲染类型或 GPUI entity 中。典型写法是在父组件持有sheet_open: bool在回调中更新状态并调用cx.notify()触发重渲染不要在每次渲染时重建持久 entity如 Input 状态、List 状态等否则会丢失焦点与内部状态。展示示例来自 crates/base/examples/showcase/components/sheet.rs中触发器通过 downgrade 的 entity 更新状态let entity cx.entity().downgrade(); let open_sheet entity.clone(); let trigger Button::new(open-sheet) ... .on_click(move |_, _, cx| { _ open_sheet.update(cx, |this, cx| { this.sheet_open true; cx.notify(); }); });关闭回调的顺序约定这是 Sheet 语义中最关键的一环先request_close请求关闭再on_close通知关闭。close辅助函数保证了这个顺序fn close(request: CloseRequest, notify: CloseHandler, window: mut Window, cx: mut App) { let event ClickEvent::default(); request(window, cx); // 1. 执行关闭请求如把状态置为 false notify(event, window, cx); // 2. 通知监听者 }三种关闭路径共用同一顺序Esc 键宿主on_action(Cancel)中调用close(...)点击遮罩on_any_mouse_down处理器中若overlay_closable且为左键调用close(...)面板内关闭按钮应用自行触发如示例中的 Done 按钮更新状态。四个测试用例在 crates/base/src/sheet.rs 的mod tests中固化验证了这些约定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_trapEsc 与点击遮罩走同一顺序且焦点陷阱已注册pointer_above_the_dismiss_cutoff_is_ignored点击位置在dismiss_before_y分界线之上时被忽略之下才关闭。遮罩交互细节遮罩的on_any_mouse_down处理器会先检查dismiss_before_y分界线用于顶部区域可拖拽、不可点击关闭这类场景随后cx.stop_propagation()阻止事件穿透再判断是否执行关闭if dismiss_before_y.is_some_and(|top| event.position.y top) { return; } cx.stop_propagation(); if overlay_closable event.button MouseButton::Left { close(request_close, on_close, window, cx); }完整 Rust 示例下面是可运行 showcase 使用的完整实现crates/base/examples/showcase/components/sheet.rs 全量内容它组合了打开触发器、遮罩、右侧滑出面板、关闭按钮与受控状态use gpui::relative; use super::*; impl BaseShowcase { pub(in super::super) fn sheet(self, cx: mut ContextSelf) - 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(); }); } }), ), ), ), ) }) } }可以观察到几个要点样式与行为分离遮罩的半透明黑底、面板的宽度210px、内边距、边框、按钮配色全部由应用侧 CSS 式 API 声明Sheet只负责把它们挂进正确的交互结构关闭入口统一遮罩点击、Esc、面板内 Done 按钮最终都落回sheet_open false这一处状态更新并由cx.notify()通知重渲染条件渲染.when(open, ...)保证 Sheet 仅在打开时挂载关闭后从元素树中移除。可访问性Sheet 应按模态层modal处理文档与实现共同要求的可访问性要点如下焦点陷阱宿主通过focus_trap(sheet, self.focus)注册陷阱Tab 循环保持在面板内focus_trap.rs中的FocusTrapManager全局管理所有注册的陷阱容器焦点恢复关闭后焦点应归还给触发元素或上一层焦点不要在 Sheet 生命周期结束后遗留失效焦点提供标题面板应包含可见标题如示例中的 Settings帮助读屏用户理解上下文提供明确的关闭方式至少保证 Esc 与面板内关闭按钮两条路径可用点击遮罩关闭应作为可选增强overlay_closable。注意事项在支持的位置使用稳定元素 ID如sheet-host、open-sheet、close-sheet以便焦点跟踪、测试与无障碍查询稳定工作在消费端设计系统中验证以下状态的外观呈现焦点focus、悬停hover、按下active、选中selected、禁用disabled同时验证**减少动态效果reduced-motion与高对比度high-contrast**场景Base 层不干预这些表现需要应用在设计系统中自行落实不要把持久状态输入框内容、列表滚动位置等的生命周期绑定在每次渲染上应在回调中更新并cx.notify()。进阶component 层的高层 Sheet 封装如果你的应用使用 gpui-kit 的 component 层crates/component/src/sheet.rs 在 BaseSheet之上提供了带主题与开箱即用样式的封装。它由gpui_kit::component导出默认参数为配置默认值placementPlacement::Right右侧滑出size350pxresizabletrueoverlaytrueoverlay_closabletruemargin_topTITLE_BAR_HEIGHT由SheetSettings配置见crates/component/src/sheet.rs它内部仍然委托给 BaseSheet完成焦点陷阱、Esc 与遮罩关闭只是把request_close接到了窗口级 APIrequest_close(|window, cx| window.close_sheet(cx))并自带 0.15 秒的滑动进入动画与可滚动正文。通过窗口 API 打开crates/component/src/window_ext.rs 提供了一组窗口级便捷方法open_sheet(cx, build)在右侧打开等价于open_sheet_at(Placement::Right, ...)open_sheet_at(placement, cx, build)指定Placement::{Left, Right, Top, Bottom}打开close_sheet(cx)关闭当前 Sheethas_active_sheet(cx)查询当前是否有活动 Sheet。这些方法最终路由到 crates/component/src/root.rs 的Root管理器中维护的active_sheet状态。完整的多方向用法可参考 crates/story/src/stories/sheet_story.rs其中覆盖了四方向打开、可滚动 Sheet、遮罩开关Overlay / Close on overlay click、以及关闭后焦点回归的验证场景。小结Base 层Sheet是无样式的模态宿主应用提供 overlay 与 surface 的外观宿主接管焦点陷阱、Esc、遮罩点击与关闭回调顺序状态控制遵循父级持有、回调更新、cx.notify()通知的约定关闭顺序固定为先request_close后on_close且被单元测试固化可访问性上需提供标题、焦点陷阱、焦点恢复与明确关闭路径并在消费端设计系统中验证全状态外观与减少动态效果场景若需要开箱即用的样式与窗口级管理可选用 component 层的Sheet配合WindowExt::open_sheet_at/close_sheetAPI。【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考