1use super::*;
4
5use iced_widget::core::Transformation;
6use iced_widget::renderer::wgpu::primitive;
7
8pub use iced_tooltip::Position;
9
10pub fn plain<'a, Message, Renderer>(
11 content: impl Into<Element<'a, Message, Theme, Renderer>>,
12 supporting_text: impl text::IntoFragment<'a>,
13 position: Position,
14) -> PlainTooltip<'a, Message, Renderer>
15where
16 Message: 'a,
17 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
18{
19 let type_scale = tokens::component::tooltip::PLAIN_SUPPORTING_TEXT;
20
21 let tooltip = Container::new(plain_supporting_text::<Renderer>(
22 supporting_text,
23 type_scale,
24 ))
25 .padding(Padding {
26 top: 0.0,
27 right: plain_tooltip_inner_horizontal_padding(),
28 bottom: 0.0,
29 left: plain_tooltip_inner_horizontal_padding(),
30 })
31 .max_width(plain_tooltip_inner_max_width());
32
33 RichTooltip::new(content, tooltip, position)
34 .gap(tokens::component::tooltip::SPACING_BETWEEN_TOOLTIP_AND_ANCHOR)
35 .padding(tokens::component::tooltip::PLAIN_VERTICAL_SPACE)
36 .interactive_surface(false)
37 .style(tooltip_style::plain)
38}
39
40pub fn rich<'a, Message, Renderer>(
41 content: impl Into<Element<'a, Message, Theme, Renderer>>,
42 supporting_text: impl text::IntoFragment<'a>,
43 position: Position,
44) -> RichTooltip<'a, Message, Renderer>
45where
46 Message: 'a,
47 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
48{
49 rich_surface(content, None, supporting_text, None, position)
50}
51
52pub fn rich_with_title<'a, Message, Renderer>(
53 content: impl Into<Element<'a, Message, Theme, Renderer>>,
54 title: impl text::IntoFragment<'a>,
55 supporting_text: impl text::IntoFragment<'a>,
56 position: Position,
57) -> RichTooltip<'a, Message, Renderer>
58where
59 Message: 'a,
60 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
61{
62 rich_surface(
63 content,
64 Some(title.into_fragment()),
65 supporting_text,
66 None,
67 position,
68 )
69}
70
71pub fn rich_with_title_action<'a, Message, Renderer>(
72 content: impl Into<Element<'a, Message, Theme, Renderer>>,
73 title: impl text::IntoFragment<'a>,
74 supporting_text: impl text::IntoFragment<'a>,
75 action: impl Into<Element<'a, Message, Theme, Renderer>>,
76 position: Position,
77) -> RichTooltip<'a, Message, Renderer>
78where
79 Message: 'a,
80 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
81{
82 rich_surface(
83 content,
84 Some(title.into_fragment()),
85 supporting_text,
86 Some(action.into()),
87 position,
88 )
89}
90
91pub fn rich_action<'a, Message, Renderer>(
92 label: impl text::IntoFragment<'a>,
93) -> button::Button<'a, Message, Renderer>
94where
95 Message: Clone + 'a,
96 Renderer:
97 iced_widget::graphics::geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
98{
99 button::text(label)
100}
101
102pub fn rich_action_button<'a, Message, Renderer>(
103 label: impl text::IntoFragment<'a>,
104 on_press: Message,
105) -> Element<'a, Message, Theme, Renderer>
106where
107 Message: Clone + 'a,
108 Renderer:
109 iced_widget::graphics::geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
110{
111 rich_action(label).on_press(on_press).into()
112}
113
114fn plain_supporting_text<'a, Renderer>(
115 supporting_text: impl text::IntoFragment<'a>,
116 type_scale: tokens::typography::TypeScale,
117) -> Text<'a, Theme, Renderer>
118where
119 Renderer: core_text::Renderer,
120{
121 text_with_metrics(supporting_text, type_scale.size, type_scale.line_height)
122 .wrapping(text::Wrapping::Word)
123}
124
125fn rich_surface<'a, Message, Renderer>(
126 content: impl Into<Element<'a, Message, Theme, Renderer>>,
127 title: Option<text::Fragment<'a>>,
128 supporting_text: impl text::IntoFragment<'a>,
129 action: Option<Element<'a, Message, Theme, Renderer>>,
130 position: Position,
131) -> RichTooltip<'a, Message, Renderer>
132where
133 Message: 'a,
134 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
135{
136 let has_title = title.is_some();
137 let has_action = action.is_some();
138 let mut tooltip = iced_widget::Column::new()
139 .width(Length::Fill)
140 .padding(Padding {
141 top: 0.0,
142 right: tokens::component::tooltip::RICH_HORIZONTAL_SPACE,
143 bottom: 0.0,
144 left: tokens::component::tooltip::RICH_HORIZONTAL_SPACE,
145 });
146
147 if let Some(title) = title {
148 tooltip = tooltip.push(
149 Container::new(rich_title_text::<Renderer>(title))
150 .padding(Padding {
151 top: rich_title_top_padding(),
152 right: 0.0,
153 bottom: 0.0,
154 left: 0.0,
155 })
156 .width(Length::Fill),
157 );
158 }
159
160 tooltip = tooltip.push(
161 Container::new(rich_supporting_text::<Renderer>(supporting_text))
162 .padding(rich_supporting_text_padding(has_title, has_action))
163 .width(Length::Fill),
164 );
165
166 if let Some(action) = action {
167 tooltip = tooltip.push(
168 Container::new(action)
169 .padding(Padding {
170 top: 0.0,
171 right: 0.0,
172 bottom: tokens::component::tooltip::RICH_ACTION_LABEL_BOTTOM_PADDING,
173 left: 0.0,
174 })
175 .width(Length::Fill),
176 );
177 }
178
179 let tooltip = Container::new(tooltip)
180 .width(Length::Fill)
181 .height(Length::Shrink)
182 .max_width(tokens::component::tooltip::RICH_MAX_WIDTH);
183
184 RichTooltip::new(content, tooltip, position)
185 .gap(tokens::component::tooltip::SPACING_BETWEEN_TOOLTIP_AND_ANCHOR)
186 .padding(0.0)
187 .clip_padding(rich_tooltip_shadow_padding())
188 .style(tooltip_style::rich)
189}
190
191pub type PlainTooltip<'a, Message, Renderer = iced_widget::Renderer> =
193 RichTooltip<'a, Message, Renderer>;
194
195pub struct RichTooltip<'a, Message, Renderer = iced_widget::Renderer>
198where
199 Renderer: core_text::Renderer,
200{
201 content: Element<'a, Message, Theme, Renderer>,
202 tooltip: Element<'a, Message, Theme, Renderer>,
203 position: Position,
204 gap: f32,
205 padding: f32,
206 clip_padding: f32,
207 snap_within_viewport: bool,
208 interactive_surface: bool,
209 class: <Theme as iced_container::Catalog>::Class<'a>,
210}
211
212impl<Message, Renderer> std::fmt::Debug for RichTooltip<'_, Message, Renderer>
213where
214 Renderer: core_text::Renderer,
215{
216 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217 f.debug_struct("RichTooltip")
218 .field("position", &self.position)
219 .field("gap", &self.gap)
220 .field("padding", &self.padding)
221 .field("clip_padding", &self.clip_padding)
222 .field("snap_within_viewport", &self.snap_within_viewport)
223 .field("interactive_surface", &self.interactive_surface)
224 .finish_non_exhaustive()
225 }
226}
227
228impl<'a, Message, Renderer> RichTooltip<'a, Message, Renderer>
229where
230 Message: 'a,
231 Renderer: core_text::Renderer + 'a,
232{
233 pub fn new(
234 content: impl Into<Element<'a, Message, Theme, Renderer>>,
235 tooltip: impl Into<Element<'a, Message, Theme, Renderer>>,
236 position: Position,
237 ) -> Self {
238 Self {
239 content: content.into(),
240 tooltip: tooltip.into(),
241 position,
242 gap: 0.0,
243 padding: 0.0,
244 clip_padding: 0.0,
245 snap_within_viewport: true,
246 interactive_surface: true,
247 class: <Theme as iced_container::Catalog>::default(),
248 }
249 }
250
251 pub fn gap(mut self, gap: impl Into<Pixels>) -> Self {
252 self.gap = gap.into().0;
253 self
254 }
255
256 pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
257 self.padding = padding.into().0;
258 self
259 }
260
261 fn clip_padding(mut self, padding: impl Into<Pixels>) -> Self {
262 self.clip_padding = padding.into().0;
263 self
264 }
265
266 pub fn snap_within_viewport(mut self, snap: bool) -> Self {
267 self.snap_within_viewport = snap;
268 self
269 }
270
271 fn interactive_surface(mut self, interactive: bool) -> Self {
272 self.interactive_surface = interactive;
273 self
274 }
275
276 #[must_use]
277 pub fn style(mut self, style: impl Fn(&Theme) -> iced_container::Style + 'a) -> Self
278 where
279 <Theme as iced_container::Catalog>::Class<'a>: From<iced_container::StyleFn<'a, Theme>>,
280 {
281 self.class = (Box::new(style) as iced_container::StyleFn<'a, Theme>).into();
282 self
283 }
284}
285
286impl<Message, Renderer> Widget<Message, Theme, Renderer> for RichTooltip<'_, Message, Renderer>
287where
288 Renderer: core_text::Renderer,
289{
290 fn tag(&self) -> tree::Tag {
291 tree::Tag::of::<RichTooltipState>()
292 }
293
294 fn state(&self) -> tree::State {
295 tree::State::new(RichTooltipState::default())
296 }
297
298 fn children(&self) -> Vec<Tree> {
299 vec![Tree::new(&self.content), Tree::new(&self.tooltip)]
300 }
301
302 fn diff(&self, tree: &mut Tree) {
303 tree.diff_children(&[self.content.as_widget(), self.tooltip.as_widget()]);
304 }
305
306 fn size(&self) -> Size<Length> {
307 self.content.as_widget().size()
308 }
309
310 fn size_hint(&self) -> Size<Length> {
311 self.content.as_widget().size_hint()
312 }
313
314 fn layout(
315 &mut self,
316 tree: &mut Tree,
317 renderer: &Renderer,
318 limits: &layout::Limits,
319 ) -> layout::Node {
320 self.content
321 .as_widget_mut()
322 .layout(&mut tree.children[0], renderer, limits)
323 }
324
325 fn update(
326 &mut self,
327 tree: &mut Tree,
328 event: &Event,
329 layout: Layout<'_>,
330 cursor: mouse::Cursor,
331 renderer: &Renderer,
332 clipboard: &mut dyn Clipboard,
333 shell: &mut Shell<'_, Message>,
334 viewport: &Rectangle,
335 ) {
336 if let Event::Mouse(_) | Event::Window(window::Event::RedrawRequested(_)) = event {
337 let state = tree.state.downcast_mut::<RichTooltipState>();
338 let now = tooltip_event_time(event);
339 let was_visible = state.is_visible();
340 let cursor_position = cursor.position_over(layout.bounds());
341
342 state.advance(now);
343
344 if let Some(cursor_position) = cursor_position {
345 state.show(now, cursor_position);
346 shell.request_redraw();
347 } else if rich_tooltip_anchor_exit_dismisses(self.interactive_surface, state) {
348 state.dismiss(now);
349 }
350
351 if was_visible != state.is_visible() {
352 shell.invalidate_layout();
353 }
354
355 request_tooltip_redraw(state, shell);
356 }
357
358 self.content.as_widget_mut().update(
359 &mut tree.children[0],
360 event,
361 layout,
362 cursor,
363 renderer,
364 clipboard,
365 shell,
366 viewport,
367 );
368 }
369
370 fn mouse_interaction(
371 &self,
372 tree: &Tree,
373 layout: Layout<'_>,
374 cursor: mouse::Cursor,
375 viewport: &Rectangle,
376 renderer: &Renderer,
377 ) -> mouse::Interaction {
378 self.content.as_widget().mouse_interaction(
379 &tree.children[0],
380 layout,
381 cursor,
382 viewport,
383 renderer,
384 )
385 }
386
387 fn draw(
388 &self,
389 tree: &Tree,
390 renderer: &mut Renderer,
391 theme: &Theme,
392 defaults: &renderer::Style,
393 layout: Layout<'_>,
394 cursor: mouse::Cursor,
395 viewport: &Rectangle,
396 ) {
397 self.content.as_widget().draw(
398 &tree.children[0],
399 renderer,
400 theme,
401 defaults,
402 layout,
403 cursor,
404 viewport,
405 );
406 }
407
408 fn overlay<'b>(
409 &'b mut self,
410 tree: &'b mut Tree,
411 layout: Layout<'b>,
412 renderer: &Renderer,
413 viewport: &Rectangle,
414 translation: Vector,
415 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
416 let state = tree.state.downcast_mut::<RichTooltipState>();
417 let mut children = tree.children.iter_mut();
418
419 let content = self.content.as_widget_mut().overlay(
420 children.next().unwrap(),
421 layout,
422 renderer,
423 viewport,
424 translation,
425 );
426
427 let content_bounds = translated_bounds(layout.bounds(), translation);
428 let tooltip = if state.is_visible() {
429 Some(overlay::Element::new(Box::new(RichTooltipOverlay {
430 tooltip: &mut self.tooltip,
431 tree: children.next().unwrap(),
432 state,
433 content_bounds,
434 snap_within_viewport: self.snap_within_viewport,
435 position: self.position,
436 gap: self.gap,
437 padding: self.padding,
438 clip_padding: self.clip_padding,
439 interactive_surface: self.interactive_surface,
440 class: &self.class,
441 })))
442 } else {
443 let _ = children.next();
444 None
445 };
446
447 if content.is_some() || tooltip.is_some() {
448 Some(
449 overlay::Group::with_children(content.into_iter().chain(tooltip).collect())
450 .overlay(),
451 )
452 } else {
453 None
454 }
455 }
456
457 fn operate(
458 &mut self,
459 tree: &mut Tree,
460 layout: Layout<'_>,
461 renderer: &Renderer,
462 operation: &mut dyn core_widget::Operation,
463 ) {
464 operation.container(None, layout.bounds());
465 operation.traverse(&mut |operation| {
466 self.content.as_widget_mut().operate(
467 &mut tree.children[0],
468 layout,
469 renderer,
470 operation,
471 );
472 });
473 }
474}
475
476impl<'a, Message, Renderer> From<RichTooltip<'a, Message, Renderer>>
477 for Element<'a, Message, Theme, Renderer>
478where
479 Message: 'a,
480 Renderer: core_text::Renderer + 'a,
481{
482 fn from(tooltip: RichTooltip<'a, Message, Renderer>) -> Self {
483 Element::new(tooltip)
484 }
485}
486
487#[derive(Debug, Clone, Copy, PartialEq, Eq)]
488enum TooltipPhase {
489 Hidden,
490 Showing,
491 Shown,
492 Dismissing,
493}
494
495#[derive(Debug, Clone, Copy, PartialEq)]
496struct RichTooltipState {
497 phase: TooltipPhase,
498 cursor_position: Point,
499 started_at: Option<Instant>,
500 scale_from: f32,
501 alpha_from: f32,
502}
503
504impl Default for RichTooltipState {
505 fn default() -> Self {
506 Self {
507 phase: TooltipPhase::Hidden,
508 cursor_position: Point::ORIGIN,
509 started_at: None,
510 scale_from: tokens::component::tooltip::SCALE_START,
511 alpha_from: 0.0,
512 }
513 }
514}
515
516#[derive(Debug, Clone, Copy, PartialEq)]
517struct TooltipAnimationFrame {
518 scale: f32,
519 alpha: f32,
520}
521
522impl RichTooltipState {
523 fn show(&mut self, now: Instant, cursor_position: Point) {
524 self.cursor_position = cursor_position;
525
526 match self.phase {
527 TooltipPhase::Showing | TooltipPhase::Shown => {}
528 TooltipPhase::Hidden | TooltipPhase::Dismissing => {
529 let frame = self.animation_frame(now);
530
531 self.scale_from = frame.scale;
532 self.alpha_from = frame.alpha;
533 self.phase = TooltipPhase::Showing;
534 self.started_at = Some(now);
535 }
536 }
537 }
538
539 fn dismiss(&mut self, now: Instant) {
540 match self.phase {
541 TooltipPhase::Hidden | TooltipPhase::Dismissing => {}
542 TooltipPhase::Showing | TooltipPhase::Shown => {
543 let frame = self.animation_frame(now);
544
545 self.scale_from = frame.scale;
546 self.alpha_from = frame.alpha;
547 self.phase = TooltipPhase::Dismissing;
548 self.started_at = Some(now);
549 }
550 }
551 }
552
553 fn advance(&mut self, now: Instant) {
554 match self.phase {
555 TooltipPhase::Hidden | TooltipPhase::Shown => {}
556 TooltipPhase::Showing if self.progress(now) >= 1.0 => {
557 self.phase = TooltipPhase::Shown;
558 self.started_at = None;
559 self.scale_from = 1.0;
560 self.alpha_from = 1.0;
561 }
562 TooltipPhase::Showing => {}
563 TooltipPhase::Dismissing if self.progress(now) >= 1.0 => {
564 *self = Self::default();
565 }
566 TooltipPhase::Dismissing => {}
567 }
568 }
569
570 fn is_visible(&self) -> bool {
571 self.phase != TooltipPhase::Hidden
572 }
573
574 fn is_animating(&self) -> bool {
575 matches!(self.phase, TooltipPhase::Showing | TooltipPhase::Dismissing)
576 }
577
578 fn animation_frame(&self, now: Instant) -> TooltipAnimationFrame {
579 match self.phase {
580 TooltipPhase::Hidden => TooltipAnimationFrame {
581 scale: tokens::component::tooltip::SCALE_START,
582 alpha: 0.0,
583 },
584 TooltipPhase::Shown => TooltipAnimationFrame {
585 scale: 1.0,
586 alpha: 1.0,
587 },
588 TooltipPhase::Showing => {
589 let progress = self.progress(now);
590 let scale_progress = tooltip_scale_easing(progress);
591
592 TooltipAnimationFrame {
593 scale: lerp(self.scale_from, 1.0, scale_progress),
594 alpha: lerp(self.alpha_from, 1.0, progress),
595 }
596 }
597 TooltipPhase::Dismissing => {
598 let progress = self.progress(now);
599 let scale_progress = tooltip_scale_easing(progress);
600
601 TooltipAnimationFrame {
602 scale: lerp(
603 self.scale_from,
604 tokens::component::tooltip::SCALE_START,
605 scale_progress,
606 ),
607 alpha: lerp(self.alpha_from, 0.0, progress),
608 }
609 }
610 }
611 }
612
613 fn progress(&self, now: Instant) -> f32 {
614 let Some(started_at) = self.started_at else {
615 return match self.phase {
616 TooltipPhase::Showing | TooltipPhase::Dismissing => 1.0,
617 TooltipPhase::Hidden | TooltipPhase::Shown => 0.0,
618 };
619 };
620
621 let duration = duration_ms(match self.phase {
622 TooltipPhase::Showing => tokens::component::tooltip::FADE_IN_DURATION_MS,
623 TooltipPhase::Dismissing => tokens::component::tooltip::FADE_OUT_DURATION_MS,
624 TooltipPhase::Hidden | TooltipPhase::Shown => 0,
625 });
626
627 if duration.is_zero() {
628 return 1.0;
629 }
630
631 (now.saturating_duration_since(started_at).as_secs_f32() / duration.as_secs_f32())
632 .clamp(0.0, 1.0)
633 }
634}
635
636struct RichTooltipOverlay<'a, 'b, Message, Renderer>
637where
638 Renderer: core_text::Renderer,
639{
640 tooltip: &'b mut Element<'a, Message, Theme, Renderer>,
641 tree: &'b mut Tree,
642 state: &'b mut RichTooltipState,
643 content_bounds: Rectangle,
644 snap_within_viewport: bool,
645 position: Position,
646 gap: f32,
647 padding: f32,
648 clip_padding: f32,
649 interactive_surface: bool,
650 class: &'b <Theme as iced_container::Catalog>::Class<'a>,
651}
652
653impl<Message, Renderer> overlay::Overlay<Message, Theme, Renderer>
654 for RichTooltipOverlay<'_, '_, Message, Renderer>
655where
656 Renderer: core_text::Renderer,
657{
658 fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
659 let viewport = Rectangle::with_size(bounds);
660 let total_padding = self.padding + self.clip_padding;
661 let tooltip_layout = self.tooltip.as_widget_mut().layout(
662 self.tree,
663 renderer,
664 &layout::Limits::new(
665 Size::ZERO,
666 if self.snap_within_viewport {
667 viewport.size()
668 } else {
669 Size::INFINITE
670 },
671 )
672 .shrink(Padding::new(total_padding)),
673 );
674
675 let tooltip_bounds = rich_tooltip_surface_bounds(
676 self.content_bounds,
677 tooltip_layout.bounds().size(),
678 viewport,
679 self.state.cursor_position,
680 self.position,
681 self.gap,
682 self.padding,
683 self.clip_padding,
684 self.snap_within_viewport,
685 );
686
687 layout::Node::with_children(
688 tooltip_bounds.size(),
689 vec![tooltip_layout.translate(Vector::new(total_padding, total_padding))],
690 )
691 .translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y))
692 }
693
694 fn update(
695 &mut self,
696 event: &Event,
697 layout: Layout<'_>,
698 cursor: mouse::Cursor,
699 renderer: &Renderer,
700 clipboard: &mut dyn Clipboard,
701 shell: &mut Shell<'_, Message>,
702 ) {
703 let tooltip_bounds = layout.bounds();
704 let now = tooltip_event_time(event);
705 let cursor_in_content = cursor.is_over(self.content_bounds);
706 let cursor_in_tooltip = cursor.is_over(tooltip_bounds);
707 let cursor_in_keep_alive = self.interactive_surface
708 && rich_tooltip_keep_alive_contains(
709 self.content_bounds,
710 tooltip_bounds,
711 self.position,
712 cursor,
713 );
714
715 if cursor_in_tooltip && let Some(child_layout) = layout.children().next() {
716 self.tooltip.as_widget_mut().update(
717 self.tree,
718 event,
719 child_layout,
720 cursor,
721 renderer,
722 clipboard,
723 shell,
724 &Rectangle::with_size(Size::INFINITE),
725 );
726 }
727
728 if let Event::Mouse(_) | Event::Window(window::Event::RedrawRequested(_)) = event {
729 let was_visible = self.state.is_visible();
730
731 if cursor_in_content || cursor_in_keep_alive {
732 if let Some(cursor_position) = cursor.position_over(self.content_bounds) {
733 self.state.show(now, cursor_position);
734 } else if self.state.phase == TooltipPhase::Dismissing {
735 self.state.show(now, self.state.cursor_position);
736 }
737 } else {
738 self.state.dismiss(now);
739 }
740
741 self.state.advance(now);
742
743 if was_visible != self.state.is_visible() {
744 shell.invalidate_layout();
745 }
746
747 request_tooltip_redraw(self.state, shell);
748 }
749
750 if self.interactive_surface
751 && (cursor_in_tooltip || cursor_in_keep_alive)
752 && !cursor_in_content
753 && matches!(event, Event::Mouse(_))
754 {
755 shell.capture_event();
756 }
757 }
758
759 fn mouse_interaction(
760 &self,
761 layout: Layout<'_>,
762 cursor: mouse::Cursor,
763 renderer: &Renderer,
764 ) -> mouse::Interaction {
765 let tooltip_bounds = layout.bounds();
766
767 if cursor.is_over(tooltip_bounds)
768 && let Some(child_layout) = layout.children().next()
769 {
770 return self.tooltip.as_widget().mouse_interaction(
771 self.tree,
772 child_layout,
773 cursor,
774 &Rectangle::with_size(Size::INFINITE),
775 renderer,
776 );
777 }
778
779 mouse::Interaction::default()
780 }
781
782 fn draw(
783 &self,
784 renderer: &mut Renderer,
785 theme: &Theme,
786 inherited_style: &renderer::Style,
787 layout: Layout<'_>,
788 cursor: mouse::Cursor,
789 ) {
790 let frame = self.state.animation_frame(Instant::now());
791 let style = tooltip_container_style_alpha(
792 iced_container::Catalog::style(theme, self.class),
793 frame.alpha,
794 );
795 let surface_bounds = rich_tooltip_visual_bounds(layout.bounds(), self.clip_padding);
796 let transformation = tooltip_scale_transformation(surface_bounds, frame.scale);
797
798 renderer.with_layer(layout.bounds(), |renderer| {
799 renderer.with_transformation(transformation, |renderer| {
800 iced_container::draw_background(renderer, &style, surface_bounds);
801
802 let defaults = renderer::Style {
803 text_color: style.text_color.unwrap_or(inherited_style.text_color),
804 };
805
806 self.tooltip.as_widget().draw(
807 self.tree,
808 renderer,
809 theme,
810 &defaults,
811 layout.children().next().unwrap(),
812 cursor,
813 &Rectangle::with_size(Size::INFINITE),
814 );
815 });
816 });
817 }
818
819 fn overlay<'c>(
820 &'c mut self,
821 layout: Layout<'c>,
822 renderer: &Renderer,
823 ) -> Option<overlay::Element<'c, Message, Theme, Renderer>> {
824 self.tooltip.as_widget_mut().overlay(
825 self.tree,
826 layout.children().next().unwrap(),
827 renderer,
828 &Rectangle::with_size(Size::INFINITE),
829 Vector::ZERO,
830 )
831 }
832}
833
834fn translated_bounds(bounds: Rectangle, translation: Vector) -> Rectangle {
835 Rectangle {
836 x: bounds.x + translation.x,
837 y: bounds.y + translation.y,
838 ..bounds
839 }
840}
841
842fn rich_tooltip_surface_bounds(
843 content_bounds: Rectangle,
844 tooltip_size: Size,
845 viewport: Rectangle,
846 cursor_position: Point,
847 position: Position,
848 gap: f32,
849 padding: f32,
850 clip_padding: f32,
851 snap_within_viewport: bool,
852) -> Rectangle {
853 let surface_size = Size::new(
854 tooltip_size.width + padding * 2.0,
855 tooltip_size.height + padding * 2.0,
856 );
857 let x_center = content_bounds.x + (content_bounds.width - surface_size.width) / 2.0;
858 let y_center = content_bounds.y + (content_bounds.height - surface_size.height) / 2.0;
859
860 let offset = match position {
861 Position::Top => Vector::new(x_center, content_bounds.y - surface_size.height - gap),
862 Position::Bottom => Vector::new(x_center, content_bounds.y + content_bounds.height + gap),
863 Position::Left => Vector::new(content_bounds.x - surface_size.width - gap, y_center),
864 Position::Right => Vector::new(content_bounds.x + content_bounds.width + gap, y_center),
865 Position::FollowCursor => {
866 Vector::new(cursor_position.x, cursor_position.y - surface_size.height)
867 }
868 };
869
870 let mut tooltip_bounds = Rectangle {
871 x: offset.x - clip_padding,
872 y: offset.y - clip_padding,
873 width: surface_size.width + clip_padding * 2.0,
874 height: surface_size.height + clip_padding * 2.0,
875 };
876
877 if snap_within_viewport {
878 if tooltip_bounds.x < viewport.x {
879 tooltip_bounds.x = viewport.x;
880 } else if viewport.x + viewport.width < tooltip_bounds.x + tooltip_bounds.width {
881 tooltip_bounds.x = viewport.x + viewport.width - tooltip_bounds.width;
882 }
883
884 if tooltip_bounds.y < viewport.y {
885 tooltip_bounds.y = viewport.y;
886 } else if viewport.y + viewport.height < tooltip_bounds.y + tooltip_bounds.height {
887 tooltip_bounds.y = viewport.y + viewport.height - tooltip_bounds.height;
888 }
889 }
890
891 tooltip_bounds
892}
893
894fn rich_tooltip_visual_bounds(tooltip_bounds: Rectangle, clip_padding: f32) -> Rectangle {
895 Rectangle {
896 x: tooltip_bounds.x + clip_padding,
897 y: tooltip_bounds.y + clip_padding,
898 width: (tooltip_bounds.width - clip_padding * 2.0).max(0.0),
899 height: (tooltip_bounds.height - clip_padding * 2.0).max(0.0),
900 }
901}
902
903fn rich_tooltip_shadow_padding() -> f32 {
904 let shadow =
905 tokens::elevation::shadow(tokens::component::tooltip::RICH_CONTAINER_ELEVATION_LEVEL)
906 .ambient;
907
908 (shadow.blur + shadow.y.abs()).ceil()
909}
910
911fn rich_tooltip_keep_alive_contains(
912 content_bounds: Rectangle,
913 tooltip_bounds: Rectangle,
914 position: Position,
915 cursor: mouse::Cursor,
916) -> bool {
917 let Some(cursor) = cursor.position() else {
918 return false;
919 };
920
921 if content_bounds.contains(cursor) || tooltip_bounds.contains(cursor) {
922 return true;
923 }
924
925 rich_tooltip_corridor_bounds(content_bounds, tooltip_bounds, position)
926 .is_some_and(|bounds| bounds.contains(cursor))
927}
928
929fn rich_tooltip_anchor_exit_dismisses(interactive_surface: bool, state: &RichTooltipState) -> bool {
930 !interactive_surface || !state.is_visible()
931}
932
933fn rich_tooltip_corridor_bounds(
934 content_bounds: Rectangle,
935 tooltip_bounds: Rectangle,
936 position: Position,
937) -> Option<Rectangle> {
938 let content_right = content_bounds.x + content_bounds.width;
939 let content_bottom = content_bounds.y + content_bounds.height;
940 let tooltip_right = tooltip_bounds.x + tooltip_bounds.width;
941 let tooltip_bottom = tooltip_bounds.y + tooltip_bounds.height;
942
943 match position {
944 Position::Top if tooltip_bottom <= content_bounds.y => Some(Rectangle {
945 x: content_bounds.x,
946 y: tooltip_bottom,
947 width: content_bounds.width,
948 height: content_bounds.y - tooltip_bottom,
949 }),
950 Position::Bottom if content_bottom <= tooltip_bounds.y => Some(Rectangle {
951 x: content_bounds.x,
952 y: content_bottom,
953 width: content_bounds.width,
954 height: tooltip_bounds.y - content_bottom,
955 }),
956 Position::Left if tooltip_right <= content_bounds.x => Some(Rectangle {
957 x: tooltip_right,
958 y: content_bounds.y,
959 width: content_bounds.x - tooltip_right,
960 height: content_bounds.height,
961 }),
962 Position::Right if content_right <= tooltip_bounds.x => Some(Rectangle {
963 x: content_right,
964 y: content_bounds.y,
965 width: tooltip_bounds.x - content_right,
966 height: content_bounds.height,
967 }),
968 _ => None,
969 }
970}
971
972fn tooltip_container_style_alpha(
973 mut style: iced_container::Style,
974 alpha: f32,
975) -> iced_container::Style {
976 style.background = style
977 .background
978 .map(|background| background.scale_alpha(alpha));
979 style.text_color = style.text_color.map(|color| alpha_color(color, alpha));
980 style.border.color = alpha_color(style.border.color, alpha);
981 style.shadow.color = alpha_color(style.shadow.color, alpha);
982
983 style
984}
985
986fn tooltip_scale_transformation(bounds: Rectangle, scale: f32) -> Transformation {
987 Transformation::translate(bounds.center_x(), bounds.center_y())
988 * Transformation::scale(scale)
989 * Transformation::translate(-bounds.center_x(), -bounds.center_y())
990}
991
992fn tooltip_event_time(event: &Event) -> Instant {
993 match event {
994 Event::Window(window::Event::RedrawRequested(now)) => *now,
995 _ => Instant::now(),
996 }
997}
998
999fn request_tooltip_redraw<Message>(state: &RichTooltipState, shell: &mut Shell<'_, Message>) {
1000 if state.is_animating() {
1001 shell.request_redraw();
1002 }
1003}
1004
1005fn tooltip_scale_easing(progress: f32) -> f32 {
1006 tokens::motion::EASING_LEGACY_DECELERATE.transform(progress)
1007}
1008
1009fn rich_title_text<'a, Renderer>(title: impl text::IntoFragment<'a>) -> Text<'a, Theme, Renderer>
1010where
1011 Renderer: core_text::Renderer,
1012{
1013 let scale = tokens::component::tooltip::RICH_SUBHEAD_TEXT;
1014
1015 text_with_metrics(title, scale.size, scale.line_height)
1016 .wrapping(text::Wrapping::Word)
1017 .style(rich_title_style)
1018}
1019
1020fn rich_supporting_text<'a, Renderer>(
1021 supporting_text: impl text::IntoFragment<'a>,
1022) -> Text<'a, Theme, Renderer>
1023where
1024 Renderer: core_text::Renderer,
1025{
1026 let scale = tokens::component::tooltip::RICH_SUPPORTING_TEXT;
1027
1028 text_with_metrics(supporting_text, scale.size, scale.line_height)
1029 .wrapping(text::Wrapping::Word)
1030 .style(rich_supporting_text_style)
1031}
1032
1033fn rich_title_top_padding() -> f32 {
1034 (tokens::component::tooltip::RICH_HEIGHT_TO_SUBHEAD_FIRST_LINE
1035 - tokens::component::tooltip::RICH_SUBHEAD_TEXT.line_height)
1036 .max(0.0)
1037}
1038
1039fn rich_supporting_text_padding(has_title: bool, has_action: bool) -> Padding {
1040 if !has_title && !has_action {
1041 return Padding {
1042 top: tokens::component::tooltip::RICH_TEXT_VERTICAL_SPACE_WITHOUT_TITLE_OR_ACTION,
1043 right: 0.0,
1044 bottom: tokens::component::tooltip::RICH_TEXT_VERTICAL_SPACE_WITHOUT_TITLE_OR_ACTION,
1045 left: 0.0,
1046 };
1047 }
1048
1049 Padding {
1050 top: (tokens::component::tooltip::RICH_HEIGHT_FROM_SUBHEAD_TO_TEXT_FIRST_LINE
1051 - tokens::component::tooltip::RICH_SUPPORTING_TEXT.line_height)
1052 .max(0.0),
1053 right: 0.0,
1054 bottom: tokens::component::tooltip::RICH_TEXT_BOTTOM_PADDING,
1055 left: 0.0,
1056 }
1057}
1058
1059fn rich_title_style(theme: &Theme) -> iced_widget::text::Style {
1060 iced_widget::text::Style {
1061 color: Some(theme.colors().surface.text_variant),
1062 }
1063}
1064
1065fn rich_supporting_text_style(theme: &Theme) -> iced_widget::text::Style {
1066 iced_widget::text::Style {
1067 color: Some(theme.colors().surface.text_variant),
1068 }
1069}
1070
1071fn plain_tooltip_inner_horizontal_padding() -> f32 {
1072 (tokens::component::tooltip::PLAIN_HORIZONTAL_SPACE
1073 - tokens::component::tooltip::PLAIN_VERTICAL_SPACE)
1074 .max(0.0)
1075}
1076
1077fn plain_tooltip_inner_max_width() -> f32 {
1078 tokens::component::tooltip::PLAIN_MAX_WIDTH
1079 - tokens::component::tooltip::PLAIN_VERTICAL_SPACE * 2.0
1080}
1081
1082#[cfg(test)]
1083#[path = "../../../tests/widget/component/tooltip.rs"]
1084mod tests;