1use crate::{Color, Shadow, Vector, color};
3
4use std::sync::LazyLock;
5
6#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct Palette {
9 pub background: Background,
11 pub primary: Swatch,
13 pub secondary: Swatch,
15 pub success: Swatch,
17 pub warning: Swatch,
19 pub danger: Swatch,
21 pub is_dark: bool,
23}
24
25impl Palette {
26 pub fn generate(palette: Seed) -> Self {
28 Self {
29 background: Background::new(palette.background, palette.text),
30 primary: Swatch::generate(palette.primary, palette.background, palette.text),
31 secondary: Swatch::derive(palette.background, palette.text),
32 success: Swatch::generate(palette.success, palette.background, palette.text),
33 warning: Swatch::generate(palette.warning, palette.background, palette.text),
34 danger: Swatch::generate(palette.danger, palette.background, palette.text),
35 is_dark: is_dark(palette.background),
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq)]
42pub struct Pair {
43 pub color: Color,
45
46 pub text: Color,
52}
53
54impl Pair {
55 pub fn new(color: Color, text: Color) -> Self {
57 Self {
58 color,
59 text: readable(color, text),
60 }
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq)]
66pub struct Background {
67 pub base: Pair,
69 pub weakest: Pair,
71 pub weaker: Pair,
73 pub weak: Pair,
75 pub neutral: Pair,
77 pub strong: Pair,
79 pub stronger: Pair,
81 pub strongest: Pair,
83}
84
85impl Background {
86 pub fn new(base: Color, text: Color) -> Self {
88 let weakest = deviate(base, 0.03);
89 let weaker = deviate(base, 0.07);
90 let weak = deviate(base, 0.1);
91 let neutral = deviate(base, 0.125);
92 let strong = deviate(base, 0.15);
93 let stronger = deviate(base, 0.175);
94 let strongest = deviate(base, 0.20);
95
96 Self {
97 base: Pair::new(base, text),
98 weakest: Pair::new(weakest, text),
99 weaker: Pair::new(weaker, text),
100 weak: Pair::new(weak, text),
101 neutral: Pair::new(neutral, text),
102 strong: Pair::new(strong, text),
103 stronger: Pair::new(stronger, text),
104 strongest: Pair::new(strongest, text),
105 }
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq)]
111pub struct Swatch {
112 pub base: Pair,
114 pub weak: Pair,
116 pub strong: Pair,
118}
119
120impl Swatch {
121 pub fn generate(base: Color, background: Color, text: Color) -> Self {
123 let weak = base.mix(background, 0.4);
124 let strong = deviate(base, 0.1);
125
126 Self {
127 base: Pair::new(base, text),
128 weak: Pair::new(weak, text),
129 strong: Pair::new(strong, text),
130 }
131 }
132
133 pub fn derive(base: Color, text: Color) -> Self {
135 let factor = if is_dark(base) { 0.2 } else { 0.4 };
136
137 let weak = deviate(base, 0.1).mix(text, factor);
138 let strong = deviate(base, 0.3).mix(text, factor);
139 let base = deviate(base, 0.2).mix(text, factor);
140
141 Self {
142 base: Pair::new(base, text),
143 weak: Pair::new(weak, text),
144 strong: Pair::new(strong, text),
145 }
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq)]
151#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
152pub struct Seed {
153 pub background: Color,
155 pub text: Color,
157 pub primary: Color,
159 pub success: Color,
161 pub warning: Color,
163 pub danger: Color,
165}
166
167impl Seed {
168 pub const LIGHT: Self = Self {
170 background: Color::WHITE,
171 text: Color::BLACK,
172 primary: color!(0x5865F2),
173 success: color!(0x12664f),
174 warning: color!(0xb77e33),
175 danger: color!(0xc3423f),
176 };
177
178 pub const DARK: Self = Self {
180 background: color!(0x2B2D31),
181 text: Color::from_rgb(0.90, 0.90, 0.90),
182 primary: color!(0x5865F2),
183 success: color!(0x12664f),
184 warning: color!(0xffc14e),
185 danger: color!(0xc3423f),
186 };
187
188 pub const DRACULA: Self = Self {
192 background: color!(0x282A36), text: color!(0xf8f8f2), primary: color!(0xbd93f9), success: color!(0x50fa7b), warning: color!(0xf1fa8c), danger: color!(0xff5555), };
199
200 pub const NORD: Self = Self {
204 background: color!(0x2e3440), text: color!(0xeceff4), primary: color!(0x8fbcbb), success: color!(0xa3be8c), warning: color!(0xebcb8b), danger: color!(0xbf616a), };
211
212 pub const SOLARIZED_LIGHT: Self = Self {
216 background: color!(0xfdf6e3), text: color!(0x657b83), primary: color!(0x2aa198), success: color!(0x859900), warning: color!(0xb58900), danger: color!(0xdc322f), };
223
224 pub const SOLARIZED_DARK: Self = Self {
228 background: color!(0x002b36), text: color!(0x839496), primary: color!(0x2aa198), success: color!(0x859900), warning: color!(0xb58900), danger: color!(0xdc322f), };
235
236 pub const GRUVBOX_LIGHT: Self = Self {
240 background: color!(0xfbf1c7), text: color!(0x282828), primary: color!(0x458588), success: color!(0x98971a), warning: color!(0xd79921), danger: color!(0xcc241d), };
247
248 pub const GRUVBOX_DARK: Self = Self {
252 background: color!(0x282828), text: color!(0xfbf1c7), primary: color!(0x458588), success: color!(0x98971a), warning: color!(0xd79921), danger: color!(0xcc241d), };
259
260 pub const CATPPUCCIN_LATTE: Self = Self {
264 background: color!(0xeff1f5), text: color!(0x4c4f69), primary: color!(0x1e66f5), success: color!(0x40a02b), warning: color!(0xdf8e1d), danger: color!(0xd20f39), };
271
272 pub const CATPPUCCIN_FRAPPE: Self = Self {
276 background: color!(0x303446), text: color!(0xc6d0f5), primary: color!(0x8caaee), success: color!(0xa6d189), warning: color!(0xe5c890), danger: color!(0xe78284), };
283
284 pub const CATPPUCCIN_MACCHIATO: Self = Self {
288 background: color!(0x24273a), text: color!(0xcad3f5), primary: color!(0x8aadf4), success: color!(0xa6da95), warning: color!(0xeed49f), danger: color!(0xed8796), };
295
296 pub const CATPPUCCIN_MOCHA: Self = Self {
300 background: color!(0x1e1e2e), text: color!(0xcdd6f4), primary: color!(0x89b4fa), success: color!(0xa6e3a1), warning: color!(0xf9e2af), danger: color!(0xf38ba8), };
307
308 pub const TOKYO_NIGHT: Self = Self {
312 background: color!(0x1a1b26), text: color!(0x9aa5ce), primary: color!(0x2ac3de), success: color!(0x9ece6a), warning: color!(0xe0af68), danger: color!(0xf7768e), };
319
320 pub const TOKYO_NIGHT_STORM: Self = Self {
324 background: color!(0x24283b), text: color!(0x9aa5ce), primary: color!(0x2ac3de), success: color!(0x9ece6a), warning: color!(0xe0af68), danger: color!(0xf7768e), };
331
332 pub const TOKYO_NIGHT_LIGHT: Self = Self {
336 background: color!(0xd5d6db), text: color!(0x565a6e), primary: color!(0x166775), success: color!(0x485e30), warning: color!(0x8f5e15), danger: color!(0x8c4351), };
343
344 pub const KANAGAWA_WAVE: Self = Self {
348 background: color!(0x1f1f28), text: color!(0xDCD7BA), primary: color!(0x7FB4CA), success: color!(0x76946A), warning: color!(0xff9e3b), danger: color!(0xC34043), };
355
356 pub const KANAGAWA_DRAGON: Self = Self {
360 background: color!(0x181616), text: color!(0xc5c9c5), primary: color!(0x223249), success: color!(0x8a9a7b), warning: color!(0xff9e3b), danger: color!(0xc4746e), };
367
368 pub const KANAGAWA_LOTUS: Self = Self {
372 background: color!(0xf2ecbc), text: color!(0x545464), primary: color!(0x4d699b), success: color!(0x6f894e), warning: color!(0xe98a00), danger: color!(0xc84053), };
379
380 pub const MOONFLY: Self = Self {
384 background: color!(0x080808), text: color!(0xbdbdbd), primary: color!(0x80a0ff), success: color!(0x8cc85f), warning: color!(0xe3c78a), danger: color!(0xff5454), };
391
392 pub const NIGHTFLY: Self = Self {
396 background: color!(0x011627), text: color!(0xbdc1c6), primary: color!(0x82aaff), success: color!(0xa1cd5e), warning: color!(0xe3d18a), danger: color!(0xfc514e), };
403
404 pub const OXOCARBON: Self = Self {
408 background: color!(0x232323),
409 text: color!(0xd0d0d0),
410 primary: color!(0x00b4ff),
411 success: color!(0x00c15a),
412 warning: color!(0xbe95ff), danger: color!(0xf62d0f),
414 };
415
416 pub const FERRA: Self = Self {
420 background: color!(0x2b292d),
421 text: color!(0xfecdb2),
422 primary: color!(0xd1d1e0),
423 success: color!(0xb1b695),
424 warning: color!(0xf5d76e), danger: color!(0xe06b75),
426 };
427}
428
429pub static LIGHT: LazyLock<Palette> = LazyLock::new(|| Palette::generate(Seed::LIGHT));
431
432pub static DARK: LazyLock<Palette> = LazyLock::new(|| Palette::generate(Seed::DARK));
434
435pub static DRACULA: LazyLock<Palette> = LazyLock::new(|| Palette::generate(Seed::DRACULA));
437
438pub static NORD: LazyLock<Palette> = LazyLock::new(|| Palette::generate(Seed::NORD));
440
441pub static SOLARIZED_LIGHT: LazyLock<Palette> =
443 LazyLock::new(|| Palette::generate(Seed::SOLARIZED_LIGHT));
444
445pub static SOLARIZED_DARK: LazyLock<Palette> =
447 LazyLock::new(|| Palette::generate(Seed::SOLARIZED_DARK));
448
449pub static GRUVBOX_LIGHT: LazyLock<Palette> =
451 LazyLock::new(|| Palette::generate(Seed::GRUVBOX_LIGHT));
452
453pub static GRUVBOX_DARK: LazyLock<Palette> =
455 LazyLock::new(|| Palette::generate(Seed::GRUVBOX_DARK));
456
457pub static CATPPUCCIN_LATTE: LazyLock<Palette> =
459 LazyLock::new(|| Palette::generate(Seed::CATPPUCCIN_LATTE));
460
461pub static CATPPUCCIN_FRAPPE: LazyLock<Palette> =
463 LazyLock::new(|| Palette::generate(Seed::CATPPUCCIN_FRAPPE));
464
465pub static CATPPUCCIN_MACCHIATO: LazyLock<Palette> =
467 LazyLock::new(|| Palette::generate(Seed::CATPPUCCIN_MACCHIATO));
468
469pub static CATPPUCCIN_MOCHA: LazyLock<Palette> =
471 LazyLock::new(|| Palette::generate(Seed::CATPPUCCIN_MOCHA));
472
473pub static TOKYO_NIGHT: LazyLock<Palette> = LazyLock::new(|| Palette::generate(Seed::TOKYO_NIGHT));
475
476pub static TOKYO_NIGHT_STORM: LazyLock<Palette> =
478 LazyLock::new(|| Palette::generate(Seed::TOKYO_NIGHT_STORM));
479
480pub static TOKYO_NIGHT_LIGHT: LazyLock<Palette> =
482 LazyLock::new(|| Palette::generate(Seed::TOKYO_NIGHT_LIGHT));
483
484pub static KANAGAWA_WAVE: LazyLock<Palette> =
486 LazyLock::new(|| Palette::generate(Seed::KANAGAWA_WAVE));
487
488pub static KANAGAWA_DRAGON: LazyLock<Palette> =
490 LazyLock::new(|| Palette::generate(Seed::KANAGAWA_DRAGON));
491
492pub static KANAGAWA_LOTUS: LazyLock<Palette> =
494 LazyLock::new(|| Palette::generate(Seed::KANAGAWA_LOTUS));
495
496pub static MOONFLY: LazyLock<Palette> = LazyLock::new(|| Palette::generate(Seed::MOONFLY));
498
499pub static NIGHTFLY: LazyLock<Palette> = LazyLock::new(|| Palette::generate(Seed::NIGHTFLY));
501
502pub static OXOCARBON: LazyLock<Palette> = LazyLock::new(|| Palette::generate(Seed::OXOCARBON));
504
505pub static FERRA: LazyLock<Palette> = LazyLock::new(|| Palette::generate(Seed::FERRA));
507
508struct Oklch {
509 l: f32,
510 c: f32,
511 h: f32,
512 a: f32,
513}
514
515pub fn darken(color: Color, amount: f32) -> Color {
517 let mut oklch = to_oklch(color);
518
519 if oklch.c > 0.0 && oklch.c < (1.0 - oklch.l) / 2.0 {
521 oklch.c *= 1.0 + (0.2 / oklch.c).min(100.0) * amount;
523 }
524
525 oklch.l = if oklch.l - amount < 0.0 {
526 0.0
527 } else {
528 oklch.l - amount
529 };
530
531 from_oklch(oklch)
532}
533
534pub fn lighten(color: Color, amount: f32) -> Color {
536 let mut oklch = to_oklch(color);
537
538 oklch.c *= 1.0 + 2.0 * amount / oklch.l.max(0.05);
541
542 oklch.l = if oklch.l + amount > 1.0 {
543 1.0
544 } else {
545 oklch.l + amount
546 };
547
548 from_oklch(oklch)
549}
550
551pub fn deviate(color: Color, amount: f32) -> Color {
554 if is_dark(color) {
555 lighten(color, amount)
556 } else {
557 darken(color, amount)
558 }
559}
560
561pub fn readable(background: Color, text: Color) -> Color {
564 if text.is_readable_on(background) {
565 return text;
566 }
567
568 let improve = if is_dark(background) { lighten } else { darken };
569
570 let candidate = improve(text, 0.1);
572
573 if candidate.is_readable_on(background) {
574 return candidate;
575 }
576
577 let candidate = improve(text, 0.2);
578
579 if candidate.is_readable_on(background) {
580 return candidate;
581 }
582
583 let white_contrast = background.relative_contrast(Color::WHITE);
584 let black_contrast = background.relative_contrast(Color::BLACK);
585
586 if white_contrast >= black_contrast {
587 Color::WHITE.mix(background, 0.05)
588 } else {
589 Color::BLACK.mix(background, 0.05)
590 }
591}
592
593pub fn is_dark(color: Color) -> bool {
595 to_oklch(color).l < 0.6
596}
597
598pub fn focus_color(accent: Color, page_bg: Color) -> Color {
605 if page_bg.relative_contrast(accent) >= 2.0 {
606 accent
607 } else if is_dark(page_bg) {
608 Color::WHITE
609 } else {
610 Color::BLACK
611 }
612}
613
614pub fn focus_border_color(widget_bg: Color, accent: Color, page_bg: Color) -> Color {
623 let base = focus_color(accent, page_bg);
624
625 if widget_bg.a < 0.1 || widget_bg.relative_contrast(base) >= 1.5 {
627 return base;
628 }
629
630 deviate(base, 0.3)
633}
634
635pub fn focus_shadow(accent: Color, page_bg: Color) -> Shadow {
642 Shadow {
643 color: Color {
644 a: 0.85,
645 ..focus_color(accent, page_bg)
646 },
647 offset: Vector::ZERO,
648 blur_radius: 10.0,
649 }
650}
651
652pub fn focus_shadow_subtle(accent: Color, page_bg: Color) -> Shadow {
656 Shadow {
657 color: Color {
658 a: 0.75,
659 ..focus_color(accent, page_bg)
660 },
661 offset: Vector::ZERO,
662 blur_radius: 6.0,
663 }
664}
665
666fn to_oklch(color: Color) -> Oklch {
668 let [r, g, b, alpha] = color.into_linear();
669
670 let l = 0.41222146 * r + 0.53633255 * g + 0.051445995 * b;
672 let m = 0.2119035 * r + 0.6806995 * g + 0.10739696 * b;
673 let s = 0.08830246 * r + 0.28171885 * g + 0.6299787 * b;
674
675 let l_ = l.cbrt();
677 let m_ = m.cbrt();
678 let s_ = s.cbrt();
679
680 let l = 0.21045426 * l_ + 0.7936178 * m_ - 0.004072047 * s_;
682 let a = 1.9779985 * l_ - 2.4285922 * m_ + 0.4505937 * s_;
683 let b = 0.025904037 * l_ + 0.78277177 * m_ - 0.80867577 * s_;
684
685 let c = (a * a + b * b).sqrt();
687 let h = b.atan2(a); Oklch { l, c, h, a: alpha }
690}
691
692fn from_oklch(oklch: Oklch) -> Color {
694 let Oklch { l, c, h, a: alpha } = oklch;
695
696 let a = c * h.cos();
697 let b = c * h.sin();
698
699 let l_ = l + 0.39633778 * a + 0.21580376 * b;
701 let m_ = l - 0.105561346 * a - 0.06385417 * b;
702 let s_ = l - 0.08948418 * a - 1.2914855 * b;
703
704 let l = l_ * l_ * l_;
706 let m = m_ * m_ * m_;
707 let s = s_ * s_ * s_;
708
709 let r = 4.0767417 * l - 3.3077116 * m + 0.23096994 * s;
710 let g = -1.268438 * l + 2.6097574 * m - 0.34131938 * s;
711 let b = -0.0041960863 * l - 0.7034186 * m + 1.7076147 * s;
712
713 Color::from_linear_rgba(
714 r.clamp(0.0, 1.0),
715 g.clamp(0.0, 1.0),
716 b.clamp(0.0, 1.0),
717 alpha,
718 )
719}