nightshade 0.54.0

A cross-platform data-oriented game engine.
Documentation
use crate::ecs::world::World;

#[cfg(target_arch = "wasm32")]
thread_local! {
    static WASM_DROPPED_FILES: std::cell::RefCell<Vec<crate::ecs::input::resources::DroppedFile>> =
        const { std::cell::RefCell::new(Vec::new()) };
}

#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn collect_directory_files(
    root: &std::path::Path,
    current: &std::path::Path,
    files: &mut Vec<crate::ecs::input::resources::DroppedFile>,
) {
    let entries = match std::fs::read_dir(current) {
        Ok(entries) => entries,
        Err(_) => return,
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            collect_directory_files(root, &path, files);
        } else if path.is_file() {
            let Ok(data) = std::fs::read(&path) else {
                continue;
            };
            let relative = path
                .strip_prefix(root)
                .unwrap_or(&path)
                .to_string_lossy()
                .replace('\\', "/");
            let prefix = root
                .file_name()
                .and_then(|name| name.to_str())
                .map(|name| format!("{}/", name))
                .unwrap_or_default();
            let name = format!("{}{}", prefix, relative);
            files.push(crate::ecs::input::resources::DroppedFile { name, data });
        }
    }
}

pub(crate) fn process_dropped_files(world: &mut World) {
    #[cfg(target_arch = "wasm32")]
    {
        WASM_DROPPED_FILES.with(|files| {
            world
                .res_mut::<crate::ecs::loading::LoadingState>()
                .dropped_files
                .append(&mut files.borrow_mut());
        });
    }

    let dropped_files: Vec<_> = world
        .res_mut::<crate::ecs::loading::LoadingState>()
        .dropped_files
        .drain(..)
        .collect();
    for file in dropped_files {
        crate::ecs::events::emit_app_event(
            world,
            crate::ecs::input::events::AppEvent::FileDropped(file),
        );
    }
}

#[cfg(target_arch = "wasm32")]
pub(crate) fn setup_wasm_file_drop() {
    use wasm_bindgen::JsCast;
    use wasm_bindgen::prelude::*;

    let window = web_sys::window().expect("no global window");
    let document = window.document().expect("no document");
    let canvas = document
        .get_element_by_id("canvas")
        .expect("no canvas element");

    let dragover_closure = Closure::wrap(Box::new(move |event: web_sys::DragEvent| {
        event.prevent_default();
        event.stop_propagation();
    }) as Box<dyn FnMut(_)>);

    canvas
        .add_event_listener_with_callback("dragover", dragover_closure.as_ref().unchecked_ref())
        .expect("failed to add dragover listener");
    dragover_closure.forget();

    let drop_closure = Closure::wrap(Box::new(move |event: web_sys::DragEvent| {
        event.prevent_default();
        event.stop_propagation();

        let Some(data_transfer) = event.data_transfer() else {
            return;
        };

        let items = data_transfer.items();
        let mut entries: Vec<web_sys::FileSystemEntry> = Vec::new();
        for index in 0..items.length() {
            let Some(item) = items.get(index) else {
                continue;
            };
            if let Ok(Some(entry)) = item.webkit_get_as_entry() {
                entries.push(entry);
            }
        }

        if entries.is_empty() {
            return;
        }

        wasm_bindgen_futures::spawn_local(async move {
            let file_entries = collect_file_entries(entries).await;
            if file_entries.is_empty() {
                return;
            }
            let read_futures = file_entries
                .into_iter()
                .map(|(file_entry, path)| async move { read_file_entry(file_entry, path).await });
            let results = futures::future::join_all(read_futures).await;
            let batch: Vec<crate::ecs::input::resources::DroppedFile> =
                results.into_iter().flatten().collect();
            if !batch.is_empty() {
                WASM_DROPPED_FILES.with(|files| files.borrow_mut().extend(batch));
            }
        });
    }) as Box<dyn FnMut(_)>);

    canvas
        .add_event_listener_with_callback("drop", drop_closure.as_ref().unchecked_ref())
        .expect("failed to add drop listener");
    drop_closure.forget();
}

#[cfg(target_arch = "wasm32")]
async fn collect_file_entries(
    entries: Vec<web_sys::FileSystemEntry>,
) -> Vec<(web_sys::FileSystemFileEntry, String)> {
    use wasm_bindgen::JsCast;

    let mut files: Vec<(web_sys::FileSystemFileEntry, String)> = Vec::new();
    let mut worklist: Vec<(web_sys::FileSystemEntry, String)> =
        entries.into_iter().map(|e| (e, String::new())).collect();
    while let Some((entry, prefix)) = worklist.pop() {
        if entry.is_file() {
            let file_entry: web_sys::FileSystemFileEntry = entry.unchecked_into();
            let path = if prefix.is_empty() {
                file_entry.name()
            } else {
                format!("{}/{}", prefix, file_entry.name())
            };
            files.push((file_entry, path));
        } else if entry.is_directory() {
            let directory_entry: web_sys::FileSystemDirectoryEntry = entry.unchecked_into();
            let next_prefix = if prefix.is_empty() {
                directory_entry.name()
            } else {
                format!("{}/{}", prefix, directory_entry.name())
            };
            let reader = directory_entry.create_reader();
            loop {
                let promise = js_sys::Promise::new(&mut |resolve, reject| {
                    let _ = reader.read_entries_with_callback_and_callback(&resolve, &reject);
                });
                let entries_value = match wasm_bindgen_futures::JsFuture::from(promise).await {
                    Ok(value) => value,
                    Err(_) => break,
                };
                let array = js_sys::Array::from(&entries_value);
                if array.length() == 0 {
                    break;
                }
                for index in 0..array.length() {
                    let raw = array.get(index);
                    let child: web_sys::FileSystemEntry = raw.unchecked_into();
                    worklist.push((child, next_prefix.clone()));
                }
            }
        }
    }
    files
}

#[cfg(target_arch = "wasm32")]
async fn read_file_entry(
    file_entry: web_sys::FileSystemFileEntry,
    path: String,
) -> Option<crate::ecs::input::resources::DroppedFile> {
    use wasm_bindgen::JsCast;

    let file_promise = js_sys::Promise::new(&mut |resolve, reject| {
        file_entry.file_with_callback_and_callback(&resolve, &reject);
    });
    let file_value = wasm_bindgen_futures::JsFuture::from(file_promise)
        .await
        .ok()?;
    let file: web_sys::File = file_value.unchecked_into();
    let buffer_value = wasm_bindgen_futures::JsFuture::from(file.array_buffer())
        .await
        .ok()?;
    let array_buffer: js_sys::ArrayBuffer = buffer_value.unchecked_into();
    let bytes = js_sys::Uint8Array::new(&array_buffer).to_vec();
    Some(crate::ecs::input::resources::DroppedFile {
        name: path,
        data: bytes,
    })
}