Skip to main content

blizz_ui/components/
timer_bar.rs

1use std::io::Write;
2
3use crossterm::{
4  queue,
5  style::{Color, Print, ResetColor, SetForegroundColor},
6};
7
8use crate::prompt::text_entry;
9use crate::{Component, Renderer};
10
11/// Single-column heavy horizontal segment (often called "heavy minus").
12const BAR_CHAR: char = '\u{2501}';
13
14/// Countdown strip aligned with prompt text-entry width (`text_entry(..).inner_width`).
15#[derive(Debug, Clone)]
16pub struct TimerBarComponent {
17  pub progress: f64,
18  pub visible: bool,
19}
20
21impl TimerBarComponent {
22  pub fn new() -> Self {
23    Self {
24      progress: 1.0,
25      visible: false,
26    }
27  }
28
29  pub fn hidden() -> Self {
30    Self::new()
31  }
32
33  /// Visible segment count when `progress` is in \[0, 1\] and terminal width `terminal_width`.
34  pub fn segment_count(progress: f64, terminal_width: u16) -> usize {
35    let inner = text_entry("", "", terminal_width).inner_width;
36    visible_segments(inner, progress)
37  }
38}
39
40#[cfg(not(tarpaulin_include))]
41impl Component for TimerBarComponent {
42  fn render<W: Write>(&self, renderer: &mut Renderer<W>) -> std::io::Result<u16> {
43    if !self.visible || renderer.panel().width == 0 {
44      return Ok(0);
45    }
46    renderer.with_panel(|writer, panel, _rng| {
47      let body: String = BAR_CHAR.to_string().repeat(panel.width as usize);
48      queue!(
49        writer,
50        SetForegroundColor(Color::DarkGrey),
51        Print(body),
52        ResetColor
53      )?;
54      Ok(1)
55    })
56  }
57}
58
59fn visible_segments(inner: u16, progress: f64) -> usize {
60  let p = progress.clamp(0.0, 1.0);
61  let n = ((inner as f64) * p).floor() as usize;
62  n.min(inner as usize)
63}
64
65impl Default for TimerBarComponent {
66  fn default() -> Self {
67    Self::new()
68  }
69}
70
71#[cfg(test)]
72mod tests {
73  use super::*;
74  use crate::LayoutPanel;
75
76  #[test]
77  fn new_is_hidden_with_full_progress() {
78    let t = TimerBarComponent::new();
79    assert!(!t.visible);
80    assert_eq!(t.progress, 1.0);
81  }
82
83  #[test]
84  fn segment_count_scales_with_progress() {
85    let terminal_width = 80_u16;
86    let inner = text_entry("", "", terminal_width).inner_width as usize;
87    assert_eq!(TimerBarComponent::segment_count(0.0, terminal_width), 0);
88    assert_eq!(TimerBarComponent::segment_count(1.0, terminal_width), inner);
89    let half = TimerBarComponent::segment_count(0.5, terminal_width);
90    assert!(half > 0 && half < inner);
91  }
92
93  #[test]
94  fn segment_count_clamps_progress() {
95    let terminal_width = 100_u16;
96    let inner = text_entry("", "", terminal_width).inner_width as usize;
97    assert_eq!(TimerBarComponent::segment_count(-1.0, terminal_width), 0);
98    assert_eq!(TimerBarComponent::segment_count(2.0, terminal_width), inner);
99  }
100
101  use crate::test_helpers::test_renderer;
102
103  #[test]
104  fn render_skips_when_not_visible() {
105    let t = TimerBarComponent {
106      progress: 1.0,
107      visible: false,
108    };
109    let panel = LayoutPanel::centered(80, 10, 5);
110    let mut renderer = test_renderer();
111    renderer.draw(&t, panel).unwrap();
112    assert!(renderer.writer.is_empty());
113  }
114
115  #[test]
116  fn render_skips_when_zero_width() {
117    let t = TimerBarComponent {
118      progress: 0.0,
119      visible: true,
120    };
121    let panel = LayoutPanel {
122      row: 5,
123      column: 0,
124      width: 0,
125    };
126    let mut renderer = test_renderer();
127    renderer.draw(&t, panel).unwrap();
128    assert!(renderer.writer.is_empty());
129  }
130
131  #[test]
132  fn hidden_is_alias_for_new() {
133    let t = TimerBarComponent::hidden();
134    assert!(!t.visible);
135    assert_eq!(t.progress, 1.0);
136  }
137
138  #[test]
139  fn default_matches_new() {
140    let t = TimerBarComponent::default();
141    assert!(!t.visible);
142    assert_eq!(t.progress, 1.0);
143  }
144
145  #[test]
146  fn render_writes_bar_when_visible() {
147    let n = TimerBarComponent::segment_count(1.0, 80);
148    let t = TimerBarComponent {
149      progress: 1.0,
150      visible: true,
151    };
152    let panel = LayoutPanel::centered(80, n as u16, 3);
153    let mut renderer = test_renderer();
154    renderer.draw(&t, panel).unwrap();
155    let s = String::from_utf8(renderer.writer).unwrap();
156    assert_eq!(s.matches(BAR_CHAR).count(), n);
157  }
158}