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