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