freya_components/
tooltip.rs1use 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#[derive(PartialEq, Clone)]
79pub struct Tooltip {
80 pub(crate) theme: Option<TooltipThemePartial>,
82 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 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 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}