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<'_>,
275        renderer: &Renderer,
276        translation: Vector,
277    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
278        let state = tree.state.downcast_ref::<State>();
279
280        let mut children = tree.children.iter_mut();
281
282        let content = self.content.as_widget_mut().overlay(
283            children.next().unwrap(),
284            layout,
285            renderer,
286            translation,
287        );
288
289        let tooltip = if let State::Hovered { cursor_position } = *state {
290            Some(overlay::Element::new(Box::new(Overlay {
291                position: layout.position() + translation,
292                tooltip: &self.tooltip,
293                state: children.next().unwrap(),
294                cursor_position,
295                content_bounds: layout.bounds(),
296                snap_within_viewport: self.snap_within_viewport,
297                positioning: self.position,
298                gap: self.gap,
299                padding: self.padding,
300                class: &self.class,
301            })))
302        } else {
303            None
304        };
305
306        if content.is_some() || tooltip.is_some() {
307            Some(
308                overlay::Group::with_children(
309                    content.into_iter().chain(tooltip).collect(),
310                )
311                .overlay(),
312            )
313        } else {
314            None
315        }
316    }
317}
318
319impl<'a, Message, Theme, Renderer> From<Tooltip<'a, Message, Theme, Renderer>>
320    for Element<'a, Message, Theme, Renderer>
321where
322    Message: 'a,
323    Theme: container::Catalog + 'a,
324    Renderer: text::Renderer + 'a,
325{
326    fn from(
327        tooltip: Tooltip<'a, Message, Theme, Renderer>,
328    ) -> Element<'a, Message, Theme, Renderer> {
329        Element::new(tooltip)
330    }
331}
332
333/// The position of the tooltip. Defaults to following the cursor.
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
335pub enum Position {
336    /// The tooltip will appear on the top of the widget.
337    #[default]
338    Top,
339    /// The tooltip will appear on the bottom of the widget.
340    Bottom,
341    /// The tooltip will appear on the left of the widget.
342    Left,
343    /// The tooltip will appear on the right of the widget.
344    Right,
345    /// The tooltip will follow the cursor.
346    FollowCursor,
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Default)]
350enum State {
351    #[default]
352    Idle,
353    Hovered {
354        cursor_position: Point,
355    },
356}
357
358struct Overlay<'a, 'b, Message, Theme, Renderer>
359where
360    Theme: container::Catalog,
361    Renderer: text::Renderer,
362{
363    position: Point,
364    tooltip: &'b Element<'a, Message, Theme, Renderer>,
365    state: &'b mut widget::Tree,
366    cursor_position: Point,
367    content_bounds: Rectangle,
368    snap_within_viewport: bool,
369    positioning: Position,
370    gap: f32,
371    padding: f32,
372    class: &'b Theme::Class<'a>,
373}
374
375impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
376    for Overlay<'_, '_, Message, Theme, Renderer>
377where
378    Theme: container::Catalog,
379    Renderer: text::Renderer,
380{
381    fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
382        let viewport = Rectangle::with_size(bounds);
383
384        let tooltip_layout = self.tooltip.as_widget().layout(
385            self.state,
386            renderer,
387            &layout::Limits::new(
388                Size::ZERO,
389                self.snap_within_viewport
390                    .then(|| viewport.size())
391                    .unwrap_or(Size::INFINITY),
392            )
393            .shrink(Padding::new(self.padding)),
394        );
395
396        let text_bounds = tooltip_layout.bounds();
397        let x_center = self.position.x
398            + (self.content_bounds.width - text_bounds.width) / 2.0;
399        let y_center = self.position.y
400            + (self.content_bounds.height - text_bounds.height) / 2.0;
401
402        let mut tooltip_bounds = {
403            let offset = match self.positioning {
404                Position::Top => Vector::new(
405                    x_center,
406                    self.position.y
407                        - text_bounds.height
408                        - self.gap
409                        - self.padding,
410                ),
411                Position::Bottom => Vector::new(
412                    x_center,
413                    self.position.y
414                        + self.content_bounds.height
415                        + self.gap
416                        + self.padding,
417                ),
418                Position::Left => Vector::new(
419                    self.position.x
420                        - text_bounds.width
421                        - self.gap
422                        - self.padding,
423                    y_center,
424                ),
425                Position::Right => Vector::new(
426                    self.position.x
427                        + self.content_bounds.width
428                        + self.gap
429                        + self.padding,
430                    y_center,
431                ),
432                Position::FollowCursor => {
433                    let translation =
434                        self.position - self.content_bounds.position();
435
436                    Vector::new(
437                        self.cursor_position.x,
438                        self.cursor_position.y - text_bounds.height,
439                    ) + translation
440                }
441            };
442
443            Rectangle {
444                x: offset.x - self.padding,
445                y: offset.y - self.padding,
446                width: text_bounds.width + self.padding * 2.0,
447                height: text_bounds.height + self.padding * 2.0,
448            }
449        };
450
451        if self.snap_within_viewport {
452            if tooltip_bounds.x < viewport.x {
453                tooltip_bounds.x = viewport.x;
454            } else if viewport.x + viewport.width
455                < tooltip_bounds.x + tooltip_bounds.width
456            {
457                tooltip_bounds.x =
458                    viewport.x + viewport.width - tooltip_bounds.width;
459            }
460
461            if tooltip_bounds.y < viewport.y {
462                tooltip_bounds.y = viewport.y;
463            } else if viewport.y + viewport.height
464                < tooltip_bounds.y + tooltip_bounds.height
465            {
466                tooltip_bounds.y =
467                    viewport.y + viewport.height - tooltip_bounds.height;
468            }
469        }
470
471        layout::Node::with_children(
472            tooltip_bounds.size(),
473            vec![tooltip_layout
474                .translate(Vector::new(self.padding, self.padding))],
475        )
476        .translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y))
477    }
478
479    fn draw(
480        &self,
481        renderer: &mut Renderer,
482        theme: &Theme,
483        inherited_style: &renderer::Style,
484        layout: Layout<'_>,
485        cursor_position: mouse::Cursor,
486    ) {
487        let style = theme.style(self.class);
488
489        container::draw_background(renderer, &style, layout.bounds());
490
491        let defaults = renderer::Style {
492            text_color: style.text_color.unwrap_or(inherited_style.text_color),
493        };
494
495        self.tooltip.as_widget().draw(
496            self.state,
497            renderer,
498            theme,
499            &defaults,
500            layout.children().next().unwrap(),
501            cursor_position,
502            &Rectangle::with_size(Size::INFINITY),
503        );
504    }
505
506    fn is_over(
507        &self,
508        _layout: Layout<'_>,
509        _renderer: &Renderer,
510        _cursor_position: Point,
511    ) -> bool {
512        false
513    }
514}