1use std::cell::RefCell;
14use std::rc::Rc;
15use std::time::Duration;
16
17use repose_core::animation::{AnimatedValue, AnimationSpec, Easing};
18use repose_core::animation_driver;
19use repose_core::{
20 Color, Indication, IndicationDrawNode, IndicationNodeFactory, InteractionSource, PressId, Rect,
21 Scene, SceneNode, Vec2, remember_state_with_key, request_frame,
22};
23
24const FADE_IN_MS: u64 = 75;
25const RADIUS_MS: u64 = 225;
26const FADE_OUT_MS: u64 = 150;
27
28const PRESS_ALPHA: f32 = 0.10;
30const FOCUS_ALPHA: f32 = 0.12;
32const HOVER_ALPHA: f32 = 0.08;
34
35#[derive(Clone, Debug)]
36pub struct RippleConfig {
37 pub bounded: bool,
38 pub radius: Option<f32>,
39 pub color: Option<Color>,
40 pub enable_press: bool,
41 pub enable_focus: bool,
42 pub enable_hover: bool,
43 pub press_offset: Option<Vec2>,
44}
45
46impl Default for RippleConfig {
47 fn default() -> Self {
48 Self {
49 bounded: true,
50 radius: None,
51 color: None,
52 enable_press: true,
53 enable_focus: true,
54 enable_hover: true,
55 press_offset: None,
56 }
57 }
58}
59
60pub fn ripple(config: RippleConfig) -> Rc<dyn IndicationNodeFactory> {
61 Rc::new(RippleNodeFactory { config })
62}
63
64pub fn default_ripple() -> Rc<dyn IndicationNodeFactory> {
66 ripple(RippleConfig::default())
67}
68
69#[derive(Clone, Debug)]
70pub struct RippleNodeFactory {
71 pub config: RippleConfig,
72}
73
74impl Indication for RippleNodeFactory {}
75
76impl IndicationNodeFactory for RippleNodeFactory {
77 fn create(&self, interaction_source: &InteractionSource) -> Box<dyn IndicationDrawNode> {
78 Box::new(RippleDrawNode::new(
79 interaction_source.clone(),
80 self.config.clone(),
81 ))
82 }
83}
84
85struct RippleDrawNode {
86 interaction_source: InteractionSource,
87 config: RippleConfig,
88}
89
90impl RippleDrawNode {
91 fn new(interaction_source: InteractionSource, config: RippleConfig) -> Self {
92 Self {
93 interaction_source,
94 config,
95 }
96 }
97
98 fn anim_base(&self) -> String {
99 format!("rp:{:p}", self.interaction_source.stable_id())
100 }
101
102 fn register_driver(key: &str, anim: Rc<RefCell<AnimatedValue<f32>>>) {
103 animation_driver::register(
104 key.to_string(),
105 Rc::new(RefCell::new(move || anim.borrow_mut().update())),
106 );
107 request_frame();
108 }
109}
110
111impl IndicationDrawNode for RippleDrawNode {
112 fn draw(&self, scene: &mut Scene, rect: Rect, radius: [f32; 4], alpha: f32) {
113 let base_color = self
114 .config
115 .color
116 .unwrap_or_else(|| repose_core::locals::content_color());
117
118 let is_pressed = self.interaction_source.collect_is_pressed();
122 let is_hovered = self.interaction_source.collect_is_hovered();
123 let is_focused = self.interaction_source.collect_is_focus_visible();
124 let bounded = self.config.bounded;
125
126 let center_scene = Vec2 {
127 x: rect.x + rect.w * 0.5,
128 y: rect.y + rect.h * 0.5,
129 };
130 let target_radius = self.config.radius.unwrap_or_else(|| {
131 let diag = (rect.w * rect.w + rect.h * rect.h).sqrt();
132 if bounded {
133 diag * 0.5 + 10.0
134 } else {
135 diag * 0.5
136 }
137 });
138
139 let layer_alpha = if self.config.enable_focus && is_focused {
140 Some(FOCUS_ALPHA)
141 } else if self.config.enable_hover && is_hovered {
142 Some(HOVER_ALPHA)
143 } else {
144 None
145 };
146
147 if let Some(layer_a) = layer_alpha {
148 let layer_a = layer_a * alpha;
149 if layer_a > 0.001 {
150 let layer_rect = Rect {
151 x: center_scene.x - target_radius,
152 y: center_scene.y - target_radius,
153 w: target_radius * 2.0,
154 h: target_radius * 2.0,
155 };
156 let brush = base_color.with_alpha_f32(layer_a).into();
157 if bounded {
158 scene.nodes.push(SceneNode::PushClip {
159 rect,
160 radius,
161 op: repose_core::ClipOp::Intersect,
162 });
163 }
164 scene.nodes.push(SceneNode::Ellipse {
165 rect: layer_rect,
166 brush,
167 });
168 if bounded {
169 scene.nodes.push(SceneNode::PopClip);
170 }
171 }
172 }
173
174 if !self.config.enable_press {
175 return;
176 }
177
178 let base = self.anim_base();
179 let start_radius = rect.w.max(rect.h) * 0.3;
180
181 let current_pid = self.interaction_source.collect_last_press_id();
182 let press_pos = self.interaction_source.collect_last_press_position();
183
184 let k_alpha = format!("{}:a", base);
185 let k_rad = format!("{}:r", base);
186 let k_ctr = format!("{}:c", base);
187
188 let alpha_anim = remember_state_with_key(&k_alpha, || {
189 AnimatedValue::new(
190 0.0f32,
191 AnimationSpec::tween(Duration::from_millis(FADE_IN_MS), Easing::Linear),
192 )
193 });
194 let rad_anim = remember_state_with_key(&k_rad, || {
195 AnimatedValue::new(
196 0.0f32,
197 AnimationSpec::tween(Duration::from_millis(RADIUS_MS), Easing::FastOutSlowIn),
198 )
199 });
200 let ctr_anim = remember_state_with_key(&k_ctr, || {
201 AnimatedValue::new(
202 0.0f32,
203 AnimationSpec::tween(Duration::from_millis(RADIUS_MS), Easing::Linear),
204 )
205 });
206
207 let k_phase = format!("{}:ph", base);
209 let phase = remember_state_with_key(&k_phase, || 0u8);
210
211 let k_last_pid = format!("{}:lpid", base);
213 let last_pid = remember_state_with_key(&k_last_pid, || None::<PressId>);
214
215 let k_release_pending = format!("{}:rpend", base);
217 let release_pending = remember_state_with_key(&k_release_pending, || false);
218
219 let prev_pid = *last_pid.borrow();
220
221 animation_driver::touch(&format!("{}:drv:a", base));
222 animation_driver::touch(&format!("{}:drv:r", base));
223 animation_driver::touch(&format!("{}:drv:c", base));
224
225 let new_press = current_pid.is_some() && current_pid != prev_pid;
228
229 if new_press {
230 *last_pid.borrow_mut() = current_pid;
231 *phase.borrow_mut() = 1;
232 *release_pending.borrow_mut() = false;
233
234 let spec_in = AnimationSpec::tween(Duration::from_millis(FADE_IN_MS), Easing::Linear);
235 let spec_rad =
236 AnimationSpec::tween(Duration::from_millis(RADIUS_MS), Easing::FastOutSlowIn);
237 let spec_ctr = AnimationSpec::tween(Duration::from_millis(RADIUS_MS), Easing::Linear);
238
239 {
240 let mut a = alpha_anim.borrow_mut();
241 a.snap_to(0.0);
242 a.set_spec(spec_in);
243 a.set_target(1.0);
244 }
245 Self::register_driver(&format!("{}:drv:a", base), alpha_anim.clone());
246
247 {
248 let mut r = rad_anim.borrow_mut();
249 r.snap_to(0.0);
250 r.set_spec(spec_rad);
251 r.set_target(1.0);
252 }
253 Self::register_driver(&format!("{}:drv:r", base), rad_anim.clone());
254
255 {
256 let mut c = ctr_anim.borrow_mut();
257 c.snap_to(0.0);
258 c.set_spec(spec_ctr);
259 c.set_target(1.0);
260 }
261 Self::register_driver(&format!("{}:drv:c", base), ctr_anim.clone());
262 }
263
264 if *phase.borrow() != 0
267 && !is_pressed
268 && current_pid.is_some()
269 && current_pid == *last_pid.borrow()
270 {
271 *release_pending.borrow_mut() = true;
272 }
273
274 if *phase.borrow() != 0 && !animation_driver::is_registered(&format!("{}:drv:a", base)) {
275 *phase.borrow_mut() = 0;
276 *last_pid.borrow_mut() = current_pid;
277 *release_pending.borrow_mut() = false;
278 alpha_anim.borrow_mut().snap_to(0.0);
279 rad_anim.borrow_mut().snap_to(0.0);
280 ctr_anim.borrow_mut().snap_to(0.0);
281 return;
282 }
283
284 let fade_pct = *alpha_anim.borrow().get();
285
286 if *phase.borrow() == 1 && fade_pct >= 1.0 {
287 if *release_pending.borrow() {
289 *phase.borrow_mut() = 3;
290 let spec_out =
291 AnimationSpec::tween(Duration::from_millis(FADE_OUT_MS), Easing::Linear);
292 alpha_anim.borrow_mut().set_target(0.0);
293 alpha_anim.borrow_mut().set_spec(spec_out);
294 Self::register_driver(&format!("{}:drv:a", base), alpha_anim.clone());
295 } else {
296 *phase.borrow_mut() = 2;
297 }
298 }
299
300 if *phase.borrow() == 2 && *release_pending.borrow() {
301 *phase.borrow_mut() = 3;
303 let spec_out = AnimationSpec::tween(Duration::from_millis(FADE_OUT_MS), Easing::Linear);
304 alpha_anim.borrow_mut().set_target(0.0);
305 alpha_anim.borrow_mut().set_spec(spec_out);
306 Self::register_driver(&format!("{}:drv:a", base), alpha_anim.clone());
307 }
308
309 if *phase.borrow() == 3 && fade_pct <= 0.01 {
310 *phase.borrow_mut() = 0;
312 *last_pid.borrow_mut() = current_pid;
315 alpha_anim.borrow_mut().snap_to(0.0);
316 rad_anim.borrow_mut().snap_to(0.0);
317 ctr_anim.borrow_mut().snap_to(0.0);
318 return;
319 }
320
321 if *phase.borrow() == 0 {
322 return;
323 }
324 let snap_finish = *release_pending.borrow() && *phase.borrow() == 1;
325 if fade_pct <= 0.01 && !snap_finish {
326 return;
327 }
328
329 let draw_alpha = if snap_finish { 1.0f32 } else { fade_pct };
332
333 let rad_pct = *rad_anim.borrow().get();
334 let ctr_pct = *ctr_anim.borrow().get();
335 let current_radius = start_radius + (target_radius - start_radius) * rad_pct;
336
337 let origin_scene = match press_pos {
338 Some(pos) => {
339 let effective = if let Some(off) = self.config.press_offset {
340 Vec2 {
341 x: pos.x - off.x,
342 y: pos.y - off.y,
343 }
344 } else {
345 pos
346 };
347 let ox = rect.x + effective.x;
348 let oy = rect.y + effective.y;
349 if bounded {
350 Vec2 {
351 x: ox + (center_scene.x - ox) * ctr_pct,
352 y: oy + (center_scene.y - oy) * ctr_pct,
353 }
354 } else {
355 center_scene
356 }
357 }
358 None => center_scene,
359 };
360
361 let ripple_alpha = PRESS_ALPHA * draw_alpha * alpha;
363 if ripple_alpha <= 0.001 {
364 return;
365 }
366
367 let draw_color = base_color.with_alpha_f32(ripple_alpha);
368
369 if bounded {
370 scene.nodes.push(SceneNode::PushClip {
371 rect,
372 radius,
373 op: repose_core::ClipOp::Intersect,
374 });
375 scene.nodes.push(SceneNode::Ellipse {
376 rect: Rect {
377 x: origin_scene.x - current_radius,
378 y: origin_scene.y - current_radius,
379 w: current_radius * 2.0,
380 h: current_radius * 2.0,
381 },
382 brush: draw_color.into(),
383 });
384 scene.nodes.push(SceneNode::PopClip);
385 } else {
386 scene.nodes.push(SceneNode::Ellipse {
387 rect: Rect {
388 x: origin_scene.x - current_radius,
389 y: origin_scene.y - current_radius,
390 w: current_radius * 2.0,
391 h: current_radius * 2.0,
392 },
393 brush: draw_color.into(),
394 });
395 }
396 }
397}