scrin-widgets 0.2.7

Scrin-native widgets and Aisling terminal effects for immersive TUIs.
Documentation
use std::{io, time::Duration};

use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use scrin::{
    PresentStrategy, Terminal, TerminalOptions,
    effects::{EffectKind, LoaderKind},
    layout::{Constraint, Direction, Layout},
    widgets::{Clear, Widget},
};
use scrin_widgets::{
    AislingExt, AislingPalette, Gauge, Paragraph, ScrinEffect, ScrinLoader, SignalPanel, StatusBar,
};

fn main() -> io::Result<()> {
    let mut terminal = Terminal::init_with(TerminalOptions {
        mouse_capture: true,
        bracketed_paste: true,
        ..TerminalOptions::default()
    })?;
    let result = run(&mut terminal);
    terminal.restore()?;
    result
}

fn run(terminal: &mut Terminal) -> io::Result<()> {
    let mut tick = 0_u64;
    let mut effect_index = 0_usize;
    let mut loader_index = 0_usize;

    loop {
        let effects = EffectKind::all();
        let loaders = LoaderKind::all();
        let effect = effects[effect_index % effects.len()];
        let loader = loaders[loader_index % loaders.len()];
        let progress = wave_ratio(tick, 160, 0) as f32;

        terminal.draw_with_present_strategy(PresentStrategy::MarkedDirty, |frame| {
            let palette = match effect_index % 3 {
                0 => AislingPalette::cypherpunk(),
                1 => AislingPalette::dream(),
                _ => AislingPalette::flare(),
            };
            frame.render_widget_timed("clear", Clear::with_bg(palette.shadow), frame.area());
            frame.mark_dirty(frame.area());

            let root = Layout::default()
                .direction(Direction::Vertical)
                .constraints([
                    Constraint::Length(5),
                    Constraint::Min(10),
                    Constraint::Length(7),
                    Constraint::Length(1),
                ])
                .split(frame.area());

            let header = palette.block("Scrin effects and loaders as widgets");
            let header_inner = header.inner(root[0]);
            frame.render_widget_timed("header:block", header, root[0]);
            frame.render_widget_timed(
                "header:text",
                Paragraph::new(format!(
                    "EffectPlayer -> ScrinEffect | LoaderPlayer -> ScrinLoader\neffect: {} ({}/{}) | loader: {} ({}/{})",
                    effect.name(),
                    effect_index % effects.len() + 1,
                    effects.len(),
                    loader.name(),
                    loader_index % loaders.len() + 1,
                    loaders.len()
                ))
                .palette(palette)
                .aisling()
                .tick(tick)
                .intensity(5),
                header_inner,
            );
            frame.mark_dirty(root[0]);

            ScrinEffect::new(effect, "scrin-widgets")
                .tick(tick)
                .duration(48)
                .seed(13 + effect_index as u64)
                .palette(palette)
                .block(palette.block(effect.name()))
                .render_with_interaction(frame, "effects:selected", root[1]);

            let lower = Layout::default()
                .direction(Direction::Horizontal)
                .constraints([
                    Constraint::Percentage(42),
                    Constraint::Percentage(28),
                    Constraint::Percentage(30),
                ])
                .split(root[2]);

            ScrinLoader::new(loader, progress)
                .tick(tick)
                .label(loader.name())
                .unit("units")
                .fraction(true)
                .palette(palette)
                .block(palette.block("loader"))
                .render_with_interaction(frame, "loaders:selected", lower[0]);

            Gauge::new(f64::from(progress))
                .label(format!("progress {:>3}%", (progress * 100.0) as u16))
                .palette(palette)
                .block(palette.block("flow"))
                .render(frame.buffer(), lower[1]);
            frame.mark_dirty(lower[1]);

            SignalPanel::new("controls")
                .line("Tab: next effect")
                .line("Shift-Tab: prev effect")
                .line("[: prev loader")
                .line("]: next loader")
                .tick(tick)
                .palette(palette)
                .render(frame.buffer(), lower[2]);
            frame.mark_dirty(lower[2]);

            frame.render_widget_timed(
                "status",
                StatusBar::new()
                    .left(format!("frame {}", frame.frame_count()))
                    .center(format!("dirty regions {}", frame.dirty_regions().len()))
                    .right("q quits")
                    .palette(palette),
                root[3],
            );
            frame.mark_dirty(root[3]);
        })?;

        if event::poll(Duration::from_millis(36))? {
            if let Event::Key(key) = event::read()? {
                if key.kind == KeyEventKind::Press {
                    match key.code {
                        KeyCode::Char('q') | KeyCode::Esc => break,
                        KeyCode::Tab => effect_index = (effect_index + 1) % EffectKind::all().len(),
                        KeyCode::BackTab => {
                            let total = EffectKind::all().len();
                            effect_index = (effect_index + total - 1) % total;
                        }
                        KeyCode::Char(']') => {
                            loader_index = (loader_index + 1) % LoaderKind::all().len()
                        }
                        KeyCode::Char('[') => {
                            let total = LoaderKind::all().len();
                            loader_index = (loader_index + total - 1) % total;
                        }
                        _ => {}
                    }
                }
            }
        }

        tick = tick.wrapping_add(1);
    }

    Ok(())
}

fn wave_ratio(tick: u64, period: u64, offset: u64) -> f64 {
    let phase = ((tick + offset) % period) as f64 / period as f64;
    1.0 - (phase - 0.5).abs() * 2.0
}