Add interactivity to Checkbox component (#3240)

This PR adds interactivity to the `Checkbox` component.

They can now be checked and unchecked by clicking them.

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2023-11-06 19:22:25 +01:00 committed by GitHub
parent 254b369624
commit d224f511fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 108 additions and 58 deletions

View file

@ -212,6 +212,19 @@ pub trait Component<V> {
{ {
self.map(|this| if condition { then(this) } else { this }) self.map(|this| if condition { then(this) } else { this })
} }
fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
where
Self: Sized,
{
self.map(|this| {
if let Some(value) = option {
then(this, value)
} else {
this
}
})
}
} }
impl<V> Component<V> for AnyElement<V> { impl<V> Component<V> for AnyElement<V> {

View file

@ -61,7 +61,7 @@ impl ButtonVariant {
} }
} }
pub type ClickHandler<S> = Arc<dyn Fn(&mut S, &mut ViewContext<S>) + Send + Sync>; pub type ClickHandler<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>) + Send + Sync>;
struct ButtonHandlers<V: 'static> { struct ButtonHandlers<V: 'static> {
click: Option<ClickHandler<V>>, click: Option<ClickHandler<V>>,

View file

@ -1,63 +1,58 @@
///! # Checkbox use std::sync::Arc;
///!
///! 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::{ use gpui2::{
div, Component, ParentElement, SharedString, StatelessInteractive, Styled, ViewContext, div, Component, ElementId, ParentElement, StatefulInteractive, StatelessInteractive, Styled,
ViewContext,
}; };
use theme2::ActiveTheme; use theme2::ActiveTheme;
use crate::{Icon, IconColor, IconElement, Selected}; use crate::{Icon, IconColor, IconElement, Selection};
pub type CheckHandler<V> = Arc<dyn Fn(Selection, &mut V, &mut ViewContext<V>) + Send + Sync>;
/// # 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.
#[derive(Component)] #[derive(Component)]
pub struct Checkbox { pub struct Checkbox<V: 'static> {
id: SharedString, id: ElementId,
checked: Selected, checked: Selection,
disabled: bool, disabled: bool,
on_click: Option<CheckHandler<V>>,
} }
impl Checkbox { impl<V: 'static> Checkbox<V> {
pub fn new(id: impl Into<SharedString>) -> Self { pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
Self { Self {
id: id.into(), id: id.into(),
checked: Selected::Unselected, checked,
disabled: false, disabled: false,
on_click: None,
} }
} }
pub fn toggle(mut self) -> Self { pub fn disabled(mut self, disabled: bool) -> 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.disabled = disabled;
self self
} }
pub fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> { pub fn on_click(
let group_id = format!("checkbox_group_{}", self.id); mut self,
handler: impl 'static + Fn(Selection, &mut V, &mut ViewContext<V>) + Send + Sync,
) -> Self {
self.on_click = Some(Arc::new(handler));
self
}
pub fn render(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 { let icon = match self.checked {
// When selected, we show a checkmark. // When selected, we show a checkmark.
Selected::Selected => { Selection::Selected => {
div().child( Some(
IconElement::new(Icon::Check) IconElement::new(Icon::Check)
.size(crate::IconSize::Small) .size(crate::IconSize::Small)
.color( .color(
@ -71,8 +66,8 @@ impl Checkbox {
) )
} }
// In an indeterminate state, we show a dash. // In an indeterminate state, we show a dash.
Selected::Indeterminate => { Selection::Indeterminate => {
div().child( Some(
IconElement::new(Icon::Dash) IconElement::new(Icon::Dash)
.size(crate::IconSize::Small) .size(crate::IconSize::Small)
.color( .color(
@ -86,7 +81,7 @@ impl Checkbox {
) )
} }
// When unselected, we show nothing. // When unselected, we show nothing.
Selected::Unselected => div(), Selection::Unselected => None,
}; };
// A checkbox could be in an indeterminate state, // A checkbox could be in an indeterminate state,
@ -98,7 +93,7 @@ impl Checkbox {
// For the sake of styles we treat the indeterminate state as selected, // For the sake of styles we treat the indeterminate state as selected,
// but it's icon will be different. // but it's icon will be different.
let selected = let selected =
self.checked == Selected::Selected || self.checked == Selected::Indeterminate; self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
// We could use something like this to make the checkbox background when selected: // We could use something like this to make the checkbox background when selected:
// //
@ -127,6 +122,7 @@ impl Checkbox {
}; };
div() div()
.id(self.id)
// Rather than adding `px_1()` to add some space around the checkbox, // Rather than adding `px_1()` to add some space around the checkbox,
// we use a larger parent element to create a slightly larger // we use a larger parent element to create a slightly larger
// click area for the checkbox. // click area for the checkbox.
@ -161,7 +157,13 @@ impl Checkbox {
el.bg(cx.theme().colors().element_hover) el.bg(cx.theme().colors().element_hover)
}) })
}) })
.child(icon), .children(icon),
)
.when_some(
self.on_click.filter(|_| !self.disabled),
|this, on_click| {
this.on_click(move |view, _, cx| on_click(self.checked.inverse(), view, cx))
},
) )
} }
} }
@ -182,7 +184,7 @@ mod stories {
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
Story::container(cx) Story::container(cx)
.child(Story::title_for::<_, Checkbox>(cx)) .child(Story::title_for::<_, Checkbox<Self>>(cx))
.child(Story::label(cx, "Default")) .child(Story::label(cx, "Default"))
.child( .child(
h_stack() h_stack()
@ -191,9 +193,12 @@ mod stories {
.rounded_md() .rounded_md()
.border() .border()
.border_color(cx.theme().colors().border) .border_color(cx.theme().colors().border)
.child(Checkbox::new("checkbox-enabled")) .child(Checkbox::new("checkbox-enabled", Selection::Unselected))
.child(Checkbox::new("checkbox-intermediate").set_indeterminate()) .child(Checkbox::new(
.child(Checkbox::new("checkbox-selected").toggle()), "checkbox-intermediate",
Selection::Indeterminate,
))
.child(Checkbox::new("checkbox-selected", Selection::Selected)),
) )
.child(Story::label(cx, "Disabled")) .child(Story::label(cx, "Disabled"))
.child( .child(
@ -203,16 +208,20 @@ mod stories {
.rounded_md() .rounded_md()
.border() .border()
.border_color(cx.theme().colors().border) .border_color(cx.theme().colors().border)
.child(Checkbox::new("checkbox-disabled").set_disabled(true))
.child( .child(
Checkbox::new("checkbox-disabled-intermediate") Checkbox::new("checkbox-disabled", Selection::Unselected)
.set_disabled(true) .disabled(true),
.set_indeterminate(),
) )
.child( .child(
Checkbox::new("checkbox-disabled-selected") Checkbox::new(
.set_disabled(true) "checkbox-disabled-intermediate",
.toggle(), Selection::Indeterminate,
)
.disabled(true),
)
.child(
Checkbox::new("checkbox-disabled-selected", Selection::Selected)
.disabled(true),
), ),
) )
} }

View file

@ -155,9 +155,18 @@ impl InteractionState {
} }
#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] #[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
pub enum Selected { pub enum Selection {
#[default] #[default]
Unselected, Unselected,
Indeterminate, Indeterminate,
Selected, Selected,
} }
impl Selection {
pub fn inverse(&self) -> Self {
match self {
Self::Unselected | Self::Indeterminate => Self::Selected,
Self::Selected => Self::Unselected,
}
}
}

