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::{
32    Clipboard, Element, Event, Length, Padding, Pixels, Point, Rectangle,
33    Shell, Size, Vector,
34};
35
36/// An element to display a widget over another.
37///
38/// # Example
39/// ```no_run
40/// # mod iced { pub mod widget { pub use iced_widget::*; } }
41/// # pub type State = ();
42/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
43/// use iced::widget::{container, tooltip};
44///
45/// enum Message {
46///     // ...
47/// }
48///
49/// fn view(_state: &State) -> Element<'_, Message> {
50///     tooltip(
51///         "Hover me to display the tooltip!",
52///         container("This is the tooltip contents!")
53///             .padding(10)
54///             .style(container::rounded_box),
55///         tooltip::Position::Bottom,
56///     ).into()
57/// }
58/// ```
59#[allow(missing_debug_implementations)]
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        &self,
183        tree: &mut widget::Tree,
184        renderer: &Renderer,
185        limits: &layout::Limits,
186    ) -> layout::Node {
187        self.content
188            .as_widget()
189            .layout(&mut tree.children[0], renderer, limits)
190    }
191
192    fn update(
193        &mut self,
194        tree: &mut widget::Tree,
195        event: &Event,
196        layout: Layout<'_>,
197        cursor: mouse::Cursor,
198        renderer: &Renderer,
199        clipboard: &mut dyn Clipboard,
200        shell: &mut Shell<'_, Message>,
201        viewport: &Rectangle,
202    ) {
203        let state = tree.state.downcast_mut::<State>();
204
205        let was_idle = *state == State::Idle;
206
207        *state = cursor
208            .position_over(layout.bounds())
209            .map(|cursor_position| State::Hovered { cursor_position })
210            .unwrap_or_default();
211
212        let is_idle = *state == State::Idle;
213
214        if was_idle != is_idle {
215            shell.invalidate_layout();
216            shell.request_redraw();
217        } else if !is_idle && self.position == Position::FollowCursor {
218            shell.request_redraw();
219        }
220
221        self.content.as_widget_mut().update(
222            &mut tree.children[0],
223            event,
224            layout,
225            cursor,
226            renderer,
227            clipboard,
228            shell,
229            viewport,
230        );
231    }
232
233    fn mouse_interaction(
234        &self,
235        tree: &widget::Tree,
236        layout: Layout<'_>,
237        cursor: mouse::Cursor,
238        viewport: &Rectangle,
239        renderer: &Renderer,
240    ) -> mouse::Interaction {
241        self.content.as_widget().mouse_interaction(
242            &tree.children[0],
243            layout,
244            cursor,
245            viewport,
246            renderer,
247        )
248    }
249
250    fn draw(
251        &self,
252        tree: &widget::Tree,
253        renderer: &mut Renderer,
254        theme: &Theme,
255        inherited_style: &renderer::Style,
256        layout: Layout<'_>,
257        cursor: mouse::Cursor,
258        viewport: &Rectangle,
259    ) {
260        self.content.as_widget().draw(
261            &tree.children[0],
262            renderer,
263            theme,
264            inherited_style,
265            layout,
266            cursor,
267            viewport,
268        );
269    }
270
271    fn overlay<'b>(
272        &'b mut self,
273        tree: &'b mut widget::Tree,
274        layout: Layout<'b>,
275        renderer: &Renderer,
276        viewport: &Rectangle,
277        translation: Vector,
278    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
279        let state = tree.state.downcast_ref::<State>();
280
281        let mut children = tree.children.iter_mut();
282
283        let content = self.content.as_widget_mut().overlay(
284            children.next().unwrap(),
285            layout,
286            renderer,
287            viewport,
288            translation,
289        );
290
291        let tooltip = if let State::Hovered { cursor_position } = *state {
292            Some(overlay::Element::new(Box::new(Overlay {
293                position: layout.position() + translation,
294                tooltip: &self.tooltip,
295                state: children.next().unwrap(),
296                cursor_position,
297                content_bounds: layout.bounds(),
298                snap_within_viewport: self.snap_within_viewport,
299                positioning: self.position,
300                gap: self.gap,
301                padding: self.padding,
302                class: &self.class,
303            })))
304        } else {
305            None
306        };
307
308        if content.is_some() || tooltip.is_some() {
309            Some(
310                overlay::Group::with_children(
311                    content.into_iter().chain(tooltip).collect(),
312                )
313                .overlay(),
314            )
315        } else {
316            None
317        }
318    }
319}
320
321impl<'a, Message, Theme, Renderer> From<Tooltip<'a, Message, Theme, Renderer>>
322    for Element<'a, Message, Theme, Renderer>
323where
324    Message: 'a,
325    Theme: container::Catalog + 'a,
326    Renderer: text::Renderer + 'a,
327{
328    fn from(
329        tooltip: Tooltip<'a, Message, Theme, Renderer>,
330    ) -> Element<'a, Message, Theme, Renderer> {
331        Element::new(tooltip)
332    }
333}
334
335/// The position of the tooltip. Defaults to following the cursor.
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
337pub enum Position {
338    /// The tooltip will appear on the top of the widget.
339    #[default]
340    Top,
341    /// The tooltip will appear on the bottom of the widget.
342    Bottom,
343    /// The tooltip will appear on the left of the widget.
344    Left,
345    /// The tooltip will appear on the right of the widget.
346    Right,
347    /// The tooltip will follow the cursor.
348    FollowCursor,
349}
350
351#[derive(Debug, Clone, Copy, PartialEq, Default)]
352enum State {
353    #[default]
354    Idle,
355    Hovered {
356        cursor_position: Point,
357    },
358}
359
360struct Overlay<'a, 'b, Message, Theme, Renderer>
361where
362    Theme: container::Catalog,
363    Renderer: text::Renderer,
364{
365    position: Point,
366    tooltip: &'b Element<'a, Message, Theme, Renderer>,
367    state: &'b mut widget::Tree,
368    cursor_position: Point,
369    content_bounds: Rectangle,
370    snap_within_viewport: bool,
371    positioning: Position,
372    gap: f32,
373    padding: f32,
374    class: &'b Theme::Class<'a>,
375}
376
377impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
378    for Overlay<'_, '_, Message, Theme, Renderer>
379where
380    Theme: container::Catalog,
381    Renderer: text::Renderer,
382{
383    fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
384        let viewport = Rectangle::with_size(bounds);
385
386        let tooltip_layout = self.tooltip.as_widget().layout(
387            self.state,
388            renderer,
389            &layout::Limits::new(
390                Size::ZERO,
391                if self.snap_within_viewport {
392                    viewport.size()
393                } else {
394                    Size::INFINITY
395                },
396            )
397            .shrink(Padding::new(self.padding)),
398        );
399
400        let text_bounds = tooltip_layout.bounds();
401        let x_center = self.position.x
402            + (self.content_bounds.width - text_bounds.width) / 2.0;
403        let y_center = self.position.y
404            + (self.content_bounds.height - text_bounds.height) / 2.0;
405
406        let mut tooltip_bounds = {
407            let offset = match self.positioning {
408                Position::Top => Vector::new(
409                    x_center,
410                    self.position.y
411                        - text_bounds.height
412                        - self.gap
413                        - self.padding,
414                ),
415                Position::Bottom => Vector::new(
416                    x_center,
417                    self.position.y
418                        + self.content_bounds.height
419                        + self.gap
420                        + self.padding,
421                ),
422                Position::Left => Vector::new(
423                    self.position.x
424                        - text_bounds.width
425                        - self.gap
426                        - self.padding,
427                    y_center,
428                ),
429                Position::Right => Vector::new(
430                    self.position.x
431                        + self.content_bounds.width
432                        + self.gap
433                        + self.padding,
434                    y_center,
435                ),
436                Position::FollowCursor => {
437                    let translation =
438                        self.position - self.content_bounds.position();
439
440                    Vector::new(
441                        self.cursor_position.x,
442                        self.cursor_position.y - text_bounds.height,
443                    ) + translation
444                }
445            };
446
447            Rectangle {
448                x: offset.x - self.padding,
449                y: offset.y - self.padding,
450                width: text_bounds.width + self.padding * 2.0,
451                height: text_bounds.height + self.padding * 2.0,
452            }
453        };
454
455        if self.snap_within_viewport {
456            if tooltip_bounds.x < viewport.x {
457                tooltip_bounds.x = viewport.x;
458            } else if viewport.x + viewport.width
459                < tooltip_bounds.x + tooltip_bounds.width
460            {
461                tooltip_bounds.x =
462                    viewport.x + viewport.width - tooltip_bounds.width;
463            }
464
465            if tooltip_bounds.y < viewport.y {
466                tooltip_bounds.y = viewport.y;
467            } else if viewport.y + viewport.height
468                < tooltip_bounds.y + tooltip_bounds.height
469            {
470                tooltip_bounds.y =
471                    viewport.y + viewport.height - tooltip_bounds.height;
472            }
473        }
474
475        layout::Node::with_children(
476            tooltip_bounds.size(),
477            vec![
478                tooltip_layout
479                    .translate(Vector::new(self.padding, self.padding)),
480            ],
481        )
482        .translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y))
483    }
484
485    fn draw(
486        &self,
487        renderer: &mut Renderer,
488        theme: &Theme,
489        inherited_style: &renderer::Style,
490        layout: Layout<'_>,
491        cursor_position: mouse::Cursor,
492    ) {
493        let style = theme.style(self.class);
494
495        container::draw_background(renderer, &style, layout.bounds());
496
497        let defaults = renderer::Style {
498            text_color: style.text_color.unwrap_or(inherited_style.text_color),
499        };
500
501        self.tooltip.as_widget().draw(
502            self.state,
503            renderer,
504            theme,
505            &defaults,
506            layout.children().next().unwrap(),
507            cursor_position,
508            &Rectangle::with_size(Size::INFINITY),
509        );
510    }
511}