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