From 834f27bccc0637b57a54459ab536cc49387af9b9 Mon Sep 17 00:00:00 2001
From: Kirill Chibisov <contact@kchibisov.com>
Date: Fri, 23 Feb 2024 14:37:21 +0400
Subject: [PATCH] api: add `ApplicationHandler` and matching run APIs

Add a simple `ApplicationHandler` trait since winit is moving towards
trait based API. Add `run_app` group of APIs to accept `&mut impl
ApplicationHandler` deprecating the old `run` APIs.

Part-of: https://github.com/rust-windowing/winit/issues/3432
---
 CHANGELOG.md                               |   4 +
 examples/control_flow.rs                   | 195 +++++++++--------
 examples/pump_events.rs                    |  73 ++++---
 examples/run_on_demand.rs                  | 116 +++++-----
 examples/window.rs                         | 242 +++++++++++----------
 examples/x11_embed.rs                      |  64 +++---
 src/application.rs                         | 221 +++++++++++++++++++
 src/event.rs                               | 207 +++---------------
 src/event_loop.rs                          |  65 ++++--
 src/lib.rs                                 |  96 ++++----
 src/platform/pump_events.rs                |  40 ++--
 src/platform/run_on_demand.rs              |  47 ++--
 src/platform/web.rs                        |  25 ++-
 src/platform_impl/ios/event_loop.rs        |   2 +-
 src/platform_impl/ios/window.rs            |   2 +-
 src/platform_impl/macos/app_delegate.rs    |   2 +-
 src/platform_impl/web/event_loop/runner.rs |   6 +-
 src/window.rs                              |  33 +--
 18 files changed, 805 insertions(+), 635 deletions(-)
 create mode 100644 src/application.rs

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0f711b3951..774c632b5c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,10 @@ Unreleased` header.
 # Unreleased
 
 - On Wayland, don't reapply cursor grab when unchanged.
+- Deprecate `EventLoop::run` in favor of `EventLoop::run_app`.
+- Deprecate `EventLoopExtRunOnDemand::run_on_demand` in favor of `EventLoop::run_app_on_demand`.
+- Deprecate `EventLoopExtPumpEvents::pump_events` in favor of `EventLoopExtPumpEvents::pump_app_events`.
+- Add `ApplicationHandler<T>` trait which mimics `Event<T>`.
 - Move `dpi` types to its own crate, and re-export it from the root crate.
 - Implement `Sync` for `EventLoopProxy<T: Send>`.
 - **Breaking:** Move `Window::new` to `ActiveEventLoop::create_window` and `EventLoop::create_window` (with the latter being deprecated).
diff --git a/examples/control_flow.rs b/examples/control_flow.rs
index cbfe360a9b..4d1049acbb 100644
--- a/examples/control_flow.rs
+++ b/examples/control_flow.rs
@@ -3,29 +3,30 @@
 use std::thread;
 #[cfg(not(web_platform))]
 use std::time;
+
 #[cfg(web_platform)]
 use web_time as time;
 
-use winit::{
-    event::{ElementState, Event, KeyEvent, WindowEvent},
-    event_loop::{ControlFlow, EventLoop},
-    keyboard::{Key, NamedKey},
-    window::Window,
-};
+use winit::application::ApplicationHandler;
+use winit::event::{ElementState, KeyEvent, StartCause, WindowEvent};
+use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
+use winit::keyboard::{Key, NamedKey};
+use winit::window::{Window, WindowId};
 
 #[path = "util/fill.rs"]
 mod fill;
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+const WAIT_TIME: time::Duration = time::Duration::from_millis(100);
+const POLL_SLEEP_TIME: time::Duration = time::Duration::from_millis(100);
+
+#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
 enum Mode {
+    #[default]
     Wait,
     WaitUntil,
     Poll,
 }
 
-const WAIT_TIME: time::Duration = time::Duration::from_millis(100);
-const POLL_SLEEP_TIME: time::Duration = time::Duration::from_millis(100);
-
 fn main() -> Result<(), impl std::error::Error> {
     tracing_subscriber::fmt::init();
 
@@ -37,96 +38,110 @@ fn main() -> Result<(), impl std::error::Error> {
 
     let event_loop = EventLoop::new().unwrap();
 
-    let mut mode = Mode::Wait;
-    let mut request_redraw = false;
-    let mut wait_cancelled = false;
-    let mut close_requested = false;
+    let mut app = ControlFlowDemo::default();
+    event_loop.run_app(&mut app)
+}
 
-    let mut window = None;
-    event_loop.run(move |event, event_loop| {
-        use winit::event::StartCause;
+#[derive(Default)]
+struct ControlFlowDemo {
+    mode: Mode,
+    request_redraw: bool,
+    wait_cancelled: bool,
+    close_requested: bool,
+    window: Option<Window>,
+}
+
+impl ApplicationHandler for ControlFlowDemo {
+    fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
+        println!("new_events: {cause:?}");
+
+        self.wait_cancelled = match cause {
+            StartCause::WaitCancelled { .. } => self.mode == Mode::WaitUntil,
+            _ => false,
+        }
+    }
+
+    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
+        let window_attributes = Window::default_attributes().with_title(
+            "Press 1, 2, 3 to change control flow mode. Press R to toggle redraw requests.",
+        );
+        self.window = Some(event_loop.create_window(window_attributes).unwrap());
+    }
+
+    fn window_event(
+        &mut self,
+        _event_loop: &ActiveEventLoop,
+        _window_id: WindowId,
+        event: WindowEvent,
+    ) {
         println!("{event:?}");
+
         match event {
-            Event::NewEvents(start_cause) => {
-                wait_cancelled = match start_cause {
-                    StartCause::WaitCancelled { .. } => mode == Mode::WaitUntil,
-                    _ => false,
-                }
-            }
-            Event::Resumed => {
-                let window_attributes = Window::default_attributes().with_title(
-                    "Press 1, 2, 3 to change control flow mode. Press R to toggle redraw requests.",
-                );
-                window = Some(event_loop.create_window(window_attributes).unwrap());
+            WindowEvent::CloseRequested => {
+                self.close_requested = true;
             }
-            Event::WindowEvent { event, .. } => match event {
-                WindowEvent::CloseRequested => {
-                    close_requested = true;
+            WindowEvent::KeyboardInput {
+                event:
+                    KeyEvent {
+                        logical_key: key,
+                        state: ElementState::Pressed,
+                        ..
+                    },
+                ..
+            } => match key.as_ref() {
+                // WARNING: Consider using `key_without_modifiers()` if available on your platform.
+                // See the `key_binding` example
+                Key::Character("1") => {
+                    self.mode = Mode::Wait;
+                    println!("\nmode: {:?}\n", self.mode);
+                }
+                Key::Character("2") => {
+                    self.mode = Mode::WaitUntil;
+                    println!("\nmode: {:?}\n", self.mode);
+                }
+                Key::Character("3") => {
+                    self.mode = Mode::Poll;
+                    println!("\nmode: {:?}\n", self.mode);
                 }
-                WindowEvent::KeyboardInput {
-                    event:
-                        KeyEvent {
-                            logical_key: key,
-                            state: ElementState::Pressed,
-                            ..
-                        },
-                    ..
-                } => match key.as_ref() {
-                    // WARNING: Consider using `key_without_modifiers()` if available on your platform.
-                    // See the `key_binding` example
-                    Key::Character("1") => {
-                        mode = Mode::Wait;
-                        println!("\nmode: {mode:?}\n");
-                    }
-                    Key::Character("2") => {
-                        mode = Mode::WaitUntil;
-                        println!("\nmode: {mode:?}\n");
-                    }
-                    Key::Character("3") => {
-                        mode = Mode::Poll;
-                        println!("\nmode: {mode:?}\n");
-                    }
-                    Key::Character("r") => {
-                        request_redraw = !request_redraw;
-                        println!("\nrequest_redraw: {request_redraw}\n");
-                    }
-                    Key::Named(NamedKey::Escape) => {
-                        close_requested = true;
-                    }
-                    _ => (),
-                },
-                WindowEvent::RedrawRequested => {
-                    let window = window.as_ref().unwrap();
-                    window.pre_present_notify();
-                    fill::fill_window(window);
+                Key::Character("r") => {
+                    self.request_redraw = !self.request_redraw;
+                    println!("\nrequest_redraw: {}\n", self.request_redraw);
+                }
+                Key::Named(NamedKey::Escape) => {
+                    self.close_requested = true;
                 }
                 _ => (),
             },
-            Event::AboutToWait => {
-                if request_redraw && !wait_cancelled && !close_requested {
-                    window.as_ref().unwrap().request_redraw();
-                }
+            WindowEvent::RedrawRequested => {
+                let window = self.window.as_ref().unwrap();
+                window.pre_present_notify();
+                fill::fill_window(window);
+            }
+            _ => (),
+        }
+    }
+
+    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
+        if self.request_redraw && !self.wait_cancelled && !self.close_requested {
+            self.window.as_ref().unwrap().request_redraw();
+        }
 
-                match mode {
-                    Mode::Wait => event_loop.set_control_flow(ControlFlow::Wait),
-                    Mode::WaitUntil => {
-                        if !wait_cancelled {
-                            event_loop.set_control_flow(ControlFlow::WaitUntil(
-                                time::Instant::now() + WAIT_TIME,
-                            ));
-                        }
-                    }
-                    Mode::Poll => {
-                        thread::sleep(POLL_SLEEP_TIME);
-                        event_loop.set_control_flow(ControlFlow::Poll);
-                    }
-                };
-
-                if close_requested {
-                    event_loop.exit();
+        match self.mode {
+            Mode::Wait => event_loop.set_control_flow(ControlFlow::Wait),
+            Mode::WaitUntil => {
+                if !self.wait_cancelled {
+                    event_loop
+                        .set_control_flow(ControlFlow::WaitUntil(time::Instant::now() + WAIT_TIME));
                 }
             }
-            _ => (),
+            Mode::Poll => {
+                thread::sleep(POLL_SLEEP_TIME);
+                event_loop.set_control_flow(ControlFlow::Poll);
+            }
+        };
+
+        if self.close_requested {
+            event_loop.exit();
         }
-    })
+    }
 }
diff --git a/examples/pump_events.rs b/examples/pump_events.rs
index 9e748d43c1..c7c64990cc 100644
--- a/examples/pump_events.rs
+++ b/examples/pump_events.rs
@@ -11,50 +11,59 @@
 fn main() -> std::process::ExitCode {
     use std::{process::ExitCode, thread::sleep, time::Duration};
 
-    use winit::{
-        event::{Event, WindowEvent},
-        event_loop::EventLoop,
-        platform::pump_events::{EventLoopExtPumpEvents, PumpStatus},
-        window::Window,
-    };
+    use winit::application::ApplicationHandler;
+    use winit::event::WindowEvent;
+    use winit::event_loop::{ActiveEventLoop, EventLoop};
+    use winit::platform::pump_events::{EventLoopExtPumpEvents, PumpStatus};
+    use winit::window::{Window, WindowId};
 
     #[path = "util/fill.rs"]
     mod fill;
 
-    let mut event_loop = EventLoop::new().unwrap();
+    #[derive(Default)]
+    struct PumpDemo {
+        window: Option<Window>,
+    }
 
-    tracing_subscriber::fmt::init();
+    impl ApplicationHandler for PumpDemo {
+        fn resumed(&mut self, event_loop: &ActiveEventLoop) {
+            let window_attributes = Window::default_attributes().with_title("A fantastic window!");
+            self.window = Some(event_loop.create_window(window_attributes).unwrap());
+        }
 
-    let mut window = None;
+        fn window_event(
+            &mut self,
+            event_loop: &ActiveEventLoop,
+            _window_id: WindowId,
+            event: WindowEvent,
+        ) {
+            println!("{event:?}");
 
-    loop {
-        let timeout = Some(Duration::ZERO);
-        let status = event_loop.pump_events(timeout, |event, event_loop| {
-            if let Event::WindowEvent { event, .. } = &event {
-                // Print only Window events to reduce noise
-                println!("{event:?}");
-            }
+            let window = match self.window.as_ref() {
+                Some(window) => window,
+                None => return,
+            };
 
             match event {
-                Event::Resumed => {
-                    let window_attributes =
-                        Window::default_attributes().with_title("A fantastic window!");
-                    window = Some(event_loop.create_window(window_attributes).unwrap());
-                }
-                Event::WindowEvent { event, .. } => {
-                    let window = window.as_ref().unwrap();
-                    match event {
-                        WindowEvent::CloseRequested => event_loop.exit(),
-                        WindowEvent::RedrawRequested => fill::fill_window(window),
-                        _ => (),
-                    }
-                }
-                Event::AboutToWait => {
-                    window.as_ref().unwrap().request_redraw();
+                WindowEvent::CloseRequested => event_loop.exit(),
+                WindowEvent::RedrawRequested => {
+                    fill::fill_window(window);
+                    window.request_redraw();
                 }
                 _ => (),
             }
-        });
+        }
+    }
+
+    let mut event_loop = EventLoop::new().unwrap();
+
+    tracing_subscriber::fmt::init();
+
+    let mut app = PumpDemo::default();
+
+    loop {
+        let timeout = Some(Duration::ZERO);
+        let status = event_loop.pump_app_events(timeout, &mut app);
 
         if let PumpStatus::Exit(exit_code) = status {
             break ExitCode::from(exit_code as u8);
diff --git a/examples/run_on_demand.rs b/examples/run_on_demand.rs
index b298fdf8c0..33488c1f9e 100644
--- a/examples/run_on_demand.rs
+++ b/examples/run_on_demand.rs
@@ -2,86 +2,94 @@
 
 // Limit this example to only compatible platforms.
 #[cfg(any(windows_platform, macos_platform, x11_platform, wayland_platform,))]
-fn main() -> Result<(), impl std::error::Error> {
+fn main() -> Result<(), Box<dyn std::error::Error>> {
     use std::time::Duration;
 
-    use winit::{
-        error::EventLoopError,
-        event::{Event, WindowEvent},
-        event_loop::EventLoop,
-        platform::run_on_demand::EventLoopExtRunOnDemand,
-        window::{Window, WindowId},
-    };
+    use winit::application::ApplicationHandler;
+    use winit::event::WindowEvent;
+    use winit::event_loop::{ActiveEventLoop, EventLoop};
+    use winit::platform::run_on_demand::EventLoopExtRunOnDemand;
+    use winit::window::{Window, WindowId};
 
     #[path = "util/fill.rs"]
     mod fill;
 
     #[derive(Default)]
     struct App {
+        idx: usize,
         window_id: Option<WindowId>,
         window: Option<Window>,
     }
 
-    tracing_subscriber::fmt::init();
-    let mut event_loop = EventLoop::new().unwrap();
+    impl ApplicationHandler for App {
+        fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
+            if let Some(window) = self.window.as_ref() {
+                window.request_redraw();
+            }
+        }
+
+        fn resumed(&mut self, event_loop: &ActiveEventLoop) {
+            let window_attributes = Window::default_attributes()
+                .with_title("Fantastic window number one!")
+                .with_inner_size(winit::dpi::LogicalSize::new(128.0, 128.0));
+            let window = event_loop.create_window(window_attributes).unwrap();
+            self.window_id = Some(window.id());
+            self.window = Some(window);
+        }
 
-    fn run_app(event_loop: &mut EventLoop<()>, idx: usize) -> Result<(), EventLoopError> {
-        let mut app = App::default();
+        fn window_event(
+            &mut self,
+            event_loop: &ActiveEventLoop,
+            window_id: WindowId,
+            event: WindowEvent,
+        ) {
+            if event == WindowEvent::Destroyed && self.window_id == Some(window_id) {
+                println!(
+                    "--------------------------------------------------------- Window {} Destroyed",
+                    self.idx
+                );
+                self.window_id = None;
+                event_loop.exit();
+                return;
+            }
 
-        event_loop.run_on_demand(move |event, event_loop| {
-            println!("Run {idx}: {:?}", event);
+            let window = match self.window.as_mut() {
+                Some(window) => window,
+                None => return,
+            };
 
-            if let Some(window) = &app.window {
-                match event {
-                    Event::WindowEvent {
-                        event: WindowEvent::CloseRequested,
-                        window_id,
-                    } if window.id() == window_id => {
-                        println!("--------------------------------------------------------- Window {idx} CloseRequested");
-                        fill::cleanup_window(window);
-                        app.window = None;
-                    }
-                    Event::AboutToWait => window.request_redraw(),
-                    Event::WindowEvent {
-                        event: WindowEvent::RedrawRequested,
-                        ..
-                    }  => {
-                        fill::fill_window(window);
-                    }
-                    _ => (),
+            match event {
+                WindowEvent::CloseRequested => {
+                    println!("--------------------------------------------------------- Window {} CloseRequested", self.idx);
+                    fill::cleanup_window(window);
+                    self.window = None;
                 }
-            } else if let Some(id) = app.window_id {
-                match event {
-                    Event::WindowEvent {
-                        event: WindowEvent::Destroyed,
-                        window_id,
-                    } if id == window_id => {
-                        println!("--------------------------------------------------------- Window {idx} Destroyed");
-                        app.window_id = None;
-                        event_loop.exit();
-                    }
-                    _ => (),
+                WindowEvent::RedrawRequested => {
+                    fill::fill_window(window);
                 }
-            } else if let Event::Resumed = event {
-                let window_attributes = Window::default_attributes()
-                        .with_title("Fantastic window number one!")
-                        .with_inner_size(winit::dpi::LogicalSize::new(128.0, 128.0));
-                let window = event_loop.create_window(window_attributes).unwrap();
-                app.window_id = Some(window.id());
-                app.window = Some(window);
+                _ => (),
             }
-        })
+        }
     }
 
-    run_app(&mut event_loop, 1)?;
+    tracing_subscriber::fmt::init();
+
+    let mut event_loop = EventLoop::new().unwrap();
+
+    let mut app = App {
+        idx: 1,
+        ..Default::default()
+    };
+    event_loop.run_app_on_demand(&mut app)?;
 
     println!("--------------------------------------------------------- Finished first loop");
     println!("--------------------------------------------------------- Waiting 5 seconds");
     std::thread::sleep(Duration::from_secs(5));
 
-    let ret = run_app(&mut event_loop, 2);
+    app.idx += 1;
+    event_loop.run_app_on_demand(&mut app)?;
     println!("--------------------------------------------------------- Finished second loop");
-    ret
+    Ok(())
 }
 
 #[cfg(not(any(windows_platform, macos_platform, x11_platform, wayland_platform,)))]
diff --git a/examples/window.rs b/examples/window.rs
index 3aa7f99096..427ee405e1 100644
--- a/examples/window.rs
+++ b/examples/window.rs
@@ -14,8 +14,9 @@ use rwh_05::HasRawDisplayHandle;
 #[cfg(not(any(android_platform, ios_platform)))]
 use softbuffer::{Context, Surface};
 
+use winit::application::ApplicationHandler;
 use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
-use winit::event::{DeviceEvent, DeviceId, Event, Ime, WindowEvent};
+use winit::event::{DeviceEvent, DeviceId, Ime, WindowEvent};
 use winit::event::{MouseButton, MouseScrollDelta};
 use winit::event_loop::{ActiveEventLoop, EventLoop};
 use winit::keyboard::{Key, ModifiersState};
@@ -53,38 +54,7 @@ fn main() -> Result<(), Box<dyn Error>> {
 
     let mut state = Application::new(&event_loop);
 
-    event_loop.run(move |event, event_loop| match event {
-        Event::NewEvents(_) => (),
-        Event::Resumed => {
-            println!("Resumed the event loop");
-            state.dump_monitors(event_loop);
-
-            // Create initial window.
-            state
-                .create_window(event_loop, None)
-                .expect("failed to create initial window");
-
-            state.print_help();
-        }
-        Event::AboutToWait => {
-            if state.windows.is_empty() {
-                println!("No windows left, exiting...");
-                event_loop.exit();
-            }
-        }
-        Event::WindowEvent { window_id, event } => {
-            state.handle_window_event(event_loop, window_id, event)
-        }
-        Event::DeviceEvent { device_id, event } => {
-            state.handle_device_event(event_loop, device_id, event)
-        }
-        Event::UserEvent(event) => {
-            println!("User event: {event:?}");
-        }
-        Event::Suspended | Event::LoopExiting | Event::MemoryWarning => (),
-    })?;
-
-    Ok(())
+    event_loop.run_app(&mut state).map_err(Into::into)
 }
 
 #[allow(dead_code)]
@@ -104,15 +74,14 @@ struct Application {
     ///
     /// With OpenGL it could be EGLDisplay.
     #[cfg(not(any(android_platform, ios_platform)))]
-    context: Context,
+    context: Option<Context>,
 }
 
 impl Application {
     fn new<T>(event_loop: &EventLoop<T>) -> Self {
-        // SAFETY: the context is dropped inside the loop, since the state we're using
-        // is moved inside the closure.
+        // SAFETY: we drop the context right before the event loop is stopped, thus making it safe.
         #[cfg(not(any(android_platform, ios_platform)))]
-        let context = unsafe { Context::from_raw(event_loop.raw_display_handle()).unwrap() };
+        let context = Some(unsafe { Context::from_raw(event_loop.raw_display_handle()).unwrap() });
 
         // You'll have to choose an icon size at your own discretion. On X11, the desired size varies
         // by WM, and on Windows, you still have to account for screen scaling. Here we use 32px,
@@ -227,7 +196,97 @@ impl Application {
         }
     }
 
-    fn handle_window_event(
+    fn dump_monitors(&self, event_loop: &ActiveEventLoop) {
+        println!("Monitors information");
+        let primary_monitor = event_loop.primary_monitor();
+        for monitor in event_loop.available_monitors() {
+            let intro = if primary_monitor.as_ref() == Some(&monitor) {
+                "Primary monitor"
+            } else {
+                "Monitor"
+            };
+
+            if let Some(name) = monitor.name() {
+                println!("{intro}: {name}");
+            } else {
+                println!("{intro}: [no name]");
+            }
+
+            let PhysicalSize { width, height } = monitor.size();
+            print!("  Current mode: {width}x{height}");
+            if let Some(m_hz) = monitor.refresh_rate_millihertz() {
+                println!(" @ {}.{} Hz", m_hz / 1000, m_hz % 1000);
+            } else {
+                println!();
+            }
+
+            let PhysicalPosition { x, y } = monitor.position();
+            println!("  Position: {x},{y}");
+
+            println!("  Scale factor: {}", monitor.scale_factor());
+
+            println!("  Available modes (width x height x bit-depth):");
+            for mode in monitor.video_modes() {
+                let PhysicalSize { width, height } = mode.size();
+                let bits = mode.bit_depth();
+                let m_hz = mode.refresh_rate_millihertz();
+                println!(
+                    "    {width}x{height}x{bits} @ {}.{} Hz",
+                    m_hz / 1000,
+                    m_hz % 1000
+                );
+            }
+        }
+    }
+
+    /// Process the key binding.
+    fn process_key_binding(key: &str, mods: &ModifiersState) -> Option<Action> {
+        KEY_BINDINGS.iter().find_map(|binding| {
+            binding
+                .is_triggered_by(&key, mods)
+                .then_some(binding.action)
+        })
+    }
+
+    /// Process mouse binding.
+    fn process_mouse_binding(button: MouseButton, mods: &ModifiersState) -> Option<Action> {
+        MOUSE_BINDINGS.iter().find_map(|binding| {
+            binding
+                .is_triggered_by(&button, mods)
+                .then_some(binding.action)
+        })
+    }
+
+    fn print_help(&self) {
+        println!("Keyboard bindings:");
+        for binding in KEY_BINDINGS {
+            println!(
+                "{}{:<10} - {} ({})",
+                modifiers_to_string(binding.mods),
+                binding.trigger,
+                binding.action,
+                binding.action.help(),
+            );
+        }
+        println!("Mouse bindings:");
+        for binding in MOUSE_BINDINGS {
+            println!(
+                "{}{:<10} - {} ({})",
+                modifiers_to_string(binding.mods),
+                mouse_button_to_string(binding.trigger),
+                binding.action,
+                binding.action.help(),
+            );
+        }
+    }
+}
+
+impl ApplicationHandler<UserEvent> for Application {
+    fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) {
+        println!("User event: {event:?}");
+    }
+
+    fn window_event(
         &mut self,
         event_loop: &ActiveEventLoop,
         window_id: WindowId,
@@ -371,92 +430,37 @@ impl Application {
         }
     }
 
-    fn handle_device_event(&mut self, _: &ActiveEventLoop, _: DeviceId, event: DeviceEvent) {
-        println!("Device event: {event:?}");
+    fn device_event(
+        &mut self,
+        _event_loop: &ActiveEventLoop,
+        device_id: DeviceId,
+        event: DeviceEvent,
+    ) {
+        println!("Device {device_id:?} event: {event:?}");
     }
 
-    fn dump_monitors(&self, event_loop: &ActiveEventLoop) {
-        println!("Monitors information");
-        let primary_monitor = event_loop.primary_monitor();
-        for monitor in event_loop.available_monitors() {
-            let intro = if primary_monitor.as_ref() == Some(&monitor) {
-                "Primary monitor"
-            } else {
-                "Monitor"
-            };
-
-            if let Some(name) = monitor.name() {
-                println!("{intro}: {name}");
-            } else {
-                println!("{intro}: [no name]");
-            }
-
-            let PhysicalSize { width, height } = monitor.size();
-            print!("  Current mode: {width}x{height}");
-            if let Some(m_hz) = monitor.refresh_rate_millihertz() {
-                println!(" @ {}.{} Hz", m_hz / 1000, m_hz % 1000);
-            } else {
-                println!();
-            }
-
-            let PhysicalPosition { x, y } = monitor.position();
-            println!("  Position: {x},{y}");
-
-            println!("  Scale factor: {}", monitor.scale_factor());
+    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
+        println!("Resumed the event loop");
+        self.dump_monitors(event_loop);
 
-            println!("  Available modes (width x height x bit-depth):");
-            for mode in monitor.video_modes() {
-                let PhysicalSize { width, height } = mode.size();
-                let bits = mode.bit_depth();
-                let m_hz = mode.refresh_rate_millihertz();
-                println!(
-                    "    {width}x{height}x{bits} @ {}.{} Hz",
-                    m_hz / 1000,
-                    m_hz % 1000
-                );
-            }
-        }
-    }
+        // Create initial window.
+        self.create_window(event_loop, None)
+            .expect("failed to create initial window");
 
-    /// Process the key binding.
-    fn process_key_binding(key: &str, mods: &ModifiersState) -> Option<Action> {
-        KEY_BINDINGS.iter().find_map(|binding| {
-            binding
-                .is_triggered_by(&key, mods)
-                .then_some(binding.action)
-        })
+        self.print_help();
     }
 
-    /// Process mouse binding.
-    fn process_mouse_binding(button: MouseButton, mods: &ModifiersState) -> Option<Action> {
-        MOUSE_BINDINGS.iter().find_map(|binding| {
-            binding
-                .is_triggered_by(&button, mods)
-                .then_some(binding.action)
-        })
+    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
+        if self.windows.is_empty() {
+            println!("No windows left, exiting...");
+            event_loop.exit();
+        }
     }
 
-    fn print_help(&self) {
-        println!("Keyboard bindings:");
-        for binding in KEY_BINDINGS {
-            println!(
-                "{}{:<10} - {} ({})",
-                modifiers_to_string(binding.mods),
-                binding.trigger,
-                binding.action,
-                binding.action.help(),
-            );
-        }
-        println!("Mouse bindings:");
-        for binding in MOUSE_BINDINGS {
-            println!(
-                "{}{:<10} - {} ({})",
-                modifiers_to_string(binding.mods),
-                mouse_button_to_string(binding.trigger),
-                binding.action,
-                binding.action.help(),
-            );
-        }
+    #[cfg(not(any(android_platform, ios_platform)))]
+    fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
+        // We must drop the context here.
+        self.context = None;
     }
 }
 
@@ -496,11 +500,11 @@ struct WindowState {
 }
 
 impl WindowState {
-    fn new(application: &Application, window: Window) -> Result<Self, Box<dyn Error>> {
+    fn new(app: &Application, window: Window) -> Result<Self, Box<dyn Error>> {
         // SAFETY: the surface is dropped before the `window` which provided it with handle, thus
         // it doesn't outlive it.
         #[cfg(not(any(android_platform, ios_platform)))]
-        let surface = unsafe { Surface::new(&application.context, &window)? };
+        let surface = unsafe { Surface::new(app.context.as_ref().unwrap(), &window)? };
 
         let theme = window.theme().unwrap_or(Theme::Dark);
         println!("Theme: {theme:?}");
@@ -515,7 +519,7 @@ impl WindowState {
         let mut state = Self {
             #[cfg(macos_platform)]
             option_as_alt: window.option_as_alt(),
-            custom_idx: application.custom_cursors.len() - 1,
+            custom_idx: app.custom_cursors.len() - 1,
             cursor_grab: CursorGrabMode::None,
             named_idx,
             #[cfg(not(any(android_platform, ios_platform)))]
diff --git a/examples/x11_embed.rs b/examples/x11_embed.rs
index 027e3ba54b..9e77694c3b 100644
--- a/examples/x11_embed.rs
+++ b/examples/x11_embed.rs
@@ -3,38 +3,37 @@ use std::error::Error;
 
 #[cfg(x11_platform)]
 fn main() -> Result<(), Box<dyn Error>> {
-    use winit::{
-        event::{Event, WindowEvent},
-        event_loop::EventLoop,
-        platform::x11::WindowAttributesExtX11,
-        window::Window,
-    };
+    use winit::application::ApplicationHandler;
+    use winit::event::WindowEvent;
+    use winit::event_loop::{ActiveEventLoop, EventLoop};
+    use winit::platform::x11::WindowAttributesExtX11;
+    use winit::window::{Window, WindowId};
 
     #[path = "util/fill.rs"]
     mod fill;
 
-    // First argument should be a 32-bit X11 window ID.
-    let parent_window_id = std::env::args()
-        .nth(1)
-        .ok_or("Expected a 32-bit X11 window ID as the first argument.")?
-        .parse::<u32>()?;
+    pub struct XEmbedDemo {
+        parent_window_id: u32,
+        window: Option<Window>,
+    }
 
-    tracing_subscriber::fmt::init();
-    let event_loop = EventLoop::new()?;
-
-    let mut window = None;
-    event_loop.run(move |event, event_loop| match event {
-        Event::Resumed => {
+    impl ApplicationHandler for XEmbedDemo {
+        fn resumed(&mut self, event_loop: &ActiveEventLoop) {
             let window_attributes = Window::default_attributes()
                 .with_title("An embedded window!")
                 .with_inner_size(winit::dpi::LogicalSize::new(128.0, 128.0))
-                .with_embed_parent_window(parent_window_id);
+                .with_embed_parent_window(self.parent_window_id);
 
-            window = Some(event_loop.create_window(window_attributes).unwrap());
+            self.window = Some(event_loop.create_window(window_attributes).unwrap());
         }
-        Event::WindowEvent { event, .. } => {
-            let window = window.as_ref().unwrap();
 
+        fn window_event(
+            &mut self,
+            event_loop: &ActiveEventLoop,
+            _window_id: WindowId,
+            event: WindowEvent,
+        ) {
+            let window = self.window.as_ref().unwrap();
             match event {
                 WindowEvent::CloseRequested => event_loop.exit(),
                 WindowEvent::RedrawRequested => {
@@ -44,13 +43,26 @@ fn main() -> Result<(), Box<dyn Error>> {
                 _ => (),
             }
         }
-        Event::AboutToWait => {
-            window.as_ref().unwrap().request_redraw();
+
+        fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
+            self.window.as_ref().unwrap().request_redraw();
         }
-        _ => (),
-    })?;
+    }
 
-    Ok(())
+    // First argument should be a 32-bit X11 window ID.
+    let parent_window_id = std::env::args()
+        .nth(1)
+        .ok_or("Expected a 32-bit X11 window ID as the first argument.")?
+        .parse::<u32>()?;
+
+    tracing_subscriber::fmt::init();
+    let event_loop = EventLoop::new()?;
+
+    let mut app = XEmbedDemo {
+        parent_window_id,
+        window: None,
+    };
+    event_loop.run_app(&mut app).map_err(Into::into)
 }
 
 #[cfg(not(x11_platform))]
diff --git a/src/application.rs b/src/application.rs
new file mode 100644
index 0000000000..acb5af0979
--- /dev/null
+++ b/src/application.rs
@@ -0,0 +1,221 @@
+//! End user application handling.
+
+use crate::event::{DeviceEvent, DeviceId, StartCause, WindowEvent};
+use crate::event_loop::ActiveEventLoop;
+use crate::window::WindowId;
+
+/// The handler of the application events.
+pub trait ApplicationHandler<T: 'static = ()> {
+    /// Emitted when new events arrive from the OS to be processed.
+    ///
+    /// This is a useful place to put code that should be done before you start processing
+    /// events, such as updating frame timing information for benchmarking or checking the
+    /// [`StartCause`] to see if a timer set by
+    /// [`ControlFlow::WaitUntil`](crate::event_loop::ControlFlow::WaitUntil) has elapsed.
+    fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
+        let _ = (event_loop, cause);
+    }
+
+    /// Emitted when the application has been resumed.
+    ///
+    /// For consistency, all platforms emit a `Resumed` event even if they don't themselves have a
+    /// formal suspend/resume lifecycle. For systems without a formal suspend/resume lifecycle
+    /// the `Resumed` event is always emitted after the [`NewEvents(StartCause::Init)`][StartCause::Init]
+    /// event.
+    ///
+    /// # Portability
+    ///
+    /// It's recommended that applications should only initialize their graphics context and create
+    /// a window after they have received their first `Resumed` event. Some systems
+    /// (specifically Android) won't allow applications to create a render surface until they are
+    /// resumed.
+    ///
+    /// Considering that the implementation of [`Suspended`] and `Resumed` events may be internally
+    /// driven by multiple platform-specific events, and that there may be subtle differences across
+    /// platforms with how these internal events are delivered, it's recommended that applications
+    /// be able to gracefully handle redundant (i.e. back-to-back) [`Suspended`] or `Resumed` events.
+    ///
+    /// Also see [`Suspended`] notes.
+    ///
+    /// ## Android
+    ///
+    /// On Android, the `Resumed` event is sent when a new [`SurfaceView`] has been created. This is
+    /// expected to closely correlate with the [`onResume`] lifecycle event but there may technically
+    /// be a discrepancy.
+    ///
+    /// [`onResume`]: https://developer.android.com/reference/android/app/Activity#onResume()
+    ///
+    /// Applications that need to run on Android must wait until they have been `Resumed`
+    /// before they will be able to create a render surface (such as an `EGLSurface`,
+    /// [`VkSurfaceKHR`] or [`wgpu::Surface`]) which depend on having a
+    /// [`SurfaceView`]. Applications must also assume that if they are [`Suspended`], then their
+    /// render surfaces are invalid and should be dropped.
+    ///
+    /// Also see [`Suspended`] notes.
+    ///
+    /// [`SurfaceView`]: https://developer.android.com/reference/android/view/SurfaceView
+    /// [Activity lifecycle]: https://developer.android.com/guide/components/activities/activity-lifecycle
+    /// [`VkSurfaceKHR`]: https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkSurfaceKHR.html
+    /// [`wgpu::Surface`]: https://docs.rs/wgpu/latest/wgpu/struct.Surface.html
+    ///
+    /// ## iOS
+    ///
+    /// On iOS, the `Resumed` event is emitted in response to an [`applicationDidBecomeActive`]
+    /// callback which means the application is "active" (according to the
+    /// [iOS application lifecycle]).
+    ///
+    /// [`applicationDidBecomeActive`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622956-applicationdidbecomeactive
+    /// [iOS application lifecycle]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle
+    ///
+    /// ## Web
+    ///
+    /// On Web, the `Resumed` event is emitted in response to a [`pageshow`] event
+    /// with the property [`persisted`] being true, which means that the page is being
+    /// restored from the [`bfcache`] (back/forward cache) - an in-memory cache that
+    /// stores a complete snapshot of a page (including the JavaScript heap) as the
+    /// user is navigating away.
+    ///
+    /// [`pageshow`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/pageshow_event
+    /// [`persisted`]: https://developer.mozilla.org/en-US/docs/Web/API/PageTransitionEvent/persisted
+    /// [`bfcache`]: https://web.dev/bfcache/
+    /// [`Suspended`]: Self::suspended
+    fn resumed(&mut self, event_loop: &ActiveEventLoop);
+
+    /// Emitted when an event is sent from [`EventLoopProxy::send_event`].
+    ///
+    /// [`EventLoopProxy::send_event`]: crate::event_loop::EventLoopProxy::send_event
+    fn user_event(&mut self, event_loop: &ActiveEventLoop, event: T) {
+        let _ = (event_loop, event);
+    }
+
+    /// Emitted when the OS sends an event to a winit window.
+    fn window_event(
+        &mut self,
+        event_loop: &ActiveEventLoop,
+        window_id: WindowId,
+        event: WindowEvent,
+    );
+
+    /// Emitted when the OS sends an event to a device.
+    fn device_event(
+        &mut self,
+        event_loop: &ActiveEventLoop,
+        device_id: DeviceId,
+        event: DeviceEvent,
+    ) {
+        let _ = (event_loop, device_id, event);
+    }
+
+    /// Emitted when the event loop is about to block and wait for new events.
+    ///
+    /// Most applications shouldn't need to hook into this event since there is no real relationship
+    /// between how often the event loop needs to wake up and the dispatching of any specific events.
+    ///
+    /// High frequency event sources, such as input devices could potentially lead to lots of wake
+    /// ups and also lots of corresponding `AboutToWait` events.
+    ///
+    /// This is not an ideal event to drive application rendering from and instead applications
+    /// should render in response to [`WindowEvent::RedrawRequested`] events.
+    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
+        let _ = event_loop;
+    }
+
+    /// Emitted when the application has been suspended.
+    ///
+    /// # Portability
+    ///
+    /// Not all platforms support the notion of suspending applications, and there may be no
+    /// technical way to guarantee being able to emit a `Suspended` event if the OS has
+    /// no formal application lifecycle (currently only Android, iOS, and Web do). For this reason,
+    /// Winit does not currently try to emit pseudo `Suspended` events before the application
+    /// quits on platforms without an application lifecycle.
+    ///
+    /// Considering that the implementation of `Suspended` and [`Resumed`] events may be internally
+    /// driven by multiple platform-specific events, and that there may be subtle differences across
+    /// platforms with how these internal events are delivered, it's recommended that applications
+    /// be able to gracefully handle redundant (i.e. back-to-back) `Suspended` or [`Resumed`] events.
+    ///
+    /// Also see [`Resumed`] notes.
+    ///
+    /// ## Android
+    ///
+    /// On Android, the `Suspended` event is only sent when the application's associated
+    /// [`SurfaceView`] is destroyed. This is expected to closely correlate with the [`onPause`]
+    /// lifecycle event but there may technically be a discrepancy.
+    ///
+    /// [`onPause`]: https://developer.android.com/reference/android/app/Activity#onPause()
+    ///
+    /// Applications that need to run on Android should assume their [`SurfaceView`] has been
+    /// destroyed, which indirectly invalidates any existing render surfaces that may have been
+    /// created outside of Winit (such as an `EGLSurface`, [`VkSurfaceKHR`] or [`wgpu::Surface`]).
+    ///
+    /// After being `Suspended` on Android applications must drop all render surfaces before
+    /// the event callback completes, which may be re-created when the application is next [`Resumed`].
+    ///
+    /// [`SurfaceView`]: https://developer.android.com/reference/android/view/SurfaceView
+    /// [Activity lifecycle]: https://developer.android.com/guide/components/activities/activity-lifecycle
+    /// [`VkSurfaceKHR`]: https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkSurfaceKHR.html
+    /// [`wgpu::Surface`]: https://docs.rs/wgpu/latest/wgpu/struct.Surface.html
+    ///
+    /// ## iOS
+    ///
+    /// On iOS, the `Suspended` event is currently emitted in response to an
+    /// [`applicationWillResignActive`] callback which means that the application is
+    /// about to transition from the active to inactive state (according to the
+    /// [iOS application lifecycle]).
+    ///
+    /// [`applicationWillResignActive`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622950-applicationwillresignactive
+    /// [iOS application lifecycle]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle
+    ///
+    /// ## Web
+    ///
+    /// On Web, the `Suspended` event is emitted in response to a [`pagehide`] event
+    /// with the property [`persisted`] being true, which means that the page is being
+    /// put in the [`bfcache`] (back/forward cache) - an in-memory cache that stores a
+    /// complete snapshot of a page (including the JavaScript heap) as the user is
+    /// navigating away.
+    ///
+    /// [`pagehide`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/pagehide_event
+    /// [`persisted`]: https://developer.mozilla.org/en-US/docs/Web/API/PageTransitionEvent/persisted
+    /// [`bfcache`]: https://web.dev/bfcache/
+    /// [`Resumed`]: Self::resumed
+    fn suspended(&mut self, event_loop: &ActiveEventLoop) {
+        let _ = event_loop;
+    }
+
+    /// Emitted when the event loop is being shut down.
+    ///
+    /// This is irreversible - if this method is called, it is guaranteed that the event loop
+    /// will exist right after.
+    fn exiting(&mut self, event_loop: &ActiveEventLoop) {
+        let _ = event_loop;
+    }
+
+    /// Emitted when the application has received a memory warning.
+    ///
+    /// ## Platform-specific
+    ///
+    /// ### Android
+    ///
+    /// On Android, the `MemoryWarning` event is sent when [`onLowMemory`] was called. The application
+    /// must [release memory] or risk being killed.
+    ///
+    /// [`onLowMemory`]: https://developer.android.com/reference/android/app/Application.html#onLowMemory()
+    /// [release memory]: https://developer.android.com/topic/performance/memory#release
+    ///
+    /// ### iOS
+    ///
+    /// On iOS, the `MemoryWarning` event is emitted in response to an [`applicationDidReceiveMemoryWarning`]
+    /// callback. The application must free as much memory as possible or risk being terminated, see
+    /// [how to respond to memory warnings].
+    ///
+    /// [`applicationDidReceiveMemoryWarning`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623063-applicationdidreceivememorywarni
+    /// [how to respond to memory warnings]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle/responding_to_memory_warnings
+    ///
+    /// ### Others
+    ///
+    /// - **macOS / Orbital / Wayland / Web / Windows:** Unsupported.
+    fn memory_warning(&mut self, event_loop: &ActiveEventLoop) {
+        let _ = event_loop;
+    }
+}
diff --git a/src/event.rs b/src/event.rs
index ff3a004219..e2bca908be 100644
--- a/src/event.rs
+++ b/src/event.rs
@@ -1,36 +1,37 @@
 //! The [`Event`] enum and assorted supporting types.
 //!
-//! These are sent to the closure given to [`EventLoop::run(...)`], where they get
+//! These are sent to the closure given to [`EventLoop::run_app(...)`], where they get
 //! processed and used to modify the program state. For more details, see the root-level documentation.
 //!
 //! Some of these events represent different "parts" of a traditional event-handling loop. You could
-//! approximate the basic ordering loop of [`EventLoop::run(...)`] like this:
+//! approximate the basic ordering loop of [`EventLoop::run_app(...)`] like this:
 //!
 //! ```rust,ignore
 //! let mut start_cause = StartCause::Init;
 //!
 //! while !elwt.exiting() {
