use std::io::Write;
use crossterm::{
cursor::MoveTo,
queue,
style::{Color, Print, ResetColor, SetForegroundColor},
};
use crate::layout::centered_column;
const ANSI_GRAYSCALE_START: u8 = 232;
const GRAYSCALE_RANGE: f64 = 15.0;
pub struct HintComponent {
pub text: &'static str,
pub fade: f64,
}
impl HintComponent {
pub fn new(text: &'static str) -> Self {
Self { text, fade: 0.0 }
}
#[cfg(not(tarpaulin_include))]
pub fn render<W: Write>(&self, writer: &mut W, tw: u16, row: u16) -> std::io::Result<()> {
if self.fade <= 0.0 {
return Ok(());
}
let color = fade_color(self.fade);
let col = centered_column(tw, self.text.chars().count() as u16);
queue!(
writer,
MoveTo(col, row),
SetForegroundColor(color),
Print(self.text),
ResetColor
)
}
}
fn fade_color(fade: f64) -> Color {
if fade >= 1.0 {
return Color::DarkGrey;
}
Color::AnsiValue(ANSI_GRAYSCALE_START + (fade * GRAYSCALE_RANGE).round() as u8)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_starts_invisible() {
let hint = HintComponent::new("(hit enter)");
assert_eq!(hint.fade, 0.0);
assert_eq!(hint.text, "(hit enter)");
}
#[test]
fn fade_color_returns_dark_grey_at_full() {
assert_eq!(fade_color(1.0), Color::DarkGrey);
assert_eq!(fade_color(1.5), Color::DarkGrey);
}
#[test]
fn fade_color_returns_ansi_gradient_below_one() {
let color = fade_color(0.5);
match color {
Color::AnsiValue(v) => {
assert!(v >= 232);
assert!(v <= 247);
}
_ => panic!("expected AnsiValue"),
}
}
#[test]
fn fade_color_at_zero_is_darkest() {
assert_eq!(fade_color(0.0), Color::AnsiValue(ANSI_GRAYSCALE_START));
}
#[test]
fn render_skips_when_invisible() {
let hint = HintComponent::new("test");
let mut buf = Vec::new();
hint.render(&mut buf, 80, 10).unwrap();
assert!(buf.is_empty());
}
#[test]
fn render_writes_when_visible() {
let mut hint = HintComponent::new("hello");
hint.fade = 1.0;
let mut buf = Vec::new();
hint.render(&mut buf, 80, 10).unwrap();
assert!(!buf.is_empty());
let output = String::from_utf8(buf).unwrap();
assert!(output.contains("hello"));
}
}