summaryrefslogtreecommitdiffstats
path: root/widget
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--widget/src/helpers.rs7
-rw-r--r--widget/src/markdown.rs68
-rw-r--r--widget/src/text/rich.rs179
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)
}