linebender / xilem

An experimental Rust native UI framework
Apache License 2.0
3.58k stars 113 forks source link

Multiple windows; async communication? #50

Open dhardy opened 1 year ago

dhardy commented 1 year ago

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:

To allow update to data on completion of a future, support one of the following:

  1. Only run app_logic (re-render); user must use poll channels and/or use shared data mechanisms
  2. Add a method like update: impl FnMut(&mut T, M) over a user-defined data type M, with spawned futures returning a message: M

Note

The above also prompts the question of whether app_logic should be a method or a trait impl.

raphlinus commented 1 year ago

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.

nicoburns commented 1 year ago

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.

dhardy commented 1 year ago

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_logicwindow_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):

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.

nicoburns commented 1 year ago

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).

xarvic commented 1 year ago

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.