diff --git a/README.md b/README.md index 18a99569..56d75854 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ https://github.com/huacnlee/gpui-app/assets/5518/ad103f02-697a-40ed-a876-8b13e42 - [x] Icon - [x] Checkbox - [x] With label -- [ ] Switch +- [x] Switch + - [ ] Toggle Animation - [ ] Radio & RadioGroup - [ ] Dropdown - [ ] Combobox diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 9e7d20cd..ad7b13b3 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -15,6 +15,7 @@ pub mod text_field; pub mod theme; pub mod title_bar; pub use styled_ext::StyledExt; +pub mod switch; pub mod tab; pub use clickable::Clickable; diff --git a/crates/ui/src/story/mod.rs b/crates/ui/src/story/mod.rs index 132ee702..e92eb26c 100644 --- a/crates/ui/src/story/mod.rs +++ b/crates/ui/src/story/mod.rs @@ -10,10 +10,12 @@ use gpui::{ RenderOnce, SharedString, StatefulInteractiveElement as _, Styled as _, View, ViewContext, VisualContext, WindowContext, }; +use switch_story::SwitchStory; mod button_story; mod checkbox_story; mod input_story; +mod switch_story; use crate::{ button::Button, @@ -75,6 +77,7 @@ enum StoryType { Button, Input, Checkbox, + Switch, } impl Display for StoryType { @@ -83,6 +86,7 @@ impl Display for StoryType { Self::Button => write!(f, "Button"), Self::Input => write!(f, "Input"), Self::Checkbox => write!(f, "Checkbox"), + Self::Switch => write!(f, "Switch"), } } } @@ -92,6 +96,7 @@ pub struct Stories { button_story: View, input_story: View, + switch_story: View, } impl Stories { @@ -100,6 +105,7 @@ impl Stories { active: StoryType::Button, button_story: cx.new_view(|cx| ButtonStory {}), input_story: cx.new_view(|cx| InputStory::new(cx)), + switch_story: cx.new_view(|cx| SwitchStory::new(cx)), } } @@ -113,18 +119,17 @@ impl Stories { } fn render_story_buttons(&self, cx: &mut ViewContext) -> impl IntoElement { - let tabs = vec![ - self.swith_button("story-button", StoryType::Button, cx), - self.swith_button("story-input", StoryType::Input, cx), - self.swith_button("story-checkbox", StoryType::Checkbox, cx), - ]; - div() .flex() .items_center() .gap_4() .w_full() - .child(TabBar::new("story-tabs").children(tabs)) + .child(TabBar::new("story-tabs").children(vec![ + self.swith_button("story-button", StoryType::Button, cx), + self.swith_button("story-input", StoryType::Input, cx), + self.swith_button("story-checkbox", StoryType::Checkbox, cx), + self.swith_button("story-switch", StoryType::Switch, cx), + ])) } fn swith_button( @@ -156,6 +161,7 @@ impl Render for Stories { StoryType::Button => this.child(self.button_story.clone()), StoryType::Input => this.child(self.input_story.clone()), StoryType::Checkbox => this.child(CheckboxStory::new(cx).into_any_element()), + StoryType::Switch => this.child(self.switch_story.clone()), }) } } diff --git a/crates/ui/src/story/switch_story.rs b/crates/ui/src/story/switch_story.rs new file mode 100644 index 00000000..6a3c9bf4 --- /dev/null +++ b/crates/ui/src/story/switch_story.rs @@ -0,0 +1,81 @@ +use gpui::{ + div, ClickEvent, IntoElement, ParentElement, Render, RenderOnce, + StatefulInteractiveElement as _, Styled, ViewContext, VisualContext as _, WindowContext, +}; + +use crate::{ + checkbox::Checkbox, + disableable::Disableable as _, + selectable::Selection, + stock::{h_flex, v_flex}, + switch::Switch, +}; + +use super::story_case; + +#[derive(Default)] +pub struct SwitchStory { + switch1: bool, + switch2: bool, +} + +impl SwitchStory { + pub(crate) fn new(cx: &mut WindowContext) -> Self { + Self { + switch1: false, + switch2: true, + } + } +} + +impl Render for SwitchStory { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + story_case( + "Switch", + "A control that allows the user to toggle between checked and not checked.", + ) + .child( + v_flex() + .items_start() + .justify_center() + .gap_4() + .child( + h_flex() + .items_center() + .gap_4() + .child( + Switch::new("switch1") + .checked(self.switch1) + .on_click(cx.listener(move |view, _, cx| { + view.switch1 = !view.switch1; + cx.notify(); + })), + ) + .child( + Switch::new("switch2") + .checked(self.switch2) + .on_click(cx.listener(move |view, _, cx| { + view.switch2 = !view.switch2; + cx.notify(); + })), + ), + ) + .child( + h_flex() + .items_center() + .gap_4() + .child(Switch::new("switch3").disabled(true).on_click(|ev, cx| { + println!("Switch value changed: {:?}", ev); + })) + .child( + Switch::new("switch3_1") + .checked(true) + .disabled(true) + .on_click(|ev, cx| { + println!("Switch value changed: {:?}", ev); + }), + ), + ), + ) + } +} diff --git a/crates/ui/src/switch.rs b/crates/ui/src/switch.rs new file mode 100644 index 00000000..a505cca1 --- /dev/null +++ b/crates/ui/src/switch.rs @@ -0,0 +1,90 @@ +use crate::{ + theme::{ActiveTheme, Colorize}, + Disableable, +}; +use gpui::{ + div, prelude::FluentBuilder as _, ClickEvent, Div, InteractiveElement, IntoElement, + ParentElement as _, RenderOnce, SharedString, Stateful, StatefulInteractiveElement, + Styled as _, WindowContext, +}; + +type OnClick = Box; + +#[derive(IntoElement)] +pub struct Switch { + base: Stateful
, + checked: bool, + disabled: bool, + label: Option, + on_click: Option, +} + +impl Switch { + pub fn new(id: impl Into) -> Self { + Self { + base: div().id(id.into()), + checked: false, + disabled: false, + label: None, + on_click: None, + } + } + + pub fn checked(mut self, checked: bool) -> Self { + self.checked = checked; + self + } + + pub fn label(mut self, label: impl Into) -> Self { + self.label = Some(label.into()); + self + } + + pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self { + self.on_click = Some(Box::new(handler)); + self + } +} + +impl Disableable for Switch { + fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} + +impl RenderOnce for Switch { + fn render(self, cx: &mut gpui::WindowContext) -> impl IntoElement { + let theme = cx.theme(); + + let (bg, toggle_bg) = match self.checked { + true => (theme.primary, theme.background), + false => (theme.input, theme.background), + }; + + let (bg, toggle_bg) = match self.disabled { + true => (bg.opacity(0.3), toggle_bg.opacity(0.8)), + false => (bg, toggle_bg), + }; + + self.base + .w_11() + .h_6() + .flex() + .items_center() + .rounded_xl() + .border_2() + .border_color(theme.transparent) + .bg(bg) + .when(!self.disabled, |this| this.cursor_pointer()) + .map(|this| match self.checked { + true => this.flex_row_reverse(), + false => this, + }) + .child(div().h_4().w_4().rounded_full().bg(toggle_bg)) + .when_some( + self.on_click.filter(|_| !self.disabled), + |this, on_click| this.on_click(move |ev, cx| on_click(ev, cx)), + ) + } +}