rust-windowing / rust-windowing/winit

A design for callbacks

Open
#4,536 6 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

C - nominated S - api
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.