retroglyph_widgets/widget/
progress_bar.rs1use retroglyph_core::{Backend, 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(mut self, theme: Theme) -> Self {
65 self.filled_style = Style::new().fg(theme.accent).bg(theme.panel_bg);
66 self.empty_style = Style::new().fg(theme.dim).bg(theme.panel_bg);
67 self
68 }
69}
70
71impl<B: Backend> Widget<B> for ProgressBar {
72 fn render(self, area: Rect, term: &mut Terminal<B>) {
73 if area.width() == 0 || self.max == 0 {
74 return;
75 }
76 let filled_cells = ((u64::from(self.value.min(self.max)) * u64::from(area.width()))
77 / u64::from(self.max)) as u16;
78 let y = area.top();
79 for x in area.left()..area.right() {
80 let is_filled = x < area.left() + filled_cells;
81 let style = if is_filled {
82 self.filled_style
83 } else {
84 self.empty_style
85 };
86 term.reset_style()
87 .fg(style.foreground())
88 .bg(style.background());
89 term.put(x, y, if is_filled { '█' } else { '░' });
90 }
91 term.reset_style();
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use retroglyph_core::Headless;
98
99 use super::*;
100
101 #[test]
102 fn fills_proportionally() {
103 let area = Rect::new(0, 0, 10, 1);
104 let mut term = Terminal::new(Headless::new(10, 1));
105 ProgressBar::new(5, 10).render(area, &mut term);
106
107 for x in 0..5 {
108 assert_eq!(term.grid().get(x, 0).glyph(), '█');
109 }
110 for x in 5..10 {
111 assert_eq!(term.grid().get(x, 0).glyph(), '░');
112 }
113 }
114
115 #[test]
116 fn zero_max_is_a_no_op() {
117 let area = Rect::new(0, 0, 10, 1);
118 let mut term = Terminal::new(Headless::new(10, 1));
119 ProgressBar::new(0, 0).render(area, &mut term);
120 assert_eq!(term.grid().get(0, 0).glyph(), ' ');
121 }
122
123 #[test]
124 fn filled_and_empty_styles_are_configurable() {
125 use retroglyph_core::Color;
126
127 let area = Rect::new(0, 0, 4, 1);
128 let mut term = Terminal::new(Headless::new(4, 1));
129 ProgressBar::new(2, 4)
130 .filled_style(Style::new().fg(Color::WHITE))
131 .empty_style(Style::new().fg(Color::BLACK))
132 .render(area, &mut term);
133
134 assert_eq!(term.grid().get(0, 0).style().foreground(), Color::WHITE);
135 assert_eq!(term.grid().get(3, 0).style().foreground(), Color::BLACK);
136 }
137
138 #[test]
139 fn theme_maps_named_roles_onto_filled_and_empty_styles() {
140 let area = Rect::new(0, 0, 4, 1);
141 let mut term = Terminal::new(Headless::new(4, 1));
142 ProgressBar::new(2, 4)
143 .theme(Theme::DARK)
144 .render(area, &mut term);
145
146 assert_eq!(
147 term.grid().get(0, 0).style().foreground(),
148 Theme::DARK.accent
149 );
150 assert_eq!(
151 term.grid().get(0, 0).style().background(),
152 Theme::DARK.panel_bg
153 );
154 assert_eq!(term.grid().get(3, 0).style().foreground(), Theme::DARK.dim);
155 assert_eq!(
156 term.grid().get(3, 0).style().background(),
157 Theme::DARK.panel_bg
158 );
159 }
160}