diff options
author | 2024-08-22 02:24:06 +0200 | |
---|---|---|
committer | 2024-08-22 02:24:06 +0200 | |
commit | 4c883f12b4761c7e0b273d9a2380552336f61d96 (patch) | |
tree | e2ef505aaa87b84bad010a633a9a96891b50df7d /widget/src/markdown.rs | |
parent | 55764b923e69afa8a92b4a6cbd34ee9ddbf8a03d (diff) | |
download | iced-4c883f12b4761c7e0b273d9a2380552336f61d96.tar.gz iced-4c883f12b4761c7e0b273d9a2380552336f61d96.tar.bz2 iced-4c883f12b4761c7e0b273d9a2380552336f61d96.zip |
Make `RichText` generic over data structure
... and decouple `markdown::parse` from theming
Diffstat (limited to 'widget/src/markdown.rs')
-rw-r--r-- | widget/src/markdown.rs | 254 |
1 files changed, 196 insertions, 58 deletions
diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index 2bcbde5c..ef4da0df 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -8,9 +8,15 @@ use crate::core::border; use crate::core::font::{self, Font}; use crate::core::padding; use crate::core::theme; -use crate::core::{self, color, Color, Element, Length, Pixels, Theme}; +use crate::core::{ + self, color, Color, Element, Length, Padding, Pixels, Theme, +}; use crate::{column, container, rich_text, row, scrollable, span, text}; +use std::cell::{Cell, RefCell}; +use std::rc::Rc; + +pub use core::text::Highlight; pub use pulldown_cmark::HeadingLevel; pub use url::Url; @@ -18,13 +24,13 @@ pub use url::Url; #[derive(Debug, Clone)] pub enum Item { /// A heading. - Heading(pulldown_cmark::HeadingLevel, Vec<text::Span<'static, Url>>), + Heading(pulldown_cmark::HeadingLevel, Text), /// A paragraph. - Paragraph(Vec<text::Span<'static, Url>>), + Paragraph(Text), /// A code block. /// /// You can enable the `highlighter` feature for syntax highligting. - CodeBlock(Vec<text::Span<'static, Url>>), + CodeBlock(Text), /// A list. List { /// The first number of the list, if it is ordered. @@ -34,11 +40,112 @@ pub enum Item { }, } +/// A bunch of parsed Markdown text. +#[derive(Debug, Clone)] +pub struct Text { + spans: Vec<Span>, + last_style: Cell<Option<Style>>, + last_styled_spans: RefCell<Rc<[text::Span<'static, Url>]>>, +} + +impl Text { + fn new(spans: Vec<Span>) -> Self { + Self { + spans, + last_style: Cell::default(), + last_styled_spans: RefCell::default(), + } + } + + /// Returns the [`rich_text`] spans ready to be used for the given style. + /// + /// This method performs caching for you. It will only reallocate if the [`Style`] + /// provided changes. + pub fn spans(&self, style: Style) -> Rc<[text::Span<'static, Url>]> { + if Some(style) != self.last_style.get() { + *self.last_styled_spans.borrow_mut() = + self.spans.iter().map(|span| span.view(&style)).collect(); + + self.last_style.set(Some(style)); + } + + self.last_styled_spans.borrow().clone() + } +} + +#[derive(Debug, Clone)] +enum Span { + Standard { + text: String, + strikethrough: bool, + link: Option<Url>, + strong: bool, + emphasis: bool, + code: bool, + }, + #[cfg(feature = "highlighter")] + Highlight { + text: String, + color: Option<Color>, + font: Option<Font>, + }, +} + +impl Span { + fn view(&self, style: &Style) -> text::Span<'static, Url> { + match self { + Span::Standard { + text, + strikethrough, + link, + strong, + emphasis, + code, + } => { + let span = span(text.clone()).strikethrough(*strikethrough); + + let span = if *code { + span.font(Font::MONOSPACE) + .color(style.inline_code_color) + .background(style.inline_code_highlight.background) + .border(style.inline_code_highlight.border) + .padding(style.inline_code_padding) + } else 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 + }; + + let span = if let Some(link) = link.as_ref() { + span.color(style.link_color).link(link.clone()) + } else { + span + }; + + span + } + #[cfg(feature = "highlighter")] + Span::Highlight { text, color, font } => { + span(text.clone()).color_maybe(*color).font_maybe(*font) + } + } + } +} + /// Parse the given Markdown content. -pub fn parse( - markdown: &str, - palette: theme::Palette, -) -> impl Iterator<Item = Item> + '_ { +pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ { struct List { start: Option<u64>, items: Vec<Vec<Item>>, @@ -158,7 +265,7 @@ pub fn parse( pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => { produce( &mut lists, - Item::Heading(level, spans.drain(..).collect()), + Item::Heading(level, Text::new(spans.drain(..).collect())), ) } pulldown_cmark::TagEnd::Strong if !metadata && !table => { @@ -178,7 +285,10 @@ pub fn parse( None } pulldown_cmark::TagEnd::Paragraph if !metadata && !table => { - produce(&mut lists, Item::Paragraph(spans.drain(..).collect())) + produce( + &mut lists, + Item::Paragraph(Text::new(spans.drain(..).collect())), + ) } pulldown_cmark::TagEnd::Item if !metadata && !table => { if spans.is_empty() { @@ -186,7 +296,7 @@ pub fn parse( } else { produce( &mut lists, - Item::Paragraph(spans.drain(..).collect()), + Item::Paragraph(Text::new(spans.drain(..).collect())), ) } } @@ -207,7 +317,10 @@ pub fn parse( highlighter = None; } - produce(&mut lists, Item::CodeBlock(spans.drain(..).collect())) + produce( + &mut lists, + Item::CodeBlock(Text::new(spans.drain(..).collect())), + ) } pulldown_cmark::TagEnd::MetadataBlock(_) => { metadata = false; @@ -227,9 +340,11 @@ pub fn parse( for (range, highlight) in highlighter.highlight_line(text.as_ref()) { - let span = span(text[range].to_owned()) - .color_maybe(highlight.color()) - .font_maybe(highlight.font()); + let span = Span::Highlight { + text: text[range].to_owned(), + color: highlight.color(), + font: highlight.font(), + }; spans.push(span); } @@ -237,30 +352,13 @@ pub fn parse( return None; } - let span = span(text.into_string()).strikethrough(strikethrough); - - 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 - }; - - let span = if let Some(link) = link.as_ref() { - span.color(palette.primary).link(link.clone()) - } else { - span + let span = Span::Standard { + text: text.into_string(), + strong, + emphasis, + strikethrough, + link: link.clone(), + code: false, }; spans.push(span); @@ -268,29 +366,38 @@ pub fn parse( None } pulldown_cmark::Event::Code(code) if !metadata && !table => { - let span = span(code.into_string()) - .font(Font::MONOSPACE) - .color(Color::WHITE) - .background(color!(0x111111)) - .border(border::rounded(2)) - .padding(padding::left(2).right(2)) - .strikethrough(strikethrough); - - let span = if let Some(link) = link.as_ref() { - span.color(palette.primary).link(link.clone()) - } else { - span + let span = Span::Standard { + text: code.into_string(), + strong, + emphasis, + strikethrough, + link: link.clone(), + code: true, }; spans.push(span); None } pulldown_cmark::Event::SoftBreak if !metadata && !table => { - spans.push(span(" ").strikethrough(strikethrough)); + spans.push(Span::Standard { + text: String::from(" "), + strikethrough, + strong, + emphasis, + link: link.clone(), + code: false, + }); None } pulldown_cmark::Event::HardBreak if !metadata && !table => { - spans.push(span("\n")); + spans.push(Span::Standard { + text: String::from("\n"), + strikethrough, + strong, + emphasis, + link: link.clone(), + code: false, + }); None } _ => None, @@ -346,12 +453,41 @@ impl Default for Settings { } } +/// The text styling of some Markdown rendering in [`view`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Style { + /// The [`Highlight`] to be applied to the background of inline code. + pub inline_code_highlight: Highlight, + /// The [`Padding`] to be applied to the background of inline code. + pub inline_code_padding: Padding, + /// The [`Color`] to be applied to inline code. + pub inline_code_color: Color, + /// The [`Color`] to be applied to links. + pub link_color: Color, +} + +impl Style { + /// Creates a new [`Style`] from the given [`theme::Palette`]. + pub fn from_palette(palette: theme::Palette) -> Self { + Self { + inline_code_padding: padding::left(1).right(1), + inline_code_highlight: Highlight { + background: color!(0x111).into(), + border: border::rounded(2), + }, + inline_code_color: Color::WHITE, + link_color: palette.primary, + } + } +} + /// Display a bunch of Markdown items. /// /// You can obtain the items with [`parse`]. pub fn view<'a, Theme, Renderer>( items: impl IntoIterator<Item = &'a Item>, settings: Settings, + style: Style, ) -> Element<'a, Url, Theme, Renderer> where Theme: Catalog + 'a, @@ -372,7 +508,7 @@ where let blocks = items.into_iter().enumerate().map(|(i, item)| match item { Item::Heading(level, heading) => { - container(rich_text(heading).size(match level { + container(rich_text(heading.spans(style)).size(match level { pulldown_cmark::HeadingLevel::H1 => h1_size, pulldown_cmark::HeadingLevel::H2 => h2_size, pulldown_cmark::HeadingLevel::H3 => h3_size, @@ -388,11 +524,11 @@ where .into() } Item::Paragraph(paragraph) => { - rich_text(paragraph).size(text_size).into() + rich_text(paragraph.spans(style)).size(text_size).into() } Item::List { start: None, items } => { column(items.iter().map(|items| { - row![text("•").size(text_size), view(items, settings)] + row![text("•").size(text_size), view(items, settings, style)] .spacing(spacing) .into() })) @@ -405,7 +541,7 @@ where } => column(items.iter().enumerate().map(|(i, items)| { row![ text!("{}.", i as u64 + *start).size(text_size), - view(items, settings) + view(items, settings, style) ] .spacing(spacing) .into() @@ -415,7 +551,9 @@ where Item::CodeBlock(code) => container( scrollable( container( - rich_text(code).font(Font::MONOSPACE).size(code_size), + rich_text(code.spans(style)) + .font(Font::MONOSPACE) + .size(code_size), ) .padding(spacing.0 / 2.0), ) |