-//!     event_handler(NewEvents(start_cause), elwt);
+//!     app.new_events(event_loop, start_cause);
 //!
-//!     for e in (window events, user events, device events) {
-//!         event_handler(e, elwt);
+//!     for event in (window events, user events, device events) {
+//!         // This will pick the right method on the application based on the event.
+//!         app.handle_event(event_loop, event);
 //!     }
 //!
-//!     for w in (redraw windows) {
-//!         event_handler(RedrawRequested(w), elwt);
+//!     for window_id in (redraw windows) {
+//!         app.window_event(event_loop, window_id, RedrawRequested);
 //!     }
 //!
-//!     event_handler(AboutToWait, elwt);
+//!     app.about_to_wait(event_loop);
 //!     start_cause = wait_if_necessary();
 //! }
 //!
-//! event_handler(LoopExiting, elwt);
+//! app.exiting(event_loop);
 //! ```
 //!
 //! This leaves out timing details like [`ControlFlow::WaitUntil`] but hopefully
 //! describes what happens in what order.
 //!
-//! [`EventLoop::run(...)`]: crate::event_loop::EventLoop::run
+//! [`EventLoop::run_app(...)`]: crate::event_loop::EventLoop::run_app
 //! [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil
 use std::path::PathBuf;
 use std::sync::{Mutex, Weak};
