#[cfg(feature = "dsl")]
use compact_str::{format_compact, CompactString, ToCompactString};
use ratatui_core::layout::{Position, Rect};
#[cfg(feature = "dsl")]
use crate::dsl::{dsl_format::fmt_f32, DslFormat};
use crate::pattern::{InstancedPattern, Pattern, PreparedPattern, TransitionProgress};
#[derive(Clone, Debug, Copy, PartialEq)]
pub struct CheckerboardPattern {
cell_size: u16,
transition_width: f32,
}
impl CheckerboardPattern {
pub fn new(cell_size: u16, transition_width: f32) -> Self {
Self {
cell_size,
transition_width: transition_width.max(0.1),
}
}
pub fn with_cell_size(cell_size: u16) -> Self {
Self { cell_size: cell_size.max(1), transition_width: 2.0 }
}
pub fn with_transition_width(mut self, width: f32) -> Self {
self.transition_width = width.max(0.1);
self
}
#[allow(clippy::manual_is_multiple_of)] fn is_white_cell(self, x: u16, y: u16) -> bool {
let cell_x = x / self.cell_size;
let cell_y = y / self.cell_size;
(cell_x + cell_y) % 2 == 0
}
}
impl Default for CheckerboardPattern {
fn default() -> Self {
Self::new(2, 2.0) }
}
impl Pattern for CheckerboardPattern {
type Context = (f32, Rect);
fn for_frame(self, alpha: f32, area: Rect) -> PreparedPattern<Self::Context, Self>
where
Self: Sized,
{
PreparedPattern { pattern: self, context: (alpha, area) }
}
}
impl InstancedPattern for PreparedPattern<(f32, Rect), CheckerboardPattern> {
fn map_alpha(&mut self, pos: Position) -> f32 {
let pattern = &self.pattern;
let (global_alpha, area) = self.context;
let rel_x = pos.x - area.x;
let rel_y = pos.y - area.y;
let is_white = pattern.is_white_cell(rel_x, rel_y);
let cell_threshold = if is_white { 0.0 } else { 0.5 };
TransitionProgress::from(pattern.transition_width)
.map_threshold(global_alpha, cell_threshold)
}
}
#[cfg(feature = "dsl")]
impl DslFormat for CheckerboardPattern {
fn dsl_format(&self) -> CompactString {
if self.cell_size == 2 && (self.transition_width - 2.0).abs() < f32::EPSILON {
"CheckerboardPattern::default()".to_compact_string()
} else if (self.transition_width - 2.0).abs() < f32::EPSILON {
format_compact!("CheckerboardPattern::with_cell_size({})", self.cell_size)
} else {
format_compact!(
"CheckerboardPattern::new({}, {})",
self.cell_size,
fmt_f32(self.transition_width)
)
}
}
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use ratatui_core::layout::{Position, Rect};
use super::*;
#[test]
fn test_checkerboard_with_different_cell_sizes() {
let pattern_1 = CheckerboardPattern::with_cell_size(1);
assert!(
pattern_1.is_white_cell(0, 0),
"Cell size 1: (0,0) should be white"
);
assert!(
!pattern_1.is_white_cell(1, 0),
"Cell size 1: (1,0) should be black"
);
let pattern_3 = CheckerboardPattern::with_cell_size(3);
assert!(
pattern_3.is_white_cell(0, 0),
"Cell size 3: (0,0) should be white"
);
assert!(
pattern_3.is_white_cell(1, 0),
"Cell size 3: (1,0) should be white"
);
assert!(
pattern_3.is_white_cell(2, 0),
"Cell size 3: (2,0) should be white"
);
assert!(
!pattern_3.is_white_cell(3, 0),
"Cell size 3: (3,0) should be black"
);
}
#[test]
fn test_checkerboard_animation_progression() {
let area = Rect::new(0, 0, 6, 2);
let pattern = CheckerboardPattern::with_cell_size(1).with_transition_width(2.0);
let white_pos = Position::new(0, 0);
let black_pos = Position::new(1, 0);
let mut white_alphas = Vec::new();
let mut black_alphas = Vec::new();
for i in 0..=10 {
let global_alpha = i as f32 / 10.0; let mut prepared = pattern.for_frame(global_alpha, area);
white_alphas.push(prepared.map_alpha(white_pos));
black_alphas.push(prepared.map_alpha(black_pos));
}
let early_white = white_alphas[3]; let early_black = black_alphas[3];
assert!(
early_white >= early_black,
"White cells should be more active early in animation: white={early_white:.3}, black={early_black:.3}"
);
let final_white = white_alphas[10];
let final_black = black_alphas[10];
assert!(
final_white > 0.8,
"White cell should be fully active at animation end, got alpha={final_white:.3}"
);
assert!(
final_black > 0.5,
"Black cell should be reasonably active at animation end, got alpha={final_black:.3}"
);
let mid_white = white_alphas[5]; let mid_black = black_alphas[5]; assert!(
mid_white > mid_black,
"At 50% progress, white cells should be more active than black cells: white={mid_white:.3}, black={mid_black:.3}"
);
}
#[test]
fn test_checkerboard_transition_width_scaling() {
let area = Rect::new(0, 0, 6, 2);
let transition_widths = [0.5, 2.0, 4.0];
for &width in &transition_widths {
let pattern = CheckerboardPattern::with_cell_size(1).with_transition_width(width);
let mut prepared = pattern.for_frame(0.4, area);
let white_pos = Position::new(0, 0);
let black_pos = Position::new(1, 0);
let white_alpha = prepared.map_alpha(white_pos);
let black_alpha = prepared.map_alpha(black_pos);
assert!(
(0.0..=1.0).contains(&white_alpha),
"White cell alpha should be in valid range for width {width}, got alpha={white_alpha:.3}"
);
assert!(
(0.0..=1.0).contains(&black_alpha),
"Black cell alpha should be in valid range for width {width}, got alpha={black_alpha:.3}"
);
}
}
#[test]
fn test_checkerboard_cell_pattern_consistency() {
let area = Rect::new(0, 0, 8, 4);
let pattern = CheckerboardPattern::with_cell_size(2).with_transition_width(1.0);
let mut prepared = pattern.for_frame(0.6, area);
let same_cell_positions = [
Position::new(0, 0),
Position::new(0, 1),
Position::new(1, 0),
Position::new(1, 1),
];
let same_cell_alphas: Vec<f32> = same_cell_positions
.iter()
.map(|&pos| prepared.map_alpha(pos))
.collect();
for (i, &alpha) in same_cell_alphas.iter().enumerate() {
for (j, &other_alpha) in same_cell_alphas.iter().enumerate() {
if i != j {
assert!(
(alpha - other_alpha).abs() < 0.001,
"Cells in same checkerboard cell should have same alpha: pos{i} alpha={alpha:.3}, pos{j} alpha={other_alpha:.3}"
);
}
}
}
let different_cell_pos = Position::new(2, 0); let different_alpha = prepared.map_alpha(different_cell_pos);
let same_cell_avg = same_cell_alphas.iter().sum::<f32>() / same_cell_alphas.len() as f32;
assert!(
(0.0..=1.0).contains(&same_cell_avg),
"Same cell average alpha should be valid: {same_cell_avg:.3}"
);
assert!(
(0.0..=1.0).contains(&different_alpha),
"Different cell alpha should be valid: {different_alpha:.3}"
);
}
#[test]
fn test_checkerboard_builder_methods() {
let pattern1 = CheckerboardPattern::new(3, 1.5);
assert_eq!(pattern1.cell_size, 3);
assert_eq!(pattern1.transition_width, 1.5);
let pattern2 = CheckerboardPattern::with_cell_size(4);
assert_eq!(pattern2.cell_size, 4);
assert_eq!(pattern2.transition_width, 2.0);
let pattern3 = CheckerboardPattern::with_cell_size(2).with_transition_width(3.0);
assert_eq!(pattern3.cell_size, 2);
assert_eq!(pattern3.transition_width, 3.0);
let pattern4 = CheckerboardPattern::new(0, 0.05); assert_eq!(pattern4.cell_size, 0); assert_eq!(pattern4.transition_width, 0.1);
let pattern5 = CheckerboardPattern::with_cell_size(0); assert_eq!(pattern5.cell_size, 1); }
#[test]
fn test_checkerboard_default() {
let pattern = CheckerboardPattern::default();
assert_eq!(pattern.cell_size, 2);
assert_eq!(pattern.transition_width, 2.0);
}
}