dropdown_menu.rs

  1use gpui::{ClickEvent, Corner, CursorStyle, Entity, Hsla, MouseButton};
  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    style: DropdownStyle,
 25    menu: Entity<ContextMenu>,
 26    full_width: bool,
 27    disabled: bool,
 28    handle: Option<PopoverMenuHandle<ContextMenu>>,
 29}
 30
 31impl DropdownMenu {
 32    pub fn new(
 33        id: impl Into<ElementId>,
 34        label: impl Into<SharedString>,
 35        menu: Entity<ContextMenu>,
 36    ) -> Self {
 37        Self {
 38            id: id.into(),
 39            label: LabelKind::Text(label.into()),
 40            style: DropdownStyle::default(),
 41            menu,
 42            full_width: false,
 43            disabled: false,
 44            handle: None,
 45        }
 46    }
 47
 48    pub fn new_with_element(
 49        id: impl Into<ElementId>,
 50        label: AnyElement,
 51        menu: Entity<ContextMenu>,
 52    ) -> Self {
 53        Self {
 54            id: id.into(),
 55            label: LabelKind::Element(label),
 56            style: DropdownStyle::default(),
 57            menu,
 58            full_width: false,
 59            disabled: false,
 60            handle: None,
 61        }
 62    }
 63
 64    pub fn style(mut self, style: DropdownStyle) -> Self {
 65        self.style = style;
 66        self
 67    }
 68
 69    pub fn full_width(mut self, full_width: bool) -> Self {
 70        self.full_width = full_width;
 71        self
 72    }
 73
 74    pub fn handle(mut self, handle: PopoverMenuHandle<ContextMenu>) -> Self {
 75        self.handle = Some(handle);
 76        self
 77    }
 78}
 79
 80impl Disableable for DropdownMenu {
 81    fn disabled(mut self, disabled: bool) -> Self {
 82        self.disabled = disabled;
 83        self
 84    }
 85}
 86
 87impl RenderOnce for DropdownMenu {
 88    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
 89        PopoverMenu::new(self.id)
 90            .full_width(self.full_width)
 91            .menu(move |_window, _cx| Some(self.menu.clone()))
 92            .trigger(
 93                DropdownMenuTrigger::new(self.label)
 94                    .full_width(self.full_width)
 95                    .disabled(self.disabled)
 96                    .style(self.style),
 97            )
 98            .attach(Corner::BottomLeft)
 99            .when_some(self.handle, |el, handle| el.with_handle(handle))
100    }
101}
102
103impl Component for DropdownMenu {
104    fn scope() -> ComponentScope {
105        ComponentScope::Input
106    }
107
108    fn name() -> &'static str {
109        "DropdownMenu"
110    }
111
112    fn description() -> Option<&'static str> {
113        Some(
114            "A dropdown menu displays a list of actions or options. A dropdown menu is always activated by clicking a trigger (or via a keybinding).",
115        )
116    }
117
118    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
119        let menu = ContextMenu::build(window, cx, |this, _, _| {
120            this.entry("Option 1", None, |_, _| {})
121                .entry("Option 2", None, |_, _| {})
122                .entry("Option 3", None, |_, _| {})
123                .separator()
124                .entry("Option 4", None, |_, _| {})
125        });
126
127        Some(
128            v_flex()
129                .gap_6()
130                .children(vec![
131                    example_group_with_title(
132                        "Basic Usage",
133                        vec![
134                            single_example(
135                                "Default",
136                                DropdownMenu::new("default", "Select an option", menu.clone())
137                                    .into_any_element(),
138                            ),
139                            single_example(
140                                "Full Width",
141                                DropdownMenu::new(
142                                    "full-width",
143                                    "Full Width Dropdown",
144                                    menu.clone(),
145                                )
146                                .full_width(true)
147                                .into_any_element(),
148                            ),
149                        ],
150                    ),
151                    example_group_with_title(
152                        "Styles",
153                        vec![
154                            single_example(
155                                "Outlined",
156                                DropdownMenu::new("outlined", "Outlined Dropdown", menu.clone())
157                                    .style(DropdownStyle::Outlined)
158                                    .into_any_element(),
159                            ),
160                            single_example(
161                                "Ghost",
162                                DropdownMenu::new("ghost", "Ghost Dropdown", menu.clone())
163                                    .style(DropdownStyle::Ghost)
164                                    .into_any_element(),
165                            ),
166                        ],
167                    ),
168                    example_group_with_title(
169                        "States",
170                        vec![single_example(
171                            "Disabled",
172                            DropdownMenu::new("disabled", "Disabled Dropdown", menu)
173                                .disabled(true)
174                                .into_any_element(),
175                        )],
176                    ),
177                ])
178                .into_any_element(),
179        )
180    }
181}
182
183#[derive(Debug, Clone, Copy)]
184pub struct DropdownTriggerStyle {
185    pub bg: Hsla,
186}
187
188impl DropdownTriggerStyle {
189    pub fn for_style(style: DropdownStyle, cx: &App) -> Self {
190        let colors = cx.theme().colors();
191
192        let bg = match style {
193            DropdownStyle::Solid => colors.editor_background,
194            DropdownStyle::Outlined => colors.surface_background,
195            DropdownStyle::Ghost => colors.ghost_element_background,
196        };
197
198        Self { bg }
199    }
200}
201
202#[derive(IntoElement)]
203struct DropdownMenuTrigger {
204    label: LabelKind,
205    full_width: bool,
206    selected: bool,
207    disabled: bool,
208    style: DropdownStyle,
209    cursor_style: CursorStyle,
210    on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
211}
212
213impl DropdownMenuTrigger {
214    pub fn new(label: LabelKind) -> Self {
215        Self {
216            label,
217            full_width: false,
218            selected: false,
219            disabled: false,
220            style: DropdownStyle::default(),
221            cursor_style: CursorStyle::default(),
222            on_click: None,
223        }
224    }
225
226    pub fn full_width(mut self, full_width: bool) -> Self {
227        self.full_width = full_width;
228        self
229    }
230
231    pub fn style(mut self, style: DropdownStyle) -> Self {
232        self.style = style;
233        self
234    }
235}
236
237impl Disableable for DropdownMenuTrigger {
238    fn disabled(mut self, disabled: bool) -> Self {
239        self.disabled = disabled;
240        self
241    }
242}
243
244impl Toggleable for DropdownMenuTrigger {
245    fn toggle_state(mut self, selected: bool) -> Self {
246        self.selected = selected;
247        self
248    }
249}
250
251impl Clickable for DropdownMenuTrigger {
252    fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
253        self.on_click = Some(Box::new(handler));
254        self
255    }
256
257    fn cursor_style(mut self, cursor_style: CursorStyle) -> Self {
258        self.cursor_style = cursor_style;
259        self
260    }
261}
262
263impl RenderOnce for DropdownMenuTrigger {
264    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
265        let disabled = self.disabled;
266
267        let style = DropdownTriggerStyle::for_style(self.style, cx);
268        let is_outlined = matches!(self.style, DropdownStyle::Outlined);
269
270        h_flex()
271            .id("dropdown-menu-trigger")
272            .min_w_20()
273            .pl_2()
274            .pr_1p5()
275            .py_0p5()
276            .gap_2()
277            .justify_between()
278            .rounded_sm()
279            .map(|this| {
280                if self.full_width {
281                    this.w_full()
282                } else {
283                    this.flex_none().w_auto()
284                }
285            })
286            .when(is_outlined, |this| {
287                this.border_1()
288                    .border_color(cx.theme().colors().border)
289                    .overflow_hidden()
290            })
291            .map(|this| {
292                if disabled {
293                    this.cursor_not_allowed()
294                        .bg(cx.theme().colors().element_disabled)
295                } else {
296                    this.bg(style.bg)
297                        .hover(|s| s.bg(cx.theme().colors().element_hover))
298                }
299            })
300            .child(match self.label {
301                LabelKind::Text(text) => Label::new(text)
302                    .color(if disabled {
303                        Color::Disabled
304                    } else {
305                        Color::Default
306                    })
307                    .into_any_element(),
308                LabelKind::Element(element) => element,
309            })
310            .child(
311                Icon::new(IconName::ChevronUpDown)
312                    .size(IconSize::XSmall)
313                    .color(if disabled {
314                        Color::Disabled
315                    } else {
316                        Color::Muted
317                    }),
318            )
319            .when_some(self.on_click.filter(|_| !disabled), |el, on_click| {
320                el.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
321                    .on_click(move |event, window, cx| {
322                        cx.stop_propagation();
323                        (on_click)(event, window, cx)
324                    })
325            })
326    }
327}