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