tessera_ui_basic_components/
material_color.rs1use std::sync::OnceLock;
5
6use material_color_utilities::{
7 dynamiccolor::{DynamicSchemeBuilder, MaterialDynamicColors, SpecVersion, Variant},
8 hct::Hct,
9};
10use parking_lot::RwLock;
11use tessera_ui::Color;
12
13const DEFAULT_COLOR: Color = Color::from_rgb(0.4039, 0.3137, 0.6431); static GLOBAL_SCHEME: OnceLock<RwLock<MaterialColorScheme>> = OnceLock::new();
16
17pub fn global_material_scheme() -> MaterialColorScheme {
22 GLOBAL_SCHEME
23 .get_or_init(|| RwLock::new(MaterialColorScheme::light_from_seed(DEFAULT_COLOR)))
24 .read()
25 .clone()
26}
27
28pub fn set_global_material_scheme(seed: Color, is_dark: bool) {
32 let scheme = if is_dark {
33 MaterialColorScheme::dark_from_seed(seed)
34 } else {
35 MaterialColorScheme::light_from_seed(seed)
36 };
37
38 GLOBAL_SCHEME
39 .get_or_init(|| RwLock::new(scheme.clone()))
40 .write()
41 .clone_from(&scheme);
42}
43
44#[derive(Clone, Debug)]
47pub struct MaterialColorScheme {
48 pub is_dark: bool,
50 pub primary: Color,
52 pub on_primary: Color,
54 pub primary_container: Color,
56 pub on_primary_container: Color,
58 pub secondary: Color,
60 pub on_secondary: Color,
62 pub secondary_container: Color,
64 pub on_secondary_container: Color,
66 pub tertiary: Color,
68 pub on_tertiary: Color,
70 pub tertiary_container: Color,
72 pub on_tertiary_container: Color,
74 pub error: Color,
76 pub on_error: Color,
78 pub error_container: Color,
80 pub on_error_container: Color,
82 pub background: Color,
84 pub on_background: Color,
86 pub surface: Color,
88 pub on_surface: Color,
90 pub surface_variant: Color,
92 pub on_surface_variant: Color,
94 pub outline: Color,
96 pub outline_variant: Color,
98 pub shadow: Color,
100 pub scrim: Color,
102 pub inverse_surface: Color,
104 pub inverse_on_surface: Color,
106 pub inverse_primary: Color,
108 pub surface_container: Color,
110 pub surface_container_high: Color,
112 pub surface_container_highest: Color,
114 pub surface_container_low: Color,
116 pub surface_container_lowest: Color,
118}
119
120impl MaterialColorScheme {
121 pub fn light_from_seed(seed: Color) -> Self {
123 scheme_from_seed(seed, false)
124 }
125
126 pub fn dark_from_seed(seed: Color) -> Self {
128 scheme_from_seed(seed, true)
129 }
130}
131
132fn scheme_from_seed(seed: Color, is_dark: bool) -> MaterialColorScheme {
133 let scheme = DynamicSchemeBuilder::default()
134 .source_color_hct(Hct::from_int(color_to_argb(seed)))
135 .variant(Variant::TonalSpot)
136 .spec_version(SpecVersion::Spec2025)
137 .is_dark(is_dark)
138 .build();
139 let dynamic_colors = MaterialDynamicColors::new();
140
141 MaterialColorScheme {
142 is_dark,
143 primary: argb_to_color(dynamic_colors.primary().get_argb(&scheme)),
144 on_primary: argb_to_color(dynamic_colors.on_primary().get_argb(&scheme)),
145 primary_container: argb_to_color(dynamic_colors.primary_container().get_argb(&scheme)),
146 on_primary_container: argb_to_color(
147 dynamic_colors.on_primary_container().get_argb(&scheme),
148 ),
149 secondary: argb_to_color(dynamic_colors.secondary().get_argb(&scheme)),
150 on_secondary: argb_to_color(dynamic_colors.on_secondary().get_argb(&scheme)),
151 secondary_container: argb_to_color(dynamic_colors.secondary_container().get_argb(&scheme)),
152 on_secondary_container: argb_to_color(
153 dynamic_colors.on_secondary_container().get_argb(&scheme),
154 ),
155 tertiary: argb_to_color(dynamic_colors.tertiary().get_argb(&scheme)),
156 on_tertiary: argb_to_color(dynamic_colors.on_tertiary().get_argb(&scheme)),
157 tertiary_container: argb_to_color(dynamic_colors.tertiary_container().get_argb(&scheme)),
158 on_tertiary_container: argb_to_color(
159 dynamic_colors.on_tertiary_container().get_argb(&scheme),
160 ),
161 error: argb_to_color(dynamic_colors.error().get_argb(&scheme)),
162 on_error: argb_to_color(dynamic_colors.on_error().get_argb(&scheme)),
163 error_container: argb_to_color(dynamic_colors.error_container().get_argb(&scheme)),
164 on_error_container: argb_to_color(dynamic_colors.on_error_container().get_argb(&scheme)),
165 background: argb_to_color(dynamic_colors.background().get_argb(&scheme)),
166 on_background: argb_to_color(dynamic_colors.on_background().get_argb(&scheme)),
167 surface: argb_to_color(dynamic_colors.surface().get_argb(&scheme)),
168 on_surface: argb_to_color(dynamic_colors.on_surface().get_argb(&scheme)),
169 surface_variant: argb_to_color(dynamic_colors.surface_variant().get_argb(&scheme)),
170 on_surface_variant: argb_to_color(dynamic_colors.on_surface_variant().get_argb(&scheme)),
171 outline: argb_to_color(dynamic_colors.outline().get_argb(&scheme)),
172 outline_variant: argb_to_color(dynamic_colors.outline_variant().get_argb(&scheme)),
173 shadow: argb_to_color(dynamic_colors.shadow().get_argb(&scheme)),
174 scrim: argb_to_color(dynamic_colors.scrim().get_argb(&scheme)),
175 inverse_surface: argb_to_color(dynamic_colors.inverse_surface().get_argb(&scheme)),
176 inverse_on_surface: argb_to_color(dynamic_colors.inverse_on_surface().get_argb(&scheme)),
177 inverse_primary: argb_to_color(dynamic_colors.inverse_primary().get_argb(&scheme)),
178 surface_container: argb_to_color(dynamic_colors.surface_container().get_argb(&scheme)),
179 surface_container_high: argb_to_color(
180 dynamic_colors.surface_container_high().get_argb(&scheme),
181 ),
182 surface_container_highest: argb_to_color(
183 dynamic_colors.surface_container_highest().get_argb(&scheme),
184 ),
185 surface_container_low: argb_to_color(
186 dynamic_colors.surface_container_low().get_argb(&scheme),
187 ),
188 surface_container_lowest: argb_to_color(
189 dynamic_colors.surface_container_lowest().get_argb(&scheme),
190 ),
191 }
192}
193
194pub fn blend_over(base: Color, overlay: Color, overlay_alpha: f32) -> Color {
199 let alpha = overlay_alpha.clamp(0.0, 1.0);
200 let r = overlay.r * alpha + base.r * (1.0 - alpha);
201 let g = overlay.g * alpha + base.g * (1.0 - alpha);
202 let b = overlay.b * alpha + base.b * (1.0 - alpha);
203 let a = overlay.a * alpha + base.a * (1.0 - alpha);
204 Color::new(r, g, b, a)
205}
206
207fn linear_to_srgb_channel(v: f32) -> f32 {
208 let v = v.clamp(0.0, 1.0);
209 if v <= 0.003_130_8 {
210 v * 12.92
211 } else {
212 1.055 * v.powf(1.0 / 2.4) - 0.055
213 }
214}
215
216fn srgb_to_linear_channel(v: f32) -> f32 {
217 let v = v.clamp(0.0, 1.0);
218 if v <= 0.04045 {
219 v / 12.92
220 } else {
221 ((v + 0.055) / 1.055).powf(2.4)
222 }
223}
224
225fn color_to_argb(color: Color) -> u32 {
226 let r = (linear_to_srgb_channel(color.r) * 255.0 + 0.5) as u32;
227 let g = (linear_to_srgb_channel(color.g) * 255.0 + 0.5) as u32;
228 let b = (linear_to_srgb_channel(color.b) * 255.0 + 0.5) as u32;
229 let a = (color.a.clamp(0.0, 1.0) * 255.0 + 0.5) as u32;
230 (a << 24) | (r << 16) | (g << 8) | b
231}
232
233fn argb_to_color(argb: u32) -> Color {
234 let a = ((argb >> 24) & 0xFF) as f32 / 255.0;
235 let r_srgb = ((argb >> 16) & 0xFF) as f32 / 255.0;
236 let g_srgb = ((argb >> 8) & 0xFF) as f32 / 255.0;
237 let b_srgb = (argb & 0xFF) as f32 / 255.0;
238 Color::new(
239 srgb_to_linear_channel(r_srgb),
240 srgb_to_linear_channel(g_srgb),
241 srgb_to_linear_channel(b_srgb),
242 a,
243 )
244}