gpui-kit DropdownButton 组合按钮指南:主按钮与下拉触发的拆分式交互
2026/9/14 15:44:23 网站建设 项目流程

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),即buttondropdown_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 中可以看到不同尺寸对应的实际内边距与高度:例如Smallh_6().px_2()Mediumh_8().px_2p5()Largeh_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 核心(TopLeftTopRightBottomLeftBottomRight等),由 dropdown_menu.rs 中的DropdownMenutrait 透传给底层的Popover。另外注意区分两个入口的默认锚点差异:

  • 直接在Button上调用dropdown_menu(...)DropdownMenutrait 方法)默认锚点是Anchor::TopLeft(dropdown_menu.rs);
  • DropdownButton上调用dropdown_menu(...)默认锚点是Anchor::TopRight

选中、禁用与 ghost 联动

DropdownButton同时实现了DisableableSelectable,支持:

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)中,核心要点:

  1. 左右两半共用一个水平 flex 容器,外层div().h_flex()包裹;
  2. 左侧主按钮通过border_corners只保留左上、左下圆角,border_edges保留四边(Edges::all(true)),并接收外层传入的selecteddisabledvariantsize
  3. 右侧触发按钮是内部自动构建的Button::new("popup"),通过.dropdown_caret(true)显示下拉箭头,只保留右上、右下圆角,并隐藏左边框(Edges { left: false, ... })使两半视觉无缝衔接;
  4. 触发按钮通过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 中还演示了通过工具栏勾选项实时切换DisabledLoadingSelectedCompact状态,以及用menu_with_check生成带勾选标记的选项菜单,适合作为交互验证模板。

测试与验证

dropdown_button.rs 内置了 4 个测试,覆盖了组件最核心的行为契约:

  • test_dropdown_button_builder:验证构建链完整设置variantoutlinesizedisabledselectedanchor等字段;
  • 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询