diff --git a/druid-shell/examples/invalidate.rs b/druid-shell/examples/invalidate.rs index 2e4277083a..82084bce95 100644 --- a/druid-shell/examples/invalidate.rs +++ b/druid-shell/examples/invalidate.rs @@ -84,8 +84,8 @@ impl WinHandler for InvalidateTest { } fn main() { - let mut app = Application::new(None); - let mut builder = WindowBuilder::new(); + let app = Application::new().unwrap(); + let mut builder = WindowBuilder::new(app.clone()); let inv_test = InvalidateTest { size: Default::default(), handle: Default::default(), @@ -98,5 +98,5 @@ fn main() { let window = builder.build().unwrap(); window.show(); - app.run(); + app.run(None); } diff --git a/druid-shell/examples/perftest.rs b/druid-shell/examples/perftest.rs index 16fbffba21..6c3914fcdf 100644 --- a/druid-shell/examples/perftest.rs +++ b/druid-shell/examples/perftest.rs @@ -107,7 +107,7 @@ impl WinHandler for PerfTest { } fn destroy(&mut self) { - Application::quit() + Application::global().quit() } fn as_any(&mut self) -> &mut dyn Any { @@ -116,8 +116,8 @@ impl WinHandler for PerfTest { } fn main() { - let mut app = Application::new(None); - let mut builder = WindowBuilder::new(); + let app = Application::new().unwrap(); + let mut builder = WindowBuilder::new(app.clone()); let perf_test = PerfTest { size: Default::default(), handle: Default::default(), @@ -130,5 +130,5 @@ fn main() { let window = builder.build().unwrap(); window.show(); - app.run(); + app.run(None); } diff --git a/druid-shell/examples/shello.rs b/druid-shell/examples/shello.rs index 14e41ab102..93b2f55a91 100644 --- a/druid-shell/examples/shello.rs +++ b/druid-shell/examples/shello.rs @@ -48,7 +48,7 @@ impl WinHandler for HelloState { match id { 0x100 => { self.handle.close(); - Application::quit(); + Application::global().quit() } 0x101 => { let options = FileDialogOptions::new().show_hidden().allowed_types(vec![ @@ -101,7 +101,7 @@ impl WinHandler for HelloState { } fn destroy(&mut self) { - Application::quit() + Application::global().quit() } fn as_any(&mut self) -> &mut dyn Any { @@ -129,8 +129,8 @@ fn main() { menubar.add_dropdown(Menu::new(), "Application", true); menubar.add_dropdown(file_menu, "&File", true); - let mut app = Application::new(None); - let mut builder = WindowBuilder::new(); + let app = Application::new().unwrap(); + let mut builder = WindowBuilder::new(app.clone()); builder.set_handler(Box::new(HelloState::default())); builder.set_title("Hello example"); builder.set_menu(menubar); @@ -138,5 +138,5 @@ fn main() { let window = builder.build().unwrap(); window.show(); - app.run(); + app.run(None); } diff --git a/druid-shell/src/application.rs b/druid-shell/src/application.rs index 4306642571..9358d235e8 100644 --- a/druid-shell/src/application.rs +++ b/druid-shell/src/application.rs @@ -14,8 +14,14 @@ //! The top-level application type. +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::atomic::{AtomicBool, Ordering}; + use crate::clipboard::Clipboard; +use crate::error::Error; use crate::platform::application as platform; +use crate::util; /// A top-level handler that is not associated with any window. /// @@ -36,44 +42,152 @@ pub trait AppHandler { fn command(&mut self, id: u32) {} } -//TODO: we may want to make the user create an instance of this (Application::global()?) -//but for now I'd like to keep changes minimal. /// The top level application object. -pub struct Application(platform::Application); +/// +/// This can be thought of as a reference and it can be safely cloned. +#[derive(Clone)] +pub struct Application { + pub(crate) platform_app: platform::Application, + state: Rc>, +} + +/// Platform-independent `Application` state. +struct State { + running: bool, +} + +/// Used to ensure only one Application instance is ever created. +static APPLICATION_CREATED: AtomicBool = AtomicBool::new(false); + +thread_local! { + /// A reference object to the current `Application`, if any. + static GLOBAL_APP: RefCell> = RefCell::new(None); +} impl Application { - pub fn new(handler: Option>) -> Application { - Application(platform::Application::new(handler)) + /// Create a new `Application`. + /// + /// # Errors + /// + /// Errors if an `Application` has already been created. + /// + /// This may change in the future. See [druid#771] for discussion. + /// + /// [druid#771]: https://github.com/xi-editor/druid/issues/771 + pub fn new() -> Result { + if APPLICATION_CREATED.compare_and_swap(false, true, Ordering::AcqRel) { + return Err(Error::ApplicationAlreadyExists); + } + util::claim_main_thread(); + let platform_app = match platform::Application::new() { + Ok(app) => app, + Err(err) => return Err(Error::Platform(err)), + }; + let state = Rc::new(RefCell::new(State { running: false })); + let app = Application { + platform_app, + state, + }; + GLOBAL_APP.with(|global_app| { + *global_app.borrow_mut() = Some(app.clone()); + }); + Ok(app) + } + + /// Get the current globally active `Application`. + /// + /// A globally active `Application` exists + /// after [`new`] is called and until [`run`] returns. + /// + /// # Panics + /// + /// Panics if there is no globally active `Application`. + /// For a non-panicking function use [`try_global`]. + /// + /// This function will also panic if called from a non-main thread. + /// + /// [`new`]: #method.new + /// [`run`]: #method.run + /// [`try_global`]: #method.try_global + #[inline] + pub fn global() -> Application { + // Main thread assertion takes place in try_global() + Application::try_global().expect("There is no globally active Application") + } + + /// Get the current globally active `Application`. + /// + /// A globally active `Application` exists + /// after [`new`] is called and until [`run`] returns. + /// + /// # Panics + /// + /// Panics if called from a non-main thread. + /// + /// [`new`]: #method.new + /// [`run`]: #method.run + pub fn try_global() -> Option { + util::assert_main_thread(); + GLOBAL_APP.with(|global_app| global_app.borrow().clone()) } - /// Start the runloop. + /// Start the `Application` runloop. + /// + /// The provided `handler` will be used to inform of events. + /// + /// This will consume the `Application` and block the current thread + /// until the `Application` has finished executing. + /// + /// # Panics /// - /// This will block the current thread until the program has finished executing. - pub fn run(&mut self) { - self.0.run() + /// Panics if the `Application` is already running. + pub fn run(self, handler: Option>) { + // Make sure this application hasn't run() yet. + if let Ok(mut state) = self.state.try_borrow_mut() { + if state.running { + panic!("Application is already running"); + } + state.running = true; + } else { + panic!("Application state already borrowed"); + } + + // Run the platform application + self.platform_app.run(handler); + + // This application is no longer active, so clear the global reference + GLOBAL_APP.with(|global_app| { + *global_app.borrow_mut() = None; + }); + // .. and release the main thread + util::release_main_thread(); } - /// Terminate the application. - pub fn quit() { - platform::Application::quit() + /// Quit the `Application`. + /// + /// This will cause [`run`] to return control back to the calling function. + /// + /// [`run`]: #method.run + pub fn quit(&self) { + self.platform_app.quit() } // TODO: do these two go in some kind of PlatformExt trait? /// Hide the application this window belongs to. (cmd+H) - pub fn hide() { + pub fn hide(&self) { #[cfg(target_os = "macos")] - platform::Application::hide() + self.platform_app.hide() } /// Hide all other applications. (cmd+opt+H) - pub fn hide_others() { + pub fn hide_others(&self) { #[cfg(target_os = "macos")] - platform::Application::hide_others() + self.platform_app.hide_others() } /// Returns a handle to the system clipboard. - pub fn clipboard() -> Clipboard { - platform::Application::clipboard().into() + pub fn clipboard(&self) -> Clipboard { + self.platform_app.clipboard().into() } /// Returns the current locale string. diff --git a/druid-shell/src/clipboard.rs b/druid-shell/src/clipboard.rs index 11cdb492d8..dce0ad3081 100644 --- a/druid-shell/src/clipboard.rs +++ b/druid-shell/src/clipboard.rs @@ -70,7 +70,7 @@ pub use crate::platform::clipboard as platform; /// ```no_run /// use druid_shell::{Application, Clipboard}; /// -/// let mut clipboard = Application::clipboard(); +/// let mut clipboard = Application::global().clipboard(); /// clipboard.put_string("watch it there pal"); /// if let Some(contents) = clipboard.get_string() { /// assert_eq!("what it there pal", contents.as_str()); @@ -83,7 +83,7 @@ pub use crate::platform::clipboard as platform; /// ```no_run /// use druid_shell::{Application, Clipboard, ClipboardFormat}; /// -/// let mut clipboard = Application::clipboard(); +/// let mut clipboard = Application::global().clipboard(); /// /// let custom_type_id = "io.xieditor.path-clipboard-type"; /// @@ -104,7 +104,7 @@ pub use crate::platform::clipboard as platform; /// ```no_run /// use druid_shell::{Application, Clipboard, ClipboardFormat}; /// -/// let clipboard = Application::clipboard(); +/// let clipboard = Application::global().clipboard(); /// /// let custom_type_id = "io.xieditor.path-clipboard-type"; /// let supported_types = &[custom_type_id, ClipboardFormat::SVG, ClipboardFormat::PDF]; diff --git a/druid-shell/src/error.rs b/druid-shell/src/error.rs index 8c7494d93b..04cdb01eed 100644 --- a/druid-shell/src/error.rs +++ b/druid-shell/src/error.rs @@ -20,8 +20,9 @@ use crate::platform::error as platform; /// Error codes. At the moment, this is little more than HRESULT, but that /// might change. -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Error { + ApplicationAlreadyExists, Other(&'static str), Platform(platform::Error), } @@ -29,6 +30,9 @@ pub enum Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match self { + Error::ApplicationAlreadyExists => { + write!(f, "An application instance has already been created.") + } Error::Other(s) => write!(f, "{}", s), Error::Platform(p) => fmt::Display::fmt(&p, f), } diff --git a/druid-shell/src/lib.rs b/druid-shell/src/lib.rs index ed9a26edb0..82496cf1aa 100644 --- a/druid-shell/src/lib.rs +++ b/druid-shell/src/lib.rs @@ -35,6 +35,7 @@ mod keycodes; mod menu; mod mouse; mod platform; +mod util; mod window; pub use application::{AppHandler, Application}; diff --git a/druid-shell/src/platform/gtk/application.rs b/druid-shell/src/platform/gtk/application.rs index 39895c4d77..575d74f182 100644 --- a/druid-shell/src/platform/gtk/application.rs +++ b/druid-shell/src/platform/gtk/application.rs @@ -20,9 +20,11 @@ use gio::prelude::ApplicationExtManual; use gio::{ApplicationExt, ApplicationFlags, Cancellable}; use gtk::{Application as GtkApplication, GtkApplicationExt}; +use crate::application::AppHandler; + use super::clipboard::Clipboard; +use super::error::Error; use super::util; -use crate::application::AppHandler; // XXX: The application needs to be global because WindowBuilder::build wants // to construct an ApplicationWindow, which needs the application, but @@ -31,10 +33,11 @@ thread_local!( static GTK_APPLICATION: RefCell> = RefCell::new(None); ); -pub struct Application; +#[derive(Clone)] +pub(crate) struct Application; impl Application { - pub fn new(_handler: Option>) -> Application { + pub fn new() -> Result { // TODO: we should give control over the application ID to the user let application = GtkApplication::new( Some("com.github.xi-editor.druid"), @@ -56,10 +59,10 @@ impl Application { .expect("Could not register GTK application"); GTK_APPLICATION.with(move |x| *x.borrow_mut() = Some(application)); - Application + Ok(Application) } - pub fn run(&mut self) { + pub fn run(self, _handler: Option>) { util::assert_main_thread(); // TODO: should we pass the command line arguments? @@ -71,7 +74,7 @@ impl Application { }); } - pub fn quit() { + pub fn quit(&self) { util::assert_main_thread(); with_application(|app| { match app.get_active_window() { @@ -86,7 +89,7 @@ impl Application { }); } - pub fn clipboard() -> Clipboard { + pub fn clipboard(&self) -> Clipboard { Clipboard } diff --git a/druid-shell/src/platform/gtk/window.rs b/druid-shell/src/platform/gtk/window.rs index 0ca8dc6ed4..38e529b1c0 100644 --- a/druid-shell/src/platform/gtk/window.rs +++ b/druid-shell/src/platform/gtk/window.rs @@ -33,7 +33,7 @@ use gtk::{AccelGroup, ApplicationWindow}; use crate::kurbo::{Point, Rect, Size, Vec2}; use crate::piet::{Piet, RenderContext}; -use super::application::with_application; +use super::application::{with_application, Application}; use super::dialog; use super::menu::Menu; use super::util::assert_main_thread; @@ -81,7 +81,7 @@ pub struct WindowHandle { } /// Builder abstraction for creating new windows -pub struct WindowBuilder { +pub(crate) struct WindowBuilder { handler: Option>, title: String, menu: Option, @@ -111,7 +111,7 @@ pub(crate) struct WindowState { } impl WindowBuilder { - pub fn new() -> WindowBuilder { + pub fn new(_app: Application) -> WindowBuilder { WindowBuilder { handler: None, title: String::new(), diff --git a/druid-shell/src/platform/mac/application.rs b/druid-shell/src/platform/mac/application.rs index 4dac5adb62..79fd923343 100644 --- a/druid-shell/src/platform/mac/application.rs +++ b/druid-shell/src/platform/mac/application.rs @@ -16,65 +16,103 @@ #![allow(non_upper_case_globals)] +use std::cell::RefCell; use std::ffi::c_void; +use std::rc::Rc; use cocoa::appkit::{NSApp, NSApplication, NSApplicationActivationPolicyRegular}; -use cocoa::base::{id, nil, YES}; -use cocoa::foundation::NSAutoreleasePool; +use cocoa::base::{id, nil, NO, YES}; +use cocoa::foundation::{NSArray, NSAutoreleasePool}; use lazy_static::lazy_static; use objc::declare::ClassDecl; use objc::runtime::{Class, Object, Sel}; use objc::{class, msg_send, sel, sel_impl}; +use crate::application::AppHandler; + use super::clipboard::Clipboard; +use super::error::Error; use super::util; -use crate::application::AppHandler; static APP_HANDLER_IVAR: &str = "druidAppHandler"; -pub struct Application { +#[derive(Clone)] +pub(crate) struct Application { ns_app: id, + state: Rc>, +} + +struct State { + quitting: bool, } impl Application { - pub fn new(handler: Option>) -> Application { + pub fn new() -> Result { + // macOS demands that we run not just on one thread, + // but specifically the first thread of the app. util::assert_main_thread(); unsafe { let _pool = NSAutoreleasePool::new(nil); - let delegate: id = msg_send![APP_DELEGATE.0, alloc]; - let () = msg_send![delegate, init]; - let state = DelegateState { handler }; - let handler_ptr = Box::into_raw(Box::new(state)); - (*delegate).set_ivar(APP_HANDLER_IVAR, handler_ptr as *mut c_void); let ns_app = NSApp(); - let () = msg_send![ns_app, setDelegate: delegate]; ns_app.setActivationPolicy_(NSApplicationActivationPolicyRegular); - Application { ns_app } + + let state = Rc::new(RefCell::new(State { quitting: false })); + + Ok(Application { ns_app, state }) } } - pub fn run(&mut self) { + pub fn run(self, handler: Option>) { unsafe { + // Initialize the application delegate + let delegate: id = msg_send![APP_DELEGATE.0, alloc]; + let () = msg_send![delegate, init]; + let state = DelegateState { handler }; + let state_ptr = Box::into_raw(Box::new(state)); + (*delegate).set_ivar(APP_HANDLER_IVAR, state_ptr as *mut c_void); + let () = msg_send![self.ns_app, setDelegate: delegate]; + + // Run the main app loop self.ns_app.run(); + + // Clean up the delegate + let () = msg_send![self.ns_app, setDelegate: nil]; + Box::from_raw(state_ptr); // Causes it to drop & dealloc automatically } } - pub fn quit() { - unsafe { - let () = msg_send![NSApp(), terminate: nil]; + pub fn quit(&self) { + if let Ok(mut state) = self.state.try_borrow_mut() { + if !state.quitting { + state.quitting = true; + unsafe { + // We want to queue up the destruction of all our windows. + // Failure to do so will lead to resource leaks. + let windows: id = msg_send![self.ns_app, windows]; + for i in 0..windows.count() { + let window: id = windows.objectAtIndex(i); + let () = msg_send![window, performSelectorOnMainThread: sel!(close) withObject: nil waitUntilDone: NO]; + } + // Stop sets a stop request flag in the OS. + // The run loop is stopped after dealing with events. + let () = msg_send![self.ns_app, stop: nil]; + } + } + } else { + log::warn!("Application state already borrowed"); } } /// Hide the application this window belongs to. (cmd+H) - pub fn hide() { + pub fn hide(&self) { unsafe { - let () = msg_send![NSApp(), hide: nil]; + let () = msg_send![self.ns_app, hide: nil]; } } /// Hide all other applications. (cmd+opt+H) - pub fn hide_others() { + pub fn hide_others(&self) { unsafe { let workspace = class!(NSWorkspace); let shared: id = msg_send![workspace, sharedWorkspace]; @@ -82,7 +120,7 @@ impl Application { } } - pub fn clipboard() -> Clipboard { + pub fn clipboard(&self) -> Clipboard { Clipboard } diff --git a/druid-shell/src/platform/mac/util.rs b/druid-shell/src/platform/mac/util.rs index aa638f2ca3..7d35320b60 100644 --- a/druid-shell/src/platform/mac/util.rs +++ b/druid-shell/src/platform/mac/util.rs @@ -20,7 +20,7 @@ use cocoa::base::{id, nil, BOOL, YES}; use cocoa::foundation::{NSAutoreleasePool, NSString, NSUInteger}; use objc::{class, msg_send, sel, sel_impl}; -/// Panic if not on the main thread.assert_main_thread() +/// Panic if not on the main thread. /// /// Many Cocoa operations are only valid on the main thread, and (I think) /// undefined behavior is possible if invoked from other threads. If so, diff --git a/druid-shell/src/platform/mac/window.rs b/druid-shell/src/platform/mac/window.rs index 9dcd5f30c1..3110b229d5 100644 --- a/druid-shell/src/platform/mac/window.rs +++ b/druid-shell/src/platform/mac/window.rs @@ -41,6 +41,7 @@ use log::{error, info}; use crate::kurbo::{Point, Rect, Size, Vec2}; use crate::piet::{Piet, RenderContext}; +use super::application::Application; use super::dialog; use super::menu::Menu; use super::util::{assert_main_thread, make_nsstring}; @@ -104,7 +105,7 @@ struct ViewState { } impl WindowBuilder { - pub fn new() -> WindowBuilder { + pub fn new(_app: Application) -> WindowBuilder { WindowBuilder { handler: None, title: String::new(), diff --git a/druid-shell/src/platform/web/application.rs b/druid-shell/src/platform/web/application.rs index fa3ddd98ba..1e7da7c465 100644 --- a/druid-shell/src/platform/web/application.rs +++ b/druid-shell/src/platform/web/application.rs @@ -14,21 +14,24 @@ //! Web implementation of features at the application scope. -use super::clipboard::Clipboard; use crate::application::AppHandler; -pub struct Application; +use super::clipboard::Clipboard; +use super::error::Error; + +#[derive(Clone)] +pub(crate) struct Application; impl Application { - pub fn new(_handler: Option>) -> Application { - Application + pub fn new() -> Result { + Ok(Application) } - pub fn run(&mut self) {} + pub fn run(self, _handler: Option>) {} - pub fn quit() {} + pub fn quit(&self) {} - pub fn clipboard() -> Clipboard { + pub fn clipboard(&self) -> Clipboard { Clipboard } diff --git a/druid-shell/src/platform/web/window.rs b/druid-shell/src/platform/web/window.rs index e94427f34b..30b8eaddd3 100644 --- a/druid-shell/src/platform/web/window.rs +++ b/druid-shell/src/platform/web/window.rs @@ -29,6 +29,7 @@ use crate::kurbo::{Point, Rect, Size, Vec2}; use crate::piet::RenderContext; +use super::application::Application; use super::error::Error; use super::keycodes::key_to_text; use super::menu::Menu; @@ -59,7 +60,7 @@ type Result = std::result::Result; const NOMINAL_DPI: f32 = 96.0; /// Builder abstraction for creating new windows. -pub struct WindowBuilder { +pub(crate) struct WindowBuilder { handler: Option>, title: String, cursor: Cursor, @@ -291,7 +292,7 @@ fn setup_web_callbacks(window_state: &Rc) { } impl WindowBuilder { - pub fn new() -> WindowBuilder { + pub fn new(_app: Application) -> WindowBuilder { WindowBuilder { handler: None, title: String::new(), diff --git a/druid-shell/src/platform/windows/application.rs b/druid-shell/src/platform/windows/application.rs index db550505fd..cfb5d0fb87 100644 --- a/druid-shell/src/platform/windows/application.rs +++ b/druid-shell/src/platform/windows/application.rs @@ -14,59 +14,56 @@ //! Windows implementation of features at the application scope. +use std::cell::RefCell; +use std::collections::HashSet; use std::mem; use std::ptr; +use std::rc::Rc; -use winapi::shared::minwindef::HINSTANCE; +use winapi::shared::minwindef::{FALSE, HINSTANCE}; use winapi::shared::ntdef::LPCWSTR; -use winapi::shared::windef::HCURSOR; +use winapi::shared::windef::{HCURSOR, HWND}; +use winapi::shared::winerror::HRESULT_FROM_WIN32; +use winapi::um::errhandlingapi::GetLastError; use winapi::um::shellscalingapi::PROCESS_SYSTEM_DPI_AWARE; use winapi::um::wingdi::CreateSolidBrush; use winapi::um::winuser::{ - DispatchMessageW, GetAncestor, GetMessageW, LoadIconW, PostQuitMessage, RegisterClassW, - TranslateAcceleratorW, TranslateMessage, GA_ROOT, IDI_APPLICATION, MSG, WNDCLASSW, + DispatchMessageW, GetAncestor, GetMessageW, LoadIconW, PostMessageW, PostQuitMessage, + RegisterClassW, TranslateAcceleratorW, TranslateMessage, GA_ROOT, IDI_APPLICATION, MSG, + WNDCLASSW, }; use crate::application::AppHandler; use super::accels; use super::clipboard::Clipboard; +use super::error::Error; use super::util::{self, ToWide, CLASS_NAME, OPTIONAL_FUNCTIONS}; -use super::window::win_proc_dispatch; +use super::window::{self, DS_REQUEST_DESTROY}; -pub struct Application; - -impl Application { - pub fn new(_handler: Option>) -> Application { - Application::init(); - Application - } +#[derive(Clone)] +pub(crate) struct Application { + state: Rc>, +} - pub fn run(&mut self) { - unsafe { - // Handle windows messages - loop { - let mut msg = mem::MaybeUninit::uninit(); - let res = GetMessageW(msg.as_mut_ptr(), ptr::null_mut(), 0, 0); - if res <= 0 { - return; - } - let mut msg: MSG = msg.assume_init(); - let accels = accels::find_accels(GetAncestor(msg.hwnd, GA_ROOT)); - let translated = accels.map_or(false, |it| { - TranslateAcceleratorW(msg.hwnd, it.handle(), &mut msg) != 0 - }); +struct State { + quitting: bool, + windows: HashSet, +} - if !translated { - TranslateMessage(&msg); - DispatchMessageW(&msg); - } - } - } +impl Application { + pub fn new() -> Result { + Application::init()?; + let state = Rc::new(RefCell::new(State { + quitting: false, + windows: HashSet::new(), + })); + Ok(Application { state }) } /// Initialize the app. At the moment, this is mostly needed for hi-dpi. - fn init() { + fn init() -> Result<(), Error> { + // TODO: Report back an error instead of panicking util::attach_console(); if let Some(func) = OPTIONAL_FUNCTIONS.SetProcessDpiAwareness { // This function is only supported on windows 10 @@ -74,14 +71,13 @@ impl Application { func(PROCESS_SYSTEM_DPI_AWARE); // TODO: per monitor (much harder) } } - unsafe { let class_name = CLASS_NAME.to_wide(); let icon = LoadIconW(0 as HINSTANCE, IDI_APPLICATION); let brush = CreateSolidBrush(0xff_ff_ff); let wnd = WNDCLASSW { style: 0, - lpfnWndProc: Some(win_proc_dispatch), + lpfnWndProc: Some(window::win_proc_dispatch), cbClsExtra: 0, cbWndExtra: 0, hInstance: 0 as HINSTANCE, @@ -96,15 +92,73 @@ impl Application { panic!("Error registering class"); } } + Ok(()) } - pub fn quit() { + pub fn add_window(&self, hwnd: HWND) -> bool { + self.state.borrow_mut().windows.insert(hwnd) + } + + pub fn remove_window(&self, hwnd: HWND) -> bool { + self.state.borrow_mut().windows.remove(&hwnd) + } + + pub fn run(self, _handler: Option>) { unsafe { - PostQuitMessage(0); + // Handle windows messages + loop { + let mut msg = mem::MaybeUninit::uninit(); + let res = GetMessageW(msg.as_mut_ptr(), ptr::null_mut(), 0, 0); + if res <= 0 { + if res == -1 { + log::error!( + "GetMessageW failed: {}", + Error::Hr(HRESULT_FROM_WIN32(GetLastError())) + ); + } + break; + } + let mut msg: MSG = msg.assume_init(); + let accels = accels::find_accels(GetAncestor(msg.hwnd, GA_ROOT)); + let translated = accels.map_or(false, |it| { + TranslateAcceleratorW(msg.hwnd, it.handle(), &mut msg) != 0 + }); + if !translated { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + } + } + + pub fn quit(&self) { + if let Ok(mut state) = self.state.try_borrow_mut() { + if !state.quitting { + state.quitting = true; + unsafe { + // We want to queue up the destruction of all our windows. + // Failure to do so will lead to resource leaks + // and an eventual error code exit for the process. + for hwnd in &state.windows { + if PostMessageW(*hwnd, DS_REQUEST_DESTROY, 0, 0) == FALSE { + log::warn!( + "PostMessageW DS_REQUEST_DESTROY failed: {}", + Error::Hr(HRESULT_FROM_WIN32(GetLastError())) + ); + } + } + // PostQuitMessage sets a quit request flag in the OS. + // The actual WM_QUIT message is queued but won't be sent + // until all other important events have been handled. + PostQuitMessage(0); + } + } + } else { + log::warn!("Application state already borrowed"); } } - pub fn clipboard() -> Clipboard { + pub fn clipboard(&self) -> Clipboard { Clipboard } diff --git a/druid-shell/src/platform/windows/util.rs b/druid-shell/src/platform/windows/util.rs index 694e58a294..b231a8d390 100644 --- a/druid-shell/src/platform/windows/util.rs +++ b/druid-shell/src/platform/windows/util.rs @@ -40,8 +40,6 @@ use winapi::um::winbase::{FILE_TYPE_UNKNOWN, STD_ERROR_HANDLE, STD_OUTPUT_HANDLE use winapi::um::wincon::{AttachConsole, ATTACH_PARENT_PROCESS}; use winapi::um::winnt::{FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE}; -use log::error; - use super::error::Error; pub fn as_result(hr: HRESULT) -> Result<(), Error> { @@ -144,9 +142,10 @@ fn load_optional_functions() -> OptionalFunctions { let function_ptr = unsafe { GetProcAddress($lib, cstr.as_ptr()) }; if function_ptr.is_null() { - error!( + log::error!( "Could not load `{}`. Windows {} or later is needed", - name, $min_windows_version + name, + $min_windows_version ); } else { let function = unsafe { mem::transmute::<_, $function>(function_ptr) }; @@ -180,14 +179,14 @@ fn load_optional_functions() -> OptionalFunctions { let mut CreateDXGIFactory2 = None; if shcore.is_null() { - error!("No shcore.dll"); + log::error!("No shcore.dll"); } else { load_function!(shcore, SetProcessDpiAwareness, "8.1"); load_function!(shcore, GetDpiForMonitor, "8.1"); } if user32.is_null() { - error!("No user32.dll"); + log::error!("No user32.dll"); } else { load_function!(user32, GetDpiForSystem, "10"); } diff --git a/druid-shell/src/platform/windows/window.rs b/druid-shell/src/platform/windows/window.rs index d2d9c3a889..0a47a51d39 100644 --- a/druid-shell/src/platform/windows/window.rs +++ b/druid-shell/src/platform/windows/window.rs @@ -47,6 +47,7 @@ use crate::kurbo::{Point, Rect, Size, Vec2}; use crate::piet::{Piet, RenderContext}; use super::accels::register_accel; +use super::application::Application; use super::dcomp::{D3D11Device, DCompositionDevice, DCompositionTarget, DCompositionVisual}; use super::dialog::get_file_dialog_path; use super::error::Error; @@ -67,7 +68,8 @@ extern "system" { } /// Builder abstraction for creating new windows. -pub struct WindowBuilder { +pub(crate) struct WindowBuilder { + app: Application, handler: Option>, title: String, menu: Option, @@ -114,7 +116,7 @@ pub struct WindowHandle { /// A handle that can get used to schedule an idle handler. Note that /// this handle is thread safe. If the handle is used after the hwnd -/// has been destroyed, probably not much will go wrong (the XI_RUN_IDLE +/// has been destroyed, probably not much will go wrong (the DS_RUN_IDLE /// message may be sent to a stray window). #[derive(Clone)] pub struct IdleHandle { @@ -142,6 +144,8 @@ struct WindowState { trait WndProc { fn connect(&self, handle: &WindowHandle, state: WndState); + fn cleanup(&self, hwnd: HWND); + fn window_proc(&self, hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM) -> Option; } @@ -149,6 +153,7 @@ trait WndProc { // State and logic for the winapi window procedure entry point. Note that this level // implements policies such as the use of Direct2D for painting. struct MyWndProc { + app: Application, handle: RefCell, d2d_factory: D2DFactory, dwrite_factory: DwriteFactory, @@ -190,9 +195,9 @@ struct DCompState { } /// Message indicating there are idle tasks to run. -const XI_RUN_IDLE: UINT = WM_USER; +const DS_RUN_IDLE: UINT = WM_USER; -/// Message relaying a request to destroy the window +/// Message relaying a request to destroy the window. /// /// Calling `DestroyWindow` from inside the handler is problematic /// because it will recursively cause a `WM_DESTROY` message to be @@ -202,7 +207,7 @@ const XI_RUN_IDLE: UINT = WM_USER; /// As a solution, instead of immediately calling `DestroyWindow`, we /// send this message to request destroying the window, so that at the /// time it is handled, we can successfully borrow the handler. -const XI_REQUEST_DESTROY: UINT = WM_USER + 1; +pub(crate) const DS_REQUEST_DESTROY: UINT = WM_USER + 1; impl Default for PresentStrategy { fn default() -> PresentStrategy { @@ -322,6 +327,10 @@ impl WndProc for MyWndProc { *self.state.borrow_mut() = Some(state); } + fn cleanup(&self, hwnd: HWND) { + self.app.remove_window(hwnd); + } + #[allow(clippy::cognitive_complexity)] fn window_proc( &self, @@ -761,7 +770,7 @@ impl WndProc for MyWndProc { Some(0) } - XI_REQUEST_DESTROY => { + DS_REQUEST_DESTROY => { unsafe { DestroyWindow(hwnd); } @@ -774,7 +783,7 @@ impl WndProc for MyWndProc { } else { self.log_dropped_msg(hwnd, msg, wparam, lparam); } - None + Some(0) } WM_TIMER => { let id = wparam; @@ -813,7 +822,7 @@ impl WndProc for MyWndProc { } Some(0) } - XI_RUN_IDLE => { + DS_RUN_IDLE => { if let Ok(mut s) = self.state.try_borrow_mut() { let s = s.as_mut().unwrap(); let queue = self.handle.borrow().take_idle_queue(); @@ -834,8 +843,9 @@ impl WndProc for MyWndProc { } impl WindowBuilder { - pub fn new() -> WindowBuilder { + pub fn new(app: Application) -> WindowBuilder { WindowBuilder { + app, handler: None, title: String::new(), menu: None, @@ -879,13 +889,11 @@ impl WindowBuilder { pub fn build(self) -> Result { unsafe { - // Maybe separate registration in build api? Probably only need to - // register once even for multiple window creation. - let class_name = super::util::CLASS_NAME.to_wide(); let dwrite_factory = DwriteFactory::new().unwrap(); let dw_clone = dwrite_factory.clone(); let wndproc = MyWndProc { + app: self.app.clone(), handle: Default::default(), d2d_factory: D2DFactory::new().unwrap(), dwrite_factory: dw_clone, @@ -967,6 +975,7 @@ impl WindowBuilder { if hwnd.is_null() { return Err(Error::NullHwnd); } + self.app.add_window(hwnd); if let Some(accels) = accels { register_accel(hwnd, &accels); @@ -1108,6 +1117,7 @@ pub(crate) unsafe extern "system" fn win_proc_dispatch( }; if msg == WM_NCDESTROY && !window_ptr.is_null() { + (*window_ptr).wndproc.cleanup(hwnd); SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0); mem::drop(Rc::from_raw(window_ptr)); } @@ -1179,7 +1189,7 @@ impl WindowHandle { if let Some(w) = self.state.upgrade() { let hwnd = w.hwnd.get(); unsafe { - PostMessageW(hwnd, XI_REQUEST_DESTROY, 0, 0); + PostMessageW(hwnd, DS_REQUEST_DESTROY, 0, 0); } } } @@ -1446,7 +1456,7 @@ impl IdleHandle { let mut queue = self.queue.lock().unwrap(); if queue.is_empty() { unsafe { - PostMessageW(self.hwnd, XI_RUN_IDLE, 0, 0); + PostMessageW(self.hwnd, DS_RUN_IDLE, 0, 0); } } queue.push(IdleKind::Callback(Box::new(callback))); @@ -1456,7 +1466,7 @@ impl IdleHandle { let mut queue = self.queue.lock().unwrap(); if queue.is_empty() { unsafe { - PostMessageW(self.hwnd, XI_RUN_IDLE, 0, 0); + PostMessageW(self.hwnd, DS_RUN_IDLE, 0, 0); } } queue.push(IdleKind::Token(token)); diff --git a/druid-shell/src/platform/x11/application.rs b/druid-shell/src/platform/x11/application.rs index a1def73d34..2a884cc7df 100644 --- a/druid-shell/src/platform/x11/application.rs +++ b/druid-shell/src/platform/x11/application.rs @@ -20,12 +20,14 @@ use std::sync::Arc; use lazy_static::lazy_static; -use super::clipboard::Clipboard; -use super::window::XWindow; use crate::application::AppHandler; use crate::kurbo::{Point, Rect}; use crate::{KeyCode, KeyModifiers, MouseButton, MouseEvent}; +use super::clipboard::Clipboard; +use super::error::Error; +use super::window::XWindow; + struct XcbConnection { connection: Arc, screen_num: i32, @@ -39,11 +41,12 @@ thread_local! { static WINDOW_MAP: RefCell> = RefCell::new(HashMap::new()); } -pub struct Application; +#[derive(Clone)] +pub(crate) struct Application; impl Application { - pub fn new(_handler: Option>) -> Application { - Application + pub fn new() -> Result { + Ok(Application) } pub(crate) fn add_xwindow(id: u32, xwindow: XWindow) { @@ -59,7 +62,7 @@ impl Application { } // TODO(x11/events): handle mouse scroll events - pub fn run(&mut self) { + pub fn run(self, _handler: Option>) { let conn = XCB_CONNECTION.connection_cloned(); loop { if let Some(ev) = conn.wait_for_event() { @@ -182,11 +185,11 @@ impl Application { } } - pub fn quit() { + pub fn quit(&self) { // No-op. } - pub fn clipboard() -> Clipboard { + pub fn clipboard(&self) -> Clipboard { // TODO(x11/clipboard): implement Application::clipboard log::warn!("Application::clipboard is currently unimplemented for X11 platforms."); Clipboard {} diff --git a/druid-shell/src/platform/x11/window.rs b/druid-shell/src/platform/x11/window.rs index 06d937f8fb..813883c039 100644 --- a/druid-shell/src/platform/x11/window.rs +++ b/druid-shell/src/platform/x11/window.rs @@ -32,7 +32,7 @@ use super::error::Error; use super::menu::Menu; use super::util; -pub struct WindowBuilder { +pub(crate) struct WindowBuilder { handler: Option>, title: String, size: Size, @@ -40,7 +40,7 @@ pub struct WindowBuilder { } impl WindowBuilder { - pub fn new() -> WindowBuilder { + pub fn new(_app: Application) -> WindowBuilder { WindowBuilder { handler: None, title: String::new(), @@ -289,7 +289,7 @@ fn get_visual_from_screen(screen: &xcb::Screen<'_>) -> Option(&self, _callback: F) @@ -307,7 +307,7 @@ impl IdleHandle { } #[derive(Clone, Default)] -pub struct WindowHandle { +pub(crate) struct WindowHandle { window_id: u32, } diff --git a/druid-shell/src/util.rs b/druid-shell/src/util.rs new file mode 100644 index 0000000000..b58aefdf1f --- /dev/null +++ b/druid-shell/src/util.rs @@ -0,0 +1,82 @@ +// Copyright 2020 The xi-editor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Utility functions for determining the main thread. + +use std::mem; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::thread; + +static MAIN_THREAD_ID: AtomicU64 = AtomicU64::new(0); + +#[inline] +fn current_thread_id() -> u64 { + // TODO: Use .as_u64() instead of mem::transmute + // when .as_u64() or something similar gets stabilized. + unsafe { mem::transmute(thread::current().id()) } +} + +/// Assert that the current thread is the registered main thread. +/// +/// # Panics +/// +/// Panics when called from a non-main thread. +pub fn assert_main_thread() { + let thread_id = current_thread_id(); + let main_thread_id = MAIN_THREAD_ID.load(Ordering::Acquire); + if thread_id != main_thread_id { + panic!( + "Main thread assertion failed {} != {}", + thread_id, main_thread_id + ); + } +} + +/// Register the current thread as the main thread. +/// +/// # Panics +/// +/// Panics if the main thread has already been claimed by another thread. +pub fn claim_main_thread() { + let thread_id = current_thread_id(); + let old_thread_id = MAIN_THREAD_ID.compare_and_swap(0, thread_id, Ordering::AcqRel); + if old_thread_id != 0 { + if old_thread_id == thread_id { + log::warn!("The main thread status was already claimed by the current thread."); + } else { + panic!( + "The main thread status has already been claimed by thread {}", + thread_id + ); + } + } +} + +/// Removes the main thread status of the current thread. +/// +/// # Panics +/// +/// Panics if the main thread status is owned by another thread. +pub fn release_main_thread() { + let thread_id = current_thread_id(); + let old_thread_id = MAIN_THREAD_ID.compare_and_swap(thread_id, 0, Ordering::AcqRel); + if old_thread_id == 0 { + log::warn!("The main thread status was already vacant."); + } else if old_thread_id != thread_id { + panic!( + "The main thread status is owned by another thread {}", + thread_id + ); + } +} diff --git a/druid-shell/src/window.rs b/druid-shell/src/window.rs index 28bb2498db..07be4c52b2 100644 --- a/druid-shell/src/window.rs +++ b/druid-shell/src/window.rs @@ -17,6 +17,7 @@ use std::any::Any; use std::time::Duration; +use crate::application::Application; use crate::common_util::Counter; use crate::dialog::{FileDialogOptions, FileInfo}; use crate::error::Error; @@ -211,9 +212,13 @@ impl WindowHandle { pub struct WindowBuilder(platform::WindowBuilder); impl WindowBuilder { - /// Create a new `WindowBuilder` - pub fn new() -> WindowBuilder { - WindowBuilder(platform::WindowBuilder::new()) + /// Create a new `WindowBuilder`. + /// + /// Takes the [`Application`] that this window is for. + /// + /// [`Application`]: struct.Application.html + pub fn new(app: Application) -> WindowBuilder { + WindowBuilder(platform::WindowBuilder::new(app.platform_app)) } /// Set the [`WinHandler`]. This is the object that will receive diff --git a/druid/src/app.rs b/druid/src/app.rs index ca729ea43f..db6c5e8cb4 100644 --- a/druid/src/app.rs +++ b/druid/src/app.rs @@ -114,21 +114,28 @@ impl AppLauncher { /// Returns an error if a window cannot be instantiated. This is usually /// a fatal error. pub fn launch(mut self, data: T) -> Result<(), PlatformError> { + let app = Application::new()?; + let mut env = theme::init(); if let Some(f) = self.env_setup.take() { f(&mut env, &data); } - let mut state = AppState::new(data, env, self.delegate.take(), self.ext_event_host); - let handler = AppHandler::new(state.clone()); + let mut state = AppState::new( + app.clone(), + data, + env, + self.delegate.take(), + self.ext_event_host, + ); - let mut app = Application::new(Some(Box::new(handler))); for desc in self.windows { let window = desc.build_native(&mut state)?; window.show(); } - app.run(); + let handler = AppHandler::new(state); + app.run(Some(Box::new(handler))); Ok(()) } } @@ -221,7 +228,7 @@ impl WindowDesc { let handler = DruidHandler::new_shared(state.clone(), self.id); - let mut builder = WindowBuilder::new(); + let mut builder = WindowBuilder::new(state.app()); builder.resizable(self.resizable); builder.show_titlebar(self.show_titlebar); diff --git a/druid/src/command.rs b/druid/src/command.rs index 371ebde8b6..2e2c516a53 100644 --- a/druid/src/command.rs +++ b/druid/src/command.rs @@ -122,6 +122,9 @@ pub mod sys { /// should be the id of the window to close. pub const CLOSE_WINDOW: Selector = Selector::new("druid-builtin.close-window"); + /// Close all windows. + pub const CLOSE_ALL_WINDOWS: Selector = Selector::new("druid-builtin.close-all-windows"); + /// The selector for a command to bring a window to the front, and give it focus. /// /// The command's argument should be the id of the target window. diff --git a/druid/src/widget/textbox.rs b/druid/src/widget/textbox.rs index f53c6d2fcd..46d09479aa 100644 --- a/druid/src/widget/textbox.rs +++ b/druid/src/widget/textbox.rs @@ -283,7 +283,7 @@ impl Widget for TextBox { || cmd.selector == crate::commands::CUT) => { if let Some(text) = data.slice(self.selection.range()) { - Application::clipboard().put_string(text); + Application::global().clipboard().put_string(text); } if !self.selection.is_caret() && cmd.selector == crate::commands::CUT { edit_action = Some(EditAction::Delete); diff --git a/druid/src/win_handler.rs b/druid/src/win_handler.rs index 74372b1c9c..2be1cab7b2 100644 --- a/druid/src/win_handler.rs +++ b/druid/src/win_handler.rs @@ -72,6 +72,7 @@ pub(crate) struct AppState { } struct Inner { + app: Application, delegate: Option>>, command_queue: CommandQueue, ext_event_host: ExtEventHost, @@ -118,6 +119,10 @@ impl Windows { fn get_mut(&mut self, id: WindowId) -> Option<&mut Window> { self.windows.get_mut(&id) } + + fn count(&self) -> usize { + self.windows.len() + self.pending.len() + } } impl AppHandler { @@ -128,12 +133,14 @@ impl AppHandler { impl AppState { pub(crate) fn new( + app: Application, data: T, env: Env, delegate: Option>>, ext_event_host: ExtEventHost, ) -> Self { let inner = Rc::new(RefCell::new(Inner { + app, delegate, command_queue: VecDeque::new(), root_menu: None, @@ -145,6 +152,10 @@ impl AppState { AppState { inner } } + + pub(crate) fn app(&self) -> Application { + self.inner.borrow().app.clone() + } } impl Inner { @@ -220,7 +231,11 @@ impl Inner { if self.windows.windows.is_empty() { // on mac we need to keep the menu around self.root_menu = win.menu.take(); - //FIXME: on windows we need to shutdown the app here? + // If there are even no pending windows, we quit the run loop. + if self.windows.count() == 0 { + #[cfg(target_os = "windows")] + self.app.quit(); + } } } @@ -262,6 +277,13 @@ impl Inner { } } + /// Requests the platform to close all windows. + fn request_close_all_windows(&mut self) { + for win in self.windows.iter_mut() { + win.handle.close(); + } + } + fn show_window(&mut self, id: WindowId) { if let Some(win) = self.windows.get_mut(id) { win.handle.bring_to_front_and_focus(); @@ -502,7 +524,7 @@ impl AppState { fn handle_cmd(&mut self, target: Target, cmd: Command) { use Target as T; match (target, &cmd.selector) { - // these are handled the same no matter where they come from + // these are handled the same no matter where they come from (_, &sys_cmd::QUIT_APP) => self.quit(), (_, &sys_cmd::HIDE_APPLICATION) => self.hide_app(), (_, &sys_cmd::HIDE_OTHERS) => self.hide_others(), @@ -511,8 +533,9 @@ impl AppState { log::error!("failed to create window: '{}'", e); } } + (_, &sys_cmd::CLOSE_ALL_WINDOWS) => self.request_close_all_windows(), // these should come from a window - // FIXME: we need to be able to open a file without a window handle + // FIXME: we need to be able to open a file without a window handle (T::Window(id), &sys_cmd::SHOW_OPEN_PANEL) => self.show_open_panel(cmd, id), (T::Window(id), &sys_cmd::SHOW_SAVE_PANEL) => self.show_save_panel(cmd, id), (T::Window(id), &sys_cmd::CLOSE_WINDOW) => self.request_close_window(cmd, id), @@ -574,6 +597,10 @@ impl AppState { self.inner.borrow_mut().request_close_window(*id); } + fn request_close_all_windows(&mut self) { + self.inner.borrow_mut().request_close_all_windows(); + } + fn show_window(&mut self, cmd: Command) { let id: WindowId = *cmd .get_object() @@ -582,22 +609,22 @@ impl AppState { } fn do_paste(&mut self, window_id: WindowId) { - let event = Event::Paste(Application::clipboard()); + let event = Event::Paste(self.inner.borrow().app.clipboard()); self.inner.borrow_mut().do_window_event(window_id, event); } fn quit(&self) { - Application::quit() + self.inner.borrow().app.quit() } fn hide_app(&self) { #[cfg(target_os = "macos")] - Application::hide() + self.inner.borrow().app.hide() } fn hide_others(&mut self) { #[cfg(target_os = "macos")] - Application::hide_others() + self.inner.borrow().app.hide_others() } }