Skip to main content

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}