Skip to main content

material_ui_rs/widget/component/
tooltip.rs

1//! Material 3 tooltip constructors with token-backed layout defaults.
2
3use 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
191/// A Material plain tooltip with Android platform tooltip show/hide animation.
192pub type PlainTooltip<'a, Message, Renderer = iced_widget::Renderer> =
193    RichTooltip<'a, Message, Renderer>;
194
195/// A Material rich tooltip that remains interactive while the pointer moves
196/// from its anchor to the tooltip surface.
197pub 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;