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/// Use [Tooltip::new_text] to show plain text in a single line:
47///
48/// ```rust
49/// # use freya::prelude::*;
50/// fn app() -> impl IntoElement {
51///     Tooltip::new_text("Hello, World!")
52/// }
53///
54/// # use freya_testing::prelude::*;
55/// # launch_doc(|| {
56/// #   rect().center().expanded().child(app())
57/// # }, "./images/gallery_tooltip.png").render();
58/// ```
59///
60/// Use [Tooltip::new] to show any element:
61///
62/// ```rust
63/// # use freya::prelude::*;
64/// fn app() -> impl IntoElement {
65///     Tooltip::new().child(
66///         rect()
67///             .horizontal()
68///             .cross_align(Alignment::Center)
69///             .spacing(4.)
70///             .child(
71///                 rect()
72///                     .width(Size::px(10.))
73///                     .height(Size::px(10.))
74///                     .corner_radius(5.)
75///                     .background(Color::GREEN),
76///             )
77///             .child("Connected"),
78///     )
79/// }
80/// # let _ = app();
81/// ```
82///
83/// # Preview
84/// ![Tooltip Preview][tooltip]
85#[cfg_attr(feature = "docs",
86    doc = embed_doc_image::embed_image!("tooltip", "images/gallery_tooltip.png")
87)]
88#[derive(PartialEq, Clone)]
89pub struct Tooltip {
90    /// Theme override.
91    pub(crate) theme: Option<TooltipThemePartial>,
92    /// Content to show in the [Tooltip].
93    children: Vec<Element>,
94    key: DiffKey,
95}
96
97impl Default for Tooltip {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103impl KeyExt for Tooltip {
104    fn write_key(&mut self) -> &mut DiffKey {
105        &mut self.key
106    }
107}
108
109impl ChildrenExt for Tooltip {
110    fn get_children(&mut self) -> &mut Vec<Element> {
111        &mut self.children
112    }
113}
114
115impl Tooltip {
116    pub fn new() -> Self {
117        Self {
118            theme: None,
119            children: vec![],
120            key: DiffKey::None,
121        }
122    }
123
124    /// Create a [Tooltip] with a single-line text label.
125    pub fn new_text(text: impl Into<Cow<'static, str>>) -> Self {
126        Self::new().child(label().max_lines(1).text(text))
127    }
128}
129
130impl Component for Tooltip {
131    fn render(&self) -> impl IntoElement {
132        let theme = get_theme!(&self.theme, TooltipThemePreference, "tooltip");
133        let TooltipTheme {
134            background,
135            color,
136            border_fill,
137            font_size,
138        } = theme;
139
140        rect()
141            .interactive(Interactive::No)
142            .padding((4., 10.))
143            .border(
144                Border::new()
145                    .width(1.)
146                    .alignment(BorderAlignment::Inner)
147                    .fill(border_fill),
148            )
149            .background(background)
150            .corner_radius(8.)
151            .font_size(font_size)
152            .color(color)
153            .children(self.children.clone())
154    }
155
156    fn render_key(&self) -> DiffKey {
157        self.key.clone().or(self.default_key())
158    }
159}
160
161#[derive(PartialEq)]
162pub struct TooltipContainer {
163    tooltip: Tooltip,
164    children: Vec<Element>,
165    position: AttachedPosition,
166    layout: LayoutData,
167    delay: Duration,
168    key: DiffKey,
169}
170
171impl KeyExt for TooltipContainer {
172    fn write_key(&mut self) -> &mut DiffKey {
173        &mut self.key
174    }
175}
176
177impl LayoutExt for TooltipContainer {
178    fn get_layout(&mut self) -> &mut LayoutData {
179        &mut self.layout
180    }
181}
182
183impl ChildrenExt for TooltipContainer {
184    fn get_children(&mut self) -> &mut Vec<Element> {
185        &mut self.children
186    }
187}
188
189impl TooltipContainer {
190    pub fn new(tooltip: Tooltip) -> Self {
191        Self {
192            tooltip,
193            children: vec![],
194            position: AttachedPosition::Bottom,
195            layout: LayoutData::default(),
196            delay: Duration::from_millis(500),
197            key: DiffKey::None,
198        }
199    }
200
201    pub fn position(mut self, position: AttachedPosition) -> Self {
202        self.position = position;
203        self
204    }
205
206    /// Delay before the tooltip is shown once the pointer starts hovering.
207    /// Defaults to 500ms.
208    pub fn delay(mut self, delay: Duration) -> Self {
209        self.delay = delay;
210        self
211    }
212}
213
214impl Component for TooltipContainer {
215    fn render(&self) -> impl IntoElement {
216        let mut is_hovering = use_state(|| false);
217        let mut delay_task = use_state::<Option<TaskHandle>>(|| None);
218
219        let animation = use_animation(move |conf| {
220            conf.on_change(OnChange::Rerun);
221            conf.on_creation(OnCreation::Finish);
222
223            let scale = AnimNum::new(0.9, 1.)
224                .time(150)
225                .ease(Ease::Out)
226                .function(Function::Expo);
227            let opacity = AnimNum::new(0., 1.)
228                .time(150)
229                .ease(Ease::Out)
230                .function(Function::Expo);
231
232            if is_hovering() {
233                (scale, opacity)
234            } else {
235                (scale.into_reversed(), opacity.into_reversed())
236            }
237        });
238
239        let (scale, opacity) = animation.read().value();
240
241        let delay = self.delay;
242        let on_pointer_over = move |_| {
243            if let Some(handle) = delay_task.write().take() {
244                handle.cancel();
245            }
246            let task = spawn(async move {
247                Timer::after(delay).await;
248                is_hovering.set_if_modified(true);
249            });
250            delay_task.set(Some(task));
251        };
252
253        let on_pointer_out = move |_| {
254            if let Some(handle) = delay_task.write().take() {
255                handle.cancel();
256            }
257            is_hovering.set_if_modified(false);
258        };
259
260        let is_visible = opacity > 0. && !ContextMenu::is_open();
261
262        let padding = match self.position {
263            AttachedPosition::Top => (0., 0., 5., 0.),
264            AttachedPosition::Bottom => (5., 0., 0., 0.),
265            AttachedPosition::Left => (0., 5., 0., 0.),
266            AttachedPosition::Right => (0., 0., 0., 5.),
267        };
268
269        rect()
270            .layout(self.layout.clone())
271            .a11y_focusable(false)
272            .a11y_role(AccessibilityRole::Tooltip)
273            .on_pointer_over(on_pointer_over)
274            .on_pointer_out(on_pointer_out)
275            .child(
276                Attached::new(rect().children(self.children.clone()))
277                    .position(self.position)
278                    .maybe_child(is_visible.then(|| {
279                        rect()
280                            .opacity(opacity)
281                            .scale(scale)
282                            .padding(padding)
283                            .child(self.tooltip.clone())
284                    })),
285            )
286    }
287
288    fn render_key(&self) -> DiffKey {
289        self.key.clone().or(self.default_key())
290    }
291}