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