use crate::theme::UiTheme;
use hjkl_splash::{CellKind, Layout, Rgb, Splash, default_trail_color};
use ratatui::Frame;
use ratatui::layout::{Alignment, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::widgets::Paragraph;
pub const ART: &str = include_str!("art.txt");
fn art_dims() -> (u16, u16) {
let rows = ART.lines().count() as u16;
let cols = ART.lines().map(|l| l.chars().count()).max().unwrap_or(0) as u16;
(rows, cols)
}
const fn art_bounds() -> (usize, usize) {
let bytes = ART.as_bytes();
let mut rows = 0;
let mut cols = 0;
let mut line = 0;
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'\n' {
if i > 0 && bytes[i - 1] == b'\r' {
line -= 1;
}
rows += 1;
if line > cols {
cols = line;
}
line = 0;
} else if bytes[i] & 0xC0 != 0x80 {
line += 1;
}
i += 1;
}
if line > 0 {
rows += 1;
if line > cols {
cols = line;
}
}
(rows, cols)
}
const ART_BOUNDS: (usize, usize) = art_bounds();
const _: () = assert!(
ART_BOUNDS.0 <= u8::MAX as usize,
"src/art.txt is taller than 255 rows: hjkl_splash takes the cursor path as \
&[(u8, u8, char)], so splash::build_path cannot address those rows"
);
const _: () = assert!(
ART_BOUNDS.1 <= u8::MAX as usize,
"src/art.txt has a line longer than 255 columns: hjkl_splash takes the cursor \
path as &[(u8, u8, char)], so splash::build_path cannot address those columns"
);
pub fn build_path() -> Vec<(u8, u8, char)> {
let mut path = Vec::new();
for (r, line) in ART.lines().enumerate() {
for (c, ch) in line.chars().enumerate() {
if !ch.is_whitespace() {
path.push((r as u8, c as u8, ch));
}
}
}
path
}
pub fn render(
frame: &mut Frame,
area: Rect,
anchor: std::time::Instant,
path: &[(u8, u8, char)],
theme: &UiTheme,
) {
let splash = Splash::new(ART, path).with_anchor(anchor);
let (rows, cols) = art_dims();
let layout = Layout::centered(area.width, area.height, rows, cols);
let buf = frame.buffer_mut();
for cell in splash.cells(layout) {
if cell.x >= area.width || cell.y >= area.height {
continue;
}
let style = match cell.kind {
CellKind::Art => Style::new().fg(theme.accent),
CellKind::Trail { age } => {
let Rgb(r, g, b) = default_trail_color(age);
Style::new().fg(crate::theme::paint(theme.mode, (r, g, b)))
}
CellKind::Cursor => Style::new().fg(theme.fg).add_modifier(Modifier::BOLD),
};
if let Some(c) = buf.cell_mut((area.x + cell.x, area.y + cell.y)) {
c.set_char(cell.ch);
c.set_style(style);
}
}
let ver_y = layout.origin_y.saturating_add(rows).saturating_add(1);
if ver_y < area.height {
let line = Rect::new(area.x, area.y + ver_y, area.width, 1);
frame.render_widget(
Paragraph::new(format!("gpur v{}", env!("CARGO_PKG_VERSION")))
.alignment(Alignment::Center)
.style(theme.dim),
line,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn const_art_bounds_agree_with_the_runtime_dimensions() {
let (rows, cols) = art_dims();
assert_eq!(ART_BOUNDS, (rows as usize, cols as usize));
}
#[test]
fn build_path_coordinates_still_name_their_own_cells_after_the_u8_cast() {
let grid: Vec<Vec<char>> = ART.lines().map(|l| l.chars().collect()).collect();
let path = build_path();
for &(r, c, ch) in &path {
assert_eq!(
grid.get(r as usize).and_then(|l| l.get(c as usize)),
Some(&ch)
);
}
let glyphs = grid
.iter()
.flatten()
.filter(|ch| !ch.is_whitespace())
.count();
assert_eq!(path.len(), glyphs);
}
}