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(又称 sidebar、slide-out panel)是 gpui-kit 提供的一种从屏幕边缘滑入的导航型组件,它不占用主视图空间,可承载导航菜单、表单、设置面板等补充内容。本指南以 website/component/sheet.md 为主线,结合crates/component/src/sheet.rs、crates/component/src/window_ext.rs、crates/component/src/root.rs等源码实现与测试用例,完整讲解从根视图接入、四种方向弹出、尺寸/遮罩/缩放定制到关闭事件与焦点恢复的全链路用法,读完后你可以直接在自己的 GPUI 应用中落地一个可复用、可缩放、可自定义样式的 Sheet 面板。
1. 什么是 Sheet
Sheet 是一个从屏幕边缘滑入的导航组件,常用于:
- 侧边导航菜单(如文件浏览器、设置中心);
- 表单填写面板(如用户资料编辑);
- 辅助内容区(如帮助面板、快捷键说明)。
它通过WindowExt扩展 trait 挂在Window上,配合应用根视图中的Root::render_sheet_layer渲染层实现。与 Dialog(模态对话框)不同,Sheet 更强调"从边缘腾出空间",因此它的定位、尺寸、边框、滑入动画都与所在边缘强相关(详见 crates/component/src/sheet.rs)。
2. 引入依赖
在代码中导入组件与定位枚举:
use gpui_kit::component::WindowExt; use gpui_kit::component::Placement;WindowExt提供open_sheet/open_sheet_at/close_sheet/has_active_sheet等窗口扩展方法(crates/component/src/window_ext.rs),Placement则定义 Sheet 从哪条边缘滑入(crates/base/src/geometry.rs)。
3. 第一步:在根视图中渲染 Sheet 层
Sheet 需要一个"承载层"才能显示。你必须在应用主结构体的render方法中调用Root::render_sheet_layer,把它作为顶层内容叠加在你的应用界面之上:
use gpui_kit::component::TitleBar; use gpui_kit::component::Root; struct MyApp { view: AnyView, } impl Render for MyApp { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let sheet_layer = Root::render_sheet_layer(window, cx); div() .size_full() .child( v_flex() .size_full() .child(TitleBar::new()) .child(div().flex_1().overflow_hidden().child(self.view.clone())), ) // 将 Sheet 层渲染到应用内容之上 .children(sheet_layer) } }底层原理:Root 持有唯一的活跃 Sheet
从源码看,Root结构体内部维护了一个active_sheet: Option<ActiveSheet>字段(crates/component/src/root.rs),同一时刻只能有一个活跃 Sheet。render_sheet_layer在active_sheet存在时,取出其中保存的 builder 闭包,重新构造Sheet::new(window, cx)并回填焦点句柄、放置方位与文本选区作用域后渲染(crates/component/src/root.rs)。
值得注意的是:如果你没有在根视图中渲染该层,Sheet 看起来就像从未打开过。crates/kit/tests/overlays.rs中的测试专门验证了这一点——测试代码在根视图中调用Root::render_sheet_layer(window, cx)并通过.children(sheets)挂载后,才能用window.try_find("sheet-content")找到 Sheet 表面元素(crates/kit/tests/overlays.rs)。
4. 基础用法:打开一个 Sheet
最简单的 Sheet 只需设置标题与内容:
window.open_sheet(cx, |sheet, _, _| { sheet .title("Navigation") .child("Sheet content goes here") })这里open_sheet是open_sheet_at(Placement::Right, ...)的便捷封装——默认从右侧滑入(源码见 crates/component/src/window_ext.rs)。builder 闭包签名是Fn(Sheet, &mut Window, &mut App) -> Sheet,你可以链式调用 builder 方法后原样返回sheet。
5. 指定弹出方向:Placement 的四种边缘
使用open_sheet_at可以精确控制滑入边缘:
// 左侧(默认场景下的显式写法) window.open_sheet_at(Placement::Left, cx, |sheet, _, _| { sheet.title("Left Sheet") }) // 右侧 window.open_sheet_at(Placement::Right, cx, |sheet, _, _| { sheet.title("Right Sheet") }) // 顶部 window.open_sheet_at(Placement::Top, cx, |sheet, _, _| { sheet.title("Top Sheet") }) // 底部 window.open_sheet_at(Placement::Bottom, cx, |sheet, _, _| { sheet.title("Bottom Sheet") })Placement定义于 crates/base/src/geometry.rs,并通过is_horizontal()(Left/Right)与is_vertical()(Top/Bottom)区分方向(crates/base/src/geometry.rs)。
不同方向的渲染差异(源码级)
在Sheet::render中(crates/component/src/sheet.rs),方位直接影响布局:
| 方位 | 锚定方式 | 边框 |
|---|---|---|
Placement::Top | top(top).left_0().right_0() | 底部边框border_b_1 |
Placement::Right | top(top).right_0().bottom_0() | 左侧边框border_l_1 |
Placement::Bottom | bottom_0().left_0().right_0() | 顶部边框border_t_1 |
Placement::Left | top(top).left_0().bottom_0() | 右侧边框border_r_1 |
同时,size()的含义取决于方向:Left/Right 时设置宽度(w),Top/Bottom 时设置高度(h)。
滑入动画
Sheet 表面通过.with_animation("slide", Animation::new(Duration::from_secs_f64(0.15)), ...)实现 150ms 的滑入动画,动画函数根据 placement 从-100px偏移滑动到目标位置(crates/component/src/sheet.rs)。
6. 自定义尺寸
size()接收DefiniteLength(通常用px()),对左右 Sheet 生效宽度、对上下 Sheet 生效高度:
window.open_sheet(cx, |sheet, _, _| { sheet .title("Wide Sheet") .size(px(500.)) // 左右方向为宽度,上下方向为高度 .child("This sheet is 500px wide") })源码中Sheet::new的默认尺寸是350px(DefiniteLength::Absolute(px(350.).into()),见 crates/component/src/sheet.rs),若不调用size()则以 350px 呈现。
7. 承载表单内容:Input + DatePicker + 底部操作区
Sheet 非常适合放置表单。先创建输入状态实体,再用footer()挂载底部操作按钮:
let input = cx.new(|cx| InputState::new(window, cx)); let date = cx.new(|cx| DatePickerState::new(window, cx)); window.open_sheet(cx, |sheet, _, _| { sheet .title("User Profile") .child( v_flex() .gap_4() .child("Enter your information:") .child(Input::new(&input).placeholder("Full Name")) .child(DatePicker::new(&date).placeholder("Date of Birth")) ) .footer( h_flex() .gap_3() .child(Button::new("save").primary().label("Save")) .child(Button::new("cancel").label("Cancel")) ) })从渲染实现看,Sheet 表面分为三段式结构(crates/component/src/sheet.rs):
- 标题栏:
h_flex布局,左侧是title()传入的元素,右侧自动渲染一个IconName::Close关闭按钮(点击后派发Cancelaction); - 内容区:
flex_1().overflow_hidden()包裹的滚动容器,内部默认内边距为四边px(16.),可通过px_*()/py_*()覆盖; - 底部区:仅当调用
footer()时渲染,h_flex两端对齐布局。
关闭按钮触发window.dispatch_action(Box::new(Cancel), cx)(crates/component/src/sheet.rs),这意味着ESC 键与关闭按钮走同一条Cancel动作通道——crates/kit/tests/overlays.rs的escape_dismisses_dialog_and_sheet_and_restores_focus测试验证了 ESC 可关闭 Sheet 并恢复焦点(crates/kit/tests/overlays.rs)。
8. 遮罩选项:overlay 与 overlay_closable
Sheet 默认带半透明背景遮罩,且点击遮罩可关闭:
window.open_sheet(cx, |sheet, _, _| { sheet .title("Settings") .overlay(true) // 显示遮罩背景(默认 true) .overlay_closable(true) // 点击遮罩关闭(默认 true) .child("Sheet settings content") }) // 无遮罩 window.open_sheet(cx, |sheet, _, _| { sheet .title("Side Panel") .overlay(false) // 不显示遮罩背景 .child("This sheet has no overlay") })遮罩的实现细节
遮罩是一个.occlude().w(size.width).h(size.height).bg(overlay_color(self.overlay, cx))的占位层,尺寸为视口减去窗口边框内边距(window_content_insets),颜色复用 dialog 的overlay_color逻辑(crates/component/src/sheet.rs)。底层BaseSheet上,overlay_closable只有在overlay == true时才会真正生效:
.overlay_interactive(self.overlay) .overlay_closable(self.overlay && self.overlay_closable)(crates/component/src/sheet.rs)即:overlay(false)时无论overlay_closable为何值,都不会因点击背景而关闭。
9. 可拖拽缩放的 Sheet
Sheet 默认允许用户拖动边缘调整大小:
window.open_sheet(cx, |sheet, _, _| { sheet .title("Resizable Panel") .resizable(true) // 允许用户缩放(默认 true) .size(px(300.)) .child("You can resize this sheet by dragging the edge") })resizable字段默认值为true(crates/component/src/sheet.rs)。关闭缩放只需传入false。该能力由底层的gpui_base::Sheet(BaseSheet)提供,gpui-kit 的Sheet通过BaseSheet::new(cx)组合了遮罩、表面、请求关闭与关闭事件等能力(crates/component/src/sheet.rs)。
10. 自定义边距与定位:避开标题栏
Sheet 默认的margin_top等于标题栏高度TITLE_BAR_HEIGHT,使其自动出现在标题栏下方:
window.open_sheet(cx, |sheet, _, _| { sheet .title("Below Title Bar") .margin_top(px(32.)) // 为窗口标题栏预留空间 .child("This sheet appears below the title bar") })该默认值定义在SheetSettings中:margin_top: TITLE_BAR_HEIGHT(crates/component/src/sheet.rs),渲染时从cx.theme().sheet.margin_top读取并作为顶部偏移(crates/component/src/sheet.rs)。如果你使用无边框窗口或自定义标题栏高度,请用margin_top显式调整。
11. 结合 List 构建文件浏览面板
Sheet 内容区自带滚动,可与List虚拟列表组合成文件浏览面板:
let delegate = ListDelegate::new(items); let list = cx.new(|cx| List::new(delegate, window, cx)); window.open_sheet_at(Placement::Left, cx, |sheet, _, _| { sheet .title("File Explorer") .size(px(400.)) .child( div() .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) .size_full() .child(list.clone()) ) })这一组合在仓库中有完整示例:crates/story/src/stories/sheet_story.rs实现了带搜索过滤的 Sheet 列表 Story(perform_search中模拟 50~100ms 的延迟搜索),并且只渲染匹配项(crates/story/src/stories/sheet_story.rs),可作为"Sheet + 可搜索列表"的参考模板。
12. 关闭事件回调:on_close
Sheet 关闭时可以挂接回调,例如弹出通知:
window.open_sheet(cx, |sheet, _, _| { sheet .title("Sheet with Handler") .child("This sheet has a custom close handler") .on_close(|_, window, cx| { window.push_notification("Sheet was closed", cx); }) })关闭链路与焦点恢复
on_close回调类型为Fn(&ClickEvent, &mut Window, &mut App),最终注册到BaseSheet.on_close(crates/component/src/sheet.rs、crates/component/src/sheet.rs)。无论通过关闭按钮、ESC 还是window.close_sheet(cx)关闭,都会走Root::close_sheet:
- 清除
focused_input注册; - 从
active_sheet取出打开前的焦点句柄previous_focused_handle并恢复焦点; - 将
active_sheet置为None; - 清空背景文本选区并通知重绘。
(见 crates/component/src/root.rs)与之对称,open_sheet_at在打开时会记录当前焦点窗口并创建新的焦点句柄(crates/component/src/root.rs),因此"打开前焦点 → Sheet 获得焦点 → 关闭后焦点还原"这一闭环由框架自动完成。
13. 导航型 Sheet:幽灵按钮菜单
左侧固定宽度 + 全宽幽灵按钮是最常见的导航抽屉形态:
window.open_sheet_at(Placement::Left, cx, |sheet, _, _| { sheet .title("Navigation") .size(px(280.)) .child( v_flex() .gap_2() .child(Button::new("home").ghost().label("Home").w_full()) .child(Button::new("profile").ghost().label("Profile").w_full()) .child(Button::new("settings").ghost().label("Settings").w_full()) .child(Button::new("logout").ghost().label("Logout").w_full()) ) })14. 自定义样式:主题化着色
Sheet 实现了 gpui 的Styledtrait,可对表面元素做任意样式精修(crates/component/src/sheet.rs):
window.open_sheet(cx, |sheet, _, cx| { sheet .title("Styled Sheet") .bg(cx.theme().accent) .text_color(cx.theme().accent_foreground) .border_color(cx.theme().primary) .child("Custom styled sheet content") })注意:bg()会覆盖表面的默认背景cx.theme().tokens.background,而内容区的默认内边距(四边 16px)则来自self.style.padding,可通过px_*()/py_*()覆盖(crates/component/src/sheet.rs)。
15. 编程式关闭
可在 Sheet 内部按钮或外部任意代码中主动关闭:
// 从 Sheet 内部关闭 Button::new("close") .label("Close Sheet") .on_click(|_, window, cx| { window.close_sheet(cx); }) // 从外部关闭 window.close_sheet(cx);close_sheet会经Root::update委托给根组件执行(crates/component/src/window_ext.rs)。你也可以用window.has_active_sheet(cx)查询当前是否有活跃 Sheet(crates/component/src/window_ext.rs)。
16. API 参考
16.1 Window 扩展方法(WindowExt)
| 方法 | 说明 |
|---|---|
open_sheet(cx, fn) | 以默认方位(右侧)打开 Sheet |
open_sheet_at(placement, cx, fn) | 在指定方位打开 Sheet |
close_sheet(cx) | 关闭当前 Sheet |
has_active_sheet(cx) -> bool | 是否存在活跃 Sheet |
(实现见 crates/component/src/window_ext.rs)
16.2 Sheet Builder 方法
| 方法 | 说明 | 默认值 |
|---|---|---|
title(el) | 设置 Sheet 标题 | 无 |
child(el) | 添加内容到正文区(可多次调用) | 无 |
footer(el) | 设置底部操作区 | 无 |
size(px) | 设置尺寸(左右为宽度,上下为高度) | 350px |
margin_top(px) | 设置顶部边距(为标题栏预留) | TITLE_BAR_HEIGHT |
resizable(bool) | 是否允许拖动缩放 | true |
overlay(bool) | 是否显示遮罩背景 | true |
overlay_closable(bool) | 点击遮罩是否可关闭 | true |
on_close(fn) | 关闭事件回调 | 空回调 |
(默认值均可在 crates/component/src/sheet.rs 中核对)
16.3 Placement 选项
| 值 | 说明 |
|---|---|
Placement::Left | 从左侧边缘滑入 |
Placement::Right | 从右侧边缘滑入(open_sheet默认) |
Placement::Top | 从顶部边缘滑入 |
Placement::Bottom | 从底部边缘滑入 |
16.4 样式方法
Sheet 继承Styledtrait 的全部方法,常用包括:
| 方法 | 说明 |
|---|---|
bg(color) | 设置背景色 |
text_color(color) | 设置文字颜色 |
border_color(color) | 设置边框颜色 |
px_*()/py_*() | 自定义内边距 |
gap_*() | 子元素间距(作用于内容容器) |
17. 实战示例
17.1 设置面板(Settings Panel)
右侧 350px 宽 + 复选分组 + 底部应用/取消按钮:
window.open_sheet_at(Placement::Right, cx, |sheet, _, _| { sheet .title("Settings") .size(px(350.)) .child( v_flex() .gap_4() .child("Appearance") .child(Checkbox::new("dark-mode").label("Dark Mode")) .child(Checkbox::new("animations").label("Enable Animations")) .child("Notifications") .child(Checkbox::new("push-notifications").label("Push Notifications")) ) .footer( h_flex() .justify_end() .gap_2() .child(Button::new("apply").primary().label("Apply")) .child(Button::new("cancel").label("Cancel")) ) })17.2 文件浏览器(File Browser)
左侧 300px,顶部放新建/上传图标按钮,下方为可滚动的文件树列表:
window.open_sheet_at(Placement::Left, cx, |sheet, _, _| { sheet .title("Files") .size(px(300.)) .child( v_flex() .size_full() .child( h_flex() .gap_2() .p_2() .child(Button::new("new-folder").small().icon(IconName::FolderPlus)) .child(Button::new("upload").small().icon(IconName::Upload)) ) .child( div() .flex_1() .overflow_hidden() .child(file_tree_list) ) ) })17.3 帮助面板(Help Panel)
底部弹出的快捷键速查表,配合Kbd展示组合键:
window.open_sheet_at(Placement::Bottom, cx, |sheet, _, _| { sheet .title("Help & Documentation") .size(px(200.)) .child( h_flex() .gap_4() .child("Keyboard Shortcuts") .child(Kbd::new("⌘").child("K")) .child("Search") .child(Kbd::new("⌘").child("P")) .child("Command Palette") ) })18. 与通知层的联动细节
一个容易忽略但很实用的实现细节:Root::render_notification_layer会根据当前活跃 Sheet 的方位与尺寸,自动把通知列表"让位"到 Sheet 之外的区域。当右侧有 Sheet 时,通知层会设置mr(sheet_size)右偏移,避免通知被 Sheet 遮挡(crates/component/src/root.rs)。这意味着你在 Sheet 里用window.push_notification(...)弹通知时,通知会自动避开 Sheet,无需手动调整位置。
19. 最佳实践
- 方位选择:左右(Left/Right)适合导航与常驻面板,上下(Top/Bottom)适合临时内容与快捷操作;
- 尺寸一致性:应用内保持 Sheet 宽度/高度一致(如统一 300/350px),避免视觉跳动;
- 清晰的标题:始终通过
title()提供描述性标题; - 多种关闭途径:为每个 Sheet 提供至少 ESC、遮罩点击、关闭按钮中的多种关闭方式(框架已内置 ESC 与关闭按钮,遮罩默认可点);
- 内容组织:正文区善用
gap_*()与分组标题,配合footer()收敛操作入口; - 响应式考量:在小尺寸窗口上,考虑使用
margin_top规避标题栏并缩小默认尺寸,避免内容被截断; - 性能:Sheet 内容(尤其是列表)建议懒加载或延迟构建,内容区滚动容器天然支持大列表,可结合 虚拟列表 提升性能。
总结
Sheet 是 gpui-kit 中承担"边缘抽屉"职责的核心组件:open_sheet_at控制四种滑入方位,size/resizable控制尺寸与缩放,overlay系列控制遮罩行为,on_close与编程式close_sheet保证关闭链路完整,而Root层自动管理焦点恢复、文本选区清理与通知避让。结合 crates/component/src/sheet.rs、crates/component/src/window_ext.rs、crates/component/src/root.rs 三份源码与 crates/kit/tests/overlays.rs 测试,你可以放心地在生产级 GPUI 桌面应用中直接使用。
【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考