egui_screensaver_matrix/lib.rs
1//! Matrix digital rain screensaver for [egui](https://github.com/emilk/egui),
2//! ported from [Rezmason's Matrix](https://github.com/Rezmason/matrix)
3//! WebGL original.
4//!
5//! Like the original, this runs the whole simulation on the GPU: ping-pong
6//! "compute" render targets drive the falling-glyph animation, an MSDF
7//! font atlas renders the glyphs, and a bloom pass plus a palette/stripe
8//! color-effect pass finish the look — all through an
9//! [`egui::PaintCallback`] via [`egui_glow`]. **This requires the host
10//! `eframe::App` to use the `glow` rendering backend** (not `wgpu`) —
11//! [`MatrixBackground::paint`] simply skips drawing if no `glow::Context`
12//! is available. Volumetric 3D mode is not implemented yet.
13//!
14//! # Usage
15//!
16//! ```rust,no_run
17//! use egui_screensaver_matrix::MatrixBackground;
18//!
19//! struct MyApp {
20//! matrix: MatrixBackground,
21//! }
22//!
23//! impl eframe::App for MyApp {
24//! fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
25//! let ctx = ui.ctx().clone();
26//! // Call paint once per frame before drawing any UI windows so the
27//! // screensaver sits on the background layer behind everything else.
28//! self.matrix.paint(&ctx, frame.gl());
29//! }
30//! }
31//! ```
32
33mod camera;
34mod config;
35pub mod fonts;
36mod geometry;
37mod gl_util;
38mod passes;
39pub mod textures;
40
41use std::cell::RefCell;
42use std::sync::Arc;
43use std::time::Duration;
44
45use egui::{Color32, Context, LayerId, Painter, Shape};
46use egui_glow::CallbackFn;
47
48pub use config::{Effect, MatrixConfig, PaletteStop, Preset, RippleType};
49pub use fonts::FontId;
50pub use textures::DecorativeTexture;
51
52const REPAINT_MS: u64 = 33;
53
54/// Matrix screensaver state.
55///
56/// Create one instance (e.g. as a field of your `eframe::App` struct) and
57/// call [`MatrixBackground::paint`] every frame from your `ui` method.
58pub struct MatrixBackground {
59 /// Every tunable parameter (rain speed, brightness, glyph shape, ...).
60 /// See [`MatrixConfig`]'s field docs.
61 pub config: MatrixConfig,
62 time: f32,
63 last_time: Option<f64>,
64 passes: Option<Arc<PassesHandle>>,
65}
66
67/// Wraps [`passes::Passes`] together with the `glow::Context` needed to
68/// tear it down, mirroring `egui-screensaver-flame`'s `GlResources`. The
69/// `RefCell` gives `Passes::run` (which needs `&mut self` to update its
70/// ping-pong buffers) interior mutability despite only being reachable
71/// through a shared `Arc` from inside the paint callback — sound because,
72/// like [`AssertSendSync`] below, this only ever runs single-threaded.
73struct PassesHandle {
74 gl: Arc<glow::Context>,
75 passes: RefCell<passes::Passes>,
76}
77
78impl Drop for PassesHandle {
79 fn drop(&mut self) {
80 unsafe {
81 self.passes.borrow().destroy(&self.gl);
82 }
83 }
84}
85
86impl Default for MatrixBackground {
87 fn default() -> Self {
88 Self {
89 config: MatrixConfig::default(),
90 time: 0.0,
91 last_time: None,
92 passes: None,
93 }
94 }
95}
96
97impl MatrixBackground {
98 /// Paint the screensaver onto the egui background layer for this frame.
99 ///
100 /// Call this once per frame **before** drawing any UI panels or windows
101 /// so the animation appears behind all other content. `gl` should come
102 /// from `eframe::Frame::gl()` — if it's `None` (e.g. the host app uses
103 /// the `wgpu` backend instead of `glow`), this draws nothing but a black
104 /// background.
105 pub fn paint(&mut self, ctx: &Context, gl: Option<&Arc<glow::Context>>) {
106 ctx.request_repaint_after(Duration::from_millis(REPAINT_MS));
107
108 let now = ctx.input(|i| i.time);
109 let dt = match self.last_time {
110 Some(last) => (now - last).clamp(0.0, 0.1) as f32,
111 None => 0.0,
112 };
113 self.last_time = Some(now);
114 // Animation speed is applied inside the shaders themselves
115 // (`simTime = time * animationSpeed`), so this stays a plain
116 // unscaled clock — applying `animation_speed` here too would
117 // double it up.
118 self.time += dt;
119
120 if self.passes.is_none()
121 && let Some(gl) = gl
122 {
123 // Constructed at a placeholder 1x1 size — `Passes::run`'s
124 // resize check fixes this up to the real screen size before
125 // the first frame is drawn (real pixel dimensions are only
126 // available from `PaintCallbackInfo::screen_size_px`, later).
127 match unsafe { passes::Passes::new(gl, &self.config, 1, 1) } {
128 // On wasm32, `Passes` (via `glow::Context`) isn't `Sync` —
129 // see `AssertSendSync` below, applied where this `Arc` is
130 // captured into the `Fn + Sync + Send` paint callback.
131 #[allow(clippy::arc_with_non_send_sync)]
132 Ok(passes) => {
133 self.passes = Some(Arc::new(PassesHandle {
134 gl: gl.clone(),
135 passes: RefCell::new(passes),
136 }))
137 }
138 Err(err) => log::error!("egui-screensaver-matrix: GL setup failed: {err}"),
139 }
140 }
141
142 let screen_rect = ctx.viewport_rect();
143 let painter = Painter::new(ctx.clone(), LayerId::background(), screen_rect);
144 painter.rect_filled(screen_rect, 0.0, Color32::BLACK);
145
146 if let Some(handle) = self.passes.clone() {
147 let time = self.time;
148 let config = self.config.clone();
149 let handle = AssertSendSync(handle);
150 painter.add(Shape::Callback(egui::PaintCallback {
151 rect: screen_rect,
152 callback: Arc::new(CallbackFn::new(move |info, _painter| {
153 let handle = handle.get();
154 unsafe {
155 handle.passes.borrow_mut().run(
156 &handle.gl,
157 &config,
158 time,
159 info.screen_size_px,
160 );
161 }
162 })),
163 }));
164 }
165 }
166}
167
168/// `CallbackFn` requires `Fn + Sync + Send` on every target, but on wasm32
169/// (WebGL) `glow::Context` isn't `Sync` — it wraps `web_sys` handles behind
170/// `RefCell`s, since JS objects aren't natively thread-safe. `wasm32`
171/// without the `atomics` target feature (which eframe doesn't enable) never
172/// actually runs on more than one thread, so this assertion is honest: the
173/// wrapped value never crosses a real thread boundary on any target.
174struct AssertSendSync<T>(T);
175unsafe impl<T> Send for AssertSendSync<T> {}
176unsafe impl<T> Sync for AssertSendSync<T> {}
177
178impl<T> AssertSendSync<T> {
179 /// A method (rather than direct `.0` field access) so closures capture
180 /// this wrapper as a whole — Rust's disjoint closure captures would
181 /// otherwise capture just the inner `T` from a bare `resources.0`,
182 /// silently defeating the wrapper.
183 fn get(&self) -> &T {
184 &self.0
185 }
186}