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