Skip to main content

iced_core/theme/
palette.rs

1//! Define the colors of a theme.
2use crate::{Color, color};
3
4use std::sync::LazyLock;
5
6/// An extended set of colors generated from a [`Seed`].
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct Palette {
9    /// The set of background colors.
10    pub background: Background,
11    /// The set of primary colors.
12    pub primary: Swatch,
13    /// The set of secondary colors.
14    pub secondary: Swatch,
15    /// The set of success colors.
16    pub success: Swatch,
17    /// The set of warning colors.
18    pub warning: Swatch,
19    /// The set of danger colors.
20    pub danger: Swatch,
21    /// Whether the palette is dark or not.
22    pub is_dark: bool,
23}
24
25impl Palette {
26    /// Generates a [`Palette`] from the given [`Seed`].
27    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/// A pair of background and text colors.
41#[derive(Debug, Clone, Copy, PartialEq)]
42pub struct Pair {
43    /// The background color.
44    pub color: Color,
45
46    /// The text color.
47    ///
48    /// It's guaranteed to be readable on top of the background [`color`].
49    ///
50    /// [`color`]: Self::color
51    pub text: Color,
52}
53
54impl Pair {
55    /// Creates a new [`Pair`] from a background [`Color`] and some text [`Color`].
56    pub fn new(color: Color, text: Color) -> Self {
57        Self {
58            color,
59            text: readable(color, text),
60        }
61    }
62}
63
64/// A set of background colors.
65#[derive(Debug, Clone, Copy, PartialEq)]
66pub struct Background {
67    /// The base background color.
68    pub base: Pair,
69    /// The weakest version of the base background color.
70    pub weakest: Pair,
71    /// A weaker version of the base background color.
72    pub weaker: Pair,
73    /// A weak version of the base background color.
74    pub weak: Pair,
75    /// A neutral version of the base background color, between weak and strong.
76    pub neutral: Pair,
77    /// A strong version of the base background color.
78    pub strong: Pair,
79    /// A stronger version of the base background color.
80    pub stronger: Pair,
81    /// The strongest version of the base background color.
82    pub strongest: Pair,
83}
84
85impl Background {
86    /// Generates a set of [`Background`] colors from the base and text colors.
87    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/// A color sample in a palette of colors.
110#[derive(Debug, Clone, Copy, PartialEq)]
111pub struct Swatch {
112    /// The base color.
113    pub base: Pair,
114    /// A weaker version of the base color.
115    pub weak: Pair,
116    /// A stronger version of the base color.
117    pub strong: Pair,
118}
119
120impl Swatch {
121    /// Generates a [`Swatch`] from a base, background and text color.
122    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    /// Derives a [`Swatch`] from a base color and text color.
134    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/// The base set of colors of a [`Palette`].
150#[derive(Debug, Clone, Copy, PartialEq)]
151#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
152pub struct Seed {
153    /// The background [`Color`] of the [`Palette`].
154    pub background: Color,
155    /// The text [`Color`] of the [`Palette`].
156    pub text: Color,
157    /// The primary [`Color`] of the [`Palette`].
158    pub primary: Color,
159    /// The success [`Color`] of the [`Palette`].
160    pub success: Color,
161    /// The warning [`Color`] of the [`Palette`].
162    pub warning: Color,
163    /// The danger [`Color`] of the [`Palette`].
164    pub danger: Color,
165}
166
167impl Seed {
168    /// The built-in light variant of a [`Palette`].
169    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    /// The built-in dark variant of a [`Palette`].
179    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    /// The built-in [Dracula] variant of a [`Palette`].
189    ///
190    /// [Dracula]: https://draculatheme.com
191    pub const DRACULA: Self = Self {
192        background: color!(0x282A36), // BACKGROUND
193        text: color!(0xf8f8f2),       // FOREGROUND
194        primary: color!(0xbd93f9),    // PURPLE
195        success: color!(0x50fa7b),    // GREEN
196        warning: color!(0xf1fa8c),    // YELLOW
197        danger: color!(0xff5555),     // RED
198    };
199
200    /// The built-in [Nord] variant of a [`Palette`].
201    ///
202    /// [Nord]: https://www.nordtheme.com/docs/colors-and-palettes
203    pub const NORD: Self = Self {
204        background: color!(0x2e3440), // nord0
205        text: color!(0xeceff4),       // nord6
206        primary: color!(0x8fbcbb),    // nord7
207        success: color!(0xa3be8c),    // nord14
208        warning: color!(0xebcb8b),    // nord13
209        danger: color!(0xbf616a),     // nord11
210    };
211
212    /// The built-in [Solarized] Light variant of a [`Palette`].
213    ///
214    /// [Solarized]: https://ethanschoonover.com/solarized
215    pub const SOLARIZED_LIGHT: Self = Self {
216        background: color!(0xfdf6e3), // base3
217        text: color!(0x657b83),       // base00
218        primary: color!(0x2aa198),    // cyan
219        success: color!(0x859900),    // green
220        warning: color!(0xb58900),    // yellow
221        danger: color!(0xdc322f),     // red
222    };
223
224    /// The built-in [Solarized] Dark variant of a [`Palette`].
225    ///
226    /// [Solarized]: https://ethanschoonover.com/solarized
227    pub const SOLARIZED_DARK: Self = Self {
228        background: color!(0x002b36), // base03
229        text: color!(0x839496),       // base0
230        primary: color!(0x2aa198),    // cyan
231        success: color!(0x859900),    // green
232        warning: color!(0xb58900),    // yellow
233        danger: color!(0xdc322f),     // red
234    };
235
236    /// The built-in [Gruvbox] Light variant of a [`Palette`].
237    ///
238    /// [Gruvbox]: https://github.com/morhetz/gruvbox
239    pub const GRUVBOX_LIGHT: Self = Self {
240        background: color!(0xfbf1c7), // light BG_0
241        text: color!(0x282828),       // light FG0_29
242        primary: color!(0x458588),    // light BLUE_4
243        success: color!(0x98971a),    // light GREEN_2
244        warning: color!(0xd79921),    // light YELLOW_3
245        danger: color!(0xcc241d),     // light RED_1
246    };
247
248    /// The built-in [Gruvbox] Dark variant of a [`Palette`].
249    ///
250    /// [Gruvbox]: https://github.com/morhetz/gruvbox
251    pub const GRUVBOX_DARK: Self = Self {
252        background: color!(0x282828), // dark BG_0
253        text: color!(0xfbf1c7),       // dark FG0_29
254        primary: color!(0x458588),    // dark BLUE_4
255        success: color!(0x98971a),    // dark GREEN_2
256        warning: color!(0xd79921),    // dark YELLOW_3
257        danger: color!(0xcc241d),     // dark RED_1
258    };
259
260    /// The built-in [Catppuccin] Latte variant of a [`Palette`].
261    ///
262    /// [Catppuccin]: https://github.com/catppuccin/catppuccin
263    pub const CATPPUCCIN_LATTE: Self = Self {
264        background: color!(0xeff1f5), // Base
265        text: color!(0x4c4f69),       // Text
266        primary: color!(0x1e66f5),    // Blue
267        success: color!(0x40a02b),    // Green
268        warning: color!(0xdf8e1d),    // Yellow
269        danger: color!(0xd20f39),     // Red
270    };
271
272    /// The built-in [Catppuccin] Frappé variant of a [`Palette`].
273    ///
274    /// [Catppuccin]: https://github.com/catppuccin/catppuccin
275    pub const CATPPUCCIN_FRAPPE: Self = Self {
276        background: color!(0x303446), // Base
277        text: color!(0xc6d0f5),       // Text
278        primary: color!(0x8caaee),    // Blue
279        success: color!(0xa6d189),    // Green
280        warning: color!(0xe5c890),    // Yellow
281        danger: color!(0xe78284),     // Red
282    };
283
284    /// The built-in [Catppuccin] Macchiato variant of a [`Palette`].
285    ///
286    /// [Catppuccin]: https://github.com/catppuccin/catppuccin
287    pub const CATPPUCCIN_MACCHIATO: Self = Self {
288        background: color!(0x24273a), // Base
289        text: color!(0xcad3f5),       // Text
290        primary: color!(0x8aadf4),    // Blue
291        success: color!(0xa6da95),    // Green
292        warning: color!(0xeed49f),    // Yellow
293        danger: color!(0xed8796),     // Red
294    };
295
296    /// The built-in [Catppuccin] Mocha variant of a [`Palette`].
297    ///
298    /// [Catppuccin]: https://github.com/catppuccin/catppuccin
299    pub const CATPPUCCIN_MOCHA: Self = Self {
300        background: color!(0x1e1e2e), // Base
301        text: color!(0xcdd6f4),       // Text
302        primary: color!(0x89b4fa),    // Blue
303        success: color!(0xa6e3a1),    // Green
304        warning: color!(0xf9e2af),    // Yellow
305        danger: color!(0xf38ba8),     // Red
306    };
307
308    /// The built-in [Tokyo Night] variant of a [`Palette`].
309    ///
310    /// [Tokyo Night]: https://github.com/enkia/tokyo-night-vscode-theme
311    pub const TOKYO_NIGHT: Self = Self {
312        background: color!(0x1a1b26), // Background (Night)
313        text: color!(0x9aa5ce),       // Text
314        primary: color!(0x2ac3de),    // Blue
315        success: color!(0x9ece6a),    // Green
316        warning: color!(0xe0af68),    // Yellow
317        danger: color!(0xf7768e),     // Red
318    };
319
320    /// The built-in [Tokyo Night] Storm variant of a [`Palette`].
321    ///
322    /// [Tokyo Night]: https://github.com/enkia/tokyo-night-vscode-theme
323    pub const TOKYO_NIGHT_STORM: Self = Self {
324        background: color!(0x24283b), // Background (Storm)
325        text: color!(0x9aa5ce),       // Text
326        primary: color!(0x2ac3de),    // Blue
327        success: color!(0x9ece6a),    // Green
328        warning: color!(0xe0af68),    // Yellow
329        danger: color!(0xf7768e),     // Red
330    };
331
332    /// The built-in [Tokyo Night] Light variant of a [`Palette`].
333    ///
334    /// [Tokyo Night]: https://github.com/enkia/tokyo-night-vscode-theme
335    pub const TOKYO_NIGHT_LIGHT: Self = Self {
336        background: color!(0xd5d6db), // Background
337        text: color!(0x565a6e),       // Text
338        primary: color!(0x166775),    // Blue
339        success: color!(0x485e30),    // Green
340        warning: color!(0x8f5e15),    // Yellow
341        danger: color!(0x8c4351),     // Red
342    };
343
344    /// The built-in [Kanagawa] Wave variant of a [`Palette`].
345    ///
346    /// [Kanagawa]: https://github.com/rebelot/kanagawa.nvim
347    pub const KANAGAWA_WAVE: Self = Self {
348        background: color!(0x1f1f28), // Sumi Ink 3
349        text: color!(0xDCD7BA),       // Fuji White
350        primary: color!(0x7FB4CA),    // Wave Blue
351        success: color!(0x76946A),    // Autumn Green
352        warning: color!(0xff9e3b),    // Ronin Yellow
353        danger: color!(0xC34043),     // Autumn Red
354    };
355
356    /// The built-in [Kanagawa] Dragon variant of a [`Palette`].
357    ///
358    /// [Kanagawa]: https://github.com/rebelot/kanagawa.nvim
359    pub const KANAGAWA_DRAGON: Self = Self {
360        background: color!(0x181616), // Dragon Black 3
361        text: color!(0xc5c9c5),       // Dragon White
362        primary: color!(0x223249),    // Wave Blue 1
363        success: color!(0x8a9a7b),    // Dragon Green 2
364        warning: color!(0xff9e3b),    // Ronin Yellow
365        danger: color!(0xc4746e),     // Dragon Red
366    };
367
368    /// The built-in [Kanagawa] Lotus variant of a [`Palette`].
369    ///
370    /// [Kanagawa]: https://github.com/rebelot/kanagawa.nvim
371    pub const KANAGAWA_LOTUS: Self = Self {
372        background: color!(0xf2ecbc), // Lotus White 3
373        text: color!(0x545464),       // Lotus Ink 1
374        primary: color!(0x4d699b),    // Lotus Blue
375        success: color!(0x6f894e),    // Lotus Green
376        warning: color!(0xe98a00),    // Lotus Orange 2
377        danger: color!(0xc84053),     // Lotus Red
378    };
379
380    /// The built-in [Moonfly] variant of a [`Palette`].
381    ///
382    /// [Moonfly]: https://github.com/bluz71/vim-moonfly-colors
383    pub const MOONFLY: Self = Self {
384        background: color!(0x080808), // Background
385        text: color!(0xbdbdbd),       // Foreground
386        primary: color!(0x80a0ff),    // Blue (normal)
387        success: color!(0x8cc85f),    // Green (normal)
388        warning: color!(0xe3c78a),    // Yellow (normal)
389        danger: color!(0xff5454),     // Red (normal)
390    };
391
392    /// The built-in [Nightfly] variant of a [`Palette`].
393    ///
394    /// [Nightfly]: https://github.com/bluz71/vim-nightfly-colors
395    pub const NIGHTFLY: Self = Self {
396        background: color!(0x011627), // Background
397        text: color!(0xbdc1c6),       // Foreground
398        primary: color!(0x82aaff),    // Blue (normal)
399        success: color!(0xa1cd5e),    // Green (normal)
400        warning: color!(0xe3d18a),    // Yellow (normal)
401        danger: color!(0xfc514e),     // Red (normal)
402    };
403
404    /// The built-in [Oxocarbon] variant of a [`Palette`].
405    ///
406    /// [Oxocarbon]: https://github.com/nyoom-engineering/oxocarbon.nvim
407    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), // Base 14
413        danger: color!(0xf62d0f),
414    };
415
416    /// The built-in [Ferra] variant of a [`Palette`].
417    ///
418    /// [Ferra]: https://github.com/casperstorm/ferra
419    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), // Honey
425        danger: color!(0xe06b75),
426    };
427}
428
429/// The built-in light variant of a [`Palette`].
430pub static LIGHT: LazyLock<Palette> = LazyLock::new(|| Palette::generate(Seed::LIGHT));
431
432/// The built-in dark variant of a [`Palette`].
433pub static DARK: LazyLock<Palette> = LazyLock::new(|| Palette::generate(Seed::DARK));
434
435/// The built-in Dracula variant of a [`Palette`].
436pub static DRACULA: LazyLock<Palette> = LazyLock::new(|| Palette::generate(Seed::DRACULA));
437
438/// The built-in Nord variant of a [`Palette`].
439pub static NORD: LazyLock<Palette> = LazyLock::new(|| Palette::generate(Seed::NORD));
440
441/// The built-in Solarized Light variant of a [`Palette`].
442pub static SOLARIZED_LIGHT: LazyLock<Palette> =
443    LazyLock::new(|| Palette::generate(Seed::SOLARIZED_LIGHT));
444
445/// The built-in Solarized Dark variant of a [`Palette`].
446pub static SOLARIZED_DARK: LazyLock<Palette> =
447    LazyLock::new(|| Palette::generate(Seed::SOLARIZED_DARK));
448
449/// The built-in Gruvbox Light variant of a [`Palette`].
450pub static GRUVBOX_LIGHT: LazyLock<Palette> =
451    LazyLock::new(|| Palette::generate(Seed::GRUVBOX_LIGHT));
452
453/// The built-in Gruvbox Dark variant of a [`Palette`].
454pub static GRUVBOX_DARK: LazyLock<Palette> =
455    LazyLock::new(|| Palette::generate(Seed::GRUVBOX_DARK));
456
457/// The built-in Catppuccin Latte variant of a [`Palette`].
458pub static CATPPUCCIN_LATTE: LazyLock<Palette> =
459    LazyLock::new(|| Palette::generate(Seed::CATPPUCCIN_LATTE));
460
461/// The built-in Catppuccin Frappé variant of a [`Palette`].
462pub static CATPPUCCIN_FRAPPE: LazyLock<Palette> =
463    LazyLock::new(|| Palette::generate(Seed::CATPPUCCIN_FRAPPE));
464
465/// The built-in Catppuccin Macchiato variant of a [`Palette`].
466pub static CATPPUCCIN_MACCHIATO: LazyLock<Palette> =
467    LazyLock::new(|| Palette::generate(Seed::CATPPUCCIN_MACCHIATO));
468
469/// The built-in Catppuccin Mocha variant of a [`Palette`].
470pub static CATPPUCCIN_MOCHA: LazyLock<Palette> =
471    LazyLock::new(|| Palette::generate(Seed::CATPPUCCIN_MOCHA));
472
473/// The built-in Tokyo Night variant of a [`Palette`].
474pub static TOKYO_NIGHT: LazyLock<Palette> = LazyLock::new(|| Palette::generate(Seed::TOKYO_NIGHT));
475
476/// The built-in Tokyo Night Storm variant of a [`Palette`].
477pub static TOKYO_NIGHT_STORM: LazyLock<Palette> =
478    LazyLock::new(|| Palette::generate(Seed::TOKYO_NIGHT_STORM));
479
480/// The built-in Tokyo Night variant of a [`Palette`].
481pub static TOKYO_NIGHT_LIGHT: LazyLock<Palette> =
482    LazyLock::new(|| Palette::generate(Seed::TOKYO_NIGHT_LIGHT));
483
484/// The built-in Kanagawa Wave variant of a [`Palette`].
485pub static KANAGAWA_WAVE: LazyLock<Palette> =
486    LazyLock::new(|| Palette::generate(Seed::KANAGAWA_WAVE));
487
488/// The built-in Kanagawa Dragon variant of a [`Palette`].
489pub static KANAGAWA_DRAGON: LazyLock<Palette> =
490    LazyLock::new(|| Palette::generate(Seed::KANAGAWA_DRAGON));
491
492/// The built-in Kanagawa Lotus variant of a [`Palette`].
493pub static KANAGAWA_LOTUS: LazyLock<Palette> =
494    LazyLock::new(|| Palette::generate(Seed::KANAGAWA_LOTUS));
495
496/// The built-in Moonfly variant of a [`Palette`].
497pub static MOONFLY: LazyLock<Palette> = LazyLock::new(|| Palette::generate(Seed::MOONFLY));
498
499/// The built-in Nightfly variant of a [`Palette`].
500pub static NIGHTFLY: LazyLock<Palette> = LazyLock::new(|| Palette::generate(Seed::NIGHTFLY));
501
502/// The built-in Oxocarbon variant of a [`Palette`].
503pub static OXOCARBON: LazyLock<Palette> = LazyLock::new(|| Palette::generate(Seed::OXOCARBON));
504
505/// The built-in Ferra variant of a [`Palette`].
506pub static FERRA: LazyLock<Palette> = LazyLock::new(|| Palette::generate(Seed::FERRA));
507
508/// Darkens a [`Color`] by the given factor.
509pub fn darken(color: Color, amount: f32) -> Color {
510    let mut oklch = color.into_oklch();
511
512    // We try to bump the chroma a bit for more colorful palettes
513    if oklch.c > 0.0 && oklch.c < (1.0 - oklch.l) / 2.0 {
514        // Formula empirically and cluelessly derived
515        oklch.c *= 1.0 + (0.2 / oklch.c).min(100.0) * amount;
516    }
517
518    oklch.l = if oklch.l - amount < 0.0 {
519        0.0
520    } else {
521        oklch.l - amount
522    };
523
524    Color::from_oklch(oklch)
525}
526
527/// Lightens a [`Color`] by the given factor.
528pub fn lighten(color: Color, amount: f32) -> Color {
529    let mut oklch = color.into_oklch();
530
531    // We try to bump the chroma a bit for more colorful palettes
532    // Formula empirically and cluelessly derived
533    oklch.c *= 1.0 + 2.0 * amount / oklch.l.max(0.05);
534
535    oklch.l = if oklch.l + amount > 1.0 {
536        1.0
537    } else {
538        oklch.l + amount
539    };
540
541    Color::from_oklch(oklch)
542}
543
544/// Deviates a [`Color`] by the given factor. Lightens if the [`Color`] is
545/// dark, darkens otherwise.
546pub fn deviate(color: Color, amount: f32) -> Color {
547    if is_dark(color) {
548        lighten(color, amount)
549    } else {
550        darken(color, amount)
551    }
552}
553
554/// Computes a [`Color`] from the given text color that is
555/// readable on top of the given background color.
556pub fn readable(background: Color, text: Color) -> Color {
557    if text.is_readable_on(background) {
558        return text;
559    }
560
561    let improve = if is_dark(background) { lighten } else { darken };
562
563    // TODO: Compute factor from relative contrast value
564    let candidate = improve(text, 0.1);
565
566    if candidate.is_readable_on(background) {
567        return candidate;
568    }
569
570    let candidate = improve(text, 0.2);
571
572    if candidate.is_readable_on(background) {
573        return candidate;
574    }
575
576    let white_contrast = background.relative_contrast(Color::WHITE);
577    let black_contrast = background.relative_contrast(Color::BLACK);
578
579    if white_contrast >= black_contrast {
580        Color::WHITE.mix(background, 0.05)
581    } else {
582        Color::BLACK.mix(background, 0.05)
583    }
584}
585
586/// Returns true if the [`Color`] is dark.
587pub fn is_dark(color: Color) -> bool {
588    color.into_oklch().l < 0.6
589}