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