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