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:
parent
254b369624
commit
d224f511fa
5 changed files with 108 additions and 58 deletions
|
@ -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> {
|
||||||
|
|
|
@ -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>>,
|
||||||
|
|
|
@ -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),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue