sigye_background/
state.rs

1//! Background animation state management.
2
3use ratatui::{
4    Frame,
5    text::{Line, Span},
6    widgets::Paragraph,
7};
8use sigye_core::{AnimationSpeed, BackgroundStyle, SystemMetrics};
9
10use crate::animations::{matrix, reactive, stateless, weather};
11
12/// Background animation state.
13#[derive(Debug)]
14pub struct BackgroundState {
15    /// Matrix rain column states.
16    matrix_columns: Vec<matrix::MatrixColumn>,
17    /// Snowfall column states.
18    snow_columns: Vec<weather::SnowColumn>,
19    /// Rain column states (for Rainy background).
20    rain_columns: Vec<weather::RainColumn>,
21    /// Storm state (for Stormy background).
22    storm_state: Option<weather::StormState>,
23    /// Wind streak states (for Windy background).
24    wind_streaks: Vec<weather::WindStreak>,
25    /// Last known terminal width.
26    last_width: u16,
27    /// Last known terminal height.
28    last_height: u16,
29    /// Last update time in milliseconds.
30    last_update_ms: u64,
31    /// Seed captured at initialization for randomness.
32    init_seed: u64,
33}
34
35impl Default for BackgroundState {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl BackgroundState {
42    /// Create a new background state.
43    pub fn new() -> Self {
44        use std::time::{SystemTime, UNIX_EPOCH};
45
46        // Capture system time as seed for randomness
47        let init_seed = SystemTime::now()
48            .duration_since(UNIX_EPOCH)
49            .map(|d| d.as_nanos() as u64)
50            .unwrap_or(0);
51
52        Self {
53            matrix_columns: Vec::new(),
54            snow_columns: Vec::new(),
55            rain_columns: Vec::new(),
56            storm_state: None,
57            wind_streaks: Vec::new(),
58            last_width: 0,
59            last_height: 0,
60            last_update_ms: 0,
61            init_seed,
62        }
63    }
64
65    /// Render the background to the frame.
66    pub fn render(
67        &mut self,
68        frame: &mut Frame,
69        style: BackgroundStyle,
70        elapsed_ms: u64,
71        speed: AnimationSpeed,
72        metrics: Option<&SystemMetrics>,
73    ) {
74        if style == BackgroundStyle::None {
75            return;
76        }
77
78        let area = frame.area();
79        let width = area.width;
80        let height = area.height;
81
82        // Handle reactive backgrounds separately
83        if style.is_reactive() {
84            if let Some(m) = metrics {
85                self.render_reactive(frame, style, elapsed_ms, speed, m);
86            }
87            return;
88        }
89
90        // Reinitialize if dimensions changed or columns not initialized
91        let dimensions_changed = width != self.last_width || height != self.last_height;
92
93        if style == BackgroundStyle::MatrixRain
94            && (dimensions_changed || self.matrix_columns.is_empty())
95        {
96            self.matrix_columns = matrix::init_columns(width, height);
97        }
98        if style == BackgroundStyle::Snowfall
99            && (dimensions_changed || self.snow_columns.is_empty())
100        {
101            self.snow_columns = weather::init_snow_columns(width, height, self.init_seed);
102        }
103        // Weather animation initialization
104        if style == BackgroundStyle::Rainy && (dimensions_changed || self.rain_columns.is_empty()) {
105            self.rain_columns = weather::init_rain_columns(width, height, self.init_seed);
106        }
107        if style == BackgroundStyle::Stormy && (dimensions_changed || self.storm_state.is_none()) {
108            self.storm_state = Some(weather::init_storm(width, height, self.init_seed));
109        }
110        if style == BackgroundStyle::Windy && (dimensions_changed || self.wind_streaks.is_empty()) {
111            self.wind_streaks = weather::init_wind_streaks(width, height, self.init_seed);
112        }
113
114        if dimensions_changed {
115            self.last_width = width;
116            self.last_height = height;
117        }
118
119        // Calculate delta time for stateful animations
120        let delta_ms = elapsed_ms.saturating_sub(self.last_update_ms);
121        self.last_update_ms = elapsed_ms;
122
123        // Update animation states
124        if style == BackgroundStyle::MatrixRain {
125            matrix::update(&mut self.matrix_columns, delta_ms, height, speed);
126        }
127        if style == BackgroundStyle::Snowfall {
128            weather::update_snow(&mut self.snow_columns, delta_ms, height, speed);
129        }
130        // Weather animation updates
131        if style == BackgroundStyle::Rainy {
132            weather::update_rain(&mut self.rain_columns, delta_ms, height, speed);
133        }
134        if style == BackgroundStyle::Stormy
135            && let Some(ref mut storm) = self.storm_state
136        {
137            weather::update_storm(storm, elapsed_ms, delta_ms, height, speed);
138        }
139        if style == BackgroundStyle::Windy {
140            weather::update_wind(&mut self.wind_streaks, delta_ms, width, height, speed);
141        }
142
143        let lines: Vec<Line> = (0..height)
144            .map(|y| {
145                let spans: Vec<Span> = (0..width)
146                    .map(|x| self.render_char(x, y, width, height, style, elapsed_ms, speed))
147                    .collect();
148                Line::from(spans)
149            })
150            .collect();
151
152        frame.render_widget(Paragraph::new(lines), area);
153    }
154
155    /// Render a single background character at the given position.
156    fn render_char(
157        &self,
158        x: u16,
159        y: u16,
160        width: u16,
161        height: u16,
162        style: BackgroundStyle,
163        elapsed_ms: u64,
164        speed: AnimationSpeed,
165    ) -> Span<'static> {
166        match style {
167            BackgroundStyle::None => Span::raw(" "),
168            BackgroundStyle::Starfield => stateless::render_starfield_char(x, y, elapsed_ms, speed),
169            BackgroundStyle::MatrixRain => matrix::render_char(&self.matrix_columns, x, y),
170            BackgroundStyle::GradientWave => {
171                stateless::render_gradient_char(x, y, width, height, elapsed_ms, speed)
172            }
173            BackgroundStyle::Snowfall => {
174                weather::render_snow_char(&self.snow_columns, x, y, elapsed_ms)
175            }
176            BackgroundStyle::Frost => {
177                stateless::render_frost_char(x, y, width, height, elapsed_ms, speed)
178            }
179            BackgroundStyle::Aurora => {
180                stateless::render_aurora_char(x, y, width, height, elapsed_ms, speed)
181            }
182            // Weather backgrounds
183            BackgroundStyle::Sunny => {
184                weather::render_sunny_char(x, y, width, height, elapsed_ms, speed)
185            }
186            BackgroundStyle::Rainy => weather::render_rain_char(&self.rain_columns, x, y),
187            BackgroundStyle::Stormy => {
188                if let Some(ref storm) = self.storm_state {
189                    weather::render_storm_char(storm, x, y, elapsed_ms)
190                } else {
191                    Span::raw(" ")
192                }
193            }
194            BackgroundStyle::Windy => {
195                weather::render_wind_char(&self.wind_streaks, x, y, elapsed_ms)
196            }
197            BackgroundStyle::Cloudy => {
198                weather::render_cloudy_char(x, y, width, height, elapsed_ms, speed)
199            }
200            BackgroundStyle::Foggy => {
201                weather::render_foggy_char(x, y, width, height, elapsed_ms, speed)
202            }
203            // Weather style should be resolved by main app before rendering.
204            // If it reaches here, fallback to Starfield.
205            BackgroundStyle::Weather => stateless::render_starfield_char(x, y, elapsed_ms, speed),
206            // Twilight backgrounds
207            BackgroundStyle::TwilightDawn => {
208                stateless::render_twilight_dawn_char(x, y, width, height, elapsed_ms, speed)
209            }
210            BackgroundStyle::TwilightDusk => {
211                stateless::render_twilight_dusk_char(x, y, width, height, elapsed_ms, speed)
212            }
213            // Reactive backgrounds are handled separately in render_reactive()
214            BackgroundStyle::SystemPulse
215            | BackgroundStyle::ResourceWave
216            | BackgroundStyle::DataFlow
217            | BackgroundStyle::HeatMap => Span::raw(" "),
218        }
219    }
220
221    /// Render reactive backgrounds that respond to system metrics.
222    fn render_reactive(
223        &mut self,
224        frame: &mut Frame,
225        style: BackgroundStyle,
226        elapsed_ms: u64,
227        speed: AnimationSpeed,
228        metrics: &SystemMetrics,
229    ) {
230        match style {
231            BackgroundStyle::SystemPulse => {
232                reactive::render_system_pulse(frame, elapsed_ms, speed, metrics)
233            }
234            BackgroundStyle::ResourceWave => {
235                reactive::render_resource_wave(frame, elapsed_ms, speed, metrics)
236            }
237            BackgroundStyle::DataFlow => {
238                reactive::render_data_flow(frame, elapsed_ms, speed, metrics)
239            }
240            BackgroundStyle::HeatMap => {
241                reactive::render_heat_map(frame, elapsed_ms, speed, metrics)
242            }
243            _ => {}
244        }
245    }
246}