gpui-kit DropdownButton 组合按钮指南:主按钮与下拉触发的拆分式交互
【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit
DropdownButton 是 gpui-kit 中一个"一分为二"的组合按钮:左侧是保留独立点击事件的普通按钮,右侧是展开下拉菜单的触发按钮,两半视觉上连成一体。本文基于 website/component/dropdown_button.md 及组件源码,完整讲解它的导入、变体、尺寸、锚点与内层按钮配置,并深入 dropdown_button.rs 的实现细节与测试用例,帮助你写出可直接落地运行的分裂式按钮(Split Button)界面。
组件定位:一个按钮 + 一个触发按钮
DropdownButton 是一种组合型按钮组件。它同时承担两种交互:
- 点击左侧主按钮时,执行一个独立动作(例如"保存");
- 点击右侧触发按钮(通常带下拉箭头)时,展开一个下拉菜单,提供更多选项(例如"另存为…"、"存为模板…")。
从源码结构看,DropdownButton 的核心字段如下:
pub struct DropdownButton { id: ElementId, style: StyleRefinement, button: Option<Button>, // 左侧主按钮 menu: Option<Box<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static>>, selected: bool, disabled: bool, outline: bool, variant: Option<ButtonVariant>, // 未设置时回退到内层按钮 size: Option<Size>, // 未设置时回退到内层按钮 anchor: Anchor, // 默认 TopRight }设计上遵循"共享与专属"的分工原则:
- 共享属性(变体、尺寸、选中/禁用状态、outline 描边)直接设置在
DropdownButton上,同时作用于两半; - 动作专属属性(文案 label、图标 icon、tooltip、加载状态 loading、点击回调 on_click)设置在内层 Button 上,只影响主按钮。
导入与基础用法
在使用了gpui_kitcrate 的项目中,按如下方式导入:
use gpui_kit::component::button::{Button, DropdownButton}; use gpui_kit::Anchor;最基础的用法是:给一个主按钮挂上由多个菜单项组成的下拉菜单。
DropdownButton::new("dropdown") .button(Button::new("btn").label("Click Me")) .dropdown_menu(|menu, _, _| { menu.menu("Option 1", Box::new(MyAction)) .menu("Option 2", Box::new(MyAction)) .separator() .menu("Option 3", Box::new(MyAction)) })几点说明:
DropdownButton::new(id)需要一个唯一的ElementId,用于状态追踪与无障碍标识;.button(...)接收一个构建好的 Button,主按钮保留自己的 label、icon、tooltip 和 on_click;.dropdown_menu(...)接收一个构建闭包,签名固定为Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu。闭包内通过链式调用向菜单追加条目,其中menu("文案", Box::new(MyAction))中的MyAction是实现了gpui::Action的动作类型(参见 popup_menu.rs),菜单项被点击后即派发对应 Action;.separator()在菜单项之间插入分隔线;- 渲染前会执行
debug_assert!(self.button.is_some() || self.menu.is_some(), ...)(dropdown_button.rs),即button与dropdown_menu至少提供其一,否则在调试构建下会直接断言失败——这保证组件不会渲染成一个空壳。
菜单构建闭包中还支持更多条目类型,参考 popup_menu.rs:
| 方法 | 作用 |
|---|---|
menu(label, action) | 添加标准菜单项,点击派发 Action |
menu_with_disabled(label, action, disabled) | 添加可禁用菜单项 |
menu_with_enable(label, action, enable) | 按布尔值控制可用状态 |
menu_with_check(label, checked, action) | 添加带勾选状态的菜单项 |
separator() | 插入分隔线 |
label(text) | 插入纯文本说明条目(不可交互) |
link(label, href) | 添加打开链接的菜单项 |
submenu(label, menu) | 添加子菜单(父菜单不可滚动时支持) |
视觉变体:与 Button 完全一致
与 Button 相同,DropdownButton通过ButtonVariantstrait(button.rs)提供全套变体方法:
DropdownButton::new("dropdown") .primary() .button(Button::new("btn").label("Primary")) .dropdown_menu(|menu, _, _| { menu.menu("Option 1", Box::new(MyAction)) })ButtonVariant 枚举共 11 种取值,对应的快捷方法如下:
| 变体 | 快捷方法 | 典型语义 |
|---|---|---|
Default | (默认) | 普通输入框风格 |
Primary | .primary() | 主操作,强调背景色 |
Secondary | .secondary() | 次操作 |
Danger | .danger() | 危险/删除类操作 |
Info | .info() | 信息提示 |
Success | .success() | 成功确认 |
Warning | .warning() | 警告操作 |
Ghost | .ghost() | 透明底,悬停才浮现 |
Link | .link() | 链接样式 |
Text | .text() | 无内边距的纯文本按钮 |
Custom(style) | .custom(style) | 自定义配色(背景/前景/悬停/按下/阴影) |
关键行为:在DropdownButton上不设置变体或尺寸时,内层按钮的值会自动应用到两半。这一点由源码中的effective_variant/effective_size实现(dropdown_button.rs):
fn effective_variant(&self) -> ButtonVariant { self.variant .or_else(|| self.button.as_ref().map(Button::variant)) .unwrap_or_default() } fn effective_size(&self) -> Size { self.size .or_else(|| self.button.as_ref().map(Button::button_size)) .unwrap_or_default() }即优先使用外层显式设置的值;外层未设置则回退到内层Button的值;两者都未设置时才落到默认值(变体Default、尺寸Medium)。
尺寸设置
DropdownButton实现了Sizabletrait,支持链式.with_size(...)或语义化快捷方法(xsmall()、small()、medium()、large())。尺寸枚举定义在 sizing.rs:
pub enum Size { Size(Pixels), // 自定义像素尺寸 XSmall, // "xs" Small, // "sm" Medium, // "md"(默认值) Large, // "lg" }尺寸同样会同步作用于左右两半;若外层未设置,则继承内层按钮的尺寸(见上文effective_size)。在 button.rs 中可以看到不同尺寸对应的实际内边距与高度:例如Small为h_6().px_2(),Medium为h_8().px_2p5(),Large为h_8().px_3()。
内层按钮选项:主按钮的完整能力
主按钮本身是一个完整的 Button,因此所有按钮专属选项都可直接使用:
DropdownButton::new("dropdown") .button( Button::new("btn") .label("Save") .compact() .loading(is_saving) .tooltip("Save the current view") .on_click(|_, _, _| println!("Saved")), ) .dropdown_menu(|menu, _, _| { menu.menu("Save as…", Box::new(MyAction)) })内层Button常用构建方法(定义于 button.rs):
| 方法 | 说明 |
|---|---|
.label(text) | 设置按钮文案;不设置则进入图标按钮模式 |
.icon(icon) | 设置图标,与 label 组合显示 |
.compact() | 紧凑模式,缩小内边距 |
.loading(bool) | 加载态:整体透明度降为 0.8 且屏蔽点击(button.rs) |
.loading_icon(icon) | 自定义加载图标,默认是 spinner |
.tooltip(text) | 悬停提示 |
.tooltip_with_action(text, action, context) | 带快捷键提示的 tooltip |
.tooltip_placement(placement) | 指定 tooltip 偏好方位 |
.on_click(handler) | 点击回调,签名Fn(&ClickEvent, &mut Window, &mut App) |
.on_hover(handler) | 悬停回调,bool 参数表示是否悬停 |
.disabled(bool) | 禁用(配合外层Disableable实现) |
.outline() | 描边风格 |
.rounded(...) | 自定义圆角 |
.tab_index / .tab_stop | 键盘焦点管理 |
注意.loading(true)时主按钮会保持自身外观但完全屏蔽交互(鼠标按下、点击均被stop_propagation拦截),这与禁用态的"置灰"表现不同——加载态只是"看起来还能点,实际上不响应"。
自定义锚点:控制菜单弹出方位
默认情况下,DropdownButton的菜单锚点是Anchor::TopRight(见 dropdown_button.rs)。需要调整弹出方位时使用dropdown_menu_with_anchor:
DropdownButton::new("dropdown") .button(Button::new("btn").label("Click Me")) .dropdown_menu_with_anchor(Anchor::BottomRight, |menu, _, _| { menu.menu("Option 1", Box::new(MyAction)) })Anchor来自 gpui 核心(TopLeft、TopRight、BottomLeft、BottomRight等),由 dropdown_menu.rs 中的DropdownMenutrait 透传给底层的Popover。另外注意区分两个入口的默认锚点差异:
- 直接在
Button上调用dropdown_menu(...)(DropdownMenutrait 方法)默认锚点是Anchor::TopLeft(dropdown_menu.rs); - 在
DropdownButton上调用dropdown_menu(...)默认锚点是Anchor::TopRight。
选中、禁用与 ghost 联动
DropdownButton同时实现了Disableable与Selectable,支持:
DropdownButton::new("dropdown") .disabled(true) // 两半同时禁用 .selected(true) // 两半同时进入选中态selected状态还会自动合并内层按钮的选中态:渲染时执行let selected = self.selected || self.button.as_ref().is_some_and(Selectable::is_selected);(dropdown_button.rs),因此从任意一层设置selected都会让整体呈现选中样式。
ghost变体有一个值得注意的交互细节:由于 ghost 默认透明、仅在悬停时浮现,左右两半通过共享悬停组HALVES_GROUP(常量"dropdown-button",见 dropdown_button.rs)联动——悬停任意一半都会让整个控件以半透明强度浮现,且菜单打开期间(menu_open状态为 true)触发侧会保持"被按住"的浮现效果,使两者读起来是一个完整控件而非两个独立按钮。相关逻辑见 dropdown_button.rs 与 button.rs。
源码实现剖析:两半如何拼接
DropdownButton的渲染逻辑在RenderOnce::render(dropdown_button.rs)中,核心要点:
- 左右两半共用一个水平 flex 容器,外层
div().h_flex()包裹; - 左侧主按钮通过
border_corners只保留左上、左下圆角,border_edges保留四边(Edges::all(true)),并接收外层传入的selected、disabled、variant、size; - 右侧触发按钮是内部自动构建的
Button::new("popup"),通过.dropdown_caret(true)显示下拉箭头,只保留右上、右下圆角,并隐藏左边框(Edges { left: false, ... })使两半视觉无缝衔接; - 触发按钮通过
DropdownMenutrait 的dropdown_menu_with_anchor挂接菜单,并通过on_open_change回调把开合状态写回menu_open状态,用于 ghost 联动的hover_group_held。
菜单弹出层由 DropdownMenuPopover 承载,它本质上是Popover的封装,有两个实现细节值得注意:
- 菜单实体只创建一次并缓存:
PopupMenu通过use_keyed_state存入DropdownMenuState,避免每次重渲染都重建实体; - 关闭时重建:
PopupMenu派发DismissEvent时,popover 关闭并清空缓存的菜单实体,从而支持菜单项的动态重建(下次打开时用最新闭包重新生成条目),订阅逻辑见 dropdown_menu.rs。
菜单打开后自动聚焦,并支持键盘导航。PopupMenu::init(popup_menu.rs)注册了enter(确认)、escape(取消)、up/down(上下选择)、left/right(左右选择)等按键绑定,因此菜单天然支持纯键盘操作。
实战示例:参考 Story 展示
仓库自带的交互演示 dropdown_button_story.rs 提供了三个可直接借鉴的场景:
1. 基础分裂按钮(Export主按钮 + CSV/PDF 导出菜单,使用primary()变体与Anchor::TopRight锚点):
DropdownButton::new("export") .with_size(self.size) .primary() .button(Button::new("export-default").label("Export").on_click(...)) .dropdown_menu_with_anchor(Anchor::TopRight, move |this, _, _| { this.menu("Export all rows (.csv)", Box::new(ButtonAction::ExportCsv)) .menu("Download report (.pdf)", Box::new(ButtonAction::ExportPdf)) })2. 内层按钮选项(Save主按钮携带 tooltip、loading、on_click,外层用outline()描边风格):
DropdownButton::new("save") .with_size(self.size) .outline() .button( Button::new("save-default") .label("Save") .tooltip("Save the current document") .loading(loading) .on_click(...), ) .dropdown_menu(move |this, _, _| { this.menu("Save as new file…", Box::new(ButtonAction::SaveCopy)) .menu("Save as template…", Box::new(ButtonAction::SaveTemplate)) })3. 样式继承(内层按钮使用ghost().small(),外层不设置变体与尺寸,两半自动继承 ghost 小尺寸):
DropdownButton::new("recent") .button(Button::new("recent-default").label("Open latest").ghost().small().on_click(...)) .dropdown_menu(move |this, _, _| { this.menu("Quarterly Report.gpui", Box::new(ButtonAction::OpenQuarterlyReport)) .menu("Watchlist Layout.gpui", Box::new(ButtonAction::OpenWatchlistLayout)) })Story 中还演示了通过工具栏勾选项实时切换Disabled、Loading、Selected、Compact状态,以及用menu_with_check生成带勾选标记的选项菜单,适合作为交互验证模板。
测试与验证
dropdown_button.rs 内置了 4 个测试,覆盖了组件最核心的行为契约:
test_dropdown_button_builder:验证构建链完整设置variant、outline、size、disabled、selected、anchor等字段;inner_button_keeps_its_own_variant_and_size:外层不设置变体/尺寸时,字段保持None,证明"回退到内层"机制的存在;inner_ghost_becomes_the_split_variant:内层按钮为ghost时,effective_variant()返回Ghost;inner_size_becomes_the_split_size:内层按钮为small时,effective_size()返回Small。
这四个测试恰好印证了前文介绍的"外层优先、未设则继承内层"的取值规则,可以作为你后续扩展或排查样式问题的起点。
小结
DropdownButton 把"主动作 + 更多选项"两种交互浓缩进一个视觉整体:通过DropdownButton::new(id).button(...).dropdown_menu(...)三件套即可快速搭建,变体与尺寸共享到两半,label/icon/tooltip/loading/on_click 等动作属性留在内层Button,锚点可用dropdown_menu_with_anchor自由控制。结合 dropdown_button.rs 源码、dropdown_menu.rs 的弹出层实现与 dropdown_button_story.rs 的示例,你可以在此基础上构造工具栏、文件导出、设置项等各类分裂按钮交互。
【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考