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