1use gpui::Corners;
2use gpui::{
3 Anchor, App, Context, Edges, ElementId, InteractiveElement as _, IntoElement, ParentElement,
4 RenderOnce, StyleRefinement, Styled, Window, div, prelude::FluentBuilder,
5};
6
7use crate::{
8 Disableable, Selectable, Sizable, Size, StyledExt as _,
9 menu::{DropdownMenu, PopupMenu},
10};
11
12use super::{Button, ButtonVariant, ButtonVariants};
13
14const HALVES_GROUP: &str = "dropdown-button";
16
17#[derive(IntoElement)]
25pub struct DropdownButton {
26 id: ElementId,
27 style: StyleRefinement,
28 button: Option<Button>,
29 menu:
30 Option<Box<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static>>,
31 selected: bool,
32 disabled: bool,
33 outline: bool,
36 variant: Option<ButtonVariant>,
37 size: Option<Size>,
38 anchor: Anchor,
39}
40
41impl DropdownButton {
42 pub fn new(id: impl Into<ElementId>) -> Self {
44 Self {
45 id: id.into(),
46 style: StyleRefinement::default(),
47 button: None,
48 menu: None,
49 selected: false,
50 disabled: false,
51 outline: false,
52 variant: None,
53 size: None,
54 anchor: Anchor::TopRight,
55 }
56 }
57
58 fn effective_variant(&self) -> ButtonVariant {
59 self.variant
60 .or_else(|| self.button.as_ref().map(Button::variant))
61 .unwrap_or_default()
62 }
63
64 fn effective_size(&self) -> Size {
65 self.size
66 .or_else(|| self.button.as_ref().map(Button::button_size))
67 .unwrap_or_default()
68 }
69
70 pub fn button(mut self, button: Button) -> Self {
77 self.button = Some(button);
78 self
79 }
80
81 pub fn dropdown_menu(
83 mut self,
84 menu: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
85 ) -> Self {
86 self.menu = Some(Box::new(menu));
87 self
88 }
89
90 pub fn dropdown_menu_with_anchor(
92 mut self,
93 anchor: impl Into<Anchor>,
94 menu: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
95 ) -> Self {
96 self.menu = Some(Box::new(menu));
97 self.anchor = anchor.into();
98 self
99 }
100
101 pub fn outline(mut self) -> Self {
105 self.outline = true;
106 self
107 }
108}
109
110impl Disableable for DropdownButton {
111 fn disabled(mut self, disabled: bool) -> Self {
112 self.disabled = disabled;
113 self
114 }
115}
116
117impl Styled for DropdownButton {
118 fn style(&mut self) -> &mut gpui::StyleRefinement {
119 &mut self.style
120 }
121}
122
123impl Sizable for DropdownButton {
124 fn with_size(mut self, size: impl Into<Size>) -> Self {
125 self.size = Some(size.into());
126 self
127 }
128}
129
130impl ButtonVariants for DropdownButton {
131 fn with_variant(mut self, variant: ButtonVariant) -> Self {
132 self.variant = Some(variant);
133 self
134 }
135}
136
137impl Selectable for DropdownButton {
138 fn selected(mut self, selected: bool) -> Self {
139 self.selected = selected;
140 self
141 }
142
143 fn is_selected(&self) -> bool {
144 self.selected
145 }
146}
147
148impl RenderOnce for DropdownButton {
149 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
150 debug_assert!(
151 self.button.is_some() || self.menu.is_some(),
152 "a DropdownButton needs a `button`, a `dropdown_menu`, or both"
153 );
154
155 let variant = self.effective_variant();
156 let size = self.effective_size();
157 let selected = self.selected || self.button.as_ref().is_some_and(Selectable::is_selected);
158 let is_ghost = variant.is_ghost();
162 let menu_open = window.use_keyed_state(self.id.clone(), cx, |_, _| false);
163 let is_menu_open = *menu_open.read(cx);
164
165 div()
166 .id(self.id)
167 .when(is_ghost, |this| this.group(HALVES_GROUP))
168 .h_flex()
169 .refine_style(&self.style)
170 .when_some(self.button, |this, button| {
171 let disabled = self.disabled || button.is_disabled();
172 this.child(
173 button
174 .border_corners(Corners {
175 top_left: true,
176 top_right: false,
177 bottom_left: true,
178 bottom_right: false,
179 })
180 .border_edges(Edges::all(true))
181 .selected(selected)
182 .disabled(disabled)
183 .when(self.outline, |this| this.outline())
184 .with_size(size)
185 .with_variant(variant)
186 .when(is_ghost, |this| {
187 this.hover_group(HALVES_GROUP)
188 .hover_group_held(is_menu_open)
189 }),
190 )
191 })
192 .when_some(self.menu, |this, menu| {
193 this.child(
194 Button::new("popup")
195 .dropdown_caret(true)
196 .border_corners(Corners {
197 top_left: false,
198 top_right: true,
199 bottom_left: false,
200 bottom_right: true,
201 })
202 .border_edges(Edges {
203 left: false,
204 top: true,
205 right: true,
206 bottom: true,
207 })
208 .selected(selected)
209 .disabled(self.disabled)
210 .when(self.outline, |this| this.outline())
211 .with_size(size)
212 .with_variant(variant)
213 .when(is_ghost, |this| this.hover_group(HALVES_GROUP))
214 .dropdown_menu_with_anchor(self.anchor, menu)
215 .on_open_change(move |open, _, cx| {
216 menu_open.update(cx, |state, cx| {
217 *state = *open;
218 cx.notify();
219 })
220 }),
221 )
222 })
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[gpui::test]
231 fn test_dropdown_button_builder(_cx: &mut gpui::TestAppContext) {
232 let button = Button::new("inner").label("Action");
233 let dropdown = DropdownButton::new("complex-dropdown")
234 .button(button)
235 .primary()
236 .outline()
237 .large()
238 .disabled(false)
239 .selected(false)
240 .dropdown_menu_with_anchor(Anchor::BottomLeft, |menu, _, _| menu);
241
242 assert!(dropdown.button.is_some());
243 assert_eq!(dropdown.variant, Some(ButtonVariant::Primary));
244 assert!(dropdown.outline);
245 assert_eq!(dropdown.size, Some(Size::Large));
246 assert!(!dropdown.disabled);
247 assert!(!dropdown.selected);
248 assert!(dropdown.menu.is_some());
249 assert_eq!(dropdown.anchor, Anchor::BottomLeft);
250 }
251
252 #[gpui::test]
255 fn inner_button_keeps_its_own_variant_and_size(_cx: &mut gpui::TestAppContext) {
256 let dropdown = DropdownButton::new("dropdown")
257 .button(Button::new("inner").label("Action").danger().small())
258 .dropdown_menu(|menu, _, _| menu);
259
260 assert_eq!(dropdown.variant, None);
261 assert_eq!(dropdown.size, None);
262 }
263
264 #[gpui::test]
265 fn inner_ghost_becomes_the_split_variant(_cx: &mut gpui::TestAppContext) {
266 let dropdown = DropdownButton::new("dropdown")
267 .button(Button::new("inner").label("Action").ghost())
268 .dropdown_menu(|menu, _, _| menu);
269
270 assert_eq!(dropdown.effective_variant(), ButtonVariant::Ghost);
271 }
272
273 #[gpui::test]
274 fn inner_size_becomes_the_split_size(_cx: &mut gpui::TestAppContext) {
275 let dropdown = DropdownButton::new("dropdown")
276 .button(Button::new("inner").label("Action").small())
277 .dropdown_menu(|menu, _, _| menu);
278
279 assert_eq!(dropdown.effective_size(), Size::Small);
280 }
281}