blizz_ui/components/
timer_bar.rs1use 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
11const BAR_CHAR: char = '\u{2501}';
13
14#[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 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}