Skip to main content

freya_components/
tooltip.rs

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