use crate::{
markdown::{elements::Line, text_style::Color},
terminal::{
printer::TerminalCommand,
virt::{TerminalGrid, TerminalRowIterator},
},
};
use std::fmt::Debug;
use unicode_width::UnicodeWidthStr;
pub(crate) mod collapse_horizontal;
pub(crate) mod fade;
pub(crate) mod slide_horizontal;
#[derive(Clone, Debug)]
pub(crate) enum TransitionDirection {
Next,
Previous,
}
pub(crate) trait AnimateTransition {
type Frame: AnimationFrame + Debug;
fn build_frame(&self, frame: usize, previous_frame: usize) -> Self::Frame;
fn total_frames(&self) -> usize;
}
pub(crate) trait AnimationFrame {
fn build_commands(&self) -> Vec<TerminalCommand<'_>>;
}
#[derive(Debug)]
pub(crate) struct LinesFrame {
pub(crate) lines: Vec<Line>,
pub(crate) background_color: Option<Color>,
}
impl LinesFrame {
fn skip_whitespace(mut text: &str) -> (&str, usize, usize) {
let mut trimmed_before = 0;
while let Some(' ') = text.chars().next() {
text = &text[1..];
trimmed_before += 1;
}
let mut trimmed_after = 0;
let mut rev = text.chars().rev();
while let Some(' ') = rev.next() {
text = &text[..text.len() - 1];
trimmed_after += 1;
}
(text, trimmed_before, trimmed_after)
}
}
impl From<&TerminalGrid> for LinesFrame {
fn from(grid: &TerminalGrid) -> Self {
let mut lines = Vec::new();
for row in &grid.rows {
let line = TerminalRowIterator::new(row).collect();
lines.push(Line(line));
}
Self { lines, background_color: grid.background_color }
}
}
impl AnimationFrame for LinesFrame {
fn build_commands(&self) -> Vec<TerminalCommand<'_>> {
use TerminalCommand::*;
let mut commands = vec![];
if let Some(color) = self.background_color {
commands.push(SetBackgroundColor(color));
}
commands.push(ClearScreen);
for (row, line) in self.lines.iter().enumerate() {
let mut column = 0;
let mut is_in_column = false;
let mut is_in_row = false;
for chunk in &line.0 {
let (text, white_before, white_after) = match chunk.style.colors.background {
Some(_) => (chunk.content.as_str(), 0, 0),
None => Self::skip_whitespace(&chunk.content),
};
if text.is_empty() {
column += chunk.content.width();
is_in_column = false;
continue;
}
if !is_in_row {
commands.push(MoveToRow(row as u16));
is_in_row = true;
}
if white_before > 0 {
column += white_before;
is_in_column = false;
}
if !is_in_column {
commands.push(MoveToColumn(column as u16));
is_in_column = true;
}
commands.push(PrintText { content: text, style: chunk.style });
column += text.width();
if white_after > 0 {
column += white_after;
is_in_column = false;
}
}
}
commands
}
}
#[cfg(test)]
mod utils {
use crate::terminal::virt::{StyledChar, TerminalGrid};
pub(crate) fn build_grid(rows: &[&str]) -> TerminalGrid {
let rows = rows
.iter()
.map(|r| r.chars().map(|c| StyledChar { character: c, style: Default::default() }).collect())
.collect();
TerminalGrid { rows, background_color: None, images: Default::default() }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::markdown::elements::Text;
#[test]
fn commands() {
let animation = LinesFrame {
lines: vec![
Line(vec![Text::from(" hi "), Text::from("bye"), Text::from("s")]),
Line(vec![Text::from("hello"), Text::from(" wor"), Text::from("s")]),
],
background_color: Some(Color::Red),
};
let commands = animation.build_commands();
use TerminalCommand::*;
let expected = &[
SetBackgroundColor(Color::Red),
ClearScreen,
MoveToRow(0),
MoveToColumn(2),
PrintText { content: "hi", style: Default::default() },
MoveToColumn(6),
PrintText { content: "bye", style: Default::default() },
PrintText { content: "s", style: Default::default() },
MoveToRow(1),
MoveToColumn(0),
PrintText { content: "hello", style: Default::default() },
MoveToColumn(6),
PrintText { content: "wor", style: Default::default() },
PrintText { content: "s", style: Default::default() },
];
assert_eq!(commands, expected);
}
}