Skip to content

Commit 875cc09

Browse files
committed
Terminate app run loop on Windows when all windows have closed.
1 parent 3ffe5e4 commit 875cc09

File tree

18 files changed

+455
-61
lines changed

18 files changed

+455
-61
lines changed

druid-shell/examples/perftest.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use time::Instant;
1919
use piet_common::kurbo::{Line, Rect};
2020
use piet_common::{Color, FontBuilder, Piet, RenderContext, Text, TextLayoutBuilder};
2121

22-
use druid_shell::{Application, KeyEvent, WinHandler, WindowBuilder, WindowHandle};
22+
use druid_shell::{AppState, Application, KeyEvent, WinHandler, WindowBuilder, WindowHandle};
2323

2424
const BG_COLOR: Color = Color::rgb8(0x27, 0x28, 0x22);
2525
const FG_COLOR: Color = Color::rgb8(0xf0, 0xf0, 0xea);
@@ -108,8 +108,9 @@ impl WinHandler for PerfTest {
108108
}
109109

110110
fn main() {
111-
let mut app = Application::new(None);
112-
let mut builder = WindowBuilder::new();
111+
let state = AppState::new();
112+
let mut app = Application::new(state.clone(), None);
113+
let mut builder = WindowBuilder::new(state);
113114
let perf_test = PerfTest {
114115
size: Default::default(),
115116
handle: Default::default(),

druid-shell/examples/shello.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ use druid_shell::kurbo::{Line, Rect, Vec2};
1818
use druid_shell::piet::{Color, RenderContext};
1919

2020
use druid_shell::{
21-
Application, Cursor, FileDialogOptions, FileSpec, HotKey, KeyEvent, KeyModifiers, Menu,
22-
MouseEvent, SysMods, TimerToken, WinHandler, WindowBuilder, WindowHandle,
21+
AppState, Application, Cursor, FileDialogOptions, FileSpec, HotKey, KeyEvent, KeyModifiers,
22+
Menu, MouseEvent, SysMods, TimerToken, WinHandler, WindowBuilder, WindowHandle,
2323
};
2424

2525
const BG_COLOR: Color = Color::rgb8(0x27, 0x28, 0x22);
@@ -129,8 +129,9 @@ fn main() {
129129
menubar.add_dropdown(Menu::new(), "Application", true);
130130
menubar.add_dropdown(file_menu, "&File", true);
131131

132-
let mut app = Application::new(None);
133-
let mut builder = WindowBuilder::new();
132+
let state = AppState::new();
133+
let mut app = Application::new(state.clone(), None);
134+
let mut builder = WindowBuilder::new(state);
134135
builder.set_handler(Box::new(HelloState::default()));
135136
builder.set_title("Hello example");
136137
builder.set_menu(menubar);

druid-shell/src/application.rs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
//! The top-level application type.
1616
17+
use std::sync::atomic::{AtomicBool, Ordering};
18+
1719
use crate::clipboard::Clipboard;
1820
use crate::platform::application as platform;
1921

@@ -36,14 +38,43 @@ pub trait AppHandler {
3638
fn command(&mut self, id: u32) {}
3739
}
3840

41+
/// The top level application state.
42+
///
43+
/// This helps the application track all the state that it has created,
44+
/// which it later needs to clean up.
45+
#[derive(Clone)]
46+
pub struct AppState(pub(crate) platform::AppState);
47+
48+
impl AppState {
49+
/// Create a new `AppState` instance.
50+
pub fn new() -> AppState {
51+
AppState(platform::AppState::new())
52+
}
53+
}
54+
3955
//TODO: we may want to make the user create an instance of this (Application::global()?)
4056
//but for now I'd like to keep changes minimal.
4157
/// The top level application object.
4258
pub struct Application(platform::Application);
4359

60+
// Used to ensure only one Application instance is ever created.
61+
// This may change in the future.
62+
// For more information see https://github.com/xi-editor/druid/issues/771
63+
static APPLICATION_CREATED: AtomicBool = AtomicBool::new(false);
64+
4465
impl Application {
45-
pub fn new(handler: Option<Box<dyn AppHandler>>) -> Application {
46-
Application(platform::Application::new(handler))
66+
/// Create a new `Application`.
67+
///
68+
/// It takes the application `state` and a `handler` which will be used to inform of events.
69+
///
70+
/// Right now only one application can be created. See [druid#771] for discussion.
71+
///
72+
/// [druid#771]: https://github.com/xi-editor/druid/issues/771
73+
pub fn new(state: AppState, handler: Option<Box<dyn AppHandler>>) -> Application {
74+
if APPLICATION_CREATED.compare_and_swap(false, true, Ordering::AcqRel) {
75+
panic!("The Application instance has already been created.");
76+
}
77+
Application(platform::Application::new(state.0, handler))
4778
}
4879

4980
/// Start the runloop.

druid-shell/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ mod mouse;
3737
mod platform;
3838
mod window;
3939

40-
pub use application::{AppHandler, Application};
40+
pub use application::{AppHandler, AppState, Application};
4141
pub use clipboard::{Clipboard, ClipboardFormat, FormatId};
4242
pub use common_util::Counter;
4343
pub use dialog::{FileDialogOptions, FileInfo, FileSpec};

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,19 @@ thread_local!(
3131
static GTK_APPLICATION: RefCell<Option<GtkApplication>> = RefCell::new(None);
3232
);
3333

34+
#[derive(Clone)]
35+
pub struct AppState;
36+
3437
pub struct Application;
3538

39+
impl AppState {
40+
pub(crate) fn new() -> AppState {
41+
AppState
42+
}
43+
}
44+
3645
impl Application {
37-
pub fn new(_handler: Option<Box<dyn AppHandler>>) -> Application {
46+
pub fn new(_state: AppState, _handler: Option<Box<dyn AppHandler>>) -> Application {
3847
// TODO: we should give control over the application ID to the user
3948
let application = GtkApplication::new(
4049
Some("com.github.xi-editor.druid"),

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ use gtk::{AccelGroup, ApplicationWindow};
3333
use crate::kurbo::{Point, Size, Vec2};
3434
use crate::piet::{Piet, RenderContext};
3535

36-
use super::application::with_application;
36+
use super::application::{with_application, AppState};
3737
use super::dialog;
3838
use super::menu::Menu;
3939
use super::util::assert_main_thread;
@@ -111,7 +111,7 @@ pub(crate) struct WindowState {
111111
}
112112

113113
impl WindowBuilder {
114-
pub fn new() -> WindowBuilder {
114+
pub fn new(_app_state: AppState) -> WindowBuilder {
115115
WindowBuilder {
116116
handler: None,
117117
title: String::new(),

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,21 @@ use crate::application::AppHandler;
3232

3333
static APP_HANDLER_IVAR: &str = "druidAppHandler";
3434

35+
#[derive(Clone)]
36+
pub struct AppState;
37+
3538
pub struct Application {
3639
ns_app: id,
3740
}
3841

42+
impl AppState {
43+
pub(crate) fn new() -> AppState {
44+
AppState
45+
}
46+
}
47+
3948
impl Application {
40-
pub fn new(handler: Option<Box<dyn AppHandler>>) -> Application {
49+
pub fn new(_state: AppState, handler: Option<Box<dyn AppHandler>>) -> Application {
4150
util::assert_main_thread();
4251
unsafe {
4352
let _pool = NSAutoreleasePool::new(nil);

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ use log::{error, info};
4141
use crate::kurbo::{Point, Size, Vec2};
4242
use crate::piet::{Piet, RenderContext};
4343

44+
use super::application::AppState;
4445
use super::dialog;
4546
use super::menu::Menu;
4647
use super::util::{assert_main_thread, make_nsstring};
@@ -104,7 +105,7 @@ struct ViewState {
104105
}
105106

106107
impl WindowBuilder {
107-
pub fn new() -> WindowBuilder {
108+
pub fn new(_app_state: AppState) -> WindowBuilder {
108109
WindowBuilder {
109110
handler: None,
110111
title: String::new(),

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

Lines changed: 108 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,101 @@
1414

1515
//! Windows implementation of features at the application scope.
1616
17+
use std::cell::RefCell;
18+
use std::collections::HashSet;
1719
use std::mem;
1820
use std::ptr;
21+
use std::rc::Rc;
1922

20-
use winapi::shared::minwindef::HINSTANCE;
23+
use winapi::shared::minwindef::{FALSE, HINSTANCE};
2124
use winapi::shared::ntdef::LPCWSTR;
22-
use winapi::shared::windef::HCURSOR;
25+
use winapi::shared::windef::{HCURSOR, HWND};
26+
use winapi::shared::winerror::HRESULT_FROM_WIN32;
27+
use winapi::um::errhandlingapi::GetLastError;
2328
use winapi::um::shellscalingapi::PROCESS_SYSTEM_DPI_AWARE;
2429
use winapi::um::wingdi::CreateSolidBrush;
2530
use winapi::um::winuser::{
26-
DispatchMessageW, GetAncestor, GetMessageW, LoadIconW, PostQuitMessage, RegisterClassW,
31+
DispatchMessageW, GetAncestor, GetMessageW, LoadIconW, PostMessageW, RegisterClassW,
2732
TranslateAcceleratorW, TranslateMessage, GA_ROOT, IDI_APPLICATION, MSG, WNDCLASSW,
2833
};
2934

3035
use crate::application::AppHandler;
3136

3237
use super::accels;
3338
use super::clipboard::Clipboard;
39+
use super::error::Error;
3440
use super::util::{self, ToWide, CLASS_NAME, OPTIONAL_FUNCTIONS};
35-
use super::window::win_proc_dispatch;
41+
use super::window::{self, DS_REQUEST_QUIT};
42+
43+
thread_local! {
44+
static GLOBAL_STATE: RefCell<Option<AppState>> = RefCell::new(None);
45+
}
46+
47+
#[derive(Clone)]
48+
pub struct AppState {
49+
state: Rc<RefCell<State>>,
50+
}
51+
52+
struct State {
53+
quitting: bool,
54+
app_hwnd: Option<HWND>,
55+
windows: HashSet<HWND>,
56+
}
3657

3758
pub struct Application;
3859

60+
impl AppState {
61+
pub(crate) fn new() -> AppState {
62+
let state = Rc::new(RefCell::new(State {
63+
quitting: false,
64+
app_hwnd: None,
65+
windows: HashSet::new(),
66+
}));
67+
AppState { state }
68+
}
69+
70+
pub(crate) fn quitting(&self) -> bool {
71+
self.state.borrow().quitting
72+
}
73+
74+
pub(crate) fn set_quitting(&self, quitting: bool) {
75+
self.state.borrow_mut().quitting = quitting;
76+
}
77+
78+
pub(crate) fn app_hwnd(&self) -> Option<HWND> {
79+
self.state.borrow().app_hwnd
80+
}
81+
82+
pub(crate) fn set_app_hwnd(&self, app_hwnd: Option<HWND>) {
83+
self.state.borrow_mut().app_hwnd = app_hwnd;
84+
}
85+
86+
/// Returns a set of `HWND` for all the current normal windows.
87+
///
88+
/// The returned set should be treated with extremely limited lifetime.
89+
/// The window handles it contains can become stale quickly.
90+
#[allow(clippy::mutable_key_type)]
91+
pub(crate) unsafe fn windows(&self) -> HashSet<HWND> {
92+
self.state.borrow().windows.clone()
93+
}
94+
95+
pub(crate) fn add_window(&self, hwnd: HWND) -> bool {
96+
self.state.borrow_mut().windows.insert(hwnd)
97+
}
98+
99+
pub(crate) fn remove_window(&self, hwnd: HWND) -> bool {
100+
self.state.borrow_mut().windows.remove(&hwnd)
101+
}
102+
}
103+
39104
impl Application {
40-
pub fn new(_handler: Option<Box<dyn AppHandler>>) -> Application {
105+
pub fn new(state: AppState, _handler: Option<Box<dyn AppHandler>>) -> Application {
106+
util::claim_main_thread();
107+
GLOBAL_STATE.with(|global_state| {
108+
*global_state.borrow_mut() = Some(state.clone());
109+
});
41110
Application::init();
111+
window::build_app_window(state).expect("Failed to build main message window");
42112
Application
43113
}
44114

@@ -49,14 +119,19 @@ impl Application {
49119
let mut msg = mem::MaybeUninit::uninit();
50120
let res = GetMessageW(msg.as_mut_ptr(), ptr::null_mut(), 0, 0);
51121
if res <= 0 {
52-
return;
122+
if res == -1 {
123+
log::error!(
124+
"GetMessageW failed: {}",
125+
Error::Hr(HRESULT_FROM_WIN32(GetLastError()))
126+
);
127+
}
128+
break;
53129
}
54130
let mut msg: MSG = msg.assume_init();
55131
let accels = accels::find_accels(GetAncestor(msg.hwnd, GA_ROOT));
56132
let translated = accels.map_or(false, |it| {
57133
TranslateAcceleratorW(msg.hwnd, it.handle(), &mut msg) != 0
58134
});
59-
60135
if !translated {
61136
TranslateMessage(&msg);
62137
DispatchMessageW(&msg);
@@ -67,6 +142,7 @@ impl Application {
67142

68143
/// Initialize the app. At the moment, this is mostly needed for hi-dpi.
69144
fn init() {
145+
util::assert_main_thread();
70146
util::attach_console();
71147
if let Some(func) = OPTIONAL_FUNCTIONS.SetProcessDpiAwareness {
72148
// This function is only supported on windows 10
@@ -81,7 +157,7 @@ impl Application {
81157
let brush = CreateSolidBrush(0xff_ff_ff);
82158
let wnd = WNDCLASSW {
83159
style: 0,
84-
lpfnWndProc: Some(win_proc_dispatch),
160+
lpfnWndProc: Some(window::win_proc_dispatch),
85161
cbClsExtra: 0,
86162
cbWndExtra: 0,
87163
hInstance: 0 as HINSTANCE,
@@ -99,9 +175,21 @@ impl Application {
99175
}
100176

101177
pub fn quit() {
102-
unsafe {
103-
PostQuitMessage(0);
104-
}
178+
util::assert_main_thread();
179+
GLOBAL_STATE.with(|global_state| {
180+
if let Some(global_state) = global_state.borrow().as_ref() {
181+
if let Some(app_hwnd) = global_state.app_hwnd() {
182+
unsafe {
183+
if PostMessageW(app_hwnd, DS_REQUEST_QUIT, 0, 0) == FALSE {
184+
log::error!(
185+
"PostMessageW failed: {}",
186+
Error::Hr(HRESULT_FROM_WIN32(GetLastError()))
187+
);
188+
}
189+
}
190+
}
191+
}
192+
});
105193
}
106194

107195
pub fn clipboard() -> Clipboard {
@@ -113,3 +201,12 @@ impl Application {
113201
"en-US".into()
114202
}
115203
}
204+
205+
impl Drop for Application {
206+
fn drop(&mut self) {
207+
GLOBAL_STATE.with(|global_state| {
208+
*global_state.borrow_mut() = None;
209+
});
210+
util::release_main_thread();
211+
}
212+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ pub enum Error {
3838
OldWindows,
3939
/// The `hwnd` pointer was null.
4040
NullHwnd,
41+
/// The main app message window already exists.
42+
AppWindowExists,
4143
}
4244

4345
fn hresult_description(hr: HRESULT) -> Option<String> {
@@ -78,6 +80,7 @@ impl fmt::Display for Error {
7880
Error::D2Error => write!(f, "Direct2D error"),
7981
Error::OldWindows => write!(f, "Attempted newer API on older Windows"),
8082
Error::NullHwnd => write!(f, "Window handle is Null"),
83+
Error::AppWindowExists => write!(f, "The main message window already exists"),
8184
}
8285
}
8386
}

0 commit comments

Comments
 (0)