Skip to content

The case for callbacks #2010

@madsmtm

Description

@madsmtm

Hey, I've been tinkering with (primarily) the macOS backend for a while now, and I feel like I've been bit several times by the callback-y nature of AppKit poorly matching the (almost) data-only Event.

So here's my proposal for remedying this.

Background

Most of the underlying system APIs work by letting the user register callbacks on a window, that then get called when an event happens. See the below table for a complete overview:

Library Method
AppKit Multiple callbacks per application/window/view class.
UIKit Same as AppKit.
Windows Single callback per window class.
Web Multiple callbacks per DOM element.
Wayland Multiple callbacks on "seats" (don't really understand this).
X11 Uses an internal event queue that you poll from.
Android Same as X11.
Orbital Same as X11.

winit then turns this into a variant of event::Event, and either calls the event handler directly or buffers it for later (currently buffered events on macOS: WindowEvent except ScaleFactorChanged, DeviceEvent and UserEvent).

The issue

Well, this is honestly a pretty good user-facing API! It's easy to share data between event handlers (since there's only one), and is in general pretty "rusty".

However, it's very much an abstraction, and as with almost any abstraction, we lose some form of control.

One problem is that it's hard for users to know which events are emitted directly / blocking, and hence need to be acted on immediately (like RedrawRequested), and which ones are buffered and can be handled at leisure.

Another is that the user can't directly respond to the events unless we break the assumption that Events are just data, see #1387.

Thirdly, on macOS, the behaviour is different depending on whether a callback is registered or not, see #1759 (comment). So we can't expose this behaviour as an event, because it would negatively affect users that aren't handling the event.

In general, the design forces you to pay for stuff you don't use, which is against the Rust principle of zero-cost abstractions.

The actual proposal

I would like to propose that we change the internals of our backends into having callback-style APIs.

We'd still build the much more user-friendly EventLoop API on top of that, but we'd have the goal of at some point in the future exposing some of these "lower-level" callbacks to user code.

Apart from being a way forward on fixing the above issues, I think it would improve our backend's code quality; one part of the backend would focus on making a safe abstraction (with all the Send and Sync that entails) that matches the OS' callback-based nature, while another could focus on the order events are buffered and how they're dispatched.

Prior art

  • Events Loop 2.0 discussion thread created the current API, but at a glance it didn't seem like this proposal came up.
  • druid-shell, which contains shared functionality with winit, exposes an entirely callback-based API.

Future possibilities

We might be able to split the lower-level callback part of winit off into a separate crate or something (probably still same repository), which might be able to become a shared base between winit and druid-shell? In any case, we should have a way forward on this.

Finishing up

I know that what I'm proposing is somewhat vauge here, please bear with me. If people are on board with this, I'll try (at some point) to spearhead with the macOS implementation, then the benefits might become more apparent.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions