use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::ffi::{c_void, OsString};
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
use std::time::Instant;
use webview2_com::CoTaskMemPWSTR;
use windows::core::{w, HRESULT, PCWSTR};
use windows::Win32::Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::System::Com::{
CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_INPROC_SERVER,
COINIT_APARTMENTTHREADED,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::Shell::{
FileOpenDialog, IFileOpenDialog, IShellItem, SHCreateItemFromParsingName, FOS_FORCEFILESYSTEM,
FOS_PATHMUSTEXIST, FOS_PICKFOLDERS, SIGDN_FILESYSPATH,
};
use windows::Win32::UI::WindowsAndMessaging::{
self, CreateWindowExW, DefWindowProcW, DestroyWindow, PostMessageW, RegisterClassW, WNDCLASSW,
};
use crate::native_services::{
checked_directory, DirectoryPickerOptions, DirectorySelection, Inner, NativeServiceError,
PickerPermit, Timer, MAX_NATIVE_DOCUMENT_PATH_BYTES,
};
const CANCEL_MESSAGE: u32 = WindowsAndMessaging::WM_APP + 23;
const CANCEL_HRESULT: HRESULT = HRESULT(0x800704C7_u32.cast_signed());
thread_local! {
static TARGETS: RefCell<HashMap<usize, Rc<Target>>> = RefCell::new(HashMap::new());
}
struct Target {
id: u64,
cookie: usize,
dialog: IFileOpenDialog,
showing: Cell<bool>,
signal: Arc<CancelSignal>,
}
pub(crate) struct CancelSignal {
id: u64,
cookie: usize,
cancelled: AtomicBool,
relay: AtomicUsize,
}
impl CancelSignal {
pub(crate) fn new(id: u64, cookie: usize) -> Self {
Self {
id,
cookie,
cancelled: AtomicBool::new(false),
relay: AtomicUsize::new(0),
}
}
pub(crate) fn cancel(&self) {
self.cancelled.store(true, Ordering::Release);
let hwnd = self.relay.load(Ordering::Acquire);
if hwnd != 0 {
let Ok(id) = usize::try_from(self.id) else {
eprintln!("WebUI: picker request ID is not representable by native wake");
return;
};
let _ = unsafe {
PostMessageW(
Some(HWND(hwnd as *mut c_void)),
CANCEL_MESSAGE,
WPARAM(self.cookie),
LPARAM(id.cast_signed()),
)
};
}
}
}
pub(crate) fn cancel_picker(owner: &Inner, expected_id: Option<u64>) {
let signal = owner
.picker_signal
.lock()
.ok()
.and_then(|slot| slot.as_ref().and_then(std::sync::Weak::upgrade));
if let Some(signal) = signal {
if expected_id.is_none_or(|id| id == signal.id) {
signal.cancel();
}
}
}
struct SlotState {
answer: Option<Result<DirectorySelection, NativeServiceError>>,
waker: Option<Waker>,
}
pub(crate) struct PickerSlot(Mutex<SlotState>);
impl PickerSlot {
pub(crate) fn new() -> Self {
Self(Mutex::new(SlotState {
answer: None,
waker: None,
}))
}
pub(crate) fn complete(&self, answer: Result<DirectorySelection, NativeServiceError>) {
let wake = if let Ok(mut state) = self.0.lock() {
if state.answer.is_some() {
return;
}
state.answer = Some(answer);
state.waker.take()
} else {
None
};
if let Some(wake) = wake {
wake.wake();
}
}
pub(crate) fn poll(
&self,
cx: &mut Context<'_>,
) -> Poll<Result<DirectorySelection, NativeServiceError>> {
let Ok(mut state) = self.0.lock() else {
return Poll::Ready(Err(NativeServiceError::Unavailable));
};
if let Some(answer) = state.answer.take() {
return Poll::Ready(answer);
}
state.waker = Some(cx.waker().clone());
Poll::Pending
}
}
pub(crate) struct PickerJob {
pub(crate) permit: Arc<PickerPermit>,
pub(crate) slot: Arc<PickerSlot>,
pub(crate) signal: Arc<CancelSignal>,
pub(crate) options: DirectoryPickerOptions,
pub(crate) identity: (u64, u64),
pub(crate) target: (usize, usize),
pub(crate) deadline: Instant,
pub(crate) timer: Arc<Timer>,
}
pub(crate) fn submit(owner: Arc<Inner>, job: PickerJob) -> Result<(), NativeServiceError> {
std::thread::Builder::new()
.name("webui-folder-picker-sta".into())
.spawn(move || {
let result = run(&owner, &job.signal, &job.options, job.identity, job.target);
let result = if !owner.picker_current(job.identity.0, job.identity.1, job.target)
|| job.signal.cancelled.load(Ordering::Acquire)
{
Err(NativeServiceError::Cancelled)
} else if Instant::now() >= job.deadline {
Err(NativeServiceError::PickerTimeout)
} else {
result
};
job.timer.finish();
job.permit.release();
job.slot.complete(result);
})
.map(|_| ())
.map_err(|_| NativeServiceError::Unavailable)
}
fn native_failure(op: &'static str, error: windows::core::Error) -> NativeServiceError {
NativeServiceError::PickerOs {
operation: op,
code: error.code().0,
}
}
unsafe extern "system" fn relay_proc(hwnd: HWND, message: u32, wp: WPARAM, lp: LPARAM) -> LRESULT {
if message == CANCEL_MESSAGE {
let target = TARGETS.with(|targets| targets.borrow().get(&(hwnd.0 as usize)).cloned());
if let Some(target) = target {
if wp.0 == target.cookie
&& u64::try_from(lp.0.cast_unsigned()).ok() == Some(target.id)
&& target.showing.get()
&& target.signal.cancelled.load(Ordering::Acquire)
{
if let Err(error) = unsafe { target.dialog.Close(CANCEL_HRESULT) } {
eprintln!(
"WebUI: IFileDialog::Close did not acknowledge cancellation (HRESULT {:#x}); waiting for native return",
error.code().0
);
}
}
}
return LRESULT(0);
}
unsafe { DefWindowProcW(hwnd, message, wp, lp) }
}
fn make_relay() -> Result<HWND, NativeServiceError> {
let instance = unsafe {
HINSTANCE(
GetModuleHandleW(None)
.map_err(|e| native_failure("GetModuleHandleW", e))?
.0,
)
};
let class = WNDCLASSW {
lpfnWndProc: Some(relay_proc),
hInstance: instance,
lpszClassName: w!("WebUIPickerWorkerSTA"),
..Default::default()
};
unsafe { RegisterClassW(&class) };
unsafe {
CreateWindowExW(
Default::default(),
w!("WebUIPickerWorkerSTA"),
w!(""),
WindowsAndMessaging::WINDOW_STYLE(0),
0,
0,
0,
0,
Some(WindowsAndMessaging::HWND_MESSAGE),
None,
Some(instance),
None,
)
}
.map_err(|e| native_failure("CreateWindowExW", e))
}
fn selected_directory(dialog: &IFileOpenDialog) -> Result<PathBuf, NativeServiceError> {
let item = unsafe { dialog.GetResult() }.map_err(|e| native_failure("GetResult", e))?;
let raw = unsafe { item.GetDisplayName(SIGDN_FILESYSPATH) }
.map_err(|e| native_failure("GetDisplayName", e))?;
let _guard = CoTaskMemPWSTR::from(raw);
if raw.is_null() {
return Err(NativeServiceError::InvalidSelection);
}
let mut len = 0;
unsafe {
while len <= MAX_NATIVE_DOCUMENT_PATH_BYTES && *raw.0.add(len) != 0 {
len += 1;
}
if len == 0 || len > MAX_NATIVE_DOCUMENT_PATH_BYTES {
return Err(NativeServiceError::InvalidSelection);
}
let path = PathBuf::from(OsString::from_wide(std::slice::from_raw_parts(raw.0, len)));
checked_directory(&path, false)
}
}
fn run(
owner: &Inner,
signal: &Arc<CancelSignal>,
options: &DirectoryPickerOptions,
(id, generation): (u64, u64),
target: (usize, usize),
) -> Result<DirectorySelection, NativeServiceError> {
if !owner.picker_current(id, generation, target) || signal.cancelled.load(Ordering::Acquire) {
return Err(NativeServiceError::Cancelled);
}
unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }
.ok()
.map_err(|e| native_failure("CoInitializeEx", e))?;
struct Apartment;
impl Drop for Apartment {
fn drop(&mut self) {
unsafe { CoUninitialize() };
}
}
let _apartment = Apartment;
let dialog: IFileOpenDialog =
unsafe { CoCreateInstance(&FileOpenDialog, None, CLSCTX_INPROC_SERVER) }
.map_err(|e| native_failure("CoCreateInstance", e))?;
let flags = unsafe { dialog.GetOptions() }.map_err(|e| native_failure("GetOptions", e))?;
unsafe { dialog.SetOptions(flags | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST) }
.map_err(|e| native_failure("SetOptions", e))?;
if let Some(title) = options.title.as_deref() {
let wide: Vec<u16> = title.encode_utf16().chain(Some(0)).collect();
unsafe { dialog.SetTitle(PCWSTR(wide.as_ptr())) }
.map_err(|e| native_failure("SetTitle", e))?;
}
if let Some(initial) = options.initial_directory.as_deref() {
let path = checked_directory(initial, true)?;
let wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
let item: IShellItem = unsafe { SHCreateItemFromParsingName(PCWSTR(wide.as_ptr()), None) }
.map_err(|e| native_failure("SHCreateItemFromParsingName", e))?;
unsafe { dialog.SetFolder(&item) }.map_err(|e| native_failure("SetFolder", e))?;
}
let relay = make_relay()?;
let live_hwnd =
unsafe { WindowsAndMessaging::IsWindow(Some(HWND(target.0 as *mut c_void))) }.as_bool();
if !owner.picker_current(id, generation, target)
|| signal.cancelled.load(Ordering::Acquire)
|| !live_hwnd
{
let _ = unsafe { DestroyWindow(relay) };
return Err(if !live_hwnd {
NativeServiceError::Closed
} else {
NativeServiceError::Cancelled
});
}
let picker = Rc::new(Target {
id,
cookie: target.1,
dialog: dialog.clone(),
showing: Cell::new(true),
signal: Arc::clone(signal),
});
TARGETS.with(|targets| {
targets
.borrow_mut()
.insert(relay.0 as usize, Rc::clone(&picker));
});
signal.relay.store(relay.0 as usize, Ordering::Release);
if !owner.picker_current(id, generation, target) || signal.cancelled.load(Ordering::Acquire) {
signal.cancel();
}
let result = unsafe { dialog.Show(Some(HWND(target.0 as *mut c_void))) };
picker.showing.set(false);
signal.relay.store(0, Ordering::Release);
TARGETS.with(|targets| {
targets.borrow_mut().remove(&(relay.0 as usize));
});
if let Err(error) = unsafe { DestroyWindow(relay) } {
return Err(native_failure("DestroyWindow", error));
}
match result {
Ok(())
if owner.picker_current(id, generation, target)
&& !signal.cancelled.load(Ordering::Acquire) =>
{
selected_directory(&dialog).map(DirectorySelection::Selected)
}
Ok(()) => Err(NativeServiceError::Cancelled),
Err(error) if error.code() == CANCEL_HRESULT => Ok(DirectorySelection::Cancelled),
Err(error) => Err(native_failure("IFileOpenDialog::Show", error)),
}
}