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: PlainTooltipMetrics::horizontal_padding(),
28 bottom: 0.0,
29 left: PlainTooltipMetrics::horizontal_padding(),
30 })
31 .max_width(PlainTooltipMetrics::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::button(label, button::ButtonVariant::Text)
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(RichTooltipSurface::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>;
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 RichTooltipHitArea::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 position_rich_tooltip_layout(
676 tooltip_layout,
677 RichTooltipPlacement {
678 content_bounds: self.content_bounds,
679 viewport,
680 cursor_position: self.state.cursor_position,
681 position: self.position,
682 gap: self.gap,
683 padding: self.padding,
684 clip_padding: self.clip_padding,
685 snap_within_viewport: self.snap_within_viewport,
686 },
687 )
688 }
689
690 fn update(
691 &mut self,
692 event: &Event,
693 layout: Layout<'_>,
694 cursor: mouse::Cursor,
695 renderer: &Renderer,
696 clipboard: &mut dyn Clipboard,
697 shell: &mut Shell<'_, Message>,
698 ) {
699 let tooltip_bounds = layout.bounds();
700 let now = tooltip_event_time(event);
701 let cursor_in_content = cursor.is_over(self.content_bounds);
702 let cursor_in_tooltip = cursor.is_over(tooltip_bounds);
703 let cursor_in_keep_alive = self.interactive_surface
704 && RichTooltipHitArea {
705 content: self.content_bounds,
706 tooltip: tooltip_bounds,
707 position: self.position,
708 }
709 .contains(cursor);
710
711 if cursor_in_tooltip && let Some(child_layout) = layout.children().next() {
712 self.tooltip.as_widget_mut().update(
713 self.tree,
714 event,
715 child_layout,
716 cursor,
717 renderer,
718 clipboard,
719 shell,
720 &Rectangle::with_size(Size::INFINITE),
721 );
722 }
723
724 if let Event::Mouse(_) | Event::Window(window::Event::RedrawRequested(_)) = event {
725 let was_visible = self.state.is_visible();
726
727 if cursor_in_content || cursor_in_keep_alive {
728 if let Some(cursor_position) = cursor.position_over(self.content_bounds) {
729 self.state.show(now, cursor_position);
730 } else if self.state.phase == TooltipPhase::Dismissing {
731 self.state.show(now, self.state.cursor_position);
732 }
733 } else {
734 self.state.dismiss(now);
735 }
736
737 self.state.advance(now);
738
739 if was_visible != self.state.is_visible() {
740 shell.invalidate_layout();
741 }
742
743 request_tooltip_redraw(self.state, shell);
744 }
745
746 if self.interactive_surface
747 && (cursor_in_tooltip || cursor_in_keep_alive)
748 && !cursor_in_content
749 && matches!(event, Event::Mouse(_))
750 {
751 shell.capture_event();
752 }
753 }
754
755 fn mouse_interaction(
756 &self,
757 layout: Layout<'_>,
758 cursor: mouse::Cursor,
759 renderer: &Renderer,
760 ) -> mouse::Interaction {
761 let tooltip_bounds = layout.bounds();
762
763 if cursor.is_over(tooltip_bounds)
764 && let Some(child_layout) = layout.children().next()
765 {
766 return self.tooltip.as_widget().mouse_interaction(
767 self.tree,
768 child_layout,
769 cursor,
770 &Rectangle::with_size(Size::INFINITE),
771 renderer,
772 );
773 }
774
775 mouse::Interaction::default()
776 }
777
778 fn draw(
779 &self,
780 renderer: &mut Renderer,
781 theme: &Theme,
782 inherited_style: &renderer::Style,
783 layout: Layout<'_>,
784 cursor: mouse::Cursor,
785 ) {
786 let frame = self.state.animation_frame(Instant::now());
787 let style = tooltip_container_style_alpha(
788 iced_container::Catalog::style(theme, self.class),
789 frame.alpha,
790 );
791 let surface_bounds = RichTooltipSurface {
792 bounds: layout.bounds(),
793 clip_padding: self.clip_padding,
794 }
795 .visual_bounds();
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
842#[derive(Debug, Clone, Copy)]
843struct RichTooltipPlacement {
844 content_bounds: Rectangle,
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}
853
854fn position_rich_tooltip_layout(
855 tooltip_layout: layout::Node,
856 placement: RichTooltipPlacement,
857) -> layout::Node {
858 let total_padding = placement.padding + placement.clip_padding;
859 let tooltip_bounds = rich_tooltip_surface_bounds(&tooltip_layout, placement);
860
861 layout::Node::with_children(
862 tooltip_bounds.size(),
863 vec![tooltip_layout.translate(Vector::new(total_padding, total_padding))],
864 )
865 .translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y))
866}
867
868fn rich_tooltip_surface_bounds(
869 tooltip_layout: &layout::Node,
870 placement: RichTooltipPlacement,
871) -> Rectangle {
872 let measured = tooltip_layout.bounds().size();
873 let surface_size = Size::new(
874 measured.width + placement.padding * 2.0,
875 measured.height + placement.padding * 2.0,
876 );
877 let x_center =
878 placement.content_bounds.x + (placement.content_bounds.width - surface_size.width) / 2.0;
879 let y_center =
880 placement.content_bounds.y + (placement.content_bounds.height - surface_size.height) / 2.0;
881
882 let offset = match placement.position {
883 Position::Top => Vector::new(
884 x_center,
885 placement.content_bounds.y - surface_size.height - placement.gap,
886 ),
887 Position::Bottom => Vector::new(
888 x_center,
889 placement.content_bounds.y + placement.content_bounds.height + placement.gap,
890 ),
891 Position::Left => Vector::new(
892 placement.content_bounds.x - surface_size.width - placement.gap,
893 y_center,
894 ),
895 Position::Right => Vector::new(
896 placement.content_bounds.x + placement.content_bounds.width + placement.gap,
897 y_center,
898 ),
899 Position::FollowCursor => Vector::new(
900 placement.cursor_position.x,
901 placement.cursor_position.y - surface_size.height,
902 ),
903 };
904
905 let mut tooltip_bounds = Rectangle {
906 x: offset.x - placement.clip_padding,
907 y: offset.y - placement.clip_padding,
908 width: surface_size.width + placement.clip_padding * 2.0,
909 height: surface_size.height + placement.clip_padding * 2.0,
910 };
911
912 if placement.snap_within_viewport {
913 if tooltip_bounds.x < placement.viewport.x {
914 tooltip_bounds.x = placement.viewport.x;
915 } else if placement.viewport.x + placement.viewport.width
916 < tooltip_bounds.x + tooltip_bounds.width
917 {
918 tooltip_bounds.x =
919 placement.viewport.x + placement.viewport.width - tooltip_bounds.width;
920 }
921
922 if tooltip_bounds.y < placement.viewport.y {
923 tooltip_bounds.y = placement.viewport.y;
924 } else if placement.viewport.y + placement.viewport.height
925 < tooltip_bounds.y + tooltip_bounds.height
926 {
927 tooltip_bounds.y =
928 placement.viewport.y + placement.viewport.height - tooltip_bounds.height;
929 }
930 }
931
932 tooltip_bounds
933}
934
935#[derive(Debug, Clone, Copy)]
936struct RichTooltipSurface {
937 bounds: Rectangle,
938 clip_padding: f32,
939}
940
941impl RichTooltipSurface {
942 fn visual_bounds(self) -> Rectangle {
943 Rectangle {
944 x: self.bounds.x + self.clip_padding,
945 y: self.bounds.y + self.clip_padding,
946 width: (self.bounds.width - self.clip_padding * 2.0).max(0.0),
947 height: (self.bounds.height - self.clip_padding * 2.0).max(0.0),
948 }
949 }
950
951 fn shadow_padding() -> f32 {
952 let shadow =
953 tokens::elevation::shadow(tokens::component::tooltip::RICH_CONTAINER_ELEVATION_LEVEL)
954 .ambient;
955
956 (shadow.blur + shadow.y.abs()).ceil()
957 }
958}
959
960#[derive(Debug, Clone, Copy)]
961struct RichTooltipHitArea {
962 content: Rectangle,
963 tooltip: Rectangle,
964 position: Position,
965}
966
967impl RichTooltipHitArea {
968 fn contains(self, cursor: mouse::Cursor) -> bool {
969 let Some(cursor) = cursor.position() else {
970 return false;
971 };
972
973 if self.content.contains(cursor) || self.tooltip.contains(cursor) {
974 return true;
975 }
976
977 self.corridor()
978 .is_some_and(|bounds| bounds.contains(cursor))
979 }
980
981 fn anchor_exit_dismisses(interactive_surface: bool, state: &RichTooltipState) -> bool {
982 !interactive_surface || !state.is_visible()
983 }
984
985 fn corridor(self) -> Option<Rectangle> {
986 let content_right = self.content.x + self.content.width;
987 let content_bottom = self.content.y + self.content.height;
988 let tooltip_right = self.tooltip.x + self.tooltip.width;
989 let tooltip_bottom = self.tooltip.y + self.tooltip.height;
990
991 match self.position {
992 Position::Top if tooltip_bottom <= self.content.y => Some(Rectangle {
993 x: self.content.x,
994 y: tooltip_bottom,
995 width: self.content.width,
996 height: self.content.y - tooltip_bottom,
997 }),
998 Position::Bottom if content_bottom <= self.tooltip.y => Some(Rectangle {
999 x: self.content.x,
1000 y: content_bottom,
1001 width: self.content.width,
1002 height: self.tooltip.y - content_bottom,
1003 }),
1004 Position::Left if tooltip_right <= self.content.x => Some(Rectangle {
1005 x: tooltip_right,
1006 y: self.content.y,
1007 width: self.content.x - tooltip_right,
1008 height: self.content.height,
1009 }),
1010 Position::Right if content_right <= self.tooltip.x => Some(Rectangle {
1011 x: content_right,
1012 y: self.content.y,
1013 width: self.tooltip.x - content_right,
1014 height: self.content.height,
1015 }),
1016 _ => None,
1017 }
1018 }
1019}
1020
1021fn tooltip_container_style_alpha(
1022 mut style: iced_container::Style,
1023 alpha: f32,
1024) -> iced_container::Style {
1025 style.background = style
1026 .background
1027 .map(|background| background.scale_alpha(alpha));
1028 style.text_color = style.text_color.map(|color| alpha_color(color, alpha));
1029 style.border.color = alpha_color(style.border.color, alpha);
1030 style.shadow.color = alpha_color(style.shadow.color, alpha);
1031
1032 style
1033}
1034
1035fn tooltip_scale_transformation(bounds: Rectangle, scale: f32) -> Transformation {
1036 Transformation::translate(bounds.center_x(), bounds.center_y())
1037 * Transformation::scale(scale)
1038 * Transformation::translate(-bounds.center_x(), -bounds.center_y())
1039}
1040
1041fn tooltip_event_time(event: &Event) -> Instant {
1042 match event {
1043 Event::Window(window::Event::RedrawRequested(now)) => *now,
1044 _ => Instant::now(),
1045 }
1046}
1047
1048fn request_tooltip_redraw<Message>(state: &RichTooltipState, shell: &mut Shell<'_, Message>) {
1049 if state.is_animating() {
1050 shell.request_redraw();
1051 }
1052}
1053
1054fn tooltip_scale_easing(progress: f32) -> f32 {
1055 tokens::motion::EASING_LEGACY_DECELERATE.transform(progress)
1056}
1057
1058fn rich_title_text<'a, Renderer>(title: impl text::IntoFragment<'a>) -> Text<'a, Theme, Renderer>
1059where
1060 Renderer: core_text::Renderer,
1061{
1062 let scale = tokens::component::tooltip::RICH_SUBHEAD_TEXT;
1063
1064 text_with_metrics(title, scale.size, scale.line_height)
1065 .wrapping(text::Wrapping::Word)
1066 .style(rich_title_style)
1067}
1068
1069fn rich_supporting_text<'a, Renderer>(
1070 supporting_text: impl text::IntoFragment<'a>,
1071) -> Text<'a, Theme, Renderer>
1072where
1073 Renderer: core_text::Renderer,
1074{
1075 let scale = tokens::component::tooltip::RICH_SUPPORTING_TEXT;
1076
1077 text_with_metrics(supporting_text, scale.size, scale.line_height)
1078 .wrapping(text::Wrapping::Word)
1079 .style(rich_supporting_text_style)
1080}
1081
1082fn rich_title_top_padding() -> f32 {
1083 (tokens::component::tooltip::RICH_HEIGHT_TO_SUBHEAD_FIRST_LINE
1084 - tokens::component::tooltip::RICH_SUBHEAD_TEXT.line_height)
1085 .max(0.0)
1086}
1087
1088fn rich_supporting_text_padding(has_title: bool, has_action: bool) -> Padding {
1089 if !has_title && !has_action {
1090 return Padding {
1091 top: tokens::component::tooltip::RICH_TEXT_VERTICAL_SPACE_WITHOUT_TITLE_OR_ACTION,
1092 right: 0.0,
1093 bottom: tokens::component::tooltip::RICH_TEXT_VERTICAL_SPACE_WITHOUT_TITLE_OR_ACTION,
1094 left: 0.0,
1095 };
1096 }
1097
1098 Padding {
1099 top: (tokens::component::tooltip::RICH_HEIGHT_FROM_SUBHEAD_TO_TEXT_FIRST_LINE
1100 - tokens::component::tooltip::RICH_SUPPORTING_TEXT.line_height)
1101 .max(0.0),
1102 right: 0.0,
1103 bottom: tokens::component::tooltip::RICH_TEXT_BOTTOM_PADDING,
1104 left: 0.0,
1105 }
1106}
1107
1108fn rich_title_style(theme: &Theme) -> iced_widget::text::Style {
1109 iced_widget::text::Style {
1110 color: Some(theme.colors().surface.text_variant),
1111 }
1112}
1113
1114fn rich_supporting_text_style(theme: &Theme) -> iced_widget::text::Style {
1115 iced_widget::text::Style {
1116 color: Some(theme.colors().surface.text_variant),
1117 }
1118}
1119
1120struct PlainTooltipMetrics;
1121
1122impl PlainTooltipMetrics {
1123 fn horizontal_padding() -> f32 {
1124 (tokens::component::tooltip::PLAIN_HORIZONTAL_SPACE
1125 - tokens::component::tooltip::PLAIN_VERTICAL_SPACE)
1126 .max(0.0)
1127 }
1128
1129 fn max_width() -> f32 {
1130 tokens::component::tooltip::PLAIN_MAX_WIDTH
1131 - tokens::component::tooltip::PLAIN_VERTICAL_SPACE * 2.0
1132 }
1133}
1134
1135#[cfg(test)]
1136#[path = "../../../tests/widget/component/tooltip.rs"]
1137mod tests;