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<'b>,
275 renderer: &Renderer,
276 viewport: &Rectangle,
277 translation: Vector,
278 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
279 let state = tree.state.downcast_ref::<State>();
280
281 let mut children = tree.children.iter_mut();
282
283 let content = self.content.as_widget_mut().overlay(
284 children.next().unwrap(),
285 layout,
286 renderer,
287 viewport,
288 translation,
289 );
290
291 let tooltip = if let State::Hovered { cursor_position } = *state {
292 Some(overlay::Element::new(Box::new(Overlay {
293 position: layout.position() + translation,
294 tooltip: &self.tooltip,
295 state: children.next().unwrap(),
296 cursor_position,
297 content_bounds: layout.bounds(),
298 snap_within_viewport: self.snap_within_viewport,
299 positioning: self.position,
300 gap: self.gap,
301 padding: self.padding,
302 class: &self.class,
303 })))
304 } else {
305 None
306 };
307
308 if content.is_some() || tooltip.is_some() {
309 Some(
310 overlay::Group::with_children(
311 content.into_iter().chain(tooltip).collect(),
312 )
313 .overlay(),
314 )
315 } else {
316 None
317 }
318 }
319}
320
321impl<'a, Message, Theme, Renderer> From<Tooltip<'a, Message, Theme, Renderer>>
322 for Element<'a, Message, Theme, Renderer>
323where
324 Message: 'a,
325 Theme: container::Catalog + 'a,
326 Renderer: text::Renderer + 'a,
327{
328 fn from(
329 tooltip: Tooltip<'a, Message, Theme, Renderer>,
330 ) -> Element<'a, Message, Theme, Renderer> {
331 Element::new(tooltip)
332 }
333}
334
335#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
337pub enum Position {
338 #[default]
340 Top,
341 Bottom,
343 Left,
345 Right,
347 FollowCursor,
349}
350
351#[derive(Debug, Clone, Copy, PartialEq, Default)]
352enum State {
353 #[default]
354 Idle,
355 Hovered {
356 cursor_position: Point,
357 },
358}
359
360struct Overlay<'a, 'b, Message, Theme, Renderer>
361where
362 Theme: container::Catalog,
363 Renderer: text::Renderer,
364{
365 position: Point,
366 tooltip: &'b Element<'a, Message, Theme, Renderer>,
367 state: &'b mut widget::Tree,
368 cursor_position: Point,
369 content_bounds: Rectangle,
370 snap_within_viewport: bool,
371 positioning: Position,
372 gap: f32,
373 padding: f32,
374 class: &'b Theme::Class<'a>,
375}
376
377impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
378 for Overlay<'_, '_, Message, Theme, Renderer>
379where
380 Theme: container::Catalog,
381 Renderer: text::Renderer,
382{
383 fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
384 let viewport = Rectangle::with_size(bounds);
385
386 let tooltip_layout = self.tooltip.as_widget().layout(
387 self.state,
388 renderer,
389 &layout::Limits::new(
390 Size::ZERO,
391 if self.snap_within_viewport {
392 viewport.size()
393 } else {
394 Size::INFINITY
395 },
396 )
397 .shrink(Padding::new(self.padding)),
398 );
399
400 let text_bounds = tooltip_layout.bounds();
401 let x_center = self.position.x
402 + (self.content_bounds.width - text_bounds.width) / 2.0;
403 let y_center = self.position.y
404 + (self.content_bounds.height - text_bounds.height) / 2.0;
405
406 let mut tooltip_bounds = {
407 let offset = match self.positioning {
408 Position::Top => Vector::new(
409 x_center,
410 self.position.y
411 - text_bounds.height
412 - self.gap
413 - self.padding,
414 ),
415 Position::Bottom => Vector::new(
416 x_center,
417 self.position.y
418 + self.content_bounds.height
419 + self.gap
420 + self.padding,
421 ),
422 Position::Left => Vector::new(
423 self.position.x
424 - text_bounds.width
425 - self.gap
426 - self.padding,
427 y_center,
428 ),
429 Position::Right => Vector::new(
430 self.position.x
431 + self.content_bounds.width
432 + self.gap
433 + self.padding,
434 y_center,
435 ),
436 Position::FollowCursor => {
437 let translation =
438 self.position - self.content_bounds.position();
439
440 Vector::new(
441 self.cursor_position.x,
442 self.cursor_position.y - text_bounds.height,
443 ) + translation
444 }
445 };
446
447 Rectangle {
448 x: offset.x - self.padding,
449 y: offset.y - self.padding,
450 width: text_bounds.width + self.padding * 2.0,
451 height: text_bounds.height + self.padding * 2.0,
452 }
453 };
454
455 if self.snap_within_viewport {
456 if tooltip_bounds.x < viewport.x {
457 tooltip_bounds.x = viewport.x;
458 } else if viewport.x + viewport.width
459 < tooltip_bounds.x + tooltip_bounds.width
460 {
461 tooltip_bounds.x =
462 viewport.x + viewport.width - tooltip_bounds.width;
463 }
464
465 if tooltip_bounds.y < viewport.y {
466 tooltip_bounds.y = viewport.y;
467 } else if viewport.y + viewport.height
468 < tooltip_bounds.y + tooltip_bounds.height
469 {
470 tooltip_bounds.y =
471 viewport.y + viewport.height - tooltip_bounds.height;
472 }
473 }
474
475 layout::Node::with_children(
476 tooltip_bounds.size(),
477 vec![
478 tooltip_layout
479 .translate(Vector::new(self.padding, self.padding)),
480 ],
481 )
482 .translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y))
483 }
484
485 fn draw(
486 &self,
487 renderer: &mut Renderer,
488 theme: &Theme,
489 inherited_style: &renderer::Style,
490 layout: Layout<'_>,
491 cursor_position: mouse::Cursor,
492 ) {
493 let style = theme.style(self.class);
494
495 container::draw_background(renderer, &style, layout.bounds());
496
497 let defaults = renderer::Style {
498 text_color: style.text_color.unwrap_or(inherited_style.text_color),
499 };
500
501 self.tooltip.as_widget().draw(
502 self.state,
503 renderer,
504 theme,
505 &defaults,
506 layout.children().next().unwrap(),
507 cursor_position,
508 &Rectangle::with_size(Size::INFINITY),
509 );
510 }
511}