use std::cell::Cell;
use std::marker::PhantomData;
use std::time::{Duration, Instant};
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use ratatui::layout::Rect;
use crate::config::MatrixConfig;
use crate::stream::Stream;
const MAX_CATCHUP_TICKS: u32 = 4;
pub struct MatrixRainState {
streams: Vec<Stream>,
last_tick: Option<Instant>,
accum: Duration,
frame: u64,
rng: SmallRng,
last_area: Option<Rect>,
color_count: Option<u16>,
last_config: Option<MatrixConfig>,
_not_sync: PhantomData<Cell<()>>,
}
impl MatrixRainState {
pub fn new() -> Self {
Self::from_rng(SmallRng::from_entropy())
}
pub fn with_seed(seed: u64) -> Self {
Self::from_rng(SmallRng::seed_from_u64(seed))
}
fn from_rng(rng: SmallRng) -> Self {
Self {
streams: Vec::new(),
last_tick: None,
accum: Duration::ZERO,
frame: 0,
rng,
last_area: None,
color_count: None,
last_config: None,
_not_sync: PhantomData,
}
}
pub fn tick(&mut self) {
let area = match self.last_area {
Some(a) if a.width > 0 && a.height > 0 => a,
_ => return,
};
let Some(config) = self.last_config.take() else {
return;
};
self.apply_one_tick(area, &config);
self.last_config = Some(config);
}
pub fn reset(&mut self) {
self.streams.clear();
self.last_tick = None;
self.accum = Duration::ZERO;
self.last_area = None;
self.last_config = None;
self.frame = 0;
}
pub fn streams_len(&self) -> usize {
self.streams.len()
}
pub(crate) fn streams(&self) -> &[Stream] {
&self.streams
}
pub(crate) fn color_count(&self) -> Option<u16> {
self.color_count
}
pub fn set_color_count(&mut self, count: u16) {
self.color_count = Some(count);
}
pub(crate) fn advance(&mut self, area: Rect, config: &MatrixConfig) {
if area.width == 0 || area.height == 0 {
self.streams.clear();
self.last_tick = None;
self.accum = Duration::ZERO;
self.last_area = None;
return;
}
self.handle_resize(area, config);
let now = Instant::now();
let ticks = self.compute_tick_budget(now, config);
for _ in 0..ticks {
self.apply_one_tick(area, config);
}
self.last_tick = Some(now);
self.last_area = Some(area);
self.last_config = Some(config.clone());
}
fn handle_resize(&mut self, area: Rect, config: &MatrixConfig) {
let prev = self.last_area;
let new_w = area.width as usize;
let width_changed = prev.map_or(true, |p| p.width != area.width);
let height_changed = prev.map_or(false, |p| p.height != area.height);
if width_changed {
if self.streams.len() < new_w {
for _ in self.streams.len()..new_w {
self.streams
.push(Stream::new_idle(config.max_trail, &mut self.rng));
}
} else if self.streams.len() > new_w {
self.streams.truncate(new_w);
}
}
if height_changed {
let max_head = (area.height as f32) + (config.max_trail as f32);
for stream in &mut self.streams {
if stream.is_active() {
let clamped = stream.head_row().clamp(0.0, max_head);
stream.set_head_row(clamped);
if (clamped - stream.length() as f32) >= area.height as f32 {
stream.force_retire(&mut self.rng);
}
}
}
}
}
fn compute_tick_budget(&mut self, now: Instant, config: &MatrixConfig) -> u32 {
let ticks_per_sec = (config.fps as f32) * config.speed;
if !ticks_per_sec.is_finite() || ticks_per_sec <= 0.0 {
self.accum = Duration::ZERO;
return 0;
}
match self.last_tick {
None => {
self.accum = Duration::ZERO;
1
}
Some(prev) => {
let elapsed = now.saturating_duration_since(prev);
let total_secs = elapsed.as_secs_f32() + self.accum.as_secs_f32();
let total_ticks = total_secs * ticks_per_sec;
if !total_ticks.is_finite() {
self.accum = Duration::ZERO;
return 0;
}
let ticks = (total_ticks.floor() as u32).min(MAX_CATCHUP_TICKS);
let leftover_ticks = (total_ticks - ticks as f32).max(0.0);
let leftover_secs = leftover_ticks / ticks_per_sec;
self.accum = Duration::from_secs_f32(leftover_secs.max(0.0));
ticks
}
}
}
fn apply_one_tick(&mut self, area: Rect, config: &MatrixConfig) {
let chars = config.charset.chars();
for stream in &mut self.streams {
stream.tick(area.height, config.fps, &mut self.rng);
}
for stream in &mut self.streams {
if stream.is_ready_to_spawn() && self.rng.gen::<f32>() < config.density {
stream.spawn(
&mut self.rng,
chars,
config.min_trail,
config.max_trail,
config.fps,
);
}
}
self.frame = self.frame.wrapping_add(1);
}
}
impl Default for MatrixRainState {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn area(w: u16, h: u16) -> Rect {
Rect::new(0, 0, w, h)
}
#[test]
fn new_starts_with_no_streams_no_timing() {
let s = MatrixRainState::new();
assert!(s.streams.is_empty());
assert!(s.last_tick.is_none());
assert!(s.last_area.is_none());
assert_eq!(s.frame, 0);
}
#[test]
fn first_render_budget_is_one_tick() {
let mut s = MatrixRainState::with_seed(0);
let cfg = MatrixConfig::default();
let ticks = s.compute_tick_budget(Instant::now(), &cfg);
assert_eq!(ticks, 1);
assert_eq!(s.accum, Duration::ZERO);
}
#[test]
fn first_render_allocates_streams_per_column() {
let mut s = MatrixRainState::with_seed(0);
let cfg = MatrixConfig::default();
s.advance(area(12, 10), &cfg);
assert_eq!(s.streams().len(), 12);
assert_eq!(s.frame, 1);
assert!(s.last_tick.is_some());
}
#[test]
fn width_resize_grows_and_shrinks_streams() {
let mut s = MatrixRainState::with_seed(0);
let cfg = MatrixConfig::default();
s.advance(area(5, 10), &cfg);
assert_eq!(s.streams().len(), 5);
s.advance(area(10, 10), &cfg);
assert_eq!(s.streams().len(), 10);
s.advance(area(3, 10), &cfg);
assert_eq!(s.streams().len(), 3);
}
#[test]
fn empty_area_clears_streams_and_resets_first_render_path() {
let mut s = MatrixRainState::with_seed(0);
let cfg = MatrixConfig::default();
s.advance(area(10, 10), &cfg);
let frame_after_first = s.frame;
s.advance(area(0, 10), &cfg);
assert_eq!(s.streams().len(), 0);
assert!(s.last_tick.is_none());
assert!(s.last_area.is_none());
s.advance(area(10, 10), &cfg);
assert_eq!(s.frame, frame_after_first + 1);
}
#[test]
fn empty_area_height_zero_also_handled() {
let mut s = MatrixRainState::with_seed(0);
let cfg = MatrixConfig::default();
s.advance(area(10, 0), &cfg);
assert_eq!(s.streams().len(), 0);
assert!(s.last_tick.is_none());
}
#[test]
fn tick_before_first_render_is_noop() {
let mut s = MatrixRainState::with_seed(0);
s.tick();
assert_eq!(s.frame, 0);
assert!(s.last_tick.is_none());
}
#[test]
fn tick_after_first_render_advances_one_frame() {
let mut s = MatrixRainState::with_seed(0);
let cfg = MatrixConfig::default();
s.advance(area(10, 20), &cfg);
let frame_before = s.frame;
let last_tick_before = s.last_tick;
s.tick();
assert_eq!(s.frame, frame_before + 1);
assert_eq!(
s.last_tick, last_tick_before,
"tick() must not touch last_tick"
);
}
#[test]
fn reset_clears_streams_and_timing_keeps_color_count() {
let mut s = MatrixRainState::with_seed(42);
let cfg = MatrixConfig::default();
s.advance(area(10, 20), &cfg);
s.set_color_count(256);
s.reset();
assert_eq!(s.streams().len(), 0);
assert!(s.last_tick.is_none());
assert!(s.last_area.is_none());
assert_eq!(s.frame, 0);
assert_eq!(s.color_count(), Some(256));
}
#[test]
fn deterministic_with_same_seed() {
let cfg = MatrixConfig::default();
let mut a = MatrixRainState::with_seed(0xC0FFEE);
let mut b = MatrixRainState::with_seed(0xC0FFEE);
a.advance(area(15, 15), &cfg);
b.advance(area(15, 15), &cfg);
assert_eq!(a.streams().len(), b.streams().len());
for (sa, sb) in a.streams().iter().zip(b.streams()) {
assert_eq!(sa.is_active(), sb.is_active());
assert_eq!(sa.length(), sb.length());
assert_eq!(sa.head_row(), sb.head_row());
}
}
#[test]
fn catchup_cap_limits_huge_elapsed() {
let mut s = MatrixRainState::with_seed(0);
let cfg = MatrixConfig::default();
s.last_tick = Some(Instant::now() - Duration::from_secs(60));
let ticks = s.compute_tick_budget(Instant::now(), &cfg);
assert_eq!(ticks, MAX_CATCHUP_TICKS);
}
#[test]
fn sub_tick_render_carries_remainder() {
let mut s = MatrixRainState::with_seed(0);
let cfg = MatrixConfig::default();
let now = Instant::now();
s.last_tick = Some(now - Duration::from_micros(500));
let ticks = s.compute_tick_budget(now, &cfg);
assert_eq!(ticks, 0);
assert!(s.accum > Duration::ZERO);
}
#[test]
fn pathological_zero_fps_no_panic() {
let mut s = MatrixRainState::with_seed(0);
let cfg = MatrixConfig {
fps: 0,
..MatrixConfig::default()
};
assert_eq!(s.compute_tick_budget(Instant::now(), &cfg), 0);
}
#[test]
fn color_count_default_none_then_set() {
let mut s = MatrixRainState::new();
assert!(s.color_count().is_none());
s.set_color_count(16);
assert_eq!(s.color_count(), Some(16));
}
#[test]
fn state_is_send() {
fn assert_send<T: Send>() {}
assert_send::<MatrixRainState>();
}
}