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