dropdown_menu.rs

  1use gpui::{Corner, Entity, Pixels, Point};
  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    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}
 33
 34impl DropdownMenu {
 35    pub fn new(
 36        id: impl Into<ElementId>,
 37        label: impl Into<SharedString>,
 38        menu: Entity<ContextMenu>,
 39    ) -> Self {
 40        Self {
 41            id: id.into(),
 42            label: LabelKind::Text(label.into()),
 43            trigger_size: ButtonSize::Default,
 44            style: DropdownStyle::default(),
 45            menu,
 46            full_width: false,
 47            disabled: false,
 48            handle: None,
 49            attach: None,
 50            offset: None,
 51        }
 52    }
 53
 54    pub fn new_with_element(
 55        id: impl Into<ElementId>,
 56        label: AnyElement,
 57        menu: Entity<ContextMenu>,
 58    ) -> Self {
 59        Self {
 60            id: id.into(),
 61            label: LabelKind::Element(label),
 62            trigger_size: ButtonSize::Default,
 63            style: DropdownStyle::default(),
 64            menu,
 65            full_width: false,
 66            disabled: false,
 67            handle: None,
 68            attach: None,
 69            offset: None,
 70        }
 71    }
 72
 73    pub fn trigger_size(mut self, size: ButtonSize) -> Self {
 74        self.trigger_size = size;
 75        self
 76    }
 77
 78    pub fn style(mut self, style: DropdownStyle) -> Self {
 79        self.style = style;
 80        self
 81    }
 82
 83    pub fn full_width(mut self, full_width: bool) -> Self {
 84        self.full_width = full_width;
 85        self
 86    }
 87
 88    pub fn handle(mut self, handle: PopoverMenuHandle<ContextMenu>) -> Self {
 89        self.handle = Some(handle);
 90        self
 91    }
 92
 93    /// Defines which corner of the handle to attach the menu's anchor to.
 94    pub fn attach(mut self, attach: Corner) -> Self {
 95        self.attach = Some(attach);
 96        self
 97    }
 98
 99    /// Offsets the position of the menu by that many pixels.
100    pub fn offset(mut self, offset: Point<Pixels>) -> Self {
101        self.offset = Some(offset);
102        self
103    }
104}
105
106impl Disableable for DropdownMenu {
107    fn disabled(mut self, disabled: bool) -> Self {
108        self.disabled = disabled;
109        self
110    }
111}
112
113impl RenderOnce for DropdownMenu {
114    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
115        let button_style = match self.style {
116            DropdownStyle::Solid => ButtonStyle::Filled,
117            DropdownStyle::Outlined => ButtonStyle::Outlined,
118            DropdownStyle::Ghost => ButtonStyle::Transparent,
119        };
120
121        let full_width = self.full_width;
122        let trigger_size = self.trigger_size;
123
124        let button = match self.label {
125            LabelKind::Text(text) => Button::new(self.id.clone(), text)
126                .style(button_style)
127                .icon(IconName::ChevronUpDown)
128                .icon_position(IconPosition::End)
129                .icon_size(IconSize::XSmall)
130                .icon_color(Color::Muted)
131                .when(full_width, |this| this.full_width())
132                .size(trigger_size)
133                .disabled(self.disabled),
134            LabelKind::Element(_element) => Button::new(self.id.clone(), "")
135                .style(button_style)
136                .icon(IconName::ChevronUpDown)
137                .icon_position(IconPosition::End)
138                .icon_size(IconSize::XSmall)
139                .icon_color(Color::Muted)
140                .when(full_width, |this| this.full_width())
141                .size(trigger_size)
142                .disabled(self.disabled),
143        };
144
145        PopoverMenu::new((self.id.clone(), "popover"))
146            .full_width(self.full_width)
147            .menu(move |_window, _cx| Some(self.menu.clone()))
148            .trigger(button)
149            .attach(match self.attach {
150                Some(attach) => attach,
151                None => Corner::BottomRight,
152            })
153            .when_some(self.offset, |this, offset| this.offset(offset))
154            .when_some(self.handle, |this, handle| this.with_handle(handle))
155    }
156}
157
158impl Component for DropdownMenu {
159    fn scope() -> ComponentScope {
160        ComponentScope::Input
161    }
162
163    fn name() -> &'static str {
164        "DropdownMenu"
165    }
166
167    fn description() -> Option<&'static str> {
168        Some(
169            "A dropdown menu displays a list of actions or options. A dropdown menu is always activated by clicking a trigger (or via a keybinding).",
170        )
171    }
172
173    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
174        let menu = ContextMenu::build(window, cx, |this, _, _| {
175            this.entry("Option 1", None, |_, _| {})
176                .entry("Option 2", None, |_, _| {})
177                .entry("Option 3", None, |_, _| {})
178                .separator()
179                .entry("Option 4", None, |_, _| {})
180        });
181
182        Some(
183            v_flex()
184                .gap_6()
185                .children(vec![
186                    example_group_with_title(
187                        "Basic Usage",
188                        vec![
189                            single_example(
190                                "Default",
191                                DropdownMenu::new("default", "Select an option", menu.clone())
192                                    .into_any_element(),
193                            ),
194                            single_example(
195                                "Full Width",
196                                DropdownMenu::new(
197                                    "full-width",
198                                    "Full Width Dropdown",
199                                    menu.clone(),
200                                )
201                                .full_width(true)
202                                .into_any_element(),
203                            ),
204                        ],
205                    ),
206                    example_group_with_title(
207                        "Styles",
208                        vec![
209                            single_example(
210                                "Outlined",
211                                DropdownMenu::new("outlined", "Outlined Dropdown", menu.clone())
212                                    .style(DropdownStyle::Outlined)
213                                    .into_any_element(),
214                            ),
215                            single_example(
216                                "Ghost",
217                                DropdownMenu::new("ghost", "Ghost Dropdown", menu.clone())
218                                    .style(DropdownStyle::Ghost)
219                                    .into_any_element(),
220                            ),
221                        ],
222                    ),
223                    example_group_with_title(
224                        "States",
225                        vec![single_example(
226                            "Disabled",
227                            DropdownMenu::new("disabled", "Disabled Dropdown", menu)
228                                .disabled(true)
229                                .into_any_element(),
230                        )],
231                    ),
232                ])
233                .into_any_element(),
234        )
235    }
236}