1use super::{
2 ContainerStyle, Element, ElementBox, Flex, KeystrokeLabel, MouseEventHandler, Overlay,
3 ParentElement, Text,
4};
5use crate::{
6 fonts::TextStyle,
7 geometry::{rect::RectF, vector::Vector2F},
8 json::json,
9 Action, Axis, ElementStateHandle, LayoutContext, PaintContext, RenderContext, SizeConstraint,
10 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<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 let state_handle = cx.element_state::<TooltipState, Rc<TooltipState>>(id);
61 let state = state_handle.read(cx).clone();
62 let tooltip = if state.visible.get() {
63 let mut collapsed_tooltip = Self::render_tooltip(
64 text.clone(),
65 style.clone(),
66 action.as_ref().map(|a| a.boxed_clone()),
67 true,
68 )
69 .boxed();
70 Some(
71 Overlay::new(
72 Self::render_tooltip(text, style, action, false)
73 .constrained()
74 .dynamically(move |constraint, cx| {
75 SizeConstraint::strict_along(
76 Axis::Vertical,
77 collapsed_tooltip.layout(constraint, cx).y(),
78 )
79 })
80 .boxed(),
81 )
82 .move_to_fit(true)
83 .with_abs_position(state.position.get())
84 .boxed(),
85 )
86 } else {
87 None
88 };
89 let child = MouseEventHandler::new::<Self, _, _>(id, cx, |_, _| child)
90 .on_hover(move |position, hover, cx| {
91 let window_id = cx.window_id();
92 if let Some(view_id) = cx.view_id() {
93 if hover {
94 if !state.visible.get() {
95 state.position.set(position);
96
97 let mut debounce = state.debounce.borrow_mut();
98 if debounce.is_none() {
99 *debounce = Some(cx.spawn({
100 let state = state.clone();
101 |mut cx| async move {
102 cx.background().timer(DEBOUNCE_TIMEOUT).await;
103 state.visible.set(true);
104 cx.update(|cx| cx.notify_view(window_id, view_id));
105 }
106 }));
107 }
108 }
109 } else {
110 state.visible.set(false);
111 state.debounce.take();
112 }
113 }
114 })
115 .boxed();
116 Self {
117 child,
118 tooltip,
119 _state: state_handle,
120 }
121 }
122
123 fn render_tooltip(
124 text: String,
125 style: TooltipStyle,
126 action: Option<Box<dyn Action>>,
127 measure: bool,
128 ) -> impl Element {
129 Flex::row()
130 .with_child({
131 let text = Text::new(text, style.text)
132 .constrained()
133 .with_max_width(style.max_text_width);
134 if measure {
135 text.flex(1., false).boxed()
136 } else {
137 text.flex(1., false).aligned().boxed()
138 }
139 })
140 .with_children(action.map(|action| {
141 let keystroke_label =
142 KeystrokeLabel::new(action, style.keystroke.container, style.keystroke.text);
143 if measure {
144 keystroke_label.boxed()
145 } else {
146 keystroke_label.aligned().boxed()
147 }
148 }))
149 .contained()
150 .with_style(style.container)
151 }
152}
153
154impl Element for Tooltip {
155 type LayoutState = ();
156 type PaintState = ();
157
158 fn layout(
159 &mut self,
160 constraint: SizeConstraint,
161 cx: &mut LayoutContext,
162 ) -> (Vector2F, Self::LayoutState) {
163 let size = self.child.layout(constraint, cx);
164 if let Some(tooltip) = self.tooltip.as_mut() {
165 tooltip.layout(SizeConstraint::new(Vector2F::zero(), cx.window_size), cx);
166 }
167 (size, ())
168 }
169
170 fn paint(
171 &mut self,
172 bounds: RectF,
173 visible_bounds: RectF,
174 _: &mut Self::LayoutState,
175 cx: &mut PaintContext,
176 ) {
177 self.child.paint(bounds.origin(), visible_bounds, cx);
178 if let Some(tooltip) = self.tooltip.as_mut() {
179 tooltip.paint(bounds.origin(), visible_bounds, cx);
180 }
181 }
182
183 fn dispatch_event(
184 &mut self,
185 event: &crate::Event,
186 _: RectF,
187 _: RectF,
188 _: &mut Self::LayoutState,
189 _: &mut Self::PaintState,
190 cx: &mut crate::EventContext,
191 ) -> bool {
192 self.child.dispatch_event(event, cx)
193 }
194
195 fn debug(
196 &self,
197 _: RectF,
198 _: &Self::LayoutState,
199 _: &Self::PaintState,
200 cx: &crate::DebugContext,
201 ) -> serde_json::Value {
202 json!({
203 "child": self.child.debug(cx),
204 "tooltip": self.tooltip.as_ref().map(|t| t.debug(cx)),
205 })
206 }
207}