summaryrefslogtreecommitdiffstats
path: root/widget/src/text/rich.rs
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2024-07-21 12:45:05 +0200
committerLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2024-07-21 12:45:05 +0200
commit9bfaf2840cffe35d689bd115a308d21961ab082a (patch)
treec9a7b4fc04607c3315dabe3ad2527251b0a3dff8 /widget/src/text/rich.rs
parent4b44079f34aa9e01977a7974e5f49ae79ff6cd90 (diff)
downloadiced-9bfaf2840cffe35d689bd115a308d21961ab082a.tar.gz
iced-9bfaf2840cffe35d689bd115a308d21961ab082a.tar.bz2
iced-9bfaf2840cffe35d689bd115a308d21961ab082a.zip
Add `Link` support to `rich_text` widget
Diffstat (limited to 'widget/src/text/rich.rs')
-rw-r--r--widget/src/text/rich.rs182
1 files changed, 156 insertions, 26 deletions
diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs
index 5c44ed9e..a44775c6 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_click: 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_click: 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,15 @@ where
self.style(move |_theme| Style { color })
}
+ /// Sets the message handler for link clicks on the [`Rich`] text.
+ pub fn on_link_click(
+ mut self,
+ on_link_click: impl Fn(Link) -> Message + 'a,
+ ) -> Self {
+ self.on_link_click = Some(Box::new(on_link_click));
+ self
+ }
+
/// Sets the default style class of the [`Rich`] text.
#[cfg(feature = "advanced")]
#[must_use]
@@ -152,14 +173,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 +194,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 +233,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 +258,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 +273,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_click.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) = state
+ .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_click.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| state.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 +380,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 +430,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 +448,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)
}