@@ -59,199 +60,55 @@ use crate::{
 /// See the module-level docs for more information on the event loop manages each event.
 #[derive(Debug, Clone, PartialEq)]
 pub enum Event<T: 'static> {
-    /// Emitted when new events arrive from the OS to be processed.
+    /// See [`ApplicationHandler::new_events`] for details.
     ///
-    /// This event type is useful as a place to put code that should be done before you start
-    /// processing events, such as updating frame timing information for benchmarking or checking
-    /// the [`StartCause`] to see if a timer set by
-    /// [`ControlFlow::WaitUntil`](crate::event_loop::ControlFlow::WaitUntil) has elapsed.
+    /// [`ApplicationHandler::new_events`]: crate::application::ApplicationHandler::new_events
     NewEvents(StartCause),
 
-    /// Emitted when the OS sends an event to a winit window.
+    /// See [`ApplicationHandler::window_event`] for details.
+    ///
+    /// [`ApplicationHandler::window_event`]: crate::application::ApplicationHandler::window_event
     WindowEvent {
         window_id: WindowId,
         event: WindowEvent,
     },
 
-    /// Emitted when the OS sends an event to a device.
+    /// See [`ApplicationHandler::device_event`] for details.
+    ///
+    /// [`ApplicationHandler::device_event`]: crate::application::ApplicationHandler::device_event
     DeviceEvent {
         device_id: DeviceId,
         event: DeviceEvent,
     },
 
-    /// Emitted when an event is sent from [`EventLoopProxy::send_event`](crate::event_loop::EventLoopProxy::send_event)
+    /// See [`ApplicationHandler::user_event`] for details.
+    ///
+    /// [`ApplicationHandler::user_event`]: crate::application::ApplicationHandler::user_event
     UserEvent(T),
 
-    /// Emitted when the application has been suspended.
-    ///
-    /// # Portability
-    ///
-    /// Not all platforms support the notion of suspending applications, and there may be no
-    /// technical way to guarantee being able to emit a `Suspended` event if the OS has
-    /// no formal application lifecycle (currently only Android, iOS, and Web do). For this reason,
-    /// Winit does not currently try to emit pseudo `Suspended` events before the application
-    /// quits on platforms without an application lifecycle.
-    ///
-    /// Considering that the implementation of `Suspended` and [`Resumed`] events may be internally
-    /// driven by multiple platform-specific events, and that there may be subtle differences across
-    /// platforms with how these internal events are delivered, it's recommended that applications
-    /// be able to gracefully handle redundant (i.e. back-to-back) `Suspended` or [`Resumed`] events.
-    ///
-    /// Also see [`Resumed`] notes.
-    ///
-    /// ## Android
-    ///
-    /// On Android, the `Suspended` event is only sent when the application's associated
-    /// [`SurfaceView`] is destroyed. This is expected to closely correlate with the [`onPause`]
-    /// lifecycle event but there may technically be a discrepancy.
-    ///
-    /// [`onPause`]: https://developer.android.com/reference/android/app/Activity#onPause()
-    ///
-    /// Applications that need to run on Android should assume their [`SurfaceView`] has been
-    /// destroyed, which indirectly invalidates any existing render surfaces that may have been
-    /// created outside of Winit (such as an `EGLSurface`, [`VkSurfaceKHR`] or [`wgpu::Surface`]).
-    ///
-    /// After being `Suspended` on Android applications must drop all render surfaces before
-    /// the event callback completes, which may be re-created when the application is next [`Resumed`].
-    ///
-    /// [`SurfaceView`]: https://developer.android.com/reference/android/view/SurfaceView
-    /// [Activity lifecycle]: https://developer.android.com/guide/components/activities/activity-lifecycle
-    /// [`VkSurfaceKHR`]: https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkSurfaceKHR.html
-    /// [`wgpu::Surface`]: https://docs.rs/wgpu/latest/wgpu/struct.Surface.html
+    /// See [`ApplicationHandler::suspended`] for details.
     ///
-    /// ## iOS
-    ///
-    /// On iOS, the `Suspended` event is currently emitted in response to an
-    /// [`applicationWillResignActive`] callback which means that the application is
-    /// about to transition from the active to inactive state (according to the
-    /// [iOS application lifecycle]).
-    ///
-    /// [`applicationWillResignActive`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622950-applicationwillresignactive
-    /// [iOS application lifecycle]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle
-    ///
-    /// ## Web
-    ///
-    /// On Web, the `Suspended` event is emitted in response to a [`pagehide`] event
-    /// with the property [`persisted`] being true, which means that the page is being
-    /// put in the [`bfcache`] (back/forward cache) - an in-memory cache that stores a
-    /// complete snapshot of a page (including the JavaScript heap) as the user is
-    /// navigating away.
-    ///
-    /// [`pagehide`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/pagehide_event
-    /// [`persisted`]: https://developer.mozilla.org/en-US/docs/Web/API/PageTransitionEvent/persisted
-    /// [`bfcache`]: https://web.dev/bfcache/
-    ///
-    /// [`Resumed`]: Self::Resumed
+    /// [`ApplicationHandler::suspended`]: crate::application::ApplicationHandler::suspended
     Suspended,
 
-    /// Emitted when the application has been resumed.
-    ///
-    /// For consistency, all platforms emit a `Resumed` event even if they don't themselves have a
-    /// formal suspend/resume lifecycle. For systems without a standard suspend/resume lifecycle
-    /// the `Resumed` event is always emitted after the [`NewEvents(StartCause::Init)`][StartCause::Init]
-    /// event.
-    ///
-    /// # Portability
-    ///
-    /// It's recommended that applications should only initialize their graphics context and create
-    /// a window after they have received their first `Resumed` event. Some systems
-    /// (specifically Android) won't allow applications to create a render surface until they are
-    /// resumed.
-    ///
-    /// Considering that the implementation of [`Suspended`] and `Resumed` events may be internally
-    /// driven by multiple platform-specific events, and that there may be subtle differences across
-    /// platforms with how these internal events are delivered, it's recommended that applications
-    /// be able to gracefully handle redundant (i.e. back-to-back) [`Suspended`] or `Resumed` events.
-    ///
-    /// Also see [`Suspended`] notes.
-    ///
-    /// ## Android
+    /// See [`ApplicationHandler::resumed`] for details.
     ///
-    /// On Android, the `Resumed` event is sent when a new [`SurfaceView`] has been created. This is
-    /// expected to closely correlate with the [`onResume`] lifecycle event but there may technically
-    /// be a discrepancy.
-    ///
-    /// [`onResume`]: https://developer.android.com/reference/android/app/Activity#onResume()
-    ///
-    /// Applications that need to run on Android must wait until they have been `Resumed`
-    /// before they will be able to create a render surface (such as an `EGLSurface`,
-    /// [`VkSurfaceKHR`] or [`wgpu::Surface`]) which depend on having a
-    /// [`SurfaceView`]. Applications must also assume that if they are [`Suspended`], then their
-    /// render surfaces are invalid and should be dropped.
-    ///
-    /// Also see [`Suspended`] notes.
-    ///
-    /// [`SurfaceView`]: https://developer.android.com/reference/android/view/SurfaceView
-    /// [Activity lifecycle]: https://developer.android.com/guide/components/activities/activity-lifecycle
-    /// [`VkSurfaceKHR`]: https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkSurfaceKHR.html
-    /// [`wgpu::Surface`]: https://docs.rs/wgpu/latest/wgpu/struct.Surface.html
-    ///
-    /// ## iOS
-    ///
-    /// On iOS, the `Resumed` event is emitted in response to an [`applicationDidBecomeActive`]
-    /// callback which means the application is "active" (according to the
-    /// [iOS application lifecycle]).
-    ///
-    /// [`applicationDidBecomeActive`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622956-applicationdidbecomeactive
-    /// [iOS application lifecycle]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle
-    ///
-    /// ## Web
-    ///
-    /// On Web, the `Resumed` event is emitted in response to a [`pageshow`] event
-    /// with the property [`persisted`] being true, which means that the page is being
-    /// restored from the [`bfcache`] (back/forward cache) - an in-memory cache that
-    /// stores a complete snapshot of a page (including the JavaScript heap) as the
-    /// user is navigating away.
-    ///
-    /// [`pageshow`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/pageshow_event
-    /// [`persisted`]: https://developer.mozilla.org/en-US/docs/Web/API/PageTransitionEvent/persisted
-    /// [`bfcache`]: https://web.dev/bfcache/
-    ///
-    /// [`Suspended`]: Self::Suspended
+    /// [`ApplicationHandler::resumed`]: crate::application::ApplicationHandler::resumed
     Resumed,
 
-    /// Emitted when the event loop is about to block and wait for new events.
-    ///
-    /// Most applications shouldn't need to hook into this event since there is no real relationship
-    /// between how often the event loop needs to wake up and the dispatching of any specific events.
+    /// See [`ApplicationHandler::about_to_wait`] for details.
     ///
-    /// High frequency event sources, such as input devices could potentially lead to lots of wake
-    /// ups and also lots of corresponding `AboutToWait` events.
-    ///
-    /// This is not an ideal event to drive application rendering from and instead applications
-    /// should render in response to [`WindowEvent::RedrawRequested`] events.
+    /// [`ApplicationHandler::about_to_wait`]: crate::application::ApplicationHandler::about_to_wait
     AboutToWait,
 
-    /// Emitted when the event loop is being shut down.
+    /// See [`ApplicationHandler::exiting`] for details.
     ///
-    /// This is irreversible - if this event is emitted, it is guaranteed to be the last event that
-    /// gets emitted. You generally want to treat this as a "do on quit" event.
+    /// [`ApplicationHandler::exiting`]: crate::application::ApplicationHandler::exiting
     LoopExiting,
 
-    /// Emitted when the application has received a memory warning.
-    ///
-    /// ## Platform-specific
-    ///
-    /// ### Android
-    ///
-    /// On Android, the `MemoryWarning` event is sent when [`onLowMemory`] was called. The application
-    /// must [release memory] or risk being killed.
-    ///
-    /// [`onLowMemory`]: https://developer.android.com/reference/android/app/Application.html#onLowMemory()
-    /// [release memory]: https://developer.android.com/topic/performance/memory#release
-    ///
-    /// ### iOS
-    ///
-    /// On iOS, the `MemoryWarning` event is emitted in response to an [`applicationDidReceiveMemoryWarning`]
-    /// callback. The application must free as much memory as possible or risk being terminated, see
-    /// [how to respond to memory warnings].
-    ///
-    /// [`applicationDidReceiveMemoryWarning`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623063-applicationdidreceivememorywarni
-    /// [how to respond to memory warnings]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle/responding_to_memory_warnings
-    ///
-    /// ### Others
+    /// See [`ApplicationHandler::memory_warning`] for details.
     ///
-    /// - **macOS / Wayland / Windows / Orbital:** Unsupported.
+    /// [`ApplicationHandler::memory_warning`]: crate::application::ApplicationHandler::memory_warning
     MemoryWarning,
 }
 
