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_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 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 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 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 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
339pub(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 (Pixels(8.0), Stretch(1.0))
372 }
373 Placement::TopEnd | Placement::BottomEnd => {
374 (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}