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::{
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);
},
)),
),
),
),
)

View file

@ -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<Text>,
checked: bool,
disabled: bool,
@ -18,6 +19,7 @@ impl Checkbox {
pub fn new(id: impl Into<ElementId>) -> 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
}

View file

@ -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<Text>,
checked: bool,
@ -23,6 +24,7 @@ impl Radio {
pub fn new(id: impl Into<ElementId>) -> 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<Radio>,
layout: Axis,
selected_index: Option<usize>,
@ -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);

View file

@ -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<Text>,
@ -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<Size>) -> Self {
self.size = size.into();
@ -98,14 +106,13 @@ impl Element for Switch {
) -> (LayoutId, Self::RequestLayoutState) {
window.with_element_state::<SwitchState, _>(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(

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 html;
mod markdown;
mod text_view;
mod utils;
#[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 {
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(),
}
}
}
pub use text_view::*;

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(),
}
}
}