dropdown_menu.rs

  1use gpui::{AnyView, Corner, Entity, Pixels, Point};
  2
  3use crate::{ButtonLike, ContextMenu, PopoverMenu, prelude::*};
  4
  5use super::PopoverMenuHandle;
  6
  7#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
  8pub enum DropdownStyle {
  9    #[default]
 10    Solid,
 11    Outlined,
 12    Subtle,
 13    Ghost,
 14}
 15
 16enum LabelKind {
 17    Text(SharedString),
 18    Element(AnyElement),
 19}
 20
 21#[derive(IntoElement, RegisterComponent)]
 22pub struct DropdownMenu {
 23    id: ElementId,
 24    label: LabelKind,
 25    trigger_size: ButtonSize,
 26    trigger_tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
 27    trigger_icon: Option<IconName>,
 28    style: DropdownStyle,
 29    menu: Entity<ContextMenu>,
 30    full_width: bool,
 31    disabled: bool,
 32    handle: Option<PopoverMenuHandle<ContextMenu>>,
 33    attach: Option<Corner>,
 34    offset: Option<Point<Pixels>>,
 35    tab_index: Option<isize>,
 36    chevron: bool,
 37}
 38
 39impl DropdownMenu {
 40    pub fn new(
 41        id: impl Into<ElementId>,
 42        label: impl Into<SharedString>,
 43        menu: Entity<ContextMenu>,
 44    ) -> Self {
 45        Self {
 46            id: id.into(),
 47            label: LabelKind::Text(label.into()),
 48            trigger_size: ButtonSize::Default,
 49            trigger_tooltip: None,
 50            trigger_icon: Some(IconName::ChevronUpDown),
 51            style: DropdownStyle::default(),
 52            menu,
 53            full_width: false,
 54            disabled: false,
 55            handle: None,
 56            attach: None,
 57            offset: None,
 58            tab_index: None,
 59            chevron: true,
 60        }
 61    }
 62
 63    pub fn new_with_element(
 64        id: impl Into<ElementId>,
 65        label: AnyElement,
 66        menu: Entity<ContextMenu>,
 67    ) -> Self {
 68        Self {
 69            id: id.into(),
 70            label: LabelKind::Element(label),
 71            trigger_size: ButtonSize::Default,
 72            trigger_tooltip: None,
 73            trigger_icon: Some(IconName::ChevronUpDown),
 74            style: DropdownStyle::default(),
 75            menu,
 76            full_width: false,
 77            disabled: false,
 78            handle: None,
 79            attach: None,
 80            offset: None,
 81            tab_index: None,
 82            chevron: true,
 83        }
 84    }
 85
 86    pub const fn style(mut self, style: DropdownStyle) -> Self {
 87        self.style = style;
 88        self
 89    }
 90
 91    pub const fn trigger_size(mut self, size: ButtonSize) -> Self {
 92        self.trigger_size = size;
 93        self
 94    }
 95
 96    pub fn trigger_tooltip(
 97        mut self,
 98        tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
 99    ) -> Self {
100        self.trigger_tooltip = Some(Box::new(tooltip));
101        self
102    }
103
104    pub const fn trigger_icon(mut self, icon: IconName) -> Self {
105        self.trigger_icon = Some(icon);
106        self
107    }
108
109    pub const fn full_width(mut self, full_width: bool) -> Self {
110        self.full_width = full_width;
111        self
112    }
113
114    pub fn handle(mut self, handle: PopoverMenuHandle<ContextMenu>) -> Self {
115        self.handle = Some(handle);
116        self
117    }
118
119    /// Defines which corner of the handle to attach the menu's anchor to.
120    pub const fn attach(mut self, attach: Corner) -> Self {
121        self.attach = Some(attach);
122        self
123    }
124
125    /// Offsets the position of the menu by that many pixels.
126    pub const fn offset(mut self, offset: Point<Pixels>) -> Self {
127        self.offset = Some(offset);
128        self
129    }
130
131    pub const fn tab_index(mut self, arg: isize) -> Self {
132        self.tab_index = Some(arg);
133        self
134    }
135
136    pub const fn no_chevron(mut self) -> Self {
137        self.chevron = false;
138        self
139    }
140}
141
142impl Disableable for DropdownMenu {
143    fn disabled(mut self, disabled: bool) -> Self {
144        self.disabled = disabled;
145        self
146    }
147}
148
149impl RenderOnce for DropdownMenu {
150    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
151        let button_style = match self.style {
152            DropdownStyle::Solid => ButtonStyle::Filled,
153            DropdownStyle::Subtle => ButtonStyle::Subtle,
154            DropdownStyle::Outlined => ButtonStyle::Outlined,
155            DropdownStyle::Ghost => ButtonStyle::Transparent,
156        };
157
158        let full_width = self.full_width;
159        let trigger_size = self.trigger_size;
160
161        let (text_button, element_button) = match self.label {
162            LabelKind::Text(text) => (
163                Some(
164                    Button::new(self.id.clone(), text)
165                        .style(button_style)
166                        .when(self.chevron, |this| {
167                            this.icon(self.trigger_icon)
168                                .icon_position(IconPosition::End)
169                                .icon_size(IconSize::XSmall)
170                                .icon_color(Color::Muted)
171                        })
172                        .when(full_width, |this| this.full_width())
173                        .size(trigger_size)
174                        .disabled(self.disabled)
175                        .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
176                ),
177                None,
178            ),
179            LabelKind::Element(element) => (
180                None,
181                Some(
182                    ButtonLike::new(self.id.clone())
183                        .child(element)
184                        .style(button_style)
185                        .when(self.chevron, |this| {
186                            this.child(
187                                Icon::new(IconName::ChevronUpDown)
188                                    .size(IconSize::XSmall)
189                                    .color(Color::Muted),
190                            )
191                        })
192                        .when(full_width, |this| this.full_width())
193                        .size(trigger_size)
194                        .disabled(self.disabled)
195                        .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
196                ),
197            ),
198        };
199
200        let mut popover = PopoverMenu::new((self.id.clone(), "popover"))
201            .full_width(self.full_width)
202            .menu(move |_window, _cx| Some(self.menu.clone()));
203
204        popover = match (text_button, element_button, self.trigger_tooltip) {
205            (Some(text_button), None, Some(tooltip)) => {
206                popover.trigger_with_tooltip(text_button, tooltip)
207            }
208            (Some(text_button), None, None) => popover.trigger(text_button),
209            (None, Some(element_button), Some(tooltip)) => {
210                popover.trigger_with_tooltip(element_button, tooltip)
211            }
212            (None, Some(element_button), None) => popover.trigger(element_button),
213            _ => popover,
214        };
215
216        popover
217            .attach(match self.attach {
218                Some(attach) => attach,
219                None => Corner::BottomRight,
220            })
221            .when_some(self.offset, |this, offset| this.offset(offset))
222            .when_some(self.handle, |this, handle| this.with_handle(handle))
223    }
224}
225
226impl Component for DropdownMenu {
227    fn scope() -> ComponentScope {
228        ComponentScope::Input
229    }
230
231    fn name() -> &'static str {
232        "DropdownMenu"
233    }
234
235    fn description() -> Option<&'static str> {
236        Some(
237            "A dropdown menu displays a list of actions or options. A dropdown menu is always activated by clicking a trigger (or via a keybinding).",
238        )
239    }
240
241    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
242        let menu = ContextMenu::build(window, cx, |this, _, _| {
243            this.entry("Option 1", None, |_, _| {})
244                .entry("Option 2", None, |_, _| {})
245                .entry("Option 3", None, |_, _| {})
246                .separator()
247                .entry("Option 4", None, |_, _| {})
248        });
249
250        Some(
251            v_flex()
252                .gap_6()
253                .children(vec![
254                    example_group_with_title(
255                        "Basic Usage",
256                        vec![
257                            single_example(
258                                "Default",
259                                DropdownMenu::new("default", "Select an option", menu.clone())
260                                    .into_any_element(),
261                            ),
262                            single_example(
263                                "Full Width",
264                                DropdownMenu::new(
265                                    "full-width",
266                                    "Full Width Dropdown",
267                                    menu.clone(),
268                                )
269                                .full_width(true)
270                                .into_any_element(),
271                            ),
272                        ],
273                    ),
274                    example_group_with_title(
275                        "Styles",
276                        vec![
277                            single_example(
278                                "Outlined",
279                                DropdownMenu::new("outlined", "Outlined Dropdown", menu.clone())
280                                    .style(DropdownStyle::Outlined)
281                                    .into_any_element(),
282                            ),
283                            single_example(
284                                "Ghost",
285                                DropdownMenu::new("ghost", "Ghost Dropdown", menu.clone())
286                                    .style(DropdownStyle::Ghost)
287                                    .into_any_element(),
288                            ),
289                        ],
290                    ),
291                    example_group_with_title(
292                        "States",
293                        vec![single_example(
294                            "Disabled",
295                            DropdownMenu::new("disabled", "Disabled Dropdown", menu)
296                                .disabled(true)
297                                .into_any_element(),
298                        )],
299                    ),
300                ])
301                .into_any_element(),
302        )
303    }
304}