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