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