pub mod color;
mod detect;
mod labels;
use std::collections::HashMap;
use material_colors::blend::harmonize;
use material_colors::color::Rgb as McRgb;
use material_colors::dynamic_color::Variant;
use material_colors::theme::ThemeBuilder;
use ratatui::style::{Color, Modifier, Style};
pub use color::{Rgb, contrast};
pub use detect::{detect_background, detect_color_depth, mode_for_background};
pub const DEFAULT_SEED: Rgb = Rgb::from_u32(0x3f6fb5);
const MISSING: Rgb = Rgb::from_u32(0xff00ff);
pub const TEXT_CONTRAST: f64 = 4.5;
pub const DECORATIVE_CONTRAST: f64 = 1.2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Mode {
Light,
Dark,
}
impl Mode {
fn container_tones(self) -> (f64, f64) {
match self {
Mode::Dark => (30.0, 90.0),
Mode::Light => (90.0, 10.0),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ColorDepth {
#[serde(alias = "24bit")]
TrueColor,
#[serde(rename = "256")]
Ansi256,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Bg {
Surface,
ContainerLowest,
ContainerLow,
Container,
ContainerHigh,
ContainerHighest,
Selected,
SelectedInactive,
SelectedHigh,
Primary,
PrimaryContainer,
SecondaryContainer,
TertiaryContainer,
ErrorContainer,
SuccessContainer,
Diff(DiffBg),
DiffSelected(DiffBg),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DiffBg {
Context,
Added,
Removed,
AddedToken,
RemovedToken,
Moved,
}
impl DiffBg {
pub const ALL: [DiffBg; 6] = [
DiffBg::Context,
DiffBg::Added,
DiffBg::Removed,
DiffBg::AddedToken,
DiffBg::RemovedToken,
DiffBg::Moved,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Fg {
OnSurface,
OnSurfaceVariant,
Primary,
Tertiary,
Error,
Success,
Disabled,
OutlineVariant,
OnPrimary,
OnPrimaryContainer,
OnSecondaryContainer,
OnTertiaryContainer,
OnErrorContainer,
OnSuccessContainer,
DiffAddedSign,
DiffRemovedSign,
Syntax(Syntax),
Heat(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Syntax {
Default,
Keyword,
String,
Comment,
Function,
Type,
Number,
Constant,
Operator,
Punctuation,
Attribute,
Property,
}
impl Syntax {
pub const ALL: [Syntax; 12] = [
Syntax::Default,
Syntax::Keyword,
Syntax::String,
Syntax::Comment,
Syntax::Function,
Syntax::Type,
Syntax::Number,
Syntax::Constant,
Syntax::Operator,
Syntax::Punctuation,
Syntax::Attribute,
Syntax::Property,
];
}
impl Bg {
const NEUTRAL: [Bg; 9] = [
Bg::Surface,
Bg::ContainerLowest,
Bg::ContainerLow,
Bg::Container,
Bg::ContainerHigh,
Bg::ContainerHighest,
Bg::Selected,
Bg::SelectedInactive,
Bg::SelectedHigh,
];
const FILLED: [(Bg, Fg); 6] = [
(Bg::Primary, Fg::OnPrimary),
(Bg::PrimaryContainer, Fg::OnPrimaryContainer),
(Bg::SecondaryContainer, Fg::OnSecondaryContainer),
(Bg::TertiaryContainer, Fg::OnTertiaryContainer),
(Bg::ErrorContainer, Fg::OnErrorContainer),
(Bg::SuccessContainer, Fg::OnSuccessContainer),
];
const COUNT: usize = Bg::NEUTRAL.len() + Bg::FILLED.len() + 2 * DiffBg::ALL.len();
fn slot(self) -> Option<usize> {
let plain = Bg::NEUTRAL.len() + Bg::FILLED.len();
match self {
Bg::Diff(d) => Some(plain + d as usize),
Bg::DiffSelected(d) => Some(plain + DiffBg::ALL.len() + d as usize),
bg => (Bg::NEUTRAL.iter().position(|n| *n == bg)).or_else(|| {
let filled = Bg::FILLED.iter().position(|(f, _)| *f == bg)?;
Some(Bg::NEUTRAL.len() + filled)
}),
}
}
pub fn all() -> Vec<Bg> {
let mut all = Bg::NEUTRAL.to_vec();
all.extend(Bg::FILLED.map(|(bg, _)| bg));
all.extend(DiffBg::ALL.map(Bg::Diff));
all.extend(DiffBg::ALL.map(Bg::DiffSelected));
all
}
fn is_neutral(self) -> bool {
Bg::NEUTRAL.contains(&self)
}
fn is_diff(self) -> bool {
matches!(self, Bg::Diff(_) | Bg::DiffSelected(_))
}
}
impl Fg {
const PLAIN: [Fg; 16] = [
Fg::OnSurface,
Fg::OnSurfaceVariant,
Fg::Primary,
Fg::Tertiary,
Fg::Error,
Fg::Success,
Fg::Disabled,
Fg::OutlineVariant,
Fg::OnPrimary,
Fg::OnPrimaryContainer,
Fg::OnSecondaryContainer,
Fg::OnTertiaryContainer,
Fg::OnErrorContainer,
Fg::OnSuccessContainer,
Fg::DiffAddedSign,
Fg::DiffRemovedSign,
];
const COUNT: usize = Fg::PLAIN.len() + Syntax::ALL.len() + HEAT_LEVELS.len();
fn slot(self) -> Option<usize> {
let syntax = Fg::PLAIN.len();
match self {
Fg::Syntax(role) => Some(syntax + role as usize),
Fg::Heat(level) => (HEAT_LEVELS.iter().position(|l| *l == level))
.map(|i| syntax + Syntax::ALL.len() + i),
fg => Fg::PLAIN.iter().position(|p| *p == fg),
}
}
pub fn all() -> Vec<Fg> {
let mut all = Fg::PLAIN.to_vec();
all.extend(Syntax::ALL.map(Fg::Syntax));
all.extend(HEAT_LEVELS.map(Fg::Heat));
all
}
}
pub const HEAT_LEVELS: [u8; 5] = [0, 1, 2, 3, 4];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Requirement {
Text,
Decorative,
Exempt,
}
impl Requirement {
pub fn min_ratio(self) -> f64 {
match self {
Requirement::Text => TEXT_CONTRAST,
Requirement::Decorative => DECORATIVE_CONTRAST,
Requirement::Exempt => 1.0,
}
}
}
pub fn requirement(fg: Fg, bg: Bg) -> Option<Requirement> {
use Requirement::{Decorative, Exempt, Text};
match fg {
Fg::OnSurface | Fg::OnSurfaceVariant | Fg::DiffAddedSign | Fg::DiffRemovedSign
if bg.is_neutral() || bg.is_diff() =>
{
Some(Text)
}
Fg::Primary | Fg::Tertiary | Fg::Error | Fg::Success if bg.is_neutral() => Some(Text),
Fg::Success | Fg::Primary | Fg::Tertiary | Fg::Syntax(_) if bg.is_diff() => Some(Text),
Fg::Disabled if bg.is_neutral() || bg.is_diff() => Some(Exempt),
Fg::OutlineVariant if bg.is_neutral() => Some(Decorative),
Fg::Heat(_) if bg.is_neutral() => Some(Exempt),
_ if Bg::FILLED.contains(&(bg, fg)) => Some(Text),
_ => None,
}
}
#[derive(Debug, Clone, Copy)]
pub struct Pair {
pub fg: Fg,
pub bg: Bg,
pub requirement: Requirement,
pub fg_color: Rgb,
pub bg_color: Rgb,
}
impl Pair {
pub fn ratio(&self) -> f64 {
contrast(self.fg_color, self.bg_color)
}
pub fn passes(&self) -> bool {
self.ratio() >= self.requirement.min_ratio()
}
}
#[derive(Debug, Clone)]
pub struct Theme {
pub mode: Mode,
pub depth: ColorDepth,
bg: HashMap<Bg, Rgb>,
fg: HashMap<Fg, Rgb>,
bg_term: [Color; Bg::COUNT],
fg_term: [Color; Fg::COUNT],
}
impl Theme {
pub fn new(seed: Rgb, mode: Mode, depth: ColorDepth) -> Self {
let material = ThemeBuilder::with_source(seed.into())
.variant(Variant::TonalSpot)
.build();
let scheme = match mode {
Mode::Light => material.schemes.light,
Mode::Dark => material.schemes.dark,
};
let rgb = |c: McRgb| Rgb::from(c);
let hue_of = |hex| {
rgb(harmonize(McRgb::from_u32(hex), seed.into()))
.to_hct()
.get_hue()
};
let green = hue_of(0x2da44e);
let red = hue_of(0xcf222e);
let orange = hue_of(0xd18616);
let primary_hue = rgb(scheme.primary).to_hct().get_hue();
let tertiary_hue = rgb(scheme.tertiary).to_hct().get_hue();
let (text_tone, diff_tone, diff_token_tone) = match mode {
Mode::Dark => (80.0, 14.0, 24.0),
Mode::Light => (40.0, 95.0, 86.0),
};
let (container_tone, on_container_tone) = mode.container_tones();
let hct = |hue: f64, chroma: f64, tone: f64| Rgb::from_hct(hue, chroma, tone);
let surface = rgb(scheme.surface);
let container_low = rgb(scheme.surface_container_low);
let container_high = rgb(scheme.surface_container_high);
let diff = |d: DiffBg| match d {
DiffBg::Context => surface,
DiffBg::Added => hct(green, 16.0, diff_tone),
DiffBg::Removed => hct(red, 16.0, diff_tone),
DiffBg::AddedToken => hct(green, 30.0, diff_token_tone),
DiffBg::RemovedToken => hct(red, 30.0, diff_token_tone),
DiffBg::Moved => hct(tertiary_hue, 16.0, diff_tone),
};
let primary = rgb(scheme.primary);
let layer = |base: Rgb, opacity: f64| -> Rgb {
let mut opacity = opacity;
loop {
let c = base.blend(primary, opacity);
if opacity >= 0.4 || displayed(depth, c) != displayed(depth, base) {
return c;
}
opacity += 0.02;
}
};
let mut bg = HashMap::from([
(Bg::Surface, surface),
(Bg::ContainerLowest, rgb(scheme.surface_container_lowest)),
(Bg::ContainerLow, container_low),
(Bg::Container, rgb(scheme.surface_container)),
(Bg::ContainerHigh, container_high),
(Bg::ContainerHighest, rgb(scheme.surface_container_highest)),
(Bg::Selected, layer(container_low, 0.12)),
(Bg::SelectedInactive, layer(surface, 0.08)),
(Bg::SelectedHigh, layer(container_high, 0.14)),
(Bg::Primary, primary),
(Bg::PrimaryContainer, rgb(scheme.primary_container)),
(Bg::SecondaryContainer, rgb(scheme.secondary_container)),
(Bg::TertiaryContainer, rgb(scheme.tertiary_container)),
(Bg::ErrorContainer, rgb(scheme.error_container)),
(Bg::SuccessContainer, hct(green, 36.0, container_tone)),
]);
bg.extend(DiffBg::ALL.map(|d| (Bg::Diff(d), diff(d))));
bg.extend(DiffBg::ALL.map(|d| (Bg::DiffSelected(d), layer(diff(d), 0.14))));
let on_surface = rgb(scheme.on_surface);
let on_surface_variant = rgb(scheme.on_surface_variant);
let mut fg = HashMap::from([
(Fg::OnSurface, on_surface),
(Fg::OnSurfaceVariant, on_surface_variant),
(Fg::Primary, primary),
(Fg::Tertiary, rgb(scheme.tertiary)),
(Fg::Error, rgb(scheme.error)),
(Fg::Success, hct(green, 48.0, text_tone)),
(Fg::Disabled, surface.blend(on_surface, 0.38)),
(Fg::OutlineVariant, rgb(scheme.outline_variant)),
(Fg::OnPrimary, rgb(scheme.on_primary)),
(Fg::OnPrimaryContainer, rgb(scheme.on_primary_container)),
(Fg::OnSecondaryContainer, rgb(scheme.on_secondary_container)),
(Fg::OnTertiaryContainer, rgb(scheme.on_tertiary_container)),
(Fg::OnErrorContainer, rgb(scheme.on_error_container)),
(Fg::OnSuccessContainer, hct(green, 36.0, on_container_tone)),
(Fg::DiffAddedSign, hct(green, 48.0, text_tone)),
(Fg::DiffRemovedSign, hct(red, 48.0, text_tone)),
]);
let syntax = [
(Syntax::Default, on_surface),
(Syntax::Keyword, hct(primary_hue, 52.0, text_tone)),
(Syntax::String, hct(tertiary_hue, 40.0, text_tone)),
(Syntax::Comment, on_surface_variant),
(Syntax::Function, hct(primary_hue - 50.0, 36.0, text_tone)),
(Syntax::Type, hct(tertiary_hue + 50.0, 36.0, text_tone)),
(Syntax::Number, hct(orange, 48.0, text_tone)),
(Syntax::Constant, hct(orange, 40.0, text_tone)),
(Syntax::Operator, on_surface_variant),
(Syntax::Punctuation, on_surface_variant),
(Syntax::Attribute, hct(primary_hue + 180.0, 32.0, text_tone)),
(Syntax::Property, hct(primary_hue, 16.0, text_tone)),
];
fg.extend(syntax.map(|(role, c)| (Fg::Syntax(role), c)));
let (heat_tones, step) = match mode {
Mode::Dark => ([22.0, 34.0, 48.0, 64.0, 82.0], 3.0),
Mode::Light => ([90.0, 80.0, 66.0, 50.0, 34.0], -3.0),
};
let mut previous = container_low;
for (level, tone) in HEAT_LEVELS.into_iter().zip(heat_tones) {
let chroma = if level == 0 { 6.0 } else { 48.0 };
let mut tone: f64 = tone;
let mut color = hct(primary_hue, chroma, tone);
while displayed(depth, color) == displayed(depth, previous)
&& (0.0..=100.0).contains(&tone)
{
tone += step;
color = hct(primary_hue, chroma, tone);
}
fg.insert(Fg::Heat(level), color);
previous = color;
}
let missing = term_color(depth, MISSING);
let mut theme = Theme {
mode,
depth,
bg,
fg,
bg_term: [missing; Bg::COUNT],
fg_term: [missing; Fg::COUNT],
};
for role in Fg::all() {
let adjusted = enforce_contrast(&theme, role);
theme.fg.insert(role, adjusted);
}
for role in Bg::all() {
let color = term_color(depth, theme.bg_rgb(role));
if let Some(slot) = role.slot().and_then(|i| theme.bg_term.get_mut(i)) {
*slot = color;
}
}
for role in Fg::all() {
let color = term_color(depth, theme.fg_rgb(role));
if let Some(slot) = role.slot().and_then(|i| theme.fg_term.get_mut(i)) {
*slot = color;
}
}
theme
}
pub fn displayed(&self, c: Rgb) -> Rgb {
displayed(self.depth, c)
}
fn term_color(&self, c: Rgb) -> Color {
term_color(self.depth, c)
}
pub fn bg_rgb(&self, bg: Bg) -> Rgb {
self.bg.get(&bg).copied().unwrap_or(MISSING)
}
pub fn fg_rgb(&self, fg: Fg) -> Rgb {
self.fg.get(&fg).copied().unwrap_or(MISSING)
}
pub fn bg_color(&self, bg: Bg) -> Color {
let shown = bg.slot().and_then(|i| self.bg_term.get(i)).copied();
shown.unwrap_or_else(|| self.term_color(MISSING))
}
pub fn fg_color(&self, fg: Fg) -> Color {
let shown = fg.slot().and_then(|i| self.fg_term.get(i)).copied();
shown.unwrap_or_else(|| self.term_color(MISSING))
}
pub fn style(&self, fg: Fg, bg: Bg) -> Style {
debug_assert!(
requirement(fg, bg).is_some(),
"undeclared theme pair: {fg:?} on {bg:?}"
);
Style::new().fg(self.fg_color(fg)).bg(self.bg_color(bg))
}
pub fn fill(&self, bg: Bg) -> Style {
let fg = match bg {
Bg::Diff(_) | Bg::DiffSelected(_) => Fg::Syntax(Syntax::Default),
_ => Bg::FILLED
.iter()
.find(|(filled, _)| *filled == bg)
.map_or(Fg::OnSurface, |&(_, on)| on),
};
self.style(fg, bg)
}
pub fn title(&self, bg: Bg) -> Style {
self.style(Fg::OnSurface, bg).add_modifier(Modifier::BOLD)
}
pub fn body(&self, bg: Bg) -> Style {
self.style(Fg::OnSurface, bg)
}
pub fn meta(&self, bg: Bg) -> Style {
self.style(Fg::OnSurfaceVariant, bg)
}
pub fn accent(&self, bg: Bg) -> Style {
self.style(Fg::Primary, bg)
}
pub fn error(&self, bg: Bg) -> Style {
self.style(Fg::Error, bg)
}
pub fn separator(&self, bg: Bg) -> Style {
self.style(Fg::OutlineVariant, bg)
}
pub fn tones_collapse(&self, a: Bg, b: Bg) -> bool {
self.bg_color(a) == self.bg_color(b)
}
pub fn diff_tints_collapse(&self) -> bool {
let context = Bg::Diff(DiffBg::Context);
self.tones_collapse(context, Bg::Diff(DiffBg::Added))
|| self.tones_collapse(context, Bg::Diff(DiffBg::Removed))
}
pub fn pairs(&self) -> Vec<Pair> {
let pair = |fg, bg| {
Some(Pair {
fg,
bg,
requirement: requirement(fg, bg)?,
fg_color: self.displayed(self.fg_rgb(fg)),
bg_color: self.displayed(self.bg_rgb(bg)),
})
};
Fg::all()
.into_iter()
.flat_map(|fg| Bg::all().into_iter().filter_map(move |bg| pair(fg, bg)))
.collect()
}
pub fn label_chip(&self, github_hex: &str) -> Style {
match Rgb::parse_hex(github_hex) {
Some(color) => {
let (bg, fg) = labels::chip_colors(color, self);
Style::new().fg(self.term_color(fg)).bg(self.term_color(bg))
}
None => self.fill(Bg::SecondaryContainer),
}
}
}
fn term_color(depth: ColorDepth, c: Rgb) -> Color {
match depth {
ColorDepth::TrueColor => Color::Rgb(c.r, c.g, c.b),
ColorDepth::Ansi256 => Color::Indexed(color::nearest_xterm(c)),
}
}
fn displayed(depth: ColorDepth, c: Rgb) -> Rgb {
match depth {
ColorDepth::TrueColor => c,
ColorDepth::Ansi256 => color::xterm_color(color::nearest_xterm(c)),
}
}
fn enforce_contrast(theme: &Theme, role: Fg) -> Rgb {
let targets: Vec<(Rgb, f64)> = Bg::all()
.into_iter()
.filter_map(|bg| {
let req = requirement(role, bg).filter(|r| *r != Requirement::Exempt)?;
Some((theme.displayed(theme.bg_rgb(bg)), req.min_ratio()))
})
.collect();
adjust_tone(theme.fg_rgb(role), &targets, |c| theme.displayed(c))
}
pub(crate) fn adjust_tone(base: Rgb, targets: &[(Rgb, f64)], display: impl Fn(Rgb) -> Rgb) -> Rgb {
let ok = |c: Rgb| {
let shown = display(c);
targets
.iter()
.all(|&(bg, ratio)| contrast(shown, bg) >= ratio)
};
if targets.is_empty() || ok(base) {
return base;
}
let hct = base.to_hct();
let mean_bg = targets.iter().map(|(bg, _)| bg.luminance()).sum::<f64>() / targets.len() as f64;
let step = if base.luminance() >= mean_bg {
1.0
} else {
-1.0
};
let mut tone = hct.get_tone();
loop {
tone += step;
if !(0.0..=100.0).contains(&tone) {
return Rgb::from_hct(hct.get_hue(), hct.get_chroma(), tone.clamp(0.0, 100.0));
}
let c = Rgb::from_hct(hct.get_hue(), hct.get_chroma(), tone);
if ok(c) {
return c;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn each_role_has_its_own_slot() {
let slots: Vec<_> = Bg::all().into_iter().map(Bg::slot).collect();
assert_eq!(slots, (0..Bg::COUNT).map(Some).collect::<Vec<_>>());
let slots: Vec<_> = Fg::all().into_iter().map(Fg::slot).collect();
assert_eq!(slots, (0..Fg::COUNT).map(Some).collect::<Vec<_>>());
assert_eq!(Fg::Heat(9).slot(), None);
}
fn seeds() -> Vec<Rgb> {
let mut seeds = vec![
DEFAULT_SEED,
Rgb::from_u32(0x6750a4),
Rgb::from_u32(0x000000),
Rgb::from_u32(0xffffff),
Rgb::from_u32(0xff0000),
Rgb::from_u32(0xffff00),
Rgb::from_u32(0x00ff00),
];
for hue in (0..360).step_by(15) {
seeds.push(Rgb::from_hct(f64::from(hue), 48.0, 50.0));
}
seeds
}
#[test]
fn every_declared_pair_meets_its_requirement() {
let mut failures = Vec::new();
for seed in seeds() {
for mode in [Mode::Light, Mode::Dark] {
for depth in [ColorDepth::TrueColor, ColorDepth::Ansi256] {
let theme = Theme::new(seed, mode, depth);
for pair in theme.pairs() {
if !pair.passes() {
failures.push(format!(
"seed {seed} {mode:?} {depth:?}: {:?} on {:?} = {:.2} (< {})",
pair.fg,
pair.bg,
pair.ratio(),
pair.requirement.min_ratio()
));
}
}
}
}
}
assert!(
failures.is_empty(),
"{} failures:\n{}",
failures.len(),
failures.join("\n")
);
}
#[test]
fn heat_levels_stay_apart() {
for seed in seeds() {
for mode in [Mode::Light, Mode::Dark] {
for depth in [ColorDepth::TrueColor, ColorDepth::Ansi256] {
let theme = Theme::new(seed, mode, depth);
let shown = |fg| theme.displayed(theme.fg_rgb(fg));
let page = theme.displayed(theme.bg_rgb(Bg::ContainerLow));
let levels: Vec<Rgb> = HEAT_LEVELS.map(|l| shown(Fg::Heat(l))).to_vec();
assert_ne!(levels[0], page, "seed {seed} {mode:?} {depth:?}");
for pair in levels.windows(2) {
assert_ne!(
pair[0], pair[1],
"seed {seed} {mode:?} {depth:?}: {levels:?}"
);
}
}
}
}
}
#[test]
fn syntax_on_diff_backgrounds_is_covered() {
let theme = Theme::new(DEFAULT_SEED, Mode::Dark, ColorDepth::TrueColor);
let pairs = theme.pairs();
for role in Syntax::ALL {
for d in DiffBg::ALL {
for bg in [Bg::Diff(d), Bg::DiffSelected(d)] {
assert!(
pairs.iter().any(|p| p.fg == Fg::Syntax(role) && p.bg == bg),
"{role:?} on {bg:?} missing"
);
}
}
}
}
#[test]
fn only_disabled_text_and_graph_shades_are_exempt() {
for fg in Fg::all() {
for bg in Bg::all() {
if requirement(fg, bg) == Some(Requirement::Exempt) {
assert!(matches!(fg, Fg::Disabled | Fg::Heat(_)), "{fg:?}");
}
}
}
}
#[test]
fn surfaces_step_in_tone() {
for mode in [Mode::Light, Mode::Dark] {
let theme = Theme::new(DEFAULT_SEED, mode, ColorDepth::TrueColor);
let tone = |bg| theme.bg_rgb(bg).to_hct().get_tone();
let levels = [
Bg::ContainerLowest,
Bg::ContainerLow,
Bg::Container,
Bg::ContainerHigh,
Bg::ContainerHighest,
];
for pair in levels.windows(2) {
let (a, b) = (tone(pair[0]), tone(pair[1]));
match mode {
Mode::Dark => assert!(a < b, "{mode:?} {:?} {a} !< {:?} {b}", pair[0], pair[1]),
Mode::Light => {
assert!(a > b, "{mode:?} {:?} {a} !> {:?} {b}", pair[0], pair[1]);
}
}
}
}
}
#[test]
fn selection_stays_visible_in_256_colors() {
for seed in seeds() {
for mode in [Mode::Light, Mode::Dark] {
let theme = Theme::new(seed, mode, ColorDepth::Ansi256);
assert!(!theme.tones_collapse(Bg::Selected, Bg::ContainerLow));
assert!(!theme.tones_collapse(Bg::SelectedInactive, Bg::Surface));
assert!(!theme.tones_collapse(Bg::SelectedHigh, Bg::ContainerHigh));
}
}
}
#[test]
fn truecolor_tones_never_collapse() {
let theme = Theme::new(DEFAULT_SEED, Mode::Dark, ColorDepth::TrueColor);
assert!(!theme.tones_collapse(Bg::Surface, Bg::ContainerLow));
assert!(!theme.diff_tints_collapse());
}
#[test]
fn diff_backgrounds_are_tinted_not_saturated() {
for mode in [Mode::Light, Mode::Dark] {
let theme = Theme::new(DEFAULT_SEED, mode, ColorDepth::TrueColor);
let surface = theme.bg_rgb(Bg::Surface).to_hct().get_tone();
let added = theme.bg_rgb(Bg::Diff(DiffBg::Added)).to_hct();
assert!((added.get_tone() - surface).abs() < 12.0);
assert!(added.get_chroma() < 24.0);
}
}
#[test]
#[should_panic(expected = "undeclared theme pair")]
#[cfg(debug_assertions)]
fn undeclared_pair_panics_in_debug() {
let theme = Theme::new(DEFAULT_SEED, Mode::Dark, ColorDepth::TrueColor);
let _ = theme.style(Fg::Syntax(Syntax::Keyword), Bg::Primary);
}
}