@@ -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({
- 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<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,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::<VimModeSetting>(
- 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::<VimModeSetting>(
+ 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::<TelemetrySettings>(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::<TelemetrySettings>(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::<TelemetrySettings>(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::<TelemetrySettings>(selection, cx, {
+ move |settings, value| {
+ settings.metrics = Some(value);
+ telemetry::event!(
+ "Settings Changed",
+ setting = "metric telemetry",
+ value
+ );
+ }
+ });
+ }),
+ )
+ .fill()
+ .elevation(ElevationIndex::ElevatedSurface),
+ ),
),
)
}