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

View file

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

View file

@ -6,9 +6,9 @@ use crate::{
}; };
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, relative, Action, AnyElement, App, ClickEvent, Corners, div, prelude::FluentBuilder as _, px, relative, Action, AnyElement, App, ClickEvent, Corners,
Div, Edges, ElementId, Hsla, InteractiveElement, Interactivity, IntoElement, ParentElement, Div, Edges, ElementId, Hsla, InteractiveElement, Interactivity, IntoElement, MouseButton,
Pixels, RenderOnce, SharedString, Stateful, StatefulInteractiveElement as _, StyleRefinement, ParentElement, Pixels, RenderOnce, SharedString, Stateful, StatefulInteractiveElement as _,
Styled, Window, StyleRefinement, Styled, Window,
}; };
#[derive(Default, Clone, Copy)] #[derive(Default, Clone, Copy)]
@ -417,6 +417,7 @@ impl RenderOnce for Button {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let style: ButtonVariant = self.variant; let style: ButtonVariant = self.variant;
let clickable = self.clickable(); let clickable = self.clickable();
let is_disabled = self.disabled;
let hoverable = self.hoverable(); let hoverable = self.hoverable();
let normal_style = style.normal(self.outline, cx); let normal_style = style.normal(self.outline, cx);
let icon_size = match self.size { 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_left, |this| {
.when(self.border_corners.top_right, |this| this.rounded_tr(rounding)) this.rounded_tl(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_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.left, |this| this.border_l_1())
.when(self.border_edges.right, |this| this.border_r_1()) .when(self.border_edges.right, |this| this.border_r_1())
.when(self.border_edges.top, |this| this.border_t_1()) .when(self.border_edges.top, |this| this.border_t_1())
@ -515,12 +524,26 @@ impl RenderOnce for Button {
.shadow_none() .shadow_none()
}) })
.refine_style(&self.style) .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. // Avoid focus on mouse down.
window.prevent_default(); 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| { 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); (on_click)(event, window, cx);
}) })
}) })

View file

@ -6,7 +6,7 @@ use gpui::{
use crate::{ use crate::{
menu::{DropdownMenu, PopupMenu}, menu::{DropdownMenu, PopupMenu},
IconName, Selectable, Sizable, Size, StyledExt as _, Disableable, IconName, Selectable, Sizable, Size, StyledExt as _,
}; };
use super::{Button, ButtonRounded, ButtonVariant, ButtonVariants}; use super::{Button, ButtonRounded, ButtonVariant, ButtonVariants};
@ -19,11 +19,13 @@ pub struct DropdownButton {
menu: menu:
Option<Box<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static>>, Option<Box<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static>>,
selected: bool, selected: bool,
disabled: bool,
// The button props // The button props
compact: Option<bool>, compact: bool,
outline: Option<bool>, outline: bool,
variant: Option<ButtonVariant>, loading: bool,
size: Option<Size>, variant: ButtonVariant,
size: Size,
rounded: ButtonRounded, rounded: ButtonRounded,
anchor: Corner, anchor: Corner,
} }
@ -37,10 +39,12 @@ impl DropdownButton {
button: None, button: None,
menu: None, menu: None,
selected: false, selected: false,
compact: None, disabled: false,
outline: None, compact: false,
variant: None, outline: false,
size: None, loading: false,
variant: ButtonVariant::default(),
size: Size::default(),
rounded: ButtonRounded::default(), rounded: ButtonRounded::default(),
anchor: Corner::TopRight, anchor: Corner::TopRight,
} }
@ -82,7 +86,7 @@ impl DropdownButton {
/// ///
/// See also: [`Button::compact`] /// See also: [`Button::compact`]
pub fn compact(mut self) -> Self { pub fn compact(mut self) -> Self {
self.compact = Some(true); self.compact = true;
self self
} }
@ -90,7 +94,20 @@ impl DropdownButton {
/// ///
/// See also: [`Button::outline`] /// See also: [`Button::outline`]
pub fn outline(mut self) -> Self { 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 self
} }
} }
@ -103,14 +120,14 @@ impl Styled for DropdownButton {
impl Sizable for DropdownButton { impl Sizable for DropdownButton {
fn with_size(mut self, size: impl Into<Size>) -> Self { fn with_size(mut self, size: impl Into<Size>) -> Self {
self.size = Some(size.into()); self.size = size.into();
self self
} }
} }
impl ButtonVariants for DropdownButton { impl ButtonVariants for DropdownButton {
fn with_variant(mut self, variant: ButtonVariant) -> Self { fn with_variant(mut self, variant: ButtonVariant) -> Self {
self.variant = Some(variant); self.variant = variant;
self self
} }
} }
@ -128,10 +145,7 @@ impl Selectable for DropdownButton {
impl RenderOnce for DropdownButton { impl RenderOnce for DropdownButton {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let rounded = self let rounded = self.variant.is_ghost() && !self.selected;
.variant
.map(|variant| variant.is_ghost() && !self.selected)
.unwrap_or(false);
div() div()
.id(self.id) .id(self.id)
@ -153,11 +167,13 @@ impl RenderOnce for DropdownButton {
right: true, right: true,
bottom: true, bottom: true,
}) })
.loading(self.loading)
.selected(self.selected) .selected(self.selected)
.when_some(self.compact, |this, _| this.compact()) .disabled(self.disabled || self.loading)
.when_some(self.outline, |this, _| this.outline()) .when(self.compact, |this| this.compact())
.when_some(self.size, |this, size| this.with_size(size)) .when(self.outline, |this| this.outline())
.when_some(self.variant, |this, variant| this.with_variant(variant)), .with_size(self.size)
.with_variant(self.variant),
) )
.when_some(self.menu, |this, menu| { .when_some(self.menu, |this, menu| {
this.child( this.child(
@ -177,10 +193,11 @@ impl RenderOnce for DropdownButton {
bottom_right: true, bottom_right: true,
}) })
.selected(self.selected) .selected(self.selected)
.when_some(self.compact, |this, _| this.compact()) .disabled(self.disabled || self.loading)
.when_some(self.outline, |this, _| this.outline()) .when(self.compact, |this| this.compact())
.when_some(self.size, |this, size| this.with_size(size)) .when(self.outline, |this| this.outline())
.when_some(self.variant, |this, variant| this.with_variant(variant)) .with_size(self.size)
.with_variant(self.variant)
.dropdown_menu_with_anchor(self.anchor, menu), .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 ## Import
```rust ```rust
use gpui_component::button::{Button, ButtonGroup, DropdownButton}; use gpui_component::button::{Button, ButtonGroup};
``` ```
## Usage ## 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 ## Custom Variant
```rust ```rust
@ -193,7 +171,6 @@ Button::new("custom-btn")
- [Button] - [Button]
- [ButtonGroup] - [ButtonGroup]
- [DropdownButton]
- [ButtonCustomVariant] - [ButtonCustomVariant]
## Examples ## Examples
@ -222,6 +199,5 @@ Button::new("btn")
[Button]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.Button.html [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 [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 [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 [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 - [Button](button) - Interactive buttons with multiple variants
- [Checkbox](checkbox) - Binary selection control - [Checkbox](checkbox) - Binary selection control
- [Collapsible](collapsible) - Expandable/collapsible content - [Collapsible](collapsible) - Expandable/collapsible content
- [DropdownButton](dropdown_button) - Button with dropdown menu
- [Icon](icon) - Icon display component - [Icon](icon) - Icon display component
- [Image](image) - Image display with fallbacks - [Image](image) - Image display with fallbacks
- [Kbd](kbd) - Keyboard shortcut display - [Kbd](kbd) - Keyboard shortcut display