use ratatui_core::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
widgets::Widget,
};
use crate::animation::{AnimationMode, cell_glyph, cell_intensity, interpolate_color, is_uniform};
use crate::defaults;
#[must_use]
#[derive(Debug, Clone)]
pub struct SkeletonBlock<'a> {
elapsed_ms: u64,
mode: AnimationMode,
braille: bool,
base: Color,
highlight: Color,
block: Option<ratatui_widgets::block::Block<'a>>,
}
impl<'a> SkeletonBlock<'a> {
pub fn new(elapsed_ms: u64) -> Self {
Self {
elapsed_ms,
mode: AnimationMode::default(),
braille: false,
base: defaults::BASE,
highlight: defaults::HIGHLIGHT,
block: None,
}
}
pub fn mode(mut self, mode: AnimationMode) -> Self {
self.mode = mode;
self
}
pub fn braille(mut self, braille: bool) -> Self {
self.braille = braille;
self
}
pub fn base(mut self, color: impl Into<Color>) -> Self {
self.base = color.into();
self
}
pub fn highlight(mut self, color: impl Into<Color>) -> Self {
self.highlight = color.into();
self
}
pub fn block(mut self, block: ratatui_widgets::block::Block<'a>) -> Self {
self.block = Some(block);
self
}
}
impl Widget for SkeletonBlock<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let inner = if let Some(ref block) = self.block {
let inner_area = block.inner(area);
block.render(area, buf);
inner_area
} else {
area
};
if inner.is_empty() {
return;
}
render_skeleton_cells(
inner,
buf,
self.mode,
self.braille,
self.elapsed_ms,
self.base,
self.highlight,
|_row, col, width| col < width,
);
}
}
#[expect(clippy::too_many_arguments)]
pub(crate) fn render_skeleton_cells(
area: Rect,
buf: &mut Buffer,
mode: AnimationMode,
braille: bool,
elapsed_ms: u64,
base: Color,
highlight: Color,
visible: impl Fn(u16, u16, u16) -> bool,
) {
let uniform_t = is_uniform(mode).then(|| cell_intensity(mode, elapsed_ms, 0, area.width));
for row in 0..area.height {
for col in 0..area.width {
if !visible(row, col, area.width) {
continue;
}
let t = uniform_t.unwrap_or_else(|| cell_intensity(mode, elapsed_ms, col, area.width));
let fg = interpolate_color(base, highlight, mode, t);
let ch = cell_glyph(braille, mode, elapsed_ms, row, col);
let cell = &mut buf[(area.x + col, area.y + row)];
cell.set_char(ch);
cell.set_style(Style::default().fg(fg));
}
}
}
#[cfg(feature = "pantry")]
#[path = "block.ingredient.rs"]
pub mod ingredient;
#[cfg(test)]
mod tests {
use super::*;
fn render_block(elapsed_ms: u64, width: u16, height: u16) -> Buffer {
let area = Rect::new(0, 0, width, height);
let mut buf = Buffer::empty(area);
SkeletonBlock::new(elapsed_ms).render(area, &mut buf);
buf
}
#[test]
fn fills_all_cells() {
let buf = render_block(1000, 10, 3);
for y in 0..3 {
for x in 0..10 {
assert_eq!(buf[(x, y)].symbol(), "█");
}
}
}
#[test]
fn noise_mode_fills_random_braille() {
let area = Rect::new(0, 0, 10, 3);
let mut buf = Buffer::empty(area);
SkeletonBlock::new(1000)
.mode(AnimationMode::Noise)
.render(area, &mut buf);
for y in 0..3u16 {
for x in 0..10u16 {
let ch = buf[(x, y)].symbol().chars().next().unwrap();
assert!((0x2800..=0x28FF).contains(&(ch as u32)));
}
}
}
#[test]
fn braille_flag_fills_solid_braille() {
let area = Rect::new(0, 0, 10, 3);
let mut buf = Buffer::empty(area);
SkeletonBlock::new(1000)
.braille(true)
.render(area, &mut buf);
for y in 0..3u16 {
for x in 0..10u16 {
assert_eq!(buf[(x, y)].symbol(), "⣿");
}
}
}
#[test]
fn empty_area_is_noop() {
let area = Rect::new(0, 0, 0, 0);
let mut buf = Buffer::empty(Rect::new(0, 0, 1, 1));
let expected = buf.clone();
SkeletonBlock::new(0).render(area, &mut buf);
assert_eq!(buf, expected);
}
#[test]
fn custom_colors_applied() {
let area = Rect::new(0, 0, 5, 1);
let mut buf = Buffer::empty(area);
SkeletonBlock::new(0)
.base(Color::Rgb(10, 20, 30))
.highlight(Color::Rgb(200, 200, 200))
.render(area, &mut buf);
for x in 0..5 {
let style = buf[(x, 0u16)].style();
assert_eq!(style.fg, Some(Color::Rgb(10, 20, 30)));
}
}
}