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