1use alloc::vec::Vec;
5use core::cell::Cell;
6use core::marker::PhantomData;
7
8#[cfg(feature = "std")]
9use core::time::Duration;
10#[cfg(feature = "std")]
11use std::time::Instant;
12
13use rand::rngs::SmallRng;
14use rand::{Rng, SeedableRng};
15use ratatui::layout::Rect;
16
17use crate::config::MatrixConfig;
18use crate::stream::Stream;
19
20#[cfg(feature = "std")]
21const MAX_CATCHUP_TICKS: u32 = 4;
22
23pub struct MatrixRainState {
47 streams: Vec<Stream>,
48 #[cfg(feature = "std")]
49 last_tick: Option<Instant>,
50 #[cfg(feature = "std")]
51 accum: Duration,
52 frame: u64,
53 rng: SmallRng,
54 last_area: Option<Rect>,
55 color_count: Option<u16>,
56 last_config: Option<MatrixConfig>,
57 paused: bool,
58 _not_sync: PhantomData<Cell<()>>,
59}
60
61impl MatrixRainState {
62 #[cfg(feature = "std")]
70 pub fn new() -> Self {
71 Self::from_rng(SmallRng::from_entropy())
72 }
73
74 pub fn with_seed(seed: u64) -> Self {
88 Self::from_rng(SmallRng::seed_from_u64(seed))
89 }
90
91 fn from_rng(rng: SmallRng) -> Self {
92 Self {
93 streams: Vec::new(),
94 #[cfg(feature = "std")]
95 last_tick: None,
96 #[cfg(feature = "std")]
97 accum: Duration::ZERO,
98 frame: 0,
99 rng,
100 last_area: None,
101 color_count: None,
102 last_config: None,
103 paused: false,
104 _not_sync: PhantomData,
105 }
106 }
107
108 pub fn tick(&mut self) {
117 let area = match self.last_area {
118 Some(a) if a.width > 0 && a.height > 0 => a,
119 _ => return,
120 };
121 let Some(config) = self.last_config.take() else {
122 return;
123 };
124 self.apply_one_tick(area, &config);
125 self.last_config = Some(config);
126 }
127
128 pub fn reset(&mut self) {
134 self.streams.clear();
135 #[cfg(feature = "std")]
136 {
137 self.last_tick = None;
138 self.accum = Duration::ZERO;
139 }
140 self.last_area = None;
141 self.last_config = None;
142 self.frame = 0;
143 self.paused = false;
144 }
145
146 pub fn pause(&mut self) {
150 self.paused = true;
151 }
152
153 pub fn resume(&mut self) {
159 self.paused = false;
160 #[cfg(feature = "std")]
161 {
162 self.last_tick = None;
163 self.accum = Duration::ZERO;
164 }
165 }
166
167 pub fn is_paused(&self) -> bool {
169 self.paused
170 }
171
172 pub fn streams_len(&self) -> usize {
178 self.streams.len()
179 }
180
181 pub(crate) fn streams(&self) -> &[Stream] {
182 &self.streams
183 }
184
185 pub(crate) fn color_count(&self) -> Option<u16> {
186 self.color_count
187 }
188
189 pub fn set_color_count(&mut self, count: u16) {
194 self.color_count = Some(count);
195 }
196
197 pub(crate) fn advance(&mut self, area: Rect, config: &MatrixConfig) {
198 if area.width == 0 || area.height == 0 {
199 self.streams.clear();
200 #[cfg(feature = "std")]
201 {
202 self.last_tick = None;
203 self.accum = Duration::ZERO;
204 }
205 self.last_area = None;
206 return;
207 }
208
209 self.handle_resize(area, config);
210
211 #[cfg(feature = "std")]
212 if !self.paused {
213 let now = Instant::now();
214 let ticks = self.compute_tick_budget(now, config);
215 for _ in 0..ticks {
216 self.apply_one_tick(area, config);
217 }
218 self.last_tick = Some(now);
219 }
220
221 self.last_area = Some(area);
222 self.last_config = Some(config.clone());
223 }
224
225 fn handle_resize(&mut self, area: Rect, config: &MatrixConfig) {
226 let prev = self.last_area;
227 let new_w = area.width as usize;
228
229 let width_changed = prev.map_or(true, |p| p.width != area.width);
230 let height_changed = prev.map_or(false, |p| p.height != area.height);
231
232 if width_changed {
233 if self.streams.len() < new_w {
234 for _ in self.streams.len()..new_w {
235 self.streams
236 .push(Stream::new_idle(config.max_trail, &mut self.rng));
237 }
238 } else if self.streams.len() > new_w {
239 self.streams.truncate(new_w);
240 }
241 }
242
243 if height_changed {
244 let max_head = (area.height as f32) + (config.max_trail as f32);
245 for stream in &mut self.streams {
246 if stream.is_active() {
247 let clamped = stream.head_row().clamp(0.0, max_head);
248 stream.set_head_row(clamped);
249 if (clamped - stream.length() as f32) >= area.height as f32 {
250 stream.force_retire(&mut self.rng);
251 }
252 }
253 }
254 }
255 }
256
257 #[cfg(feature = "std")]
258 fn compute_tick_budget(&mut self, now: Instant, config: &MatrixConfig) -> u32 {
259 let ticks_per_sec = (config.fps as f32) * config.speed;
260 if !ticks_per_sec.is_finite() || ticks_per_sec <= 0.0 {
261 self.accum = Duration::ZERO;
262 return 0;
263 }
264
265 match self.last_tick {
266 None => {
267 self.accum = Duration::ZERO;
268 1
269 }
270 Some(prev) => {
271 let elapsed = now.saturating_duration_since(prev);
272 let total_secs = elapsed.as_secs_f32() + self.accum.as_secs_f32();
273 let total_ticks = total_secs * ticks_per_sec;
274 if !total_ticks.is_finite() {
275 self.accum = Duration::ZERO;
276 return 0;
277 }
278 let ticks = (total_ticks.floor() as u32).min(MAX_CATCHUP_TICKS);
279 let leftover_ticks = (total_ticks - ticks as f32).max(0.0);
280 let leftover_secs = leftover_ticks / ticks_per_sec;
281 self.accum = Duration::from_secs_f32(leftover_secs.max(0.0));
282 ticks
283 }
284 }
285 }
286
287 fn apply_one_tick(&mut self, area: Rect, config: &MatrixConfig) {
288 let chars = config.charset.chars();
289 for stream in &mut self.streams {
290 stream.tick(area.height, config.fps, &mut self.rng);
291 }
292 if config.mutation_rate > 0.0 {
293 for stream in &mut self.streams {
294 stream.mutate(&mut self.rng, chars, config.mutation_rate);
295 }
296 }
297 if config.glitch > 0.0 {
298 for stream in &mut self.streams {
299 stream.glitch_roll(&mut self.rng, config.glitch);
300 }
301 }
302 for stream in &mut self.streams {
303 if stream.is_ready_to_spawn() && self.rng.gen::<f32>() < config.density {
304 stream.spawn(
305 &mut self.rng,
306 chars,
307 config.min_trail,
308 config.max_trail,
309 config.fps,
310 );
311 }
312 }
313 self.frame = self.frame.wrapping_add(1);
314 }
315}
316
317#[cfg(feature = "std")]
318impl Default for MatrixRainState {
319 fn default() -> Self {
320 Self::new()
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 fn area(w: u16, h: u16) -> Rect {
329 Rect::new(0, 0, w, h)
330 }
331
332 #[test]
333 fn new_starts_with_no_streams_no_timing() {
334 let s = MatrixRainState::new();
335 assert!(s.streams.is_empty());
336 assert!(s.last_tick.is_none());
337 assert!(s.last_area.is_none());
338 assert_eq!(s.frame, 0);
339 }
340
341 #[test]
342 fn first_render_budget_is_one_tick() {
343 let mut s = MatrixRainState::with_seed(0);
344 let cfg = MatrixConfig::default();
345 let ticks = s.compute_tick_budget(Instant::now(), &cfg);
346 assert_eq!(ticks, 1);
347 assert_eq!(s.accum, Duration::ZERO);
348 }
349
350 #[test]
351 fn first_render_allocates_streams_per_column() {
352 let mut s = MatrixRainState::with_seed(0);
353 let cfg = MatrixConfig::default();
354 s.advance(area(12, 10), &cfg);
355 assert_eq!(s.streams().len(), 12);
356 assert_eq!(s.frame, 1);
357 assert!(s.last_tick.is_some());
358 }
359
360 #[test]
361 fn width_resize_grows_and_shrinks_streams() {
362 let mut s = MatrixRainState::with_seed(0);
363 let cfg = MatrixConfig::default();
364 s.advance(area(5, 10), &cfg);
365 assert_eq!(s.streams().len(), 5);
366 s.advance(area(10, 10), &cfg);
367 assert_eq!(s.streams().len(), 10);
368 s.advance(area(3, 10), &cfg);
369 assert_eq!(s.streams().len(), 3);
370 }
371
372 #[test]
373 fn empty_area_clears_streams_and_resets_first_render_path() {
374 let mut s = MatrixRainState::with_seed(0);
375 let cfg = MatrixConfig::default();
376 s.advance(area(10, 10), &cfg);
377 let frame_after_first = s.frame;
378
379 s.advance(area(0, 10), &cfg);
380 assert_eq!(s.streams().len(), 0);
381 assert!(s.last_tick.is_none());
382 assert!(s.last_area.is_none());
383
384 s.advance(area(10, 10), &cfg);
385 assert_eq!(s.frame, frame_after_first + 1);
386 }
387
388 #[test]
389 fn empty_area_height_zero_also_handled() {
390 let mut s = MatrixRainState::with_seed(0);
391 let cfg = MatrixConfig::default();
392 s.advance(area(10, 0), &cfg);
393 assert_eq!(s.streams().len(), 0);
394 assert!(s.last_tick.is_none());
395 }
396
397 #[test]
398 fn tick_before_first_render_is_noop() {
399 let mut s = MatrixRainState::with_seed(0);
400 s.tick();
401 assert_eq!(s.frame, 0);
402 assert!(s.last_tick.is_none());
403 }
404
405 #[test]
406 fn tick_after_first_render_advances_one_frame() {
407 let mut s = MatrixRainState::with_seed(0);
408 let cfg = MatrixConfig::default();
409 s.advance(area(10, 20), &cfg);
410 let frame_before = s.frame;
411 let last_tick_before = s.last_tick;
412 s.tick();
413 assert_eq!(s.frame, frame_before + 1);
414 assert_eq!(
415 s.last_tick, last_tick_before,
416 "tick() must not touch last_tick"
417 );
418 }
419
420 #[test]
421 fn reset_clears_streams_and_timing_keeps_color_count() {
422 let mut s = MatrixRainState::with_seed(42);
423 let cfg = MatrixConfig::default();
424 s.advance(area(10, 20), &cfg);
425 s.set_color_count(256);
426 s.reset();
427 assert_eq!(s.streams().len(), 0);
428 assert!(s.last_tick.is_none());
429 assert!(s.last_area.is_none());
430 assert_eq!(s.frame, 0);
431 assert_eq!(s.color_count(), Some(256));
432 }
433
434 #[test]
435 fn deterministic_with_same_seed() {
436 let cfg = MatrixConfig::default();
437 let mut a = MatrixRainState::with_seed(0xC0FFEE);
438 let mut b = MatrixRainState::with_seed(0xC0FFEE);
439 a.advance(area(15, 15), &cfg);
440 b.advance(area(15, 15), &cfg);
441 assert_eq!(a.streams().len(), b.streams().len());
442 for (sa, sb) in a.streams().iter().zip(b.streams()) {
443 assert_eq!(sa.is_active(), sb.is_active());
444 assert_eq!(sa.length(), sb.length());
445 assert_eq!(sa.head_row(), sb.head_row());
446 }
447 }
448
449 #[test]
450 fn catchup_cap_limits_huge_elapsed() {
451 let mut s = MatrixRainState::with_seed(0);
452 let cfg = MatrixConfig::default();
453 s.last_tick = Some(Instant::now() - Duration::from_secs(60));
454 let ticks = s.compute_tick_budget(Instant::now(), &cfg);
455 assert_eq!(ticks, MAX_CATCHUP_TICKS);
456 }
457
458 #[test]
459 fn sub_tick_render_carries_remainder() {
460 let mut s = MatrixRainState::with_seed(0);
461 let cfg = MatrixConfig::default();
462 let now = Instant::now();
463 s.last_tick = Some(now - Duration::from_micros(500));
464 let ticks = s.compute_tick_budget(now, &cfg);
465 assert_eq!(ticks, 0);
466 assert!(s.accum > Duration::ZERO);
467 }
468
469 #[test]
470 fn pathological_zero_fps_no_panic() {
471 let mut s = MatrixRainState::with_seed(0);
472 let cfg = MatrixConfig {
473 fps: 0,
474 ..MatrixConfig::default()
475 };
476 assert_eq!(s.compute_tick_budget(Instant::now(), &cfg), 0);
477 }
478
479 #[test]
480 fn color_count_default_none_then_set() {
481 let mut s = MatrixRainState::new();
482 assert!(s.color_count().is_none());
483 s.set_color_count(16);
484 assert_eq!(s.color_count(), Some(16));
485 }
486
487 #[test]
488 fn state_is_send() {
489 fn assert_send<T: Send>() {}
490 assert_send::<MatrixRainState>();
491 }
492
493 #[test]
494 fn mutation_rate_zero_keeps_glyphs_unchanged_per_tick() {
495 let cfg = MatrixConfig::builder()
497 .fps(30)
498 .density(1.0)
499 .mutation_rate(0.0)
500 .min_trail(8)
501 .max_trail(8)
502 .charset(crate::charset::CharSet::Custom(vec!['a', 'b', 'c']))
503 .build()
504 .unwrap();
505 let mut s = MatrixRainState::with_seed(0x1234);
506 s.advance(area(8, 400), &cfg);
507 for _ in 0..15 {
508 s.apply_one_tick(area(8, 400), &cfg);
509 }
510 let idx = s.streams.iter().position(|st| st.is_active()).expect("active");
511 let before: Vec<char> = s.streams[idx].glyphs().to_vec();
512 s.apply_one_tick(area(8, 400), &cfg);
513 assert!(s.streams[idx].is_active());
514 assert_eq!(s.streams[idx].glyphs(), before.as_slice());
515 }
516
517 #[test]
518 fn pause_freezes_frame_advance_in_render_path() {
519 let cfg = MatrixConfig::default();
520 let mut s = MatrixRainState::with_seed(0xBABE);
521 s.advance(area(8, 20), &cfg);
522 let frame_after_first = s.frame;
523 assert!(frame_after_first > 0);
524
525 s.pause();
526 assert!(s.is_paused());
527 for _ in 0..50 {
529 s.advance(area(8, 20), &cfg);
530 }
531 assert_eq!(s.frame, frame_after_first);
532 assert_eq!(s.last_area, Some(area(8, 20)));
534 }
535
536 #[test]
537 fn resume_clears_last_tick_so_next_render_is_first_render() {
538 let cfg = MatrixConfig::default();
539 let mut s = MatrixRainState::with_seed(0xBABE);
540 s.advance(area(8, 20), &cfg);
541 s.pause();
542 s.advance(area(8, 20), &cfg);
543
544 s.resume();
545 assert!(!s.is_paused());
546 assert!(s.last_tick.is_none());
547 assert_eq!(s.accum, Duration::ZERO);
548
549 let frame_before = s.frame;
550 s.advance(area(8, 20), &cfg);
551 assert_eq!(
552 s.frame,
553 frame_before + 1,
554 "post-resume render should apply exactly one tick (first-render path)"
555 );
556 }
557
558 #[test]
559 fn tick_bypasses_pause() {
560 let cfg = MatrixConfig::default();
561 let mut s = MatrixRainState::with_seed(0xBABE);
562 s.advance(area(8, 20), &cfg);
563 s.pause();
564 let frame_before = s.frame;
565 s.tick();
566 assert_eq!(s.frame, frame_before + 1);
567 assert!(s.is_paused(), "tick must not implicitly resume");
568 }
569
570 #[test]
571 fn pause_and_resume_are_idempotent() {
572 let mut s = MatrixRainState::new();
573 s.pause();
574 s.pause();
575 assert!(s.is_paused());
576 s.resume();
577 s.resume();
578 assert!(!s.is_paused());
579 }
580
581 #[test]
582 fn reset_clears_paused_state() {
583 let mut s = MatrixRainState::new();
584 s.pause();
585 s.reset();
586 assert!(!s.is_paused());
587 }
588
589 #[test]
590 fn resize_while_paused_still_resizes_streams() {
591 let cfg = MatrixConfig::default();
592 let mut s = MatrixRainState::with_seed(0xBABE);
593 s.advance(area(8, 20), &cfg);
594 s.pause();
595 s.advance(area(16, 20), &cfg);
596 assert_eq!(s.streams.len(), 16);
597 s.advance(area(4, 20), &cfg);
598 assert_eq!(s.streams.len(), 4);
599 }
600
601 #[test]
602 fn glitch_zero_leaves_flags_unset_after_apply_one_tick() {
603 let cfg = MatrixConfig::builder()
604 .fps(30)
605 .density(1.0)
606 .glitch(0.0)
607 .build()
608 .unwrap();
609 let mut s = MatrixRainState::with_seed(0xFEED);
610 s.advance(area(8, 200), &cfg);
611 for _ in 0..10 {
612 s.apply_one_tick(area(8, 200), &cfg);
613 }
614 for stream in &s.streams {
615 if stream.is_active() {
616 for i in 0..stream.length() {
617 assert!(!stream.is_glitched(i));
618 }
619 }
620 }
621 }
622
623 #[test]
624 fn glitch_one_sets_all_flags_after_apply_one_tick() {
625 let cfg = MatrixConfig::builder()
626 .fps(30)
627 .density(1.0)
628 .glitch(1.0)
629 .min_trail(6)
630 .max_trail(6)
631 .build()
632 .unwrap();
633 let mut s = MatrixRainState::with_seed(0xFEED);
634 s.advance(area(8, 200), &cfg);
635 for _ in 0..15 {
636 s.apply_one_tick(area(8, 200), &cfg);
637 }
638 let stream = s.streams.iter().find(|st| st.is_active()).expect("active");
639 for i in 0..stream.length() {
640 assert!(stream.is_glitched(i), "cell {i} should be glitched at rate=1.0");
641 }
642 }
643
644 #[test]
645 fn mutation_rate_one_changes_at_least_one_glyph_per_tick() {
646 let cfg = MatrixConfig::builder()
650 .fps(30)
651 .density(1.0)
652 .mutation_rate(1.0)
653 .min_trail(8)
654 .max_trail(8)
655 .charset(crate::charset::CharSet::Custom(vec!['a', 'b']))
656 .build()
657 .unwrap();
658 let mut s = MatrixRainState::with_seed(0xABCD);
659 s.advance(area(8, 400), &cfg);
660 for _ in 0..15 {
661 s.apply_one_tick(area(8, 400), &cfg);
662 }
663 let idx = s.streams.iter().position(|st| st.is_active()).expect("active");
664 let before: Vec<char> = s.streams[idx].glyphs().to_vec();
665 s.apply_one_tick(area(8, 400), &cfg);
666 assert!(s.streams[idx].is_active());
667 let changed = s.streams[idx]
668 .glyphs()
669 .iter()
670 .zip(before.iter())
671 .filter(|(a, b)| a != b)
672 .count();
673 assert!(changed > 0, "expected at least one glyph to mutate");
674 for g in s.streams[idx].glyphs() {
675 assert!(['a', 'b'].contains(g), "mutated glyph {g} not from charset");
676 }
677 }
678}