Repository: rust-windowing/winit
Pull request #4136: Fix a pause in the event loop when clicking the title bar on windows
Author: login: aloucks
Labels: name: B - bug
description: Dang, that shouldn't have happened
name: DS - win32
description: Affects the Win32/Windows backend
Created: 2025-02-23T20:25:46Z | Merged: 2025-03-17T01:27:28Z

When clicking the title bar on Windows, to drag the window, there is a noticeable ~500ms pause in continuous redraw requests. This was fixed in #839 and then regressed in #1852. The cursor blinks in both cases and is unrelated. The regression made the blink happen after the pause instead of immediately.

- [x] Tested on all platforms changed
- [x] Added an entry to the `changelog` module if knowledge of this change could be valuable to users




Diff:
diff --git a/examples/application.rs b/examples/application.rs
index 4c58d4c14f..8b63a089f9 100644
--- a/examples/application.rs
+++ b/examples/application.rs
@@ -42,6 +42,9 @@ use winit::window::{
 #[path = "util/tracing.rs"]
 mod tracing;
 
+#[path = "util/fill.rs"]
+mod fill;
+
 /// The amount of points to around the window for drag resize direction calculations.
 const BORDER_SIZE: f64 = 20.;
 
@@ -314,6 +317,13 @@ impl Application {
                 self.sender.send(Action::Message).unwrap();
                 event_loop.create_proxy().wake_up();
             },
+            Action::ToggleAnimatedFillColor => {
+                window.animated_fill_color = !window.animated_fill_color;
+            },
+            Action::ToggleContinuousRedraw => {
+                window.continuous_redraw = !window.continuous_redraw;
+                window.window.request_redraw();
+            },
         }
     }
 
