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