use alloc::{boxed::Box, vec::Vec};
use core::{fmt::Debug, ops::Range};
use bon::Builder;
#[cfg(feature = "dsl")]
use compact_str::ToCompactString;
use ratatui_core::{
buffer::Buffer,
layout::{Position, Rect},
};
use crate::{
shader::Shader,
simple_rng::{RangeSampler, SimpleRng},
CellFilter, Duration, EffectTimer,
};
#[derive(Clone, Debug)]
pub enum GlitchType {
ChangeCase,
ChangeCharByValue(i8),
}
#[derive(Builder, Clone, Debug)]
pub struct GlitchCell {
cell_idx: usize,
glitch_remaining_ms: u32,
presleep_remaining_ms: u32,
glitch: GlitchType,
}
#[derive(Builder, Clone, Debug)]
pub struct Glitch {
cell_glitch_ratio: f32,
action_start_delay_ms: Range<u32>,
action_ms: Range<u32>,
#[builder(default)]
rng: SimpleRng,
#[builder(default)]
selection: CellFilter,
#[builder(skip)]
glitch_cells: Vec<GlitchCell>,
area: Option<Rect>,
}
impl Glitch {
fn ensure_population(&mut self, screen: Rect) {
let total_cells =
crate::math::round(screen.width as f32 * screen.height as f32 * self.cell_glitch_ratio)
as u32;
let current_population = self.glitch_cells.len() as u32;
if current_population < total_cells {
for _ in 0..(total_cells - current_population) {
let cell = GlitchCell::builder()
.cell_idx(
self.rng
.gen_range(0..(screen.width * screen.height) as usize),
)
.glitch(self.glitch_type())
.glitch_remaining_ms(self.rng.gen_range(self.action_ms.clone()))
.presleep_remaining_ms(
self.rng
.gen_range(self.action_start_delay_ms.clone()),
)
.build();
self.glitch_cells.push(cell);
}
}
}
fn update_cell(cell: &mut GlitchCell, last_frame_ms: u32) {
let f = |v: u32, sub: u32| (v.saturating_sub(sub), sub.saturating_sub(v));
let (updated, remaining) = f(cell.presleep_remaining_ms, last_frame_ms);
cell.presleep_remaining_ms = updated;
cell.glitch_remaining_ms = cell.glitch_remaining_ms.saturating_sub(remaining);
}
fn is_running(cell: &GlitchCell) -> bool {
cell.glitch_remaining_ms > 0
}
fn glitch_type(&mut self) -> GlitchType {
let idx: u32 = self.rng.gen();
match idx % 2 {
0 => GlitchType::ChangeCase,
1 => GlitchType::ChangeCharByValue(-10 + self.rng.gen_range(0..20) as i8),
_ => unreachable!(),
}
}
}
impl Shader for Glitch {
fn name(&self) -> &'static str {
"glitch"
}
fn process(&mut self, duration: Duration, buf: &mut Buffer, area: Rect) -> Option<Duration> {
self.ensure_population(area);
let last_frame_ms = duration.as_millis();
self.glitch_cells
.iter_mut()
.for_each(|cell| Self::update_cell(cell, last_frame_ms as _));
self.glitch_cells
.retain(|cell| cell.cell_idx < buf.content.len());
let predicate = self.selection.predicate(area);
self.glitch_cells
.iter()
.filter(|c| c.presleep_remaining_ms == 0)
.for_each(|cell| {
let x = cell.cell_idx % area.width as usize;
let y = cell.cell_idx / area.width as usize;
let pos = Position::new(area.x + x as u16, area.y + y as u16);
let c = buf
.cell_mut(Position::new(area.x + x as u16, area.y + y as u16))
.unwrap();
if !predicate.is_valid(pos, c) {
return;
}
match cell.glitch {
GlitchType::ChangeCase if c.symbol().is_ascii() => {
let ch = c.symbol().chars().next().unwrap();
c.set_char(if ch.is_ascii_uppercase() {
ch.to_ascii_lowercase()
} else {
ch.to_ascii_uppercase()
});
},
GlitchType::ChangeCharByValue(v) if c.symbol().len() == 1 => {
if c.symbol()
.chars()
.next()
.is_some_and(|ch| ch == ' ')
{
return;
}
c.set_char(if v > 0 {
c.symbol().as_bytes()[0]
.saturating_add(v as u8)
.clamp(32, 255) as char
} else {
c.symbol().as_bytes()[0]
.saturating_sub(v.unsigned_abs())
.clamp(32, 255) as char
});
},
_ => {},
}
});
self.glitch_cells.retain(Self::is_running);
None
}
fn done(&self) -> bool {
false
}
fn clone_box(&self) -> Box<dyn Shader> {
Box::new(self.clone())
}
fn area(&self) -> Option<Rect> {
self.area
}
fn set_area(&mut self, area: Rect) {
self.area = Some(area);
}
fn filter(&mut self, strategy: CellFilter) {
self.selection = strategy;
}
fn timer_mut(&mut self) -> Option<&mut EffectTimer> {
None
}
fn cell_filter(&self) -> Option<&CellFilter> {
Some(&self.selection)
}
fn reset(&mut self) {
self.glitch_cells.clear();
}
fn set_rng(&mut self, rng: SimpleRng) {
self.rng = rng;
}
#[cfg(feature = "dsl")]
fn to_dsl(&self) -> Result<crate::dsl::EffectExpression, crate::dsl::DslError> {
use crate::dsl::DslError;
Err(DslError::UnsupportedEffect { name: self.name().to_compact_string() })
}
}