winit icon indicating copy to clipboard operation
winit copied to clipboard

`ControlFlow` is ignored while window is being resized on Windows

Open hecrj opened this issue 2 years ago • 14 comments

The ControlFlow set on the EventLoopWindowTarget is currently ignored on Windows while a window is being resized.

As a result, the EventLoop is not resumed when it should during the resizing process when WaitUntil or Poll are set as the control flow.

Other platforms are unaffected and work as expected.

hecrj avatar Dec 19 '23 13:12 hecrj

I generally don't see what could be causing this, like the WM_SIZE handling is no different than anything else.

Would suggest to step with debugger or something like that before it tries to go back to polling.

kchibisov avatar Dec 19 '23 14:12 kchibisov

This seems to be happening when interacting with the window buttons as well.

https://github.com/rust-windowing/winit/assets/29180043/0d5b74c1-5e80-4a62-8336-702f3287ecdc

https://github.com/rust-windowing/winit/assets/29180043/e74b29eb-d0bc-4fca-beac-6fcd9a8efdf2

AustinMReppert avatar Dec 20 '23 14:12 AustinMReppert

I mean, the only thing that is helpful would be debugging the wait_for_msg inside the windows backend, given that I generally don't have a way to debug due to hard to access windows hardware, it could take time until someone on windows try to look into it.

kchibisov avatar Dec 20 '23 14:12 kchibisov

Some more info:

dispatch_peeked_messages never returns after the DispatchMessageW(msg = WM_NCLMOUSEDOWN) until after the user releases the mouse.

WM_PAINT events continuously flow while it's paused there.

dtzxporter avatar Dec 20 '23 23:12 dtzxporter

Some (long running) actions in Windows start modal event loops (e.g as mentioned resizing and non-client area interactions) on top of the current running event loop iteration. Before the event loop changes the Windows backend internally checked the control flow on WM_PAINT events to as sync points to handle control flow changes (feeding in NewEvent events and so on), but added quite some complexity. Redraws are now more decoupled from the normal event flow and don't act as these sync points anymore. I'm not sure what is the desired behavior to be fair.

Continous presentation should be possible by requesting redraws inside the redraw event to periodically schedule new paint events. (IIRC non-client area didn't work in the past and might need some internal handling to correctly trigger redraws)

msiglreith avatar Dec 21 '23 14:12 msiglreith

@msiglreith I think the issue is that there's no resize at all now and the events are not sent to user? Though, it's hard to get that based on video (where you must hunt for log and could easily miss) and report saying that it's just broken.

Could you look into that when you have time?

kchibisov avatar Dec 21 '23 18:12 kchibisov

Yes, this is the issue. It will repaint because the window itself is receiving paint events but the dispatcher is still stuck waiting for the resize event to stop. No other events will flow back to the user at this time, which makes it impossible to do other things besides repaint while resizing, but normally you'd be able to (and other platforms work properly).

dtzxporter avatar Dec 21 '23 18:12 dtzxporter

Posting an update on this as I tried a few things so far but couldn't come close to a proper fix so far. I looked into the non-client control part primarily as this appears to be more strict compared to the resize border:

As mentioned, the root cause is that DefWindowProc starts to block on WM_NCLBUTTONDOWN and won't return unless user input happens: nclbuttondown

Trying to wakeup this modal loop by manually scheduling a WM_PAINT, a timer to trigger WM_TIMER, or a custom event via PostMessageW doesn't work, the message may only be received afterwards. Not calling DefWindowProc to manually, for example, call the SC_CLOSE syscommand allows to re-implement the close button behavior similar to the case where the client draws the system controls. But this is still not free of issues as the button hover rendering is incorrect then. SetCapture preserves the pressed-hover rendering once moving the mouse inside the non-client area but re-entering will break the rendering again..

msiglreith avatar Jan 23 '24 20:01 msiglreith

@msiglreith Do you know why winit uses a hidden window on win32 to receive events? I believe this issue would also be solved by forwarding window events in the wndproc of each window separately rather than using a separate window to receive the events.

I've played with this design in my own rusty-windows testbed and I don't see why the current design was chosen.

dtzxporter avatar Jan 23 '24 20:01 dtzxporter

@dtzxporter There is basically one eventloop window which kind of ties things together: it handles 'global' events (like raw input) but also as interface for the user to handle custom events or calls, which should execute in the same thread/execution context as the event loop.

msiglreith avatar Jan 23 '24 21:01 msiglreith

Can't windows just reside on the main thread and requests basically being called via threaded_executor or something? And maybe some stuff via side channels to remove the delays, like for Window::request_redraw?

kchibisov avatar Jan 23 '24 21:01 kchibisov

Right, but why is that logic in an event loop window instead of literally in the event loop before:

TranslateMessage -> DispatchMessageW

You can handle raw input directly in that loop before TranslateMessage fires.

Custom events can be registered that also make it to the 'thread' that is running the dispatch loop.

Then, each window's wndproc handles it's specific events and forwards them to the winit event loop.

Adding a window and using it as an event loop seems like an unnecessary workaround for just processing in the main event loop. (And I believe this is the root cause of the issue because we can't wake up the other windows event loop while another is resizing, however, the main loop's dispatch loop will always fire)

dtzxporter avatar Jan 23 '24 21:01 dtzxporter

Yes, it probably could be replaced with another solution - raw input could be associated to no window and the OS will then pick the one currently in focus, but currently I don't see how this would help with the current issue. For resizing, in comparison to the control buttons, it's possible to poke the message queue to execute our callback but the OS will still block until the current action finishes.

msiglreith avatar Jan 24 '24 20:01 msiglreith

What I mean is that the window 'stuck' in the resize loop's wndproc can be used to continue to flow winit events?

SetTimer can also be used to wake up that loop provided you specify the hwnd of the window currently in the resize loop:

unsafe { SetTimer(window, 1337, 5000, None) };

WM_TIMER => {
            println!("timer!");
        }

dtzxporter avatar Jan 25 '24 17:01 dtzxporter