dropdown_menu.rs

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