use std::cell::Cell;
use std::rc::Weak;
use std::time::{SystemTime, UNIX_EPOCH};
use windows::Win32::Foundation::{CloseHandle, HANDLE, HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::System::RemoteDesktop::{
NOTIFY_FOR_THIS_SESSION, WTSRegisterSessionNotification, WTSUnRegisterSessionNotification,
};
use windows::Win32::System::Threading::{
CancelWaitableTimer, CreateWaitableTimerExW, INFINITE, SetWaitableTimer, TIMER_ALL_ACCESS,
};
use windows::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GWLP_USERDATA,
GetWindowLongPtrW, MSG, MWMO_INPUTAVAILABLE, MsgWaitForMultipleObjectsEx, PM_REMOVE,
PeekMessageW, QS_ALLINPUT, SetWindowLongPtrW, TranslateMessage, WM_NCDESTROY, WM_QUIT,
WNDCLASSEXW, WS_EX_TOOLWINDOW, WS_POPUP,
};
use windows::core::{PCWSTR, w};
use crate::Ui;
use crate::error::{Call, Result};
use crate::ui::Notice;
const CLASS_NAME: PCWSTR = w!("GlasskitEvents");
const WM_WTSSESSION_CHANGE: u32 = 0x02B1;
const WTS_SESSION_UNLOCK: usize = 0x8;
const UNIX_EPOCH_AS_FILETIME: i64 = 116_444_736_000_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PopupId(pub(crate) u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DialogId(pub(crate) u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Event<C> {
Command(C),
Popup(PopupId),
Dialog(DialogId),
Deadline,
SessionUnlocked,
Quit,
}
pub(crate) trait Source<C> {
fn take_command(&self) -> Option<C>;
}
pub struct EventLoop<C> {
ui: Ui,
timer: DeadlineTimer,
hwnd: HWND,
unlocked: *const Cell<bool>,
sources: Vec<Weak<dyn Source<C>>>,
}
impl<C> EventLoop<C> {
pub fn new(ui: &Ui) -> Result<Self> {
let timer = DeadlineTimer::new()?;
crate::register_class(&WNDCLASSEXW {
cbSize: size_of::<WNDCLASSEXW>() as u32,
lpfnWndProc: Some(wndproc),
hInstance: ui.hinstance(),
lpszClassName: CLASS_NAME,
..Default::default()
})?;
unsafe {
let hwnd = CreateWindowExW(
WS_EX_TOOLWINDOW,
CLASS_NAME,
CLASS_NAME,
WS_POPUP,
0,
0,
0,
0,
None,
None,
Some(ui.hinstance()),
None,
)
.call("CreateWindowExW")?;
let unlocked = Box::into_raw(Box::new(Cell::new(false)));
SetWindowLongPtrW(hwnd, GWLP_USERDATA, unlocked as isize);
let _ = WTSRegisterSessionNotification(hwnd, NOTIFY_FOR_THIS_SESSION);
Ok(Self {
ui: ui.clone(),
timer,
hwnd,
unlocked,
sources: Vec::new(),
})
}
}
pub fn ui(&self) -> &Ui {
&self.ui
}
pub(crate) fn add_source(&mut self, source: Weak<dyn Source<C>>) {
self.sources.push(source);
}
pub fn next(&mut self, until: Option<SystemTime>) -> Result<Event<C>> {
if until.is_none() {
self.timer.cancel();
}
loop {
unsafe {
let mut msg = MSG::default();
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
if msg.message == WM_QUIT {
return Ok(Event::Quit);
}
if self.ui.dialog_message(&msg) {
continue;
}
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
self.sources.retain(|s| s.strong_count() > 0);
for source in &self.sources {
if let Some(command) = source.upgrade().and_then(|s| s.take_command()) {
return Ok(Event::Command(command));
}
}
match self.ui.take_notice() {
Some(Notice::Popup(id)) => return Ok(Event::Popup(PopupId(id))),
Some(Notice::Dialog(id)) => return Ok(Event::Dialog(DialogId(id))),
None => {}
}
if unsafe { &*self.unlocked }.replace(false) {
return Ok(Event::SessionUnlocked);
}
if let Some(t) = until {
if SystemTime::now() >= t {
return Ok(Event::Deadline);
}
self.timer.set(t)?;
}
unsafe {
MsgWaitForMultipleObjectsEx(
Some(&[self.timer.0]),
INFINITE,
QS_ALLINPUT,
MWMO_INPUTAVAILABLE,
);
}
}
}
}
impl<C> Drop for EventLoop<C> {
fn drop(&mut self) {
unsafe {
let _ = WTSUnRegisterSessionNotification(self.hwnd);
let _ = DestroyWindow(self.hwnd);
drop(Box::from_raw(self.unlocked as *mut Cell<bool>));
}
}
}
extern "system" fn wndproc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
unsafe {
let unlocked = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *const Cell<bool>;
match msg {
WM_WTSSESSION_CHANGE if !unlocked.is_null() => {
if wparam.0 == WTS_SESSION_UNLOCK {
(*unlocked).set(true);
}
LRESULT(0)
}
WM_NCDESTROY => {
SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0);
DefWindowProcW(hwnd, msg, wparam, lparam)
}
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
}
fn absolute_due_time(at: SystemTime) -> i64 {
let since_unix = at.duration_since(UNIX_EPOCH).unwrap_or_default();
i64::try_from(since_unix.as_nanos().div_ceil(100))
.unwrap_or(i64::MAX)
.saturating_add(UNIX_EPOCH_AS_FILETIME)
}
struct DeadlineTimer(HANDLE);
impl DeadlineTimer {
fn new() -> Result<Self> {
let handle = unsafe {
CreateWaitableTimerExW(None, PCWSTR::null(), 0, TIMER_ALL_ACCESS.0)
.call("CreateWaitableTimerExW")?
};
Ok(Self(handle))
}
fn set(&self, at: SystemTime) -> Result<()> {
let due = absolute_due_time(at);
unsafe { SetWaitableTimer(self.0, &due, 0, None, None, false).call("SetWaitableTimer") }
}
fn cancel(&self) {
unsafe {
let _ = CancelWaitableTimer(self.0);
}
}
}
impl Drop for DeadlineTimer {
fn drop(&mut self) {
unsafe {
let _ = CloseHandle(self.0);
}
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
#[test]
fn due_time_counts_from_1601() {
assert_eq!(absolute_due_time(UNIX_EPOCH), UNIX_EPOCH_AS_FILETIME);
let one_second = UNIX_EPOCH + Duration::from_secs(1);
assert_eq!(
absolute_due_time(one_second),
UNIX_EPOCH_AS_FILETIME + 10_000_000
);
}
#[test]
fn due_time_stays_positive_far_in_the_future() {
const YEAR: u64 = 365 * 24 * 60 * 60;
let far = UNIX_EPOCH
.checked_add(Duration::from_secs(20_000 * YEAR))
.expect("SystemTime holds 20000 years");
assert!(absolute_due_time(far) > UNIX_EPOCH_AS_FILETIME);
}
#[test]
fn due_time_before_1970_is_1970() {
let before = UNIX_EPOCH - Duration::from_secs(60);
assert_eq!(absolute_due_time(before), UNIX_EPOCH_AS_FILETIME);
}
#[test]
fn a_deadline_in_the_past_returns_at_once() {
let ui = Ui::new().unwrap();
let mut events: EventLoop<()> = EventLoop::new(&ui).unwrap();
let past = SystemTime::now() - Duration::from_secs(1);
assert_eq!(events.next(Some(past)).unwrap(), Event::Deadline);
}
#[test]
fn a_notice_wakes_the_loop() {
let ui = Ui::new().unwrap();
let mut events: EventLoop<()> = EventLoop::new(&ui).unwrap();
crate::ui::notify(Notice::Popup(7));
assert_eq!(events.next(None).unwrap(), Event::Popup(PopupId(7)));
}
}