dropdown_button: Add more button option methods to DropdownButton. (#1633)

- Fix to not handle `dropdown_menu` when Button is disabled.
- Split a single DropdownButtonStory.
This commit is contained in:
Jason Lee 2025-11-18 14:31:14 +08:00 committed by GitHub
parent 446831af33
commit f8a7dd71bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 369 additions and 59 deletions

View file

@ -0,0 +1,230 @@
use gpui::{
Action, App, AppContext as _, Context, Corner, Entity, Focusable, IntoElement,
ParentElement as _, Render, Styled as _, Window, prelude::FluentBuilder as _,
};
use serde::Deserialize;
use crate::section;
use gpui_component::{
ActiveTheme, Disableable, Selectable as _, Sizable as _, Theme,
button::{Button, ButtonVariants as _, DropdownButton},
checkbox::Checkbox,
h_flex, v_flex,
};
#[derive(Clone, Action, PartialEq, Eq, Deserialize)]
#[action(namespace = dropdown_button_story, no_json)]
enum ButtonAction {
Disabled,
Loading,
Selected,
Compact,
}
pub struct DropdownButtonStory {
focus_handle: gpui::FocusHandle,
disabled: bool,
loading: bool,
selected: bool,
compact: bool,
}
impl DropdownButtonStory {
pub fn view(_: &mut Window, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self {
focus_handle: cx.focus_handle(),
disabled: false,
loading: false,
selected: false,
compact: false,
})
}
}
impl super::Story for DropdownButtonStory {
fn title() -> &'static str {
"DropdownButton"
}
fn description() -> &'static str {
"A button with an attached dropdown menu for additional options."
}
fn closable() -> bool {
false
}
fn new_view(window: &mut Window, cx: &mut App) -> Entity<impl Render> {
Self::view(window, cx)
}
}
impl Focusable for DropdownButtonStory {
fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Render for DropdownButtonStory {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let disabled = self.disabled;
let loading = self.loading;
let selected = self.selected;
let compact = self.compact;
v_flex()
.gap_6()
.child(
h_flex()
.gap_3()
.child(
Checkbox::new("disabled-button")
.label("Disabled")
.checked(self.disabled)
.on_click(cx.listener(|view, _, _, cx| {
view.disabled = !view.disabled;
cx.notify();
})),
)
.child(
Checkbox::new("loading-button")
.label("Loading")
.checked(self.loading)
.on_click(cx.listener(|view, _, _, cx| {
view.loading = !view.loading;
cx.notify();
})),
)
.child(
Checkbox::new("selected-button")
.label("Selected")
.checked(self.selected)
.on_click(cx.listener(|view, _, _, cx| {
view.selected = !view.selected;
cx.notify();
})),
)
.child(
Checkbox::new("compact-button")
.label("Compact")
.checked(self.compact)
.on_click(cx.listener(|view, _, _, cx| {
view.compact = !view.compact;
cx.notify();
})),
)
.child(
Checkbox::new("shadow-button")
.label("Shadow")
.checked(cx.theme().shadow)
.on_click(cx.listener(|_, _, window, cx| {
let mut theme = cx.theme().clone();
theme.shadow = !theme.shadow;
cx.set_global::<Theme>(theme);
window.refresh();
})),
),
)
.child(
section("Dropdown Button").child(
DropdownButton::new("btn0")
.primary()
.button(Button::new("btn").label("Primary Dropdown"))
.when(self.compact, |this| this.compact())
.loading(self.loading)
.disabled(self.disabled)
.selected(selected)
.dropdown_menu_with_anchor(Corner::BottomRight, move |this, _, _| {
this.menu_with_check(
"Disabled",
disabled,
Box::new(ButtonAction::Disabled),
)
.menu_with_check("Loading", loading, Box::new(ButtonAction::Loading))
.menu_with_check("Selected", selected, Box::new(ButtonAction::Selected))
.menu_with_check(
"Compact",
compact,
Box::new(ButtonAction::Compact),
)
}),
),
)
.child(
section("Small Size").child(
DropdownButton::new("btn-sm")
.small()
.button(Button::new("btn").label("Small Dropdown"))
.when(self.compact, |this| this.compact())
.loading(self.loading)
.disabled(self.disabled)
.selected(selected)
.dropdown_menu(move |this, _, _| {
this.menu_with_check(
"Disabled",
disabled,
Box::new(ButtonAction::Disabled),
)
.menu_with_check("Loading", loading, Box::new(ButtonAction::Loading))
.menu_with_check("Selected", selected, Box::new(ButtonAction::Selected))
.menu_with_check(
"Compact",
compact,
Box::new(ButtonAction::Compact),
)
}),
),
)
.child(
section("Outline").child(
DropdownButton::new("btn-outline")
.outline()
.danger()
.button(Button::new("btn").label("Outline Dropdown"))
.when(self.compact, |this| this.compact())
.loading(self.loading)
.disabled(self.disabled)
.selected(selected)
.dropdown_menu(move |this, _, _| {
this.menu_with_check(
"Disabled",
disabled,
Box::new(ButtonAction::Disabled),
)
.menu_with_check("Loading", loading, Box::new(ButtonAction::Loading))
.menu_with_check("Selected", selected, Box::new(ButtonAction::Selected))
.menu_with_check(
"Compact",
compact,
Box::new(ButtonAction::Compact),
)
}),
),
)
.child(
section("Ghost").child(
DropdownButton::new("btn-ghost")
.ghost()
.button(Button::new("btn").label("Ghost Dropdown"))
.when(self.compact, |this| this.compact())
.loading(self.loading)
.disabled(self.disabled)
.selected(selected)
.dropdown_menu(move |this, _, _| {
this.menu_with_check(
"Disabled",
disabled,
Box::new(ButtonAction::Disabled),
)
.menu_with_check("Loading", loading, Box::new(ButtonAction::Loading))
.menu_with_check("Selected", selected, Box::new(ButtonAction::Selected))
.menu_with_check(
"Compact",
compact,
Box::new(ButtonAction::Compact),
)
}),
),
)
}
}

