diff --git a/crates/story/examples/switch.rs b/crates/story/examples/switch.rs new file mode 100644 index 00000000..f3587b89 --- /dev/null +++ b/crates/story/examples/switch.rs @@ -0,0 +1,35 @@ +use gpui::*; +use story::{Assets, SwitchStory}; + +pub struct Example { + story: Entity, +} + +impl Example { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let story = SwitchStory::view(window, cx); + + Self { story } + } + + fn view(window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| Self::new(window, cx)) + } +} + +impl Render for Example { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().p_4().size_full().child(self.story.clone()) + } +} + +fn main() { + let app = Application::new().with_assets(Assets); + + app.run(move |cx| { + story::init(cx); + cx.activate(true); + + story::create_new_window("Switch Example", Example::view, cx); + }); +} diff --git a/crates/story/src/switch_story.rs b/crates/story/src/switch_story.rs index 2842676a..2ef91e01 100644 --- a/crates/story/src/switch_story.rs +++ b/crates/story/src/switch_story.rs @@ -9,6 +9,7 @@ use gpui_component::{ label::Label, radio::{Radio, RadioGroup}, switch::Switch, + text::TextView, v_flex, ActiveTheme, Disableable as _, Side, Sizable, StyledExt, }; @@ -203,12 +204,18 @@ impl Render for SwitchStory { v.check3 = !v.check3; })), ) - .child(div().w(px(300.)).child( - Checkbox::new("longlong-checkbox").label( - "The long long label text, \ - it should ellipsis when the text is too long.", + .child( + div().w(px(300.)).child( + Checkbox::new("longlong-checkbox").label( + TextView::markdown( + "longlong-checkbox", + "The **long long label** text, \ + it should ellipsis when the text is too long.", + ) + .inline(), + ), ), - )), + ), ), ) .child( diff --git a/crates/ui/src/checkbox.rs b/crates/ui/src/checkbox.rs index b4c169b8..8ac120b4 100644 --- a/crates/ui/src/checkbox.rs +++ b/crates/ui/src/checkbox.rs @@ -1,15 +1,14 @@ -use crate::{h_flex, v_flex, ActiveTheme, Disableable, IconName, Selectable}; +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, SharedString, StatefulInteractiveElement as _, - Styled as _, Window, + IntoElement, ParentElement, RenderOnce, StatefulInteractiveElement as _, Styled as _, Window, }; /// A Checkbox element. #[derive(IntoElement)] pub struct Checkbox { id: ElementId, - label: Option, + label: Option, checked: bool, disabled: bool, on_click: Option>, @@ -26,7 +25,7 @@ impl Checkbox { } } - pub fn label(mut self, label: impl Into) -> Self { + pub fn label(mut self, label: impl Into) -> Self { self.label = Some(label.into()); self } diff --git a/crates/ui/src/radio.rs b/crates/ui/src/radio.rs index 83012cdc..596b123d 100644 --- a/crates/ui/src/radio.rs +++ b/crates/ui/src/radio.rs @@ -1,6 +1,6 @@ use std::rc::Rc; -use crate::{h_flex, v_flex, ActiveTheme, AxisExt, IconName}; +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, @@ -13,7 +13,7 @@ use gpui::{ #[derive(IntoElement)] pub struct Radio { id: ElementId, - label: Option, + label: Option, checked: bool, disabled: bool, on_click: Option>, @@ -30,7 +30,7 @@ impl Radio { } } - pub fn label(mut self, label: impl Into) -> Self { + pub fn label(mut self, label: impl Into) -> Self { self.label = Some(label.into()); self } @@ -114,24 +114,6 @@ impl RenderOnce for Radio { } } -impl From<&'static str> for Radio { - fn from(label: &'static str) -> Self { - Self::new(label).label(label) - } -} - -impl From for Radio { - fn from(label: SharedString) -> Self { - Self::new(label.clone()).label(label) - } -} - -impl From for Radio { - fn from(label: String) -> Self { - Self::new(SharedString::from(label.clone())).label(SharedString::from(label)) - } -} - /// A Radio group element. #[derive(IntoElement)] pub struct RadioGroup { @@ -200,6 +182,24 @@ impl RadioGroup { } } +impl From<&'static str> for Radio { + fn from(label: &'static str) -> Self { + Self::new(label).label(label) + } +} + +impl From for Radio { + fn from(label: SharedString) -> Self { + Self::new(label.clone()).label(label) + } +} + +impl From for Radio { + fn from(label: String) -> Self { + Self::new(SharedString::from(label.clone())).label(SharedString::from(label)) + } +} + impl RenderOnce for RadioGroup { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { let on_change = self.on_change; diff --git a/crates/ui/src/switch.rs b/crates/ui/src/switch.rs index 9acbb1f3..4ea8f3f1 100644 --- a/crates/ui/src/switch.rs +++ b/crates/ui/src/switch.rs @@ -1,8 +1,8 @@ -use crate::{h_flex, ActiveTheme, Disableable, Side, Sizable, Size}; +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 _, - SharedString, Styled as _, Window, + Styled as _, Window, }; use std::{cell::RefCell, rc::Rc, time::Duration}; @@ -10,7 +10,7 @@ pub struct Switch { id: ElementId, checked: bool, disabled: bool, - label: Option, + label: Option, label_side: Side, on_click: Option>, size: Size, @@ -35,7 +35,7 @@ impl Switch { self } - pub fn label(mut self, label: impl Into) -> Self { + pub fn label(mut self, label: impl Into) -> Self { self.label = Some(label.into()); self } @@ -193,7 +193,7 @@ impl Element for Switch { ), ), ) - .when_some(self.label.clone(), |this, label| { + .when_some(self.label.take(), |this, label| { this.child(div().child(label).map(|this| match self.size { Size::XSmall | Size::Small => this.text_sm(), _ => this.text_base(), diff --git a/crates/ui/src/text/element.rs b/crates/ui/src/text/element.rs index ed6520aa..6842f58d 100644 --- a/crates/ui/src/text/element.rs +++ b/crates/ui/src/text/element.rs @@ -9,7 +9,7 @@ use gpui::{ use crate::{h_flex, v_flex, ActiveTheme as _, Icon, IconName}; -use super::utils::list_item_prefix; +use super::{utils::list_item_prefix, TextViewStyle}; #[allow(unused)] #[derive(Debug, Default, Clone, PartialEq)] @@ -172,8 +172,10 @@ impl Paragraph { } } +/// Ref: +/// https://ui.shadcn.com/docs/components/typography #[allow(unused)] -#[derive(Debug, Clone, IntoElement, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum Node { Root { children: Vec, @@ -317,7 +319,7 @@ impl RenderOnce for Paragraph { } #[derive(Default)] -struct ListState { +pub(crate) struct ListState { todo: bool, ordered: bool, depth: usize, @@ -328,6 +330,7 @@ impl Node { item: Node, ix: usize, state: ListState, + text_view_style: &TextViewStyle, window: &mut Window, cx: &mut App, ) -> impl IntoElement { @@ -373,24 +376,26 @@ impl Node { }), ) }) - .child(child.render_node( + .child(child.render( Some(ListState { depth: state.depth + 1, ordered: state.ordered, todo: checked.is_some(), }), + text_view_style, window, cx, )), ); } Node::List { .. } => { - items.push(div().ml(rems(1.)).child(child.render_node( + items.push(div().ml(rems(1.)).child(child.render( Some(ListState { depth: state.depth + 1, ordered: state.ordered, todo: checked.is_some(), }), + text_view_style, window, cx, ))) @@ -477,17 +482,28 @@ impl Node { } } - fn render_node( + pub(crate) fn render( self, list_state: Option, + text_view_style: &TextViewStyle, window: &mut Window, cx: &mut App, ) -> impl IntoElement { let in_list = list_state.is_some(); - let mb = if in_list { rems(0.0) } else { rems(1.) }; + let mb = if in_list { + rems(0.) + } else { + text_view_style.paragraph_gap + }; match self { - Node::Root { children } => div().children(children).into_any_element(), + Node::Root { children } => div() + .children( + children + .into_iter() + .map(|c| c.render(None, text_view_style, window, cx)), + ) + .into_any_element(), Node::Paragraph(paragraph) => div().mb(mb).child(paragraph).into_any_element(), Node::Heading { level, children } => { let (text_size, font_weight) = match level { @@ -535,6 +551,7 @@ impl Node { todo: list_state.todo, depth: list_state.depth, }, + text_view_style, window, cx, )); @@ -573,11 +590,3 @@ impl Node { } } } - -/// Ref: -/// https://ui.shadcn.com/docs/components/typography -impl RenderOnce for Node { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - self.render_node(None, window, cx) - } -} diff --git a/crates/ui/src/text/html.rs b/crates/ui/src/text/html.rs index 0105f6fa..807a8370 100644 --- a/crates/ui/src/text/html.rs +++ b/crates/ui/src/text/html.rs @@ -19,6 +19,7 @@ use crate::v_flex; use super::element::{ self, ImageNode, InlineTextStyle, LinkMark, Paragraph, Table, TableRow, TextNode, }; +use super::TextViewStyle; const BLOCK_ELEMENTS: [&str; 33] = [ "html", @@ -78,9 +79,11 @@ pub(super) fn parse_html(source: &str) -> Result { Ok(node) } +#[derive(Clone)] pub(super) struct HtmlElement { id: ElementId, text: SharedString, + style: TextViewStyle, } impl HtmlElement { @@ -88,6 +91,7 @@ impl HtmlElement { Self { id: id.into(), text: raw.into(), + style: TextViewStyle::default(), } } @@ -96,6 +100,12 @@ impl HtmlElement { self.text = raw.into(); self } + + /// Set TextViewStyle. + pub(crate) fn style(mut self, style: impl Into) -> Self { + self.style = style.into(); + self + } } #[derive(Default)] @@ -150,7 +160,7 @@ impl Element for HtmlElement { let mut el = div() .map(|this| match root { - Ok(node) => this.child(node), + Ok(node) => this.child(node.render(None, &self.style, window, cx)), Err(err) => this.child( v_flex() .gap_1() diff --git a/crates/ui/src/text/markdown.rs b/crates/ui/src/text/markdown.rs index 6dbc56f4..8ad52b98 100644 --- a/crates/ui/src/text/markdown.rs +++ b/crates/ui/src/text/markdown.rs @@ -12,6 +12,7 @@ use crate::v_flex; use super::{ element::{self, ImageNode, InlineTextStyle, LinkMark, Paragraph, Span, Table, TableRow}, html::parse_html, + TextViewStyle, }; /// Markdown GFM renderer @@ -30,9 +31,11 @@ use super::{ /// - As a markdown editor. /// - Add custom markdown syntax. /// - Complex styles cumstomization. +#[derive(Clone)] pub(super) struct MarkdownElement { id: ElementId, text: SharedString, + style: TextViewStyle, } impl MarkdownElement { @@ -40,6 +43,7 @@ impl MarkdownElement { Self { id: id.into(), text: raw.into(), + style: TextViewStyle::default(), } } @@ -48,6 +52,12 @@ impl MarkdownElement { self.text = raw.into(); self } + + /// Set TextViewStyle. + pub(crate) fn style(mut self, style: impl Into) -> Self { + self.style = style.into(); + self + } } #[derive(Default)] @@ -106,7 +116,7 @@ impl Element for MarkdownElement { let mut el = div() .map(|this| match root { - Ok(node) => this.child(node), + Ok(node) => this.child(node.render(None, &self.style, window, cx)), Err(err) => this.child( v_flex() .gap_1() diff --git a/crates/ui/src/text/mod.rs b/crates/ui/src/text/mod.rs index a2b21457..40de37bb 100644 --- a/crates/ui/src/text/mod.rs +++ b/crates/ui/src/text/mod.rs @@ -1,4 +1,4 @@ -use gpui::{App, ElementId, IntoElement, RenderOnce, SharedString, Window}; +use gpui::{rems, App, ElementId, IntoElement, Rems, RenderOnce, SharedString, Window}; use html::HtmlElement; use markdown::MarkdownElement; @@ -7,9 +7,79 @@ mod html; mod markdown; 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)] +#[derive(IntoElement, Clone)] pub enum TextView { Markdown(MarkdownElement), Html(HtmlElement), @@ -33,6 +103,19 @@ impl TextView { 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 { diff --git a/crates/ui/src/tooltip.rs b/crates/ui/src/tooltip.rs index 3f4f2f62..70edc712 100644 --- a/crates/ui/src/tooltip.rs +++ b/crates/ui/src/tooltip.rs @@ -1,17 +1,17 @@ use gpui::{ div, prelude::FluentBuilder, px, AnyElement, AnyView, App, AppContext, Context, IntoElement, - ParentElement, Render, SharedString, Styled, Window, + ParentElement, Render, Styled, Window, }; -use crate::ActiveTheme; +use crate::{text::Text, ActiveTheme}; pub struct Tooltip { - text: SharedString, + text: Text, element_builder: Option AnyElement>>, } impl Tooltip { - pub fn new(text: impl Into, _: &mut Window, cx: &mut App) -> AnyView { + pub fn new(text: impl Into, _: &mut Window, cx: &mut App) -> AnyView { cx.new(|_| Self { text: text.into(), element_builder: None,