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