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