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 styling = theme.style(&self.style);
305
306 let icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color);
307
308 draw::<_, crate::Theme>(
309 renderer,
310 bounds,
311 *viewport,
312 &styling,
313 |renderer, _styling| {
314 self.content.as_widget().draw(
315 &tree.children[0],
316 renderer,
317 theme,
318 &renderer::Style {
319 icon_color,
320 text_color: styling.text_color,
321 scale_factor: renderer_style.scale_factor,
322 },
323 content_layout.with_virtual_offset(layout.virtual_offset()),
324 cursor,
325 &viewport.intersection(&bounds).unwrap_or_default(),
326 );
327 },
328 );
329 }
330
331 fn mouse_interaction(
332 &self,
333 tree: &Tree,
334 layout: Layout<'_>,
335 cursor: mouse::Cursor,
336 viewport: &Rectangle,
337 renderer: &crate::Renderer,
338 ) -> mouse::Interaction {
339 self.content.as_widget().mouse_interaction(
340 &tree.children[0],
341 layout.children().next().unwrap(),
342 cursor,
343 viewport,
344 renderer,
345 )
346 }
347
348 fn overlay<'b>(
349 &'b mut self,
350 tree: &'b mut Tree,
351 layout: Layout<'b>,
352 renderer: &crate::Renderer,
353 viewport: &Rectangle,
354 mut translation: Vector,
355 ) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
356 let position = layout.bounds().position();
357 translation.x += position.x;
358 translation.y += position.y;
359 self.content.as_widget_mut().overlay(
360 &mut tree.children[0],
361 layout
362 .children()
363 .next()
364 .unwrap()
365 .with_virtual_offset(layout.virtual_offset()),
366 renderer,
367 viewport,
368 translation,
369 )
370 }
371
372 #[cfg(feature = "a11y")]
373 fn a11y_nodes(
375 &self,
376 layout: Layout<'_>,
377 state: &Tree,
378 p: mouse::Cursor,
379 ) -> iced_accessibility::A11yTree {
380 let c_layout = layout.children().next().unwrap();
381
382 self.content.as_widget().a11y_nodes(
383 c_layout.with_virtual_offset(layout.virtual_offset()),
384 state,
385 p,
386 )
387 }
388
389 fn id(&self) -> Option<Id> {
390 Some(self.id.clone())
391 }
392
393 fn set_id(&mut self, id: Id) {
394 self.id = id;
395 }
396}
397
398impl<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>
399 From<Tooltip<'a, Message, TopLevelMessage>> for crate::Element<'a, Message>
400{
401 fn from(button: Tooltip<'a, Message, TopLevelMessage>) -> Self {
402 Self::new(button)
403 }
404}
405
406#[derive(Debug, Clone, Default)]
408#[allow(clippy::struct_field_names)]
409pub struct State {
410 is_hovered: Arc<Mutex<bool>>,
411}
412
413impl State {
414 pub fn is_hovered(self) -> bool {
416 let guard = self.is_hovered.lock().unwrap();
417 *guard
418 }
419}
420
421#[allow(clippy::needless_pass_by_value)]
424pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>(
425 _id: Id,
426 event: Event,
427 layout: Layout<'_>,
428 cursor: mouse::Cursor,
429 shell: &mut Shell<'_, Message>,
430 settings: Option<
431 &Arc<
432 dyn Fn(Rectangle) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
433 + Send
434 + Sync
435 + 'static,
436 >,
437 >,
438 view: &Arc<
439 dyn Fn() -> crate::Element<'static, crate::Action<TopLevelMessage>> + Send + Sync + 'static,
440 >,
441 delay: Option<Duration>,
442 on_leave: &Message,
443 on_surface_action: &dyn Fn(crate::surface::Action) -> Message,
444 state: impl FnOnce() -> &'a mut State,
445) {
446 match event {
447 Event::Touch(touch::Event::FingerLifted { .. }) => {
448 let state = state();
449 let mut guard = state.is_hovered.lock().unwrap();
450 if *guard {
451 *guard = false;
452
453 shell.publish(on_leave.clone());
454
455 shell.capture_event();
456 return;
457 }
458 }
459
460 Event::Touch(touch::Event::FingerLost { .. }) | Event::Mouse(mouse::Event::CursorLeft) => {
461 let state = state();
462 let mut guard = state.is_hovered.lock().unwrap();
463
464 if *guard {
465 *guard = false;
466
467 shell.publish(on_leave.clone());
468 }
469 }
470
471 Event::Mouse(mouse::Event::CursorMoved { .. }) => {
472 let state = state();
473 let bounds = layout.bounds();
474 let is_hovered = state.is_hovered.clone();
475 let mut guard = state.is_hovered.lock().unwrap();
476
477 if *guard {
478 *guard = cursor.is_over(bounds);
479 if !*guard {
480 shell.publish(on_leave.clone());
481 }
482 } else {
483 *guard = cursor.is_over(bounds);
484 if *guard {
485 if let Some(settings) = settings {
486 if let Some(delay) = delay {
487 let s = settings.clone();
488 let view = view.clone();
489 let bounds = layout.bounds();
490
491 let sm = crate::surface::Action::Task(Arc::new(move || {
492 let s = s.clone();
493 let view = view.clone();
494 let is_hovered = is_hovered.clone();
495 Task::future(async move {
496 #[cfg(feature = "tokio")]
497 {
498 _ = tokio::time::sleep(delay).await;
499 }
500 #[cfg(feature = "async-std")]
501 {
502 _ = async_std::task::sleep(delay).await;
503 }
504 let is_hovered = is_hovered.clone();
505 let g = is_hovered.lock().unwrap();
506 if !*g {
507 return crate::surface::Action::Ignore;
508 }
509 let boxed: Box<
510 dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
511 + Send
512 + Sync
513 + 'static,
514 > = Box::new(move || s(bounds));
515 let boxed: Box<dyn Any + Send + Sync + 'static> =
516 Box::new(boxed);
517
518 let theme = THEME.lock().unwrap();
519
520 let corners = theme.cosmic().corner_radii.radius_s;
521 let boxed_live: Box<
522 dyn Fn() -> LiveSettings + Send + Sync + 'static,
523 > = Box::new(move || LiveSettings {
524 corners: Some(CornerRadius {
525 top_left: corners[0] as u32,
526 top_right: corners[1] as u32,
527 bottom_left: corners[3] as u32,
528 bottom_right: corners[2] as u32,
529 }),
530 ..Default::default()
531 });
532 let boxed_live: Box<dyn Any + Send + Sync + 'static> =
533 Box::new(boxed_live);
534 crate::surface::Action::Popup(
535 Arc::new(boxed),
536 Arc::new(boxed_live),
537 Some({
538 let boxed: Box<
539 dyn Fn() -> crate::Element<
540 'static,
541 crate::Action<TopLevelMessage>,
542 > + Send
543 + Sync
544 + 'static,
545 > = Box::new(move || view());
546 let boxed: Box<dyn Any + Send + Sync + 'static> =
547 Box::new(boxed);
548 Arc::new(boxed)
549 }),
550 )
551 })
552 }));
553
554 shell.publish((on_surface_action)(sm));
555 } else {
556 let s = settings.clone();
557 let view = view.clone();
558 let bounds = layout.bounds();
559
560 let boxed: Box<
561 dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
562 + Send
563 + Sync
564 + 'static,
565 > = Box::new(move || s(bounds));
566 let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
567 let theme = THEME.lock().unwrap();
568
569 let corners = theme.cosmic().corner_radii.radius_s;
570 let boxed_live: Box<dyn Fn() -> LiveSettings + Send + Sync + 'static> =
571 Box::new(move || LiveSettings {
572 corners: Some(CornerRadius {
573 top_left: corners[0] as u32,
574 top_right: corners[1] as u32,
575 bottom_left: corners[3] as u32,
576 bottom_right: corners[2] as u32,
577 }),
578 ..Default::default()
579 });
580 let boxed_live: Box<dyn Any + Send + Sync + 'static> =
581 Box::new(boxed_live);
582
583 let sm = crate::surface::Action::Popup(
584 Arc::new(boxed),
585 Arc::new(boxed_live),
586 Some({
587 let boxed: Box<
588 dyn Fn() -> crate::Element<
589 'static,
590 crate::Action<TopLevelMessage>,
591 > + Send
592 + Sync
593 + 'static,
594 > = Box::new(move || view());
595 let boxed: Box<dyn Any + Send + Sync + 'static> =
596 Box::new(boxed);
597 Arc::new(boxed)
598 }),
599 );
600 shell.publish((on_surface_action)(sm));
601 }
602 }
603 }
604 }
605 }
606 _ => {}
607 }
608}
609
610#[allow(clippy::too_many_arguments)]
611pub fn draw<Renderer: iced_core::Renderer, Theme>(
612 renderer: &mut Renderer,
613 bounds: Rectangle,
614 viewport_bounds: Rectangle,
615 styling: &super::Style,
616 draw_contents: impl FnOnce(&mut Renderer, &Style),
617) where
618 Theme: super::Catalog,
619{
620 let doubled_border_width = styling.border_width * 2.0;
621 let doubled_outline_width = styling.outline_width * 2.0;
622
623 if styling.outline_width > 0.0 {
624 renderer.fill_quad(
625 renderer::Quad {
626 bounds: Rectangle {
627 x: bounds.x - styling.border_width - styling.outline_width,
628 y: bounds.y - styling.border_width - styling.outline_width,
629 width: bounds.width + doubled_border_width + doubled_outline_width,
630 height: bounds.height + doubled_border_width + doubled_outline_width,
631 },
632 border: Border {
633 width: styling.outline_width,
634 color: styling.outline_color,
635 radius: styling.border_radius,
636 },
637 shadow: Shadow::default(),
638 snap: true,
639 },
640 Color::TRANSPARENT,
641 );
642 }
643
644 if styling.background.is_some() || styling.border_width > 0.0 {
645 if styling.shadow_offset != Vector::default() {
646 renderer.fill_quad(
648 renderer::Quad {
649 bounds: Rectangle {
650 x: bounds.x + styling.shadow_offset.x,
651 y: bounds.y + styling.shadow_offset.y,
652 width: bounds.width,
653 height: bounds.height,
654 },
655 border: Border {
656 radius: styling.border_radius,
657 ..Default::default()
658 },
659 shadow: Shadow::default(),
660 snap: true,
661 },
662 Background::Color([0.0, 0.0, 0.0, 0.5].into()),
663 );
664 }
665
666 if let Some(background) = styling.background {
668 renderer.fill_quad(
669 renderer::Quad {
670 bounds,
671 border: Border {
672 radius: styling.border_radius,
673 ..Default::default()
674 },
675 shadow: Shadow::default(),
676 snap: true,
677 },
678 background,
679 );
680 }
681
682 draw_contents(renderer, styling);
684
685 let mut clipped_bounds = viewport_bounds.intersection(&bounds).unwrap_or_default();
686 clipped_bounds.height += styling.border_width;
687
688 renderer.with_layer(clipped_bounds, |renderer| {
689 renderer.fill_quad(
691 renderer::Quad {
692 bounds,
693 border: Border {
694 width: styling.border_width,
695 color: styling.border_color,
696 radius: styling.border_radius,
697 },
698 shadow: Shadow::default(),
699 snap: true,
700 },
701 Color::TRANSPARENT,
702 );
703 });
704 } else {
705 draw_contents(renderer, styling);
706 }
707}
708
709pub fn layout<Renderer>(
711 renderer: &Renderer,
712 limits: &layout::Limits,
713 width: Length,
714 height: Length,
715 padding: Padding,
716 layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
717) -> layout::Node {
718 let limits = limits.width(width).height(height);
719
720 let mut content = layout_content(renderer, &limits.shrink(padding));
721 let padding = padding.fit(content.size(), limits.max());
722 let size = limits
723 .shrink(padding)
724 .resolve(width, height, content.size())
725 .expand(padding);
726
727 content = content.move_to(Point::new(padding.left, padding.top));
728
729 layout::Node::with_children(size, vec![content])
730}