1use gpui::{ClickEvent, Corner, CursorStyle, Entity, Hsla, MouseButton};
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 style: DropdownStyle,
25 menu: Entity<ContextMenu>,
26 full_width: bool,
27 disabled: bool,
28 handle: Option<PopoverMenuHandle<ContextMenu>>,
29}
30
31impl DropdownMenu {
32 pub fn new(
33 id: impl Into<ElementId>,
34 label: impl Into<SharedString>,
35 menu: Entity<ContextMenu>,
36 ) -> Self {
37 Self {
38 id: id.into(),
39 label: LabelKind::Text(label.into()),
40 style: DropdownStyle::default(),
41 menu,
42 full_width: false,
43 disabled: false,
44 handle: None,
45 }
46 }
47
48 pub fn new_with_element(
49 id: impl Into<ElementId>,
50 label: AnyElement,
51 menu: Entity<ContextMenu>,
52 ) -> Self {
53 Self {
54 id: id.into(),
55 label: LabelKind::Element(label),
56 style: DropdownStyle::default(),
57 menu,
58 full_width: false,
59 disabled: false,
60 handle: None,
61 }
62 }
63
64 pub fn style(mut self, style: DropdownStyle) -> Self {
65 self.style = style;
66 self
67 }
68
69 pub fn full_width(mut self, full_width: bool) -> Self {
70 self.full_width = full_width;
71 self
72 }
73
74 pub fn handle(mut self, handle: PopoverMenuHandle<ContextMenu>) -> Self {
75 self.handle = Some(handle);
76 self
77 }
78}
79
80impl Disableable for DropdownMenu {
81 fn disabled(mut self, disabled: bool) -> Self {
82 self.disabled = disabled;
83 self
84 }
85}
86
87impl RenderOnce for DropdownMenu {
88 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
89 PopoverMenu::new(self.id)
90 .full_width(self.full_width)
91 .menu(move |_window, _cx| Some(self.menu.clone()))
92 .trigger(
93 DropdownMenuTrigger::new(self.label)
94 .full_width(self.full_width)
95 .disabled(self.disabled)
96 .style(self.style),
97 )
98 .attach(Corner::BottomLeft)
99 .when_some(self.handle, |el, handle| el.with_handle(handle))
100 }
101}
102
103impl Component for DropdownMenu {
104 fn scope() -> ComponentScope {
105 ComponentScope::Input
106 }
107
108 fn name() -> &'static str {
109 "DropdownMenu"
110 }
111
112 fn description() -> Option<&'static str> {
113 Some(
114 "A dropdown menu displays a list of actions or options. A dropdown menu is always activated by clicking a trigger (or via a keybinding).",
115 )
116 }
117
118 fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
119 let menu = ContextMenu::build(window, cx, |this, _, _| {
120 this.entry("Option 1", None, |_, _| {})
121 .entry("Option 2", None, |_, _| {})
122 .entry("Option 3", None, |_, _| {})
123 .separator()
124 .entry("Option 4", None, |_, _| {})
125 });
126
127 Some(
128 v_flex()
129 .gap_6()
130 .children(vec![
131 example_group_with_title(
132 "Basic Usage",
133 vec![
134 single_example(
135 "Default",
136 DropdownMenu::new("default", "Select an option", menu.clone())
137 .into_any_element(),
138 ),
139 single_example(
140 "Full Width",
141 DropdownMenu::new(
142 "full-width",
143 "Full Width Dropdown",
144 menu.clone(),
145 )
146 .full_width(true)
147 .into_any_element(),
148 ),
149 ],
150 ),
151 example_group_with_title(
152 "Styles",
153 vec![
154 single_example(
155 "Outlined",
156 DropdownMenu::new("outlined", "Outlined Dropdown", menu.clone())
157 .style(DropdownStyle::Outlined)
158 .into_any_element(),
159 ),
160 single_example(
161 "Ghost",
162 DropdownMenu::new("ghost", "Ghost Dropdown", menu.clone())
163 .style(DropdownStyle::Ghost)
164 .into_any_element(),
165 ),
166 ],
167 ),
168 example_group_with_title(
169 "States",
170 vec![single_example(
171 "Disabled",
172 DropdownMenu::new("disabled", "Disabled Dropdown", menu)
173 .disabled(true)
174 .into_any_element(),
175 )],
176 ),
177 ])
178 .into_any_element(),
179 )
180 }
181}
182
183#[derive(Debug, Clone, Copy)]
184pub struct DropdownTriggerStyle {
185 pub bg: Hsla,
186}
187
188impl DropdownTriggerStyle {
189 pub fn for_style(style: DropdownStyle, cx: &App) -> Self {
190 let colors = cx.theme().colors();
191
192 let bg = match style {
193 DropdownStyle::Solid => colors.editor_background,
194 DropdownStyle::Outlined => colors.surface_background,
195 DropdownStyle::Ghost => colors.ghost_element_background,
196 };
197
198 Self { bg }
199 }
200}
201
202#[derive(IntoElement)]
203struct DropdownMenuTrigger {
204 label: LabelKind,
205 full_width: bool,
206 selected: bool,
207 disabled: bool,
208 style: DropdownStyle,
209 cursor_style: CursorStyle,
210 on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
211}
212
213impl DropdownMenuTrigger {
214 pub fn new(label: LabelKind) -> Self {
215 Self {
216 label,
217 full_width: false,
218 selected: false,
219 disabled: false,
220 style: DropdownStyle::default(),
221 cursor_style: CursorStyle::default(),
222 on_click: None,
223 }
224 }
225
226 pub fn full_width(mut self, full_width: bool) -> Self {
227 self.full_width = full_width;
228 self
229 }
230
231 pub fn style(mut self, style: DropdownStyle) -> Self {
232 self.style = style;
233 self
234 }
235}
236
237impl Disableable for DropdownMenuTrigger {
238 fn disabled(mut self, disabled: bool) -> Self {
239 self.disabled = disabled;
240 self
241 }
242}
243
244impl Toggleable for DropdownMenuTrigger {
245 fn toggle_state(mut self, selected: bool) -> Self {
246 self.selected = selected;
247 self
248 }
249}
250
251impl Clickable for DropdownMenuTrigger {
252 fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
253 self.on_click = Some(Box::new(handler));
254 self
255 }
256
257 fn cursor_style(mut self, cursor_style: CursorStyle) -> Self {
258 self.cursor_style = cursor_style;
259 self
260 }
261}
262
263impl RenderOnce for DropdownMenuTrigger {
264 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
265 let disabled = self.disabled;
266
267 let style = DropdownTriggerStyle::for_style(self.style, cx);
268 let is_outlined = matches!(self.style, DropdownStyle::Outlined);
269
270 h_flex()
271 .id("dropdown-menu-trigger")
272 .min_w_20()
273 .pl_2()
274 .pr_1p5()
275 .py_0p5()
276 .gap_2()
277 .justify_between()
278 .rounded_sm()
279 .map(|this| {
280 if self.full_width {
281 this.w_full()
282 } else {
283 this.flex_none().w_auto()
284 }
285 })
286 .when(is_outlined, |this| {
287 this.border_1()
288 .border_color(cx.theme().colors().border)
289 .overflow_hidden()
290 })
291 .map(|this| {
292 if disabled {
293 this.cursor_not_allowed()
294 .bg(cx.theme().colors().element_disabled)
295 } else {
296 this.bg(style.bg)
297 .hover(|s| s.bg(cx.theme().colors().element_hover))
298 }
299 })
300 .child(match self.label {
301 LabelKind::Text(text) => Label::new(text)
302 .color(if disabled {
303 Color::Disabled
304 } else {
305 Color::Default
306 })
307 .into_any_element(),
308 LabelKind::Element(element) => element,
309 })
310 .child(
311 Icon::new(IconName::ChevronUpDown)
312 .size(IconSize::XSmall)
313 .color(if disabled {
314 Color::Disabled
315 } else {
316 Color::Muted
317 }),
318 )
319 .when_some(self.on_click.filter(|_| !disabled), |el, on_click| {
320 el.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
321 .on_click(move |event, window, cx| {
322 cx.stop_propagation();
323 (on_click)(event, window, cx)
324 })
325 })
326 }
327}