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<TopLevelMessage>) -> 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<TopLevelMessage>) -> 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<TopLevelMessage>) -> 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(Arc::new(move || view())
538                                            as crate::surface::View<TopLevelMessage>),
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                            let theme = THEME.lock().unwrap();
557
558                            let corners = theme.cosmic().corner_radii.radius_s;
559                            let boxed_live: Box<dyn Fn() -> LiveSettings + Send + Sync + 'static> =
560                                Box::new(move || LiveSettings {
561                                    corners: Some(CornerRadius {
562                                        top_left: corners[0] as u32,
563                                        top_right: corners[1] as u32,
564                                        bottom_left: corners[3] as u32,
565                                        bottom_right: corners[2] as u32,
566                                    }),
567                                    ..Default::default()
568                                });
569                            let boxed_live: Box<dyn Any + Send + Sync + 'static> =
570                                Box::new(boxed_live);
571
572                            let sm = crate::surface::Action::Popup(
573                                Arc::new(boxed),
574                                Arc::new(boxed_live),
575                                Some(Arc::new(move || view())
576                                    as crate::surface::View<TopLevelMessage>),
577                            );
578                            shell.publish((on_surface_action)(sm));
579                        }
580                    }
581                }
582            }
583        }
584        _ => {}
585    }
586}
587
588#[allow(clippy::too_many_arguments)]
589pub fn draw<Renderer: iced_core::Renderer, Theme>(
590    renderer: &mut Renderer,
591    bounds: Rectangle,
592    viewport_bounds: Rectangle,
593    styling: &super::Style,
594    draw_contents: impl FnOnce(&mut Renderer, &Style),
595) where
596    Theme: super::Catalog,
597{
598    let doubled_border_width = styling.border_width * 2.0;
599    let doubled_outline_width = styling.outline_width * 2.0;
600
601    if styling.outline_width > 0.0 {
602        renderer.fill_quad(
603            renderer::Quad {
604                bounds: Rectangle {
605                    x: bounds.x - styling.border_width - styling.outline_width,
606                    y: bounds.y - styling.border_width - styling.outline_width,
607                    width: bounds.width + doubled_border_width + doubled_outline_width,
608                    height: bounds.height + doubled_border_width + doubled_outline_width,
609                },
610                border: Border {
611                    width: styling.outline_width,
612                    color: styling.outline_color,
613                    radius: styling.border_radius,
614                },
615                shadow: Shadow::default(),
616                snap: true,
617            },
618            Color::TRANSPARENT,
619        );
620    }
621
622    if styling.background.is_some() || styling.border_width > 0.0 {
623        if styling.shadow_offset != Vector::default() {
624            // TODO: Implement proper shadow support
625            renderer.fill_quad(
626                renderer::Quad {
627                    bounds: Rectangle {
628                        x: bounds.x + styling.shadow_offset.x,
629                        y: bounds.y + styling.shadow_offset.y,
630                        width: bounds.width,
631                        height: bounds.height,
632                    },
633                    border: Border {
634                        radius: styling.border_radius,
635                        ..Default::default()
636                    },
637                    shadow: Shadow::default(),
638                    snap: true,
639                },
640                Background::Color([0.0, 0.0, 0.0, 0.5].into()),
641            );
642        }
643
644        // Draw the button background first.
645        if let Some(background) = styling.background {
646            renderer.fill_quad(
647                renderer::Quad {
648                    bounds,
649                    border: Border {
650                        radius: styling.border_radius,
651                        ..Default::default()
652                    },
653                    shadow: Shadow::default(),
654                    snap: true,
655                },
656                background,
657            );
658        }
659
660        // Then draw the button contents onto the background.
661        draw_contents(renderer, styling);
662
663        let mut clipped_bounds = viewport_bounds.intersection(&bounds).unwrap_or_default();
664        clipped_bounds.height += styling.border_width;
665
666        renderer.with_layer(clipped_bounds, |renderer| {
667            // Finish by drawing the border above the contents.
668            renderer.fill_quad(
669                renderer::Quad {
670                    bounds,
671                    border: Border {
672                        width: styling.border_width,
673                        color: styling.border_color,
674                        radius: styling.border_radius,
675                    },
676                    shadow: Shadow::default(),
677                    snap: true,
678                },
679                Color::TRANSPARENT,
680            );
681        });
682    } else {
683        draw_contents(renderer, styling);
684    }
685}
686
687/// Computes the layout of a [`Tooltip`].
688pub fn layout<Renderer>(
689    renderer: &Renderer,
690    limits: &layout::Limits,
691    width: Length,
692    height: Length,
693    padding: Padding,
694    layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
695) -> layout::Node {
696    let limits = limits.width(width).height(height);
697
698    let mut content = layout_content(renderer, &limits.shrink(padding));
699    let padding = padding.fit(content.size(), limits.max());
700    let size = limits
701        .shrink(padding)
702        .resolve(width, height, content.size())
703        .expand(padding);
704
705    content = content.move_to(Point::new(padding.left, padding.top));
706
707    layout::Node::with_children(size, vec![content])
708}