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