#![allow(non_snake_case)]
use std::cell::RefCell;
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use std::path::PathBuf;
use std::rc::Rc;
use bevy_ecs::prelude::Entity;
use tracing::{debug, info, warn};
use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::UI::Shell::{DragFinish, DragQueryFileW, HDROP};
use super::lifecycle::DESPAWNED_SKIP_TAG;
use crate::ecs::window::OnFilesDropped;
use crate::ecs::world::EcsWorld;
type HandlerResult = Option<LRESULT>;
#[derive(Debug, thiserror::Error)]
pub(crate) enum DropReadError {
#[error("NullHandle: 取っ手が無い(wParam が 0)")]
NullHandle,
#[error("Query: 全 {total} 本のうち {index} 番目(0 起点)のパスの長さか中身が読めなかった")]
Query { index: u32, total: u32 },
}
struct FinishOnDrop(HDROP);
impl Drop for FinishOnDrop {
fn drop(&mut self) {
unsafe { DragFinish(self.0) };
}
}
#[inline]
pub(super) fn WM_DROPFILES(
world: &Rc<RefCell<EcsWorld>>,
entity: Entity,
_hwnd: HWND,
wparam: WPARAM,
_lparam: LPARAM,
) -> HandlerResult {
if wparam.0 == 0 {
warn_read_failed(entity, &DropReadError::NullHandle);
return Some(LRESULT(0));
}
let hdrop = HDROP(wparam.0 as _);
let _finish = FinishOnDrop(hdrop);
match read_dropped_paths(hdrop) {
Ok(paths) => deliver_dropped_files(world, entity, paths),
Err(e) => {
warn_read_failed(entity, &e);
Some(LRESULT(0))
}
}
}
fn warn_read_failed(entity: Entity, error: &DropReadError) {
warn!(
event = "files_dropped_read_failed",
entity = ?entity,
error = %error,
"[WM_DROPFILES] 落とされた物の一覧を OS から読めない → 受け手を呼ばず捨てる"
);
}
fn read_dropped_paths(hdrop: HDROP) -> Result<Vec<PathBuf>, DropReadError> {
let total = unsafe { DragQueryFileW(hdrop, u32::MAX, None) };
(0..total)
.map(|index| {
let err = DropReadError::Query { index, total };
let len = unsafe { DragQueryFileW(hdrop, index, None) };
if len == 0 {
return Err(err);
}
let mut buf = vec![0u16; len as usize + 1];
let got = unsafe { DragQueryFileW(hdrop, index, Some(&mut buf)) } as usize;
if got == 0 {
return Err(err);
}
Ok(PathBuf::from(OsString::from_wide(
&buf[..got.min(len as usize)],
)))
})
.collect()
}
pub(super) fn deliver_dropped_files(
world: &Rc<RefCell<EcsWorld>>,
entity: Entity,
paths: Vec<PathBuf>,
) -> HandlerResult {
let count = paths.len();
let Ok(mut w) = world.try_borrow_mut() else {
warn!(
event = "files_dropped_world_busy",
entity = ?entity,
count,
"[WM_DROPFILES] World が借用中で投げ込みの受け手を呼べない → この投げ込みは捨てる"
);
return Some(LRESULT(0));
};
if w.world().get_entity(entity).is_err() {
debug!(
entity = ?entity,
count,
"{DESPAWNED_SKIP_TAG} WM_DROPFILES: 対象 entity は既に破棄済み(despawn)→ \
投げ込みを正常系として打ち切り"
);
} else if let Some(cb) = w.world().get::<OnFilesDropped>(entity).copied() {
info!(
event = "files_dropped",
entity = ?entity,
count,
"[WM_DROPFILES] 落とされた物の一覧を利用側の関数へ渡す"
);
(cb.0)(w.world_mut(), entity, paths);
} else {
debug!(
event = "files_dropped_no_receiver",
entity = ?entity,
count,
"[WM_DROPFILES] 投げ込みの関数を持たない窓 → 何もしない"
);
}
Some(LRESULT(0))
}
#[cfg(test)]
#[path = "drop_files_tests.rs"]
mod drop_files_tests;