From 6bcc34c7f003e34944dfcadd44e347a94d002fa1 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 24 Feb 2025 18:04:06 +0800 Subject: [PATCH] checkbox, switch: Improve Checkbox, Switch to use wrap text and TextView. (#651) This change to revert #233 to let label to wrap. image --- crates/story/src/switch_story.rs | 64 ++++++++++------ crates/ui/src/checkbox.rs | 31 ++++---- crates/ui/src/radio.rs | 34 +++++++-- crates/ui/src/switch.rs | 31 +++++--- crates/ui/src/text/mod.rs | 125 +------------------------------ crates/ui/src/text/text_view.rs | 123 ++++++++++++++++++++++++++++++ 6 files changed, 227 insertions(+), 181 deletions(-) create mode 100644 crates/ui/src/text/text_view.rs diff --git a/crates/story/src/switch_story.rs b/crates/story/src/switch_story.rs index 2ef91e01..b14ade1a 100644 --- a/crates/story/src/switch_story.rs +++ b/crates/story/src/switch_story.rs @@ -1,5 +1,5 @@ use gpui::{ - div, px, App, AppContext, Context, Div, Entity, Focusable, IntoElement, ParentElement, Render, + px, App, AppContext, Context, Div, Entity, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Window, }; @@ -150,6 +150,7 @@ impl Render for SwitchStory { })) .child( Switch::new("switch3_1") + .w(px(200.)) .label("Airplane Mode") .checked(true) .disabled(true) @@ -204,17 +205,21 @@ impl Render for SwitchStory { v.check3 = !v.check3; })), ) + .child(Checkbox::new("longlong-checkbox").w(px(300.)).label( + "The long long label text, \ + it should wrap when the text is too long.", + )) .child( - div().w(px(300.)).child( - Checkbox::new("longlong-checkbox").label( + Checkbox::new("longlong-markdown-checkbox") + .w(px(300.)) + .label( TextView::markdown( - "longlong-checkbox", - "The **long long label** text, \ - it should ellipsis when the text is too long.", + "longlong-markdown-checkbox", + "The [long long label](https://github.com) text used markdown, \ + it should wrap when the text is too long.", ) .inline(), ), - ), ), ), ) @@ -264,12 +269,14 @@ impl Render for SwitchStory { .disabled(true), ) .child( - div().w(px(200.)).child( - Radio::new("radio3") - .label("A long long long text radio label") - .checked(true) - .disabled(true), - ), + Radio::new("radio3") + .label( + "The long long label text, \ + it should wrap when the text is too long.", + ) + .w(px(300.)) + .checked(true) + .disabled(true), ), ), ) @@ -289,17 +296,26 @@ impl Render for SwitchStory { ), ) .child( - section("Radio Group Vertical", cx).flex_1().child( - RadioGroup::vertical() - .disabled(true) - .child(Radio::new("one1").label("United States")) - .child(Radio::new("one2").label("Canada")) - .child(Radio::new("one3").label("Mexico")) - .selected_index(self.radio_group_checked) - .on_change(cx.listener(|this, selected_ix: &usize, _, _| { - this.radio_group_checked = Some(*selected_ix); - })), - ), + section("Radio Group Vertical (With container style)", cx) + .flex_1() + .child( + RadioGroup::vertical() + .w(px(220.)) + .p_2() + .border_1() + .border_color(cx.theme().border) + .rounded_md() + .disabled(true) + .child(Radio::new("one1").label("United States")) + .child(Radio::new("one2").label("Canada")) + .child(Radio::new("one3").label("Mexico")) + .selected_index(self.radio_group_checked) + .on_change(cx.listener( + |this, selected_ix: &usize, _, _| { + this.radio_group_checked = Some(*selected_ix); + }, + )), + ), ), ), ) diff --git a/crates/ui/src/checkbox.rs b/crates/ui/src/checkbox.rs index 8ac120b4..d364ed35 100644 --- a/crates/ui/src/checkbox.rs +++ b/crates/ui/src/checkbox.rs @@ -1,13 +1,14 @@ use crate::{h_flex, text::Text, v_flex, ActiveTheme, Disableable, IconName, Selectable}; use gpui::{ - div, prelude::FluentBuilder as _, px, relative, svg, App, ElementId, InteractiveElement, - IntoElement, ParentElement, RenderOnce, StatefulInteractiveElement as _, Styled as _, Window, + div, prelude::FluentBuilder as _, px, relative, svg, App, Div, ElementId, InteractiveElement, + IntoElement, ParentElement, RenderOnce, StatefulInteractiveElement as _, Styled, Window, }; /// A Checkbox element. #[derive(IntoElement)] pub struct Checkbox { id: ElementId, + base: Div, label: Option, checked: bool, disabled: bool, @@ -18,6 +19,7 @@ impl Checkbox { pub fn new(id: impl Into) -> Self { Self { id: id.into(), + base: div(), label: None, checked: false, disabled: false, @@ -41,6 +43,12 @@ impl Checkbox { } } +impl Styled for Checkbox { + fn style(&mut self) -> &mut gpui::StyleRefinement { + self.base.style() + } +} + impl Disableable for Checkbox { fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; @@ -70,21 +78,21 @@ impl RenderOnce for Checkbox { }; let radius = (cx.theme().radius / 2.).min(px(6.)); - // wrap a flex to patch for let Checkbox display inline - div().flex().child( + self.base.child( h_flex() .id(self.id) .gap_2() - .items_center() + .items_start() .line_height(relative(1.)) + .text_color(cx.theme().foreground) .child( v_flex() .relative() + .size_4() + .flex_shrink_0() .border_1() .border_color(color) .rounded(radius) - .size_4() - .flex_shrink_0() .map(|this| match self.checked { false => this.bg(cx.theme().transparent), _ => this.bg(color), @@ -104,14 +112,7 @@ impl RenderOnce for Checkbox { ) .map(|this| { if let Some(label) = self.label { - this.text_color(cx.theme().foreground).child( - div() - .w_full() - .overflow_x_hidden() - .text_ellipsis() - .line_height(relative(1.)) - .child(label), - ) + this.child(div().size_full().line_height(relative(1.)).child(label)) } else { this } diff --git a/crates/ui/src/radio.rs b/crates/ui/src/radio.rs index 596b123d..268f70d4 100644 --- a/crates/ui/src/radio.rs +++ b/crates/ui/src/radio.rs @@ -2,9 +2,9 @@ use std::rc::Rc; use crate::{h_flex, text::Text, v_flex, ActiveTheme, AxisExt, IconName}; use gpui::{ - div, prelude::FluentBuilder, relative, svg, App, Axis, ElementId, InteractiveElement, - IntoElement, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, - Window, + div, prelude::FluentBuilder, relative, svg, App, Axis, Div, ElementId, InteractiveElement, + IntoElement, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, + StyleRefinement, Styled, Window, }; /// A Radio element. @@ -12,6 +12,7 @@ use gpui::{ /// This is not included the Radio group implementation, you can manage the group by yourself. #[derive(IntoElement)] pub struct Radio { + base: Div, id: ElementId, label: Option, checked: bool, @@ -23,6 +24,7 @@ impl Radio { pub fn new(id: impl Into) -> Self { Self { id: id.into(), + base: div(), label: None, checked: false, disabled: false, @@ -51,6 +53,12 @@ impl Radio { } } +impl Styled for Radio { + fn style(&mut self) -> &mut gpui::StyleRefinement { + self.base.style() + } +} + impl RenderOnce for Radio { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let color = if self.disabled { @@ -60,12 +68,12 @@ impl RenderOnce for Radio { }; // wrap a flex to patch for let Radio display inline - h_flex().child( + self.base.child( h_flex() .id(self.id) .gap_x_2() .text_color(cx.theme().foreground) - .items_center() + .items_start() .line_height(relative(1.)) .child( div() @@ -96,8 +104,7 @@ impl RenderOnce for Radio { this.child( div() .size_full() - .overflow_x_hidden() - .text_ellipsis() + .overflow_hidden() .line_height(relative(1.)) .child(label), ) @@ -117,6 +124,7 @@ impl RenderOnce for Radio { /// A Radio group element. #[derive(IntoElement)] pub struct RadioGroup { + style: StyleRefinement, radios: Vec, layout: Axis, selected_index: Option, @@ -127,6 +135,7 @@ pub struct RadioGroup { impl RadioGroup { fn new() -> Self { Self { + style: StyleRefinement::default(), on_change: None, layout: Axis::Vertical, selected_index: None, @@ -182,6 +191,12 @@ impl RadioGroup { } } +impl Styled for RadioGroup { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} + impl From<&'static str> for Radio { fn from(label: &'static str) -> Self { Self::new(label).label(label) @@ -212,7 +227,10 @@ impl RenderOnce for RadioGroup { h_flex().flex_wrap() }; - div().flex().child( + let mut container = div(); + *container.style() = self.style; + + container.child( base.gap_3() .children(self.radios.into_iter().enumerate().map(|(ix, radio)| { let checked = selected_ix == Some(ix); diff --git a/crates/ui/src/switch.rs b/crates/ui/src/switch.rs index 4ea8f3f1..2ec4c353 100644 --- a/crates/ui/src/switch.rs +++ b/crates/ui/src/switch.rs @@ -1,13 +1,14 @@ use crate::{h_flex, text::Text, ActiveTheme, Disableable, Side, Sizable, Size}; use gpui::{ - div, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, AnyElement, App, Element, - ElementId, GlobalElementId, InteractiveElement, IntoElement, LayoutId, ParentElement as _, - Styled as _, Window, + div, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, AnyElement, App, Div, + Element, ElementId, GlobalElementId, InteractiveElement, IntoElement, LayoutId, + ParentElement as _, Styled, Window, }; use std::{cell::RefCell, rc::Rc, time::Duration}; pub struct Switch { id: ElementId, + base: Div, checked: bool, disabled: bool, label: Option, @@ -21,6 +22,7 @@ impl Switch { let id: ElementId = id.into(); Self { id: id.clone(), + base: div(), checked: false, disabled: false, label: None, @@ -54,6 +56,12 @@ impl Switch { } } +impl Styled for Switch { + fn style(&mut self) -> &mut gpui::StyleRefinement { + self.base.style() + } +} + impl Sizable for Switch { fn with_size(mut self, size: impl Into) -> Self { self.size = size.into(); @@ -98,14 +106,13 @@ impl Element for Switch { ) -> (LayoutId, Self::RequestLayoutState) { window.with_element_state::(global_id.unwrap(), move |state, window| { let state = state.unwrap_or_default(); - - let theme = cx.theme(); let checked = self.checked; let on_click = self.on_click.clone(); + let style = self.base.style(); let (bg, toggle_bg) = match self.checked { - true => (theme.primary, theme.background), - false => (theme.input, theme.background), + true => (cx.theme().primary, cx.theme().background), + false => (cx.theme().input, cx.theme().background), }; let (bg, toggle_bg) = match self.disabled { @@ -128,13 +135,15 @@ impl Element for Switch { cx.theme().radius }; - let mut element = div() - .flex() + let mut root = div(); + *root.style() = style.clone(); + + let mut element = root .child( h_flex() .id(self.id.clone()) - .items_center() .gap_2() + .items_start() .when(self.label_side.is_left(), |this| this.flex_row_reverse()) .child( // Switch Bar @@ -146,7 +155,7 @@ impl Element for Switch { .flex() .items_center() .border(inset) - .border_color(theme.transparent) + .border_color(cx.theme().transparent) .bg(bg) .when(!self.disabled, |this| this.cursor_pointer()) .child( diff --git a/crates/ui/src/text/mod.rs b/crates/ui/src/text/mod.rs index 40de37bb..7156ccc4 100644 --- a/crates/ui/src/text/mod.rs +++ b/crates/ui/src/text/mod.rs @@ -1,128 +1,7 @@ -use gpui::{rems, App, ElementId, IntoElement, Rems, RenderOnce, SharedString, Window}; -use html::HtmlElement; -use markdown::MarkdownElement; - mod element; mod html; mod markdown; +mod text_view; mod utils; -#[derive(IntoElement, Clone)] -pub enum Text { - String(SharedString), - TextView(TextView), -} - -impl From for Text { - fn from(s: SharedString) -> Self { - Self::String(s) - } -} - -impl From<&str> for Text { - fn from(s: &str) -> Self { - Self::String(SharedString::from(s.to_string())) - } -} - -impl From for Text { - fn from(s: String) -> Self { - Self::String(s.into()) - } -} - -impl From for Text { - fn from(e: TextView) -> Self { - Self::TextView(e) - } -} - -impl RenderOnce for Text { - fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { - match self { - Self::String(s) => s.into_any_element(), - Self::TextView(e) => e.into_any_element(), - } - } -} - -/// TextViewStyle used to customize the style for [`TextView`]. -#[derive(Copy, Clone)] -pub struct TextViewStyle { - paragraph_gap: Rems, -} - -impl Default for TextViewStyle { - fn default() -> Self { - Self { - paragraph_gap: rems(1.), - } - } -} - -impl TextViewStyle { - /// Default style for inline text. - /// - /// This style has no paragraph gap. - pub fn inline() -> Self { - Self { - paragraph_gap: rems(0.), - } - } - - /// Set paragraph gap, default is 1 rem. - pub fn paragraph_gap(mut self, gap: Rems) -> Self { - self.paragraph_gap = gap; - self - } -} - -/// A text view that can render Markdown or HTML. -#[allow(private_interfaces)] -#[derive(IntoElement, Clone)] -pub enum TextView { - Markdown(MarkdownElement), - Html(HtmlElement), -} - -impl TextView { - /// Create a new markdown text view. - pub fn markdown(id: impl Into, raw: impl Into) -> Self { - Self::Markdown(MarkdownElement::new(id, raw)) - } - - /// Create a new html text view. - pub fn html(id: impl Into, raw: impl Into) -> Self { - Self::Html(HtmlElement::new(id, raw)) - } - - /// Set the source text of the text view. - pub fn text(self, raw: impl Into) -> Self { - match self { - Self::Markdown(el) => Self::Markdown(el.text(raw)), - Self::Html(el) => Self::Html(el.text(raw)), - } - } - - /// Set [`TextViewStyle`]. - pub fn style(self, style: TextViewStyle) -> Self { - match self { - Self::Markdown(el) => Self::Markdown(el.style(style)), - Self::Html(el) => Self::Html(el.style(style)), - } - } - - /// Set to use [`TextViewStyle::inline`]. - pub fn inline(self) -> Self { - self.style(TextViewStyle::inline()) - } -} - -impl RenderOnce for TextView { - fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { - match self { - Self::Markdown(el) => el.into_any_element(), - Self::Html(el) => el.into_any_element(), - } - } -} +pub use text_view::*; diff --git a/crates/ui/src/text/text_view.rs b/crates/ui/src/text/text_view.rs new file mode 100644 index 00000000..7dcdf438 --- /dev/null +++ b/crates/ui/src/text/text_view.rs @@ -0,0 +1,123 @@ +use gpui::{rems, App, ElementId, IntoElement, Rems, RenderOnce, SharedString, Window}; + +use super::{html::HtmlElement, markdown::MarkdownElement}; + +/// A text view that can render Markdown or HTML. +#[allow(private_interfaces)] +#[derive(IntoElement, Clone)] +pub enum TextView { + Markdown(MarkdownElement), + Html(HtmlElement), +} + +#[derive(IntoElement, Clone)] +pub enum Text { + String(SharedString), + TextView(TextView), +} + +impl From for Text { + fn from(s: SharedString) -> Self { + Self::String(s) + } +} + +impl From<&str> for Text { + fn from(s: &str) -> Self { + Self::String(SharedString::from(s.to_string())) + } +} + +impl From for Text { + fn from(s: String) -> Self { + Self::String(s.into()) + } +} + +impl From for Text { + fn from(e: TextView) -> Self { + Self::TextView(e) + } +} + +impl RenderOnce for Text { + fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { + match self { + Self::String(s) => s.into_any_element(), + Self::TextView(e) => e.into_any_element(), + } + } +} + +/// TextViewStyle used to customize the style for [`TextView`]. +#[derive(Copy, Clone)] +pub struct TextViewStyle { + pub paragraph_gap: Rems, +} + +impl Default for TextViewStyle { + fn default() -> Self { + Self { + paragraph_gap: rems(1.), + } + } +} + +impl TextViewStyle { + /// Default style for inline text. + /// + /// This style has no paragraph gap. + pub fn inline() -> Self { + Self { + paragraph_gap: rems(0.), + } + } + + /// Set paragraph gap, default is 1 rem. + pub fn paragraph_gap(mut self, gap: Rems) -> Self { + self.paragraph_gap = gap; + self + } +} + +impl TextView { + /// Create a new markdown text view. + pub fn markdown(id: impl Into, raw: impl Into) -> Self { + Self::Markdown(MarkdownElement::new(id, raw)) + } + + /// Create a new html text view. + pub fn html(id: impl Into, raw: impl Into) -> Self { + Self::Html(HtmlElement::new(id, raw)) + } + + /// Set the source text of the text view. + pub fn text(self, raw: impl Into) -> Self { + match self { + Self::Markdown(el) => Self::Markdown(el.text(raw)), + Self::Html(el) => Self::Html(el.text(raw)), + } + } + + /// Set [`TextViewStyle`]. + pub fn style(self, style: TextViewStyle) -> Self { + match self { + Self::Markdown(el) => Self::Markdown(el.style(style)), + Self::Html(el) => Self::Html(el.style(style)), + } + } + + /// Set to use [`TextViewStyle::inline`]. + pub fn inline(self) -> Self { + self.style(TextViewStyle::inline()) + } +} + +impl RenderOnce for TextView { + fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { + match self { + Self::Markdown(el) => el.into_any_element(), + Self::Html(el) => el.into_any_element(), + } + } +}