Skip to main content

cosmic/widget/wayland/tooltip/
widget.rs

1// Copyright 2019 H�ctor Ram�n, Iced contributors
2// Copyright 2023 System76 <info@system76.com>
3// SPDX-License-Identifier: MIT
4
5//! Allow your users to perform actions by pressing a button.
6//!
7//! A [`Tooltip`] has some local [`State`].
8
9use 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/// A generic button which emits a message when pressed.
31#[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    /// Creates a new [`Tooltip`] with the given content.
65    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    /// Sets the [`Id`] of the [`Tooltip`].
112    pub fn id(mut self, id: Id) -> Self {
113        self.id = id;
114        self
115    }
116
117    /// Sets the width of the [`Tooltip`].
118    pub fn width(mut self, width: impl Into<Length>) -> Self {
119        self.width = width.into();
120        self
121    }
122
123    /// Sets the height of the [`Tooltip`].
124    pub fn height(mut self, height: impl Into<Length>) -> Self {
125        self.height = height.into();
126        self
127    }
128
129    /// Sets the [`Padding`] of the [`Tooltip`].
130    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
131        self.padding = padding.into();
132        self
133    }
134
135    /// Sets the widget to a selected state.
136    ///
137    /// Displays a selection indicator on image buttons.
138    pub fn selected(mut self, selected: bool) -> Self {
139        self.selected = selected;
140
141        self
142    }
143
144    /// Sets the style variant of this [`Tooltip`].
145    pub fn class(mut self, style: crate::theme::Tooltip) -> Self {
146        self.style = style;
147        self
148    }
149
150    #[cfg(feature = "a11y")]
151    /// Sets the name of the [`Tooltip`].
152    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    /// Sets the description of the [`Tooltip`].
159    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    /// Sets the description of the [`Tooltip`].
168    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    /// Sets the label of the [`Tooltip`].
175    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    /// get the a11y nodes for the widget
374    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/// The local state of a [`Tooltip`].
407#[derive(Debug, Clone, Default)]
408#[allow(clippy::struct_field_names)]
409pub struct State {
410    is_hovered: Arc<Mutex<bool>>,
411}
412
413impl State {
414    /// Returns whether the [`Tooltip`] is currently hovered or not.
415    pub fn is_hovered(self) -> bool {
416        let guard = self.is_hovered.lock().unwrap();
417        *guard
418    }
419}
420
421/// Processes the given [`Event`] and updates the [`State`] of a [`Tooltip`]
422/// accordingly.
423#[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            // TODO: Implement proper shadow support
647            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        // Draw the button background first.
667        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        // Then draw the button contents onto the background.
683        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            // Finish by drawing the border above the contents.
690            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
709/// Computes the layout of a [`Tooltip`].
710pub 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}