diff --git a/src/event_loop.rs b/src/event_loop.rs
index c1f32da61b..af99ea1bda 100644
--- a/src/event_loop.rs
+++ b/src/event_loop.rs
@@ -18,6 +18,7 @@ use std::time::{Duration, Instant};
 #[cfg(web_platform)]
 use web_time::{Duration, Instant};
 
+use crate::application::ApplicationHandler;
 use crate::error::{EventLoopError, OsError};
 use crate::window::{CustomCursor, CustomCursorSource, Window, WindowAttributes};
 use crate::{event::Event, monitor::MonitorHandle, platform_impl};
@@ -215,8 +216,22 @@ impl<T> EventLoop<T> {
         }
     }
 
-    /// Runs the event loop in the calling thread and calls the given `event_handler` closure
-    /// to dispatch any pending events.
+    /// See [`run_app`].
+    ///
+    /// [`run_app`]: Self::run_app
+    #[inline]
+    #[deprecated = "use `EventLoop::run_app` instead"]
+    #[cfg(not(all(web_platform, target_feature = "exception-handling")))]
+    pub fn run<F>(self, event_handler: F) -> Result<(), EventLoopError>
+    where
+        F: FnMut(Event<T>, &ActiveEventLoop),
+    {
+        let _span = tracing::debug_span!("winit::EventLoop::run").entered();
+
+        self.event_loop.run(event_handler)
+    }
+
+    /// Run the application with the event loop on the calling thread.
     ///
     /// See the [`set_control_flow()`] docs on how to change the event loop's behavior.
     ///
