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}