plavno 0.4.0

A lightweight terminal music player for Linux, macOS and Windows with service waves and library shuffle
use crate::{dance_motion::Style, dancer::Dancer, engine::Engine, wave::Wave};
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};

#[derive(Clone, Copy, Default, PartialEq)]
pub enum Mode {
    #[default]
    Wave,
    Dance,
}

#[derive(Clone, Copy, Default, PartialEq)]
pub enum Renderer {
    #[default]
    Braille,
    Sixel,
}

#[derive(Clone, Copy, Default)]
pub struct Options {
    pub mode: Mode,
    pub style: Style,
    pub renderer: Renderer,
    pub sixel_update: crate::sixel::UpdateMode,
    pub skin: crate::dancer::skin::Skin,
}

impl Mode {
    pub fn label(self) -> &'static str {
        match self {
            Self::Wave => "Wave",
            Self::Dance => "Dance",
        }
    }

    pub fn toggle(self) -> Self {
        match self {
            Self::Wave => Self::Dance,
            Self::Dance => Self::Wave,
        }
    }
}

pub struct Visualization {
    pub mode: Mode,
    pub renderer: Renderer,
    wave: Wave,
    dancer: Dancer,
    skin: crate::dancer::skin::Skin,
}

impl Visualization {
    pub fn new(options: Options) -> Self {
        Self {
            mode: options.mode,
            renderer: options.renderer,
            wave: Wave::default(),
            dancer: Dancer::new(options.style),
            skin: options.skin,
        }
    }

    pub fn update(&mut self, engine: &Engine, columns: u16) {
        self.wave.update(engine);
        if self.mode == Mode::Dance {
            self.dancer.update(engine, columns);
        }
    }

    pub fn wants_frames(&self, engine: &Engine) -> bool {
        self.mode == Mode::Dance && self.dancer.wants_frames(engine)
    }

    pub fn render(&self, wave_area: Rect, dance_area: Rect, buffer: &mut Buffer) {
        (&self.wave).render(wave_area, buffer);
        if self.mode == Mode::Dance && self.renderer == Renderer::Braille {
            (&self.dancer).render(dance_area, buffer);
        }
    }

    pub fn sixel(
        &self,
        area: Rect,
        cell: crate::sixel::CellSize,
        buffer: &Buffer,
    ) -> Option<crate::sixel::Image> {
        let mut image = self.dancer.sixel(area, cell, self.skin)?;
        image.protect_text(buffer, cell);
        Some(image)
    }
}

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

    #[test]
    fn dance_overlays_the_wave_and_preserves_every_cell_outside_its_stage() {
        let wave_area = Rect::new(0, 0, 80, 36);
        let dance_area = Rect::new(0, 21, 80, 15);
        let wave = Visualization::new(Options {
            mode: Mode::Wave,
            style: Style::Flow,
            ..Options::default()
        });
        let dance = Visualization::new(Options {
            mode: Mode::Dance,
            style: Style::Flow,
            ..Options::default()
        });
        let mut background = Buffer::empty(wave_area);
        let mut combined = Buffer::empty(wave_area);
        wave.render(wave_area, dance_area, &mut background);
        dance.render(wave_area, dance_area, &mut combined);
        let mut changed = 0;
        for y in 0..wave_area.height {
            for x in 0..wave_area.width {
                if !dance_area.contains((x, y).into()) {
                    assert_eq!(background[(x, y)], combined[(x, y)]);
                }
                changed += usize::from(background[(x, y)] != combined[(x, y)]);
            }
        }
        assert!(changed > 10);
        assert!(combined.content.iter().any(|cell| cell.symbol() != " "));
    }
}