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
285impl ComponentPreview for Checkbox {
286    fn description() -> impl Into<Option<&'static str>> {
287        "A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
288    }
289
290    fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
291        vec![
292            example_group_with_title(
293                "Default",
294                vec![
295                    single_example(
296                        "Unselected",
297                        Checkbox::new("checkbox_unselected", ToggleState::Unselected),
298                    ),
299                    single_example(
300                        "Indeterminate",
301                        Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate),
302                    ),
303                    single_example(
304                        "Selected",
305                        Checkbox::new("checkbox_selected", ToggleState::Selected),
306                    ),
307                ],
308            ),
309            example_group_with_title(
310                "Disabled",
311                vec![
312                    single_example(
313                        "Unselected",
314                        Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected)
315                            .disabled(true),
316                    ),
317                    single_example(
318                        "Indeterminate",
319                        Checkbox::new(
320                            "checkbox_disabled_indeterminate",
321                            ToggleState::Indeterminate,
322                        )
323                        .disabled(true),
324                    ),
325                    single_example(
326                        "Selected",
327                        Checkbox::new("checkbox_disabled_selected", ToggleState::Selected)
328                            .disabled(true),
329                    ),
330                ],
331            ),
332        ]
333    }
334}
335
336impl ComponentPreview for Switch {
337    fn description() -> impl Into<Option<&'static str>> {
338        "A switch toggles between two mutually exclusive states, typically used for enabling or disabling a setting."
339    }
340
341    fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
342        vec![
343            example_group_with_title(
344                "Default",
345                vec![
346                    single_example(
347                        "Off",
348                        Switch::new("switch_off", ToggleState::Unselected).on_click(|_, _cx| {}),
349                    ),
350                    single_example(
351                        "On",
352                        Switch::new("switch_on", ToggleState::Selected).on_click(|_, _cx| {}),
353                    ),
354                ],
355            ),
356            example_group_with_title(
357                "Disabled",
358                vec![
359                    single_example(
360                        "Off",
361                        Switch::new("switch_disabled_off", ToggleState::Unselected).disabled(true),
362                    ),
363                    single_example(
364                        "On",
365                        Switch::new("switch_disabled_on", ToggleState::Selected).disabled(true),
366                    ),
367                ],
368            ),
369        ]
370    }
371}
372
373impl ComponentPreview for CheckboxWithLabel {
374    fn description() -> impl Into<Option<&'static str>> {
375        "A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
376    }
377
378    fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
379        vec![example_group(vec![
380            single_example(
381                "Unselected",
382                CheckboxWithLabel::new(
383                    "checkbox_with_label_unselected",
384                    Label::new("Always save on quit"),
385                    ToggleState::Unselected,
386                    |_, _| {},
387                ),
388            ),
389            single_example(
390                "Indeterminate",
391                CheckboxWithLabel::new(
392                    "checkbox_with_label_indeterminate",
393                    Label::new("Always save on quit"),
394                    ToggleState::Indeterminate,
395                    |_, _| {},
396                ),
397            ),
398            single_example(
399                "Selected",
400                CheckboxWithLabel::new(
401                    "checkbox_with_label_selected",
402                    Label::new("Always save on quit"),
403                    ToggleState::Selected,
404                    |_, _| {},
405                ),
406            ),
407        ])]
408    }
409}