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