Skip to main content

vizia_core/views/
tooltip.rs

1use crate::context::TreeProps;
2use crate::prelude::*;
3use crate::vg;
4
5/// A tooltip view.
6///
7/// Should be used with the [tooltip](crate::modifiers::ActionModifiers::tooltip) modifier.
8///
9/// # Example
10/// ```
11/// # use vizia_core::prelude::*;
12/// #
13/// # enum AppEvent {
14/// #     Action,
15/// # }
16/// #
17/// # let cx = &mut Context::default();
18/// #
19/// Button::new(cx, |cx| Label::new(cx, "Text"))
20///     .tooltip(|cx|{
21///         Tooltip::new(cx, |cx|{
22///             Label::new(cx, "Tooltip Text");
23///         })
24///     });
25/// ```
26pub struct Tooltip {
27    placement: Signal<Placement>,
28    shift: Signal<Placement>,
29    show_arrow: Signal<bool>,
30    arrow_size: Signal<Length>,
31}
32
33impl Tooltip {
34    /// Creates a new Tooltip view with the given content.
35    ///
36    /// Should be used with the [tooltip](crate::modifiers::ActionModifiers::tooltip) modifier.
37    ///
38    /// # Example
39    /// ```
40    /// # use vizia_core::prelude::*;
41    /// #
42    /// # enum AppEvent {
43    /// #     Action,
44    /// # }
45    /// #
46    /// # let cx = &mut Context::default();
47    /// #
48    /// Button::new(cx, |cx| Label::new(cx, "Text"))
49    ///     .tooltip(|cx|{
50    ///         Tooltip::new(cx, |cx|{
51    ///             Label::new(cx, "Tooltip Text");
52    ///         })
53    ///     });
54    /// ```
55    pub fn new(cx: &mut Context, content: impl FnOnce(&mut Context)) -> Handle<Self> {
56        let placement = Signal::new(Placement::Top);
57        let shift = Signal::new(Placement::Top);
58        let show_arrow = Signal::new(true);
59        let arrow_size = Signal::new(Length::Value(LengthValue::Px(8.0)));
60
61        Self { placement, shift, show_arrow, arrow_size }
62            .build(cx, |cx| {
63                Binding::new(cx, show_arrow, move |cx| {
64                    let show_arrow = show_arrow.get();
65                    if show_arrow {
66                        Arrow::new(cx, shift, arrow_size);
67                    }
68                });
69                (content)(cx);
70            })
71            .role(Role::Tooltip)
72            .z_index(110)
73            .ignore_clipping(true)
74            .hoverable(false)
75            .position_type(PositionType::Absolute)
76            .space(Pixels(0.0))
77            .on_build(|ex| {
78                ex.add_listener(move |tooltip: &mut Tooltip, ex, event| {
79                    event.map(|window_event, _| match window_event {
80                        WindowEvent::MouseMove(x, y) => {
81                            if tooltip.placement == Placement::Cursor && !x.is_nan() && !y.is_nan()
82                            {
83                                let scale = ex.scale_factor();
84                                let parent = ex.parent();
85                                let parent_bounds = ex.cache.get_bounds(parent);
86                                if parent_bounds.contains_point(*x, *y) {
87                                    ex.set_left(Pixels(
88                                        ((*x - parent_bounds.x) - ex.bounds().width() / 2.0)
89                                            / scale,
90                                    ));
91                                    ex.set_top(Pixels((*y - parent_bounds.y) / scale));
92                                }
93                            }
94                        }
95
96                        _ => {}
97                    });
98                });
99            })
100    }
101}
102
103impl View for Tooltip {
104    fn element(&self) -> Option<&'static str> {
105        Some("tooltip")
106    }
107
108    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
109        event.map(|window_event, _| match window_event {
110            // Reposition popup if there isn't enough room for it.
111            WindowEvent::GeometryChanged(_) => {
112                let parent = cx.parent();
113                let parent_bounds = cx.cache.get_bounds(parent);
114                let bounds = cx.bounds();
115                let window_bounds = cx.cache.get_bounds(cx.parent_window());
116
117                let arrow_size = self.arrow_size.get().to_px().unwrap() * cx.scale_factor();
118
119                let mut available = AvailablePlacement::all();
120
121                let top_start_bounds = BoundingBox::from_min_max(
122                    parent_bounds.left(),
123                    parent_bounds.top() - bounds.height() - arrow_size,
124                    parent_bounds.left() + bounds.width(),
125                    parent_bounds.top(),
126                );
127
128                available
129                    .set(AvailablePlacement::TOP_START, window_bounds.contains(&top_start_bounds));
130
131                let top_bounds = BoundingBox::from_min_max(
132                    parent_bounds.center().0 - bounds.width() / 2.0,
133                    parent_bounds.top() - bounds.height() - arrow_size,
134                    parent_bounds.center().0 + bounds.width() / 2.0,
135                    parent_bounds.top(),
136                );
137
138                available.set(AvailablePlacement::TOP, window_bounds.contains(&top_bounds));
139
140                let top_end_bounds = BoundingBox::from_min_max(
141                    parent_bounds.right() - bounds.width(),
142                    parent_bounds.top() - bounds.height() - arrow_size,
143                    parent_bounds.right(),
144                    parent_bounds.top(),
145                );
146
147                available.set(AvailablePlacement::TOP_END, window_bounds.contains(&top_end_bounds));
148
149                let bottom_start_bounds = BoundingBox::from_min_max(
150                    parent_bounds.left(),
151                    parent_bounds.bottom(),
152                    parent_bounds.left() + bounds.width(),
153                    parent_bounds.bottom() + bounds.height() + arrow_size,
154                );
155
156                available.set(
157                    AvailablePlacement::BOTTOM_START,
158                    window_bounds.contains(&bottom_start_bounds),
159                );
160
161                let bottom_bounds = BoundingBox::from_min_max(
162                    parent_bounds.center().0 - bounds.width() / 2.0,
163                    parent_bounds.bottom(),
164                    parent_bounds.center().0 + bounds.width() / 2.0,
165                    parent_bounds.bottom() + bounds.height() + arrow_size,
166                );
167
168                available.set(AvailablePlacement::BOTTOM, window_bounds.contains(&bottom_bounds));
169
170                let bottom_end_bounds = BoundingBox::from_min_max(
171                    parent_bounds.right() - bounds.width(),
172                    parent_bounds.bottom(),
173                    parent_bounds.right(),
174                    parent_bounds.bottom() + bounds.height() + arrow_size,
175                );
176
177                available.set(
178                    AvailablePlacement::BOTTOM_END,
179                    window_bounds.contains(&bottom_end_bounds),
180                );
181
182                let left_start_bounds = BoundingBox::from_min_max(
183                    parent_bounds.left() - bounds.width() - arrow_size,
184                    parent_bounds.top(),
185                    parent_bounds.left(),
186                    parent_bounds.top() + bounds.height(),
187                );
188
189                available.set(
190                    AvailablePlacement::LEFT_START,
191                    window_bounds.contains(&left_start_bounds),
192                );
193
194                let left_bounds = BoundingBox::from_min_max(
195                    parent_bounds.left() - bounds.width() - arrow_size,
196                    parent_bounds.center().1 - bounds.height() / 2.0,
197                    parent_bounds.left(),
198                    parent_bounds.center().1 + bounds.height() / 2.0,
199                );
200
201                available.set(AvailablePlacement::LEFT, window_bounds.contains(&left_bounds));
202
203                let left_end_bounds = BoundingBox::from_min_max(
204                    parent_bounds.left() - bounds.width() - arrow_size,
205                    parent_bounds.bottom() - bounds.height(),
206                    parent_bounds.left(),
207                    parent_bounds.bottom(),
208                );
209
210                available
211                    .set(AvailablePlacement::LEFT_END, window_bounds.contains(&left_end_bounds));
212
213                let right_start_bounds = BoundingBox::from_min_max(
214                    parent_bounds.right(),
215                    parent_bounds.top(),
216                    parent_bounds.right() + bounds.width() + arrow_size,
217                    parent_bounds.top() + bounds.height(),
218                );
219
220                available.set(
221                    AvailablePlacement::RIGHT_START,
222                    window_bounds.contains(&right_start_bounds),
223                );
224
225                let right_bounds = BoundingBox::from_min_max(
226                    parent_bounds.right(),
227                    parent_bounds.center().1 - bounds.height() / 2.0,
228                    parent_bounds.right() + bounds.width() + arrow_size,
229                    parent_bounds.center().1 + bounds.height() / 2.0,
230                );
231
232                available.set(AvailablePlacement::RIGHT, window_bounds.contains(&right_bounds));
233
234                let right_end_bounds = BoundingBox::from_min_max(
235                    parent_bounds.right(),
236                    parent_bounds.bottom() - bounds.height(),
237                    parent_bounds.right() + bounds.width() + arrow_size,
238                    parent_bounds.bottom(),
239                );
240
241                available
242                    .set(AvailablePlacement::RIGHT_END, window_bounds.contains(&right_end_bounds));
243
244                let scale = cx.scale_factor();
245
246                self.shift.set_if_changed(self.placement.get().place(available));
247
248                let arrow_size = self.arrow_size.get().to_px().unwrap();
249
250                let translate = match self.shift.get() {
251                    Placement::Top => (
252                        -(bounds.width() - parent_bounds.width()) / (2.0 * scale),
253                        -bounds.height() / scale - arrow_size,
254                    ),
255                    Placement::TopStart => (0.0, -bounds.height() / scale - arrow_size),
256                    Placement::TopEnd => (
257                        -(bounds.width() - parent_bounds.width()) / scale,
258                        -bounds.height() / scale - arrow_size,
259                    ),
260                    Placement::Bottom => (
261                        -(bounds.width() - parent_bounds.width()) / (2.0 * scale),
262                        parent_bounds.height() / scale + arrow_size,
263                    ),
264                    Placement::BottomStart => (0.0, parent_bounds.height() / scale + arrow_size),
265                    Placement::BottomEnd => (
266                        -(bounds.width() - parent_bounds.width()) / scale,
267                        parent_bounds.height() / scale + arrow_size,
268                    ),
269                    Placement::LeftStart => (-(bounds.width() / scale) - arrow_size, 0.0),
270                    Placement::Left => (
271                        -(bounds.width() / scale) - arrow_size,
272                        -(bounds.height() - parent_bounds.height()) / (2.0 * scale),
273                    ),
274                    Placement::LeftEnd => (
275                        -(bounds.width() / scale) - arrow_size,
276                        -(bounds.height() - parent_bounds.height()) / scale,
277                    ),
278                    Placement::RightStart => ((parent_bounds.width() / scale) + arrow_size, 0.0),
279                    Placement::Right => (
280                        (parent_bounds.width() / scale) + arrow_size,
281                        -(bounds.height() - parent_bounds.height()) / (2.0 * scale),
282                    ),
283                    Placement::RightEnd => (
284                        (parent_bounds.width() / scale) + arrow_size,
285                        -(bounds.height() - parent_bounds.height()) / scale,
286                    ),
287
288                    _ => (0.0, 0.0),
289                };
290
291                cx.set_translate((Pixels(translate.0.round()), Pixels(translate.1.round())));
292            }
293
294            _ => {}
295        });
296    }
297}
298
299impl Handle<'_, Tooltip> {
300    /// Sets the position where the tooltip should appear relative to its parent element.
301    /// Defaults to `Placement::Bottom`.
302    pub fn placement<U: Into<Placement> + Clone + 'static>(
303        self,
304        placement: impl Res<U> + 'static,
305    ) -> Self {
306        let placement = placement.to_signal(self.cx);
307        self.bind(placement, move |handle| {
308            let val = placement.get();
309            let placement = val.into();
310            handle.modify(|tooltip| {
311                tooltip.placement.set(placement);
312                tooltip.shift.set(placement);
313            });
314        })
315    }
316
317    /// Sets whether the tooltip should include an arrow. Defaults to true.
318    pub fn arrow<U: Into<bool> + Clone + 'static>(self, show_arrow: impl Res<U> + 'static) -> Self {
319        let show_arrow = show_arrow.to_signal(self.cx);
320        self.bind(show_arrow, move |handle| {
321            let val = show_arrow.get();
322            let show_arrow = val.into();
323            handle.modify(|tooltip| tooltip.show_arrow.set(show_arrow));
324        })
325    }
326
327    /// Sets the size of the tooltip arrow if enabled.
328    pub fn arrow_size<U: Into<Length> + Clone + 'static>(
329        self,
330        size: impl Res<U> + 'static,
331    ) -> Self {
332        let size = size.to_signal(self.cx);
333        self.bind(size, move |handle| {
334            let val = size.get();
335            let size = val.into();
336            handle.modify(|tooltip| tooltip.arrow_size.set(size));
337        })
338    }
339}
340
341/// An arrow view used by the Tooltip view.
342pub(crate) struct Arrow {
343    shift: Signal<Placement>,
344}
345
346impl Arrow {
347    pub(crate) fn new(
348        cx: &mut Context,
349        shift: Signal<Placement>,
350        arrow_size: Signal<Length>,
351    ) -> Handle<Self> {
352        Self { shift }.build(cx, |_| {}).bind(shift, move |mut handle| {
353            let placement = shift.get();
354            let (t, b) = match placement {
355                Placement::TopStart | Placement::Top | Placement::TopEnd => {
356                    (Percentage(100.0), Stretch(1.0))
357                }
358                Placement::BottomStart | Placement::Bottom | Placement::BottomEnd => {
359                    (Stretch(1.0), Percentage(100.0))
360                }
361                _ => (Stretch(1.0), Stretch(1.0)),
362            };
363
364            let (l, r) = match placement {
365                Placement::LeftStart | Placement::Left | Placement::LeftEnd => {
366                    (Percentage(100.0), Stretch(1.0))
367                }
368                Placement::RightStart | Placement::Right | Placement::RightEnd => {
369                    (Stretch(1.0), Percentage(100.0))
370                }
371                Placement::TopStart | Placement::BottomStart => {
372                    // TODO: Use border radius
373                    (Pixels(8.0), Stretch(1.0))
374                }
375                Placement::TopEnd | Placement::BottomEnd => {
376                    // TODO: Use border radius
377                    (Stretch(1.0), Pixels(8.0))
378                }
379                _ => (Stretch(1.0), Stretch(1.0)),
380            };
381
382            handle = handle.top(t).bottom(b).left(l).right(r).position_type(PositionType::Absolute);
383
384            handle.bind(arrow_size, move |handle| {
385                let arrow_size = arrow_size.get();
386                let arrow_size = arrow_size.to_px().unwrap_or(8.0);
387                let (w, h) = match placement {
388                    Placement::Top
389                    | Placement::Bottom
390                    | Placement::TopStart
391                    | Placement::BottomStart
392                    | Placement::TopEnd
393                    | Placement::BottomEnd => (Pixels(arrow_size * 2.0), Pixels(arrow_size)),
394
395                    _ => (Pixels(arrow_size), Pixels(arrow_size * 2.0)),
396                };
397
398                handle.width(w).height(h);
399            });
400        })
401    }
402}
403
404impl View for Arrow {
405    fn element(&self) -> Option<&'static str> {
406        Some("arrow")
407    }
408    fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) {
409        let bounds = cx.bounds();
410        let mut path = vg::PathBuilder::new();
411        match self.shift.get() {
412            Placement::Bottom | Placement::BottomStart | Placement::BottomEnd => {
413                path.move_to(bounds.bottom_left());
414                path.line_to(bounds.center_top());
415                path.line_to(bounds.bottom_right());
416                path.line_to(bounds.bottom_left());
417            }
418
419            Placement::Top | Placement::TopStart | Placement::TopEnd => {
420                path.move_to(bounds.top_left());
421                path.line_to(bounds.center_bottom());
422                path.line_to(bounds.top_right());
423                path.line_to(bounds.top_left());
424            }
425
426            Placement::Left | Placement::LeftStart | Placement::LeftEnd => {
427                path.move_to(bounds.top_left());
428                path.line_to(bounds.center_right());
429                path.line_to(bounds.bottom_left());
430                path.line_to(bounds.top_left());
431            }
432
433            Placement::Right | Placement::RightStart | Placement::RightEnd => {
434                path.move_to(bounds.top_right());
435                path.line_to(bounds.center_left());
436                path.line_to(bounds.bottom_right());
437                path.line_to(bounds.top_right());
438            }
439
440            _ => {}
441        }
442        path.close();
443
444        let bg = cx.background_color();
445
446        let mut paint = vg::Paint::default();
447        paint.set_color(bg);
448        let path = path.detach();
449        canvas.draw_path(&path, &paint);
450    }
451}