@@ -231,10 +246,10 @@ impl<T> EventLoop<T> {
     ///   Web applications are recommended to use
     #[cfg_attr(
         web_platform,
-        doc = "[`EventLoopExtWebSys::spawn()`][crate::platform::web::EventLoopExtWebSys::spawn()]"
+        doc = "[`EventLoopExtWebSys::spawn_app()`][crate::platform::web::EventLoopExtWebSys::spawn_app()]"
     )]
     #[cfg_attr(not(web_platform), doc = "`EventLoopExtWebSys::spawn()`")]
-    ///   [^1] instead of [`run()`] to avoid the need
+    ///   [^1] instead of [`run_app()`] to avoid the need
     ///   for the Javascript exception trick, and to make it clearer that the event loop runs
     ///   asynchronously (via the browser's own, internal, event loop) and doesn't block the
     ///   current thread of execution like it does on other platforms.
@@ -242,17 +257,13 @@ impl<T> EventLoop<T> {
     ///   This function won't be available with `target_feature = "exception-handling"`.
     ///
     /// [`set_control_flow()`]: ActiveEventLoop::set_control_flow()
-    /// [`run()`]: Self::run()
-    /// [^1]: `EventLoopExtWebSys::spawn()` is only available on Web.
+    /// [`run_app()`]: Self::run_app()
+    /// [^1]: `EventLoopExtWebSys::spawn_app()` is only available on Web.
     #[inline]
     #[cfg(not(all(web_platform, target_feature = "exception-handling")))]
-    pub fn run<F>(self, event_handler: F) -> Result<(), EventLoopError>
-    where
-        F: FnMut(Event<T>, &ActiveEventLoop),
-    {
-        let _span = tracing::debug_span!("winit::EventLoop::run").entered();
-
-        self.event_loop.run(event_handler)
+    pub fn run_app<A: ApplicationHandler<T>>(self, app: &mut A) -> Result<(), EventLoopError> {
+        self.event_loop
+            .run(|event, event_loop| dispatch_event_for_app(app, event_loop, event))
     }
 
     /// Creates an [`EventLoopProxy`] that can be used to dispatch user events
@@ -344,11 +355,11 @@ unsafe impl<T> rwh_05::HasRawDisplayHandle for EventLoop<T> {
 impl<T> AsFd for EventLoop<T> {
     /// Get the underlying [EventLoop]'s `fd` which you can register
     /// into other event loop, like [`calloop`] or [`mio`]. When doing so, the
-    /// loop must be polled with the [`pump_events`] API.
+    /// loop must be polled with the [`pump_app_events`] API.
     ///
     /// [`calloop`]: https://crates.io/crates/calloop
     /// [`mio`]: https://crates.io/crates/mio
-    /// [`pump_events`]: crate::platform::pump_events::EventLoopExtPumpEvents::pump_events
+    /// [`pump_app_events`]: crate::platform::pump_events::EventLoopExtPumpEvents::pump_app_events
     fn as_fd(&self) -> BorrowedFd<'_> {
         self.event_loop.as_fd()
     }
@@ -358,11 +369,11 @@ impl<T> AsFd for EventLoop<T> {
 impl<T> AsRawFd for EventLoop<T> {
     /// Get the underlying [EventLoop]'s raw `fd` which you can register
     /// into other event loop, like [`calloop`] or [`mio`]. When doing so, the
-    /// loop must be polled with the [`pump_events`] API.
+    /// loop must be polled with the [`pump_app_events`] API.
     ///
     /// [`calloop`]: https://crates.io/crates/calloop
     /// [`mio`]: https://crates.io/crates/mio
-    /// [`pump_events`]: crate::platform::pump_events::EventLoopExtPumpEvents::pump_events
+    /// [`pump_app_events`]: crate::platform::pump_events::EventLoopExtPumpEvents::pump_app_events
     fn as_raw_fd(&self) -> RawFd {
         self.event_loop.as_raw_fd()
     }
@@ -630,3 +641,23 @@ impl AsyncRequestSerial {
         Self { serial }
     }
 }
+
+/// Shim for various run APIs.
+#[inline(always)]
+pub(crate) fn dispatch_event_for_app<T: 'static, A: ApplicationHandler<T>>(
+    app: &mut A,
+    event_loop: &ActiveEventLoop,
+    event: Event<T>,
+) {
+    match event {
+        Event::NewEvents(cause) => app.new_events(event_loop, cause),
+        Event::WindowEvent { window_id, event } => app.window_event(event_loop, window_id, event),
+        Event::DeviceEvent { device_id, event } => app.device_event(event_loop, device_id, event),
+        Event::UserEvent(event) => app.user_event(event_loop, event),
+        Event::Suspended => app.suspended(event_loop),
+        Event::Resumed => app.resumed(event_loop),
+        Event::AboutToWait => app.about_to_wait(event_loop),
+        Event::LoopExiting => app.exiting(event_loop),
+        Event::MemoryWarning => app.memory_warning(event_loop),
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 28a9879be0..5f99012cd1 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -21,7 +21,7 @@
 //! Some user activity, like mouse movement, can generate both a [`WindowEvent`] *and* a
 //! [`DeviceEvent`]. You can also create and handle your own custom [`Event::UserEvent`]s, if desired.
 //!
-//! You can retrieve events by calling [`EventLoop::run()`]. This function will
+//! You can retrieve events by calling [`EventLoop::run_app()`]. This function will
 //! dispatch events for every [`Window`] that was created with that particular [`EventLoop`], and
 //! will run until [`exit()`] is used, at which point [`Event::LoopExiting`].
 //!
@@ -36,7 +36,7 @@
         x11_platform,
         wayland_platform
     ),
-    doc = "[`EventLoopExtPumpEvents::pump_events()`][platform::pump_events::EventLoopExtPumpEvents::pump_events()]"
+    doc = "[`EventLoopExtPumpEvents::pump_app_events()`][platform::pump_events::EventLoopExtPumpEvents::pump_app_events()]"
 )]
 #![cfg_attr(
     not(any(
@@ -46,18 +46,54 @@
         x11_platform,
         wayland_platform
     )),
-    doc = "`EventLoopExtPumpEvents::pump_events()`"
+    doc = "`EventLoopExtPumpEvents::pump_app_events()`"
 )]
 //! [^1]. See that method's documentation for more reasons about why
 //! it's discouraged beyond compatibility reasons.
 //!
 //!
 //! ```no_run
