cursive_async_view/
infinite.rs

1use std::thread;
2use std::time::{Duration, Instant};
3
4use crossbeam::channel::{self, Receiver, Sender, TryRecvError};
5use cursive_core::align::HAlign;
6use cursive_core::direction::Direction;
7use cursive_core::event::{AnyCb, Event, EventResult};
8use cursive_core::theme::PaletteColor;
9use cursive_core::utils::markup::StyledString;
10use cursive_core::view::{CannotFocus, Selector, View, ViewNotFound};
11use cursive_core::views::TextView;
12use cursive_core::{Cursive, Printer, Rect, Vec2};
13use interpolation::Ease;
14use log::warn;
15use num::clamp;
16use send_wrapper::SendWrapper;
17
18use crate::utils;
19
20/// This struct represents the content of a single loading or error animation frame,
21/// produced by a animation function of the `AsyncView`. Read the documentation
22/// of the `default_animation` or `default_error` to see how to implement your own
23/// animation functions.
24pub struct AnimationFrame {
25    /// A `StyledString` that will be displayed inside a `TextView` for this frame.
26    pub content: StyledString,
27
28    /// The next `frame_idx` passed to the animation function when calculating
29    /// the next frame.
30    pub next_frame_idx: usize,
31}
32
33/// The default loading animation for a `AsyncView`.
34///
35/// # Creating your own loading function
36///
37/// As an example a very basic loading function would look like this:
38///
39/// ```
40/// use std::time::{Instant, Duration};
41/// use cursive::{Cursive, CursiveExt};
42/// use cursive::views::TextView;
43/// use cursive::utils::markup::StyledString;
44/// use cursive_async_view::{AsyncView, AsyncState, AnimationFrame};
45///
46/// fn my_loading_animation(
47///     _width: usize,
48///     _height: usize,
49///     frame_idx: usize,
50/// ) -> AnimationFrame {
51///     let content = if frame_idx < 30 {
52///         StyledString::plain("loading")
53///     } else {
54///         StyledString::plain("content")
55///     };
56///
57///     AnimationFrame {
58///         content,
59///         next_frame_idx: (frame_idx + 1) % 60,
60///     }
61/// }
62///
63/// let mut siv = Cursive::default();
64/// let instant = Instant::now();
65/// let async_view = AsyncView::new(&mut siv, move || {
66///     if instant.elapsed() > Duration::from_secs(5) {
67///         AsyncState::Available(
68///             TextView::new("Yay!\n\nThe content has loaded!")
69///         )
70///     } else {
71///         AsyncState::Pending
72///     }
73/// }).with_animation_fn(my_loading_animation);
74///
75/// siv.add_layer(async_view);
76/// // siv.run();
77/// ```
78///
79/// This animation function will first display `loading` for half a second and then display
80/// `content` for half a second.
81///
82/// The `width` and `height` parameters contain the maximum size the content may have
83/// (in characters). The initial `frame_idx` is 0.
84pub fn default_animation(width: usize, _height: usize, frame_idx: usize) -> AnimationFrame {
85    let foreground = PaletteColor::Highlight;
86    let background = PaletteColor::HighlightInactive;
87    let symbol = "━";
88
89    let duration = 60; // one second
90    let durationf = duration as f64;
91
92    let idx = frame_idx % duration;
93    let idxf = idx as f64;
94    let factor = idxf / durationf;
95    let begin_factor = clamp((factor % 1.0).circular_in_out(), 0.0, 1.0);
96    let end_factor = clamp(((factor + 0.25) % 1.0).circular_in_out() * 2.0, 0.0, 1.0);
97    let begin = (begin_factor * width as f64) as usize;
98    let end = (end_factor * width as f64) as usize;
99
100    let mut result = StyledString::default();
101    if end >= begin {
102        result.append_styled(utils::repeat_str(symbol, begin), background);
103        result.append_styled(utils::repeat_str(symbol, end - begin), foreground);
104        result.append_styled(utils::repeat_str(symbol, width - end), background);
105    } else {
106        result.append_styled(utils::repeat_str(symbol, end), foreground);
107        result.append_styled(utils::repeat_str(symbol, begin - end), background);
108        result.append_styled(utils::repeat_str(symbol, width - begin), foreground);
109    }
110
111    AnimationFrame {
112        content: result,
113        next_frame_idx: (idx + 1) % duration,
114    }
115}
116
117/// The default error animation for a `AsyncView`.
118///
119/// # Creating your own error function
120///
121/// As an example a very basic error function would look like this:
122///
123/// ```
124/// use std::time::{Instant, Duration};
125/// use cursive::{Cursive, CursiveExt};
126/// use cursive::views::TextView;
127/// use cursive::utils::markup::StyledString;
128/// use cursive_async_view::{AsyncView, AsyncState, AnimationFrame};
129///
130/// fn my_error_animation(
131///     msg: &str,
132///     _width: usize,
133///     _height: usize,
134///     _error_idx: usize,
135///     _frame_idx: usize,
136/// ) -> AnimationFrame {
137///     AnimationFrame {
138///         content: StyledString::plain(msg),
139///         next_frame_idx: 0,
140///     }
141/// }
142///
143/// let mut siv = Cursive::default();
144/// let instant = Instant::now();
145/// let async_view: AsyncView<TextView> = AsyncView::new(&mut siv, move || {
146///     if instant.elapsed() > Duration::from_secs(5) {
147///         AsyncState::Error("Oh no, an error occured!".to_string())
148///     } else {
149///         AsyncState::Pending
150///     }
151/// }).with_error_fn(my_error_animation);
152///
153/// siv.add_layer(async_view);
154/// // siv.run();
155/// ```
156///
157/// This error function will just display the error message itself.
158///
159/// The `width` and `height` prameters contain the maximum size the content may have
160/// (in characters). The initial `frame_idx` is 0.
161pub fn default_error(
162    msg: &str,
163    width: usize,
164    _height: usize,
165    error_idx: usize,
166    frame_idx: usize,
167) -> AnimationFrame {
168    let foreground = PaletteColor::Highlight;
169    let background = PaletteColor::HighlightInactive;
170    let symbol = "━";
171
172    let offset = utils::repeat_str(" ", HAlign::Center.get_offset(msg.len(), width));
173    let mut msg = format!("{}{}{}", offset, msg, offset);
174
175    let duration = 60; // one second
176    let durationf = duration as f64;
177    let cycle = if error_idx % duration > duration / 2 {
178        duration
179    } else {
180        0
181    };
182
183    let idx = frame_idx - (error_idx / duration) * duration;
184    let idxf = idx as f64;
185    let factor = idxf / durationf;
186    let begin_factor = clamp((factor % 1.0).circular_in_out(), 0.0, 1.0);
187    let end_factor = clamp(((factor + 0.25) % 1.0).circular_in_out() * 2.0, 0.0, 1.0);
188    let mut begin = (begin_factor * width as f64) as usize;
189    let end = (end_factor * width as f64) as usize;
190    if frame_idx == cycle + duration {
191        // Text can be fully shown
192        return AnimationFrame {
193            content: StyledString::plain(msg),
194            next_frame_idx: frame_idx,
195        };
196    }
197
198    let mut result = StyledString::default();
199    if end >= begin && idx > cycle {
200        if msg.as_str().get(0..begin).is_none() {
201            begin += 2;
202        }
203        msg.truncate(begin);
204        result.append_plain(msg);
205        result.append_styled(utils::repeat_str(symbol, end - begin), foreground);
206        result.append_styled(utils::repeat_str(symbol, width - end), background);
207    } else if end >= begin && idx <= cycle {
208        result.append_styled(utils::repeat_str(symbol, begin), background);
209        result.append_styled(utils::repeat_str(symbol, end - begin), foreground);
210        result.append_styled(utils::repeat_str(symbol, width - end), background);
211    } else if idx > cycle + duration / 2 {
212        if msg.as_str().get(0..begin).is_none() {
213            begin += 2
214        }
215        msg.truncate(begin);
216        result.append_plain(msg);
217        result.append_styled(utils::repeat_str(symbol, width - begin), foreground);
218    } else {
219        // Complete animation until text can be unveiled
220        result.append_styled(utils::repeat_str(symbol, end), foreground);
221        result.append_styled(utils::repeat_str(symbol, begin - end), background);
222        result.append_styled(utils::repeat_str(symbol, width - begin), foreground);
223    }
224
225    AnimationFrame {
226        content: result,
227        next_frame_idx: frame_idx + 1,
228    }
229}
230
231/// This enum is used in the ready_poll callback to tell the async view
232/// whether the view is already available, an error occured, or is still pending.
233pub enum AsyncState<V: View> {
234    /// The view of type `V` is now available and ready to be owned by the async view
235    /// where it will get layouted and drawn instead of the loading animation.
236    Available(V),
237
238    /// Loading of the view failed with the given error.
239    Error(String),
240
241    /// The view is not available yet, try again later.
242    Pending,
243}
244
245/// An `AsyncView` is a wrapper view that displays a loading screen, until the
246/// child view is ready to be created. The view can be used in two different
247/// ways.
248///
249/// # Poll-based AsyncView
250///
251/// The poll-based `AsyncView` is constructed via the `AsyncView::new` function
252/// and regularly calls the provided `poll_ready` function. It indicates
253/// whether the child view is available or not by returning an `AsyncState`
254/// enum. The `poll_ready` callback should only **check** for data to be
255/// available and create the child view when the data got available. It must
256/// **never** block until the data is available or do heavy calculations!
257///
258/// Use a different thread for long taking calculations. Check the `bg_task`
259/// example for an example on how to use a dedicated calculation thread with
260/// the `AsyncView`.
261///
262/// ## Example usage of the poll-based variant
263///
264/// ```
265/// use std::time::{Instant, Duration};
266/// use cursive::{views::TextView, Cursive, CursiveExt};
267/// use cursive_async_view::{AsyncView, AsyncState};
268///
269/// let mut siv = Cursive::default();
270/// let instant = Instant::now();
271/// let async_view = AsyncView::new(&mut siv, move || {
272///     // check if the view can be created
273///     if instant.elapsed() > Duration::from_secs(10) {
274///         AsyncState::Available(
275///             TextView::new("Yay!\n\nThe content has loaded!")
276///         )
277///     } else {
278///         AsyncState::Pending
279///     }
280/// });
281///
282/// siv.add_layer(async_view);
283/// // siv.run();
284/// ```
285///
286/// The content will be displayed after 10 seconds.
287///
288/// # Producing view data in a background thread
289///
290/// The second variant produces custom data in a background thread via the
291/// provided `bg_task` function. The produced data is then sent to the cursive
292/// thread and given to the provided `view_creator` function. This function
293/// should construct the child view and return it to the async view.
294///
295/// All heavy work **must** be done in the `bg_task` function. Otherwise,
296/// the cursive event loop will be blocked, preventing any rendering or event
297/// handling taking place.
298///
299/// ## Example usage for the background thread variant
300///
301/// ```
302/// use std::thread;
303/// use std::time::Duration;
304///
305/// use cursive::views::TextView;
306/// use cursive::{Cursive, CursiveExt};
307/// use cursive_async_view::AsyncView;
308///
309/// let mut siv = Cursive::default();
310/// let async_view = AsyncView::new_with_bg_creator(&mut siv, move || {
311///     // this function is executed in a background thread, so we can block
312///     // here as long as we like
313///     thread::sleep(Duration::from_secs(10));
314///
315///     // enough blocking, let's show the content
316///     Ok("Yeet! It worked 🖖")
317/// }, TextView::new); // create a text view from the string
318///
319/// siv.add_layer(async_view);
320/// // siv.run();
321/// ```
322///
323/// The content will be displayed after 10 seconds.
324pub struct AsyncView<T: View> {
325    view: AsyncState<T>,
326    loading: TextView,
327    animation_fn: Box<dyn Fn(usize, usize, usize) -> AnimationFrame + Send + Sync + 'static>,
328    error_fn:
329        Box<dyn Fn(&str, usize, usize, usize, usize) -> AnimationFrame + Send + Sync + 'static>,
330    width: Option<usize>,
331    height: Option<usize>,
332    pos: usize,
333    error_idx: usize,
334    rx: Receiver<AsyncState<T>>,
335    error_sender: Sender<()>,
336}
337
338lazy_static::lazy_static! {
339    pub(crate) static ref FPS: Duration = Duration::from_secs(1) / 60;
340}
341
342impl<T: View> AsyncView<T> {
343    /// Create a new `AsyncView` instance. The cursive reference is used
344    /// to control the refresh rate of the terminal when the loading animation
345    /// is running. In order to show the view, it has to be directly or indirectly
346    /// added to a cursive layer like any other view.
347    ///
348    /// The `ready_poll` function will be called regularly until the view has
349    /// either been loaded or errored. Use this function only to check whether
350    /// your data is available. Do not run heavy calculations in this function.
351    /// Instead use a dedicated thread for it as shown in the `bg_task` example.
352    pub fn new<F>(siv: &mut Cursive, ready_poll: F) -> Self
353    where
354        F: FnMut() -> AsyncState<T> + 'static,
355    {
356        // create communication channel between cursive event loop and
357        // this views layout code
358        let (tx, rx) = channel::unbounded();
359        let (error_tx, error_rx) = channel::bounded(1);
360
361        let instant = Instant::now();
362        Self::polling_cb(siv, instant, SendWrapper::new(tx), error_rx, ready_poll);
363
364        Self {
365            view: AsyncState::Pending,
366            loading: TextView::new(""),
367            animation_fn: Box::new(default_animation),
368            error_fn: Box::new(default_error),
369            width: None,
370            height: None,
371            pos: 0,
372            error_idx: 0,
373            rx,
374            error_sender: error_tx,
375        }
376    }
377
378    /// Create a new `AsyncView` instance. The cursive reference is used
379    /// to control the refresh rate of the terminal when the loading animation
380    /// is running. In order to show the view, it has to be directly or indirectly
381    /// added to a cursive layer like any other view.
382    ///
383    /// The `bg_task` function is executed on a background thread called
384    /// `cursive-async-view::bg_task`. It should be used to produce data of
385    /// type `D` which is converted to a view by the `view_creator` function.
386    pub fn new_with_bg_creator<F, C, D>(siv: &mut Cursive, bg_task: F, mut view_creator: C) -> Self
387    where
388        D: Send + 'static,
389        F: FnOnce() -> Result<D, String> + Send + 'static,
390        C: FnMut(D) -> T + 'static,
391    {
392        let (tx, rx) = channel::unbounded();
393
394        thread::Builder::new()
395            .name("cursive-async-view::bg_task".into())
396            .spawn(move || {
397                tx.send(bg_task()).unwrap();
398            })
399            .unwrap();
400
401        Self::new(siv, move || match rx.try_recv() {
402            Ok(Ok(data)) => AsyncState::Available(view_creator(data)),
403            Ok(Err(err)) => AsyncState::Error(err),
404            Err(TryRecvError::Empty) => AsyncState::Pending,
405            Err(TryRecvError::Disconnected) => {
406                AsyncState::Error("Internal error: bg_task disconnected unexpectedly!".to_string())
407            }
408        })
409    }
410
411    fn polling_cb<F>(
412        siv: &mut Cursive,
413        instant: Instant,
414        chan: SendWrapper<Sender<AsyncState<T>>>,
415        end_anim: Receiver<()>,
416        mut cb: F,
417    ) where
418        F: FnMut() -> AsyncState<T> + 'static,
419    {
420        match cb() {
421            AsyncState::Pending => {
422                let sink = siv.cb_sink().clone();
423                let cb = SendWrapper::new(cb);
424                thread::spawn(move || {
425                    // ensure ~60fps
426                    if let Some(duration) = FPS.checked_sub(instant.elapsed()) {
427                        thread::sleep(duration);
428                    }
429
430                    match sink.send(Box::new(move |siv| {
431                        Self::polling_cb(siv, Instant::now(), chan, end_anim, cb.take())
432                    })) {
433                        Ok(_) => {}
434                        Err(send_err) => {
435                            warn!("Could not send callback to cursive. It probably has been dropped before the asynchronous initialization of a view has been finished: {}", send_err);
436                        }
437                    }
438                });
439            }
440            AsyncState::Error(content) => {
441                // Start a thread running until the object has been dropped
442                Self::error_anim_cb(siv, end_anim);
443
444                // This may panic if the other site has been dropped Can happen
445                // if the view gets removed before the event loop has finished
446                // causing the sender to try to to communicate with a dead
447                // receiver To fix this we drop this error and warn the user
448                // that this behaviour is discouraged
449                match chan.send(AsyncState::Error(content)) {
450                    Ok(_) => {}
451                    Err(send_err) => {
452                        warn!("View has been dropped before asynchronous initialization has been finished. Check if you removed this view from Cursive: {}", send_err);
453                    }
454                }
455                // chan dropped here, so the rx must handle disconnected
456            }
457            AsyncState::Available(view) => match chan.send(AsyncState::Available(view)) {
458                Ok(_) => {}
459                Err(send_err) => {
460                    warn!("View has been dropped before asynchronous initialization has been finished. Check if you removed this view from Cursive: {}", send_err);
461                }
462            },
463        }
464    }
465
466    pub(crate) fn error_anim_cb(siv: &mut Cursive, chan: Receiver<()>) {
467        let sink = siv.cb_sink().clone();
468        thread::spawn(move || loop {
469            thread::sleep(Duration::from_millis(16));
470
471            match chan.try_recv() {
472                Ok(()) => break,
473                Err(_) => match sink.send(Box::new(|_| {})) {
474                    Ok(_) => {}
475                    Err(send_err) => {
476                        warn!(
477                            "Cursive has been dropped before AsyncView has been: {}",
478                            send_err
479                        );
480                    }
481                },
482            }
483        });
484    }
485
486    /// Mark the maximum allowed width in characters, the loading animation may consume.
487    /// By default, the width will be inherited by the parent view.
488    pub fn with_width(mut self, width: usize) -> Self {
489        self.set_width(width);
490        self
491    }
492
493    /// Mark the maximum allowed height in characters, the loading animation may consume.
494    /// By default, the height will be inherited by the parent view.
495    pub fn with_height(mut self, height: usize) -> Self {
496        self.set_height(height);
497        self
498    }
499
500    /// Set a custom animation function for this view, indicating that the wrapped view is
501    /// not available yet. See the `default_animation` function reference for an example on
502    /// how to create a custom animation function.
503    pub fn with_animation_fn<F>(mut self, animation_fn: F) -> Self
504    where
505        // We cannot use a lifetime bound to the AsyncView struct because View has a
506        //  'static requirement. Therefore we have to make sure the animation_fn is
507        // 'static, meaning it owns all values and does not reference anything
508        // outside of its scope. In practice this means all animation_fn must be
509        // `move |width| {...}` or fn's.
510        F: Fn(usize, usize, usize) -> AnimationFrame + Send + Sync + 'static,
511    {
512        self.set_animation_fn(animation_fn);
513        self
514    }
515
516    /// Set a custom error animation function for this view, indicating that the
517    /// wrapped view has failed to load. See the `default_error` function
518    /// reference for an example on how to create a custom error animation
519    /// function.
520    pub fn with_error_fn<F>(mut self, error_fn: F) -> Self
521    where
522        // We cannot use a lifetime bound to the AsyncView struct because View has a
523        //  'static requirement. Therefore we have to make sure the error_fn is
524        // 'static, meaning it owns all values and does not reference anything
525        // outside of its scope. In practice this means all animation_fn must be
526        // `move |width| {...}` or fn's.
527        F: Fn(&str, usize, usize, usize, usize) -> AnimationFrame + Send + Sync + 'static,
528    {
529        self.set_error_fn(error_fn);
530        self
531    }
532
533    /// Set the maximum allowed width in characters, the loading animation may consume.
534    pub fn set_width(&mut self, width: usize) {
535        self.width = Some(width);
536    }
537
538    /// Set the maximum allowed height in characters, the loading animation may consume.
539    pub fn set_height(&mut self, height: usize) {
540        self.height = Some(height);
541    }
542
543    /// Set a custom animation function for this view, indicating that the wrapped view is
544    /// not available yet. See the `default_animation` function reference for an example on
545    /// how to create a custom animation function.
546    ///
547    /// This function may be set at any time. The loading animation can be changed even if
548    /// the previous loading animation has already started.
549    pub fn set_animation_fn<F>(&mut self, animation_fn: F)
550    where
551        F: Fn(usize, usize, usize) -> AnimationFrame + Send + Sync + 'static,
552    {
553        self.animation_fn = Box::new(animation_fn);
554    }
555
556    /// Set a custom error animation function for this view, indicating that the wrapped view
557    /// has failed to load. See the `default_error` function reference for an example on
558    /// how to create a custom error animation function.
559    ///
560    /// This function may be set at any time. The error animation can be changed even if
561    /// the previous error animation has already started.
562    pub fn set_error_fn<F>(&mut self, error_fn: F)
563    where
564        F: Fn(&str, usize, usize, usize, usize) -> AnimationFrame + Send + Sync + 'static,
565    {
566        self.error_fn = Box::new(error_fn);
567    }
568
569    /// Make the loading animation inherit its width from the parent view. This is the default.
570    pub fn inherit_width(&mut self) {
571        self.width = None;
572    }
573
574    /// Make the loading animation inherit its height from the parent view. This is the default.
575    pub fn inherit_height(&mut self) {
576        self.height = None;
577    }
578}
579
580impl<T: View> Drop for AsyncView<T> {
581    fn drop(&mut self) {
582        match self.error_sender.send(()) {
583            Ok(_) => {}
584            Err(send_err) => warn!(
585                "Refreshing thread has been dropped before view has, this has no impact on your code and is a bug: {}",
586                send_err
587            ),
588        }
589    }
590}
591
592impl<T: View + Sized> View for AsyncView<T> {
593    fn draw(&self, printer: &Printer) {
594        match self.view {
595            AsyncState::Available(ref view) => view.draw(printer),
596            _ => self.loading.draw(printer),
597        }
598    }
599
600    fn layout(&mut self, vec: Vec2) {
601        match self.view {
602            AsyncState::Available(ref mut view) => view.layout(vec),
603            _ => self.loading.layout(vec),
604        }
605    }
606
607    fn needs_relayout(&self) -> bool {
608        match self.view {
609            AsyncState::Available(ref view) => view.needs_relayout(),
610            _ => true,
611        }
612    }
613
614    fn required_size(&mut self, constraint: Vec2) -> Vec2 {
615        match self.rx.try_recv() {
616            Ok(view) => {
617                if let AsyncState::Error(_) = view {
618                    self.error_idx = self.pos;
619                }
620
621                self.view = view;
622            }
623            Err(TryRecvError::Empty) => {
624                // if empty, try next tick
625            }
626            Err(TryRecvError::Disconnected) => {
627                // if disconnected, view is loaded or error message is displayed
628            }
629        }
630
631        match self.view {
632            AsyncState::Available(ref mut view) => view.required_size(constraint),
633            AsyncState::Error(ref msg) => {
634                let width = self.width.unwrap_or(constraint.x);
635                let height = self.height.unwrap_or(constraint.y);
636
637                let AnimationFrame {
638                    content,
639                    next_frame_idx,
640                } = (self.error_fn)(msg, width, height, self.error_idx, self.pos);
641                self.loading.set_content(content);
642                self.pos = next_frame_idx;
643
644                self.loading.required_size(constraint)
645            }
646            AsyncState::Pending => {
647                let width = self.width.unwrap_or(constraint.x);
648                let height = self.height.unwrap_or(constraint.y);
649
650                let AnimationFrame {
651                    content,
652                    next_frame_idx,
653                } = (self.animation_fn)(width, height, self.pos);
654                self.loading.set_content(content);
655                self.pos = next_frame_idx;
656
657                self.loading.required_size(constraint)
658            }
659        }
660    }
661
662    fn on_event(&mut self, ev: Event) -> EventResult {
663        match self.view {
664            AsyncState::Available(ref mut view) => view.on_event(ev),
665            _ => EventResult::Ignored,
666        }
667    }
668
669    fn call_on_any<'a>(&mut self, sel: &Selector, cb: AnyCb<'a>) {
670        if let AsyncState::Available(ref mut view) = self.view {
671            view.call_on_any(sel, cb)
672        }
673    }
674
675    fn focus_view(&mut self, sel: &Selector) -> Result<EventResult, ViewNotFound> {
676        match self.view {
677            AsyncState::Available(ref mut view) => view.focus_view(sel),
678            _ => Err(ViewNotFound),
679        }
680    }
681
682    fn take_focus(&mut self, source: Direction) -> Result<EventResult, CannotFocus> {
683        match self.view {
684            AsyncState::Available(ref mut view) => view.take_focus(source),
685            _ => Err(CannotFocus),
686        }
687    }
688
689    fn important_area(&self, view_size: Vec2) -> Rect {
690        match self.view {
691            AsyncState::Available(ref view) => view.important_area(view_size),
692            _ => self.loading.important_area(view_size),
693        }
694    }
695}