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