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