From d224f511fabbe9db12023bdf97a3cf3d4a148d3b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 6 Nov 2023 19:22:25 +0100 Subject: [PATCH] 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 --- crates/gpui2/src/element.rs | 13 +++ crates/ui2/src/components/button.rs | 2 +- crates/ui2/src/components/checkbox.rs | 117 +++++++++++++------------ crates/ui2/src/prelude.rs | 11 ++- crates/ui2/src/to_extract/workspace.rs | 23 ++++- 5 files changed, 108 insertions(+), 58 deletions(-) diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index a92dbd6ff9..2a0f557272 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -212,6 +212,19 @@ pub trait Component { { self.map(|this| if condition { then(this) } else { this }) } + + fn when_some(self, option: Option, then: impl FnOnce(Self, T) -> Self) -> Self + where + Self: Sized, + { + self.map(|this| { + if let Some(value) = option { + then(this, value) + } else { + this + } + }) + } } impl Component for AnyElement { diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs index c13460aadd..178813dd5f 100644 --- a/crates/ui2/src/components/button.rs +++ b/crates/ui2/src/components/button.rs @@ -61,7 +61,7 @@ impl ButtonVariant { } } -pub type ClickHandler = Arc) + Send + Sync>; +pub type ClickHandler = Arc) + Send + Sync>; struct ButtonHandlers { click: Option>, diff --git a/crates/ui2/src/components/checkbox.rs b/crates/ui2/src/components/checkbox.rs index 4b6a6240bb..9632a66810 100644 --- a/crates/ui2/src/components/checkbox.rs +++ b/crates/ui2/src/components/checkbox.rs @@ -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 = Arc) + 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 { + id: ElementId, + checked: Selection, disabled: bool, + on_click: Option>, } -impl Checkbox { - pub fn new(id: impl Into) -> Self { +impl Checkbox { + pub fn new(id: impl Into, 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(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - let group_id = format!("checkbox_group_{}", self.id); + pub fn on_click( + mut self, + handler: impl 'static + Fn(Selection, &mut V, &mut ViewContext) + Send + Sync, + ) -> Self { + self.on_click = Some(Arc::new(handler)); + 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( + 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::Element { Story::container(cx) - .child(Story::title_for::<_, Checkbox>(cx)) + .child(Story::title_for::<_, Checkbox>(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), ), ) } diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index 072ed00060..134b293d3d 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -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, + } + } +} diff --git a/crates/ui2/src/to_extract/workspace.rs b/crates/ui2/src/to_extract/workspace.rs index 97570a33e3..77b9bc4539 100644 --- a/crates/ui2/src/to_extract/workspace.rs +++ b/crates/ui2/src/to_extract/workspace.rs @@ -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()