1use super::{
2 ContainerStyle, Element, ElementBox, Flex, KeystrokeLabel, MouseEventHandler, ParentElement,
3 Text,
4};
5use crate::{
6 fonts::TextStyle,
7 geometry::{rect::RectF, vector::Vector2F},
8 json::json,
9 Action, Axis, ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint,
10 Task, View,
11};
12use serde::Deserialize;
13use std::{
14 cell::{Cell, RefCell},
15 rc::Rc,
16 time::Duration,
17};
18
19const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500);
20
21pub struct Tooltip {
22 child: ElementBox,
23 tooltip: Option<ElementBox>,
24 state: ElementStateHandle<Rc<TooltipState>>,
25}
26
27#[derive(Default)]
28struct TooltipState {
29 visible: Cell<bool>,
30 position: Cell<Vector2F>,
31 debounce: RefCell<Option<Task<()>>>,
32}
33
34#[derive(Clone, Deserialize, Default)]
35pub struct TooltipStyle {
36 #[serde(flatten)]
37 container: ContainerStyle,
38 text: TextStyle,
39 keystroke: KeystrokeStyle,
40 max_text_width: f32,
41}
42
43#[derive(Clone, Deserialize, Default)]
44pub struct KeystrokeStyle {
45 #[serde(flatten)]
46 container: ContainerStyle,
47 #[serde(flatten)]
48 text: TextStyle,
49}
50
51impl Tooltip {
52 pub fn new<T: View>(
53 id: usize,
54 text: String,
55 action: Option<Box<dyn Action>>,
56 style: TooltipStyle,
57 child: ElementBox,
58 cx: &mut RenderContext<T>,
59 ) -> Self {
60 let state_handle = cx.element_state::<TooltipState, Rc<TooltipState>>(id);
61 let state = state_handle.read(cx).clone();
62 let tooltip = if state.visible.get() {
63 let mut collapsed_tooltip = Self::render_tooltip(
64 text.clone(),
65 style.clone(),
66 action.as_ref().map(|a| a.boxed_clone()),
67 true,
68 )
69 .boxed();
70 Some(
71 Self::render_tooltip(text, style, action, false)
72 .constrained()
73 .dynamically(move |constraint, cx| {
74 SizeConstraint::strict_along(
75 Axis::Vertical,
76 collapsed_tooltip.layout(constraint, cx).y(),
77 )
78 })
79 .boxed(),
80 )
81 } else {
82 None
83 };
84 let child = MouseEventHandler::new::<Self, _, _>(id, cx, |_, _| child)
85 .on_hover(move |position, hover, cx| {
86 let window_id = cx.window_id();
87 if let Some(view_id) = cx.view_id() {
88 if hover {
89 if !state.visible.get() {
90 state.position.set(position);
91
92 let mut debounce = state.debounce.borrow_mut();
93 if debounce.is_none() {
94 *debounce = Some(cx.spawn({
95 let state = state.clone();
96 |mut cx| async move {
97 cx.background().timer(DEBOUNCE_TIMEOUT).await;
98 state.visible.set(true);
99 cx.update(|cx| cx.notify_view(window_id, view_id));
100 }
101 }));
102 }
103 }
104 } else {
105 state.visible.set(false);
106 state.debounce.take();
107 }
108 }
109 })
110 .boxed();
111 Self {
112 child,
113 tooltip,
114 state: state_handle,
115 }
116 }
117
118 fn render_tooltip(
119 text: String,
120 style: TooltipStyle,
121 action: Option<Box<dyn Action>>,
122 measure: bool,
123 ) -> impl Element {
124 Flex::row()
125 .with_child({
126 let text = Text::new(text, style.text)
127 .constrained()
128 .with_max_width(style.max_text_width);
129 if measure {
130 text.flex(1., false).boxed()
131 } else {
132 text.flex(1., false).aligned().boxed()
133 }
134 })
135 .with_children(action.map(|action| {
136 let keystroke_label =
137 KeystrokeLabel::new(action, style.keystroke.container, style.keystroke.text);
138 if measure {
139 keystroke_label.boxed()
140 } else {
141 keystroke_label.aligned().boxed()
142 }
143 }))
144 .contained()
145 .with_style(style.container)
146 }
147}
148
149impl Element for Tooltip {
150 type LayoutState = ();
151 type PaintState = ();
152
153 fn layout(
154 &mut self,
155 constraint: SizeConstraint,
156 cx: &mut LayoutContext,
157 ) -> (Vector2F, Self::LayoutState) {
158 let size = self.child.layout(constraint, cx);
159 if let Some(tooltip) = self.tooltip.as_mut() {
160 tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx);
161 }
162 (size, ())
163 }
164
165 fn paint(
166 &mut self,
167 bounds: RectF,
168 visible_bounds: RectF,
169 _: &mut Self::LayoutState,
170 cx: &mut PaintContext,
171 ) {
172 self.child.paint(bounds.origin(), visible_bounds, cx);
173 if let Some(tooltip) = self.tooltip.as_mut() {
174 let origin = self.state.read(cx).position.get();
175 let mut bounds = RectF::new(origin, tooltip.size());
176
177 // Align tooltip to the left if its bounds overflow the window width.
178 if bounds.lower_right().x() > cx.window_size.x() {
179 bounds.set_origin_x(bounds.origin_x() - bounds.width());
180 }
181
182 // Align tooltip to the top if its bounds overflow the window height.
183 if bounds.lower_right().y() > cx.window_size.y() {
184 bounds.set_origin_y(bounds.origin_y() - bounds.height());
185 }
186
187 cx.scene.push_stacking_context(None);
188 tooltip.paint(bounds.origin(), bounds, cx);
189 cx.scene.pop_stacking_context();
190 }
191 }
192
193 fn dispatch_event(
194 &mut self,
195 event: &crate::Event,
196 _: RectF,
197 _: RectF,
198 _: &mut Self::LayoutState,
199 _: &mut Self::PaintState,
200 cx: &mut crate::EventContext,
201 ) -> bool {
202 self.child.dispatch_event(event, cx)
203 }
204
205 fn debug(
206 &self,
207 _: RectF,
208 _: &Self::LayoutState,
209 _: &Self::PaintState,
210 cx: &crate::DebugContext,
211 ) -> serde_json::Value {
212 json!({
213 "child": self.child.debug(cx),
214 "tooltip": self.tooltip.as_ref().map(|t| t.debug(cx)),
215 })
216 }
217}