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