-//! use winit::{
-//!     event::{Event, WindowEvent},
-//!     event_loop::{ControlFlow, EventLoop},
-//!     window::Window,
-//! };
+//! use winit::application::ApplicationHandler;
+//! use winit::event::WindowEvent;
+//! use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
+//! use winit::window::{Window, WindowId};
+//!
+//! #[derive(Default)]
+//! struct App {
+//!     window: Option<Window>,
+//! }
+//!
+//! impl ApplicationHandler for App {
+//!     fn resumed(&mut self, event_loop: &ActiveEventLoop) {
+//!         self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap());
+//!     }
+//!
+//!     fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
+//!         match event {
+//!             WindowEvent::CloseRequested => {
+//!                 println!("The close button was pressed; stopping");
+//!                 event_loop.exit();
+//!             },
+//!             WindowEvent::RedrawRequested => {
+//!                 // Redraw the application.
+//!                 //
+//!                 // It's preferable for applications that do not render continuously to render in
+//!                 // this event rather than in AboutToWait, since rendering in here allows
+//!                 // the program to gracefully handle redraws requested by the OS.
+//!
+//!                 // Draw.
+//!
+//!                 // Queue a RedrawRequested event.
+//!                 //
+//!                 // You only need to call this if you've determined that you need to redraw in
+//!                 // applications which do not always need to. Applications that redraw continuously
+//!                 // can render here instead.
+//!                 self.window.as_ref().unwrap().request_redraw();
+//!             }
+//!             _ => (),
+//!         }
+//!     }
+//! }
 //!
 //! let event_loop = EventLoop::new().unwrap();
 //!
