diff --git a/shell/platform/common/accessibility_bridge.cc b/shell/platform/common/accessibility_bridge.cc index 765304edf9b5f..56b0851accd78 100644 --- a/shell/platform/common/accessibility_bridge.cc +++ b/shell/platform/common/accessibility_bridge.cc @@ -36,11 +36,26 @@ AccessibilityBridge::~AccessibilityBridge() { tree_->RemoveObserver(static_cast(this)); } +void AccessibilityBridge::AddFlutterSemanticsNodeUpdate( + const FlutterSemanticsNode2& node) { + pending_semantics_node_updates_[node.id] = FromFlutterSemanticsNode(node); +} + +void AccessibilityBridge::AddFlutterSemanticsCustomActionUpdate( + const FlutterSemanticsCustomAction2& action) { + pending_semantics_custom_action_updates_[action.id] = + FromFlutterSemanticsCustomAction(action); +} + +// TODO(loicsharma): Remove this as FlutterSemanticsNode is deprecated. +// See: https://github.com/flutter/flutter/issues/121176 void AccessibilityBridge::AddFlutterSemanticsNodeUpdate( const FlutterSemanticsNode& node) { pending_semantics_node_updates_[node.id] = FromFlutterSemanticsNode(node); } +// TODO(loicsharma): Remove this as FlutterSemanticsNode is deprecated. +// See: https://github.com/flutter/flutter/issues/121176 void AccessibilityBridge::AddFlutterSemanticsCustomActionUpdate( const FlutterSemanticsCustomAction& action) { pending_semantics_custom_action_updates_[action.id] = @@ -576,6 +591,74 @@ void AccessibilityBridge::SetTreeData(const SemanticsNode& node, } } +AccessibilityBridge::SemanticsNode +AccessibilityBridge::FromFlutterSemanticsNode( + const FlutterSemanticsNode2& flutter_node) { + SemanticsNode result; + result.id = flutter_node.id; + result.flags = flutter_node.flags; + result.actions = flutter_node.actions; + result.text_selection_base = flutter_node.text_selection_base; + result.text_selection_extent = flutter_node.text_selection_extent; + result.scroll_child_count = flutter_node.scroll_child_count; + result.scroll_index = flutter_node.scroll_index; + result.scroll_position = flutter_node.scroll_position; + result.scroll_extent_max = flutter_node.scroll_extent_max; + result.scroll_extent_min = flutter_node.scroll_extent_min; + result.elevation = flutter_node.elevation; + result.thickness = flutter_node.thickness; + if (flutter_node.label) { + result.label = std::string(flutter_node.label); + } + if (flutter_node.hint) { + result.hint = std::string(flutter_node.hint); + } + if (flutter_node.value) { + result.value = std::string(flutter_node.value); + } + if (flutter_node.increased_value) { + result.increased_value = std::string(flutter_node.increased_value); + } + if (flutter_node.decreased_value) { + result.decreased_value = std::string(flutter_node.decreased_value); + } + if (flutter_node.tooltip) { + result.tooltip = std::string(flutter_node.tooltip); + } + result.text_direction = flutter_node.text_direction; + result.rect = flutter_node.rect; + result.transform = flutter_node.transform; + if (flutter_node.child_count > 0) { + result.children_in_traversal_order = std::vector( + flutter_node.children_in_traversal_order, + flutter_node.children_in_traversal_order + flutter_node.child_count); + } + if (flutter_node.custom_accessibility_actions_count > 0) { + result.custom_accessibility_actions = std::vector( + flutter_node.custom_accessibility_actions, + flutter_node.custom_accessibility_actions + + flutter_node.custom_accessibility_actions_count); + } + return result; +} + +AccessibilityBridge::SemanticsCustomAction +AccessibilityBridge::FromFlutterSemanticsCustomAction( + const FlutterSemanticsCustomAction2& flutter_custom_action) { + SemanticsCustomAction result; + result.id = flutter_custom_action.id; + result.override_action = flutter_custom_action.override_action; + if (flutter_custom_action.label) { + result.label = std::string(flutter_custom_action.label); + } + if (flutter_custom_action.hint) { + result.hint = std::string(flutter_custom_action.hint); + } + return result; +} + +// TODO(loicsharma): Remove this as FlutterSemanticsNode is deprecated. +// See: https://github.com/flutter/flutter/issues/121176 AccessibilityBridge::SemanticsNode AccessibilityBridge::FromFlutterSemanticsNode( const FlutterSemanticsNode& flutter_node) { @@ -627,6 +710,9 @@ AccessibilityBridge::FromFlutterSemanticsNode( return result; } +// TODO(loicsharma): Remove this as FlutterSemanticsCustomAction is +// deprecated. +// See: https://github.com/flutter/flutter/issues/121176 AccessibilityBridge::SemanticsCustomAction AccessibilityBridge::FromFlutterSemanticsCustomAction( const FlutterSemanticsCustomAction& flutter_custom_action) { diff --git a/shell/platform/common/accessibility_bridge.h b/shell/platform/common/accessibility_bridge.h index bc9567624f4b1..7f9e51dd523cc 100644 --- a/shell/platform/common/accessibility_bridge.h +++ b/shell/platform/common/accessibility_bridge.h @@ -54,6 +54,27 @@ class AccessibilityBridge /// To flush the pending updates, call the CommitUpdates(). /// /// @param[in] node A reference to the semantics node update. + void AddFlutterSemanticsNodeUpdate(const FlutterSemanticsNode2& node); + + //------------------------------------------------------------------------------ + /// @brief Adds a custom semantics action update to the pending semantics + /// update. Calling this method alone will NOT update the + /// semantics tree. To flush the pending updates, call the + /// CommitUpdates(). + /// + /// @param[in] action A reference to the custom semantics action + /// update. + void AddFlutterSemanticsCustomActionUpdate( + const FlutterSemanticsCustomAction2& action); + + //------------------------------------------------------------------------------ + /// @brief Adds a semantics node update to the pending semantics update. + /// Calling this method alone will NOT update the semantics tree. + /// To flush the pending updates, call the CommitUpdates(). + /// + /// @param[in] node A reference to the semantics node update. + // TODO(loicsharma): Remove this as FlutterSemanticsNode is deprecated. + // See: https://github.com/flutter/flutter/issues/121176 void AddFlutterSemanticsNodeUpdate(const FlutterSemanticsNode& node); //------------------------------------------------------------------------------ @@ -64,6 +85,9 @@ class AccessibilityBridge /// /// @param[in] action A reference to the custom semantics action /// update. + // TODO(loicsharma): Remove this as FlutterSemanticsCustomAction is + // deprecated. + // See: https://github.com/flutter/flutter/issues/121176 void AddFlutterSemanticsCustomActionUpdate( const FlutterSemanticsCustomAction& action); @@ -244,8 +268,19 @@ class AccessibilityBridge void SetTooltipFromFlutterUpdate(ui::AXNodeData& node_data, const SemanticsNode& node); void SetTreeData(const SemanticsNode& node, ui::AXTreeUpdate& tree_update); + SemanticsNode FromFlutterSemanticsNode( + const FlutterSemanticsNode2& flutter_node); + SemanticsCustomAction FromFlutterSemanticsCustomAction( + const FlutterSemanticsCustomAction2& flutter_custom_action); + + // TODO(loicsharma): Remove this as FlutterSemanticsNode is deprecated. + // See: https://github.com/flutter/flutter/issues/121176 SemanticsNode FromFlutterSemanticsNode( const FlutterSemanticsNode& flutter_node); + + // TODO(loicsharma): Remove this as FlutterSemanticsCustomAction is + // deprecated. + // See: https://github.com/flutter/flutter/issues/121176 SemanticsCustomAction FromFlutterSemanticsCustomAction( const FlutterSemanticsCustomAction& flutter_custom_action); diff --git a/shell/platform/embedder/embedder.cc b/shell/platform/embedder/embedder.cc index 39815333f7ba0..e1c74cf041d08 100644 --- a/shell/platform/embedder/embedder.cc +++ b/shell/platform/embedder/embedder.cc @@ -1268,10 +1268,10 @@ FlutterSemanticsNode CreateEmbedderSemanticsNode( transform.get(SkMatrix::kMScaleY), transform.get(SkMatrix::kMTransY), transform.get(SkMatrix::kMPersp0), transform.get(SkMatrix::kMPersp1), transform.get(SkMatrix::kMPersp2)}; + // Do not add new members to FlutterSemanticsNode. // This would break the forward compatibility of FlutterSemanticsUpdate. - // TODO(loicsharma): Introduce FlutterSemanticsNode2. - // https://github.com/flutter/flutter/issues/121176 + // All new members must be added to FlutterSemanticsNode2 instead. return { sizeof(FlutterSemanticsNode), node.id, @@ -1305,14 +1305,56 @@ FlutterSemanticsNode CreateEmbedderSemanticsNode( }; } +// Translates engine semantic nodes to embedder semantic nodes. +FlutterSemanticsNode2 CreateEmbedderSemanticsNode2( + const flutter::SemanticsNode& node) { + SkMatrix transform = node.transform.asM33(); + FlutterTransformation flutter_transform{ + transform.get(SkMatrix::kMScaleX), transform.get(SkMatrix::kMSkewX), + transform.get(SkMatrix::kMTransX), transform.get(SkMatrix::kMSkewY), + transform.get(SkMatrix::kMScaleY), transform.get(SkMatrix::kMTransY), + transform.get(SkMatrix::kMPersp0), transform.get(SkMatrix::kMPersp1), + transform.get(SkMatrix::kMPersp2)}; + return { + sizeof(FlutterSemanticsNode2), + node.id, + static_cast(node.flags), + static_cast(node.actions), + node.textSelectionBase, + node.textSelectionExtent, + node.scrollChildren, + node.scrollIndex, + node.scrollPosition, + node.scrollExtentMax, + node.scrollExtentMin, + node.elevation, + node.thickness, + node.label.c_str(), + node.hint.c_str(), + node.value.c_str(), + node.increasedValue.c_str(), + node.decreasedValue.c_str(), + static_cast(node.textDirection), + FlutterRect{node.rect.fLeft, node.rect.fTop, node.rect.fRight, + node.rect.fBottom}, + flutter_transform, + node.childrenInTraversalOrder.size(), + node.childrenInTraversalOrder.data(), + node.childrenInHitTestOrder.data(), + node.customAccessibilityActions.size(), + node.customAccessibilityActions.data(), + node.platformViewId, + node.tooltip.c_str(), + }; +} + // Translates engine semantic custom actions to embedder semantic custom // actions. FlutterSemanticsCustomAction CreateEmbedderSemanticsCustomAction( const flutter::CustomAccessibilityAction& action) { // Do not add new members to FlutterSemanticsCustomAction. // This would break the forward compatibility of FlutterSemanticsUpdate. - // TODO(loicsharma): Introduce FlutterSemanticsCustomAction2. - // https://github.com/flutter/flutter/issues/121176 + // All new members must be added to FlutterSemanticsCustomAction2 instead. return { sizeof(FlutterSemanticsCustomAction), action.id, @@ -1322,8 +1364,21 @@ FlutterSemanticsCustomAction CreateEmbedderSemanticsCustomAction( }; } +// Translates engine semantic custom actions to embedder semantic custom +// actions. +FlutterSemanticsCustomAction2 CreateEmbedderSemanticsCustomAction2( + const flutter::CustomAccessibilityAction& action) { + return { + sizeof(FlutterSemanticsCustomAction2), + action.id, + static_cast(action.overrideId), + action.label.c_str(), + action.hint.c_str(), + }; +} + // Create a callback to notify the embedder of semantic updates -// using the new embedder callback 'update_semantics_callback'. +// using the deprecated embedder callback 'update_semantics_callback'. flutter::PlatformViewEmbedder::UpdateSemanticsCallback CreateNewEmbedderSemanticsUpdateCallback( FlutterUpdateSemanticsCallback update_semantics_callback, @@ -1354,6 +1409,58 @@ CreateNewEmbedderSemanticsUpdateCallback( }; } +// Create a callback to notify the embedder of semantic updates +// using the new embedder callback 'update_semantics_callback2'. +flutter::PlatformViewEmbedder::UpdateSemanticsCallback +CreateNewEmbedderSemanticsUpdateCallback2( + FlutterUpdateSemanticsCallback2 update_semantics_callback, + void* user_data) { + return [update_semantics_callback, user_data]( + const flutter::SemanticsNodeUpdates& nodes, + const flutter::CustomAccessibilityActionUpdates& actions) { + std::vector embedder_nodes; + std::vector embedder_custom_actions; + + embedder_nodes.reserve(nodes.size()); + embedder_custom_actions.reserve(actions.size()); + + for (const auto& value : nodes) { + embedder_nodes.push_back(CreateEmbedderSemanticsNode2(value.second)); + } + + for (const auto& value : actions) { + embedder_custom_actions.push_back( + CreateEmbedderSemanticsCustomAction2(value.second)); + } + + // Provide the embedder an array of pointers to maintain full forward and + // backward compatibility even if new members are added to semantic structs. + std::vector embedder_node_pointers; + std::vector embedder_custom_action_pointers; + + embedder_node_pointers.reserve(embedder_nodes.size()); + embedder_custom_action_pointers.reserve(embedder_custom_actions.size()); + + for (auto& node : embedder_nodes) { + embedder_node_pointers.push_back(&node); + } + + for (auto& action : embedder_custom_actions) { + embedder_custom_action_pointers.push_back(&action); + } + + FlutterSemanticsUpdate2 update{ + .struct_size = sizeof(FlutterSemanticsUpdate2), + .node_count = embedder_node_pointers.size(), + .nodes = embedder_node_pointers.data(), + .custom_action_count = embedder_custom_action_pointers.size(), + .custom_actions = embedder_custom_action_pointers.data(), + }; + + update_semantics_callback(&update, user_data); + }; +} + // Create a callback to notify the embedder of semantic updates // using the legacy embedder callbacks 'update_semantics_node_callback' and // 'update_semantics_custom_action_callback'. @@ -1413,6 +1520,11 @@ CreateEmbedderSemanticsUpdateCallback(const FlutterProjectArgs* args, // The embedder can register the new callback, or the legacy callbacks, or // nothing at all. Handle the case where the embedder registered the 'new' // callback. + if (SAFE_ACCESS(args, update_semantics_callback2, nullptr) != nullptr) { + return CreateNewEmbedderSemanticsUpdateCallback2( + args->update_semantics_callback2, user_data); + } + if (SAFE_ACCESS(args, update_semantics_callback, nullptr) != nullptr) { return CreateNewEmbedderSemanticsUpdateCallback( args->update_semantics_callback, user_data); @@ -1590,15 +1702,27 @@ FlutterEngineResult FlutterEngineInitialize(size_t version, settings.log_tag = SAFE_ACCESS(args, log_tag, nullptr); } - if (args->update_semantics_callback != nullptr && - (args->update_semantics_node_callback != nullptr || - args->update_semantics_custom_action_callback != nullptr)) { + bool has_update_semantics_2_callback = + SAFE_ACCESS(args, update_semantics_callback2, nullptr) != nullptr; + bool has_update_semantics_callback = + SAFE_ACCESS(args, update_semantics_callback, nullptr) != nullptr; + bool has_legacy_update_semantics_callback = + SAFE_ACCESS(args, update_semantics_node_callback, nullptr) != nullptr || + SAFE_ACCESS(args, update_semantics_custom_action_callback, nullptr) != + nullptr; + + int semantic_callback_count = (has_update_semantics_2_callback ? 1 : 0) + + (has_update_semantics_callback ? 1 : 0) + + (has_legacy_update_semantics_callback ? 1 : 0); + + if (semantic_callback_count > 1) { return LOG_EMBEDDER_ERROR( kInvalidArguments, "Multiple semantics update callbacks provided. " - "Embedders should provide either `update_semantics_callback` " - "or both `update_semantics_nodes_callback` and " - "`update_semantics_custom_actions_callback`."); + "Embedders should provide either `update_semantics_callback2`, " + "`update_semantics_callback`, or both " + "`update_semantics_node_callback` and " + "`update_semantics_custom_action_callback`."); } flutter::PlatformViewEmbedder::UpdateSemanticsCallback diff --git a/shell/platform/embedder/embedder.h b/shell/platform/embedder/embedder.h index 989b9cce6be9f..5cdba06ef505d 100644 --- a/shell/platform/embedder/embedder.h +++ b/shell/platform/embedder/embedder.h @@ -1045,7 +1045,7 @@ typedef int64_t FlutterPlatformViewIdentifier; /// `FlutterSemanticsNode` ID used as a sentinel to signal the end of a batch of /// semantics node updates. This is unused if using -/// `FlutterUpdateSemanticsCallback`. +/// `FlutterUpdateSemanticsCallback2`. FLUTTER_EXPORT extern const int32_t kFlutterSemanticsNodeIdBatchEnd; @@ -1055,6 +1055,11 @@ extern const int32_t kFlutterSemanticsNodeIdBatchEnd; /// (i.e., during PipelineOwner.flushSemantics), which happens after /// compositing. Updates are then pushed to embedders via the registered /// `FlutterUpdateSemanticsCallback`. +/// +/// @deprecated Use `FlutterSemanticsNode2` instead. In order to preserve +/// ABI compatibility for existing users, no new fields will be +/// added to this struct. New fields will continue to be added +/// to `FlutterSemanticsNode2`. typedef struct { /// The size of this struct. Must be sizeof(FlutterSemanticsNode). size_t struct_size; @@ -1122,9 +1127,84 @@ typedef struct { const char* tooltip; } FlutterSemanticsNode; +/// A node in the Flutter semantics tree. +/// +/// The semantics tree is maintained during the semantics phase of the pipeline +/// (i.e., during PipelineOwner.flushSemantics), which happens after +/// compositing. Updates are then pushed to embedders via the registered +/// `FlutterUpdateSemanticsCallback2`. +/// +/// @see https://api.flutter.dev/flutter/semantics/SemanticsNode-class.html +typedef struct { + /// The size of this struct. Must be sizeof(FlutterSemanticsNode). + size_t struct_size; + /// The unique identifier for this node. + int32_t id; + /// The set of semantics flags associated with this node. + FlutterSemanticsFlag flags; + /// The set of semantics actions applicable to this node. + FlutterSemanticsAction actions; + /// The position at which the text selection originates. + int32_t text_selection_base; + /// The position at which the text selection terminates. + int32_t text_selection_extent; + /// The total number of scrollable children that contribute to semantics. + int32_t scroll_child_count; + /// The index of the first visible semantic child of a scroll node. + int32_t scroll_index; + /// The current scrolling position in logical pixels if the node is + /// scrollable. + double scroll_position; + /// The maximum in-range value for `scrollPosition` if the node is scrollable. + double scroll_extent_max; + /// The minimum in-range value for `scrollPosition` if the node is scrollable. + double scroll_extent_min; + /// The elevation along the z-axis at which the rect of this semantics node is + /// located above its parent. + double elevation; + /// Describes how much space the semantics node takes up along the z-axis. + double thickness; + /// A textual description of the node. + const char* label; + /// A brief description of the result of performing an action on the node. + const char* hint; + /// A textual description of the current value of the node. + const char* value; + /// A value that `value` will have after a kFlutterSemanticsActionIncrease` + /// action has been performed. + const char* increased_value; + /// A value that `value` will have after a kFlutterSemanticsActionDecrease` + /// action has been performed. + const char* decreased_value; + /// The reading direction for `label`, `value`, `hint`, `increasedValue`, + /// `decreasedValue`, and `tooltip`. + FlutterTextDirection text_direction; + /// The bounding box for this node in its coordinate system. + FlutterRect rect; + /// The transform from this node's coordinate system to its parent's + /// coordinate system. + FlutterTransformation transform; + /// The number of children this node has. + size_t child_count; + /// Array of child node IDs in traversal order. Has length `child_count`. + const int32_t* children_in_traversal_order; + /// Array of child node IDs in hit test order. Has length `child_count`. + const int32_t* children_in_hit_test_order; + /// The number of custom accessibility action associated with this node. + size_t custom_accessibility_actions_count; + /// Array of `FlutterSemanticsCustomAction` IDs associated with this node. + /// Has length `custom_accessibility_actions_count`. + const int32_t* custom_accessibility_actions; + /// Identifier of the platform view associated with this semantics node, or + /// -1 if none. + FlutterPlatformViewIdentifier platform_view_id; + /// A textual tooltip attached to the node. + const char* tooltip; +} FlutterSemanticsNode2; + /// `FlutterSemanticsCustomAction` ID used as a sentinel to signal the end of a /// batch of semantics custom action updates. This is unused if using -/// `FlutterUpdateSemanticsCallback`. +/// `FlutterUpdateSemanticsCallback2`. FLUTTER_EXPORT extern const int32_t kFlutterSemanticsCustomActionIdBatchEnd; @@ -1137,6 +1217,11 @@ extern const int32_t kFlutterSemanticsCustomActionIdBatchEnd; /// Action overrides are custom actions that the application developer requests /// to be used in place of the standard actions in the `FlutterSemanticsAction` /// enum. +/// +/// @deprecated Use `FlutterSemanticsCustomAction2` instead. In order to +/// preserve ABI compatility for existing users, no new fields +/// will be added to this struct. New fields will continue to +/// be added to `FlutterSemanticsCustomAction2`. typedef struct { /// The size of the struct. Must be sizeof(FlutterSemanticsCustomAction). size_t struct_size; @@ -1151,7 +1236,37 @@ typedef struct { const char* hint; } FlutterSemanticsCustomAction; +/// A custom semantics action, or action override. +/// +/// Custom actions can be registered by applications in order to provide +/// semantic actions other than the standard actions available through the +/// `FlutterSemanticsAction` enum. +/// +/// Action overrides are custom actions that the application developer requests +/// to be used in place of the standard actions in the `FlutterSemanticsAction` +/// enum. +/// +/// @see +/// https://api.flutter.dev/flutter/semantics/CustomSemanticsAction-class.html +typedef struct { + /// The size of the struct. Must be sizeof(FlutterSemanticsCustomAction). + size_t struct_size; + /// The unique custom action or action override ID. + int32_t id; + /// For overridden standard actions, corresponds to the + /// `FlutterSemanticsAction` to override. + FlutterSemanticsAction override_action; + /// The user-readable name of this custom semantics action. + const char* label; + /// The hint description of this custom semantics action. + const char* hint; +} FlutterSemanticsCustomAction2; + /// A batch of updates to semantics nodes and custom actions. +/// +/// @deprecated Use `FlutterSemanticsUpdate2` instead. Adding members +/// to `FlutterSemanticsNode` or `FlutterSemanticsCustomAction` +/// breaks the ABI of this struct. typedef struct { /// The size of the struct. Must be sizeof(FlutterSemanticsUpdate). size_t struct_size; @@ -1165,6 +1280,21 @@ typedef struct { FlutterSemanticsCustomAction* custom_actions; } FlutterSemanticsUpdate; +/// A batch of updates to semantics nodes and custom actions. +typedef struct { + /// The size of the struct. Must be sizeof(FlutterSemanticsUpdate2). + size_t struct_size; + /// The number of semantics node updates. + size_t node_count; + // Array of semantics node pointers. Has length `node_count`. + FlutterSemanticsNode2** nodes; + /// The number of semantics custom action updates. + size_t custom_action_count; + /// Array of semantics custom action pointers. Has length + /// `custom_action_count`. + FlutterSemanticsCustomAction2** custom_actions; +} FlutterSemanticsUpdate2; + typedef void (*FlutterUpdateSemanticsNodeCallback)( const FlutterSemanticsNode* /* semantics node */, void* /* user data */); @@ -1177,6 +1307,10 @@ typedef void (*FlutterUpdateSemanticsCallback)( const FlutterSemanticsUpdate* /* semantics update */, void* /* user data*/); +typedef void (*FlutterUpdateSemanticsCallback2)( + const FlutterSemanticsUpdate2* /* semantics update */, + void* /* user data*/); + typedef struct _FlutterTaskRunner* FlutterTaskRunner; typedef struct { @@ -1781,9 +1915,11 @@ typedef struct { /// The callback will be invoked on the thread on which the `FlutterEngineRun` /// call is made. /// - /// @deprecated Prefer using `update_semantics_callback` instead. If this - /// calback is provided, `update_semantics_callback` must not - /// be provided. + /// @deprecated Use `update_semantics_callback2` instead. Only one of + /// `update_semantics_node_callback`, + /// `update_semantics_callback`, and + /// `update_semantics_callback2` may be provided; the others + /// should be set to null. FlutterUpdateSemanticsNodeCallback update_semantics_node_callback; /// The legacy callback invoked by the engine in order to give the embedder /// the chance to respond to updates to semantics custom actions from the Dart @@ -1795,9 +1931,11 @@ typedef struct { /// The callback will be invoked on the thread on which the `FlutterEngineRun` /// call is made. /// - /// @deprecated Prefer using `update_semantics_callback` instead. If this - /// calback is provided, `update_semantics_callback` must not - /// be provided. + /// @deprecated Use `update_semantics_callback2` instead. Only one of + /// `update_semantics_node_callback`, + /// `update_semantics_callback`, and + /// `update_semantics_callback2` may be provided; the others + /// should be set to null. FlutterUpdateSemanticsCustomActionCallback update_semantics_custom_action_callback; /// Path to a directory used to store data that is cached across runs of a @@ -1942,9 +2080,24 @@ typedef struct { /// The callback will be invoked on the thread on which the `FlutterEngineRun` /// call is made. /// - /// If this callback is provided, update_semantics_node_callback and - /// update_semantics_custom_action_callback must not be provided. + /// @deprecated Use `update_semantics_callback2` instead. Only one of + /// `update_semantics_node_callback`, + /// `update_semantics_callback`, and + /// `update_semantics_callback2` may be provided; the others + /// must be set to null. FlutterUpdateSemanticsCallback update_semantics_callback; + + /// The callback invoked by the engine in order to give the embedder the + /// chance to respond to updates to semantics nodes and custom actions from + /// the Dart application. + /// + /// The callback will be invoked on the thread on which the `FlutterEngineRun` + /// call is made. + /// + /// Only one of `update_semantics_node_callback`, `update_semantics_callback`, + /// and `update_semantics_callback2` may be provided; the others must be set + /// to null. + FlutterUpdateSemanticsCallback2 update_semantics_callback2; } FlutterProjectArgs; #ifndef FLUTTER_ENGINE_NO_PROTOTYPES @@ -2286,8 +2439,8 @@ FlutterEngineResult FlutterEngineMarkExternalTextureFrameAvailable( /// @param[in] engine A running engine instance. /// @param[in] enabled When enabled, changes to the semantic contents of the /// window are sent via the -/// `FlutterUpdateSemanticsCallback` registered to -/// `update_semantics_callback` in +/// `FlutterUpdateSemanticsCallback2` registered to +/// `update_semantics_callback2` in /// `FlutterProjectArgs`. /// /// @return The result of the call. diff --git a/shell/platform/embedder/tests/embedder_a11y_unittests.cc b/shell/platform/embedder/tests/embedder_a11y_unittests.cc index 104775a4fe0a4..4c64169e7564a 100644 --- a/shell/platform/embedder/tests/embedder_a11y_unittests.cc +++ b/shell/platform/embedder/tests/embedder_a11y_unittests.cc @@ -26,19 +26,66 @@ using EmbedderA11yTest = testing::EmbedderTest; constexpr static char kTooltip[] = "tooltip"; -TEST_F(EmbedderTest, CannotProvideNewAndLegacySemanticsCallback) { - EmbedderConfigBuilder builder( - GetEmbedderContext(EmbedderTestContextType::kSoftwareContext)); - builder.SetSoftwareRendererConfig(); - builder.GetProjectArgs().update_semantics_callback = - [](const FlutterSemanticsUpdate* update, void* user_data) {}; - builder.GetProjectArgs().update_semantics_node_callback = - [](const FlutterSemanticsNode* update, void* user_data) {}; - builder.GetProjectArgs().update_semantics_custom_action_callback = - [](const FlutterSemanticsCustomAction* update, void* user_data) {}; - auto engine = builder.InitializeEngine(); - ASSERT_FALSE(engine.is_valid()); - engine.reset(); +TEST_F(EmbedderTest, CannotProvideMultipleSemanticsCallbacks) { + { + EmbedderConfigBuilder builder( + GetEmbedderContext(EmbedderTestContextType::kSoftwareContext)); + builder.SetSoftwareRendererConfig(); + builder.GetProjectArgs().update_semantics_callback = + [](const FlutterSemanticsUpdate* update, void* user_data) {}; + builder.GetProjectArgs().update_semantics_callback2 = + [](const FlutterSemanticsUpdate2* update, void* user_data) {}; + auto engine = builder.InitializeEngine(); + ASSERT_FALSE(engine.is_valid()); + engine.reset(); + } + + { + EmbedderConfigBuilder builder( + GetEmbedderContext(EmbedderTestContextType::kSoftwareContext)); + builder.SetSoftwareRendererConfig(); + builder.GetProjectArgs().update_semantics_callback2 = + [](const FlutterSemanticsUpdate2* update, void* user_data) {}; + builder.GetProjectArgs().update_semantics_node_callback = + [](const FlutterSemanticsNode* update, void* user_data) {}; + builder.GetProjectArgs().update_semantics_custom_action_callback = + [](const FlutterSemanticsCustomAction* update, void* user_data) {}; + auto engine = builder.InitializeEngine(); + ASSERT_FALSE(engine.is_valid()); + engine.reset(); + } + + { + EmbedderConfigBuilder builder( + GetEmbedderContext(EmbedderTestContextType::kSoftwareContext)); + builder.SetSoftwareRendererConfig(); + builder.GetProjectArgs().update_semantics_callback = + [](const FlutterSemanticsUpdate* update, void* user_data) {}; + builder.GetProjectArgs().update_semantics_node_callback = + [](const FlutterSemanticsNode* update, void* user_data) {}; + builder.GetProjectArgs().update_semantics_custom_action_callback = + [](const FlutterSemanticsCustomAction* update, void* user_data) {}; + auto engine = builder.InitializeEngine(); + ASSERT_FALSE(engine.is_valid()); + engine.reset(); + } + + { + EmbedderConfigBuilder builder( + GetEmbedderContext(EmbedderTestContextType::kSoftwareContext)); + builder.SetSoftwareRendererConfig(); + builder.GetProjectArgs().update_semantics_callback2 = + [](const FlutterSemanticsUpdate2* update, void* user_data) {}; + builder.GetProjectArgs().update_semantics_callback = + [](const FlutterSemanticsUpdate* update, void* user_data) {}; + builder.GetProjectArgs().update_semantics_node_callback = + [](const FlutterSemanticsNode* update, void* user_data) {}; + builder.GetProjectArgs().update_semantics_custom_action_callback = + [](const FlutterSemanticsCustomAction* update, void* user_data) {}; + auto engine = builder.InitializeEngine(); + ASSERT_FALSE(engine.is_valid()); + engine.reset(); + } } TEST_F(EmbedderA11yTest, A11yTreeIsConsistent) { @@ -46,13 +93,189 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistent) { GTEST_SKIP() << "This test crashes on Fuchsia. https://fxbug.dev/87493 "; #endif // OS_FUCHSIA -#ifdef SHELL_ENABLE_GL - auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext); -#elif SHELL_ENABLE_METAL - auto& context = GetEmbedderContext(EmbedderTestContextType::kMetalContext); -#else auto& context = GetEmbedderContext(EmbedderTestContextType::kSoftwareContext); -#endif + + fml::AutoResetWaitableEvent signal_native_latch; + + // Called by the Dart text fixture on the UI thread to signal that the C++ + // unittest should resume. + context.AddNativeCallback( + "SignalNativeTest", + CREATE_NATIVE_ENTRY(([&signal_native_latch](Dart_NativeArguments) { + signal_native_latch.Signal(); + }))); + + // Called by test fixture on UI thread to pass data back to this test. + NativeEntry notify_semantics_enabled_callback; + context.AddNativeCallback( + "NotifySemanticsEnabled", + CREATE_NATIVE_ENTRY( + ([¬ify_semantics_enabled_callback](Dart_NativeArguments args) { + ASSERT_NE(notify_semantics_enabled_callback, nullptr); + notify_semantics_enabled_callback(args); + }))); + + NativeEntry notify_accessibility_features_callback; + context.AddNativeCallback( + "NotifyAccessibilityFeatures", + CREATE_NATIVE_ENTRY(( + [¬ify_accessibility_features_callback](Dart_NativeArguments args) { + ASSERT_NE(notify_accessibility_features_callback, nullptr); + notify_accessibility_features_callback(args); + }))); + + NativeEntry notify_semantics_action_callback; + context.AddNativeCallback( + "NotifySemanticsAction", + CREATE_NATIVE_ENTRY( + ([¬ify_semantics_action_callback](Dart_NativeArguments args) { + ASSERT_NE(notify_semantics_action_callback, nullptr); + notify_semantics_action_callback(args); + }))); + + fml::AutoResetWaitableEvent semantics_update_latch; + context.SetSemanticsUpdateCallback2( + [&](const FlutterSemanticsUpdate2* update) { + ASSERT_EQ(size_t(4), update->node_count); + ASSERT_EQ(size_t(1), update->custom_action_count); + + for (size_t i = 0; i < update->node_count; i++) { + const FlutterSemanticsNode2* node = update->nodes[i]; + + ASSERT_EQ(1.0, node->transform.scaleX); + ASSERT_EQ(2.0, node->transform.skewX); + ASSERT_EQ(3.0, node->transform.transX); + ASSERT_EQ(4.0, node->transform.skewY); + ASSERT_EQ(5.0, node->transform.scaleY); + ASSERT_EQ(6.0, node->transform.transY); + ASSERT_EQ(7.0, node->transform.pers0); + ASSERT_EQ(8.0, node->transform.pers1); + ASSERT_EQ(9.0, node->transform.pers2); + ASSERT_EQ(std::strncmp(kTooltip, node->tooltip, sizeof(kTooltip) - 1), + 0); + + if (node->id == 128) { + ASSERT_EQ(0x3f3, node->platform_view_id); + } else { + ASSERT_NE(kFlutterSemanticsNodeIdBatchEnd, node->id); + ASSERT_EQ(0, node->platform_view_id); + } + } + + semantics_update_latch.Signal(); + }); + + EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); + builder.SetDartEntrypoint("a11y_main"); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + + // Wait for initial NotifySemanticsEnabled(false). + fml::AutoResetWaitableEvent notify_semantics_enabled_latch; + notify_semantics_enabled_callback = [&](Dart_NativeArguments args) { + bool enabled = true; + auto handle = Dart_GetNativeBooleanArgument(args, 0, &enabled); + ASSERT_FALSE(Dart_IsError(handle)); + ASSERT_FALSE(enabled); + notify_semantics_enabled_latch.Signal(); + }; + notify_semantics_enabled_latch.Wait(); + + // Prepare to NotifyAccessibilityFeatures call + fml::AutoResetWaitableEvent notify_features_latch; + notify_accessibility_features_callback = [&](Dart_NativeArguments args) { + bool enabled = true; + auto handle = Dart_GetNativeBooleanArgument(args, 0, &enabled); + ASSERT_FALSE(Dart_IsError(handle)); + ASSERT_FALSE(enabled); + notify_features_latch.Signal(); + }; + + // Enable semantics. Wait for NotifySemanticsEnabled(true). + fml::AutoResetWaitableEvent notify_semantics_enabled_latch_2; + notify_semantics_enabled_callback = [&](Dart_NativeArguments args) { + bool enabled = false; + auto handle = Dart_GetNativeBooleanArgument(args, 0, &enabled); + ASSERT_FALSE(Dart_IsError(handle)); + ASSERT_TRUE(enabled); + notify_semantics_enabled_latch_2.Signal(); + }; + auto result = FlutterEngineUpdateSemanticsEnabled(engine.get(), true); + ASSERT_EQ(result, FlutterEngineResult::kSuccess); + notify_semantics_enabled_latch_2.Wait(); + + // Wait for initial accessibility features (reduce_motion == false) + notify_features_latch.Wait(); + + // Set accessibility features: (reduce_motion == true) + fml::AutoResetWaitableEvent notify_features_latch_2; + notify_accessibility_features_callback = [&](Dart_NativeArguments args) { + bool enabled = false; + auto handle = Dart_GetNativeBooleanArgument(args, 0, &enabled); + ASSERT_FALSE(Dart_IsError(handle)); + ASSERT_TRUE(enabled); + notify_features_latch_2.Signal(); + }; + result = FlutterEngineUpdateAccessibilityFeatures( + engine.get(), kFlutterAccessibilityFeatureReduceMotion); + ASSERT_EQ(result, FlutterEngineResult::kSuccess); + notify_features_latch_2.Wait(); + + // Wait for UpdateSemantics callback on platform (current) thread. + signal_native_latch.Wait(); + fml::MessageLoop::GetCurrent().RunExpiredTasksNow(); + semantics_update_latch.Wait(); + + // Dispatch a tap to semantics node 42. Wait for NotifySemanticsAction. + fml::AutoResetWaitableEvent notify_semantics_action_latch; + notify_semantics_action_callback = [&](Dart_NativeArguments args) { + int64_t node_id = 0; + Dart_GetNativeIntegerArgument(args, 0, &node_id); + ASSERT_EQ(42, node_id); + + int64_t action_id; + auto handle = Dart_GetNativeIntegerArgument(args, 1, &action_id); + ASSERT_FALSE(Dart_IsError(handle)); + ASSERT_EQ(static_cast(flutter::SemanticsAction::kTap), action_id); + + Dart_Handle semantic_args = Dart_GetNativeArgument(args, 2); + int64_t data; + Dart_Handle dart_int = Dart_ListGetAt(semantic_args, 0); + Dart_IntegerToInt64(dart_int, &data); + ASSERT_EQ(2, data); + + dart_int = Dart_ListGetAt(semantic_args, 1); + Dart_IntegerToInt64(dart_int, &data); + ASSERT_EQ(1, data); + notify_semantics_action_latch.Signal(); + }; + std::vector bytes({2, 1}); + result = FlutterEngineDispatchSemanticsAction( + engine.get(), 42, kFlutterSemanticsActionTap, &bytes[0], bytes.size()); + ASSERT_EQ(result, FlutterEngineResult::kSuccess); + notify_semantics_action_latch.Wait(); + + // Disable semantics. Wait for NotifySemanticsEnabled(false). + fml::AutoResetWaitableEvent notify_semantics_enabled_latch_3; + notify_semantics_enabled_callback = [&](Dart_NativeArguments args) { + bool enabled = true; + Dart_GetNativeBooleanArgument(args, 0, &enabled); + ASSERT_FALSE(enabled); + notify_semantics_enabled_latch_3.Signal(); + }; + result = FlutterEngineUpdateSemanticsEnabled(engine.get(), false); + ASSERT_EQ(result, FlutterEngineResult::kSuccess); + notify_semantics_enabled_latch_3.Wait(); +} + +TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingUnstableCallbacks) { +#if defined(OS_FUCHSIA) + GTEST_SKIP() << "This test crashes on Fuchsia. https://fxbug.dev/87493 "; +#endif // OS_FUCHSIA + + auto& context = GetEmbedderContext(EmbedderTestContextType::kSoftwareContext); fml::AutoResetWaitableEvent signal_native_latch; @@ -228,13 +451,7 @@ TEST_F(EmbedderA11yTest, A11yTreeIsConsistent) { } TEST_F(EmbedderA11yTest, A11yTreeIsConsistentUsingLegacyCallbacks) { -#ifdef SHELL_ENABLE_GL - auto& context = GetEmbedderContext(EmbedderTestContextType::kOpenGLContext); -#elif SHELL_ENABLE_METAL - auto& context = GetEmbedderContext(EmbedderTestContextType::kMetalContext); -#else auto& context = GetEmbedderContext(EmbedderTestContextType::kSoftwareContext); -#endif fml::AutoResetWaitableEvent signal_native_latch; diff --git a/shell/platform/embedder/tests/embedder_config_builder.cc b/shell/platform/embedder/tests/embedder_config_builder.cc index 232615b7dae83..2a29c0cb54531 100644 --- a/shell/platform/embedder/tests/embedder_config_builder.cc +++ b/shell/platform/embedder/tests/embedder_config_builder.cc @@ -247,6 +247,8 @@ void EmbedderConfigBuilder::SetIsolateCreateCallbackHook() { } void EmbedderConfigBuilder::SetSemanticsCallbackHooks() { + project_args_.update_semantics_callback2 = + context_.GetUpdateSemanticsCallback2Hook(); project_args_.update_semantics_callback = context_.GetUpdateSemanticsCallbackHook(); project_args_.update_semantics_node_callback = diff --git a/shell/platform/embedder/tests/embedder_test_context.cc b/shell/platform/embedder/tests/embedder_test_context.cc index 904eed9cb77c5..99b2fe0f5f8af 100644 --- a/shell/platform/embedder/tests/embedder_test_context.cc +++ b/shell/platform/embedder/tests/embedder_test_context.cc @@ -121,6 +121,11 @@ void EmbedderTestContext::AddNativeCallback(const char* name, native_resolver_->AddNativeCallback({name}, function); } +void EmbedderTestContext::SetSemanticsUpdateCallback2( + SemanticsUpdateCallback2 update_semantics_callback) { + update_semantics_callback2_ = std::move(update_semantics_callback); +} + void EmbedderTestContext::SetSemanticsUpdateCallback( SemanticsUpdateCallback update_semantics_callback) { update_semantics_callback_ = std::move(update_semantics_callback); @@ -154,6 +159,20 @@ void EmbedderTestContext::SetLogMessageCallback( log_message_callback_ = callback; } +FlutterUpdateSemanticsCallback2 +EmbedderTestContext::GetUpdateSemanticsCallback2Hook() { + if (update_semantics_callback2_ == nullptr) { + return nullptr; + } + + return [](const FlutterSemanticsUpdate2* update, void* user_data) { + auto context = reinterpret_cast(user_data); + if (context->update_semantics_callback2_) { + context->update_semantics_callback2_(update); + } + }; +} + FlutterUpdateSemanticsCallback EmbedderTestContext::GetUpdateSemanticsCallbackHook() { if (update_semantics_callback_ == nullptr) { diff --git a/shell/platform/embedder/tests/embedder_test_context.h b/shell/platform/embedder/tests/embedder_test_context.h index 53c117447e467..d59afad2d4777 100644 --- a/shell/platform/embedder/tests/embedder_test_context.h +++ b/shell/platform/embedder/tests/embedder_test_context.h @@ -24,6 +24,8 @@ namespace flutter { namespace testing { +using SemanticsUpdateCallback2 = + std::function; using SemanticsUpdateCallback = std::function; using SemanticsNodeCallback = std::function; @@ -71,6 +73,8 @@ class EmbedderTestContext { void AddIsolateCreateCallback(const fml::closure& closure); + void SetSemanticsUpdateCallback2(SemanticsUpdateCallback2 update_semantics); + void SetSemanticsUpdateCallback(SemanticsUpdateCallback update_semantics); void AddNativeCallback(const char* name, Dart_NativeFunction function); @@ -124,6 +128,7 @@ class EmbedderTestContext { UniqueAOTData aot_data_; std::vector isolate_create_callbacks_; std::shared_ptr native_resolver_; + SemanticsUpdateCallback2 update_semantics_callback2_; SemanticsUpdateCallback update_semantics_callback_; SemanticsNodeCallback update_semantics_node_callback_; SemanticsActionCallback update_semantics_custom_action_callback_; @@ -136,6 +141,8 @@ class EmbedderTestContext { static VoidCallback GetIsolateCreateCallbackHook(); + FlutterUpdateSemanticsCallback2 GetUpdateSemanticsCallback2Hook(); + FlutterUpdateSemanticsCallback GetUpdateSemanticsCallbackHook(); FlutterUpdateSemanticsNodeCallback GetUpdateSemanticsNodeCallbackHook();