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