1use crate::container;
28use crate::core::layout::{self, Layout};
29use crate::core::mouse;
30use crate::core::overlay;
31use crate::core::renderer;
32use crate::core::text;
33use crate::core::time::{Duration, Instant};
34use crate::core::widget::{self, Widget};
35use crate::core::window;
36use crate::core::{
37 Clipboard, Element, Event, Length, Padding, Pixels, Point, Rectangle,
38 Shell, Size, Vector,
39};
40
41pub struct Tooltip<
65 'a,
66 Message,
67 Theme = crate::Theme,
68 Renderer = crate::Renderer,
69> where
70 Theme: container::Catalog,
71 Renderer: text::Renderer,
72{
73 content: Element<'a, Message, Theme, Renderer>,
74 tooltip: Element<'a, Message, Theme, Renderer>,
75 position: Position,
76 gap: f32,
77 padding: f32,
78 snap_within_viewport: bool,
79 delay: Duration,
80 class: Theme::Class<'a>,
81}
82
83impl<'a, Message, Theme, Renderer> Tooltip<'a, Message, Theme, Renderer>
84where
85 Theme: container::Catalog,
86 Renderer: text::Renderer,
87{
88 const DEFAULT_PADDING: f32 = 5.0;
90
91 pub fn new(
95 content: impl Into<Element<'a, Message, Theme, Renderer>>,
96 tooltip: impl Into<Element<'a, Message, Theme, Renderer>>,
97 position: Position,
98 ) -> Self {
99 Tooltip {
100 content: content.into(),
101 tooltip: tooltip.into(),
102 position,
103 gap: 0.0,
104 padding: Self::DEFAULT_PADDING,
105 snap_within_viewport: true,
106 delay: Duration::ZERO,
107 class: Theme::default(),
108 }
109 }
110
111 pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
113 self.gap = gap.into().0;
114 self
115 }
116
117 pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
119 self.padding = padding.into().0;
120 self
121 }
122
123 pub fn delay(mut self, delay: Duration) -> Self {
127 self.delay = delay;
128 self
129 }
130
131 pub fn snap_within_viewport(mut self, snap: bool) -> Self {
133 self.snap_within_viewport = snap;
134 self
135 }
136
137 #[must_use]
139 pub fn style(
140 mut self,
141 style: impl Fn(&Theme) -> container::Style + 'a,
142 ) -> Self
143 where
144 Theme::Class<'a>: From<container::StyleFn<'a, Theme>>,
145 {
146 self.class = (Box::new(style) as container::StyleFn<'a, Theme>).into();
147 self
148 }
149
150 #[cfg(feature = "advanced")]
152 #[must_use]
153 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
154 self.class = class.into();
155 self
156 }
157}
158
159impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
160 for Tooltip<'_, Message, Theme, Renderer>
161where
162 Theme: container::Catalog,
163 Renderer: text::Renderer,
164{
165 fn children(&self) -> Vec<widget::Tree> {
166 vec![
167 widget::Tree::new(&self.content),
168 widget::Tree::new(&self.tooltip),
169 ]
170 }
171
172 fn diff(&self, tree: &mut widget::Tree) {
173 tree.diff_children(&[
174 self.content.as_widget(),
175 self.tooltip.as_widget(),
176 ]);
177 }
178
179 fn state(&self) -> widget::tree::State {
180 widget::tree::State::new(State::default())
181 }
182
183 fn tag(&self) -> widget::tree::Tag {
184 widget::tree::Tag::of::<State>()
185 }
186
187 fn size(&self) -> Size<Length> {
188 self.content.as_widget().size()
189 }
190
191 fn size_hint(&self) -> Size<Length> {
192 self.content.as_widget().size_hint()
193 }
194
195 fn layout(
196 &mut self,
197 tree: &mut widget::Tree,
198 renderer: &Renderer,
199 limits: &layout::Limits,
200 ) -> layout::Node {
201 self.content.as_widget_mut().layout(
202 &mut tree.children[0],
203 renderer,
204 limits,
205 )
206 }
207
208 fn update(
209 &mut self,
210 tree: &mut widget::Tree,
211 event: &Event,
212 layout: Layout<'_>,
213 cursor: mouse::Cursor,
214 renderer: &Renderer,
215 clipboard: &mut dyn Clipboard,
216 shell: &mut Shell<'_, Message>,
217 viewport: &Rectangle,
218 ) {
219 if let Event::Mouse(_)
220 | Event::Window(window::Event::RedrawRequested(_)) = event
221 {
222 let state = tree.state.downcast_mut::<State>();
223 let now = Instant::now();
224 let cursor_position = cursor.position_over(layout.bounds());
225
226 match (*state, cursor_position) {
227 (State::Idle, Some(cursor_position)) => {
228 if self.delay == Duration::ZERO {
229 *state = State::Open { cursor_position };
230 shell.invalidate_layout();
231 } else {
232 *state = State::Hovered { at: now };
233 }
234
235 shell.request_redraw_at(now + self.delay);
236 }
237 (State::Hovered { .. }, None) => {
238 *state = State::Idle;
239 }
240 (State::Hovered { at, .. }, _) if at.elapsed() < self.delay => {
241 shell.request_redraw_at(now + self.delay - at.elapsed());
242 }
243 (State::Hovered { .. }, Some(cursor_position)) => {
244 *state = State::Open { cursor_position };
245 shell.invalidate_layout();
246 }
247 (
248 State::Open {
249 cursor_position: last_position,
250 },
251 Some(cursor_position),
252 ) if self.position == Position::FollowCursor
253 && last_position != cursor_position =>
254 {
255 *state = State::Open { cursor_position };
256 shell.request_redraw();
257 }
258 (State::Open { .. }, None) => {
259 *state = State::Idle;
260 shell.invalidate_layout();
261
262 if !matches!(
263 event,
264 Event::Window(window::Event::RedrawRequested(_)),
265 ) {
266 shell.request_redraw();
267 }
268 }
269 (State::Open { .. }, Some(_)) | (State::Idle, None) => (),
270 }
271 }
272
273 self.content.as_widget_mut().update(
274 &mut tree.children[0],
275 event,
276 layout,
277 cursor,
278 renderer,
279 clipboard,
280 shell,
281 viewport,
282 );
283 }
284
285 fn mouse_interaction(
286 &self,
287 tree: &widget::Tree,
288 layout: Layout<'_>,
289 cursor: mouse::Cursor,
290 viewport: &Rectangle,
291 renderer: &Renderer,
292 ) -> mouse::Interaction {
293 self.content.as_widget().mouse_interaction(
294 &tree.children[0],
295 layout,
296 cursor,
297 viewport,
298 renderer,
299 )
300 }
301
302 fn draw(
303 &self,
304 tree: &widget::Tree,
305 renderer: &mut Renderer,
306 theme: &Theme,
307 inherited_style: &renderer::Style,
308 layout: Layout<'_>,
309 cursor: mouse::Cursor,
310 viewport: &Rectangle,
311 ) {
312 self.content.as_widget().draw(
313 &tree.children[0],
314 renderer,
315 theme,
316 inherited_style,
317 layout,
318 cursor,
319 viewport,
320 );
321 }
322
323 fn overlay<'b>(
324 &'b mut self,
325 tree: &'b mut widget::Tree,
326 layout: Layout<'b>,
327 renderer: &Renderer,
328 viewport: &Rectangle,
329 translation: Vector,
330 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
331 let state = tree.state.downcast_ref::<State>();
332
333 let mut children = tree.children.iter_mut();
334
335 let content = self.content.as_widget_mut().overlay(
336 children.next().unwrap(),
337 layout,
338 renderer,
339 viewport,
340 translation,
341 );
342
343 let tooltip = if let State::Open { cursor_position } = *state {
344 Some(overlay::Element::new(Box::new(Overlay {
345 position: layout.position() + translation,
346 tooltip: &mut self.tooltip,
347 tree: children.next().unwrap(),
348 cursor_position,
349 content_bounds: layout.bounds(),
350 snap_within_viewport: self.snap_within_viewport,
351 positioning: self.position,
352 gap: self.gap,
353 padding: self.padding,
354 class: &self.class,
355 })))
356 } else {
357 None
358 };
359
360 if content.is_some() || tooltip.is_some() {
361 Some(
362 overlay::Group::with_children(
363 content.into_iter().chain(tooltip).collect(),
364 )
365 .overlay(),
366 )
367 } else {
368 None
369 }
370 }
371
372 fn operate(
373 &mut self,
374 tree: &mut widget::Tree,
375 layout: Layout<'_>,
376 renderer: &Renderer,
377 operation: &mut dyn widget::Operation,
378 ) {
379 operation.container(None, layout.bounds());
380 operation.traverse(&mut |operation| {
381 self.content.as_widget_mut().operate(
382 &mut tree.children[0],
383 layout,
384 renderer,
385 operation,
386 );
387 });
388 }
389}
390
391impl<'a, Message, Theme, Renderer> From<Tooltip<'a, Message, Theme, Renderer>>
392 for Element<'a, Message, Theme, Renderer>
393where
394 Message: 'a,
395 Theme: container::Catalog + 'a,
396 Renderer: text::Renderer + 'a,
397{
398 fn from(
399 tooltip: Tooltip<'a, Message, Theme, Renderer>,
400 ) -> Element<'a, Message, Theme, Renderer> {
401 Element::new(tooltip)
402 }
403}
404
405#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
407pub enum Position {
408 #[default]
410 Top,
411 Bottom,
413 Left,
415 Right,
417 FollowCursor,
419}
420
421#[derive(Debug, Clone, Copy, PartialEq, Default)]
422enum State {
423 #[default]
424 Idle,
425 Hovered {
426 at: Instant,
427 },
428 Open {
429 cursor_position: Point,
430 },
431}
432
433struct Overlay<'a, 'b, Message, Theme, Renderer>
434where
435 Theme: container::Catalog,
436 Renderer: text::Renderer,
437{
438 position: Point,
439 tooltip: &'b mut Element<'a, Message, Theme, Renderer>,
440 tree: &'b mut widget::Tree,
441 cursor_position: Point,
442 content_bounds: Rectangle,
443 snap_within_viewport: bool,
444 positioning: Position,
445 gap: f32,
446 padding: f32,
447 class: &'b Theme::Class<'a>,
448}
449
450impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
451 for Overlay<'_, '_, Message, Theme, Renderer>
452where
453 Theme: container::Catalog,
454 Renderer: text::Renderer,
455{
456 fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
457 let viewport = Rectangle::with_size(bounds);
458
459 let tooltip_layout = self.tooltip.as_widget_mut().layout(
460 self.tree,
461 renderer,
462 &layout::Limits::new(
463 Size::ZERO,
464 if self.snap_within_viewport {
465 viewport.size()
466 } else {
467 Size::INFINITE
468 },
469 )
470 .shrink(Padding::new(self.padding)),
471 );
472
473 let text_bounds = tooltip_layout.bounds();
474 let x_center = self.position.x
475 + (self.content_bounds.width - text_bounds.width) / 2.0;
476 let y_center = self.position.y
477 + (self.content_bounds.height - text_bounds.height) / 2.0;
478
479 let mut tooltip_bounds = {
480 let offset = match self.positioning {
481 Position::Top => Vector::new(
482 x_center,
483 self.position.y
484 - text_bounds.height
485 - self.gap
486 - self.padding,
487 ),
488 Position::Bottom => Vector::new(
489 x_center,
490 self.position.y
491 + self.content_bounds.height
492 + self.gap
493 + self.padding,
494 ),
495 Position::Left => Vector::new(
496 self.position.x
497 - text_bounds.width
498 - self.gap
499 - self.padding,
500 y_center,
501 ),
502 Position::Right => Vector::new(
503 self.position.x
504 + self.content_bounds.width
505 + self.gap
506 + self.padding,
507 y_center,
508 ),
509 Position::FollowCursor => {
510 let translation =
511 self.position - self.content_bounds.position();
512
513 Vector::new(
514 self.cursor_position.x,
515 self.cursor_position.y - text_bounds.height,
516 ) + translation
517 }
518 };
519
520 Rectangle {
521 x: offset.x - self.padding,
522 y: offset.y - self.padding,
523 width: text_bounds.width + self.padding * 2.0,
524 height: text_bounds.height + self.padding * 2.0,
525 }
526 };
527
528 if self.snap_within_viewport {
529 if tooltip_bounds.x < viewport.x {
530 tooltip_bounds.x = viewport.x;
531 } else if viewport.x + viewport.width
532 < tooltip_bounds.x + tooltip_bounds.width
533 {
534 tooltip_bounds.x =
535 viewport.x + viewport.width - tooltip_bounds.width;
536 }
537
538 if tooltip_bounds.y < viewport.y {
539 tooltip_bounds.y = viewport.y;
540 } else if viewport.y + viewport.height
541 < tooltip_bounds.y + tooltip_bounds.height
542 {
543 tooltip_bounds.y =
544 viewport.y + viewport.height - tooltip_bounds.height;
545 }
546 }
547
548 layout::Node::with_children(
549 tooltip_bounds.size(),
550 vec![
551 tooltip_layout
552 .translate(Vector::new(self.padding, self.padding)),
553 ],
554 )
555 .translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y))
556 }
557
558 fn draw(
559 &self,
560 renderer: &mut Renderer,
561 theme: &Theme,
562 inherited_style: &renderer::Style,
563 layout: Layout<'_>,
564 cursor_position: mouse::Cursor,
565 ) {
566 let style = theme.style(self.class);
567
568 container::draw_background(renderer, &style, layout.bounds());
569
570 let defaults = renderer::Style {
571 text_color: style.text_color.unwrap_or(inherited_style.text_color),
572 };
573
574 self.tooltip.as_widget().draw(
575 self.tree,
576 renderer,
577 theme,
578 &defaults,
579 layout.children().next().unwrap(),
580 cursor_position,
581 &Rectangle::with_size(Size::INFINITE),
582 );
583 }
584}