diff options
| -rw-r--r-- | core/src/renderer/null.rs | 5 | ||||
| -rw-r--r-- | core/src/text.rs | 148 | ||||
| -rw-r--r-- | core/src/text/paragraph.rs | 5 | ||||
| -rw-r--r-- | core/src/widget/text.rs | 91 | ||||
| -rw-r--r-- | examples/markdown/Cargo.toml | 12 | ||||
| -rw-r--r-- | examples/markdown/src/main.rs | 172 | ||||
| -rw-r--r-- | graphics/src/text.rs | 13 | ||||
| -rw-r--r-- | graphics/src/text/paragraph.rs | 80 | ||||
| -rw-r--r-- | widget/src/helpers.rs | 39 | ||||
| -rw-r--r-- | widget/src/text.rs | 4 | ||||
| -rw-r--r-- | widget/src/text/rich.rs | 317 | 
11 files changed, 787 insertions, 99 deletions
| diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 560b5b43..f9d1a5b0 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -77,6 +77,11 @@ impl text::Paragraph for () {      fn with_text(_text: Text<&str>) -> Self {} +    fn with_spans( +        _text: Text<&[text::Span<'_, Self::Font>], Self::Font>, +    ) -> Self { +    } +      fn resize(&mut self, _new_bounds: Size) {}      fn compare(&self, _text: Text<()>) -> text::Difference { diff --git a/core/src/text.rs b/core/src/text.rs index e437a635..d73eb94a 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -10,6 +10,7 @@ pub use paragraph::Paragraph;  use crate::alignment;  use crate::{Color, Pixels, Point, Rectangle, Size}; +use std::borrow::Cow;  use std::hash::{Hash, Hasher};  /// A paragraph. @@ -220,3 +221,150 @@ pub trait Renderer: crate::Renderer {          clip_bounds: Rectangle,      );  } + +/// A span of text. +#[derive(Debug, Clone, PartialEq)] +pub struct Span<'a, Font = crate::Font> { +    /// The [`Fragment`] of text. +    pub text: Fragment<'a>, +    /// The size of the [`Span`] in [`Pixels`]. +    pub size: Option<Pixels>, +    /// The [`LineHeight`] of the [`Span`]. +    pub line_height: Option<LineHeight>, +    /// The font of the [`Span`]. +    pub font: Option<Font>, +    /// The [`Color`] of the [`Span`]. +    pub color: Option<Color>, +} + +impl<'a, Font> Span<'a, Font> { +    /// Creates a new [`Span`] of text with the given text fragment. +    pub fn new(fragment: impl IntoFragment<'a>) -> Self { +        Self { +            text: fragment.into_fragment(), +            size: None, +            line_height: None, +            font: None, +            color: None, +        } +    } + +    /// Sets the size of the [`Span`]. +    pub fn size(mut self, size: impl Into<Pixels>) -> Self { +        self.size = Some(size.into()); +        self +    } + +    /// Sets the [`LineHeight`] of the [`Span`]. +    pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self { +        self.line_height = Some(line_height.into()); +        self +    } + +    /// Sets the font of the [`Span`]. +    pub fn font(mut self, font: impl Into<Font>) -> Self { +        self.font = Some(font.into()); +        self +    } + +    /// Sets the [`Color`] of the [`Span`]. +    pub fn color(mut self, color: impl Into<Color>) -> Self { +        self.color = Some(color.into()); +        self +    } + +    /// Turns the [`Span`] into a static one. +    pub fn to_static(self) -> Span<'static, Font> { +        Span { +            text: Cow::Owned(self.text.into_owned()), +            size: self.size, +            line_height: self.line_height, +            font: self.font, +            color: self.color, +        } +    } +} + +impl<'a, Font> From<&'a str> for Span<'a, Font> { +    fn from(value: &'a str) -> Self { +        Span::new(value) +    } +} + +/// A fragment of [`Text`]. +/// +/// This is just an alias to a string that may be either +/// borrowed or owned. +pub type Fragment<'a> = Cow<'a, str>; + +/// A trait for converting a value to some text [`Fragment`]. +pub trait IntoFragment<'a> { +    /// Converts the value to some text [`Fragment`]. +    fn into_fragment(self) -> Fragment<'a>; +} + +impl<'a> IntoFragment<'a> for Fragment<'a> { +    fn into_fragment(self) -> Fragment<'a> { +        self +    } +} + +impl<'a, 'b> IntoFragment<'a> for &'a Fragment<'b> { +    fn into_fragment(self) -> Fragment<'a> { +        Fragment::Borrowed(self) +    } +} + +impl<'a> IntoFragment<'a> for &'a str { +    fn into_fragment(self) -> Fragment<'a> { +        Fragment::Borrowed(self) +    } +} + +impl<'a> IntoFragment<'a> for &'a String { +    fn into_fragment(self) -> Fragment<'a> { +        Fragment::Borrowed(self.as_str()) +    } +} + +impl<'a> IntoFragment<'a> for String { +    fn into_fragment(self) -> Fragment<'a> { +        Fragment::Owned(self) +    } +} + +macro_rules! into_fragment { +    ($type:ty) => { +        impl<'a> IntoFragment<'a> for $type { +            fn into_fragment(self) -> Fragment<'a> { +                Fragment::Owned(self.to_string()) +            } +        } + +        impl<'a> IntoFragment<'a> for &$type { +            fn into_fragment(self) -> Fragment<'a> { +                Fragment::Owned(self.to_string()) +            } +        } +    }; +} + +into_fragment!(char); +into_fragment!(bool); + +into_fragment!(u8); +into_fragment!(u16); +into_fragment!(u32); +into_fragment!(u64); +into_fragment!(u128); +into_fragment!(usize); + +into_fragment!(i8); +into_fragment!(i16); +into_fragment!(i32); +into_fragment!(i64); +into_fragment!(i128); +into_fragment!(isize); + +into_fragment!(f32); +into_fragment!(f64); diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs index 66cadb5c..4ee83798 100644 --- a/core/src/text/paragraph.rs +++ b/core/src/text/paragraph.rs @@ -1,6 +1,6 @@  //! Draw paragraphs.  use crate::alignment; -use crate::text::{Difference, Hit, Text}; +use crate::text::{Difference, Hit, Span, Text};  use crate::{Point, Size};  /// A text paragraph. @@ -11,6 +11,9 @@ pub trait Paragraph: Sized + Default {      /// Creates a new [`Paragraph`] laid out with the given [`Text`].      fn with_text(text: Text<&str, Self::Font>) -> Self; +    /// Creates a new [`Paragraph`] laid out with the given [`Text`]. +    fn with_spans(text: Text<&[Span<'_, Self::Font>], Self::Font>) -> Self; +      /// Lays out the [`Paragraph`] with some new boundaries.      fn resize(&mut self, new_bounds: Size); diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 2aeb0765..d0ecd27b 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -11,8 +11,6 @@ use crate::{      Widget,  }; -use std::borrow::Cow; -  pub use text::{LineHeight, Shaping};  /// A paragraph of text. @@ -22,7 +20,7 @@ where      Theme: Catalog,      Renderer: text::Renderer,  { -    fragment: Fragment<'a>, +    fragment: text::Fragment<'a>,      size: Option<Pixels>,      line_height: LineHeight,      width: Length, @@ -40,7 +38,7 @@ where      Renderer: text::Renderer,  {      /// Create a new fragment of [`Text`] with the given contents. -    pub fn new(fragment: impl IntoFragment<'a>) -> Self { +    pub fn new(fragment: impl text::IntoFragment<'a>) -> Self {          Text {              fragment: fragment.into_fragment(),              size: None, @@ -216,7 +214,7 @@ where          let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();          let style = theme.style(&self.class); -        draw(renderer, defaults, layout, state, style, viewport); +        draw(renderer, defaults, layout, state.0.raw(), style, viewport);      }  } @@ -275,13 +273,12 @@ pub fn draw<Renderer>(      renderer: &mut Renderer,      style: &renderer::Style,      layout: Layout<'_>, -    state: &State<Renderer::Paragraph>, +    paragraph: &Renderer::Paragraph,      appearance: Style,      viewport: &Rectangle,  ) where      Renderer: text::Renderer,  { -    let State(ref paragraph) = state;      let bounds = layout.bounds();      let x = match paragraph.horizontal_alignment() { @@ -297,7 +294,7 @@ pub fn draw<Renderer>(      };      renderer.fill_paragraph( -        paragraph.raw(), +        paragraph,          Point::new(x, y),          appearance.color.unwrap_or(style.text_color),          *viewport, @@ -415,81 +412,3 @@ pub fn danger(theme: &Theme) -> Style {          color: Some(theme.palette().danger),      }  } - -/// A fragment of [`Text`]. -/// -/// This is just an alias to a string that may be either -/// borrowed or owned. -pub type Fragment<'a> = Cow<'a, str>; - -/// A trait for converting a value to some text [`Fragment`]. -pub trait IntoFragment<'a> { -    /// Converts the value to some text [`Fragment`]. -    fn into_fragment(self) -> Fragment<'a>; -} - -impl<'a> IntoFragment<'a> for Fragment<'a> { -    fn into_fragment(self) -> Fragment<'a> { -        self -    } -} - -impl<'a, 'b> IntoFragment<'a> for &'a Fragment<'b> { -    fn into_fragment(self) -> Fragment<'a> { -        Fragment::Borrowed(self) -    } -} - -impl<'a> IntoFragment<'a> for &'a str { -    fn into_fragment(self) -> Fragment<'a> { -        Fragment::Borrowed(self) -    } -} - -impl<'a> IntoFragment<'a> for &'a String { -    fn into_fragment(self) -> Fragment<'a> { -        Fragment::Borrowed(self.as_str()) -    } -} - -impl<'a> IntoFragment<'a> for String { -    fn into_fragment(self) -> Fragment<'a> { -        Fragment::Owned(self) -    } -} - -macro_rules! into_fragment { -    ($type:ty) => { -        impl<'a> IntoFragment<'a> for $type { -            fn into_fragment(self) -> Fragment<'a> { -                Fragment::Owned(self.to_string()) -            } -        } - -        impl<'a> IntoFragment<'a> for &$type { -            fn into_fragment(self) -> Fragment<'a> { -                Fragment::Owned(self.to_string()) -            } -        } -    }; -} - -into_fragment!(char); -into_fragment!(bool); - -into_fragment!(u8); -into_fragment!(u16); -into_fragment!(u32); -into_fragment!(u64); -into_fragment!(u128); -into_fragment!(usize); - -into_fragment!(i8); -into_fragment!(i16); -into_fragment!(i32); -into_fragment!(i64); -into_fragment!(i128); -into_fragment!(isize); - -into_fragment!(f32); -into_fragment!(f64); diff --git a/examples/markdown/Cargo.toml b/examples/markdown/Cargo.toml new file mode 100644 index 00000000..f9bf4042 --- /dev/null +++ b/examples/markdown/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "markdown" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced.workspace = true +iced.features = ["debug"] + +pulldown-cmark = "0.11" diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs new file mode 100644 index 00000000..43adaf72 --- /dev/null +++ b/examples/markdown/src/main.rs @@ -0,0 +1,172 @@ +use iced::font; +use iced::padding; +use iced::widget::{ +    self, column, container, rich_text, row, span, text_editor, +}; +use iced::{Element, Fill, Font, Task, Theme}; + +pub fn main() -> iced::Result { +    iced::application("Markdown - Iced", Markdown::update, Markdown::view) +        .theme(Markdown::theme) +        .run_with(Markdown::new) +} + +struct Markdown { +    content: text_editor::Content, +} + +#[derive(Debug, Clone)] +enum Message { +    Edit(text_editor::Action), +} + +impl Markdown { +    fn new() -> (Self, Task<Message>) { +        ( +            Self { +                content: text_editor::Content::with_text( +                    "# Markdown Editor\nType your Markdown here...", +                ), +            }, +            widget::focus_next(), +        ) +    } +    fn update(&mut self, message: Message) { +        match message { +            Message::Edit(action) => { +                self.content.perform(action); +            } +        } +    } + +    fn view(&self) -> Element<Message> { +        let editor = text_editor(&self.content) +            .on_action(Message::Edit) +            .height(Fill) +            .padding(10) +            .font(Font::MONOSPACE); + +        let preview = { +            let markdown = self.content.text(); +            let parser = pulldown_cmark::Parser::new(&markdown); + +            let mut strong = false; +            let mut emphasis = false; +            let mut heading = None; +            let mut spans = Vec::new(); + +            let items = parser.filter_map(|event| match event { +                pulldown_cmark::Event::Start(tag) => match tag { +                    pulldown_cmark::Tag::Strong => { +                        strong = true; +                        None +                    } +                    pulldown_cmark::Tag::Emphasis => { +                        emphasis = true; +                        None +                    } +                    pulldown_cmark::Tag::Heading { level, .. } => { +                        heading = Some(level); +                        None +                    } +                    _ => None, +                }, +                pulldown_cmark::Event::End(tag) => match tag { +                    pulldown_cmark::TagEnd::Emphasis => { +                        emphasis = false; +                        None +                    } +                    pulldown_cmark::TagEnd::Strong => { +                        strong = false; +                        None +                    } +                    pulldown_cmark::TagEnd::Heading(_) => { +                        heading = None; +                        Some( +                            container(rich_text(spans.drain(..))) +                                .padding(padding::bottom(5)) +                                .into(), +                        ) +                    } +                    pulldown_cmark::TagEnd::Paragraph => Some( +                        container(rich_text(spans.drain(..))) +                            .padding(padding::bottom(15)) +                            .into(), +                    ), +                    pulldown_cmark::TagEnd::CodeBlock => Some( +                        container( +                            container( +                                rich_text(spans.drain(..)) +                                    .font(Font::MONOSPACE), +                            ) +                            .width(Fill) +                            .padding(10) +                            .style(container::rounded_box), +                        ) +                        .padding(padding::bottom(15)) +                        .into(), +                    ), +                    _ => None, +                }, +                pulldown_cmark::Event::Text(text) => { +                    let span = span(text.into_string()); + +                    let span = match heading { +                        None => span, +                        Some(heading) => span.size(match heading { +                            pulldown_cmark::HeadingLevel::H1 => 32, +                            pulldown_cmark::HeadingLevel::H2 => 28, +                            pulldown_cmark::HeadingLevel::H3 => 24, +                            pulldown_cmark::HeadingLevel::H4 => 20, +                            pulldown_cmark::HeadingLevel::H5 => 16, +                            pulldown_cmark::HeadingLevel::H6 => 16, +                        }), +                    }; + +                    let span = if strong || emphasis { +                        span.font(Font { +                            weight: if strong { +                                font::Weight::Bold +                            } else { +                                font::Weight::Normal +                            }, +                            style: if emphasis { +                                font::Style::Italic +                            } else { +                                font::Style::Normal +                            }, +                            ..Font::default() +                        }) +                    } else { +                        span +                    }; + +                    spans.push(span); + +                    None +                } +                pulldown_cmark::Event::Code(code) => { +                    spans.push(span(code.into_string()).font(Font::MONOSPACE)); +                    None +                } +                pulldown_cmark::Event::SoftBreak => { +                    spans.push(span(" ")); +                    None +                } +                pulldown_cmark::Event::HardBreak => { +                    spans.push(span("\n")); +                    None +                } +                _ => None, +            }); + +            column(items).width(Fill) +        }; + +        row![editor, preview].spacing(10).padding(10).into() +    } + +    fn theme(&self) -> Theme { +        Theme::TokyoNight +    } +} diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 30269e69..23ec14d4 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -232,13 +232,14 @@ impl PartialEq for Raw {  /// Measures the dimensions of the given [`cosmic_text::Buffer`].  pub fn measure(buffer: &cosmic_text::Buffer) -> Size { -    let (width, total_lines) = buffer -        .layout_runs() -        .fold((0.0, 0usize), |(width, total_lines), run| { -            (run.line_w.max(width), total_lines + 1) -        }); +    let (width, height) = +        buffer +            .layout_runs() +            .fold((0.0, 0.0), |(width, height), run| { +                (run.line_w.max(width), height + run.line_height) +            }); -    Size::new(width, total_lines as f32 * buffer.metrics().line_height) +    Size::new(width, height)  }  /// Returns the attributes of the given [`Font`]. diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index ea59c0af..37fa97f2 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -1,7 +1,7 @@  //! Draw paragraphs.  use crate::core;  use crate::core::alignment; -use crate::core::text::{Hit, Shaping, Text}; +use crate::core::text::{Hit, Shaping, Span, Text};  use crate::core::{Font, Point, Size};  use crate::text; @@ -60,7 +60,7 @@ impl core::text::Paragraph for Paragraph {      type Font = Font;      fn with_text(text: Text<&str>) -> Self { -        log::trace!("Allocating paragraph: {}", text.content); +        log::trace!("Allocating plain paragraph: {}", text.content);          let mut font_system =              text::font_system().write().expect("Write font system"); @@ -100,6 +100,82 @@ impl core::text::Paragraph for Paragraph {          }))      } +    fn with_spans(text: Text<&[Span<'_>]>) -> Self { +        log::trace!("Allocating rich paragraph: {:?}", text.content); + +        let mut font_system = +            text::font_system().write().expect("Write font system"); + +        let mut buffer = cosmic_text::Buffer::new( +            font_system.raw(), +            cosmic_text::Metrics::new( +                text.size.into(), +                text.line_height.to_absolute(text.size).into(), +            ), +        ); + +        buffer.set_size( +            font_system.raw(), +            Some(text.bounds.width), +            Some(text.bounds.height), +        ); + +        buffer.set_rich_text( +            font_system.raw(), +            text.content.iter().map(|span| { +                let attrs = cosmic_text::Attrs::new(); + +                let attrs = if let Some(font) = span.font { +                    attrs +                        .family(text::to_family(font.family)) +                        .weight(text::to_weight(font.weight)) +                        .stretch(text::to_stretch(font.stretch)) +                        .style(text::to_style(font.style)) +                } else { +                    text::to_attributes(text.font) +                }; + +                let attrs = match (span.size, span.line_height) { +                    (None, None) => attrs, +                    _ => { +                        let size = span.size.unwrap_or(text.size); + +                        attrs.metrics(cosmic_text::Metrics::new( +                            size.into(), +                            span.line_height +                                .unwrap_or(text.line_height) +                                .to_absolute(size) +                                .into(), +                        )) +                    } +                }; + +                let attrs = if let Some(color) = span.color { +                    attrs.color(text::to_color(color)) +                } else { +                    attrs +                }; + +                (span.text.as_ref(), attrs) +            }), +            text::to_attributes(text.font), +            text::to_shaping(text.shaping), +        ); + +        let min_bounds = text::measure(&buffer); + +        Self(Arc::new(Internal { +            buffer, +            font: text.font, +            horizontal_alignment: text.horizontal_alignment, +            vertical_alignment: text.vertical_alignment, +            shaping: text.shaping, +            bounds: text.bounds, +            min_bounds, +            version: font_system.version(), +        })) +    } +      fn resize(&mut self, new_bounds: Size) {          let paragraph = Arc::make_mut(&mut self.0); diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 1f282f54..66b37ccb 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -112,6 +112,19 @@ macro_rules! text {      };  } +/// Creates some [`Rich`] text with the given spans. +/// +/// [`Rich`]: text::Rich +#[macro_export] +macro_rules! rich_text { +    () => ( +        $crate::Column::new() +    ); +    ($($x:expr),+ $(,)?) => ( +        $crate::text::Rich::with_spans([$($crate::text::Span::from($x)),+]) +    ); +} +  /// Creates a new [`Container`] with the provided content.  ///  /// [`Container`]: crate::Container @@ -646,8 +659,6 @@ where  }  /// Creates a new [`Text`] widget with the provided content. -/// -/// [`Text`]: core::widget::Text  pub fn text<'a, Theme, Renderer>(      text: impl text::IntoFragment<'a>,  ) -> Text<'a, Theme, Renderer> @@ -659,8 +670,6 @@ where  }  /// Creates a new [`Text`] widget that displays the provided value. -/// -/// [`Text`]: core::widget::Text  pub fn value<'a, Theme, Renderer>(      value: impl ToString,  ) -> Text<'a, Theme, Renderer> @@ -671,6 +680,28 @@ where      Text::new(value.to_string())  } +/// Creates a new [`Rich`] text widget with the provided spans. +/// +/// [`Rich`]: text::Rich +pub fn rich_text<'a, Theme, Renderer>( +    spans: impl IntoIterator<Item = text::Span<'a, Renderer::Font>>, +) -> text::Rich<'a, Theme, Renderer> +where +    Theme: text::Catalog + 'a, +    Renderer: core::text::Renderer, +{ +    text::Rich::with_spans(spans) +} + +/// Creates a new [`Span`] of text with the provided content. +/// +/// [`Span`]: text::Span +pub fn span<'a, Font>( +    text: impl text::IntoFragment<'a>, +) -> text::Span<'a, Font> { +    text::Span::new(text) +} +  /// Creates a new [`Checkbox`].  ///  /// [`Checkbox`]: crate::Checkbox diff --git a/widget/src/text.rs b/widget/src/text.rs index 0d689295..c32f9be1 100644 --- a/widget/src/text.rs +++ b/widget/src/text.rs @@ -1,5 +1,9 @@  //! Draw and interact with text. +mod rich; + +pub use crate::core::text::{Fragment, IntoFragment, Span};  pub use crate::core::widget::text::*; +pub use rich::Rich;  /// A paragraph.  pub type Text<'a, Theme = crate::Theme, Renderer = crate::Renderer> = diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs new file mode 100644 index 00000000..dc784310 --- /dev/null +++ b/widget/src/text/rich.rs @@ -0,0 +1,317 @@ +use crate::core::alignment; +use crate::core::layout::{self, Layout}; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::text::{Paragraph, Span}; +use crate::core::widget::text::{ +    self, Catalog, LineHeight, Shaping, Style, StyleFn, +}; +use crate::core::widget::tree::{self, Tree}; +use crate::core::{ +    self, Color, Element, Length, Pixels, Rectangle, Size, Widget, +}; + +/// A bunch of [`Rich`] text. +#[derive(Debug)] +pub struct Rich<'a, Theme = crate::Theme, Renderer = crate::Renderer> +where +    Theme: Catalog, +    Renderer: core::text::Renderer, +{ +    spans: Vec<Span<'a, Renderer::Font>>, +    size: Option<Pixels>, +    line_height: LineHeight, +    width: Length, +    height: Length, +    font: Option<Renderer::Font>, +    align_x: alignment::Horizontal, +    align_y: alignment::Vertical, +    class: Theme::Class<'a>, +} + +impl<'a, Theme, Renderer> Rich<'a, Theme, Renderer> +where +    Theme: Catalog, +    Renderer: core::text::Renderer, +{ +    /// Creates a new empty [`Rich`] text. +    pub fn new() -> Self { +        Self { +            spans: Vec::new(), +            size: None, +            line_height: LineHeight::default(), +            width: Length::Shrink, +            height: Length::Shrink, +            font: None, +            align_x: alignment::Horizontal::Left, +            align_y: alignment::Vertical::Top, +            class: Theme::default(), +        } +    } + +    /// Creates a new [`Rich`] text with the given text spans. +    pub fn with_spans( +        spans: impl IntoIterator<Item = Span<'a, Renderer::Font>>, +    ) -> Self { +        Self { +            spans: spans.into_iter().collect(), +            ..Self::new() +        } +    } + +    /// Sets the default size of the [`Rich`] text. +    pub fn size(mut self, size: impl Into<Pixels>) -> Self { +        self.size = Some(size.into()); +        self +    } + +    /// Sets the defualt [`LineHeight`] of the [`Rich`] text. +    pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self { +        self.line_height = line_height.into(); +        self +    } + +    /// Sets the default font of the [`Rich`] text. +    pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { +        self.font = Some(font.into()); +        self +    } + +    /// Sets the width of the [`Rich`] text boundaries. +    pub fn width(mut self, width: impl Into<Length>) -> Self { +        self.width = width.into(); +        self +    } + +    /// Sets the height of the [`Rich`] text boundaries. +    pub fn height(mut self, height: impl Into<Length>) -> Self { +        self.height = height.into(); +        self +    } + +    /// Centers the [`Rich`] text, both horizontally and vertically. +    pub fn center(self) -> Self { +        self.align_x(alignment::Horizontal::Center) +            .align_y(alignment::Vertical::Center) +    } + +    /// Sets the [`alignment::Horizontal`] of the [`Rich`] text. +    pub fn align_x( +        mut self, +        alignment: impl Into<alignment::Horizontal>, +    ) -> Self { +        self.align_x = alignment.into(); +        self +    } + +    /// Sets the [`alignment::Vertical`] of the [`Rich`] text. +    pub fn align_y( +        mut self, +        alignment: impl Into<alignment::Vertical>, +    ) -> Self { +        self.align_y = alignment.into(); +        self +    } + +    /// Sets the default style of the [`Rich`] text. +    #[must_use] +    pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self +    where +        Theme::Class<'a>: From<StyleFn<'a, Theme>>, +    { +        self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); +        self +    } + +    /// Sets the default [`Color`] of the [`Rich`] text. +    pub fn color(self, color: impl Into<Color>) -> Self +    where +        Theme::Class<'a>: From<StyleFn<'a, Theme>>, +    { +        self.color_maybe(Some(color)) +    } + +    /// Sets the default [`Color`] of the [`Rich`] text, if `Some`. +    pub fn color_maybe(self, color: Option<impl Into<Color>>) -> Self +    where +        Theme::Class<'a>: From<StyleFn<'a, Theme>>, +    { +        let color = color.map(Into::into); + +        self.style(move |_theme| Style { color }) +    } + +    /// Sets the default style class of the [`Rich`] text. +    #[cfg(feature = "advanced")] +    #[must_use] +    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { +        self.class = class.into(); +        self +    } + +    /// Adds a new text [`Span`] to the [`Rich`] text. +    pub fn push(mut self, span: impl Into<Span<'a, Renderer::Font>>) -> Self { +        self.spans.push(span.into()); +        self +    } +} + +impl<'a, Theme, Renderer> Default for Rich<'a, Theme, Renderer> +where +    Theme: Catalog, +    Renderer: core::text::Renderer, +{ +    fn default() -> Self { +        Self::new() +    } +} + +struct State<P: Paragraph> { +    spans: Vec<Span<'static, P::Font>>, +    paragraph: P, +} + +impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> +    for Rich<'a, Theme, Renderer> +where +    Theme: Catalog, +    Renderer: core::text::Renderer, +{ +    fn tag(&self) -> tree::Tag { +        tree::Tag::of::<State<Renderer::Paragraph>>() +    } + +    fn state(&self) -> tree::State { +        tree::State::new(State { +            spans: Vec::new(), +            paragraph: Renderer::Paragraph::default(), +        }) +    } + +    fn size(&self) -> Size<Length> { +        Size { +            width: self.width, +            height: self.height, +        } +    } + +    fn layout( +        &self, +        tree: &mut Tree, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        layout( +            tree.state.downcast_mut::<State<Renderer::Paragraph>>(), +            renderer, +            limits, +            self.width, +            self.height, +            self.spans.as_slice(), +            self.line_height, +            self.size, +            self.font, +            self.align_x, +            self.align_y, +        ) +    } + +    fn draw( +        &self, +        tree: &Tree, +        renderer: &mut Renderer, +        theme: &Theme, +        defaults: &renderer::Style, +        layout: Layout<'_>, +        _cursor_position: mouse::Cursor, +        viewport: &Rectangle, +    ) { +        let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); +        let style = theme.style(&self.class); + +        text::draw( +            renderer, +            defaults, +            layout, +            &state.paragraph, +            style, +            viewport, +        ); +    } +} + +fn layout<Renderer>( +    state: &mut State<Renderer::Paragraph>, +    renderer: &Renderer, +    limits: &layout::Limits, +    width: Length, +    height: Length, +    spans: &[Span<'_, Renderer::Font>], +    line_height: LineHeight, +    size: Option<Pixels>, +    font: Option<Renderer::Font>, +    horizontal_alignment: alignment::Horizontal, +    vertical_alignment: alignment::Vertical, +) -> layout::Node +where +    Renderer: core::text::Renderer, +{ +    layout::sized(limits, width, height, |limits| { +        let bounds = limits.max(); + +        let size = size.unwrap_or_else(|| renderer.default_size()); +        let font = font.unwrap_or_else(|| renderer.default_font()); + +        let text_with_spans = || core::Text { +            content: spans, +            bounds, +            size, +            line_height, +            font, +            horizontal_alignment, +            vertical_alignment, +            shaping: Shaping::Advanced, +        }; + +        if state.spans != spans { +            state.paragraph = +                Renderer::Paragraph::with_spans(text_with_spans()); +            state.spans = spans.iter().cloned().map(Span::to_static).collect(); +        } else { +            match state.paragraph.compare(core::Text { +                content: (), +                bounds, +                size, +                line_height, +                font, +                horizontal_alignment, +                vertical_alignment, +                shaping: Shaping::Advanced, +            }) { +                core::text::Difference::None => {} +                core::text::Difference::Bounds => { +                    state.paragraph.resize(bounds); +                } +                core::text::Difference::Shape => { +                    state.paragraph = +                        Renderer::Paragraph::with_spans(text_with_spans()); +                } +            } +        } + +        state.paragraph.min_bounds() +    }) +} + +impl<'a, Message, Theme, Renderer> From<Rich<'a, Theme, Renderer>> +    for Element<'a, Message, Theme, Renderer> +where +    Theme: Catalog + 'a, +    Renderer: core::text::Renderer + 'a, +{ +    fn from( +        text: Rich<'a, Theme, Renderer>, +    ) -> Element<'a, Message, Theme, Renderer> { +        Element::new(text) +    } +} | 
