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