diff --git a/engine/src/flutter/shell/platform/windows/flutter_window.cc b/engine/src/flutter/shell/platform/windows/flutter_window.cc index 82e6cd9c48..fddba02679 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_window.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_window.cc @@ -185,6 +185,20 @@ void FlutterWindow::SetFlutterCursor(HCURSOR cursor) { ::SetCursor(current_cursor_); } +bool FlutterWindow::Focus() { + auto hwnd = GetWindowHandle(); + if (hwnd == nullptr) { + return false; + } + + HWND prevFocus = ::SetFocus(hwnd); + if (prevFocus == nullptr) { + return false; + } + + return true; +} + void FlutterWindow::OnDpiScale(unsigned int dpi) {}; // When DesktopWindow notifies that a WM_Size message has come in @@ -377,9 +391,19 @@ void FlutterWindow::OnWindowStateEvent(WindowStateEvent event) { break; case WindowStateEvent::kFocus: focused_ = true; + if (binding_handler_delegate_) { + binding_handler_delegate_->OnFocus( + FlutterViewFocusState::kFocused, + FlutterViewFocusDirection::kUndefined); + } break; case WindowStateEvent::kUnfocus: focused_ = false; + if (binding_handler_delegate_) { + binding_handler_delegate_->OnFocus( + FlutterViewFocusState::kUnfocused, + FlutterViewFocusDirection::kUndefined); + } break; } HWND hwnd = GetWindowHandle(); diff --git a/engine/src/flutter/shell/platform/windows/flutter_window.h b/engine/src/flutter/shell/platform/windows/flutter_window.h index b8560794ec..1ce6cd5987 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_window.h +++ b/engine/src/flutter/shell/platform/windows/flutter_window.h @@ -187,6 +187,9 @@ class FlutterWindow : public KeyboardManager::WindowDelegate, // |WindowBindingHandler| virtual ui::AXPlatformNodeWin* GetAlert() override; + // [WindowBindingHandler] + virtual bool Focus() override; + // Called to obtain a pointer to the fragment root delegate. virtual ui::AXFragmentRootDelegateWin* GetAxFragmentRootDelegate(); 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 4b6d076911..8f0d6578fe 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_window_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_window_unittests.cc @@ -18,6 +18,7 @@ namespace testing { using ::testing::_; using ::testing::AnyNumber; +using ::testing::Eq; using ::testing::Invoke; using ::testing::Return; @@ -372,9 +373,15 @@ TEST_F(FlutterWindowTest, LifecycleFocusMessages) { win32window.InjectWindowMessage(WM_SIZE, 0, MAKEWORD(1, 1)); EXPECT_EQ(last_event, WindowStateEvent::kShow); + EXPECT_CALL(delegate, OnFocus(Eq(FlutterViewFocusState::kFocused), + Eq(FlutterViewFocusDirection::kUndefined))) + .Times(1); win32window.InjectWindowMessage(WM_SETFOCUS, 0, 0); EXPECT_EQ(last_event, WindowStateEvent::kFocus); + EXPECT_CALL(delegate, OnFocus(Eq(FlutterViewFocusState::kUnfocused), + Eq(FlutterViewFocusDirection::kUndefined))) + .Times(1); win32window.InjectWindowMessage(WM_KILLFOCUS, 0, 0); EXPECT_EQ(last_event, WindowStateEvent::kUnfocus); } @@ -407,6 +414,9 @@ TEST_F(FlutterWindowTest, CachedLifecycleMessage) { } }); + EXPECT_CALL(delegate, OnFocus(Eq(FlutterViewFocusState::kFocused), + Eq(FlutterViewFocusDirection::kUndefined))) + .Times(1); win32window.SetView(&delegate); EXPECT_TRUE(focused); EXPECT_TRUE(restored); 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 6f993167ad..2833fe0fc7 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.cc @@ -393,6 +393,11 @@ bool FlutterWindowsEngine::Run(std::string_view entrypoint) { SAFE_ACCESS(update, listening, false)); } }; + args.view_focus_change_request_callback = + [](const FlutterViewFocusChangeRequest* request, void* user_data) { + auto host = static_cast(user_data); + host->OnViewFocusChangeRequest(request); + }; args.custom_task_runners = &custom_task_runners; @@ -711,6 +716,13 @@ void FlutterWindowsEngine::SendKeyEvent(const FlutterKeyEvent& event, } } +void FlutterWindowsEngine::SendViewFocusEvent( + const FlutterViewFocusEvent& event) { + if (engine_) { + embedder_api_.SendViewFocusEvent(engine_, &event); + } +} + bool FlutterWindowsEngine::SendPlatformMessage( const char* channel, const uint8_t* message, @@ -998,6 +1010,19 @@ void FlutterWindowsEngine::OnChannelUpdate(std::string name, bool listening) { } } +void FlutterWindowsEngine::OnViewFocusChangeRequest( + const FlutterViewFocusChangeRequest* request) { + std::shared_lock read_lock(views_mutex_); + + auto iterator = views_.find(request->view_id); + if (iterator == views_.end()) { + return; + } + + FlutterWindowsView* view = iterator->second; + view->Focus(); +} + bool FlutterWindowsEngine::Present(const FlutterPresentViewInfo* info) { // This runs on the raster thread. Lock the views map for the entirety of the // present operation to block the platform thread from destroying the 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 a2a33e277b..8b68ee15f6 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_engine.h @@ -185,6 +185,9 @@ class FlutterWindowsEngine { FlutterKeyEventCallback callback, void* user_data); + // Informs the engine of an incoming focus event. + void SendViewFocusEvent(const FlutterViewFocusEvent& event); + KeyboardHandlerBase* keyboard_key_handler() { return keyboard_key_handler_.get(); } @@ -331,6 +334,9 @@ class FlutterWindowsEngine { // channel. virtual void OnChannelUpdate(std::string name, bool listening); + virtual void OnViewFocusChangeRequest( + const FlutterViewFocusChangeRequest* request); + private: // Allows swapping out embedder_api_ calls in tests. friend class EngineModifier; diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc index 94e8452d4b..bda12c2674 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_engine_unittests.cc @@ -99,6 +99,7 @@ TEST_F(FlutterWindowsEngineTest, RunDoesExpectedInitialization) { EXPECT_NE(args->update_semantics_callback2, nullptr); EXPECT_EQ(args->update_semantics_node_callback, nullptr); EXPECT_EQ(args->update_semantics_custom_action_callback, nullptr); + EXPECT_NE(args->view_focus_change_request_callback, nullptr); args->custom_task_runners->thread_priority_setter( FlutterThreadPriority::kRaster); @@ -661,6 +662,7 @@ class MockFlutterWindowsView : public FlutterWindowsView { (ui::AXPlatformNodeWin*, ax::mojom::Event), (override)); MOCK_METHOD(HWND, GetWindowHandle, (), (const, override)); + MOCK_METHOD(bool, Focus, (), (override)); private: FML_DISALLOW_COPY_AND_ASSIGN(MockFlutterWindowsView); @@ -1346,5 +1348,22 @@ TEST_F(FlutterWindowsEngineTest, MergedUIThread) { ASSERT_EQ(*ui_thread_id, std::this_thread::get_id()); } +TEST_F(FlutterWindowsEngineTest, OnViewFocusChangeRequest) { + FlutterWindowsEngineBuilder builder{GetContext()}; + std::unique_ptr engine = builder.Build(); + auto window_binding_handler = + std::make_unique<::testing::NiceMock>(); + MockFlutterWindowsView view(engine.get(), std::move(window_binding_handler)); + + EngineModifier modifier(engine.get()); + modifier.SetImplicitView(&view); + + FlutterViewFocusChangeRequest request; + request.view_id = kImplicitViewId; + + EXPECT_CALL(view, Focus()).WillOnce(Return(true)); + modifier.OnViewFocusChangeRequest(&request); +} + } // namespace testing } // namespace flutter 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 dec6a111be..7a99004891 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_view.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_view.cc @@ -315,6 +315,11 @@ void FlutterWindowsView::OnKey(int key, SendKey(key, scancode, action, character, extended, was_down, callback); } +void FlutterWindowsView::OnFocus(FlutterViewFocusState focus_state, + FlutterViewFocusDirection direction) { + SendFocus(focus_state, direction); +} + void FlutterWindowsView::OnComposeBegin() { SendComposeBegin(); } @@ -368,7 +373,7 @@ void FlutterWindowsView::OnResetImeComposing() { binding_handler_->OnResetImeComposing(); } -// Sends new size information to FlutterEngine. +// Sends new size information to FlutterEngine. void FlutterWindowsView::SendWindowMetrics(size_t width, size_t height, double pixel_ratio) const { @@ -557,6 +562,16 @@ void FlutterWindowsView::SendKey(int key, }); } +void FlutterWindowsView::SendFocus(FlutterViewFocusState focus_state, + FlutterViewFocusDirection direction) { + FlutterViewFocusEvent event = {}; + event.struct_size = sizeof(event); + event.view_id = view_id_; + event.state = focus_state; + event.direction = direction; + engine_->SendViewFocusEvent(event); +} + void FlutterWindowsView::SendComposeBegin() { engine_->text_input_plugin()->ComposeBeginHook(); } @@ -826,6 +841,10 @@ void FlutterWindowsView::OnWindowStateEvent(HWND hwnd, WindowStateEvent event) { engine_->OnWindowStateEvent(hwnd, event); } +bool FlutterWindowsView::Focus() { + return binding_handler_->Focus(); +} + bool FlutterWindowsView::NeedsVsync() const { // If the Desktop Window Manager composition is enabled, // the system itself synchronizes with vsync. 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 4adb38bc07..98689daee2 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_view.h +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_view.h @@ -190,6 +190,10 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate { bool was_down, KeyEventCallback callback) override; + // |WindowBindingHandlerDelegate| + void OnFocus(FlutterViewFocusState focus_state, + FlutterViewFocusDirection direction) override; + // |WindowBindingHandlerDelegate| void OnComposeBegin() override; @@ -246,6 +250,10 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate { // |WindowBindingHandlerDelegate| void OnWindowStateEvent(HWND hwnd, WindowStateEvent event) override; + // Focus the view. + // Returns true if the view was focused. + virtual bool Focus(); + protected: virtual void NotifyWinEventWrapper(ui::AXPlatformNodeWin* node, ax::mojom::Event event); @@ -352,6 +360,10 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate { bool was_down, KeyEventCallback callback); + // Reports a focus event to Flutter engine. + void SendFocus(FlutterViewFocusState focus_state, + FlutterViewFocusDirection direction); + // Reports an IME compose begin event. // // Triggered when the user begins editing composing text using a multi-step diff --git a/engine/src/flutter/shell/platform/windows/flutter_windows_view_unittests.cc b/engine/src/flutter/shell/platform/windows/flutter_windows_view_unittests.cc index ed83153cdb..e7ce8f6aaf 100644 --- a/engine/src/flutter/shell/platform/windows/flutter_windows_view_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/flutter_windows_view_unittests.cc @@ -1668,5 +1668,38 @@ TEST(FlutterWindowsViewTest, UpdatesVSyncOnDwmUpdates) { } } +TEST(FlutterWindowsViewTest, FocusTriggersWindowFocus) { + std::unique_ptr engine = GetTestEngine(); + auto window_binding_handler = + std::make_unique>(); + EXPECT_CALL(*window_binding_handler, Focus()).WillOnce(Return(true)); + std::unique_ptr view = + engine->CreateView(std::move(window_binding_handler)); + EXPECT_TRUE(view->Focus()); +} + +TEST(FlutterWindowsViewTest, OnFocusTriggersSendFocusViewEvent) { + std::unique_ptr engine = GetTestEngine(); + auto window_binding_handler = + std::make_unique>(); + std::unique_ptr view = + engine->CreateView(std::move(window_binding_handler)); + + EngineModifier modifier(engine.get()); + bool received_focus_event = false; + modifier.embedder_api().SendViewFocusEvent = MOCK_ENGINE_PROC( + SendViewFocusEvent, [&](FLUTTER_API_SYMBOL(FlutterEngine) raw_engine, + FlutterViewFocusEvent const* event) { + EXPECT_EQ(event->state, FlutterViewFocusState::kFocused); + EXPECT_EQ(event->direction, FlutterViewFocusDirection::kUndefined); + EXPECT_EQ(event->view_id, view->view_id()); + EXPECT_EQ(event->struct_size, sizeof(FlutterViewFocusEvent)); + received_focus_event = true; + return kSuccess; + }); + view->OnFocus(FlutterViewFocusState::kFocused, + FlutterViewFocusDirection::kUndefined); + EXPECT_TRUE(received_focus_event); +} } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/shell/platform/windows/testing/engine_modifier.h b/engine/src/flutter/shell/platform/windows/testing/engine_modifier.h index bbf9925592..7874937a92 100644 --- a/engine/src/flutter/shell/platform/windows/testing/engine_modifier.h +++ b/engine/src/flutter/shell/platform/windows/testing/engine_modifier.h @@ -87,6 +87,10 @@ class EngineModifier { engine_->platform_view_plugin_ = std::move(manager); } + void OnViewFocusChangeRequest(const FlutterViewFocusChangeRequest* request) { + engine_->OnViewFocusChangeRequest(request); + } + private: FlutterWindowsEngine* engine_; 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 b53cfcbc07..9524428500 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 @@ -38,6 +38,7 @@ class MockWindowBindingHandler : public WindowBindingHandler { MOCK_METHOD(PointerLocation, GetPrimaryPointerLocation, (), (override)); MOCK_METHOD(AlertPlatformNodeDelegate*, GetAlertDelegate, (), (override)); MOCK_METHOD(ui::AXPlatformNodeWin*, GetAlert, (), (override)); + MOCK_METHOD(bool, Focus, (), (override)); private: FML_DISALLOW_COPY_AND_ASSIGN(MockWindowBindingHandler); diff --git a/engine/src/flutter/shell/platform/windows/testing/mock_window_binding_handler_delegate.h b/engine/src/flutter/shell/platform/windows/testing/mock_window_binding_handler_delegate.h index 9cc0ff4d47..7d50fba84d 100644 --- a/engine/src/flutter/shell/platform/windows/testing/mock_window_binding_handler_delegate.h +++ b/engine/src/flutter/shell/platform/windows/testing/mock_window_binding_handler_delegate.h @@ -53,6 +53,10 @@ class MockWindowBindingHandlerDelegate : public WindowBindingHandlerDelegate { OnKey, (int, int, int, char32_t, bool, bool, KeyEventCallback), (override)); + MOCK_METHOD(void, + OnFocus, + (FlutterViewFocusState, FlutterViewFocusDirection), + (override)); MOCK_METHOD(void, OnComposeBegin, (), (override)); MOCK_METHOD(void, OnComposeCommit, (), (override)); MOCK_METHOD(void, OnComposeEnd, (), (override)); 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 d0ef4565ba..bfd24e0871 100644 --- a/engine/src/flutter/shell/platform/windows/window_binding_handler.h +++ b/engine/src/flutter/shell/platform/windows/window_binding_handler.h @@ -90,6 +90,10 @@ class WindowBindingHandler { // Retrieve the alert node. virtual ui::AXPlatformNodeWin* GetAlert() = 0; + + // Focuses the current window. + // Returns true if the window was successfully focused. + virtual bool Focus() = 0; }; } // namespace flutter diff --git a/engine/src/flutter/shell/platform/windows/window_binding_handler_delegate.h b/engine/src/flutter/shell/platform/windows/window_binding_handler_delegate.h index 64694c9c1a..0245a74228 100644 --- a/engine/src/flutter/shell/platform/windows/window_binding_handler_delegate.h +++ b/engine/src/flutter/shell/platform/windows/window_binding_handler_delegate.h @@ -93,6 +93,13 @@ class WindowBindingHandlerDelegate { bool was_down, KeyEventCallback callback) = 0; + /// Notifies the delegate that the backing window has received or + /// lost focus. + /// + /// Typically called by currently configured WindowBindingHandler. + virtual void OnFocus(FlutterViewFocusState focus_state, + FlutterViewFocusDirection direction) = 0; + // Notifies the delegate that IME composing mode has begun. // // Triggered when the user begins editing composing text using a multi-step