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