gpui_kit_material/tokens/
elevation.rs1use crate::color::ColorRole;
21use crate::theme::color;
22use gpui::{BoxShadow, Hsla, px};
23
24pub const MAX_LEVEL: u8 = 5;
26
27const KEY_OPACITY: f32 = 0.3;
28const AMBIENT_OPACITY: f32 = 0.15;
29
30const GEOMETRY: [[(f32, f32, f32); 2]; MAX_LEVEL as usize] = [
32 [(1., 2., 0.), (1., 3., 1.)],
33 [(1., 2., 0.), (2., 6., 2.)],
34 [(1., 3., 0.), (4., 8., 3.)],
35 [(2., 3., 0.), (6., 10., 4.)],
36 [(4., 4., 0.), (8., 12., 6.)],
37];
38
39fn shadow_color(opacity: f32) -> Hsla {
40 let mut shadow = color(ColorRole::SHADOW);
41 shadow.a *= opacity;
42 shadow.into()
43}
44
45fn layer(y: f32, blur: f32, spread: f32, opacity: f32) -> BoxShadow {
46 BoxShadow::new(px(0.), px(y), shadow_color(opacity))
47 .blur_radius(px(blur))
48 .spread_radius(px(spread))
49}
50
51pub fn layers(level: u8) -> [BoxShadow; 2] {
57 match level.min(MAX_LEVEL) {
58 0 => std::array::from_fn(|_| layer(0., 0., 0., 0.)),
59 level => {
60 let [key, ambient] = GEOMETRY[level as usize - 1];
61 [
62 layer(key.0, key.1, key.2, KEY_OPACITY),
63 layer(ambient.0, ambient.1, ambient.2, AMBIENT_OPACITY),
64 ]
65 }
66 }
67}
68
69pub fn level(level: u8) -> Vec<BoxShadow> {
71 if level == 0 {
72 Vec::new()
73 } else {
74 layers(level).into()
75 }
76}
77
78pub fn level0() -> Vec<BoxShadow> {
80 level(0)
81}
82
83pub fn level1() -> Vec<BoxShadow> {
85 level(1)
86}
87
88pub fn level2() -> Vec<BoxShadow> {
90 level(2)
91}
92
93pub fn level3() -> Vec<BoxShadow> {
95 level(3)
96}
97
98pub fn level4() -> Vec<BoxShadow> {
100 level(4)
101}
102
103pub fn level5() -> Vec<BoxShadow> {
105 level(5)
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 fn assert_layer(shadow: &BoxShadow, y: f32, blur: f32, spread: f32, opacity: f32) {
113 let mut expected = color(ColorRole::SHADOW);
114 expected.a *= opacity;
115 assert_eq!(shadow.offset.x, px(0.));
116 assert_eq!(shadow.offset.y, px(y));
117 assert_eq!(shadow.blur_radius, px(blur));
118 assert_eq!(shadow.spread_radius, px(spread));
119 assert_eq!(shadow.color, Hsla::from(expected));
120 assert!(!shadow.inset);
121 }
122
123 #[test]
124 fn levels_match_the_m3_two_layer_shadows() {
125 assert!(level0().is_empty());
126 let expected = [
127 [(1., 2., 0.), (1., 3., 1.)],
128 [(1., 2., 0.), (2., 6., 2.)],
129 [(1., 3., 0.), (4., 8., 3.)],
130 [(2., 3., 0.), (6., 10., 4.)],
131 [(4., 4., 0.), (8., 12., 6.)],
132 ];
133 let levels = [level1(), level2(), level3(), level4(), level5()];
134 for (shadows, [key, ambient]) in levels.iter().zip(expected) {
135 assert_eq!(shadows.len(), 2);
136 assert_layer(&shadows[0], key.0, key.1, key.2, 0.3);
137 assert_layer(&shadows[1], ambient.0, ambient.1, ambient.2, 0.15);
138 }
139 assert_eq!(level(9), level5(), "levels above 5 clamp");
140 }
141
142 #[test]
143 fn levels_match_the_figma_kit_effect_styles() {
144 let fixture: serde_json::Value =
145 serde_json::from_str(include_str!("../../tests/fixtures/figma-sys-tokens.json"))
146 .unwrap();
147 for theme in ["light", "dark"] {
148 for level in 1..=MAX_LEVEL {
149 let style = &fixture["elevation"][theme][level.to_string()];
150 let mut reference = style["layers"].as_array().unwrap().clone();
151 reference.sort_by(|a, b| {
153 b["opacity"]
154 .as_f64()
155 .unwrap()
156 .total_cmp(&a["opacity"].as_f64().unwrap())
157 });
158 let shadows = super::level(level);
159 assert_eq!(shadows.len(), reference.len(), "{theme} {level}");
160 for (shadow, layer) in shadows.iter().zip(&reference) {
161 let value = |key: &str| layer[key].as_f64().unwrap() as f32;
162 assert_eq!(value("x"), 0.);
163 assert_eq!(layer["color"], "#000000", "the shadow role");
164 assert_layer(
165 shadow,
166 value("y"),
167 value("blur"),
168 value("spread"),
169 value("opacity"),
170 );
171 }
172 }
173 }
174 }
175
176 #[test]
177 fn level_zero_pads_two_transparent_entries() {
178 for shadow in layers(0) {
179 assert_layer(&shadow, 0., 0., 0., 0.);
180 }
181 }
182}