toggle.rs

  1#![allow(missing_docs)]
  2
  3use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
  4use std::sync::Arc;
  5
  6use crate::prelude::*;
  7use crate::utils::is_light;
  8use crate::{Color, Icon, IconName, ToggleState};
  9
 10/// Creates a new checkbox
 11pub fn checkbox(id: impl Into<ElementId>, toggle_state: ToggleState) -> Checkbox {
 12    Checkbox::new(id, toggle_state)
 13}
 14
 15/// Creates a new switch
 16pub fn switch(id: impl Into<ElementId>, toggle_state: ToggleState) -> Switch {
 17    Switch::new(id, toggle_state)
 18}
 19
 20/// # Checkbox
 21///
 22/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
 23/// Each checkbox works independently from other checkboxes in the list,
 24/// therefore checking an additional box does not affect any other selections.
 25#[derive(IntoElement)]
 26pub struct Checkbox {
 27    id: ElementId,
 28    toggle_state: ToggleState,
 29    disabled: bool,
 30    on_click: Option<Box<dyn Fn(&ToggleState, &mut WindowContext) + 'static>>,
 31}
 32
 33impl Checkbox {
 34    pub fn new(id: impl Into<ElementId>, checked: ToggleState) -> Self {
 35        Self {
 36            id: id.into(),
 37            toggle_state: checked,
 38            disabled: false,
 39            on_click: None,
 40        }
 41    }
 42
 43    pub fn disabled(mut self, disabled: bool) -> Self {
 44        self.disabled = disabled;
 45        self
 46    }
 47
 48    pub fn on_click(
 49        mut self,
 50        handler: impl Fn(&ToggleState, &mut WindowContext) + 'static,
 51    ) -> Self {
 52        self.on_click = Some(Box::new(handler));
 53        self
 54    }
 55}
 56
 57impl RenderOnce for Checkbox {
 58    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
 59        let group_id = format!("checkbox_group_{:?}", self.id);
 60
 61        let icon = match self.toggle_state {
 62            ToggleState::Selected => Some(Icon::new(IconName::Check).size(IconSize::Small).color(
 63                if self.disabled {
 64                    Color::Disabled
 65                } else {
 66                    Color::Selected
 67                },
 68            )),
 69            ToggleState::Indeterminate => Some(
 70                Icon::new(IconName::Dash)
 71                    .size(IconSize::Small)
 72                    .color(if self.disabled {
 73                        Color::Disabled
 74                    } else {
 75                        Color::Selected
 76                    }),
 77            ),
 78            ToggleState::Unselected => None,
 79        };
 80
 81        let selected = self.toggle_state == ToggleState::Selected
 82            || self.toggle_state == ToggleState::Indeterminate;
 83
 84        let (bg_color, border_color) = match (self.disabled, selected) {
 85            (true, _) => (
 86                cx.theme().colors().ghost_element_disabled,
 87                cx.theme().colors().border_disabled,
 88            ),
 89            (false, true) => (
 90                cx.theme().colors().element_selected,
 91                cx.theme().colors().border,
 92            ),
 93            (false, false) => (
 94                cx.theme().colors().element_background,
 95                cx.theme().colors().border,
 96            ),
 97        };
 98
 99        h_flex()
100            .id(self.id)
101            .justify_center()
102            .items_center()
103            .size(DynamicSpacing::Base20.rems(cx))
104            .group(group_id.clone())
105            .child(
106                div()
107                    .flex()
108                    .flex_none()
109                    .justify_center()
110                    .items_center()
111                    .m(DynamicSpacing::Base04.px(cx))
112                    .size(DynamicSpacing::Base16.rems(cx))
113                    .rounded_sm()
114                    .bg(bg_color)
115                    .border_1()
116                    .border_color(border_color)
117                    .when(!self.disabled, |this| {
118                        this.group_hover(group_id.clone(), |el| {
119                            el.bg(cx.theme().colors().element_hover)
120                        })
121                    })
122                    .children(icon),
123            )
124            .when_some(
125                self.on_click.filter(|_| !self.disabled),
126                |this, on_click| {
127                    this.on_click(move |_, cx| on_click(&self.toggle_state.inverse(), cx))
128                },
129            )
130    }
131}
132
133/// A [`Checkbox`] that has a [`Label`].
134#[derive(IntoElement)]
135pub struct CheckboxWithLabel {
136    id: ElementId,
137    label: Label,
138    checked: ToggleState,
139    on_click: Arc<dyn Fn(&ToggleState, &mut WindowContext) + 'static>,
140}
141
142impl CheckboxWithLabel {
143    pub fn new(
144        id: impl Into<ElementId>,
145        label: Label,
146        checked: ToggleState,
147        on_click: impl Fn(&ToggleState, &mut WindowContext) + 'static,
148    ) -> Self {
149        Self {
150            id: id.into(),
151            label,
152            checked,
153            on_click: Arc::new(on_click),
154        }
155    }
156}
157
158impl RenderOnce for CheckboxWithLabel {
159    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
160        h_flex()
161            .gap(DynamicSpacing::Base08.rems(cx))
162            .child(Checkbox::new(self.id.clone(), self.checked).on_click({
163                let on_click = self.on_click.clone();
164                move |checked, cx| {
165                    (on_click)(checked, cx);
166                }
167            }))
168            .child(
169                div()
170                    .id(SharedString::from(format!("{}-label", self.id)))
171                    .on_click(move |_event, cx| {
172                        (self.on_click)(&self.checked.inverse(), cx);
173                    })
174                    .child(self.label),
175            )
176    }
177}
178
179/// # Switch
180///
181/// Switches are used to represent opposite states, such as enabled or disabled.
182#[derive(IntoElement)]
183pub struct Switch {
184    id: ElementId,
185    toggle_state: ToggleState,
186    disabled: bool,
187    on_click: Option<Box<dyn Fn(&ToggleState, &mut WindowContext) + 'static>>,
188}
189
190impl Switch {
191    pub fn new(id: impl Into<ElementId>, state: ToggleState) -> Self {
192        Self {
193            id: id.into(),
194            toggle_state: state,
195            disabled: false,
196            on_click: None,
197        }
198    }
199
200    pub fn disabled(mut self, disabled: bool) -> Self {
201        self.disabled = disabled;
202        self
203    }
204
205    pub fn on_click(
206        mut self,
207        handler: impl Fn(&ToggleState, &mut WindowContext) + 'static,
208    ) -> Self {
209        self.on_click = Some(Box::new(handler));
210        self
211    }
212}
213
214impl RenderOnce for Switch {
215    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
216        let is_on = self.toggle_state == ToggleState::Selected;
217        let adjust_ratio = if is_light(cx) { 1.5 } else { 1.0 };
218        let base_color = cx.theme().colors().text;
219
220        let bg_color = if is_on {
221            cx.theme()
222                .colors()
223                .element_background
224                .blend(base_color.opacity(0.08))
225        } else {
226            cx.theme().colors().element_background
227        };
228        let thumb_color = base_color.opacity(0.8);
229        let thumb_hover_color = base_color;
230        let border_color = cx.theme().colors().border_variant;
231        // Lighter themes need higher contrast borders
232        let border_hover_color = if is_on {
233            border_color.blend(base_color.opacity(0.16 * adjust_ratio))
234        } else {
235            border_color.blend(base_color.opacity(0.05 * adjust_ratio))
236        };
237        let thumb_opacity = match (is_on, self.disabled) {
238            (_, true) => 0.2,
239            (true, false) => 1.0,
240            (false, false) => 0.5,
241        };
242
243        let group_id = format!("switch_group_{:?}", self.id);
244
245        h_flex()
246            .id(self.id)
247            .items_center()
248            .w(DynamicSpacing::Base32.rems(cx))
249            .h(DynamicSpacing::Base20.rems(cx))
250            .group(group_id.clone())
251            .child(
252                h_flex()
253                    .when(is_on, |on| on.justify_end())
254                    .when(!is_on, |off| off.justify_start())
255                    .items_center()
256                    .size_full()
257                    .rounded_full()
258                    .px(DynamicSpacing::Base02.px(cx))
259                    .bg(bg_color)
260                    .border_1()
261                    .border_color(border_color)
262                    .when(!self.disabled, |this| {
263                        this.group_hover(group_id.clone(), |el| el.border_color(border_hover_color))
264                    })
265                    .child(
266                        div()
267                            .size(DynamicSpacing::Base12.rems(cx))
268                            .rounded_full()
269                            .bg(thumb_color)
270                            .when(!self.disabled, |this| {
271                                this.group_hover(group_id.clone(), |el| el.bg(thumb_hover_color))
272                            })
273                            .opacity(thumb_opacity),
274                    ),
275            )
276            .when_some(
277                self.on_click.filter(|_| !self.disabled),
278                |this, on_click| {
279                    this.on_click(move |_, cx| on_click(&self.toggle_state.inverse(), cx))
280                },
281            )
282    }
283}
284
285/// A [`Switch`] that has a [`Label`].
286#[derive(IntoElement)]
287pub struct SwitchWithLabel {
288    id: ElementId,
289    label: Label,
290    checked: ToggleState,
291    on_click: Arc<dyn Fn(&ToggleState, &mut WindowContext) + 'static>,
292}
293
294impl SwitchWithLabel {
295    pub fn new(
296        id: impl Into<ElementId>,
297        label: Label,
298        checked: ToggleState,
299        on_click: impl Fn(&ToggleState, &mut WindowContext) + 'static,
300    ) -> Self {
301        Self {
302            id: id.into(),
303            label,
304            checked,
305            on_click: Arc::new(on_click),
306        }
307    }
308}
309
310impl RenderOnce for SwitchWithLabel {
311    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
312        h_flex()
313            .gap(DynamicSpacing::Base08.rems(cx))
314            .child(Switch::new(self.id.clone(), self.checked).on_click({
315                let on_click = self.on_click.clone();
316                move |checked, cx| {
317                    (on_click)(checked, cx);
318                }
319            }))
320            .child(
321                div()
322                    .id(SharedString::from(format!("{}-label", self.id)))
323                    .on_click(move |_event, cx| {
324                        (self.on_click)(&self.checked.inverse(), cx);
325                    })
326                    .child(self.label),
327            )
328    }
329}
330
331impl ComponentPreview for Checkbox {
332    fn description() -> impl Into<Option<&'static str>> {
333        "A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
334    }
335
336    fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
337        vec![
338            example_group_with_title(
339                "Default",
340                vec![
341                    single_example(
342                        "Unselected",
343                        Checkbox::new("checkbox_unselected", ToggleState::Unselected),
344                    ),
345                    single_example(
346                        "Indeterminate",
347                        Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate),
348                    ),
349                    single_example(
350                        "Selected",
351                        Checkbox::new("checkbox_selected", ToggleState::Selected),
352                    ),
353                ],
354            ),
355            example_group_with_title(
356                "Disabled",
357                vec![
358                    single_example(
359                        "Unselected",
360                        Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected)
361                            .disabled(true),
362                    ),
363                    single_example(
364                        "Indeterminate",
365                        Checkbox::new(
366                            "checkbox_disabled_indeterminate",
367                            ToggleState::Indeterminate,
368                        )
369                        .disabled(true),
370                    ),
371                    single_example(
372                        "Selected",
373                        Checkbox::new("checkbox_disabled_selected", ToggleState::Selected)
374                            .disabled(true),
375                    ),
376                ],
377            ),
378        ]
379    }
380}
381
382impl ComponentPreview for Switch {
383    fn description() -> impl Into<Option<&'static str>> {
384        "A switch toggles between two mutually exclusive states, typically used for enabling or disabling a setting."
385    }
386
387    fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
388        vec![
389            example_group_with_title(
390                "Default",
391                vec![
392                    single_example(
393                        "Off",
394                        Switch::new("switch_off", ToggleState::Unselected).on_click(|_, _cx| {}),
395                    ),
396                    single_example(
397                        "On",
398                        Switch::new("switch_on", ToggleState::Selected).on_click(|_, _cx| {}),
399                    ),
400                ],
401            ),
402            example_group_with_title(
403                "Disabled",
404                vec![
405                    single_example(
406                        "Off",
407                        Switch::new("switch_disabled_off", ToggleState::Unselected).disabled(true),
408                    ),
409                    single_example(
410                        "On",
411                        Switch::new("switch_disabled_on", ToggleState::Selected).disabled(true),
412                    ),
413                ],
414            ),
415        ]
416    }
417}
418
419impl ComponentPreview for CheckboxWithLabel {
420    fn description() -> impl Into<Option<&'static str>> {
421        "A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
422    }
423
424    fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
425        vec![example_group(vec![
426            single_example(
427                "Unselected",
428                CheckboxWithLabel::new(
429                    "checkbox_with_label_unselected",
430                    Label::new("Always save on quit"),
431                    ToggleState::Unselected,
432                    |_, _| {},
433                ),
434            ),
435            single_example(
436                "Indeterminate",
437                CheckboxWithLabel::new(
438                    "checkbox_with_label_indeterminate",
439                    Label::new("Always save on quit"),
440                    ToggleState::Indeterminate,
441                    |_, _| {},
442                ),
443            ),
444            single_example(
445                "Selected",
446                CheckboxWithLabel::new(
447                    "checkbox_with_label_selected",
448                    Label::new("Always save on quit"),
449                    ToggleState::Selected,
450                    |_, _| {},
451                ),
452            ),
453        ])]
454    }
455}
456
457impl ComponentPreview for SwitchWithLabel {
458    fn description() -> impl Into<Option<&'static str>> {
459        "A switch with an associated label, allowing users to select an option while providing a descriptive text."
460    }
461
462    fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
463        vec![example_group(vec![
464            single_example(
465                "Off",
466                SwitchWithLabel::new(
467                    "switch_with_label_unselected",
468                    Label::new("Always save on quit"),
469                    ToggleState::Unselected,
470                    |_, _| {},
471                ),
472            ),
473            single_example(
474                "On",
475                SwitchWithLabel::new(
476                    "switch_with_label_selected",
477                    Label::new("Always save on quit"),
478                    ToggleState::Selected,
479                    |_, _| {},
480                ),
481            ),
482        ])]
483    }
484}