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