View file

@ -7,8 +7,8 @@ use theme2::ThemeSettings;
use crate::prelude::*; use crate::prelude::*;
use crate::{ use crate::{
static_livestream, v_stack, AssistantPanel, Button, ChatMessage, ChatPanel, CollabPanel, static_livestream, v_stack, AssistantPanel, Button, ChatMessage, ChatPanel, Checkbox,
EditorPane, Label, LanguageSelector, NotificationsPanel, Pane, PaneGroup, Panel, CollabPanel, EditorPane, Label, LanguageSelector, NotificationsPanel, Pane, PaneGroup, Panel,
PanelAllowedSides, PanelSide, ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar, PanelAllowedSides, PanelSide, ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar,
Toast, ToastOrigin, Toast, ToastOrigin,
}; };
@ -42,6 +42,7 @@ pub struct Workspace {
show_terminal: bool, show_terminal: bool,
show_debug: bool, show_debug: bool,
show_language_selector: bool, show_language_selector: bool,
test_checkbox_selection: Selection,
debug: Gpui2UiDebug, debug: Gpui2UiDebug,
} }
@ -58,6 +59,7 @@ impl Workspace {
show_language_selector: false, show_language_selector: false,
show_debug: false, show_debug: false,
show_notifications_panel: true, show_notifications_panel: true,
test_checkbox_selection: Selection::Unselected,
debug: Gpui2UiDebug::default(), debug: Gpui2UiDebug::default(),
} }
} }
@ -217,6 +219,23 @@ impl Render for Workspace {
.text_color(cx.theme().colors().text) .text_color(cx.theme().colors().text)
.bg(cx.theme().colors().background) .bg(cx.theme().colors().background)
.child(self.title_bar.clone()) .child(self.title_bar.clone())
.child(
div()
.absolute()
.top_12()
.left_12()
.z_index(99)
.bg(cx.theme().colors().background)
.child(
Checkbox::new("test_checkbox", self.test_checkbox_selection).on_click(
|selection, workspace: &mut Workspace, cx| {
workspace.test_checkbox_selection = selection;
cx.notify();
},
),
),
)
.child( .child(
div() div()
.flex_1() .flex_1()