forgewright 0.2.0

Standalone UI automation — CDP for browsers, UIA for Windows desktop apps
Documentation
//! Core types for frame capture and compression.
//!
//! Inlined from the original GPU ops crate to eliminate the internal dependency.

use serde::{Serialize, Deserialize};

/// A raw frame buffer (BGRA, 8 bits per channel).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Frame {
    pub width: u32,
    pub height: u32,
    /// Row-major BGRA pixels. Length = width * height * 4.
    pub data: Vec<u8>,
}

impl Frame {
    pub fn new(width: u32, height: u32, data: Vec<u8>) -> Self {
        debug_assert_eq!(data.len(), (width as usize) * (height as usize) * 4);
        Self { width, height, data }
    }

    /// Create a zeroed frame (black).
    pub fn zeroed(width: u32, height: u32) -> Self {
        Self {
            width,
            height,
            data: vec![0u8; (width as usize) * (height as usize) * 4],
        }
    }

    /// Number of tiles in a grid of tile_size x tile_size.
    pub fn tile_count(&self, tile_size: u32) -> (u32, u32) {
        let cols = (self.width + tile_size - 1) / tile_size;
        let rows = (self.height + tile_size - 1) / tile_size;
        (cols, rows)
    }
}

/// A rectangular region within a frame.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rect {
    pub x: u32,
    pub y: u32,
    pub width: u32,
    pub height: u32,
}

/// A tile hash for change detection.
/// Each tile is tile_size x tile_size pixels.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TileHash {
    pub col: u16,
    pub row: u16,
    pub hash: u64,
}

/// Result of comparing two frames via tile hashing.
#[derive(Debug, Clone)]
pub struct DeltaResult {
    pub tile_size: u32,
    pub cols: u32,
    pub rows: u32,
    /// Indices of tiles that changed between frames.
    pub changed_indices: Vec<u32>,
    /// Hashes of the current frame's tiles.
    pub current_hashes: Vec<u64>,
}

impl DeltaResult {
    /// Fraction of tiles that changed.
    pub fn change_ratio(&self) -> f32 {
        let total = self.cols * self.rows;
        if total == 0 { return 0.0; }
        self.changed_indices.len() as f32 / total as f32
    }

    /// Changed tile coordinates.
    pub fn changed_tiles(&self) -> Vec<(u16, u16)> {
        self.changed_indices.iter().map(|&idx| {
            let col = (idx % self.cols) as u16;
            let row = (idx / self.cols) as u16;
            (col, row)
        }).collect()
    }
}

/// Compact representation of a frame delta for token output.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CompressedFrame {
    pub width: u32,
    pub height: u32,
    pub tile_size: u32,
    pub total_tiles: u32,
    pub changed_tiles: u32,
    /// Changed tile descriptors: (col, row, feature_bytes).
    pub tiles: Vec<TileDescriptor>,
}

/// A single changed tile with optional extracted features.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TileDescriptor {
    pub col: u16,
    pub row: u16,
    /// Mean color (BGRA).
    pub mean_color: [u8; 4],
    /// Edge density (0.0 = flat, 1.0 = all edges). From Sobel if available.
    pub edge_density: f32,
    /// Whether this tile likely contains text (high horizontal contrast).
    pub likely_text: bool,
}

#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;
    use proptest::collection::vec;

    // Feature: forgewright-oss, Property 1: Inlined types serde round-trip
    // **Validates: Requirements 1.4, 1.6, 4.4**

    fn arb_tile_descriptor() -> impl Strategy<Value = TileDescriptor> {
        (
            any::<u16>(),
            any::<u16>(),
            any::<[u8; 4]>(),
            (0.0f32..=1.0f32),
            any::<bool>(),
        )
            .prop_map(|(col, row, mean_color, edge_density, likely_text)| TileDescriptor {
                col,
                row,
                mean_color,
                edge_density,
                likely_text,
            })
    }

    fn arb_compressed_frame() -> impl Strategy<Value = CompressedFrame> {
        (
            any::<u32>(),
            any::<u32>(),
            any::<u32>(),
            any::<u32>(),
            any::<u32>(),
            vec(arb_tile_descriptor(), 0..16),
        )
            .prop_map(
                |(width, height, tile_size, total_tiles, changed_tiles, tiles)| CompressedFrame {
                    width,
                    height,
                    tile_size,
                    total_tiles,
                    changed_tiles,
                    tiles,
                },
            )
    }

    fn arb_frame() -> impl Strategy<Value = Frame> {
        (1u32..=64, 1u32..=64).prop_flat_map(|(w, h)| {
            let size = (w as usize) * (h as usize) * 4;
            vec(any::<u8>(), size).prop_map(move |data| Frame {
                width: w,
                height: h,
                data,
            })
        })
    }

    proptest! {
        // Feature: forgewright-oss, Property 1: Inlined types serde round-trip
        #[test]
        fn prop_compressed_frame_serde_roundtrip(cf in arb_compressed_frame()) {
            let json = serde_json::to_string(&cf).unwrap();
            let decoded: CompressedFrame = serde_json::from_str(&json).unwrap();
            prop_assert_eq!(cf, decoded);
        }

        // Feature: forgewright-oss, Property 1: Inlined types serde round-trip
        #[test]
        fn prop_frame_serde_roundtrip(frame in arb_frame()) {
            let json = serde_json::to_string(&frame).unwrap();
            let decoded: Frame = serde_json::from_str(&json).unwrap();
            prop_assert_eq!(frame, decoded);
        }

        // Feature: forgewright-oss, Property 1: Inlined types serde round-trip
        #[test]
        fn prop_tile_descriptor_serde_roundtrip(td in arb_tile_descriptor()) {
            let json = serde_json::to_string(&td).unwrap();
            let decoded: TileDescriptor = serde_json::from_str(&json).unwrap();
            prop_assert_eq!(td, decoded);
        }
    }
}