scrin 0.1.79

A terminal UI toolkit with panes, widgets, overlays, animations, and Aisling-powered effects/loaders.
Documentation
use std::cell::RefCell;

use aisling::effects::EffectKind;
use aisling::loaders::{LoaderKind, LoaderProgress};

use crate::core::buffer::Buffer;
use crate::core::color::Color;
use crate::core::rect::Rect;
use crate::effects::{EffectPlayer, LoaderPlayer};
use crate::widgets::Widget;

struct CachedEffectFrame {
    width: u16,
    height: u16,
    frame: usize,
    buffer: Buffer,
}

pub struct RetainedEffectWidget {
    pub player: EffectPlayer,
    cache: RefCell<Option<CachedEffectFrame>>,
}

impl RetainedEffectWidget {
    pub fn new(kind: EffectKind, text: &str) -> Self {
        Self::from_player(EffectPlayer::new(kind, text))
    }

    pub fn from_player(player: EffectPlayer) -> Self {
        Self {
            player,
            cache: RefCell::new(None),
        }
    }

    pub fn with_accent(mut self, accent: Color) -> Self {
        self.player = self.player.with_accent(accent);
        self.invalidate();
        self
    }

    pub fn with_duration(mut self, duration: usize) -> Self {
        self.player = self.player.with_duration(duration);
        self.invalidate();
        self
    }

    pub fn with_size(mut self, width: usize, height: usize) -> Self {
        self.player = self.player.with_size(width, height);
        self.invalidate();
        self
    }

    pub fn with_seed(mut self, seed: u64) -> Self {
        self.player = self.player.with_seed(seed);
        self.invalidate();
        self
    }

    pub fn with_gradient_colors(mut self, colors: Vec<Color>, angle: f32) -> Self {
        self.player = self.player.with_gradient_colors(colors, angle);
        self.invalidate();
        self
    }

    pub fn advance(&mut self) {
        self.player.advance();
    }

    pub fn advance_n(&mut self, n: usize) {
        self.player.advance_n(n);
    }

    pub fn set_frame(&mut self, frame: usize) {
        self.player.set_frame(frame);
    }

    pub fn invalidate(&self) {
        *self.cache.borrow_mut() = None;
    }

    pub fn with_cached_buffer<R>(
        &self,
        width: u16,
        height: u16,
        f: impl FnOnce(&Buffer) -> R,
    ) -> R {
        let frame = self.player.current_frame_index();
        let needs_rebuild = match self.cache.borrow().as_ref() {
            Some(cached) => {
                cached.width != width || cached.height != height || cached.frame != frame
            }
            None => true,
        };
        if needs_rebuild {
            let mut buffer = Buffer::new(width as usize, height as usize);
            self.player
                .render_to_buffer(&mut buffer, Rect::new(0, 0, width, height));
            *self.cache.borrow_mut() = Some(CachedEffectFrame {
                width,
                height,
                frame,
                buffer,
            });
        }
        let cache = self.cache.borrow();
        f(&cache
            .as_ref()
            .expect("retained effect cache must exist")
            .buffer)
    }
}

impl Widget for RetainedEffectWidget {
    fn render(&self, buffer: &mut Buffer, area: Rect) {
        if area.is_empty() {
            return;
        }
        self.with_cached_buffer(area.width, area.height, |cached| {
            buffer.copy_from_at(cached, area.x as usize, area.y as usize);
        });
    }
}

struct CachedLoaderFrame {
    width: u16,
    height: u16,
    tick: usize,
    progress: LoaderProgress,
    buffer: Buffer,
}

pub struct RetainedLoaderWidget {
    pub player: LoaderPlayer,
    pub tick: usize,
    pub progress: LoaderProgress,
    cache: RefCell<Option<CachedLoaderFrame>>,
}

impl RetainedLoaderWidget {
    pub fn new(kind: LoaderKind) -> Self {
        Self::from_player(LoaderPlayer::new(kind))
    }

