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