diff options
author | 2024-07-17 22:04:11 +0200 | |
---|---|---|
committer | 2024-07-17 22:04:11 +0200 | |
commit | 910eb72a0620b34e5b3d7793bbd5ab7290e08dd6 (patch) | |
tree | 0a0940d56a9bc4147b7ef06fe9fb5099c761090a /widget/src/text/rich.rs | |
parent | ffb520fb3703ce4ece9fb6d5ee2c7aa0b846879f (diff) | |
download | iced-910eb72a0620b34e5b3d7793bbd5ab7290e08dd6.tar.gz iced-910eb72a0620b34e5b3d7793bbd5ab7290e08dd6.tar.bz2 iced-910eb72a0620b34e5b3d7793bbd5ab7290e08dd6.zip |
Implement `rich_text` widget and `markdown` example
Diffstat (limited to 'widget/src/text/rich.rs')
-rw-r--r-- | widget/src/text/rich.rs | 317 |
1 files changed, 317 insertions, 0 deletions
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) + } +} |