diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index aa7e4b181a..cc2d7cffae 100644 --- a/crates/ui/src/components/toggle.rs +++ b/crates/ui/src/components/toggle.rs @@ -1,12 +1,13 @@ -#![allow(missing_docs)] - -use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext}; +use gpui::{div, hsla, prelude::*, ElementId, Hsla, IntoElement, Styled, WindowContext}; use std::sync::Arc; -use crate::prelude::*; use crate::utils::is_light; +use crate::{prelude::*, ElevationIndex}; use crate::{Color, Icon, IconName, ToggleState}; +// TODO: Checkbox, CheckboxWithLabel, Switch, SwitchWithLabel all could be +// restructured to use a ToggleLike, similar to Button/Buttonlike, Label/Labellike + /// Creates a new checkbox pub fn checkbox(id: impl Into, toggle_state: ToggleState) -> Checkbox { Checkbox::new(id, toggle_state) @@ -17,6 +18,19 @@ pub fn switch(id: impl Into, toggle_state: ToggleState) -> Switch { Switch::new(id, toggle_state) } +#[derive(Debug, Default, Clone, PartialEq, Eq)] +/// The visual style of a toggle +pub enum ToggleStyle { + /// Toggle has a transparent background + #[default] + Ghost, + /// Toggle has a filled background based on the + /// elevation index of the parent container + ElevationBased(ElevationIndex), + /// A custom style using a color to tint the toggle + Custom(Hsla), +} + /// # Checkbox /// /// Checkboxes are used for multiple choices, not for mutually exclusive choices. @@ -28,23 +42,30 @@ pub struct Checkbox { toggle_state: ToggleState, disabled: bool, on_click: Option>, + filled: bool, + style: ToggleStyle, } impl Checkbox { + /// Creates a new [`Checkbox`] pub fn new(id: impl Into, checked: ToggleState) -> Self { Self { id: id.into(), toggle_state: checked, disabled: false, on_click: None, + filled: false, + style: ToggleStyle::default(), } } + /// Sets the disabled state of the [`Checkbox`] pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } + /// Binds a handler to the [`Checkbox`] that will be called when clicked pub fn on_click( mut self, handler: impl Fn(&ToggleState, &mut WindowContext) + 'static, @@ -52,12 +73,55 @@ impl Checkbox { self.on_click = Some(Box::new(handler)); self } + + /// Sets the `fill` setting of the checkbox, indicating whether it should be filled or not + pub fn fill(mut self) -> Self { + self.filled = true; + self + } + + /// Sets the style of the checkbox using the specified [`ToggleStyle`] + pub fn style(mut self, style: ToggleStyle) -> Self { + self.style = style; + self + } + + /// Match the style of the checkbox to the current elevation using [`ToggleStyle::ElevationBased`] + pub fn elevation(mut self, elevation: ElevationIndex) -> Self { + self.style = ToggleStyle::ElevationBased(elevation); + self + } +} + +impl Checkbox { + fn bg_color(&self, cx: &WindowContext) -> Hsla { + let style = self.style.clone(); + match (style, self.filled) { + (ToggleStyle::Ghost, false) => cx.theme().colors().ghost_element_background, + (ToggleStyle::Ghost, true) => cx.theme().colors().element_background, + (ToggleStyle::ElevationBased(_), false) => gpui::transparent_black(), + (ToggleStyle::ElevationBased(elevation), true) => elevation.darker_bg(cx), + (ToggleStyle::Custom(_), false) => gpui::transparent_black(), + (ToggleStyle::Custom(color), true) => color.opacity(0.2), + } + } + + fn border_color(&self, cx: &WindowContext) -> Hsla { + if self.disabled { + return cx.theme().colors().border_disabled; + } + + match self.style.clone() { + ToggleStyle::Ghost => cx.theme().colors().border_variant, + ToggleStyle::ElevationBased(elevation) => elevation.on_elevation_bg(cx), + ToggleStyle::Custom(color) => color.opacity(0.3), + } + } } 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 { @@ -78,23 +142,8 @@ impl RenderOnce for Checkbox { 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, - ), - }; + let bg_color = self.bg_color(cx); + let border_color = self.border_color(cx); h_flex() .id(self.id) @@ -137,9 +186,12 @@ pub struct CheckboxWithLabel { label: Label, checked: ToggleState, on_click: Arc, + filled: bool, + style: ToggleStyle, } impl CheckboxWithLabel { + /// Creates a checkbox with an attached label pub fn new( id: impl Into, label: Label, @@ -151,20 +203,45 @@ impl CheckboxWithLabel { label, checked, on_click: Arc::new(on_click), + filled: false, + style: ToggleStyle::default(), } } + + /// Sets the style of the checkbox using the specified [`ToggleStyle`] + pub fn style(mut self, style: ToggleStyle) -> Self { + self.style = style; + self + } + + /// Match the style of the checkbox to the current elevation using [`ToggleStyle::ElevationBased`] + pub fn elevation(mut self, elevation: ElevationIndex) -> Self { + self.style = ToggleStyle::ElevationBased(elevation); + self + } + + /// Sets the `fill` setting of the checkbox, indicating whether it should be filled or not + pub fn fill(mut self) -> Self { + self.filled = true; + self + } } 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( + Checkbox::new(self.id.clone(), self.checked) + .style(self.style) + .when(self.filled, Checkbox::fill) + .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))) @@ -188,6 +265,7 @@ pub struct Switch { } impl Switch { + /// Creates a new [`Switch`] pub fn new(id: impl Into, state: ToggleState) -> Self { Self { id: id.into(), @@ -197,11 +275,13 @@ impl Switch { } } + /// Sets the disabled state of the [`Switch`] pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } + /// Binds a handler to the [`Switch`] that will be called when clicked pub fn on_click( mut self, handler: impl Fn(&ToggleState, &mut WindowContext) + 'static, @@ -282,8 +362,8 @@ impl RenderOnce for Switch { } } -/// A [`Switch`] that has a [`Label`]. #[derive(IntoElement)] +/// A [`Switch`] that has a [`Label`]. pub struct SwitchWithLabel { id: ElementId, label: Label, @@ -292,6 +372,7 @@ pub struct SwitchWithLabel { } impl SwitchWithLabel { + /// Creates a switch with an attached label pub fn new( id: impl Into, label: Label, @@ -352,6 +433,120 @@ impl ComponentPreview for Checkbox { ), ], ), + example_group_with_title( + "Default (Filled)", + vec![ + single_example( + "Unselected", + Checkbox::new("checkbox_unselected", ToggleState::Unselected).fill(), + ), + single_example( + "Indeterminate", + Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate).fill(), + ), + single_example( + "Selected", + Checkbox::new("checkbox_selected", ToggleState::Selected).fill(), + ), + ], + ), + example_group_with_title( + "ElevationBased", + vec![ + single_example( + "Unselected", + Checkbox::new("checkbox_unfilled_unselected", ToggleState::Unselected) + .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)), + ), + single_example( + "Indeterminate", + Checkbox::new( + "checkbox_unfilled_indeterminate", + ToggleState::Indeterminate, + ) + .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)), + ), + single_example( + "Selected", + Checkbox::new("checkbox_unfilled_selected", ToggleState::Selected) + .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)), + ), + ], + ), + example_group_with_title( + "ElevationBased (Filled)", + vec![ + single_example( + "Unselected", + Checkbox::new("checkbox_filled_unselected", ToggleState::Unselected) + .fill() + .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)), + ), + single_example( + "Indeterminate", + Checkbox::new("checkbox_filled_indeterminate", ToggleState::Indeterminate) + .fill() + .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)), + ), + single_example( + "Selected", + Checkbox::new("checkbox_filled_selected", ToggleState::Selected) + .fill() + .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)), + ), + ], + ), + example_group_with_title( + "Custom Color", + vec![ + single_example( + "Unselected", + Checkbox::new("checkbox_custom_unselected", ToggleState::Unselected) + .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))), + ), + single_example( + "Indeterminate", + Checkbox::new("checkbox_custom_indeterminate", ToggleState::Indeterminate) + .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))), + ), + single_example( + "Selected", + Checkbox::new("checkbox_custom_selected", ToggleState::Selected) + .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))), + ), + ], + ), + example_group_with_title( + "Custom Color (Filled)", + vec![ + single_example( + "Unselected", + Checkbox::new("checkbox_custom_filled_unselected", ToggleState::Unselected) + .fill() + .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))), + ), + single_example( + "Indeterminate", + Checkbox::new( + "checkbox_custom_filled_indeterminate", + ToggleState::Indeterminate, + ) + .fill() + .style(ToggleStyle::Custom(hsla( + 142.0 / 360., + 0.68, + 0.45, + 0.7, + ))), + ), + single_example( + "Selected", + Checkbox::new("checkbox_custom_filled_selected", ToggleState::Selected) + .fill() + .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))), + ), + ], + ), example_group_with_title( "Disabled", vec![ @@ -375,6 +570,35 @@ impl ComponentPreview for Checkbox { ), ], ), + example_group_with_title( + "Disabled (Filled)", + vec![ + single_example( + "Unselected", + Checkbox::new( + "checkbox_disabled_filled_unselected", + ToggleState::Unselected, + ) + .fill() + .disabled(true), + ), + single_example( + "Indeterminate", + Checkbox::new( + "checkbox_disabled_filled_indeterminate", + ToggleState::Indeterminate, + ) + .fill() + .disabled(true), + ), + single_example( + "Selected", + Checkbox::new("checkbox_disabled_filled_selected", ToggleState::Selected) + .fill() + .disabled(true), + ), + ], + ), ] } } diff --git a/crates/ui/src/styles/elevation.rs b/crates/ui/src/styles/elevation.rs index 932fd3a944..913d0e812e 100644 --- a/crates/ui/src/styles/elevation.rs +++ b/crates/ui/src/styles/elevation.rs @@ -22,12 +22,8 @@ pub enum ElevationIndex { EditorSurface, /// A surface that is elevated above the primary surface. but below washes, models, and dragged elements. ElevatedSurface, - /// A surface that is above all non-modal surfaces, and separates the app from focused intents, like dialogs, alerts, modals, etc. - Wash, /// A surface above the [ElevationIndex::Wash] that is used for dialogs, alerts, modals, etc. ModalSurface, - /// A surface above all other surfaces, reserved exclusively for dragged elements, like a dragged file, tab or other draggable element. - DraggedElement, } impl Display for ElevationIndex { @@ -37,9 +33,7 @@ impl Display for ElevationIndex { ElevationIndex::Surface => write!(f, "Surface"), ElevationIndex::EditorSurface => write!(f, "Editor Surface"), ElevationIndex::ElevatedSurface => write!(f, "Elevated Surface"), - ElevationIndex::Wash => write!(f, "Wash"), ElevationIndex::ModalSurface => write!(f, "Modal Surface"), - ElevationIndex::DraggedElement => write!(f, "Dragged Element"), } } } @@ -90,9 +84,31 @@ impl ElevationIndex { ElevationIndex::Surface => cx.theme().colors().surface_background, ElevationIndex::EditorSurface => cx.theme().colors().editor_background, ElevationIndex::ElevatedSurface => cx.theme().colors().elevated_surface_background, - ElevationIndex::Wash => gpui::transparent_black(), ElevationIndex::ModalSurface => cx.theme().colors().elevated_surface_background, - ElevationIndex::DraggedElement => gpui::transparent_black(), + } + } + + /// Returns a color that is appropriate a filled element on this elevation + pub fn on_elevation_bg(&self, cx: &WindowContext) -> Hsla { + match self { + ElevationIndex::Background => cx.theme().colors().surface_background, + ElevationIndex::Surface => cx.theme().colors().background, + ElevationIndex::EditorSurface => cx.theme().colors().surface_background, + ElevationIndex::ElevatedSurface => cx.theme().colors().background, + ElevationIndex::ModalSurface => cx.theme().colors().background, + } + } + + /// Attempts to return a darker background color than the current elevation index's background. + /// + /// If the current background color is already dark, it will return a lighter color instead. + pub fn darker_bg(&self, cx: &WindowContext) -> Hsla { + match self { + ElevationIndex::Background => cx.theme().colors().surface_background, + ElevationIndex::Surface => cx.theme().colors().editor_background, + ElevationIndex::EditorSurface => cx.theme().colors().surface_background, + ElevationIndex::ElevatedSurface => cx.theme().colors().editor_background, + ElevationIndex::ModalSurface => cx.theme().colors().editor_background, } } } diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 6264f72a12..cf4886e1eb 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -11,7 +11,7 @@ use gpui::{ }; use settings::{Settings, SettingsStore}; use std::sync::Arc; -use ui::{prelude::*, CheckboxWithLabel, Tooltip}; +use ui::{prelude::*, CheckboxWithLabel, ElevationIndex, Tooltip}; use vim_mode_setting::VimModeSetting; use workspace::{ dock::DockPosition, @@ -269,72 +269,96 @@ impl Render for WelcomePage { .child( h_flex() .justify_between() - .child(CheckboxWithLabel::new( - "enable-vim", - Label::new("Enable Vim Mode"), - if VimModeSetting::get_global(cx).0 { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - cx.listener(move |this, selection, cx| { - this.telemetry - .report_app_event("welcome page: toggle vim".to_string()); - this.update_settings::( - selection, - cx, - |setting, value| *setting = Some(value), - ); - }), - )) + .child( + CheckboxWithLabel::new( + "enable-vim", + Label::new("Enable Vim Mode"), + if VimModeSetting::get_global(cx).0 { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + cx.listener(move |this, selection, cx| { + this.telemetry + .report_app_event("welcome page: toggle vim".to_string()); + this.update_settings::( + selection, + cx, + |setting, value| *setting = Some(value), + ); + }), + ) + .fill() + .elevation(ElevationIndex::ElevatedSurface), + ) .child( IconButton::new("vim-mode", IconName::Info) .icon_size(IconSize::XSmall) .icon_color(Color::Muted) - .tooltip(|cx| Tooltip::text("You can also toggle Vim Mode via the command palette or Editor Controls menu.", cx)), - ) + .tooltip(|cx| { + Tooltip::text( + "You can also toggle Vim Mode via the command palette or Editor Controls menu.", + cx, + ) + }), + ), ) - .child(CheckboxWithLabel::new( - "enable-crash", - Label::new("Send Crash Reports"), - if TelemetrySettings::get_global(cx).diagnostics { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - cx.listener(move |this, selection, cx| { - this.telemetry.report_app_event( - "welcome page: toggle diagnostic telemetry".to_string(), - ); - this.update_settings::(selection, cx, { - move |settings, value| { - settings.diagnostics = Some(value); - - telemetry::event!("Settings Changed", setting = "diagnostic telemetry", value); - } - }); - }), - )) - .child(CheckboxWithLabel::new( - "enable-telemetry", - Label::new("Send Telemetry"), - if TelemetrySettings::get_global(cx).metrics { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - cx.listener(move |this, selection, cx| { - this.telemetry.report_app_event( - "welcome page: toggle metric telemetry".to_string(), - ); - this.update_settings::(selection, cx, { - move |settings, value| { - settings.metrics = Some(value); - telemetry::event!("Settings Changed", setting = "metric telemetry", value); - } - }); - }), - )), + .child( + CheckboxWithLabel::new( + "enable-crash", + Label::new("Send Crash Reports"), + if TelemetrySettings::get_global(cx).diagnostics { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + cx.listener(move |this, selection, cx| { + this.telemetry.report_app_event( + "welcome page: toggle diagnostic telemetry".to_string(), + ); + this.update_settings::(selection, cx, { + move |settings, value| { + settings.diagnostics = Some(value); + telemetry::event!( + "Settings Changed", + setting = "diagnostic telemetry", + value + ); + } + }); + }), + ) + .fill() + .elevation(ElevationIndex::ElevatedSurface), + ) + .child( + CheckboxWithLabel::new( + "enable-telemetry", + Label::new("Send Telemetry"), + if TelemetrySettings::get_global(cx).metrics { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + cx.listener(move |this, selection, cx| { + this.telemetry.report_app_event( + "welcome page: toggle metric telemetry".to_string(), + ); + this.update_settings::(selection, cx, { + move |settings, value| { + settings.metrics = Some(value); + telemetry::event!( + "Settings Changed", + setting = "metric telemetry", + value + ); + } + }); + }), + ) + .fill() + .elevation(ElevationIndex::ElevatedSurface), + ), ), ) }