@@ -70,43 +106,8 @@
 //! // input, and uses significantly less power/CPU time than ControlFlow::Poll.
 //! event_loop.set_control_flow(ControlFlow::Wait);
 //!
-//! let mut window = None;
-//!
-//! event_loop.run(move |event, event_loop| {
-//!     match event {
-//!         Event::Resumed => {
-//!             window = Some(event_loop.create_window(Window::default_attributes()).unwrap());
-//!         }
-//!         Event::WindowEvent {
-//!             event: WindowEvent::CloseRequested,
-//!             ..
-//!         } => {
-//!             println!("The close button was pressed; stopping");
-//!             event_loop.exit();
-//!         },
-//!         Event::AboutToWait => {
-//!             // Application update code.
-//!
-//!             // Queue a RedrawRequested event.
-//!             //
-//!             // You only need to call this if you've determined that you need to redraw in
-//!             // applications which do not always need to. Applications that redraw continuously
-//!             // can render here instead.
-//!             window.as_ref().unwrap().request_redraw();
-//!         },
-//!         Event::WindowEvent {
-//!             event: WindowEvent::RedrawRequested,
-//!             ..
-//!         } => {
-//!             // Redraw the application.
-//!             //
-//!             // It's preferable for applications that do not render continuously to render in
-//!             // this event rather than in AboutToWait, since rendering in here allows
-//!             // the program to gracefully handle redraws requested by the OS.
-//!         },
-//!         _ => ()
-//!     }
-//! });
+//! let mut app = App::default();
+//! event_loop.run_app(&mut app);
 //! ```
 //!
 //! [`WindowEvent`] has a [`WindowId`] member. In multi-window environments, it should be
@@ -164,7 +165,7 @@
 //!
 //! [`EventLoop`]: event_loop::EventLoop
 //! [`EventLoop::new()`]: event_loop::EventLoop::new
-//! [`EventLoop::run()`]: event_loop::EventLoop::run
+//! [`EventLoop::run_app()`]: event_loop::EventLoop::run_app
 //! [`exit()`]: event_loop::ActiveEventLoop::exit
 //! [`Window`]: window::Window
 //! [`WindowId`]: window::WindowId
@@ -178,7 +179,7 @@
 //! [`Event::LoopExiting`]: event::Event::LoopExiting
 //! [`raw_window_handle`]: ./window/struct.Window.html#method.raw_window_handle
 //! [`raw_display_handle`]: ./window/struct.Window.html#method.raw_display_handle
-//! [^1]: `EventLoopExtPumpEvents::pump_events()` is only available on Windows, macOS, Android, X11 and Wayland.
+//! [^1]: `EventLoopExtPumpEvents::pump_app_events()` is only available on Windows, macOS, Android, X11 and Wayland.
 
 #![deny(rust_2018_idioms)]
 #![deny(rustdoc::broken_intra_doc_links)]
@@ -200,6 +201,7 @@ pub use rwh_06 as raw_window_handle;
 #[doc(inline)]
 pub use dpi;
 
+pub mod application;
 #[macro_use]
 pub mod error;
 mod cursor;
diff --git a/src/platform/pump_events.rs b/src/platform/pump_events.rs
index 804723dcbf..37a31ea615 100644
--- a/src/platform/pump_events.rs
+++ b/src/platform/pump_events.rs
@@ -1,22 +1,13 @@
 use std::time::Duration;
 
