dropdown_menu.rs

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