agg_gui/screenshot.rs
1//! Screenshot capture handle for agg-gui apps.
2//!
3//! The GL rendering harness (`GlGfxCtx::read_screenshot` on the desktop GL
4//! path + the equivalent WebGL2 read-back in the WASM harness) produces a
5//! top-down RGBA8 buffer of the current back buffer. This module supplies
6//! the small shared-state handle that a button or hotkey uses to
7//! **request** a capture and that a widget uses to **display** the result.
8//!
9//! # Threading / ownership
10//!
11//! All fields are `Rc<...>` — single-threaded, cheap to clone. Never
12//! transfer a [`ScreenshotHandle`] across threads.
13//!
14//! # Wiring on native (winit + glow)
15//!
16//! ```ignore
17//! let shot = agg_gui::ScreenshotHandle::new();
18//!
19//! // In a button's on_click:
20//! let req = shot.request.clone();
21//! Button::new("📷 Capture", font).on_click(move || req.set(true))
22//!
23//! // In the event loop, AFTER render_frame but BEFORE swap_buffers:
24//! if shot.request.get() {
25//! let (rgba, w, h) = gl_ctx.read_screenshot();
26//! *shot.image.borrow_mut() = Some((rgba, w, h));
27//! shot.request.set(false);
28//! }
29//!
30//! // Display: pass `shot.image` to `ImageView`.
31//! ```
32//!
33//! # Wiring on WASM
34//!
35//! Same Rust-side flow — the browser's WebGL2 context still provides
36//! `glReadPixels`, so `GlGfxCtx::read_screenshot()` works unchanged. The
37//! JS side needs no special code beyond driving the animation loop:
38//!
39//! ```ignore
40//! // In the WASM render export (called from JS requestAnimationFrame):
41//! if shot.request.get() {
42//! let (rgba, w, h) = gl_ctx.read_screenshot(); // must be BEFORE presenting
43//! *shot.image.borrow_mut() = Some((rgba, w, h));
44//! shot.request.set(false);
45//! }
46//! ```
47//!
48//! Note for the LLM / future dev: on WASM, `read_screenshot` MUST be called
49//! before the browser composites the canvas (i.e. within the same rAF
50//! tick, before yielding). Because WebGL uses a preserved-drawing-buffer
51//! only when explicitly requested, calling it outside that window yields
52//! a blank image. The natural "after paint, before yield" position in the
53//! render function is correct.
54//!
55//! If the app wants to TRIGGER a browser download instead of displaying
56//! in-canvas, export a WASM function that calls `read_screenshot`, encode
57//! with the `png` crate via `agg_gui::encode_png_rgba` (if available in
58//! the surrounding app), and pass the bytes to a JS helper that creates a
59//! `Blob` + `URL.createObjectURL` + synthetic `<a download>` click.
60
61use std::cell::{Cell, RefCell};
62use std::rc::Rc;
63
64/// Shared capture state. Clone freely; all inner fields are `Rc<...>`.
65#[derive(Clone)]
66pub struct ScreenshotHandle {
67 /// Set to `true` to request a capture on the next rendered frame. The
68 /// platform harness reads this cell after painting, captures the
69 /// framebuffer into `image`, and clears the flag.
70 pub request: Rc<Cell<bool>>,
71 /// Most recent captured image — top-down RGBA8, plus `(width, height)`.
72 /// `None` until the first capture completes.
73 pub image: Rc<RefCell<Option<(Vec<u8>, u32, u32)>>>,
74}
75
76impl ScreenshotHandle {
77 pub fn new() -> Self {
78 Self {
79 request: Rc::new(Cell::new(false)),
80 image: Rc::new(RefCell::new(None)),
81 }
82 }
83
84 /// Convenience: request a capture. Equivalent to `self.request.set(true)`.
85 pub fn take(&self) { self.request.set(true); }
86
87 /// `true` while the latest request has not yet been fulfilled.
88 pub fn pending(&self) -> bool { self.request.get() }
89
90 /// Access the most recent capture without consuming it.
91 pub fn has_image(&self) -> bool { self.image.borrow().is_some() }
92}
93
94impl Default for ScreenshotHandle {
95 fn default() -> Self { Self::new() }
96}
97
98// ─── Capture-aware render orchestration ─────────────────────────────────
99//
100// Both the native (winit/glutin) and wasm (rAF/WebGL2) harnesses need the
101// same "screenshot capture" flow around their per-frame render:
102//
103// 1. If a capture was requested:
104// a. Flip `capturing` to true so the Screenshot preview pane
105// paints empty (so captured pixels don't include last frame's
106// preview — the hall-of-mirrors bug).
107// b. Render the frame (platform-specific: clear + paint widgets).
108// c. `glReadPixels` the back buffer (platform-specific).
109// d. Publish the bytes into `image` and clear both flags.
110// e. Render again — this time the preview pane reveals the
111// freshly-captured image.
112// 2. Otherwise: render once.
113//
114// The orchestration (flag flipping, double-render, Arc wrap) is
115// platform-agnostic and belongs here; each host supplies two closures:
116// - `render_fn()` : clear the framebuffer and paint the widget
117// tree once (the host's existing frame path).
118// - `read_back_buffer()` : glReadPixels the current framebuffer and
119// return `(rgba, width, height)`.
120
121/// Run one frame through the screenshot capture flow.
122///
123/// Call this instead of invoking the per-frame render directly. It runs
124/// the single-render path in the common case and the double-render
125/// capture path when `request` is set.
126///
127/// `ctx` is the host's rendering context (e.g. the GL `GlGfxCtx`) —
128/// passed in once and handed through to each closure so the two
129/// closures don't both borrow it from their capture environment (which
130/// the borrow checker can't reconcile statically even though the
131/// closures are invoked sequentially).
132///
133/// The `image` field uses the `Arc<Vec<u8>>` form so the GL back-end's
134/// texture cache can key on the Arc's pointer identity — see
135/// `gfx_ctx::draw_image_rgba_arc`.
136pub fn run_frame_with_capture<C>(
137 request: &Rc<Cell<bool>>,
138 capturing: &Rc<Cell<bool>>,
139 image: &Rc<RefCell<Option<(std::sync::Arc<Vec<u8>>, u32, u32)>>>,
140 ctx: &mut C,
141 mut render_fn: impl FnMut(&mut C),
142 read_back_buffer: impl FnOnce(&mut C) -> (Vec<u8>, u32, u32),
143) {
144 if !request.get() {
145 render_fn(ctx);
146 return;
147 }
148 capturing.set(true);
149 render_fn(ctx);
150 let (rgba, w, h) = read_back_buffer(ctx);
151 *image.borrow_mut() = Some((std::sync::Arc::new(rgba), w, h));
152 capturing.set(false);
153 request.set(false);
154 render_fn(ctx);
155}