use std::{
cell::RefCell,
rc::Rc,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};
use anyhow::{Context as _, Result, anyhow};
use futures::{
channel::oneshot,
future::{Either, select},
lock::Mutex,
};
use gpui::ForegroundExecutor;
use gpui_util::ResultExt;
use windows::{
Win32::{
Foundation::*,
System::{Com::*, Threading::GetCurrentThreadId},
UI::{
Controls::{TASKDIALOG_NOTIFICATIONS, TDN_BUTTON_CLICKED},
WindowsAndMessaging::*,
},
},
core::{BOOL, HRESULT, w},
};
use crate::SafeHwnd;
pub(crate) struct DialogOwner {
hwnd: HWND,
closed: Arc<AtomicBool>,
serialization: Mutex<()>,
}
impl DialogOwner {
pub(crate) fn new(hwnd: HWND) -> Rc<Self> {
Rc::new(Self {
hwnd,
closed: Arc::new(AtomicBool::new(false)),
serialization: Mutex::new(()),
})
}
pub(crate) fn close(&self) {
self.closed.store(true, Ordering::Release);
}
pub(crate) async fn when_idle(&self) {
let _guard = self.serialization.lock().await;
}
}
#[derive(Clone, Default)]
struct Cancellation {
requested: Arc<AtomicBool>,
owner_closed: Option<Arc<AtomicBool>>,
}
impl Cancellation {
fn new(owner: Option<&DialogOwner>) -> Self {
Self {
owner_closed: owner.map(|owner| owner.closed.clone()),
..Self::default()
}
}
fn is_requested(&self) -> bool {
self.requested.load(Ordering::Acquire)
|| self
.owner_closed
.as_ref()
.is_some_and(|closed| closed.load(Ordering::Acquire))
}
}
pub(crate) fn show_dialog<T: Send + 'static>(
owner: Option<Rc<DialogOwner>>,
executor: &ForegroundExecutor,
dialog: impl FnOnce(HWND) -> Result<T> + Send + 'static,
) -> oneshot::Receiver<Result<T>> {
let (mut sender, receiver) = oneshot::channel();
let cancellation = Cancellation::new(owner.as_deref());
if cancellation.is_requested() {
sender.send(Err(anyhow!("dialog owner has closed"))).ok();
return receiver;
}
let hwnd = owner.as_ref().map(|owner| SafeHwnd::from(owner.hwnd));
executor
.spawn(async move {
let serialization = if let Some(owner) = owner.as_ref() {
match select(owner.serialization.lock(), sender.cancellation()).await {
Either::Left((guard, _)) => Some(guard),
Either::Right(((), _)) => return,
}
} else {
None
};
if sender.is_canceled() || cancellation.is_requested() {
return;
}
let (result_sender, mut result_receiver) = oneshot::channel();
let thread = std::thread::Builder::new()
.name("windows-native-dialog".into())
.spawn({
let cancellation = cancellation.clone();
move || {
let result = (|| {
let _apartment = Apartment::new()?;
let _timer = CancellationTimer::new(cancellation.clone())?;
if cancellation.is_requested() {
return Err(anyhow!("dialog cancelled"));
}
let result = dialog(hwnd.map(|hwnd| hwnd.as_raw()).unwrap_or_default());
if cancellation.is_requested() {
return Err(anyhow!("dialog cancelled"));
}
result
})();
result_sender.send(result).ok();
}
});
if let Err(error) = thread {
drop(serialization);
sender.send(Err(error.into())).ok();
return;
}
let result = match select(&mut result_receiver, sender.cancellation()).await {
Either::Left((result, _)) => result,
Either::Right(((), _)) => {
cancellation.requested.store(true, Ordering::Release);
result_receiver.await
}
};
drop(serialization);
sender
.send(result.unwrap_or_else(|error| Err(error.into())))
.ok();
})
.detach();
receiver
}
struct Apartment;
impl Apartment {
fn new() -> Result<Self> {
unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED).ok()? };
Ok(Self)
}
}
impl Drop for Apartment {
fn drop(&mut self) {
unsafe { CoUninitialize() };
}
}
thread_local! {
static CANCELLATION: RefCell<Option<Cancellation>> = const { RefCell::new(None) };
}
struct CancellationTimer(usize);
impl CancellationTimer {
fn new(cancellation: Cancellation) -> Result<Self> {
let timer = unsafe { SetTimer(None, 0, 100, Some(cancel_dialog)) };
if timer == 0 {
return Err(windows::core::Error::from_thread())
.context("starting native dialog cancellation timer");
}
CANCELLATION.with(|slot| slot.replace(Some(cancellation)));
Ok(Self(timer))
}
}
impl Drop for CancellationTimer {
fn drop(&mut self) {
CANCELLATION.with(|slot| slot.take());
unsafe { KillTimer(None, self.0).log_err() };
}
}
unsafe extern "system" fn cancel_dialog(_: HWND, _: u32, _: usize, _: u32) {
if cancellation_requested() {
unsafe {
EnumThreadWindows(GetCurrentThreadId(), Some(close_dialog), LPARAM(0))
.ok()
.log_err();
};
}
}
fn cancellation_requested() -> bool {
CANCELLATION.with(|slot| {
slot.borrow()
.as_ref()
.is_some_and(Cancellation::is_requested)
})
}
pub(crate) unsafe extern "system" fn task_dialog_callback(
_: HWND,
notification: TASKDIALOG_NOTIFICATIONS,
button: WPARAM,
_: LPARAM,
has_cancel_button: isize,
) -> HRESULT {
if notification == TDN_BUTTON_CLICKED
&& button.0 == IDCANCEL.0 as usize
&& has_cancel_button == 0
&& !cancellation_requested()
{
S_FALSE
} else {
S_OK
}
}
unsafe extern "system" fn close_dialog(hwnd: HWND, _: LPARAM) -> BOOL {
let mut class_name = [0u16; 32];
let length = unsafe { GetClassNameW(hwnd, &mut class_name) } as usize;
let dialog_class = w!("#32770");
if class_name.get(..length) == Some(unsafe { dialog_class.as_wide() }) {
unsafe { PostMessageW(Some(hwnd), WM_CLOSE, WPARAM(0), LPARAM(0)).log_err() };
}
TRUE
}
#[cfg(test)]
mod tests {
use super::*;
use futures::FutureExt as _;
#[test]
fn task_dialog_cancellation_requires_cancel_button_or_internal_request() {
let cancellation = Cancellation::default();
CANCELLATION.with(|slot| slot.replace(Some(cancellation.clone())));
for internally_cancelled in [false, true] {
cancellation
.requested
.store(internally_cancelled, Ordering::Release);
for has_cancel_button in [false, true] {
for button in [IDCANCEL.0, IDOK.0, -1] {
let result = unsafe {
task_dialog_callback(
HWND::default(),
TDN_BUTTON_CLICKED,
WPARAM(button as usize),
LPARAM(0),
has_cancel_button as isize,
)
};
let should_veto =
button == IDCANCEL.0 && !has_cancel_button && !internally_cancelled;
assert_eq!(result, if should_veto { S_FALSE } else { S_OK });
}
}
}
CANCELLATION.with(|slot| slot.take());
}
#[test]
fn closing_owner_cancels_every_outstanding_dialog() {
let owner = DialogOwner::new(HWND::default());
let first = Cancellation::new(Some(&owner));
let second = Cancellation::new(Some(&owner));
assert!(!first.is_requested());
assert!(!second.is_requested());
owner.close();
owner.close();
assert!(first.is_requested());
assert!(second.is_requested());
assert!(Cancellation::new(Some(&owner)).is_requested());
}
#[test]
fn closed_owner_waits_for_active_dialog_but_queued_requests_stay_cancelled() {
let owner = DialogOwner::new(HWND::default());
let active = owner.serialization.try_lock().expect("first dialog");
let queued = Cancellation::new(Some(&owner));
owner.close();
assert!(owner.when_idle().now_or_never().is_none());
drop(active);
assert!(owner.when_idle().now_or_never().is_some());
let _guard = owner.serialization.try_lock().expect("queued request");
assert!(queued.is_requested());
}
#[test]
fn cancelling_one_request_does_not_cancel_its_owner_or_other_requests() {
let owner = DialogOwner::new(HWND::default());
let first = Cancellation::new(Some(&owner));
let second = Cancellation::new(Some(&owner));
first.requested.store(true, Ordering::Release);
assert!(first.is_requested());
assert!(!second.is_requested());
assert!(!owner.closed.load(Ordering::Acquire));
}
#[test]
fn dialogs_serialize_per_owner() {
let owner = DialogOwner::new(HWND::default());
let other_owner = DialogOwner::new(HWND::default());
let guard = owner.serialization.try_lock().expect("first dialog");
assert!(owner.serialization.try_lock().is_none());
assert!(other_owner.serialization.try_lock().is_some());
drop(guard);
assert!(owner.serialization.try_lock().is_some());
}
#[test]
fn ownerless_request_can_be_cancelled_without_a_window() {
let cancellation = Cancellation::new(None);
assert!(!cancellation.is_requested());
cancellation.requested.store(true, Ordering::Release);
assert!(cancellation.is_requested());
}
}