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::*; } }
9//! # pub type State = ();
10//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
11//! use iced::widget::{container, tooltip};
12//!
13//! enum Message {
14//!     // ...
15//! }
16//!
17//! fn view(_state: &State) -> Element<'_, 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//!     ).into()
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::operation::Focusable;
35use crate::core::widget::{self, Widget};
36use crate::core::window;
37use crate::core::{Element, Event, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Vector};
38
39/// An element to display a widget over another.
40///
41/// # Example
42/// ```no_run
43/// # mod iced { pub mod widget { pub use iced_widget::*; } }
44/// # pub type State = ();
45/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
46/// use iced::widget::{container, tooltip};
47///
48/// enum Message {
49///     // ...
50/// }
51///
52/// fn view(_state: &State) -> Element<'_, Message> {
53///     tooltip(
54///         "Hover me to display the tooltip!",
55///         container("This is the tooltip contents!")
56///             .padding(10)
57///             .style(container::rounded_box),
58///         tooltip::Position::Bottom,
59///     ).into()
60/// }
61/// ```
62pub struct Tooltip<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
63where
64    Theme: container::Catalog,
65    Renderer: text::Renderer,
66{
67    content: Element<'a, Message, Theme, Renderer>,
68    tooltip: Element<'a, Message, Theme, Renderer>,
69    position: Position,
70    gap: f32,
71    padding: f32,
72    snap_within_viewport: bool,
73    delay: Duration,
74    class: Theme::Class<'a>,
75}
76
77impl<'a, Message, Theme, Renderer> Tooltip<'a, Message, Theme, Renderer>
78where
79    Theme: container::Catalog,
80    Renderer: text::Renderer,
81{
82    /// The default padding of a [`Tooltip`] drawn by this renderer.
83    const DEFAULT_PADDING: f32 = 5.0;
84
85    /// Creates a new [`Tooltip`].
86    ///
87    /// [`Tooltip`]: struct.Tooltip.html
88    pub fn new(
89        content: impl Into<Element<'a, Message, Theme, Renderer>>,
90        tooltip: impl Into<Element<'a, Message, Theme, Renderer>>,
91        position: Position,
92    ) -> Self {
93        Tooltip {
94            content: content.into(),
95            tooltip: tooltip.into(),
96            position,
97            gap: 0.0,
98            padding: Self::DEFAULT_PADDING,
99            snap_within_viewport: true,
100            delay: Duration::ZERO,
101            class: Theme::default(),
102        }
103    }
104
105    /// Sets the gap between the content and its [`Tooltip`].
106    pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
107        self.gap = gap.into().0;
108        self
109    }
110
111    /// Sets the padding of the [`Tooltip`].
112    pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
113        self.padding = padding.into().0;
114        self
115    }
116
117    /// Sets the delay before the [`Tooltip`] is shown.
118    ///
119    /// Set to [`Duration::ZERO`] to be shown immediately.
120    pub fn delay(mut self, delay: Duration) -> Self {
121        self.delay = delay;
122        self
123    }
124
125    /// Sets whether the [`Tooltip`] is snapped within the viewport.
126    pub fn snap_within_viewport(mut self, snap: bool) -> Self {
127        self.snap_within_viewport = snap;
128        self
129    }
130
131    /// Sets the style of the [`Tooltip`].
132    #[must_use]
133    pub fn style(mut self, style: impl Fn(&Theme) -> container::Style + 'a) -> Self
134    where
135        Theme::Class<'a>: From<container::StyleFn<'a, Theme>>,
136    {
137        self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into();
138        self
139    }
140
141    /// Sets the style class of the [`Tooltip`].
142    #[cfg(feature = "advanced")]
143    #[must_use]
144    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
145        self.class = class.into();
146        self
147    }
148}
149
150impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
151    for Tooltip<'_, Message, Theme, Renderer>
152where
153    Theme: container::Catalog,
154    Renderer: text::Renderer,
155{
156    fn children(&self) -> Vec<widget::Tree> {
157        vec![
158            widget::Tree::new(&self.content),
159            widget::Tree::new(&self.tooltip),
160        ]
161    }
162
163    fn diff(&self, tree: &mut widget::Tree) {
164        tree.diff_children(&[self.content.as_widget(), self.tooltip.as_widget()]);
165    }
166
167    fn state(&self) -> widget::tree::State {
168        widget::tree::State::new(State::default())
169    }
170
171    fn tag(&self) -> widget::tree::Tag {
172        widget::tree::Tag::of::<State>()
173    }
174
175    fn size(&self) -> Size<Length> {
176        self.content.as_widget().size()
177    }
178
179    fn size_hint(&self) -> Size<Length> {
180        self.content.as_widget().size_hint()
181    }
182
183    fn layout(
184        &mut self,
185        tree: &mut widget::Tree,
186        renderer: &Renderer,
187        limits: &layout::Limits,
188    ) -> layout::Node {
189        self.content
190            .as_widget_mut()
191            .layout(&mut tree.children[0], renderer, limits)
192    }
193
194    fn update(
195        &mut self,
196        tree: &mut widget::Tree,
197        event: &Event,
198        layout: Layout<'_>,
199        cursor: mouse::Cursor,
200        renderer: &Renderer,
201        shell: &mut Shell<'_, Message>,
202        viewport: &Rectangle,
203    ) {
204        if let Event::Mouse(_) | Event::Window(window::Event::RedrawRequested(_)) = event {
205            let state = tree.state.downcast_mut::<State>();
206            let now = Instant::now();
207            let cursor_position = cursor.position_over(layout.bounds());
208
209            match (state.interaction, cursor_position) {
210                (Interaction::Idle, Some(cursor_position)) => {
211                    if self.delay == Duration::ZERO {
212                        state.interaction = Interaction::Open { cursor_position };
213                        shell.invalidate_layout();
214                    } else {
215                        state.interaction = Interaction::Hovered { at: now };
216                    }
217
218                    shell.request_redraw_at(now + self.delay);
219                }
220                (Interaction::Hovered { .. }, None) => {
221                    state.interaction = Interaction::Idle;
222                }
223                (Interaction::Hovered { at, .. }, _) if at.elapsed() < self.delay => {
224                    shell.request_redraw_at(now + self.delay - at.elapsed());
225                }
226                (Interaction::Hovered { .. }, Some(cursor_position)) => {
227                    state.interaction = Interaction::Open { cursor_position };
228                    shell.invalidate_layout();
229                }
230                (
231                    Interaction::Open {
232                        cursor_position: last_position,
233                    },
234                    Some(cursor_position),
235                ) if self.position == Position::FollowCursor
236                    && last_position != cursor_position =>
237                {
238                    state.interaction = Interaction::Open { cursor_position };
239                    shell.request_redraw();
240                }
241                (Interaction::Open { .. }, None) => {
242                    // Only close if child is not focused
243                    if !state.child_focused {
244                        state.interaction = Interaction::Idle;
245                        shell.invalidate_layout();
246
247                        if !matches!(event, Event::Window(window::Event::RedrawRequested(_)),) {
248                            shell.request_redraw();
249                        }
250                    }
251                }
252                (Interaction::Open { .. }, Some(_)) | (Interaction::Idle, None) => (),
253            }
254        }
255
256        self.content.as_widget_mut().update(
257            &mut tree.children[0],
258            event,
259            layout,
260            cursor,
261            renderer,
262            shell,
263            viewport,
264        );
265    }
266
267    fn mouse_interaction(
268        &self,
269        tree: &widget::Tree,
270        layout: Layout<'_>,
271        cursor: mouse::Cursor,
272        viewport: &Rectangle,
273        renderer: &Renderer,
274    ) -> mouse::Interaction {
275        self.content.as_widget().mouse_interaction(
276            &tree.children[0],
277            layout,
278            cursor,
279            viewport,
280            renderer,
281        )
282    }
283
284    fn draw(
285        &self,
286        tree: &widget::Tree,
287        renderer: &mut Renderer,
288        theme: &Theme,
289        inherited_style: &renderer::Style,
290        layout: Layout<'_>,
291        cursor: mouse::Cursor,
292        viewport: &Rectangle,
293    ) {
294        self.content.as_widget().draw(
295            &tree.children[0],
296            renderer,
297            theme,
298            inherited_style,
299            layout,
300            cursor,
301            viewport,
302        );
303    }
304
305    fn overlay<'b>(
306        &'b mut self,
307        tree: &'b mut widget::Tree,
308        layout: Layout<'b>,
309        renderer: &Renderer,
310        viewport: &Rectangle,
311        translation: Vector,
312    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
313        let state = tree.state.downcast_ref::<State>();
314
315        let mut children = tree.children.iter_mut();
316
317        let content = self.content.as_widget_mut().overlay(
318            children.next().unwrap(),
319            layout,
320            renderer,
321            viewport,
322            translation,
323        );
324
325        // Show the tooltip when the child is hovered (Open state) OR when
326        // the child has keyboard focus. For focus-triggered display, anchor
327        // the tooltip to the centre of the child widget.
328        let show_cursor = match state.interaction {
329            Interaction::Open { cursor_position } => Some(cursor_position),
330            _ if state.child_focused => {
331                let bounds = layout.bounds();
332                Some(bounds.center())
333            }
334            _ => None,
335        };
336
337        let tooltip = if let Some(cursor_position) = show_cursor {
338            Some(overlay::Element::new(Box::new(Overlay {
339                position: layout.position() + translation,
340                tooltip: &mut self.tooltip,
341                tree: children.next().unwrap(),
342                cursor_position,
343                content_bounds: layout.bounds(),
344                snap_within_viewport: self.snap_within_viewport,
345                positioning: self.position,
346                gap: self.gap,
347                padding: self.padding,
348                class: &self.class,
349            })))
350        } else {
351            None
352        };
353
354        if content.is_some() || tooltip.is_some() {
355            Some(
356                overlay::Group::with_children(content.into_iter().chain(tooltip).collect())
357                    .overlay(),
358            )
359        } else {
360            None
361        }
362    }
363
364    fn operate(
365        &mut self,
366        tree: &mut widget::Tree,
367        layout: Layout<'_>,
368        renderer: &Renderer,
369        operation: &mut dyn widget::Operation,
370    ) {
371        operation.container(None, layout.bounds());
372        operation.traverse(&mut |operation| {
373            self.content.as_widget_mut().operate(
374                &mut tree.children[0],
375                layout,
376                renderer,
377                operation,
378            );
379        });
380
381        // After the main operation (which may have changed focus), check
382        // whether any focusable child is currently focused and update our
383        // state so overlay() can show the tooltip for keyboard users.
384        let mut focus_check = FocusCheck(false);
385        self.content.as_widget_mut().operate(
386            &mut tree.children[0],
387            layout,
388            renderer,
389            &mut focus_check,
390        );
391
392        let state = tree.state.downcast_mut::<State>();
393        state.child_focused = focus_check.0;
394    }
395}
396
397impl<'a, Message, Theme, Renderer> From<Tooltip<'a, Message, Theme, Renderer>>
398    for Element<'a, Message, Theme, Renderer>
399where
400    Message: 'a,
401    Theme: container::Catalog + 'a,
402    Renderer: text::Renderer + 'a,
403{
404    fn from(
405        tooltip: Tooltip<'a, Message, Theme, Renderer>,
406    ) -> Element<'a, Message, Theme, Renderer> {
407        Element::new(tooltip)
408    }
409}
410
411/// The position of the tooltip. Defaults to following the cursor.
412#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
413pub enum Position {
414    /// The tooltip will appear on the top of the widget.
415    #[default]
416    Top,
417    /// The tooltip will appear on the bottom of the widget.
418    Bottom,
419    /// The tooltip will appear on the left of the widget.
420    Left,
421    /// The tooltip will appear on the right of the widget.
422    Right,
423    /// The tooltip will follow the cursor.
424    FollowCursor,
425}
426
427#[derive(Debug, Clone, Copy, PartialEq, Default)]
428enum Interaction {
429    #[default]
430    Idle,
431    Hovered {
432        at: Instant,
433    },
434    Open {
435        cursor_position: Point,
436    },
437}
438
439#[derive(Debug, Clone, Copy, PartialEq, Default)]
440struct State {
441    interaction: Interaction,
442    child_focused: bool,
443}
444
445/// A lightweight operation that checks whether any focusable child widget
446/// currently holds focus. Used by the tooltip to decide whether to show
447/// itself for keyboard-only users.
448struct FocusCheck(bool);
449
450impl widget::Operation for FocusCheck {
451    fn focusable(
452        &mut self,
453        _id: Option<&widget::Id>,
454        _bounds: Rectangle,
455        state: &mut dyn Focusable,
456    ) {
457        if state.is_focused() {
458            self.0 = true;
459        }
460    }
461
462    fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn widget::Operation)) {
463        operate(self);
464    }
465}
466
467struct Overlay<'a, 'b, Message, Theme, Renderer>
468where
469    Theme: container::Catalog,
470    Renderer: text::Renderer,
471{
472    position: Point,
473    tooltip: &'b mut Element<'a, Message, Theme, Renderer>,
474    tree: &'b mut widget::Tree,
475    cursor_position: Point,
476    content_bounds: Rectangle,
477    snap_within_viewport: bool,
478    positioning: Position,
479    gap: f32,
480    padding: f32,
481    class: &'b Theme::Class<'a>,
482}
483
484impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
485    for Overlay<'_, '_, Message, Theme, Renderer>
486where
487    Theme: container::Catalog,
488    Renderer: text::Renderer,
489{
490    fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
491        let viewport = Rectangle::with_size(bounds);
492
493        let tooltip_layout = self.tooltip.as_widget_mut().layout(
494            self.tree,
495            renderer,
496            &layout::Limits::new(
497                Size::ZERO,
498                if self.snap_within_viewport {
499                    viewport.size()
500                } else {
501                    Size::INFINITE
502                },
503            )
504            .shrink(Padding::new(self.padding)),
505        );
506
507        let text_bounds = tooltip_layout.bounds();
508        let x_center = self.position.x + (self.content_bounds.width - text_bounds.width) / 2.0;
509        let y_center = self.position.y + (self.content_bounds.height - text_bounds.height) / 2.0;
510
511        let mut tooltip_bounds = {
512            let offset = match self.positioning {
513                Position::Top => Vector::new(
514                    x_center,
515                    self.position.y - text_bounds.height - self.gap - self.padding,
516                ),
517                Position::Bottom => Vector::new(
518                    x_center,
519                    self.position.y + self.content_bounds.height + self.gap + self.padding,
520                ),
521                Position::Left => Vector::new(
522                    self.position.x - text_bounds.width - self.gap - self.padding,
523                    y_center,
524                ),
525                Position::Right => Vector::new(
526                    self.position.x + self.content_bounds.width + self.gap + self.padding,
527                    y_center,
528                ),
529                Position::FollowCursor => {
530                    let translation = self.position - self.content_bounds.position();
531
532                    Vector::new(
533                        self.cursor_position.x,
534                        self.cursor_position.y - text_bounds.height,
535                    ) + translation
536                }
537            };
538
539            Rectangle {
540                x: offset.x - self.padding,
541                y: offset.y - self.padding,
542                width: text_bounds.width + self.padding * 2.0,
543                height: text_bounds.height + self.padding * 2.0,
544            }
545        };
546
547        if self.snap_within_viewport {
548            if tooltip_bounds.x < viewport.x {
549                tooltip_bounds.x = viewport.x;
550            } else if viewport.x + viewport.width < tooltip_bounds.x + tooltip_bounds.width {
551                tooltip_bounds.x = viewport.x + viewport.width - tooltip_bounds.width;
552            }
553
554            if tooltip_bounds.y < viewport.y {
555                tooltip_bounds.y = viewport.y;
556            } else if viewport.y + viewport.height < tooltip_bounds.y + tooltip_bounds.height {
557                tooltip_bounds.y = viewport.y + viewport.height - tooltip_bounds.height;
558            }
559        }
560
561        layout::Node::with_children(
562            tooltip_bounds.size(),
563            vec![tooltip_layout.translate(Vector::new(self.padding, self.padding))],
564        )
565        .translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y))
566    }
567
568    fn draw(
569        &self,
570        renderer: &mut Renderer,
571        theme: &Theme,
572        inherited_style: &renderer::Style,
573        layout: Layout<'_>,
574        cursor_position: mouse::Cursor,
575    ) {
576        let style = theme.style(self.class);
577
578        container::draw_background(renderer, &style, layout.bounds());
579
580        let defaults = renderer::Style {
581            text_color: style.text_color.unwrap_or(inherited_style.text_color),
582        };
583
584        self.tooltip.as_widget().draw(
585            self.tree,
586            renderer,
587            theme,
588            &defaults,
589            layout.children().next().unwrap(),
590            cursor_position,
591            &Rectangle::with_size(Size::INFINITE),
592        );
593    }
594}