Skip to main content

termint/widgets/
progress_bar.rs

1use std::{cell::Cell, rc::Rc};
2
3use crate::{
4    buffer::Buffer,
5    geometry::{Rect, Vec2},
6    style::Style,
7    widgets::cache::Cache,
8};
9
10use super::{Element, Widget};
11
12/// A widget that displays a horizontal progress bar.
13///
14/// The [`ProgressBar`] visually represents a percentage value in the range
15/// `0.0` to `100.0`. It can be styled and configured to use custom characters.
16///
17/// # Example
18/// ```rust
19/// # use std::{cell::Cell, rc::Rc};
20/// # use termint::{widgets::ProgressBar, enums::Color, term::Term};
21/// # fn example() -> Result<(), termint::Error> {
22/// let state = Rc::new(Cell::new(69.0));
23/// let pb = ProgressBar::new(state.clone())
24///     .thumb_chars(['▎', '▌', '▊', '█'])
25///     .thumb_style(Color::Blue)
26///     .track_char('=')
27///     .style(Color::White);
28///
29/// // You can then render it using Term
30/// let mut term = Term::default();
31/// term.render(pb)?;
32/// # Ok(())
33/// # }
34/// ```
35pub struct ProgressBar {
36    state: Rc<Cell<f64>>,
37    thumb_chars: Vec<char>,
38    thumb_style: Style,
39    track_char: char,
40    style: Style,
41}
42
43impl ProgressBar {
44    /// Creates a new [`ProgressBar`] with given percentage state.
45    ///
46    /// # Example
47    /// ```rust
48    /// # use std::{cell::Cell, rc::Rc};
49    /// # use termint::widgets::ProgressBar;
50    /// let state = Rc::new(Cell::new(69.0));
51    /// let pb = ProgressBar::new(state.clone());
52    /// ```
53    #[must_use]
54    pub fn new(state: Rc<Cell<f64>>) -> Self {
55        Self {
56            state,
57            thumb_style: Default::default(),
58            thumb_chars: vec!['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'],
59            track_char: ' ',
60            style: Default::default(),
61        }
62    }
63
64    /// Sets the thumb characters used for rendering the progress.
65    ///
66    /// It can contain any number of character, but the iterator should start
67    /// with the least progress character and end with most progress character.
68    ///
69    /// # Example
70    /// ```rust
71    /// # use std::{cell::Cell, rc::Rc};
72    /// # use termint::widgets::ProgressBar;
73    /// # let state = Rc::new(Cell::new(69.0));
74    /// let pb = ProgressBar::new(state.clone())
75    ///     .thumb_chars(['▎', '▌', '▊', '█']);
76    /// ```
77    #[must_use]
78    pub fn thumb_chars<C>(mut self, chars: C) -> Self
79    where
80        C: IntoIterator<Item = char>,
81    {
82        self.thumb_chars = chars.into_iter().collect();
83        self
84    }
85
86    /// Sets the style of the [`ProgressBar`]'s thumb.
87    ///
88    /// You can provide any type convertible to [`Style`].
89    #[must_use]
90    pub fn thumb_style<S>(mut self, style: S) -> Self
91    where
92        S: Into<Style>,
93    {
94        self.thumb_style = style.into();
95        self
96    }
97
98    /// Sets the character used for the track of the [`ProgressBar`].§
99    #[must_use]
100    pub fn track_char(mut self, track: char) -> Self {
101        self.track_char = track;
102        self
103    }
104
105    /// Sets the base style of the [`ProgressBar`].
106    ///
107    /// You can provide any type convertible to [`Style`].
108    #[must_use]
109    pub fn style<S>(mut self, style: S) -> Self
110    where
111        S: Into<Style>,
112    {
113        self.style = style.into();
114        self
115    }
116}
117
118impl Widget for ProgressBar {
119    fn render(&self, buffer: &mut Buffer, rect: Rect, _cache: &mut Cache) {
120        if rect.is_empty() || self.thumb_chars.is_empty() {
121            return;
122        }
123
124        let (full_cells, head_id) = self.calc_size(&rect);
125        let mut rest_len = rect.width().saturating_sub(full_cells);
126
127        let mut track_pos = Vec2::new(rect.x() + full_cells, rect.y());
128        if head_id > 0 {
129            rest_len = rest_len.saturating_sub(1);
130            buffer[track_pos]
131                .char(self.thumb_chars[head_id])
132                .style(self.thumb_style);
133            track_pos.x += 1;
134        }
135
136        let thumb = self.thumb_chars[self.thumb_chars.len() - 1];
137        buffer.set_str_styled(
138            thumb.to_string().repeat(full_cells),
139            rect.pos(),
140            self.thumb_style,
141        );
142
143        buffer.set_str_styled(
144            self.track_char.to_string().repeat(rest_len),
145            &track_pos,
146            self.style,
147        );
148    }
149
150    fn height(&self, _size: &Vec2) -> usize {
151        1
152    }
153
154    fn width(&self, size: &Vec2) -> usize {
155        size.x
156    }
157}
158
159impl ProgressBar {
160    /// Calculates the size of full cells and head ID to get corresponding
161    /// progress character with.
162    fn calc_size(&self, rect: &Rect) -> (usize, usize) {
163        let progress = (self.state.get() / 100.0).clamp(0.0, 1.0);
164        let len = rect.width() as f64 * progress;
165        let full_cells = len.floor() as usize;
166
167        let frac = len - full_cells as f64;
168        let head_id = (frac * (self.thumb_chars.len() - 1) as f64).round();
169        (full_cells, head_id as usize)
170    }
171}
172
173impl From<ProgressBar> for Element {
174    fn from(value: ProgressBar) -> Self {
175        Element::new(value)
176    }
177}
178
179impl From<ProgressBar> for Box<dyn Widget> {
180    fn from(value: ProgressBar) -> Self {
181        Box::new(value)
182    }
183}