blizz_ui/components/
timer_bar.rs1use std::io::Write;
2
3use crossterm::{
4 cursor::MoveTo,
5 queue,
6 style::{Color, Print, ResetColor, SetForegroundColor},
7};
8
9use crate::layout::centered_column;
10use crate::prompt::text_entry;
11
12const BAR_CHAR: char = '\u{2501}';
14
15#[derive(Debug, Clone)]
17pub struct TimerBarComponent {
18 pub progress: f64,
19 pub visible: bool,
20}
21
22impl TimerBarComponent {
23 pub fn new() -> Self {
24 Self {
25 progress: 1.0,
26 visible: false,
27 }
28 }
29
30 pub fn hidden() -> Self {
31 Self::new()
32 }
33
34 pub fn segment_count(progress: f64, tw: u16) -> usize {
36 let inner = text_entry("", "", tw).inner_width;
37 visible_segments(inner, progress)
38 }
39
40 #[cfg(not(tarpaulin_include))]
41 pub fn render<W: Write>(&self, writer: &mut W, tw: u16, row: u16) -> std::io::Result<()> {
42 if !self.visible {
43 return Ok(());
44 }
45
46 let seg = Self::segment_count(self.progress, tw);
47 if seg == 0 {
48 return Ok(());
49 }
50
51 let col = centered_column(tw, seg as u16);
52 let body: String = BAR_CHAR.to_string().repeat(seg);
53 queue!(
54 writer,
55 MoveTo(col, row),
56 SetForegroundColor(Color::DarkGrey),
57 Print(body),
58 ResetColor
59 )
60 }
61}
62
63fn visible_segments(inner: u16, progress: f64) -> usize {
64 let p = progress.clamp(0.0, 1.0);
65 let n = ((inner as f64) * p).floor() as usize;
66 n.min(inner as usize)
67}
68
69impl Default for TimerBarComponent {
70 fn default() -> Self {
71 Self::new()
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn new_is_hidden_with_full_progress() {
81 let t = TimerBarComponent::new();
82 assert!(!t.visible);
83 assert_eq!(t.progress, 1.0);
84 }
85
86 #[test]
87 fn segment_count_scales_with_progress() {
88 let tw = 80_u16;
89 let inner = text_entry("", "", tw).inner_width as usize;
90 assert_eq!(TimerBarComponent::segment_count(0.0, tw), 0);
91 assert_eq!(TimerBarComponent::segment_count(1.0, tw), inner);
92 let half = TimerBarComponent::segment_count(0.5, tw);
93 assert!(half > 0 && half < inner);
94 }
95
96 #[test]
97 fn segment_count_clamps_progress() {
98 let tw = 100_u16;
99 let inner = text_entry("", "", tw).inner_width as usize;
100 assert_eq!(TimerBarComponent::segment_count(-1.0, tw), 0);
101 assert_eq!(TimerBarComponent::segment_count(2.0, tw), inner);
102 }
103
104 #[test]
105 fn render_skips_when_not_visible() {
106 let t = TimerBarComponent {
107 progress: 1.0,
108 visible: false,
109 };
110 let mut buf = Vec::new();
111 t.render(&mut buf, 80, 5).unwrap();
112 assert!(buf.is_empty());
113 }
114
115 #[test]
116 fn render_skips_when_zero_segments() {
117 let t = TimerBarComponent {
118 progress: 0.0,
119 visible: true,
120 };
121 let mut buf = Vec::new();
122 t.render(&mut buf, 80, 5).unwrap();
123 assert!(buf.is_empty());
124 }
125
126 #[test]
127 fn render_writes_bar_when_visible() {
128 let n = TimerBarComponent::segment_count(1.0, 80);
129 let t = TimerBarComponent {
130 progress: 1.0,
131 visible: true,
132 };
133 let mut buf = Vec::new();
134 t.render(&mut buf, 80, 3).unwrap();
135 let s = String::from_utf8(buf).unwrap();
136 assert_eq!(s.matches(BAR_CHAR).count(), n);
137 }
138}