diff options
Diffstat (limited to '')
-rw-r--r-- | widget/src/helpers.rs | 7 | ||||
-rw-r--r-- | widget/src/markdown.rs | 68 | ||||
-rw-r--r-- | widget/src/text/rich.rs | 179 |
3 files changed, 198 insertions, 56 deletions
diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 6def61d5..5b1cb5bc 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -683,10 +683,11 @@ where /// Creates a new [`Rich`] text widget with the provided spans. /// /// [`Rich`]: text::Rich -pub fn rich_text<'a, Theme, Renderer>( - spans: impl Into<Cow<'a, [text::Span<'a, Renderer::Font>]>>, -) -> text::Rich<'a, Theme, Renderer> +pub fn rich_text<'a, Message, Link, Theme, Renderer>( + spans: impl Into<Cow<'a, [text::Span<'a, Link, Renderer::Font>]>>, +) -> text::Rich<'a, Message, Link, Theme, Renderer> where + Link: Clone, Theme: text::Catalog + 'a, Renderer: core::text::Renderer, { diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs index de691a4d..ae4020bc 100644 --- a/widget/src/markdown.rs +++ b/widget/src/markdown.rs @@ -14,13 +14,13 @@ use crate::{column, container, rich_text, row, span, text}; #[derive(Debug, Clone)] pub enum Item { /// A heading. - Heading(Vec<text::Span<'static>>), + Heading(Vec<text::Span<'static, String>>), /// A paragraph. - Paragraph(Vec<text::Span<'static>>), + Paragraph(Vec<text::Span<'static, String>>), /// A code block. /// /// You can enable the `highlighter` feature for syntax highligting. - CodeBlock(Vec<text::Span<'static>>), + CodeBlock(Vec<text::Span<'static, String>>), /// A list. List { /// The first number of the list, if it is ordered. @@ -46,7 +46,7 @@ pub fn parse( let mut emphasis = false; let mut metadata = false; let mut table = false; - let mut link = false; + let mut link = None; let mut lists = Vec::new(); #[cfg(feature = "highlighter")] @@ -93,8 +93,10 @@ pub fn parse( emphasis = true; None } - pulldown_cmark::Tag::Link { .. } if !metadata && !table => { - link = true; + pulldown_cmark::Tag::Link { dest_url, .. } + if !metadata && !table => + { + link = Some(dest_url); None } pulldown_cmark::Tag::List(first_item) if !metadata && !table => { @@ -150,7 +152,7 @@ pub fn parse( None } pulldown_cmark::TagEnd::Link if !metadata && !table => { - link = false; + link = None; None } pulldown_cmark::TagEnd::Paragraph if !metadata && !table => { @@ -245,7 +247,11 @@ pub fn parse( span }; - let span = span.color_maybe(link.then_some(palette.primary)); + let span = if let Some(link) = link.as_ref() { + span.color(palette.primary).link(link.to_string()) + } else { + span + }; spans.push(span); @@ -272,40 +278,48 @@ pub fn parse( /// You can obtain the items with [`parse`]. pub fn view<'a, Message, Renderer>( items: impl IntoIterator<Item = &'a Item>, + on_link: impl Fn(String) -> Message + Copy + 'a, ) -> Element<'a, Message, Theme, Renderer> where Message: 'a, Renderer: core::text::Renderer<Font = Font> + 'a, { let blocks = items.into_iter().enumerate().map(|(i, item)| match item { - Item::Heading(heading) => container(rich_text(heading)) - .padding(padding::top(if i > 0 { 8 } else { 0 })) - .into(), - Item::Paragraph(paragraph) => rich_text(paragraph).into(), - Item::List { start: None, items } => column( - items - .iter() - .map(|items| row!["•", view(items)].spacing(10).into()), - ) - .spacing(10) - .into(), + Item::Heading(heading) => { + container(rich_text(heading).on_link(on_link)) + .padding(padding::top(if i > 0 { 8 } else { 0 })) + .into() + } + Item::Paragraph(paragraph) => { + rich_text(paragraph).on_link(on_link).into() + } + Item::List { start: None, items } => { + column(items.iter().map(|items| { + row!["•", view(items, on_link)].spacing(10).into() + })) + .spacing(10) + .into() + } Item::List { start: Some(start), items, } => column(items.iter().enumerate().map(|(i, items)| { - row![text!("{}.", i as u64 + *start), view(items)] + row![text!("{}.", i as u64 + *start), view(items, on_link)] .spacing(10) .into() })) .spacing(10) .into(), - Item::CodeBlock(code) => { - container(rich_text(code).font(Font::MONOSPACE).size(12)) - .width(Length::Fill) - .padding(10) - .style(container::rounded_box) - .into() - } + Item::CodeBlock(code) => container( + rich_text(code) + .font(Font::MONOSPACE) + .size(12) + .on_link(on_link), + ) + .width(Length::Fill) + .padding(10) + .style(container::rounded_box) + .into(), }); Element::new(column(blocks).width(Length::Fill).spacing(16)) diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs index 5c44ed9e..625ea089 100644 --- a/widget/src/text/rich.rs +++ b/widget/src/text/rich.rs @@ -1,5 +1,6 @@ use crate::core::alignment; -use crate::core::layout::{self, Layout}; +use crate::core::event; +use crate::core::layout; use crate::core::mouse; use crate::core::renderer; use crate::core::text::{Paragraph, Span}; @@ -8,19 +9,26 @@ use crate::core::widget::text::{ }; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - self, Color, Element, Length, Pixels, Rectangle, Size, Widget, + self, Clipboard, Color, Element, Event, Layout, Length, Pixels, Rectangle, + Shell, Size, Widget, }; use std::borrow::Cow; /// A bunch of [`Rich`] text. -#[derive(Debug)] -pub struct Rich<'a, Theme = crate::Theme, Renderer = crate::Renderer> -where +#[allow(missing_debug_implementations)] +pub struct Rich< + 'a, + Message, + Link = (), + Theme = crate::Theme, + Renderer = crate::Renderer, +> where + Link: Clone + 'static, Theme: Catalog, Renderer: core::text::Renderer, { - spans: Cow<'a, [Span<'a, Renderer::Font>]>, + spans: Cow<'a, [Span<'a, Link, Renderer::Font>]>, size: Option<Pixels>, line_height: LineHeight, width: Length, @@ -29,10 +37,13 @@ where align_x: alignment::Horizontal, align_y: alignment::Vertical, class: Theme::Class<'a>, + on_link: Option<Box<dyn Fn(Link) -> Message + 'a>>, } -impl<'a, Theme, Renderer> Rich<'a, Theme, Renderer> +impl<'a, Message, Link, Theme, Renderer> + Rich<'a, Message, Link, Theme, Renderer> where + Link: Clone + 'static, Theme: Catalog, Renderer: core::text::Renderer, { @@ -48,12 +59,13 @@ where align_x: alignment::Horizontal::Left, align_y: alignment::Vertical::Top, class: Theme::default(), + on_link: None, } } /// Creates a new [`Rich`] text with the given text spans. pub fn with_spans( - spans: impl Into<Cow<'a, [Span<'a, Renderer::Font>]>>, + spans: impl Into<Cow<'a, [Span<'a, Link, Renderer::Font>]>>, ) -> Self { Self { spans: spans.into(), @@ -143,6 +155,12 @@ where self.style(move |_theme| Style { color }) } + /// Sets the message handler for link clicks on the [`Rich`] text. + pub fn on_link(mut self, on_link: impl Fn(Link) -> Message + 'a) -> Self { + self.on_link = Some(Box::new(on_link)); + self + } + /// Sets the default style class of the [`Rich`] text. #[cfg(feature = "advanced")] #[must_use] @@ -152,14 +170,19 @@ where } /// Adds a new text [`Span`] to the [`Rich`] text. - pub fn push(mut self, span: impl Into<Span<'a, Renderer::Font>>) -> Self { + pub fn push( + mut self, + span: impl Into<Span<'a, Link, Renderer::Font>>, + ) -> Self { self.spans.to_mut().push(span.into()); self } } -impl<'a, Theme, Renderer> Default for Rich<'a, Theme, Renderer> +impl<'a, Message, Link, Theme, Renderer> Default + for Rich<'a, Message, Link, Theme, Renderer> where + Link: Clone + 'static, Theme: Catalog, Renderer: core::text::Renderer, { @@ -168,24 +191,27 @@ where } } -struct State<P: Paragraph> { - spans: Vec<Span<'static, P::Font>>, +struct State<Link, P: Paragraph> { + spans: Vec<Span<'static, Link, P::Font>>, + span_pressed: Option<usize>, paragraph: P, } -impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for Rich<'a, Theme, Renderer> +impl<'a, Message, Link, Theme, Renderer> Widget<Message, Theme, Renderer> + for Rich<'a, Message, Link, Theme, Renderer> where + Link: Clone + 'static, Theme: Catalog, Renderer: core::text::Renderer, { fn tag(&self) -> tree::Tag { - tree::Tag::of::<State<Renderer::Paragraph>>() + tree::Tag::of::<State<Link, Renderer::Paragraph>>() } fn state(&self) -> tree::State { - tree::State::new(State { + tree::State::new(State::<Link, _> { spans: Vec::new(), + span_pressed: None, paragraph: Renderer::Paragraph::default(), }) } @@ -204,7 +230,8 @@ where limits: &layout::Limits, ) -> layout::Node { layout( - tree.state.downcast_mut::<State<Renderer::Paragraph>>(), + tree.state + .downcast_mut::<State<Link, Renderer::Paragraph>>(), renderer, limits, self.width, @@ -228,7 +255,10 @@ where _cursor_position: mouse::Cursor, viewport: &Rectangle, ) { - let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); + let state = tree + .state + .downcast_ref::<State<Link, Renderer::Paragraph>>(); + let style = theme.style(&self.class); text::draw( @@ -240,15 +270,106 @@ where viewport, ); } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) -> event::Status { + let Some(on_link_click) = self.on_link.as_ref() else { + return event::Status::Ignored; + }; + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + if let Some(position) = cursor.position_in(layout.bounds()) { + let state = tree + .state + .downcast_mut::<State<Link, Renderer::Paragraph>>(); + + if let Some(span) = state.paragraph.hit_span(position) { + state.span_pressed = Some(span); + + return event::Status::Captured; + } + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + let state = tree + .state + .downcast_mut::<State<Link, Renderer::Paragraph>>(); + + if let Some(span_pressed) = state.span_pressed { + state.span_pressed = None; + + if let Some(position) = cursor.position_in(layout.bounds()) + { + match state.paragraph.hit_span(position) { + Some(span) if span == span_pressed => { + if let Some(link) = self + .spans + .get(span) + .and_then(|span| span.link.clone()) + { + shell.publish(on_link_click(link)); + } + } + _ => {} + } + } + } + } + _ => {} + } + + event::Status::Ignored + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + if self.on_link.is_none() { + return mouse::Interaction::None; + } + + if let Some(position) = cursor.position_in(layout.bounds()) { + let state = tree + .state + .downcast_ref::<State<Link, Renderer::Paragraph>>(); + + if let Some(span) = state + .paragraph + .hit_span(position) + .and_then(|span| self.spans.get(span)) + { + if span.link.is_some() { + return mouse::Interaction::Pointer; + } + } + } + + mouse::Interaction::None + } } -fn layout<Renderer>( - state: &mut State<Renderer::Paragraph>, +fn layout<Link, Renderer>( + state: &mut State<Link, Renderer::Paragraph>, renderer: &Renderer, limits: &layout::Limits, width: Length, height: Length, - spans: &[Span<'_, Renderer::Font>], + spans: &[Span<'_, Link, Renderer::Font>], line_height: LineHeight, size: Option<Pixels>, font: Option<Renderer::Font>, @@ -256,6 +377,7 @@ fn layout<Renderer>( vertical_alignment: alignment::Vertical, ) -> layout::Node where + Link: Clone, Renderer: core::text::Renderer, { layout::sized(limits, width, height, |limits| { @@ -305,13 +427,15 @@ where }) } -impl<'a, Theme, Renderer> FromIterator<Span<'a, Renderer::Font>> - for Rich<'a, Theme, Renderer> +impl<'a, Message, Link, Theme, Renderer> + FromIterator<Span<'a, Link, Renderer::Font>> + for Rich<'a, Message, Link, Theme, Renderer> where + Link: Clone + 'static, Theme: Catalog, Renderer: core::text::Renderer, { - fn from_iter<T: IntoIterator<Item = Span<'a, Renderer::Font>>>( + fn from_iter<T: IntoIterator<Item = Span<'a, Link, Renderer::Font>>>( spans: T, ) -> Self { Self { @@ -321,14 +445,17 @@ where } } -impl<'a, Message, Theme, Renderer> From<Rich<'a, Theme, Renderer>> +impl<'a, Message, Link, Theme, Renderer> + From<Rich<'a, Message, Link, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where + Message: 'a, + Link: Clone + 'static, Theme: Catalog + 'a, Renderer: core::text::Renderer + 'a, { fn from( - text: Rich<'a, Theme, Renderer>, + text: Rich<'a, Message, Link, Theme, Renderer>, ) -> Element<'a, Message, Theme, Renderer> { Element::new(text) } |