Skip to main content

iced_core/theme/
palette.rs

1//! Define the colors of a theme.
2use crate::{Color, Shadow, Vector, 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
508struct Oklch {
509    l: f32,
510    c: f32,
511    h: f32,
512    a: f32,
513}
514
515/// Darkens a [`Color`] by the given factor.
516pub fn darken(color: Color, amount: f32) -> Color {
517    let mut oklch = to_oklch(color);
518
519    // We try to bump the chroma a bit for more colorful palettes
520    if oklch.c > 0.0 && oklch.c < (1.0 - oklch.l) / 2.0 {
521        // Formula empirically and cluelessly derived
522        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
534/// Lightens a [`Color`] by the given factor.
535pub fn lighten(color: Color, amount: f32) -> Color {
536    let mut oklch = to_oklch(color);
537
538    // We try to bump the chroma a bit for more colorful palettes
539    // Formula empirically and cluelessly derived
540    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
551/// Deviates a [`Color`] by the given factor. Lightens if the [`Color`] is
552/// dark, darkens otherwise.
553pub 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
561/// Computes a [`Color`] from the given text color that is
562/// readable on top of the given background color.
563pub 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    // TODO: Compute factor from relative contrast value
571    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
593/// Returns true if the [`Color`] is dark.
594pub fn is_dark(color: Color) -> bool {
595    to_oklch(color).l < 0.6
596}
597
598/// Returns the base keyboard-focus color for the current palette.
599///
600/// Chosen to contrast with the page background. All widgets share
601/// the same base color for visual consistency. Use
602/// [`focus_border_color`] to adjust the border for widgets whose
603/// background matches the base focus color.
604pub 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
614/// Returns a focus border color that contrasts with the widget's
615/// background while staying in the same color family as [`focus_color`].
616///
617/// When the base focus color already contrasts with the widget, it is
618/// returned unchanged. When they blend (e.g. accent-colored button
619/// with an accent focus color), the color is deviated (lightened or
620/// darkened) to create a visible border that still reads as the same
621/// hue.
622pub fn focus_border_color(widget_bg: Color, accent: Color, page_bg: Color) -> Color {
623    let base = focus_color(accent, page_bg);
624
625    // Transparent widgets sit on the page -- base already contrasts.
626    if widget_bg.a < 0.1 || widget_bg.relative_contrast(base) >= 1.5 {
627        return base;
628    }
629
630    // Deviate away from the widget background to create contrast
631    // while keeping the same hue family.
632    deviate(base, 0.3)
633}
634
635/// Builds a prominent [`Shadow`] for a keyboard-focus glow ring.
636///
637/// Designed for compact widgets (slider handles, radio buttons,
638/// checkboxes, togglers) where a subtle glow would be hard to see.
639/// Use [`focus_shadow_subtle`] for large widgets like text inputs
640/// and buttons where less blur is sufficient.
641pub 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
652/// A less prominent variant of [`focus_shadow`] for large widgets
653/// (buttons, text inputs, pick lists) where the glow doesn't need
654/// to compensate for a small surface area.
655pub 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
666// https://en.wikipedia.org/wiki/Oklab_color_space#Conversions_between_color_spaces
667fn to_oklch(color: Color) -> Oklch {
668    let [r, g, b, alpha] = color.into_linear();
669
670    // linear RGB → LMS
671    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    // Nonlinear transform (cube root)
676    let l_ = l.cbrt();
677    let m_ = m.cbrt();
678    let s_ = s.cbrt();
679
680    // LMS → Oklab
681    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    // Oklab → Oklch
686    let c = (a * a + b * b).sqrt();
687    let h = b.atan2(a); // radians
688
689    Oklch { l, c, h, a: alpha }
690}
691
692// https://en.wikipedia.org/wiki/Oklab_color_space#Conversions_between_color_spaces
693fn 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    // Oklab → LMS (nonlinear)
700    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    // Cubing back
705    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}