1use crate::contrast::{DIFF_ROLES, MIN_SEPARATION, SYNTAX_ROLES, apart, contrast};
19use crate::schema::{COLOR_ROLE_COUNT, Color, ColorRole, ColorScheme, DesignSystem};
20use crate::tokens::table;
21
22pub const PALETTE_ENTRY_LEN: usize = 8;
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub struct Palette {
44 light: [Option<Color>; COLOR_ROLE_COUNT],
45 dark: [Option<Color>; COLOR_ROLE_COUNT],
46}
47
48impl Default for Palette {
49 fn default() -> Self {
50 Self::new()
51 }
52}
53
54#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub struct PaletteEntry {
57 pub role: ColorRole,
58 pub scheme: ColorScheme,
60 pub color: Color,
61}
62
63impl Palette {
64 pub const fn new() -> Self {
66 Self {
67 light: [None; COLOR_ROLE_COUNT],
68 dark: [None; COLOR_ROLE_COUNT],
69 }
70 }
71
72 pub const fn with(mut self, role: ColorRole, light: Color, dark: Color) -> Self {
77 self.light[role as usize - 1] = Some(light);
78 self.dark[role as usize - 1] = Some(dark);
79 self
80 }
81
82 pub const fn with_light(mut self, role: ColorRole, light: Color) -> Self {
84 self.light[role as usize - 1] = Some(light);
85 self
86 }
87
88 pub const fn with_dark(mut self, role: ColorRole, dark: Color) -> Self {
90 self.dark[role as usize - 1] = Some(dark);
91 self
92 }
93
94 pub const fn get(&self, role: ColorRole, scheme: ColorScheme) -> Option<Color> {
97 match scheme {
98 ColorScheme::Light => self.light[role as usize - 1],
99 ColorScheme::Dark => self.dark[role as usize - 1],
100 ColorScheme::FollowSystem => None,
101 }
102 }
103
104 pub fn entries(&self) -> impl Iterator<Item = PaletteEntry> + '_ {
106 crate::schema::COLOR_ROLE_SCHEMA
107 .iter()
108 .flat_map(move |variant| {
109 let role = ColorRole::try_from(variant.tag).expect("a role from the role schema");
110 [ColorScheme::Light, ColorScheme::Dark]
111 .into_iter()
112 .filter_map(move |scheme| {
113 self.get(role, scheme).map(|color| PaletteEntry {
114 role,
115 scheme,
116 color,
117 })
118 })
119 })
120 }
121
122 pub fn len(&self) -> usize {
124 self.light.iter().chain(self.dark.iter()).flatten().count()
125 }
126
127 pub fn is_empty(&self) -> bool {
128 self.len() == 0
129 }
130
131 pub fn write_into(&self, out: &mut Vec<u8>) {
133 for entry in self.entries() {
134 out.extend_from_slice(&(entry.role as u16).to_le_bytes());
135 out.extend_from_slice(&(entry.scheme as u16).to_le_bytes());
136 out.extend_from_slice(&entry.color.to_argb().to_le_bytes());
137 }
138 }
139
140 pub fn resolve(&self, system: DesignSystem, scheme: ColorScheme) -> [Color; COLOR_ROLE_COUNT] {
152 let base = table(system);
153 let mut out = [Color::default(); COLOR_ROLE_COUNT];
154 for (index, slot) in out.iter_mut().enumerate() {
155 let role = base.colors[index].role;
156 *slot = self
157 .get(role, scheme)
158 .unwrap_or_else(|| base.color(role, scheme));
159 }
160 for (fill, ink, required) in INK_PAIRS {
161 if self.get(fill, scheme).is_none() || self.get(ink, scheme).is_some() {
162 continue;
163 }
164 out[ink as usize - 1] =
165 ink_for(out[fill as usize - 1], out[ink as usize - 1], required);
166 }
167 out
168 }
169
170 pub fn check(&self, system: DesignSystem) -> Vec<PaletteViolation> {
180 let mut found = Vec::new();
181 for scheme in [ColorScheme::Light, ColorScheme::Dark] {
182 let colors = self.resolve(system, scheme);
183 let color = |role: ColorRole| colors[role as usize - 1];
184 let mut contrast_pair = |ink: ColorRole, fill: ColorRole, required: f64| {
185 let ratio = contrast(color(ink), color(fill));
186 if ratio < required {
187 found.push(PaletteViolation::Contrast {
188 scheme,
189 ink,
190 fill,
191 ratio,
192 required,
193 });
194 }
195 };
196 for (fill, ink, required) in INK_PAIRS {
197 contrast_pair(ink, fill, required);
198 }
199 contrast_pair(ColorRole::OnSurface, ColorRole::SurfaceContainer, 4.5);
200 for ink in SYNTAX_ROLES {
201 contrast_pair(ink, ColorRole::SurfaceContainer, 4.5);
202 }
203 for (ink, line, _) in DIFF_ROLES {
204 contrast_pair(ink, line, 4.5);
205 contrast_pair(ColorRole::OnSurface, line, 4.5);
206 }
207 let mut apart_pair = |first: ColorRole, second: ColorRole| {
208 let distance = apart(color(first), color(second));
209 if distance < MIN_SEPARATION {
210 found.push(PaletteViolation::Indistinct {
211 scheme,
212 first,
213 second,
214 distance,
215 });
216 }
217 };
218 for container in [
219 ColorRole::SurfaceContainer,
220 ColorRole::PrimaryContainer,
221 ColorRole::SecondaryContainer,
222 ColorRole::TertiaryContainer,
223 ] {
224 apart_pair(container, ColorRole::Background);
225 }
226 for (_, line, word) in DIFF_ROLES {
227 apart_pair(word, line);
228 }
229 }
230 found
231 }
232}
233
234#[derive(Clone, Copy, Debug, PartialEq)]
236pub enum PaletteViolation {
237 Contrast {
239 scheme: ColorScheme,
240 ink: ColorRole,
241 fill: ColorRole,
242 ratio: f64,
243 required: f64,
244 },
245 Indistinct {
248 scheme: ColorScheme,
249 first: ColorRole,
250 second: ColorRole,
251 distance: u32,
252 },
253}
254
255impl std::fmt::Display for PaletteViolation {
256 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257 match self {
258 Self::Contrast {
259 scheme,
260 ink,
261 fill,
262 ratio,
263 required,
264 } => write!(
265 formatter,
266 "{scheme:?}: {ink:?} on {fill:?} is {ratio:.2}:1, under the {required}:1 it needs to be read"
267 ),
268 Self::Indistinct {
269 scheme,
270 first,
271 second,
272 distance,
273 } => write!(
274 formatter,
275 "{scheme:?}: {first:?} and {second:?} are {distance} apart, so one cannot be seen on the other"
276 ),
277 }
278 }
279}
280
281pub const INK_PAIRS: [(ColorRole, ColorRole, f64); 10] = [
286 (ColorRole::Primary, ColorRole::OnPrimary, 3.0),
287 (ColorRole::Secondary, ColorRole::OnSecondary, 3.0),
288 (ColorRole::Tertiary, ColorRole::OnTertiary, 3.0),
289 (ColorRole::Error, ColorRole::OnError, 3.0),
290 (
291 ColorRole::PrimaryContainer,
292 ColorRole::OnPrimaryContainer,
293 4.5,
294 ),
295 (
296 ColorRole::SecondaryContainer,
297 ColorRole::OnSecondaryContainer,
298 4.5,
299 ),
300 (
301 ColorRole::TertiaryContainer,
302 ColorRole::OnTertiaryContainer,
303 4.5,
304 ),
305 (ColorRole::Surface, ColorRole::OnSurface, 4.5),
306 (ColorRole::SurfaceVariant, ColorRole::OnSurfaceVariant, 4.5),
307 (ColorRole::Background, ColorRole::OnBackground, 4.5),
308];
309
310pub fn ink_for(fill: Color, system_ink: Color, required: f64) -> Color {
312 if contrast(system_ink, fill) >= required {
313 return system_ink;
314 }
315 let white = Color::rgb(0xffffff);
316 let black = Color::rgb(0x000000);
317 if contrast(white, fill) >= contrast(black, fill) {
318 white
319 } else {
320 black
321 }
322}
323
324#[derive(Clone, Copy, Debug, Eq, PartialEq)]
330pub enum PaletteProblem {
331 Length {
333 len: usize,
334 },
335 UnknownRole {
336 index: usize,
337 tag: u16,
338 },
339 InvalidScheme {
341 index: usize,
342 scheme: u16,
343 },
344 Duplicate {
346 index: usize,
347 role: ColorRole,
348 scheme: ColorScheme,
349 },
350}
351
352pub fn decode_palette(bytes: &[u8]) -> (Palette, Vec<PaletteProblem>) {
354 let mut palette = Palette::new();
355 let mut problems = Vec::new();
356 if bytes.len() % PALETTE_ENTRY_LEN != 0 {
357 problems.push(PaletteProblem::Length { len: bytes.len() });
358 }
359 for (index, entry) in bytes.chunks_exact(PALETTE_ENTRY_LEN).enumerate() {
360 let tag = u16::from_le_bytes([entry[0], entry[1]]);
361 let scheme_tag = u16::from_le_bytes([entry[2], entry[3]]);
362 let argb = u32::from_le_bytes([entry[4], entry[5], entry[6], entry[7]]);
363 let Ok(role) = ColorRole::try_from(tag) else {
364 problems.push(PaletteProblem::UnknownRole { index, tag });
365 continue;
366 };
367 let scheme = match ColorScheme::try_from(scheme_tag) {
368 Ok(scheme @ (ColorScheme::Light | ColorScheme::Dark)) => scheme,
369 _ => {
370 problems.push(PaletteProblem::InvalidScheme {
371 index,
372 scheme: scheme_tag,
373 });
374 continue;
375 }
376 };
377 if palette.get(role, scheme).is_some() {
378 problems.push(PaletteProblem::Duplicate {
379 index,
380 role,
381 scheme,
382 });
383 continue;
384 }
385 let color = Color::argb(argb);
386 palette = match scheme {
387 ColorScheme::Light => palette.with_light(role, color),
388 _ => palette.with_dark(role, color),
389 };
390 }
391 (palette, problems)
392}