aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPolesznyák Márk <contact@pml68.dev>2025-10-31 23:57:50 +0100
committerPolesznyák Márk <contact@pml68.dev>2025-11-03 17:35:42 +0100
commitf560a25f2867ad1d40752313eed712299850e2b6 (patch)
tree3a2dda76042ba515fc146e02e876ba52e3025836
parentfeat: add `span!` macro (same as `text!`) (diff)
downloadiced_selection-f560a25f2867ad1d40752313eed712299850e2b6.tar.gz
feat(rich): add `on_link_hover` and `on_hover_lost`
-rw-r--r--examples/rich/src/main.rs50
-rw-r--r--src/text/rich.rs75
2 files changed, 104 insertions, 21 deletions
diff --git a/examples/rich/src/main.rs b/examples/rich/src/main.rs
index f8f7414..095651a 100644
--- a/examples/rich/src/main.rs
+++ b/examples/rich/src/main.rs
@@ -1,5 +1,5 @@
-use iced::widget::{center, column, responsive};
-use iced::{Center, Element, color};
+use iced::widget::{column, container, responsive};
+use iced::{Center, Element, Fill, Font, color, font, padding};
use iced_selection::{rich_text, span};
fn main() -> iced::Result {
@@ -8,12 +8,15 @@ fn main() -> iced::Result {
#[derive(Default)]
struct State {
- link: Option<String>,
+ clicked: Option<String>,
+ hovered: Option<String>,
}
#[derive(Debug, Clone)]
enum Message {
LinkClicked(String),
+ LinkHovered(String),
+ HoverLost,
}
impl State {
@@ -21,14 +24,18 @@ impl State {
match message {
Message::LinkClicked(link) => {
let _ = open::that_in_background(&link);
- self.link = Some(link);
+ self.clicked = Some(link);
}
+ Message::LinkHovered(link) => {
+ self.hovered = Some(link);
+ }
+ Message::HoverLost => self.hovered = None,
};
}
fn view(&self) -> Element<'_, Message> {
responsive(|size| {
- center(
+ container(
column![
rich_text![
span("iced")
@@ -42,19 +49,44 @@ impl State {
span("Elm")
.color(color!(0x2b79a2))
.link("https://elm-lang.org"),
- "."
+ ", a delightful functional language for building web applications."
+ ]
+ .on_link_click(Message::LinkClicked)
+ .on_link_hover(Message::LinkHovered)
+ .on_hover_lost(Message::HoverLost),
+ rich_text![
+ "As a GUI library, iced helps you build ",
+ span("graphical user interfaces")
+ .color(color!(0x2b79a2))
+ .link("https://en.wikipedia.org/wiki/Graphical_user_interface")
+ .font(Font {
+ style:font::Style::Italic,
+ ..Font::DEFAULT
+ }),
+ " for your Rust applications."
+ ]
+ .on_link_click(Message::LinkClicked)
+ .on_link_hover(Message::LinkHovered)
+ .on_hover_lost(Message::HoverLost),
+ self.hovered.as_deref().map(|link| rich_text![
+ "Currently hovered link: ",
+ span(link).color(color!(0x2b79a2)).link(link)
]
- .on_link_click(Message::LinkClicked),
- self.link.as_deref().map(|link| rich_text![
+ .on_link_click(Message::LinkClicked)),
+ self.clicked.as_deref().map(|link| rich_text![
"Last clicked link: ",
span(link).color(color!(0x2b79a2)).link(link)
]
- .on_link_click(Message::LinkClicked))
+ .on_link_click(Message::LinkClicked)
+ .on_link_hover(Message::LinkHovered)
+ .on_hover_lost(Message::HoverLost))
]
.spacing(10)
.align_x(Center)
.max_width(size.width * 0.8),
)
+ .padding(padding::top(size.height * 0.4))
+ .center_x(Fill)
.into()
})
.into()
diff --git a/src/text/rich.rs b/src/text/rich.rs
index 3268160..a30663b 100644
--- a/src/text/rich.rs
+++ b/src/text/rich.rs
@@ -40,8 +40,9 @@ pub struct Rich<
align_y: alignment::Vertical,
wrapping: Wrapping,
class: Theme::Class<'a>,
- hovered_link: Option<usize>,
on_link_click: Option<Box<dyn Fn(Link) -> Message + 'a>>,
+ on_link_hover: Option<Box<dyn Fn(Link) -> Message + 'a>>,
+ on_hover_lost: Option<Box<dyn Fn() -> Message + 'a>>,
}
impl<'a, Link, Message, Theme, Renderer>
@@ -65,8 +66,9 @@ where
align_y: alignment::Vertical::Top,
wrapping: Wrapping::default(),
class: Theme::default(),
- hovered_link: None,
on_link_click: None,
+ on_link_hover: None,
+ on_hover_lost: None,
}
}
@@ -147,6 +149,36 @@ where
self
}
+ /// Sets the message that will be produced when a link of the [`Rich`] text
+ /// is hovered.
+ pub fn on_link_hover(
+ mut self,
+ on_link_hovered: impl Fn(Link) -> Message + 'a,
+ ) -> Self {
+ self.on_link_hover = Some(Box::new(on_link_hovered));
+ self
+ }
+
+ /// Sets the message that will be produced when a link of the [`Rich`] text
+ /// is no longer hovered.
+ pub fn on_hover_lost(mut self, on_hover_lost: Message) -> Self
+ where
+ Message: Clone + 'a,
+ {
+ self.on_hover_lost = Some(Box::new(move || on_hover_lost.clone()));
+ self
+ }
+
+ /// Sets the message that will be produced when a link of the [`Rich`] text
+ /// is no longer hovered.
+ pub fn on_hover_lost_with(
+ mut self,
+ on_hover_lost: impl Fn() -> Message + 'a,
+ ) -> Self {
+ self.on_hover_lost = Some(Box::new(on_hover_lost));
+ self
+ }
+
/// Sets the default style of the [`Rich`] text.
#[must_use]
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
@@ -181,6 +213,7 @@ where
struct State<Link> {
spans: Vec<Span<'static, Link, Font>>,
span_pressed: Option<usize>,
+ hovered_link: Option<usize>,
paragraph: Paragraph,
is_hovered: bool,
selection: Selection,
@@ -335,6 +368,7 @@ where
tree::State::new(State::<Link> {
spans: Vec::new(),
span_pressed: None,
+ hovered_link: None,
paragraph: Paragraph::default(),
is_hovered: false,
selection: Selection::default(),
@@ -393,7 +427,7 @@ where
for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() {
let is_hovered_link = self.on_link_click.is_some()
- && Some(index) == self.hovered_link;
+ && Some(index) == state.hovered_link;
if span.highlight.is_some()
|| span.underline
@@ -572,14 +606,14 @@ where
return;
}
- let link_was_hovered = self.hovered_link.is_some();
let was_hovered = state.is_hovered;
+ let hovered_link_before = state.hovered_link;
let selection_before = state.selection;
state.is_hovered = click_position.is_some();
if let Some(position) = click_position {
- self.hovered_link =
+ state.hovered_link =
state.paragraph.hit_span(position).and_then(|span| {
if self.spans.as_ref().as_ref().get(span)?.link.is_some() {
Some(span)
@@ -588,14 +622,14 @@ where
}
});
} else {
- self.hovered_link = None;
+ state.hovered_link = None;
}
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
- if self.hovered_link.is_some() {
- state.span_pressed = self.hovered_link;
+ if state.hovered_link.is_some() {
+ state.span_pressed = state.hovered_link;
shell.capture_event();
}
@@ -653,7 +687,7 @@ where
) && state.selection.is_empty()
{
match state.span_pressed {
- Some(span) if Some(span) == self.hovered_link => {
+ Some(span) if Some(span) == state.hovered_link => {
if let Some((link, on_link_clicked)) = self
.spans
.as_ref()
@@ -823,21 +857,38 @@ where
if state.is_hovered != was_hovered
|| state.selection != selection_before
- || self.hovered_link.is_some() != link_was_hovered
+ || state.hovered_link != hovered_link_before
{
+ if let Some(span) = state.hovered_link {
+ if let Some((link, on_link_hovered)) = self
+ .spans
+ .as_ref()
+ .as_ref()
+ .get(span)
+ .and_then(|span| span.link.clone())
+ .zip(self.on_link_hover.as_deref())
+ {
+ shell.publish(on_link_hovered(link));
+ }
+ } else if let Some(on_hover_lost) = self.on_hover_lost.as_deref() {
+ shell.publish(on_hover_lost());
+ }
+
shell.request_redraw();
}
}
fn mouse_interaction(
&self,
- _tree: &Tree,
+ tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
- if self.hovered_link.is_some() {
+ let state = tree.state.downcast_ref::<State<Link>>();
+
+ if state.hovered_link.is_some() {
mouse::Interaction::Pointer
} else if cursor.is_over(layout.bounds()) {
mouse::Interaction::Text