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