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

src/main.rs (10.9K)

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