1use std::{
2 cell::{Cell, RefCell},
3 rc::Rc,
4 time::Duration,
5};
6
7use super::{Element, ElementBox, MouseEventHandler};
8use crate::{
9 geometry::{rect::RectF, vector::Vector2F},
10 json::json,
11 ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint, Task, View,
12};
13
14const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500);
15
16pub struct Tooltip {
17 child: ElementBox,
18 tooltip: Option<ElementBox>,
19 state: ElementStateHandle<Rc<TooltipState>>,
20}
21
22#[derive(Default)]
23struct TooltipState {
24 visible: Cell<bool>,
25 position: Cell<Vector2F>,
26 debounce: RefCell<Option<Task<()>>>,
27}
28
29impl Tooltip {
30 pub fn new<T: View>(
31 id: usize,
32 child: ElementBox,
33 tooltip: ElementBox,
34 cx: &mut RenderContext<T>,
35 ) -> Self {
36 let state_handle = cx.element_state::<TooltipState, Rc<TooltipState>>(id);
37 let state = state_handle.read(cx).clone();
38 let tooltip = if state.visible.get() {
39 Some(tooltip)
40 } else {
41 None
42 };
43 let child = MouseEventHandler::new::<Self, _, _>(id, cx, |_, _| child)
44 .on_hover(move |position, hover, cx| {
45 let window_id = cx.window_id();
46 if let Some(view_id) = cx.view_id() {
47 if hover {
48 if !state.visible.get() {
49 state.position.set(position);
50
51 let mut debounce = state.debounce.borrow_mut();
52 if debounce.is_none() {
53 *debounce = Some(cx.spawn({
54 let state = state.clone();
55 |mut cx| async move {
56 cx.background().timer(DEBOUNCE_TIMEOUT).await;
57 state.visible.set(true);
58 cx.update(|cx| cx.notify_view(window_id, view_id));
59 }
60 }));
61 }
62 }
63 } else {
64 state.visible.set(false);
65 state.debounce.take();
66 }
67 }
68 })
69 .boxed();
70 Self {
71 child,
72 tooltip,
73 state: state_handle,
74 }
75 }
76}
77
78impl Element for Tooltip {
79 type LayoutState = ();
80 type PaintState = ();
81
82 fn layout(
83 &mut self,
84 constraint: SizeConstraint,
85 cx: &mut LayoutContext,
86 ) -> (Vector2F, Self::LayoutState) {
87 let size = self.child.layout(constraint, cx);
88 if let Some(tooltip) = self.tooltip.as_mut() {
89 tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx);
90 }
91 (size, ())
92 }
93
94 fn paint(
95 &mut self,
96 bounds: RectF,
97 visible_bounds: RectF,
98 _: &mut Self::LayoutState,
99 cx: &mut PaintContext,
100 ) {
101 self.child.paint(bounds.origin(), visible_bounds, cx);
102 if let Some(tooltip) = self.tooltip.as_mut() {
103 let origin = self.state.read(cx).position.get();
104 let mut bounds = RectF::new(origin, tooltip.size());
105
106 // Align tooltip to the left if its bounds overflow the window width.
107 if bounds.lower_right().x() > cx.window_size.x() {
108 bounds.set_origin_x(bounds.origin_x() - bounds.width());
109 }
110
111 // Align tooltip to the top if its bounds overflow the window height.
112 if bounds.lower_right().y() > cx.window_size.y() {
113 bounds.set_origin_y(bounds.origin_y() - bounds.height());
114 }
115
116 cx.scene.push_stacking_context(None);
117 tooltip.paint(bounds.origin(), bounds, cx);
118 cx.scene.pop_stacking_context();
119 }
120 }
121
122 fn dispatch_event(
123 &mut self,
124 event: &crate::Event,
125 _: RectF,
126 _: RectF,
127 _: &mut Self::LayoutState,
128 _: &mut Self::PaintState,
129 cx: &mut crate::EventContext,
130 ) -> bool {
131 self.child.dispatch_event(event, cx)
132 }
133
134 fn debug(
135 &self,
136 _: RectF,
137 _: &Self::LayoutState,
138 _: &Self::PaintState,
139 cx: &crate::DebugContext,
140 ) -> serde_json::Value {
141 json!({
142 "child": self.child.debug(cx),
143 "tooltip": self.tooltip.as_ref().map(|t| t.debug(cx)),
144 })
145 }
146}