1use rgpui::Corners;
2use rgpui::{
3 Anchor, App, Context, Edges, ElementId, InteractiveElement as _, IntoElement, ParentElement,
4 RenderOnce, SharedString, StyleRefinement, Styled, Window, div, prelude::FluentBuilder,
5};
6
7use crate::{
8 Disableable, IconName, Selectable, Sizable, Size, StyledExt as _,
9 menu::{DropdownMenu, PopupMenu},
10 tooltip::ComponentTooltip,
11};
12
13use super::{Button, ButtonRounded, ButtonVariant, ButtonVariants};
14
15#[derive(IntoElement)]
16pub struct DropdownButton {
17 id: ElementId,
18 style: StyleRefinement,
19 button: Option<Button>,
20 menu:
21 Option<Box<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static>>,
22 selected: bool,
23 disabled: bool,
24 compact: bool,
26 outline: bool,
27 loading: bool,
28 variant: ButtonVariant,
29 size: Size,
30 rounded: ButtonRounded,
31 anchor: Anchor,
32 tooltip: ComponentTooltip,
33}
34
35impl DropdownButton {
36 pub fn new(id: impl Into<ElementId>) -> Self {
38 Self {
39 id: id.into(),
40 style: StyleRefinement::default(),
41 button: None,
42 menu: None,
43 selected: false,
44 disabled: false,
45 compact: false,
46 outline: false,
47 loading: false,
48 variant: ButtonVariant::default(),
49 size: Size::default(),
50 rounded: ButtonRounded::default(),
51 anchor: Anchor::TopRight,
52 tooltip: ComponentTooltip::default(),
53 }
54 }
55
56 pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
58 self.tooltip.text = Some((tooltip.into(), None));
59 self
60 }
61
62 pub fn button(mut self, button: Button) -> Self {
64 self.button = Some(button);
65 self
66 }
67
68 pub fn dropdown_menu(
70 mut self,
71 menu: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
72 ) -> Self {
73 self.menu = Some(Box::new(menu));
74 self
75 }
76
77 pub fn dropdown_menu_with_anchor(
79 mut self,
80 anchor: impl Into<Anchor>,
81 menu: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
82 ) -> Self {
83 self.menu = Some(Box::new(menu));
84 self.anchor = anchor.into();
85 self
86 }
87
88 pub fn rounded(mut self, rounded: impl Into<ButtonRounded>) -> Self {
90 self.rounded = rounded.into();
91 self
92 }
93
94 pub fn compact(mut self) -> Self {
98 self.compact = true;
99 self
100 }
101
102 pub fn outline(mut self) -> Self {
106 self.outline = true;
107 self
108 }
109
110 pub fn loading(mut self, loading: bool) -> Self {
112 self.loading = loading;
113 self
114 }
115}
116
117impl Disableable for DropdownButton {
118 fn disabled(mut self, disabled: bool) -> Self {
119 self.disabled = disabled;
120 self
121 }
122}
123
124impl Styled for DropdownButton {
125 fn style(&mut self) -> &mut rgpui::StyleRefinement {
126 &mut self.style
127 }
128}
129
130impl Sizable for DropdownButton {
131 fn with_size(mut self, size: impl Into<Size>) -> Self {
132 self.size = size.into();
133 self
134 }
135}
136
137impl ButtonVariants for DropdownButton {
138 fn with_variant(mut self, variant: ButtonVariant) -> Self {
139 self.variant = variant;
140 self
141 }
142}
143
144impl Selectable for DropdownButton {
145 fn selected(mut self, selected: bool) -> Self {
146 self.selected = selected;
147 self
148 }
149
150 fn is_selected(&self) -> bool {
151 self.selected
152 }
153}
154
155impl RenderOnce for DropdownButton {
156 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
157 let rounded = self.variant.is_ghost() && !self.selected;
158
159 div()
160 .id(self.id)
161 .h_flex()
162 .refine_style(&self.style)
163 .when_some(self.button, |this, button| {
164 this.child(
165 button
166 .rounded(self.rounded)
167 .border_corners(Corners {
168 top_left: true,
169 top_right: rounded,
170 bottom_left: true,
171 bottom_right: rounded,
172 })
173 .border_edges(Edges {
174 left: true,
175 top: true,
176 right: true,
177 bottom: true,
178 })
179 .loading(self.loading)
180 .selected(self.selected)
181 .disabled(self.disabled || self.loading)
182 .when(self.compact, |this| this.compact())
183 .when(self.outline, |this| this.outline())
184 .with_size(self.size)
185 .with_variant(self.variant),
186 )
187 .when_some(self.menu, |this, menu| {
188 this.child(
189 Button::new("popup")
190 .icon(IconName::ChevronDown)
191 .rounded(self.rounded)
192 .border_edges(Edges {
193 left: rounded,
194 top: true,
195 right: true,
196 bottom: true,
197 })
198 .border_corners(Corners {
199 top_left: rounded,
200 top_right: true,
201 bottom_left: rounded,
202 bottom_right: true,
203 })
204 .selected(self.selected)
205 .disabled(self.disabled || self.loading)
206 .when(self.compact, |this| this.compact())
207 .when(self.outline, |this| this.outline())
208 .with_size(self.size)
209 .with_variant(self.variant)
210 .dropdown_menu_with_anchor(self.anchor, menu),
211 )
212 })
213 })
214 .map(|this| self.tooltip.apply(this))
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[rgpui::test]
223 fn test_dropdown_button_builder(_cx: &mut rgpui::TestAppContext) {
224 let button = Button::new("inner").label("Action");
225 let dropdown = DropdownButton::new("complex-dropdown")
226 .button(button)
227 .primary()
228 .outline()
229 .large()
230 .compact()
231 .loading(false)
232 .disabled(false)
233 .selected(false)
234 .rounded(ButtonRounded::Medium)
235 .dropdown_menu_with_anchor(Anchor::BottomLeft, |menu, _, _| menu);
236
237 assert!(dropdown.button.is_some());
238 assert_eq!(dropdown.variant, ButtonVariant::Primary);
239 assert!(dropdown.outline);
240 assert_eq!(dropdown.size, Size::Large);
241 assert!(dropdown.compact);
242 assert!(!dropdown.loading);
243 assert!(!dropdown.disabled);
244 assert!(!dropdown.selected);
245 assert!(matches!(dropdown.rounded, ButtonRounded::Medium));
246 assert!(dropdown.menu.is_some());
247 assert_eq!(dropdown.anchor, Anchor::BottomLeft);
248 }
249}