diff options
Diffstat (limited to '')
| -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 | 
3 files changed, 356 insertions, 4 deletions
| 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) +    } +} | 
