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