checkbox, switch: Improve Checkbox, Switch to use wrap text and TextView. (#651)

This change to revert #233 to let label to wrap.


<img width="1279" alt="image"
src="https://github.com/user-attachments/assets/8081216f-0ea0-4ba2-9095-6fd84102e03b"
/>
This commit is contained in:
Jason Lee 2025-02-24 18:04:06 +08:00 committed by GitHub
parent 8fa210bc21
commit 6bcc34c7f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 227 additions and 181 deletions

View file

@ -1,5 +1,5 @@
use gpui::{ 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, SharedString, Styled, Window,
}; };
@ -150,6 +150,7 @@ impl Render for SwitchStory {
})) }))
.child( .child(
Switch::new("switch3_1") Switch::new("switch3_1")
.w(px(200.))
.label("Airplane Mode") .label("Airplane Mode")
.checked(true) .checked(true)
.disabled(true) .disabled(true)
@ -204,17 +205,21 @@ impl Render for SwitchStory {
v.check3 = !v.check3; 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( .child(
div().w(px(300.)).child( Checkbox::new("longlong-markdown-checkbox")
Checkbox::new("longlong-checkbox").label( .w(px(300.))
.label(
TextView::markdown( TextView::markdown(
"longlong-checkbox", "longlong-markdown-checkbox",
"The **long long label** text, \ "The [long long label](https://github.com) text used markdown, \
it should ellipsis when the text is too long.", it should wrap when the text is too long.",
) )
.inline(), .inline(),
), ),
),
), ),
), ),
) )
@ -264,12 +269,14 @@ impl Render for SwitchStory {
.disabled(true), .disabled(true),
) )
.child( .child(
div().w(px(200.)).child( Radio::new("radio3")
Radio::new("radio3") .label(
.label("A long long long text radio label") "The long long label text, \
.checked(true) it should wrap when the text is too long.",
.disabled(true), )
), .w(px(300.))
.checked(true)
.disabled(true),
), ),
), ),
) )
@ -289,17 +296,26 @@ impl Render for SwitchStory {
), ),
) )
.child( .child(
section("Radio Group Vertical", cx).flex_1().child( section("Radio Group Vertical (With container style)", cx)
RadioGroup::vertical() .flex_1()
.disabled(true) .child(
.child(Radio::new("one1").label("United States")) RadioGroup::vertical()
.child(Radio::new("one2").label("Canada")) .w(px(220.))
.child(Radio::new("one3").label("Mexico")) .p_2()
.selected_index(self.radio_group_checked) .border_1()
.on_change(cx.listener(|this, selected_ix: &usize, _, _| { .border_color(cx.theme().border)
this.radio_group_checked = Some(*selected_ix); .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);
},
)),
),
), ),
), ),
) )

View file

