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",
86 doc = embed_doc_image::embed_image!("tooltip", "images/gallery_tooltip.png")
87)]
88#[derive(PartialEq, Clone)]
89pub struct Tooltip {
90 pub(crate) theme: Option<TooltipThemePartial>,
92 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 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 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}