Repository: flutter/flutter
Pull request #163855: Update the mouse cursor handler to work with multi-view on Windows
Author: login: hbatagelo
Labels: name: framework
description: flutter/packages/flutter repository. See also f: labels.
name: engine
description: flutter/engine related. See also e: labels.
name: platform-windows
description: Building on or for Windows specifically
name: a: desktop
description: Running on desktop
Created: 2025-02-21T16:26:10Z | Merged: 2025-03-13T22:39:53Z

Partially fixes https://github.com/flutter/flutter/issues/142845.

This PR removes implicit view assumptions from the `'flutter/mousecursor'` channel handler on Windows.

The cursor is now set for all existing views, ensuring that the current `FlutterWindow` with the cursor in its client area uses the correct cursor. The method arguments remain unchanged.

Cursor handler tests have also been updated to avoid assuming an implicit view.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is [test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on [Discord].

<!-- Links -->
[Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md



Diff:
diff --git a/engine/src/flutter/shell/platform/windows/cursor_handler.cc b/engine/src/flutter/shell/platform/windows/cursor_handler.cc
index 19cd90567a695..638fd59ead112 100644
--- a/engine/src/flutter/shell/platform/windows/cursor_handler.cc
+++ b/engine/src/flutter/shell/platform/windows/cursor_handler.cc
@@ -8,7 +8,6 @@
 
 #include "flutter/shell/platform/common/client_wrapper/include/flutter/standard_method_codec.h"
 #include "flutter/shell/platform/windows/flutter_windows_engine.h"
-#include "flutter/shell/platform/windows/flutter_windows_view.h"
 
 static constexpr char kChannelName[] = "flutter/mousecursor";
 
@@ -74,16 +73,7 @@ void CursorHandler::HandleMethodCall(
       return;
     }
     const auto& kind = std::get<std::string>(kind_iter->second);
-
-    // TODO(loicsharma): Remove implicit view assumption.
-    // https://github.com/flutter/flutter/issues/142845
-    FlutterWindowsView* view = engine_->view(kImplicitViewId);
-    if (view == nullptr) {
-      result->Error(kCursorError,
-                    "Cursor is not available in Windows headless mode");
-      return;
-    }
-    view->UpdateFlutterCursor(kind);
+    engine_->UpdateFlutterCursor(kind);
     result->Success();
   } else if (method.compare(kCreateCustomCursorMethod) == 0) {
     const auto& arguments = std::get<EncodableMap>(*method_call.arguments());
@@ -167,16 +157,7 @@ void CursorHandler::HandleMethodCall(
       return;
     }
     HCURSOR cursor = custom_cursors_[name];
-
-    // TODO(loicsharma): Remove implicit view assumption.
-    // https://github.com/flutter/flutter/issues/142845
-    FlutterWindowsView* view = engine_->view(kImplicitViewId);
-    if (view == nullptr) {
-      result->Error(kCursorError,
-                    "Cursor is not available in Windows headless mode");
-      return;
-    }
-    view->SetFlutterCursor(cursor);
+    engine_->SetFlutterCursor(cursor);
     result->Success();
   } else if (method.compare(kDeleteCustomCursorMethod) == 0) {
     const auto& arguments = std::get<EncodableMap>(*method_call.arguments());
diff --git a/engine/src/flutter/shell/platform/windows/cursor_handler_unittests.cc b/engine/src/flutter/shell/platform/windows/cursor_handler_unittests.cc
index f6bc9c50a8126..c93cc4905bfbe 100644
--- a/engine/src/flutter/shell/platform/windows/cursor_handler_unittests.cc
+++ b/engine/src/flutter/shell/platform/windows/cursor_handler_unittests.cc
@@ -14,6 +14,7 @@
 #include "flutter/shell/platform/windows/testing/engine_modifier.h"
 #include "flutter/shell/platform/windows/testing/flutter_windows_engine_builder.h"
 #include "flutter/shell/platform/windows/testing/mock_window_binding_handler.h"
+#include "flutter/shell/platform/windows/testing/mock_windows_proc_table.h"
 #include "flutter/shell/platform/windows/testing/test_binary_messenger.h"
 #include "flutter/shell/platform/windows/testing/windows_test.h"
 #include "gmock/gmock.h"
@@ -24,6 +25,7 @@ namespace testing {
 
 namespace {
 using ::testing::_;
+using ::testing::IsNull;
 using ::testing::NotNull;
 using ::testing::Return;
 
@@ -63,6 +65,7 @@ class CursorHandlerTest : public WindowsTest {
   FlutterWindowsEngine* engine() { return engine_.get(); }
   FlutterWindowsView* view() { return view_.get(); }
   MockWindowBindingHandler* window() { return window_; }
+  MockWindowsProcTable* proc_table() { return windows_proc_table_.get(); }
 
   void UseHeadlessEngine() {
     FlutterWindowsEngineBuilder builder{GetContext()};
@@ -71,7 +74,10 @@ class CursorHandlerTest : public WindowsTest {
   }
 
   void UseEngineWithView() {
+    windows_proc_table_ = std::make_shared<MockWindowsProcTable>();
+
     FlutterWindowsEngineBuilder builder{GetContext()};
+    builder.SetWindowsProcTable(windows_proc_table_);
 
     auto window = std::make_unique<MockWindowBindingHandler>();
     EXPECT_CALL(*window.get(), SetView).Times(1);
@@ -79,17 +85,14 @@ class CursorHandlerTest : public WindowsTest {
 
     window_ = window.get();
     engine_ = builder.Build();
-    view_ = std::make_unique<FlutterWindowsView>(kImplicitViewId, engine_.get(),
-                                                 std::move(window));
-
-    EngineModifier modifier{engine_.get()};
-    modifier.SetImplicitView(view_.get());
+    view_ = engine_->CreateView(std::move(window));
   }
 
  private:
   std::unique_ptr<FlutterWindowsEngine> engine_;
   std::unique_ptr<FlutterWindowsView> view_;
   MockWindowBindingHandler* window_;
+  std::shared_ptr<MockWindowsProcTable> windows_proc_table_;
 
   FML_DISALLOW_COPY_AND_ASSIGN(CursorHandlerTest);
 };
@@ -100,7 +103,8 @@ TEST_F(CursorHandlerTest, ActivateSystemCursor) {
   TestBinaryMessenger messenger;
   CursorHandler cursor_handler(&messenger, engine());
 
-  EXPECT_CALL(*window(), UpdateFlutterCursor("click")).Times(1);
+  EXPECT_CALL(*proc_table(), LoadCursor(IsNull(), IDC_HAND)).Times(1);
+  EXPECT_CALL(*proc_table(), SetCursor).Times(1);
 
   bool success = false;
   MethodResultFunctions<> result_handler(
@@ -120,33 +124,6 @@ TEST_F(CursorHandlerTest, ActivateSystemCursor) {
   EXPECT_TRUE(success);
 }
 
-TEST_F(CursorHandlerTest, ActivateSystemCursorRequiresView) {
-  UseHeadlessEngine();
-
-  TestBinaryMessenger messenger;
-  CursorHandler cursor_handler(&messenger, engine());
-
-  bool error = false;
-  MethodResultFunctions<> result_handler(
-      nullptr,
-      [&error](const std::string& error_code, const std::string& error_message,
-               const EncodableValue* value) {
-        error = true;
-        EXPECT_EQ(error_message,
-                  "Cursor is not available in Windows headless mode");
-      },
-      nullptr);
-
-  SimulateCursorMessage(&messenger, kActivateSystemCursorMethod,
-                        std::make_unique<EncodableValue>(EncodableMap{
-                            {EncodableValue("device"), EncodableValue(0)},
-                            {EncodableValue("kind"), EncodableValue("click")},
-                        }),
-                        &result_handler);
-
-  EXPECT_TRUE(error);
-}
-
 TEST_F(CursorHandlerTest, CreateCustomCursor) {
   UseEngineWithView();
 
@@ -196,7 +173,8 @@ TEST_F(CursorHandlerTest, SetCustomCursor) {
       },
       nullptr, nullptr);
 
-  EXPECT_CALL(*window(), SetFlutterCursor(/*cursor=*/NotNull())).Times(1);
+  EXPECT_CALL(*proc_table(), LoadCursor).Times(0);
+  EXPECT_CALL(*proc_table(), SetCursor(NotNull())).Times(1);
 
   SimulateCursorMessage(&messenger, kCreateCustomCursorMethod,
                         std::make_unique<EncodableValue>(EncodableMap{
@@ -218,47 +196,6 @@ TEST_F(CursorHandlerTest, SetCustomCursor) {
   EXPECT_TRUE(success);
 }
 
-TEST_F(CursorHandlerTest, SetCustomCursorRequiresView) {
-  UseHeadlessEngine();
-
-  TestBinaryMessenger messenger;
-  CursorHandler cursor_handler(&messenger, engine());
-
-  // Create a 4x4 raw BGRA test cursor buffer.
-  std::vector<uint8_t> buffer(4 * 4 * 4, 0);
-
-  bool error = false;
-  MethodResultFunctions<> create_result_handler(nullptr, nullptr, nullptr);
-  MethodResultFunctions<> set_result_handler(
-      nullptr,
-      [&error](const std::string& error_code, const std::string& error_message,
-               const EncodableValue* value) {
-        error = true;
-        EXPECT_EQ(error_message,
-                  "Cursor is not available in Windows headless mode");
-      },
-      nullptr);
-
-  SimulateCursorMessage(&messenger, kCreateCustomCursorMethod,
-                        std::make_unique<EncodableValue>(EncodableMap{
-                            {EncodableValue("name"), EncodableValue("hello")},
-                            {EncodableValue("buffer"), EncodableValue(buffer)},
-                            {EncodableValue("width"), EncodableValue(4)},
-                            {EncodableValue("height"), EncodableValue(4)},
-                            {EncodableValue("hotX"), EncodableValue(0.0)},
-                            {EncodableValue("hotY"), EncodableValue(0.0)},
-                        }),
-                        &create_result_handler);
-
-  SimulateCursorMessage(&messenger, kSetCustomCursorMethod,
-                        std::make_unique<EncodableValue>(EncodableMap{
-                            {EncodableValue("name"), EncodableValue("hello")},
-                        }),
-                        &set_result_handler);
-
-  EXPECT_TRUE(error);
-}
-
 TEST_F(CursorHandlerTest, SetNonexistentCustomCursor) {
   UseEngineWithView();
 
@@ -277,7 +214,8 @@ TEST_F(CursorHandlerTest, SetNonexistentCustomCursor) {
       },
       nullptr);
 
-  EXPECT_CALL(*window(), SetFlutterCursor).Times(0);
+  EXPECT_CALL(*proc_table(), LoadCursor).Times(0);
+  EXPECT_CALL(*proc_table(), SetCursor).Times(0);
 
   SimulateCursorMessage(&messenger, kSetCustomCursorMethod,
                         std::make_unique<EncodableValue>(EncodableMap{
diff --git a/engine/src/flutter/shell/platform/windows/flutter_window.cc b/engine/src/flutter/shell/platform/windows/flutter_window.cc
index fddba0267910e..6c781c41d12e9 100644
--- a/engine/src/flutter/shell/platform/windows/flutter_window.cc
+++ b/engine/src/flutter/shell/platform/windows/flutter_window.cc
@@ -30,49 +30,6 @@ static const int kMaxTouchDeviceId = 128;
 
 static const int kLinesPerScrollWindowsDefault = 3;
 
-// Maps a Flutter cursor name to an HCURSOR.
-//
-// Returns the arrow cursor for unknown constants.
-//
-// This map must be kept in sync with Flutter framework's
-// services/mouse_cursor.dart.
-static HCURSOR GetCursorByName(const std::string& cursor_name) {
-  static auto* cursors = new std::map<std::string, const wchar_t*>{
-      {"allScroll", IDC_SIZEALL},
-      {"basic", IDC_ARROW},
-      {"click", IDC_HAND},
-      {"forbidden", IDC_NO},
-      {"help", IDC_HELP},
-      {"move", IDC_SIZEALL},
-      {"none", nullptr},
-      {"noDrop", IDC_NO},
-      {"precise", IDC_CROSS},
-      {"progress", IDC_APPSTARTING},
-      {"text", IDC_IBEAM},
-      {"resizeColumn", IDC_SIZEWE},
-      {"resizeDown", IDC_SIZENS},
-      {"resizeDownLeft", IDC_SIZENESW},
-      {"resizeDownRight", IDC_SIZENWSE},
-      {"resizeLeft", IDC_SIZEWE},
-      {"resizeLeftRight", IDC_SIZEWE},
-      {"resizeRight", IDC_SIZEWE},
-      {"resizeRow", IDC_SIZENS},
-      {"resizeUp", IDC_SIZENS},
-      {"resizeUpDown", IDC_SIZENS},
-      {"resizeUpLeft", IDC_SIZENWSE},
-      {"resizeUpRight", IDC_SIZENESW},
-      {"resizeUpLeftDownRight", IDC_SIZENWSE},
-      {"resizeUpRightDownLeft", IDC_SIZENESW},
-      {"wait", IDC_WAIT},
-  };
-  const wchar_t* idc_name = IDC_ARROW;
-  auto it = cursors->find(cursor_name);
-  if (it != cursors->end()) {
-    idc_name = it->second;
-  }
-  return ::LoadCursor(nullptr, idc_name);
-}
-
 static constexpr int32_t kDefaultPointerDeviceId = 0;
 
 // This method is only valid during a window message related to mouse/touch
@@ -144,7 +101,6 @@ FlutterWindow::FlutterWindow(
   keyboard_manager_ = std::make_unique<KeyboardManager>(this);
 
   InitializeChild("FLUTTERVIEW", width, height);
-  current_cursor_ = ::LoadCursor(nullptr, IDC_ARROW);
 }
 
 // Base constructor for mocks
@@ -176,15 +132,6 @@ PhysicalWindowBounds FlutterWindow::GetPhysicalWindowBounds() {
   return {GetCurrentWidth(), GetCurrentHeight()};
 }
 
-void FlutterWindow::UpdateFlutterCursor(const std::string& cursor_name) {
-  SetFlutterCursor(GetCursorByName(cursor_name));
-}
-
-void FlutterWindow::SetFlutterCursor(HCURSOR cursor) {
-  current_cursor_ = cursor;
-  ::SetCursor(current_cursor_);
-}
-
 bool FlutterWindow::Focus() {
   auto hwnd = GetWindowHandle();
   if (hwnd == nullptr) {
@@ -257,10 +204,6 @@ void FlutterWindow::OnPointerLeave(double x,
   binding_handler_delegate_->OnPointerLeave(x, y, device_kind, device_id);
 }
 
-void FlutterWindow::OnSetCursor() {
-  ::SetCursor(current_cursor_);
-}
-
 void FlutterWindow::OnText(const std::u16string& text) {
   binding_handler_delegate_->OnText(text);
 }
@@ -649,7 +592,8 @@ FlutterWindow::HandleMessage(UINT const message,
     case WM_SETCURSOR: {
       UINT hit_test_result = LOWORD(lparam);
       if (hit_test_result == HTCLIENT) {
-        OnSetCursor();
+        // Halt further processing to prevent DefWindowProc from setting the
+        // cursor back to the registered class cursor.
         return TRUE;
       }
       break;
diff --git a/engine/src/flutter/shell/platform/windows/flutter_window.h b/engine/src/flutter/shell/platform/windows/flutter_window.h
index 1ce6cd5987e20..01d3175b0410e 100644
--- a/engine/src/flutter/shell/platform/windows/flutter_window.h
+++ b/engine/src/flutter/shell/platform/windows/flutter_window.h
@@ -104,9 +104,6 @@ class FlutterWindow : public KeyboardManager::WindowDelegate,
                               FlutterPointerDeviceKind device_kind,
                               int32_t device_id);
 
-  // Called when the cursor should be set for the client area.
-  virtual void OnSetCursor();
-
   // |WindowBindingHandlerDelegate|
   virtual void OnText(const std::u16string& text) override;
 
@@ -161,12 +158,6 @@ class FlutterWindow : public KeyboardManager::WindowDelegate,
   // |FlutterWindowBindingHandler|
   virtual PhysicalWindowBounds GetPhysicalWindowBounds() override;
 
-  // |FlutterWindowBindingHandler|
-  virtual void UpdateFlutterCursor(const std::string& cursor_name) override;
-
-  // |FlutterWindowBindingHandler|
-  virtual void SetFlutterCursor(HCURSOR cursor) override;
-
   // |FlutterWindowBindingHandler|
   virtual bool OnBitmapSurfaceCleared() override;
 
@@ -329,9 +320,6 @@ class FlutterWindow : public KeyboardManager::WindowDelegate,
   // windowing and input state.
   WindowBindingHandlerDelegate* binding_handler_delegate_ = nullptr;
 
-  // The last cursor set by Flutter. Defaults to the arrow cursor.
-  HCURSOR current_cursor_;
-
   // The cursor rect set by Flutter.
   RECT cursor_rect_;
 
diff --git a/engine/src/flutter/shell/platform/windows/flutter_window_unittests.cc b/engine/src/flutter/shell/platform/windows/flutter_window_unittests.cc
index 8f0d6578fe835..6d4304d6e87e3 100644
--- a/engine/src/flutter/shell/platform/windows/flutter_window_unittests.cc
+++ b/engine/src/flutter/shell/platform/windows/flutter_window_unittests.cc
@@ -66,7 +66,6 @@ class MockFlutterWindow : public FlutterWindow {
               OnPointerLeave,
               (double, double, FlutterPointerDeviceKind, int32_t),
               (override));
-  MOCK_METHOD(void, OnSetCursor, (), (override));
   MOCK_METHOD(float, GetScrollOffsetMultiplier, (), (override));
   MOCK_METHOD(float, GetDpiScale, (), (override));
   MOCK_METHOD(void, UpdateCursorRect, (const Rect&), (override));
@@ -422,12 +421,5 @@ TEST_F(FlutterWindowTest, CachedLifecycleMessage) {
   EXPECT_TRUE(restored);
 }
 
-TEST_F(FlutterWindowTest, UpdateCursor) {
-  FlutterWindow win32window(100, 100);
-  win32window.UpdateFlutterCursor("text");
-  HCURSOR cursor = ::GetCursor();
-  EXPECT_EQ(cursor, ::LoadCursor(nullptr, IDC_IBEAM));
-}
-
 }  // namespace testing
 }  // namespace flutter
diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc
index e8033bd098c3c..42667d00503ca 100644
--- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc
+++ b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc
@@ -801,6 +801,44 @@ void FlutterWindowsEngine::SetNextFrameCallback(fml::closure callback) {
       this);
 }
 
+HCURSOR FlutterWindowsEngine::GetCursorByName(
+    const std::string& cursor_name) const {
+  static auto* cursors = new std::map<std::string, const wchar_t*>{
+      {"allScroll", IDC_SIZEALL},
+      {"basic", IDC_ARROW},
+      {"click", IDC_HAND},
+      {"forbidden", IDC_NO},
+      {"help", IDC_HELP},
+      {"move", IDC_SIZEALL},
+      {"none", nullptr},
+      {"noDrop", IDC_NO},
+      {"precise", IDC_CROSS},
+      {"progress", IDC_APPSTARTING},
+      {"text", IDC_IBEAM},
+      {"resizeColumn", IDC_SIZEWE},
+      {"resizeDown", IDC_SIZENS},
+      {"resizeDownLeft", IDC_SIZENESW},
+      {"resizeDownRight", IDC_SIZENWSE},
+      {"resizeLeft", IDC_SIZEWE},
+      {"resizeLeftRight", IDC_SIZEWE},
+      {"resizeRight", IDC_SIZEWE},
+      {"resizeRow", IDC_SIZENS},
+      {"resizeUp", IDC_SIZENS},
+      {"resizeUpDown", IDC_SIZENS},
+      {"resizeUpLeft", IDC_SIZENWSE},
+      {"resizeUpRight", IDC_SIZENESW},
+      {"resizeUpLeftDownRight", IDC_SIZENWSE},
+      {"resizeUpRightDownLeft", IDC_SIZENESW},
+      {"wait", IDC_WAIT},
+  };
+  const wchar_t* idc_name = IDC_ARROW;
+  auto it = cursors->find(cursor_name);
+  if (it != cursors->end()) {
+    idc_name = it->second;
+  }
+  return windows_proc_table_->LoadCursor(nullptr, idc_name);
+}
+
 void FlutterWindowsEngine::SendSystemLocales() {
   std::vector<LanguageInfo> languages =
       GetPreferredLanguageInfo(*windows_proc_table_);
@@ -995,6 +1033,15 @@ std::optional<LRESULT> FlutterWindowsEngine::ProcessExternalWindowMessage(
   return std::nullopt;
 }
 
+void FlutterWindowsEngine::UpdateFlutterCursor(
+    const std::string& cursor_name) const {
+  SetFlutterCursor(GetCursorByName(cursor_name));
+}
+
+void FlutterWindowsEngine::SetFlutterCursor(HCURSOR cursor) const {
+  windows_proc_table_->SetCursor(cursor);
+}
+
 void FlutterWindowsEngine::OnChannelUpdate(std::string name, bool listening) {
   if (name == "flutter/platform" && listening) {
     lifecycle_manager_->BeginProcessingExit();
diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h
index 739912d943cc0..c4e590023ccd8 100644
--- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h
+++ b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h
@@ -307,6 +307,13 @@ class FlutterWindowsEngine {
     return windows_proc_table_;
   }
 
+  // Sets the cursor that should be used when the mouse is over the Flutter
+  // content. See mouse_cursor.dart for the values and meanings of cursor_name.
+  void UpdateFlutterCursor(const std::string& cursor_name) const;
+
+  // Sets the cursor directly from a cursor handle.
+  void SetFlutterCursor(HCURSOR cursor) const;
+
  protected:
   // Creates the keyboard key handler.
   //
@@ -341,6 +348,14 @@ class FlutterWindowsEngine {
   // Allows swapping out embedder_api_ calls in tests.
   friend class EngineModifier;
 
+  // Maps a Flutter cursor name to an HCURSOR.
+  //
+  // Returns the arrow cursor for unknown constants.
+  //
+  // This map must be kept in sync with Flutter framework's
+  // services/mouse_cursor.dart.
+  HCURSOR GetCursorByName(const std::string& cursor_name) const;
+
   // Sends system locales to the engine.
   //
   // Should be called just after the engine is run, and after any relevant
diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_view.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_view.cc
index 7a990048916ab..a4f7523bad2a7 100644
--- a/engine/src/flutter/shell/platform/windows/flutter_windows_view.cc
+++ b/engine/src/flutter/shell/platform/windows/flutter_windows_view.cc
@@ -176,14 +176,6 @@ bool FlutterWindowsView::OnFrameGenerated(size_t width, size_t height) {
   return true;
 }
 
-void FlutterWindowsView::UpdateFlutterCursor(const std::string& cursor_name) {
-  binding_handler_->UpdateFlutterCursor(cursor_name);
-}
-
-void FlutterWindowsView::SetFlutterCursor(HCURSOR cursor) {
-  binding_handler_->SetFlutterCursor(cursor);
-}
-
 void FlutterWindowsView::ForceRedraw() {
   if (resize_status_ == ResizeState::kDone) {
     // Request new frame.
diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_view.h b/engine/src/flutter/shell/platform/windows/flutter_windows_view.h
index 98689daee28cb..66ab0f0f0a4da 100644
--- a/engine/src/flutter/shell/platform/windows/flutter_windows_view.h
+++ b/engine/src/flutter/shell/platform/windows/flutter_windows_view.h
@@ -125,13 +125,6 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate {
   // This completes a view resize if one is pending.
   virtual void OnFramePresented();
 
-  // Sets the cursor that should be used when the mouse is over the Flutter
-  // content. See mouse_cursor.dart for the values and meanings of cursor_name.
-  void UpdateFlutterCursor(const std::string& cursor_name);
-
-  // Sets the cursor directly from a cursor handle.
-  void SetFlutterCursor(HCURSOR cursor);
-
   // |WindowBindingHandlerDelegate|
   bool OnWindowSizeChanged(size_t width, size_t height) override;
 
diff --git a/engine/src/flutter/shell/platform/windows/testing/mock_window.h b/engine/src/flutter/shell/platform/windows/testing/mock_window.h
index d62601c619f40..4a1007840ba30 100644
--- a/engine/src/flutter/shell/platform/windows/testing/mock_window.h
+++ b/engine/src/flutter/shell/platform/windows/testing/mock_window.h
@@ -54,7 +54,6 @@ class MockWindow : public FlutterWindow {
               OnPointerLeave,
               (double, double, FlutterPointerDeviceKind, int32_t),
               (override));
-  MOCK_METHOD(void, OnSetCursor, (), (override));
   MOCK_METHOD(void, OnText, (const std::u16string&), (override));
   MOCK_METHOD(void,
               OnKey,
diff --git a/engine/src/flutter/shell/platform/windows/testing/mock_window_binding_handler.h b/engine/src/flutter/shell/platform/windows/testing/mock_window_binding_handler.h
index 9524428500428..dcb6b50d8d41a 100644
--- a/engine/src/flutter/shell/platform/windows/testing/mock_window_binding_handler.h
+++ b/engine/src/flutter/shell/platform/windows/testing/mock_window_binding_handler.h
@@ -23,11 +23,6 @@ class MockWindowBindingHandler : public WindowBindingHandler {
   MOCK_METHOD(HWND, GetWindowHandle, (), (override));
   MOCK_METHOD(float, GetDpiScale, (), (override));
   MOCK_METHOD(PhysicalWindowBounds, GetPhysicalWindowBounds, (), (override));
-  MOCK_METHOD(void,
-              UpdateFlutterCursor,
-              (const std::string& cursor_name),
-              (override));
-  MOCK_METHOD(void, SetFlutterCursor, (HCURSOR cursor_name), (override));
   MOCK_METHOD(void, OnCursorRectUpdated, (const Rect& rect), (override));
   MOCK_METHOD(void, OnResetImeComposing, (), (override));
   MOCK_METHOD(bool, OnBitmapSurfaceCleared, (), (override));
diff --git a/engine/src/flutter/shell/platform/windows/testing/mock_windows_proc_table.h b/engine/src/flutter/shell/platform/windows/testing/mock_windows_proc_table.h
index 0d79e67c8e43c..f7e47b9f6030a 100644
--- a/engine/src/flutter/shell/platform/windows/testing/mock_windows_proc_table.h
+++ b/engine/src/flutter/shell/platform/windows/testing/mock_windows_proc_table.h
@@ -34,6 +34,13 @@ class MockWindowsProcTable : public WindowsProcTable {
 
   MOCK_METHOD(HRESULT, DwmFlush, (), (const, override));
 
+  MOCK_METHOD(HCURSOR,
+              LoadCursor,
+              (HINSTANCE instance, LPCWSTR cursor_name),
+              (const, override));
+
+  MOCK_METHOD(HCURSOR, SetCursor, (HCURSOR cursor), (const, override));
+
  private:
   FML_DISALLOW_COPY_AND_ASSIGN(MockWindowsProcTable);
 };
diff --git a/engine/src/flutter/shell/platform/windows/window_binding_handler.h b/engine/src/flutter/shell/platform/windows/window_binding_handler.h
index bfd24e0871362..90fa690ad3928 100644
--- a/engine/src/flutter/shell/platform/windows/window_binding_handler.h
+++ b/engine/src/flutter/shell/platform/windows/window_binding_handler.h
@@ -54,13 +54,6 @@ class WindowBindingHandler {
   // Returns the bounds of the backing window in physical pixels.
   virtual PhysicalWindowBounds GetPhysicalWindowBounds() = 0;
 
-  // Sets the cursor that should be used when the mouse is over the Flutter
-  // content. See mouse_cursor.dart for the values and meanings of cursor_name.
-  virtual void UpdateFlutterCursor(const std::string& cursor_name) = 0;
-
-  // Sets the cursor directly from a cursor handle.
-  virtual void SetFlutterCursor(HCURSOR cursor) = 0;
-
   // Invoked when the cursor/composing rect has been updated in the framework.
   virtual void OnCursorRectUpdated(const Rect& rect) = 0;
 
diff --git a/engine/src/flutter/shell/platform/windows/windows_proc_table.cc b/engine/src/flutter/shell/platform/windows/windows_proc_table.cc
index 663faaa73e789..2cb8e26fc1c66 100644
--- a/engine/src/flutter/shell/platform/windows/windows_proc_table.cc
+++ b/engine/src/flutter/shell/platform/windows/windows_proc_table.cc
@@ -58,4 +58,13 @@ HRESULT WindowsProcTable::DwmFlush() const {
   return ::DwmFlush();
 }
 
+HCURSOR WindowsProcTable::LoadCursor(HINSTANCE instance,
+                                     LPCWSTR cursor_name) const {
+  return ::LoadCursorW(instance, cursor_name);
+}
+
+HCURSOR WindowsProcTable::SetCursor(HCURSOR cursor) const {
+  return ::SetCursor(cursor);
+}
+
 }  // namespace flutter
diff --git a/engine/src/flutter/shell/platform/windows/windows_proc_table.h b/engine/src/flutter/shell/platform/windows/windows_proc_table.h
index ed0ed04ae6de8..dd1190a7f245d 100644
--- a/engine/src/flutter/shell/platform/windows/windows_proc_table.h
+++ b/engine/src/flutter/shell/platform/windows/windows_proc_table.h
@@ -57,6 +57,19 @@ class WindowsProcTable {
   // https://learn.microsoft.com/windows/win32/api/dwmapi/nf-dwmapi-dwmflush
   virtual HRESULT DwmFlush() const;
 
+  // Loads the specified cursor resource from the executable (.exe) file
+  // associated with an application instance.
+  //
+  // See:
+  // https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-loadcursorw
+  virtual HCURSOR LoadCursor(HINSTANCE instance, LPCWSTR cursor_name) const;
+
+  // Sets the cursor shape.
+  //
+  // See:
+  // https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-setcursor
+  virtual HCURSOR SetCursor(HCURSOR cursor) const;
+
  private:
   using GetPointerType_ = BOOL __stdcall(UINT32 pointerId,
                                          POINTER_INPUT_TYPE* pointerType);
diff --git a/packages/flutter/lib/src/services/mouse_cursor.dart b/packages/flutter/lib/src/services/mouse_cursor.dart
index 09fd6beb943d1..bfccef85dce14 100644
--- a/packages/flutter/lib/src/services/mouse_cursor.dart
+++ b/packages/flutter/lib/src/services/mouse_cursor.dart
@@ -408,8 +408,7 @@ abstract final class SystemMouseCursors {
   //
   // * Android: shell/platform/android/io/flutter/plugin/mouse/MouseCursorPlugin.java
   // * Web: lib/web_ui/lib/src/engine/mouse_cursor.dart
-  // * Windows: shell/platform/windows/win32_flutter_window_win32.cc
-  // * Windows UWP: shell/platform/windows/win32_flutter_window_winuwp.cc
+  // * Windows: shell/platform/windows/flutter_windows_engine.cc
   // * Linux: shell/platform/linux/fl_mouse_cursor_plugin.cc
   // * macOS: shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.mm
 


Reviews:
user: login: mattkae
body: This seems reasonable :+1: Do you have an example app that demonstrates this working.

Also, I would let others review this in addition to myself, just to be safe.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T16:07:31Z
user: login: loic-sharma
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-24T21:30:26Z
user: login: hbatagelo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T13:50:01Z
user: login: loic-sharma
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T16:03:22Z
user: login: hbatagelo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T17:42:38Z
user: login: dkwingsmt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T04:12:14Z
user: login: hbatagelo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T15:42:41Z
user: login: loic-sharma
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T19:07:05Z
user: login: loic-sharma
body: Very nice clean up and great test app! Excellent work 🥳 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-13T19:08:52Z

Review comments:
diff_hunk: @@ -983,6 +983,29 @@ std::optional<LRESULT> FlutterWindowsEngine::ProcessExternalWindowMessage(
   return std::nullopt;
 }
 
+bool FlutterWindowsEngine::UpdateFlutterCursor(
+    const std::string& cursor_name) const {
path: engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc
user: login: loic-sharma
body: If I update the cursor, then add a new view, will that new view use the correct cursor? Could you add a test for this scenario?
created_at: 2025-02-24T21:30:25Z
author_association: MEMBER
diff_hunk: @@ -983,6 +983,29 @@ std::optional<LRESULT> FlutterWindowsEngine::ProcessExternalWindowMessage(
   return std::nullopt;
 }
 
+bool FlutterWindowsEngine::UpdateFlutterCursor(
+    const std::string& cursor_name) const {
path: engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc
user: login: hbatagelo
body: Since the cursor is a shared global state on Windows, the cursor isn't set when a view is added. Instead, it's updated later when the framework calls "activateSystemCursor" or "setCustomCursor/windows", as setting the cursor only affects the window that contains the cursor in its client area or the window capturing the mouse. Now that the engine sets the cursor for all existing views, the view with the cursor in it will display the correct cursor type.

I could add a test to verify that the engine properly sets the cursor for all existing views and that the global cursor state matches the specified one. Would that help/be enough?
created_at: 2025-02-26T13:50:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -983,6 +983,29 @@ std::optional<LRESULT> FlutterWindowsEngine::ProcessExternalWindowMessage(
   return std::nullopt;
 }
 
+bool FlutterWindowsEngine::UpdateFlutterCursor(
+    const std::string& cursor_name) const {
path: engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc
user: login: loic-sharma
body: I apologize for the very late response. Feel free to ping me on Discord if I forget to follow-up on a pull request! :)

> Since the cursor is a shared global state on Windows

Ah right! Reading win32's `SetCursor` docs: https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-setcursor

```
The cursor is a shared resource. A window should set the cursor shape only when the cursor is in its client area or when the window is capturing mouse input. In systems without a mouse, the window should restore the previous cursor before the cursor leaves the client area or before it relinquishes control to another window.
```

Given the cursor is shared, it seems we should move cursor update/set logic up from the view and to the engine. Ideally we'd remove `FlutterWindowsView::UpdateFlutterCursor`, `FlutterWindowsView::SetFlutterCursor`, `WindowBindingHandler::UpdateFlutterCursor`, `WindowBindingHandler::SetFlutterCursor` and we'd add the `LoadCursor` and `SetCursor` win32 APIs to the [`WindowsProcTable`](https://github.com/flutter/flutter/blob/b60d60a166732ab7282523199b1f975b2c3dcd59/engine/src/flutter/shell/platform/windows/windows_proc_table.h) to allow tests to mock these APIs. If we do this, we don't need a test for the "new view uses the correct cursor" scenario. What do you think?

cc @dkwingsmt who worked on the desktop cursor implementation.

created_at: 2025-03-11T16:03:22Z
author_association: MEMBER
diff_hunk: @@ -983,6 +983,29 @@ std::optional<LRESULT> FlutterWindowsEngine::ProcessExternalWindowMessage(
   return std::nullopt;
 }
 
+bool FlutterWindowsEngine::UpdateFlutterCursor(
+    const std::string& cursor_name) const {
path: engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc
user: login: hbatagelo
body: Makes sense to me! I'll refactor as suggested -- no worries about the delay, this PR isn't blocking anything.
created_at: 2025-03-11T17:42:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -983,6 +983,29 @@ std::optional<LRESULT> FlutterWindowsEngine::ProcessExternalWindowMessage(
   return std::nullopt;
 }
 
+bool FlutterWindowsEngine::UpdateFlutterCursor(
+    const std::string& cursor_name) const {
path: engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc
user: login: dkwingsmt
body: Makes sense, but then does it mean we need to hook up the callbacks when Flutter windows gain focus and lose focus to set/reset the cursors? Is it currently supported?
created_at: 2025-03-12T04:12:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -983,6 +983,29 @@ std::optional<LRESULT> FlutterWindowsEngine::ProcessExternalWindowMessage(
   return std::nullopt;
 }
 
+bool FlutterWindowsEngine::UpdateFlutterCursor(
+    const std::string& cursor_name) const {
path: engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc
user: login: hbatagelo
body: This already seems to be properly set up. The framework is notified of pointer enter/leave events, which occur when focus changes.
created_at: 2025-03-12T15:42:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -341,6 +348,14 @@ class FlutterWindowsEngine {
   // Allows swapping out embedder_api_ calls in tests.
   friend class EngineModifier;
 
+  // Maps a Flutter cursor name to an HCURSOR.
+  //
+  // Returns the arrow cursor for unknown constants.
+  //
+  // This map must be kept in sync with Flutter framework's
+  // services/mouse_cursor.dart.
path: engine/src/flutter/shell/platform/windows/flutter_windows_engine.h
user: login: loic-sharma
body: Could we update these lines too? (The UWP line is very stale and can be removed entirely, we forgot to do that when we dropped the UWP embedder)

https://github.com/flutter/flutter/blob/c38f0b7aaee3651f0eb98c7987da420f4225f504/packages/flutter/lib/src/services/mouse_cursor.dart#L411-L412

created_at: 2025-03-13T19:07:05Z
author_association: MEMBER

PR comments:
user: login: hbatagelo
created_at: 2025-02-24T18:14:49Z
body: Thanks. For an example, merge with `canonical:foundation` and run the reference app. Hover over the "Regular" push button. The system cursor should change from the default arrow (`IDC_ARROW`) to a hand (`IDC_HAND`). Now, press the button to open a regular window with a new view. In the new window, the mouse cursor should also change to a hand when you hover over the push button (which it didn't before).



author_association: CONTRIBUTOR
user: login: chinmaygarde
created_at: 2025-03-10T18:01:05Z
body: @loic-sharma There is an open question for you. Can you clarify or approve?
author_association: CONTRIBUTOR
user: login: hbatagelo
created_at: 2025-03-13T16:38:41Z
body: I created a sample application and recorded a video to demonstrate cursor changes and custom cursors across multiple windows after the refactor. It uses the `custom_mouse_cursor` package along with the windowing API from [canonical:foundation-plus-framework](https://github.com/canonical/flutter/tree/foundation-plus-framework).

Video:
[cursor_multi_view.webm](https://github.com/user-attachments/assets/0789d44e-69bf-471c-87d1-b3281e98318d)

Code:
```dart
// Add to pubspec.yaml:
// dependencies:
//   custom_mouse_cursor: ^1.1.2

import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:custom_mouse_cursor/custom_mouse_cursor.dart';

late CustomMouseCursor iconCursor;

Future<void> initializeCursor() async {
  const List<Shadow> shadows = [
    BoxShadow(
      color: Color.fromRGBO(0, 0, 0, 0.8),
      offset: Offset(1, 1),
      blurRadius: 3,
      spreadRadius: 2,
    ),
  ];
  iconCursor = await CustomMouseCursor.icon(
    Icons.celebration,
    size: 48.0,
    hotX: 24,
    hotY: 24,
    color: Colors.blueAccent,
    shadows: shadows,
  );
}

void main() async {
  const Size size = Size(400, 300);
  final controller1 = RegularWindowController(size: size);
  final controller2 = RegularWindowController(size: size);

  await initializeCursor();

  runWidget(ViewCollection(views: [
    RegularWindow(
      controller: controller1,
      child: MyApp(otherController: controller2),
    ),
    RegularWindow(
      controller: controller2,
      child: MyApp(otherController: controller1),
    ),
  ]));
}

class MyApp extends StatefulWidget {
  MyApp({super.key, required this.otherController});

  final RegularWindowController otherController;
  final CustomMouseCursor cursor = iconCursor;

  @override
  MyAppState createState() => MyAppState();
}

class MyAppState extends State<MyApp> with TickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 4),
    )..repeat(reverse: true);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _focusOtherWindow() {
    WidgetsBinding.instance.platformDispatcher.requestViewFocusChange(
      viewId: widget.otherController.rootView.viewId,
      direction: ViewFocusDirection.forward,
      state: ViewFocusState.focused,
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('View ${View.of(context).viewId}')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              SizedBox(
                height: 40,
                child: LayoutBuilder(
                  builder: (context, constraints) {
                    return AnimatedBuilder(
                      animation: _controller,
                      builder: (context, child) {
                        return Stack(
                          children: [
                            Positioned(
                              left: _controller.value *
                                  (constraints.maxWidth - 150),
                              child: SizedBox(
                                width: 150,
                                child: OutlinedButton(
                                  onPressed: _focusOtherWindow,
                                  child: Text(
                                    'Focus View ${widget.otherController.isReady ? widget.otherController.rootView.viewId : 0}',
                                  ),
                                ),
                              ),
                            ),
                          ],
                        );
                      },
                    );
                  },
                ),
              ),
              const SizedBox(height: 30),
              MouseRegion(
                cursor: widget.cursor, // Use widget's cursor property
                child: GestureDetector(
                  child: Container(
                    decoration: const BoxDecoration(
                      color: Colors.white,
                      border: Border(
                        top: BorderSide(color: Colors.grey, width: 1),
                        bottom: BorderSide(color: Colors.grey, width: 1),
                      ),
                    ),
                    child: const Column(
                      mainAxisAlignment: MainAxisAlignment.spaceAround,
                      children: [
                        SizedBox(height: 8),
                        Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                              Text("Hover me!", style: TextStyle(fontSize: 18)),
                            ]),
                        SizedBox(height: 8),
                      ],
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
```

The animated button shows that the cursor changes even when the pointer remains still. It also shows that switching focus correctly updates the cursor in the new view.

Previously, setting the cursor when no views were available was an error. Now that the engine calls `SetCursor` directly, `SetFlutterCursor` and `UpdateFlutterCursor` no longer trigger errors when no views are present.
author_association: CONTRIBUTOR