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 cx.notify();
119 }
120 }
121 })
122 .boxed();
123 Self {
124 child,
125 tooltip,
126 _state: state_handle,
127 }
128 }
129
130 pub fn render_tooltip(
131 text: String,
132 style: TooltipStyle,
133 action: Option<Box<dyn Action>>,
134 measure: bool,
135 ) -> impl Element {
136 Flex::row()
137 .with_child({
138 let text = Text::new(text, style.text)
139 .constrained()
140 .with_max_width(style.max_text_width);
141 if measure {
142 text.flex(1., false).boxed()
143 } else {
144 text.flex(1., false).aligned().boxed()
145 }
146 })
147 .with_children(action.map(|action| {
148 let keystroke_label =
149 KeystrokeLabel::new(action, style.keystroke.container, style.keystroke.text);
150 if measure {
151 keystroke_label.boxed()
152 } else {
153 keystroke_label.aligned().boxed()
154 }
155 }))
156 .contained()
157 .with_style(style.container)
158 }
159}
160
161impl Element for Tooltip {
162 type LayoutState = ();
163 type PaintState = ();
164
165 fn layout(
166 &mut self,
167 constraint: SizeConstraint,
168 cx: &mut LayoutContext,
169 ) -> (Vector2F, Self::LayoutState) {
170 let size = self.child.layout(constraint, cx);
171 if let Some(tooltip) = self.tooltip.as_mut() {
172 tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx);
173 }
174 (size, ())
175 }
176
177 fn paint(
178 &mut self,
179 bounds: RectF,
180 visible_bounds: RectF,
181 _: &mut Self::LayoutState,
182 cx: &mut PaintContext,
183 ) {
184 self.child.paint(bounds.origin(), visible_bounds, cx);
185 if let Some(tooltip) = self.tooltip.as_mut() {
186 tooltip.paint(bounds.origin(), visible_bounds, cx);
187 }
188 }
189
190 fn rect_for_text_range(
191 &self,
192 range: Range<usize>,
193 _: RectF,
194 _: RectF,
195 _: &Self::LayoutState,
196 _: &Self::PaintState,
197 cx: &MeasurementContext,
198 ) -> Option<RectF> {
199 self.child.rect_for_text_range(range, cx)
200 }
201
202 fn debug(
203 &self,
204 _: RectF,
205 _: &Self::LayoutState,
206 _: &Self::PaintState,
207 cx: &crate::DebugContext,
208 ) -> serde_json::Value {
209 json!({
210 "child": self.child.debug(cx),
211 "tooltip": self.tooltip.as_ref().map(|t| t.debug(cx)),
212 })
213 }
214}