Add checkboxes and their stories
This commit is contained in:
parent
b243de407e
commit
1bce5dcc69
9 changed files with 245 additions and 3 deletions
1
assets/icons/dash.svg
Normal file
1
assets/icons/dash.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-minus"><path d="M5 12h14"/></svg>
|
After Width: | Height: | Size: 229 B |
|
@ -19,6 +19,7 @@ pub enum ComponentStory {
|
||||||
Buffer,
|
Buffer,
|
||||||
Button,
|
Button,
|
||||||
ChatPanel,
|
ChatPanel,
|
||||||
|
Checkbox,
|
||||||
CollabPanel,
|
CollabPanel,
|
||||||
Colors,
|
Colors,
|
||||||
CommandPalette,
|
CommandPalette,
|
||||||
|
@ -61,6 +62,7 @@ impl ComponentStory {
|
||||||
Self::Buffer => cx.build_view(|_| ui::BufferStory).into(),
|
Self::Buffer => cx.build_view(|_| ui::BufferStory).into(),
|
||||||
Self::Button => cx.build_view(|_| ButtonStory).into(),
|
Self::Button => cx.build_view(|_| ButtonStory).into(),
|
||||||
Self::ChatPanel => cx.build_view(|_| ui::ChatPanelStory).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::CollabPanel => cx.build_view(|_| ui::CollabPanelStory).into(),
|
||||||
Self::Colors => cx.build_view(|_| ColorsStory).into(),
|
Self::Colors => cx.build_view(|_| ColorsStory).into(),
|
||||||
Self::CommandPalette => cx.build_view(|_| ui::CommandPaletteStory).into(),
|
Self::CommandPalette => cx.build_view(|_| ui::CommandPaletteStory).into(),
|
||||||
|
|
|
@ -54,7 +54,9 @@ pub struct ThemeColors {
|
||||||
pub border: Hsla,
|
pub border: Hsla,
|
||||||
pub border_variant: Hsla,
|
pub border_variant: Hsla,
|
||||||
pub border_focused: Hsla,
|
pub border_focused: Hsla,
|
||||||
|
pub border_selected: Hsla,
|
||||||
pub border_transparent: Hsla,
|
pub border_transparent: Hsla,
|
||||||
|
pub border_disabled: Hsla,
|
||||||
pub elevated_surface: Hsla,
|
pub elevated_surface: Hsla,
|
||||||
pub surface: Hsla,
|
pub surface: Hsla,
|
||||||
pub background: Hsla,
|
pub background: Hsla,
|
||||||
|
|
|
@ -205,6 +205,8 @@ impl ThemeColors {
|
||||||
border: neutral().light().step_6(),
|
border: neutral().light().step_6(),
|
||||||
border_variant: neutral().light().step_5(),
|
border_variant: neutral().light().step_5(),
|
||||||
border_focused: blue().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,
|
border_transparent: system.transparent,
|
||||||
elevated_surface: neutral().light().step_2(),
|
elevated_surface: neutral().light().step_2(),
|
||||||
surface: neutral().light().step_2(),
|
surface: neutral().light().step_2(),
|
||||||
|
@ -250,6 +252,8 @@ impl ThemeColors {
|
||||||
border: neutral().dark().step_6(),
|
border: neutral().dark().step_6(),
|
||||||
border_variant: neutral().dark().step_5(),
|
border_variant: neutral().dark().step_5(),
|
||||||
border_focused: blue().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,
|
border_transparent: system.transparent,
|
||||||
elevated_surface: neutral().dark().step_2(),
|
elevated_surface: neutral().dark().step_2(),
|
||||||
surface: neutral().dark().step_2(),
|
surface: neutral().dark().step_2(),
|
||||||
|
|
|
@ -2,6 +2,16 @@
|
||||||
|
|
||||||
## Common patterns
|
## Common patterns
|
||||||
|
|
||||||
|
### Method ordering
|
||||||
|
|
||||||
|
- id
|
||||||
|
- Flex properties
|
||||||
|
- Position properties
|
||||||
|
- Size properties
|
||||||
|
- Style properties
|
||||||
|
- Handlers
|
||||||
|
- State properties
|
||||||
|
|
||||||
### Using the Label Component to Create UI Text
|
### 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.
|
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.
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
mod avatar;
|
mod avatar;
|
||||||
mod button;
|
mod button;
|
||||||
|
mod checkbox;
|
||||||
mod context_menu;
|
mod context_menu;
|
||||||
mod details;
|
mod details;
|
||||||
mod facepile;
|
mod facepile;
|
||||||
|
@ -25,6 +26,7 @@ mod tool_divider;
|
||||||
|
|
||||||
pub use avatar::*;
|
pub use avatar::*;
|
||||||
pub use button::*;
|
pub use button::*;
|
||||||
|
pub use checkbox::*;
|
||||||
pub use context_menu::*;
|
pub use context_menu::*;
|
||||||
pub use details::*;
|
pub use details::*;
|
||||||
pub use facepile::*;
|
pub use facepile::*;
|
||||||
|
|
217
crates/ui2/src/components/checkbox.rs
Normal file
217
crates/ui2/src/components/checkbox.rs
Normal file
|
@ -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<SharedString>) -> 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<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||||
|
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<Self>;
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> 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(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ pub enum IconColor {
|
||||||
Warning,
|
Warning,
|
||||||
Success,
|
Success,
|
||||||
Info,
|
Info,
|
||||||
|
Selected,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IconColor {
|
impl IconColor {
|
||||||
|
@ -36,6 +37,7 @@ impl IconColor {
|
||||||
IconColor::Warning => cx.theme().status().warning,
|
IconColor::Warning => cx.theme().status().warning,
|
||||||
IconColor::Success => cx.theme().status().success,
|
IconColor::Success => cx.theme().status().success,
|
||||||
IconColor::Info => cx.theme().status().info,
|
IconColor::Info => cx.theme().status().info,
|
||||||
|
IconColor::Selected => cx.theme().colors().icon_accent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,6 +57,7 @@ pub enum Icon {
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Close,
|
Close,
|
||||||
|
Dash,
|
||||||
Exit,
|
Exit,
|
||||||
ExclamationTriangle,
|
ExclamationTriangle,
|
||||||
File,
|
File,
|
||||||
|
@ -112,6 +115,7 @@ impl Icon {
|
||||||
Icon::ChevronRight => "icons/chevron_right.svg",
|
Icon::ChevronRight => "icons/chevron_right.svg",
|
||||||
Icon::ChevronUp => "icons/chevron_up.svg",
|
Icon::ChevronUp => "icons/chevron_up.svg",
|
||||||
Icon::Close => "icons/x.svg",
|
Icon::Close => "icons/x.svg",
|
||||||
|
Icon::Dash => "icons/dash.svg",
|
||||||
Icon::Exit => "icons/exit.svg",
|
Icon::Exit => "icons/exit.svg",
|
||||||
Icon::ExclamationTriangle => "icons/warning.svg",
|
Icon::ExclamationTriangle => "icons/warning.svg",
|
||||||
Icon::File => "icons/file.svg",
|
Icon::File => "icons/file.svg",
|
||||||
|
|
|
@ -154,10 +154,10 @@ impl InteractionState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, PartialEq)]
|
#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
|
||||||
pub enum SelectedState {
|
pub enum Selected {
|
||||||
#[default]
|
#[default]
|
||||||
Unselected,
|
Unselected,
|
||||||
PartiallySelected,
|
Indeterminate,
|
||||||
Selected,
|
Selected,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue