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::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
9//! # use iced::widget::Widget;
10//! # pub type State = ();
11//! use iced::widget::{container, tooltip};
12//!
13//! enum Message {
14//!     // ...
15//! }
16//!
17//! fn view(_state: &State) -> impl Widget<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//!     )
24//!     .position(tooltip::Position::Bottom)
25//! }
26//! ```
27use crate::core::layout::{self, Layout};
28use crate::core::mouse;
29use crate::core::overlay;
30use crate::core::renderer;
31use crate::core::text;
32use crate::core::time::{Duration, Instant};
33use crate::core::widget::{self, Widget};
34use crate::core::window;
35use crate::core::{Element, Event, Length, Pixels, Point, Rectangle, Shell, Size, Vector};
36
37/// An element to display a widget over another.
38///
39/// # Example
40/// ```no_run
41/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
42/// # use iced::widget::Widget;
43/// # pub type State = ();
44/// use iced::widget::{container, tooltip};
45///
46/// enum Message {
47///     // ...
48/// }
49///
50/// fn view(_state: &State) -> impl Widget<Message> {
51///     tooltip(
52///         "Hover me to display the tooltip!",
53///         container("This is the tooltip contents!")
54///             .padding(10)
55///             .style(container::rounded_box),
56///     )
57///     .position(tooltip::Position::Bottom)
58/// }
59/// ```
60pub struct Tooltip<W, V> {
61    content: W,
62    tooltip: V,
63    position: Position,
64    gap: f32,
65    snap_within_viewport: bool,
66    delay: Duration,
67}
68
69impl<W, V> Tooltip<W, V> {
70    /// Creates a new [`Tooltip`].
71    ///
72    /// [`Tooltip`]: struct.Tooltip.html
73    ///
74    /// By default, the [`Tooltip`] is positioned [`Position::Auto`], which
75    /// places it on the side of the hovered element with the most available
76    /// space.
77    pub fn new(content: W, tooltip: V) -> Self {
78        Tooltip {
79            content,
80            tooltip,
81            position: Position::default(),
82            gap: 0.0,
83            snap_within_viewport: true,
84            delay: Duration::ZERO,
85        }
86    }
87
88    /// Sets the [`Position`] of the [`Tooltip`].
89    ///
90    /// By default, the [`Tooltip`] is positioned [`Position::Auto`], which
91    /// places it on the side of the hovered element with the most available
92    /// space.
93    pub fn position(mut self, position: Position) -> Self {
94        self.position = position;
95        self
96    }
97
98    /// Sets the gap between the content and its [`Tooltip`].
99    pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
100        self.gap = gap.into().0;
101        self
102    }
103
104    /// Sets the delay before the [`Tooltip`] is shown.
105    ///
106    /// Set to [`Duration::ZERO`] to be shown immediately.
107    pub fn delay(mut self, delay: Duration) -> Self {
108        self.delay = delay;
109        self
110    }
111
112    /// Sets whether the [`Tooltip`] is snapped within the viewport.
113    pub fn snap_within_viewport(mut self, snap: bool) -> Self {
114        self.snap_within_viewport = snap;
115        self
116    }
117}
118
119impl<W, V> widget::Meta for Tooltip<W, V> {}
120
121impl<W, V, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Tooltip<W, V>
122where
123    Renderer: text::Renderer,
124    W: Widget<Message, Theme, Renderer>,
125    V: Widget<Message, Theme, Renderer>,
126{
127    fn diff(&mut self, tree: &mut widget::Tree) {
128        let state = tree.state.downcast_mut::<State>();
129
130        // The tooltip's contents may have changed, so the cached layout
131        // (if any) is no longer valid
132        if let State::Open { needs_relayout, .. } = state {
133            *needs_relayout = true;
134        }
135
136        if tree.children.len() != 2 {
137            tree.children = vec![
138                widget::Tree::new(&self.content),
139                widget::Tree::new(&self.tooltip),
140            ];
141        }
142
143        tree.children[0].diff(&mut self.content);
144        tree.children[1].diff(&mut self.tooltip);
145    }
146
147    fn state(&self) -> widget::tree::State {
148        widget::tree::State::new(State::default())
149    }
150
151    fn tag(&self) -> widget::tree::Tag {
152        widget::tree::Tag::of::<State>()
153    }
154
155    fn size(&self) -> Size<Length> {
156        self.content.size()
157    }
158
159    fn layout(&mut self, tree: &mut widget::Tree, renderer: &Renderer, limits: &layout::Limits) {
160        self.content.layout(&mut tree.children[0], renderer, limits);
161
162        tree.size = tree.children[0].size;
163    }
164
165    fn update(
166        &mut self,
167        tree: &mut widget::Tree,
168        event: &Event,
169        layout: Layout,
170        cursor: mouse::Cursor,
171        renderer: &Renderer,
172        shell: &mut Shell<'_, Message>,
173        viewport: &Rectangle,
174    ) {
175        if let Event::Mouse(_) | Event::Window(window::Event::RedrawRequested(_)) = event {
176            let state = tree.state.downcast_mut::<State>();
177            let now = Instant::now();
178            let cursor_position = cursor.position_over(layout.bounds());
179
180            match (&*state, cursor_position) {
181                (State::Idle, Some(cursor_position)) => {
182                    if self.delay == Duration::ZERO {
183                        *state = State::Open {
184                            cursor_position,
185                            needs_relayout: true,
186                        };
187                        shell.invalidate_overlay();
188                    } else {
189                        *state = State::Hovered { at: now };
190                    }
191
192                    shell.request_redraw_at(now + self.delay);
193                }
194                (State::Hovered { .. }, None) => {
195                    *state = State::Idle;
196                }
197                (State::Hovered { at, .. }, _) if at.elapsed() < self.delay => {
198                    shell.request_redraw_at(now + self.delay - at.elapsed());
199                }
200                (State::Hovered { .. }, Some(cursor_position)) => {
201                    *state = State::Open {
202                        cursor_position,
203                        needs_relayout: true,
204                    };
205                    shell.invalidate_overlay();
206                }
207                (
208                    &State::Open {
209                        cursor_position: last_position,
210                        ..
211                    },
212                    Some(cursor_position),
213                ) if self.position == Position::FollowCursor
214                    && last_position != cursor_position =>
215                {
216                    if let State::Open {
217                        cursor_position: ref mut position,
218                        ..
219                    } = *state
220                    {
221                        *position = cursor_position;
222                    }
223
224                    shell.request_redraw();
225                }
226                (State::Open { .. }, None) => {
227                    *state = State::Idle;
228                    shell.invalidate_overlay();
229
230                    if !matches!(event, Event::Window(window::Event::RedrawRequested(_)),) {
231                        shell.request_redraw();
232                    }
233                }
234                (State::Open { .. }, Some(_)) | (&State::Idle, None) => (),
235            }
236        }
237
238        self.content.update(
239            &mut tree.children[0],
240            event,
241            layout,
242            cursor,
243            renderer,
244            shell,
245            viewport,
246        );
247    }
248
249    fn mouse_interaction(
250        &self,
251        tree: &widget::Tree,
252        layout: Layout,
253        cursor: mouse::Cursor,
254        viewport: &Rectangle,
255        renderer: &Renderer,
256    ) -> mouse::Interaction {
257        self.content
258            .mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer)
259    }
260
261    fn draw(
262        &self,
263        tree: &widget::Tree,
264        renderer: &mut Renderer,
265        theme: &Theme,
266        inherited_style: &renderer::Style,
267        layout: Layout,
268        cursor: mouse::Cursor,
269        viewport: &Rectangle,
270    ) {
271        self.content.draw(
272            &tree.children[0],
273            renderer,
274            theme,
275            inherited_style,
276            layout,
277            cursor,
278            viewport,
279        );
280    }
281
282    fn overlay<'b>(
283        &'b mut self,
284        tree: &'b mut widget::Tree,
285        layout: Layout,
286        renderer: &Renderer,
287        viewport: &Rectangle,
288        translation: Vector,
289        window: Size,
290    ) -> Vec<overlay::Element<'b, Message, Theme, Renderer>> {
291        let state = tree.state.downcast_mut::<State>();
292
293        let mut children = tree.children.iter_mut();
294
295        let content = self.content.overlay(
296            children.next().unwrap(),
297            layout,
298            renderer,
299            viewport,
300            translation,
301            window,
302        );
303
304        let tooltip_tree = children.next().unwrap();
305
306        // (Re)compute the tooltip's layout if it was invalidated by
307        // `Widget::diff`
308        if let State::Open { needs_relayout, .. } = state
309            && *needs_relayout
310        {
311            self.tooltip.layout(
312                tooltip_tree,
313                renderer,
314                &layout::Limits::new(
315                    Size::ZERO,
316                    if self.snap_within_viewport {
317                        window
318                    } else {
319                        Size::INFINITE
320                    },
321                ),
322            );
323
324            *needs_relayout = false;
325        }
326
327        let tooltip = if let State::Open {
328            cursor_position, ..
329        } = &*state
330        {
331            let content_bounds = layout.bounds() + translation;
332            let cursor_position = *cursor_position + translation;
333            let tooltip_size = tooltip_tree.size;
334
335            let viewport = Rectangle::with_size(window);
336
337            let tooltip_bounds = self.position.resolve(
338                content_bounds,
339                tooltip_size,
340                self.gap,
341                cursor_position,
342                viewport,
343                self.snap_within_viewport,
344            );
345
346            Some(overlay::Element::new(Box::new(Overlay {
347                layout: Layout::new(tooltip_size).move_to(tooltip_bounds.position()),
348                tooltip: &mut self.tooltip,
349                tree: tooltip_tree,
350                window,
351            })))
352        } else {
353            None
354        };
355
356        content.into_iter().chain(tooltip).collect()
357    }
358
359    fn operate(
360        &mut self,
361        tree: &mut widget::Tree,
362        layout: Layout,
363        viewport: &Rectangle,
364        renderer: &Renderer,
365        operation: &mut dyn widget::Operation,
366    ) {
367        operation.container(None, layout.bounds(), viewport);
368        operation.traverse(&mut |operation| {
369            self.content
370                .operate(&mut tree.children[0], layout, viewport, renderer, operation);
371        });
372    }
373}
374
375impl<'a, W, V, Message, Theme, Renderer> From<Tooltip<W, V>>
376    for Element<'a, Message, Theme, Renderer>
377where
378    Message: 'a,
379    Theme: 'a,
380    Renderer: text::Renderer + 'a,
381    W: Widget<Message, Theme, Renderer> + 'a,
382    V: Widget<Message, Theme, Renderer> + 'a,
383{
384    fn from(tooltip: Tooltip<W, V>) -> Element<'a, Message, Theme, Renderer> {
385        tooltip._boxed()
386    }
387}
388
389/// The position of the tooltip.
390pub use crate::overlay::Position;
391
392#[derive(Debug, Clone, PartialEq, Default)]
393enum State {
394    #[default]
395    Idle,
396    Hovered {
397        at: Instant,
398    },
399    Open {
400        cursor_position: Point,
401        needs_relayout: bool,
402    },
403}
404
405struct Overlay<'b, V> {
406    layout: Layout,
407    tooltip: &'b mut V,
408    tree: &'b mut widget::Tree,
409    window: Size,
410}
411
412impl<V, Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer> for Overlay<'_, V>
413where
414    Renderer: text::Renderer,
415    V: Widget<Message, Theme, Renderer>,
416{
417    fn update(
418        &mut self,
419        event: &Event,
420        cursor: mouse::Cursor,
421        renderer: &Renderer,
422        shell: &mut Shell<'_, Message>,
423    ) {
424        self.tooltip.update(
425            self.tree,
426            event,
427            self.layout,
428            cursor,
429            renderer,
430            shell,
431            &Rectangle::with_size(self.window),
432        );
433    }
434
435    fn mouse_interaction(&self, cursor: mouse::Cursor, renderer: &Renderer) -> mouse::Interaction {
436        self.tooltip.mouse_interaction(
437            self.tree,
438            self.layout,
439            cursor,
440            &Rectangle::with_size(self.window),
441            renderer,
442        )
443    }
444
445    fn draw(
446        &self,
447        renderer: &mut Renderer,
448        theme: &Theme,
449        inherited_style: &renderer::Style,
450        cursor_position: mouse::Cursor,
451    ) {
452        // The tooltip content is drawn directly; users are responsible for
453        // styling it.
454        let viewport = Rectangle::with_size(self.window);
455
456        renderer.with_layer(viewport, |renderer| {
457            self.tooltip.draw(
458                self.tree,
459                renderer,
460                theme,
461                inherited_style,
462                self.layout,
463                cursor_position,
464                &viewport,
465            );
466        });
467    }
468
469    fn operate(&mut self, renderer: &Renderer, operation: &mut dyn widget::Operation) {
470        let viewport = Rectangle::with_size(self.window);
471
472        operation.container(None, self.layout.bounds(), &viewport);
473
474        operation.traverse(&mut |operation| {
475            self.tooltip
476                .operate(self.tree, self.layout, &viewport, renderer, operation);
477        });
478    }
479
480    fn overlay<'c>(
481        &'c mut self,
482        renderer: &Renderer,
483    ) -> Vec<overlay::Element<'c, Message, Theme, Renderer>> {
484        self.tooltip.overlay(
485            self.tree,
486            self.layout,
487            renderer,
488            &Rectangle::with_size(self.window),
489            Vector::ZERO,
490            self.window,
491        )
492    }
493}