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