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 diff(&mut self, tree: &mut widget::Tree) {
156        tree.diff_children(&mut [self.content.as_widget_mut(), self.tooltip.as_widget_mut()]);
157    }
158
159    fn state(&self) -> widget::tree::State {
160        widget::tree::State::new(State::default())
161    }
162
163    fn tag(&self) -> widget::tree::Tag {
164        widget::tree::Tag::of::<State>()
165    }
166
167    fn size(&self) -> Size<Length> {
168        self.content.as_widget().size()
169    }
170
171    fn layout(
172        &mut self,
173        tree: &mut widget::Tree,
174        renderer: &Renderer,
175        limits: &layout::Limits,
176    ) -> layout::Node {
177        self.content
178            .as_widget_mut()
179            .layout(&mut tree.children[0], renderer, limits)
180    }
181
182    fn update(
183        &mut self,
184        tree: &mut widget::Tree,
185        event: &Event,
186        layout: Layout<'_>,
187        cursor: mouse::Cursor,
188        renderer: &Renderer,
189        shell: &mut Shell<'_, Message>,
190        viewport: &Rectangle,
191    ) {
192        if let Event::Mouse(_) | Event::Window(window::Event::RedrawRequested(_)) = event {
193            let state = tree.state.downcast_mut::<State>();
194            let now = Instant::now();
195            let cursor_position = cursor.position_over(layout.bounds());
196
197            match (*state, cursor_position) {
198                (State::Idle, Some(cursor_position)) => {
199                    if self.delay == Duration::ZERO {
200                        *state = State::Open { cursor_position };
201                        shell.invalidate_layout();
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 { cursor_position };
216                    shell.invalidate_layout();
217                }
218                (
219                    State::Open {
220                        cursor_position: last_position,
221                    },
222                    Some(cursor_position),
223                ) if self.position == Position::FollowCursor
224                    && last_position != cursor_position =>
225                {
226                    *state = State::Open { cursor_position };
227                    shell.request_redraw();
228                }
229                (State::Open { .. }, None) => {
230                    *state = State::Idle;
231                    shell.invalidate_layout();
232
233                    if !matches!(event, Event::Window(window::Event::RedrawRequested(_)),) {
234                        shell.request_redraw();
235                    }
236                }
237                (State::Open { .. }, Some(_)) | (State::Idle, None) => (),
238            }
239        }
240
241        self.content.as_widget_mut().update(
242            &mut tree.children[0],
243            event,
244            layout,
245            cursor,
246            renderer,
247            shell,
248            viewport,
249        );
250    }
251
252    fn mouse_interaction(
253        &self,
254        tree: &widget::Tree,
255        layout: Layout<'_>,
256        cursor: mouse::Cursor,
257        viewport: &Rectangle,
258        renderer: &Renderer,
259    ) -> mouse::Interaction {
260        self.content.as_widget().mouse_interaction(
261            &tree.children[0],
262            layout,
263            cursor,
264            viewport,
265            renderer,
266        )
267    }
268
269    fn draw(
270        &self,
271        tree: &widget::Tree,
272        renderer: &mut Renderer,
273        theme: &Theme,
274        inherited_style: &renderer::Style,
275        layout: Layout<'_>,
276        cursor: mouse::Cursor,
277        viewport: &Rectangle,
278    ) {
279        self.content.as_widget().draw(
280            &tree.children[0],
281            renderer,
282            theme,
283            inherited_style,
284            layout,
285            cursor,
286            viewport,
287        );
288    }
289
290    fn overlay<'b>(
291        &'b mut self,
292        tree: &'b mut widget::Tree,
293        layout: Layout<'b>,
294        renderer: &Renderer,
295        viewport: &Rectangle,
296        translation: Vector,
297    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
298        let state = tree.state.downcast_ref::<State>();
299
300        let mut children = tree.children.iter_mut();
301
302        let content = self.content.as_widget_mut().overlay(
303            children.next().unwrap(),
304            layout,
305            renderer,
306            viewport,
307            translation,
308        );
309
310        let tooltip = if let State::Open { cursor_position } = *state {
311            Some(overlay::Element::new(Box::new(Overlay {
312                position: layout.position() + translation,
313                tooltip: &mut self.tooltip,
314                tree: children.next().unwrap(),
315                cursor_position,
316                content_bounds: layout.bounds(),
317                snap_within_viewport: self.snap_within_viewport,
318                positioning: self.position,
319                gap: self.gap,
320                padding: self.padding,
321                class: &self.class,
322            })))
323        } else {
324            None
325        };
326
327        if content.is_some() || tooltip.is_some() {
328            Some(
329                overlay::Group::with_children(content.into_iter().chain(tooltip).collect())
330                    .overlay(),
331            )
332        } else {
333            None
334        }
335    }
336
337    fn operate(
338        &mut self,
339        tree: &mut widget::Tree,
340        layout: Layout<'_>,
341        renderer: &Renderer,
342        operation: &mut dyn widget::Operation,
343    ) {
344        operation.container(None, layout.bounds());
345        operation.traverse(&mut |operation| {
346            self.content.as_widget_mut().operate(
347                &mut tree.children[0],
348                layout,
349                renderer,
350                operation,
351            );
352        });
353    }
354}
355
356impl<'a, Message, Theme, Renderer> From<Tooltip<'a, Message, Theme, Renderer>>
357    for Element<'a, Message, Theme, Renderer>
358where
359    Message: 'a,
360    Theme: container::Catalog + 'a,
361    Renderer: text::Renderer + 'a,
362{
363    fn from(
364        tooltip: Tooltip<'a, Message, Theme, Renderer>,
365    ) -> Element<'a, Message, Theme, Renderer> {
366        Element::new(tooltip)
367    }
368}
369
370/// The position of the tooltip. Defaults to following the cursor.
371#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
372pub enum Position {
373    /// The tooltip will appear on the top of the widget.
374    #[default]
375    Top,
376    /// The tooltip will appear on the bottom of the widget.
377    Bottom,
378    /// The tooltip will appear on the left of the widget.
379    Left,
380    /// The tooltip will appear on the right of the widget.
381    Right,
382    /// The tooltip will follow the cursor.
383    FollowCursor,
384}
385
386#[derive(Debug, Clone, Copy, PartialEq, Default)]
387enum State {
388    #[default]
389    Idle,
390    Hovered {
391        at: Instant,
392    },
393    Open {
394        cursor_position: Point,
395    },
396}
397
398struct Overlay<'a, 'b, Message, Theme, Renderer>
399where
400    Theme: container::Catalog,
401    Renderer: text::Renderer,
402{
403    position: Point,
404    tooltip: &'b mut Element<'a, Message, Theme, Renderer>,
405    tree: &'b mut widget::Tree,
406    cursor_position: Point,
407    content_bounds: Rectangle,
408    snap_within_viewport: bool,
409    positioning: Position,
410    gap: f32,
411    padding: f32,
412    class: &'b Theme::Class<'a>,
413}
414
415impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
416    for Overlay<'_, '_, Message, Theme, Renderer>
417where
418    Theme: container::Catalog,
419    Renderer: text::Renderer,
420{
421    fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
422        let viewport = Rectangle::with_size(bounds);
423
424        let tooltip_layout = self.tooltip.as_widget_mut().layout(
425            self.tree,
426            renderer,
427            &layout::Limits::new(
428                Size::ZERO,
429                if self.snap_within_viewport {
430                    viewport.size()
431                } else {
432                    Size::INFINITE
433                },
434            )
435            .shrink(Padding::new(self.padding)),
436        );
437
438        let text_bounds = tooltip_layout.bounds();
439        let x_center = self.position.x + (self.content_bounds.width - text_bounds.width) / 2.0;
440        let y_center = self.position.y + (self.content_bounds.height - text_bounds.height) / 2.0;
441
442        let mut tooltip_bounds = {
443            let offset = match self.positioning {
444                Position::Top => Vector::new(
445                    x_center,
446                    self.position.y - text_bounds.height - self.gap - self.padding,
447                ),
448                Position::Bottom => Vector::new(
449                    x_center,
450                    self.position.y + self.content_bounds.height + self.gap + self.padding,
451                ),
452                Position::Left => Vector::new(
453                    self.position.x - text_bounds.width - self.gap - self.padding,
454                    y_center,
455                ),
456                Position::Right => Vector::new(
457                    self.position.x + self.content_bounds.width + self.gap + self.padding,
458                    y_center,
459                ),
460                Position::FollowCursor => {
461                    let translation = self.position - self.content_bounds.position();
462
463                    Vector::new(
464                        self.cursor_position.x,
465                        self.cursor_position.y - text_bounds.height,
466                    ) + translation
467                }
468            };
469
470            Rectangle {
471                x: offset.x - self.padding,
472                y: offset.y - self.padding,
473                width: text_bounds.width + self.padding * 2.0,
474                height: text_bounds.height + self.padding * 2.0,
475            }
476        };
477
478        if self.snap_within_viewport {
479            if tooltip_bounds.x < viewport.x {
480                tooltip_bounds.x = viewport.x;
481            } else if viewport.x + viewport.width < tooltip_bounds.x + tooltip_bounds.width {
482                tooltip_bounds.x = viewport.x + viewport.width - tooltip_bounds.width;
483            }
484
485            if tooltip_bounds.y < viewport.y {
486                tooltip_bounds.y = viewport.y;
487            } else if viewport.y + viewport.height < tooltip_bounds.y + tooltip_bounds.height {
488                tooltip_bounds.y = viewport.y + viewport.height - tooltip_bounds.height;
489            }
490        }
491
492        layout::Node::with_children(
493            tooltip_bounds.size(),
494            vec![tooltip_layout.translate(Vector::new(self.padding, self.padding))],
495        )
496        .translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y))
497    }
498
499    fn draw(
500        &self,
501        renderer: &mut Renderer,
502        theme: &Theme,
503        inherited_style: &renderer::Style,
504        layout: Layout<'_>,
505        cursor_position: mouse::Cursor,
506    ) {
507        let style = theme.style(self.class);
508
509        container::draw_background(renderer, &style, layout.bounds());
510
511        let defaults = renderer::Style {
512            text_color: style.text_color.unwrap_or(inherited_style.text_color),
513        };
514
515        self.tooltip.as_widget().draw(
516            self.tree,
517            renderer,
518            theme,
519            &defaults,
520            layout.children().next().unwrap(),
521            cursor_position,
522            &Rectangle::with_size(Size::INFINITE),
523        );
524    }
525}