diff --git a/core/src/pixels.rs b/core/src/pixels.rs index f5550a105e..a1ea0f1524 100644 --- a/core/src/pixels.rs +++ b/core/src/pixels.rs @@ -9,6 +9,11 @@ #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)] pub struct Pixels(pub f32); +impl Pixels { + /// Zero pixels. + pub const ZERO: Self = Self(0.0); +} + impl From for Pixels { fn from(amount: f32) -> Self { Self(amount) @@ -58,3 +63,19 @@ impl std::ops::Mul for Pixels { Pixels(self.0 * rhs) } } + +impl std::ops::Div for Pixels { + type Output = Pixels; + + fn div(self, rhs: Self) -> Self { + Pixels(self.0 / rhs.0) + } +} + +impl std::ops::Div for Pixels { + type Output = Pixels; + + fn div(self, rhs: f32) -> Self { + Pixels(self.0 / rhs) + } +} diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index 47fcfc72f0..44bf57c5b0 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -64,7 +64,11 @@ impl Markdown { .padding(10) .font(Font::MONOSPACE); - let preview = markdown(&self.items, Message::LinkClicked); + let preview = markdown( + &self.items, + markdown::Settings::default(), + Message::LinkClicked, + ); row![editor, scrollable(preview).spacing(10).height(Fill)] .spacing(10) diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index 1df3503627..e84ff8d6b7 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -7,16 +7,17 @@ use crate::core::font::{self, Font}; use crate::core::padding; use crate::core::theme::{self, Theme}; -use crate::core::{self, Element, Length}; +use crate::core::{self, Element, Length, Pixels}; use crate::{column, container, rich_text, row, span, text}; +pub use pulldown_cmark::HeadingLevel; pub use url::Url; /// A Markdown item. #[derive(Debug, Clone)] pub enum Item { /// A heading. - Heading(Vec>), + Heading(pulldown_cmark::HeadingLevel, Vec>), /// A paragraph. Paragraph(Vec>), /// A code block. @@ -43,7 +44,6 @@ pub fn parse( } let mut spans = Vec::new(); - let mut heading = None; let mut strong = false; let mut emphasis = false; let mut metadata = false; @@ -81,12 +81,6 @@ pub fn parse( #[allow(clippy::drain_collect)] parser.filter_map(move |event| match event { pulldown_cmark::Event::Start(tag) => match tag { - pulldown_cmark::Tag::Heading { level, .. } - if !metadata && !table => - { - heading = Some(level); - None - } pulldown_cmark::Tag::Strong if !metadata && !table => { strong = true; None @@ -119,7 +113,11 @@ pub fn parse( None } pulldown_cmark::Tag::Item => { - lists.last_mut().expect("List").items.push(Vec::new()); + lists + .last_mut() + .expect("list context") + .items + .push(Vec::new()); None } pulldown_cmark::Tag::CodeBlock( @@ -150,9 +148,11 @@ pub fn parse( _ => None, }, pulldown_cmark::Event::End(tag) => match tag { - pulldown_cmark::TagEnd::Heading(_) if !metadata && !table => { - heading = None; - produce(&mut lists, Item::Heading(spans.drain(..).collect())) + pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => { + produce( + &mut lists, + Item::Heading(level, spans.drain(..).collect()), + ) } pulldown_cmark::TagEnd::Emphasis if !metadata && !table => { emphasis = false; @@ -180,7 +180,7 @@ pub fn parse( } } pulldown_cmark::TagEnd::List(_) if !metadata && !table => { - let list = lists.pop().expect("List"); + let list = lists.pop().expect("list context"); produce( &mut lists, @@ -228,18 +228,6 @@ pub fn parse( 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 { @@ -269,7 +257,15 @@ pub fn parse( None } pulldown_cmark::Event::Code(code) if !metadata && !table => { - spans.push(span(code.into_string()).font(Font::MONOSPACE)); + let span = span(code.into_string()).font(Font::MONOSPACE); + + let span = if let Some(link) = link.as_ref() { + span.color(palette.primary).link(link.clone()) + } else { + span + }; + + spans.push(span); None } pulldown_cmark::Event::SoftBreak if !metadata && !table => { @@ -284,54 +280,133 @@ pub fn parse( }) } +/// Configuration controlling Markdown rendering in [`view`]. +#[derive(Debug, Clone, Copy)] +pub struct Settings { + /// The base text size. + pub text_size: Pixels, + /// The text size of level 1 heading. + pub h1_size: Pixels, + /// The text size of level 2 heading. + pub h2_size: Pixels, + /// The text size of level 3 heading. + pub h3_size: Pixels, + /// The text size of level 4 heading. + pub h4_size: Pixels, + /// The text size of level 5 heading. + pub h5_size: Pixels, + /// The text size of level 6 heading. + pub h6_size: Pixels, + /// The text size used in code blocks. + pub code_size: Pixels, +} + +impl Settings { + /// Creates new [`Settings`] with the given base text size in [`Pixels`]. + /// + /// Heading levels will be adjusted automatically. Specifically, + /// the first level will be twice the base size, and then every level + /// after that will be 25% smaller. + pub fn with_text_size(text_size: impl Into) -> Self { + let text_size = text_size.into(); + + Self { + text_size, + h1_size: text_size * 2.0, + h2_size: text_size * 1.75, + h3_size: text_size * 1.5, + h4_size: text_size * 1.25, + h5_size: text_size, + h6_size: text_size, + code_size: text_size * 0.75, + } + } +} + +impl Default for Settings { + fn default() -> Self { + Self::with_text_size(16) + } +} + /// Display a bunch of Markdown items. /// /// You can obtain the items with [`parse`]. pub fn view<'a, Message, Renderer>( items: impl IntoIterator, + settings: Settings, on_link: impl Fn(Url) -> Message + Copy + 'a, ) -> Element<'a, Message, Theme, Renderer> where Message: 'a, Renderer: core::text::Renderer + 'a, { + let Settings { + text_size, + h1_size, + h2_size, + h3_size, + h4_size, + h5_size, + h6_size, + code_size, + } = settings; + + let spacing = text_size * 0.625; + let blocks = items.into_iter().enumerate().map(|(i, item)| match item { - Item::Heading(heading) => { - container(rich_text(heading).on_link(on_link)) - .padding(padding::top(if i > 0 { 8 } else { 0 })) - .into() + Item::Heading(level, heading) => { + container(rich_text(heading).on_link(on_link).size(match level { + pulldown_cmark::HeadingLevel::H1 => h1_size, + pulldown_cmark::HeadingLevel::H2 => h2_size, + pulldown_cmark::HeadingLevel::H3 => h3_size, + pulldown_cmark::HeadingLevel::H4 => h4_size, + pulldown_cmark::HeadingLevel::H5 => h5_size, + pulldown_cmark::HeadingLevel::H6 => h6_size, + })) + .padding(padding::top(if i > 0 { + text_size / 2.0 + } else { + Pixels::ZERO + })) + .into() } Item::Paragraph(paragraph) => { - rich_text(paragraph).on_link(on_link).into() + rich_text(paragraph).on_link(on_link).size(text_size).into() } Item::List { start: None, items } => { column(items.iter().map(|items| { - row!["•", view(items, on_link)].spacing(10).into() + row![text("•").size(text_size), view(items, settings, on_link)] + .spacing(spacing) + .into() })) - .spacing(10) + .spacing(spacing) .into() } Item::List { start: Some(start), items, } => column(items.iter().enumerate().map(|(i, items)| { - row![text!("{}.", i as u64 + *start), view(items, on_link)] - .spacing(10) - .into() + row![ + text!("{}.", i as u64 + *start).size(text_size), + view(items, settings, on_link) + ] + .spacing(spacing) + .into() })) - .spacing(10) + .spacing(spacing) .into(), Item::CodeBlock(code) => container( rich_text(code) .font(Font::MONOSPACE) - .size(12) + .size(code_size) .on_link(on_link), ) .width(Length::Fill) - .padding(10) + .padding(spacing.0) .style(container::rounded_box) .into(), }); - Element::new(column(blocks).width(Length::Fill).spacing(16)) + Element::new(column(blocks).width(Length::Fill).spacing(text_size)) } diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index 625ea089dd..05ad6576da 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -161,6 +161,15 @@ where self } + /// Sets the message handler for link clicks on the [`Rich`] text. + pub fn on_link_maybe( + mut self, + on_link: Option Message + 'a>, + ) -> Self { + self.on_link = on_link.map(|on_link| Box::new(on_link) as _); + self + } + /// Sets the default style class of the [`Rich`] text. #[cfg(feature = "advanced")] #[must_use]