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