From 940fb3c8b1ad208aa4157db775b65ae4d120b791 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Tue, 25 Feb 2025 18:37:06 +0100 Subject: [PATCH] [Embedder] Wire view focus event and focus request (#163930) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This wires `PlatformDispatcher.onViewFocusChange` and `PlatformDispatches.requestViewFocusChange` through embedder API. *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## 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 readand 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]. [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 --------- Co-authored-by: Matthew Kosarek Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com> --- .../ci/licenses_golden/licenses_flutter | 4 + engine/src/flutter/lib/ui/BUILD.gn | 2 + engine/src/flutter/lib/ui/dart_ui.cc | 1 + engine/src/flutter/lib/ui/hooks.dart | 10 ++ .../flutter/lib/ui/platform_dispatcher.dart | 11 +- .../lib/ui/window/platform_configuration.cc | 30 +++++ .../lib/ui/window/platform_configuration.h | 23 ++++ .../src/flutter/lib/ui/window/view_focus.cc | 23 ++++ engine/src/flutter/lib/ui/window/view_focus.h | 69 ++++++++++ .../flutter/runtime/dart_isolate_unittests.cc | 1 + .../src/flutter/runtime/runtime_controller.cc | 13 ++ .../src/flutter/runtime/runtime_controller.h | 11 ++ engine/src/flutter/runtime/runtime_delegate.h | 4 + engine/src/flutter/shell/common/engine.cc | 8 ++ engine/src/flutter/shell/common/engine.h | 18 +++ .../shell/common/engine_animator_unittests.cc | 4 + .../flutter/shell/common/engine_unittests.cc | 8 ++ .../shell/common/fixtures/shell_test.dart | 8 ++ .../src/flutter/shell/common/platform_view.cc | 9 ++ .../src/flutter/shell/common/platform_view.h | 17 +++ engine/src/flutter/shell/common/shell.cc | 26 ++++ engine/src/flutter/shell/common/shell.h | 6 + .../flutter/shell/common/shell_unittests.cc | 55 ++++++++ .../Source/FlutterEnginePlatformViewTest.mm | 1 + .../Source/FlutterPlatformViewsTest.mm | 1 + .../Source/accessibility_bridge_test.mm | 1 + .../shell/platform/embedder/embedder.cc | 52 ++++++++ .../shell/platform/embedder/embedder.h | 92 ++++++++++++++ .../platform/embedder/fixtures/main.dart | 27 ++++ .../embedder/platform_view_embedder.cc | 7 ++ .../embedder/platform_view_embedder.h | 7 ++ .../platform_view_embedder_unittests.cc | 4 + .../embedder/tests/embedder_config_builder.cc | 11 ++ .../embedder/tests/embedder_config_builder.h | 9 ++ .../embedder/tests/embedder_test_context.cc | 15 +++ .../embedder/tests/embedder_test_context.h | 8 ++ .../embedder/tests/embedder_unittests.cc | 118 ++++++++++++++++++ .../flutter/tests/platform_view_unittest.cc | 3 + 38 files changed, 716 insertions(+), 1 deletion(-) create mode 100644 engine/src/flutter/lib/ui/window/view_focus.cc create mode 100644 engine/src/flutter/lib/ui/window/view_focus.h diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index ceddc67532..7c45a24340 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -42555,6 +42555,8 @@ ORIGIN: ../../../flutter/lib/ui/window/pointer_data_packet.cc + ../../../flutter ORIGIN: ../../../flutter/lib/ui/window/pointer_data_packet.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/ui/window/pointer_data_packet_converter.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/ui/window/pointer_data_packet_converter.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/ui/window/view_focus.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/ui/window/view_focus.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/ui/window/viewport_metrics.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/ui/window/viewport_metrics.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/flutter_js/src/browser_environment.js + ../../../flutter/LICENSE @@ -45471,6 +45473,8 @@ FILE: ../../../flutter/lib/ui/window/pointer_data_packet.cc FILE: ../../../flutter/lib/ui/window/pointer_data_packet.h FILE: ../../../flutter/lib/ui/window/pointer_data_packet_converter.cc FILE: ../../../flutter/lib/ui/window/pointer_data_packet_converter.h +FILE: ../../../flutter/lib/ui/window/view_focus.cc +FILE: ../../../flutter/lib/ui/window/view_focus.h FILE: ../../../flutter/lib/ui/window/viewport_metrics.cc FILE: ../../../flutter/lib/ui/window/viewport_metrics.h FILE: ../../../flutter/lib/web_ui/flutter_js/src/browser_environment.js diff --git a/engine/src/flutter/lib/ui/BUILD.gn b/engine/src/flutter/lib/ui/BUILD.gn index e63038b9bf..407934d1a8 100644 --- a/engine/src/flutter/lib/ui/BUILD.gn +++ b/engine/src/flutter/lib/ui/BUILD.gn @@ -156,6 +156,8 @@ source_set("ui") { "window/pointer_data_packet.h", "window/pointer_data_packet_converter.cc", "window/pointer_data_packet_converter.h", + "window/view_focus.cc", + "window/view_focus.h", "window/viewport_metrics.cc", "window/viewport_metrics.h", ] diff --git a/engine/src/flutter/lib/ui/dart_ui.cc b/engine/src/flutter/lib/ui/dart_ui.cc index 7bb1aef0ed..7d1864308c 100644 --- a/engine/src/flutter/lib/ui/dart_ui.cc +++ b/engine/src/flutter/lib/ui/dart_ui.cc @@ -107,6 +107,7 @@ typedef CanvasPath Path; V(PlatformConfigurationNativeApi::GetRootIsolateToken) \ V(PlatformConfigurationNativeApi::RegisterBackgroundIsolate) \ V(PlatformConfigurationNativeApi::SendPortPlatformMessage) \ + V(PlatformConfigurationNativeApi::RequestViewFocusChange) \ V(PlatformConfigurationNativeApi::SendChannelUpdate) \ V(PlatformConfigurationNativeApi::GetScaledFontSize) \ V(PlatformIsolateNativeApi::IsRunningOnPlatformThread) \ diff --git a/engine/src/flutter/lib/ui/hooks.dart b/engine/src/flutter/lib/ui/hooks.dart index 60db8e2183..4404d97bcf 100644 --- a/engine/src/flutter/lib/ui/hooks.dart +++ b/engine/src/flutter/lib/ui/hooks.dart @@ -58,6 +58,16 @@ void _removeView(int viewId) { PlatformDispatcher.instance._removeView(viewId); } +@pragma('vm:entry-point') +void _sendViewFocusEvent(int viewId, int viewFocusState, int viewFocusDirection) { + final ViewFocusEvent viewFocusEvent = ViewFocusEvent( + viewId: viewId, + state: ViewFocusState.values[viewFocusState], + direction: ViewFocusDirection.values[viewFocusDirection], + ); + PlatformDispatcher.instance._sendViewFocusEvent(viewFocusEvent); +} + @pragma('vm:entry-point') void _updateDisplays( List ids, diff --git a/engine/src/flutter/lib/ui/platform_dispatcher.dart b/engine/src/flutter/lib/ui/platform_dispatcher.dart index 4c0f9d1c62..d8a55ffbae 100644 --- a/engine/src/flutter/lib/ui/platform_dispatcher.dart +++ b/engine/src/flutter/lib/ui/platform_dispatcher.dart @@ -301,6 +301,10 @@ class PlatformDispatcher { _invoke(onMetricsChanged, _onMetricsChangedZone); } + void _sendViewFocusEvent(ViewFocusEvent event) { + _invoke1(onViewFocusChange, _onViewFocusChangeZone, event); + } + // Called from the engine, via hooks.dart. // // Updates the available displays. @@ -384,9 +388,14 @@ class PlatformDispatcher { required ViewFocusState state, required ViewFocusDirection direction, }) { - // TODO(tugorez): implement this method. At the moment will be a no op call. + _requestViewFocusChange(viewId, state.index, direction.index); } + @Native( + symbol: 'PlatformConfigurationNativeApi::RequestViewFocusChange', + ) + external static void _requestViewFocusChange(int viewId, int state, int direction); + /// A callback invoked when any view begins a frame. /// /// A callback that is invoked to notify the application that it is an diff --git a/engine/src/flutter/lib/ui/window/platform_configuration.cc b/engine/src/flutter/lib/ui/window/platform_configuration.cc index a63d098726..d87b16a636 100644 --- a/engine/src/flutter/lib/ui/window/platform_configuration.cc +++ b/engine/src/flutter/lib/ui/window/platform_configuration.cc @@ -46,6 +46,9 @@ void PlatformConfiguration::DidCreateIsolate() { Dart_GetField(library, tonic::ToDart("_addView"))); remove_view_.Set(tonic::DartState::Current(), Dart_GetField(library, tonic::ToDart("_removeView"))); + send_view_focus_event_.Set( + tonic::DartState::Current(), + Dart_GetField(library, tonic::ToDart("_sendViewFocusEvent"))); update_window_metrics_.Set( tonic::DartState::Current(), Dart_GetField(library, tonic::ToDart("_updateWindowMetrics"))); @@ -149,6 +152,22 @@ bool PlatformConfiguration::RemoveView(int64_t view_id) { return true; } +bool PlatformConfiguration::SendFocusEvent(const ViewFocusEvent& event) { + std::shared_ptr dart_state = + remove_view_.dart_state().lock(); + if (!dart_state) { + return false; + } + tonic::DartState::Scope scope(dart_state); + tonic::CheckAndHandleError(tonic::DartInvoke( + send_view_focus_event_.Get(), { + tonic::ToDart(event.view_id()), + tonic::ToDart(event.state()), + tonic::ToDart(event.direction()), + })); + return true; +} + bool PlatformConfiguration::UpdateViewMetrics( int64_t view_id, const ViewportMetrics& view_metrics) { @@ -552,6 +571,17 @@ Dart_Handle PlatformConfigurationNativeApi::SendPortPlatformMessage( return HandlePlatformMessage(dart_state, name, data_handle, response); } +void PlatformConfigurationNativeApi::RequestViewFocusChange(int64_t view_id, + int64_t state, + int64_t direction) { + ViewFocusChangeRequest request{view_id, // + static_cast(state), + static_cast(direction)}; + UIDartState* dart_state = UIDartState::Current(); + dart_state->platform_configuration()->client()->RequestViewFocusChange( + request); +} + void PlatformConfigurationNativeApi::RespondToPlatformMessage( int response_id, const tonic::DartByteData& data) { diff --git a/engine/src/flutter/lib/ui/window/platform_configuration.h b/engine/src/flutter/lib/ui/window/platform_configuration.h index 6d227ce347..a335da659d 100644 --- a/engine/src/flutter/lib/ui/window/platform_configuration.h +++ b/engine/src/flutter/lib/ui/window/platform_configuration.h @@ -16,6 +16,7 @@ #include "flutter/lib/ui/semantics/semantics_update.h" #include "flutter/lib/ui/window/platform_message_response.h" #include "flutter/lib/ui/window/pointer_data_packet.h" +#include "flutter/lib/ui/window/view_focus.h" #include "flutter/lib/ui/window/viewport_metrics.h" #include "flutter/shell/common/display.h" #include "fml/macros.h" @@ -257,6 +258,14 @@ class PlatformConfigurationClient { virtual double GetScaledFontSize(double unscaled_font_size, int configuration_id) const = 0; + //-------------------------------------------------------------------------- + /// @brief Notifies the client that the Flutter view focus state has + /// changed and the platform view should be updated. + /// + /// @param[in] request The request to change the focus state of the view. + virtual void RequestViewFocusChange( + const ViewFocusChangeRequest& request) = 0; + virtual std::shared_ptr GetPlatformIsolateManager() = 0; @@ -338,6 +347,15 @@ class PlatformConfiguration final { /// bool RemoveView(int64_t view_id); + //---------------------------------------------------------------------------- + /// @brief Notify the isolate that the focus state of a native view has + /// changed. + /// + /// @param[in] event The focus event describing the change. + /// + /// @return Whether the focus event was sent. + bool SendFocusEvent(const ViewFocusEvent& event); + //---------------------------------------------------------------------------- /// @brief Update the view metrics for the specified view. /// @@ -529,6 +547,7 @@ class PlatformConfiguration final { tonic::DartPersistentValue on_error_; tonic::DartPersistentValue add_view_; tonic::DartPersistentValue remove_view_; + tonic::DartPersistentValue send_view_focus_event_; tonic::DartPersistentValue update_window_metrics_; tonic::DartPersistentValue update_displays_; tonic::DartPersistentValue update_locales_; @@ -617,6 +636,10 @@ class PlatformConfigurationNativeApi { static void SendChannelUpdate(const std::string& name, bool listening); + static void RequestViewFocusChange(int64_t view_id, + int64_t state, + int64_t direction); + //-------------------------------------------------------------------------- /// @brief Requests the Dart VM to adjusts the GC heuristics based on /// the requested `performance_mode`. Returns the old performance diff --git a/engine/src/flutter/lib/ui/window/view_focus.cc b/engine/src/flutter/lib/ui/window/view_focus.cc new file mode 100644 index 0000000000..d09e154449 --- /dev/null +++ b/engine/src/flutter/lib/ui/window/view_focus.cc @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/lib/ui/window/view_focus.h" + +namespace flutter { +ViewFocusChangeRequest::ViewFocusChangeRequest(int64_t view_id, + ViewFocusState state, + ViewFocusDirection direction) + : view_id_(view_id), state_(state), direction_(direction) {} + +int64_t ViewFocusChangeRequest::view_id() const { + return view_id_; +} +ViewFocusState ViewFocusChangeRequest::state() const { + return state_; +} +ViewFocusDirection ViewFocusChangeRequest::direction() const { + return direction_; +} + +} // namespace flutter diff --git a/engine/src/flutter/lib/ui/window/view_focus.h b/engine/src/flutter/lib/ui/window/view_focus.h new file mode 100644 index 0000000000..242dd01fbd --- /dev/null +++ b/engine/src/flutter/lib/ui/window/view_focus.h @@ -0,0 +1,69 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_LIB_UI_WINDOW_VIEW_FOCUS_H_ +#define FLUTTER_LIB_UI_WINDOW_VIEW_FOCUS_H_ + +#include + +namespace flutter { + +// Focus state of a View. +// Must match ViewFocusState in ui/platform_dispatcher.dart. +enum class ViewFocusState : int64_t { + kUnfocused = 0, + kFocused, +}; + +// Represents the direction of which the focus transitioned over +// a FlutterView. +// Must match ViewFocusDirection in ui/platform_dispatcher.dart. +enum class ViewFocusDirection : int64_t { + kUndefined = 0, + kForward, + kBackward, +}; + +// Event sent by the embedder to the engine indicating that native view focus +// state has changed. +class ViewFocusEvent { + public: + ViewFocusEvent(int64_t view_id, + ViewFocusState state, + ViewFocusDirection direction) + : view_id_(view_id), state_(state), direction_(direction) {} + + int64_t view_id() const { return view_id_; } + ViewFocusState state() const { return state_; } + ViewFocusDirection direction() const { return direction_; } + + private: + int64_t view_id_; + ViewFocusState state_; + ViewFocusDirection direction_; +}; + +// Request sent by the engine to the embedder indicating that the FlutterView +// focus state has changed and the native view should be updated. +class ViewFocusChangeRequest { + public: + ViewFocusChangeRequest(int64_t view_id, + ViewFocusState state, + ViewFocusDirection direction); + + int64_t view_id() const; + ViewFocusState state() const; + ViewFocusDirection direction() const; + + private: + ViewFocusChangeRequest() = delete; + + int64_t view_id_ = 0; + ViewFocusState state_ = ViewFocusState::kUnfocused; + ViewFocusDirection direction_ = ViewFocusDirection::kUndefined; +}; + +} // namespace flutter + +#endif // FLUTTER_LIB_UI_WINDOW_VIEW_FOCUS_H_ diff --git a/engine/src/flutter/runtime/dart_isolate_unittests.cc b/engine/src/flutter/runtime/dart_isolate_unittests.cc index 76b45dfdf7..2168a72cec 100644 --- a/engine/src/flutter/runtime/dart_isolate_unittests.cc +++ b/engine/src/flutter/runtime/dart_isolate_unittests.cc @@ -735,6 +735,7 @@ class FakePlatformConfigurationClient : public PlatformConfigurationClient { int configuration_id) const override { return 0; } + void RequestViewFocusChange(const ViewFocusChangeRequest& request) override {} }; TEST_F(DartIsolateTest, PlatformIsolateCreationAndShutdown) { diff --git a/engine/src/flutter/runtime/runtime_controller.cc b/engine/src/flutter/runtime/runtime_controller.cc index 27193cdf73..f037274d0a 100644 --- a/engine/src/flutter/runtime/runtime_controller.cc +++ b/engine/src/flutter/runtime/runtime_controller.cc @@ -209,6 +209,14 @@ bool RuntimeController::RemoveView(int64_t view_id) { return platform_configuration->RemoveView(view_id); } +bool RuntimeController::SendViewFocusEvent(const ViewFocusEvent& event) { + auto* platform_configuration = GetPlatformConfigurationIfAvailable(); + if (!platform_configuration) { + return false; + } + return platform_configuration->SendFocusEvent(event); +} + bool RuntimeController::ViewExists(int64_t view_id) const { return platform_data_.viewport_metrics_for_views.count(view_id) != 0; } @@ -649,6 +657,11 @@ double RuntimeController::GetScaledFontSize(double unscaled_font_size, return client_.GetScaledFontSize(unscaled_font_size, configuration_id); } +void RuntimeController::RequestViewFocusChange( + const ViewFocusChangeRequest& request) { + client_.RequestViewFocusChange(request); +} + void RuntimeController::ShutdownPlatformIsolates() { platform_isolate_manager_->ShutdownPlatformIsolates(); } diff --git a/engine/src/flutter/runtime/runtime_controller.h b/engine/src/flutter/runtime/runtime_controller.h index 2eec09de06..0f7c355b17 100644 --- a/engine/src/flutter/runtime/runtime_controller.h +++ b/engine/src/flutter/runtime/runtime_controller.h @@ -20,6 +20,7 @@ #include "flutter/lib/ui/window/platform_configuration.h" #include "flutter/lib/ui/window/pointer_data_packet.h" #include "flutter/lib/ui/window/pointer_data_packet_converter.h" +#include "flutter/lib/ui/window/view_focus.h" #include "flutter/runtime/dart_vm.h" #include "flutter/runtime/platform_data.h" #include "flutter/runtime/platform_isolate_manager.h" @@ -224,6 +225,13 @@ class RuntimeController : public PlatformConfigurationClient, /// cancelled and the return value is always false. bool RemoveView(int64_t view_id); + //---------------------------------------------------------------------------- + /// @brief Notify the isolate that the focus state of a native view has + /// changed. + /// + /// @param[in] event The focus event describing the change. + bool SendViewFocusEvent(const ViewFocusEvent& event); + //---------------------------------------------------------------------------- /// @brief Forward the specified viewport metrics to the running isolate. /// If the isolate is not running, these metrics will be saved and @@ -785,6 +793,9 @@ class RuntimeController : public PlatformConfigurationClient, double GetScaledFontSize(double unscaled_font_size, int configuration_id) const override; + // |PlatformConfigurationClient| + void RequestViewFocusChange(const ViewFocusChangeRequest& request) override; + FML_DISALLOW_COPY_AND_ASSIGN(RuntimeController); }; diff --git a/engine/src/flutter/runtime/runtime_delegate.h b/engine/src/flutter/runtime/runtime_delegate.h index 7c031f1a53..8e5b082b96 100644 --- a/engine/src/flutter/runtime/runtime_delegate.h +++ b/engine/src/flutter/runtime/runtime_delegate.h @@ -14,6 +14,7 @@ #include "flutter/lib/ui/semantics/semantics_node.h" #include "flutter/lib/ui/text/font_collection.h" #include "flutter/lib/ui/window/platform_message.h" +#include "flutter/lib/ui/window/view_focus.h" #include "flutter/shell/common/platform_message_handler.h" #include "third_party/dart/runtime/include/dart_api.h" @@ -62,6 +63,9 @@ class RuntimeDelegate { virtual double GetScaledFontSize(double unscaled_font_size, int configuration_id) const = 0; + virtual void RequestViewFocusChange( + const ViewFocusChangeRequest& request) = 0; + protected: virtual ~RuntimeDelegate(); }; diff --git a/engine/src/flutter/shell/common/engine.cc b/engine/src/flutter/shell/common/engine.cc index aaa560b42b..c05f9d041f 100644 --- a/engine/src/flutter/shell/common/engine.cc +++ b/engine/src/flutter/shell/common/engine.cc @@ -312,6 +312,10 @@ bool Engine::RemoveView(int64_t view_id) { return runtime_controller_->RemoveView(view_id); } +bool Engine::SendViewFocusEvent(const ViewFocusEvent& event) { + return runtime_controller_->SendViewFocusEvent(event); +} + void Engine::SetViewportMetrics(int64_t view_id, const ViewportMetrics& metrics) { runtime_controller_->SetViewportMetrics(view_id, metrics); @@ -522,6 +526,10 @@ double Engine::GetScaledFontSize(double unscaled_font_size, return delegate_.GetScaledFontSize(unscaled_font_size, configuration_id); } +void Engine::RequestViewFocusChange(const ViewFocusChangeRequest& request) { + delegate_.RequestViewFocusChange(request); +} + void Engine::SetNeedsReportTimings(bool needs_reporting) { delegate_.SetNeedsReportTimings(needs_reporting); } diff --git a/engine/src/flutter/shell/common/engine.h b/engine/src/flutter/shell/common/engine.h index 5beb9c6e60..85517f0dcf 100644 --- a/engine/src/flutter/shell/common/engine.h +++ b/engine/src/flutter/shell/common/engine.h @@ -323,6 +323,14 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { /// virtual double GetScaledFontSize(double unscaled_font_size, int configuration_id) const = 0; + + //-------------------------------------------------------------------------- + /// @brief Notifies the client that the Flutter view focus state has + /// changed and the platform view should be updated. + /// + /// @param[in] request The request to change the focus state of the view. + virtual void RequestViewFocusChange( + const ViewFocusChangeRequest& request) = 0; }; //---------------------------------------------------------------------------- @@ -747,6 +755,13 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { /// bool RemoveView(int64_t view_id); + //---------------------------------------------------------------------------- + /// @brief Notify the Flutter application that the focus state of a + /// native view has changed. + /// + /// @param[in] event The focus event describing the change. + bool SendViewFocusEvent(const ViewFocusEvent& event); + //---------------------------------------------------------------------------- /// @brief Updates the viewport metrics for a view. The viewport metrics /// detail the size of the rendering viewport in texels as well as @@ -1024,6 +1039,9 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { double GetScaledFontSize(double unscaled_font_size, int configuration_id) const override; + // |RuntimeDelegate| + void RequestViewFocusChange(const ViewFocusChangeRequest& request) override; + void SetNeedsReportTimings(bool value) override; bool HandleLifecyclePlatformMessage(PlatformMessage* message); diff --git a/engine/src/flutter/shell/common/engine_animator_unittests.cc b/engine/src/flutter/shell/common/engine_animator_unittests.cc index 8d8261bc02..f0d7407ad3 100644 --- a/engine/src/flutter/shell/common/engine_animator_unittests.cc +++ b/engine/src/flutter/shell/common/engine_animator_unittests.cc @@ -82,6 +82,10 @@ class MockDelegate : public Engine::Delegate { GetScaledFontSize, (double font_size, int configuration_id), (const, override)); + MOCK_METHOD(void, + RequestViewFocusChange, + (const ViewFocusChangeRequest&), + (override)); }; class MockAnimatorDelegate : public Animator::Delegate { diff --git a/engine/src/flutter/shell/common/engine_unittests.cc b/engine/src/flutter/shell/common/engine_unittests.cc index d549a6ccd5..97a589d486 100644 --- a/engine/src/flutter/shell/common/engine_unittests.cc +++ b/engine/src/flutter/shell/common/engine_unittests.cc @@ -90,6 +90,10 @@ class MockDelegate : public Engine::Delegate { GetScaledFontSize, (double font_size, int configuration_id), (const, override)); + MOCK_METHOD(void, + RequestViewFocusChange, + (const ViewFocusChangeRequest&), + (override)); }; class MockResponse : public PlatformMessageResponse { @@ -137,6 +141,10 @@ class MockRuntimeDelegate : public RuntimeDelegate { GetScaledFontSize, (double font_size, int configuration_id), (const, override)); + MOCK_METHOD(void, + RequestViewFocusChange, + (const ViewFocusChangeRequest&), + (override)); }; class MockRuntimeController : public RuntimeController { diff --git a/engine/src/flutter/shell/common/fixtures/shell_test.dart b/engine/src/flutter/shell/common/fixtures/shell_test.dart index c4fd1f2cd3..147948d887 100644 --- a/engine/src/flutter/shell/common/fixtures/shell_test.dart +++ b/engine/src/flutter/shell/common/fixtures/shell_test.dart @@ -632,3 +632,11 @@ void testDispatchEvents() { notifyNative(); }; } + +@pragma('vm:entry-point') +void testSendViewFocusEvent() { + PlatformDispatcher.instance.onViewFocusChange = (ViewFocusEvent event) { + notifyMessage('${event.viewId} ${event.state} ${event.direction}'); + }; + notifyNative(); +} diff --git a/engine/src/flutter/shell/common/platform_view.cc b/engine/src/flutter/shell/common/platform_view.cc index c6473c0ac7..414155d37b 100644 --- a/engine/src/flutter/shell/common/platform_view.cc +++ b/engine/src/flutter/shell/common/platform_view.cc @@ -97,6 +97,10 @@ void PlatformView::RemoveView(int64_t view_id, RemoveViewCallback callback) { delegate_.OnPlatformViewRemoveView(view_id, std::move(callback)); } +void PlatformView::SendViewFocusEvent(const ViewFocusEvent& event) { + delegate_.OnPlatformViewSendViewFocusEvent(event); +} + sk_sp PlatformView::CreateResourceContext() const { FML_DLOG(WARNING) << "This platform does not set up the resource " "context on the IO thread for async texture uploads."; @@ -219,4 +223,9 @@ double PlatformView::GetScaledFontSize(double unscaled_font_size, return -1; } +void PlatformView::RequestViewFocusChange( + const ViewFocusChangeRequest& request) { + // No-op by default. +} + } // namespace flutter diff --git a/engine/src/flutter/shell/common/platform_view.h b/engine/src/flutter/shell/common/platform_view.h index dfe1b2eeb3..78236428e4 100644 --- a/engine/src/flutter/shell/common/platform_view.h +++ b/engine/src/flutter/shell/common/platform_view.h @@ -121,6 +121,12 @@ class PlatformView { virtual void OnPlatformViewRemoveView(int64_t view_id, RemoveViewCallback callback) = 0; + /// @brief Notify the delegate that platform view focus state has changed. + /// + /// @param[in] event The focus event describing the change. + virtual void OnPlatformViewSendViewFocusEvent( + const ViewFocusEvent& event) = 0; + //-------------------------------------------------------------------------- /// @brief Notifies the delegate that the specified callback needs to /// be invoked after the rasterizer is done rendering the next @@ -605,6 +611,8 @@ class PlatformView { /// void RemoveView(int64_t view_id, RemoveViewCallback callback); + void SendViewFocusEvent(const ViewFocusEvent& event); + //---------------------------------------------------------------------------- /// @brief Used by the shell to obtain a Skia GPU context that is capable /// of operating on the IO thread. The context must be in the same @@ -954,6 +962,15 @@ class PlatformView { virtual double GetScaledFontSize(double unscaled_font_size, int configuration_id) const; + //-------------------------------------------------------------------------- + /// @brief Notifies the client that the Flutter view focus state has + /// changed and the platform view should be updated. + /// + /// Called on platform thread. + /// + /// @param[in] request The request to change the focus state of the view. + virtual void RequestViewFocusChange(const ViewFocusChangeRequest& request); + protected: // This is the only method called on the raster task runner. virtual std::unique_ptr CreateRenderingSurface(); diff --git a/engine/src/flutter/shell/common/shell.cc b/engine/src/flutter/shell/common/shell.cc index 3c37ad0ac7..214483596e 100644 --- a/engine/src/flutter/shell/common/shell.cc +++ b/engine/src/flutter/shell/common/shell.cc @@ -1550,6 +1550,18 @@ double Shell::GetScaledFontSize(double unscaled_font_size, configuration_id); } +void Shell::RequestViewFocusChange(const ViewFocusChangeRequest& request) { + FML_DCHECK(is_set_up_); + + fml::TaskRunner::RunNowOrPostTask( + task_runners_.GetPlatformTaskRunner(), + [view = platform_view_->GetWeakPtr(), request] { + if (view) { + view->RequestViewFocusChange(request); + } + }); +} + void Shell::ReportTimings() { FML_DCHECK(is_set_up_); FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); @@ -2102,6 +2114,20 @@ void Shell::OnPlatformViewRemoveView(int64_t view_id, }); } +void Shell::OnPlatformViewSendViewFocusEvent(const ViewFocusEvent& event) { + TRACE_EVENT0("flutter", "Shell:: OnPlatformViewSendViewFocusEvent"); + FML_DCHECK(is_set_up_); + FML_DCHECK(task_runners_.GetPlatformTaskRunner()->RunsTasksOnCurrentThread()); + + task_runners_.GetUITaskRunner()->RunNowOrPostTask( + task_runners_.GetUITaskRunner(), + [engine = engine_->GetWeakPtr(), event = event] { + if (engine) { + engine->SendViewFocusEvent(event); + } + }); +} + Rasterizer::Screenshot Shell::Screenshot( Rasterizer::ScreenshotType screenshot_type, bool base64_encode) { diff --git a/engine/src/flutter/shell/common/shell.h b/engine/src/flutter/shell/common/shell.h index 0379d61db8..971118fbb1 100644 --- a/engine/src/flutter/shell/common/shell.h +++ b/engine/src/flutter/shell/common/shell.h @@ -588,6 +588,9 @@ class Shell final : public PlatformView::Delegate, void OnPlatformViewRemoveView(int64_t view_id, RemoveViewCallback callback) override; + // |PlatformView::Delegate| + void OnPlatformViewSendViewFocusEvent(const ViewFocusEvent& event) override; + // |PlatformView::Delegate| void OnPlatformViewSetViewportMetrics( int64_t view_id, @@ -702,6 +705,9 @@ class Shell final : public PlatformView::Delegate, double GetScaledFontSize(double unscaled_font_size, int configuration_id) const override; + // |Engine::Delegate| + void RequestViewFocusChange(const ViewFocusChangeRequest& request) override; + // |Rasterizer::Delegate| void OnFrameRasterized(const FrameTiming&) override; diff --git a/engine/src/flutter/shell/common/shell_unittests.cc b/engine/src/flutter/shell/common/shell_unittests.cc index c8f1c796cd..7802ee3723 100644 --- a/engine/src/flutter/shell/common/shell_unittests.cc +++ b/engine/src/flutter/shell/common/shell_unittests.cc @@ -116,6 +116,11 @@ class MockPlatformViewDelegate : public PlatformView::Delegate { (int64_t view_id, RemoveViewCallback callback), (override)); + MOCK_METHOD(void, + OnPlatformViewSendViewFocusEvent, + (const ViewFocusEvent& event), + (override)); + MOCK_METHOD(void, OnPlatformViewSetNextFrameCallback, (const fml::closure& closure), @@ -4952,6 +4957,56 @@ TEST_F(ShellTest, WillLogWarningWhenImpellerIsOptedOut) { DestroyShell(std::move(shell), task_runners); } +TEST_F(ShellTest, SendViewFocusEvent) { + Settings settings = CreateSettingsForFixture(); + TaskRunners task_runners = GetTaskRunnersForFixture(); + fml::AutoResetWaitableEvent latch; + std::string last_event; + + AddNativeCallback( + "NotifyNative", + CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { latch.Signal(); })); + + AddNativeCallback("NotifyMessage", + CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + const auto message_from_dart = + tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 0)); + last_event = message_from_dart; + latch.Signal(); + })); + fml::AutoResetWaitableEvent check_latch; + + std::unique_ptr shell = CreateShell(settings, task_runners); + ASSERT_TRUE(shell->IsSetup()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + + configuration.SetEntrypoint("testSendViewFocusEvent"); + RunEngine(shell.get(), std::move(configuration)); + latch.Wait(); + latch.Reset(); + + PostSync(shell->GetTaskRunners().GetPlatformTaskRunner(), [&shell]() { + shell->GetPlatformView()->SendViewFocusEvent(ViewFocusEvent( + 1, ViewFocusState::kFocused, ViewFocusDirection::kUndefined)); + }); + latch.Wait(); + ASSERT_EQ(last_event, + "1 ViewFocusState.focused ViewFocusDirection.undefined"); + + latch.Reset(); + PostSync(shell->GetTaskRunners().GetPlatformTaskRunner(), [&shell]() { + shell->GetPlatformView()->SendViewFocusEvent(ViewFocusEvent( + 2, ViewFocusState::kUnfocused, ViewFocusDirection::kBackward)); + }); + latch.Wait(); + ASSERT_EQ(last_event, + "2 ViewFocusState.unfocused ViewFocusDirection.backward"); + + DestroyShell(std::move(shell), task_runners); +} + } // namespace testing } // namespace flutter diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm index c7f9e7d3e5..5d5fc3456f 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEnginePlatformViewTest.mm @@ -28,6 +28,7 @@ class FakeDelegate : public PlatformView::Delegate { const ViewportMetrics& viewport_metrics, AddViewCallback callback) override {} void OnPlatformViewRemoveView(int64_t view_id, RemoveViewCallback callback) override {} + void OnPlatformViewSendViewFocusEvent(const ViewFocusEvent& event) override {} void OnPlatformViewSetNextFrameCallback(const fml::closure& closure) override {} void OnPlatformViewSetViewportMetrics(int64_t view_id, const ViewportMetrics& metrics) override {} const flutter::Settings& OnPlatformViewGetSettings() const override { return settings_; } diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 7247d15764..59bd45042b 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -263,6 +263,7 @@ class FlutterPlatformViewsTestMockPlatformViewDelegate : public PlatformView::De const ViewportMetrics& viewport_metrics, AddViewCallback callback) override {} void OnPlatformViewRemoveView(int64_t view_id, RemoveViewCallback callback) override {} + void OnPlatformViewSendViewFocusEvent(const ViewFocusEvent& event) override {}; void OnPlatformViewSetNextFrameCallback(const fml::closure& closure) override {} void OnPlatformViewSetViewportMetrics(int64_t view_id, const ViewportMetrics& metrics) override {} const flutter::Settings& OnPlatformViewGetSettings() const override { return settings_; } diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm index 343ed99eae..4a553eb735 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm @@ -75,6 +75,7 @@ class MockDelegate : public PlatformView::Delegate { const ViewportMetrics& viewport_metrics, AddViewCallback callback) override {} void OnPlatformViewRemoveView(int64_t view_id, RemoveViewCallback callback) override {} + void OnPlatformViewSendViewFocusEvent(const ViewFocusEvent& event) override {}; void OnPlatformViewSetNextFrameCallback(const fml::closure& closure) override {} void OnPlatformViewSetViewportMetrics(int64_t view_id, const ViewportMetrics& metrics) override {} const flutter::Settings& OnPlatformViewGetSettings() const override { return settings_; } diff --git a/engine/src/flutter/shell/platform/embedder/embedder.cc b/engine/src/flutter/shell/platform/embedder/embedder.cc index 3d633cd345..616465807e 100644 --- a/engine/src/flutter/shell/platform/embedder/embedder.cc +++ b/engine/src/flutter/shell/platform/embedder/embedder.cc @@ -2226,6 +2226,24 @@ FlutterEngineResult FlutterEngineInitialize(size_t version, }; } + flutter::PlatformViewEmbedder::ViewFocusChangeRequestCallback + view_focus_change_request_callback = nullptr; + if (SAFE_ACCESS(args, view_focus_change_request_callback, nullptr) != + nullptr) { + view_focus_change_request_callback = + [ptr = args->view_focus_change_request_callback, + user_data](const flutter::ViewFocusChangeRequest& request) { + FlutterViewFocusChangeRequest embedder_request{ + .struct_size = sizeof(FlutterViewFocusChangeRequest), + .view_id = request.view_id(), + .state = static_cast(request.state()), + .direction = + static_cast(request.direction()), + }; + ptr(&embedder_request, user_data); + }; + } + auto external_view_embedder_result = InferExternalViewEmbedderFromArgs( SAFE_ACCESS(args, compositor, nullptr), settings.enable_impeller); if (external_view_embedder_result.second) { @@ -2241,6 +2259,7 @@ FlutterEngineResult FlutterEngineInitialize(size_t version, compute_platform_resolved_locale_callback, // on_pre_engine_restart_callback, // channel_update_callback, // + view_focus_change_request_callback, // }; auto on_create_platform_view = InferPlatformViewCreationCallback( @@ -2556,6 +2575,38 @@ FlutterEngineResult FlutterEngineRemoveView(FLUTTER_API_SYMBOL(FlutterEngine) return kSuccess; } +FlutterEngineResult FlutterEngineSendViewFocusEvent( + FLUTTER_API_SYMBOL(FlutterEngine) engine, + const FlutterViewFocusEvent* event) { + if (!engine) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, "Engine handle was invalid."); + } + if (!event) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, + "View focus event must not be null."); + } + // The engine must be running to focus a view. + auto embedder_engine = reinterpret_cast(engine); + if (!embedder_engine->IsValid()) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, "Engine handle was invalid."); + } + + if (!STRUCT_HAS_MEMBER(event, direction)) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, + "The event struct has invalid size."); + } + + flutter::ViewFocusEvent flutter_event( + event->view_id, // + static_cast(event->state), + static_cast(event->direction)); + + embedder_engine->GetShell().GetPlatformView()->SendViewFocusEvent( + flutter_event); + + return kSuccess; +} + FLUTTER_EXPORT FlutterEngineResult FlutterEngineDeinitialize(FLUTTER_API_SYMBOL(FlutterEngine) engine) { @@ -3657,6 +3708,7 @@ FlutterEngineResult FlutterEngineGetProcAddresses( SET_PROC(SetNextFrameCallback, FlutterEngineSetNextFrameCallback); SET_PROC(AddView, FlutterEngineAddView); SET_PROC(RemoveView, FlutterEngineRemoveView); + SET_PROC(SendViewFocusEvent, FlutterEngineSendViewFocusEvent); #undef SET_PROC return kSuccess; diff --git a/engine/src/flutter/shell/platform/embedder/embedder.h b/engine/src/flutter/shell/platform/embedder/embedder.h index a0700aa13a..12c28cb720 100644 --- a/engine/src/flutter/shell/platform/embedder/embedder.h +++ b/engine/src/flutter/shell/platform/embedder/embedder.h @@ -1060,6 +1060,74 @@ typedef struct { FlutterRemoveViewCallback remove_view_callback; } FlutterRemoveViewInfo; +/// Represents the direction in which the focus transitioned across +/// [FlutterView]s. +typedef enum { + /// Indicates the focus transition did not have a direction. + /// + /// This is typically associated with focus being programmatically requested + /// or when focus is lost. + kUndefined, + + /// Indicates the focus transition was performed in a forward direction. + /// + /// This is typically result of the user pressing tab. + kForward, + + /// Indicates the focus transition was performed in a backward direction. + /// + /// This is typically result of the user pressing shift + tab. + kBackward, +} FlutterViewFocusDirection; + +/// Represents the focus state of a given [FlutterView]. +typedef enum { + /// Specifies that a view does not have platform focus. + kUnfocused, + + /// Specifies that a view has platform focus. + kFocused, +} FlutterViewFocusState; + +/// A view focus event is sent to the engine by the embedder when a native view +/// focus state has changed. +/// +/// Passed through FlutterEngineSendViewFocusEvent. +typedef struct { + /// The size of this struct. + /// Must be sizeof(FlutterViewFocusEvent). + size_t struct_size; + + /// The identifier of the view that received the focus event. + FlutterViewId view_id; + + /// The focus state of the view. + FlutterViewFocusState state; + + /// The direction in which the focus transitioned across [FlutterView]s. + FlutterViewFocusDirection direction; +} FlutterViewFocusEvent; + +/// A FlutterViewFocusChangeRequest is sent by the engine to the embedder when +/// when a FlutterView focus state has changed and native view focus +/// needs to be updated. +/// +/// Received in FlutterProjectArgs.view_focus_change_request_callback. +typedef struct { + /// The size of this struct. + /// Must be sizeof(FlutterViewFocusChangeRequest). + size_t struct_size; + + /// The identifier of the view that received the focus event. + FlutterViewId view_id; + + /// The focus state of the view. + FlutterViewFocusState state; + + /// The direction in which the focus transitioned across [FlutterView]s. + FlutterViewFocusDirection direction; +} FlutterViewFocusChangeRequest; + /// The phase of the pointer event. typedef enum { kCancel, @@ -1644,6 +1712,10 @@ typedef void (*FlutterChannelUpdateCallback)( const FlutterChannelUpdate* /* channel update */, void* /* user data */); +typedef void (*FlutterViewFocusChangeRequestCallback)( + const FlutterViewFocusChangeRequest* /* request */, + void* /* user data */); + typedef struct _FlutterTaskRunner* FlutterTaskRunner; typedef struct { @@ -2553,6 +2625,12 @@ typedef struct { /// being registered on the framework side. The callback is invoked from /// a task posted to the platform thread. FlutterChannelUpdateCallback channel_update_callback; + + /// The callback invoked by the engine when FlutterView focus state has + /// changed. The embedder can use this callback to request focus change for + /// the native view. The callback is invoked from a task posted to the + /// platform thread. + FlutterViewFocusChangeRequestCallback view_focus_change_request_callback; } FlutterProjectArgs; #ifndef FLUTTER_ENGINE_NO_PROTOTYPES @@ -2757,6 +2835,16 @@ FlutterEngineResult FlutterEngineRemoveView(FLUTTER_API_SYMBOL(FlutterEngine) engine, const FlutterRemoveViewInfo* info); +//------------------------------------------------------------------------------ +/// @brief Notifies the engine that platform view focus state has changed. +/// +/// @param[in] engine A running engine instance +/// @param[in] event The focus event data describing the change. +FLUTTER_EXPORT +FlutterEngineResult FlutterEngineSendViewFocusEvent( + FLUTTER_API_SYMBOL(FlutterEngine) engine, + const FlutterViewFocusEvent* event); + FLUTTER_EXPORT FlutterEngineResult FlutterEngineSendWindowMetricsEvent( FLUTTER_API_SYMBOL(FlutterEngine) engine, @@ -3433,6 +3521,9 @@ typedef FlutterEngineResult (*FlutterEngineAddViewFnPtr)( typedef FlutterEngineResult (*FlutterEngineRemoveViewFnPtr)( FLUTTER_API_SYMBOL(FlutterEngine) engine, const FlutterRemoveViewInfo* info); +typedef FlutterEngineResult (*FlutterEngineSendViewFocusEventFnPtr)( + FLUTTER_API_SYMBOL(FlutterEngine) engine, + const FlutterViewFocusEvent* event); /// Function-pointer-based versions of the APIs above. typedef struct { @@ -3481,6 +3572,7 @@ typedef struct { FlutterEngineSetNextFrameCallbackFnPtr SetNextFrameCallback; FlutterEngineAddViewFnPtr AddView; FlutterEngineRemoveViewFnPtr RemoveView; + FlutterEngineSendViewFocusEventFnPtr SendViewFocusEvent; } FlutterEngineProcTable; //------------------------------------------------------------------------------ diff --git a/engine/src/flutter/shell/platform/embedder/fixtures/main.dart b/engine/src/flutter/shell/platform/embedder/fixtures/main.dart index 36be1c7fb6..5ced1e22fe 100644 --- a/engine/src/flutter/shell/platform/embedder/fixtures/main.dart +++ b/engine/src/flutter/shell/platform/embedder/fixtures/main.dart @@ -1600,3 +1600,30 @@ Future render_impeller_image_snapshot_test() async { final bool result = (pixel & 0xFF) == color.alpha && ((pixel >> 8) & 0xFF) == color.blue; notifyBoolValue(result); } + +@pragma('vm:entry-point') +void testSendViewFocusEvent() { + PlatformDispatcher.instance.onViewFocusChange = (ViewFocusEvent event) { + notifyStringValue('${event.viewId} ${event.state} ${event.direction}'); + }; + signalNativeTest(); +} + +@pragma('vm:entry-point') +void testSendViewFocusChangeRequest() { + PlatformDispatcher.instance.requestViewFocusChange( + viewId: 1, + state: ViewFocusState.unfocused, + direction: ViewFocusDirection.undefined, + ); + PlatformDispatcher.instance.requestViewFocusChange( + viewId: 2, + state: ViewFocusState.focused, + direction: ViewFocusDirection.forward, + ); + PlatformDispatcher.instance.requestViewFocusChange( + viewId: 3, + state: ViewFocusState.focused, + direction: ViewFocusDirection.backward, + ); +} diff --git a/engine/src/flutter/shell/platform/embedder/platform_view_embedder.cc b/engine/src/flutter/shell/platform/embedder/platform_view_embedder.cc index 01bfbe97cb..950d3ee58b 100644 --- a/engine/src/flutter/shell/platform/embedder/platform_view_embedder.cc +++ b/engine/src/flutter/shell/platform/embedder/platform_view_embedder.cc @@ -207,6 +207,13 @@ void PlatformViewEmbedder::SendChannelUpdate(const std::string& name, } } +void PlatformViewEmbedder::RequestViewFocusChange( + const ViewFocusChangeRequest& request) { + if (platform_dispatch_table_.view_focus_change_request_callback != nullptr) { + platform_dispatch_table_.view_focus_change_request_callback(request); + } +} + std::shared_ptr PlatformViewEmbedder::GetPlatformMessageHandler() const { return platform_message_handler_; diff --git a/engine/src/flutter/shell/platform/embedder/platform_view_embedder.h b/engine/src/flutter/shell/platform/embedder/platform_view_embedder.h index 090e6920df..2c4ab691f3 100644 --- a/engine/src/flutter/shell/platform/embedder/platform_view_embedder.h +++ b/engine/src/flutter/shell/platform/embedder/platform_view_embedder.h @@ -45,6 +45,8 @@ class PlatformViewEmbedder final : public PlatformView { const std::vector& supported_locale_data)>; using OnPreEngineRestartCallback = std::function; using ChanneUpdateCallback = std::function; + using ViewFocusChangeRequestCallback = + std::function; struct PlatformDispatchTable { UpdateSemanticsCallback update_semantics_callback; // optional @@ -55,6 +57,8 @@ class PlatformViewEmbedder final : public PlatformView { compute_platform_resolved_locale_callback; OnPreEngineRestartCallback on_pre_engine_restart_callback; // optional ChanneUpdateCallback on_channel_update; // optional + ViewFocusChangeRequestCallback + view_focus_change_request_callback; // optional }; // Create a platform view that sets up a software rasterizer. @@ -142,6 +146,9 @@ class PlatformViewEmbedder final : public PlatformView { // |PlatformView| void SendChannelUpdate(const std::string& name, bool listening) override; + // |PlatformView| + void RequestViewFocusChange(const ViewFocusChangeRequest& request) override; + FML_DISALLOW_COPY_AND_ASSIGN(PlatformViewEmbedder); }; diff --git a/engine/src/flutter/shell/platform/embedder/platform_view_embedder_unittests.cc b/engine/src/flutter/shell/platform/embedder/platform_view_embedder_unittests.cc index b76c745b77..9963a2397b 100644 --- a/engine/src/flutter/shell/platform/embedder/platform_view_embedder_unittests.cc +++ b/engine/src/flutter/shell/platform/embedder/platform_view_embedder_unittests.cc @@ -32,6 +32,10 @@ class MockDelegate : public PlatformView::Delegate { OnPlatformViewRemoveView, (int64_t view_id, RemoveViewCallback callback), (override)); + MOCK_METHOD(void, + OnPlatformViewSendViewFocusEvent, + (const ViewFocusEvent& event), + (override)); MOCK_METHOD(void, OnPlatformViewSetNextFrameCallback, (const fml::closure& closure), diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_config_builder.cc b/engine/src/flutter/shell/platform/embedder/tests/embedder_config_builder.cc index 545a6848ef..26dd3ea4d9 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_config_builder.cc +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_config_builder.cc @@ -37,6 +37,7 @@ EmbedderConfigBuilder::EmbedderConfigBuilder( SetLogMessageCallbackHook(); SetLocalizationCallbackHooks(); SetChannelUpdateCallbackHook(); + SetViewFocusChangeRequestHook(); AddCommandLineArgument("--disable-vm-service"); if (preference == InitializationPreference::kSnapshotsInitialize || @@ -112,6 +113,11 @@ void EmbedderConfigBuilder::SetChannelUpdateCallbackHook() { context_.GetChannelUpdateCallbackHook(); } +void EmbedderConfigBuilder::SetViewFocusChangeRequestHook() { + project_args_.view_focus_change_request_callback = + context_.GetViewFocusChangeRequestCallbackHook(); +} + void EmbedderConfigBuilder::SetLogTag(std::string tag) { log_tag_ = std::move(tag); project_args_.log_tag = log_tag_.c_str(); @@ -194,6 +200,11 @@ void EmbedderConfigBuilder::SetPlatformMessageCallback( context_.SetPlatformMessageCallback(callback); } +void EmbedderConfigBuilder::SetViewFocusChangeRequestCallback( + const std::function& callback) { + context_.SetViewFocusChangeRequestCallback(callback); +} + void EmbedderConfigBuilder::SetCompositor(bool avoid_backing_store_cache, bool use_present_layers_callback) { context_.SetupCompositor(); diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_config_builder.h b/engine/src/flutter/shell/platform/embedder/tests/embedder_config_builder.h index f572b52ed9..399f84d3c8 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_config_builder.h +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_config_builder.h @@ -59,6 +59,8 @@ class EmbedderConfigBuilder { void SetChannelUpdateCallbackHook(); + void SetViewFocusChangeRequestHook(); + // Used to set a custom log tag. void SetLogTag(std::string tag); @@ -81,6 +83,10 @@ class EmbedderConfigBuilder { void SetPlatformMessageCallback( const std::function& callback); + void SetViewFocusChangeRequestCallback( + const std::function& + callback); + void SetCompositor(bool avoid_backing_store_cache = false, bool use_present_layers_callback = false); @@ -101,6 +107,9 @@ class EmbedderConfigBuilder { // text context vis `SetVsyncCallback`. void SetupVsyncCallback(); + void SetViewFocusChangeRequestCallback( + const FlutterViewFocusChangeRequestCallback& callback); + private: EmbedderTestContext& context_; FlutterProjectArgs project_args_ = {}; diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.cc b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.cc index 0b6959cb02..78ffd1ed0c 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.cc +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.cc @@ -156,6 +156,11 @@ void EmbedderTestContext::SetChannelUpdateCallback( channel_update_callback_ = callback; } +void EmbedderTestContext::SetViewFocusChangeRequestCallback( + const ViewFocusChangeRequestCallback& callback) { + view_focus_change_request_callback_ = callback; +} + void EmbedderTestContext::PlatformMessageCallback( const FlutterPlatformMessage* message) { if (platform_message_callback_) { @@ -255,6 +260,16 @@ EmbedderTestContext::GetChannelUpdateCallbackHook() { }; } +FlutterViewFocusChangeRequestCallback +EmbedderTestContext::GetViewFocusChangeRequestCallbackHook() { + return [](const FlutterViewFocusChangeRequest* request, void* user_data) { + auto context = reinterpret_cast(user_data); + if (context->view_focus_change_request_callback_) { + context->view_focus_change_request_callback_(request); + } + }; +} + FlutterTransformation EmbedderTestContext::GetRootSurfaceTransformation() { return FlutterTransformationMake(root_surface_transformation_); } diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.h b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.h index 903c47d02a..15421ef8dd 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.h +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_test_context.h @@ -34,6 +34,8 @@ using SemanticsActionCallback = using LogMessageCallback = std::function; using ChannelUpdateCallback = std::function; +using ViewFocusChangeRequestCallback = + std::function; struct AOTDataDeleter { void operator()(FlutterEngineAOTData aot_data) { @@ -94,6 +96,9 @@ class EmbedderTestContext { void SetChannelUpdateCallback(const ChannelUpdateCallback& callback); + void SetViewFocusChangeRequestCallback( + const ViewFocusChangeRequestCallback& callback); + std::future> GetNextSceneImage(); EmbedderTestCompositor& GetCompositor(); @@ -133,6 +138,7 @@ class EmbedderTestContext { SemanticsNodeCallback update_semantics_node_callback_; SemanticsActionCallback update_semantics_custom_action_callback_; ChannelUpdateCallback channel_update_callback_; + ViewFocusChangeRequestCallback view_focus_change_request_callback_; std::function platform_message_callback_; LogMessageCallback log_message_callback_; std::unique_ptr compositor_; @@ -158,6 +164,8 @@ class EmbedderTestContext { FlutterChannelUpdateCallback GetChannelUpdateCallbackHook(); + FlutterViewFocusChangeRequestCallback GetViewFocusChangeRequestCallbackHook(); + void SetupAOTMappingsIfNecessary(); void SetupAOTDataIfNecessary(); diff --git a/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests.cc b/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests.cc index 6f2cb0f703..5530b65a0e 100644 --- a/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests.cc +++ b/engine/src/flutter/shell/platform/embedder/tests/embedder_unittests.cc @@ -2045,6 +2045,124 @@ TEST_F(EmbedderTest, CanRenderMultipleViews) { latch123.Wait(); } +bool operator==(const FlutterViewFocusChangeRequest& lhs, + const FlutterViewFocusChangeRequest& rhs) { + return lhs.view_id == rhs.view_id && lhs.state == rhs.state && + lhs.direction == rhs.direction; +} + +TEST_F(EmbedderTest, SendsViewFocusChangeRequest) { + auto& context = GetEmbedderContext(); + auto platform_task_runner = CreateNewThread("test_platform_thread"); + UniqueEngine engine; + static std::mutex engine_mutex; + EmbedderTestTaskRunner test_platform_task_runner( + platform_task_runner, [&](FlutterTask task) { + std::scoped_lock lock(engine_mutex); + if (!engine.is_valid()) { + return; + } + FlutterEngineRunTask(engine.get(), &task); + }); + fml::CountDownLatch latch(3); + std::vector received_requests; + platform_task_runner->PostTask([&]() { + EmbedderConfigBuilder builder(context); + builder.SetSurface(SkISize::Make(1, 1)); + builder.SetDartEntrypoint("testSendViewFocusChangeRequest"); + const auto platform_task_runner_description = + test_platform_task_runner.GetFlutterTaskRunnerDescription(); + builder.SetPlatformTaskRunner(&platform_task_runner_description); + builder.SetViewFocusChangeRequestCallback( + [&](const FlutterViewFocusChangeRequest* request) { + EXPECT_TRUE(platform_task_runner->RunsTasksOnCurrentThread()); + received_requests.push_back(*request); + latch.CountDown(); + }); + engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + }); + latch.Wait(); + + std::vector expected_requests{ + {.view_id = 1, .state = kUnfocused, .direction = kUndefined}, + {.view_id = 2, .state = kFocused, .direction = kForward}, + {.view_id = 3, .state = kFocused, .direction = kBackward}, + }; + + ASSERT_EQ(received_requests.size(), expected_requests.size()); + for (size_t i = 0; i < received_requests.size(); ++i) { + ASSERT_TRUE(received_requests[i] == expected_requests[i]); + } + + fml::AutoResetWaitableEvent kill_latch; + platform_task_runner->PostTask(fml::MakeCopyable([&]() mutable { + std::scoped_lock lock(engine_mutex); + engine.reset(); + + // There may still be pending tasks on the platform thread that were queued + // by the test_task_runner. Signal the latch after these tasks have been + // consumed. + platform_task_runner->PostTask([&kill_latch] { kill_latch.Signal(); }); + })); + kill_latch.Wait(); +} + +TEST_F(EmbedderTest, CanSendViewFocusEvent) { + auto& context = GetEmbedderContext(); + EmbedderConfigBuilder builder(context); + builder.SetSurface(SkISize::Make(1, 1)); + builder.SetDartEntrypoint("testSendViewFocusEvent"); + + fml::AutoResetWaitableEvent latch; + std::string last_event; + + context.AddNativeCallback( + "SignalNativeTest", + CREATE_NATIVE_ENTRY( + [&latch](Dart_NativeArguments args) { latch.Signal(); })); + context.AddNativeCallback("NotifyStringValue", + CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + const auto message_from_dart = + tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 0)); + last_event = message_from_dart; + latch.Signal(); + })); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + // Wait until the focus change handler is attached. + latch.Wait(); + latch.Reset(); + + FlutterViewFocusEvent event1{ + .struct_size = sizeof(FlutterViewFocusEvent), + .view_id = 1, + .state = kFocused, + .direction = kUndefined, + }; + FlutterEngineResult result = + FlutterEngineSendViewFocusEvent(engine.get(), &event1); + ASSERT_EQ(result, kSuccess); + latch.Wait(); + ASSERT_EQ(last_event, + "1 ViewFocusState.focused ViewFocusDirection.undefined"); + + FlutterViewFocusEvent event2{ + .struct_size = sizeof(FlutterViewFocusEvent), + .view_id = 2, + .state = kUnfocused, + .direction = kBackward, + }; + latch.Reset(); + result = FlutterEngineSendViewFocusEvent(engine.get(), &event2); + ASSERT_EQ(result, kSuccess); + latch.Wait(); + ASSERT_EQ(last_event, + "2 ViewFocusState.unfocused ViewFocusDirection.backward"); +} + //------------------------------------------------------------------------------ /// Test that the backing store is created with the correct view ID, is used /// for the correct view, and is cached according to their views. diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/tests/platform_view_unittest.cc b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/platform_view_unittest.cc index 911c618d66..aac3bf5591 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/tests/platform_view_unittest.cc +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/tests/platform_view_unittest.cc @@ -157,6 +157,9 @@ class MockPlatformViewDelegate : public flutter::PlatformView::Delegate { void UpdateAssetResolverByType( std::unique_ptr updated_asset_resolver, flutter::AssetResolver::AssetResolverType type) {} + // |flutter::PlatformView::Delegate| + void OnPlatformViewSendViewFocusEvent(const flutter::ViewFocusEvent& event) { + }; flutter::Surface* surface() const { return surface_.get(); } flutter::PlatformMessage* message() const { return message_.get(); }