1use std::cell::{Cell, RefCell};
62use std::fmt;
63use std::rc::Rc;
64
65#[derive(Clone)]
67pub struct ScreenshotHandle {
68 pub request: Rc<Cell<bool>>,
72 pub image: Rc<RefCell<Option<(Vec<u8>, u32, u32)>>>,
75}
76
77impl ScreenshotHandle {
78 pub fn new() -> Self {
79 Self {
80 request: Rc::new(Cell::new(false)),
81 image: Rc::new(RefCell::new(None)),
82 }
83 }
84
85 pub fn take(&self) {
87 self.request.set(true);
88 }
89
90 pub fn pending(&self) -> bool {
92 self.request.get()
93 }
94
95 pub fn has_image(&self) -> bool {
97 self.image.borrow().is_some()
98 }
99}
100
101impl Default for ScreenshotHandle {
102 fn default() -> Self {
103 Self::new()
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
111pub enum ScreenshotExportOutcome {
112 Saved(std::path::PathBuf),
114 Started,
116}
117
118#[derive(Debug)]
120pub enum ScreenshotExportError {
121 InvalidBuffer { expected: usize, actual: usize },
122 Encode(String),
123 Io(std::io::Error),
124 Clipboard(String),
125 Unsupported(&'static str),
126}
127
128impl fmt::Display for ScreenshotExportError {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 match self {
131 Self::InvalidBuffer { expected, actual } => {
132 write!(
133 f,
134 "invalid RGBA buffer: expected {expected} bytes, got {actual}"
135 )
136 }
137 Self::Encode(msg) => write!(f, "PNG encode failed: {msg}"),
138 Self::Io(err) => write!(f, "I/O failed: {err}"),
139 Self::Clipboard(msg) => write!(f, "clipboard failed: {msg}"),
140 Self::Unsupported(msg) => write!(f, "unsupported screenshot export: {msg}"),
141 }
142 }
143}
144
145impl std::error::Error for ScreenshotExportError {}
146
147impl From<std::io::Error> for ScreenshotExportError {
148 fn from(err: std::io::Error) -> Self {
149 Self::Io(err)
150 }
151}
152
153fn validate_rgba_len(rgba: &[u8], width: u32, height: u32) -> Result<(), ScreenshotExportError> {
154 let expected = (width as usize)
155 .checked_mul(height as usize)
156 .and_then(|px| px.checked_mul(4))
157 .ok_or_else(|| ScreenshotExportError::Encode("image dimensions overflow".to_string()))?;
158 if rgba.len() != expected {
159 return Err(ScreenshotExportError::InvalidBuffer {
160 expected,
161 actual: rgba.len(),
162 });
163 }
164 Ok(())
165}
166
167pub fn encode_png_rgba(
169 rgba: &[u8],
170 width: u32,
171 height: u32,
172) -> Result<Vec<u8>, ScreenshotExportError> {
173 validate_rgba_len(rgba, width, height)?;
174
175 let mut out = Vec::with_capacity(rgba.len() / 2);
176 {
177 let mut encoder = png::Encoder::new(&mut out, width, height);
178 encoder.set_color(png::ColorType::Rgba);
179 encoder.set_depth(png::BitDepth::Eight);
180 let mut writer = encoder
181 .write_header()
182 .map_err(|e| ScreenshotExportError::Encode(e.to_string()))?;
183 writer
184 .write_image_data(rgba)
185 .map_err(|e| ScreenshotExportError::Encode(e.to_string()))?;
186 }
187 Ok(out)
188}
189
190pub fn download_rgba_as_png(
192 rgba: &[u8],
193 width: u32,
194 height: u32,
195 filename: &str,
196) -> Result<ScreenshotExportOutcome, ScreenshotExportError> {
197 let png = encode_png_rgba(rgba, width, height)?;
198 download_png(filename, &png)
199}
200
201pub fn copy_rgba_to_clipboard(
203 rgba: &[u8],
204 width: u32,
205 height: u32,
206) -> Result<ScreenshotExportOutcome, ScreenshotExportError> {
207 validate_rgba_len(rgba, width, height)?;
208 copy_rgba_to_clipboard_impl(rgba, width, height)
209}
210
211#[cfg(not(target_arch = "wasm32"))]
212fn download_png(
213 filename: &str,
214 png: &[u8],
215) -> Result<ScreenshotExportOutcome, ScreenshotExportError> {
216 let dir = downloads_dir();
217 std::fs::create_dir_all(&dir)?;
218 let path = unique_download_path(&dir, filename);
219 std::fs::write(&path, png)?;
220 Ok(ScreenshotExportOutcome::Saved(path))
221}
222
223#[cfg(not(target_arch = "wasm32"))]
224fn downloads_dir() -> std::path::PathBuf {
225 #[cfg(target_os = "windows")]
226 {
227 if let Some(profile) = std::env::var_os("USERPROFILE") {
228 return std::path::PathBuf::from(profile).join("Downloads");
229 }
230 }
231 #[cfg(not(target_os = "windows"))]
232 {
233 if let Some(home) = std::env::var_os("HOME") {
234 return std::path::PathBuf::from(home).join("Downloads");
235 }
236 }
237 std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))
238}
239
240#[cfg(not(target_arch = "wasm32"))]
241fn unique_download_path(dir: &std::path::Path, filename: &str) -> std::path::PathBuf {
242 let candidate = dir.join(filename);
243 if !candidate.exists() {
244 return candidate;
245 }
246
247 let path = std::path::Path::new(filename);
248 let stem = path
249 .file_stem()
250 .and_then(|s| s.to_str())
251 .unwrap_or("screenshot");
252 let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("png");
253 for i in 1.. {
254 let name = format!("{stem}-{i}.{ext}");
255 let candidate = dir.join(name);
256 if !candidate.exists() {
257 return candidate;
258 }
259 }
260 unreachable!("unbounded integer iterator should always produce a path")
261}
262
263#[cfg(all(not(target_arch = "wasm32"), feature = "clipboard"))]
264fn copy_rgba_to_clipboard_impl(
265 rgba: &[u8],
266 width: u32,
267 height: u32,
268) -> Result<ScreenshotExportOutcome, ScreenshotExportError> {
269 let image = arboard::ImageData {
270 width: width as usize,
271 height: height as usize,
272 bytes: std::borrow::Cow::Borrowed(rgba),
273 };
274 arboard::Clipboard::new()
275 .and_then(|mut clipboard| clipboard.set_image(image))
276 .map_err(|e| ScreenshotExportError::Clipboard(e.to_string()))?;
277 Ok(ScreenshotExportOutcome::Started)
278}
279
280#[cfg(all(not(target_arch = "wasm32"), not(feature = "clipboard")))]
281fn copy_rgba_to_clipboard_impl(
282 _: &[u8],
283 _: u32,
284 _: u32,
285) -> Result<ScreenshotExportOutcome, ScreenshotExportError> {
286 Err(ScreenshotExportError::Unsupported(
287 "enable the `clipboard` feature for native image clipboard support",
288 ))
289}
290
291#[cfg(target_arch = "wasm32")]
292fn download_png(
293 filename: &str,
294 png: &[u8],
295) -> Result<ScreenshotExportOutcome, ScreenshotExportError> {
296 if wasm_download_png(filename, png) {
297 Ok(ScreenshotExportOutcome::Started)
298 } else {
299 Err(ScreenshotExportError::Unsupported(
300 "browser download API is unavailable",
301 ))
302 }
303}
304
305#[cfg(target_arch = "wasm32")]
306fn copy_rgba_to_clipboard_impl(
307 rgba: &[u8],
308 width: u32,
309 height: u32,
310) -> Result<ScreenshotExportOutcome, ScreenshotExportError> {
311 let png = encode_png_rgba(rgba, width, height)?;
312 if wasm_copy_png_to_clipboard(&png) {
313 Ok(ScreenshotExportOutcome::Started)
314 } else {
315 Err(ScreenshotExportError::Unsupported(
316 "browser image clipboard API is unavailable",
317 ))
318 }
319}
320
321#[cfg(target_arch = "wasm32")]
322#[wasm_bindgen::prelude::wasm_bindgen(inline_js = r#"
323export function wasm_download_png(filename, bytes) {
324 try {
325 const blob = new Blob([bytes], { type: "image/png" });
326 const url = URL.createObjectURL(blob);
327 const a = document.createElement("a");
328 a.href = url;
329 a.download = filename || "agg-gui-screenshot.png";
330 a.style.display = "none";
331 document.body.appendChild(a);
332 a.click();
333 a.remove();
334 URL.revokeObjectURL(url);
335 return true;
336 } catch (err) {
337 console.error("agg-gui screenshot download failed", err);
338 return false;
339 }
340}
341
342export function wasm_copy_png_to_clipboard(bytes) {
343 try {
344 if (!navigator.clipboard || typeof ClipboardItem === "undefined") {
345 return false;
346 }
347 const blob = new Blob([bytes], { type: "image/png" });
348 navigator.clipboard
349 .write([new ClipboardItem({ "image/png": blob })])
350 .catch(err => console.error("agg-gui screenshot clipboard failed", err));
351 return true;
352 } catch (err) {
353 console.error("agg-gui screenshot clipboard failed", err);
354 return false;
355 }
356}
357"#)]
358extern "C" {
359 fn wasm_download_png(filename: &str, bytes: &[u8]) -> bool;
360 fn wasm_copy_png_to_clipboard(bytes: &[u8]) -> bool;
361}
362
363pub fn run_frame_with_capture<C>(
402 request: &Rc<Cell<bool>>,
403 capturing: &Rc<Cell<bool>>,
404 image: &Rc<RefCell<Option<(std::sync::Arc<Vec<u8>>, u32, u32)>>>,
405 ctx: &mut C,
406 mut render_fn: impl FnMut(&mut C),
407 read_back_buffer: impl FnOnce(&mut C) -> (Vec<u8>, u32, u32),
408) {
409 if !request.get() {
410 render_fn(ctx);
411 return;
412 }
413 capturing.set(true);
414 render_fn(ctx);
415 let (rgba, w, h) = read_back_buffer(ctx);
416 *image.borrow_mut() = Some((std::sync::Arc::new(rgba), w, h));
417 capturing.set(false);
418 request.set(false);
419 render_fn(ctx);
420}