rust-windowing / rust-windowing/winit
A design for callbacks
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 6.2k
- Forks
- 1.3k
- Avg merge
- 2d 19h
- Merged PRs (30d)
- 9
Description
We've moved to ApplicationHandler with the primary motivation that to properly handle some events, you must synchronously provide a response to the event. See https://github.com/rust-windowing/winit/issues/3432 for a list of cases where it makes sense.
The idea then was that we'd split WindowEvent out into methods on ApplicationHandler, possibly something like this:
trait ApplicationHandler {
// Handle both `WindowEvent::SurfaceResized` and `WindowEvent::ScaleFactorChanged`
fn surface_configured(&mut self, new_scale_factor: f32, requested_size: Size) -> Size {
let _ = new_scale_factor;
requested_size // By default, resize to the requested size.
}
// ^ Could also be a good way to support resize increments.
// etc. ...
}
No matter much much that would probably be the most "rusty" solution, I don't think we should go down this route if we can avoid it; the feedback we got from users on this was... not positive to state it mildly.
We still need to allow responding synchronously, so what are we to do? Do we keep doing the SurfaceSizeWriter w. Weak<Mutex<STATE>>, and just extend that to the other events? Do we re-add a lifetime to events?
An alternative
I have an idea for an alternative solution. We keep WindowEvents, but we modify methods on Window such that they work differently when called while synchronously handling the corresponding event.
struct Window {
surface_size: PhysicalSize<u32>,
/// The event loop is currently handling a resize.
requested_new_size: Option<PhysicalSize<u32>>,
// ...
}
impl Window {
pub fn surface_size(&mut self) -> PhysicalSize<u32> {
if let Some(requested_new_size) = self.requested_new_size {
// Act as-if the size was applied immediately
// (even though it may actually only be so after the event is finished).
requested_size
} else {
// Return what we'd usually return.
self.surface_size
}
}
pub fn request_surface_size(&mut self, size: Size) -> Option<PhysicalSize<u32>> {
if let Some(requested_new_size) = self.requested_new_size {
// If we're currently in a resize event provide the new value to the event loop.
self.requested_new_size = Some(size.to_physical(self.scale_factor())); // With new scale factor
Some(self.surface_size) // Or maybe `None`? Depends on how we think users should handle it.
} else {
// Or whatever the backend usually does.
self.surface_size = size.to_physical(self.scale_factor());
Some(self.surface_size)
}
}
}
impl EventLoop {
fn run<F>(&self, callback: F) {
loop {
match receive_platform_event() {
DpiChanged(window, new_scale_factor) => {
window.requested_new_size = Some(window.surface_size / old_scale_factor * new_scale_factor);
window.scale_factor = new_scale_factor;
// Synchronously ask the user to handle the event.
app.window_event(WindowEvent::ScaleFactorChanged { new_scale_factor });
let new_size = window.requested_new_size.expect("new size set");
// Respond to platform DPI change event with `new_size` value
window.requested_new_size = None; // Reset extra state
}
// ...
}
}
}
}
A downside is that if users bundle up events and only handle them at the end of the event loop (like e.g. Bevy is currently doing), they are not going to be able to do synchronous resizes. But it's not like we can really prevent them from doing that anyhow; even if all we exposed were callback methods, users can still wrap that in an enum of their own.
No default value
An important insight is that all events have reasonable defaults (yes, even surface creation, you can choose to not create a surface). That is, there is something reasonable for Winit to do in the case where no value was given back. This is part of what makes this design attractive.
If this assumption turns out to not hold, there is always the option of panicking if the user didn't call the "callback method" while handling the event. I'm not aware of a situation where we'll need this, but there might be cases?
While researching this, I see it was proposed previously (I guess there really is nothing new under the sun when it comes to windowing).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the existing ApplicationHandler, WindowEvent, Window, and SurfaceSizeWriter APIs, then review the linked discussions on synchronous event responses and callback designs. Compare the proposed stateful Window behavior with the current event-loop handling. Done means reaching agreement on a viable API design and its default-response semantics; this issue does not name implementation files or tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- desktop
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100