diff --git a/README.md b/README.md index 56d75854..c48eb0dc 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ https://github.com/huacnlee/gpui-app/assets/5518/ad103f02-697a-40ed-a876-8b13e42 - [x] Checkbox - [x] With label - [x] Switch + - [x] With Label (Left, Right side) - [ ] Toggle Animation - [ ] Radio & RadioGroup - [ ] Dropdown diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index 5d4bc89c..52ba733f 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -175,7 +175,7 @@ impl RenderOnce for Button { normal_style.fg }; - Label::new(self.label, cx) + Label::new(self.label) .text_color(text_color) .map(|this| match self.size { ButtonSize::Small => this.text_sm(), diff --git a/crates/ui/src/checkbox.rs b/crates/ui/src/checkbox.rs index f11f5be4..57da762c 100644 --- a/crates/ui/src/checkbox.rs +++ b/crates/ui/src/checkbox.rs @@ -122,7 +122,7 @@ impl RenderOnce for Checkbox { ) .map(|this| { if let Some(label) = self.label { - this.child(Label::new(label, cx).text_color(color)) + this.child(Label::new(label).text_color(color)) } else { this } diff --git a/crates/ui/src/label.rs b/crates/ui/src/label.rs index 9035422e..834b8ffb 100644 --- a/crates/ui/src/label.rs +++ b/crates/ui/src/label.rs @@ -9,22 +9,15 @@ use crate::theme::ActiveTheme; pub struct Label { base: Div, label: SharedString, - color: Hsla, multiple_lines: bool, - line_height: Option, - text_size: Option, } impl Label { - pub fn new(label: impl Into, cx: &mut WindowContext) -> Self { - let theme = cx.theme(); + pub fn new(label: impl Into) -> Self { Self { - base: div().text_color(theme.foreground).text_size(px(14.0)), + base: div(), label: label.into(), - multiple_lines: false, - color: Hsla::white(), - line_height: None, - text_size: None, + multiple_lines: true, } } @@ -42,24 +35,17 @@ impl Styled for Label { impl RenderOnce for Label { fn render(self, cx: &mut WindowContext) -> impl IntoElement { - let label_text = if !self.multiple_lines { + let theme = cx.theme(); + + let text = if !self.multiple_lines { SharedString::from(self.label.replace('\n', "␤")) } else { self.label }; self.base - .child(label_text) - .map(|this| { - if let Some(text_size) = self.text_size { - this.text_size(text_size) - } else { - this - } - }) - .map(|this| match self.line_height { - Some(line_height) => this.line_height(line_height), - None => this, - }) + .text_color(theme.foreground) + .text_size(px(theme.font_size)) + .child(text) } } diff --git a/crates/ui/src/story/mod.rs b/crates/ui/src/story/mod.rs index e92eb26c..986f8978 100644 --- a/crates/ui/src/story/mod.rs +++ b/crates/ui/src/story/mod.rs @@ -65,8 +65,8 @@ impl RenderOnce for StoryContainer { .flex() .flex_col() .gap_2() - .child(Label::new(self.name, cx).text_size(px(24.0))) - .child(Label::new(self.description, cx).text_size(px(16.0))), + .child(Label::new(self.name).text_size(px(24.0))) + .child(Label::new(self.description).text_size(px(16.0))), ) .children(self.children) } diff --git a/crates/ui/src/story/switch_story.rs b/crates/ui/src/story/switch_story.rs index 6a3c9bf4..42346207 100644 --- a/crates/ui/src/story/switch_story.rs +++ b/crates/ui/src/story/switch_story.rs @@ -1,14 +1,17 @@ use gpui::{ - div, ClickEvent, IntoElement, ParentElement, Render, RenderOnce, + div, ClickEvent, Div, IntoElement, ParentElement, Render, RenderOnce, SharedString, StatefulInteractiveElement as _, Styled, ViewContext, VisualContext as _, WindowContext, }; use crate::{ checkbox::Checkbox, disableable::Disableable as _, + label::Label, selectable::Selection, stock::{h_flex, v_flex}, - switch::Switch, + switch::{LabelSide, Switch}, + theme::ActiveTheme, + StyledExt, }; use super::story_case; @@ -30,6 +33,25 @@ impl SwitchStory { impl Render for SwitchStory { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let theme = cx.theme(); + + fn title(title: impl Into) -> Div { + v_flex().flex_1().gap_2().child(Label::new(title).text_xl()) + } + + fn card(cx: &ViewContext) -> Div { + let theme = cx.theme(); + + h_flex() + .items_center() + .gap_4() + .p_4() + .w_full() + .rounded_lg() + .border_1() + .border_color(theme.border) + } + story_case( "Switch", "A control that allows the user to toggle between checked and not checked.", @@ -40,17 +62,30 @@ impl Render for SwitchStory { .justify_center() .gap_4() .child( - h_flex() - .items_center() - .gap_4() + card(cx) + .child( + title("Marketing emails").child( + Label::new("Receive emails about new products, features, and more.").text_color(theme.muted) + ) + ) .child( Switch::new("switch1") .checked(self.switch1) + .label_side(LabelSide::Left) + .label("Subscribe") .on_click(cx.listener(move |view, _, cx| { view.switch1 = !view.switch1; cx.notify(); })), + ), + ) + .child( + card(cx) + .child( + title("Security emails").child( + Label::new("Receive emails about your account security. When turn off, you never recive email again.").text_color(theme.muted) ) + ) .child( Switch::new("switch2") .checked(self.switch2) @@ -61,20 +96,20 @@ impl Render for SwitchStory { ), ) .child( - h_flex() - .items_center() - .gap_4() + card(cx).v_flex() .items_start().child(title("Disabled Switchs")).child( + h_flex().items_center() + .gap_6() .child(Switch::new("switch3").disabled(true).on_click(|ev, cx| { println!("Switch value changed: {:?}", ev); })) .child( - Switch::new("switch3_1") + Switch::new("switch3_1").label("Airplane Mode") .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 index a505cca1..bf14c7f4 100644 --- a/crates/ui/src/switch.rs +++ b/crates/ui/src/switch.rs @@ -1,4 +1,6 @@ use crate::{ + label::Label, + stock::h_flex, theme::{ActiveTheme, Colorize}, Disableable, }; @@ -10,23 +12,40 @@ use gpui::{ type OnClick = Box; +pub enum LabelSide { + Left, + Right, +} + +impl LabelSide { + fn left(&self) -> bool { + matches!(self, Self::Left) + } +} + #[derive(IntoElement)] pub struct Switch { + id: SharedString, base: Stateful
, checked: bool, disabled: bool, label: Option, + label_side: LabelSide, on_click: Option, } impl Switch { pub fn new(id: impl Into) -> Self { + let id: SharedString = id.into(); + Self { - base: div().id(id.into()), + id: id.clone(), + base: div().id(id), checked: false, disabled: false, label: None, on_click: None, + label_side: LabelSide::Right, } } @@ -44,6 +63,11 @@ impl Switch { self.on_click = Some(Box::new(handler)); self } + + pub fn label_side(mut self, label_side: LabelSide) -> Self { + self.label_side = label_side; + self + } } impl Disableable for Switch { @@ -56,6 +80,7 @@ impl Disableable for Switch { impl RenderOnce for Switch { fn render(self, cx: &mut gpui::WindowContext) -> impl IntoElement { let theme = cx.theme(); + let group_id = format!("switch_group_{:?}", self.id); let (bg, toggle_bg) = match self.checked { true => (theme.primary, theme.background), @@ -67,21 +92,30 @@ impl RenderOnce for Switch { false => (bg, toggle_bg), }; - self.base - .w_11() - .h_6() - .flex() + h_flex() + .id(self.id) + .group(group_id) .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)) + .gap_2() + .when(self.label_side.left(), |this| this.flex_row_reverse()) + .child( + 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.label, |this, label| this.child(Label::new(label))) .when_some( self.on_click.filter(|_| !self.disabled), |this, on_click| this.on_click(move |ev, cx| on_click(ev, cx)), diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index c10c3f7a..143a7009 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -206,6 +206,8 @@ pub struct Theme { pub mode: ThemeMode, pub transparent: Hsla, pub title_bar_background: Hsla, + /// Basic font size + pub font_size: f32, pub background: Hsla, pub foreground: Hsla, pub card: Hsla, @@ -241,6 +243,7 @@ impl From for Theme { Theme { mode: ThemeMode::Dark, transparent: Hsla::transparent_black(), + font_size: 14.0, title_bar_background: colors.title_bar_background, background: colors.background, foreground: colors.foreground, diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 5413689f..98036bf8 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -49,6 +49,10 @@ impl Workspace { appears_transparent: true, traffic_light_position: Some(point(px(9.0), px(9.0))), }), + window_min_size: Size { + width: px(640.), + height: px(480.), + }, kind: WindowKind::Normal, ..Default::default() }; @@ -120,12 +124,7 @@ impl Render for Workspace { }) }) // left side - .child( - div() - .flex() - .items_center() - .child(Label::new("GPUI App", cx)), - ) + .child(div().flex().items_center().child(Label::new("GPUI App"))) .child( div() .flex()