-use crate::{
-    event::Event,
-    event_loop::{ActiveEventLoop, EventLoop},
-};
-
-/// The return status for `pump_events`
-pub enum PumpStatus {
-    /// Continue running external loop.
-    Continue,
-    /// Exit external loop.
-    Exit(i32),
-}
+use crate::application::ApplicationHandler;
+use crate::event::Event;
+use crate::event_loop::{self, ActiveEventLoop, EventLoop};
 
 /// Additional methods on [`EventLoop`] for pumping events within an external event loop
 pub trait EventLoopExtPumpEvents {
     /// A type provided by the user that can be passed through [`Event::UserEvent`].
-    type UserEvent;
+    type UserEvent: 'static;
 
     /// Pump the `EventLoop` to check for and dispatch pending events.
     ///
@@ -113,6 +104,21 @@ pub trait EventLoopExtPumpEvents {
     ///   If you render outside of Winit you are likely to see window resizing artifacts
     ///   since MacOS expects applications to render synchronously during any `drawRect`
     ///   callback.
+    fn pump_app_events<A: ApplicationHandler<Self::UserEvent>>(
+        &mut self,
+        timeout: Option<Duration>,
+        app: &mut A,
+    ) -> PumpStatus {
+        #[allow(deprecated)]
+        self.pump_events(timeout, |event, event_loop| {
+            event_loop::dispatch_event_for_app(app, event_loop, event)
+        })
+    }
+
+    /// See [`pump_app_events`].
+    ///
+    /// [`pump_app_events`]: Self::pump_app_events
+    #[deprecated = "use EventLoopExtPumpEvents::pump_app_events"]
     fn pump_events<F>(&mut self, timeout: Option<Duration>, event_handler: F) -> PumpStatus
     where
         F: FnMut(Event<Self::UserEvent>, &ActiveEventLoop);
@@ -128,3 +134,11 @@ impl<T> EventLoopExtPumpEvents for EventLoop<T> {
         self.event_loop.pump_events(timeout, event_handler)
     }
 }
+
+/// The return status for `pump_events`
+pub enum PumpStatus {
+    /// Continue running external loop.
+    Continue,
+    /// Exit external loop.
+    Exit(i32),
+}
diff --git a/src/platform/run_on_demand.rs b/src/platform/run_on_demand.rs
index 0010eab886..886ff49df1 100644
--- a/src/platform/run_on_demand.rs
+++ b/src/platform/run_on_demand.rs
@@ -1,8 +1,7 @@
-use crate::{
-    error::EventLoopError,
-    event::Event,
-    event_loop::{ActiveEventLoop, EventLoop},
-};
+use crate::application::ApplicationHandler;
+use crate::error::EventLoopError;
+use crate::event::Event;
+use crate::event_loop::{self, ActiveEventLoop, EventLoop};
 
 #[cfg(doc)]
 use crate::{platform::pump_events::EventLoopExtPumpEvents, window::Window};
@@ -10,12 +9,19 @@ use crate::{platform::pump_events::EventLoopExtPumpEvents, window::Window};
 /// Additional methods on [`EventLoop`] to return control flow to the caller.
 pub trait EventLoopExtRunOnDemand {
     /// A type provided by the user that can be passed through [`Event::UserEvent`].
-    type UserEvent;
+    type UserEvent: 'static;
 
-    /// Runs the event loop in the calling thread and calls the given `event_handler` closure
-    /// to dispatch any window system events.
+    /// See [`run_app_on_demand`].
     ///
-    /// Unlike [`EventLoop::run`], this function accepts non-`'static` (i.e. non-`move`) closures
+    /// [`run_app_on_demand`]: Self::run_app_on_demand
+    #[deprecated = "use EventLoopExtRunOnDemand::run_app_on_demand"]
+    fn run_on_demand<F>(&mut self, event_handler: F) -> Result<(), EventLoopError>
+    where
+        F: FnMut(Event<Self::UserEvent>, &ActiveEventLoop);
+
+    /// Run the application with the event loop on the calling thread.
+    ///
+    /// Unlike [`EventLoop::run_app`], this function accepts non-`'static` (i.e. non-`move`) closures
     /// and it is possible to return control back to the caller without
     /// consuming the `EventLoop` (by using [`exit()`]) and
     /// so the event loop can be re-run after it has exit.
@@ -26,11 +32,10 @@ pub trait EventLoopExtRunOnDemand {
     ///
     /// This API is not designed to run an event loop in bursts that you can exit from and return
     /// to while maintaining the full state of your application. (If you need something like this
-    /// you can look at the [`EventLoopExtPumpEvents::pump_events()`] API)
+    /// you can look at the [`EventLoopExtPumpEvents::pump_app_events()`] API)
     ///
-    /// Each time `run_on_demand` is called the `event_handler` can expect to receive a
-    /// `NewEvents(Init)` and `Resumed` event (even on platforms that have no suspend/resume
-    /// lifecycle) - which can be used to consistently initialize application state.
+    /// Each time `run_app_on_demand` is called the startup sequence of `init`, followed by
+    /// `resume` is being preserved.
     ///
     /// See the [`set_control_flow()`] docs on how to change the event loop's behavior.
     ///
@@ -40,8 +45,8 @@ pub trait EventLoopExtRunOnDemand {
     ///   backend it is possible to use `EventLoopExtWebSys::spawn()`[^1] more than once instead).
     /// - No [`Window`] state can be carried between separate runs of the event loop.
     ///
-    /// You are strongly encouraged to use [`EventLoop::run()`] for portability, unless you specifically need
-    /// the ability to re-run a single event loop more than once
+    /// You are strongly encouraged to use [`EventLoop::run_app()`] for portability, unless you
+    /// specifically need the ability to re-run a single event loop more than once
     ///
     /// # Supported Platforms
     /// - Windows
@@ -64,9 +69,15 @@ pub trait EventLoopExtRunOnDemand {
     ///
     /// [`exit()`]: ActiveEventLoop::exit()
     /// [`set_control_flow()`]: ActiveEventLoop::set_control_flow()
-    fn run_on_demand<F>(&mut self, event_handler: F) -> Result<(), EventLoopError>
-    where
-        F: FnMut(Event<Self::UserEvent>, &ActiveEventLoop);
+    fn run_app_on_demand<A: ApplicationHandler<Self::UserEvent>>(
+        &mut self,
+        app: &mut A,
+    ) -> Result<(), EventLoopError> {
+        #[allow(deprecated)]
+        self.run_on_demand(|event, event_loop| {
+            event_loop::dispatch_event_for_app(app, event_loop, event)
+        })
+    }
 }
 
 impl<T> EventLoopExtRunOnDemand for EventLoop<T> {
diff --git a/src/platform/web.rs b/src/platform/web.rs
index fd4982702a..dfca6cf54b 100644
--- a/src/platform/web.rs
+++ b/src/platform/web.rs
@@ -53,9 +53,10 @@ use std::time::Duration;
 #[cfg(web_platform)]
 use web_sys::HtmlCanvasElement;
 
+use crate::application::ApplicationHandler;
 use crate::cursor::CustomCursorSource;
 use crate::event::Event;
-use crate::event_loop::{ActiveEventLoop, EventLoop};
+use crate::event_loop::{self, ActiveEventLoop, EventLoop};
 #[cfg(web_platform)]
 use crate::platform_impl::CustomCursorFuture as PlatformCustomCursorFuture;
 use crate::platform_impl::PlatformCustomCursorSource;
@@ -160,18 +161,18 @@ impl WindowAttributesExtWebSys for WindowAttributes {
 /// Additional methods on `EventLoop` that are specific to the web.
 pub trait EventLoopExtWebSys {
     /// A type provided by the user that can be passed through `Event::UserEvent`.
-    type UserEvent;
+    type UserEvent: 'static;
 
     /// Initializes the winit event loop.
     ///
     /// Unlike
     #[cfg_attr(
         all(web_platform, target_feature = "exception-handling"),
-        doc = "`run()`"
+        doc = "`run_app()`"
     )]
     #[cfg_attr(
         not(all(web_platform, target_feature = "exception-handling")),
-        doc = "[`run()`]"
+        doc = "[`run_app()`]"
     )]
     /// [^1], this returns immediately, and doesn't throw an exception in order to
     /// satisfy its [`!`] return type.
@@ -183,9 +184,15 @@ pub trait EventLoopExtWebSys {
     ///
     #[cfg_attr(
         not(all(web_platform, target_feature = "exception-handling")),
-        doc = "[`run()`]: EventLoop::run()"
+        doc = "[`run_app()`]: EventLoop::run_app()"
     )]
-    /// [^1]: `run()` is _not_ available on WASM when the target supports `exception-handling`.
+    /// [^1]: `run_app()` is _not_ available on WASM when the target supports `exception-handling`.
+    fn spawn_app<A: ApplicationHandler<Self::UserEvent> + 'static>(self, app: A);
+
+    /// See [`spawn_app`].
+    ///
+    /// [`spawn_app`]: Self::spawn_app
+    #[deprecated = "use EventLoopExtWebSys::spawn_app"]
     fn spawn<F>(self, event_handler: F)
     where
         F: 'static + FnMut(Event<Self::UserEvent>, &ActiveEventLoop);
@@ -194,6 +201,12 @@ pub trait EventLoopExtWebSys {
 impl<T> EventLoopExtWebSys for EventLoop<T> {
     type UserEvent = T;
 
+    fn spawn_app<A: ApplicationHandler<Self::UserEvent> + 'static>(self, mut app: A) {
+        self.event_loop.spawn(move |event, event_loop| {
+            event_loop::dispatch_event_for_app(&mut app, event_loop, event)
+        });
+    }
+
     fn spawn<F>(self, event_handler: F)
     where
         F: 'static + FnMut(Event<Self::UserEvent>, &ActiveEventLoop),
diff --git a/src/platform_impl/ios/event_loop.rs b/src/platform_impl/ios/event_loop.rs
index 5fc30ea658..27921c1bed 100644
--- a/src/platform_impl/ios/event_loop.rs
+++ b/src/platform_impl/ios/event_loop.rs
@@ -182,7 +182,7 @@ impl<T: 'static> EventLoop<T> {
             application.is_none(),
             "\
                 `EventLoop` cannot be `run` after a call to `UIApplicationMain` on iOS\n\
-                 Note: `EventLoop::run` calls `UIApplicationMain` on iOS",
+                 Note: `EventLoop::run_app` calls `UIApplicationMain` on iOS",
         );
 
         let handler = map_user_event(handler, self.receiver);
diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs
index a7c7b328fe..03f63cc73e 100644
--- a/src/platform_impl/ios/window.rs
+++ b/src/platform_impl/ios/window.rs
@@ -689,7 +689,7 @@ impl Inner {
             let screen_frame = self.rect_to_screen_space(bounds);
             let status_bar_frame = {
                 let app = UIApplication::shared(MainThreadMarker::new().unwrap()).expect(
-                    "`Window::get_inner_position` cannot be called before `EventLoop::run` on iOS",
+                    "`Window::get_inner_position` cannot be called before `EventLoop::run_app` on iOS",
                 );
                 app.statusBarFrame()
             };
diff --git a/src/platform_impl/macos/app_delegate.rs b/src/platform_impl/macos/app_delegate.rs
index 574b115d3c..7d528ab2f3 100644
--- a/src/platform_impl/macos/app_delegate.rs
+++ b/src/platform_impl/macos/app_delegate.rs
@@ -91,7 +91,7 @@ declare_class!(
             self.set_is_running(true);
             self.dispatch_init_events();
 
-            // If the application is being launched via `EventLoop::pump_events()` then we'll
+            // If the application is being launched via `EventLoop::pump_app_events()` then we'll
             // want to stop the app once it is launched (and return to the external loop)
             //
             // In this case we still want to consider Winit's `EventLoop` to be "running",
diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs
index 9f7ea93464..06f925316d 100644
--- a/src/platform_impl/web/event_loop/runner.rs
+++ b/src/platform_impl/web/event_loop/runner.rs
@@ -75,8 +75,8 @@ enum RunnerEnum {
     Pending,
     /// The `EventLoop` is being run.
     Running(Runner),
-    /// The `EventLoop` is exited after being started with `EventLoop::run`. Since
-    /// `EventLoop::run` takes ownership of the `EventLoop`, we can be certain
+    /// The `EventLoop` is exited after being started with `EventLoop::run_app`. Since
+    /// `EventLoop::run_app` takes ownership of the `EventLoop`, we can be certain
     /// that this event loop will never be run again.
     Destroyed,
 }
@@ -735,7 +735,7 @@ impl Shared {
         // * `self`, i.e. the item which triggered this event loop wakeup, which
         //   is usually a `wasm-bindgen` `Closure`, which will be dropped after
         //   returning to the JS glue code.
-        // * The `ActiveEventLoop` leaked inside `EventLoop::run` due to the
+        // * The `ActiveEventLoop` leaked inside `EventLoop::run_app` due to the
         //   JS exception thrown at the end.
         // * For each undropped `Window`:
         //     * The `register_redraw_request` closure.
diff --git a/src/window.rs b/src/window.rs
index 2aa49dde79..e3c456d4b7 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -20,7 +20,7 @@ use serde::{Deserialize, Serialize};
 ///
 /// The window is closed when dropped.
 ///
-/// # Threading
+/// ## Threading
 ///
 /// This is `Send + Sync`, meaning that it can be freely used from other
 /// threads.
@@ -30,37 +30,6 @@ use serde::{Deserialize, Serialize};
 /// window from a thread other than the main, the code is scheduled to run on
 /// the main thread, and your thread may be blocked until that completes.
 ///
-/// # Example
-///
-/// ```no_run
-/// use winit::{
-///     event::{Event, WindowEvent},
-///     event_loop::{ControlFlow, EventLoop},
-///     window::Window,
-/// };
-///
-/// let mut event_loop = EventLoop::new().unwrap();
-/// event_loop.set_control_flow(ControlFlow::Wait);
-/// let mut windows = Vec::new();
-///
-/// event_loop.run(move |event, event_loop| {
-///     match event {
-///         Event::Resumed => {
-///             let window = event_loop.create_window(Window::default_attributes()).unwrap();
-///             windows.push(window);
-///         }
-///         Event::WindowEvent {
-///             event: WindowEvent::CloseRequested,
-///             ..
-///         } => {
-///             windows.clear();
-///             event_loop.exit();
-///         }
-///         _ => (),
-///     }
-/// });
-/// ```
-///
 /// ## Platform-specific
 ///
 /// **Web:** The [`Window`], which is represented by a `HTMLElementCanvas`, can