Skip to content

Commit

Permalink
Terminate app run loop on Windows when all windows have closed.
Browse files Browse the repository at this point in the history
  • Loading branch information
xStrom committed Apr 12, 2020
1 parent 3ffe5e4 commit e815edb
Show file tree
Hide file tree
Showing 18 changed files with 452 additions and 61 deletions.
7 changes: 4 additions & 3 deletions druid-shell/examples/perftest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use time::Instant;
use piet_common::kurbo::{Line, Rect};
use piet_common::{Color, FontBuilder, Piet, RenderContext, Text, TextLayoutBuilder};

use druid_shell::{Application, KeyEvent, WinHandler, WindowBuilder, WindowHandle};
use druid_shell::{AppState, Application, KeyEvent, WinHandler, WindowBuilder, WindowHandle};

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

fn main() {
let mut app = Application::new(None);
let mut builder = WindowBuilder::new();
let state = AppState::new();
let mut app = Application::new(state.clone(), None);
let mut builder = WindowBuilder::new(state);
let perf_test = PerfTest {
size: Default::default(),
handle: Default::default(),
Expand Down
9 changes: 5 additions & 4 deletions druid-shell/examples/shello.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ use druid_shell::kurbo::{Line, Rect, Vec2};
use druid_shell::piet::{Color, RenderContext};

use druid_shell::{
Application, Cursor, FileDialogOptions, FileSpec, HotKey, KeyEvent, KeyModifiers, Menu,
MouseEvent, SysMods, TimerToken, WinHandler, WindowBuilder, WindowHandle,
AppState, Application, Cursor, FileDialogOptions, FileSpec, HotKey, KeyEvent, KeyModifiers,
Menu, MouseEvent, SysMods, TimerToken, WinHandler, WindowBuilder, WindowHandle,
};

const BG_COLOR: Color = Color::rgb8(0x27, 0x28, 0x22);
Expand Down Expand Up @@ -129,8 +129,9 @@ 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 state = AppState::new();
let mut app = Application::new(state.clone(), None);
let mut builder = WindowBuilder::new(state);
builder.set_handler(Box::new(HelloState::default()));
builder.set_title("Hello example");
builder.set_menu(menubar);
Expand Down
35 changes: 33 additions & 2 deletions druid-shell/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

//! The top-level application type.

use std::sync::atomic::{AtomicBool, Ordering};

use crate::clipboard::Clipboard;
use crate::platform::application as platform;

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

/// The top level application state.
///
/// This helps the application track all the state that it has created,
/// which it later needs to clean up.
#[derive(Clone)]
pub struct AppState(pub(crate) platform::AppState);

impl AppState {
/// Create a new `AppState` instance.
pub fn new() -> AppState {
AppState(platform::AppState::new())
}
}

//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);

// Used to ensure only one Application instance is ever created.
// This may change in the future.
// For more information see https://github.com/xi-editor/druid/issues/771
static APPLICATION_CREATED: AtomicBool = AtomicBool::new(false);

impl Application {
pub fn new(handler: Option<Box<dyn AppHandler>>) -> Application {
Application(platform::Application::new(handler))
/// Create a new `Application`.
///
/// It takes the application `state` and a `handler` which will be used to inform of events.
///
/// Right now only one application can be created. See [druid#771] for discussion.
///
/// [druid#771]: https://github.com/xi-editor/druid/issues/771
pub fn new(state: AppState, handler: Option<Box<dyn AppHandler>>) -> Application {
if APPLICATION_CREATED.compare_and_swap(false, true, Ordering::AcqRel) {
panic!("The Application instance has already been created.");
}
Application(platform::Application::new(state.0, handler))
}

/// Start the runloop.
Expand Down
2 changes: 1 addition & 1 deletion druid-shell/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ mod mouse;
mod platform;
mod window;

pub use application::{AppHandler, Application};
pub use application::{AppHandler, AppState, Application};
pub use clipboard::{Clipboard, ClipboardFormat, FormatId};
pub use common_util::Counter;
pub use dialog::{FileDialogOptions, FileInfo, FileSpec};
Expand Down
11 changes: 10 additions & 1 deletion druid-shell/src/platform/gtk/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,19 @@ thread_local!(
static GTK_APPLICATION: RefCell<Option<GtkApplication>> = RefCell::new(None);
);

#[derive(Clone)]
pub struct AppState;

pub struct Application;

impl AppState {
pub(crate) fn new() -> AppState {
AppState
}
}

impl Application {
pub fn new(_handler: Option<Box<dyn AppHandler>>) -> Application {
pub fn new(_state: AppState, _handler: Option<Box<dyn AppHandler>>) -> Application {
// TODO: we should give control over the application ID to the user
let application = GtkApplication::new(
Some("com.github.xi-editor.druid"),
Expand Down
4 changes: 2 additions & 2 deletions druid-shell/src/platform/gtk/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use gtk::{AccelGroup, ApplicationWindow};
use crate::kurbo::{Point, Size, Vec2};
use crate::piet::{Piet, RenderContext};

use super::application::with_application;
use super::application::{with_application, AppState};
use super::dialog;
use super::menu::Menu;
use super::util::assert_main_thread;
Expand Down Expand Up @@ -111,7 +111,7 @@ pub(crate) struct WindowState {
}

impl WindowBuilder {
pub fn new() -> WindowBuilder {
pub fn new(_app_state: AppState) -> WindowBuilder {
WindowBuilder {
handler: None,
title: String::new(),
Expand Down
11 changes: 10 additions & 1 deletion druid-shell/src/platform/mac/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,21 @@ use crate::application::AppHandler;

static APP_HANDLER_IVAR: &str = "druidAppHandler";

#[derive(Clone)]
pub struct AppState;

pub struct Application {
ns_app: id,
}

impl AppState {
pub(crate) fn new() -> AppState {
AppState
}
}

impl Application {
pub fn new(handler: Option<Box<dyn AppHandler>>) -> Application {
pub fn new(_state: AppState, handler: Option<Box<dyn AppHandler>>) -> Application {
util::assert_main_thread();
unsafe {
let _pool = NSAutoreleasePool::new(nil);
Expand Down
3 changes: 2 additions & 1 deletion druid-shell/src/platform/mac/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ use log::{error, info};
use crate::kurbo::{Point, Size, Vec2};
use crate::piet::{Piet, RenderContext};

use super::application::AppState;
use super::dialog;
use super::menu::Menu;
use super::util::{assert_main_thread, make_nsstring};
Expand Down Expand Up @@ -104,7 +105,7 @@ struct ViewState {
}

impl WindowBuilder {
pub fn new() -> WindowBuilder {
pub fn new(_app_state: AppState) -> WindowBuilder {
WindowBuilder {
handler: None,
title: String::new(),
Expand Down
119 changes: 108 additions & 11 deletions druid-shell/src/platform/windows/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,101 @@

//! 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,
DispatchMessageW, GetAncestor, GetMessageW, LoadIconW, PostMessageW, 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_QUIT};

thread_local! {
static GLOBAL_STATE: RefCell<Option<AppState>> = RefCell::new(None);
}

#[derive(Clone)]
pub struct AppState {
state: Rc<RefCell<State>>,
}

struct State {
quitting: bool,
app_hwnd: Option<HWND>,
windows: HashSet<HWND>,
}

pub struct Application;

impl AppState {
pub(crate) fn new() -> AppState {
let state = Rc::new(RefCell::new(State {
quitting: false,
app_hwnd: None,
windows: HashSet::new(),
}));
AppState { state }
}

pub(crate) fn quitting(&self) -> bool {
self.state.borrow().quitting
}

pub(crate) fn set_quitting(&self, quitting: bool) {
self.state.borrow_mut().quitting = quitting;
}

pub(crate) fn app_hwnd(&self) -> Option<HWND> {
self.state.borrow().app_hwnd
}

pub(crate) fn set_app_hwnd(&self, app_hwnd: Option<HWND>) {
self.state.borrow_mut().app_hwnd = app_hwnd;
}

/// Returns a set of `HWND` for all the current normal windows.
///
/// The returned set should be treated with extremely limited lifetime.
/// The window handles it contains can become stale quickly.
#[allow(clippy::mutable_key_type)]
pub(crate) unsafe fn windows(&self) -> HashSet<HWND> {
self.state.borrow().windows.clone()
}

pub(crate) fn add_window(&self, hwnd: HWND) -> bool {
self.state.borrow_mut().windows.insert(hwnd)
}

pub(crate) fn remove_window(&self, hwnd: HWND) -> bool {
self.state.borrow_mut().windows.remove(&hwnd)
}
}

impl Application {
pub fn new(_handler: Option<Box<dyn AppHandler>>) -> Application {
pub fn new(state: AppState, _handler: Option<Box<dyn AppHandler>>) -> Application {
util::claim_main_thread();
GLOBAL_STATE.with(|global_state| {
*global_state.borrow_mut() = Some(state.clone());
});
Application::init();
window::build_app_window(state).expect("Failed to build main message window");
Application
}

Expand All @@ -49,14 +119,19 @@ impl Application {
let mut msg = mem::MaybeUninit::uninit();
let res = GetMessageW(msg.as_mut_ptr(), ptr::null_mut(), 0, 0);
if res <= 0 {
return;
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);
Expand All @@ -67,6 +142,7 @@ impl Application {

/// Initialize the app. At the moment, this is mostly needed for hi-dpi.
fn init() {
util::assert_main_thread();
util::attach_console();
if let Some(func) = OPTIONAL_FUNCTIONS.SetProcessDpiAwareness {
// This function is only supported on windows 10
Expand All @@ -81,7 +157,7 @@ impl 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,
Expand All @@ -99,9 +175,21 @@ impl Application {
}

pub fn quit() {
unsafe {
PostQuitMessage(0);
}
util::assert_main_thread();
GLOBAL_STATE.with(|global_state| {
if let Some(global_state) = global_state.borrow().as_ref() {
if let Some(app_hwnd) = global_state.app_hwnd() {
unsafe {
if PostMessageW(app_hwnd, DS_REQUEST_QUIT, 0, 0) == FALSE {
log::error!(
"PostMessageW failed: {}",
Error::Hr(HRESULT_FROM_WIN32(GetLastError()))
);
}
}
}
}
});
}

pub fn clipboard() -> Clipboard {
Expand All @@ -113,3 +201,12 @@ impl Application {
"en-US".into()
}
}

impl Drop for Application {
fn drop(&mut self) {
GLOBAL_STATE.with(|global_state| {
*global_state.borrow_mut() = None;
});
util::release_main_thread();
}
}
3 changes: 3 additions & 0 deletions druid-shell/src/platform/windows/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ pub enum Error {
OldWindows,
/// The `hwnd` pointer was null.
NullHwnd,
/// The main app message window already exists.
AppWindowExists,
}

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

0 comments on commit e815edb

Please sign in to comment.