lv_tui/widgets/
progressbar.rs1use crate::component::{Component, EventCx, MeasureCx};
2use crate::event::Event;
3use crate::geom::{Rect, Size};
4use crate::layout::Constraint;
5use crate::render::RenderCx;
6use crate::style::Style;
7
8const BLOCKS: &[&str] = &[" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
9
10pub struct ProgressBar {
16 ratio: f64,
17 width: u16,
18 style: Style,
19 track_style: Style,
20 label: Option<String>,
21}
22
23impl ProgressBar {
24 pub fn new() -> Self {
26 Self {
27 ratio: 0.0,
28 width: 20,
29 style: Style::default().fg(crate::style::Color::Cyan),
30 track_style: Style::default().fg(crate::style::Color::Gray),
31 label: None,
32 }
33 }
34
35 pub fn label(mut self, show: bool) -> Self {
37 self.label = if show { Some(String::new()) } else { None };
38 self
39 }
40
41 pub fn ratio(mut self, ratio: f64) -> Self {
43 self.ratio = ratio.clamp(0.0, 1.0);
44 self
45 }
46
47 pub fn width(mut self, width: u16) -> Self {
49 self.width = width;
50 self
51 }
52
53 pub fn style(mut self, style: Style) -> Self {
55 self.style = style;
56 self
57 }
58
59 pub fn track_style(mut self, style: Style) -> Self {
61 self.track_style = style;
62 self
63 }
64
65 pub fn set_ratio(&mut self, ratio: f64, cx: &mut EventCx) {
67 let r = ratio.clamp(0.0, 1.0);
68 if (self.ratio - r).abs() > f64::EPSILON {
69 self.ratio = r;
70 cx.invalidate_paint();
71 }
72 }
73}
74
75impl Component for ProgressBar {
76 fn render(&self, cx: &mut RenderCx) {
77 let has_label = self.label.is_some();
78 let bar_width = if has_label { self.width.saturating_sub(5) } else { self.width };
79
80 let filled = (self.ratio * bar_width as f64) as u16;
81 let whole = filled.min(bar_width);
82 let frac = ((self.ratio * bar_width as f64) - whole as f64) * 8.0;
83 let frac_idx = (frac as usize).min(BLOCKS.len() - 1);
84
85 if whole > 0 {
87 cx.set_style(self.style.clone());
88 cx.text("█".repeat(whole as usize));
89 }
90
91 if whole < bar_width {
93 if frac_idx > 0 {
94 cx.set_style(self.style.clone());
95 cx.text(BLOCKS[frac_idx]);
96 }
97 let frac_used = if frac_idx > 0 { 1 } else { 0 };
98 let remaining = bar_width.saturating_sub(whole).saturating_sub(frac_used);
99 if remaining > 0 {
100 cx.set_style(self.track_style.clone());
101 cx.text("░".repeat(remaining as usize));
102 }
103 }
104
105 if has_label {
107 let pct = format!(" {:3}%", (self.ratio * 100.0) as u8);
108 cx.set_style(self.style.clone());
109 cx.text(&pct);
110 }
111
112 cx.set_style(self.track_style.clone());
113 cx.line("");
114 }
115
116 fn measure(&self, _constraint: Constraint, _cx: &mut MeasureCx) -> Size {
117 Size { width: self.width, height: 1 }
118 }
119
120 fn event(&mut self, _event: &Event, _cx: &mut EventCx) {}
121 fn layout(&mut self, _rect: Rect, _cx: &mut crate::component::LayoutCx) {}
122 fn focusable(&self) -> bool { false }
123 fn style(&self) -> Style { self.style.clone() }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::testbuffer::TestBuffer;
130
131 #[test]
132 fn test_empty() {
133 let mut tb = TestBuffer::new(30, 1);
134 tb.render(&ProgressBar::new().ratio(0.0).width(30));
135 let line = (0..30).map(|_| "░").collect::<String>();
137 tb.assert_line(0, &line);
138 }
139
140 #[test]
141 fn test_full() {
142 let mut tb = TestBuffer::new(30, 1);
143 tb.render(&ProgressBar::new().ratio(1.0).width(30));
144 let line = (0..30).map(|_| "█").collect::<String>();
145 tb.assert_line(0, &line);
146 }
147
148 #[test]
149 fn test_with_label() {
150 let mut tb = TestBuffer::new(30, 1);
151 tb.render(&ProgressBar::new().ratio(0.5).width(30).label(true));
152 assert!(tb.buffer.cells.iter().any(|c| c.symbol == "%"));
154 }
155}