diff --git a/crates/story/src/dropdown_button_story.rs b/crates/story/src/dropdown_button_story.rs new file mode 100644 index 00000000..7afcd7a7 --- /dev/null +++ b/crates/story/src/dropdown_button_story.rs @@ -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 { + 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 { + 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) -> 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); + 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), + ) + }), + ), + ) + } +} diff --git a/crates/story/src/lib.rs b/crates/story/src/lib.rs index 01dfceae..5f8b3a90 100644 --- a/crates/story/src/lib.rs +++ b/crates/story/src/lib.rs @@ -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; diff --git a/crates/story/src/main.rs b/crates/story/src/main.rs index e1dbb502..ddaca766 100644 --- a/crates/story/src/main.rs +++ b/crates/story/src/main.rs @@ -51,6 +51,7 @@ impl Gallery { StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), + StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), diff --git a/crates/ui/src/button/button.rs b/crates/ui/src/button/button.rs index 69ee2484..b266250c 100644 --- a/crates/ui/src/button/button.rs +++ b/crates/ui/src/button/button.rs @@ -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); }) }) diff --git a/crates/ui/src/button/dropdown_button.rs b/crates/ui/src/button/dropdown_button.rs index 093612ab..1acef80e 100644 --- a/crates/ui/src/button/dropdown_button.rs +++ b/crates/ui/src/button/dropdown_button.rs @@ -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) -> PopupMenu + 'static>>, selected: bool, + disabled: bool, // The button props - compact: Option, - outline: Option, - variant: Option, - size: Option, + 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) -> 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), ) }) diff --git a/docs/docs/components/button.md b/docs/docs/components/button.md index 747316b8..7be60530 100644 --- a/docs/docs/components/button.md +++ b/docs/docs/components/button.md @@ -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 diff --git a/docs/docs/components/dropdown_button.md b/docs/docs/components/dropdown_button.md new file mode 100644 index 00000000..cbdb2726 --- /dev/null +++ b/docs/docs/components/dropdown_button.md @@ -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 diff --git a/docs/docs/components/index.md b/docs/docs/components/index.md index ea6a9c4e..b1bcdd3b 100644 --- a/docs/docs/components/index.md +++ b/docs/docs/components/index.md @@ -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