1use std::any::Any;
10use std::sync::{Arc, Mutex};
11use std::time::Duration;
12
13use iced::Task;
14use iced_runtime::core::widget::Id;
15
16use iced_core::event::{self, Event};
17use iced_core::renderer;
18use iced_core::touch;
19use iced_core::widget::Operation;
20use iced_core::widget::tree::{self, Tree};
21use iced_core::{
22 Background, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget,
23};
24use iced_core::{Border, mouse};
25use iced_core::{Shadow, overlay};
26use iced_core::{layout, svg};
27
28pub use super::{Catalog, Style};
29
30enum Variant<Message> {
32 Normal,
33 Image {
34 close_icon: svg::Handle,
35 on_remove: Option<Message>,
36 },
37}
38
39#[allow(missing_debug_implementations)]
41#[must_use]
42pub struct Tooltip<'a, Message, TopLevelMessage> {
43 id: Id,
44 #[cfg(feature = "a11y")]
45 name: Option<std::borrow::Cow<'a, str>>,
46 #[cfg(feature = "a11y")]
47 description: Option<iced_accessibility::Description<'a>>,
48 #[cfg(feature = "a11y")]
49 label: Option<Vec<iced_accessibility::accesskit::NodeId>>,
50 content: crate::Element<'a, Message>,
51 on_leave: Message,
52 on_surface_action: Box<dyn Fn(crate::surface::Action) -> Message>,
53 width: Length,
54 height: Length,
55 padding: Padding,
56 selected: bool,
57 style: crate::theme::Tooltip,
58 delay: Option<Duration>,
59 settings: Option<
60 Arc<
61 dyn Fn(Rectangle) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
62 + Send
63 + Sync
64 + 'static,
65 >,
66 >,
67 view: Arc<
68 dyn Fn() -> crate::Element<'static, crate::Action<TopLevelMessage>> + Send + Sync + 'static,
69 >,
70}
71
72impl<'a, Message, TopLevelMessage> Tooltip<'a, Message, TopLevelMessage> {
73 pub fn new(
75 content: impl Into<crate::Element<'a, Message>>,
76 settings: Option<
77 impl Fn(Rectangle) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
78 + Send
79 + Sync
80 + 'static,
81 >,
82 view: impl Fn() -> crate::Element<'static, crate::Action<TopLevelMessage>>
83 + Send
84 + Sync
85 + 'static,
86 on_leave: Message,
87 on_surface_action: impl Fn(crate::surface::Action) -> Message + 'static,
88 ) -> Self {
89 Self {
90 id: Id::unique(),
91 #[cfg(feature = "a11y")]
92 name: None,
93 #[cfg(feature = "a11y")]
94 description: None,
95 #[cfg(feature = "a11y")]
96 label: None,
97 content: content.into(),
98 width: Length::Shrink,
99 height: Length::Shrink,
100 padding: Padding::new(0.0),
101 selected: false,
102 style: crate::theme::Tooltip::default(),
103 on_leave,
104 on_surface_action: Box::new(on_surface_action),
105 delay: None,
106 settings: if let Some(s) = settings {
107 Some(Arc::new(s))
108 } else {
109 None
110 },
111 view: Arc::new(view),
112 }
113 }
114
115 pub fn delay(mut self, dur: Duration) -> Self {
116 self.delay = Some(dur);
117 self
118 }
119
120 pub fn id(mut self, id: Id) -> Self {
122 self.id = id;
123 self
124 }
125
126 pub fn width(mut self, width: impl Into<Length>) -> Self {
128 self.width = width.into();
129 self
130 }
131
132 pub fn height(mut self, height: impl Into<Length>) -> Self {
134 self.height = height.into();
135 self
136 }
137
138 pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
140 self.padding = padding.into();
141 self
142 }
143
144 pub fn selected(mut self, selected: bool) -> Self {
148 self.selected = selected;
149
150 self
151 }
152
153 pub fn class(mut self, style: crate::theme::Tooltip) -> Self {
155 self.style = style;
156 self
157 }
158
159 #[cfg(feature = "a11y")]
160 pub fn name(mut self, name: impl Into<std::borrow::Cow<'a, str>>) -> Self {
162 self.name = Some(name.into());
163 self
164 }
165
166 #[cfg(feature = "a11y")]
167 pub fn description_widget<T: iced_accessibility::Describes>(mut self, description: &T) -> Self {
169 self.description = Some(iced_accessibility::Description::Id(
170 description.description(),
171 ));
172 self
173 }
174
175 #[cfg(feature = "a11y")]
176 pub fn description(mut self, description: impl Into<std::borrow::Cow<'a, str>>) -> Self {
178 self.description = Some(iced_accessibility::Description::Text(description.into()));
179 self
180 }
181
182 #[cfg(feature = "a11y")]
183 pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self {
185 self.label = Some(label.label().into_iter().map(|l| l.into()).collect());
186 self
187 }
188}
189
190impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone>
191 Widget<Message, crate::Theme, crate::Renderer> for Tooltip<'a, Message, TopLevelMessage>
192{
193 fn tag(&self) -> tree::Tag {
194 tree::Tag::of::<State>()
195 }
196
197 fn state(&self) -> tree::State {
198 tree::State::new(State::default())
199 }
200
201 fn children(&self) -> Vec<Tree> {
202 vec![Tree::new(&self.content)]
203 }
204
205 fn diff(&mut self, tree: &mut Tree) {
206 tree.diff_children(std::slice::from_mut(&mut self.content));
207 }
208
209 fn size(&self) -> iced_core::Size<Length> {
210 iced_core::Size::new(self.width, self.height)
211 }
212
213 fn layout(
214 &mut self,
215 tree: &mut Tree,
216 renderer: &crate::Renderer,
217 limits: &layout::Limits,
218 ) -> layout::Node {
219 layout(
220 renderer,
221 limits,
222 self.width,
223 self.height,
224 self.padding,
225 |renderer, limits| {
226 self.content
227 .as_widget_mut()
228 .layout(&mut tree.children[0], renderer, limits)
229 },
230 )
231 }
232
233 fn operate(
234 &mut self,
235 tree: &mut Tree,
236 layout: Layout<'_>,
237 renderer: &crate::Renderer,
238 operation: &mut dyn Operation<()>,
239 ) {
240 operation.container(Some(&self.id), layout.bounds());
241 operation.traverse(&mut |operation| {
242 self.content.as_widget_mut().operate(
243 &mut tree.children[0],
244 layout
245 .children()
246 .next()
247 .unwrap()
248 .with_virtual_offset(layout.virtual_offset()),
249 renderer,
250 operation,
251 );
252 });
253 }
254
255 fn update(
256 &mut self,
257 tree: &mut Tree,
258 event: &Event,
259 layout: Layout<'_>,
260 cursor: mouse::Cursor,
261 renderer: &crate::Renderer,
262 clipboard: &mut dyn Clipboard,
263 shell: &mut Shell<'_, Message>,
264 viewport: &Rectangle,
265 ) {
266 update(
267 self.id.clone(),
268 event.clone(),
269 layout,
270 cursor,
271 shell,
272 self.settings.as_ref(),
273 &self.view,
274 self.delay,
275 &self.on_leave,
276 &self.on_surface_action,
277 || tree.state.downcast_mut::<State>(),
278 );
279
280 self.content.as_widget_mut().update(
281 &mut tree.children[0],
282 event,
283 layout
284 .children()
285 .next()
286 .unwrap()
287 .with_virtual_offset(layout.virtual_offset()),
288 cursor,
289 renderer,
290 clipboard,
291 shell,
292 viewport,
293 );
294 }
295
296 #[allow(clippy::too_many_lines)]
297 fn draw(
298 &self,
299 tree: &Tree,
300 renderer: &mut crate::Renderer,
301 theme: &crate::Theme,
302 renderer_style: &renderer::Style,
303 layout: Layout<'_>,
304 cursor: mouse::Cursor,
305 viewport: &Rectangle,
306 ) {
307 let bounds = layout.bounds();
308 if !viewport.intersects(&bounds) {
309 return;
310 }
311 let content_layout = layout.children().next().unwrap();
312
313 let state = tree.state.downcast_ref::<State>();
314
315 let styling = theme.style(&self.style);
316
317 let icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color);
318
319 draw::<_, crate::Theme>(
320 renderer,
321 bounds,
322 *viewport,
323 &styling,
324 |renderer, _styling| {
325 self.content.as_widget().draw(
326 &tree.children[0],
327 renderer,
328 theme,
329 &renderer::Style {
330 icon_color,
331 text_color: styling.text_color,
332 scale_factor: renderer_style.scale_factor,
333 },
334 content_layout.with_virtual_offset(layout.virtual_offset()),
335 cursor,
336 &viewport.intersection(&bounds).unwrap_or_default(),
337 );
338 },
339 );
340 }
341
342 fn mouse_interaction(
343 &self,
344 tree: &Tree,
345 layout: Layout<'_>,
346 cursor: mouse::Cursor,
347 viewport: &Rectangle,
348 renderer: &crate::Renderer,
349 ) -> mouse::Interaction {
350 self.content.as_widget().mouse_interaction(
351 &tree.children[0],
352 layout.children().next().unwrap(),
353 cursor,
354 viewport,
355 renderer,
356 )
357 }
358
359 fn overlay<'b>(
360 &'b mut self,
361 tree: &'b mut Tree,
362 layout: Layout<'b>,
363 renderer: &crate::Renderer,
364 viewport: &Rectangle,
365 mut translation: Vector,
366 ) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
367 let position = layout.bounds().position();
368 translation.x += position.x;
369 translation.y += position.y;
370 self.content.as_widget_mut().overlay(
371 &mut tree.children[0],
372 layout
373 .children()
374 .next()
375 .unwrap()
376 .with_virtual_offset(layout.virtual_offset()),
377 renderer,
378 viewport,
379 translation,
380 )
381 }
382
383 #[cfg(feature = "a11y")]
384 fn a11y_nodes(
386 &self,
387 layout: Layout<'_>,
388 state: &Tree,
389 p: mouse::Cursor,
390 ) -> iced_accessibility::A11yTree {
391 let c_layout = layout.children().next().unwrap();
392
393 self.content.as_widget().a11y_nodes(
394 c_layout.with_virtual_offset(layout.virtual_offset()),
395 state,
396 p,
397 )
398 }
399
400 fn id(&self) -> Option<Id> {
401 Some(self.id.clone())
402 }
403
404 fn set_id(&mut self, id: Id) {
405 self.id = id;
406 }
407}
408
409impl<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>
410 From<Tooltip<'a, Message, TopLevelMessage>> for crate::Element<'a, Message>
411{
412 fn from(button: Tooltip<'a, Message, TopLevelMessage>) -> Self {
413 Self::new(button)
414 }
415}
416
417#[derive(Debug, Clone, Default)]
419#[allow(clippy::struct_field_names)]
420pub struct State {
421 is_hovered: Arc<Mutex<bool>>,
422}
423
424impl State {
425 pub fn is_hovered(self) -> bool {
427 let guard = self.is_hovered.lock().unwrap();
428 *guard
429 }
430}
431
432#[allow(clippy::needless_pass_by_value)]
435pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>(
436 _id: Id,
437 event: Event,
438 layout: Layout<'_>,
439 cursor: mouse::Cursor,
440 shell: &mut Shell<'_, Message>,
441 settings: Option<
442 &Arc<
443 dyn Fn(Rectangle) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
444 + Send
445 + Sync
446 + 'static,
447 >,
448 >,
449 view: &Arc<
450 dyn Fn() -> crate::Element<'static, crate::Action<TopLevelMessage>> + Send + Sync + 'static,
451 >,
452 delay: Option<Duration>,
453 on_leave: &Message,
454 on_surface_action: &dyn Fn(crate::surface::Action) -> Message,
455 state: impl FnOnce() -> &'a mut State,
456) {
457 match event {
458 Event::Touch(touch::Event::FingerLifted { .. }) => {
459 let state = state();
460 let mut guard = state.is_hovered.lock().unwrap();
461 if *guard {
462 *guard = false;
463
464 shell.publish(on_leave.clone());
465
466 shell.capture_event();
467 return;
468 }
469 }
470
471 Event::Touch(touch::Event::FingerLost { .. }) | Event::Mouse(mouse::Event::CursorLeft) => {
472 let state = state();
473 let mut guard = state.is_hovered.lock().unwrap();
474
475 if *guard {
476 *guard = false;
477
478 shell.publish(on_leave.clone());
479 }
480 }
481
482 Event::Mouse(mouse::Event::CursorMoved { .. }) => {
483 let state = state();
484 let bounds = layout.bounds();
485 let is_hovered = state.is_hovered.clone();
486 let mut guard = state.is_hovered.lock().unwrap();
487
488 if *guard {
489 *guard = cursor.is_over(bounds);
490 if !*guard {
491 shell.publish(on_leave.clone());
492 }
493 } else {
494 *guard = cursor.is_over(bounds);
495 if *guard {
496 if let Some(settings) = settings {
497 if let Some(delay) = delay {
498 let s = settings.clone();
499 let view = view.clone();
500 let bounds = layout.bounds();
501
502 let sm = crate::surface::Action::Task(Arc::new(move || {
503 let s = s.clone();
504 let view = view.clone();
505 let is_hovered = is_hovered.clone();
506 Task::future(async move {
507 #[cfg(feature = "tokio")]
508 {
509 _ = tokio::time::sleep(delay).await;
510 }
511 #[cfg(feature = "async-std")]
512 {
513 _ = async_std::task::sleep(delay).await;
514 }
515 let is_hovered = is_hovered.clone();
516 let g = is_hovered.lock().unwrap();
517 if !*g {
518 return crate::surface::Action::Ignore;
519 }
520 let boxed: Box<
521 dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
522 + Send
523 + Sync
524 + 'static,
525 > = Box::new(move || s(bounds));
526 let boxed: Box<dyn Any + Send + Sync + 'static> =
527 Box::new(boxed);
528 crate::surface::Action::Popup(
529 Arc::new(boxed),
530 Some({
531 let boxed: Box<
532 dyn Fn() -> crate::Element<
533 'static,
534 crate::Action<TopLevelMessage>,
535 > + Send
536 + Sync
537 + 'static,
538 > = Box::new(move || view());
539 let boxed: Box<dyn Any + Send + Sync + 'static> =
540 Box::new(boxed);
541 Arc::new(boxed)
542 }),
543 )
544 })
545 }));
546
547 shell.publish((on_surface_action)(sm));
548 } else {
549 let s = settings.clone();
550 let view = view.clone();
551 let bounds = layout.bounds();
552
553 let boxed: Box<
554 dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
555 + Send
556 + Sync
557 + 'static,
558 > = Box::new(move || s(bounds));
559 let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
560
561 let sm = crate::surface::Action::Popup(
562 Arc::new(boxed),
563 Some({
564 let boxed: Box<
565 dyn Fn() -> crate::Element<
566 'static,
567 crate::Action<TopLevelMessage>,
568 > + Send
569 + Sync
570 + 'static,
571 > = Box::new(move || view());
572 let boxed: Box<dyn Any + Send + Sync + 'static> =
573 Box::new(boxed);
574 Arc::new(boxed)
575 }),
576 );
577 shell.publish((on_surface_action)(sm));
578 }
579 }
580 }
581 }
582 }
583 _ => {}
584 }
585}
586
587#[allow(clippy::too_many_arguments)]
588pub fn draw<Renderer: iced_core::Renderer, Theme>(
589 renderer: &mut Renderer,
590 bounds: Rectangle,
591 viewport_bounds: Rectangle,
592 styling: &super::Style,
593 draw_contents: impl FnOnce(&mut Renderer, &Style),
594) where
595 Theme: super::Catalog,
596{
597 let doubled_border_width = styling.border_width * 2.0;
598 let doubled_outline_width = styling.outline_width * 2.0;
599
600 if styling.outline_width > 0.0 {
601 renderer.fill_quad(
602 renderer::Quad {
603 bounds: Rectangle {
604 x: bounds.x - styling.border_width - styling.outline_width,
605 y: bounds.y - styling.border_width - styling.outline_width,
606 width: bounds.width + doubled_border_width + doubled_outline_width,
607 height: bounds.height + doubled_border_width + doubled_outline_width,
608 },
609 border: Border {
610 width: styling.outline_width,
611 color: styling.outline_color,
612 radius: styling.border_radius,
613 },
614 shadow: Shadow::default(),
615 snap: true,
616 },
617 Color::TRANSPARENT,
618 );
619 }
620
621 if styling.background.is_some() || styling.border_width > 0.0 {
622 if styling.shadow_offset != Vector::default() {
623 renderer.fill_quad(
625 renderer::Quad {
626 bounds: Rectangle {
627 x: bounds.x + styling.shadow_offset.x,
628 y: bounds.y + styling.shadow_offset.y,
629 width: bounds.width,
630 height: bounds.height,
631 },
632 border: Border {
633 radius: styling.border_radius,
634 ..Default::default()
635 },
636 shadow: Shadow::default(),
637 snap: true,
638 },
639 Background::Color([0.0, 0.0, 0.0, 0.5].into()),
640 );
641 }
642
643 if let Some(background) = styling.background {
645 renderer.fill_quad(
646 renderer::Quad {
647 bounds,
648 border: Border {
649 radius: styling.border_radius,
650 ..Default::default()
651 },
652 shadow: Shadow::default(),
653 snap: true,
654 },
655 background,
656 );
657 }
658
659 draw_contents(renderer, styling);
661
662 let mut clipped_bounds = viewport_bounds.intersection(&bounds).unwrap_or_default();
663 clipped_bounds.height += styling.border_width;
664
665 renderer.with_layer(clipped_bounds, |renderer| {
666 renderer.fill_quad(
668 renderer::Quad {
669 bounds,
670 border: Border {
671 width: styling.border_width,
672 color: styling.border_color,
673 radius: styling.border_radius,
674 },
675 shadow: Shadow::default(),
676 snap: true,
677 },
678 Color::TRANSPARENT,
679 );
680 });
681 } else {
682 draw_contents(renderer, styling);
683 }
684}
685
686pub fn layout<Renderer>(
688 renderer: &Renderer,
689 limits: &layout::Limits,
690 width: Length,
691 height: Length,
692 padding: Padding,
693 layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
694) -> layout::Node {
695 let limits = limits.width(width).height(height);
696
697 let mut content = layout_content(renderer, &limits.shrink(padding));
698 let padding = padding.fit(content.size(), limits.max());
699 let size = limits
700 .shrink(padding)
701 .resolve(width, height, content.size())
702 .expand(padding);
703
704 content = content.move_to(Point::new(padding.left, padding.top));
705
706 layout::Node::with_children(size, vec![content])
707}