Terminate app run loop on Windows when all windows have closed.#763
Terminate app run loop on Windows when all windows have closed.#763xStrom merged 3 commits intolinebender:masterfrom
Conversation
cmyr
left a comment
There was a problem hiding this comment.
Happy to have this addressed.
I'm a bit curious about the decision to do this entirely in druid-shell, it seems like maybe a very slight mixing of concerns? Although I also appreciate this is how gtk does it, but the gtk backend is rougher in a variety of ways.
I'd also be more inclined to have this in druid, as druid-shell is mostly mechanism, leaving policy to the higher levels, but there is a case to be made for doing it in shell: it's behavior that's platform specific, and doing it this way makes all platforms behave like mac. |
7144b4a to
3201797
Compare
|
I moved the decision to exit the run loop into I also fixed the I also made a bit of a style change for the few windows |
| 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")] | ||
| Application::quit(); | ||
| } |
There was a problem hiding this comment.
This windows.windows.is_empty() vs windows.count() == 0 situation has me wondering a bit still. I think the additional check for pending windows makes sense for exiting, because if there is a pending window it'll very shortly be a real window, right?
I did some minor looking for the macOS menu implementation but couldn't quickly figure out whether or where does the menu get assigned back to the window that is pending right now. Perhaps you can enlighten me a bit @cmyr.
There was a problem hiding this comment.
The window that is pending will create it's own menu. I don't think it's an issue that this menu sticks around, although it isn't helpful that it does.
This is all about handling the fact that on macOS if you close the last window you still get a menu. In druid we associate menus with windows; this stashes the menu of the last window when closing the last window.
There was a problem hiding this comment.
Okay, so this self.root_menu is never visible if there are actual windows, even if they are minimized/hidden?
There was a problem hiding this comment.
that is currently the case. I don't think this is the best way to do this long-term, but it was an expedient way to make sure that menus were still available after the last window had been closed on macOS.
cmyr
left a comment
There was a problem hiding this comment.
Okay this feels like it is moving in the right direction, but I have some lingering concerns and questions; in particular it doesn't feel quite right that druid-shell calls back in to druid to request that windows are closed; I think druid should be able to ensure that all windows are closed before requesting termination, if that is necessary?
druid-shell/src/application.rs
Outdated
| /// in the future. | ||
| pub trait AppHandler { | ||
| /// Closes all the windows. | ||
| fn close_all_windows(&mut self) {} |
There was a problem hiding this comment.
when do we expect this to be called? Assuming we get a termination request from the OS, there's a bunch of subtlety; for instance if the user has unsaved changes, do we prompt them to save them? Can the user cancel, and so cancel the request? etc.
There was a problem hiding this comment.
This function is meant to just close all windows no questions asked. I agree that it's confusing, I also found a bunch of the existing closing related code confusing. I think we should introduce some naming conventions to differentiate these things. Perhaps close for the path that works like the user has pressed the titlebar corner X button and destory for the lower path that won't ask any questions and is determined to actually get rid of the window.
There was a problem hiding this comment.
I think this convention makes sense, and I agree that the existing closing related code is confusing.
| // We want to queue up the destruction of all open windows. | ||
| // Failure to do so will lead to resource leaks | ||
| // and an eventual error code exit for the process. | ||
| handler.close_all_windows(); |
There was a problem hiding this comment.
I wonder if we can invert this?
My naive vision: if you want to quit a druid app in windows, the druid layer is responsible for ensuring that all windows get closed, and then we call into druid-shell and ask it to quit.
The inverse here feels difficult. quit in shell should not be the equivalent of a user-initiated quit; it should be a final request to drud-shell to terminate the process. This still feels like druid-shell is implementing policy.
One reason some of this stuff might be a bit weird is that there is a lingering assumption in a lot of this code that there's only ever one window, and a lot of our logic is oriented around that assumption.
There was a problem hiding this comment.
I think the quit in shell should work without issues. Calling it should never lead to resource leaks or errors. If we move the window closing decision to druid then all other users of druid-shell will either have to read the documentation and be sure to do the same, or they will have a resource leak by just using the druid-shell quit.
There was a problem hiding this comment.
It depends what the tradeoffs are.
Basically: if the tradeoff is that by making druid-shell be more difficult to use correctly, we allow druid to have a clearer and more intuitive API, it might be worth it.
druid-shell is hairy. It is 100% possible to misuse. In many ways, it makes sense to think of it as a C API.
If the documentation of quit clearly states that it should only be called when all windows are closed, I think that's reasonable. A compromise might be to have some mechanism in druid-shell to count windows, and then make quit return Result<_>, with an error if windows remain open; and an even better compromise might be to have the Application type become fuller featured, and include handles to open windows etc. But having us call quit and then it calling us back and asking us to close our windows feels like it is equally compromised, and it isn't clear to me that it offers any significant benefit.
There was a problem hiding this comment.
I'll play around with this a bit and see if I can perform the cleanup in another way without calling back to druid. The solution in this PR right now was just the quickest way to do it, as druid was already doing that bookkeeping. However I agree it's a bit spaghetti.
Now more generally, the AppHandler is passed to Application and right now only used for mac root menus. What kind of features do you think would roughly fit AppHandler? It's only calling back to druid right, but if we don't want to do that, when is it correct to use it? For some sort of event passing that isn't associated with any window? Cases where druid-shell is informing and doesn't depend on anyone listening or behaving in any specific way?
There was a problem hiding this comment.
So the genesis here: up until recently, druid-shell was entirely window based. That is, all events were associated with a window, and were delivered to a WinHandler.
This means that if you didn't have any open windows, we had no way to deliver events. In many cases this isn't a huge problem; but it really is on mac, which expects the application to remain open even without any windows.
So AppHandler is an attempt to introduce some type 'above' WinHandler that receives events at the 'application scope'.
Currently, yes, this is really just doing menus on mac. I could definitely imagine it having other roles; in particular it might make sense that the AppHandler gets notified when windows are added or removed (there is currently an AppDelegate for this, which predates AppHandler, and could perhaps be removed) and it may also make sense for AppHandler to receive certain system level events where it doesn't make sense to deliver them to a particular window. I can't think of great examples right now, but one might be if your app is registered to be able to open some type of file, and the user double clicks one of those files; we might notify your AppHandler at launch?
In any case this didn't get a ton of long-term planning; it felt like something we would eventually need if we wanted to support windowless applications, and it was something that I clearly needed for runebender on mac.
raphlinus
left a comment
There was a problem hiding this comment.
Just a couple of comments. Overall I think the approach is valid and really want to see a solution land :)
| if !translated { | ||
| TranslateMessage(&msg); | ||
| DispatchMessageW(&msg); | ||
| // We check for DS_REQUEST_QUIT here because thread messages |
There was a problem hiding this comment.
I'm uneasy about adding more logic in the runloop, as we're not always in control. When there's a modal dialog, or when the window is being resized, we're at the mercy of the system's runloop. This is why I prefer logic to be in the wndproc.
I'm not sure whether that's a serious concern here, but wanted to indicate my thinking.
There was a problem hiding this comment.
You mean window::win_proc_dispatch? If so, then that only handles messages sent to a window. This is a message that's sent to the thread (i.e. the app) and it doesn't get delivered by DispatchMessageW to any window procedure.
There was a problem hiding this comment.
Ah, right. So what will happen if this is sent during live resize or when a modal dialog is open? My guess would be dropped on the floor.
There was a problem hiding this comment.
Answering my own question, the Microsoft docs confirm this:
Messages sent by PostThreadMessage are not associated with a window. As a general rule, messages that are not associated with a window cannot be dispatched by the DispatchMessage function. Therefore, if the recipient thread is in a modal loop (as used by MessageBox or DialogBox), the messages will be lost. To intercept thread messages while in a modal loop, use a thread-specific hook.
I'm uneasy about the complexity and fragility of posting messages to threads, but don't at the moment have a better idea.
There was a problem hiding this comment.
Indeed, it's a problem. I guess the next best thing is to use our own flag. 🤔
There was a problem hiding this comment.
Perhaps a message-only window could be the solution. I'm assuming that still receives its messages while a modal loop is active. I'll look deeper into it and try out some code.
There was a problem hiding this comment.
Yeah, I think the concept of message-only window has come up in other contexts (maybe for system tray apps). I definitely support looking into it. I'd even (personally) be fine with merging this as an interim solution and having an issue open for migration to message-only.
| } | ||
|
|
||
| pub fn run(&mut self) { | ||
| claim_main_thread(); |
There was a problem hiding this comment.
I'm a little unclear what the motivation for this is; it seems orthogonal to quit logic. I'm also concerned about safety implications if the main thread is changed; right now we don't require Sync or Send on the wrapped wndproc, but if the wndproc can be called from a different thread than the one it was created on, it could violate soundness.
There was a problem hiding this comment.
The motivation is to know the thread id where to send DS_REQUEST_QUIT (or even PostQuitMessage for completeness). The Application::quit() function is just a static associated function and operates on global state.
There was a problem hiding this comment.
Ah, I see. I was confused by the naming; I thought it was changing what Windows considered the main thread, but on re-read I see that isn't the case.
There was a problem hiding this comment.
Yeah I just mimicked what the other platforms do with util::assert_main_thread().
690832d to
e815edb
Compare
|
I rewrote the implementation. Now druid-shell is self-sufficient in its capability of correctly quitting on Windows. Druid simply calls I added a new I also introduced a message-only window on Windows. This can be considered the application message loop for druid-shell purposes. It is created first, dies last, and is invisible to druid. This apporach also makes it all work even during a modal loop. |
119c513 to
3b3c4e7
Compare
cmyr
left a comment
There was a problem hiding this comment.
Okay, sorry for letting this sit around.
I think generally this is a better approach, and I could imagine merging it as-is.
The one question that jumps out at me, though, is whether or not Application and AppState is a valid distinction?
Basically: what if there was a single global Application, we ensured it was only created once, and we held a reference to it in druid-shell? And that application could be retrieved with some method like Application::current() or Application::global() or something?
|
I think you're right, having the I have pushed yet another new iteration of this work. I removed the message window from the Windows platform code and perform the quit work inside of I also changed the macOS The GTK/X11 platform code can be improved thanks to the new |
cmyr
left a comment
There was a problem hiding this comment.
Looks good! Some more api discussion, but I think this is the right direction, and is something I've been imagining for a while, happy to see it realized.
druid-shell/src/application.rs
Outdated
| /// This may change in the future. See [druid#771] for discussion. | ||
| /// | ||
| /// [druid#771]: https://github.com/xi-editor/druid/issues/771 | ||
| pub fn new() -> Application { |
There was a problem hiding this comment.
I'm trying to think a bit about what the API should look like in this new world.
Some basic questions:
- should we require explicit initialization, or should we just lazily initialize in
global? - if we do require explicit initialization, should that be a method like
newthat returns the application, or should it be a method likeinitthat returnsResult<(), SomeError>?, and then the actual application is always retrieved viaglobal? - Should
Applicationbe passed by value, or by reference? Havingglobalreturn&'static Application' (and havingApplication: !Clone` perhaps better expresses the semantics of the program, where there is a single shared application).
There was a problem hiding this comment.
About your second bullet (In particular with gtk and perhaps macOS)
I think returning a Result might be good for cases where we are dealing with a remote application that was previously launched. E.g. In the MacOS case there is NSRunningApplication, and in GTK the application is less functional, cannot display windows, but can send signals such as to open a new window to the remote application.
Being able to differentiate these via Result<ApplicationInitializationStatus, SomeError> or such might work for exposing this behavior on both platforms?
There was a problem hiding this comment.
I'll play around a bit with the &'static Application idea and see if and how well I can get it working.
There was a problem hiding this comment.
Okay I ran into a dead end with &'static Application but opened a thread on zulip for it.
Regarding initialization, I favor explicit because it gives clear control over the timing of when this significant work takes place, and if we plan on returning errors here in the future instead of panicking - then also a single place to handle those errors. The global function would always be fast and there wouldn't be a need to reason about whether one should check for initialization errors at every call location.
Naming wise, new vs init, I think new is better if we don't want to start painting ourselves into a strategy corner in terms of #771. That's because I think it makes more sense to have new if this function is going to be called more than once.
Return value wise I think changing it to fn new() -> Result<Application, Error> would be a good upgrade. This would allow the calling app to choose whether to panic or to do something else. Right now druid-shell is a bit too panic friendly in its initialization code.
There was a problem hiding this comment.
I pushed a new commit containing the Result<Application, Error> change.
| unsafe { | ||
| let _pool = NSAutoreleasePool::new(nil); | ||
|
|
||
| let delegate: id = msg_send![APP_DELEGATE.0, alloc]; |
There was a problem hiding this comment.
this stuff was very carefully organized after much trial and error, and based on a close reading of how appkit starts an application; I would leave it as is unless there's a clear problem.
edit: I think I see your rationale; you would like run to be symmetric. Do we imagine a world where run can be called multiple times? Wouldn't you need to call new between calls to quit and run in this case, anyway? I think this probably should be in the run docs, in any case.
There was a problem hiding this comment.
The delegate was previously set after creating the app but before running it - and that is still the case now. I just moved it from new to run. There's no actual change to the order of events though. On my macOS testing it seems to work fine, including the applicationDidFinishLaunching event.
Also yes, there's a clear problem that's behind this move. The new function doesn't have handler anymore.
I can imagine run being called twice by accident, but that will panic on purpose. The check is done in the platform-independent Application::run which also documents this. Also yes, you can't call run on the same exact copy of Application because run consumes self. That's one safe-guard. However because Application is clonable someone might for some bizarre reason call run on that clone elsewhere. That's why there's a check for it which will panic.
cmyr
left a comment
There was a problem hiding this comment.
Okay, I think this is a reasonable compromise!
This PR solves one of the most reported annoyances of druid (Fixes #265, #362, #395, #438, #674, #681). When all windows have been closed on Windows, the
Application::runloop now returns execution back to the host app.