vtcode-theme 0.98.1

Shared theme registry and runtime state for VT Code UI crates
use anstyle::RgbColor;
use vtcode_config::constants::ui;

pub(crate) const MAX_DARK_BG_TEXT_LUMINANCE: f64 = 0.92;
pub(crate) const MIN_DARK_BG_TEXT_LUMINANCE: f64 = 0.20;
pub(crate) const MAX_LIGHT_BG_TEXT_LUMINANCE: f64 = 0.68;

pub(crate) fn relative_luminance(color: RgbColor) -> f64 {
    fn channel(value: u8) -> f64 {
        let c = (value as f64) / 255.0;
        if c <= ui::THEME_RELATIVE_LUMINANCE_CUTOFF {
            c / ui::THEME_RELATIVE_LUMINANCE_LOW_FACTOR
        } else {
            ((c + ui::THEME_RELATIVE_LUMINANCE_OFFSET)
                / (1.0 + ui::THEME_RELATIVE_LUMINANCE_OFFSET))
                .powf(ui::THEME_RELATIVE_LUMINANCE_EXPONENT)
        }
    }

    let r = channel(color.0);
    let g = channel(color.1);
    let b = channel(color.2);

    ui::THEME_RED_LUMINANCE_COEFFICIENT * r
        + ui::THEME_GREEN_LUMINANCE_COEFFICIENT * g
        + ui::THEME_BLUE_LUMINANCE_COEFFICIENT * b
}

pub(crate) fn contrast_ratio(foreground: RgbColor, background: RgbColor) -> f64 {
    let fg = relative_luminance(foreground);
    let bg = relative_luminance(background);
    let (lighter, darker) = if fg > bg { (fg, bg) } else { (bg, fg) };
    (lighter + ui::THEME_CONTRAST_RATIO_OFFSET) / (darker + ui::THEME_CONTRAST_RATIO_OFFSET)
}

fn darken(color: RgbColor, ratio: f64) -> RgbColor {
    mix(color, RgbColor(0, 0, 0), ratio)
}

fn adjust_luminance_to_target(color: RgbColor, target: f64) -> RgbColor {
    let current = relative_luminance(color);
    if (current - target).abs() < 1e-3 {
        return color;
    }

    if current < target {
        let denom = (1.0 - current).max(1e-6);
        let ratio = ((target - current) / denom).clamp(0.0, 1.0);
        lighten(color, ratio)
    } else {
        let denom = current.max(1e-6);
        let ratio = ((current - target) / denom).clamp(0.0, 1.0);
        darken(color, ratio)
    }
}

pub(crate) fn balance_text_luminance(
    color: RgbColor,
    background: RgbColor,
    min_contrast: f64,
) -> RgbColor {
    let bg_luminance = relative_luminance(background);
    let mut candidate = color;
    let current = relative_luminance(candidate);
    if bg_luminance < 0.5 {
        if current < MIN_DARK_BG_TEXT_LUMINANCE {
            candidate = adjust_luminance_to_target(candidate, MIN_DARK_BG_TEXT_LUMINANCE);
        } else if current > MAX_DARK_BG_TEXT_LUMINANCE {
            candidate = adjust_luminance_to_target(candidate, MAX_DARK_BG_TEXT_LUMINANCE);
        }
    } else if current > MAX_LIGHT_BG_TEXT_LUMINANCE {
        candidate = adjust_luminance_to_target(candidate, MAX_LIGHT_BG_TEXT_LUMINANCE);
    }

    ensure_contrast(candidate, background, min_contrast, &[color])
}

pub(crate) fn ensure_contrast(
    candidate: RgbColor,
    background: RgbColor,
    min_ratio: f64,
    fallbacks: &[RgbColor],
) -> RgbColor {
    if contrast_ratio(candidate, background) >= min_ratio {
        return candidate;
    }

    for &fallback in fallbacks {
        if contrast_ratio(fallback, background) >= min_ratio {
            return fallback;
        }
    }

    let black = RgbColor(0, 0, 0);
    let white = RgbColor(255, 255, 255);
    if contrast_ratio(black, background) >= contrast_ratio(white, background) {
        black
    } else {
        white
    }
}

pub(crate) fn mix(color: RgbColor, target: RgbColor, ratio: f64) -> RgbColor {
    let ratio = ratio.clamp(ui::THEME_MIX_RATIO_MIN, ui::THEME_MIX_RATIO_MAX);
    let blend = |c: u8, t: u8| -> u8 {
        let c = c as f64;
        let t = t as f64;
        ((c + (t - c) * ratio).round()).clamp(ui::THEME_BLEND_CLAMP_MIN, ui::THEME_BLEND_CLAMP_MAX)
            as u8
    };

    RgbColor(
        blend(color.0, target.0),
        blend(color.1, target.1),
        blend(color.2, target.2),
    )
}

pub(crate) fn lighten(color: RgbColor, ratio: f64) -> RgbColor {
    mix(
        color,
        RgbColor(
            ui::THEME_COLOR_WHITE_RED,
            ui::THEME_COLOR_WHITE_GREEN,
            ui::THEME_COLOR_WHITE_BLUE,
        ),
        ratio,
    )
}