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}