dropdown_menu.rs

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