@@ -441,6 +451,9 @@ impl ApplicationHandler for Application {
                 if let Err(err) = window.draw() {
                     error!("Error drawing window: {err}");
                 }
+                if window.continuous_redraw {
+                    window.window.request_redraw();
+                }
             },
             WindowEvent::Occluded(occluded) => {
                 window.set_occluded(occluded);
@@ -616,6 +629,13 @@ struct WindowState {
     window: Arc<dyn Window>,
     /// The window theme we're drawing with.
     theme: Theme,
+    /// Fill the window with animated color
+    animated_fill_color: bool,
+    /// The application start time. Used for color fill animation
+    #[cfg(not(android_platform))]
+    start_time: std::time::Instant,
+    /// Redraw continuously
+    continuous_redraw: bool,
     /// Cursor position over the window.
     cursor_position: Option<PhysicalPosition<f64>>,
     /// Window modifiers state.
@@ -669,6 +689,10 @@ impl WindowState {
             surface,
             window,
             theme,
+            animated_fill_color: false,
+            continuous_redraw: false,
+            #[cfg(not(android_platform))]
+            start_time: std::time::Instant::now(),
             ime,
             cursor_position: Default::default(),
             cursor_hidden: Default::default(),
@@ -947,6 +971,11 @@ impl WindowState {
             return Ok(());
         }
 
+        if self.animated_fill_color {
+            fill::fill_window_with_animated_color(&*self.window, self.start_time);
+            return Ok(());
+        }
+
         let mut buffer = self.surface.buffer_mut()?;
 
         // Draw a different color inside the safe area
@@ -1038,6 +1067,8 @@ enum Action {
     RequestResize,
     DumpMonitors,
     Message,
+    ToggleAnimatedFillColor,
+    ToggleContinuousRedraw,
 }
 
 impl Action {
@@ -1082,6 +1113,8 @@ impl Action {
                  information"
             },
             Action::Message => "Prints a message through a user wake up",
+            Action::ToggleAnimatedFillColor => "Toggle animated fill color",
+            Action::ToggleContinuousRedraw => "Toggle continuous redraw",
         }
     }
 }
@@ -1196,6 +1229,7 @@ const CURSORS: &[CursorIcon] = &[
 const KEY_BINDINGS: &[Binding<&'static str>] = &[
     Binding::new("Q", ModifiersState::CONTROL, Action::CloseWindow),
     Binding::new("H", ModifiersState::CONTROL, Action::PrintHelp),
+    Binding::new("F", ModifiersState::SHIFT, Action::ToggleAnimatedFillColor),
     Binding::new("F", ModifiersState::CONTROL, Action::ToggleFullscreen),
     #[cfg(macos_platform)]
     Binding::new("F", ModifiersState::ALT, Action::ToggleSimpleFullscreen),
@@ -1205,6 +1239,7 @@ const KEY_BINDINGS: &[Binding<&'static str>] = &[
     Binding::new("P", ModifiersState::CONTROL, Action::ToggleResizeIncrements),
     Binding::new("R", ModifiersState::CONTROL, Action::ToggleResizable),
     Binding::new("R", ModifiersState::ALT, Action::RequestResize),
+    Binding::new("R", ModifiersState::SHIFT, Action::ToggleContinuousRedraw),
     // M.
     Binding::new("M", ModifiersState::CONTROL.union(ModifiersState::ALT), Action::DumpMonitors),
     Binding::new("M", ModifiersState::CONTROL, Action::ToggleMaximize),
diff --git a/examples/util/fill.rs b/examples/util/fill.rs
index dab4a1d776..f7a6a4e2f0 100644
--- a/examples/util/fill.rs
+++ b/examples/util/fill.rs
@@ -12,6 +12,8 @@ pub use platform::cleanup_window;
 #[allow(unused_imports)]
 pub use platform::fill_window;
 #[allow(unused_imports)]
+pub use platform::fill_window_with_animated_color;
+#[allow(unused_imports)]
 pub use platform::fill_window_with_color;
 
 #[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -102,6 +104,16 @@ mod platform {
         fill_window_with_color(window, 0xff181818);
     }
 
+    #[allow(dead_code)]
+    pub fn fill_window_with_animated_color(window: &dyn Window, start: std::time::Instant) {
+        let time = start.elapsed().as_secs_f32() * 1.5;
+        let blue = (time.sin() * 255.0) as u32;
+        let green = ((time.cos() * 255.0) as u32) << 8;
+        let red = ((1.0 - time.sin() * 255.0) as u32) << 16;
+        let color = red | green | blue;
+        fill_window_with_color(window, color);
+    }
+
     #[allow(dead_code)]
     pub fn cleanup_window(window: &dyn Window) {
         GC.with(|gc| {
@@ -115,6 +127,7 @@ mod platform {
 
 #[cfg(any(target_os = "android", target_os = "ios"))]
 mod platform {
+    #[allow(dead_code)]
     pub fn fill_window(_window: &dyn winit::window::Window) {
         // No-op on mobile platforms.
     }
@@ -124,6 +137,14 @@ mod platform {
         // No-op on mobile platforms.
     }
 
+    #[allow(dead_code)]
+    pub fn fill_window_with_animated_color(
+        _window: &dyn winit::window::Window,
+        _start: std::time::Instant,
+    ) {
+        // No-op on mobile platforms.
+    }
+
     #[allow(dead_code)]
     pub fn cleanup_window(_window: &dyn winit::window::Window) {
         // No-op on mobile platforms.
diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md
index d1ce8b8000..26988a5900 100644
--- a/src/changelog/unreleased.md
+++ b/src/changelog/unreleased.md
@@ -236,3 +236,4 @@ changelog entry.
 - On macOS, fixed the scancode conversion for audio volume keys.
 - On macOS, fixed the scancode conversion for `IntlBackslash`.
 - On macOS, fixed redundant `SurfaceResized` event at window creation.
+- On Windows, fixed ~500 ms pause when clicking the title bar during continuous redraw.
diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs
index fb843d098b..95019a7196 100644
--- a/src/platform_impl/windows/event_loop.rs
+++ b/src/platform_impl/windows/event_loop.rs
@@ -1135,6 +1135,31 @@ unsafe fn public_window_callback_inner(
 
         WM_NCLBUTTONDOWN => {
             if wparam == HTCAPTION as _ {
+                // Prevent the user event loop from pausing when left clicking the title bar.
+                //
+                // When the user interacts with the title bar, Windows enters the modal event
+                // loop. Currently, a left click causes a pause for about 500ms. Sending a dummy
+                // mouse-move event seems to cancel the modal loop early, preventing the pause.
+                // The application will never see this dummy event.
+                //
+                // The mouse coordinates are encoded into the lparam value, however the WM_MOUSEMOVE
+                // event is not using the same coordinate system of the WM_NCLBUTTONDOWN event.
+                // One uses client-area coordinates and the other is screen-coordinates. In any
+                // case, passing the lparam as-is with the dummy event does not seem the cancel
+                // the modal loop.
+                //
+                // However, passing in a value of 0 has been observed to always cancel the pause.
+                //
+                // Other notes:
+                //
+                // For some unknown reason, the cursor will blink when clicking the title bar.
+                // Cancelling the modal loop early causes the blink to happen *immediately*.
+                // Otherwise, the blank happens *after* the pause.
+                //
+                // When right-click the title bar, the system window menu is presented to the user,
+                // and the modal event loop begins. This dummy event does *not* prevent the freeze
+                // in the main event loop caused by that popup menu.
+                let lparam = 0;
                 unsafe { PostMessageW(window, WM_MOUSEMOVE, 0, lparam) };
             }
             result = ProcResult::DefWindowProc(wparam);


Reviews:
user: login: madsmtm
body: So, I would quite like to get this fix in, as I believe it's desired by Bevy people, but we don't really have a Windows maintainer at the moment, and I don't feel like I understand enough of what's going on here myself to confidently merge this.

Maybe you could expand on why the `PostMessageW` is necessary in the first place (why is the default handler not sufficient?), why passing `lparam = 0` is correct, and if you know, how we'd go about fixing the blinking then? Ideally as code-comments with links to official docs, makes it easier to track in the future.

Also CC author of https://github.com/rust-windowing/winit/pull/1852 @Ssaely
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-09T21:18:53Z
user: login: madsmtm
body: Could I get you to you update the `application.rs` example instead?
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-16T13:11:37Z
user: login: filnet
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-16T20:07:27Z
user: login: aloucks
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-16T22:07:56Z
user: login: madsmtm
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-17T01:25:13Z

Review comments:
diff_hunk: @@ -1135,6 +1135,31 @@ unsafe fn public_window_callback_inner(
 
         WM_NCLBUTTONDOWN => {
             if wparam == HTCAPTION as _ {
+                // Prevent the user event loop from pausing when left clicking the title bar.
+                //
+                // When the user interacts with the title bar, Windows enters the modal event
+                // loop. Currently, a left click causes a pause for about 500ms. Sending a dummy
+                // mouse-move event seems to cancel the modal loop early, preventing the pause.
+                // The application will never see this dummy event.
+                //
+                // The mouse coordinates are encoded into the lparam value, however the WM_MOUSEMOVE
+                // event is not using the same coordinate system of the WM_NCLBUTTONDOWN event.
+                // One uses client-area coordinates and the other is screen-coordinates. In any
+                // case, passing the lparam as-is with the dummy event does not seem the cancel
+                // the modal loop.
+                //
+                // However, passing in a value of 0 has been observed to always cancel the pause.
+                //
+                // Other notes:
+                //
+                // For some unknown reason, the cursor will blink when clicking the title bar.
+                // Cancelling the modal loop early causes the blink to happen *immediately*.
+                // Otherwise, the blank happens *after* the pause.
+                //
+                // When right-click the title bar, the system window menu is presented to the user,
+                // and the modal event loop begins. This dummy event does *not* prevent the freeze
+                // in the main event loop imposed caused by that popup menu.
path: src/platform_impl/windows/event_loop.rs
user: login: filnet
body: imposed or caused ?
created_at: 2025-03-16T20:07:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1135,6 +1135,31 @@ unsafe fn public_window_callback_inner(
 
         WM_NCLBUTTONDOWN => {
             if wparam == HTCAPTION as _ {
+                // Prevent the user event loop from pausing when left clicking the title bar.
+                //
+                // When the user interacts with the title bar, Windows enters the modal event
+                // loop. Currently, a left click causes a pause for about 500ms. Sending a dummy
+                // mouse-move event seems to cancel the modal loop early, preventing the pause.
+                // The application will never see this dummy event.
+                //
+                // The mouse coordinates are encoded into the lparam value, however the WM_MOUSEMOVE
+                // event is not using the same coordinate system of the WM_NCLBUTTONDOWN event.
+                // One uses client-area coordinates and the other is screen-coordinates. In any
+                // case, passing the lparam as-is with the dummy event does not seem the cancel
+                // the modal loop.
+                //
+                // However, passing in a value of 0 has been observed to always cancel the pause.
+                //
+                // Other notes:
+                //
+                // For some unknown reason, the cursor will blink when clicking the title bar.
+                // Cancelling the modal loop early causes the blink to happen *immediately*.
+                // Otherwise, the blank happens *after* the pause.
+                //
+                // When right-click the title bar, the system window menu is presented to the user,
+                // and the modal event loop begins. This dummy event does *not* prevent the freeze
+                // in the main event loop imposed caused by that popup menu.
path: src/platform_impl/windows/event_loop.rs
user: login: aloucks
body: Updated
created_at: 2025-03-16T22:07:56Z
author_association: CONTRIBUTOR

PR comments:
user: login: madsmtm
created_at: 2025-03-01T19:53:05Z
body: Will this fix https://github.com/rust-windowing/winit/issues/1885 too?
author_association: MEMBER
user: login: aloucks
created_at: 2025-03-01T20:16:32Z
body: > Will this fix #1885 too?

Yes
author_association: CONTRIBUTOR
user: login: aloucks
created_at: 2025-03-09T22:37:06Z
body: > So, I would quite like to get this fix in, as I believe it's desired by Bevy people, but we don't really have a Windows maintainer at the moment, and I don't feel like I understand enough of what's going on here myself to confidently merge this.
> 
> Maybe you could expand on why the `PostMessageW` is necessary in the first place (why is the default handler not sufficient?), why passing `lparam = 0` is correct, and if you know, how we'd go about fixing the blinking then? Ideally as code-comments with links to official docs, makes it easier to track in the future.
> 
> Also CC author of #1852 @Ssaely

To be honest, I [fixed this 6 years ago](https://github.com/rust-windowing/winit/pull/839) and I don't really remember the details. I noticed it had regressed while exploring something else. The blinking happens even when the entire `WM_NCLBUTTONDOWN` event handler is commented out. The primary difference is that with the change, it blinks immediately instead of after the short pause.

The short pause is ingrained in how Windows works. If you start dragging the window immediately when you click, you won't notice a pause. 

I think what's happening is that the dummy `WM_MOUSEMOVE` is received in the system modal event loop and simulates a window drag, which cancels the pause. This would explain why the application never sees the dummy event.

I'm completely speculating here, but I'm guessing that windows is computing a delta to determine drag status and `(0, 0)` is always going to be far enough away that the simulated bogus drag cancels the pause. The coordinates are encoded into the lo/high bits of the `lparam` value. 

What I can't wrap my head around is that the `lparam` from `WM_NCLBUTTONDOWN` shouldn't even be valid for the `WM_MOUSEMOVE` event. The `lparam` value of `WM_MOUSEMOVE` is the `(x, y)` coordinates of the **client area**, where as the `WM_NCLBUTTONDOWN` `lparam` is the `(x, y)` in **screen coordinates**. I really don't know why passing in the `lparam` from `WM_NCLBUTTONDOWN` doesn't also cancel the pause. I tested other `lparam` values for the dummy event with mixed results. Passing in `!lparam` would cancel the pause, where as a value that was only marginally different would not cancel it. (e.g. `lparam + 1`). 

In summary, this is absolutely a hacky work-around :)

**The actual robust and generally accepted solution is to execute your simulation and rendering logic from a different thread.** 
author_association: CONTRIBUTOR
user: login: madsmtm
created_at: 2025-03-10T11:38:52Z
body: Thanks a lot for the context! I'd be okay with merging this if you included basically the block of text you wrote here as a code comment.
author_association: MEMBER
user: login: aloucks
created_at: 2025-03-14T19:33:59Z
body: @madsmtm 
I added additional comments and updated the window example to better detect when the event loop freezes. There's now an option to animate the fill color. 
author_association: CONTRIBUTOR
user: login: aloucks
created_at: 2025-03-16T15:01:12Z
body: > Could I get you to you update the `application.rs` example instead?

I was actually going to ask about adding back more of the single purpose examples. That applicaiton.rs example is overwhelming. I can see how it may be useful in some cases, but the individual examples are sorely missed. 
author_association: CONTRIBUTOR
user: login: kchibisov
created_at: 2025-03-16T15:08:32Z
body: one line examples should be in a `method` docs respectively, no point to create too many files to show 1 function call.
author_association: MEMBER
user: login: aloucks
created_at: 2025-03-16T19:04:38Z
body: > no point to create too many files to show 1 function call.

Respectfully, I disagree with that philosophy when it comes to examples. Short concise and complete examples are easier for someone unfamiliar with the code to understand and help communicate what is relevant to the the goal of the example. They also serve as a way to test functionality independently. I'm a big fan of examples in documentation itself, but they are generally not complete or aren't helpful for iterating on a change. I see value in a larger integrated example; I just don't agree with the idea that there shouldn't be smaller examples too.

In any case, I've added new actions in `application.rs` for toggling continuous redraw as well as toggling the animated fill color.
author_association: CONTRIBUTOR
user: login: madsmtm
created_at: 2025-03-17T01:15:47Z
body: > > no point to create too many files to show 1 function call.
> 
> Respectfully, I disagree with that philosophy when it comes to examples.

I tend to agree, but this is the wrong place/time to discuss that (we can do that in another issue/PR if you want).

> In any case, I've added new actions in application.rs for toggling continuous redraw as well as toggling the animated fill color.

Thanks! Could I get you to remove it from `examples/window.rs`, then? I'd like to have at least one example that is as simple as possible (regardless of the number of examples we should have).
author_association: MEMBER
user: login: madsmtm
created_at: 2025-03-17T01:24:42Z
body: > Thanks! Could I get you to remove it from `examples/window.rs`, then? I'd like to have at least one example that is as simple as possible (regardless of the number of examples we should have).

Nvmd, I did it myself, hope you don't mind.

Again, happy to discuss changing our examples, but should be done in a separate issue/PR (EDIT: see https://github.com/rust-windowing/winit/issues/3512).
author_association: MEMBER