cursive_async_view/
progress.rs

1use crossbeam::channel::{bounded, unbounded, Receiver, Sender};
2use cursive_core::direction::Direction;
3use cursive_core::event::{AnyCb, Event, EventResult};
4use cursive_core::theme::PaletteColor;
5use cursive_core::utils::markup::StyledString;
6use cursive_core::view::{CannotFocus, Selector, View, ViewNotFound};
7use cursive_core::views::TextView;
8use cursive_core::{Cursive, Printer, Rect, Vec2};
9use interpolation::Ease;
10use log::warn;
11use num::clamp;
12use send_wrapper::SendWrapper;
13
14use std::thread;
15use std::time::Instant;
16
17use crate::{infinite::FPS, utils, AsyncView};
18
19/// An enum to be returned by the `poll_ready` callback, with additional information about the creation progress.
20pub enum AsyncProgressState<V: View> {
21    /// Indicates a not completed creation, which is still ongoing. Also reports the progress made as float value between 0 and 1.
22    Pending(f32),
23    /// Indicates a not completed creation, which cannot proceed further. Contains an error message to be displayed for the user.
24    Error(String),
25    /// Indicates a completed creation. Contains the new child view.
26    Available(V),
27}
28
29/// This struct contains the content of a single frame for `AsyncProgressView` with some metadata about the current frame.
30pub struct AnimationProgressFrame {
31    /// Stylized String which gets printed until the view is ready, or if the creation has failed.
32    pub content: StyledString,
33    /// Current position of the loading bar.
34    pub pos: usize,
35    /// Index of the next frame to be drawn, useful if you want to interpolate between two states of progress.
36    pub next_frame_idx: usize,
37}
38
39/// The default progress animation for a `AsyncProgressView`.
40///
41/// # Creating your own progress function
42///
43/// As an example a very basic progress function would look like this:
44///
45/// ```
46/// use crossbeam::channel::Sender;
47/// use cursive::{Cursive, CursiveExt};
48/// use cursive::views::TextView;
49/// use cursive::utils::markup::StyledString;
50/// use cursive_async_view::{AnimationProgressFrame, AsyncProgressView, AsyncProgressState};
51///
52/// fn my_progress_function(
53///     _width: usize,
54///     _height: usize,
55///     progress: f32,
56///     _pos: usize,
57///     frame_idx: usize,
58/// ) -> AnimationProgressFrame {
59///     AnimationProgressFrame {
60///         content: StyledString::plain(format!("{:.0}%", progress * 100.0)),
61///         pos: 0,
62///         next_frame_idx: frame_idx,
63///     }
64/// }
65///
66/// let mut siv = Cursive::default();
67/// let start = std::time::Instant::now();
68/// let async_view = AsyncProgressView::new(&mut siv, move || {
69///     if start.elapsed().as_secs() > 5 {
70///         AsyncProgressState::Pending(start.elapsed().as_secs() as f32 /5f32)
71///     } else {
72///         AsyncProgressState::Available(TextView::new("Loaded!"))
73///     }
74/// })
75/// .with_progress_fn(my_progress_function);
76/// ```
77///
78/// The progress function will display the progress in percent as a simple string.
79///
80/// The `width` and `height` parameters contain the maximum size the content may have
81/// (in characters). The `progress` parameter is guaranteed to be a `f32` between 0 and 1.
82/// The `pos` and `frame_idx` parameter are always from the animation frame of the previous iteration.
83pub fn default_progress(
84    width: usize,
85    _height: usize,
86    progress: f32,
87    pos: usize,
88    frame_idx: usize,
89) -> AnimationProgressFrame {
90    assert!(progress >= 0.0);
91    assert!(progress <= 1.0);
92
93    let foreground = PaletteColor::Highlight;
94    let background = PaletteColor::HighlightInactive;
95    let symbol = "━";
96
97    let duration = 30; //one second
98    let durationf = duration as f64;
99
100    let next_pos = width as f32 * progress;
101    let offset = next_pos as usize - pos;
102
103    let idx = frame_idx % duration;
104    let idxf = idx as f64;
105    let factor = (idxf / durationf).circular_out();
106    let end = (pos as f64 + offset as f64 * factor) as usize;
107
108    let mut result = StyledString::new();
109    result.append_styled(utils::repeat_str(symbol, end), foreground);
110    result.append_styled(utils::repeat_str(symbol, width - end), background);
111
112    AnimationProgressFrame {
113        content: result,
114        pos: end,
115        next_frame_idx: idx + 1,
116    }
117}
118
119/// The default error animation for a `AsyncProgressView`.
120///
121/// # Creating your own error animation
122///
123/// The creation is very similar to the progress animation, but the error message is given now as the first parameter.
124///
125/// ```
126/// use crossbeam::channel::Sender;
127/// use cursive::{Cursive, CursiveExt};
128/// use cursive::views::TextView;
129/// use cursive::utils::markup::StyledString;
130/// use cursive_async_view::{AnimationProgressFrame, AsyncProgressView, AsyncProgressState};
131///
132/// fn my_error_function(
133///     msg: String,
134///     _width: usize,
135///     _height: usize,
136///     progress: f32,
137///     _pos: usize,
138///     frame_idx: usize,
139/// ) -> AnimationProgressFrame {
140///     AnimationProgressFrame {
141///         content: StyledString::plain(format!("Error: {}", msg)),
142///         pos: 0,
143///         next_frame_idx: frame_idx,
144///     }
145/// }
146///
147/// let mut siv = Cursive::default();
148/// let start = std::time::Instant::now();
149/// let async_view = AsyncProgressView::new(&mut siv, move || {
150///     if start.elapsed().as_secs() > 5 {
151///         AsyncProgressState::Pending(start.elapsed().as_secs() as f32 /5f32)
152///     } else if true {
153///         AsyncProgressState::Error("Oh no, the view could not be loaded!".to_string())
154///     } else {
155///         AsyncProgressState::Available(TextView::new("I thought we never would get here!"))
156///     }
157/// })
158/// .with_error_fn(my_error_function);
159/// ```
160pub fn default_progress_error(
161    msg: String,
162    width: usize,
163    _height: usize,
164    progress: f32,
165    pos: usize,
166    frame_idx: usize,
167) -> AnimationProgressFrame {
168    assert!(progress >= 0.0);
169    assert!(progress <= 1.0);
170
171    let foreground = PaletteColor::Highlight;
172    let background = PaletteColor::HighlightInactive;
173    let symbol = "━";
174
175    let duration = 30; // half a second
176    let durationf = duration as f64;
177    let idx = frame_idx;
178    let idxf = idx as f64;
179    let factor = (idxf / durationf).circular_in_out();
180    let mut offset = width as f64 * factor;
181
182    let padding = width.saturating_sub(msg.len()) / 2;
183    let mut background_content = format!(
184        "{}{}{}",
185        utils::repeat_str(" ", padding),
186        msg,
187        utils::repeat_str(" ", padding),
188    );
189    // Check for non-char symbols
190    if background_content
191        .as_str()
192        .get(0..offset as usize)
193        .is_none()
194    {
195        offset += 2_f64;
196    }
197    let end = pos + offset as usize;
198    background_content.truncate(offset as usize);
199    let mut result = StyledString::new();
200    result.append_plain(background_content);
201    result.append_styled(
202        utils::repeat_str(symbol, {
203            if (pos + offset as usize) < width {
204                pos
205            } else {
206                width.saturating_sub(offset as usize)
207            }
208        }),
209        foreground,
210    );
211    result.append_styled(
212        utils::repeat_str(symbol, width.saturating_sub(end)),
213        background,
214    );
215
216    AnimationProgressFrame {
217        content: result,
218        pos,
219        next_frame_idx: frame_idx + 1,
220    }
221}
222
223/// An `AsyncProgressView` is a wrapper view that displays a progress bar, until the
224/// child view is successfully created or an error in the creation progress occured.
225///
226/// To achieve this a `poll_ready` callback is passed in the creation of `AsyncProgressView` which
227/// returns an `AsyncProgressState` that can indicate that the process is still `Pending` (this contains a float
228/// between 0 and 1, communicating the progress, this information is displayed in the bar), has been successfully
229/// completed `Available` containing the view to be displayed, or if the creation has thrown an `Error`
230/// containing a message to be shown to the user.
231///
232/// The `poll_ready` callback should only **check** for data to be
233/// available and create the child view when the data got available. It must
234/// **never** block until the data is available or do heavy calculations!
235/// Otherwise cursive cannot proceed displaying and your
236/// application will have a blocking loading process!
237///
238/// If you have troubles and need some more in-depth examples have a look at the provided `examples` in the project.
239///
240/// # Example usage
241///
242/// ```
243/// use cursive::{views::TextView, Cursive, CursiveExt};
244/// use cursive_async_view::{AsyncProgressView, AsyncProgressState};
245///
246/// let mut siv = Cursive::default();
247/// let start = std::time::Instant::now();
248/// let async_view = AsyncProgressView::new(&mut siv, move || {
249///     if start.elapsed().as_secs() < 3 {
250///         AsyncProgressState::Pending(start.elapsed().as_secs() as f32 / 3f32)
251///     } else {
252///         AsyncProgressState::Available(TextView::new("Finally it loaded!"))
253///     }
254/// });
255///
256/// siv.add_layer(async_view);
257/// // siv.run();
258/// ```
259///
260pub struct AsyncProgressView<T: View> {
261    view: AsyncProgressState<T>,
262    loading: TextView,
263    progress_fn: Box<
264        dyn Fn(usize, usize, f32, usize, usize) -> AnimationProgressFrame + Send + Sync + 'static,
265    >,
266    error_fn: Box<
267        dyn Fn(String, usize, usize, f32, usize, usize) -> AnimationProgressFrame
268            + Send
269            + Sync
270            + 'static,
271    >,
272    width: Option<usize>,
273    height: Option<usize>,
274    view_rx: Receiver<AsyncProgressState<T>>,
275    frame_index: usize,
276    dropped: Sender<()>,
277    pos: usize,
278}
279
280impl<T: View> AsyncProgressView<T> {
281    /// Create a new `AsyncProgressView` instance. The cursive reference is only used to
282    /// update the screen when a progress update is received. In order to show the view,
283    /// it has to be directly or indirectly added to a cursive layer like any other view.
284    ///
285    /// The creator function will be executed on a dedicated thread in the background.
286    /// Make sure that this function will never block indefinitely. Otherwise, the
287    /// creation thread will get stuck.
288    pub fn new<F>(siv: &mut Cursive, creator: F) -> Self
289    where
290        F: FnMut() -> AsyncProgressState<T> + 'static,
291    {
292        let (view_tx, view_rx) = unbounded();
293        let (error_tx, error_rx) = bounded(1);
294
295        Self::polling_cb(
296            siv,
297            Instant::now(),
298            SendWrapper::new(view_tx),
299            error_rx,
300            creator,
301        );
302
303        Self {
304            view: AsyncProgressState::Pending(0.0),
305            loading: TextView::new(""),
306            progress_fn: Box::new(default_progress),
307            error_fn: Box::new(default_progress_error),
308            width: None,
309            height: None,
310            view_rx,
311            frame_index: 0,
312            dropped: error_tx,
313            pos: 0,
314        }
315    }
316
317    fn polling_cb<F>(
318        siv: &mut Cursive,
319        instant: Instant,
320        chan: SendWrapper<Sender<AsyncProgressState<T>>>,
321        error_chan: Receiver<()>,
322        mut cb: F,
323    ) where
324        F: FnMut() -> AsyncProgressState<T> + 'static,
325    {
326        let res = cb();
327        match res {
328            AsyncProgressState::Pending(_) => {
329                let sink = siv.cb_sink().clone();
330                let cb = SendWrapper::new(cb);
331                match chan.send(res) {
332                    Ok(_) => {},
333                    Err(send_err) => warn!("Could not send progress to AsyncProgressView. It probably has been dropped before the asynchronous initialization of a view has been finished: {}", send_err),
334                }
335                thread::spawn(move || {
336                    // ensure ~60fps
337                    if let Some(duration) = FPS.checked_sub(instant.elapsed()) {
338                        thread::sleep(duration);
339                    }
340
341                    match sink.send(Box::new(move |siv| {
342                        Self::polling_cb(siv, Instant::now(), chan, error_chan, cb.take())
343                    })) {
344                        Ok(_) => {}
345                        Err(send_err) => {
346                            warn!("Could not send callback to cursive. It probably has been dropped before the asynchronous initialization of a view has been finished: {}", send_err);
347                        }
348                    }
349                });
350            }
351            AsyncProgressState::Error(content) => {
352                AsyncView::<T>::error_anim_cb(siv, error_chan);
353
354                match chan.send(AsyncProgressState::Error(content)) {
355                    Ok(_) => {}
356                    Err(send_err) => {
357                        warn!("View has been dropped before asynchronous initialization has been finished. Check if you removed this view from Cursive: {}", send_err);
358                    }
359                }
360                // chan dropped here, so the rx must handle disconnected
361            }
362            AsyncProgressState::Available(view) => {
363                match chan.send(AsyncProgressState::Available(view)) {
364                    Ok(_) => {}
365                    Err(send_err) => {
366                        warn!("View has been dropped before asynchronous initialization has been finished. Check if you removed this view from Cursive: {}", send_err);
367                    }
368                }
369            }
370        }
371    }
372
373    /// Mark the maximum allowed width in characters, the progress bar may consume.
374    /// By default, the width will be inherited by the parent view.
375    pub fn with_width(mut self, width: usize) -> Self {
376        self.set_width(width);
377        self
378    }
379
380    /// Mark the maximum allowed height in characters, the progress bar may consume.
381    /// By default, the height will be inherited by the parent view.
382    pub fn with_height(mut self, height: usize) -> Self {
383        self.set_height(height);
384        self
385    }
386
387    /// Set a custom progress function for this view, indicating the progress of the
388    /// wrapped view creation. See the `default_progress` function reference for an
389    /// example on how to create a custom progress function.
390    pub fn with_progress_fn<F>(mut self, progress_fn: F) -> Self
391    where
392        F: Fn(usize, usize, f32, usize, usize) -> AnimationProgressFrame + Send + Sync + 'static,
393    {
394        self.set_progress_fn(progress_fn);
395        self
396    }
397
398    pub fn with_error_fn<F>(mut self, error_fn: F) -> Self
399    where
400        F: Fn(String, usize, usize, f32, usize, usize) -> AnimationProgressFrame
401            + Send
402            + Sync
403            + 'static,
404    {
405        self.set_error_fn(error_fn);
406        self
407    }
408
409    /// Set the maximum allowed width in characters, the progress bar may consume.
410    pub fn set_width(&mut self, width: usize) {
411        self.width = Some(width);
412    }
413
414    /// Set the maximum allowed height in characters, the progress bar may consume.
415    pub fn set_height(&mut self, height: usize) {
416        self.height = Some(height);
417    }
418
419    /// Set a custom progress function for this view, indicating the progress of the
420    /// wrapped view creation. See the `default_progress` function reference for an
421    /// example on how to create a custom progress function.
422    ///
423    /// The function may be set at any time. The progress bar can be changed even if
424    /// the previous progress bar has already be drawn.
425    pub fn set_progress_fn<F>(&mut self, progress_fn: F)
426    where
427        F: Fn(usize, usize, f32, usize, usize) -> AnimationProgressFrame + Send + Sync + 'static,
428    {
429        self.progress_fn = Box::new(progress_fn);
430    }
431
432    /// Set a custom error function for this view, indicating that an error occured during the
433    /// wrapped view creation. See the `default_progress_error` function reference for an
434    /// example on how to create a custom error function.
435    ///
436    /// The function may be set at any time. The progress bar can be changed even if
437    /// the previous progress bar has already be drawn.
438    pub fn set_error_fn<F>(&mut self, error_fn: F)
439    where
440        F: Fn(String, usize, usize, f32, usize, usize) -> AnimationProgressFrame
441            + Send
442            + Sync
443            + 'static,
444    {
445        self.error_fn = Box::new(error_fn);
446    }
447
448    /// Make the progress bar inherit its width from the parent view. This is the default.
449    pub fn inherit_width(&mut self) {
450        self.width = None;
451    }
452
453    /// Make the progress bar inherit its height from the parent view. This is the default.
454    pub fn inherit_height(&mut self) {
455        self.height = None;
456    }
457}
458
459impl<T: View> Drop for AsyncProgressView<T> {
460    fn drop(&mut self) {
461        match self.dropped.send(()) {
462            Ok(_) => {}
463            Err(send_err) => warn!(
464                "Refreshing thread has been dropped before view has, this has no impact on your code and is a bug: {}",
465                send_err
466            ),
467        }
468    }
469}
470
471impl<T: View + Sized> View for AsyncProgressView<T> {
472    fn draw(&self, printer: &Printer) {
473        match &self.view {
474            AsyncProgressState::Available(v) => {
475                v.draw(printer);
476            }
477            AsyncProgressState::Error(_) | AsyncProgressState::Pending(_) => {
478                self.loading.draw(printer)
479            }
480        }
481    }
482
483    fn layout(&mut self, vec: Vec2) {
484        match &mut self.view {
485            AsyncProgressState::Available(v) => v.layout(vec),
486            AsyncProgressState::Error(_) | AsyncProgressState::Pending(_) => {
487                self.loading.layout(vec)
488            }
489        }
490    }
491
492    fn needs_relayout(&self) -> bool {
493        match &self.view {
494            AsyncProgressState::Available(v) => v.needs_relayout(),
495            AsyncProgressState::Error(_) | AsyncProgressState::Pending(_) => {
496                self.loading.needs_relayout()
497            }
498        }
499    }
500
501    fn required_size(&mut self, constraint: Vec2) -> Vec2 {
502        if !matches!(self.view, AsyncProgressState::Available(_)) {
503            if let Ok(state) = self.view_rx.try_recv() {
504                self.view = state
505            }
506        }
507
508        match &mut self.view {
509            AsyncProgressState::Available(v) => v.required_size(constraint),
510            AsyncProgressState::Pending(value) => {
511                let width = self.width.unwrap_or(constraint.x);
512                let height = self.height.unwrap_or(constraint.y);
513                let AnimationProgressFrame {
514                    content,
515                    pos,
516                    next_frame_idx,
517                } = (self.progress_fn)(
518                    width,
519                    height,
520                    clamp(*value, 0.0, 1.0),
521                    self.pos,
522                    self.frame_index,
523                );
524                self.pos = pos;
525                self.frame_index = next_frame_idx;
526                self.loading.set_content(content);
527                self.loading.required_size(constraint)
528            }
529            AsyncProgressState::Error(msg) => {
530                let width = self.width.unwrap_or(constraint.x);
531                let height = self.height.unwrap_or(constraint.y);
532                let AnimationProgressFrame {
533                    content,
534                    pos,
535                    next_frame_idx,
536                } = (self.error_fn)(
537                    (*msg).to_string(),
538                    width,
539                    height,
540                    0.5,
541                    self.pos,
542                    self.frame_index,
543                );
544                self.pos = pos;
545                self.frame_index = next_frame_idx;
546                self.loading.set_content(content);
547                self.loading.required_size(constraint)
548            }
549        }
550    }
551
552    fn on_event(&mut self, ev: Event) -> EventResult {
553        match &mut self.view {
554            AsyncProgressState::Available(v) => v.on_event(ev),
555            AsyncProgressState::Error(_) | AsyncProgressState::Pending(_) => {
556                self.loading.on_event(ev)
557            }
558        }
559    }
560
561    fn call_on_any<'a>(&mut self, sel: &Selector, cb: AnyCb<'a>) {
562        match &mut self.view {
563            AsyncProgressState::Available(v) => v.call_on_any(sel, cb),
564            AsyncProgressState::Error(_) | AsyncProgressState::Pending(_) => {
565                self.loading.call_on_any(sel, cb)
566            }
567        }
568    }
569
570    fn focus_view(&mut self, sel: &Selector) -> Result<EventResult, ViewNotFound> {
571        match &mut self.view {
572            AsyncProgressState::Available(v) => v.focus_view(sel),
573            AsyncProgressState::Error(_) | AsyncProgressState::Pending(_) => {
574                self.loading.focus_view(sel)
575            }
576        }
577    }
578
579    fn take_focus(&mut self, source: Direction) -> Result<EventResult, CannotFocus> {
580        match &mut self.view {
581            AsyncProgressState::Available(v) => v.take_focus(source),
582            AsyncProgressState::Error(_) | AsyncProgressState::Pending(_) => {
583                self.loading.take_focus(source)
584            }
585        }
586    }
587
588    fn important_area(&self, view_size: Vec2) -> Rect {
589        match &self.view {
590            AsyncProgressState::Available(v) => v.important_area(view_size),
591            AsyncProgressState::Error(_) | AsyncProgressState::Pending(_) => {
592                self.loading.important_area(view_size)
593            }
594        }
595    }
596}