1use std::cell::Cell;
2use std::marker::PhantomData;
3use std::time::{Duration, Instant};
4
5use rand::rngs::SmallRng;
6use rand::{Rng, SeedableRng};
7use ratatui::layout::Rect;
8
9use crate::config::MatrixConfig;
10use crate::stream::Stream;
11
12const MAX_CATCHUP_TICKS: u32 = 4;
13
14pub struct MatrixRainState {
15 streams: Vec<Stream>,
16 last_tick: Option<Instant>,
17 accum: Duration,
18 frame: u64,
19 rng: SmallRng,
20 last_area: Option<Rect>,
21 color_count: Option<u16>,
22 last_config: Option<MatrixConfig>,
23 _not_sync: PhantomData<Cell<()>>,
24}
25
26impl MatrixRainState {
27 pub fn new() -> Self {
28 Self::from_rng(SmallRng::from_entropy())
29 }
30
31 pub fn with_seed(seed: u64) -> Self {
32 Self::from_rng(SmallRng::seed_from_u64(seed))
33 }
34
35 fn from_rng(rng: SmallRng) -> Self {
36 Self {
37 streams: Vec::new(),
38 last_tick: None,
39 accum: Duration::ZERO,
40 frame: 0,
41 rng,
42 last_area: None,
43 color_count: None,
44 last_config: None,
45 _not_sync: PhantomData,
46 }
47 }
48
49 pub fn tick(&mut self) {
50 let area = match self.last_area {
51 Some(a) if a.width > 0 && a.height > 0 => a,
52 _ => return,
53 };
54 let Some(config) = self.last_config.take() else {
55 return;
56 };
57 self.apply_one_tick(area, &config);
58 self.last_config = Some(config);
59 }
60
61 pub fn reset(&mut self) {
62 self.streams.clear();
63 self.last_tick = None;
64 self.accum = Duration::ZERO;
65 self.last_area = None;
66 self.last_config = None;
67 self.frame = 0;
68 }
69
70 pub fn streams_len(&self) -> usize {
71 self.streams.len()
72 }
73
74 pub(crate) fn streams(&self) -> &[Stream] {
75 &self.streams
76 }
77
78 pub(crate) fn color_count(&self) -> Option<u16> {
79 self.color_count
80 }
81
82 pub fn set_color_count(&mut self, count: u16) {
87 self.color_count = Some(count);
88 }
89
90 pub(crate) fn advance(&mut self, area: Rect, config: &MatrixConfig) {
91 if area.width == 0 || area.height == 0 {
92 self.streams.clear();
93 self.last_tick = None;
94 self.accum = Duration::ZERO;
95 self.last_area = None;
96 return;
97 }
98
99 self.handle_resize(area, config);
100
101 let now = Instant::now();
102 let ticks = self.compute_tick_budget(now, config);
103 for _ in 0..ticks {
104 self.apply_one_tick(area, config);
105 }
106
107 self.last_tick = Some(now);
108 self.last_area = Some(area);
109 self.last_config = Some(config.clone());
110 }
111
112 fn handle_resize(&mut self, area: Rect, config: &MatrixConfig) {
113 let prev = self.last_area;
114 let new_w = area.width as usize;
115
116 let width_changed = prev.map_or(true, |p| p.width != area.width);
117 let height_changed = prev.map_or(false, |p| p.height != area.height);
118
119 if width_changed {
120 if self.streams.len() < new_w {
121 for _ in self.streams.len()..new_w {
122 self.streams
123 .push(Stream::new_idle(config.max_trail, &mut self.rng));
124 }
125 } else if self.streams.len() > new_w {
126 self.streams.truncate(new_w);
127 }
128 }
129
130 if height_changed {
131 let max_head = (area.height as f32) + (config.max_trail as f32);
132 for stream in &mut self.streams {
133 if stream.is_active() {
134 let clamped = stream.head_row().clamp(0.0, max_head);
135 stream.set_head_row(clamped);
136 if (clamped - stream.length() as f32) >= area.height as f32 {
137 stream.force_retire(&mut self.rng);
138 }
139 }
140 }
141 }
142 }
143
144 fn compute_tick_budget(&mut self, now: Instant, config: &MatrixConfig) -> u32 {
145 let ticks_per_sec = (config.fps as f32) * config.speed;
146 if !ticks_per_sec.is_finite() || ticks_per_sec <= 0.0 {
147 self.accum = Duration::ZERO;
148 return 0;
149 }
150
151 match self.last_tick {
152 None => {
153 self.accum = Duration::ZERO;
154 1
155 }
156 Some(prev) => {
157 let elapsed = now.saturating_duration_since(prev);
158 let total_secs = elapsed.as_secs_f32() + self.accum.as_secs_f32();
159 let total_ticks = total_secs * ticks_per_sec;
160 if !total_ticks.is_finite() {
161 self.accum = Duration::ZERO;
162 return 0;
163 }
164 let ticks = (total_ticks.floor() as u32).min(MAX_CATCHUP_TICKS);
165 let leftover_ticks = (total_ticks - ticks as f32).max(0.0);
166 let leftover_secs = leftover_ticks / ticks_per_sec;
167 self.accum = Duration::from_secs_f32(leftover_secs.max(0.0));
168 ticks
169 }
170 }
171 }
172
173 fn apply_one_tick(&mut self, area: Rect, config: &MatrixConfig) {
174 let chars = config.charset.chars();
175 for stream in &mut self.streams {
176 stream.tick(area.height, config.fps, &mut self.rng);
177 }
178 for stream in &mut self.streams {
179 if stream.is_ready_to_spawn() && self.rng.gen::<f32>() < config.density {
180 stream.spawn(
181 &mut self.rng,
182 chars,
183 config.min_trail,
184 config.max_trail,
185 config.fps,
186 );
187 }
188 }
189 self.frame = self.frame.wrapping_add(1);
190 }
191}
192
193impl Default for MatrixRainState {
194 fn default() -> Self {
195 Self::new()
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 fn area(w: u16, h: u16) -> Rect {
204 Rect::new(0, 0, w, h)
205 }
206
207 #[test]
208 fn new_starts_with_no_streams_no_timing() {
209 let s = MatrixRainState::new();
210 assert!(s.streams.is_empty());
211 assert!(s.last_tick.is_none());
212 assert!(s.last_area.is_none());
213 assert_eq!(s.frame, 0);
214 }
215
216 #[test]
217 fn first_render_budget_is_one_tick() {
218 let mut s = MatrixRainState::with_seed(0);
219 let cfg = MatrixConfig::default();
220 let ticks = s.compute_tick_budget(Instant::now(), &cfg);
221 assert_eq!(ticks, 1);
222 assert_eq!(s.accum, Duration::ZERO);
223 }
224
225 #[test]
226 fn first_render_allocates_streams_per_column() {
227 let mut s = MatrixRainState::with_seed(0);
228 let cfg = MatrixConfig::default();
229 s.advance(area(12, 10), &cfg);
230 assert_eq!(s.streams().len(), 12);
231 assert_eq!(s.frame, 1);
232 assert!(s.last_tick.is_some());
233 }
234
235 #[test]
236 fn width_resize_grows_and_shrinks_streams() {
237 let mut s = MatrixRainState::with_seed(0);
238 let cfg = MatrixConfig::default();
239 s.advance(area(5, 10), &cfg);
240 assert_eq!(s.streams().len(), 5);
241 s.advance(area(10, 10), &cfg);
242 assert_eq!(s.streams().len(), 10);
243 s.advance(area(3, 10), &cfg);
244 assert_eq!(s.streams().len(), 3);
245 }
246
247 #[test]
248 fn empty_area_clears_streams_and_resets_first_render_path() {
249 let mut s = MatrixRainState::with_seed(0);
250 let cfg = MatrixConfig::default();
251 s.advance(area(10, 10), &cfg);
252 let frame_after_first = s.frame;
253
254 s.advance(area(0, 10), &cfg);
255 assert_eq!(s.streams().len(), 0);
256 assert!(s.last_tick.is_none());
257 assert!(s.last_area.is_none());
258
259 s.advance(area(10, 10), &cfg);
260 assert_eq!(s.frame, frame_after_first + 1);
261 }
262
263 #[test]
264 fn empty_area_height_zero_also_handled() {
265 let mut s = MatrixRainState::with_seed(0);
266 let cfg = MatrixConfig::default();
267 s.advance(area(10, 0), &cfg);
268 assert_eq!(s.streams().len(), 0);
269 assert!(s.last_tick.is_none());
270 }
271
272 #[test]
273 fn tick_before_first_render_is_noop() {
274 let mut s = MatrixRainState::with_seed(0);
275 s.tick();
276 assert_eq!(s.frame, 0);
277 assert!(s.last_tick.is_none());
278 }
279
280 #[test]
281 fn tick_after_first_render_advances_one_frame() {
282 let mut s = MatrixRainState::with_seed(0);
283 let cfg = MatrixConfig::default();
284 s.advance(area(10, 20), &cfg);
285 let frame_before = s.frame;
286 let last_tick_before = s.last_tick;
287 s.tick();
288 assert_eq!(s.frame, frame_before + 1);
289 assert_eq!(
290 s.last_tick, last_tick_before,
291 "tick() must not touch last_tick"
292 );
293 }
294
295 #[test]
296 fn reset_clears_streams_and_timing_keeps_color_count() {
297 let mut s = MatrixRainState::with_seed(42);
298 let cfg = MatrixConfig::default();
299 s.advance(area(10, 20), &cfg);
300 s.set_color_count(256);
301 s.reset();
302 assert_eq!(s.streams().len(), 0);
303 assert!(s.last_tick.is_none());
304 assert!(s.last_area.is_none());
305 assert_eq!(s.frame, 0);
306 assert_eq!(s.color_count(), Some(256));
307 }
308
309 #[test]
310 fn deterministic_with_same_seed() {
311 let cfg = MatrixConfig::default();
312 let mut a = MatrixRainState::with_seed(0xC0FFEE);
313 let mut b = MatrixRainState::with_seed(0xC0FFEE);
314 a.advance(area(15, 15), &cfg);
315 b.advance(area(15, 15), &cfg);
316 assert_eq!(a.streams().len(), b.streams().len());
317 for (sa, sb) in a.streams().iter().zip(b.streams()) {
318 assert_eq!(sa.is_active(), sb.is_active());
319 assert_eq!(sa.length(), sb.length());
320 assert_eq!(sa.head_row(), sb.head_row());
321 }
322 }
323
324 #[test]
325 fn catchup_cap_limits_huge_elapsed() {
326 let mut s = MatrixRainState::with_seed(0);
327 let cfg = MatrixConfig::default();
328 s.last_tick = Some(Instant::now() - Duration::from_secs(60));
329 let ticks = s.compute_tick_budget(Instant::now(), &cfg);
330 assert_eq!(ticks, MAX_CATCHUP_TICKS);
331 }
332
333 #[test]
334 fn sub_tick_render_carries_remainder() {
335 let mut s = MatrixRainState::with_seed(0);
336 let cfg = MatrixConfig::default();
337 let now = Instant::now();
338 s.last_tick = Some(now - Duration::from_micros(500));
339 let ticks = s.compute_tick_budget(now, &cfg);
340 assert_eq!(ticks, 0);
341 assert!(s.accum > Duration::ZERO);
342 }
343
344 #[test]
345 fn pathological_zero_fps_no_panic() {
346 let mut s = MatrixRainState::with_seed(0);
347 let cfg = MatrixConfig {
348 fps: 0,
349 ..MatrixConfig::default()
350 };
351 assert_eq!(s.compute_tick_budget(Instant::now(), &cfg), 0);
352 }
353
354 #[test]
355 fn color_count_default_none_then_set() {
356 let mut s = MatrixRainState::new();
357 assert!(s.color_count().is_none());
358 s.set_color_count(16);
359 assert_eq!(s.color_count(), Some(16));
360 }
361
362 #[test]
363 fn state_is_send() {
364 fn assert_send<T: Send>() {}
365 assert_send::<MatrixRainState>();
366 }
367}