1use rgpui::Corners;
2use rgpui::InteractiveElement;
3use rgpui::ParentElement;
4use rgpui::{App, Axis, Edges, ElementId, IntoElement, Window};
5use rgpui::{
6 RenderOnce, StatefulInteractiveElement as _, StyleRefinement, Styled, div,
7 prelude::FluentBuilder as _,
8};
9use std::{cell::Cell, rc::Rc};
10
11use crate::{
12 Disableable, Sizable, Size, StyledExt,
13 button::{Button, ButtonVariant, ButtonVariants},
14};
15
16#[derive(IntoElement)]
18pub struct ButtonGroup {
19 id: ElementId,
20 style: StyleRefinement,
21 children: Vec<Button>,
22 pub(super) multiple: bool,
23 pub(super) disabled: bool,
24 pub(super) layout: Axis,
25
26 pub(super) compact: bool,
28 pub(super) outline: bool,
29 pub(super) variant: Option<ButtonVariant>,
30 pub(super) size: Option<Size>,
31
32 on_click: Option<Box<dyn Fn(&Vec<usize>, &mut Window, &mut App) + 'static>>,
33}
34
35impl Disableable for ButtonGroup {
36 fn disabled(mut self, disabled: bool) -> Self {
37 self.disabled = disabled;
38 self
39 }
40}
41
42impl ButtonGroup {
43 pub fn new(id: impl Into<ElementId>) -> Self {
45 Self {
46 id: id.into(),
47 style: StyleRefinement::default(),
48 children: Vec::new(),
49 variant: None,
50 size: None,
51 compact: false,
52 outline: false,
53 multiple: false,
54 disabled: false,
55 layout: Axis::Horizontal,
56 on_click: None,
57 }
58 }
59
60 pub fn child(mut self, child: Button) -> Self {
62 self.children.push(child.disabled(self.disabled));
63 self
64 }
65
66 pub fn children(mut self, children: impl IntoIterator<Item = Button>) -> Self {
68 self.children.extend(children);
69 self
70 }
71
72 pub fn multiple(mut self, multiple: bool) -> Self {
74 self.multiple = multiple;
75 self
76 }
77
78 pub fn layout(mut self, layout: Axis) -> Self {
80 self.layout = layout;
81 self
82 }
83
84 pub fn compact(mut self) -> Self {
88 self.compact = true;
89 self
90 }
91
92 pub fn outline(mut self) -> Self {
96 self.outline = true;
97 self
98 }
99
100 pub fn on_click(
124 mut self,
125 handler: impl Fn(&Vec<usize>, &mut Window, &mut App) + 'static,
126 ) -> Self {
127 self.on_click = Some(Box::new(handler));
128 self
129 }
130}
131
132impl Sizable for ButtonGroup {
133 fn with_size(mut self, size: impl Into<Size>) -> Self {
134 self.size = Some(size.into());
135 self
136 }
137}
138
139impl Styled for ButtonGroup {
140 fn style(&mut self) -> &mut rgpui::StyleRefinement {
141 &mut self.style
142 }
143}
144
145impl ButtonVariants for ButtonGroup {
146 fn with_variant(mut self, variant: ButtonVariant) -> Self {
147 self.variant = Some(variant);
148 self
149 }
150}
151
152impl RenderOnce for ButtonGroup {
153 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
154 let children_len = self.children.len();
155 let mut selected_ixs: Vec<usize> = Vec::new();
156 let state = Rc::new(Cell::new(None));
157
158 for (ix, child) in self.children.iter().enumerate() {
159 if child.selected {
160 selected_ixs.push(ix);
161 }
162 }
163
164 let vertical = self.layout == Axis::Vertical;
165
166 div()
167 .id(self.id)
168 .flex()
169 .when(vertical, |this| this.flex_col().justify_center())
170 .when(!vertical, |this| this.items_center())
171 .refine_style(&self.style)
172 .children(
173 self.children
174 .into_iter()
175 .enumerate()
176 .map(|(child_index, child)| {
177 let state = Rc::clone(&state);
178 let child = if children_len == 1 {
179 child
180 } else if child_index == 0 {
181 child
183 .border_corners(Corners {
184 top_left: true,
185 top_right: vertical,
186 bottom_left: !vertical,
187 bottom_right: false,
188 })
189 .border_edges(Edges {
190 left: true,
191 top: true,
192 right: true,
193 bottom: true,
194 })
195 } else if child_index == children_len - 1 {
196 child
198 .border_edges(Edges {
199 left: vertical,
200 top: !vertical,
201 right: true,
202 bottom: true,
203 })
204 .border_corners(Corners {
205 top_left: false,
206 top_right: !vertical,
207 bottom_left: vertical,
208 bottom_right: true,
209 })
210 } else {
211 child
213 .border_corners(Corners {
214 top_left: false,
215 top_right: false,
216 bottom_left: false,
217 bottom_right: false,
218 })
219 .border_edges(Edges {
220 left: vertical,
221 top: !vertical,
222 right: true,
223 bottom: true,
224 })
225 }
226 .when_some(self.size, |this, size| this.with_size(size))
227 .when_some(self.variant, |this, variant| this.with_variant(variant))
228 .when(self.compact, |this| this.compact())
229 .when(self.outline, |this| this.outline())
230 .when(self.on_click.is_some(), |this| {
231 this.on_click(move |_, _, _| {
232 state.set(Some(child_index));
233 })
234 });
235
236 child
237 }),
238 )
239 .when_some(
240 self.on_click.filter(|_| !self.disabled),
241 move |this, on_click| {
242 this.on_click(move |_, window, cx| {
243 let mut selected_ixs = selected_ixs.clone();
244 if let Some(ix) = state.get() {
245 if self.multiple {
246 if let Some(pos) = selected_ixs.iter().position(|&i| i == ix) {
247 selected_ixs.remove(pos);
248 } else {
249 selected_ixs.push(ix);
250 }
251 } else {
252 selected_ixs.clear();
253 selected_ixs.push(ix);
254 }
255 }
256
257 on_click(&selected_ixs, window, cx);
258 })
259 },
260 )
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use rgpui::Axis;
268
269 #[rgpui::test]
270 fn test_button_group_builder(_cx: &mut rgpui::TestAppContext) {
271 let group = ButtonGroup::new("complex-group")
272 .child(Button::new("btn1").label("One"))
273 .child(Button::new("btn2").label("Two"))
274 .child(Button::new("btn3").label("Three"))
275 .primary()
276 .large()
277 .outline()
278 .compact()
279 .multiple(true)
280 .layout(Axis::Vertical)
281 .disabled(false)
282 .on_click(|_, _, _| {});
283
284 assert_eq!(group.children.len(), 3);
285 assert_eq!(group.variant, Some(ButtonVariant::Primary));
286 assert_eq!(group.size, Some(Size::Large));
287 assert!(group.outline);
288 assert!(group.compact);
289 assert!(group.multiple);
290 assert_eq!(group.layout, Axis::Vertical);
291 assert!(!group.disabled);
292 assert!(group.on_click.is_some());
293 }
294}