1use crate::{Color, PaletteId};
7
8macro_rules! roles {
9 ($($(#[$doc:meta])* $name:ident),+ $(,)?) => {
10 #[allow(non_camel_case_types, clippy::upper_case_acronyms)]
13 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14 #[repr(usize)]
15 pub enum ColorRole { $($(#[$doc])* $name),+ }
16
17 impl ColorRole {
18 pub const ALL: &[ColorRole] = &[$(ColorRole::$name),+];
20 }
21 }
22}
23
24roles!(
25 ERROR,
26 ERROR_CONTAINER,
27 INFO,
28 INVERSE_ON_SURFACE,
29 INVERSE_PRIMARY,
30 INVERSE_SURFACE,
31 ON_ERROR,
32 ON_ERROR_CONTAINER,
33 ON_INFO,
34 ON_PRIMARY,
35 ON_PRIMARY_CONTAINER,
36 ON_PRIMARY_FIXED,
37 ON_PRIMARY_FIXED_VARIANT,
38 ON_SECONDARY,
39 ON_SECONDARY_CONTAINER,
40 ON_SECONDARY_FIXED,
41 ON_SECONDARY_FIXED_VARIANT,
42 ON_SUCCESS,
43 ON_SURFACE,
44 ON_SURFACE_VARIANT,
45 ON_TERTIARY,
46 ON_TERTIARY_CONTAINER,
47 ON_TERTIARY_FIXED,
48 ON_TERTIARY_FIXED_VARIANT,
49 ON_WARNING,
50 OUTLINE,
51 OUTLINE_VARIANT,
52 PRIMARY,
53 PRIMARY_CONTAINER,
54 PRIMARY_FIXED,
55 PRIMARY_FIXED_DIM,
56 SCRIM,
57 SECONDARY,
58 SECONDARY_CONTAINER,
59 SECONDARY_FIXED,
60 SECONDARY_FIXED_DIM,
61 SHADOW,
62 SUCCESS,
63 SURFACE,
64 SURFACE_BRIGHT,
65 SURFACE_CONTAINER,
66 SURFACE_CONTAINER_HIGH,
67 SURFACE_CONTAINER_HIGHEST,
68 SURFACE_CONTAINER_LOW,
69 SURFACE_CONTAINER_LOWEST,
70 SURFACE_DIM,
71 SURFACE_VARIANT,
72 TERTIARY,
73 TERTIARY_CONTAINER,
74 TERTIARY_FIXED,
75 TERTIARY_FIXED_DIM,
76 WARNING,
77 INSET,
78 FAINT,
79 CARD_LINE,
80 SUCCESS_BG,
81 WARNING_BG,
82 FIND_HIGHLIGHT,
83 FIND_CURRENT,
84 FIND_CURRENT_LINE,
85 SELECTION,
86 READING_SELECTION,
87);
88
89impl ColorRole {
92 #[deprecated(note = "use `ColorRole::SURFACE_CONTAINER`")]
94 pub const CANVAS: Self = Self::SURFACE_CONTAINER;
95 #[deprecated(note = "use `ColorRole::SURFACE_CONTAINER_HIGHEST`")]
99 pub const SURFACE_HIGH: Self = Self::SURFACE_CONTAINER_HIGHEST;
100 #[deprecated(note = "use `ColorRole::ON_SURFACE`")]
102 pub const INK: Self = Self::ON_SURFACE;
103 #[deprecated(note = "use `ColorRole::ON_SURFACE_VARIANT`")]
106 pub const GRAY: Self = Self::ON_SURFACE_VARIANT;
107 #[deprecated(note = "use `ColorRole::OUTLINE_VARIANT`")]
109 pub const HAIRLINE: Self = Self::OUTLINE_VARIANT;
110 #[deprecated(note = "use `ColorRole::OUTLINE`")]
113 pub const CONTROL: Self = Self::OUTLINE;
114 #[deprecated(note = "use `ColorRole::PRIMARY`")]
116 pub const ACCENT: Self = Self::PRIMARY;
117 #[deprecated(note = "use `ColorRole::ERROR`")]
119 pub const DANGER: Self = Self::ERROR;
120 #[deprecated(note = "use `ColorRole::ERROR_CONTAINER`")]
122 pub const DANGER_BG: Self = Self::ERROR_CONTAINER;
123}
124
125pub const ROLE_COUNT: usize = ColorRole::ALL.len();
127
128#[derive(Clone, Copy, Debug, PartialEq)]
130pub struct Scheme {
131 colors: [Color; ROLE_COUNT],
132 dark: bool,
133}
134
135impl Scheme {
136 pub fn with_role(mut self, role: ColorRole, color: Color) -> Self {
138 self.colors[role as usize] = color;
139 self
140 }
141
142 pub fn from_palette(id: PaletteId) -> Self {
144 let p = id.colors();
145 let dark = id.is_dark();
146 let hex = Color::hex;
147 let tint = |foreground: u32, amount| hex(p.surface).blend(hex(foreground), amount);
148 let canvas = hex(p.canvas).blend(hex(p.accent), if dark { 0.03 } else { 0.045 });
151 let colors = std::array::from_fn(|index| match ColorRole::ALL[index] {
152 ColorRole::SURFACE_CONTAINER => canvas,
153 ColorRole::SURFACE => {
157 if dark {
158 hex(p.surface).blend(hex(p.text), 0.045)
159 } else {
160 hex(p.surface)
161 }
162 }
163 ColorRole::SURFACE_CONTAINER_HIGHEST => {
167 if dark {
168 hex(p.surface).blend(hex(p.text), 0.13)
169 } else {
170 canvas.blend(hex(p.text), 0.06)
171 }
172 }
173 ColorRole::INSET => hex(p.inset),
174 ColorRole::ON_SURFACE => hex(p.text),
175 ColorRole::ON_SURFACE_VARIANT => hex(p.muted),
176 ColorRole::FAINT => hex(p.subtle),
177 ColorRole::OUTLINE_VARIANT => hex(p.border),
178 ColorRole::CARD_LINE => hex(p.surface).blend(hex(p.border), 0.55),
180 ColorRole::OUTLINE => hex(p.control),
181 ColorRole::PRIMARY => hex(p.accent),
182 ColorRole::ON_PRIMARY => hex(p.on_accent),
183 ColorRole::SECONDARY_CONTAINER => tint(p.accent, if dark { 0.26 } else { 0.24 }),
186 ColorRole::ON_SECONDARY_CONTAINER => {
187 hex(p.text).blend(hex(p.accent), if dark { 0.30 } else { 0.40 })
188 }
189 ColorRole::SUCCESS => hex(p.success),
190 ColorRole::SUCCESS_BG => tint(p.success, 0.12),
191 ColorRole::WARNING => hex(p.warning),
192 ColorRole::WARNING_BG => tint(p.warning, 0.12),
193 ColorRole::ERROR => hex(p.danger),
194 ColorRole::ERROR_CONTAINER => tint(p.danger, 0.12),
195 ColorRole::SELECTION => hex(p.selection),
196 ColorRole::READING_SELECTION => hex(p.accent).alpha(if dark { 0.42 } else { 0.30 }),
199 ColorRole::FIND_HIGHLIGHT => tint(p.warning, 0.16),
200 ColorRole::FIND_CURRENT => tint(p.warning, 0.32),
201 ColorRole::FIND_CURRENT_LINE => hex(p.warning),
202 ColorRole::PRIMARY_CONTAINER | ColorRole::TERTIARY_CONTAINER => tint(p.accent, 0.24),
205 ColorRole::ON_PRIMARY_CONTAINER | ColorRole::ON_TERTIARY_CONTAINER => hex(p.text),
206 ColorRole::SECONDARY | ColorRole::TERTIARY => hex(p.accent),
207 ColorRole::ON_SECONDARY | ColorRole::ON_TERTIARY => hex(p.on_accent),
208 ColorRole::PRIMARY_FIXED | ColorRole::SECONDARY_FIXED | ColorRole::TERTIARY_FIXED => {
209 hex(p.accent).blend(hex(0xffffff), 0.8)
210 }
211 ColorRole::PRIMARY_FIXED_DIM
212 | ColorRole::SECONDARY_FIXED_DIM
213 | ColorRole::TERTIARY_FIXED_DIM => hex(p.accent).blend(hex(0xffffff), 0.6),
214 ColorRole::ON_PRIMARY_FIXED
215 | ColorRole::ON_SECONDARY_FIXED
216 | ColorRole::ON_TERTIARY_FIXED => hex(p.accent).blend(hex(0), 0.8),
217 ColorRole::ON_PRIMARY_FIXED_VARIANT
218 | ColorRole::ON_SECONDARY_FIXED_VARIANT
219 | ColorRole::ON_TERTIARY_FIXED_VARIANT => hex(p.accent).blend(hex(0), 0.4),
220 ColorRole::SURFACE_DIM => hex(p.canvas),
221 ColorRole::SURFACE_BRIGHT => {
222 if dark {
223 hex(p.surface).blend(hex(p.text), 0.18)
224 } else {
225 hex(p.surface)
226 }
227 }
228 ColorRole::SURFACE_VARIANT | ColorRole::SURFACE_CONTAINER_HIGH => {
229 tint(p.text, if dark { 0.10 } else { 0.06 })
230 }
231 ColorRole::SURFACE_CONTAINER_LOWEST => {
232 if dark {
233 hex(p.canvas)
234 } else {
235 hex(p.surface)
236 }
237 }
238 ColorRole::SURFACE_CONTAINER_LOW => tint(p.text, 0.025),
239 ColorRole::INVERSE_SURFACE => hex(p.text),
240 ColorRole::INVERSE_ON_SURFACE => hex(p.surface),
241 ColorRole::INVERSE_PRIMARY => hex(p.accent).blend(hex(p.surface), 0.4),
242 ColorRole::ON_ERROR => hex(p.on_accent),
243 ColorRole::ON_ERROR_CONTAINER => hex(p.danger),
244 ColorRole::ON_SUCCESS | ColorRole::ON_WARNING => hex(p.on_accent),
245 ColorRole::INFO => hex(p.accent),
246 ColorRole::ON_INFO => hex(p.on_accent),
247 ColorRole::SHADOW | ColorRole::SCRIM => hex(0),
248 });
249 Self { colors, dark }
250 }
251
252 pub(crate) fn from_reference(colors: &[u32; 52], dark: bool) -> Self {
255 let mut scheme = Self {
256 colors: [Color::hex(0); ROLE_COUNT],
257 dark,
258 };
259 for (role, value) in crate::themes::REFERENCE_ROLES.iter().zip(colors) {
260 scheme.colors[*role as usize] = Color::hex(*value);
261 }
262 let surface = scheme.get(ColorRole::SURFACE);
263 let accent = scheme.get(ColorRole::PRIMARY);
264 let warning = scheme.get(ColorRole::WARNING);
265 scheme
266 .with_role(
267 ColorRole::INSET,
268 scheme.get(ColorRole::SURFACE_CONTAINER_LOWEST),
269 )
270 .with_role(ColorRole::FAINT, scheme.get(ColorRole::OUTLINE))
271 .with_role(ColorRole::CARD_LINE, scheme.get(ColorRole::OUTLINE_VARIANT))
272 .with_role(
273 ColorRole::SUCCESS_BG,
274 surface.blend(scheme.get(ColorRole::SUCCESS), 0.12),
275 )
276 .with_role(ColorRole::WARNING_BG, surface.blend(warning, 0.12))
277 .with_role(ColorRole::FIND_HIGHLIGHT, surface.blend(warning, 0.16))
278 .with_role(ColorRole::FIND_CURRENT, surface.blend(warning, 0.32))
279 .with_role(ColorRole::FIND_CURRENT_LINE, warning)
280 .with_role(
281 ColorRole::SELECTION,
282 scheme.get(ColorRole::PRIMARY_CONTAINER),
283 )
284 .with_role(
285 ColorRole::READING_SELECTION,
286 accent.alpha(if dark { 0.42 } else { 0.30 }),
287 )
288 }
289
290 pub fn get(&self, role: ColorRole) -> Color {
291 self.colors[role as usize]
292 }
293
294 pub fn is_dark(&self) -> bool {
296 self.dark
297 }
298
299 pub fn blend(&self, to: &Scheme, t: f32) -> Scheme {
302 Scheme {
303 colors: std::array::from_fn(|i| self.colors[i].blend(to.colors[i], t)),
304 dark: if t < 0.5 { self.dark } else { to.dark },
305 }
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312 use crate::palette::ALL;
313
314 #[test]
315 fn layers_stay_distinct_and_text_reads_against_surfaces() {
316 for id in ALL {
317 let scheme = Scheme::from_palette(id);
318 assert_eq!(scheme.is_dark(), id.is_dark());
319 let (canvas, surface, high) = (
320 scheme.get(ColorRole::SURFACE_CONTAINER),
321 scheme.get(ColorRole::SURFACE),
322 scheme.get(ColorRole::SURFACE_CONTAINER_HIGHEST),
323 );
324 assert_ne!(canvas, surface, "{}", id.name());
325 assert_ne!(surface, high, "{}", id.name());
326 let ink = scheme.get(ColorRole::ON_SURFACE).luminance();
327 assert_eq!(
328 ink > surface.luminance(),
329 id.is_dark(),
330 "{}: dark palettes have light text",
331 id.name()
332 );
333 }
334 }
335
336 #[test]
337 fn blend_reaches_its_target() {
338 let paper = Scheme::from_palette(PaletteId::Paper);
339 let ink = Scheme::from_palette(PaletteId::Ink);
340 assert_eq!(paper.blend(&ink, 0.), paper);
341 assert_eq!(paper.blend(&ink, 1.), ink);
342 assert!(paper.blend(&ink, 0.6).is_dark());
343 }
344}
345
346#[cfg(test)]
347mod custom_scheme_tests {
348 use super::*;
349 #[test]
350 fn overriding_one_role_preserves_other_roles_and_appearance() {
351 let before = Scheme::from_palette(PaletteId::Paper);
352 let after = before.with_role(ColorRole::PRIMARY, Color::hex(0x0b57d0));
353 assert_eq!(after.get(ColorRole::PRIMARY), Color::hex(0x0b57d0));
354 for role in ColorRole::ALL.iter().filter(|r| **r != ColorRole::PRIMARY) {
355 assert_eq!(before.get(*role), after.get(*role));
356 }
357 assert_eq!(before.is_dark(), after.is_dark());
358 }
359}