Skip to content
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

Add ApplicationHandler trait #3073

Closed
wants to merge 12 commits into from
Closed

Add ApplicationHandler trait #3073

wants to merge 12 commits into from

Conversation

madsmtm
Copy link
Member

@madsmtm madsmtm commented Aug 30, 2023

Part of #2903.

Builds upon #3056, so see the last commit(s) for the changes in this PR.

Add an ApplicationHandler<T> trait that replaces the Event<T> enum (should probably be kept around for at least one release, but be marked deprecated) by encoding these events as callbacks on a user-specified mutable application object instead.
This allows us to fix many outstanding issues with the current event loop design, including ensuring that windows are only created after initialization, and helping users to properly suspended/resume their application (also here: recreating surfaces and such).

TODO:

  • Figure out what to do about timing.
  • Experiment with returning a closure on suspend, instead of having to manually specify the suspended data.
  • Should we do application initialization differently?
  • How exactly should pump_events work?
  • The user currently has to type a lot compared to the old closure, can we make things even nicer?
  • How do we do platform-specific extensions?
  • Naming things.
  • Test on all platforms that the events are actually delivered in the order we want.
  • Changelog entry and update examples.
  • ...

@madsmtm madsmtm added S - enhancement Wouldn't this be the coolest? S - api Design and usability S - platform parity Unintended platform differences labels Aug 30, 2023
@daxpedda
Copy link
Member

daxpedda commented Sep 2, 2023

Sorry about the delay in #3056, will try to finish it up today.

@notgull
Copy link
Member

notgull commented Sep 2, 2023

Now that I see it in real life, I'm not that sure about this one. It's still possible for users to just keep the Window around regardless of what the type system says. So it's a handful of extra complexity that may not provide strong enough guarantees.

@madsmtm
Copy link
Member Author

madsmtm commented Sep 2, 2023

For my part, this PR is mostly about ensuring that the application is initialized before you try to create windows, which solves sooo many issues on macOS and iOS, and unblocks a lot of my other work (it's really hard to make sure that e.g. fullscreen-ing works correctly when you basically have to work with the window in two states). This would also be possible with other solutions, e.g. nested closures in EventLoop::run, but that would still be ugly.

It's still possible for users to just keep the Window around

True, but there are ways to fix that; one approach could be, drum roll, you guessed it, lifetimes!

Queue the most general example I could conjure:

mod winit {
    pub struct Window<'running> { ... }

    impl<'running> Window<'running> {
        pub fn new(elwt: &'running EventLoopWindowTarget) -> Self { ... }
    }

    pub trait ApplicationHandler<'suspended: 'running, 'running, T = ()> {
        type Suspended: 'suspended;

        // Note that binding the elwt here to `'running` is non-trivial, because then we
        // have to allow it to escape. But should be doable, in one form or another.
        fn resume(suspended: Self::Suspended, elwt: &'running EventLoopWindowTarget) -> Self;

        fn suspend(self) -> Self::Suspended;
    }
}

struct State<'suspended> { ... }

struct App<'suspended, 'running> {
    window: Window<'running>,
    state: State<'suspended>,
}

impl<'suspended: 'running, 'running> ApplicationHandler<'suspended, 'running> for App<'suspended, 'running> {
    type Suspended = State<'suspended>;

    fn resume(
        state: Self::Suspended,
        elwt: &'running EventLoopWindowTarget,
    ) -> Self {
        Self {
            window: Window::new(elwt),
            state,
        }
    }

    fn suspend(self) -> Self::Suspended {
        self.state
    }
}

But whether or not we decide to do that, I still think moving to a trait like this is the way forwards.

(Again, the above is still possible using closures, but it becomes a nested mess. One closure is nice, three nested closures is quite enough thank you very much).

@dhardy
Copy link
Contributor

dhardy commented Sep 2, 2023

The trait in the PR looks usable, but I think I'd just set type Suspended = Self and continue using this:

struct WindowData<S: WindowSurface> {
    window: winit::window::Window,
    #[cfg(all(wayland_platform, feature = "clipboard"))]
    wayland_clipboard: Option<smithay_clipboard::Clipboard>,
    surface: S,
    // ...
}

/// Per-window data
pub struct Window<A: AppData, S: WindowSurface> {
    widget: kas::Window<A>,
    ev_state: EventState,
    // ...
    window: Option<WindowData<S>>,
}

For any app with multiple windows this strategy probably makes more sense than using a separate Suspended type.

Regarding the lifetimes idea, I don't think it enforces anything (unless you are deliberately not passing elwt into other methods, thus forcing users to store a copy in their app state). Regardless, it looks more painful than useful, so please don't.

@madsmtm
Copy link
Member Author

madsmtm commented Sep 2, 2023

The trait in the PR looks usable, but I think I'd just set type Suspended = Self and continue using this:

struct WindowData<S: WindowSurface> {
    window: winit::window::Window,
    #[cfg(all(wayland_platform, feature = "clipboard"))]
    wayland_clipboard: Option<smithay_clipboard::Clipboard>,
    surface: S,
    // ...
}

/// Per-window data
pub struct Window<A: AppData, S: WindowSurface> {
    widget: kas::Window<A>,
    ev_state: EventState,
    // ...
    window: Option<WindowData<S>>,
}

For any app with multiple windows this strategy probably makes more sense than using a separate Suspended type.

The idea was that winit would allow you to know statically that a window has graphics attached; e.g. for your case, it'd look something like this (and you'd avoid the Option around the window data):