@ -1,13 +1,14 @@
use crate::{h_flex, text::Text, v_flex, ActiveTheme, Disableable, IconName, Selectable}; use crate::{h_flex, text::Text, v_flex, ActiveTheme, Disableable, IconName, Selectable};
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, relative, svg, App, ElementId, InteractiveElement, div, prelude::FluentBuilder as _, px, relative, svg, App, Div, ElementId, InteractiveElement,
IntoElement, ParentElement, RenderOnce, StatefulInteractiveElement as _, Styled as _, Window, IntoElement, ParentElement, RenderOnce, StatefulInteractiveElement as _, Styled, Window,
}; };
/// A Checkbox element. /// A Checkbox element.
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct Checkbox { pub struct Checkbox {
id: ElementId, id: ElementId,
base: Div,
label: Option<Text>, label: Option<Text>,
checked: bool, checked: bool,
disabled: bool, disabled: bool,
@ -18,6 +19,7 @@ impl Checkbox {
pub fn new(id: impl Into<ElementId>) -> Self { pub fn new(id: impl Into<ElementId>) -> Self {
Self { Self {
id: id.into(), id: id.into(),
base: div(),
label: None, label: None,
checked: false, checked: false,
disabled: 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 { impl Disableable for Checkbox {
fn disabled(mut self, disabled: bool) -> Self { fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled; self.disabled = disabled;
@ -70,21 +78,21 @@ impl RenderOnce for Checkbox {
}; };
let radius = (cx.theme().radius / 2.).min(px(6.)); let radius = (cx.theme().radius / 2.).min(px(6.));
// wrap a flex to patch for let Checkbox display inline self.base.child(
div().flex().child(
h_flex() h_flex()
.id(self.id) .id(self.id)
.gap_2() .gap_2()
.items_center() .items_start()
.line_height(relative(1.)) .line_height(relative(1.))
.text_color(cx.theme().foreground)
.child( .child(
v_flex() v_flex()
.relative() .relative()
.size_4()
.flex_shrink_0()
.border_1() .border_1()
.border_color(color) .border_color(color)
.rounded(radius) .rounded(radius)
.size_4()
.flex_shrink_0()
.map(|this| match self.checked { .map(|this| match self.checked {
false => this.bg(cx.theme().transparent), false => this.bg(cx.theme().transparent),
_ => this.bg(color), _ => this.bg(color),
@ -104,14 +112,7 @@ impl RenderOnce for Checkbox {
) )
.map(|this| { .map(|this| {
if let Some(label) = self.label { if let Some(label) = self.label {
this.text_color(cx.theme().foreground).child( this.child(div().size_full().line_height(relative(1.)).child(label))
div()
.w_full()
.overflow_x_hidden()
.text_ellipsis()
.line_height(relative(1.))
.child(label),
)
} else { } else {
this this
} }

View file

@ -2,9 +2,9 @@ use std::rc::Rc;
use crate::{h_flex, text::Text, v_flex, ActiveTheme, AxisExt, IconName}; use crate::{h_flex, text::Text, v_flex, ActiveTheme, AxisExt, IconName};
use gpui::{ use gpui::{
div, prelude::FluentBuilder, relative, svg, App, Axis, ElementId, InteractiveElement, div, prelude::FluentBuilder, relative, svg, App, Axis, Div, ElementId, InteractiveElement,
IntoElement, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, IntoElement, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement,
Window, StyleRefinement, Styled, Window,
}; };
/// A Radio element. /// A Radio element.
@ -12,6 +12,7 @@ use gpui::{
/// This is not included the Radio group implementation, you can manage the group by yourself. /// This is not included the Radio group implementation, you can manage the group by yourself.
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct Radio { pub struct Radio {
base: Div,
id: ElementId, id: ElementId,
label: Option<Text>, label: Option<Text>,
checked: bool, checked: bool,
@ -23,6 +24,7 @@ impl Radio {
pub fn new(id: impl Into<ElementId>) -> Self { pub fn new(id: impl Into<ElementId>) -> Self {
Self { Self {
id: id.into(), id: id.into(),
base: div(),
label: None, label: None,
checked: false, checked: false,
disabled: 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 { impl RenderOnce for Radio {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let color = if self.disabled { let color = if self.disabled {
@ -60,12 +68,12 @@ impl RenderOnce for Radio {
}; };
// wrap a flex to patch for let Radio display inline // wrap a flex to patch for let Radio display inline
h_flex().child( self.base.child(
h_flex() h_flex()
.id(self.id) .id(self.id)
.gap_x_2() .gap_x_2()
.text_color(cx.theme().foreground) .text_color(cx.theme().foreground)
.items_center() .items_start()
.line_height(relative(1.)) .line_height(relative(1.))
.child( .child(
div() div()
@ -96,8 +104,7 @@ impl RenderOnce for Radio {
this.child( this.child(
div() div()
.size_full() .size_full()
.overflow_x_hidden() .overflow_hidden()
.text_ellipsis()
.line_height(relative(1.)) .line_height(relative(1.))
.child(label), .child(label),
) )
@ -117,6 +124,7 @@ impl RenderOnce for Radio {
/// A Radio group element. /// A Radio group element.
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct RadioGroup { pub struct RadioGroup {
style: StyleRefinement,
radios: Vec<Radio>, radios: Vec<Radio>,
layout: Axis, layout: Axis,
selected_index: Option<usize>, selected_index: Option<usize>,
@ -127,6 +135,7 @@ pub struct RadioGroup {
impl RadioGroup { impl RadioGroup {
fn new() -> Self { fn new() -> Self {
Self { Self {
style: StyleRefinement::default(),
on_change: None, on_change: None,
layout: Axis::Vertical, layout: Axis::Vertical,
selected_index: None, 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 { impl From<&'static str> for Radio {
fn from(label: &'static str) -> Self { fn from(label: &'static str) -> Self {
Self::new(label).label(label) Self::new(label).label(label)
@ -212,7 +227,10 @@ impl RenderOnce for RadioGroup {
h_flex().flex_wrap() h_flex().flex_wrap()
}; };
div().flex().child( let mut container = div();
*container.style() = self.style;
container.child(
base.gap_3() base.gap_3()
.children(self.radios.into_iter().enumerate().map(|(ix, radio)| { .children(self.radios.into_iter().enumerate().map(|(ix, radio)| {
let checked = selected_ix == Some(ix); let checked = selected_ix == Some(ix);

View file

@ -1,13 +1,14 @@
use crate::{h_flex, text::Text, ActiveTheme, Disableable, Side, Sizable, Size}; use crate::{h_flex, text::Text, ActiveTheme, Disableable, Side, Sizable, Size};
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, AnyElement, App, Element, div, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, AnyElement, App, Div,
ElementId, GlobalElementId, InteractiveElement, IntoElement, LayoutId, ParentElement as _, Element, ElementId, GlobalElementId, InteractiveElement, IntoElement, LayoutId,
Styled as _, Window, ParentElement as _, Styled, Window,
}; };
use std::{cell::RefCell, rc::Rc, time::Duration}; use std::{cell::RefCell, rc::Rc, time::Duration};
pub struct Switch { pub struct Switch {
id: ElementId, id: ElementId,
base: Div,
checked: bool, checked: bool,
disabled: bool, disabled: bool,
label: Option<Text>, label: Option<Text>,
@ -21,6 +22,7 @@ impl Switch {
let id: ElementId = id.into(); let id: ElementId = id.into();
Self { Self {
id: id.clone(), id: id.clone(),
base: div(),
checked: false, checked: false,
disabled: false, disabled: false,
label: None, 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 { impl Sizable for Switch {
fn with_size(mut self, size: impl Into<Size>) -> Self { fn with_size(mut self, size: impl Into<Size>) -> Self {
self.size = size.into(); self.size = size.into();
@ -98,14 +106,13 @@ impl Element for Switch {
) -> (LayoutId, Self::RequestLayoutState) { ) -> (LayoutId, Self::RequestLayoutState) {
window.with_element_state::<SwitchState, _>(global_id.unwrap(), move |state, window| { window.with_element_state::<SwitchState, _>(global_id.unwrap(), move |state, window| {
let state = state.unwrap_or_default(); let state = state.unwrap_or_default();
let theme = cx.theme();
let checked = self.checked; let checked = self.checked;
let on_click = self.on_click.clone(); let on_click = self.on_click.clone();
let style = self.base.style();
let (bg, toggle_bg) = match self.checked { let (bg, toggle_bg) = match self.checked {
true => (theme.primary, theme.background), true => (cx.theme().primary, cx.theme().background),
false => (theme.input, theme.background), false => (cx.theme().input, cx.theme().background),
}; };
let (bg, toggle_bg) = match self.disabled { let (bg, toggle_bg) = match self.disabled {
@ -128,13 +135,15 @@ impl Element for Switch {
cx.theme().radius cx.theme().radius
}; };
let mut element = div() let mut root = div();
.flex() *root.style() = style.clone();
let mut element = root
.child( .child(
h_flex() h_flex()
.id(self.id.clone()) .id(self.id.clone())
.items_center()
.gap_2() .gap_2()
.items_start()
.when(self.label_side.is_left(), |this| this.flex_row_reverse()) .when(self.label_side.is_left(), |this| this.flex_row_reverse())
.child( .child(
// Switch Bar // Switch Bar
@ -146,7 +155,7 @@ impl Element for Switch {
.flex() .flex()
.items_center() .items_center()
.border(inset) .border(inset)
.border_color(theme.transparent) .border_color(cx.theme().transparent)
.bg(bg) .bg(bg)
.when(!self.disabled, |this| this.cursor_pointer()) .when(!self.disabled, |this| this.cursor_pointer())
.child( .child(

View file

@ -1,128 +1,7 @@
use gpui::{rems, App, ElementId, IntoElement, Rems, RenderOnce, SharedString, Window};
use html::HtmlElement;
use markdown::MarkdownElement;
mod element; mod element;
mod html; mod html;
mod markdown; mod markdown;
mod text_view;
mod utils; mod utils;
#[derive(IntoElement, Clone)] pub use text_view::*;
pub enum Text {
String(SharedString),
TextView(TextView),
}
impl From<SharedString> 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<String> for Text {
fn from(s: String) -> Self {
Self::String(s.into())
}
}
impl From<TextView> 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<ElementId>, raw: impl Into<SharedString>) -> Self {
Self::Markdown(MarkdownElement::new(id, raw))
}
/// Create a new html text view.
pub fn html(id: impl Into<ElementId>, raw: impl Into<SharedString>) -> Self {
Self::Html(HtmlElement::new(id, raw))
}
/// Set the source text of the text view.
pub fn text(self, raw: impl Into<SharedString>) -> 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(),
}
}
}

View file

@ -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<SharedString> 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<String> for Text {
fn from(s: String) -> Self {
Self::String(s.into())
}
}
impl From<TextView> 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<ElementId>, raw: impl Into<SharedString>) -> Self {
Self::Markdown(MarkdownElement::new(id, raw))
}
/// Create a new html text view.
pub fn html(id: impl Into<ElementId>, raw: impl Into<SharedString>) -> Self {
Self::Html(HtmlElement::new(id, raw))
}
/// Set the source text of the text view.
pub fn text(self, raw: impl Into<SharedString>) -> 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(),
}
}
}