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