Skip to main content

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}