1#![deny(
5 clippy::unwrap_used,
6 clippy::expect_used,
7 clippy::panic,
8 clippy::unimplemented,
9 clippy::todo,
10 clippy::unreachable
11)]
12#![forbid(unsafe_code)]
13
14use serde::{Deserialize, Serialize};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub struct Rgba {
19 pub r: u8,
20 pub g: u8,
21 pub b: u8,
22 pub a: u8,
23}
24
25impl Default for Rgba {
26 fn default() -> Self {
27 Rgba::BLACK
28 }
29}
30
31impl Rgba {
32 pub const BLACK: Rgba = Rgba::rgb(0, 0, 0);
33 pub const WHITE: Rgba = Rgba::rgb(255, 255, 255);
34 pub const TRANSPARENT: Rgba = Rgba {
35 r: 0,
36 g: 0,
37 b: 0,
38 a: 0,
39 };
40
41 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
42 Rgba { r, g, b, a: 255 }
43 }
44 pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
45 Rgba { r, g, b, a }
46 }
47 pub fn from_hex(s: &str) -> Option<Self> {
49 let s = s.trim().trim_start_matches('#');
50 if !s.is_ascii() {
51 return None;
52 }
53 let byte = |i: usize| s.get(i..i + 2).and_then(|h| u8::from_str_radix(h, 16).ok());
54 match s.len() {
55 6 => Some(Rgba::rgb(byte(0)?, byte(2)?, byte(4)?)),
56 8 => Some(Rgba::rgba(byte(0)?, byte(2)?, byte(4)?, byte(6)?)),
57 3 => {
58 let n = |i: usize| {
59 s.get(i..i + 1)
60 .and_then(|h| u8::from_str_radix(h, 16).ok())
61 .map(|v| v * 17)
62 };
63 Some(Rgba::rgb(n(0)?, n(1)?, n(2)?))
64 }
65 _ => None,
66 }
67 }
68 pub fn hex(&self) -> String {
70 format!("{:02X}{:02X}{:02X}", self.r, self.g, self.b)
71 }
72 pub fn with_alpha(self, a: u8) -> Self {
73 Rgba { a, ..self }
74 }
75 pub fn to_f32(self) -> [f32; 4] {
76 [
77 self.r as f32 / 255.0,
78 self.g as f32 / 255.0,
79 self.b as f32 / 255.0,
80 self.a as f32 / 255.0,
81 ]
82 }
83 pub fn from_f64(r: f64, g: f64, b: f64, a: f64) -> Self {
84 let c = |v: f64| {
85 if v.is_finite() {
86 (v.clamp(0.0, 1.0) * 255.0).round() as u8
87 } else {
88 0
89 }
90 };
91 Rgba {
92 r: c(r),
93 g: c(g),
94 b: c(b),
95 a: c(a),
96 }
97 }
98 pub fn luminance(self) -> f64 {
100 let lin = |c: u8| {
101 let v = c as f64 / 255.0;
102 if v <= 0.04045 {
103 v / 12.92
104 } else {
105 ((v + 0.055) / 1.055).powf(2.4)
106 }
107 };
108 0.2126 * lin(self.r) + 0.7152 * lin(self.g) + 0.0722 * lin(self.b)
109 }
110 pub fn contrast(self, other: Rgba) -> f64 {
112 let (a, b) = (self.luminance(), other.luminance());
113 let (hi, lo) = if a > b { (a, b) } else { (b, a) };
114 (hi + 0.05) / (lo + 0.05)
115 }
116 pub fn to_hsl(self) -> (f64, f64, f64) {
117 let r = self.r as f64 / 255.0;
118 let g = self.g as f64 / 255.0;
119 let b = self.b as f64 / 255.0;
120 let max = r.max(g).max(b);
121 let min = r.min(g).min(b);
122 let l = (max + min) / 2.0;
123 if (max - min).abs() < 1e-12 {
124 return (0.0, 0.0, l);
125 }
126 let d = max - min;
127 let s = if l > 0.5 {
128 d / (2.0 - max - min)
129 } else {
130 d / (max + min)
131 };
132 let h = if (max - r).abs() < 1e-12 {
133 (g - b) / d + if g < b { 6.0 } else { 0.0 }
134 } else if (max - g).abs() < 1e-12 {
135 (b - r) / d + 2.0
136 } else {
137 (r - g) / d + 4.0
138 };
139 (h * 60.0, s, l)
140 }
141 pub fn from_hsl(h: f64, s: f64, l: f64, a: u8) -> Self {
142 let h = ((h % 360.0) + 360.0) % 360.0 / 360.0;
143 let s = s.clamp(0.0, 1.0);
144 let l = l.clamp(0.0, 1.0);
145 if s == 0.0 {
146 return Rgba::from_f64(l, l, l, a as f64 / 255.0);
147 }
148 let q = if l < 0.5 {
149 l * (1.0 + s)
150 } else {
151 l + s - l * s
152 };
153 let p = 2.0 * l - q;
154 let f = |t: f64| {
155 let t = if t < 0.0 {
156 t + 1.0
157 } else if t > 1.0 {
158 t - 1.0
159 } else {
160 t
161 };
162 if t < 1.0 / 6.0 {
163 p + (q - p) * 6.0 * t
164 } else if t < 0.5 {
165 q
166 } else if t < 2.0 / 3.0 {
167 p + (q - p) * (2.0 / 3.0 - t) * 6.0
168 } else {
169 p
170 }
171 };
172 Rgba::from_f64(f(h + 1.0 / 3.0), f(h), f(h - 1.0 / 3.0), a as f64 / 255.0)
173 }
174 pub fn lerp(self, o: Rgba, t: f64) -> Rgba {
176 let t = if t.is_finite() {
177 t.clamp(0.0, 1.0)
178 } else {
179 0.0
180 };
181 let m = |a: u8, b: u8| {
182 (a as f64 + (b as f64 - a as f64) * t)
183 .round()
184 .clamp(0.0, 255.0) as u8
185 };
186 Rgba {
187 r: m(self.r, o.r),
188 g: m(self.g, o.g),
189 b: m(self.b, o.b),
190 a: m(self.a, o.a),
191 }
192 }
193}
194
195#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
198#[serde(rename_all = "camelCase")]
199pub enum SchemeSlot {
200 Dk1,
201 Lt1,
202 Dk2,
203 Lt2,
204 Accent1,
205 Accent2,
206 Accent3,
207 Accent4,
208 Accent5,
209 Accent6,
210 Hlink,
211 FolHlink,
212 Bg1,
213 Tx1,
214 Bg2,
215 Tx2,
216 PhClr,
217}
218
219impl SchemeSlot {
220 pub const THEME: [SchemeSlot; 12] = [
221 SchemeSlot::Dk1,
222 SchemeSlot::Lt1,
223 SchemeSlot::Dk2,
224 SchemeSlot::Lt2,
225 SchemeSlot::Accent1,
226 SchemeSlot::Accent2,
227 SchemeSlot::Accent3,
228 SchemeSlot::Accent4,
229 SchemeSlot::Accent5,
230 SchemeSlot::Accent6,
231 SchemeSlot::Hlink,
232 SchemeSlot::FolHlink,
233 ];
234 pub fn xml_name(self) -> &'static str {
235 match self {
236 SchemeSlot::Dk1 => "dk1",
237 SchemeSlot::Lt1 => "lt1",
238 SchemeSlot::Dk2 => "dk2",
239 SchemeSlot::Lt2 => "lt2",
240 SchemeSlot::Accent1 => "accent1",
241 SchemeSlot::Accent2 => "accent2",
242 SchemeSlot::Accent3 => "accent3",
243 SchemeSlot::Accent4 => "accent4",
244 SchemeSlot::Accent5 => "accent5",
245 SchemeSlot::Accent6 => "accent6",
246 SchemeSlot::Hlink => "hlink",
247 SchemeSlot::FolHlink => "folHlink",
248 SchemeSlot::Bg1 => "bg1",
249 SchemeSlot::Tx1 => "tx1",
250 SchemeSlot::Bg2 => "bg2",
251 SchemeSlot::Tx2 => "tx2",
252 SchemeSlot::PhClr => "phClr",
253 }
254 }
255 pub fn from_xml(s: &str) -> Option<Self> {
256 Some(match s {
257 "dk1" => SchemeSlot::Dk1,
258 "lt1" => SchemeSlot::Lt1,
259 "dk2" => SchemeSlot::Dk2,
260 "lt2" => SchemeSlot::Lt2,
261 "accent1" => SchemeSlot::Accent1,
262 "accent2" => SchemeSlot::Accent2,
263 "accent3" => SchemeSlot::Accent3,
264 "accent4" => SchemeSlot::Accent4,
265 "accent5" => SchemeSlot::Accent5,
266 "accent6" => SchemeSlot::Accent6,
267 "hlink" => SchemeSlot::Hlink,
268 "folHlink" => SchemeSlot::FolHlink,
269 "bg1" => SchemeSlot::Bg1,
270 "tx1" => SchemeSlot::Tx1,
271 "bg2" => SchemeSlot::Bg2,
272 "tx2" => SchemeSlot::Tx2,
273 "phClr" => SchemeSlot::PhClr,
274 _ => return None,
275 })
276 }
277 pub fn label(self) -> &'static str {
278 match self {
279 SchemeSlot::Dk1 | SchemeSlot::Tx1 => "Text/Background - Dark 1",
280 SchemeSlot::Lt1 | SchemeSlot::Bg1 => "Text/Background - Light 1",
281 SchemeSlot::Dk2 | SchemeSlot::Tx2 => "Text/Background - Dark 2",
282 SchemeSlot::Lt2 | SchemeSlot::Bg2 => "Text/Background - Light 2",
283 SchemeSlot::Accent1 => "Accent 1",
284 SchemeSlot::Accent2 => "Accent 2",
285 SchemeSlot::Accent3 => "Accent 3",
286 SchemeSlot::Accent4 => "Accent 4",
287 SchemeSlot::Accent5 => "Accent 5",
288 SchemeSlot::Accent6 => "Accent 6",
289 SchemeSlot::Hlink => "Hyperlink",
290 SchemeSlot::FolHlink => "Followed Hyperlink",
291 SchemeSlot::PhClr => "Placeholder colour",
292 }
293 }
294}
295
296#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
298#[serde(rename_all = "camelCase")]
299pub struct ColorScheme {
300 pub name: String,
301 pub colors: [Rgba; 12],
302}
303
304impl ColorScheme {
305 pub fn get(&self, slot: SchemeSlot) -> Rgba {
306 let i = match slot {
307 SchemeSlot::Dk1 | SchemeSlot::Tx1 => 0,
308 SchemeSlot::Lt1 | SchemeSlot::Bg1 => 1,
309 SchemeSlot::Dk2 | SchemeSlot::Tx2 => 2,
310 SchemeSlot::Lt2 | SchemeSlot::Bg2 => 3,
311 SchemeSlot::Accent1 => 4,
312 SchemeSlot::Accent2 => 5,
313 SchemeSlot::Accent3 => 6,
314 SchemeSlot::Accent4 => 7,
315 SchemeSlot::Accent5 => 8,
316 SchemeSlot::Accent6 => 9,
317 SchemeSlot::Hlink => 10,
318 SchemeSlot::FolHlink => 11,
319 SchemeSlot::PhClr => 4,
320 };
321 self.colors.get(i).copied().unwrap_or(Rgba::BLACK)
322 }
323 pub fn set(&mut self, slot: SchemeSlot, c: Rgba) {
324 if let Some(i) = SchemeSlot::THEME.iter().position(|s| *s == slot)
325 && let Some(v) = self.colors.get_mut(i)
326 {
327 *v = c;
328 }
329 }
330}
331
332#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
334#[serde(rename_all = "camelCase", tag = "op", content = "val")]
335pub enum ColorTransform {
336 Tint(i32),
337 Shade(i32),
338 LumMod(i32),
339 LumOff(i32),
340 SatMod(i32),
341 SatOff(i32),
342 HueMod(i32),
343 HueOff(i32),
344 Alpha(i32),
345 AlphaMod(i32),
346 AlphaOff(i32),
347 Comp,
348 Inv,
349 Gray,
350 RedMod(i32),
351 GreenMod(i32),
352 BlueMod(i32),
353}
354
355impl ColorTransform {
356 pub fn xml_name(&self) -> &'static str {
357 match self {
358 ColorTransform::Tint(_) => "tint",
359 ColorTransform::Shade(_) => "shade",
360 ColorTransform::LumMod(_) => "lumMod",
361 ColorTransform::LumOff(_) => "lumOff",
362 ColorTransform::SatMod(_) => "satMod",
363 ColorTransform::SatOff(_) => "satOff",
364 ColorTransform::HueMod(_) => "hueMod",
365 ColorTransform::HueOff(_) => "hueOff",
366 ColorTransform::Alpha(_) => "alpha",
367 ColorTransform::AlphaMod(_) => "alphaMod",
368 ColorTransform::AlphaOff(_) => "alphaOff",
369 ColorTransform::Comp => "comp",
370 ColorTransform::Inv => "inv",
371 ColorTransform::Gray => "gray",
372 ColorTransform::RedMod(_) => "redMod",
373 ColorTransform::GreenMod(_) => "greenMod",
374 ColorTransform::BlueMod(_) => "blueMod",
375 }
376 }
377 pub fn value(&self) -> Option<i32> {
378 match *self {
379 ColorTransform::Tint(v)
380 | ColorTransform::Shade(v)
381 | ColorTransform::LumMod(v)
382 | ColorTransform::LumOff(v)
383 | ColorTransform::SatMod(v)
384 | ColorTransform::SatOff(v)
385 | ColorTransform::HueMod(v)
386 | ColorTransform::HueOff(v)
387 | ColorTransform::Alpha(v)
388 | ColorTransform::AlphaMod(v)
389 | ColorTransform::AlphaOff(v)
390 | ColorTransform::RedMod(v)
391 | ColorTransform::GreenMod(v)
392 | ColorTransform::BlueMod(v) => Some(v),
393 _ => None,
394 }
395 }
396 pub fn from_xml(name: &str, val: Option<i32>) -> Option<Self> {
397 let v = val.unwrap_or(0);
398 Some(match name {
399 "tint" => ColorTransform::Tint(v),
400 "shade" => ColorTransform::Shade(v),
401 "lumMod" => ColorTransform::LumMod(v),
402 "lumOff" => ColorTransform::LumOff(v),
403 "satMod" => ColorTransform::SatMod(v),
404 "satOff" => ColorTransform::SatOff(v),
405 "hueMod" => ColorTransform::HueMod(v),
406 "hueOff" => ColorTransform::HueOff(v),
407 "alpha" => ColorTransform::Alpha(v),
408 "alphaMod" => ColorTransform::AlphaMod(v),
409 "alphaOff" => ColorTransform::AlphaOff(v),
410 "comp" => ColorTransform::Comp,
411 "inv" => ColorTransform::Inv,
412 "gray" => ColorTransform::Gray,
413 "redMod" => ColorTransform::RedMod(v),
414 "greenMod" => ColorTransform::GreenMod(v),
415 "blueMod" => ColorTransform::BlueMod(v),
416 _ => return None,
417 })
418 }
419}
420
421pub fn apply(base: Rgba, transforms: &[ColorTransform]) -> Rgba {
423 let mut c = base;
424 for t in transforms {
425 c = apply_one(c, *t);
426 }
427 c
428}
429
430fn pct(v: i32) -> f64 {
431 v as f64 / 100_000.0
432}
433
434fn apply_one(c: Rgba, t: ColorTransform) -> Rgba {
435 match t {
436 ColorTransform::Tint(v) => {
439 let k = pct(v).clamp(0.0, 1.0);
440 map_linear(c, |x| x * k + (1.0 - k))
441 }
442 ColorTransform::Shade(v) => {
443 let k = pct(v).clamp(0.0, 1.0);
444 map_linear(c, |x| x * k)
445 }
446 ColorTransform::LumMod(v) => {
447 let (h, s, l) = c.to_hsl();
448 Rgba::from_hsl(h, s, l * pct(v), c.a)
449 }
450 ColorTransform::LumOff(v) => {
451 let (h, s, l) = c.to_hsl();
452 Rgba::from_hsl(h, s, l + pct(v), c.a)
453 }
454 ColorTransform::SatMod(v) => {
455 let (h, s, l) = c.to_hsl();
456 Rgba::from_hsl(h, s * pct(v), l, c.a)
457 }
458 ColorTransform::SatOff(v) => {
459 let (h, s, l) = c.to_hsl();
460 Rgba::from_hsl(h, s + pct(v), l, c.a)
461 }
462 ColorTransform::HueMod(v) => {
463 let (h, s, l) = c.to_hsl();
464 Rgba::from_hsl(h * pct(v), s, l, c.a)
465 }
466 ColorTransform::HueOff(v) => {
467 let (h, s, l) = c.to_hsl();
468 Rgba::from_hsl(h + v as f64 / 60_000.0, s, l, c.a)
469 }
470 ColorTransform::Alpha(v) => c.with_alpha((pct(v).clamp(0.0, 1.0) * 255.0).round() as u8),
471 ColorTransform::AlphaMod(v) => {
472 c.with_alpha((c.a as f64 * pct(v)).round().clamp(0.0, 255.0) as u8)
473 }
474 ColorTransform::AlphaOff(v) => {
475 c.with_alpha((c.a as f64 + pct(v) * 255.0).round().clamp(0.0, 255.0) as u8)
476 }
477 ColorTransform::Comp => {
478 let (h, s, l) = c.to_hsl();
479 Rgba::from_hsl(h + 180.0, s, l, c.a)
480 }
481 ColorTransform::Inv => Rgba {
482 r: 255 - c.r,
483 g: 255 - c.g,
484 b: 255 - c.b,
485 a: c.a,
486 },
487 ColorTransform::Gray => {
488 let y = (0.299 * c.r as f64 + 0.587 * c.g as f64 + 0.114 * c.b as f64)
489 .round()
490 .clamp(0.0, 255.0) as u8;
491 Rgba {
492 r: y,
493 g: y,
494 b: y,
495 a: c.a,
496 }
497 }
498 ColorTransform::RedMod(v) => Rgba {
499 r: (c.r as f64 * pct(v)).round().clamp(0.0, 255.0) as u8,
500 ..c
501 },
502 ColorTransform::GreenMod(v) => Rgba {
503 g: (c.g as f64 * pct(v)).round().clamp(0.0, 255.0) as u8,
504 ..c
505 },
506 ColorTransform::BlueMod(v) => Rgba {
507 b: (c.b as f64 * pct(v)).round().clamp(0.0, 255.0) as u8,
508 ..c
509 },
510 }
511}
512
513fn to_lin(v: u8) -> f64 {
514 let v = v as f64 / 255.0;
515 if v <= 0.04045 {
516 v / 12.92
517 } else {
518 ((v + 0.055) / 1.055).powf(2.4)
519 }
520}
521fn from_lin(v: f64) -> f64 {
522 let v = v.clamp(0.0, 1.0);
523 if v <= 0.003_130_8 {
524 v * 12.92
525 } else {
526 1.055 * v.powf(1.0 / 2.4) - 0.055
527 }
528}
529fn map_linear(c: Rgba, f: impl Fn(f64) -> f64) -> Rgba {
530 Rgba::from_f64(
531 from_lin(f(to_lin(c.r))),
532 from_lin(f(to_lin(c.g))),
533 from_lin(f(to_lin(c.b))),
534 c.a as f64 / 255.0,
535 )
536}
537
538pub fn theme_variants(c: Rgba) -> [(Vec<ColorTransform>, &'static str); 5] {
541 let (_, _, l) = c.to_hsl();
542 if l < 0.2 {
543 [
544 (
545 vec![ColorTransform::LumMod(10000), ColorTransform::LumOff(90000)],
546 "Lighter 90%",
547 ),
548 (
549 vec![ColorTransform::LumMod(25000), ColorTransform::LumOff(75000)],
550 "Lighter 75%",
551 ),
552 (
553 vec![ColorTransform::LumMod(50000), ColorTransform::LumOff(50000)],
554 "Lighter 50%",
555 ),
556 (
557 vec![ColorTransform::LumMod(75000), ColorTransform::LumOff(25000)],
558 "Lighter 25%",
559 ),
560 (
561 vec![ColorTransform::LumMod(90000), ColorTransform::LumOff(10000)],
562 "Lighter 10%",
563 ),
564 ]
565 } else if l > 0.8 {
566 [
567 (vec![ColorTransform::LumMod(95000)], "Darker 5%"),
568 (vec![ColorTransform::LumMod(85000)], "Darker 15%"),
569 (vec![ColorTransform::LumMod(75000)], "Darker 25%"),
570 (vec![ColorTransform::LumMod(65000)], "Darker 35%"),
571 (vec![ColorTransform::LumMod(50000)], "Darker 50%"),
572 ]
573 } else {
574 [
575 (
576 vec![ColorTransform::LumMod(20000), ColorTransform::LumOff(80000)],
577 "Lighter 80%",
578 ),
579 (
580 vec![ColorTransform::LumMod(40000), ColorTransform::LumOff(60000)],
581 "Lighter 60%",
582 ),
583 (
584 vec![ColorTransform::LumMod(60000), ColorTransform::LumOff(40000)],
585 "Lighter 40%",
586 ),
587 (vec![ColorTransform::LumMod(75000)], "Darker 25%"),
588 (vec![ColorTransform::LumMod(50000)], "Darker 50%"),
589 ]
590 }
591}
592
593pub const STANDARD_COLORS: [(Rgba, &str); 10] = [
595 (Rgba::rgb(0xC0, 0x00, 0x00), "Dark Red"),
596 (Rgba::rgb(0xFF, 0x00, 0x00), "Red"),
597 (Rgba::rgb(0xFF, 0xC0, 0x00), "Orange"),
598 (Rgba::rgb(0xFF, 0xFF, 0x00), "Yellow"),
599 (Rgba::rgb(0x92, 0xD0, 0x50), "Light Green"),
600 (Rgba::rgb(0x00, 0xB0, 0x50), "Green"),
601 (Rgba::rgb(0x00, 0xB0, 0xF0), "Light Blue"),
602 (Rgba::rgb(0x00, 0x70, 0xC0), "Blue"),
603 (Rgba::rgb(0x00, 0x20, 0x60), "Dark Blue"),
604 (Rgba::rgb(0x70, 0x30, 0xA0), "Purple"),
605];
606
607pub fn preset(name: &str) -> Option<Rgba> {
609 let c = match name {
610 "black" => (0, 0, 0),
611 "white" => (255, 255, 255),
612 "red" => (255, 0, 0),
613 "green" => (0, 128, 0),
614 "lime" => (0, 255, 0),
615 "blue" => (0, 0, 255),
616 "yellow" => (255, 255, 0),
617 "cyan" | "aqua" => (0, 255, 255),
618 "magenta" | "fuchsia" => (255, 0, 255),
619 "gray" | "grey" => (128, 128, 128),
620 "silver" => (192, 192, 192),
621 "maroon" => (128, 0, 0),
622 "navy" => (0, 0, 128),
623 "olive" => (128, 128, 0),
624 "purple" => (128, 0, 128),
625 "teal" => (0, 128, 128),
626 "orange" => (255, 165, 0),
627 "pink" => (255, 192, 203),
628 "brown" => (165, 42, 42),
629 "gold" => (255, 215, 0),
630 "ltGray" | "lightGray" => (211, 211, 211),
631 "dkGray" | "darkGray" => (169, 169, 169),
632 _ => return None,
633 };
634 Some(Rgba::rgb(c.0, c.1, c.2))
635}
636
637#[cfg(test)]
638mod tests {
639 use super::*;
640
641 #[test]
642 fn hex_roundtrip() {
643 assert_eq!(Rgba::from_hex("#156082"), Some(Rgba::rgb(0x15, 0x60, 0x82)));
644 assert_eq!(Rgba::rgb(0x15, 0x60, 0x82).hex(), "156082");
645 assert_eq!(Rgba::from_hex("fff"), Some(Rgba::WHITE));
646 assert_eq!(Rgba::from_hex("zz0000"), None);
647 assert_eq!(Rgba::from_hex("ééé"), None);
648 assert_eq!(Rgba::from_hex(""), None);
649 }
650
651 #[test]
652 fn hsl_roundtrip() {
653 for c in [
654 Rgba::rgb(10, 200, 30),
655 Rgba::rgb(255, 0, 0),
656 Rgba::rgb(128, 128, 128),
657 Rgba::rgb(21, 96, 130),
658 ] {
659 let (h, s, l) = c.to_hsl();
660 let d = Rgba::from_hsl(h, s, l, 255);
661 assert!(
662 (d.r as i32 - c.r as i32).abs() <= 1
663 && (d.g as i32 - c.g as i32).abs() <= 1
664 && (d.b as i32 - c.b as i32).abs() <= 1,
665 "{c:?} {d:?}"
666 );
667 }
668 }
669
670 #[test]
671 fn transforms() {
672 let c = Rgba::rgb(0x40, 0x80, 0xC0);
673 assert_eq!(apply(c, &[ColorTransform::Alpha(50000)]).a, 128);
674 let lighter = apply(
675 c,
676 &[ColorTransform::LumMod(20000), ColorTransform::LumOff(80000)],
677 );
678 assert!(lighter.luminance() > c.luminance());
679 let darker = apply(c, &[ColorTransform::LumMod(50000)]);
680 assert!(darker.luminance() < c.luminance());
681 assert_eq!(apply(Rgba::WHITE, &[ColorTransform::Shade(0)]), Rgba::BLACK);
682 assert_eq!(apply(Rgba::BLACK, &[ColorTransform::Tint(0)]), Rgba::WHITE);
683 assert_eq!(
684 apply(c, &[ColorTransform::Inv]),
685 Rgba::rgb(0xBF, 0x7F, 0x3F)
686 );
687 let _ = apply(
689 c,
690 &[
691 ColorTransform::LumMod(i32::MAX),
692 ColorTransform::HueOff(i32::MIN),
693 ColorTransform::AlphaMod(i32::MIN),
694 ],
695 );
696 }
697
698 #[test]
699 fn contrast_ratio() {
700 assert!((Rgba::BLACK.contrast(Rgba::WHITE) - 21.0).abs() < 0.01);
701 assert!((Rgba::WHITE.contrast(Rgba::WHITE) - 1.0).abs() < 1e-9);
702 }
703
704 #[test]
705 fn scheme_slots() {
706 let mut s = ColorScheme {
707 name: "t".into(),
708 colors: [Rgba::BLACK; 12],
709 };
710 s.set(SchemeSlot::Accent3, Rgba::WHITE);
711 assert_eq!(s.get(SchemeSlot::Accent3), Rgba::WHITE);
712 assert_eq!(s.get(SchemeSlot::Tx1), Rgba::BLACK);
713 for slot in SchemeSlot::THEME {
714 assert_eq!(SchemeSlot::from_xml(slot.xml_name()), Some(slot));
715 }
716 }
717}