struct WindowData<S: WindowSurface{
    window: winit::window::Window,
    #[cfg(all(wayland_platform, feature = "clipboard"))]
    wayland_clipboard: Option<smithay_clipboard::Clipboard>,
    surface: S,
    // ...
}

pub struct Window<A: AppData, S: WindowSurface{
    widget: kas::Window<A>,
    ev_state: EventState,
    // ...
}

struct App {
    windows: Vec<(Window<A, S>, WindowData<S>)>,
}

struct Suspended {
    windows: Vec<Window<A, S>>,
}

Regarding the lifetimes idea, I don't think it enforces anything (unless you are deliberately not passing elwt into other methods, thus forcing users to store a copy in their app state). Regardless, it looks more painful than useful, so please don't.

No I definitely agree, we'd want something that was actually understandable and usable, the idea I sent was just to prove the concept (if you replace the Suspended type with App<'suspended, 'running>;, and fix the suspend/resume methods, it does fail to compile, because the Suspended type must be longer than 'suspended, which in turn must be longer than 'running).
I've edited the comment to make that slightly more clear.


But again, I'm definitely on the pragmatic side here, fixing the Suspend/Resume stuff is not that important to me atm, but there are things this trait would be more useful for moving forwards. So I'm fine with ripping it out, at least in the first iteration.

@dhardy
Copy link
Contributor

dhardy commented Sep 3, 2023

(if you replace the Suspended type with App<'suspended, 'running>;, and fix the suspend/resume methods, it does fail to compile, because the Suspended type must be longer than 'suspended, which in turn must be longer than 'running).

This works with your App, but it's still possible to get around the restriction by using only a single lifetime: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=3d8728a864a3f9f119d77388592d2473

@madsmtm
Copy link
Member Author

madsmtm commented Sep 3, 2023

it's still possible to get around the restriction by using only a single lifetime

Oh yeah... I suspect there's probably still a way to do it, but let's handle that when we get there.

@madsmtm
Copy link
Member Author

madsmtm commented Sep 1, 2024

Replaced by #3517

@madsmtm madsmtm closed this Sep 1, 2024
@madsmtm madsmtm deleted the application-handler branch September 1, 2024 07:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S - api Design and usability S - enhancement Wouldn't this be the coolest? S - platform parity Unintended platform differences
Development

Successfully merging this pull request may close these issues.

4 participants