cursive_core/views/
progress_bar.rs

1use crate::align::HAlign;
2use crate::style::{ColorStyle, ColorType, Effect, PaletteColor};
3use crate::utils::Counter;
4use crate::view::View;
5use crate::{Printer, With};
6use std::cmp;
7use std::thread;
8
9// pub type CbPromise = Option<Box<Fn(&mut Cursive) + Send>>;
10
11/// Animated bar showing a progress value.
12///
13/// This bar has an internal counter, and adapts the length of the displayed
14/// bar to the relative position of the counter between a minimum and maximum
15/// values.
16///
17/// It also prints a customizable text in the center of the bar, which
18/// defaults to the progression percentage.
19///
20/// The bar defaults to the current theme's highlight color,
21/// but that can be customized.
22///
23/// # Example
24///
25/// ```
26/// # use cursive_core::views::ProgressBar;
27/// let bar = ProgressBar::new().with_task(|counter| {
28///     // This closure is called in parallel.
29///     for _ in 0..100 {
30///         // Here we can communicate some
31///         // advancement back to the bar.
32///         counter.tick(1);
33///     }
34/// });
35/// ```
36pub struct ProgressBar {
37    min: usize,
38    max: usize,
39    value: Counter,
40    color: ColorType,
41    // TODO: use a Promise instead?
42    label_maker: Box<dyn Fn(usize, (usize, usize)) -> String + Send + Sync>,
43}
44
45fn make_percentage(value: usize, (min, max): (usize, usize)) -> String {
46    if value < min {
47        return String::from("0 %");
48    }
49
50    let (percentage, extra) = ratio(value - min, max - min, 100);
51    let percentage = if extra > 4 {
52        percentage + 1
53    } else {
54        percentage
55    };
56    format!("{percentage} %")
57}
58
59/// Returns length * value/max
60///
61/// Constraint: `value` from 0 to `max` should, as much as possible, produce equal-sized segments
62/// from 0 to length.
63///
64/// Returns a tuple with:
65/// * The integer part of the division
66/// * A value between 0 and 8 (exclusive) corresponding to the remainder.
67fn ratio(value: usize, max: usize, length: usize) -> (usize, usize) {
68    let integer = length * value / max;
69    let fraction = length * value - max * integer;
70
71    let fraction = fraction * 8 / max;
72
73    (integer, fraction)
74}
75
76new_default!(ProgressBar);
77
78impl ProgressBar {
79    /// Creates a new progress bar.
80    ///
81    /// Default values:
82    ///
83    /// * `min`: 0
84    /// * `max`: 100
85    /// * `value`: 0
86    pub fn new() -> Self {
87        ProgressBar {
88            min: 0,
89            max: 100,
90            value: Counter::new(0),
91            color: PaletteColor::Highlight.into(),
92            label_maker: Box::new(make_percentage),
93        }
94    }
95
96    /// Sets the value to follow.
97    ///
98    /// Use this to manually control the progress to display
99    /// by directly modifying the value pointed to by `value`.
100    #[must_use]
101    pub fn with_value(self, value: Counter) -> Self {
102        self.with(|s| s.set_counter(value))
103    }
104
105    /// Starts a function in a separate thread, and monitor the progress.
106    ///
107    /// `f` will be given a `Counter` to increment the bar's progress.
108    ///
109    /// This does not reset the value, so it can be called several times
110    /// to advance the progress in multiple sessions.
111    pub fn start<F: FnOnce(Counter) + Send + 'static>(&mut self, f: F) {
112        let counter: Counter = self.value.clone();
113
114        thread::spawn(move || {
115            f(counter);
116        });
117    }
118
119    /// Starts a function in a separate thread, and monitor the progress.
120    ///
121    /// Chainable variant.
122    #[must_use]
123    pub fn with_task<F: FnOnce(Counter) + Send + 'static>(self, task: F) -> Self {
124        self.with(|s| s.start(task))
125    }
126
127    /// Sets the label generator.
128    ///
129    /// The given function will be called with `(value, (min, max))`.
130    /// Its output will be used as the label to print inside the progress bar.
131    ///
132    /// The default one shows a percentage progress:
133    ///
134    /// ```
135    /// fn make_progress(value: usize, (min, max): (usize, usize)) -> String {
136    ///     let percent = 101 * (value - min) / (1 + max - min);
137    ///     format!("{} %", percent)
138    /// }
139    /// ```
140    #[must_use]
141    pub fn with_label<F: Fn(usize, (usize, usize)) -> String + 'static + Send + Sync>(
142        self,
143        label_maker: F,
144    ) -> Self {
145        self.with(|s| s.set_label(label_maker))
146    }
147
148    /// Sets the label generator.
149    ///
150    /// The given function will be called with `(value, (min, max))`.
151    /// Its output will be used as the label to print inside the progress bar.
152    #[crate::callback_helpers]
153    pub fn set_label<F: Fn(usize, (usize, usize)) -> String + 'static + Send + Sync>(
154        &mut self,
155        label_maker: F,
156    ) {
157        self.label_maker = Box::new(label_maker);
158    }
159
160    /// Sets the minimum value.
161    ///
162    /// When `value` equals `min`, the bar is at the minimum level.
163    ///
164    /// If `self.min > max`, `self.min` is set to `max`.
165    ///
166    /// Chainable variant.
167    #[must_use]
168    pub fn min(self, min: usize) -> Self {
169        self.with(|s| s.set_min(min))
170    }
171
172    /// Sets the minimum value.
173    ///
174    /// When `value` equals `min`, the bar is at the minimum level.
175    ///
176    /// If `self.min > max`, `self.min` is set to `max`.
177    pub fn set_min(&mut self, min: usize) {
178        self.min = min;
179        self.max = cmp::max(self.max, self.min);
180    }
181
182    /// Sets the maximum value.
183    ///
184    /// When `value` equals `max`, the bar is at the maximum level.
185    ///
186    /// If `min > self.max`, `self.max` is set to `min`.
187    ///
188    /// Chainable variant.
189    #[must_use]
190    pub fn max(self, max: usize) -> Self {
191        self.with(|s| s.set_max(max))
192    }
193
194    /// Sets the maximum value.
195    ///
196    /// When `value` equals `max`, the bar is at the maximum level.
197    ///
198    /// If `min > self.max`, `self.max` is set to `min`.
199    pub fn set_max(&mut self, max: usize) {
200        self.max = max;
201        self.min = cmp::min(self.min, self.max);
202    }
203
204    /// Sets the `min` and `max` range for the value.
205    ///
206    /// If `min > max`, swap the two values.
207    ///
208    /// Chainable variant.
209    #[must_use]
210    pub fn range(self, min: usize, max: usize) -> Self {
211        self.with(|s| s.set_range(min, max))
212    }
213
214    /// Sets the `min` and `max` range for the value.
215    ///
216    /// If `min > max`, swap the two values.
217    pub fn set_range(&mut self, min: usize, max: usize) {
218        if min > max {
219            self.set_min(max);
220            self.set_max(min);
221        } else {
222            self.set_min(min);
223            self.set_max(max);
224        }
225    }
226
227    /// Sets the current value.
228    ///
229    /// Value is clamped between `min` and `max`.
230    pub fn set_value(&mut self, value: usize) {
231        self.value.set(value);
232    }
233
234    /// Sets the value to follow.
235    ///
236    /// Use this to manually control the progress to display
237    /// by directly modifying the value pointed to by `value`.
238    pub fn set_counter(&mut self, value: Counter) {
239        self.value = value;
240    }
241
242    /// Sets the color style.
243    ///
244    /// The default color is `PaletteColor::Highlight`.
245    pub fn set_color<C>(&mut self, color: C)
246    where
247        C: Into<ColorType>,
248    {
249        self.color = color.into();
250    }
251
252    /// Sets the color style.
253    ///
254    /// Chainable variant of `set_color`.
255    #[must_use]
256    pub fn with_color<C>(self, color: C) -> Self
257    where
258        C: Into<ColorType>,
259    {
260        self.with(|s| s.set_color(color))
261    }
262}
263
264fn sub_block(extra: usize) -> &'static str {
265    match extra {
266        0 => " ",
267        1 => "▏",
268        2 => "▎",
269        3 => "▍",
270        4 => "▌",
271        5 => "▋",
272        6 => "▊",
273        7 => "▉",
274        _ => "█",
275    }
276}
277
278impl View for ProgressBar {
279    fn draw(&self, printer: &Printer) {
280        // Now, the bar itself...
281        let available = printer.size.x;
282
283        let value = self.value.get();
284
285        // If we're under the minimum, don't draw anything.
286        // If we're over the maximum, we'll try to draw more, but the printer
287        // will crop us anyway, so it's not a big deal.
288        let (length, extra) = if value < self.min {
289            (0, 0)
290        } else {
291            ratio(value - self.min, self.max - self.min, available)
292        };
293
294        let label = (self.label_maker)(value, (self.min, self.max));
295        let offset = HAlign::Center.get_offset(label.len(), printer.size.x);
296
297        let color_style = ColorStyle::new(PaletteColor::HighlightText, self.color);
298
299        printer.with_color(color_style, |printer| {
300            // TODO: Instead, write it with self.color and inherit_parent background?
301            // Draw the right half of the label in reverse
302            printer.with_effect(Effect::Reverse, |printer| {
303                printer.print((length, 0), sub_block(extra));
304                printer.print((offset, 0), &label);
305            });
306            let printer = &printer.cropped((length, 1));
307            printer.print_hline((0, 0), length, " ");
308
309            // Draw the left part in color_style (it may be cropped)
310            printer.print((offset, 0), &label);
311        });
312    }
313}
314
315#[crate::blueprint(ProgressBar::new())]
316struct Blueprint {
317    min: Option<usize>,
318    max: Option<usize>,
319    value: Option<usize>,
320    color: Option<ColorType>,
321    label: Option<_>,
322}