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