-
Notifications
You must be signed in to change notification settings - Fork 920
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Platform-specific event callbacks #2120
Comments
The minimal API for Windows would look something like this: impl WindowsEventLoopBuilder for EventLoopBuilder {
pub fn with_callback<F>(&mut self, msg_mask: u32, callback: F)
where
F: FnMut(*mut c_void, u32, usize, usize) -> usize,
{
}
} Although something should probably be done to let end users correlate the |
Thanks @maroider for working on this. I'll settle for having a separate I assume the |
The API you proposed for Windows looks totally reasonable. Maybe it would be better to pass in the WindowId instead of the HWND and provide a platform specific extension trait for getting the HWND from the WindowId. I would be happy to implement this for macos. |
I like the idea of using |
I started implementing something that would allow this on macOS. It can be found at https://github.com/ArturKovacs/winit/tree/macos-any-event The difficulty is that one has to define methods in the application delegate or NSView for the event that one is interested in. To allow the user to do this, we define a new application delegate class during runtime when the user wants to register a new event handler. The API is fairly simple though: pub trait DelegateMethod {
fn register_method<T>(self, sel: Sel, el: &mut PlatformEventLoop<T>) -> Result<(), String>;
}
// Implement DelegateMethod for a bunch of closure types
pub trait EventLoopExtMacOS {
/// Adds a new callback method for the application delegate.
fn add_application_method<F: DelegateMethod>(&mut self, sel: Sel, method: F) -> Result<(), String>;
}
/////////////////////////////////////
// main.rs
use winit::{
platform::macos::{EventLoopExtMacOS, objc::{self, sel, sel_impl}}
};
fn main() {
//...
let mut event_loop = EventLoop::new();
event_loop.add_application_method(
sel!(applicationDidChangeOcclusionState:),
Box::new(|_notification: *mut objc::runtime::Object| {
println!("The occlusion state has changed!");
}) as Box<dyn Fn(_)>
).unwrap();
//...
} |
I agree that this is something that we need, would the intention then be to move e.g. #1759 to a separate crate that depends on I haven't really thought deeply about it but I think I agree with @ArturKovacs for the macOS approach, though with the addition that we move the initialization of |
I've had thoughts along the lines of compiling commonly-wanted functionality that doesn't quite make the cut for inclusion in winit proper into something like a |
This all sounds good. For what it's worth, AccessKit for Mac will need to override some methods on the |
If I understand correctly, this will suffer from the same problem that we discovered with the "file open callback" PR. In short the problem is that it's going to make it very difficult to refer to instances of
Why? As far as I understand only those functions need to be unsafe where the function makes assumptions that aren't guaranteed to be true and that the function doesn't check. As far as I can tell no such assumption is made by this function. |
Ah, right, the callback has to actually be usable... Maybe we could somehow inject the current event loop as a parameter to the closure, but let's defer that discussion to when we get an actual PR up n' running.
Because this is effectively equivalent to In your example, the closure takes |
I'm not sure that it could be done on Wayland at all. The thing is that there's no thing like getting extra events that winit can't handle, since winit asks only for specific events and handle all of them. As for extra events, well, you basically create one more event queue downstream and add things to it and since winit owns the main queue (which dispatches events to your queue) winit will wakeup naturally. But it's not like there's a strong need for that on Wayland as well, so it shouldn't block on anything, since it's platform-specific. |
This is an indirect response to this commentSo this whole platform-specific callback thing has a major usability issue as-is: it's kind of annoying to share state between the platform-specific callback and the main event loop callback. I did initially have a more comprehensive proposal in mind, but in the interest of moving this along, I've chosen to only focus on the simplest one instead. The basic idea is to add another type parameter pub struct EventLoopBuilder<T, S=()> { .. }
impl<T> EventLoopBuilder<T, ()> {
// `init_fn` should be called as late as possible when
// initializing the event loop, but early enough that
// any platform-specific callbacks can receive `&mut S`.
// This lets users stuff a plain `Window` into whichever
// type they specify as `S`, without having to resort to
// `Option<T>` or similar constructs.
pub fn with_custom_state<S, F>(self, init_fn: F) -> EventLoopBuilder<T, S>
where
F: FnOnce(&EventLoopWindowTarget) -> S
{ .. }
}
impl<T, S> EventLoopBuilder<T, S> {
pub fn build(self) -> EventLoop<T, S> { .. }
}
impl<T, S> EventLoopBuilderExtWindows for EventLoopBuilder<T, S> {
// Passing `&mut S` here may be suspect for a couple of reasons:
// 1. `WndProc` is re-entrant
// 2. Users may want to have their callback called while we're inside
// `CreateWindowExW` (since `CreateWindowExW` dispatches some rather
// important events before returning), but this becomes a bit of a
// conundrum if `S` is supposed to contain their one and only window.
// Issue no. 1 should be easy to mitigate with existing checks used on the
// main callback.
pub fn with_callback<F>(self, msg_mask: u32, callback: F)
where
F: FnMut(&mut S, WindowId, u32, usize, usize) -> usize,
{ .... }
} |
FWIW, initializing accessibility inside the call to |
|
Well, the HWND is the first parameter to the window proc itself. Combine that with the Note that winit already handles both |
I've come up with a half-baked proposal for a slightly higher-level API for this. Basically, allow registering A rough sketch of the API (I don't know most of the platform-specific details):pub trait EventListener {
// Whatever event type is produced by this event listener.
type Event;
fn register_macos_callbacks(registrar: MacOSEventRegistrar<Self>) {}
fn register_windows_callbacks(registrar: WindowsEventRegistrar<Self>) {}
// other platforms...
}
// Based on https://github.com/rust-windowing/winit/issues/2120#issuecomment-1003594325.
impl<T: EventListener> MacOSEventRegistrar<T> {
pub fn add_application_method(
sel: Sel,
// The handler gets a mutable reference to the `EventListener`,
// so that it can keep state, and is passed a callback to send an event to the event loop.
// (Note that this uses a callback instead of just returning events so that things like
// `ScaleFactorChanged` with lifetimes will work.)
handler: impl FnMut(&mut T, /* actual callback arguments somehow */, impl FnMut(T::Event)),
) {
/* ... */
}
}
impl<T> EventLoopBuilder<T> {
fn add_event_listener<L>(listener: L)
where
L: EventListener,
// Most of the time, this would probably produce `UserEvent`s.
L::Event: Into<Event<T>>,
{
/* ... */
}
}
// Example event listener:
struct CountHides {
hides: u32,
}
struct HideEvent(u32);
impl From<HideEvent> for Event<HideEvent> {
fn from(e: HideEvent) -> Self {
Event::UserEvent(e)
}
}
impl EventListener for CountHides {
type Event = HideEvent;
fn register_macos_callbacks(registrar: MacOSEventRegistrar<Self>) {
registrar.add_application_method(sel!(applicationDidHide:), |this, _, callback| {
this.hides += 1;
callback(HideEvent(this.hides));
});
}
} Then, all of the builtin callbacks could be switched to enabled-by-default |
I wonder if instead of adding a separate callback (which makes it awkward to handle cross talk between handling platform vs common events) we could instead look at adding a general On Android for example it would be good to be able to emit an event for inset changes (such as when a soft keyboard is shown), or forward This wouldn't be a general purpose, extensible mechanism for integrating things like file descriptors with the event loop but would make it possible for backends to expose window system and life cycle events that are important for a given platform or backend, when it doesn't really make sense to force a generic, platform-agnostic abstraction. |
I agree with this, we don't need callbacks at all, winit is not a callback driven and adding callbacks is just a mess. What we want is a separate event for such things. |
In an attempt to move this forward, I'm going to try to summarize the arguments for using callbacks and their counter-arguments. There are too many kinds of events on some platforms for winit to handleThis only seems to be true for Windows - but on Windows, we can completely sidestep this problem by simply sending Some events need a response from the applicationThe solution is to call the user callback synchronously (The user callback must return before the original event handler returns. An example of this is the On macOS, the behavior is different depending on whether a callback is registered or notThis can be dealt with by different means (for example by having functions like All-in-all I'm in favour of whichever approach - it would just be nice to see this move forward. Let's have a vote for the fun of it:
|
My rationale for event variants: The existing winit event handler callback is the one place in a typical winit-based application that has access to mutable application state, without having to use reference counting and interior mutability. As such, this makes it easier to integrate custom event handling that may modify an application's state, such as lazily and synchronously initializing an accessibility tree, into an existing architecture. In addition to covering custom events on Windows (with an out parameter for the Use of event variants for both custom Win32 messages and custom |
I would suggest custom per platform events, since only windows/x11 has a common source of all events. On Wayland everything is very strict. But for custom events I can just move their any arbitrary stuff into it without bringing such event into other platform. |
Unfortunately, I think I've identified a fatal flaw in the idea of extending winit's existing event handling to cover platform-specific events, particularly those that require synchronous handling with a result, such as the Windows |
One solution for this is to have events that effectively have an out parameter of some kind. The event handling in android-activity (used in the Android backend) is similar to Winits and we have a few situations where it's critical that the event is handled synchronously (e.g. notifying of surface invalidation and requesting that the application save state) and when requesting that the application saves state that also requires data to be provided synchronously via an out parameter. It might not be a typical case (since it may be a large blob of data being saved vs a primitive return value) but maybe it can serve as one point of reference to consider here: https://github.com/rust-mobile/android-activity/blob/6942637c3ccfdb2d02af97fdf174c1aa273c16a8/android-activity/src/lib.rs#L236 Example usage: |
This causes a lot of issues on macOS as well, so I don't think it would be so far-fetched to require at some point. |
One of the reasons behind making all backends emit a Resumed event was to address this too. (Considering that you cant e.g. create a rendering surface on Android before you have a window and it also doesn't make sense to do that while paused either. It wasn't possible to write portable code that could account for this that worked on mobile and desktop systems until we emitted a Resumed event consistently) The Resumed event should probably be delayed if necessary for any platform that is currently emitting it before it's possible to create their first window. |
This will be affected quite vastly by #2903, if we go through with that design |
Web event callbacks can entirely be done externally, no Winit support required. See |
Also, since this PR is talking about a generic sink for events, it could also be exposed via such interface on platforms where it's all supported and it won't be blocked by common API design. |
Issues like #1878, #1928, and #1759 have made it clear that there is a need to hook into platform-specific events that winit itself doesn't (and arguably shouldn't) handle. Ideally, we'd give users the ability to hook into these events with minimal effort, and in a way that doesn't interfere with winit (although some would perhaps like to do that as well).
The simplest solution I can think of to this is to add an
EventLoopBuilder
struct where you can register platform-specific callback functions before the event loop is ever entered. This will make it so that these callbacks don't miss any events, like the file opening events in #1759.Granted, this doesn't lend itself to being a particularly ergonomic API, as any data shared between the platform-specific callbacks and the platform-independent on would have to be behind something like an
Rc<RefCell<T>>
or anArc<Mutex<T>>
. I think there may exist a solution where sharing such state is easier, but I don't want to spend much energy on that ATM.Another issue is that this will (by necessity) make some projects (like AccessKit) end up depending directly on winit or have to provide an adapter crate. Maybe it could be possible to pull a
raw-window-handle
and create some cross-windowing-library interface for this, but I don't think it's ultimately going to be all that feasible.The Windows backend for this should be fairly trivial, so I'll go and implement that soon-ish, unless there are any major concerns with this proposal.
This is on some level related to #2010, but this concerns the public API, rather than the internal one.
The text was updated successfully, but these errors were encountered: