From ca1bf717b33549c02784a27589b3fc694243d0c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 27 Jun 2025 16:47:54 +0200 Subject: [PATCH] Implement `Quote` support in `markdown` widget --- widget/src/container.rs | 4 +- widget/src/markdown.rs | 89 +++++++++++++++++++++++++++++++++++++++-- widget/src/rule.rs | 15 ++----- 3 files changed, 91 insertions(+), 17 deletions(-) diff --git a/widget/src/container.rs b/widget/src/container.rs index 4f6725b1..84bb5237 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -400,9 +400,9 @@ where Renderer: core::Renderer + 'a, { fn from( - column: Container<'a, Message, Theme, Renderer>, + container: Container<'a, Message, Theme, Renderer>, ) -> Element<'a, Message, Theme, Renderer> { - Element::new(column) + Element::new(container) } } diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index 55fe34e4..a7d012a1 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -50,7 +50,10 @@ use crate::core::theme; use crate::core::{ self, Color, Element, Length, Padding, Pixels, Theme, color, }; -use crate::{column, container, rich_text, row, scrollable, span, text}; +use crate::{ + column, container, rich_text, row, rule, scrollable, span, text, + vertical_rule, +}; use std::borrow::BorrowMut; use std::cell::{Cell, RefCell}; @@ -208,6 +211,8 @@ pub enum Item { /// The alternative text of the image. alt: Text, }, + /// A quote. + Quote(Vec), } /// A bunch of parsed Markdown text. @@ -454,6 +459,7 @@ fn parse_with<'a>( ) -> impl Iterator)> + 'a { enum Scope { List(List), + Quote(Vec), } struct List { @@ -524,6 +530,9 @@ fn parse_with<'a>( Scope::List(list) => { list.items.last_mut().expect("item context").push(item); } + Scope::Quote(items) => { + items.push(item); + } } None @@ -605,6 +614,22 @@ fn parse_with<'a>( None } + pulldown_cmark::Tag::BlockQuote(_kind) if !metadata && !table => { + let prev = if spans.is_empty() { + None + } else { + produce( + state.borrow_mut(), + &mut stack, + Item::Paragraph(Text::new(spans.drain(..).collect())), + source, + ) + }; + + stack.push(Scope::Quote(Vec::new())); + + prev + } pulldown_cmark::Tag::CodeBlock( pulldown_cmark::CodeBlockKind::Fenced(language), ) if !metadata && !table => { @@ -703,7 +728,9 @@ fn parse_with<'a>( pulldown_cmark::TagEnd::List(_) if !metadata && !table => { let scope = stack.pop()?; - let Scope::List(list) = scope; + let Scope::List(list) = scope else { + return None; + }; produce( state.borrow_mut(), @@ -715,6 +742,22 @@ fn parse_with<'a>( source, ) } + pulldown_cmark::TagEnd::BlockQuote(_kind) + if !metadata && !table => + { + let scope = stack.pop()?; + + let Scope::Quote(quote) = scope else { + return None; + }; + + produce( + state.borrow_mut(), + &mut stack, + Item::Quote(quote), + source, + ) + } pulldown_cmark::TagEnd::Image if !metadata && !table => { let (url, title) = image.take()?; let alt = Text::new(spans.drain(..).collect()); @@ -1063,6 +1106,7 @@ where start: Some(start), items, } => viewer.ordered_list(settings, *start, items), + Item::Quote(quote) => viewer.quote(settings, quote), } } @@ -1226,7 +1270,33 @@ where .into() } -/// A view strategy to display a Markdown [`Item`].j +/// Displays a quote using the default look. +pub fn quote<'a, Message, Theme, Renderer>( + viewer: &impl Viewer<'a, Message, Theme, Renderer>, + settings: Settings, + contents: &'a [Item], +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: Catalog + 'a, + Renderer: core::text::Renderer + 'a, +{ + row![ + vertical_rule(4), + column( + contents + .iter() + .enumerate() + .map(|(i, content)| item(viewer, settings, content, i)), + ) + .spacing(settings.spacing.0), + ] + .height(Length::Shrink) + .spacing(settings.spacing.0) + .into() +} + +/// A view strategy to display a Markdown [`Item`]. pub trait Viewer<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> where Self: Sized + 'a, @@ -1321,6 +1391,17 @@ where ) -> Element<'a, Message, Theme, Renderer> { ordered_list(self, settings, start, items) } + + /// Displays a quote. + /// + /// By default, it call [`quote`]. + fn quote( + &self, + settings: Settings, + contents: &'a [Item], + ) -> Element<'a, Message, Theme, Renderer> { + quote(self, settings, contents) + } } #[derive(Debug, Clone, Copy)] @@ -1338,7 +1419,7 @@ where /// The theme catalog of Markdown items. pub trait Catalog: - container::Catalog + scrollable::Catalog + text::Catalog + container::Catalog + scrollable::Catalog + rule::Catalog + text::Catalog { /// The styling class of a Markdown code block. fn code_block<'a>() -> ::Class<'a>; diff --git a/widget/src/rule.rs b/widget/src/rule.rs index 03c57c94..2bb893b2 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -134,9 +134,7 @@ where let style = theme.style(&self.class); let bounds = if self.is_horizontal { - let line_y = (bounds.y + (bounds.height / 2.0) - - (style.width as f32 / 2.0)) - .round(); + let line_y = (bounds.y + (bounds.height / 2.0)).round(); let (offset, line_width) = style.fill_mode.fill(bounds.width); let line_x = bounds.x + offset; @@ -145,12 +143,10 @@ where x: line_x, y: line_y, width: line_width, - height: style.width as f32, + height: bounds.height, } } else { - let line_x = (bounds.x + (bounds.width / 2.0) - - (style.width as f32 / 2.0)) - .round(); + let line_x = (bounds.x + (bounds.width / 2.0)).round(); let (offset, line_height) = style.fill_mode.fill(bounds.height); let line_y = bounds.y + offset; @@ -158,7 +154,7 @@ where Rectangle { x: line_x, y: line_y, - width: style.width as f32, + width: bounds.width, height: line_height, } }; @@ -192,8 +188,6 @@ where pub struct Style { /// The color of the rule. pub color: Color, - /// The width (thickness) of the rule line. - pub width: u16, /// The radius of the line corners. pub radius: border::Radius, /// The [`FillMode`] of the rule. @@ -301,7 +295,6 @@ pub fn default(theme: &Theme) -> Style { Style { color: palette.background.strong.color, - width: 1, radius: 0.0.into(), fill_mode: FillMode::Full, snap: true,