#![allow(missing_docs)] use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext}; use std::sync::Arc; use crate::prelude::*; use crate::utils::is_light; use crate::{Color, Icon, IconName, ToggleState}; /// Creates a new checkbox pub fn checkbox(id: impl Into, toggle_state: ToggleState) -> Checkbox { Checkbox::new(id, toggle_state) } /// Creates a new switch pub fn switch(id: impl Into, toggle_state: ToggleState) -> Switch { Switch::new(id, toggle_state) } /// # Checkbox /// /// Checkboxes are used for multiple choices, not for mutually exclusive choices. /// Each checkbox works independently from other checkboxes in the list, /// therefore checking an additional box does not affect any other selections. #[derive(IntoElement)] pub struct Checkbox { id: ElementId, toggle_state: ToggleState, disabled: bool, on_click: Option>, } impl Checkbox { pub fn new(id: impl Into, checked: ToggleState) -> Self { Self { id: id.into(), toggle_state: checked, disabled: false, on_click: None, } } pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } pub fn on_click( mut self, handler: impl Fn(&ToggleState, &mut WindowContext) + 'static, ) -> Self { self.on_click = Some(Box::new(handler)); self } } impl RenderOnce for Checkbox { fn render(self, cx: &mut WindowContext) -> impl IntoElement { let group_id = format!("checkbox_group_{:?}", self.id); let icon = match self.toggle_state { ToggleState::Selected => Some(Icon::new(IconName::Check).size(IconSize::Small).color( if self.disabled { Color::Disabled } else { Color::Selected }, )), ToggleState::Indeterminate => Some( Icon::new(IconName::Dash) .size(IconSize::Small) .color(if self.disabled { Color::Disabled } else { Color::Selected }), ), ToggleState::Unselected => None, }; let selected = self.toggle_state == ToggleState::Selected || self.toggle_state == ToggleState::Indeterminate; let (bg_color, border_color) = match (self.disabled, selected) { (true, _) => ( cx.theme().colors().ghost_element_disabled, cx.theme().colors().border_disabled, ), (false, true) => ( cx.theme().colors().element_selected, cx.theme().colors().border, ), (false, false) => ( cx.theme().colors().element_background, cx.theme().colors().border, ), }; h_flex() .id(self.id) .justify_center() .items_center() .size(DynamicSpacing::Base20.rems(cx)) .group(group_id.clone()) .child( div() .flex() .flex_none() .justify_center() .items_center() .m(DynamicSpacing::Base04.px(cx)) .size(DynamicSpacing::Base16.rems(cx)) .rounded_sm() .bg(bg_color) .border_1() .border_color(border_color) .when(!self.disabled, |this| { this.group_hover(group_id.clone(), |el| { el.bg(cx.theme().colors().element_hover) }) }) .children(icon), ) .when_some( self.on_click.filter(|_| !self.disabled), |this, on_click| { this.on_click(move |_, cx| on_click(&self.toggle_state.inverse(), cx)) }, ) } } /// A [`Checkbox`] that has a [`Label`]. #[derive(IntoElement)] pub struct CheckboxWithLabel { id: ElementId, label: Label, checked: ToggleState, on_click: Arc, } impl CheckboxWithLabel { pub fn new( id: impl Into, label: Label, checked: ToggleState, on_click: impl Fn(&ToggleState, &mut WindowContext) + 'static, ) -> Self { Self { id: id.into(), label, checked, on_click: Arc::new(on_click), } } } impl RenderOnce for CheckboxWithLabel { fn render(self, cx: &mut WindowContext) -> impl IntoElement { h_flex() .gap(DynamicSpacing::Base08.rems(cx)) .child(Checkbox::new(self.id.clone(), self.checked).on_click({ let on_click = self.on_click.clone(); move |checked, cx| { (on_click)(checked, cx); } })) .child( div() .id(SharedString::from(format!("{}-label", self.id))) .on_click(move |_event, cx| { (self.on_click)(&self.checked.inverse(), cx); }) .child(self.label), ) } } /// # Switch /// /// Switches are used to represent opposite states, such as enabled or disabled. #[derive(IntoElement)] pub struct Switch { id: ElementId, toggle_state: ToggleState, disabled: bool, on_click: Option>, } impl Switch { pub fn new(id: impl Into, state: ToggleState) -> Self { Self { id: id.into(), toggle_state: state, disabled: false, on_click: None, } } pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } pub fn on_click( mut self, handler: impl Fn(&ToggleState, &mut WindowContext) + 'static, ) -> Self { self.on_click = Some(Box::new(handler)); self } } impl RenderOnce for Switch { fn render(self, cx: &mut WindowContext) -> impl IntoElement { let is_on = self.toggle_state == ToggleState::Selected; let adjust_ratio = if is_light(cx) { 1.5 } else { 1.0 }; let base_color = cx.theme().colors().text; let bg_color = if is_on { cx.theme() .colors() .element_background .blend(base_color.opacity(0.08)) } else { cx.theme().colors().element_background }; let thumb_color = base_color.opacity(0.8); let thumb_hover_color = base_color; let border_color = cx.theme().colors().border_variant; // Lighter themes need higher contrast borders let border_hover_color = if is_on { border_color.blend(base_color.opacity(0.16 * adjust_ratio)) } else { border_color.blend(base_color.opacity(0.05 * adjust_ratio)) }; let thumb_opacity = match (is_on, self.disabled) { (_, true) => 0.2, (true, false) => 1.0, (false, false) => 0.5, }; let group_id = format!("switch_group_{:?}", self.id); h_flex() .id(self.id) .items_center() .w(DynamicSpacing::Base32.rems(cx)) .h(DynamicSpacing::Base20.rems(cx)) .group(group_id.clone()) .child( h_flex() .when(is_on, |on| on.justify_end()) .when(!is_on, |off| off.justify_start()) .items_center() .size_full() .rounded_full() .px(DynamicSpacing::Base02.px(cx)) .bg(bg_color) .border_1() .border_color(border_color) .when(!self.disabled, |this| { this.group_hover(group_id.clone(), |el| el.border_color(border_hover_color)) }) .child( div() .size(DynamicSpacing::Base12.rems(cx)) .rounded_full() .bg(thumb_color) .when(!self.disabled, |this| { this.group_hover(group_id.clone(), |el| el.bg(thumb_hover_color)) }) .opacity(thumb_opacity), ), ) .when_some( self.on_click.filter(|_| !self.disabled), |this, on_click| { this.on_click(move |_, cx| on_click(&self.toggle_state.inverse(), cx)) }, ) } } /// A [`Switch`] that has a [`Label`]. #[derive(IntoElement)] pub struct SwitchWithLabel { id: ElementId, label: Label, checked: ToggleState, on_click: Arc, } impl SwitchWithLabel { pub fn new( id: impl Into, label: Label, checked: ToggleState, on_click: impl Fn(&ToggleState, &mut WindowContext) + 'static, ) -> Self { Self { id: id.into(), label, checked, on_click: Arc::new(on_click), } } } impl RenderOnce for SwitchWithLabel { fn render(self, cx: &mut WindowContext) -> impl IntoElement { h_flex() .gap(DynamicSpacing::Base08.rems(cx)) .child(Switch::new(self.id.clone(), self.checked).on_click({ let on_click = self.on_click.clone(); move |checked, cx| { (on_click)(checked, cx); } })) .child( div() .id(SharedString::from(format!("{}-label", self.id))) .on_click(move |_event, cx| { (self.on_click)(&self.checked.inverse(), cx); }) .child(self.label), ) } } impl ComponentPreview for Checkbox { fn description() -> impl Into> { "A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state." } fn examples(_: &mut WindowContext) -> Vec> { vec![ example_group_with_title( "Default", vec![ single_example( "Unselected", Checkbox::new("checkbox_unselected", ToggleState::Unselected), ), single_example( "Indeterminate", Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate), ), single_example( "Selected", Checkbox::new("checkbox_selected", ToggleState::Selected), ), ], ), example_group_with_title( "Disabled", vec![ single_example( "Unselected", Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected) .disabled(true), ), single_example( "Indeterminate", Checkbox::new( "checkbox_disabled_indeterminate", ToggleState::Indeterminate, ) .disabled(true), ), single_example( "Selected", Checkbox::new("checkbox_disabled_selected", ToggleState::Selected) .disabled(true), ), ], ), ] } } impl ComponentPreview for Switch { fn description() -> impl Into> { "A switch toggles between two mutually exclusive states, typically used for enabling or disabling a setting." } fn examples(_cx: &mut WindowContext) -> Vec> { vec![ example_group_with_title( "Default", vec![ single_example( "Off", Switch::new("switch_off", ToggleState::Unselected).on_click(|_, _cx| {}), ), single_example( "On", Switch::new("switch_on", ToggleState::Selected).on_click(|_, _cx| {}), ), ], ), example_group_with_title( "Disabled", vec![ single_example( "Off", Switch::new("switch_disabled_off", ToggleState::Unselected).disabled(true), ), single_example( "On", Switch::new("switch_disabled_on", ToggleState::Selected).disabled(true), ), ], ), ] } } impl ComponentPreview for CheckboxWithLabel { fn description() -> impl Into> { "A checkbox with an associated label, allowing users to select an option while providing a descriptive text." } fn examples(_: &mut WindowContext) -> Vec> { vec![example_group(vec![ single_example( "Unselected", CheckboxWithLabel::new( "checkbox_with_label_unselected", Label::new("Always save on quit"), ToggleState::Unselected, |_, _| {}, ), ), single_example( "Indeterminate", CheckboxWithLabel::new( "checkbox_with_label_indeterminate", Label::new("Always save on quit"), ToggleState::Indeterminate, |_, _| {}, ), ), single_example( "Selected", CheckboxWithLabel::new( "checkbox_with_label_selected", Label::new("Always save on quit"), ToggleState::Selected, |_, _| {}, ), ), ])] } } impl ComponentPreview for SwitchWithLabel { fn description() -> impl Into> { "A switch with an associated label, allowing users to select an option while providing a descriptive text." } fn examples(_: &mut WindowContext) -> Vec> { vec![example_group(vec![ single_example( "Off", SwitchWithLabel::new( "switch_with_label_unselected", Label::new("Always save on quit"), ToggleState::Unselected, |_, _| {}, ), ), single_example( "On", SwitchWithLabel::new( "switch_with_label_selected", Label::new("Always save on quit"), ToggleState::Selected, |_, _| {}, ), ), ])] } }