use crate::host_bridge::file_payloads::{import_image_file_payload, import_image_mime_type}; use crate::shell::events::host_bridge_event_script; use crate::shell::lifecycle::log_desktop_host_event_result; use serde_json::Value; use std::path::PathBuf; use tauri::{DragDropEvent, WebviewWindow, WindowEvent}; fn first_valid_desktop_image_drop_payload( paths: &[PathBuf], position: (i32, i32), ) -> Option { paths.iter().find_map(|path| { if !path.is_file() || import_image_mime_type(path).is_none() { return None; } match import_image_file_payload(path.clone(), "dropped", Some(position)) { Ok(payload) => Some(payload), Err(_error) => { log_desktop_image_drop_payload_failure(); None } } }) } fn log_desktop_image_drop_payload_failure() -> bool { eprintln!("desktop host event failed for file.imageDropped.payload"); false } fn normalize_desktop_drop_position(x: f64, y: f64) -> (i32, i32) { (x.round().max(0.0) as i32, y.round().max(0.0) as i32) } fn emit_desktop_image_drop_event( window: &WebviewWindow, paths: &[PathBuf], position: (i32, i32), ) -> tauri::Result<()> { let Some(payload) = first_valid_desktop_image_drop_payload(paths, position) else { return Ok(()); }; let script = host_bridge_event_script("file.imageDropped", payload).map_err(tauri::Error::Json)?; window.eval(script) } pub(crate) fn register_desktop_file_drop_events(window: &WebviewWindow) { let drop_window = window.clone(); window.on_window_event(move |event| { if let WindowEvent::DragDrop(DragDropEvent::Drop { paths, position }) = event { let drop_position = normalize_desktop_drop_position(position.x, position.y); log_desktop_host_event_result( "file.imageDropped", emit_desktop_image_drop_event(&drop_window, paths, drop_position), ); } }); } #[cfg(test)] mod tests { use super::*; use std::fs; fn png_bytes() -> Vec { vec![0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 0] } #[test] fn image_drop_skips_disguised_image_before_valid_image() { let invalid_path = std::env::temp_dir().join(format!( "genarrative-desktop-drop-invalid-{}.png", std::process::id() )); let valid_path = std::env::temp_dir().join(format!( "genarrative-desktop-drop-valid-{}.png", std::process::id() )); fs::write(&invalid_path, b"text").expect("write invalid drop image"); fs::write(&valid_path, png_bytes()).expect("write valid drop image"); let payload = first_valid_desktop_image_drop_payload( &[invalid_path.clone(), valid_path.clone()], (7, 9), ) .expect("valid image drop payload"); assert_eq!( payload["fileName"], valid_path.file_name().unwrap().to_str().unwrap() ); assert_eq!(payload["position"], serde_json::json!({ "x": 7, "y": 9 })); fs::remove_file(invalid_path).expect("remove invalid drop image"); fs::remove_file(valid_path).expect("remove valid drop image"); } #[test] fn image_drop_logs_invalid_candidate_then_uses_next_valid_image() { let disguised_path = std::env::temp_dir().join(format!( "genarrative-desktop-drop-disguised-{}.png", std::process::id() )); let valid_path = std::env::temp_dir().join(format!( "genarrative-desktop-drop-valid-after-error-{}.png", std::process::id() )); fs::write(&disguised_path, b"text").expect("write disguised drop image"); fs::write(&valid_path, png_bytes()).expect("write valid drop image"); let payload = first_valid_desktop_image_drop_payload( &[disguised_path.clone(), valid_path.clone()], (11, 13), ) .expect("valid image drop payload after invalid candidate"); assert_eq!( payload["fileName"], valid_path.file_name().unwrap().to_str().unwrap() ); assert_eq!(payload["position"], serde_json::json!({ "x": 11, "y": 13 })); fs::remove_file(disguised_path).expect("remove disguised drop image"); fs::remove_file(valid_path).expect("remove valid drop image"); } #[test] fn image_drop_payload_failure_reports_failure() { assert!(!log_desktop_image_drop_payload_failure()); } #[test] fn image_drop_payload_failure_logs_stable_label_only() { assert!(!log_desktop_image_drop_payload_failure()); } #[test] fn image_drop_without_valid_image_does_not_emit_payload() { let text_path = std::env::temp_dir().join(format!( "genarrative-desktop-drop-text-{}.txt", std::process::id() )); let directory_path = std::env::temp_dir().join(format!( "genarrative-desktop-drop-directory-{}", std::process::id() )); fs::write(&text_path, b"not an image").expect("write text drop file"); fs::create_dir_all(&directory_path).expect("create drop directory"); let payload = first_valid_desktop_image_drop_payload( &[directory_path.clone(), text_path.clone()], (3, 5), ); assert_eq!(payload, None); fs::remove_file(text_path).expect("remove text drop file"); fs::remove_dir(directory_path).expect("remove drop directory"); } #[test] fn desktop_drop_position_is_rounded_and_clamped_to_window_bounds() { assert_eq!(normalize_desktop_drop_position(12.4, 24.6), (12, 25)); assert_eq!(normalize_desktop_drop_position(-4.2, -0.6), (0, 0)); } }