freya_components/
tooltip.rs1use 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#[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 pub(crate) theme: Option<TooltipThemePartial>,
67 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 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}