1use crate::context::TreeProps;
2use crate::prelude::*;
3use crate::vg;
4
5pub 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 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 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 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 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 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
341pub(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 (Pixels(8.0), Stretch(1.0))
374 }
375 Placement::TopEnd | Placement::BottomEnd => {
376 (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}