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