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