Skip to content

Commit c50ecfe

Browse files
authored
Merge pull request #763 from xStrom/win-quit
Terminate app run loop on Windows when all windows have closed.
2 parents cc2929b + 27c373a commit c50ecfe

25 files changed

+517
-162
lines changed

druid-shell/examples/invalidate.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ impl WinHandler for InvalidateTest {
8484
}
8585

8686
fn main() {
87-
let mut app = Application::new(None);
88-
let mut builder = WindowBuilder::new();
87+
let app = Application::new().unwrap();
88+
let mut builder = WindowBuilder::new(app.clone());
8989
let inv_test = InvalidateTest {
9090
size: Default::default(),
9191
handle: Default::default(),
@@ -98,5 +98,5 @@ fn main() {
9898

9999
let window = builder.build().unwrap();
100100
window.show();
101-
app.run();
101+
app.run(None);
102102
}

druid-shell/examples/perftest.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ impl WinHandler for PerfTest {
107107
}
108108

109109
fn destroy(&mut self) {
110-
Application::quit()
110+
Application::global().quit()
111111
}
112112

113113
fn as_any(&mut self) -> &mut dyn Any {
@@ -116,8 +116,8 @@ impl WinHandler for PerfTest {
116116
}
117117

118118
fn main() {
119-
let mut app = Application::new(None);
120-
let mut builder = WindowBuilder::new();
119+
let app = Application::new().unwrap();
120+
let mut builder = WindowBuilder::new(app.clone());
121121
let perf_test = PerfTest {
122122
size: Default::default(),
123123
handle: Default::default(),
@@ -130,5 +130,5 @@ fn main() {
130130
let window = builder.build().unwrap();
131131
window.show();
132132

133-
app.run();
133+
app.run(None);
134134
}

druid-shell/examples/shello.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ impl WinHandler for HelloState {
4848
match id {
4949
0x100 => {
5050
self.handle.close();
51-
Application::quit();
51+
Application::global().quit()
5252
}
5353
0x101 => {
5454
let options = FileDialogOptions::new().show_hidden().allowed_types(vec![
@@ -101,7 +101,7 @@ impl WinHandler for HelloState {
101101
}
102102

103103
fn destroy(&mut self) {
104-
Application::quit()
104+
Application::global().quit()
105105
}
106106

107107
fn as_any(&mut self) -> &mut dyn Any {
@@ -129,14 +129,14 @@ 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 app = Application::new().unwrap();
133+
let mut builder = WindowBuilder::new(app.clone());
134134
builder.set_handler(Box::new(HelloState::default()));
135135
builder.set_title("Hello example");
136136
builder.set_menu(menubar);
137137

138138
let window = builder.build().unwrap();
139139
window.show();
140140

141-
app.run();
141+
app.run(None);
142142
}

druid-shell/src/application.rs

+132-18
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,14 @@
1414

1515
//! The top-level application type.
1616
17+
use std::cell::RefCell;
18+
use std::rc::Rc;
19+
use std::sync::atomic::{AtomicBool, Ordering};
20+
1721
use crate::clipboard::Clipboard;
22+
use crate::error::Error;
1823
use crate::platform::application as platform;
24+
use crate::util;
1925

2026
/// A top-level handler that is not associated with any window.
2127
///
@@ -36,44 +42,152 @@ pub trait AppHandler {
3642
fn command(&mut self, id: u32) {}
3743
}
3844

39-
//TODO: we may want to make the user create an instance of this (Application::global()?)
40-
//but for now I'd like to keep changes minimal.
4145
/// The top level application object.
42-
pub struct Application(platform::Application);
46+
///
47+
/// This can be thought of as a reference and it can be safely cloned.
48+
#[derive(Clone)]
49+
pub struct Application {
50+
pub(crate) platform_app: platform::Application,
51+
state: Rc<RefCell<State>>,
52+
}
53+
54+
/// Platform-independent `Application` state.
55+
struct State {
56+
running: bool,
57+
}
58+
59+
/// Used to ensure only one Application instance is ever created.
60+
static APPLICATION_CREATED: AtomicBool = AtomicBool::new(false);
61+
62+
thread_local! {
63+
/// A reference object to the current `Application`, if any.
64+
static GLOBAL_APP: RefCell<Option<Application>> = RefCell::new(None);
65+
}
4366

4467
impl Application {
45-
pub fn new(handler: Option<Box<dyn AppHandler>>) -> Application {
46-
Application(platform::Application::new(handler))
68+
/// Create a new `Application`.
69+
///
70+
/// # Errors
71+
///
72+
/// Errors if an `Application` has already been created.
73+
///
74+
/// This may change in the future. See [druid#771] for discussion.
75+
///
76+
/// [druid#771]: https://github.com/xi-editor/druid/issues/771
77+
pub fn new() -> Result<Application, Error> {
78+
if APPLICATION_CREATED.compare_and_swap(false, true, Ordering::AcqRel) {
79+
return Err(Error::ApplicationAlreadyExists);
80+
}
81+
util::claim_main_thread();
82+
let platform_app = match platform::Application::new() {
83+
Ok(app) => app,
84+
Err(err) => return Err(Error::Platform(err)),
85+
};
86+
let state = Rc::new(RefCell::new(State { running: false }));
87+
let app = Application {
88+
platform_app,
89+
state,
90+
};
91+
GLOBAL_APP.with(|global_app| {
92+
*global_app.borrow_mut() = Some(app.clone());
93+
});
94+
Ok(app)
95+
}
96+
97+
/// Get the current globally active `Application`.
98+
///
99+
/// A globally active `Application` exists
100+
/// after [`new`] is called and until [`run`] returns.
101+
///
102+
/// # Panics
103+
///
104+
/// Panics if there is no globally active `Application`.
105+
/// For a non-panicking function use [`try_global`].
106+
///
107+
/// This function will also panic if called from a non-main thread.
108+
///
109+
/// [`new`]: #method.new
110+
/// [`run`]: #method.run
111+
/// [`try_global`]: #method.try_global
112+
#[inline]
113+
pub fn global() -> Application {
114+
// Main thread assertion takes place in try_global()
115+
Application::try_global().expect("There is no globally active Application")
116+
}
117+
118+
/// Get the current globally active `Application`.
119+
///
120+
/// A globally active `Application` exists
121+
/// after [`new`] is called and until [`run`] returns.
122+
///
123+
/// # Panics
124+
///
125+
/// Panics if called from a non-main thread.
126+
///
127+
/// [`new`]: #method.new
128+
/// [`run`]: #method.run
129+
pub fn try_global() -> Option<Application> {
130+
util::assert_main_thread();
131+
GLOBAL_APP.with(|global_app| global_app.borrow().clone())
47132
}
48133

49-
/// Start the runloop.
134+
/// Start the `Application` runloop.
135+
///
136+
/// The provided `handler` will be used to inform of events.
137+
///
138+
/// This will consume the `Application` and block the current thread
139+
/// until the `Application` has finished executing.
140+
///
141+
/// # Panics
50142
///
51-
/// This will block the current thread until the program has finished executing.
52-
pub fn run(&mut self) {
53-
self.0.run()
143+
/// Panics if the `Application` is already running.
144+
pub fn run(self, handler: Option<Box<dyn AppHandler>>) {
145+
// Make sure this application hasn't run() yet.
146+
if let Ok(mut state) = self.state.try_borrow_mut() {
147+
if state.running {
148+
panic!("Application is already running");
149+
}
150+
state.running = true;
151+
} else {
152+
panic!("Application state already borrowed");
153+
}
154+
155+
// Run the platform application
156+
self.platform_app.run(handler);
157+
158+
// This application is no longer active, so clear the global reference
159+
GLOBAL_APP.with(|global_app| {
160+
*global_app.borrow_mut() = None;
161+
});
162+
// .. and release the main thread
163+
util::release_main_thread();
54164
}
55165

56-
/// Terminate the application.
57-
pub fn quit() {
58-
platform::Application::quit()
166+
/// Quit the `Application`.
167+
///
168+
/// This will cause [`run`] to return control back to the calling function.
169+
///
170+
/// [`run`]: #method.run
171+
pub fn quit(&self) {
172+
self.platform_app.quit()
59173
}
60174

61175
// TODO: do these two go in some kind of PlatformExt trait?
62176
/// Hide the application this window belongs to. (cmd+H)
63-
pub fn hide() {
177+
pub fn hide(&self) {
64178
#[cfg(target_os = "macos")]
65-
platform::Application::hide()
179+
self.platform_app.hide()
66180
}
67181

68182
/// Hide all other applications. (cmd+opt+H)
69-
pub fn hide_others() {
183+
pub fn hide_others(&self) {
70184
#[cfg(target_os = "macos")]
71-
platform::Application::hide_others()
185+
self.platform_app.hide_others()
72186
}
73187

74188
/// Returns a handle to the system clipboard.
75-
pub fn clipboard() -> Clipboard {
76-
platform::Application::clipboard().into()
189+
pub fn clipboard(&self) -> Clipboard {
190+
self.platform_app.clipboard().into()
77191
}
78192

79193
/// Returns the current locale string.

druid-shell/src/clipboard.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ pub use crate::platform::clipboard as platform;
7070
/// ```no_run
7171
/// use druid_shell::{Application, Clipboard};
7272
///
73-
/// let mut clipboard = Application::clipboard();
73+
/// let mut clipboard = Application::global().clipboard();
7474
/// clipboard.put_string("watch it there pal");
7575
/// if let Some(contents) = clipboard.get_string() {
7676
/// assert_eq!("what it there pal", contents.as_str());
@@ -83,7 +83,7 @@ pub use crate::platform::clipboard as platform;
8383
/// ```no_run
8484
/// use druid_shell::{Application, Clipboard, ClipboardFormat};
8585
///
86-
/// let mut clipboard = Application::clipboard();
86+
/// let mut clipboard = Application::global().clipboard();
8787
///
8888
/// let custom_type_id = "io.xieditor.path-clipboard-type";
8989
///
@@ -104,7 +104,7 @@ pub use crate::platform::clipboard as platform;
104104
/// ```no_run
105105
/// use druid_shell::{Application, Clipboard, ClipboardFormat};
106106
///
107-
/// let clipboard = Application::clipboard();
107+
/// let clipboard = Application::global().clipboard();
108108
///
109109
/// let custom_type_id = "io.xieditor.path-clipboard-type";
110110
/// let supported_types = &[custom_type_id, ClipboardFormat::SVG, ClipboardFormat::PDF];

druid-shell/src/error.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,19 @@ use crate::platform::error as platform;
2020

2121
/// Error codes. At the moment, this is little more than HRESULT, but that
2222
/// might change.
23-
#[derive(Debug)]
23+
#[derive(Debug, Clone)]
2424
pub enum Error {
25+
ApplicationAlreadyExists,
2526
Other(&'static str),
2627
Platform(platform::Error),
2728
}
2829

2930
impl fmt::Display for Error {
3031
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
3132
match self {
33+
Error::ApplicationAlreadyExists => {
34+
write!(f, "An application instance has already been created.")
35+
}
3236
Error::Other(s) => write!(f, "{}", s),
3337
Error::Platform(p) => fmt::Display::fmt(&p, f),
3438
}

druid-shell/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ mod keycodes;
3535
mod menu;
3636
mod mouse;
3737
mod platform;
38+
mod util;
3839
mod window;
3940

4041
pub use application::{AppHandler, Application};

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

+10-7
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ use gio::prelude::ApplicationExtManual;
2020
use gio::{ApplicationExt, ApplicationFlags, Cancellable};
2121
use gtk::{Application as GtkApplication, GtkApplicationExt};
2222

23+
use crate::application::AppHandler;
24+
2325
use super::clipboard::Clipboard;
26+
use super::error::Error;
2427
use super::util;
25-
use crate::application::AppHandler;
2628

2729
// XXX: The application needs to be global because WindowBuilder::build wants
2830
// to construct an ApplicationWindow, which needs the application, but
@@ -31,10 +33,11 @@ thread_local!(
3133
static GTK_APPLICATION: RefCell<Option<GtkApplication>> = RefCell::new(None);
3234
);
3335

34-
pub struct Application;
36+
#[derive(Clone)]
37+
pub(crate) struct Application;
3538

3639
impl Application {
37-
pub fn new(_handler: Option<Box<dyn AppHandler>>) -> Application {
40+
pub fn new() -> Result<Application, Error> {
3841
// TODO: we should give control over the application ID to the user
3942
let application = GtkApplication::new(
4043
Some("com.github.xi-editor.druid"),
@@ -56,10 +59,10 @@ impl Application {
5659
.expect("Could not register GTK application");
5760

5861
GTK_APPLICATION.with(move |x| *x.borrow_mut() = Some(application));
59-
Application
62+
Ok(Application)
6063
}
6164

62-
pub fn run(&mut self) {
65+
pub fn run(self, _handler: Option<Box<dyn AppHandler>>) {
6366
util::assert_main_thread();
6467

6568
// TODO: should we pass the command line arguments?
@@ -71,7 +74,7 @@ impl Application {
7174
});
7275
}
7376

74-
pub fn quit() {
77+
pub fn quit(&self) {
7578
util::assert_main_thread();
7679
with_application(|app| {
7780
match app.get_active_window() {
@@ -86,7 +89,7 @@ impl Application {
8689
});
8790
}
8891

89-
pub fn clipboard() -> Clipboard {
92+
pub fn clipboard(&self) -> Clipboard {
9093
Clipboard
9194
}
9295

0 commit comments

Comments
 (0)