Skip to main content

freya_components/
tooltip.rs

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