From 1bce5dcc69b4b89b9be06142ac7fe843c4bcdf06 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Sun, 5 Nov 2023 01:06:41 -0500 Subject: [PATCH] Add checkboxes and their stories --- assets/icons/dash.svg | 1 + crates/storybook2/src/story_selector.rs | 2 + crates/theme2/src/colors.rs | 2 + crates/theme2/src/default_colors.rs | 4 + crates/ui2/docs/building-ui.md | 10 ++ crates/ui2/src/components.rs | 2 + crates/ui2/src/components/checkbox.rs | 217 ++++++++++++++++++++++++ crates/ui2/src/components/icon.rs | 4 + crates/ui2/src/prelude.rs | 6 +- 9 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 assets/icons/dash.svg create mode 100644 crates/ui2/src/components/checkbox.rs diff --git a/assets/icons/dash.svg b/assets/icons/dash.svg new file mode 100644 index 0000000000..efff9eab5e --- /dev/null +++ b/assets/icons/dash.svg @@ -0,0 +1 @@ + diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index a78705c7bb..2adf2956d3 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/crates/storybook2/src/story_selector.rs @@ -19,6 +19,7 @@ pub enum ComponentStory { Buffer, Button, ChatPanel, + Checkbox, CollabPanel, Colors, CommandPalette, @@ -61,6 +62,7 @@ impl ComponentStory { Self::Buffer => cx.build_view(|_| ui::BufferStory).into(), Self::Button => cx.build_view(|_| ButtonStory).into(), Self::ChatPanel => cx.build_view(|_| ui::ChatPanelStory).into(), + Self::Checkbox => cx.build_view(|_| ui::CheckboxStory).into(), Self::CollabPanel => cx.build_view(|_| ui::CollabPanelStory).into(), Self::Colors => cx.build_view(|_| ColorsStory).into(), Self::CommandPalette => cx.build_view(|_| ui::CommandPaletteStory).into(), diff --git a/crates/theme2/src/colors.rs b/crates/theme2/src/colors.rs index b02a9c14df..b9f8804205 100644 --- a/crates/theme2/src/colors.rs +++ b/crates/theme2/src/colors.rs @@ -54,7 +54,9 @@ pub struct ThemeColors { pub border: Hsla, pub border_variant: Hsla, pub border_focused: Hsla, + pub border_selected: Hsla, pub border_transparent: Hsla, + pub border_disabled: Hsla, pub elevated_surface: Hsla, pub surface: Hsla, pub background: Hsla, diff --git a/crates/theme2/src/default_colors.rs b/crates/theme2/src/default_colors.rs index 802392d296..4ecae43b15 100644 --- a/crates/theme2/src/default_colors.rs +++ b/crates/theme2/src/default_colors.rs @@ -205,6 +205,8 @@ impl ThemeColors { border: neutral().light().step_6(), border_variant: neutral().light().step_5(), border_focused: blue().light().step_5(), + border_disabled: neutral().light().step_3(), + border_selected: blue().light().step_5(), border_transparent: system.transparent, elevated_surface: neutral().light().step_2(), surface: neutral().light().step_2(), @@ -250,6 +252,8 @@ impl ThemeColors { border: neutral().dark().step_6(), border_variant: neutral().dark().step_5(), border_focused: blue().dark().step_5(), + border_disabled: neutral().dark().step_3(), + border_selected: blue().dark().step_5(), border_transparent: system.transparent, elevated_surface: neutral().dark().step_2(), surface: neutral().dark().step_2(), diff --git a/crates/ui2/docs/building-ui.md b/crates/ui2/docs/building-ui.md index a2a3ff697b..e0160e336e 100644 --- a/crates/ui2/docs/building-ui.md +++ b/crates/ui2/docs/building-ui.md @@ -2,6 +2,16 @@ ## Common patterns +### Method ordering + +- id +- Flex properties +- Position properties +- Size properties +- Style properties +- Handlers +- State properties + ### Using the Label Component to Create UI Text The `Label` component helps in displaying text on user interfaces. It creates an interface where specific parameters such as label color, line height style, and strikethrough can be set. diff --git a/crates/ui2/src/components.rs b/crates/ui2/src/components.rs index 692cd55e8e..a8a7ddfd46 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -1,5 +1,6 @@ mod avatar; mod button; +mod checkbox; mod context_menu; mod details; mod facepile; @@ -25,6 +26,7 @@ mod tool_divider; pub use avatar::*; pub use button::*; +pub use checkbox::*; pub use context_menu::*; pub use details::*; pub use facepile::*; diff --git a/crates/ui2/src/components/checkbox.rs b/crates/ui2/src/components/checkbox.rs new file mode 100644 index 0000000000..3add6cebac --- /dev/null +++ b/crates/ui2/src/components/checkbox.rs @@ -0,0 +1,217 @@ +///! # 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. +use gpui2::{ + div, Component, ParentElement, SharedString, StatelessInteractive, Styled, ViewContext, +}; +use theme2::ActiveTheme; + +use crate::{Icon, IconColor, IconElement, Selected}; + +#[derive(Component)] +pub struct Checkbox { + id: SharedString, + checked: Selected, + disabled: bool, +} + +impl Checkbox { + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + checked: Selected::Unselected, + disabled: false, + } + } + + pub fn toggle(mut self) -> Self { + self.checked = match self.checked { + Selected::Selected => Selected::Unselected, + Selected::Unselected => Selected::Selected, + Selected::Indeterminate => Selected::Selected, + }; + self + } + + pub fn set_indeterminate(mut self) -> Self { + self.checked = Selected::Indeterminate; + self + } + + pub fn set_disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } + + pub fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + let group_id = format!("checkbox_group_{}", self.id); + + // The icon is different depending on the state of the checkbox. + // + // We need the match to return all the same type, + // so we wrap the eatch result in a div. + // + // We are still exploring the best way to handle this. + let icon = match self.checked { + // When selected, we show a checkmark. + Selected::Selected => { + div().child( + IconElement::new(Icon::Check) + .size(crate::IconSize::Small) + .color( + // If the checkbox is disabled we change the color of the icon. + if self.disabled { + IconColor::Disabled + } else { + IconColor::Selected + }, + ), + ) + } + // In an indeterminate state, we show a dash. + Selected::Indeterminate => { + div().child( + IconElement::new(Icon::Dash) + .size(crate::IconSize::Small) + .color( + // If the checkbox is disabled we change the color of the icon. + if self.disabled { + IconColor::Disabled + } else { + IconColor::Selected + }, + ), + ) + } + // When unselected, we show nothing. + Selected::Unselected => div(), + }; + + // A checkbox could be in an indeterminate state, + // for example the indeterminate state could represent: + // - a group of options of which only some are selected + // - an enabled option that is no longer available + // - a previously agreed to license that has been updated + // + // For the sake of styles we treat the indeterminate state as selected, + // but it's icon will be different. + let selected = + self.checked == Selected::Selected || self.checked == Selected::Indeterminate; + + // We could use something like this to make the checkbox background when selected: + // + // ~~~rust + // ... + // .when(selected, |this| { + // this.bg(cx.theme().colors().element_selected) + // }) + // ~~~ + // + // But we use a match instead here because the checkbox might be disabled, + // and it could be disabled _while_ it is selected, as well as while it is not selected. + 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, cx.theme().colors().border), + }; + + div() + // Rather than adding `px_1()` to add some space around the checkbox, + // we use a larger parent element to create a slightly larger + // click area for the checkbox. + .size_5() + // Because we've enlarged the click area, we need to create a + // `group` to pass down interaction events to the checkbox. + .group(group_id.clone()) + .child( + div() + .flex() + // This prevent the flex element from growing + // or shrinking in response to any size changes + .flex_none() + // The combo of `justify_center()` and `items_center()` + // is used frequently to center elements in a flex container. + // + // We use this to center the icon in the checkbox. + .justify_center() + .items_center() + .m_1() + .size_4() + .rounded_sm() + .bg(bg_color) + .border() + .border_color(border_color) + // We only want the interaction states to fire when we + // are in a checkbox that isn't disabled. + .when(!self.disabled, |this| { + // Here instead of `hover()` we use `group_hover()` + // to pass it the group id. + this.group_hover(group_id.clone(), |el| { + el.bg(cx.theme().colors().element_hover) + }) + }) + .child(icon), + ) + } +} + +#[cfg(feature = "stories")] +pub use stories::*; + +#[cfg(feature = "stories")] +mod stories { + use super::*; + use crate::{h_stack, Story}; + use gpui2::{Div, Render}; + + pub struct CheckboxStory; + + impl Render for CheckboxStory { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + Story::container(cx) + .child(Story::title_for::<_, Checkbox>(cx)) + .child(Story::label(cx, "Default")) + .child( + h_stack() + .p_2() + .gap_2() + .rounded_md() + .border() + .border_color(cx.theme().colors().border) + .child(Checkbox::new("checkbox-enabled")) + .child(Checkbox::new("checkbox-intermediate").set_indeterminate()) + .child(Checkbox::new("checkbox-selected").toggle()), + ) + .child(Story::label(cx, "Disabled")) + .child( + h_stack() + .p_2() + .gap_2() + .rounded_md() + .border() + .border_color(cx.theme().colors().border) + .child(Checkbox::new("checkbox-disabled").set_disabled(true)) + .child( + Checkbox::new("checkbox-disabled-intermediate") + .set_disabled(true) + .set_indeterminate(), + ) + .child( + Checkbox::new("checkbox-disabled-selected") + .set_disabled(true) + .toggle(), + ), + ) + } + } +} diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index 5885d76101..8075352b30 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -22,6 +22,7 @@ pub enum IconColor { Warning, Success, Info, + Selected, } impl IconColor { @@ -36,6 +37,7 @@ impl IconColor { IconColor::Warning => cx.theme().status().warning, IconColor::Success => cx.theme().status().success, IconColor::Info => cx.theme().status().info, + IconColor::Selected => cx.theme().colors().icon_accent, } } } @@ -55,6 +57,7 @@ pub enum Icon { ChevronRight, ChevronUp, Close, + Dash, Exit, ExclamationTriangle, File, @@ -112,6 +115,7 @@ impl Icon { Icon::ChevronRight => "icons/chevron_right.svg", Icon::ChevronUp => "icons/chevron_up.svg", Icon::Close => "icons/x.svg", + Icon::Dash => "icons/dash.svg", Icon::Exit => "icons/exit.svg", Icon::ExclamationTriangle => "icons/warning.svg", Icon::File => "icons/file.svg", diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index 8ba74cce95..072ed00060 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -154,10 +154,10 @@ impl InteractionState { } } -#[derive(Default, PartialEq)] -pub enum SelectedState { +#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] +pub enum Selected { #[default] Unselected, - PartiallySelected, + Indeterminate, Selected, }