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 })
}
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> {

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> {
click: Option<ClickHandler<V>>,

View file

@ -1,63 +1,58 @@
///! # 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 std::sync::Arc;
use gpui2::{
div, Component, ParentElement, SharedString, StatelessInteractive, Styled, ViewContext,
div, Component, ElementId, ParentElement, StatefulInteractive, StatelessInteractive, Styled,
ViewContext,
};
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)]
pub struct Checkbox {
id: SharedString,
checked: Selected,
pub struct Checkbox<V: 'static> {
id: ElementId,
checked: Selection,
disabled: bool,
on_click: Option<CheckHandler<V>>,
}
impl Checkbox {
pub fn new(id: impl Into<SharedString>) -> Self {
impl<V: 'static> Checkbox<V> {
pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
Self {
id: id.into(),
checked: Selected::Unselected,
checked,
disabled: false,
on_click: None,
}
}
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 {
pub fn 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);
pub fn on_click(
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 {
// When selected, we show a checkmark.
Selected::Selected => {
div().child(
Selection::Selected => {
Some(
IconElement::new(Icon::Check)
.size(crate::IconSize::Small)
.color(
@ -71,8 +66,8 @@ impl Checkbox {
)
}
// In an indeterminate state, we show a dash.
Selected::Indeterminate => {
div().child(
Selection::Indeterminate => {
Some(
IconElement::new(Icon::Dash)
.size(crate::IconSize::Small)
.color(
@ -86,7 +81,7 @@ impl Checkbox {
)
}
// When unselected, we show nothing.
Selected::Unselected => div(),
Selection::Unselected => None,
};
// 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,
// but it's icon will be different.
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:
//
@ -127,6 +122,7 @@ impl Checkbox {
};
div()
.id(self.id)
// 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.
@ -161,7 +157,13 @@ impl Checkbox {
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 {
Story::container(cx)
.child(Story::title_for::<_, Checkbox>(cx))
.child(Story::title_for::<_, Checkbox<Self>>(cx))
.child(Story::label(cx, "Default"))
.child(
h_stack()
@ -191,9 +193,12 @@ mod stories {
.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(Checkbox::new("checkbox-enabled", Selection::Unselected))
.child(Checkbox::new(
"checkbox-intermediate",
Selection::Indeterminate,
))
.child(Checkbox::new("checkbox-selected", Selection::Selected)),
)
.child(Story::label(cx, "Disabled"))
.child(
@ -203,16 +208,20 @@ mod stories {
.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(),
Checkbox::new("checkbox-disabled", Selection::Unselected)
.disabled(true),
)
.child(
Checkbox::new("checkbox-disabled-selected")
.set_disabled(true)
.toggle(),
Checkbox::new(
"checkbox-disabled-intermediate",
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)]
pub enum Selected {
pub enum Selection {
#[default]
Unselected,
Indeterminate,
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::{
static_livestream, v_stack, AssistantPanel, Button, ChatMessage, ChatPanel, CollabPanel,
EditorPane, Label, LanguageSelector, NotificationsPanel, Pane, PaneGroup, Panel,
static_livestream, v_stack, AssistantPanel, Button, ChatMessage, ChatPanel, Checkbox,
CollabPanel, EditorPane, Label, LanguageSelector, NotificationsPanel, Pane, PaneGroup, Panel,
PanelAllowedSides, PanelSide, ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar,
Toast, ToastOrigin,
};
@ -42,6 +42,7 @@ pub struct Workspace {
show_terminal: bool,
show_debug: bool,
show_language_selector: bool,
test_checkbox_selection: Selection,
debug: Gpui2UiDebug,
}
@ -58,6 +59,7 @@ impl Workspace {
show_language_selector: false,
show_debug: false,
show_notifications_panel: true,
test_checkbox_selection: Selection::Unselected,
debug: Gpui2UiDebug::default(),
}
}
@ -217,6 +219,23 @@ impl Render for Workspace {
.text_color(cx.theme().colors().text)
.bg(cx.theme().colors().background)
.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(
div()
.flex_1()