use crate::base::palette::{SYSTEM_16, XTERM_256};
use crate::base::Rgba;
const CUBE_THRESHOLDS: [u8; 5] = [48, 115, 155, 195, 235];
fn cube_index(v: u8) -> usize {
CUBE_THRESHOLDS.iter().position(|&t| v < t).unwrap_or(5)
}
fn sq_dist(a: Rgba, b: Rgba) -> u32 {
let d = |x: u8, y: u8| {
let d = x as i32 - y as i32;
(d * d) as u32
};
d(a.r, b.r) + d(a.g, b.g) + d(a.b, b.b)
}
fn luma(c: Rgba) -> u32 {
(2126 * c.r as u32 + 7152 * c.g as u32 + 722 * c.b as u32) / 10000
}
pub fn nearest_xterm256(c: Rgba) -> u8 {
let ci = (cube_index(c.r), cube_index(c.g), cube_index(c.b));
let cube_idx = (16 + 36 * ci.0 + 6 * ci.1 + ci.2) as u8;
let cube_dist = sq_dist(c, XTERM_256[cube_idx as usize]);
let gray_step = (luma(c) as i32 - 8 + 5).div_euclid(10).clamp(0, 23);
let gray_idx = (232 + gray_step) as u8;
let gray_dist = sq_dist(c, XTERM_256[gray_idx as usize]);
if gray_dist < cube_dist {
gray_idx
} else {
cube_idx
}
}
pub fn nearest_ansi16(c: Rgba) -> u8 {
nearest_in(&SYSTEM_16, 0, c, None, None).0
}
pub fn quantize_pair_256(fg: Rgba, bg: Rgba) -> (u8, u8) {
let qbg = nearest_xterm256(bg);
let qfg = nearest_xterm256(fg);
if qfg != qbg || rgb_eq(fg, bg) {
return (qfg, qbg);
}
let (nudged, _) = nearest_in(&XTERM_256[16..], 16, fg, Some(qbg), Some(ordering(fg, bg)));
(nudged, qbg)
}
pub fn quantize_pair_16(fg: Rgba, bg: Rgba) -> (u8, u8) {
let qbg = nearest_ansi16(bg);
let qfg = nearest_ansi16(fg);
if qfg != qbg || rgb_eq(fg, bg) {
return (qfg, qbg);
}
let (nudged, _) = nearest_in(&SYSTEM_16, 0, fg, Some(qbg), Some(ordering(fg, bg)));
(nudged, qbg)
}
fn rgb_eq(a: Rgba, b: Rgba) -> bool {
a.r == b.r && a.g == b.g && a.b == b.b
}
fn ordering(fg: Rgba, bg: Rgba) -> bool {
luma(fg) >= luma(bg)
}
fn nearest_in(
table: &[Rgba],
base: u8,
c: Rgba,
exclude: Option<u8>,
fg_not_darker: Option<bool>,
) -> (u8, u32) {
let anchor_luma = exclude.map(|i| luma(XTERM_256[i as usize]));
let mut best: Option<(u8, u32)> = None;
let mut best_unordered: Option<(u8, u32)> = None;
for (i, &entry) in table.iter().enumerate() {
let idx = base + i as u8;
if exclude == Some(idx) {
continue;
}
let d = sq_dist(c, entry);
if best_unordered.is_none_or(|(_, bd)| d < bd) {
best_unordered = Some((idx, d));
}
if let (Some(lighter), Some(anchor)) = (fg_not_darker, anchor_luma) {
let ok = if lighter {
luma(entry) >= anchor
} else {
luma(entry) <= anchor
};
if !ok {
continue;
}
}
if best.is_none_or(|(_, bd)| d < bd) {
best = Some((idx, d));
}
}
best.or(best_unordered)
.expect("palette tables are non-empty")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::palette;
#[test]
fn thresholds_are_base_level_midpoints() {
for (i, &t) in CUBE_THRESHOLDS.iter().enumerate() {
let lo = palette::CUBE_LEVELS[i] as u16;
let hi = palette::CUBE_LEVELS[i + 1] as u16;
assert_eq!(t as u16, (lo + hi).div_ceil(2), "midpoint {i}");
}
assert_eq!(palette::CUBE_LEVELS, [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]);
}
#[test]
fn cube_corners_map_exactly() {
assert_eq!(nearest_xterm256(Rgba::rgb(0, 0, 0)), 16);
assert_eq!(nearest_xterm256(Rgba::rgb(255, 255, 255)), 231);
assert_eq!(nearest_xterm256(Rgba::rgb(255, 0, 0)), 196);
assert_eq!(nearest_xterm256(Rgba::rgb(0, 255, 0)), 46);
assert_eq!(nearest_xterm256(Rgba::rgb(0, 0, 255)), 21);
assert_eq!(nearest_xterm256(Rgba::rgb(95, 135, 175)), 67); for c in [Rgba::rgb(3, 7, 250), Rgba::rgb(130, 128, 126)] {
let idx = nearest_xterm256(c);
assert!(idx >= 16);
let _ = palette::xterm_256(idx); }
}
#[test]
fn grays_prefer_the_ramp() {
assert_eq!(nearest_xterm256(Rgba::rgb(128, 128, 128)), 244);
assert_eq!(nearest_xterm256(Rgba::rgb(8, 8, 8)), 232);
assert_eq!(nearest_xterm256(Rgba::rgb(238, 238, 238)), 255);
}
#[test]
fn ansi16_primaries_against_shared_table() {
assert_eq!(nearest_ansi16(Rgba::rgb(0, 0, 0)), 0);
assert_eq!(nearest_ansi16(Rgba::rgb(255, 0, 0)), 9);
assert_eq!(nearest_ansi16(Rgba::rgb(130, 10, 10)), 1); assert_eq!(nearest_ansi16(Rgba::rgb(255, 255, 255)), 15);
assert_eq!(nearest_ansi16(Rgba::rgb(0, 190, 190)), 6);
assert_eq!(nearest_ansi16(Rgba::rgb(192, 192, 192)), 7);
}
#[test]
fn pair_preserves_dark_theme_faint_text() {
let bg = Rgba::rgb(26, 27, 38);
let fg = Rgba::rgb(30, 30, 40);
assert_eq!(
nearest_xterm256(bg),
nearest_xterm256(fg),
"premise: collision"
);
let (qfg, qbg) = quantize_pair_256(fg, bg);
assert_ne!(qfg, qbg, "distinct colors stay distinct");
assert!(
luma(XTERM_256[qfg as usize]) >= luma(XTERM_256[qbg as usize]),
"ordering preserved: fg {qfg} vs bg {qbg}"
);
}
#[test]
fn pair_without_collision_is_plain_nearest() {
let fg = Rgba::rgb(255, 0, 0);
let bg = Rgba::rgb(0, 0, 0);
assert_eq!(quantize_pair_256(fg, bg), (196, 16));
assert_eq!(quantize_pair_16(fg, bg), (9, 0));
}
#[test]
fn pair_identical_colors_stay_identical() {
let c = Rgba::rgb(30, 30, 40);
let (qfg, qbg) = quantize_pair_256(c, c);
assert_eq!(qfg, qbg, "genuinely identical colors may collapse");
}
#[test]
fn pair_16_collision_nudges_with_ordering() {
let bg = Rgba::rgb(10, 10, 10);
let fg = Rgba::rgb(40, 40, 40);
assert_eq!(nearest_ansi16(bg), nearest_ansi16(fg), "premise: collision");
let (qfg, qbg) = quantize_pair_16(fg, bg);
assert_ne!(qfg, qbg);
assert!(luma(SYSTEM_16[qfg as usize]) >= luma(SYSTEM_16[qbg as usize]));
}
#[test]
fn pair_darker_fg_ordering() {
let bg = Rgba::rgb(255, 255, 255);
let fg = Rgba::rgb(246, 246, 248);
let (qfg, qbg) = quantize_pair_256(fg, bg);
assert_ne!(qfg, qbg);
assert!(luma(XTERM_256[qfg as usize]) <= luma(XTERM_256[qbg as usize]));
}
}