blizz-ui 3.0.0-dev.10

Self-rendering terminal UI components for the blizz wizard
Documentation
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"));
  }
}