ui: Update Checkbox design (#22794)
This PR shifts the design of checkboxes and introduces ways to style checkboxes based on Elevation, or tint them with a custom color. This may have some impacts on existing uses of checkboxes. When creating a checkbox you now need to call `.fill` if you want the checkbox to have a filled style. Before:   After:   Release Notes: - N/A
This commit is contained in:
parent
7a66c764b4
commit
c4b470685d
3 changed files with 363 additions and 99 deletions
|
@ -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<ElementId>, toggle_state: ToggleState) -> Checkbox {
|
||||
Checkbox::new(id, toggle_state)
|
||||
|
@ -17,6 +18,19 @@ pub fn switch(id: impl Into<ElementId>, 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<Box<dyn Fn(&ToggleState, &mut WindowContext) + 'static>>,
|
||||
filled: bool,
|
||||
style: ToggleStyle,
|
||||
}
|
||||
|
||||
impl Checkbox {
|
||||
/// Creates a new [`Checkbox`]
|
||||
pub fn new(id: impl Into<ElementId>, 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<dyn Fn(&ToggleState, &mut WindowContext) + 'static>,
|
||||
filled: bool,
|
||||
style: ToggleStyle,
|
||||
}
|
||||
|
||||
impl CheckboxWithLabel {
|
||||
/// Creates a checkbox with an attached label
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
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({
|
||||
.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<ElementId>, 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<ElementId>,
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,7 +269,8 @@ impl Render for WelcomePage {
|
|||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(CheckboxWithLabel::new(
|
||||
.child(
|
||||
CheckboxWithLabel::new(
|
||||
"enable-vim",
|
||||
Label::new("Enable Vim Mode"),
|
||||
if VimModeSetting::get_global(cx).0 {
|
||||
|
@ -286,15 +287,24 @@ impl Render for WelcomePage {
|
|||
|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(
|
||||
.child(
|
||||
CheckboxWithLabel::new(
|
||||
"enable-crash",
|
||||
Label::new("Send Crash Reports"),
|
||||
if TelemetrySettings::get_global(cx).diagnostics {
|
||||
|
@ -309,13 +319,20 @@ impl Render for WelcomePage {
|
|||
this.update_settings::<TelemetrySettings>(selection, cx, {
|
||||
move |settings, value| {
|
||||
settings.diagnostics = Some(value);
|
||||
|
||||
telemetry::event!("Settings Changed", setting = "diagnostic telemetry", value);
|
||||
telemetry::event!(
|
||||
"Settings Changed",
|
||||
setting = "diagnostic telemetry",
|
||||
value
|
||||
);
|
||||
}
|
||||
});
|
||||
}),
|
||||
))
|
||||
.child(CheckboxWithLabel::new(
|
||||
)
|
||||
.fill()
|
||||
.elevation(ElevationIndex::ElevatedSurface),
|
||||
)
|
||||
.child(
|
||||
CheckboxWithLabel::new(
|
||||
"enable-telemetry",
|
||||
Label::new("Send Telemetry"),
|
||||
if TelemetrySettings::get_global(cx).metrics {
|
||||
|
@ -330,11 +347,18 @@ impl Render for WelcomePage {
|
|||
this.update_settings::<TelemetrySettings>(selection, cx, {
|
||||
move |settings, value| {
|
||||
settings.metrics = Some(value);
|
||||
telemetry::event!("Settings Changed", setting = "metric telemetry", value);
|
||||
telemetry::event!(
|
||||
"Settings Changed",
|
||||
setting = "metric telemetry",
|
||||
value
|
||||
);
|
||||
}
|
||||
});
|
||||
}),
|
||||
)),
|
||||
)
|
||||
.fill()
|
||||
.elevation(ElevationIndex::ElevatedSurface),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue