1use gpui::{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    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 (text_button, element_button) = match self.label {
141            LabelKind::Text(text) => (
142                Some(
143                    Button::new(self.id.clone(), text)
144                        .style(button_style)
145                        .when(self.chevron, |this| {
146                            this.icon(IconName::ChevronUpDown)
147                                .icon_position(IconPosition::End)
148                                .icon_size(IconSize::XSmall)
149                                .icon_color(Color::Muted)
150                        })
151                        .when(full_width, |this| this.full_width())
152                        .size(trigger_size)
153                        .disabled(self.disabled)
154                        .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
155                ),
156                None,
157            ),
158            LabelKind::Element(element) => (
159                None,
160                Some(
161                    ButtonLike::new(self.id.clone())
162                        .child(element)
163                        .style(button_style)
164                        .when(self.chevron, |this| {
165                            this.child(
166                                Icon::new(IconName::ChevronUpDown)
167                                    .size(IconSize::XSmall)
168                                    .color(Color::Muted),
169                            )
170                        })
171                        .when(full_width, |this| this.full_width())
172                        .size(trigger_size)
173                        .disabled(self.disabled)
174                        .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
175                ),
176            ),
177        };
178
179        PopoverMenu::new((self.id.clone(), "popover"))
180            .full_width(self.full_width)
181            .menu(move |_window, _cx| Some(self.menu.clone()))
182            .when_some(text_button, |this, text_button| this.trigger(text_button))
183            .when_some(element_button, |this, element_button| {
184                this.trigger(element_button)
185            })
186            .attach(match self.attach {
187                Some(attach) => attach,
188                None => Corner::BottomRight,
189            })
190            .when_some(self.offset, |this, offset| this.offset(offset))
191            .when_some(self.handle, |this, handle| this.with_handle(handle))
192    }
193}
194
195impl Component for DropdownMenu {
196    fn scope() -> ComponentScope {
197        ComponentScope::Input
198    }
199
200    fn name() -> &'static str {
201        "DropdownMenu"
202    }
203
204    fn description() -> Option<&'static str> {
205        Some(
206            "A dropdown menu displays a list of actions or options. A dropdown menu is always activated by clicking a trigger (or via a keybinding).",
207        )
208    }
209
210    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
211        let menu = ContextMenu::build(window, cx, |this, _, _| {
212            this.entry("Option 1", None, |_, _| {})
213                .entry("Option 2", None, |_, _| {})
214                .entry("Option 3", None, |_, _| {})
215                .separator()
216                .entry("Option 4", None, |_, _| {})
217        });
218
219        Some(
220            v_flex()
221                .gap_6()
222                .children(vec![
223                    example_group_with_title(
224                        "Basic Usage",
225                        vec![
226                            single_example(
227                                "Default",
228                                DropdownMenu::new("default", "Select an option", menu.clone())
229                                    .into_any_element(),
230                            ),
231                            single_example(
232                                "Full Width",
233                                DropdownMenu::new(
234                                    "full-width",
235                                    "Full Width Dropdown",
236                                    menu.clone(),
237                                )
238                                .full_width(true)
239                                .into_any_element(),
240                            ),
241                        ],
242                    ),
243                    example_group_with_title(
244                        "Styles",
245                        vec![
246                            single_example(
247                                "Outlined",
248                                DropdownMenu::new("outlined", "Outlined Dropdown", menu.clone())
249                                    .style(DropdownStyle::Outlined)
250                                    .into_any_element(),
251                            ),
252                            single_example(
253                                "Ghost",
254                                DropdownMenu::new("ghost", "Ghost Dropdown", menu.clone())
255                                    .style(DropdownStyle::Ghost)
256                                    .into_any_element(),
257                            ),
258                        ],
259                    ),
260                    example_group_with_title(
261                        "States",
262                        vec![single_example(
263                            "Disabled",
264                            DropdownMenu::new("disabled", "Disabled Dropdown", menu)
265                                .disabled(true)
266                                .into_any_element(),
267                        )],
268                    ),
269                ])
270                .into_any_element(),
271        )
272    }
273}