vs-engine-webkit 0.1.13

Browser engine bindings for vibesurfer.
Documentation
//! `capture` for the Cocoa backend: WKWebView snapshot →
//! NSImage → TIFF → NSBitmapImageRep → PNG → on-disk file.

use std::cell::RefCell;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::time::Duration;

use block2::RcBlock;
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2_app_kit::{NSBitmapImageFileType, NSBitmapImageRep, NSImage};
use objc2_foundation::{NSData, NSDictionary, NSError};
use objc2_web_kit::WKWebView;

use super::eval::run_loop_until;
use crate::engine::{EngineError, EngineResult, PageHandle};

pub(super) fn capture_to_png(
    web_view: &WKWebView,
    page: PageHandle,
    captures_dir: Option<&Path>,
) -> EngineResult<PathBuf> {
    let slot: Rc<RefCell<Option<Result<Retained<NSImage>, String>>>> = Rc::new(RefCell::new(None));
    let slot_for_block = slot.clone();
    let block = RcBlock::new(move |image: *mut NSImage, error: *mut NSError| {
        if !error.is_null() {
            let err = unsafe { &*error };
            *slot_for_block.borrow_mut() = Some(Err(err.localizedDescription().to_string()));
            return;
        }
        if image.is_null() {
            *slot_for_block.borrow_mut() = Some(Err("null NSImage".into()));
            return;
        }
        let img: Retained<NSImage> = unsafe { Retained::retain(image).expect("non-null NSImage") };
        *slot_for_block.borrow_mut() = Some(Ok(img));
    });

    unsafe {
        web_view.takeSnapshotWithConfiguration_completionHandler(None, &block);
    }

    let slot_check = slot.clone();
    let ok = run_loop_until(
        move || slot_check.borrow().is_some(),
        Duration::from_secs(10),
    );
    if !ok {
        return Err(EngineError::Timeout {
            budget: Duration::from_secs(10),
            primitive: "capture",
        });
    }
    let result = slot.borrow_mut().take();
    let image = match result {
        Some(Ok(img)) => img,
        Some(Err(msg)) => return Err(EngineError::Other(format!("snapshot failed: {msg}"))),
        None => unreachable!(),
    };

    // NSImage → TIFF → NSBitmapImageRep → PNG.
    let tiff = image
        .TIFFRepresentation()
        .ok_or_else(|| EngineError::Other("NSImage has no TIFF representation".into()))?;
    let bitmap = NSBitmapImageRep::imageRepWithData(&tiff)
        .ok_or_else(|| EngineError::Other("imageRepWithData returned nil".into()))?;
    let empty: Retained<NSDictionary<objc2_foundation::NSString, AnyObject>> = NSDictionary::new();
    let png_data: Retained<NSData> =
        unsafe { bitmap.representationUsingType_properties(NSBitmapImageFileType::PNG, &empty) }
            .ok_or_else(|| EngineError::Other("PNG encoding returned nil".into()))?;

    let dir = captures_dir
        .map(Path::to_path_buf)
        .unwrap_or_else(|| std::env::temp_dir().join("vibesurfer-webkit-captures"));
    std::fs::create_dir_all(&dir).map_err(|e| EngineError::Other(e.to_string()))?;
    let path = dir.join(format!(
        "wk-{}-{}.png",
        page.0,
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_millis())
            .unwrap_or(0)
    ));
    let bytes: &[u8] = unsafe { png_data.as_bytes_unchecked() };
    std::fs::write(&path, bytes).map_err(|e| EngineError::Other(e.to_string()))?;
    Ok(path)
}