bevy_simple_screenshot 0.1.2

A plug-and-play screenshot library for Bevy 0.17+ with ring-buffered capture and automatic saving
//! Ring buffer implementation for storing captured screenshots.

use bevy::prelude::*;
use chrono::{DateTime, Local};
use std::collections::{HashMap, VecDeque};

use crate::capture::CropRegion;
use crate::config::ScreenshotConfig;

/// A captured screenshot with metadata.
#[derive(Clone)]
pub struct CapturedScreenshot {
    /// The raw image data.
    pub image: Image,

    /// Capture timestamp.
    pub timestamp: DateTime<Local>,

    /// User-provided description.
    pub description: String,

    /// The key this screenshot belongs to.
    pub key: String,

    /// The frame number when the screenshot was captured.
    /// This is the render frame (1 behind game thread).
    pub frame_number: u32,

    /// Optional crop region to extract from the full screenshot.
    pub crop_region: Option<CropRegion>,
}

/// Result of validating a screenshot image.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImageValidation {
    /// The image is valid and can be saved.
    Valid,
    /// The image has zero dimensions.
    ZeroDimensions { width: u32, height: u32 },
    /// The image has no data.
    EmptyData,
    /// The image data size doesn't match dimensions.
    DataSizeMismatch {
        expected: usize,
        actual: usize,
        width: u32,
        height: u32,
    },
}

impl CapturedScreenshot {
    /// Validate that the screenshot image data is valid for saving.
    pub fn validate(&self) -> ImageValidation {
        let width = self.image.width();
        let height = self.image.height();

        if width == 0 || height == 0 {
            return ImageValidation::ZeroDimensions { width, height };
        }

        let data = match &self.image.data {
            Some(d) if !d.is_empty() => d,
            _ => return ImageValidation::EmptyData,
        };

        let expected_size = (width * height * 4) as usize;
        let actual_size = data.len();
        if actual_size != expected_size {
            return ImageValidation::DataSizeMismatch {
                expected: expected_size,
                actual: actual_size,
                width,
                height,
            };
        }

        ImageValidation::Valid
    }

    /// Returns true if the screenshot image is valid for saving.
    pub fn is_valid(&self) -> bool {
        self.validate() == ImageValidation::Valid
    }
}

/// A ring buffer that stores screenshots for a specific key.
pub struct ScreenshotRingBuffer {
    buffer: VecDeque<CapturedScreenshot>,
    capacity: usize,
}

impl ScreenshotRingBuffer {
    /// Create a new ring buffer with the specified capacity.
    pub fn new(capacity: usize) -> Self {
        Self {
            buffer: VecDeque::with_capacity(capacity),
            capacity,
        }
    }

    /// Push a new screenshot, discarding the oldest if at capacity.
    /// Returns the evicted screenshot if one was removed.
    pub fn push(&mut self, screenshot: CapturedScreenshot) -> Option<CapturedScreenshot> {
        let evicted = if self.buffer.len() >= self.capacity {
            self.buffer.pop_front()
        } else {
            None
        };
        self.buffer.push_back(screenshot);
        evicted
    }

    /// Returns the number of screenshots in the buffer.
    pub fn len(&self) -> usize {
        self.buffer.len()
    }

    /// Returns true if the buffer is empty.
    pub fn is_empty(&self) -> bool {
        self.buffer.is_empty()
    }

    /// Returns the capacity of the buffer.
    pub fn capacity(&self) -> usize {
        self.capacity
    }

    /// Iterate over all screenshots in the buffer.
    pub fn iter(&self) -> impl Iterator<Item = &CapturedScreenshot> {
        self.buffer.iter()
    }

    /// Drain all screenshots from the buffer.
    pub fn drain(&mut self) -> impl Iterator<Item = CapturedScreenshot> + '_ {
        self.buffer.drain(..)
    }

    /// Clear all screenshots from the buffer.
    pub fn clear(&mut self) {
        self.buffer.clear();
    }

    /// Get the most recent screenshot.
    pub fn latest(&self) -> Option<&CapturedScreenshot> {
        self.buffer.back()
    }
}

/// Resource that manages screenshot buffers for different keys.
#[derive(Resource)]
pub struct ScreenshotBufferManager {
    buffers: HashMap<String, ScreenshotRingBuffer>,
    config: ScreenshotConfig,
}

impl ScreenshotBufferManager {
    /// Create a new buffer manager with the given configuration.
    pub fn new(config: ScreenshotConfig) -> Self {
        Self {
            buffers: HashMap::new(),
            config,
        }
    }

    /// Get or create a buffer for the specified key.
    pub fn get_or_create_buffer(&mut self, key: &str) -> &mut ScreenshotRingBuffer {
        let capacity = self
            .config
            .keys
            .get(key)
            .and_then(|k| k.buffer_capacity)
            .unwrap_or(self.config.buffer_capacity);

        self.buffers
            .entry(key.to_string())
            .or_insert_with(|| ScreenshotRingBuffer::new(capacity))
    }

    /// Push a screenshot to its corresponding buffer.
    /// Returns any evicted screenshot.
    pub fn push(&mut self, screenshot: CapturedScreenshot) -> Option<CapturedScreenshot> {
        let key = screenshot.key.clone();
        self.get_or_create_buffer(&key).push(screenshot)
    }

    /// Get a reference to the configuration.
    pub fn config(&self) -> &ScreenshotConfig {
        &self.config
    }

    /// Get a buffer by key.
    pub fn get_buffer(&self, key: &str) -> Option<&ScreenshotRingBuffer> {
        self.buffers.get(key)
    }

    /// Get a mutable buffer by key.
    pub fn get_buffer_mut(&mut self, key: &str) -> Option<&mut ScreenshotRingBuffer> {
        self.buffers.get_mut(key)
    }

    /// Iterate over all buffer keys.
    pub fn keys(&self) -> impl Iterator<Item = &String> {
        self.buffers.keys()
    }

    /// Get the total number of screenshots across all buffers.
    pub fn total_screenshots(&self) -> usize {
        self.buffers.values().map(|b| b.len()).sum()
    }
}