1mod interactive;
28mod render;
29
30pub(crate) use render::{begin_tooltip_frame, paint_global_tooltips};
31
32use std::rc::Rc;
33use std::sync::Arc;
34use std::time::Duration;
35use web_time::Instant;
36
37use crate::draw_ctx::DrawCtx;
38use crate::event::{Event, EventResult};
39use crate::geometry::{Point, Rect, Size};
40use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
41use crate::text::Font;
42use crate::widget::{current_mouse_world, Widget};
43
44use render::{submit_tooltip, TooltipRequest};
45
46const TOOLTIP_INITIAL_DELAY: Duration = Duration::from_millis(500);
52pub(super) const TOOLTIP_FONT_SIZE: f64 = 12.0;
53pub(super) const TOOLTIP_PAD_X: f64 = 8.0;
54pub(super) const TOOLTIP_PAD_Y: f64 = 6.0;
55pub(super) const TOOLTIP_GAP: f64 = 4.0;
56pub(super) const POINTER_TOOLTIP_EXTRA_DROP: f64 = 10.0;
59pub(super) const SCREEN_MARGIN: f64 = 4.0;
60
61#[derive(Clone)]
62pub(super) enum TooltipLineKind {
63 Text,
64 Code,
65 Link,
66}
67
68#[derive(Clone)]
69pub(super) struct TooltipLine {
70 pub text: String,
71 pub kind: TooltipLineKind,
72}
73
74pub struct Tooltip {
76 bounds: Rect,
77 children: Vec<Box<dyn Widget>>,
79 base: WidgetBase,
80
81 hover_started_at: Option<Instant>,
85 hovered: bool,
87 tooltip_visible: bool,
91 cursor: Point,
93
94 font: Arc<Font>,
95 lines: Vec<TooltipLine>,
96 disabled_lines: Vec<TooltipLine>,
97 disabled_when: Option<Rc<dyn Fn() -> bool>>,
98 at_pointer: bool,
99
100 interactive: bool,
104 content: Option<Box<dyn Widget>>,
108 content_size: Size,
110 tip_open: bool,
114 tip_hovered: bool,
116 tip_panel_local: Option<Rect>,
119 content_origin_local: Point,
121 close_requested_at: Option<Instant>,
124 last_content_path: Option<Vec<usize>>,
127}
128
129impl Tooltip {
130 pub fn new(child: Box<dyn Widget>, text: impl Into<String>, font: Arc<Font>) -> Self {
132 Self {
133 bounds: Rect::default(),
134 children: vec![child],
135 base: WidgetBase::new(),
136 hover_started_at: None,
137 hovered: false,
138 tooltip_visible: false,
139 cursor: Point::ORIGIN,
140 font,
141 lines: text_to_lines(text),
142 disabled_lines: Vec::new(),
143 disabled_when: None,
144 at_pointer: true,
145 interactive: false,
146 content: None,
147 content_size: Size::new(0.0, 0.0),
148 tip_open: false,
149 tip_hovered: false,
150 tip_panel_local: None,
151 content_origin_local: Point::ORIGIN,
152 close_requested_at: None,
153 last_content_path: None,
154 }
155 }
156
157 pub fn with_text(mut self, text: impl Into<String>) -> Self {
160 self.lines.extend(text_to_lines(text));
161 self
162 }
163
164 pub fn with_code_line(mut self, text: impl Into<String>) -> Self {
166 self.lines.push(TooltipLine {
167 text: text.into(),
168 kind: TooltipLineKind::Code,
169 });
170 self
171 }
172
173 pub fn with_link_line(mut self, text: impl Into<String>) -> Self {
177 self.lines.push(TooltipLine {
178 text: text.into(),
179 kind: TooltipLineKind::Link,
180 });
181 self
182 }
183
184 pub fn with_interactive_content(mut self, content: Box<dyn Widget>) -> Self {
190 self.interactive = true;
191 self.content = Some(content);
192 self.at_pointer = false;
193 self
194 }
195
196 pub fn at_pointer(mut self) -> Self {
199 self.at_pointer = true;
200 self
201 }
202
203 pub fn at_widget(mut self) -> Self {
206 self.at_pointer = false;
207 self
208 }
209
210 pub fn with_disabled_text(
212 mut self,
213 text: impl Into<String>,
214 disabled_when: impl Fn() -> bool + 'static,
215 ) -> Self {
216 self.disabled_lines = text_to_lines(text);
217 self.disabled_when = Some(Rc::new(disabled_when));
218 self
219 }
220
221 pub fn with_margin(mut self, m: Insets) -> Self {
222 self.base.margin = m;
223 self
224 }
225 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
226 self.base.h_anchor = h;
227 self
228 }
229 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
230 self.base.v_anchor = v;
231 self
232 }
233
234 fn show_tip(&self) -> bool {
235 self.hovered
236 && self
237 .hover_started_at
238 .map(|started| started.elapsed() >= TOOLTIP_INITIAL_DELAY)
239 .unwrap_or(false)
240 }
241
242 fn remaining_delay(&self) -> Option<Duration> {
243 if !self.hovered {
244 return None;
245 }
246 let elapsed = self.hover_started_at?.elapsed();
247 Some(TOOLTIP_INITIAL_DELAY.saturating_sub(elapsed))
248 }
249
250 fn active_lines(&self) -> Vec<TooltipLine> {
251 if self.disabled_when.as_ref().map(|f| f()).unwrap_or(false)
252 && !self.disabled_lines.is_empty()
253 {
254 self.disabled_lines.clone()
255 } else {
256 self.lines.clone()
257 }
258 }
259}
260
261impl Widget for Tooltip {
262 fn type_name(&self) -> &'static str {
263 "Tooltip"
264 }
265 fn bounds(&self) -> Rect {
266 self.bounds
267 }
268 fn set_bounds(&mut self, b: Rect) {
269 self.bounds = b;
270 }
271 fn children(&self) -> &[Box<dyn Widget>] {
272 &self.children
273 }
274 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
275 &mut self.children
276 }
277
278 fn margin(&self) -> Insets {
279 self.base.margin
280 }
281 fn widget_base(&self) -> Option<&WidgetBase> {
282 Some(&self.base)
283 }
284 fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
285 Some(&mut self.base)
286 }
287 fn h_anchor(&self) -> HAnchor {
288 self.base.h_anchor
289 }
290 fn v_anchor(&self) -> VAnchor {
291 self.base.v_anchor
292 }
293
294 fn is_focusable(&self) -> bool {
295 self.children
296 .first()
297 .map(|c| c.is_focusable())
298 .unwrap_or(false)
299 }
300
301 fn layout(&mut self, available: Size) -> Size {
302 let s = if let Some(child) = self.children.first_mut() {
303 let cs = child.layout(available);
304 child.set_bounds(Rect::new(0.0, 0.0, cs.width, cs.height));
305 cs
306 } else {
307 available
308 };
309 self.bounds = Rect::new(0.0, 0.0, s.width, s.height);
310 s
311 }
312
313 fn paint(&mut self, _: &mut dyn DrawCtx) {}
314
315 fn paint_overlay(&mut self, ctx: &mut dyn DrawCtx) {
316 if self.interactive {
319 return;
320 }
321
322 let should_show = self.show_tip();
323
324 if self.hovered && !should_show {
325 if let Some(remaining) = self.remaining_delay() {
326 if remaining.is_zero() {
327 crate::animation::request_draw();
328 } else {
329 crate::animation::request_draw_after(remaining);
330 }
331 }
332 }
333
334 if should_show != self.tooltip_visible {
335 self.tooltip_visible = should_show;
336 crate::animation::request_draw();
342 }
343
344 if !should_show {
345 return;
346 }
347
348 let anchor = if self.at_pointer {
349 current_mouse_world().unwrap_or(self.cursor)
350 } else {
351 let mut x = self.bounds.width * 0.5;
352 let mut y = 0.0;
358 ctx.root_transform().transform(&mut x, &mut y);
359 Point::new(x, y)
360 };
361 submit_tooltip(TooltipRequest {
362 font: Arc::clone(&self.font),
363 lines: self.active_lines(),
364 anchor,
365 at_pointer: self.at_pointer,
366 });
367 }
368
369 fn paint_global_overlay(&mut self, ctx: &mut dyn DrawCtx) {
370 if self.interactive {
371 self.paint_interactive_tip(ctx);
372 }
373 }
374
375 fn hit_test_global_overlay(&self, local_pos: Point) -> bool {
376 self.interactive && self.interactive_hit(local_pos)
377 }
378
379 fn on_unconsumed_key(
380 &mut self,
381 key: &crate::event::Key,
382 _modifiers: crate::event::Modifiers,
383 ) -> EventResult {
384 if self.interactive {
385 self.interactive_unconsumed_key(key)
386 } else {
387 EventResult::Ignored
388 }
389 }
390
391 fn on_event(&mut self, event: &Event) -> EventResult {
392 if self.interactive {
393 return self.on_interactive_event(event);
394 }
395 match event {
396 Event::MouseMove { pos } => {
397 let was = self.hovered;
398 self.hovered = self.hit_test(*pos);
399 self.cursor = *pos;
400 if self.hovered && !was {
401 self.hover_started_at = Some(Instant::now());
402 crate::animation::request_draw_after(TOOLTIP_INITIAL_DELAY);
403 } else if !self.hovered {
404 self.hover_started_at = None;
405 if self.tooltip_visible {
406 self.tooltip_visible = false;
407 crate::animation::request_draw();
408 }
409 }
410 if self.hovered != was {
411 crate::animation::request_draw();
412 }
413 self.children
414 .first_mut()
415 .map(|child| child.on_event(event))
416 .unwrap_or(EventResult::Ignored)
417 }
418 Event::MouseWheel { .. } => {
419 self.hovered = false;
420 self.hover_started_at = None;
421 if self.tooltip_visible {
422 self.tooltip_visible = false;
423 crate::animation::request_draw();
424 }
425 self.children
426 .first_mut()
427 .map(|child| child.on_event(event))
428 .unwrap_or(EventResult::Ignored)
429 }
430 _ => self
431 .children
432 .first_mut()
433 .map(|child| child.on_event(event))
434 .unwrap_or(EventResult::Ignored),
435 }
436 }
437
438 fn hit_test(&self, local_pos: Point) -> bool {
439 local_pos.x >= 0.0
440 && local_pos.x <= self.bounds.width
441 && local_pos.y >= 0.0
442 && local_pos.y <= self.bounds.height
443 }
444}
445
446fn text_to_lines(text: impl Into<String>) -> Vec<TooltipLine> {
447 text.into()
448 .lines()
449 .map(|line| TooltipLine {
450 text: line.to_owned(),
451 kind: TooltipLineKind::Text,
452 })
453 .collect()
454}
455
456#[cfg(test)]
457mod tests;