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::{
32 Clipboard, Element, Event, Length, Padding, Pixels, Point, Rectangle,
33 Shell, Size, Vector,
34};
35
36#[allow(missing_debug_implementations)]
60pub 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 let state = tree.state.downcast_mut::<State>();
206
207 let previous_state = *state;
208 let was_idle = *state == State::Idle;
209
210 *state = cursor
211 .position_over(layout.bounds())
212 .map(|cursor_position| State::Hovered { cursor_position })
213 .unwrap_or_default();
214
215 let is_idle = *state == State::Idle;
216
217 if was_idle != is_idle
218 || (self.position == Position::FollowCursor
219 && previous_state != *state)
220 {
221 shell.request_redraw();
222 }
223
224 self.content.as_widget_mut().update(
225 &mut tree.children[0],
226 event,
227 layout,
228 cursor,
229 renderer,
230 clipboard,
231 shell,
232 viewport,
233 );
234 }
235
236 fn mouse_interaction(
237 &self,
238 tree: &widget::Tree,
239 layout: Layout<'_>,
240 cursor: mouse::Cursor,
241 viewport: &Rectangle,
242 renderer: &Renderer,
243 ) -> mouse::Interaction {
244 self.content.as_widget().mouse_interaction(
245 &tree.children[0],
246 layout,
247 cursor,
248 viewport,
249 renderer,
250 )
251 }
252
253 fn draw(
254 &self,
255 tree: &widget::Tree,
256 renderer: &mut Renderer,
257 theme: &Theme,
258 inherited_style: &renderer::Style,
259 layout: Layout<'_>,
260 cursor: mouse::Cursor,
261 viewport: &Rectangle,
262 ) {
263 self.content.as_widget().draw(
264 &tree.children[0],
265 renderer,
266 theme,
267 inherited_style,
268 layout,
269 cursor,
270 viewport,
271 );
272 }
273
274 fn overlay<'b>(
275 &'b mut self,
276 tree: &'b mut widget::Tree,
277 layout: Layout<'b>,
278 renderer: &Renderer,
279 viewport: &Rectangle,
280 translation: Vector,
281 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
282 let state = tree.state.downcast_ref::<State>();
283
284 let mut children = tree.children.iter_mut();
285
286 let content = self.content.as_widget_mut().overlay(
287 children.next().unwrap(),
288 layout,
289 renderer,
290 viewport,
291 translation,
292 );
293
294 let tooltip = if let State::Hovered { cursor_position } = *state {
295 Some(overlay::Element::new(Box::new(Overlay {
296 position: layout.position() + translation,
297 tooltip: &mut self.tooltip,
298 state: children.next().unwrap(),
299 cursor_position,
300 content_bounds: layout.bounds(),
301 snap_within_viewport: self.snap_within_viewport,
302 positioning: self.position,
303 gap: self.gap,
304 padding: self.padding,
305 class: &self.class,
306 })))
307 } else {
308 None
309 };
310
311 if content.is_some() || tooltip.is_some() {
312 Some(
313 overlay::Group::with_children(
314 content.into_iter().chain(tooltip).collect(),
315 )
316 .overlay(),
317 )
318 } else {
319 None
320 }
321 }
322}
323
324impl<'a, Message, Theme, Renderer> From<Tooltip<'a, Message, Theme, Renderer>>
325 for Element<'a, Message, Theme, Renderer>
326where
327 Message: 'a,
328 Theme: container::Catalog + 'a,
329 Renderer: text::Renderer + 'a,
330{
331 fn from(
332 tooltip: Tooltip<'a, Message, Theme, Renderer>,
333 ) -> Element<'a, Message, Theme, Renderer> {
334 Element::new(tooltip)
335 }
336}
337
338#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
340pub enum Position {
341 #[default]
343 Top,
344 Bottom,
346 Left,
348 Right,
350 FollowCursor,
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Default)]
355enum State {
356 #[default]
357 Idle,
358 Hovered {
359 cursor_position: Point,
360 },
361}
362
363struct Overlay<'a, 'b, Message, Theme, Renderer>
364where
365 Theme: container::Catalog,
366 Renderer: text::Renderer,
367{
368 position: Point,
369 tooltip: &'b mut Element<'a, Message, Theme, Renderer>,
370 state: &'b mut widget::Tree,
371 cursor_position: Point,
372 content_bounds: Rectangle,
373 snap_within_viewport: bool,
374 positioning: Position,
375 gap: f32,
376 padding: f32,
377 class: &'b Theme::Class<'a>,
378}
379
380impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
381 for Overlay<'_, '_, Message, Theme, Renderer>
382where
383 Theme: container::Catalog,
384 Renderer: text::Renderer,
385{
386 fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
387 let viewport = Rectangle::with_size(bounds);
388
389 let tooltip_layout = self.tooltip.as_widget_mut().layout(
390 self.state,
391 renderer,
392 &layout::Limits::new(
393 Size::ZERO,
394 if self.snap_within_viewport {
395 viewport.size()
396 } else {
397 Size::INFINITE
398 },
399 )
400 .shrink(Padding::new(self.padding)),
401 );
402
403 let text_bounds = tooltip_layout.bounds();
404 let x_center = self.position.x
405 + (self.content_bounds.width - text_bounds.width) / 2.0;
406 let y_center = self.position.y
407 + (self.content_bounds.height - text_bounds.height) / 2.0;
408
409 let mut tooltip_bounds = {
410 let offset = match self.positioning {
411 Position::Top => Vector::new(
412 x_center,
413 self.position.y
414 - text_bounds.height
415 - self.gap
416 - self.padding,
417 ),
418 Position::Bottom => Vector::new(
419 x_center,
420 self.position.y
421 + self.content_bounds.height
422 + self.gap
423 + self.padding,
424 ),
425 Position::Left => Vector::new(
426 self.position.x
427 - text_bounds.width
428 - self.gap
429 - self.padding,
430 y_center,
431 ),
432 Position::Right => Vector::new(
433 self.position.x
434 + self.content_bounds.width
435 + self.gap
436 + self.padding,
437 y_center,
438 ),
439 Position::FollowCursor => {
440 let translation =
441 self.position - self.content_bounds.position();
442
443 Vector::new(
444 self.cursor_position.x,
445 self.cursor_position.y - text_bounds.height,
446 ) + translation
447 }
448 };
449
450 Rectangle {
451 x: offset.x - self.padding,
452 y: offset.y - self.padding,
453 width: text_bounds.width + self.padding * 2.0,
454 height: text_bounds.height + self.padding * 2.0,
455 }
456 };
457
458 if self.snap_within_viewport {
459 if tooltip_bounds.x < viewport.x {
460 tooltip_bounds.x = viewport.x;
461 } else if viewport.x + viewport.width
462 < tooltip_bounds.x + tooltip_bounds.width
463 {
464 tooltip_bounds.x =
465 viewport.x + viewport.width - tooltip_bounds.width;
466 }
467
468 if tooltip_bounds.y < viewport.y {
469 tooltip_bounds.y = viewport.y;
470 } else if viewport.y + viewport.height
471 < tooltip_bounds.y + tooltip_bounds.height
472 {
473 tooltip_bounds.y =
474 viewport.y + viewport.height - tooltip_bounds.height;
475 }
476 }
477
478 layout::Node::with_children(
479 tooltip_bounds.size(),
480 vec![
481 tooltip_layout
482 .translate(Vector::new(self.padding, self.padding)),
483 ],
484 )
485 .translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y))
486 }
487
488 fn draw(
489 &self,
490 renderer: &mut Renderer,
491 theme: &Theme,
492 inherited_style: &renderer::Style,
493 layout: Layout<'_>,
494 cursor_position: mouse::Cursor,
495 ) {
496 let style = theme.style(self.class);
497
498 container::draw_background(renderer, &style, layout.bounds());
499
500 let defaults = renderer::Style {
501 text_color: style.text_color.unwrap_or(inherited_style.text_color),
502 };
503
504 self.tooltip.as_widget().draw(
505 self.state,
506 renderer,
507 theme,
508 &defaults,
509 layout.children().next().unwrap(),
510 cursor_position,
511 &Rectangle::with_size(Size::INFINITE),
512 );
513 }
514}