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, TypeTag, 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> {
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: 'static> 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 Self::new_dynamic(TypeTag::new::<Tag>(), id, text, action, style, child, cx)
65 }
66
67 pub fn new_dynamic(
68 mut tag: TypeTag,
69 id: usize,
70 text: impl Into<Cow<'static, str>>,
71 action: Option<Box<dyn Action>>,
72 style: TooltipStyle,
73 child: AnyElement<V>,
74 cx: &mut ViewContext<V>,
75 ) -> Self {
76 tag = tag.compose(TypeTag::new::<Self>());
77
78 let focused_view_id = cx.focused_view_id();
79
80 let state_handle = cx.default_element_state_dynamic::<Rc<TooltipState>>(tag, id);
81 let state = state_handle.read(cx).clone();
82 let text = text.into();
83
84 let tooltip = if state.visible.get() {
85 let mut collapsed_tooltip = Self::render_tooltip(
86 focused_view_id,
87 text.clone(),
88 style.clone(),
89 action.as_ref().map(|a| a.boxed_clone()),
90 true,
91 );
92 Some(
93 Overlay::new(
94 Self::render_tooltip(focused_view_id, text, style, action, false)
95 .constrained()
96 .dynamically(move |constraint, view, cx| {
97 SizeConstraint::strict_along(
98 Axis::Vertical,
99 collapsed_tooltip.layout(constraint, view, cx).0.y(),
100 )
101 }),
102 )
103 .with_fit_mode(OverlayFitMode::SwitchAnchor)
104 .with_anchor_position(state.position.get())
105 .into_any(),
106 )
107 } else {
108 None
109 };
110 let child = MouseEventHandler::new_dynamic(tag, id, cx, |_, _| child)
111 .on_hover(move |e, _, cx| {
112 let position = e.position;
113 if e.started {
114 if !state.visible.get() {
115 state.position.set(position);
116
117 let mut debounce = state.debounce.borrow_mut();
118 if debounce.is_none() {
119 *debounce = Some(cx.spawn({
120 let state = state.clone();
121 |view, mut cx| async move {
122 cx.background().timer(DEBOUNCE_TIMEOUT).await;
123 state.visible.set(true);
124 view.update(&mut cx, |_, cx| cx.notify()).log_err();
125 }
126 }));
127 }
128 }
129 } else {
130 state.visible.set(false);
131 state.debounce.take();
132 cx.notify();
133 }
134 })
135 .into_any();
136 Self {
137 child,
138 tooltip,
139 _state: state_handle,
140 }
141 }
142
143 pub fn render_tooltip(
144 focused_view_id: Option<usize>,
145 text: impl Into<Cow<'static, str>>,
146 style: TooltipStyle,
147 action: Option<Box<dyn Action>>,
148 measure: bool,
149 ) -> impl Element<V> {
150 Flex::row()
151 .with_child({
152 let text = if let Some(max_text_width) = style.max_text_width {
153 Text::new(text, style.text)
154 .constrained()
155 .with_max_width(max_text_width)
156 } else {
157 Text::new(text, style.text).constrained()
158 };
159
160 if measure {
161 text.flex(1., false).into_any()
162 } else {
163 text.flex(1., false).aligned().into_any()
164 }
165 })
166 .with_children(action.and_then(|action| {
167 let keystroke_label = KeystrokeLabel::new(
168 focused_view_id?,
169 action,
170 style.keystroke.container,
171 style.keystroke.text,
172 );
173 if measure {
174 Some(keystroke_label.into_any())
175 } else {
176 Some(keystroke_label.aligned().into_any())
177 }
178 }))
179 .contained()
180 .with_style(style.container)
181 }
182}
183
184impl<V: 'static> Element<V> for Tooltip<V> {
185 type LayoutState = ();
186 type PaintState = ();
187
188 fn layout(
189 &mut self,
190 constraint: SizeConstraint,
191 view: &mut V,
192 cx: &mut LayoutContext<V>,
193 ) -> (Vector2F, Self::LayoutState) {
194 let size = self.child.layout(constraint, view, cx);
195 if let Some(tooltip) = self.tooltip.as_mut() {
196 tooltip.layout(
197 SizeConstraint::new(Vector2F::zero(), cx.window_size()),
198 view,
199 cx,
200 );
201 }
202 (size, ())
203 }
204
205 fn paint(
206 &mut self,
207 scene: &mut SceneBuilder,
208 bounds: RectF,
209 visible_bounds: RectF,
210 _: &mut Self::LayoutState,
211 view: &mut V,
212 cx: &mut PaintContext<V>,
213 ) {
214 self.child
215 .paint(scene, bounds.origin(), visible_bounds, view, cx);
216 if let Some(tooltip) = self.tooltip.as_mut() {
217 tooltip.paint(scene, bounds.origin(), visible_bounds, view, cx);
218 }
219 }
220
221 fn rect_for_text_range(
222 &self,
223 range: Range<usize>,
224 _: RectF,
225 _: RectF,
226 _: &Self::LayoutState,
227 _: &Self::PaintState,
228 view: &V,
229 cx: &ViewContext<V>,
230 ) -> Option<RectF> {
231 self.child.rect_for_text_range(range, view, cx)
232 }
233
234 fn debug(
235 &self,
236 _: RectF,
237 _: &Self::LayoutState,
238 _: &Self::PaintState,
239 view: &V,
240 cx: &ViewContext<V>,
241 ) -> serde_json::Value {
242 json!({
243 "child": self.child.debug(view, cx),
244 "tooltip": self.tooltip.as_ref().map(|t| t.debug(view, cx)),
245 })
246 }
247}