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::{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
26/// Internally defines different button widget variants.
27enum Variant<Message> {
28    Normal,
29    Image {
30        close_icon: svg::Handle,
31        on_remove: Option<Message>,
32    },
33}
34
35/// A generic button which emits a message when pressed.
36#[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    /// Creates a new [`Tooltip`] with the given content.
70    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    /// Sets the [`Id`] of the [`Tooltip`].
117    pub fn id(mut self, id: Id) -> Self {
118        self.id = id;
119        self
120    }
121
122    /// Sets the width of the [`Tooltip`].
123    pub fn width(mut self, width: impl Into<Length>) -> Self {
124        self.width = width.into();
125        self
126    }
127
128    /// Sets the height of the [`Tooltip`].
129    pub fn height(mut self, height: impl Into<Length>) -> Self {
130        self.height = height.into();
131        self
132    }
133
134    /// Sets the [`Padding`] of the [`Tooltip`].
135    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
136        self.padding = padding.into();
137        self
138    }
139
140    /// Sets the widget to a selected state.
141    ///
142    /// Displays a selection indicator on image buttons.
143    pub fn selected(mut self, selected: bool) -> Self {
144        self.selected = selected;
145
146        self
147    }
148
149    /// Sets the style variant of this [`Tooltip`].
150    pub fn class(mut self, style: crate::theme::Tooltip) -> Self {
151        self.style = style;
152        self
153    }
154
155    #[cfg(feature = "a11y")]
156    /// Sets the name of the [`Tooltip`].
157    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    /// Sets the description of the [`Tooltip`].
164    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    /// Sets the description of the [`Tooltip`].
173    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    /// Sets the label of the [`Tooltip`].
180    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    /// get the a11y nodes for the widget
381    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/// The local state of a [`Tooltip`].
414#[derive(Debug, Clone, Default)]
415#[allow(clippy::struct_field_names)]
416pub struct State {
417    is_hovered: Arc<Mutex<bool>>,
418}
419
420impl State {
421    /// Returns whether the [`Tooltip`] is currently hovered or not.
422    pub fn is_hovered(self) -> bool {
423        let guard = self.is_hovered.lock().unwrap();
424        *guard
425    }
426}
427
428/// Processes the given [`Event`] and updates the [`State`] of a [`Tooltip`]
429/// accordingly.
430#[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            // TODO: Implement proper shadow support
620            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        // Draw the button background first.
640        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        // Then draw the button contents onto the background.
656        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            // Finish by drawing the border above the contents.
663            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
682/// Computes the layout of a [`Tooltip`].
683pub 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}