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