1use frus_core::{
20 BorderRadius, ClipShape, Color, Curve, LayerFilter, Path, Point, Primitive, Rect, Scene, Size,
21};
22use frus_layout::Style;
23
24use crate::interaction::Status;
25use crate::theme::Theme;
26use crate::widget::Widget;
27
28const FADE_IN: f32 = 0.075;
30const RADIUS_HELD: f32 = 1.0;
33const RADIUS_CONFIRMED: f32 = 0.225;
35const FADE_OUT: f32 = 0.375;
37const CANCEL: f32 = 0.075;
39const FADE_OUT_START: f32 = 225.0 / 375.0;
42const START_FRACTION: f32 = 0.30;
44const OVERSHOOT: f32 = 5.0;
46
47const DEFAULT_ALPHA: f32 = 0.12;
50
51pub fn default_splash(theme: &Theme) -> Color {
57 theme
58 .widgets
59 .ink
60 .color
61 .unwrap_or_else(|| theme.scheme.on_surface.fade(DEFAULT_ALPHA))
62}
63
64#[derive(Clone, Copy, Debug, PartialEq)]
73pub struct InkStyle {
74 pub color: Color,
76 pub radius: BorderRadius,
79}
80
81impl InkStyle {
82 pub fn of(theme: &Theme) -> Self {
84 Self {
85 color: default_splash(theme),
86 radius: BorderRadius::ZERO,
87 }
88 }
89
90 pub fn color(mut self, color: Color) -> Self {
92 self.color = color;
93 self
94 }
95
96 pub fn radius(mut self, radius: impl Into<BorderRadius>) -> Self {
98 self.radius = radius.into();
99 self
100 }
101}
102
103#[derive(Clone, Copy, Debug)]
109struct Ripple {
110 origin: Point,
112 centre: Point,
114 target: f32,
116 fade_in: f32,
118 fade_in_running: bool,
121 radius_t: f32,
123 radius_seconds: f32,
127 fade_out: f32,
129 fade_out_rate: f32,
131}
132
133impl Ripple {
134 fn new(origin: Point, size: Size) -> Self {
135 Self {
136 origin,
137 centre: Point::new(size.width / 2.0, size.height / 2.0),
138 target: target_radius(size),
139 fade_in: 0.0,
140 fade_in_running: true,
141 radius_t: 0.0,
142 radius_seconds: RADIUS_HELD,
143 fade_out: 0.0,
144 fade_out_rate: 0.0,
145 }
146 }
147
148 fn held(&self) -> bool {
150 self.fade_out_rate == 0.0
151 }
152
153 fn confirm(&mut self) {
156 if !self.held() {
157 return;
158 }
159 self.radius_seconds = RADIUS_CONFIRMED;
160 self.fade_out_rate = 1.0 / FADE_OUT;
161 }
162
163 fn cancel(&mut self) {
171 if !self.held() {
172 return;
173 }
174 self.fade_in_running = false;
175 self.fade_out = 1.0 - self.fade_in;
176 let left = 1.0 - self.fade_out;
177 self.fade_out_rate = if left > 0.0 { left / CANCEL } else { f32::MAX };
178 }
179
180 fn advance(&mut self, dt: f32) -> bool {
183 if self.fade_in_running && self.fade_in < 1.0 {
184 self.fade_in = (self.fade_in + dt / FADE_IN).min(1.0);
185 }
186 if self.radius_t < 1.0 {
187 self.radius_t = (self.radius_t + dt / self.radius_seconds).min(1.0);
188 }
189 if self.fade_out_rate > 0.0 && self.fade_out < 1.0 {
190 self.fade_out = (self.fade_out + dt * self.fade_out_rate).min(1.0);
191 }
192 !self.finished()
193 }
194
195 fn finished(&self) -> bool {
197 !self.held() && self.fade_out >= 1.0
198 }
199
200 fn alpha(&self) -> f32 {
202 if self.fade_in_running && self.fade_in < 1.0 {
203 self.fade_in
204 } else {
205 1.0 - ((self.fade_out - FADE_OUT_START) / (1.0 - FADE_OUT_START)).clamp(0.0, 1.0)
206 }
207 }
208
209 fn circle(&self) -> (Point, f32) {
211 let t = Curve::ease().transform(self.radius_t);
212 let radius = self.target * START_FRACTION
213 + (self.target + OVERSHOOT - self.target * START_FRACTION) * t;
214 let centre = Point::new(
215 self.origin.x + (self.centre.x - self.origin.x) * t,
216 self.origin.y + (self.centre.y - self.origin.y) * t,
217 );
218 (centre, radius)
219 }
220}
221
222fn target_radius(size: Size) -> f32 {
229 (size.width * size.width + size.height * size.height).sqrt() / 2.0
230}
231
232#[derive(Clone, Debug, Default)]
238pub struct Ripples {
239 list: Vec<Ripple>,
240}
241
242const MAX_RIPPLES: usize = 8;
245
246impl Ripples {
247 pub fn press(&mut self, origin: Point, size: Size) {
249 if size.width <= 0.0 || size.height <= 0.0 {
250 return;
251 }
252 if self.list.len() >= MAX_RIPPLES {
253 self.list.remove(0);
254 }
255 self.list.push(Ripple::new(origin, size));
256 }
257
258 pub fn confirm(&mut self) {
260 if let Some(last) = self.list.iter_mut().rev().find(|r| r.held()) {
261 last.confirm();
262 }
263 }
264
265 pub fn cancel(&mut self) {
267 for ripple in self.list.iter_mut().filter(|r| r.held()) {
268 ripple.cancel();
269 }
270 }
271
272 pub fn advance(&mut self, dt: f32) -> bool {
275 let mut animating = false;
276 self.list.retain_mut(|ripple| {
277 animating |= ripple.advance(dt);
278 !ripple.finished()
279 });
280 animating
281 }
282
283 pub fn is_idle(&self) -> bool {
285 self.list.is_empty()
286 }
287
288 pub fn hash_state<H: std::hash::Hasher>(&self, h: &mut H) {
291 use std::hash::Hash;
292 self.list.len().hash(h);
293 for ripple in &self.list {
294 ripple.radius_t.to_bits().hash(h);
295 ripple.fade_in.to_bits().hash(h);
296 ripple.fade_out.to_bits().hash(h);
297 }
298 }
299
300 pub fn paint(
306 &self,
307 owner: u64,
308 bounds: Rect,
309 radius: BorderRadius,
310 color: Color,
311 scene: &mut Scene,
312 ) {
313 let start = scene.primitives().len();
314 for ripple in &self.list {
315 let alpha = ripple.alpha();
316 if alpha <= 0.002 {
317 continue;
318 }
319 let (centre, r) = ripple.circle();
320 let circle = Rect::new(
321 bounds.x + centre.x - r,
322 bounds.y + centre.y - r,
323 r * 2.0,
324 r * 2.0,
325 );
326 scene.fill_path(&Path::oval(circle), color.fade(alpha));
327 }
328 let group = scene.split_off(start);
329 if group.is_empty() {
330 return;
331 }
332 scene.push_primitive(Primitive::Layer {
333 primitives: group,
334 opacity: 1.0,
335 clip: bounds,
336 clip_shape: if radius == BorderRadius::ZERO {
337 ClipShape::Rect
338 } else {
339 ClipShape::RRect(radius)
340 },
341 transform: None,
342 filter: LayerFilter::NONE,
343 owner,
344 });
345 }
346}
347
348pub struct InkWell<Msg = crate::callback::Callback> {
366 color: Option<Color>,
368 radius: BorderRadius,
369 on_click: Option<Msg>,
370 on_long_press: Option<Msg>,
371 children: Vec<Box<dyn Widget<Msg>>>,
372}
373
374impl<Msg> Default for InkWell<Msg> {
375 fn default() -> Self {
376 Self::new()
377 }
378}
379
380impl<Msg> InkWell<Msg> {
381 pub fn new() -> Self {
383 Self {
384 color: None,
385 radius: BorderRadius::ZERO,
386 on_click: None,
387 on_long_press: None,
388 children: Vec::new(),
389 }
390 }
391
392 pub fn color(mut self, color: Color) -> Self {
395 self.color = Some(color);
396 self
397 }
398
399 pub fn radius(mut self, radius: impl Into<BorderRadius>) -> Self {
402 self.radius = radius.into();
403 self
404 }
405
406 pub fn on_click(mut self, msg: impl Into<Msg>) -> Self {
408 self.on_click = Some(msg.into());
409 self
410 }
411
412 pub fn on_long_press(mut self, msg: impl Into<Msg>) -> Self {
414 self.on_long_press = Some(msg.into());
415 self
416 }
417
418 pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
420 self.children.clear();
421 self.children.push(Box::new(child));
422 self
423 }
424}
425
426impl<Msg: Clone> Widget<Msg> for InkWell<Msg> {
427 fn style(&self) -> Style {
428 Style::default()
430 }
431
432 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
433 &self.children
434 }
435
436 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {
437 }
439
440 fn ink(&self, theme: &Theme) -> Option<InkStyle> {
441 Some(
442 InkStyle::of(theme)
443 .color(self.color.unwrap_or_else(|| default_splash(theme)))
444 .radius(self.radius),
445 )
446 }
447
448 fn on_click(&self) -> Option<Msg> {
449 self.on_click.clone()
450 }
451
452 fn on_long_press(&self) -> Option<Msg> {
453 self.on_long_press.clone()
454 }
455
456 fn debug_name(&self) -> &'static str {
457 "InkWell"
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 const BOX: Size = Size {
466 width: 100.0,
467 height: 60.0,
468 };
469
470 fn pressed(origin: Point) -> Ripples {
471 let mut ripples = Ripples::default();
472 ripples.press(origin, BOX);
473 ripples
474 }
475
476 #[test]
477 fn a_splash_starts_where_the_finger_landed_and_at_thirty_percent() {
478 let ripples = pressed(Point::new(10.0, 10.0));
479 let (centre, radius) = ripples.list[0].circle();
480 assert!(
481 (centre.x - 10.0).abs() < 0.01 && (centre.y - 10.0).abs() < 0.01,
482 "it starts under the finger: {centre:?}"
483 );
484 let target = target_radius(BOX);
485 assert!(
486 (radius - target * 0.30).abs() < 1e-3,
487 "the first frame is 30% of the target radius, got {radius} of {target}"
488 );
489 }
490
491 #[test]
492 fn the_target_radius_covers_the_far_corner() {
493 let target = target_radius(BOX);
496 let half_diagonal = (100.0f32.powi(2) + 60.0f32.powi(2)).sqrt() / 2.0;
497 assert!((target - half_diagonal).abs() < 1e-3);
498 }
499
500 #[test]
501 fn a_held_splash_swells_slowly_and_never_leaves() {
502 let mut ripples = pressed(Point::new(50.0, 30.0));
503 for _ in 0..15 {
506 ripples.advance(1.0 / 60.0);
507 }
508 let ripple = ripples.list[0];
509 assert!(
510 ripple.radius_t > 0.2 && ripple.radius_t < 0.3,
511 "held, it is only a quarter of the way out: {}",
512 ripple.radius_t
513 );
514 assert!(!ripple.finished(), "and it waits for the finger");
515 assert_eq!(
516 ripple.alpha(),
517 1.0,
518 "though it reached full opacity long ago"
519 );
520 }
521
522 #[test]
523 fn a_confirmed_splash_fades_out_and_is_dropped() {
524 let mut ripples = pressed(Point::new(50.0, 30.0));
525 ripples.advance(1.0 / 60.0);
526 ripples.confirm();
527 let mut frames = 0;
528 while ripples.advance(1.0 / 60.0) && frames < 200 {
529 frames += 1;
530 }
531 assert!(ripples.is_idle(), "the splash is gone");
532 assert!(
534 (20..=24).contains(&frames),
535 "it takes the reference's 375 ms, got {frames} frames"
536 );
537 }
538
539 #[test]
540 fn the_ink_holds_its_colour_before_it_starts_leaving() {
541 let mut ripples = pressed(Point::new(50.0, 30.0));
542 for _ in 0..6 {
543 ripples.advance(1.0 / 60.0);
544 }
545 ripples.confirm();
546 for _ in 0..9 {
549 ripples.advance(1.0 / 60.0);
550 }
551 assert_eq!(ripples.list[0].alpha(), 1.0);
552 for _ in 0..9 {
555 ripples.advance(1.0 / 60.0);
556 }
557 let alpha = ripples.list[0].alpha();
558 assert!(
559 (alpha - 0.5).abs() < 0.05,
560 "past the hold it drops fast: {alpha}"
561 );
562 }
563
564 #[test]
565 fn a_cancelled_splash_leaves_five_times_faster_than_a_confirmed_one() {
566 let mut held = pressed(Point::new(50.0, 30.0));
567 for _ in 0..6 {
568 held.advance(1.0 / 60.0); }
570 let mut cancelled = held.clone();
571 held.confirm();
572 cancelled.cancel();
573
574 let count = |mut r: Ripples| {
575 let mut frames = 0;
576 while r.advance(1.0 / 60.0) && frames < 200 {
577 frames += 1;
578 }
579 frames
580 };
581 let (confirmed_frames, cancelled_frames) = (count(held), count(cancelled));
582 assert!(
583 confirmed_frames > cancelled_frames * 3,
584 "a cancel is 75 ms against 375: {cancelled_frames} frames against {confirmed_frames}"
585 );
586 }
587
588 #[test]
589 fn the_splash_drifts_to_the_middle_of_the_box() {
590 let mut ripples = pressed(Point::new(5.0, 5.0));
591 ripples.confirm();
592 for _ in 0..15 {
595 ripples.advance(1.0 / 60.0);
596 }
597 let (centre, _) = ripples.list[0].circle();
598 assert!(
599 (centre.x - 50.0).abs() < 0.5 && (centre.y - 30.0).abs() < 0.5,
600 "it ends up centred: {centre:?}"
601 );
602 }
603
604 #[test]
605 fn two_taps_splash_twice() {
606 let mut ripples = pressed(Point::new(10.0, 10.0));
607 ripples.confirm();
608 ripples.advance(1.0 / 60.0);
609 ripples.press(Point::new(90.0, 50.0), BOX);
610 assert_eq!(ripples.list.len(), 2, "the first fade is not interrupted");
611 ripples.confirm();
613 assert!(!ripples.list[1].held());
614 }
615
616 #[test]
617 fn a_press_on_an_empty_box_splashes_nothing() {
618 let mut ripples = Ripples::default();
619 ripples.press(Point::new(0.0, 0.0), Size::new(0.0, 0.0));
620 assert!(ripples.is_idle());
621 }
622
623 #[test]
624 fn the_ink_is_clipped_to_the_rounding_it_was_given() {
625 let mut ripples = pressed(Point::new(50.0, 30.0));
626 ripples.advance(1.0 / 60.0);
627 let mut scene = Scene::new();
628 let bounds = Rect::new(0.0, 0.0, 100.0, 60.0);
629 ripples.paint(
630 7,
631 bounds,
632 BorderRadius::uniform(12.0),
633 Color::WHITE,
634 &mut scene,
635 );
636 assert_eq!(scene.primitives().len(), 1, "one composited layer");
637 match &scene.primitives()[0] {
638 Primitive::Layer {
639 clip_shape,
640 clip,
641 owner,
642 ..
643 } => {
644 assert_eq!(*clip_shape, ClipShape::RRect(BorderRadius::uniform(12.0)));
645 assert_eq!(*clip, bounds, "ink never escapes its box");
646 assert_eq!(*owner, 7);
647 }
648 other => panic!("expected a layer, got {other:?}"),
649 }
650 }
651}