scrin 0.1.76

A terminal UI toolkit with panes, widgets, overlays, animations, and Aisling-powered effects/loaders.
Documentation
use crate::core::buffer::Buffer;
use crate::core::color::Color;
use crate::core::rect::Rect;
use aisling::color::{Color as AislingColor, Gradient as AislingGradient, GradientDirection};
use aisling::effects::{Effect, EffectConfig, EffectKind};

pub struct EffectPlayer {
    kind: EffectKind,
    text: String,
    config: EffectConfig,
    frames: Vec<aisling::frame::Frame>,
    current_frame: usize,
    accent: Color,
}

impl EffectPlayer {
    pub fn new(kind: EffectKind, text: &str) -> Self {
        let config = EffectConfig::default();
        let frames = Effect::new(kind, text).frames();
        Self {
            kind,
            text: text.to_string(),
            config,
            frames,
            current_frame: 0,
            accent: Color::rgb(88, 166, 255),
        }
    }

    pub fn with_config(kind: EffectKind, text: &str, config: EffectConfig) -> Self {
        let frames = Effect::with_config(kind, text, config.clone()).frames();
        Self {
            kind,
            text: text.to_string(),
            config,
            frames,
            current_frame: 0,
            accent: Color::rgb(88, 166, 255),
        }
    }

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

    pub fn with_duration(mut self, duration: usize) -> Self {
        self.config = self.config.with_duration(duration);
        let frames = Effect::with_config(self.kind, &self.text, self.config.clone()).frames();
        self.frames = frames;
        self
    }

    pub fn with_size(mut self, width: usize, height: usize) -> Self {
        self.config = self.config.with_canvas_size(width, height);
        let frames = Effect::with_config(self.kind, &self.text, self.config.clone()).frames();
        self.frames = frames;
        self
    }

    pub fn with_seed(mut self, seed: u64) -> Self {
        self.config = self.config.with_seed(seed);
        let frames = Effect::with_config(self.kind, &self.text, self.config.clone()).frames();
        self.frames = frames;
        self
    }

    pub fn with_gradient_colors(mut self, colors: Vec<Color>, angle: f32) -> Self {
        let a_colors: Vec<AislingColor> = colors
            .into_iter()
            .map(|c| AislingColor::rgb(c.r, c.g, c.b))
            .collect();
        let grad = AislingGradient::new(a_colors, 32);
        let dir = match angle as u32 {
            0 => GradientDirection::Horizontal,
            90 => GradientDirection::Vertical,
            45 => GradientDirection::Diagonal,
            _ => GradientDirection::Diagonal,
        };
        self.config = self.config.with_gradient(grad, dir);
        let frames = Effect::with_config(self.kind, &self.text, self.config.clone()).frames();
        self.frames = frames;
        self
    }

    pub fn total_frames(&self) -> usize {
        self.frames.len()
    }

    pub fn current_frame_index(&self) -> usize {
        self.current_frame
    }

    pub fn advance(&mut self) {
        if !self.frames.is_empty() {
            self.current_frame = (self.current_frame + 1) % self.frames.len();
        }
    }

    pub fn advance_n(&mut self, n: usize) {
        if !self.frames.is_empty() {
            self.current_frame = (self.current_frame + n) % self.frames.len();
        }
    }

    pub fn set_frame(&mut self, frame: usize) {
        if !self.frames.is_empty() {
            self.current_frame = frame % self.frames.len();
        }
    }

    pub fn progress(&self) -> f32 {
        if self.frames.is_empty() {
            1.0
        } else {
            self.current_frame as f32 / self.frames.len() as f32
        }
    }

    pub fn is_complete(&self) -> bool {
        self.current_frame >= self.frames.len().saturating_sub(1)
    }

    pub fn render_to_buffer(&self, buffer: &mut Buffer, area: Rect) {
        if self.frames.is_empty() {
            return;
        }
        let frame = &self.frames[self.current_frame];
        let fw = frame.width();
        let fh = frame.height();
        for fy in 0..fh.min(area.height as usize) {
            for fx in 0..fw.min(area.width as usize) {
                if let Some(cell) = frame.cell(fx, fy) {
                    let x = area.x as usize + fx;
                    let y = area.y as usize + fy;
                    if x < buffer.width && y < buffer.height {
                        let mapped = map_aisling_cell(cell, self.accent);
                        buffer.set(x, y, mapped);
                    }
                }
            }
        }
    }

    pub fn render_frame_to_buffer(&self, frame_idx: usize, buffer: &mut Buffer, area: Rect) {
        if self.frames.is_empty() {
            return;
        }
        let idx = frame_idx % self.frames.len();
        let frame = &self.frames[idx];
        let fw = frame.width();
        let fh = frame.height();
        for fy in 0..fh.min(area.height as usize) {
            for fx in 0..fw.min(area.width as usize) {
                if let Some(cell) = frame.cell(fx, fy) {
                    let x = area.x as usize + fx;
                    let y = area.y as usize + fy;
                    if x < buffer.width && y < buffer.height {
                        let mapped = map_aisling_cell(cell, self.accent);
                        buffer.set(x, y, mapped);
                    }
                }
            }
        }
    }

    pub fn get_ansi_string(&self) -> String {
        if self.frames.is_empty() {
            return String::new();
        }
        self.frames[self.current_frame].to_ansi_string()
    }

    pub fn kind(&self) -> EffectKind {
        self.kind
    }

    pub fn name(&self) -> &'static str {
        self.kind.name()
    }

    pub fn all_kinds() -> &'static [EffectKind] {
        EffectKind::all()
    }
}

fn map_aisling_cell(cell: &aisling::frame::Cell, accent: Color) -> crate::core::buffer::Cell {
    let fg = if let Some(c) = cell.colors.fg {
        Color::rgb(c.r, c.g, c.b)
    } else {
        accent
    };
    let bg = cell.colors.bg.map(|c| Color::rgb(c.r, c.g, c.b));
    crate::core::buffer::Cell {
        ch: cell.ch,
        fg,
        bg,
        bold: cell.bold,
        italic: false,
        underlined: false,
    }
}

pub fn effect_kind_from_name(name: &str) -> Option<EffectKind> {
    name.parse().ok()
}