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