use std::io::Write;
use crossterm::{
queue,
style::{Color, Print, ResetColor, SetForegroundColor},
};
use crate::prompt::text_entry;
use crate::{Component, Renderer};
const BAR_CHAR: char = '\u{2501}';
#[derive(Debug, Clone)]
pub struct TimerBarComponent {
pub progress: f64,
pub visible: bool,
}
impl TimerBarComponent {
pub fn new() -> Self {
Self {
progress: 1.0,
visible: false,
}
}
pub fn hidden() -> Self {
Self::new()
}
pub fn segment_count(progress: f64, terminal_width: u16) -> usize {
let inner = text_entry("", "", terminal_width).inner_width;
visible_segments(inner, progress)
}
}
#[cfg(not(tarpaulin_include))]
impl Component for TimerBarComponent {
fn render<W: Write>(&self, renderer: &mut Renderer<W>) -> std::io::Result<u16> {
if !self.visible || renderer.panel().width == 0 {
return Ok(0);
}
renderer.with_panel(|writer, panel, _rng| {
let body: String = BAR_CHAR.to_string().repeat(panel.width as usize);
queue!(
writer,
SetForegroundColor(Color::DarkGrey),
Print(body),
ResetColor
)?;
Ok(1)
})
}
}
fn visible_segments(inner: u16, progress: f64) -> usize {
let p = progress.clamp(0.0, 1.0);
let n = ((inner as f64) * p).floor() as usize;
n.min(inner as usize)
}
impl Default for TimerBarComponent {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::LayoutPanel;
#[test]
fn new_is_hidden_with_full_progress() {
let t = TimerBarComponent::new();
assert!(!t.visible);
assert_eq!(t.progress, 1.0);
}
#[test]
fn segment_count_scales_with_progress() {
let terminal_width = 80_u16;
let inner = text_entry("", "", terminal_width).inner_width as usize;
assert_eq!(TimerBarComponent::segment_count(0.0, terminal_width), 0);
assert_eq!(TimerBarComponent::segment_count(1.0, terminal_width), inner);
let half = TimerBarComponent::segment_count(0.5, terminal_width);
assert!(half > 0 && half < inner);
}
#[test]
fn segment_count_clamps_progress() {
let terminal_width = 100_u16;
let inner = text_entry("", "", terminal_width).inner_width as usize;
assert_eq!(TimerBarComponent::segment_count(-1.0, terminal_width), 0);
assert_eq!(TimerBarComponent::segment_count(2.0, terminal_width), inner);
}
use crate::test_helpers::test_renderer;
#[test]
fn render_skips_when_not_visible() {
let t = TimerBarComponent {
progress: 1.0,
visible: false,
};
let panel = LayoutPanel::centered(80, 10, 5);
let mut renderer = test_renderer();
renderer.draw(&t, panel).unwrap();
assert!(renderer.writer.is_empty());
}
#[test]
fn render_skips_when_zero_width() {
let t = TimerBarComponent {
progress: 0.0,
visible: true,
};
let panel = LayoutPanel {
row: 5,
column: 0,
width: 0,
};
let mut renderer = test_renderer();
renderer.draw(&t, panel).unwrap();
assert!(renderer.writer.is_empty());
}
#[test]
fn hidden_is_alias_for_new() {
let t = TimerBarComponent::hidden();
assert!(!t.visible);
assert_eq!(t.progress, 1.0);
}
#[test]
fn default_matches_new() {
let t = TimerBarComponent::default();
assert!(!t.visible);
assert_eq!(t.progress, 1.0);
}
#[test]
fn render_writes_bar_when_visible() {
let n = TimerBarComponent::segment_count(1.0, 80);
let t = TimerBarComponent {
progress: 1.0,
visible: true,
};
let panel = LayoutPanel::centered(80, n as u16, 3);
let mut renderer = test_renderer();
renderer.draw(&t, panel).unwrap();
let s = String::from_utf8(renderer.writer).unwrap();
assert_eq!(s.matches(BAR_CHAR).count(), n);
}
}