git.lucas.co / cce-screenaver
screensaver
git clone https://git.lucas.co/cce-screenaver.git

src/main.rs (10.8K)

  1 use cce_ui::engine::{Application, LogicalPosition, LogicalSize, WindowSettings};
  2 use cce_ui::widget::{MouseButton, ElementState, MouseScrollDelta, KeyEvent};
  3 use std::time::SystemTime;
  4 
  5 /// How long after the screensaver appears a mouse move is ignored, so the
  6 /// wiggle that was already in flight does not dismiss it instantly.
  7 const GRACE: std::time::Duration = std::time::Duration::from_secs(1);
  8 
  9 #[derive(Debug, Clone, Copy, PartialEq)]
 10 enum ScreensaverStyle {
 11     Blank,
 12     Starfield,
 13     Matrix,
 14 }
 15 
 16 struct Lcg {
 17     state: u64,
 18 }
 19 
 20 impl Lcg {
 21     fn new(seed: u64) -> Self {
 22         Self { state: seed }
 23     }
 24 
 25     fn next_f32(&mut self) -> f32 {
 26         self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
 27         let val = (self.state >> 32) as u32;
 28         (val as f32) / (u32::MAX as f32)
 29     }
 30 
 31     fn next_range(&mut self, min: f32, max: f32) -> f32 {
 32         min + self.next_f32() * (max - min)
 33     }
 34 }
 35 
 36 struct Star {
 37     x: f32, // NDC coordinates: -1.0 to 1.0
 38     y: f32, // NDC coordinates: -1.0 to 1.0
 39     z: f32, // Distance: 0.1 to 1.0
 40     speed: f32,
 41     color: [f32; 4],
 42 }
 43 
 44 struct MatrixColumn {
 45     x: f32,     // X coordinate in pixels
 46     y: f32,     // Current falling Y coordinate of the head
 47     speed: f32, // Pixels per second
 48     len: usize, // Tail length (number of cells)
 49 }
 50 
 51 struct ScreensaverApp {
 52     style: ScreensaverStyle,
 53     width: u32,
 54     height: u32,
 55     lcg: Lcg,
 56     stars: Vec<Star>,
 57     matrix_columns: Vec<MatrixColumn>,
 58     init_cursor: Option<LogicalPosition>,
 59     /// End of the one-second grace period that keeps the startup mouse
 60     /// wiggle from dismissing the screensaver instantly. A wall clock, not a
 61     /// `dt` countdown: the Blank style draws nothing and so asks for no
 62     /// frames, and the runner clamps `dt` to one frame per idle wake — the
 63     /// grace period outlasted a minute there.
 64     grace_until: std::time::Instant,
 65 }
 66 
 67 #[derive(Debug, Clone)]
 68 #[allow(dead_code)]
 69 enum AppMessage {
 70     Exit,
 71 }
 72 
 73 impl Application for ScreensaverApp {
 74     type Message = AppMessage;
 75 
 76     fn create(_sender: cce_ui::engine::AppSender<Self::Message>) -> Self {
 77         let style_env = std::env::var("CCE_SCREENSAVER_STYLE").unwrap_or_default();
 78         let style = match style_env.to_lowercase().as_str() {
 79             "starfield" => ScreensaverStyle::Starfield,
 80             "matrix" => ScreensaverStyle::Matrix,
 81             _ => {
 82                 // Also parse command line args
 83                 let args: Vec<String> = std::env::args().collect();
 84                 if args.iter().any(|arg| arg == "starfield") {
 85                     ScreensaverStyle::Starfield
 86                 } else if args.iter().any(|arg| arg == "matrix") {
 87                     ScreensaverStyle::Matrix
 88                 } else {
 89                     ScreensaverStyle::Blank
 90                 }
 91             }
 92         };
 93 
 94         let seed = SystemTime::now()
 95             .duration_since(SystemTime::UNIX_EPOCH)
 96             .map(|d| d.as_nanos() as u64)
 97             .unwrap_or(42);
 98         
 99         let mut lcg = Lcg::new(seed);
100         
101         // Initialize stars
102         let mut stars = Vec::with_capacity(200);
103         for _ in 0..200 {
104             stars.push(Star {
105                 x: lcg.next_range(-1.0, 1.0),
106                 y: lcg.next_range(-1.0, 1.0),
107                 z: lcg.next_range(0.1, 1.0),
108                 speed: lcg.next_range(0.15, 0.4),
109                 color: [
110                     lcg.next_range(0.8, 1.0),
111                     lcg.next_range(0.8, 1.0),
112                     1.0,
113                     1.0,
114                 ],
115             });
116         }
117 
118         Self {
119             style,
120             width: 1920,
121             height: 1080,
122             lcg,
123             stars,
124             matrix_columns: Vec::new(),
125             init_cursor: None,
126             grace_until: std::time::Instant::now() + GRACE,
127         }
128     }
129 
130     fn settings(&self) -> WindowSettings {
131         WindowSettings {
132             title: "CCE Screensaver".to_string(),
133             app_id: "cce-screenaver".to_string(),
134             width: 1920,
135             height: 1080,
136             fullscreen: true,
137             min_size: None,
138         }
139     }
140 
141     fn update(&mut self, msg: Self::Message, _needs_rebuild: &mut bool, exit: &mut bool) {
142         match msg {
143             AppMessage::Exit => {
144                 *exit = true;
145             }
146         }
147     }
148 
149     fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
150         match self.style {
151             ScreensaverStyle::Starfield => {
152                 for star in &mut self.stars {
153                     star.z -= star.speed * dt;
154                     if star.z <= 0.0 {
155                         star.z = 1.0;
156                         star.x = self.lcg.next_range(-1.0, 1.0);
157                         star.y = self.lcg.next_range(-1.0, 1.0);
158                         star.speed = self.lcg.next_range(0.15, 0.4);
159                     }
160                 }
161                 *needs_rebuild = true;
162             }
163             ScreensaverStyle::Matrix => {
164                 // Initialize columns if screen size changes or empty
165                 let cell_w = 20.0f32;
166                 let needed_cols = (self.width as f32 / cell_w).ceil() as usize;
167                 
168                 if self.matrix_columns.len() < needed_cols {
169                     let old_len = self.matrix_columns.len();
170                     for i in old_len..needed_cols {
171                         self.matrix_columns.push(MatrixColumn {
172                             x: i as f32 * cell_w,
173                             y: self.lcg.next_range(-600.0, 0.0),
174                             speed: self.lcg.next_range(120.0, 320.0),
175                             len: self.lcg.next_range(8.0, 24.0) as usize,
176                         });
177                     }
178                 }
179 
180                 for col in &mut self.matrix_columns {
181                     col.y += col.speed * dt;
182                     let cell_h = 24.0f32;
183                     let total_h = col.len as f32 * cell_h;
184                     if col.y - total_h > self.height as f32 {
185                         col.y = -cell_h;
186                         col.speed = self.lcg.next_range(120.0, 320.0);
187                         col.len = self.lcg.next_range(8.0, 24.0) as usize;
188                     }
189                 }
190                 *needs_rebuild = true;
191             }
192             ScreensaverStyle::Blank => {}
193         }
194     }
195 
196     /// Phase 6: the whole frame is one display list (background fill + the simulation quads).
197     // style-audit: opt-out a full-bleed black surface with nothing standing on it
198     fn display_list(&mut self, size: LogicalSize, _scale: f64) -> Option<cce_ui::scene::paint::DisplayList> {
199         use cce_ui::scene::layout::Rect;
200         self.width = size.width as u32;
201         self.height = size.height as u32;
202         let mut pc = cce_ui::scene::paint::PaintCtx::new();
203         let mut quads: Vec<(f32, f32, f32, f32, [f32; 4])> = Vec::new();
204 
205         // Clear screen background
206         quads.push((0.0, 0.0, self.width as f32, self.height as f32, [0.0, 0.0, 0.0, 1.0]));
207 
208         match self.style {
209             ScreensaverStyle::Starfield => {
210                 let half_w = self.width as f32 / 2.0;
211                 let half_h = self.height as f32 / 2.0;
212                 let max_dim = half_w.max(half_h);
213 
214                 for star in &self.stars {
215                     // Project NDC style coordinates to screen space
216                     let px = half_w + (star.x / star.z) * max_dim;
217                     let py = half_h + (star.y / star.z) * max_dim;
218 
219                     // Only draw if inside screen bounds
220                     if px >= 0.0 && px < self.width as f32 && py >= 0.0 && py < self.height as f32 {
221                         // Star grows larger as it gets closer
222                         let size = (2.0 / star.z).clamp(1.0, 8.0);
223                         
224                         // Fade in stars as they move out from the dark center
225                         let alpha = ((1.0 - star.z) * 1.5).clamp(0.0, 1.0);
226                         let mut color = star.color;
227                         color[3] = alpha;
228 
229                         quads.push((px - size / 2.0, py - size / 2.0, size, size, color));
230                     }
231                 }
232             }
233             ScreensaverStyle::Matrix => {
234                 let cell_w = 16.0f32;
235                 let cell_h = 20.0f32;
236 
237                 for col in &self.matrix_columns {
238                     // Draw each cell in the tail
239                     for i in 0..col.len {
240                         let cell_y = col.y - (i as f32 * cell_h);
241                         if cell_y >= 0.0 && cell_y < self.height as f32 {
242                             // Head cell is bright white/green, trailing cells fade to dark green
243                             let color = if i == 0 {
244                                 [0.85, 1.0, 0.85, 1.0]
245                             } else {
246                                 let fade = 1.0 - (i as f32 / col.len as f32);
247                                 [0.0, 0.7 * fade, 0.0, fade]
248                             };
249                             quads.push((col.x + 2.0, cell_y, cell_w - 4.0, cell_h - 4.0, color));
250                         }
251                     }
252                 }
253             }
254             ScreensaverStyle::Blank => {}
255         }
256 
257         for (qx, qy, qw, qh, qc) in quads {
258             pc.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
259         }
260         Some(pc.finish())
261     }
262 
263     fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool) {
264         if std::time::Instant::now() < self.grace_until {
265             // Keep track of first position during grace period to measure movement distance
266             if self.init_cursor.is_none() {
267                 self.init_cursor = Some(pos);
268             }
269             return;
270         }
271 
272         if let Some(init) = self.init_cursor {
273             let dx = (pos.x - init.x).abs();
274             let dy = (pos.y - init.y).abs();
275             if dx > 10.0 || dy > 10.0 {
276                 // Moved significantly after grace period, trigger exit
277                 *needs_rebuild = true;
278                 std::process::exit(0);
279             }
280         } else {
281             *needs_rebuild = true;
282             std::process::exit(0);
283         }
284     }
285 
286     fn handle_mouse_input(&mut self, _button: MouseButton, state: ElementState, _pos: LogicalPosition, _needs_rebuild: &mut bool) -> Option<Self::Message> {
287         if state == ElementState::Pressed {
288             std::process::exit(0);
289         }
290         None
291     }
292 
293     fn handle_mouse_wheel(&mut self, _delta: &MouseScrollDelta, _pos: LogicalPosition, _needs_rebuild: &mut bool) {
294         std::process::exit(0);
295     }
296 
297     fn handle_key_input(&mut self, event: &KeyEvent, _needs_rebuild: &mut bool) -> Option<Self::Message> {
298         if event.state == ElementState::Pressed {
299             std::process::exit(0);
300         }
301         None
302     }
303 }
304 
305 fn main() {
306     let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
307     let _guard = rt.enter();
308 
309     cce_ui::engine::run::<ScreensaverApp>();
310 }