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