1use gpui::{AnyView, Corner, Entity, Pixels, Point};
2
3use crate::{ButtonLike, 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 Subtle,
13 Ghost,
14}
15
16enum LabelKind {
17 Text(SharedString),
18 Element(AnyElement),
19}
20
21#[derive(IntoElement, RegisterComponent)]
22pub struct DropdownMenu {
23 id: ElementId,
24 label: LabelKind,
25 trigger_size: ButtonSize,
26 trigger_tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
27 trigger_icon: Option<IconName>,
28 style: DropdownStyle,
29 menu: Entity<ContextMenu>,
30 full_width: bool,
31 disabled: bool,
32 handle: Option<PopoverMenuHandle<ContextMenu>>,
33 attach: Option<Corner>,
34 offset: Option<Point<Pixels>>,
35 tab_index: Option<isize>,
36 chevron: bool,
37}
38
39impl DropdownMenu {
40 pub fn new(
41 id: impl Into<ElementId>,
42 label: impl Into<SharedString>,
43 menu: Entity<ContextMenu>,
44 ) -> Self {
45 Self {
46 id: id.into(),
47 label: LabelKind::Text(label.into()),
48 trigger_size: ButtonSize::Default,
49 trigger_tooltip: None,
50 trigger_icon: Some(IconName::ChevronUpDown),
51 style: DropdownStyle::default(),
52 menu,
53 full_width: false,
54 disabled: false,
55 handle: None,
56 attach: None,
57 offset: None,
58 tab_index: None,
59 chevron: true,
60 }
61 }
62
63 pub fn new_with_element(
64 id: impl Into<ElementId>,
65 label: AnyElement,
66 menu: Entity<ContextMenu>,
67 ) -> Self {
68 Self {
69 id: id.into(),
70 label: LabelKind::Element(label),
71 trigger_size: ButtonSize::Default,
72 trigger_tooltip: None,
73 trigger_icon: Some(IconName::ChevronUpDown),
74 style: DropdownStyle::default(),
75 menu,
76 full_width: false,
77 disabled: false,
78 handle: None,
79 attach: None,
80 offset: None,
81 tab_index: None,
82 chevron: true,
83 }
84 }
85
86 pub fn style(mut self, style: DropdownStyle) -> Self {
87 self.style = style;
88 self
89 }
90
91 pub fn trigger_size(mut self, size: ButtonSize) -> Self {
92 self.trigger_size = size;
93 self
94 }
95
96 pub fn trigger_tooltip(
97 mut self,
98 tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
99 ) -> Self {
100 self.trigger_tooltip = Some(Box::new(tooltip));
101 self
102 }
103
104 pub fn trigger_icon(mut self, icon: IconName) -> Self {
105 self.trigger_icon = Some(icon);
106 self
107 }
108
109 pub fn full_width(mut self, full_width: bool) -> Self {
110 self.full_width = full_width;
111 self
112 }
113
114 pub fn handle(mut self, handle: PopoverMenuHandle<ContextMenu>) -> Self {
115 self.handle = Some(handle);
116 self
117 }
118
119 /// Defines which corner of the handle to attach the menu's anchor to.
120 pub fn attach(mut self, attach: Corner) -> Self {
121 self.attach = Some(attach);
122 self
123 }
124
125 /// Offsets the position of the menu by that many pixels.
126 pub fn offset(mut self, offset: Point<Pixels>) -> Self {
127 self.offset = Some(offset);
128 self
129 }
130
131 pub fn tab_index(mut self, arg: isize) -> Self {
132 self.tab_index = Some(arg);
133 self
134 }
135
136 pub fn no_chevron(mut self) -> Self {
137 self.chevron = false;
138 self
139 }
140}
141
142impl Disableable for DropdownMenu {
143 fn disabled(mut self, disabled: bool) -> Self {
144 self.disabled = disabled;
145 self
146 }
147}
148
149impl RenderOnce for DropdownMenu {
150 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
151 let button_style = match self.style {
152 DropdownStyle::Solid => ButtonStyle::Filled,
153 DropdownStyle::Subtle => ButtonStyle::Subtle,
154 DropdownStyle::Outlined => ButtonStyle::Outlined,
155 DropdownStyle::Ghost => ButtonStyle::Transparent,
156 };
157
158 let full_width = self.full_width;
159 let trigger_size = self.trigger_size;
160
161 let (text_button, element_button) = match self.label {
162 LabelKind::Text(text) => (
163 Some(
164 Button::new(self.id.clone(), text)
165 .style(button_style)
166 .when(self.chevron, |this| {
167 this.icon(self.trigger_icon)
168 .icon_position(IconPosition::End)
169 .icon_size(IconSize::XSmall)
170 .icon_color(Color::Muted)
171 })
172 .when(full_width, |this| this.full_width())
173 .size(trigger_size)
174 .disabled(self.disabled)
175 .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
176 ),
177 None,
178 ),
179 LabelKind::Element(element) => (
180 None,
181 Some(
182 ButtonLike::new(self.id.clone())
183 .child(element)
184 .style(button_style)
185 .when(self.chevron, |this| {
186 this.child(
187 Icon::new(IconName::ChevronUpDown)
188 .size(IconSize::XSmall)
189 .color(Color::Muted),
190 )
191 })
192 .when(full_width, |this| this.full_width())
193 .size(trigger_size)
194 .disabled(self.disabled)
195 .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
196 ),
197 ),
198 };
199
200 let mut popover = PopoverMenu::new((self.id.clone(), "popover"))
201 .full_width(self.full_width)
202 .menu(move |_window, _cx| Some(self.menu.clone()));
203
204 popover = match (text_button, element_button, self.trigger_tooltip) {
205 (Some(text_button), None, Some(tooltip)) => {
206 popover.trigger_with_tooltip(text_button, tooltip)
207 }
208 (Some(text_button), None, None) => popover.trigger(text_button),
209 (None, Some(element_button), Some(tooltip)) => {
210 popover.trigger_with_tooltip(element_button, tooltip)
211 }
212 (None, Some(element_button), None) => popover.trigger(element_button),
213 _ => popover,
214 };
215
216 popover
217 .attach(match self.attach {
218 Some(attach) => attach,
219 None => Corner::BottomRight,
220 })
221 .when_some(self.offset, |this, offset| this.offset(offset))
222 .when_some(self.handle, |this, handle| this.with_handle(handle))
223 }
224}
225
226impl Component for DropdownMenu {
227 fn scope() -> ComponentScope {
228 ComponentScope::Input
229 }
230
231 fn name() -> &'static str {
232 "DropdownMenu"
233 }
234
235 fn description() -> Option<&'static str> {
236 Some(
237 "A dropdown menu displays a list of actions or options. A dropdown menu is always activated by clicking a trigger (or via a keybinding).",
238 )
239 }
240
241 fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
242 let menu = ContextMenu::build(window, cx, |this, _, _| {
243 this.entry("Option 1", None, |_, _| {})
244 .entry("Option 2", None, |_, _| {})
245 .entry("Option 3", None, |_, _| {})
246 .separator()
247 .entry("Option 4", None, |_, _| {})
248 });
249
250 let menu_with_submenu = ContextMenu::build(window, cx, |this, _, _| {
251 this.entry("Toggle All Docks", None, |_, _| {})
252 .submenu("Editor Layout", |menu, _, _| {
253 menu.entry("Split Up", None, |_, _| {})
254 .entry("Split Down", None, |_, _| {})
255 .separator()
256 .entry("Split Side", None, |_, _| {})
257 })
258 .separator()
259 .entry("Project Panel", None, |_, _| {})
260 .entry("Outline Panel", None, |_, _| {})
261 .separator()
262 .submenu("Autofill", |menu, _, _| {
263 menu.entry("Contact…", None, |_, _| {})
264 .entry("Passwords…", None, |_, _| {})
265 })
266 .submenu_with_icon("Predict", IconName::ZedPredict, |menu, _, _| {
267 menu.entry("Everywhere", None, |_, _| {})
268 .entry("At Cursor", None, |_, _| {})
269 .entry("Over Here", None, |_, _| {})
270 .entry("Over There", None, |_, _| {})
271 })
272 });
273
274 Some(
275 v_flex()
276 .gap_6()
277 .children(vec![
278 example_group_with_title(
279 "Basic Usage",
280 vec![
281 single_example(
282 "Default",
283 DropdownMenu::new("default", "Select an option", menu.clone())
284 .into_any_element(),
285 ),
286 single_example(
287 "Full Width",
288 DropdownMenu::new(
289 "full-width",
290 "Full Width Dropdown",
291 menu.clone(),
292 )
293 .full_width(true)
294 .into_any_element(),
295 ),
296 ],
297 ),
298 example_group_with_title(
299 "Submenus",
300 vec![single_example(
301 "With Submenus",
302 DropdownMenu::new("submenu", "Submenu", menu_with_submenu)
303 .into_any_element(),
304 )],
305 ),
306 example_group_with_title(
307 "Styles",
308 vec![
309 single_example(
310 "Outlined",
311 DropdownMenu::new("outlined", "Outlined Dropdown", menu.clone())
312 .style(DropdownStyle::Outlined)
313 .into_any_element(),
314 ),
315 single_example(
316 "Ghost",
317 DropdownMenu::new("ghost", "Ghost Dropdown", menu.clone())
318 .style(DropdownStyle::Ghost)
319 .into_any_element(),
320 ),
321 ],
322 ),
323 example_group_with_title(
324 "States",
325 vec![single_example(
326 "Disabled",
327 DropdownMenu::new("disabled", "Disabled Dropdown", menu)
328 .disabled(true)
329 .into_any_element(),
330 )],
331 ),
332 ])
333 .into_any_element(),
334 )
335 }
336}