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