Skip to content

Commit 7144b4a

Browse files
committed
Terminate app run loop on Windows when all windows have closed.
1 parent c17983d commit 7144b4a

File tree

5 files changed

+156
-31
lines changed

5 files changed

+156
-31
lines changed

druid-shell/src/application.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,16 @@ use crate::platform::application as platform;
2424
///
2525
/// # Note
2626
///
27-
/// This is currently very limited in its functionality, and is currently
28-
/// designed to address a single case, which is handling menu commands when
29-
/// no window is open.
27+
/// This is currently limited in its functionality,
28+
/// having the ability to close all windows
29+
/// and handling menu commands when no window is open.
3030
///
3131
/// It is possible that this will expand to cover additional functionality
3232
/// in the future.
3333
pub trait AppHandler {
34+
/// Closes all the windows.
35+
fn close_all_windows(&mut self) {}
36+
3437
/// Called when a menu item is selected.
3538
#[allow(unused_variables)]
3639
fn command(&mut self, id: u32) {}

druid-shell/src/platform/windows/application.rs

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,52 +17,86 @@
1717
use std::mem;
1818
use std::ptr;
1919

20+
use winapi::shared::minwindef::FALSE;
2021
use winapi::shared::minwindef::HINSTANCE;
2122
use winapi::shared::ntdef::LPCWSTR;
2223
use winapi::shared::windef::HCURSOR;
24+
use winapi::shared::winerror::HRESULT_FROM_WIN32;
25+
use winapi::um::errhandlingapi::GetLastError;
2326
use winapi::um::shellscalingapi::PROCESS_SYSTEM_DPI_AWARE;
2427
use winapi::um::wingdi::CreateSolidBrush;
2528
use winapi::um::winuser::{
26-
DispatchMessageW, GetAncestor, GetMessageW, LoadIconW, PostQuitMessage, RegisterClassW,
27-
TranslateAcceleratorW, TranslateMessage, GA_ROOT, IDI_APPLICATION, MSG, WNDCLASSW,
29+
DispatchMessageW, GetAncestor, GetMessageW, LoadIconW, PostQuitMessage, PostThreadMessageW,
30+
RegisterClassW, TranslateAcceleratorW, TranslateMessage, GA_ROOT, IDI_APPLICATION, MSG,
31+
WNDCLASSW,
2832
};
2933

34+
use log::{error, warn};
35+
3036
use crate::application::AppHandler;
3137

3238
use super::accels;
3339
use super::clipboard::Clipboard;
34-
use super::util::{self, ToWide, CLASS_NAME, OPTIONAL_FUNCTIONS};
35-
use super::window::win_proc_dispatch;
40+
use super::error::Error;
41+
use super::util::{
42+
self, claim_main_thread, main_thread_id, release_main_thread, ToWide, CLASS_NAME,
43+
OPTIONAL_FUNCTIONS,
44+
};
45+
use super::window::{win_proc_dispatch, DS_REQUEST_QUIT};
3646

37-
pub struct Application;
47+
pub struct Application {
48+
handler: Option<Box<dyn AppHandler>>,
49+
}
3850

3951
impl Application {
40-
pub fn new(_handler: Option<Box<dyn AppHandler>>) -> Application {
52+
pub fn new(handler: Option<Box<dyn AppHandler>>) -> Application {
4153
Application::init();
42-
Application
54+
Application { handler }
4355
}
4456

4557
pub fn run(&mut self) {
58+
claim_main_thread();
4659
unsafe {
4760
// Handle windows messages
4861
loop {
4962
let mut msg = mem::MaybeUninit::uninit();
5063
let res = GetMessageW(msg.as_mut_ptr(), ptr::null_mut(), 0, 0);
5164
if res <= 0 {
52-
return;
65+
if res == -1 {
66+
error!(
67+
"GetMessageW failed: {}",
68+
Error::Hr(HRESULT_FROM_WIN32(GetLastError()))
69+
);
70+
}
71+
break;
5372
}
5473
let mut msg: MSG = msg.assume_init();
5574
let accels = accels::find_accels(GetAncestor(msg.hwnd, GA_ROOT));
5675
let translated = accels.map_or(false, |it| {
5776
TranslateAcceleratorW(msg.hwnd, it.handle(), &mut msg) != 0
5877
});
59-
6078
if !translated {
61-
TranslateMessage(&msg);
62-
DispatchMessageW(&msg);
79+
// We check for DS_REQUEST_QUIT here because thread messages
80+
// will not be forwarded to any window procedures by DispatchMessageW.
81+
if msg.message == DS_REQUEST_QUIT {
82+
if let Some(handler) = &mut self.handler {
83+
// We want to queue up the destruction of all open windows.
84+
// Failure to do so will lead to resource leaks
85+
// and an eventual error code exit for the process.
86+
handler.close_all_windows();
87+
}
88+
// PostQuitMessage sets a quit request flag in the OS.
89+
// The actual WM_QUIT message is queued but won't be sent
90+
// until all other important events have been handled.
91+
PostQuitMessage(0);
92+
} else {
93+
TranslateMessage(&msg);
94+
DispatchMessageW(&msg);
95+
}
6396
}
6497
}
6598
}
99+
release_main_thread();
66100
}
67101

68102
/// Initialize the app. At the moment, this is mostly needed for hi-dpi.
@@ -99,8 +133,15 @@ impl Application {
99133
}
100134

101135
pub fn quit() {
102-
unsafe {
103-
PostQuitMessage(0);
136+
if let Some(thread_id) = main_thread_id() {
137+
unsafe {
138+
if PostThreadMessageW(thread_id, DS_REQUEST_QUIT, 0, 0) == FALSE {
139+
warn!(
140+
"PostThreadMessageW failed: {}",
141+
Error::Hr(HRESULT_FROM_WIN32(GetLastError()))
142+
);
143+
}
144+
}
104145
}
105146
}
106147

druid-shell/src/platform/windows/util.rs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,78 @@ use std::mem;
2222
use std::os::windows::ffi::{OsStrExt, OsStringExt};
2323
use std::ptr;
2424
use std::slice;
25+
use std::sync::atomic::{AtomicU32, Ordering};
2526

2627
use winapi::ctypes::c_void;
2728
use winapi::shared::guiddef::REFIID;
28-
use winapi::shared::minwindef::{HMODULE, UINT};
29+
use winapi::shared::minwindef::{DWORD, HMODULE, UINT};
2930
use winapi::shared::ntdef::{HRESULT, LPWSTR};
3031
use winapi::shared::windef::HMONITOR;
3132
use winapi::shared::winerror::SUCCEEDED;
3233
use winapi::um::fileapi::{CreateFileA, GetFileType, OPEN_EXISTING};
3334
use winapi::um::handleapi::INVALID_HANDLE_VALUE;
3435
use winapi::um::libloaderapi::{GetModuleHandleW, GetProcAddress, LoadLibraryW};
3536
use winapi::um::processenv::{GetStdHandle, SetStdHandle};
37+
use winapi::um::processthreadsapi::GetCurrentThreadId;
3638
use winapi::um::shellscalingapi::{MONITOR_DPI_TYPE, PROCESS_DPI_AWARENESS};
3739
use winapi::um::unknwnbase::IUnknown;
3840
use winapi::um::winbase::{FILE_TYPE_UNKNOWN, STD_ERROR_HANDLE, STD_OUTPUT_HANDLE};
3941
use winapi::um::wincon::{AttachConsole, ATTACH_PARENT_PROCESS};
4042
use winapi::um::winnt::{FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE};
4143

42-
use log::error;
44+
use log::{error, warn};
4345

4446
use super::error::Error;
4547

48+
static MAIN_THREAD_ID: AtomicU32 = AtomicU32::new(0);
49+
50+
#[inline]
51+
fn current_thread_id() -> DWORD {
52+
unsafe { GetCurrentThreadId() }
53+
}
54+
55+
#[allow(dead_code)]
56+
pub fn assert_main_thread() {
57+
let thread_id = current_thread_id();
58+
let main_thread_id = MAIN_THREAD_ID.load(Ordering::Acquire);
59+
assert_eq!(thread_id, main_thread_id);
60+
}
61+
62+
pub fn claim_main_thread() {
63+
let thread_id = current_thread_id();
64+
let old_thread_id = MAIN_THREAD_ID.compare_and_swap(0, thread_id, Ordering::AcqRel);
65+
if old_thread_id != 0 {
66+
panic!(
67+
"The main thread status has already been claimed by thread {}",
68+
thread_id
69+
);
70+
}
71+
}
72+
73+
pub fn release_main_thread() {
74+
let thread_id = current_thread_id();
75+
let old_thread_id = MAIN_THREAD_ID.compare_and_swap(thread_id, 0, Ordering::AcqRel);
76+
if old_thread_id == 0 {
77+
warn!("The main thread status was already vacant.");
78+
} else if old_thread_id != thread_id {
79+
panic!(
80+
"The main thread status is owned by another thread {}",
81+
thread_id
82+
);
83+
}
84+
}
85+
86+
pub fn main_thread_id() -> Option<DWORD> {
87+
let thread_id = MAIN_THREAD_ID.load(Ordering::Acquire);
88+
// Although not explicitly documented, zero is an invalid thread id.
89+
// It can be deducted from the behavior of GetThreadId / SetWindowsHookExA.
90+
// https://devblogs.microsoft.com/oldnewthing/20040223-00/?p=40503
91+
match thread_id {
92+
0 => None,
93+
id => Some(id),
94+
}
95+
}
96+
4697
pub fn as_result(hr: HRESULT) -> Result<(), Error> {
4798
if SUCCEEDED(hr) {
4899
Ok(())

druid-shell/src/platform/windows/window.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ pub struct WindowHandle {
113113

114114
/// A handle that can get used to schedule an idle handler. Note that
115115
/// this handle is thread safe. If the handle is used after the hwnd
116-
/// has been destroyed, probably not much will go wrong (the XI_RUN_IDLE
116+
/// has been destroyed, probably not much will go wrong (the DS_RUN_IDLE
117117
/// message may be sent to a stray window).
118118
#[derive(Clone)]
119119
pub struct IdleHandle {
@@ -187,9 +187,9 @@ struct DCompState {
187187
}
188188

189189
/// Message indicating there are idle tasks to run.
190-
const XI_RUN_IDLE: UINT = WM_USER;
190+
const DS_RUN_IDLE: UINT = WM_USER;
191191

192-
/// Message relaying a request to destroy the window
192+
/// Message relaying a request to destroy the window.
193193
///
194194
/// Calling `DestroyWindow` from inside the handler is problematic
195195
/// because it will recursively cause a `WM_DESTROY` message to be
@@ -199,7 +199,12 @@ const XI_RUN_IDLE: UINT = WM_USER;
199199
/// As a solution, instead of immediately calling `DestroyWindow`, we
200200
/// send this message to request destroying the window, so that at the
201201
/// time it is handled, we can successfully borrow the handler.
202-
const XI_REQUEST_DESTROY: UINT = WM_USER + 1;
202+
const DS_REQUEST_DESTROY: UINT = WM_USER + 1;
203+
204+
/// Message relaying a request to quit the app run loop.
205+
///
206+
/// Directly calling `PostQuitMessage` won't do proper cleanup.
207+
pub const DS_REQUEST_QUIT: UINT = WM_USER + 2;
203208

204209
impl Default for PresentStrategy {
205210
fn default() -> PresentStrategy {
@@ -669,7 +674,7 @@ impl WndProc for MyWndProc {
669674
}
670675
Some(0)
671676
}
672-
XI_REQUEST_DESTROY => {
677+
DS_REQUEST_DESTROY => {
673678
unsafe {
674679
DestroyWindow(hwnd);
675680
}
@@ -682,7 +687,7 @@ impl WndProc for MyWndProc {
682687
} else {
683688
self.log_dropped_msg(hwnd, msg, wparam, lparam);
684689
}
685-
None
690+
Some(0)
686691
}
687692
WM_TIMER => {
688693
let id = wparam;
@@ -719,7 +724,7 @@ impl WndProc for MyWndProc {
719724
}
720725
Some(0)
721726
}
722-
XI_RUN_IDLE => {
727+
DS_RUN_IDLE => {
723728
if let Ok(mut s) = self.state.try_borrow_mut() {
724729
let s = s.as_mut().unwrap();
725730
let queue = self.handle.borrow().take_idle_queue();
@@ -1104,7 +1109,7 @@ impl WindowHandle {
11041109
if let Some(w) = self.state.upgrade() {
11051110
let hwnd = w.hwnd.get();
11061111
unsafe {
1107-
PostMessageW(hwnd, XI_REQUEST_DESTROY, 0, 0);
1112+
PostMessageW(hwnd, DS_REQUEST_DESTROY, 0, 0);
11081113
}
11091114
}
11101115
}
@@ -1344,7 +1349,7 @@ impl IdleHandle {
13441349
let mut queue = self.queue.lock().unwrap();
13451350
if queue.is_empty() {
13461351
unsafe {
1347-
PostMessageW(self.hwnd, XI_RUN_IDLE, 0, 0);
1352+
PostMessageW(self.hwnd, DS_RUN_IDLE, 0, 0);
13481353
}
13491354
}
13501355
queue.push(IdleKind::Callback(Box::new(callback)));
@@ -1354,7 +1359,7 @@ impl IdleHandle {
13541359
let mut queue = self.queue.lock().unwrap();
13551360
if queue.is_empty() {
13561361
unsafe {
1357-
PostMessageW(self.hwnd, XI_RUN_IDLE, 0, 0);
1362+
PostMessageW(self.hwnd, DS_RUN_IDLE, 0, 0);
13581363
}
13591364
}
13601365
queue.push(IdleKind::Token(token));

druid/src/win_handler.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ impl<T> Windows<T> {
118118
fn get_mut(&mut self, id: WindowId) -> Option<&mut Window<T>> {
119119
self.windows.get_mut(&id)
120120
}
121+
122+
fn count(&self) -> usize {
123+
self.windows.len() + self.pending.len()
124+
}
121125
}
122126

123127
impl<T> AppHandler<T> {
@@ -220,7 +224,13 @@ impl<T: Data> Inner<T> {
220224
if self.windows.windows.is_empty() {
221225
// on mac we need to keep the menu around
222226
self.root_menu = win.menu.take();
223-
//FIXME: on windows we need to shutdown the app here?
227+
228+
#[cfg(target_os = "windows")]
229+
{
230+
if self.windows.count() == 0 {
231+
Application::quit();
232+
}
233+
}
224234
}
225235
}
226236

@@ -262,6 +272,13 @@ impl<T: Data> Inner<T> {
262272
}
263273
}
264274

275+
/// Requests the platform to close all windows.
276+
fn request_close_all_windows(&mut self) {
277+
for win in self.windows.iter_mut() {
278+
win.handle.close();
279+
}
280+
}
281+
265282
fn show_window(&mut self, id: WindowId) {
266283
if let Some(win) = self.windows.get_mut(id) {
267284
win.handle.bring_to_front_and_focus();
@@ -495,7 +512,7 @@ impl<T: Data> AppState<T> {
495512
fn handle_cmd(&mut self, target: Target, cmd: Command) {
496513
use Target as T;
497514
match (target, &cmd.selector) {
498-
// these are handled the same no matter where they come from
515+
// these are handled the same no matter where they come from
499516
(_, &sys_cmd::QUIT_APP) => self.quit(),
500517
(_, &sys_cmd::HIDE_APPLICATION) => self.hide_app(),
501518
(_, &sys_cmd::HIDE_OTHERS) => self.hide_others(),
@@ -505,7 +522,7 @@ impl<T: Data> AppState<T> {
505522
}
506523
}
507524
// these should come from a window
508-
// FIXME: we need to be able to open a file without a window handle
525+
// FIXME: we need to be able to open a file without a window handle
509526
(T::Window(id), &sys_cmd::SHOW_OPEN_PANEL) => self.show_open_panel(cmd, id),
510527
(T::Window(id), &sys_cmd::SHOW_SAVE_PANEL) => self.show_save_panel(cmd, id),
511528
(T::Window(id), &sys_cmd::CLOSE_WINDOW) => self.request_close_window(cmd, id),
@@ -567,6 +584,10 @@ impl<T: Data> AppState<T> {
567584
self.inner.borrow_mut().request_close_window(*id);
568585
}
569586

587+
fn request_close_all_windows(&mut self) {
588+
self.inner.borrow_mut().request_close_all_windows();
589+
}
590+
570591
fn show_window(&mut self, cmd: Command) {
571592
let id: WindowId = *cmd
572593
.get_object()
@@ -595,6 +616,10 @@ impl<T: Data> AppState<T> {
595616
}
596617

597618
impl<T: Data> crate::shell::AppHandler for AppHandler<T> {
619+
fn close_all_windows(&mut self) {
620+
self.app_state.request_close_all_windows();
621+
}
622+
598623
fn command(&mut self, id: u32) {
599624
self.app_state.handle_system_cmd(id, None)
600625
}

0 commit comments

Comments
 (0)