tessera_ui_basic_components/
material_color.rs

1//! Material Design color utilities for HCT and dynamic scheme generation.
2//! ## Usage Generate Material-compliant dynamic palettes for consistent UI theming.
3
4use 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); // #6750A4
14
15static GLOBAL_SCHEME: OnceLock<RwLock<MaterialColorScheme>> = OnceLock::new();
16
17/// Returns the global Material Design 3 color scheme.
18///
19/// If no scheme has been set, it initializes a default light scheme
20/// with a seed color of #6750A4.
21pub 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
28/// Sets the global Material Design 3 color scheme.
29///
30/// The scheme is generated based on the provided `seed` color and `is_dark` flag.
31pub 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/// A Material Design color scheme, which can be light or dark,
45/// produced from a seed color.
46#[derive(Clone, Debug)]
47pub struct MaterialColorScheme {
48    /// Indicates if the scheme is dark mode (`true`) or light mode (`false`).
49    pub is_dark: bool,
50    /// The primary color of the scheme.
51    pub primary: Color,
52    /// Color used for content on top of `primary`.
53    pub on_primary: Color,
54    /// A container color for `primary`.
55    pub primary_container: Color,
56    /// Color used for content on top of `primary_container`.
57    pub on_primary_container: Color,
58    /// The secondary color of the scheme.
59    pub secondary: Color,
60    /// Color used for content on top of `secondary`.
61    pub on_secondary: Color,
62    /// A container color for `secondary`.
63    pub secondary_container: Color,
64    /// Color used for content on top of `secondary_container`.
65    pub on_secondary_container: Color,
66    /// The tertiary color of the scheme.
67    pub tertiary: Color,
68    /// Color used for content on top of `tertiary`.
69    pub on_tertiary: Color,
70    /// A container color for `tertiary`.
71    pub tertiary_container: Color,
72    /// Color used for content on top of `tertiary_container`.
73    pub on_tertiary_container: Color,
74    /// The error color of the scheme.
75    pub error: Color,
76    /// Color used for content on top of `error`.
77    pub on_error: Color,
78    /// A container color for `error`.
79    pub error_container: Color,
80    /// Color used for content on top of `error_container`.
81    pub on_error_container: Color,
82    /// The background color of the scheme.
83    pub background: Color,
84    /// Color used for content on top of `background`.
85    pub on_background: Color,
86    /// The surface color of the scheme.
87    pub surface: Color,
88    /// Color used for content on top of `surface`.
89    pub on_surface: Color,
90    /// A variant of the surface color.
91    pub surface_variant: Color,
92    /// Color used for content on top of `surface_variant`.
93    pub on_surface_variant: Color,
94    /// The outline color.
95    pub outline: Color,
96    /// A variant of the outline color.
97    pub outline_variant: Color,
98    /// The shadow color.
99    pub shadow: Color,
100    /// The scrim color.
101    pub scrim: Color,
102    /// An inverse of the surface color.
103    pub inverse_surface: Color,
104    /// Color used for content on top of `inverse_surface`.
105    pub inverse_on_surface: Color,
106    /// An inverse of the primary color.
107    pub inverse_primary: Color,
108    /// A container color for surfaces.
109    pub surface_container: Color,
110    /// A high container color for surfaces.
111    pub surface_container_high: Color,
112    /// A low container color for surfaces.
113    pub surface_container_highest: Color,
114    /// A low container color for surfaces.
115    pub surface_container_low: Color,
116    /// A lowest container color for surfaces.
117    pub surface_container_lowest: Color,
118}
119
120impl MaterialColorScheme {
121    /// Generates a light color scheme derived from the provided seed color.
122    pub fn light_from_seed(seed: Color) -> Self {
123        scheme_from_seed(seed, false)
124    }
125
126    /// Generates a dark color scheme derived from the provided seed color.
127    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
194/// Blends two colors, `overlay` drawn over `base`, using the provided `overlay_alpha`.
195///
196/// The `overlay_alpha` parameter controls the opacity of the `overlay` color,
197/// ranging from 0.0 (fully transparent) to 1.0 (fully opaque).
198pub 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}