    pub fn from_player(player: LoaderPlayer) -> Self {
        Self {
            player,
            tick: 0,
            progress: LoaderProgress::new(0.0),
            cache: RefCell::new(None),
        }
    }

    pub fn with_accent(mut self, accent: Color) -> Self {
        self.player = self.player.with_accent(accent);
        self.invalidate();
        self
    }

    pub fn with_size(mut self, width: usize, height: usize) -> Self {
        self.player = self.player.with_size(width, height);
        self.invalidate();
        self
    }

    pub fn with_label(mut self, label: String) -> Self {
        self.player = self.player.with_label(label);
        self.invalidate();
        self
    }

    pub fn with_unit(mut self, unit: &str) -> Self {
        self.player = self.player.with_unit(unit);
        self.invalidate();
        self
    }

    pub fn with_fraction(mut self, fraction: bool) -> Self {
        self.player = self.player.with_fraction(fraction);
        self.invalidate();
        self
    }

    pub fn with_gradient_colors(mut self, colors: Vec<Color>, angle: f32) -> Self {
        self.player = self.player.with_gradient_colors(colors, angle);
        self.invalidate();
        self
    }

    pub fn with_tick(mut self, tick: usize) -> Self {
        self.tick = tick;
        self
    }

    pub fn with_progress(mut self, progress: LoaderProgress) -> Self {
        self.progress = progress;
        self
    }

    pub fn set_tick(&mut self, tick: usize) {
        self.tick = tick;
    }

    pub fn advance(&mut self) {
        self.tick = self.tick.wrapping_add(1);
    }

    pub fn set_progress(&mut self, progress: LoaderProgress) {
        self.progress = progress;
    }

    pub fn invalidate(&self) {
        *self.cache.borrow_mut() = None;
    }

    pub fn with_cached_buffer<R>(
        &self,
        width: u16,
        height: u16,
        f: impl FnOnce(&Buffer) -> R,
    ) -> R {
        let needs_rebuild = match self.cache.borrow().as_ref() {
            Some(cached) => {
                cached.width != width
                    || cached.height != height
                    || cached.tick != self.tick
                    || cached.progress != self.progress
            }
            None => true,
        };
        if needs_rebuild {
            let mut buffer = Buffer::new(width as usize, height as usize);
            self.player.render(
                self.tick,
                self.progress,
                &mut buffer,
                Rect::new(0, 0, width, height),
            );
            *self.cache.borrow_mut() = Some(CachedLoaderFrame {
                width,
                height,
                tick: self.tick,
                progress: self.progress,
                buffer,
            });
        }
        let cache = self.cache.borrow();
        f(&cache
            .as_ref()
            .expect("retained loader cache must exist")
            .buffer)
    }
}

impl Widget for RetainedLoaderWidget {
    fn render(&self, buffer: &mut Buffer, area: Rect) {
        if area.is_empty() {
            return;
        }
        self.with_cached_buffer(area.width, area.height, |cached| {
            buffer.copy_from_at(cached, area.x as usize, area.y as usize);
        });
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::widgets::Widget;

    #[test]
    fn retained_effect_renders_cached_frame() {
        let widget = RetainedEffectWidget::new(EffectKind::Matrix, "hi").with_size(8, 2);
        let mut buffer = Buffer::new(8, 2);
        widget.render(&mut buffer, Rect::new(0, 0, 8, 2));
        assert_eq!(widget.with_cached_buffer(8, 2, |cached| cached.width()), 8);
    }

    #[test]
    fn retained_loader_renders_cached_frame() {
        let widget = RetainedLoaderWidget::new(LoaderKind::Bar)
            .with_size(12, 2)
            .with_progress(LoaderProgress::new(0.5));
        let mut buffer = Buffer::new(12, 2);
        widget.render(&mut buffer, Rect::new(0, 0, 12, 2));
        assert_eq!(
            widget.with_cached_buffer(12, 2, |cached| cached.width()),
            12
        );
    }
}