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