Skip to main content

freya_components/
tooltip.rs

1use std::{
2    borrow::Cow,
3    time::Duration,
4};
5
6use async_io::Timer;
7use freya_animation::{
8    easing::Function,
9    hook::{
10        AnimatedValue,
11        Ease,
12        OnChange,
13        OnCreation,
14        ReadAnimatedValue,
15        use_animation,
16    },
17    prelude::AnimNum,
18};
19use freya_core::prelude::*;
20
21use crate::{
22    attached::{
23        Attached,
24        AttachedPosition,
25    },
26    context_menu::ContextMenu,
27    define_theme,
28    get_theme,
29};
30
31define_theme! {
32    %[component]
33    pub Tooltip {
34        %[fields]
35        color: Color,
36        background: Color,
37        border_fill: Color,
38        font_size: f32,
39    }
40}
41
42/// Tooltip component.
43///
44/// # Example
45///
46/// ```rust
47/// # use freya::prelude::*;
48/// fn app() -> impl IntoElement {
49///     Tooltip::new("Hello, World!")
50/// }
51///
52/// # use freya_testing::prelude::*;
53/// # launch_doc(|| {
54/// #   rect().center().expanded().child(app())
55/// # }, "./images/gallery_tooltip.png").render();
56/// ```
57///
58/// # Preview
59/// ![Tooltip Preview][tooltip]
60#[cfg_attr(feature = "docs",
61    doc = embed_doc_image::embed_image!("tooltip", "images/gallery_tooltip.png")
62)]
63#[derive(PartialEq, Clone)]
64pub struct Tooltip {
65    /// Theme override.
66    pub(crate) theme: Option<TooltipThemePartial>,
67    /// Text to show in the [Tooltip].
68    text: Cow<'static, str>,
69    key: DiffKey,
70}
71
72impl KeyExt for Tooltip {
73    fn write_key(&mut self) -> &mut DiffKey {
74        &mut self.key
75    }
76}
77
78impl Tooltip {
79    pub fn new(text: impl Into<Cow<'static, str>>) -> Self {
80        Self {
81            theme: None,
82            text: text.into(),
83            key: DiffKey::None,
84        }
85    }
86}
87
88impl Component for Tooltip {
89    fn render(&self) -> impl IntoElement {
90        let theme = get_theme!(&self.theme, TooltipThemePreference, "tooltip");
91        let TooltipTheme {
92            background,
93            color,
94            border_fill,
95            font_size,
96        } = theme;
97
98        rect()
99            .interactive(Interactive::No)
100            .padding((4., 10.))
101            .border(
102                Border::new()
103                    .width(1.)
104                    .alignment(BorderAlignment::Inner)
105                    .fill(border_fill),
106            )
107            .background(background)
108            .corner_radius(8.)
109            .child(
110                label()
111                    .max_lines(1)
112                    .font_size(font_size)
113                    .color(color)
114                    .text(self.text.clone()),
115            )
116    }
117
118    fn render_key(&self) -> DiffKey {
119        self.key.clone().or(self.default_key())
120    }
121}
122
123#[derive(PartialEq)]
124pub struct TooltipContainer {
125    tooltip: Tooltip,
126    children: Vec<Element>,
127    position: AttachedPosition,
128    layout: LayoutData,
129    delay: Duration,
130    key: DiffKey,
131}
132
133impl KeyExt for TooltipContainer {
134    fn write_key(&mut self) -> &mut DiffKey {
135        &mut self.key
136    }
137}
138
139impl LayoutExt for TooltipContainer {
140    fn get_layout(&mut self) -> &mut LayoutData {
141        &mut self.layout
142    }
143}
144
145impl ChildrenExt for TooltipContainer {
146    fn get_children(&mut self) -> &mut Vec<Element> {
147        &mut self.children
148    }
149}
150
151impl TooltipContainer {
152    pub fn new(tooltip: Tooltip) -> Self {
153        Self {
154            tooltip,
155            children: vec![],
156            position: AttachedPosition::Bottom,
157            layout: LayoutData::default(),
158            delay: Duration::from_millis(500),
159            key: DiffKey::None,
160        }
161    }
162
163    pub fn position(mut self, position: AttachedPosition) -> Self {
164        self.position = position;
165        self
166    }
167
168    /// Delay before the tooltip is shown once the pointer starts hovering.
169    /// Defaults to 500ms.
170    pub fn delay(mut self, delay: Duration) -> Self {
171        self.delay = delay;
172        self
173    }
174}
175
176impl Component for TooltipContainer {
177    fn render(&self) -> impl IntoElement {
178        let mut is_hovering = use_state(|| false);
179        let mut delay_task = use_state::<Option<TaskHandle>>(|| None);
180
181        let animation = use_animation(move |conf| {
182            conf.on_change(OnChange::Rerun);
183            conf.on_creation(OnCreation::Finish);
184
185            let scale = AnimNum::new(0.9, 1.)
186                .time(150)
187                .ease(Ease::Out)
188                .function(Function::Expo);
189            let opacity = AnimNum::new(0., 1.)
190                .time(150)
191                .ease(Ease::Out)
192                .function(Function::Expo);
193
194            if is_hovering() {
195                (scale, opacity)
196            } else {
197                (scale.into_reversed(), opacity.into_reversed())
198            }
199        });
200
201        let (scale, opacity) = animation.read().value();
202
203        let delay = self.delay;
204        let on_pointer_over = move |_| {
205            if let Some(handle) = delay_task.write().take() {
206                handle.cancel();
207            }
208            let task = spawn(async move {
209                Timer::after(delay).await;
210                is_hovering.set_if_modified(true);
211            });
212            delay_task.set(Some(task));
213        };
214
215        let on_pointer_out = move |_| {
216            if let Some(handle) = delay_task.write().take() {
217                handle.cancel();
218            }
219            is_hovering.set_if_modified(false);
220        };
221
222        let is_visible = opacity > 0. && !ContextMenu::is_open();
223
224        let padding = match self.position {
225            AttachedPosition::Top => (0., 0., 5., 0.),
226            AttachedPosition::Bottom => (5., 0., 0., 0.),
227            AttachedPosition::Left => (0., 5., 0., 0.),
228            AttachedPosition::Right => (0., 0., 0., 5.),
229        };
230
231        rect()
232            .layout(self.layout.clone())
233            .a11y_focusable(false)
234            .a11y_role(AccessibilityRole::Tooltip)
235            .on_pointer_over(on_pointer_over)
236            .on_pointer_out(on_pointer_out)
237            .child(
238                Attached::new(rect().children(self.children.clone()))
239                    .position(self.position)
240                    .maybe_child(is_visible.then(|| {
241                        rect()
242                            .opacity(opacity)
243                            .scale(scale)
244                            .padding(padding)
245                            .child(self.tooltip.clone())
246                    })),
247            )
248    }
249
250    fn render_key(&self) -> DiffKey {
251        self.key.clone().or(self.default_key())
252    }
253}