xilem
xilem copied to clipboard
Multiple windows; async communication?
Summary
It is probably worth thinking about how to support multiple windows now, even if not implementing yet.
Multiple windows and worker threads may need to communicate, with shared data.
Status quo
An App
is a struct over a data: T
and app_logic: impl FnMut(&mut T) -> V + Send + 'static where V: View
; data is stored in an AppTask
which is spawned to its own thread. AppLauncher
is a wrapper around one App
and one window handle.
async
is used internally but there does not appear to be support for e.g. user worker threads. The app_logic
is run once per frame and can in theory poll futures and channels, but can only be woken by a "window event" or accessibility event.
Possibilities: multiple monitors
One app_logic
No support for multiple windows except as children of a single App
: a View
constructs one master window but may also have child (modal?) windows.
One data, multiple app_logics
Each window has its own app_logic
method, but shares data. This requires some redesign; possibly a single AppTask
contains multiple app_logic
methods.
Multiple (data, app_logic) pairs
Add a wrapper, Window
, around App
; allow multiple in AppLauncher
. Each window has its own data and (task) thread. Data can be an Arc<..>
or contain channels allowing inter-window communication.
Possibilities: inter-window/app/logic communication
To allow an app_logic
to launch a worker thread, this probably implies that the method needs an input parameter which can do one of the following:
- construct a
Waker
(or some proxy which can be used to construct a waker) - (best option?) spawn a future
To allow update to data on completion of a future, support one of the following:
- Only run
app_logic
(re-render); user must use poll channels and/or use shared data mechanisms - Add a method like
update: impl FnMut(&mut T, M)
over a user-defined data typeM
, with spawned futures returning amessage: M
Note
The above also prompts the question of whether app_logic
should be a method or a trait impl.
I've given a bit of thought to this. My most useful inspiration has been SwiftUI, which addresses this problem with the WindowGroup mechanism.
So generally I'd vote for a single app_logic (this is why it's not called "window_logic") that is generally a container of multiple windows at the top level. It is incredibly important for windows to retain stable identity, so I can see the primary containers being statically typed tuples and a dynamic map using an app-managed Id
as the key.
Having an app_logic per window might be slightly nicer in uses cases where the windows are more or less independent, but considerably less pleasant when they are coordinating.
macOS has an additional twist, which is that it considers tabs to be windows that simply happen to be hosted inside an another app window. That makes certain things easier (in particular, tabs can be torn off to separate windows or the reverse without much having to bother the app), but raises extreme complications for Xilem, as having one window host another window basically requires coordination with the compositor, and that's not on our roadmap any time soon.
Now is a good time to be exploring this, to avoid painting ourselves in a corner where retrofitting multiple windows would be hard. It's a painful transition for a number of UI toolkits.
Another case that's worth bringing up is the possibility of an app with zero windows. I think that case also points towards the direction of an "app logic" that exists independently of a window.
macOS has an additional twist, which is that it considers tabs to be windows that simply happen to be hosted inside an another app window.
If Xilem is to be a cross-platform toolkit then you either need to emulate this pattern on all platforms or ignore it and draw your own tabs on MacOS. Winit already supports child windows on Windows and X11, so emulating this behaviour on all platforms is probably feasible.
an app with zero windows
That would require some method of running "app logic" besides "redraw the window".
Also, you probably want to be able to redraw one window without re-constructing every window's view tree (especially if a tab is considered a window since you could have a lot).
So... rename app_logic
→ window_logic
, add another layer for app logic and the ability to add windows at run-time? Or have one app_logic
method construct all view trees, but with some filter specifying whether each window needs an update?
Having an app_logic per window might be slightly nicer in uses cases where the windows are more or less independent, but considerably less pleasant when they are coordinating.
Not sure I agree. Some examples of multi-window apps (ignoring hidden windows which hopefully most apps can completely ignore):
- Web browsers. Though most now use multiple processes so really they're multiple apps.
- Some text editors/IDEs. Windows are independent document editors that just happen to run in the same app. (A few things like the project configuration/document list are shared.)
- GIMP. But no modern app does this for good reason.
- Some IDEs/image editors/.. with internal tool palettes — according to desktop behaviour these apps use one window, but the palettes could be modelled internally as windows.
- Modal dialogs
The first two don't have much inter-window coordination; the next two only really need to send messages between windows and modal dialogs might be handled by resolving a Future
.
The first two don't have much inter-window coordination
That's not entirely true. Browser windows/tabs can talk to each other via JavaScript if one opens the other.
So... rename app_logic → window_logic, add another layer for app logic and the ability to add windows at run-time? Or have one app_logic method construct all view trees, but with some filter specifying whether each window needs an update?
I don't think I'd want the global "app logic" responsible for rendering. I'd want it to handle opening/closing windows and be responsible for some global state management / event processing. I guess that means that you want additionally "window logic" (1 per window) to handle the window specific things (which would include rendering in my mind).
As Raph, i am in favor of a single app_logic closure which builds some kind of WindowGroup
.
The first two don't have much inter-window coordination; the next two only really need to send messages between windows and modal dialogs might be handled by resolving a Future.
Still some form of manual synchronization is needed. If there is only one View tree, you can't forget to send a state update message.
Or have one app_logic method construct all view trees, but with some filter specifying whether each window needs an update?
This would be the task of the Memoize
view.
macOS has an additional twist, which is that it considers tabs to be windows that simply happen to be hosted inside an another app window. That makes certain things easier (in particular, tabs can be torn off to separate windows or the reverse without much having to bother the app), but raises extreme complications for Xilem, as having one window host another window basically requires coordination with the compositor, and that's not on our roadmap any time soon.
In our case it probably makes more sense to move the Tab
views between the windows instead of having a window for each Tab
.