iced_native/widget/
tooltip.rs

1//! Display a widget over another.
2use crate::event;
3use crate::layout;
4use crate::mouse;
5use crate::renderer;
6use crate::text;
7use crate::widget;
8use crate::widget::container;
9use crate::widget::overlay;
10use crate::widget::{Text, Tree};
11use crate::{
12    Clipboard, Element, Event, Layout, Length, Padding, Pixels, Point,
13    Rectangle, Shell, Size, Vector, Widget,
14};
15
16use std::borrow::Cow;
17
18/// An element to display a widget over another.
19#[allow(missing_debug_implementations)]
20pub struct Tooltip<'a, Message, Renderer: text::Renderer>
21where
22    Renderer: text::Renderer,
23    Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,
24{
25    content: Element<'a, Message, Renderer>,
26    tooltip: Text<'a, Renderer>,
27    position: Position,
28    gap: f32,
29    padding: f32,
30    snap_within_viewport: bool,
31    style: <Renderer::Theme as container::StyleSheet>::Style,
32}
33
34impl<'a, Message, Renderer> Tooltip<'a, Message, Renderer>
35where
36    Renderer: text::Renderer,
37    Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,
38{
39    /// The default padding of a [`Tooltip`] drawn by this renderer.
40    const DEFAULT_PADDING: f32 = 5.0;
41
42    /// Creates a new [`Tooltip`].
43    ///
44    /// [`Tooltip`]: struct.Tooltip.html
45    pub fn new(
46        content: impl Into<Element<'a, Message, Renderer>>,
47        tooltip: impl Into<Cow<'a, str>>,
48        position: Position,
49    ) -> Self {
50        Tooltip {
51            content: content.into(),
52            tooltip: Text::new(tooltip),
53            position,
54            gap: 0.0,
55            padding: Self::DEFAULT_PADDING,
56            snap_within_viewport: true,
57            style: Default::default(),
58        }
59    }
60
61    /// Sets the size of the text of the [`Tooltip`].
62    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
63        self.tooltip = self.tooltip.size(size);
64        self
65    }
66
67    /// Sets the font of the [`Tooltip`].
68    ///
69    /// [`Font`]: Renderer::Font
70    pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
71        self.tooltip = self.tooltip.font(font);
72        self
73    }
74
75    /// Sets the gap between the content and its [`Tooltip`].
76    pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
77        self.gap = gap.into().0;
78        self
79    }
80
81    /// Sets the padding of the [`Tooltip`].
82    pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
83        self.padding = padding.into().0;
84        self
85    }
86
87    /// Sets whether the [`Tooltip`] is snapped within the viewport.
88    pub fn snap_within_viewport(mut self, snap: bool) -> Self {
89        self.snap_within_viewport = snap;
90        self
91    }
92
93    /// Sets the style of the [`Tooltip`].
94    pub fn style(
95        mut self,
96        style: impl Into<<Renderer::Theme as container::StyleSheet>::Style>,
97    ) -> Self {
98        self.style = style.into();
99        self
100    }
101}
102
103impl<'a, Message, Renderer> Widget<Message, Renderer>
104    for Tooltip<'a, Message, Renderer>
105where
106    Renderer: text::Renderer,
107    Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,
108{
109    fn children(&self) -> Vec<Tree> {
110        vec![Tree::new(&self.content)]
111    }
112
113    fn diff(&self, tree: &mut Tree) {
114        tree.diff_children(std::slice::from_ref(&self.content))
115    }
116
117    fn width(&self) -> Length {
118        self.content.as_widget().width()
119    }
120
121    fn height(&self) -> Length {
122        self.content.as_widget().height()
123    }
124
125    fn layout(
126        &self,
127        renderer: &Renderer,
128        limits: &layout::Limits,
129    ) -> layout::Node {
130        self.content.as_widget().layout(renderer, limits)
131    }
132
133    fn on_event(
134        &mut self,
135        tree: &mut Tree,
136        event: Event,
137        layout: Layout<'_>,
138        cursor_position: Point,
139        renderer: &Renderer,
140        clipboard: &mut dyn Clipboard,
141        shell: &mut Shell<'_, Message>,
142    ) -> event::Status {
143        self.content.as_widget_mut().on_event(
144            &mut tree.children[0],
145            event,
146            layout,
147            cursor_position,
148            renderer,
149            clipboard,
150            shell,
151        )
152    }
153
154    fn mouse_interaction(
155        &self,
156        tree: &Tree,
157        layout: Layout<'_>,
158        cursor_position: Point,
159        viewport: &Rectangle,
160        renderer: &Renderer,
161    ) -> mouse::Interaction {
162        self.content.as_widget().mouse_interaction(
163            &tree.children[0],
164            layout,
165            cursor_position,
166            viewport,
167            renderer,
168        )
169    }
170
171    fn draw(
172        &self,
173        tree: &Tree,
174        renderer: &mut Renderer,
175        theme: &Renderer::Theme,
176        inherited_style: &renderer::Style,
177        layout: Layout<'_>,
178        cursor_position: Point,
179        viewport: &Rectangle,
180    ) {
181        self.content.as_widget().draw(
182            &tree.children[0],
183            renderer,
184            theme,
185            inherited_style,
186            layout,
187            cursor_position,
188            viewport,
189        );
190
191        let tooltip = &self.tooltip;
192
193        draw(
194            renderer,
195            theme,
196            inherited_style,
197            layout,
198            cursor_position,
199            viewport,
200            self.position,
201            self.gap,
202            self.padding,
203            self.snap_within_viewport,
204            &self.style,
205            |renderer, limits| {
206                Widget::<(), Renderer>::layout(tooltip, renderer, limits)
207            },
208            |renderer, defaults, layout, cursor_position, viewport| {
209                Widget::<(), Renderer>::draw(
210                    tooltip,
211                    &Tree::empty(),
212                    renderer,
213                    theme,
214                    defaults,
215                    layout,
216                    cursor_position,
217                    viewport,
218                );
219            },
220        );
221    }
222
223    fn overlay<'b>(
224        &'b mut self,
225        tree: &'b mut Tree,
226        layout: Layout<'_>,
227        renderer: &Renderer,
228    ) -> Option<overlay::Element<'b, Message, Renderer>> {
229        self.content.as_widget_mut().overlay(
230            &mut tree.children[0],
231            layout,
232            renderer,
233        )
234    }
235}
236
237impl<'a, Message, Renderer> From<Tooltip<'a, Message, Renderer>>
238    for Element<'a, Message, Renderer>
239where
240    Message: 'a,
241    Renderer: 'a + text::Renderer,
242    Renderer::Theme: container::StyleSheet + widget::text::StyleSheet,
243{
244    fn from(
245        tooltip: Tooltip<'a, Message, Renderer>,
246    ) -> Element<'a, Message, Renderer> {
247        Element::new(tooltip)
248    }
249}
250
251/// The position of the tooltip. Defaults to following the cursor.
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub enum Position {
254    /// The tooltip will follow the cursor.
255    FollowCursor,
256    /// The tooltip will appear on the top of the widget.
257    Top,
258    /// The tooltip will appear on the bottom of the widget.
259    Bottom,
260    /// The tooltip will appear on the left of the widget.
261    Left,
262    /// The tooltip will appear on the right of the widget.
263    Right,
264}
265
266/// Draws a [`Tooltip`].
267pub fn draw<Renderer>(
268    renderer: &mut Renderer,
269    theme: &Renderer::Theme,
270    inherited_style: &renderer::Style,
271    layout: Layout<'_>,
272    cursor_position: Point,
273    viewport: &Rectangle,
274    position: Position,
275    gap: f32,
276    padding: f32,
277    snap_within_viewport: bool,
278    style: &<Renderer::Theme as container::StyleSheet>::Style,
279    layout_text: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
280    draw_text: impl FnOnce(
281        &mut Renderer,
282        &renderer::Style,
283        Layout<'_>,
284        Point,
285        &Rectangle,
286    ),
287) where
288    Renderer: crate::Renderer,
289    Renderer::Theme: container::StyleSheet,
290{
291    use container::StyleSheet;
292
293    let bounds = layout.bounds();
294
295    if bounds.contains(cursor_position) {
296        let style = theme.appearance(style);
297
298        let defaults = renderer::Style {
299            text_color: style.text_color.unwrap_or(inherited_style.text_color),
300        };
301
302        let text_layout = layout_text(
303            renderer,
304            &layout::Limits::new(
305                Size::ZERO,
306                snap_within_viewport
307                    .then(|| viewport.size())
308                    .unwrap_or(Size::INFINITY),
309            )
310            .pad(Padding::new(padding)),
311        );
312
313        let text_bounds = text_layout.bounds();
314        let x_center = bounds.x + (bounds.width - text_bounds.width) / 2.0;
315        let y_center = bounds.y + (bounds.height - text_bounds.height) / 2.0;
316
317        let mut tooltip_bounds = {
318            let offset = match position {
319                Position::Top => Vector::new(
320                    x_center,
321                    bounds.y - text_bounds.height - gap - padding,
322                ),
323                Position::Bottom => Vector::new(
324                    x_center,
325                    bounds.y + bounds.height + gap + padding,
326                ),
327                Position::Left => Vector::new(
328                    bounds.x - text_bounds.width - gap - padding,
329                    y_center,
330                ),
331                Position::Right => Vector::new(
332                    bounds.x + bounds.width + gap + padding,
333                    y_center,
334                ),
335                Position::FollowCursor => Vector::new(
336                    cursor_position.x,
337                    cursor_position.y - text_bounds.height,
338                ),
339            };
340
341            Rectangle {
342                x: offset.x - padding,
343                y: offset.y - padding,
344                width: text_bounds.width + padding * 2.0,
345                height: text_bounds.height + padding * 2.0,
346            }
347        };
348
349        if snap_within_viewport {
350            if tooltip_bounds.x < viewport.x {
351                tooltip_bounds.x = viewport.x;
352            } else if viewport.x + viewport.width
353                < tooltip_bounds.x + tooltip_bounds.width
354            {
355                tooltip_bounds.x =
356                    viewport.x + viewport.width - tooltip_bounds.width;
357            }
358
359            if tooltip_bounds.y < viewport.y {
360                tooltip_bounds.y = viewport.y;
361            } else if viewport.y + viewport.height
362                < tooltip_bounds.y + tooltip_bounds.height
363            {
364                tooltip_bounds.y =
365                    viewport.y + viewport.height - tooltip_bounds.height;
366            }
367        }
368
369        renderer.with_layer(Rectangle::with_size(Size::INFINITY), |renderer| {
370            container::draw_background(renderer, &style, tooltip_bounds);
371
372            draw_text(
373                renderer,
374                &defaults,
375                Layout::with_offset(
376                    Vector::new(
377                        tooltip_bounds.x + padding,
378                        tooltip_bounds.y + padding,
379                    ),
380                    &text_layout,
381                ),
382                cursor_position,
383                viewport,
384            )
385        });
386    }
387}