Skip to main content

floem/views/
tooltip.rs

1#![deny(missing_docs)]
2use peniko::kurbo::Point;
3use std::cell::RefCell;
4use std::rc::Rc;
5use ui_events::pointer::PointerEvent;
6
7use crate::{
8    action::{TimerToken, add_overlay, exec_after, remove_overlay},
9    context::{EventCx, UpdateCx},
10    event::{Event, EventPropagation, Phase},
11    platform::Duration,
12    prop, prop_extractor, style_class,
13    view::{IntoView, View, ViewId},
14    views::Decorators,
15};
16
17style_class!(
18    /// A class for the tooltip views.
19    pub TooltipClass
20);
21style_class!(
22    /// A class for the tooltip container view.
23    pub TooltipContainerClass
24);
25
26prop!(pub Delay: Duration {} = Duration::from_millis(600));
27
28prop_extractor! {
29    TooltipStyle {
30        delay: Delay,
31    }
32}
33
34/// A view that displays a tooltip for its child.
35pub struct Tooltip {
36    id: ViewId,
37    /// Holds the hover point and time token needed to
38    /// evaluate if - or when and where - display tooltip.
39    hover_point: Option<(Point, TimerToken)>,
40    /// Tooltip overlay view id.
41    overlay: Rc<RefCell<Option<ViewId>>>,
42    /// Provided by user function that dislays tooltip content.
43    tip: Rc<dyn Fn() -> Box<dyn View>>,
44    /// A tooltip specific styles (currently its just a delay).
45    style: TooltipStyle,
46}
47
48/// A view that displays a tooltip for its child.
49pub fn tooltip<V: IntoView + 'static, T: IntoView + 'static>(
50    child: V,
51    tip: impl Fn() -> T + 'static,
52) -> Tooltip {
53    let id = ViewId::new();
54    let child = child.into_view();
55    id.set_children([child]);
56    let overlay = Rc::new(RefCell::new(None));
57    Tooltip {
58        id,
59        tip: Rc::new(move || tip().into_any()),
60        hover_point: None,
61        overlay: overlay.clone(),
62        style: Default::default(),
63    }
64    .class(TooltipContainerClass)
65    .on_cleanup(move || {
66        if let Some(overlay_id) = overlay.borrow_mut().take() {
67            remove_overlay(overlay_id);
68        }
69    })
70}
71
72impl View for Tooltip {
73    fn id(&self) -> ViewId {
74        self.id
75    }
76
77    fn update(&mut self, _cx: &mut UpdateCx, state: Box<dyn std::any::Any>) {
78        if let Ok(token) = state.downcast::<TimerToken>()
79            && self.hover_point.map(|(_, t)| t) == Some(*token)
80        {
81            let point =
82                self.id.get_visual_origin() + self.hover_point.unwrap().0.to_vec2() + (0., 10.);
83            let overlay_id = add_overlay(
84                (self.tip)()
85                    .class(TooltipClass)
86                    .style(move |s| s.inset_left(point.x).inset_top(point.y)),
87            );
88            overlay_id.set_style_parent(self.id);
89            *self.overlay.borrow_mut() = Some(overlay_id);
90        }
91    }
92
93    fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
94        self.style.read(cx);
95        if self.overlay.borrow().is_some() && self.id.is_hidden() {
96            let id = self.overlay.take().unwrap();
97            self.hover_point = None;
98            remove_overlay(id);
99        }
100    }
101
102    fn event_capture(&mut self, cx: &mut EventCx) -> EventPropagation {
103        self.handle_event(cx)
104    }
105
106    fn event(&mut self, cx: &mut EventCx) -> EventPropagation {
107        if cx.phase != Phase::Target {
108            return EventPropagation::Continue;
109        }
110        self.handle_event(cx)
111    }
112}
113
114impl Tooltip {
115    fn handle_event(&mut self, cx: &mut EventCx) -> EventPropagation {
116        match &cx.event {
117            Event::Pointer(PointerEvent::Move(pu)) => {
118                if self.overlay.borrow().is_none() {
119                    let id = self.id();
120                    let token = exec_after(self.style.delay(), move |token| {
121                        id.update_state(token);
122                    });
123                    self.hover_point = Some((pu.current.logical_point(), token));
124                }
125            }
126            Event::Pointer(_) | Event::Key(_) => {
127                self.hover_point = None;
128                if let Some(id) = self.overlay.borrow_mut().take() {
129                    remove_overlay(id);
130                }
131            }
132            _ => {}
133        }
134        EventPropagation::Continue
135    }
136}
137
138/// Adds a [tooltip] function to a type that implements [`IntoView`].
139pub trait TooltipExt {
140    /// Adds a tooltip to the view.
141    ///
142    /// ### Examples
143    /// ```rust
144    /// # use floem::views::TooltipExt;
145    /// # use floem::views::{text, Decorators};
146    /// # use floem::prelude::{RwSignal, SignalGet};
147    /// // Simple usage:
148    /// let simple = text("A text with tooltip")
149    ///     .tooltip(|| "This is a tooltip.");
150    /// // More complex usage:
151    /// let mut click_counter = RwSignal::new(0);
152    /// let complex = text("A text with a tooltip that changes on click")
153    ///     .on_click_stop(move|_| click_counter += 1)
154    ///     .tooltip(move || format!("Clicked {} times on the label", click_counter.get()));
155    /// ```
156    /// ### Reactivity
157    /// This function is not reactive, but it is computing its result on every tooltip trigger.
158    /// It is possible then to have different tooltip output, but the output it will **not** change
159    /// once while displaying a hover.
160    fn tooltip<V: IntoView + 'static>(self, tip: impl Fn() -> V + 'static) -> Tooltip;
161}
162
163impl<T: IntoView + 'static> TooltipExt for T {
164    fn tooltip<V: IntoView + 'static>(self, tip: impl Fn() -> V + 'static) -> Tooltip {
165        tooltip(self, tip)
166    }
167}