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 }