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,
}