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