retroglyph_widgets/widget/
progress_bar.rs1use retroglyph_core::{Backend, Color, Rect, Style, Terminal};
3
4use super::Widget;
5use crate::Theme;
6
7#[derive(Clone, Copy, Debug)]
14pub struct ProgressBar {
15 value: u32,
16 max: u32,
17 filled_style: Style,
18 empty_style: Style,
19}
20
21impl ProgressBar {
22 #[must_use]
24 pub fn new(value: u32, max: u32) -> Self {
25 Self {
26 value,
27 max,
28 filled_style: Style::new(),
29 empty_style: Style::new(),
30 }
31 }
32
33 #[must_use]
35 pub const fn filled_style(mut self, style: Style) -> Self {
36 self.filled_style = style;
37 self
38 }
39
40 #[must_use]
42 pub const fn empty_style(mut self, style: Style) -> Self {
43 self.empty_style = style;
44 self
45 }
46
47 #[must_use]
64 pub fn theme(self, theme: Theme) -> Self {
65 self.theme_on(theme, theme.panel_bg)
66 }
67
68 #[must_use]
73 pub fn theme_on(mut self, theme: Theme, bg: Color) -> Self {
74 self.filled_style = Style::new().fg(theme.accent).bg(bg);
75 self.empty_style = Style::new().fg(theme.dim).bg(bg);
76 self
77 }
78}
79
80impl<B: Backend> Widget<B> for ProgressBar {
81 fn render(self, area: Rect, term: &mut Terminal<B>) {
82 if area.width() == 0 || self.max == 0 {
83 return;
84 }
85 let filled_cells = ((u64::from(self.value.min(self.max)) * u64::from(area.width()))
86 / u64::from(self.max)) as u16;
87 let y = area.top();
88 for x in area.left()..area.right() {
89 let is_filled = x < area.left() + filled_cells;
90 let style = if is_filled {
91 self.filled_style
92 } else {
93 self.empty_style
94 };
95 term.reset_style()
96 .fg(style.foreground())
97 .bg(style.background());
98 term.put(x, y, if is_filled { '█' } else { '░' });
99 }
100 term.reset_style();
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use retroglyph_core::Headless;
107
108 use super::*;
109
110 #[test]
111 fn fills_proportionally() {
112 let area = Rect::new(0, 0, 10, 1);
113 let mut term = Terminal::new(Headless::new(10, 1));
114 ProgressBar::new(5, 10).render(area, &mut term);
115
116 for x in 0..5 {
117 assert_eq!(term.grid().get(x, 0).glyph(), '█');
118 }
119 for x in 5..10 {
120 assert_eq!(term.grid().get(x, 0).glyph(), '░');
121 }
122 }
123
124 #[test]
125 fn zero_max_is_a_no_op() {
126 let area = Rect::new(0, 0, 10, 1);
127 let mut term = Terminal::new(Headless::new(10, 1));
128 ProgressBar::new(0, 0).render(area, &mut term);
129 assert_eq!(term.grid().get(0, 0).glyph(), ' ');
130 }
131
132 #[test]
133 fn filled_and_empty_styles_are_configurable() {
134 use retroglyph_core::Color;
135
136 let area = Rect::new(0, 0, 4, 1);
137 let mut term = Terminal::new(Headless::new(4, 1));
138 ProgressBar::new(2, 4)
139 .filled_style(Style::new().fg(Color::WHITE))
140 .empty_style(Style::new().fg(Color::BLACK))
141 .render(area, &mut term);
142
143 assert_eq!(term.grid().get(0, 0).style().foreground(), Color::WHITE);
144 assert_eq!(term.grid().get(3, 0).style().foreground(), Color::BLACK);
145 }
146
147 #[test]
148 fn theme_maps_named_roles_onto_filled_and_empty_styles() {
149 let area = Rect::new(0, 0, 4, 1);
150 let mut term = Terminal::new(Headless::new(4, 1));
151 ProgressBar::new(2, 4)
152 .theme(Theme::DARK)
153 .render(area, &mut term);
154
155 assert_eq!(
156 term.grid().get(0, 0).style().foreground(),
157 Theme::DARK.accent
158 );
159 assert_eq!(
160 term.grid().get(0, 0).style().background(),
161 Theme::DARK.panel_bg
162 );
163 assert_eq!(term.grid().get(3, 0).style().foreground(), Theme::DARK.dim);
164 assert_eq!(
165 term.grid().get(3, 0).style().background(),
166 Theme::DARK.panel_bg
167 );
168 }
169
170 #[test]
171 fn theme_on_uses_the_given_backdrop_instead_of_panel_bg() {
172 let area = Rect::new(0, 0, 4, 1);
173 let mut term = Terminal::new(Headless::new(4, 1));
174 ProgressBar::new(2, 4)
175 .theme_on(Theme::DARK, Color::Default)
176 .render(area, &mut term);
177
178 assert_eq!(
179 term.grid().get(0, 0).style().foreground(),
180 Theme::DARK.accent
181 );
182 assert_eq!(term.grid().get(0, 0).style().background(), Color::Default);
183 assert_eq!(term.grid().get(3, 0).style().foreground(), Theme::DARK.dim);
184 assert_eq!(term.grid().get(3, 0).style().background(), Color::Default);
185 }
186}