1use tessera_ui::{Color, Dp};
8
9use crate::theme::MaterialColorScheme;
10
11#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct ShadowLayer {
14 pub color: Color,
16 pub offset: [f32; 2],
18 pub smoothness: f32,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Default)]
24pub struct ShadowLayers {
25 pub ambient: Option<ShadowLayer>,
27 pub spot: Option<ShadowLayer>,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq)]
32struct ElevationGeometry {
33 key_y: f64,
34 key_blur: f64,
35 ambient_y: f64,
36 ambient_blur: f64,
37 spread: f64,
38}
39
40fn elevation_geometry(elevation: Dp) -> ElevationGeometry {
41 const LEVELS: [(f64, [f64; 5]); 6] = [
42 (0.0, [0.0, 0.0, 0.0, 0.0, 0.0]),
43 (1.0, [1.0, 2.0, 1.0, 3.0, 1.0]),
44 (3.0, [1.0, 2.0, 2.0, 6.0, 2.0]),
45 (6.0, [1.0, 3.0, 4.0, 8.0, 3.0]),
46 (8.0, [2.0, 3.0, 6.0, 10.0, 4.0]),
47 (12.0, [4.0, 4.0, 8.0, 12.0, 6.0]),
48 ];
49 let elevation = if elevation.0.is_nan() {
50 0.0
51 } else {
52 elevation.0.clamp(0.0, 12.0)
53 };
54 let mut values = LEVELS[5].1;
55 for pair in LEVELS.windows(2) {
56 let [(start, from), (end, to)] = pair else {
57 unreachable!()
58 };
59 if elevation <= *end {
60 let fraction = (elevation - start) / (end - start);
61 values =
62 std::array::from_fn(|index| from[index] + (to[index] - from[index]) * fraction);
63 break;
64 }
65 }
66 ElevationGeometry {
67 key_y: values[0],
68 key_blur: values[1],
69 ambient_y: values[2],
70 ambient_blur: values[3],
71 spread: values[4],
72 }
73}
74
75pub(crate) fn material_shadow(elevation: Dp, scheme: &MaterialColorScheme) -> (ShadowLayers, f32) {
76 let geometry = elevation_geometry(elevation);
77 if geometry.key_blur == 0.0 {
78 return (ShadowLayers::default(), 0.0);
79 }
80 let layer = |y: f64, blur: f64, alpha| ShadowLayer {
82 color: scheme.shadow.with_alpha(alpha),
83 offset: [0.0, Dp(y).to_pixels_f32()],
84 smoothness: Dp(blur * 1.5).to_pixels_f32(),
85 };
86 (
87 ShadowLayers {
88 ambient: Some(layer(geometry.ambient_y, geometry.ambient_blur, 0.15)),
89 spot: Some(layer(geometry.key_y, geometry.key_blur, 0.3)),
90 },
91 Dp(geometry.spread).to_pixels_f32(),
92 )
93}
94
95#[cfg(test)]
96mod tests {
97 use tessera_ui::Dp;
98
99 use crate::theme::MaterialColorScheme;
100
101 use super::{elevation_geometry, material_shadow};
102
103 #[test]
104 fn menu_elevation_converts_blur_and_spread_without_losing_density() {
105 let (layers, spread) = material_shadow(Dp(3.0), &MaterialColorScheme::default());
106 let pixel = Dp(1.0).to_pixels_f32();
107 let key = layers.spot.expect("key shadow");
108 let ambient = layers.ambient.expect("ambient shadow");
109 assert_eq!(key.offset, [0.0, pixel]);
110 assert_eq!(key.smoothness, 3.0 * pixel);
111 assert_eq!(key.color.a, 0.3);
112 assert_eq!(ambient.offset, [0.0, 2.0 * pixel]);
113 assert_eq!(ambient.smoothness, 9.0 * pixel);
114 assert_eq!(ambient.color.a, 0.15);
115 assert_eq!(spread, 2.0 * pixel);
116 let (zero, spread) = material_shadow(Dp::ZERO, &MaterialColorScheme::default());
117 assert!(zero.ambient.is_none() && zero.spot.is_none());
118 assert_eq!(spread, 0.0);
119 }
120
121 #[test]
122 fn elevation_interpolates_and_clamps_inputs() {
123 let midpoint = elevation_geometry(Dp(2.0));
124 assert_eq!(midpoint.ambient_y, 1.5);
125 assert_eq!(midpoint.ambient_blur, 4.5);
126 assert_eq!(midpoint.spread, 1.5);
127 assert_eq!(elevation_geometry(Dp(-1.0)), elevation_geometry(Dp::ZERO));
128 assert_eq!(
129 elevation_geometry(Dp(f64::NAN)),
130 elevation_geometry(Dp::ZERO)
131 );
132 assert_eq!(
133 elevation_geometry(Dp(f64::INFINITY)),
134 elevation_geometry(Dp(12.0))
135 );
136 }
137}