gpui_kit_material/tokens/
state.rs1use gpui::Rgba;
15
16pub const HOVER: f32 = 0.08;
17pub const FOCUS: f32 = 0.10;
18pub const PRESS: f32 = 0.10;
19pub const DRAG: f32 = 0.16;
20pub const DISABLED_CONTAINER: f32 = 0.12;
22pub const DISABLED_CONTENT: f32 = 0.38;
23pub const TEXT_SELECTION: f32 = 0.40;
26
27pub fn layer(container: Rgba, content: Rgba, opacity: f32) -> Rgba {
35 let top = content.a * opacity.clamp(0., 1.);
36 let a = top + container.a * (1. - top);
37 if a <= 0. {
38 return Rgba {
39 r: 0.,
40 g: 0.,
41 b: 0.,
42 a: 0.,
43 };
44 }
45 let c = |x: f32, y: f32| (x * top + y * container.a * (1. - top)) / a;
46 Rgba {
47 r: c(content.r, container.r),
48 g: c(content.g, container.g),
49 b: c(content.b, container.b),
50 a,
51 }
52}
53
54pub fn hovered(container: Rgba, content: Rgba) -> Rgba {
56 layer(container, content, HOVER)
57}
58
59pub fn pressed(container: Rgba, content: Rgba) -> Rgba {
61 layer(container, content, PRESS)
62}
63
64pub fn disabled(color: Rgba) -> Rgba {
68 Rgba {
69 a: color.a * DISABLED_CONTENT,
70 ..color
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 fn rgba(r: f32, g: f32, b: f32, a: f32) -> Rgba {
79 Rgba { r, g, b, a }
80 }
81
82 fn close(left: Rgba, right: Rgba) -> bool {
83 [
84 (left.r, right.r),
85 (left.g, right.g),
86 (left.b, right.b),
87 (left.a, right.a),
88 ]
89 .into_iter()
90 .all(|(x, y)| (x - y).abs() < 1e-6)
91 }
92
93 #[test]
94 fn layer_opacities_match_the_figma_kit() {
95 let fixture: serde_json::Value =
96 serde_json::from_str(include_str!("../../tests/fixtures/figma-sys-tokens.json"))
97 .unwrap();
98 let reference = &fixture["state_layer_opacity"];
99 let defined = reference["defined_opacities"].as_array().unwrap();
100 for (opacity, name) in [
101 (HOVER, "hover"),
102 (FOCUS, "focus"),
103 (PRESS, "pressed"),
104 (DRAG, "dragged"),
105 ] {
106 assert_eq!(opacity, reference[name].as_f64().unwrap() as f32, "{name}");
107 assert!(
108 defined
109 .iter()
110 .any(|v| v.as_f64().unwrap() as f32 == opacity),
111 "{name} is one of the kit's State Layers opacities"
112 );
113 }
114 assert_eq!(
115 DISABLED_CONTENT,
116 fixture["disabled"]["content_opacity"].as_f64().unwrap() as f32
117 );
118 }
119
120 #[test]
121 fn disabled_multiplies_alpha() {
122 assert_eq!(disabled(rgba(1., 0., 0., 1.)).a, DISABLED_CONTENT);
123 assert_eq!(disabled(rgba(1., 0., 0., 0.5)).a, 0.5 * DISABLED_CONTENT);
124 assert_eq!(disabled(rgba(1., 0., 0., 0.)).a, 0.);
125 }
126
127 #[test]
128 fn layer_over_an_opaque_container_is_a_linear_mix() {
129 let container = rgba(1., 1., 1., 1.);
130 let content = rgba(0., 0., 0., 1.);
131 assert!(close(
132 hovered(container, content),
133 rgba(0.92, 0.92, 0.92, 1.)
134 ));
135 assert!(close(pressed(container, content), rgba(0.9, 0.9, 0.9, 1.)));
136 }
137
138 #[test]
139 fn layer_over_a_transparent_container_keeps_its_hue() {
140 let content = rgba(0.2, 0.4, 0.6, 1.);
141 let result = layer(rgba(0., 0., 0., 0.), content, HOVER);
142 assert!(close(result, rgba(0.2, 0.4, 0.6, HOVER)));
143 assert_eq!(layer(rgba(0., 0., 0., 0.), content, 0.).a, 0.);
144 }
145
146 #[test]
147 fn layer_opacity_multiplies_content_alpha() {
148 let result = layer(rgba(0., 0., 0., 0.), rgba(1., 1., 1., 0.5), PRESS);
149 assert!((result.a - 0.05).abs() < 1e-6);
150 }
151}