1use super::{
2 ContainerStyle, Element, ElementBox, Flex, KeystrokeLabel, MouseEventHandler, Overlay,
3 OverlayFitMode, ParentElement, Text,
4};
5use crate::{
6 fonts::TextStyle,
7 geometry::{rect::RectF, vector::Vector2F},
8 json::json,
9 presenter::MeasurementContext,
10 Action, Axis, ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint,
11 Task, View,
12};
13use serde::Deserialize;
14use std::{
15 cell::{Cell, RefCell},
16 ops::Range,
17 rc::Rc,
18 time::Duration,
19};
20
21const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500);
22
23pub struct Tooltip {
24 child: ElementBox,
25 tooltip: Option<ElementBox>,
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: 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 Tooltip {
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: ElementBox,
60 cx: &mut RenderContext<T>,
61 ) -> Self {
62 struct ElementState<Tag>(Tag);
63 struct MouseEventHandlerState<Tag>(Tag);
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 text.clone(),
70 style.clone(),
71 action.as_ref().map(|a| a.boxed_clone()),
72 true,
73 )
74 .boxed();
75 Some(
76 Overlay::new(
77 Self::render_tooltip(text, style, action, false)
78 .constrained()
79 .dynamically(move |constraint, cx| {
80 SizeConstraint::strict_along(
81 Axis::Vertical,
82 collapsed_tooltip.layout(constraint, cx).y(),
83 )
84 })
85 .boxed(),
86 )
87 .with_fit_mode(OverlayFitMode::SwitchAnchor)
88 .with_anchor_position(state.position.get())
89 .boxed(),
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 let window_id = cx.window_id();
98 if let Some(view_id) = cx.view_id() {
99 if e.started {
100 if !state.visible.get() {
101 state.position.set(position);
102
103 let mut debounce = state.debounce.borrow_mut();
104 if debounce.is_none() {
105 *debounce = Some(cx.spawn({
106 let state = state.clone();
107 |mut cx| async move {
108 cx.background().timer(DEBOUNCE_TIMEOUT).await;
109 state.visible.set(true);
110 cx.update(|cx| cx.notify_view(window_id, view_id));
111 }
112 }));
113 }
114 }
115 } else {
116 state.visible.set(false);
117 state.debounce.take();
118 }
119 }
120 })
121 .boxed();
122 Self {
123 child,
124 tooltip,
125 _state: state_handle,
126 }
127 }
128
129 pub fn render_tooltip(
130 text: String,
131 style: TooltipStyle,
132 action: Option<Box<dyn Action>>,
133 measure: bool,
134 ) -> impl Element {
135 Flex::row()
136 .with_child({
137 let text = Text::new(text, style.text)
138 .constrained()
139 .with_max_width(style.max_text_width);
140 if measure {
141 text.flex(1., false).boxed()
142 } else {
143 text.flex(1., false).aligned().boxed()
144 }
145 })
146 .with_children(action.map(|action| {
147 let keystroke_label =
148 KeystrokeLabel::new(action, style.keystroke.container, style.keystroke.text);
149 if measure {
150 keystroke_label.boxed()
151 } else {
152 keystroke_label.aligned().boxed()
153 }
154 }))
155 .contained()
156 .with_style(style.container)
157 }
158}
159
160impl Element for Tooltip {
161 type LayoutState = ();
162 type PaintState = ();
163
164 fn layout(
165 &mut self,
166 constraint: SizeConstraint,
167 cx: &mut LayoutContext,
168 ) -> (Vector2F, Self::LayoutState) {
169 let size = self.child.layout(constraint, cx);
170 if let Some(tooltip) = self.tooltip.as_mut() {
171 tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx);
172 }
173 (size, ())
174 }
175
176 fn paint(
177 &mut self,
178 bounds: RectF,
179 visible_bounds: RectF,
180 _: &mut Self::LayoutState,
181 cx: &mut PaintContext,
182 ) {
183 self.child.paint(bounds.origin(), visible_bounds, cx);
184 if let Some(tooltip) = self.tooltip.as_mut() {
185 tooltip.paint(bounds.origin(), visible_bounds, cx);
186 }
187 }
188
189 fn rect_for_text_range(
190 &self,
191 range: Range<usize>,
192 _: RectF,
193 _: RectF,
194 _: &Self::LayoutState,
195 _: &Self::PaintState,
196 cx: &MeasurementContext,
197 ) -> Option<RectF> {
198 self.child.rect_for_text_range(range, cx)
199 }
200
201 fn debug(
202 &self,
203 _: RectF,
204 _: &Self::LayoutState,
205 _: &Self::PaintState,
206 cx: &crate::DebugContext,
207 ) -> serde_json::Value {
208 json!({
209 "child": self.child.debug(cx),
210 "tooltip": self.tooltip.as_ref().map(|t| t.debug(cx)),
211 })
212 }
213}