Skip to main content

rgpui_component/button/
dropdown_button.rs

1use rgpui::Corners;
2use rgpui::{
3    Anchor, App, Context, Edges, ElementId, InteractiveElement as _, IntoElement, ParentElement,
4    RenderOnce, SharedString, StyleRefinement, Styled, Window, div, prelude::FluentBuilder,
5};
6
7use crate::{
8    Disableable, IconName, Selectable, Sizable, Size, StyledExt as _,
9    menu::{DropdownMenu, PopupMenu},
10    tooltip::ComponentTooltip,
11};
12
13use super::{Button, ButtonRounded, ButtonVariant, ButtonVariants};
14
15#[derive(IntoElement)]
16pub struct DropdownButton {
17    id: ElementId,
18    style: StyleRefinement,
19    button: Option<Button>,
20    menu:
21        Option<Box<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static>>,
22    selected: bool,
23    disabled: bool,
24    // The button props
25    compact: bool,
26    outline: bool,
27    loading: bool,
28    variant: ButtonVariant,
29    size: Size,
30    rounded: ButtonRounded,
31    anchor: Anchor,
32    tooltip: ComponentTooltip,
33}
34
35impl DropdownButton {
36    /// Create a new DropdownButton.
37    pub fn new(id: impl Into<ElementId>) -> Self {
38        Self {
39            id: id.into(),
40            style: StyleRefinement::default(),
41            button: None,
42            menu: None,
43            selected: false,
44            disabled: false,
45            compact: false,
46            outline: false,
47            loading: false,
48            variant: ButtonVariant::default(),
49            size: Size::default(),
50            rounded: ButtonRounded::default(),
51            anchor: Anchor::TopRight,
52            tooltip: ComponentTooltip::default(),
53        }
54    }
55
56    /// Set tooltip text for the dropdown button.
57    pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
58        self.tooltip.text = Some((tooltip.into(), None));
59        self
60    }
61
62    /// Set the left button of the dropdown button.
63    pub fn button(mut self, button: Button) -> Self {
64        self.button = Some(button);
65        self
66    }
67
68    /// Set the dropdown menu of the button.
69    pub fn dropdown_menu(
70        mut self,
71        menu: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
72    ) -> Self {
73        self.menu = Some(Box::new(menu));
74        self
75    }
76
77    /// Set the dropdown menu of the button with anchor corner.
78    pub fn dropdown_menu_with_anchor(
79        mut self,
80        anchor: impl Into<Anchor>,
81        menu: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
82    ) -> Self {
83        self.menu = Some(Box::new(menu));
84        self.anchor = anchor.into();
85        self
86    }
87
88    /// Set the rounded style of the button.
89    pub fn rounded(mut self, rounded: impl Into<ButtonRounded>) -> Self {
90        self.rounded = rounded.into();
91        self
92    }
93
94    /// Set the button to compact style.
95    ///
96    /// See also: [`Button::compact`]
97    pub fn compact(mut self) -> Self {
98        self.compact = true;
99        self
100    }
101
102    /// Set the button to outline style.
103    ///
104    /// See also: [`Button::outline`]
105    pub fn outline(mut self) -> Self {
106        self.outline = true;
107        self
108    }
109
110    /// Set the button to loading state.
111    pub fn loading(mut self, loading: bool) -> Self {
112        self.loading = loading;
113        self
114    }
115}
116
117impl Disableable for DropdownButton {
118    fn disabled(mut self, disabled: bool) -> Self {
119        self.disabled = disabled;
120        self
121    }
122}
123
124impl Styled for DropdownButton {
125    fn style(&mut self) -> &mut rgpui::StyleRefinement {
126        &mut self.style
127    }
128}
129
130impl Sizable for DropdownButton {
131    fn with_size(mut self, size: impl Into<Size>) -> Self {
132        self.size = size.into();
133        self
134    }
135}
136
137impl ButtonVariants for DropdownButton {
138    fn with_variant(mut self, variant: ButtonVariant) -> Self {
139        self.variant = variant;
140        self
141    }
142}
143
144impl Selectable for DropdownButton {
145    fn selected(mut self, selected: bool) -> Self {
146        self.selected = selected;
147        self
148    }
149
150    fn is_selected(&self) -> bool {
151        self.selected
152    }
153}
154
155impl RenderOnce for DropdownButton {
156    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
157        let rounded = self.variant.is_ghost() && !self.selected;
158
159        div()
160            .id(self.id)
161            .h_flex()
162            .refine_style(&self.style)
163            .when_some(self.button, |this, button| {
164                this.child(
165                    button
166                        .rounded(self.rounded)
167                        .border_corners(Corners {
168                            top_left: true,
169                            top_right: rounded,
170                            bottom_left: true,
171                            bottom_right: rounded,
172                        })
173                        .border_edges(Edges {
174                            left: true,
175                            top: true,
176                            right: true,
177                            bottom: true,
178                        })
179                        .loading(self.loading)
180                        .selected(self.selected)
181                        .disabled(self.disabled || self.loading)
182                        .when(self.compact, |this| this.compact())
183                        .when(self.outline, |this| this.outline())
184                        .with_size(self.size)
185                        .with_variant(self.variant),
186                )
187                .when_some(self.menu, |this, menu| {
188                    this.child(
189                        Button::new("popup")
190                            .icon(IconName::ChevronDown)
191                            .rounded(self.rounded)
192                            .border_edges(Edges {
193                                left: rounded,
194                                top: true,
195                                right: true,
196                                bottom: true,
197                            })
198                            .border_corners(Corners {
199                                top_left: rounded,
200                                top_right: true,
201                                bottom_left: rounded,
202                                bottom_right: true,
203                            })
204                            .selected(self.selected)
205                            .disabled(self.disabled || self.loading)
206                            .when(self.compact, |this| this.compact())
207                            .when(self.outline, |this| this.outline())
208                            .with_size(self.size)
209                            .with_variant(self.variant)
210                            .dropdown_menu_with_anchor(self.anchor, menu),
211                    )
212                })
213            })
214            .map(|this| self.tooltip.apply(this))
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[rgpui::test]
223    fn test_dropdown_button_builder(_cx: &mut rgpui::TestAppContext) {
224        let button = Button::new("inner").label("Action");
225        let dropdown = DropdownButton::new("complex-dropdown")
226            .button(button)
227            .primary()
228            .outline()
229            .large()
230            .compact()
231            .loading(false)
232            .disabled(false)
233            .selected(false)
234            .rounded(ButtonRounded::Medium)
235            .dropdown_menu_with_anchor(Anchor::BottomLeft, |menu, _, _| menu);
236
237        assert!(dropdown.button.is_some());
238        assert_eq!(dropdown.variant, ButtonVariant::Primary);
239        assert!(dropdown.outline);
240        assert_eq!(dropdown.size, Size::Large);
241        assert!(dropdown.compact);
242        assert!(!dropdown.loading);
243        assert!(!dropdown.disabled);
244        assert!(!dropdown.selected);
245        assert!(matches!(dropdown.rounded, ButtonRounded::Medium));
246        assert!(dropdown.menu.is_some());
247        assert_eq!(dropdown.anchor, Anchor::BottomLeft);
248    }
249}