fix(tui/colors): preserve assistant tint across palettes

Skip color quantization when we detect a truecolor terminal so the 5% info blend renders exactly. Rework the ANSI-256 quantizer to favor saturated cube entries over grayscale for lightly tinted colors and reuse the theme background for weave shading. Update the header wave animation to pull highlight, mid, and low tones from the info palette instead of the old rainbow.
This commit is contained in:
James Peter
2025-09-30 11:21:12 +10:00
parent 54718dc5a6
commit 87817ae676
2 changed files with 87 additions and 43 deletions
+10 -17
View File
@@ -185,21 +185,26 @@ fn render_border_weave(area: Rect, buf: &mut Buffer, phase: f32, tick: u32) {
if positions.is_empty() {
return;
}
let base_color = crate::colors::border();
let highlight = crate::colors::mix_toward(base_color, prism_color(phase), 0.75);
let base_bg = crate::colors::background();
let info = crate::colors::info();
let wave = ((phase * TAU).sin() * 0.5) + 0.5;
let highlight_strength = 0.65 + wave * 0.25;
let highlight = crate::colors::mix_toward(base_bg, info, highlight_strength);
let midtone = crate::colors::mix_toward(base_bg, info, 0.5);
let lowtone = crate::colors::mix_toward(base_bg, info, 0.3);
for (idx, &(x, y)) in positions.iter().enumerate() {
let tick_step = (tick as usize) % 12;
let phase_idx = (idx + (12 - tick_step)) % 12;
let color = if phase_idx < 4 {
highlight
} else if phase_idx < 8 {
crate::colors::mix_toward(highlight, base_color, 0.5)
midtone
} else {
crate::colors::mix_toward(base_color, crate::colors::text_dim(), 0.35)
lowtone
};
let cell = &mut buf[(x, y)];
cell.set_fg(color);
cell.set_bg(crate::colors::background());
cell.set_bg(base_bg);
}
}
@@ -234,15 +239,3 @@ fn border_positions(area: Rect) -> Vec<(u16, u16)> {
positions
}
fn prism_color(t: f32) -> Color {
let angle = wrap_unit(t) * TAU;
let r = (angle.sin() * 0.5 + 0.5).powf(0.55);
let g = ((angle + TAU / 3.0).sin() * 0.5 + 0.5).powf(0.55);
let b = ((angle + 2.0 * TAU / 3.0).sin() * 0.5 + 0.5).powf(0.55);
Color::Rgb(
(r * 255.0).clamp(0.0, 255.0) as u8,
(g * 255.0).clamp(0.0, 255.0) as u8,
(b * 255.0).clamp(0.0, 255.0) as u8,
)
}
+77 -26
View File
@@ -4,6 +4,7 @@ use codex_core::config_types::ThemeName;
use lazy_static::lazy_static;
use ratatui::style::Color;
use std::cmp::Ordering;
use std::collections::HashSet;
use std::sync::RwLock;
lazy_static! {
@@ -491,10 +492,15 @@ fn quantize_color_to_ansi16(c: Color) -> Color {
pub(crate) fn quantize_color_for_palette(c: Color) -> Color {
match c {
Color::Indexed(_) | Color::Reset => c,
_ => match palette_mode() {
PaletteMode::Ansi16 => quantize_color_to_ansi16(c),
PaletteMode::Ansi256 => quantize_color_to_ansi256(c),
},
_ => {
if has_truecolor_terminal() {
return c;
}
match palette_mode() {
PaletteMode::Ansi16 => quantize_color_to_ansi16(c),
PaletteMode::Ansi256 => quantize_color_to_ansi256(c),
}
}
}
}
@@ -509,6 +515,17 @@ fn rgb_to_ansi256_index(r: u8, g: u8, b: u8) -> u8 {
dr * dr + dg * dg + db * db
}
const SAT_THRESHOLD: u8 = 8;
const SAT_PENALTY_FACTOR: i32 = 10;
fn saturation_penalty(target_sat: u8, candidate_sat: u8) -> i32 {
if candidate_sat >= target_sat {
return 0;
}
let delta = target_sat as i32 - candidate_sat as i32;
delta * delta * SAT_PENALTY_FACTOR
}
// Candidate 1: color cube (16..231)
const STEPS: [u8; 6] = [0, 95, 135, 175, 215, 255];
let idx = |v: u8| -> usize {
@@ -524,10 +541,60 @@ fn rgb_to_ansi256_index(r: u8, g: u8, b: u8) -> u8 {
let ri = idx(r);
let gi = idx(g);
let bi = idx(b);
let cube_r = STEPS[ri];
let cube_g = STEPS[gi];
let cube_b = STEPS[bi];
let cube_index = 16 + 36 * ri as u8 + 6 * gi as u8 + bi as u8;
let target_rgb = (r, g, b);
let target_sat = {
let max = r.max(g).max(b);
let min = r.min(g).min(b);
max - min
};
let prefer_preserving_saturation = target_sat >= SAT_THRESHOLD;
let mut best_index = 0u8;
let mut best_score = i32::MAX;
let mut record_candidate = |
index: u8,
candidate_rgb: (u8, u8, u8),
best_index: &mut u8,
best_score: &mut i32,
| {
let mut score = dist2(target_rgb, candidate_rgb);
if prefer_preserving_saturation {
let cand_sat = candidate_rgb.0.max(candidate_rgb.1).max(candidate_rgb.2)
- candidate_rgb.0.min(candidate_rgb.1).min(candidate_rgb.2);
score += saturation_penalty(target_sat, cand_sat);
}
if score < *best_score {
*best_score = score;
*best_index = index;
}
};
let neighbor_steps = |i: usize| -> Vec<usize> {
let mut out = Vec::with_capacity(3);
if i > 0 { out.push(i - 1); }
out.push(i);
if i < 5 { out.push(i + 1); }
out
};
let mut seen_cube_indices: HashSet<u8> = HashSet::new();
for rr in neighbor_steps(ri as usize) {
for gg in neighbor_steps(gi as usize) {
for bb in neighbor_steps(bi as usize) {
if rr > 5 || gg > 5 || bb > 5 {
continue;
}
let candidate_index = 16 + 36 * rr as u8 + 6 * gg as u8 + bb as u8;
if !seen_cube_indices.insert(candidate_index) {
continue;
}
let candidate_rgb = (STEPS[rr], STEPS[gg], STEPS[bb]);
record_candidate(candidate_index, candidate_rgb, &mut best_index, &mut best_score);
}
}
}
// Candidate 2: grayscale (232..255)
let gray_level = {
@@ -536,28 +603,12 @@ fn rgb_to_ansi256_index(r: u8, g: u8, b: u8) -> u8 {
};
let gray_value = 8 + 10 * gray_level;
let gray_index = 232 + gray_level;
record_candidate(gray_index, (gray_value, gray_value, gray_value), &mut best_index, &mut best_score);
// Candidate 3: 16-color ANSI (0..15), includes true white (15) which the
// grayscale ramp does not reach. This fixes near-white mapping to gray.
let rgb = (r, g, b);
let cube_rgb = (cube_r, cube_g, cube_b);
let gray_rgb = (gray_value, gray_value, gray_value);
let mut best_index = cube_index;
let mut best_dist = dist2(rgb, cube_rgb);
let gray_dist = dist2(rgb, gray_rgb);
if gray_dist < best_dist {
best_dist = gray_dist;
best_index = gray_index;
}
for (i, &(ar, ag, ab)) in ANSI16_COLORS.iter().enumerate() {
let d = dist2(rgb, (ar, ag, ab));
if d < best_dist {
best_dist = d;
best_index = i as u8;
}
record_candidate(i as u8, (ar, ag, ab), &mut best_index, &mut best_score);
}
best_index