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