diff options
| author | Polesznyák Márk <contact@pml68.dev> | 2025-10-31 23:57:50 +0100 |
|---|---|---|
| committer | Polesznyák Márk <contact@pml68.dev> | 2025-11-03 17:35:42 +0100 |
| commit | f560a25f2867ad1d40752313eed712299850e2b6 (patch) | |
| tree | 3a2dda76042ba515fc146e02e876ba52e3025836 | |
| parent | feat: add `span!` macro (same as `text!`) (diff) | |
| download | iced_selection-f560a25f2867ad1d40752313eed712299850e2b6.tar.gz | |
feat(rich): add `on_link_hover` and `on_hover_lost`
| -rw-r--r-- | examples/rich/src/main.rs | 50 | ||||
| -rw-r--r-- | src/text/rich.rs | 75 |
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 |
