Skip to main content

iced_widget/
tooltip.rs

1//! Tooltips display a hint of information over some element when hovered.
2//!
3//! By default, the tooltip is displayed immediately, however, this can be adjusted
4//! with [`Tooltip::delay`].
5//!
6//! # Example
7//! ```no_run
8//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
9//! # use iced::widget::Widget;
10//! # pub type State = ();
11//! use iced::widget::{container, tooltip};
12//!
13//! enum Message {
14//!     // ...
15//! }
16//!
17//! fn view(_state: &State) -> impl Widget<Message> {
18//!     tooltip(
19//!         "Hover me to display the tooltip!",
20//!         container("This is the tooltip contents!")
21//!             .padding(10)
22//!             .style(container::rounded_box),
23//!         tooltip::Position::Bottom,
24//!     )
25//! }
26//! ```
27use crate::container;
28use crate::core::layout::{self, Layout};
29use crate::core::mouse;
30use crate::core::overlay;
31use crate::core::renderer;
32use crate::core::text;
33use crate::core::time::{Duration, Instant};
34use crate::core::widget::{self, Widget};
35use crate::core::window;
36use crate::core::{Event, Length, Pixels, Point, Rectangle, Shell, Size, Vector};
37
38/// An element to display a widget over another.
39///
40/// # Example
41/// ```no_run
42/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
43/// # use iced::widget::Widget;
44/// # pub type State = ();
45/// use iced::widget::{container, tooltip};
46///
47/// enum Message {
48///     // ...
49/// }
50///
51/// fn view(_state: &State) -> impl Widget<Message> {
52///     tooltip(
53///         "Hover me to display the tooltip!",
54///         container("This is the tooltip contents!")
55///             .padding(10)
56///             .style(container::rounded_box),
57///         tooltip::Position::Bottom,
58///     )
59/// }
60/// ```
61pub struct Tooltip<'a, W, V, Theme = crate::Theme>
62where
63    Theme: container::Catalog,
64{
65    content: W,
66    tooltip: V,
67    position: Position,
68    gap: f32,
69    snap_within_viewport: bool,
70    delay: Duration,
71    class: Theme::Class<'a>,
72}
73
74impl<'a, W, V, Theme> Tooltip<'a, W, V, Theme>
75where
76    Theme: container::Catalog,
77{
78    /// Creates a new [`Tooltip`].
79    ///
80    /// [`Tooltip`]: struct.Tooltip.html
81    pub fn new(content: W, tooltip: V, position: Position) -> Self {
82        Tooltip {
83            content,
84            tooltip,
85            position,
86            gap: 0.0,
87            snap_within_viewport: true,
88            delay: Duration::ZERO,
89            class: Theme::default(),
90        }
91    }
92
93    /// Sets the gap between the content and its [`Tooltip`].
94    pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
95        self.gap = gap.into().0;
96        self
97    }
98
99    /// Sets the delay before the [`Tooltip`] is shown.
100    ///
101    /// Set to [`Duration::ZERO`] to be shown immediately.
102    pub fn delay(mut self, delay: Duration) -> Self {
103        self.delay = delay;
104        self
105    }
106
107    /// Sets whether the [`Tooltip`] is snapped within the viewport.
108    pub fn snap_within_viewport(mut self, snap: bool) -> Self {
109        self.snap_within_viewport = snap;
110        self
111    }
112
113    /// Sets the style of the [`Tooltip`].
114    #[must_use]
115    pub fn style(mut self, style: impl Fn(&Theme) -> container::Style + 'a) -> Self
116    where
117        Theme::Class<'a>: From<container::StyleFn<'a, Theme>>,
118    {
119        self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into();
120        self
121    }
122
123    /// Sets the style class of the [`Tooltip`].
124    #[cfg(feature = "advanced")]
125    #[must_use]
126    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
127        self.class = class.into();
128        self
129    }
130}
131
132impl<W, V, Theme> widget::Meta for Tooltip<'_, W, V, Theme> where Theme: container::Catalog {}
133
134impl<W, V, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Tooltip<'_, W, V, Theme>
135where
136    Theme: container::Catalog,
137    Renderer: text::Renderer,
138    W: Widget<Message, Theme, Renderer>,
139    V: Widget<Message, Theme, Renderer>,
140{
141    fn diff(&mut self, tree: &mut widget::Tree) {
142        let state = tree.state.downcast_mut::<State>();
143
144        // The tooltip's contents may have changed, so the cached node
145        // (if any) is no longer valid
146        if let State::Open { needs_relayout, .. } = state {
147            *needs_relayout = true;
148        }
149
150        if tree.children.len() != 2 {
151            tree.children = vec![
152                widget::Tree::new(&self.content),
153                widget::Tree::new(&self.tooltip),
154            ];
155        }
156
157        tree.children[0].diff(&mut self.content);
158        tree.children[1].diff(&mut self.tooltip);
159    }
160
161    fn state(&self) -> widget::tree::State {
162        widget::tree::State::new(State::default())
163    }
164
165    fn tag(&self) -> widget::tree::Tag {
166        widget::tree::Tag::of::<State>()
167    }
168
169    fn size(&self) -> Size<Length> {
170        self.content.size()
171    }
172
173    fn layout(&mut self, tree: &mut widget::Tree, renderer: &Renderer, limits: &layout::Limits) {
174        self.content.layout(&mut tree.children[0], renderer, limits);
175
176        tree.size = tree.children[0].size;
177    }
178
179    fn update(
180        &mut self,
181        tree: &mut widget::Tree,
182        event: &Event,
183        layout: Layout,
184        cursor: mouse::Cursor,
185        renderer: &Renderer,
186        shell: &mut Shell<'_, Message>,
187        viewport: &Rectangle,
188    ) {
189        if let Event::Mouse(_) | Event::Window(window::Event::RedrawRequested(_)) = event {
190            let state = tree.state.downcast_mut::<State>();
191            let now = Instant::now();
192            let cursor_position = cursor.position_over(layout.bounds());
193
194            match (&*state, cursor_position) {
195                (State::Idle, Some(cursor_position)) => {
196                    if self.delay == Duration::ZERO {
197                        *state = State::Open {
198                            cursor_position,
199                            needs_relayout: true,
200                        };
201                        shell.invalidate_overlay();
202                    } else {
203                        *state = State::Hovered { at: now };
204                    }
205
206                    shell.request_redraw_at(now + self.delay);
207                }
208                (State::Hovered { .. }, None) => {
209                    *state = State::Idle;
210                }
211                (State::Hovered { at, .. }, _) if at.elapsed() < self.delay => {
212                    shell.request_redraw_at(now + self.delay - at.elapsed());
213                }
214                (State::Hovered { .. }, Some(cursor_position)) => {
215                    *state = State::Open {
216                        cursor_position,
217                        needs_relayout: true,
218                    };
219                    shell.invalidate_overlay();
220                }
221                (
222                    &State::Open {
223                        cursor_position: last_position,
224                        ..
225                    },
226                    Some(cursor_position),
227                ) if self.position == Position::FollowCursor
228                    && last_position != cursor_position =>
229                {
230                    if let State::Open {
231                        cursor_position: ref mut position,
232                        ..
233                    } = *state
234                    {
235                        *position = cursor_position;
236                    }
237
238                    shell.request_redraw();
239                }
240                (State::Open { .. }, None) => {
241                    *state = State::Idle;
242                    shell.invalidate_overlay();
243
244                    if !matches!(event, Event::Window(window::Event::RedrawRequested(_)),) {
245                        shell.request_redraw();
246                    }
247                }
248                (State::Open { .. }, Some(_)) | (&State::Idle, None) => (),
249            }
250        }
251
252        self.content.update(
253            &mut tree.children[0],
254            event,
255            layout,
256            cursor,
257            renderer,
258            shell,
259            viewport,
260        );
261    }
262
263    fn mouse_interaction(
264        &self,
265        tree: &widget::Tree,
266        layout: Layout,
267        cursor: mouse::Cursor,
268        viewport: &Rectangle,
269        renderer: &Renderer,
270    ) -> mouse::Interaction {
271        self.content
272            .mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer)
273    }
274
275    fn draw(
276        &self,
277        tree: &widget::Tree,
278        renderer: &mut Renderer,
279        theme: &Theme,
280        inherited_style: &renderer::Style,
281        layout: Layout,
282        cursor: mouse::Cursor,
283        viewport: &Rectangle,
284    ) {
285        self.content.draw(
286            &tree.children[0],
287            renderer,
288            theme,
289            inherited_style,
290            layout,
291            cursor,
292            viewport,
293        );
294    }
295
296    fn overlay<'b>(
297        &'b mut self,
298        tree: &'b mut widget::Tree,
299        layout: Layout,
300        renderer: &Renderer,
301        viewport: &Rectangle,
302        translation: Vector,
303        window: Size,
304    ) -> Vec<overlay::Element<'b, Message, Theme, Renderer>> {
305        let state = tree.state.downcast_mut::<State>();
306
307        let mut children = tree.children.iter_mut();
308
309        let content = self.content.overlay(
310            children.next().unwrap(),
311            layout,
312            renderer,
313            viewport,
314            translation,
315            window,
316        );
317
318        let tooltip_tree = children.next().unwrap();
319
320        // (Re)compute the tooltip's node if it was cleared by
321        // `Widget::diff` or `Widget::update`
322        if let State::Open { needs_relayout, .. } = state
323            && *needs_relayout
324        {
325            self.tooltip.layout(
326                tooltip_tree,
327                renderer,
328                &layout::Limits::new(
329                    Size::ZERO,
330                    if self.snap_within_viewport {
331                        window
332                    } else {
333                        Size::INFINITE
334                    },
335                ),
336            );
337
338            *needs_relayout = false;
339        }
340
341        let tooltip = if let State::Open {
342            cursor_position, ..
343        } = state
344        {
345            let position = layout.position() + translation;
346            let content_bounds = layout.bounds();
347            let tooltip_size = tooltip_tree.size;
348
349            let viewport = Rectangle::with_size(window);
350
351            let x_center = position.x + (content_bounds.width - tooltip_size.width) / 2.0;
352            let y_center = position.y + (content_bounds.height - tooltip_size.height) / 2.0;
353
354            let mut tooltip_bounds = {
355                let position = match self.position {
356                    Position::Top => {
357                        Point::new(x_center, position.y - tooltip_size.height - self.gap)
358                    }
359                    Position::Bottom => {
360                        Point::new(x_center, position.y + content_bounds.height + self.gap)
361                    }
362                    Position::Left => {
363                        Point::new(position.x - tooltip_size.width - self.gap, y_center)
364                    }
365                    Position::Right => {
366                        Point::new(position.x + content_bounds.width + self.gap, y_center)
367                    }
368                    Position::FollowCursor => {
369                        let translation = position - content_bounds.position();
370
371                        Point::new(cursor_position.x, cursor_position.y - tooltip_size.height)
372                            + translation
373                    }
374                };
375
376                Rectangle::new(position, tooltip_size)
377            };
378
379            if self.snap_within_viewport {
380                if tooltip_bounds.x < viewport.x {
381                    tooltip_bounds.x = viewport.x;
382                } else if viewport.x + viewport.width < tooltip_bounds.x + tooltip_bounds.width {
383                    tooltip_bounds.x = viewport.x + viewport.width - tooltip_bounds.width;
384                }
385
386                if tooltip_bounds.y < viewport.y {
387                    tooltip_bounds.y = viewport.y;
388                } else if viewport.y + viewport.height < tooltip_bounds.y + tooltip_bounds.height {
389                    tooltip_bounds.y = viewport.y + viewport.height - tooltip_bounds.height;
390                }
391            }
392
393            Some(overlay::Element::new(Box::new(Overlay {
394                layout: Layout::new(tooltip_tree.size).move_to(tooltip_bounds.position()),
395                tooltip: &mut self.tooltip,
396                tree: tooltip_tree,
397                class: &self.class,
398                window,
399            })))
400        } else {
401            None
402        };
403
404        content.into_iter().chain(tooltip).collect()
405    }
406
407    fn operate(
408        &mut self,
409        tree: &mut widget::Tree,
410        layout: Layout,
411        viewport: &Rectangle,
412        renderer: &Renderer,
413        operation: &mut dyn widget::Operation,
414    ) {
415        operation.container(None, layout.bounds(), viewport);
416        operation.traverse(&mut |operation| {
417            self.content
418                .operate(&mut tree.children[0], layout, viewport, renderer, operation);
419        });
420    }
421}
422
423/// The position of the tooltip. Defaults to following the cursor.
424#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
425pub enum Position {
426    /// The tooltip will appear on the top of the widget.
427    #[default]
428    Top,
429    /// The tooltip will appear on the bottom of the widget.
430    Bottom,
431    /// The tooltip will appear on the left of the widget.
432    Left,
433    /// The tooltip will appear on the right of the widget.
434    Right,
435    /// The tooltip will follow the cursor.
436    FollowCursor,
437}
438
439#[derive(Debug, Clone, PartialEq, Default)]
440enum State {
441    #[default]
442    Idle,
443    Hovered {
444        at: Instant,
445    },
446    Open {
447        cursor_position: Point,
448        needs_relayout: bool,
449    },
450}
451
452struct Overlay<'a, 'b, V, Theme>
453where
454    Theme: container::Catalog,
455{
456    layout: Layout,
457    tooltip: &'b mut V,
458    tree: &'b mut widget::Tree,
459    class: &'b Theme::Class<'a>,
460    window: Size,
461}
462
463impl<V, Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
464    for Overlay<'_, '_, V, Theme>
465where
466    Theme: container::Catalog,
467    Renderer: text::Renderer,
468    V: Widget<Message, Theme, Renderer>,
469{
470    fn operate(&mut self, renderer: &Renderer, operation: &mut dyn widget::Operation) {
471        operation.container(None, self.layout.bounds(), &self.layout.bounds());
472
473        operation.traverse(&mut |operation| {
474            self.tooltip.operate(
475                self.tree,
476                self.layout,
477                &Rectangle::with_size(self.window),
478                renderer,
479                operation,
480            );
481        });
482    }
483
484    fn draw(
485        &self,
486        renderer: &mut Renderer,
487        theme: &Theme,
488        inherited_style: &renderer::Style,
489        cursor_position: mouse::Cursor,
490    ) {
491        let viewport = Rectangle::with_size(self.window);
492        let bounds = self.layout.bounds();
493        let style = theme.style(self.class);
494
495        renderer.with_layer(viewport, |renderer| {
496            container::draw_background(renderer, &style, bounds);
497
498            let defaults = renderer::Style {
499                text_color: style.text_color.unwrap_or(inherited_style.text_color),
500            };
501
502            self.tooltip.draw(
503                self.tree,
504                renderer,
505                theme,
506                &defaults,
507                self.layout,
508                cursor_position,
509                &viewport,
510            );
511        });
512    }
513}