use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use crate::ui::theme;
pub fn lerp_color(a: Color, b: Color, t: f32) -> Color {
let t = t.clamp(0.0, 1.0);
match (a, b) {
(Color::Rgb(r1, g1, b1), Color::Rgb(r2, g2, b2)) => {
let r = (r1 as f32 + (r2 as f32 - r1 as f32) * t).round() as u8;
let g = (g1 as f32 + (g2 as f32 - g1 as f32) * t).round() as u8;
let b = (b1 as f32 + (b2 as f32 - b1 as f32) * t).round() as u8;
Color::Rgb(r, g, b)
}
_ => a,
}
}
pub fn scale_color(color: Color, factor: f32) -> Color {
match color {
Color::Rgb(r, g, b) => {
let r = ((r as f32) * factor).round().clamp(0.0, 255.0) as u8;
let g = ((g as f32) * factor).round().clamp(0.0, 255.0) as u8;
let b = ((b as f32) * factor).round().clamp(0.0, 255.0) as u8;
Color::Rgb(r, g, b)
}
_ => color,
}
}
pub fn gradient_spans(text: &str, start: Color, end: Color, bold: bool) -> Vec<Span<'static>> {
let chars: Vec<char> = text.chars().collect();
let len = chars.len();
if len == 0 {
return vec![];
}
let divisor = if len > 1 { (len - 1) as f32 } else { 1.0 };
chars
.into_iter()
.enumerate()
.map(|(i, ch)| {
let t = i as f32 / divisor;
let color = lerp_color(start, end, t);
let mut style = Style::default().fg(color);
if bold {
style = style.add_modifier(Modifier::BOLD);
}
Span::styled(ch.to_string(), style)
})
.collect()
}
pub fn perimeter_color(i: usize, total: usize, start: Color, end: Color, phase: f32) -> Color {
let angle = (i as f32 / total as f32) * std::f32::consts::TAU + phase;
let t = (1.0 - angle.cos()) / 2.0;
lerp_color(start, end, t)
}
pub fn walk_perimeter(area: Rect, mut f: impl FnMut(u16, u16, usize, usize)) {
if area.width < 2 || area.height < 2 {
return;
}
let total = 2 * (area.width as usize - 1) + 2 * (area.height as usize - 1);
let x1 = area.x;
let x2 = area.x + area.width - 1;
let y1 = area.y;
let y2 = area.y + area.height - 1;
let mut i = 0;
for x in x1..=x2 {
f(x, y1, i, total);
i += 1;
}
for y in (y1 + 1)..y2 {
f(x2, y, i, total);
i += 1;
}
for x in (x1..=x2).rev() {
f(x, y2, i, total);
i += 1;
}
for y in ((y1 + 1)..y2).rev() {
f(x1, y, i, total);
i += 1;
}
}
pub fn fill_row_highlight(
buf: &mut Buffer,
bounds: Rect,
row_y: u16,
x_start: u16,
width: u16,
clear: bool,
) {
let w = width as f32;
for x in x_start..x_start + width {
if x >= bounds.x + bounds.width || row_y < bounds.y || row_y >= bounds.y + bounds.height {
continue;
}
let t = (x - x_start) as f32 / w.max(1.0);
let bg = lerp_color(
theme::GRADIENT_HIGHLIGHT_START,
theme::GRADIENT_HIGHLIGHT_END,
t,
);
let fg = lerp_color(theme::BLACK, theme::CYAN, t);
let cell = &mut buf[(x, row_y)];
if clear {
cell.set_char(' ');
}
cell.set_style(Style::default().fg(fg).bg(bg).add_modifier(Modifier::BOLD));
}
}
pub fn gradient_title_line(text: &str) -> Line<'static> {
Line::from(gradient_spans(
text,
theme::CYAN,
theme::GRADIENT_TITLE_END,
true,
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lerp_at_zero() {
let a = Color::Rgb(0, 0, 0);
let b = Color::Rgb(255, 255, 255);
assert_eq!(lerp_color(a, b, 0.0), a);
}
#[test]
fn lerp_at_one() {
let a = Color::Rgb(0, 0, 0);
let b = Color::Rgb(255, 255, 255);
assert_eq!(lerp_color(a, b, 1.0), b);
}
#[test]
fn lerp_at_half() {
let a = Color::Rgb(0, 0, 0);
let b = Color::Rgb(200, 100, 50);
assert_eq!(lerp_color(a, b, 0.5), Color::Rgb(100, 50, 25));
}
#[test]
fn lerp_clamps_t() {
let a = Color::Rgb(10, 20, 30);
let b = Color::Rgb(100, 200, 250);
assert_eq!(lerp_color(a, b, -1.0), a);
assert_eq!(lerp_color(a, b, 2.0), b);
}
#[test]
fn lerp_non_rgb_fallback() {
let a = Color::Red;
let b = Color::Rgb(255, 0, 0);
assert_eq!(lerp_color(a, b, 0.5), Color::Red);
}
#[test]
fn scale_color_half() {
let c = Color::Rgb(200, 100, 50);
assert_eq!(scale_color(c, 0.5), Color::Rgb(100, 50, 25));
}
#[test]
fn scale_color_clamps() {
let c = Color::Rgb(200, 200, 200);
assert_eq!(scale_color(c, 2.0), Color::Rgb(255, 255, 255));
}
#[test]
fn scale_color_non_rgb_fallback() {
assert_eq!(scale_color(Color::Blue, 0.5), Color::Blue);
}
#[test]
fn gradient_spans_empty() {
assert!(
gradient_spans("", Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255), false).is_empty()
);
}
#[test]
fn gradient_spans_single_char() {
let spans = gradient_spans("X", Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255), true);
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].content, "X");
}
#[test]
fn gradient_spans_length() {
let spans = gradient_spans(
"hello",
Color::Rgb(0, 0, 0),
Color::Rgb(255, 255, 255),
false,
);
assert_eq!(spans.len(), 5);
}
#[test]
fn gradient_spans_bold_modifier() {
let spans = gradient_spans("AB", Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255), true);
for span in &spans {
assert!(span.style.add_modifier.contains(Modifier::BOLD));
}
}
}