Skip to main content

rgpui_component/button/
button_group.rs

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/// A ButtonGroup element, to wrap multiple buttons in a group.
17#[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    // The button props
27    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    /// Creates a new ButtonGroup.
44    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    /// Adds a button as a child to the ButtonGroup.
61    pub fn child(mut self, child: Button) -> Self {
62        self.children.push(child.disabled(self.disabled));
63        self
64    }
65
66    /// Adds multiple buttons as children to the ButtonGroup.
67    pub fn children(mut self, children: impl IntoIterator<Item = Button>) -> Self {
68        self.children.extend(children);
69        self
70    }
71
72    /// With the multiple selection mode, default is false (single selection).
73    pub fn multiple(mut self, multiple: bool) -> Self {
74        self.multiple = multiple;
75        self
76    }
77
78    /// Set the layout of the button group. Default is `Axis::Horizontal`.
79    pub fn layout(mut self, layout: Axis) -> Self {
80        self.layout = layout;
81        self
82    }
83
84    /// With the compact mode for the ButtonGroup.
85    ///
86    /// See also: [`Button::compact()`]
87    pub fn compact(mut self) -> Self {
88        self.compact = true;
89        self
90    }
91
92    /// With the outline mode for the ButtonGroup.
93    ///
94    /// See also: [`Button::outline()`]
95    pub fn outline(mut self) -> Self {
96        self.outline = true;
97        self
98    }
99
100    /// Sets the on_click handler for the ButtonGroup.
101    ///
102    /// The handler first argument is a vector of the selected button indices.
103    ///
104    /// The `&Vec<usize>` is the indices of the clicked (selected in `multiple` mode) buttons.
105    /// For example: `[0, 2, 3]` is means the first, third and fourth buttons are clicked.
106    ///
107    /// ```ignore
108    /// ButtonGroup::new("size-button")
109    ///    .child(Button::new("large").label("Large").selected(self.size == Size::Large))
110    ///    .child(Button::new("medium").label("Medium").selected(self.size == Size::Medium))
111    ///    .child(Button::new("small").label("Small").selected(self.size == Size::Small))
112    ///    .on_click(cx.listener(|view, clicks: &Vec<usize>, _, cx| {
113    ///        if clicks.contains(&0) {
114    ///            view.size = Size::Large;
115    ///        } else if clicks.contains(&1) {
116    ///            view.size = Size::Medium;
117    ///        } else if clicks.contains(&2) {
118    ///            view.size = Size::Small;
119    ///        }
120    ///        cx.notify();
121    ///    }))
122    /// ```
123    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                            // First
182                            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                            // Last
197                            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                            // Middle
212                            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}