View file

@ -13,6 +13,7 @@ mod color_picker_story;
mod date_picker_story;
mod description_list_story;
mod dialog_story;
mod dropdown_button_story;
mod form_story;
mod group_box_story;
mod icon_story;
@ -72,6 +73,7 @@ pub use color_picker_story::ColorPickerStory;
pub use date_picker_story::DatePickerStory;
pub use description_list_story::DescriptionListStory;
pub use dialog_story::DialogStory;
pub use dropdown_button_story::DropdownButtonStory;
pub use form_story::FormStory;
pub use group_box_story::GroupBoxStory;
pub use icon_story::IconStory;

View file

@ -51,6 +51,7 @@ impl Gallery {
StoryContainer::panel::<DatePickerStory>(window, cx),
StoryContainer::panel::<DescriptionListStory>(window, cx),
StoryContainer::panel::<DialogStory>(window, cx),
StoryContainer::panel::<DropdownButtonStory>(window, cx),
StoryContainer::panel::<FormStory>(window, cx),
StoryContainer::panel::<GroupBoxStory>(window, cx),
StoryContainer::panel::<IconStory>(window, cx),

View file

@ -6,9 +6,9 @@ use crate::{
};
use gpui::{
div, prelude::FluentBuilder as _, px, relative, Action, AnyElement, App, ClickEvent, Corners,
Div, Edges, ElementId, Hsla, InteractiveElement, Interactivity, IntoElement, ParentElement,
Pixels, RenderOnce, SharedString, Stateful, StatefulInteractiveElement as _, StyleRefinement,
Styled, Window,
Div, Edges, ElementId, Hsla, InteractiveElement, Interactivity, IntoElement, MouseButton,
ParentElement, Pixels, RenderOnce, SharedString, Stateful, StatefulInteractiveElement as _,
StyleRefinement, Styled, Window,
};
#[derive(Default, Clone, Copy)]
@ -417,6 +417,7 @@ impl RenderOnce for Button {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let style: ButtonVariant = self.variant;
let clickable = self.clickable();
let is_disabled = self.disabled;
let hoverable = self.hoverable();
let normal_style = style.normal(self.outline, cx);
let icon_size = match self.size {
@ -475,10 +476,18 @@ impl RenderOnce for Button {
}
}
})
.when(self.border_corners.top_left, |this| this.rounded_tl(rounding))
.when(self.border_corners.top_right, |this| this.rounded_tr(rounding))
.when(self.border_corners.bottom_left, |this| this.rounded_bl(rounding))
.when(self.border_corners.bottom_right, |this| this.rounded_br(rounding))
.when(self.border_corners.top_left, |this| {
this.rounded_tl(rounding)
})
.when(self.border_corners.top_right, |this| {
this.rounded_tr(rounding)
})
.when(self.border_corners.bottom_left, |this| {
this.rounded_bl(rounding)
})
.when(self.border_corners.bottom_right, |this| {
this.rounded_br(rounding)
})
.when(self.border_edges.left, |this| this.border_l_1())
.when(self.border_edges.right, |this| this.border_r_1())
.when(self.border_edges.top, |this| this.border_t_1())
@ -515,12 +524,26 @@ impl RenderOnce for Button {
.shadow_none()
})
.refine_style(&self.style)
.on_mouse_down(gpui::MouseButton::Left, |_, window, _| {
.on_mouse_down(MouseButton::Left, move |_, window, cx| {
// Stop handle any click event when disabled.
// To avoid handle dropdown menu open when button is disabled.
if is_disabled {
cx.stop_propagation();
return;
}
// Avoid focus on mouse down.
window.prevent_default();
})
.when_some(self.on_click.filter(|_| clickable), |this, on_click| {
.when_some(self.on_click, |this, on_click| {
this.on_click(move |event, window, cx| {
// Stop handle any click event when disabled.
// To avoid handle dropdown menu open when button is disabled.
if !clickable {
cx.stop_propagation();
return;
}
(on_click)(event, window, cx);
})
})

View file

@ -6,7 +6,7 @@ use gpui::{
use crate::{
menu::{DropdownMenu, PopupMenu},
IconName, Selectable, Sizable, Size, StyledExt as _,
Disableable, IconName, Selectable, Sizable, Size, StyledExt as _,
};
use super::{Button, ButtonRounded, ButtonVariant, ButtonVariants};
@ -19,11 +19,13 @@ pub struct DropdownButton {
menu:
Option<Box<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static>>,
selected: bool,
disabled: bool,
// The button props
compact: Option<bool>,
outline: Option<bool>,
variant: Option<ButtonVariant>,
size: Option<Size>,
compact: bool,
outline: bool,
loading: bool,
variant: ButtonVariant,
size: Size,
rounded: ButtonRounded,
anchor: Corner,
}
@ -37,10 +39,12 @@ impl DropdownButton {
button: None,
menu: None,
selected: false,
compact: None,
outline: None,
variant: None,
size: None,
disabled: false,
compact: false,
outline: false,
loading: false,
variant: ButtonVariant::default(),
size: Size::default(),
rounded: ButtonRounded::default(),
anchor: Corner::TopRight,
}
@ -82,7 +86,7 @@ impl DropdownButton {
///
/// See also: [`Button::compact`]
pub fn compact(mut self) -> Self {
self.compact = Some(true);
self.compact = true;
self
}
@ -90,7 +94,20 @@ impl DropdownButton {
///
/// See also: [`Button::outline`]
pub fn outline(mut self) -> Self {
self.outline = Some(true);
self.outline = true;
self
}
/// Set the button to loading state.
pub fn loading(mut self, loading: bool) -> Self {
self.loading = loading;
self
}
}
impl Disableable for DropdownButton {
fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
@ -103,14 +120,14 @@ impl Styled for DropdownButton {
impl Sizable for DropdownButton {
fn with_size(mut self, size: impl Into<Size>) -> Self {
self.size = Some(size.into());
self.size = size.into();
self
}
}
impl ButtonVariants for DropdownButton {
fn with_variant(mut self, variant: ButtonVariant) -> Self {
self.variant = Some(variant);
self.variant = variant;
self
}
}
@ -128,10 +145,7 @@ impl Selectable for DropdownButton {
impl RenderOnce for DropdownButton {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let rounded = self
.variant
.map(|variant| variant.is_ghost() && !self.selected)
.unwrap_or(false);
let rounded = self.variant.is_ghost() && !self.selected;
div()
.id(self.id)
@ -153,11 +167,13 @@ impl RenderOnce for DropdownButton {
right: true,
bottom: true,
})
.loading(self.loading)
.selected(self.selected)
.when_some(self.compact, |this, _| this.compact())
.when_some(self.outline, |this, _| this.outline())
.when_some(self.size, |this, size| this.with_size(size))
.when_some(self.variant, |this, variant| this.with_variant(variant)),
.disabled(self.disabled || self.loading)
.when(self.compact, |this| this.compact())
.when(self.outline, |this| this.outline())
.with_size(self.size)
.with_variant(self.variant),
)
.when_some(self.menu, |this, menu| {
this.child(
@ -177,10 +193,11 @@ impl RenderOnce for DropdownButton {
bottom_right: true,
})
.selected(self.selected)
.when_some(self.compact, |this, _| this.compact())
.when_some(self.outline, |this, _| this.outline())
.when_some(self.size, |this, size| this.with_size(size))
.when_some(self.variant, |this, variant| this.with_variant(variant))
.disabled(self.disabled || self.loading)
.when(self.compact, |this| this.compact())
.when(self.outline, |this| this.outline())
.with_size(self.size)
.with_variant(self.variant)
.dropdown_menu_with_anchor(self.anchor, menu),
)
})

View file

@ -10,7 +10,7 @@ The [Button] element with multiple variants, sizes, and states. Supports icons,
## Import
```rust
use gpui_component::button::{Button, ButtonGroup, DropdownButton};
use gpui_component::button::{Button, ButtonGroup};
```
## Usage
@ -150,28 +150,6 @@ ButtonGroup::new("toggle-group")
})
```
## Dropdown Button
```rust
use gpui::Corner;
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))
})
// With custom anchor
DropdownButton::new("dropdown")
.button(Button::new("btn").label("Click Me"))
.dropdown_menu_with_anchor(Corner::BottomRight, |menu, _, _| {
menu.menu("Option 1", Box::new(MyAction))
})
```
## Custom Variant
```rust
@ -193,7 +171,6 @@ Button::new("custom-btn")
- [Button]
- [ButtonGroup]
- [DropdownButton]
- [ButtonCustomVariant]
## Examples
@ -222,6 +199,5 @@ Button::new("btn")
[Button]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.Button.html
[ButtonGroup]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.ButtonGroup.html
[DropdownButton]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.DropdownButton.html
[ButtonCustomVariant]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.ButtonCustomVariant.html
[Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html

View file

@ -0,0 +1,60 @@
---
title: DropdownButton
description: A DropdownButton is a combination of a button and a trigger button. It allows us to display a dropdown menu when the trigger is clicked, but the left Button can still respond to independent events.
---
# DropdownButton
A [DropdownButton] is a combination of a button and a trigger button. It allows us to display a dropdown menu when the trigger is clicked, but the left Button can still respond to independent events.
And more option methods of [Button] are also available for the DropdownButton, such as setting different variants using [ButtonCustomVariant], sizes using [Sizable], adding icons, loading states.
## Import
```rust
use gpui_component::button::{Button, DropdownButton};
```
## Usage
```rust
use gpui::Corner;
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))
})
```
### Variants
Same as [Button], DropdownButton supports different variants.
````rust
DropdownButton::new("dropdown")
.primary()
.button(Button::new("btn").label("Primary"))
.dropdown_menu(|menu, _, _| {
menu.menu("Option 1", Box::new(MyAction))
})
```
### With custom anchor
```rust
// With custom anchor
DropdownButton::new("dropdown")
.button(Button::new("btn").label("Click Me"))
.dropdown_menu_with_anchor(Corner::BottomRight, |menu, _, _| {
menu.menu("Option 1", Box::new(MyAction))
})
````
[Button]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.Button.html
[DropdownButton]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.DropdownButton.html
[ButtonCustomVariant]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.ButtonCustomVariant.html
[Sizable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Sizable.html

View file

@ -15,6 +15,7 @@ collapsed: false
- [Button](button) - Interactive buttons with multiple variants
- [Checkbox](checkbox) - Binary selection control
- [Collapsible](collapsible) - Expandable/collapsible content
- [DropdownButton](dropdown_button) - Button with dropdown menu
- [Icon](icon) - Icon display component
- [Image](image) - Image display with fallbacks
- [Kbd](kbd) - Keyboard shortcut display