1use gpui::Hsla;
11use herogpui_core::{mix_oklab, oklch, soft_mix, with_alpha};
12
13pub fn white() -> Hsla {
19 oklch(1.0, 0.0, 0.0)
20}
21pub fn black() -> Hsla {
23 oklch(0.0, 0.0, 0.0)
24}
25pub fn snow() -> Hsla {
27 oklch(0.9911, 0.0, 0.0)
28}
29pub fn eclipse() -> Hsla {
31 oklch(0.2103, 0.0059, 285.89)
32}
33
34#[derive(Clone, Copy, Debug)]
36pub enum SoftForeground {
37 RoleForeground,
39 Mix { color: f32, foreground: f32 },
42}
43
44#[derive(Clone, Copy, Debug)]
49pub struct RoleColor {
50 pub color: Hsla,
52 pub foreground: Hsla,
54 pub hover_mix: f32,
57 hover_override: Option<Hsla>,
65 soft_mix: f32,
67 soft_hover_mix: f32,
69 soft_foreground: SoftForeground,
71 vibrant_palette: bool,
77}
78
79impl RoleColor {
80 pub fn new(color: Hsla, foreground: Hsla) -> Self {
81 Self {
82 color,
83 foreground,
84 hover_mix: 0.10,
85 hover_override: None,
86 soft_mix: 0.15,
87 soft_hover_mix: 0.20,
88 soft_foreground: SoftForeground::Mix {
89 color: 70.0,
90 foreground: 30.0,
91 },
92 vibrant_palette: false,
93 }
94 }
95
96 pub fn with_hover_mix(mut self, hover_mix: f32) -> Self {
100 self.hover_mix = hover_mix;
101 self
102 }
103
104 pub fn with_hover(mut self, hover: Hsla) -> Self {
117 self.hover_override = Some(hover);
118 self
119 }
120
121 pub fn hover_override(&self) -> Option<Hsla> {
123 self.hover_override
124 }
125
126 pub fn with_soft_mix(mut self, soft: f32, soft_hover: f32) -> Self {
130 self.soft_mix = soft;
131 self.soft_hover_mix = soft_hover;
132 self
133 }
134
135 pub fn with_soft_foreground_mix(mut self, color: f32, foreground: f32) -> Self {
137 self.soft_foreground = SoftForeground::Mix { color, foreground };
138 self
139 }
140
141 pub fn with_vibrant_palette(mut self, vibrant: bool) -> Self {
145 self.vibrant_palette = vibrant;
146 self
147 }
148
149 pub fn is_vibrant_palette(&self) -> bool {
151 self.vibrant_palette
152 }
153
154 pub fn with_soft_foreground_role(mut self) -> Self {
156 self.soft_foreground = SoftForeground::RoleForeground;
157 self
158 }
159
160 pub fn hover(&self) -> Hsla {
164 self.hover_override
165 .unwrap_or_else(|| mix_oklab(self.color, self.foreground, self.hover_mix))
166 }
167
168 pub fn soft(&self) -> Hsla {
170 soft_mix(self.color, self.soft_mix)
171 }
172
173 pub fn soft_hover(&self) -> Hsla {
175 soft_mix(self.color, self.soft_hover_mix)
176 }
177
178 pub fn soft_foreground(&self, page_foreground: Hsla) -> Hsla {
184 match self.soft_foreground {
185 SoftForeground::RoleForeground => self.foreground,
186 SoftForeground::Mix { .. } if self.vibrant_palette => {
189 mix_oklab(self.color, page_foreground, 8.0 / 100.0)
190 }
191 SoftForeground::Mix { color, foreground } => {
192 mix_oklab(
196 self.color,
197 page_foreground,
198 foreground / (color + foreground),
199 )
200 }
201 }
202 }
203
204 pub fn with_alpha(&self, alpha: f32) -> Hsla {
205 with_alpha(self.color, alpha)
206 }
207}
208
209#[derive(Clone, Copy, Debug)]
211pub struct SurfaceColor {
212 pub background: Hsla,
213 pub foreground: Hsla,
214}
215
216impl SurfaceColor {
217 pub fn hover(&self) -> Hsla {
219 mix_oklab(self.background, self.foreground, 0.08)
220 }
221}
222
223#[derive(Clone, Copy, Debug)]
226#[non_exhaustive]
227pub struct FieldColors {
228 pub background: Hsla,
230 pub foreground: Hsla,
232 pub placeholder: Hsla,
234 pub border: Hsla,
236}
237
238impl FieldColors {
239 pub fn hover(&self) -> Hsla {
243 mix_oklab(self.background, self.foreground, 2.0 / 92.0)
244 }
245
246 pub fn focus(&self) -> Hsla {
248 self.background
249 }
250
251 pub fn border_hover(&self) -> Hsla {
257 mix_oklab(self.border, self.foreground, 10.0 / 98.0)
258 }
259
260 pub fn border_focus(&self) -> Hsla {
262 mix_oklab(self.border, self.foreground, 22.0 / 96.0)
263 }
264}
265
266#[derive(Clone, Debug)]
268#[non_exhaustive]
269pub struct ThemeColors {
270 pub background: Hsla,
273 pub foreground: Hsla,
275 pub muted: Hsla,
277 pub scrollbar: Hsla,
279
280 pub surface: SurfaceColor,
284 pub surface_secondary: Hsla,
286 pub surface_tertiary: Hsla,
288 pub overlay: SurfaceColor,
291 pub segment: SurfaceColor,
294
295 pub default: RoleColor,
298 pub accent: RoleColor,
300 pub success: RoleColor,
302 pub warning: RoleColor,
304 pub danger: RoleColor,
306
307 pub field: FieldColors,
309
310 pub border: Hsla,
313 pub separator: Hsla,
315 pub focus: Hsla,
317 pub link: Hsla,
319 pub backdrop: Hsla,
321}
322
323impl ThemeColors {
324 pub fn vibrant_palette(&self) -> bool {
329 self.accent.is_vibrant_palette()
330 }
331
332 pub fn set_vibrant_palette(&mut self, vibrant: bool) {
336 self.accent = self.accent.with_vibrant_palette(vibrant);
337 self.success = self.success.with_vibrant_palette(vibrant);
338 self.warning = self.warning.with_vibrant_palette(vibrant);
339 self.danger = self.danger.with_vibrant_palette(vibrant);
340 }
341
342 pub fn background_secondary(&self) -> Hsla {
346 mix_oklab(self.background, self.foreground, 0.04)
347 }
348
349 pub fn background_tertiary(&self) -> Hsla {
351 mix_oklab(self.background, self.foreground, 0.08)
352 }
353
354 pub fn background_inverse(&self) -> Hsla {
356 self.foreground
357 }
358
359 pub fn separator_secondary(&self) -> Hsla {
363 mix_oklab(self.surface.background, self.surface.foreground, 0.15)
364 }
365
366 pub fn separator_tertiary(&self) -> Hsla {
368 mix_oklab(self.surface.background, self.surface.foreground, 0.19)
369 }
370
371 pub fn border_secondary(&self) -> Hsla {
375 mix_oklab(self.surface.background, self.surface.foreground, 0.22)
376 }
377
378 pub fn border_tertiary(&self) -> Hsla {
380 mix_oklab(self.surface.background, self.surface.foreground, 0.34)
381 }
382
383 pub fn surface_secondary_foreground(&self) -> Hsla {
389 self.foreground
390 }
391
392 pub fn surface_tertiary_foreground(&self) -> Hsla {
394 self.foreground
395 }
396
397 pub fn role(&self, name: &str) -> &RoleColor {
399 match name {
400 "default" => &self.default,
401 "success" => &self.success,
402 "warning" => &self.warning,
403 "danger" => &self.danger,
404 _ => &self.accent,
405 }
406 }
407
408 pub fn light() -> Self {
411 let foreground = eclipse();
412 let muted = oklch(0.5517, 0.0138, 285.94);
413 let accent = RoleColor::new(oklch(0.6204, 0.195, 253.83), snow())
414 .with_soft_mix(0.15, 0.20)
415 .with_soft_foreground_mix(70.0, 30.0);
416 Self {
417 background: oklch(0.9702, 0.0, 0.0),
418 foreground,
419 muted,
420 scrollbar: with_alpha(foreground, 0.15),
421
422 surface: SurfaceColor {
423 background: white(),
424 foreground,
425 },
426 surface_secondary: oklch(0.9524, 0.0013, 286.37),
427 surface_tertiary: oklch(0.9373, 0.0013, 286.37),
428 overlay: SurfaceColor {
429 background: white(),
430 foreground,
431 },
432 segment: SurfaceColor {
433 background: white(),
434 foreground: eclipse(),
435 },
436
437 default: RoleColor::new(oklch(0.94, 0.001, 286.375), eclipse())
438 .with_hover_mix(0.04)
439 .with_soft_mix(0.50, 0.60)
440 .with_soft_foreground_role(),
441 accent,
442 success: RoleColor::new(oklch(0.7329, 0.1935, 150.81), eclipse())
443 .with_soft_foreground_mix(80.0, 60.0),
444 warning: RoleColor::new(oklch(0.7819, 0.1585, 72.33), eclipse())
445 .with_soft_foreground_mix(80.0, 70.0),
446 danger: RoleColor::new(oklch(0.6532, 0.2328, 25.74), snow())
447 .with_soft_foreground_mix(70.0, 40.0),
448
449 field: FieldColors {
450 background: white(),
451 foreground: oklch(0.2103, 0.0059, 285.89),
452 placeholder: muted,
453 border: with_alpha(black(), 0.0),
454 },
455
456 border: oklch(0.9, 0.004, 286.32),
459 separator: oklch(0.92, 0.004, 286.32),
460 focus: accent.color,
461 link: foreground,
462 backdrop: with_alpha(black(), 0.5),
463 }
464 }
465
466 pub fn dark() -> Self {
469 let foreground = snow();
470 let muted = oklch(0.705, 0.015, 286.067);
471 let accent = RoleColor::new(oklch(0.6204, 0.195, 253.83), snow())
472 .with_soft_mix(0.12, 0.16)
473 .with_soft_foreground_mix(80.0, 30.0);
474 let default = RoleColor::new(oklch(0.274, 0.006, 286.033), snow())
475 .with_hover_mix(0.04)
476 .with_soft_mix(0.50, 0.60)
477 .with_soft_foreground_role();
478 Self {
479 background: oklch(0.12, 0.005, 285.823),
480 foreground,
481 muted,
482 scrollbar: with_alpha(foreground, 0.15),
483
484 surface: SurfaceColor {
485 background: oklch(0.2103, 0.0059, 285.89),
486 foreground,
487 },
488 surface_secondary: oklch(0.257, 0.0037, 286.14),
489 surface_tertiary: oklch(0.2721, 0.0024, 247.91),
490 overlay: SurfaceColor {
495 background: oklch(0.2103, 0.0059, 285.89),
496 foreground,
497 },
498 segment: SurfaceColor {
499 background: oklch(0.3964, 0.01, 285.93),
500 foreground,
501 },
502
503 default,
504 accent,
505 success: RoleColor::new(oklch(0.7329, 0.1935, 150.81), eclipse())
508 .with_soft_mix(0.12, 0.16)
509 .with_soft_foreground_mix(80.0, 30.0),
510 warning: RoleColor::new(oklch(0.8203, 0.1388, 76.34), eclipse())
511 .with_soft_mix(0.12, 0.16)
512 .with_soft_foreground_mix(80.0, 30.0),
513 danger: RoleColor::new(oklch(0.594, 0.1967, 24.63), snow())
514 .with_soft_foreground_mix(80.0, 30.0),
515
516 field: FieldColors {
517 background: oklch(0.2103, 0.0059, 285.89),
520 foreground,
521 placeholder: muted,
522 border: with_alpha(black(), 0.0),
523 },
524
525 border: oklch(0.28, 0.006, 286.033),
528 separator: oklch(0.25, 0.006, 286.033),
529 focus: accent.color,
530 link: foreground,
531 backdrop: with_alpha(black(), 0.6),
532 }
533 }
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539
540 fn rgb8(c: Hsla) -> (u8, u8, u8) {
541 let r = gpui::Rgba::from(c);
542 (
543 (r.r * 255.0).round() as u8,
544 (r.g * 255.0).round() as u8,
545 (r.b * 255.0).round() as u8,
546 )
547 }
548
549 #[test]
550 fn an_explicit_role_hover_replaces_the_derived_mix() {
551 let stock = ThemeColors::light();
552 let derived = stock.accent.hover();
553 let named = gpui::hsla(0.6, 0.9, 0.3, 1.0);
556 let role = stock.accent.with_hover(named);
557 assert_ne!(rgb8(derived), rgb8(named), "the fixture must differ");
558 assert_eq!(rgb8(role.hover()), rgb8(named));
559 assert_eq!(role.hover_override(), Some(named));
560 assert_eq!(rgb8(role.soft()), rgb8(stock.accent.soft()));
563 assert_eq!(rgb8(role.soft_hover()), rgb8(stock.accent.soft_hover()));
564 assert_eq!(rgb8(role.color), rgb8(stock.accent.color));
565 }
566
567 #[test]
568 fn a_named_role_hover_outranks_the_hover_mix_whichever_order_they_are_set() {
569 let base = ThemeColors::light().accent;
570 let named = gpui::hsla(0.1, 0.8, 0.4, 1.0);
571 assert_eq!(
572 rgb8(base.with_hover(named).with_hover_mix(0.5).hover()),
573 rgb8(named)
574 );
575 assert_eq!(
576 rgb8(base.with_hover_mix(0.5).with_hover(named).hover()),
577 rgb8(named)
578 );
579 }
580
581 #[test]
582 fn accent_soft_is_fifteen_percent_accent() {
583 let c = ThemeColors::light();
584 assert!((c.accent.soft().a - 0.15).abs() < 1e-4);
585 assert!((c.accent.soft_hover().a - 0.20).abs() < 1e-4);
586 }
587
588 #[test]
589 fn light_scrollbar_thumb_is_fifteen_percent_foreground() {
590 let c = ThemeColors::light();
591 assert_eq!(c.scrollbar, with_alpha(c.foreground, 0.15));
592 }
593
594 #[test]
595 fn dark_scrollbar_thumb_is_fifteen_percent_foreground() {
596 let c = ThemeColors::dark();
597 assert_eq!(c.scrollbar, with_alpha(c.foreground, 0.15));
598 }
599
600 #[test]
601 fn the_soft_matrix_matches_the_pinned_stylesheet() {
602 let light = ThemeColors::light();
606 let dark = ThemeColors::dark();
607 for (colors, role_name, soft, soft_hover, foreground) in [
608 (&light, "default", 0.50, 0.60, None),
609 (&light, "accent", 0.15, 0.20, Some((70.0, 30.0))),
610 (&light, "success", 0.15, 0.20, Some((80.0, 60.0))),
611 (&light, "warning", 0.15, 0.20, Some((80.0, 70.0))),
612 (&light, "danger", 0.15, 0.20, Some((70.0, 40.0))),
613 (&dark, "default", 0.50, 0.60, None),
614 (&dark, "accent", 0.12, 0.16, Some((80.0, 30.0))),
615 (&dark, "success", 0.12, 0.16, Some((80.0, 30.0))),
616 (&dark, "warning", 0.12, 0.16, Some((80.0, 30.0))),
617 (&dark, "danger", 0.15, 0.20, Some((80.0, 30.0))),
618 ] {
619 let role = colors.role(role_name);
620 assert!(
621 (role.soft().a - soft).abs() < 1e-4,
622 "{role_name} soft alpha"
623 );
624 assert!(
625 (role.soft_hover().a - soft_hover).abs() < 1e-4,
626 "{role_name} soft-hover alpha"
627 );
628 match foreground {
629 Some((color, page)) => assert_eq!(
630 role.soft_foreground(colors.foreground),
631 mix_oklab(role.color, colors.foreground, page / (color + page)),
632 "{role_name} soft-foreground mix"
633 ),
634 None => assert_eq!(
635 role.soft_foreground(colors.foreground),
636 role.foreground,
637 "{role_name} soft-foreground follows the role"
638 ),
639 }
640 }
641 }
642
643 #[test]
648 fn every_token_resolves_to_the_stylesheets_own_pixels() {
649 type Pick = fn(&ThemeColors) -> Hsla;
650 type Row = (&'static str, Pick, (u8, u8, u8, f32), (u8, u8, u8, f32));
652 #[rustfmt::skip]
653 let table: &[Row] = &[
654 ("accent", |c| c.accent.color, (4, 133, 247, 1.00), (4, 133, 247, 1.00)),
655 ("accent_foreground", |c| c.accent.foreground, (252, 252, 252, 1.00), (252, 252, 252, 1.00)),
656 ("accent_hover", |c| c.accent.hover(), (53, 146, 249, 1.00), (53, 146, 249, 1.00)),
657 ("accent_soft", |c| c.accent.soft(), (4, 133, 247, 0.15), (4, 133, 247, 0.12)),
658 ("accent_soft_foreground", |c| c.accent.soft_foreground(c.foreground), (30, 99, 174, 1.00), (97, 168, 251, 1.00)),
659 ("backdrop", |c| c.backdrop, (0, 0, 0, 0.50), (0, 0, 0, 0.60)),
660 ("background", |c| c.background, (245, 245, 245, 1.00), (6, 6, 7, 1.00)),
661 ("background_inverse", |c| c.background_inverse(), (24, 24, 27, 1.00), (252, 252, 252, 1.00)),
662 ("background_secondary", |c| c.background_secondary(), (235, 235, 235, 1.00), (12, 12, 14, 1.00)),
663 ("background_tertiary", |c| c.background_tertiary(), (225, 225, 225, 1.00), (19, 19, 22, 1.00)),
664 ("border", |c| c.border, (221, 222, 224, 1.00), (40, 40, 44, 1.00)),
665 ("border_secondary", |c| c.border_secondary(), (198, 198, 199, 1.00), (67, 67, 69, 1.00)),
666 ("border_tertiary", |c| c.border_tertiary(), (168, 168, 169, 1.00), (92, 92, 95, 1.00)),
667 ("danger", |c| c.danger.color, (255, 56, 60, 1.00), (219, 59, 62, 1.00)),
668 ("danger_foreground", |c| c.danger.foreground, (252, 252, 252, 1.00), (252, 252, 252, 1.00)),
669 ("danger_hover", |c| c.danger.hover(), (255, 85, 81, 1.00), (225, 84, 81, 1.00)),
670 ("danger_soft", |c| c.danger.soft(), (255, 56, 60, 0.15), (219, 59, 62, 0.15)),
671 ("danger_soft_foreground", |c| c.danger.soft_foreground(c.foreground), (164, 53, 51, 1.00), (235, 120, 114, 1.00)),
672 ("default", |c| c.default.color, (235, 235, 236, 1.00), (39, 39, 42, 1.00)),
673 ("default_foreground", |c| c.default.foreground, (24, 24, 27, 1.00), (252, 252, 252, 1.00)),
674 ("default_hover", |c| c.default.hover(), (225, 225, 226, 1.00), (46, 46, 49, 1.00)),
675 ("default_soft", |c| c.default.soft(), (235, 235, 235, 0.50), (39, 39, 42, 0.50)),
676 ("default_soft_foreground", |c| c.default.soft_foreground(c.foreground), (24, 24, 27, 1.00), (252, 252, 252, 1.00)),
677 ("field_background", |c| c.field.background, (255, 255, 255, 1.00), (24, 24, 27, 1.00)),
678 ("field_foreground", |c| c.field.foreground, (24, 24, 27, 1.00), (252, 252, 252, 1.00)),
679 ("field_placeholder", |c| c.field.placeholder, (113, 113, 122, 1.00), (159, 159, 169, 1.00)),
680 ("focus", |c| c.focus, (4, 133, 247, 1.00), (4, 133, 247, 1.00)),
681 ("foreground", |c| c.foreground, (24, 24, 27, 1.00), (252, 252, 252, 1.00)),
682 ("link", |c| c.link, (24, 24, 27, 1.00), (252, 252, 252, 1.00)),
683 ("muted", |c| c.muted, (113, 113, 122, 1.00), (159, 159, 169, 1.00)),
684 ("overlay", |c| c.overlay.background, (255, 255, 255, 1.00), (24, 24, 27, 1.00)),
685 ("overlay_foreground", |c| c.overlay.foreground, (24, 24, 27, 1.00), (252, 252, 252, 1.00)),
686 ("scrollbar", |c| c.scrollbar, (24, 24, 27, 0.15), (255, 255, 255, 0.15)),
687 ("segment", |c| c.segment.background, (255, 255, 255, 1.00), (70, 70, 76, 1.00)),
688 ("segment_foreground", |c| c.segment.foreground, (24, 24, 27, 1.00), (252, 252, 252, 1.00)),
689 ("separator", |c| c.separator, (228, 228, 231, 1.00), (33, 33, 36, 1.00)),
690 ("separator_secondary", |c| c.separator_secondary(), (216, 216, 216, 1.00), (52, 52, 55, 1.00)),
691 ("separator_tertiary", |c| c.separator_tertiary(), (205, 205, 206, 1.00), (60, 60, 63, 1.00)),
692 ("success", |c| c.success.color, (23, 201, 100, 1.00), (23, 201, 100, 1.00)),
693 ("success_foreground", |c| c.success.foreground, (24, 24, 27, 1.00), (24, 24, 27, 1.00)),
694 ("success_hover", |c| c.success.hover(), (33, 181, 93, 1.00), (33, 181, 93, 1.00)),
695 ("success_soft", |c| c.success.soft(), (23, 201, 100, 0.15), (23, 201, 100, 0.12)),
696 ("success_soft_foreground", |c| c.success.soft_foreground(c.foreground), (43, 119, 69, 1.00), (116, 216, 143, 1.00)),
697 ("surface", |c| c.surface.background, (255, 255, 255, 1.00), (24, 24, 27, 1.00)),
698 ("surface_foreground", |c| c.surface.foreground, (24, 24, 27, 1.00), (252, 252, 252, 1.00)),
699 ("surface_hover", |c| c.surface.hover(), (234, 234, 234, 1.00), (39, 39, 42, 1.00)),
700 ("surface_secondary", |c| c.surface_secondary, (239, 239, 240, 1.00), (35, 35, 37, 1.00)),
701 ("surface_tertiary", |c| c.surface_tertiary, (234, 234, 235, 1.00), (38, 39, 40, 1.00)),
702 ("warning", |c| c.warning.color, (245, 165, 36, 1.00), (247, 183, 80, 1.00)),
703 ("warning_foreground", |c| c.warning.foreground, (24, 24, 27, 1.00), (24, 24, 27, 1.00)),
704 ("warning_hover", |c| c.warning.hover(), (220, 150, 42, 1.00), (222, 165, 76, 1.00)),
705 ("warning_soft", |c| c.warning.soft(), (245, 165, 36, 0.15), (247, 183, 80, 0.12)),
706 ("warning_soft_foreground", |c| c.warning.soft_foreground(c.foreground), (133, 95, 46, 1.00), (249, 203, 134, 1.00)),
707 ];
708 for (name, pick, light, dark) in table {
709 for (mode, c, want) in [
710 ("light", ThemeColors::light(), light),
711 ("dark", ThemeColors::dark(), dark),
712 ] {
713 let got = pick(&c);
714 let (r, g, b) = rgb8(got);
715 let slack = (2.0 / want.3).ceil() as i32;
718 let near = |a: u8, b: u8| (a as i32 - b as i32).abs() <= slack;
719 assert!(
720 near(r, want.0) && near(g, want.1) && near(b, want.2)
721 && (got.a - want.3).abs() < 0.011,
722 "{name} ({mode}) resolved to ({r}, {g}, {b}, {:.3}), upstream resolves ({}, {}, {}, {:.3})",
723 got.a, want.0, want.1, want.2, want.3
724 );
725 }
726 }
727 }
728
729 #[test]
730 fn soft_foreground_resolves_to_the_stylesheets_own_pixels() {
731 let c = ThemeColors::light();
735 for (role_name, expected) in [
736 ("accent", (30, 99, 174)),
737 ("success", (43, 119, 69)),
738 ("warning", (133, 95, 46)),
739 ("danger", (164, 53, 51)),
740 ] {
741 let got = rgb8(c.role(role_name).soft_foreground(c.foreground));
742 let near = |a: u8, b: u8| (a as i32 - b as i32).abs() <= 2;
743 assert!(
744 near(got.0, expected.0) && near(got.1, expected.1) && near(got.2, expected.2),
745 "{role_name} soft-foreground was {got:?}, expected {expected:?}"
746 );
747 }
748 }
749
750 #[test]
751 fn a_custom_foreground_stays_live_in_the_soft_foreground_mix() {
752 let base = ThemeColors::light();
753 let custom = ThemeColors {
754 foreground: oklch(0.30, 0.05, 120.0),
755 ..base
756 };
757 assert_eq!(
760 custom.accent.soft_foreground(custom.foreground),
761 mix_oklab(custom.accent.color, custom.foreground, 0.30)
762 );
763 assert_ne!(
764 custom.accent.soft_foreground(custom.foreground),
765 base.accent.soft_foreground(base.foreground)
766 );
767 assert_eq!(
770 custom.default.soft_foreground(custom.foreground),
771 custom.default.foreground
772 );
773 }
774
775 #[test]
776 fn the_derived_borders_step_away_from_the_surface() {
777 let c = ThemeColors::light();
778 let steps = [
782 c.separator_secondary(),
783 c.border_secondary(),
784 c.border_tertiary(),
785 ];
786 for pair in steps.windows(2) {
787 assert!(
788 pair[1].l < pair[0].l,
789 "each step is darker than the last on a light surface"
790 );
791 }
792 }
793
794 #[test]
795 fn a_secondary_surface_keeps_the_page_foreground() {
796 let c = ThemeColors::light();
797 assert_eq!(c.surface_secondary_foreground(), c.foreground);
798 assert_eq!(c.surface_tertiary_foreground(), c.foreground);
799 }
800
801 #[test]
802 fn a_field_border_mixes_toward_its_own_foreground() {
803 let c = ThemeColors::light();
804 let border = c.field.border;
807 assert_ne!(c.field.border_hover(), border);
808 assert_ne!(c.field.border_focus(), c.field.border_hover());
809 }
810
811 #[test]
812 fn field_focus_matches_field_background() {
813 let c = ThemeColors::light();
814 assert_eq!(c.field.focus(), c.field.background);
815 }
816
817 #[test]
818 #[ignore = "v3 gives dark mode one colour for both; the shadow separates them"]
819 fn dark_surface_is_darker_than_overlay() {
820 let c = ThemeColors::dark();
821 assert!(c.surface.background.l < c.overlay.background.l);
822 }
823
824 #[test]
825 fn light_background_is_lighter_than_its_derived_levels() {
826 let c = ThemeColors::light();
827 assert!(c.background.l > c.background_secondary().l);
828 assert!(c.background_secondary().l > c.background_tertiary().l);
829 }
830}