pub mod presets;
#[cfg(feature = "ratatui")]
mod ratatui_adapter;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Rgb(pub u8, pub u8, pub u8);
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum CellKind {
Art,
Trail { age: u8 },
Cursor,
}
#[derive(Copy, Clone, Debug)]
pub struct SplashCell {
pub x: u16,
pub y: u16,
pub ch: char,
pub kind: CellKind,
}
#[derive(Copy, Clone, Debug)]
pub struct Layout {
pub origin_x: u16,
pub origin_y: u16,
pub rows: u16,
pub cols: u16,
}
impl Layout {
pub fn centered(viewport_w: u16, viewport_h: u16, art_rows: u16, art_cols: u16) -> Self {
let origin_y = viewport_h.saturating_sub(art_rows + 4) / 2;
let origin_x = viewport_w.saturating_sub(art_cols) / 2;
Self {
origin_x,
origin_y,
rows: art_rows,
cols: art_cols,
}
}
}
pub struct Splash<'a> {
art: &'a str,
path: &'a [(u8, u8, char)],
trail_len: u8,
tick: u64,
}
impl<'a> Splash<'a> {
pub fn new(art: &'a str, path: &'a [(u8, u8, char)]) -> Self {
Self {
art,
path,
trail_len: 6,
tick: 0,
}
}
pub fn with_trail_len(mut self, n: u8) -> Self {
self.trail_len = n;
self
}
pub fn advance(&mut self) {
self.tick = self.tick.wrapping_add(1);
}
pub fn tick(&self) -> u64 {
self.tick
}
pub fn trail_len(&self) -> u8 {
self.trail_len
}
pub fn cells(&self, layout: Layout) -> impl Iterator<Item = SplashCell> + '_ {
let art_cells = self.art_cells(layout);
let trail_cells = self.trail_cells(layout);
art_cells.chain(trail_cells)
}
fn art_cells(&self, layout: Layout) -> impl Iterator<Item = SplashCell> + '_ {
self.art
.lines()
.take(layout.rows as usize)
.enumerate()
.flat_map(move |(row_idx, line)| {
line.chars()
.enumerate()
.map(move |(col_idx, ch)| SplashCell {
x: layout.origin_x + col_idx as u16,
y: layout.origin_y + row_idx as u16,
ch,
kind: CellKind::Art,
})
})
}
fn trail_cells(&self, layout: Layout) -> impl Iterator<Item = SplashCell> + '_ {
let path_len = self.path.len();
let trail_len = self.trail_len as usize;
let cursor_idx = self.tick as usize % path_len;
(0..=trail_len).rev().map(move |age| {
let idx = if cursor_idx + path_len >= age {
(cursor_idx + path_len - age) % path_len
} else {
0
};
let (row, col, ch) = self.path[idx];
let kind = if age == 0 {
CellKind::Cursor
} else {
CellKind::Trail {
age: (age - 1) as u8,
}
};
SplashCell {
x: layout.origin_x + col as u16,
y: layout.origin_y + row as u16,
ch,
kind,
}
})
}
}
pub fn default_trail_color(age: u8) -> Rgb {
match age {
0 => Rgb(0xe5, 0xe9, 0xf0), 1 => Rgb(0xa0, 0xa8, 0xb8), 2 => Rgb(0x60, 0x68, 0x78), 3 => Rgb(0x38, 0x40, 0x50), 4 => Rgb(0x20, 0x26, 0x32), _ => Rgb(0x10, 0x14, 0x1c), }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn splash_advances_tick() {
let path: &[(u8, u8, char)] = &[(0, 0, 'a'), (0, 1, 'b'), (0, 2, 'c')];
let mut splash = Splash::new("abc", path);
assert_eq!(splash.tick(), 0);
splash.advance();
assert_eq!(splash.tick(), 1);
splash.advance();
assert_eq!(splash.tick(), 2);
}
#[test]
fn splash_emits_art_then_trail_then_cursor() {
let art = "abc";
let path: &[(u8, u8, char)] = &[(0, 0, 'a'), (0, 1, 'b'), (0, 2, 'c')];
let mut splash = Splash::new(art, path).with_trail_len(1);
splash.advance();
splash.advance();
let layout = Layout {
origin_x: 0,
origin_y: 0,
rows: 1,
cols: 3,
};
let cells: Vec<_> = splash.cells(layout).collect();
let art_cells: Vec<_> = cells.iter().filter(|c| c.kind == CellKind::Art).collect();
assert_eq!(art_cells.len(), 3);
let trail_cells: Vec<_> = cells
.iter()
.filter(|c| matches!(c.kind, CellKind::Trail { .. }))
.collect();
assert_eq!(trail_cells.len(), 1);
assert_eq!(trail_cells[0].x, 1);
assert_eq!(trail_cells[0].kind, CellKind::Trail { age: 0 });
let cursor: Vec<_> = cells
.iter()
.filter(|c| c.kind == CellKind::Cursor)
.collect();
assert_eq!(cursor.len(), 1);
assert_eq!(cursor[0].x, 2);
assert_eq!(cursor[0].ch, 'c');
}
#[test]
fn default_trail_color_clamps_at_high_age() {
let age0 = default_trail_color(0);
let age5 = default_trail_color(5);
let age10 = default_trail_color(10);
assert!(age0.0 > age5.0, "age0 red should be brighter than age5");
assert_eq!(age5, age10);
}
#[test]
fn layout_centers_art() {
let layout = Layout::centered(40, 20, 5, 32);
assert_eq!(layout.origin_y, 5);
assert_eq!(layout.origin_x, 4);
}
}