iced_widget/
tooltip.rs

1//! Tooltips display a hint of information over some element when hovered.
2//!
3//! # Example
4//! ```no_run
5//! # mod iced { pub mod widget { pub use iced_widget::*; } }
6//! # pub type State = ();
7//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
8//! use iced::widget::{container, tooltip};
9//!
10//! enum Message {
11//!     // ...
12//! }
13//!
14//! fn view(_state: &State) -> Element<'_, Message> {
15//!     tooltip(
16//!         "Hover me to display the tooltip!",
17//!         container("This is the tooltip contents!")
18//!             .padding(10)
19//!             .style(container::rounded_box),
20//!         tooltip::Position::Bottom,
21//!     ).into()
22//! }
23//! ```
24use crate::container;
25use crate::core::layout::{self, Layout};
26use crate::core::mouse;
27use crate::core::overlay;
28use crate::core::renderer;
29use crate::core::text;
30use crate::core::widget::{self, Widget};
31use crate::core::window;
32use crate::core::{
33    Clipboard, Element, Event, Length, Padding, Pixels, Point, Rectangle,
34    Shell, Size, Vector,
35};
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::*; } }
42/// # pub type State = ();
43/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
44/// use iced::widget::{container, tooltip};
45///
46/// enum Message {
47///     // ...
48/// }
49///
50/// fn view(_state: &State) -> Element<'_, 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///         tooltip::Position::Bottom,
57///     ).into()
58/// }
59/// ```
60pub struct Tooltip<
61    'a,
62    Message,
63    Theme = crate::Theme,
64    Renderer = crate::Renderer,
65> where
66    Theme: container::Catalog,
67    Renderer: text::Renderer,
68{
69    content: Element<'a, Message, Theme, Renderer>,
70    tooltip: Element<'a, Message, Theme, Renderer>,
71    position: Position,
72    gap: f32,
73    padding: f32,
74    snap_within_viewport: bool,
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            class: Theme::default(),
102        }
103    }
104
105    /// Sets the gap between the content and its [`Tooltip`].
106    pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
107        self.gap = gap.into().0;
108        self
109    }
110
111    /// Sets the padding of the [`Tooltip`].
112    pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
113        self.padding = padding.into().0;
114        self
115    }
116
117    /// Sets whether the [`Tooltip`] is snapped within the viewport.
118    pub fn snap_within_viewport(mut self, snap: bool) -> Self {
119        self.snap_within_viewport = snap;
120        self
121    }
122
123    /// Sets the style of the [`Tooltip`].
124    #[must_use]
125    pub fn style(
126        mut self,
127        style: impl Fn(&Theme) -> container::Style + 'a,
128    ) -> Self
129    where
130        Theme::Class<'a>: From<container::StyleFn<'a, Theme>>,
131    {
132        self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into();
133        self
134    }
135
136    /// Sets the style class of the [`Tooltip`].
137    #[cfg(feature = "advanced")]
138    #[must_use]
139    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
140        self.class = class.into();
141        self
142    }
143}
144
145impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
146    for Tooltip<'_, Message, Theme, Renderer>
147where
148    Theme: container::Catalog,
149    Renderer: text::Renderer,
150{
151    fn children(&self) -> Vec<widget::Tree> {
152        vec![
153            widget::Tree::new(&self.content),
154            widget::Tree::new(&self.tooltip),
155        ]
156    }
157
158    fn diff(&self, tree: &mut widget::Tree) {
159        tree.diff_children(&[
160            self.content.as_widget(),
161            self.tooltip.as_widget(),
162        ]);
163    }
164
165    fn state(&self) -> widget::tree::State {
166        widget::tree::State::new(State::default())
167    }
168
169    fn tag(&self) -> widget::tree::Tag {
170        widget::tree::Tag::of::<State>()
171    }
172
173    fn size(&self) -> Size<Length> {
174        self.content.as_widget().size()
175    }
176
177    fn size_hint(&self) -> Size<Length> {
178        self.content.as_widget().size_hint()
179    }
180
181    fn layout(
182        &mut self,
183        tree: &mut widget::Tree,
184        renderer: &Renderer,
185        limits: &layout::Limits,
186    ) -> layout::Node {
187        self.content.as_widget_mut().layout(
188            &mut tree.children[0],
189            renderer,
190            limits,
191        )
192    }
193
194    fn update(
195        &mut self,
196        tree: &mut widget::Tree,
197        event: &Event,
198        layout: Layout<'_>,
199        cursor: mouse::Cursor,
200        renderer: &Renderer,
201        clipboard: &mut dyn Clipboard,
202        shell: &mut Shell<'_, Message>,
203        viewport: &Rectangle,
204    ) {
205        if let Event::Mouse(_)
206        | Event::Window(window::Event::RedrawRequested(_)) = event
207        {
208            let state = tree.state.downcast_mut::<State>();
209            let previous_state = *state;
210            let was_idle = *state == State::Idle;
211
212            *state = cursor
213                .position_over(layout.bounds())
214                .map(|cursor_position| State::Hovered { cursor_position })
215                .unwrap_or_default();
216
217            let is_idle = *state == State::Idle;
218
219            if was_idle != is_idle {
220                shell.invalidate_layout();
221                shell.request_redraw();
222            } else if self.position == Position::FollowCursor
223                && *state != previous_state
224            {
225                shell.request_redraw();
226            }
227        }
228
229        self.content.as_widget_mut().update(
230            &mut tree.children[0],
231            event,
232            layout,
233            cursor,
234            renderer,
235            clipboard,
236            shell,
237            viewport,
238        );
239    }
240
241    fn mouse_interaction(
242        &self,
243        tree: &widget::Tree,
244        layout: Layout<'_>,
245        cursor: mouse::Cursor,
246        viewport: &Rectangle,
247        renderer: &Renderer,
248    ) -> mouse::Interaction {
249        self.content.as_widget().mouse_interaction(
250            &tree.children[0],
251            layout,
252            cursor,
253            viewport,
254            renderer,
255        )
256    }
257
258    fn draw(
259        &self,
260        tree: &widget::Tree,
261        renderer: &mut Renderer,
262        theme: &Theme,
263        inherited_style: &renderer::Style,
264        layout: Layout<'_>,
265        cursor: mouse::Cursor,
266        viewport: &Rectangle,
267    ) {
268        self.content.as_widget().draw(
269            &tree.children[0],
270            renderer,
271            theme,
272            inherited_style,
273            layout,
274            cursor,
275            viewport,
276        );
277    }
278
279    fn overlay<'b>(
280        &'b mut self,
281        tree: &'b mut widget::Tree,
282        layout: Layout<'b>,
283        renderer: &Renderer,
284        viewport: &Rectangle,
285        translation: Vector,
286    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
287        let state = tree.state.downcast_ref::<State>();
288
289        let mut children = tree.children.iter_mut();
290
291        let content = self.content.as_widget_mut().overlay(
292            children.next().unwrap(),
293            layout,
294            renderer,
295            viewport,
296            translation,
297        );
298
299        let tooltip = if let State::Hovered { cursor_position } = *state {
300            Some(overlay::Element::new(Box::new(Overlay {
301                position: layout.position() + translation,
302                tooltip: &mut self.tooltip,
303                state: children.next().unwrap(),
304                cursor_position,
305                content_bounds: layout.bounds(),
306                snap_within_viewport: self.snap_within_viewport,
307                positioning: self.position,
308                gap: self.gap,
309                padding: self.padding,
310                class: &self.class,
311            })))
312        } else {
313            None
314        };
315
316        if content.is_some() || tooltip.is_some() {
317            Some(
318                overlay::Group::with_children(
319                    content.into_iter().chain(tooltip).collect(),
320                )
321                .overlay(),
322            )
323        } else {
324            None
325        }
326    }
327}
328
329impl<'a, Message, Theme, Renderer> From<Tooltip<'a, Message, Theme, Renderer>>
330    for Element<'a, Message, Theme, Renderer>
331where
332    Message: 'a,
333    Theme: container::Catalog + 'a,
334    Renderer: text::Renderer + 'a,
335{
336    fn from(
337        tooltip: Tooltip<'a, Message, Theme, Renderer>,
338    ) -> Element<'a, Message, Theme, Renderer> {
339        Element::new(tooltip)
340    }
341}
342
343/// The position of the tooltip. Defaults to following the cursor.
344#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
345pub enum Position {
346    /// The tooltip will appear on the top of the widget.
347    #[default]
348    Top,
349    /// The tooltip will appear on the bottom of the widget.
350    Bottom,
351    /// The tooltip will appear on the left of the widget.
352    Left,
353    /// The tooltip will appear on the right of the widget.
354    Right,
355    /// The tooltip will follow the cursor.
356    FollowCursor,
357}
358
359#[derive(Debug, Clone, Copy, PartialEq, Default)]
360enum State {
361    #[default]
362    Idle,
363    Hovered {
364        cursor_position: Point,
365    },
366}
367
368struct Overlay<'a, 'b, Message, Theme, Renderer>
369where
370    Theme: container::Catalog,
371    Renderer: text::Renderer,
372{
373    position: Point,
374    tooltip: &'b mut Element<'a, Message, Theme, Renderer>,
375    state: &'b mut widget::Tree,
376    cursor_position: Point,
377    content_bounds: Rectangle,
378    snap_within_viewport: bool,
379    positioning: Position,
380    gap: f32,
381    padding: f32,
382    class: &'b Theme::Class<'a>,
383}
384
385impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
386    for Overlay<'_, '_, Message, Theme, Renderer>
387where
388    Theme: container::Catalog,
389    Renderer: text::Renderer,
390{
391    fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
392        let viewport = Rectangle::with_size(bounds);
393
394        let tooltip_layout = self.tooltip.as_widget_mut().layout(
395            self.state,
396            renderer,
397            &layout::Limits::new(
398                Size::ZERO,
399                if self.snap_within_viewport {
400                    viewport.size()
401                } else {
402                    Size::INFINITE
403                },
404            )
405            .shrink(Padding::new(self.padding)),
406        );
407
408        let text_bounds = tooltip_layout.bounds();
409        let x_center = self.position.x
410            + (self.content_bounds.width - text_bounds.width) / 2.0;
411        let y_center = self.position.y
412            + (self.content_bounds.height - text_bounds.height) / 2.0;
413
414        let mut tooltip_bounds = {
415            let offset = match self.positioning {
416                Position::Top => Vector::new(
417                    x_center,
418                    self.position.y
419                        - text_bounds.height
420                        - self.gap
421                        - self.padding,
422                ),
423                Position::Bottom => Vector::new(
424                    x_center,
425                    self.position.y
426                        + self.content_bounds.height
427                        + self.gap
428                        + self.padding,
429                ),
430                Position::Left => Vector::new(
431                    self.position.x
432                        - text_bounds.width
433                        - self.gap
434                        - self.padding,
435                    y_center,
436                ),
437                Position::Right => Vector::new(
438                    self.position.x
439                        + self.content_bounds.width
440                        + self.gap
441                        + self.padding,
442                    y_center,
443                ),
444                Position::FollowCursor => {
445                    let translation =
446                        self.position - self.content_bounds.position();
447
448                    Vector::new(
449                        self.cursor_position.x,
450                        self.cursor_position.y - text_bounds.height,
451                    ) + translation
452                }
453            };
454
455            Rectangle {
456                x: offset.x - self.padding,
457                y: offset.y - self.padding,
458                width: text_bounds.width + self.padding * 2.0,
459                height: text_bounds.height + self.padding * 2.0,
460            }
461        };
462
463        if self.snap_within_viewport {
464            if tooltip_bounds.x < viewport.x {
465                tooltip_bounds.x = viewport.x;
466            } else if viewport.x + viewport.width
467                < tooltip_bounds.x + tooltip_bounds.width
468            {
469                tooltip_bounds.x =
470                    viewport.x + viewport.width - tooltip_bounds.width;
471            }
472
473            if tooltip_bounds.y < viewport.y {
474                tooltip_bounds.y = viewport.y;
475            } else if viewport.y + viewport.height
476                < tooltip_bounds.y + tooltip_bounds.height
477            {
478                tooltip_bounds.y =
479                    viewport.y + viewport.height - tooltip_bounds.height;
480            }
481        }
482
483        layout::Node::with_children(
484            tooltip_bounds.size(),
485            vec![
486                tooltip_layout
487                    .translate(Vector::new(self.padding, self.padding)),
488            ],
489        )
490        .translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y))
491    }
492
493    fn draw(
494        &self,
495        renderer: &mut Renderer,
496        theme: &Theme,
497        inherited_style: &renderer::Style,
498        layout: Layout<'_>,
499        cursor_position: mouse::Cursor,
500    ) {
501        let style = theme.style(self.class);
502
503        container::draw_background(renderer, &style, layout.bounds());
504
505        let defaults = renderer::Style {
506            text_color: style.text_color.unwrap_or(inherited_style.text_color),
507        };
508
509        self.tooltip.as_widget().draw(
510            self.state,
511            renderer,
512            theme,
513            &defaults,
514            layout.children().next().unwrap(),
515            cursor_position,
516            &Rectangle::with_size(Size::INFINITE),
517        );
518    }
519}