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 }