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