gpui_component/button/
toggle.rs

1use std::{cell::Cell, rc::Rc};
2
3use gpui::{
4    div, prelude::FluentBuilder as _, AnyElement, App, ElementId, InteractiveElement, IntoElement,
5    ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, StyleRefinement, Styled,
6    Window,
7};
8use smallvec::{smallvec, SmallVec};
9
10use crate::{h_flex, ActiveTheme, Disableable, Icon, Sizable, Size, StyledExt};
11
12#[derive(Default, Copy, Debug, Clone, PartialEq, Eq, Hash)]
13pub enum ToggleVariant {
14    #[default]
15    Ghost,
16    Outline,
17}
18
19pub trait ToggleVariants: Sized {
20    /// Set the variant of the toggle.
21    fn with_variant(self, variant: ToggleVariant) -> Self;
22    /// Set the variant to ghost.
23    fn ghost(self) -> Self {
24        self.with_variant(ToggleVariant::Ghost)
25    }
26    /// Set the variant to outline.
27    fn outline(self) -> Self {
28        self.with_variant(ToggleVariant::Outline)
29    }
30}
31
32#[derive(IntoElement)]
33pub struct Toggle {
34    id: ElementId,
35    style: StyleRefinement,
36    checked: bool,
37    size: Size,
38    variant: ToggleVariant,
39    disabled: bool,
40    children: SmallVec<[AnyElement; 1]>,
41    on_click: Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
42}
43
44impl Toggle {
45    /// Create a new Toggle element.
46    pub fn new(id: impl Into<ElementId>) -> Self {
47        Self {
48            id: id.into(),
49            style: StyleRefinement::default(),
50            checked: false,
51            size: Size::default(),
52            variant: ToggleVariant::default(),
53            disabled: false,
54            children: smallvec![],
55            on_click: None,
56        }
57    }
58
59    /// Add a label to the toggle.
60    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
61        let label: SharedString = label.into();
62        self.children.push(label.into_any_element());
63        self
64    }
65
66    /// Add icon to the toggle.
67    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
68        let icon: Icon = icon.into();
69        self.children.push(icon.into());
70        self
71    }
72
73    /// Set the checked state of the toggle, default: false
74    pub fn checked(mut self, checked: bool) -> Self {
75        self.checked = checked;
76        self
77    }
78
79    /// Set the callback to be called when the toggle is clicked.
80    ///
81    /// The `&bool` parameter represents the new checked state of the toggle.
82    pub fn on_click(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
83        self.on_click = Some(Box::new(handler));
84        self
85    }
86}
87
88impl ToggleVariants for Toggle {
89    fn with_variant(mut self, variant: ToggleVariant) -> Self {
90        self.variant = variant;
91        self
92    }
93}
94
95impl ParentElement for Toggle {
96    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
97        self.children.extend(elements);
98    }
99}
100
101impl Disableable for Toggle {
102    fn disabled(mut self, disabled: bool) -> Self {
103        self.disabled = disabled;
104        self
105    }
106}
107
108impl Sizable for Toggle {
109    fn with_size(mut self, size: impl Into<Size>) -> Self {
110        self.size = size.into();
111        self
112    }
113}
114
115impl Styled for Toggle {
116    fn style(&mut self) -> &mut StyleRefinement {
117        &mut self.style
118    }
119}
120
121impl RenderOnce for Toggle {
122    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
123        let checked = self.checked;
124        let disabled = self.disabled;
125        let hoverable = !disabled && !checked;
126
127        div()
128            .id(self.id)
129            .flex()
130            .flex_row()
131            .items_center()
132            .justify_center()
133            .map(|this| match self.size {
134                Size::XSmall => this.min_w_5().h_5().px_0p5().text_xs(),
135                Size::Small => this.min_w_6().h_6().px_1().text_sm(),
136                Size::Large => this.min_w_9().h_9().px_3().text_lg(),
137                _ => this.min_w_8().h_8().px_2(),
138            })
139            .rounded(cx.theme().radius)
140            .when(self.variant == ToggleVariant::Outline, |this| {
141                this.border_1()
142                    .border_color(cx.theme().border)
143                    .bg(cx.theme().background)
144                    .when(cx.theme().shadow, |this| this.shadow_xs())
145            })
146            .when(hoverable, |this| {
147                this.hover(|this| {
148                    this.bg(cx.theme().accent)
149                        .text_color(cx.theme().accent_foreground)
150                })
151            })
152            .when(checked, |this| {
153                this.bg(cx.theme().accent)
154                    .text_color(cx.theme().accent_foreground)
155            })
156            .refine_style(&self.style)
157            .children(self.children)
158            .when(!disabled, |this| {
159                this.when_some(self.on_click, |this, on_click| {
160                    this.on_click(move |_, window, cx| on_click(&!checked, window, cx))
161                })
162            })
163    }
164}
165
166/// A group of toggles.
167#[derive(IntoElement)]
168pub struct ToggleGroup {
169    id: ElementId,
170    style: StyleRefinement,
171    size: Size,
172    variant: ToggleVariant,
173    disabled: bool,
174    items: Vec<Toggle>,
175    on_click: Option<Rc<dyn Fn(&Vec<bool>, &mut Window, &mut App) + 'static>>,
176}
177
178impl ToggleGroup {
179    /// Create a new ToggleGroup element.
180    pub fn new(id: impl Into<ElementId>) -> Self {
181        Self {
182            id: id.into(),
183            style: StyleRefinement::default(),
184            size: Size::default(),
185            variant: ToggleVariant::default(),
186            disabled: false,
187            items: Vec::new(),
188            on_click: None,
189        }
190    }
191
192    /// Add a child [`Toggle`] to the group.
193    pub fn child(mut self, toggle: impl Into<Toggle>) -> Self {
194        self.items.push(toggle.into());
195        self
196    }
197
198    /// Add multiple [`Toggle`]s to the group.
199    pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Toggle>>) -> Self {
200        self.items.extend(children.into_iter().map(Into::into));
201        self
202    }
203
204    /// Set the callback to be called when the toggle group changes.
205    ///
206    /// The `&Vec<bool>` parameter represents the new check state of each [`Toggle`] in the group.
207    pub fn on_click(
208        mut self,
209        on_click: impl Fn(&Vec<bool>, &mut Window, &mut App) + 'static,
210    ) -> Self {
211        self.on_click = Some(Rc::new(on_click));
212        self
213    }
214}
215
216impl Sizable for ToggleGroup {
217    fn with_size(mut self, size: impl Into<Size>) -> Self {
218        self.size = size.into();
219        self
220    }
221}
222
223impl ToggleVariants for ToggleGroup {
224    fn with_variant(mut self, variant: ToggleVariant) -> Self {
225        self.variant = variant;
226        self
227    }
228}
229
230impl Disableable for ToggleGroup {
231    fn disabled(mut self, disabled: bool) -> Self {
232        self.disabled = disabled;
233        self
234    }
235}
236
237impl Styled for ToggleGroup {
238    fn style(&mut self) -> &mut StyleRefinement {
239        &mut self.style
240    }
241}
242
243impl RenderOnce for ToggleGroup {
244    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
245        let disabled = self.disabled;
246        let checks = self
247            .items
248            .iter()
249            .map(|item| item.checked)
250            .collect::<Vec<bool>>();
251        let state = Rc::new(Cell::new(None));
252
253        h_flex()
254            .id(self.id)
255            .gap_1()
256            .refine_style(&self.style)
257            .children(self.items.into_iter().enumerate().map({
258                |(ix, item)| {
259                    let state = state.clone();
260                    item.disabled(disabled)
261                        .with_size(self.size)
262                        .with_variant(self.variant)
263                        .on_click(move |_, _, _| {
264                            state.set(Some(ix));
265                        })
266                }
267            }))
268            .when(!disabled, |this| {
269                this.when_some(self.on_click, |this, on_click| {
270                    this.on_click(move |_, window, cx| {
271                        if let Some(ix) = state.get() {
272                            let mut checks = checks.clone();
273                            checks[ix] = !checks[ix];
274                            on_click(&checks, window, cx);
275                        }
276                    })
277                })
278            })
279    }
280}