use super::super::WebFile;
use super::WebEvent;
use crate::drop_validation::{file_name, media_type, parse_size, validate_file_count};
use std::io;
use wasm_bindgen::JsCast;
use web_sys::{DragEvent, File, FileList, HtmlInputElement, MouseEvent};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DroppedFile {
name: String,
media_type: String,
size_bytes: u64,
}
impl DroppedFile {
fn from_browser_file(file: &File) -> io::Result<Self> {
let size_bytes = parse_size(file.size())?;
Self::from_browser_file_with_size(file, size_bytes)
}
fn from_browser_file_with_size(file: &File, size_bytes: u64) -> io::Result<Self> {
let name = file_name(file.name())?;
let media_type = media_type(file.type_())?;
Ok(Self {
name,
media_type,
size_bytes,
})
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn media_type(&self) -> &str {
&self.media_type
}
#[must_use]
pub const fn size_bytes(&self) -> u64 {
self.size_bytes
}
}
pub struct DroppedFileAccess {
metadata: DroppedFile,
reader: WebFile,
}
impl DroppedFileAccess {
#[must_use]
pub fn metadata(&self) -> &DroppedFile {
&self.metadata
}
#[must_use]
pub fn name(&self) -> &str {
self.metadata.name()
}
#[must_use]
pub fn media_type(&self) -> &str {
self.metadata.media_type()
}
#[must_use]
pub const fn size_bytes(&self) -> u64 {
self.metadata.size_bytes()
}
pub async fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
self.reader.read(buffer).await
}
#[must_use]
pub const fn position(&self) -> u64 {
self.reader.position()
}
pub fn seek(&mut self, position: u64) -> io::Result<()> {
self.reader.seek(position)
}
}
pub struct BrowserFiles {
files: Box<[DroppedFileAccess]>,
}
impl BrowserFiles {
fn from_files(files: Box<[DroppedFileAccess]>) -> Self {
Self { files }
}
#[must_use]
pub fn files(&self) -> &[DroppedFileAccess] {
&self.files
}
pub fn files_mut(&mut self) -> &mut [DroppedFileAccess] {
&mut self.files
}
}
pub struct DropFiles {
client_x: i32,
client_y: i32,
files: Box<[DroppedFileAccess]>,
}
impl DropFiles {
#[must_use]
pub const fn client_x(&self) -> i32 {
self.client_x
}
#[must_use]
pub const fn client_y(&self) -> i32 {
self.client_y
}
#[must_use]
pub fn files(&self) -> &[DroppedFileAccess] {
&self.files
}
pub fn files_mut(&mut self) -> &mut [DroppedFileAccess] {
&mut self.files
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DropMetadata {
client_x: i32,
client_y: i32,
files: Box<[DroppedFile]>,
}
impl DropMetadata {
#[must_use]
pub const fn client_x(&self) -> i32 {
self.client_x
}
#[must_use]
pub const fn client_y(&self) -> i32 {
self.client_y
}
#[must_use]
pub fn files(&self) -> &[DroppedFile] {
&self.files
}
}
impl WebEvent {
pub fn drop_metadata(&self) -> io::Result<Option<DropMetadata>> {
let Some((client_x, client_y, files)) = drop_context(&self.event)? else {
return Ok(None);
};
let entries = collect_files(&files)?;
Ok(Some(DropMetadata {
client_x,
client_y,
files: entries,
}))
}
pub fn drop_files(&self) -> io::Result<Option<DropFiles>> {
let Some((client_x, client_y, files)) = drop_context(&self.event)? else {
return Ok(None);
};
let entries = collect_file_access(&files)?;
Ok(Some(DropFiles {
client_x,
client_y,
files: entries,
}))
}
pub fn selected_files(&self) -> io::Result<Option<BrowserFiles>> {
let Some(input) = self
.event
.target()
.and_then(|target| target.dyn_into::<HtmlInputElement>().ok())
else {
return Ok(None);
};
if input.type_() != "file" {
return Ok(None);
}
let files = input.files().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"File input has no selected file list",
)
})?;
Ok(Some(BrowserFiles::from_files(collect_file_access(&files)?)))
}
}
fn drop_context(event: &web_sys::Event) -> io::Result<Option<(i32, i32, FileList)>> {
let Some(drag) = event.dyn_ref::<DragEvent>() else {
return Ok(None);
};
let mouse = event.dyn_ref::<MouseEvent>().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Drag event has no mouse coordinates",
)
})?;
let transfer = drag.data_transfer().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Drag event has no data transfer",
)
})?;
let files = transfer.files().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Drag event has no file list")
})?;
Ok(Some((mouse.client_x(), mouse.client_y(), files)))
}
fn collect_files(files: &FileList) -> io::Result<Box<[DroppedFile]>> {
let length = bounded_file_count(files)?;
let capacity = usize::try_from(length).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Browser file drop count cannot be represented",
)
})?;
let mut entries = Vec::with_capacity(capacity);
for index in 0..length {
let file = files.item(index).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Browser file list changed during metadata capture",
)
})?;
entries.push(DroppedFile::from_browser_file(&file)?);
}
Ok(entries.into_boxed_slice())
}
fn collect_file_access(files: &FileList) -> io::Result<Box<[DroppedFileAccess]>> {
let length = bounded_file_count(files)?;
let capacity = usize::try_from(length).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Browser file drop count cannot be represented",
)
})?;
let mut entries = Vec::with_capacity(capacity);
for index in 0..length {
let file = files.item(index).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Browser file list changed during access capture",
)
})?;
let reader = WebFile::from_js_file(file.clone())?;
let metadata = DroppedFile::from_browser_file_with_size(&file, reader.size())?;
entries.push(DroppedFileAccess { metadata, reader });
}
Ok(entries.into_boxed_slice())
}
fn bounded_file_count(files: &FileList) -> io::Result<u32> {
let length = files.length();
validate_file_count(length)?;
Ok(length)
}