Skip to content

[windows] Restore framework cursor when mouse re-enters client area#189568

Draft
sero583 wants to merge 2 commits into
flutter:masterfrom
sero583:fix-windows-stale-cursor
Draft

[windows] Restore framework cursor when mouse re-enters client area#189568
sero583 wants to merge 2 commits into
flutter:masterfrom
sero583:fix-windows-stale-cursor

Conversation

@sero583

@sero583 sero583 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

On Windows, a cursor set while the pointer is over the non-client area (for example the resize arrow from a window border) sticks around after the pointer moves back into the Flutter content whenever the UI thread is busy at that moment. The cause is in FlutterWindow's WM_SETCURSOR handler: for HTCLIENT hits it returns TRUE without setting any cursor. That return is intentional (it stops DefWindowProc from resetting the cursor to the window class cursor on every mouse move), but it means the visible cursor is only corrected by the framework's next mouse_cursor channel update. On a responsive app that round trip completes within milliseconds; under UI-thread latency (app startup, shader-compilation jank, heavy frames, debug builds) the stale cursor stays visible for the whole stall. #189563 documents the analysis, measurements, and a deterministic repro.

This change makes the engine remember the cursor most recently requested by the framework (FlutterWindowsEngine::SetFlutterCursor already funnels every cursor update, both system and custom cursors) and makes the WM_SETCURSOR handler restore that cursor before halting processing, the way native Win32 apps answer the message. The cursor is then correct as soon as the window processes the message, without waiting for a channel round trip and without depending on the framework's cursor cache. The window pulls the cursor through a new WindowBindingHandlerDelegate::GetFlutterCursor(), so the engine remains the single source of truth and windows created at any time behave consistently.

Changes:

  • FlutterWindowsEngine: remember the last cursor set by the framework, expose it via GetFlutterCursor(). UpdateFlutterCursor/SetFlutterCursor lose their const since they now record state.
  • WindowBindingHandlerDelegate: new GetFlutterCursor() pure virtual, implemented by FlutterWindowsView (forwards to the engine) and mocked for tests.
  • FlutterWindow: WM_SETCURSOR/HTCLIENT now restores the framework cursor before returning TRUE.

Tests:

  • FlutterWindowTest.OnSetCursorRestoresFlutterCursor: WM_SETCURSOR over the client area queries the delegate and activates the returned cursor.
  • FlutterWindowTest.OnSetCursorNonClientAreaLeavesCursorAlone: non-client hits do not touch the cursor.
  • FlutterWindowsEngineTest.SetFlutterCursorStoresCursor: the engine records the cursor and still forwards it to ::SetCursor via WindowsProcTable.

Part of #189563

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

The cursor set while the mouse is over the non-client area (e.g. a
resize arrow over the window border) persisted after the mouse
re-entered the client area: the WM_SETCURSOR handler halts processing
to protect against DefWindowProc resetting the class cursor, but that
also prevents the stale non-client cursor from being replaced until
the framework happens to push a new cursor update.

Remember the last cursor set by the framework in FlutterWindowsEngine
and restore it from the WM_SETCURSOR handler when the hit test result
is HTCLIENT.
@sero583
sero583 requested a review from a team as a code owner July 16, 2026 09:25
@github-actions github-actions Bot added engine flutter/engine related. See also e: labels. platform-windows Building on or for Windows specifically a: desktop Running on desktop team-windows Owned by the Windows platform team labels Jul 16, 2026
@sero583
sero583 marked this pull request as draft July 16, 2026 09:27

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request modifies the Windows platform shell to restore the cursor requested by the Flutter framework when the mouse re-enters the window's client area during WM_SETCURSOR events. The engine now tracks the current cursor, and a new GetFlutterCursor method has been introduced to retrieve it. The review feedback suggests declaring this new GetFlutterCursor method as const across the interface, implementation, and mock classes to adhere to C++ const-correctness guidelines.

// framework's cursor when the mouse re-enters the client area, as the
// cursor may have been changed while over the non-client area (e.g. a
// resize arrow over the window border).
virtual HCURSOR GetFlutterCursor() = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The GetFlutterCursor method is a getter that does not modify any state. To adhere to C++ const-correctness best practices (as recommended by the Google C++ Style Guide), this virtual method should be declared as const.

Suggested change
virtual HCURSOR GetFlutterCursor() = 0;
virtual HCURSOR GetFlutterCursor() const = 0;
References
  1. Google C++ Style Guide recommends using const on member functions that do not modify the state of the object. (link)

void OnHighContrastChanged() override;

// |WindowBindingHandlerDelegate|
HCURSOR GetFlutterCursor() override;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since GetFlutterCursor in the base interface WindowBindingHandlerDelegate should be const, this override in FlutterWindowsView should also be declared as const.

Suggested change
HCURSOR GetFlutterCursor() override;
HCURSOR GetFlutterCursor() const override;
References
  1. Google C++ Style Guide recommends using const on member functions that do not modify the state of the object. (link)

Comment on lines +868 to +870
HCURSOR FlutterWindowsView::GetFlutterCursor() {
return engine_->GetFlutterCursor();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the implementation of GetFlutterCursor to be const to match the updated declaration in the header file.

Suggested change
HCURSOR FlutterWindowsView::GetFlutterCursor() {
return engine_->GetFlutterCursor();
}
HCURSOR FlutterWindowsView::GetFlutterCursor() const {
return engine_->GetFlutterCursor();
}
References
  1. Google C++ Style Guide recommends using const on member functions that do not modify the state of the object. (link)

(),
(override));

MOCK_METHOD(HCURSOR, GetFlutterCursor, (), (override));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the mock method definition to include the const qualifier to match the updated interface.

Suggested change
MOCK_METHOD(HCURSOR, GetFlutterCursor, (), (override));
MOCK_METHOD(HCURSOR, GetFlutterCursor, (), (const, override));
References
  1. Google C++ Style Guide recommends using const on member functions that do not modify the state of the object. (link)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: desktop Running on desktop engine flutter/engine related. See also e: labels. platform-windows Building on or for Windows specifically team-windows Owned by the Windows platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant