| // 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. |
| |
| // @dart = 2.6 |
| part of engine; |
| |
| /// Set this flag to `true` to cause the engine to visualize the semantics tree |
| /// on the screen. |
| /// |
| /// This is useful for debugging. |
| const bool _debugShowSemanticsNodes = false; |
| |
| /// Contains updates for the semantics tree. |
| /// |
| /// This class provides private engine-side API that's not available in the |
| /// `dart:ui` [ui.SemanticsUpdate]. |
| class SemanticsUpdate implements ui.SemanticsUpdate { |
| SemanticsUpdate({List<SemanticsNodeUpdate> nodeUpdates}) |
| : _nodeUpdates = nodeUpdates; |
| |
| /// Updates for individual nodes. |
| final List<SemanticsNodeUpdate> _nodeUpdates; |
| |
| @override |
| void dispose() { |
| // Intentionally left blank. This method exists for API compatibility with |
| // Flutter, but it is not required as memory resource management is handled |
| // by JavaScript's garbage collector. |
| } |
| } |
| |
| /// Updates the properties of a particular semantics node. |
| class SemanticsNodeUpdate { |
| SemanticsNodeUpdate({ |
| this.id, |
| this.flags, |
| this.actions, |
| this.maxValueLength, |
| this.currentValueLength, |
| this.textSelectionBase, |
| this.textSelectionExtent, |
| this.platformViewId, |
| this.scrollChildren, |
| this.scrollIndex, |
| this.scrollPosition, |
| this.scrollExtentMax, |
| this.scrollExtentMin, |
| this.rect, |
| this.label, |
| this.hint, |
| this.value, |
| this.increasedValue, |
| this.decreasedValue, |
| this.textDirection, |
| this.transform, |
| this.elevation, |
| this.thickness, |
| this.childrenInTraversalOrder, |
| this.childrenInHitTestOrder, |
| this.additionalActions, |
| }); |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final int id; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final int flags; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final int actions; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final int maxValueLength; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final int currentValueLength; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final int textSelectionBase; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final int textSelectionExtent; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final int platformViewId; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final int scrollChildren; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final int scrollIndex; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final double scrollPosition; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final double scrollExtentMax; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final double scrollExtentMin; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final ui.Rect rect; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final String label; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final String hint; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final String value; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final String increasedValue; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final String decreasedValue; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final ui.TextDirection textDirection; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final Float32List transform; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final Int32List childrenInTraversalOrder; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final Int32List childrenInHitTestOrder; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final Int32List additionalActions; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final double elevation; |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| final double thickness; |
| } |
| |
| /// Identifies one of the roles a [SemanticsObject] plays. |
| enum Role { |
| /// Supports incrementing and/or decrementing its value. |
| incrementable, |
| |
| /// Able to scroll its contents vertically or horizontally. |
| scrollable, |
| |
| /// Contains a label or a value. |
| /// |
| /// The two are combined into the same role because they interact with each |
| /// other. |
| labelAndValue, |
| |
| /// Accepts tap or click gestures. |
| tappable, |
| |
| /// Contains editable text. |
| textField, |
| |
| /// A control that has a checked state, such as a check box or a radio button. |
| checkable, |
| |
| /// Visual only element. |
| image, |
| |
| /// Contains a region whose changes will be announced to the screen reader |
| /// without having to be in focus. |
| /// |
| /// These regions can be a snackbar or a text field error. Once identified |
| /// with this role, they will be able to get the assistive technology's |
| /// attention right away. |
| liveRegion, |
| } |
| |
| /// A function that creates a [RoleManager] for a [SemanticsObject]. |
| typedef RoleManagerFactory = RoleManager Function(SemanticsObject object); |
| |
| final Map<Role, RoleManagerFactory> _roleFactories = <Role, RoleManagerFactory>{ |
| Role.incrementable: (SemanticsObject object) => Incrementable(object), |
| Role.scrollable: (SemanticsObject object) => Scrollable(object), |
| Role.labelAndValue: (SemanticsObject object) => LabelAndValue(object), |
| Role.tappable: (SemanticsObject object) => Tappable(object), |
| Role.textField: (SemanticsObject object) => TextField(object), |
| Role.checkable: (SemanticsObject object) => Checkable(object), |
| Role.image: (SemanticsObject object) => ImageRoleManager(object), |
| Role.liveRegion: (SemanticsObject object) => LiveRegion(object), |
| }; |
| |
| /// Provides the functionality associated with the role of the given |
| /// [semanticsObject]. |
| /// |
| /// The role is determined by [ui.SemanticsFlag]s and [ui.SemanticsAction]s set |
| /// on the object. |
| abstract class RoleManager { |
| /// Initializes a role for [semanticsObject]. |
| /// |
| /// A single role object manages exactly one [SemanticsObject]. |
| RoleManager(this.role, this.semanticsObject) |
| : assert(semanticsObject != null); |
| |
| /// Role identifier. |
| final Role role; |
| |
| /// The semantics object managed by this role. |
| final SemanticsObject semanticsObject; |
| |
| /// Called immediately after the [semanticsObject] updates some of its fields. |
| /// |
| /// A concrete implementation of this method would typically use some of the |
| /// "is*Dirty" getters to find out exactly what's changed and apply the |
| /// minimum DOM updates. |
| void update(); |
| |
| /// Called when [semanticsObject] is removed, or when it changes its role such |
| /// that this role is no longer relevant. |
| /// |
| /// This method is expected to remove role-specific functionality from the |
| /// DOM. In particular, this method is the appropriate place to call |
| /// [EngineSemanticsOwner.removeGestureModeListener] if this role reponds to |
| /// gesture mode changes. |
| void dispose(); |
| } |
| |
| /// Instantiation of a framework-side semantics node in the DOM. |
| /// |
| /// Instances of this class are retained from frame to frame. Each instance is |
| /// permanently attached to an [id] and a DOM [element] used to convey semantics |
| /// information to the browser. |
| class SemanticsObject { |
| /// Creates a semantics tree node with the given [id] and [owner]. |
| SemanticsObject(this.id, this.owner) { |
| // DOM nodes created for semantics objects are positioned absolutely using |
| // transforms. We use a transparent color instead of "visibility:hidden" or |
| // "display:none" so that a screen reader does not ignore these elements. |
| element.style.position = 'absolute'; |
| |
| // The root node has some properties that other nodes do not. |
| if (id == 0) { |
| // Make all semantics transparent. We use `filter` instead of `opacity` |
| // attribute because `filter` is stronger. `opacity` does not apply to |
| // some elements, particularly on iOS, such as the slider thumb and track. |
| element.style.filter = 'opacity(0%)'; |
| |
| // Make text explicitly transparent to signal to the browser that no |
| // rasterization needs to be done. |
| element.style.color = 'rgba(0,0,0,0)'; |
| } |
| |
| if (_debugShowSemanticsNodes) { |
| element.style |
| ..filter = 'opacity(90%)' |
| ..outline = '1px solid green' |
| ..color = 'purple'; |
| } |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| int get flags => _flags; |
| int _flags; |
| |
| /// Whether the [flags] field has been updated but has not been applied to the |
| /// DOM yet. |
| bool get isFlagsDirty => _isDirty(_flagsIndex); |
| static const int _flagsIndex = 1 << 0; |
| void _markFlagsDirty() { |
| _dirtyFields |= _flagsIndex; |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| int get actions => _actions; |
| int _actions; |
| |
| static const int _actionsIndex = 1 << 1; |
| |
| /// Whether the [actions] field has been updated but has not been applied to |
| /// the DOM yet. |
| bool get isActionsDirty => _isDirty(_actionsIndex); |
| void _markActionsDirty() { |
| _dirtyFields |= _actionsIndex; |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| int get textSelectionBase => _textSelectionBase; |
| int _textSelectionBase; |
| |
| static const int _textSelectionBaseIndex = 1 << 2; |
| |
| /// Whether the [textSelectionBase] field has been updated but has not been |
| /// applied to the DOM yet. |
| bool get isTextSelectionBaseDirty => _isDirty(_textSelectionBaseIndex); |
| void _markTextSelectionBaseDirty() { |
| _dirtyFields |= _textSelectionBaseIndex; |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| int get textSelectionExtent => _textSelectionExtent; |
| int _textSelectionExtent; |
| |
| static const int _textSelectionExtentIndex = 1 << 3; |
| |
| /// Whether the [textSelectionExtent] field has been updated but has not been |
| /// applied to the DOM yet. |
| bool get isTextSelectionExtentDirty => _isDirty(_textSelectionExtentIndex); |
| void _markTextSelectionExtentDirty() { |
| _dirtyFields |= _textSelectionExtentIndex; |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| int get scrollChildren => _scrollChildren; |
| int _scrollChildren; |
| |
| static const int _scrollChildrenIndex = 1 << 4; |
| |
| /// Whether the [scrollChildren] field has been updated but has not been |
| /// applied to the DOM yet. |
| bool get isScrollChildrenDirty => _isDirty(_scrollChildrenIndex); |
| void _markScrollChildrenDirty() { |
| _dirtyFields |= _scrollChildrenIndex; |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| int get scrollIndex => _scrollIndex; |
| int _scrollIndex; |
| |
| static const int _scrollIndexIndex = 1 << 5; |
| |
| /// Whether the [scrollIndex] field has been updated but has not been |
| /// applied to the DOM yet. |
| bool get isScrollIndexDirty => _isDirty(_scrollIndexIndex); |
| void _markScrollIndexDirty() { |
| _dirtyFields |= _scrollIndexIndex; |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| double get scrollPosition => _scrollPosition; |
| double _scrollPosition; |
| |
| static const int _scrollPositionIndex = 1 << 6; |
| |
| /// Whether the [scrollPosition] field has been updated but has not been |
| /// applied to the DOM yet. |
| bool get isScrollPositionDirty => _isDirty(_scrollPositionIndex); |
| void _markScrollPositionDirty() { |
| _dirtyFields |= _scrollPositionIndex; |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| double get scrollExtentMax => _scrollExtentMax; |
| double _scrollExtentMax; |
| |
| static const int _scrollExtentMaxIndex = 1 << 7; |
| |
| /// Whether the [scrollExtentMax] field has been updated but has not been |
| /// applied to the DOM yet. |
| bool get isScrollExtentMaxDirty => _isDirty(_scrollExtentMaxIndex); |
| void _markScrollExtentMaxDirty() { |
| _dirtyFields |= _scrollExtentMaxIndex; |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| double get scrollExtentMin => _scrollExtentMin; |
| double _scrollExtentMin; |
| |
| static const int _scrollExtentMinIndex = 1 << 8; |
| |
| /// Whether the [scrollExtentMin] field has been updated but has not been |
| /// applied to the DOM yet. |
| bool get isScrollExtentMinDirty => _isDirty(_scrollExtentMinIndex); |
| void _markScrollExtentMinDirty() { |
| _dirtyFields |= _scrollExtentMinIndex; |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| ui.Rect get rect => _rect; |
| ui.Rect _rect; |
| |
| static const int _rectIndex = 1 << 9; |
| |
| /// Whether the [rect] field has been updated but has not been |
| /// applied to the DOM yet. |
| bool get isRectDirty => _isDirty(_rectIndex); |
| void _markRectDirty() { |
| _dirtyFields |= _rectIndex; |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| String get label => _label; |
| String _label; |
| |
| /// Whether this object contains a non-empty label. |
| bool get hasLabel => _label != null && _label.isNotEmpty; |
| |
| static const int _labelIndex = 1 << 10; |
| |
| /// Whether the [label] field has been updated but has not been |
| /// applied to the DOM yet. |
| bool get isLabelDirty => _isDirty(_labelIndex); |
| void _markLabelDirty() { |
| _dirtyFields |= _labelIndex; |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| String get hint => _hint; |
| String _hint; |
| |
| static const int _hintIndex = 1 << 11; |
| |
| /// Whether the [hint] field has been updated but has not been |
| /// applied to the DOM yet. |
| bool get isHintDirty => _isDirty(_hintIndex); |
| void _markHintDirty() { |
| _dirtyFields |= _hintIndex; |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| String get value => _value; |
| String _value; |
| |
| /// Whether this object contains a non-empty value. |
| bool get hasValue => _value != null && _value.isNotEmpty; |
| |
| static const int _valueIndex = 1 << 12; |
| |
| /// Whether the [value] field has been updated but has not been |
| /// applied to the DOM yet. |
| bool get isValueDirty => _isDirty(_valueIndex); |
| void _markValueDirty() { |
| _dirtyFields |= _valueIndex; |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| String get increasedValue => _increasedValue; |
| String _increasedValue; |
| |
| static const int _increasedValueIndex = 1 << 13; |
| |
| /// Whether the [increasedValue] field has been updated but has not been |
| /// applied to the DOM yet. |
| bool get isIncreasedValueDirty => _isDirty(_increasedValueIndex); |
| void _markIncreasedValueDirty() { |
| _dirtyFields |= _increasedValueIndex; |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| String get decreasedValue => _decreasedValue; |
| String _decreasedValue; |
| |
| static const int _decreasedValueIndex = 1 << 14; |
| |
| /// Whether the [decreasedValue] field has been updated but has not been |
| /// applied to the DOM yet. |
| bool get isDecreasedValueDirty => _isDirty(_decreasedValueIndex); |
| void _markDecreasedValueDirty() { |
| _dirtyFields |= _decreasedValueIndex; |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| ui.TextDirection get textDirection => _textDirection; |
| ui.TextDirection _textDirection; |
| |
| static const int _textDirectionIndex = 1 << 15; |
| |
| /// Whether the [textDirection] field has been updated but has not been |
| /// applied to the DOM yet. |
| bool get isTextDirectionDirty => _isDirty(_textDirectionIndex); |
| void _markTextDirectionDirty() { |
| _dirtyFields |= _textDirectionIndex; |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| Float32List get transform => _transform; |
| Float32List _transform; |
| |
| static const int _transformIndex = 1 << 16; |
| |
| /// Whether the [transform] field has been updated but has not been |
| /// applied to the DOM yet. |
| bool get isTransformDirty => _isDirty(_transformIndex); |
| void _markTransformDirty() { |
| _dirtyFields |= _transformIndex; |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| Int32List get childrenInTraversalOrder => _childrenInTraversalOrder; |
| Int32List _childrenInTraversalOrder; |
| |
| static const int _childrenInTraversalOrderIndex = 1 << 19; |
| |
| /// Whether the [childrenInTraversalOrder] field has been updated but has not |
| /// been applied to the DOM yet. |
| bool get isChildrenInTraversalOrderDirty => |
| _isDirty(_childrenInTraversalOrderIndex); |
| void _markChildrenInTraversalOrderDirty() { |
| _dirtyFields |= _childrenInTraversalOrderIndex; |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| Int32List get childrenInHitTestOrder => _childrenInHitTestOrder; |
| Int32List _childrenInHitTestOrder; |
| |
| static const int _childrenInHitTestOrderIndex = 1 << 20; |
| |
| /// Whether the [childrenInHitTestOrder] field has been updated but has not |
| /// been applied to the DOM yet. |
| bool get isChildrenInHitTestOrderDirty => |
| _isDirty(_childrenInHitTestOrderIndex); |
| void _markChildrenInHitTestOrderDirty() { |
| _dirtyFields |= _childrenInHitTestOrderIndex; |
| } |
| |
| /// See [ui.SemanticsUpdateBuilder.updateNode]. |
| Int32List get additionalActions => _additionalActions; |
| Int32List _additionalActions; |
| |
| static const int _additionalActionsIndex = 1 << 21; |
| |
| /// Whether the [additionalActions] field has been updated but has not been |
| /// applied to the DOM yet. |
| bool get isAdditionalActionsDirty => _isDirty(_additionalActionsIndex); |
| void _markAdditionalActionsDirty() { |
| _dirtyFields |= _additionalActionsIndex; |
| } |
| |
| /// A unique permanent identifier of the semantics node in the tree. |
| final int id; |
| |
| /// Controls the semantics tree that this node participates in. |
| final EngineSemanticsOwner owner; |
| |
| /// The DOM element used to convey semantics information to the browser. |
| final html.Element element = html.Element.tag('flt-semantics'); |
| |
| /// Bitfield showing which fields have been updated but have not yet been |
| /// applied to the DOM. |
| /// |
| /// Instead of use this field directly, prefer using one of the "is*Dirty" |
| /// getters, e.g. [isFlagsDirty]. |
| /// |
| /// The bitfield supports up to 31 bits. |
| int _dirtyFields = -1; // initial value is when all relevant bits are set |
| |
| /// Whether the field corresponding to the [fieldIndex] has been updated. |
| bool _isDirty(int fieldIndex) => (_dirtyFields & fieldIndex) != 0; |
| |
| /// Returns the HTML element that contains the HTML elements of direct |
| /// children of this object. |
| /// |
| /// The element is created lazily. When the child list is empty this element |
| /// is not created. This is necessary for "aria-label" to function correctly. |
| /// The browser will ignore the [label] of HTML element that contain child |
| /// elements. |
| html.Element getOrCreateChildContainer() { |
| if (_childContainerElement == null) { |
| _childContainerElement = html.Element.tag('flt-semantics-container'); |
| _childContainerElement.style.position = 'absolute'; |
| element.append(_childContainerElement); |
| } |
| return _childContainerElement; |
| } |
| |
| /// The element that contains the elements belonging to the child semantics |
| /// nodes. |
| /// |
| /// This element is used to correct for [_rect] offsets. It is only non-`null` |
| /// when there are non-zero children (i.e. when [hasChildren] is `true`). |
| html.Element _childContainerElement; |
| |
| /// The parent of this semantics object. |
| SemanticsObject _parent; |
| |
| /// Whether this node currently has a given [SemanticsFlag]. |
| bool hasFlag(ui.SemanticsFlag flag) => _flags & flag.index != 0; |
| |
| /// Whether [actions] contains the given action. |
| bool hasAction(ui.SemanticsAction action) => (_actions & action.index) != 0; |
| |
| /// Whether this object represents a vertically scrollable area. |
| bool get isVerticalScrollContainer => |
| hasAction(ui.SemanticsAction.scrollDown) || |
| hasAction(ui.SemanticsAction.scrollUp); |
| |
| /// Whether this object represents a hotizontally scrollable area. |
| bool get isHorizontalScrollContainer => |
| hasAction(ui.SemanticsAction.scrollLeft) || |
| hasAction(ui.SemanticsAction.scrollRight); |
| |
| /// Whether this object has a non-empty list of children. |
| bool get hasChildren => |
| _childrenInTraversalOrder != null && _childrenInTraversalOrder.isNotEmpty; |
| |
| /// Whether this object represents an editable text field. |
| bool get isTextField => hasFlag(ui.SemanticsFlag.isTextField); |
| |
| /// Whether this object needs screen readers attention right away. |
| bool get isLiveRegion => |
| hasFlag(ui.SemanticsFlag.isLiveRegion) && |
| !hasFlag(ui.SemanticsFlag.isHidden); |
| |
| /// Whether this object represents an image with no tappable functionality. |
| bool get isVisualOnly => |
| hasFlag(ui.SemanticsFlag.isImage) && |
| !hasAction(ui.SemanticsAction.tap) && |
| !hasFlag(ui.SemanticsFlag.isButton); |
| |
| /// Updates this object from data received from a semantics [update]. |
| /// |
| /// This method creates [SemanticsObject]s for the direct children of this |
| /// object. However, it does not recursively populate them. |
| void updateWith(SemanticsNodeUpdate update) { |
| // Update all field values and their corresponding dirty flags before |
| // applying the updates to the DOM. |
| assert(update.flags != null); |
| if (_flags != update.flags) { |
| _flags = update.flags; |
| _markFlagsDirty(); |
| } |
| |
| if (_value != update.value) { |
| _value = update.value; |
| _markValueDirty(); |
| } |
| |
| if (_label != update.label) { |
| _label = update.label; |
| _markLabelDirty(); |
| } |
| |
| if (_rect != update.rect) { |
| _rect = update.rect; |
| _markRectDirty(); |
| } |
| |
| if (_transform != update.transform) { |
| _transform = update.transform; |
| _markTransformDirty(); |
| } |
| |
| if (_scrollPosition != update.scrollPosition) { |
| _scrollPosition = update.scrollPosition; |
| _markScrollPositionDirty(); |
| } |
| |
| if (_actions != update.actions) { |
| _actions = update.actions; |
| _markActionsDirty(); |
| } |
| |
| if (_textSelectionBase != update.textSelectionBase) { |
| _textSelectionBase = update.textSelectionBase; |
| _markTextSelectionBaseDirty(); |
| } |
| |
| if (_textSelectionExtent != update.textSelectionExtent) { |
| _textSelectionExtent = update.textSelectionExtent; |
| _markTextSelectionExtentDirty(); |
| } |
| |
| if (_scrollChildren != update.scrollChildren) { |
| _scrollChildren = update.scrollChildren; |
| _markScrollChildrenDirty(); |
| } |
| |
| if (_scrollIndex != update.scrollIndex) { |
| _scrollIndex = update.scrollIndex; |
| _markScrollIndexDirty(); |
| } |
| |
| if (_scrollExtentMax != update.scrollExtentMax) { |
| _scrollExtentMax = update.scrollExtentMax; |
| _markScrollExtentMaxDirty(); |
| } |
| |
| if (_scrollExtentMin != update.scrollExtentMin) { |
| _scrollExtentMin = update.scrollExtentMin; |
| _markScrollExtentMinDirty(); |
| } |
| |
| if (_hint != update.hint) { |
| _hint = update.hint; |
| _markHintDirty(); |
| } |
| |
| if (_increasedValue != update.increasedValue) { |
| _increasedValue = update.increasedValue; |
| _markIncreasedValueDirty(); |
| } |
| |
| if (_decreasedValue != update.decreasedValue) { |
| _decreasedValue = update.decreasedValue; |
| _markDecreasedValueDirty(); |
| } |
| |
| if (_textDirection != update.textDirection) { |
| _textDirection = update.textDirection; |
| _markTextDirectionDirty(); |
| } |
| |
| if (_childrenInHitTestOrder != update.childrenInHitTestOrder) { |
| _childrenInHitTestOrder = update.childrenInHitTestOrder; |
| _markChildrenInHitTestOrderDirty(); |
| } |
| |
| if (_childrenInTraversalOrder != update.childrenInTraversalOrder) { |
| _childrenInTraversalOrder = update.childrenInTraversalOrder; |
| _markChildrenInTraversalOrderDirty(); |
| } |
| |
| if (_additionalActions != update.additionalActions) { |
| _additionalActions = update.additionalActions; |
| _markAdditionalActionsDirty(); |
| } |
| |
| // Apply updates to the DOM. |
| _updateRoles(); |
| _updateChildrenInTraversalOrder(); |
| |
| // All properties that affect positioning and sizing are checked together |
| // any one of them triggers position and size recomputation. |
| if (isRectDirty || isTransformDirty || isScrollPositionDirty) { |
| recomputePositionAndSize(); |
| } |
| |
| // Make sure we create a child container only when there are children. |
| assert(_childContainerElement == null || hasChildren); |
| _dirtyFields = 0; |
| } |
| |
| /// Populates the HTML "role" attribute based on a [condition]. |
| /// |
| /// If [condition] is true, sets the value to [ariaRoleName]. |
| /// |
| /// If [condition] is false, removes the HTML "role" attribute from [element] |
| /// if the current role is set to [ariaRoleName]. Otherwise, leaves the value |
| /// unchanged. This is done so we gracefully handle multiple competing roles. |
| /// For example, if the role changes from "button" to "img" and tappable role |
| /// manager attempts to clean up after the image role manager applied the new |
| /// role, we do not want it to erase the new role. |
| void setAriaRole(String ariaRoleName, bool condition) { |
| if (condition) { |
| element.setAttribute('role', ariaRoleName); |
| } else if (element.getAttribute('role') == ariaRoleName) { |
| element.attributes.remove('role'); |
| } |
| } |
| |
| /// Role managers. |
| /// |
| /// The [_roleManagers] map needs to have a stable order for easier debugging |
| /// and testing. Dart's map literal guarantees the order as described in the |
| /// spec: |
| /// |
| /// > A map literal is ordered: iterating over the keys and/or values of the maps always happens in the order the keys appeared in the source code. |
| final Map<Role, RoleManager> _roleManagers = <Role, RoleManager>{}; |
| |
| /// Detects the roles that this semantics object corresponds to and manages |
| /// the lifecycles of [SemanticsObjectRole] objects. |
| void _updateRoles() { |
| _updateRole(Role.labelAndValue, (hasLabel || hasValue) && !isVisualOnly); |
| _updateRole(Role.textField, isTextField); |
| _updateRole( |
| Role.tappable, |
| hasAction(ui.SemanticsAction.tap) || |
| hasFlag(ui.SemanticsFlag.isButton)); |
| _updateRole(Role.incrementable, isIncrementable); |
| _updateRole(Role.scrollable, |
| isVerticalScrollContainer || isHorizontalScrollContainer); |
| _updateRole( |
| Role.checkable, |
| hasFlag(ui.SemanticsFlag.hasCheckedState) || |
| hasFlag(ui.SemanticsFlag.hasToggledState)); |
| _updateRole(Role.image, isVisualOnly); |
| _updateRole(Role.liveRegion, isLiveRegion); |
| } |
| |
| void _updateRole(Role role, bool enabled) { |
| RoleManager manager = _roleManagers[role]; |
| if (enabled) { |
| if (manager == null) { |
| manager = _roleFactories[role](this); |
| _roleManagers[role] = manager; |
| } |
| manager.update(); |
| } else if (manager != null) { |
| manager.dispose(); |
| _roleManagers.remove(role); |
| } |
| // Nothing to do in the "else case" as it means that we want to disable a |
| // role that we don't currently have in the first place. |
| } |
| |
| /// Whether the object represents an UI element with "increase" or "decrease" |
| /// controls, e.g. a slider. |
| /// |
| /// Such objects are expressed in HTML using `<input type="range">`. |
| bool get isIncrementable => |
| hasAction(ui.SemanticsAction.increase) || |
| hasAction(ui.SemanticsAction.decrease); |
| |
| /// Role-specific adjustment of the vertical position of the child container. |
| /// |
| /// This is used, for example, by the [Scrollable] to compensate for the |
| /// `scrollTop` offset in the DOM. |
| /// |
| /// This field must not be null. |
| double verticalContainerAdjustment = 0.0; |
| |
| /// Role-specific adjustment of the horizontal position of the child |
| /// container. |
| /// |
| /// This is used, for example, by the [Scrollable] to compensate for the |
| /// `scrollLeft` offset in the DOM. |
| /// |
| /// This field must not be null. |
| double horizontalContainerAdjustment = 0.0; |
| |
| /// Computes the size and position of [element] and, if this element |
| /// [hasChildren], of [getOrCreateChildContainer]. |
| void recomputePositionAndSize() { |
| element.style |
| ..width = '${_rect.width}px' |
| ..height = '${_rect.height}px'; |
| |
| final html.Element containerElement = |
| hasChildren ? getOrCreateChildContainer() : null; |
| |
| final bool hasZeroRectOffset = _rect.top == 0.0 && _rect.left == 0.0; |
| final bool hasIdentityTransform = |
| _transform == null || isIdentityFloat32ListTransform(_transform); |
| |
| if (hasZeroRectOffset && |
| hasIdentityTransform && |
| verticalContainerAdjustment == 0.0 && |
| horizontalContainerAdjustment == 0.0) { |
| element.style |
| ..removeProperty('transform-origin') |
| ..removeProperty('transform'); |
| if (containerElement != null) { |
| containerElement.style |
| ..removeProperty('transform-origin') |
| ..removeProperty('transform'); |
| } |
| return; |
| } |
| |
| Matrix4 effectiveTransform; |
| bool effectiveTransformIsIdentity = true; |
| if (!hasZeroRectOffset) { |
| if (_transform == null) { |
| final double left = _rect.left; |
| final double top = _rect.top; |
| effectiveTransform = Matrix4.translationValues(left, top, 0.0); |
| effectiveTransformIsIdentity = left == 0.0 && top == 0.0; |
| } else { |
| // Clone to avoid mutating _transform. |
| effectiveTransform = Matrix4.fromFloat32List(_transform).clone() |
| ..translate(_rect.left, _rect.top, 0.0); |
| effectiveTransformIsIdentity = effectiveTransform.isIdentity(); |
| } |
| } else if (!hasIdentityTransform) { |
| effectiveTransform = Matrix4.fromFloat32List(_transform); |
| effectiveTransformIsIdentity = false; |
| } |
| |
| if (!effectiveTransformIsIdentity) { |
| element.style |
| ..transformOrigin = '0 0 0' |
| ..transform = matrix4ToCssTransform(effectiveTransform); |
| } else { |
| element.style |
| ..removeProperty('transform-origin') |
| ..removeProperty('transform'); |
| } |
| |
| if (containerElement != null) { |
| if (!hasZeroRectOffset || |
| verticalContainerAdjustment != 0.0 || |
| horizontalContainerAdjustment != 0.0) { |
| final double translateX = -_rect.left + horizontalContainerAdjustment; |
| final double translateY = -_rect.top + verticalContainerAdjustment; |
| containerElement.style |
| ..transformOrigin = '0 0 0' |
| ..transform = 'translate(${translateX}px, ${translateY}px)'; |
| } else { |
| containerElement.style |
| ..removeProperty('transform-origin') |
| ..removeProperty('transform'); |
| } |
| } |
| } |
| |
| Int32List _previousChildrenInTraversalOrder; |
| |
| /// Updates the traversal child list of [object] from the given [update]. |
| /// |
| /// This method does not recursively update child elements' properties or |
| /// their grandchildren. This is handled by [updateSemantics] method walking |
| /// all the update nodes. |
| void _updateChildrenInTraversalOrder() { |
| // Remove all children case. |
| if (_childrenInTraversalOrder == null || |
| _childrenInTraversalOrder.isEmpty) { |
| if (_previousChildrenInTraversalOrder == null || |
| _previousChildrenInTraversalOrder.isEmpty) { |
| // We must not have created a container element when child list is empty. |
| assert(_childContainerElement == null); |
| _previousChildrenInTraversalOrder = _childrenInTraversalOrder; |
| return; |
| } |
| |
| // We must have created a container element when child list is not empty. |
| assert(_childContainerElement != null); |
| |
| // Remove all children from this semantics object. |
| final int len = _previousChildrenInTraversalOrder.length; |
| for (int i = 0; i < len; i++) { |
| owner._detachObject(_previousChildrenInTraversalOrder[i]); |
| } |
| _previousChildrenInTraversalOrder = null; |
| _childContainerElement.remove(); |
| _childContainerElement = null; |
| _previousChildrenInTraversalOrder = _childrenInTraversalOrder; |
| return; |
| } |
| |
| final html.Element containerElement = getOrCreateChildContainer(); |
| |
| // Empty case. |
| if (_previousChildrenInTraversalOrder == null || |
| _previousChildrenInTraversalOrder.isEmpty) { |
| _previousChildrenInTraversalOrder = _childrenInTraversalOrder; |
| for (int id in _previousChildrenInTraversalOrder) { |
| final SemanticsObject child = owner.getOrCreateObject(id); |
| containerElement.append(child.element); |
| owner._attachObject(parent: this, child: child); |
| } |
| _previousChildrenInTraversalOrder = _childrenInTraversalOrder; |
| return; |
| } |
| |
| // Both non-empty case. |
| |
| // Indices into the new child list pointing at children that also exist in |
| // the old child list. |
| final List<int> intersectionIndicesNew = <int>[]; |
| |
| // Indices into the old child list pointing at children that also exist in |
| // the new child list. |
| final List<int> intersectionIndicesOld = <int>[]; |
| |
| int newIndex = 0; |
| |
| // The smallest of the two child list lengths. |
| final int minLength = math.min( |
| _previousChildrenInTraversalOrder.length, |
| _childrenInTraversalOrder.length, |
| ); |
| |
| // Scan forward until first discrepancy. |
| while (newIndex < minLength && |
| _previousChildrenInTraversalOrder[newIndex] == |
| _childrenInTraversalOrder[newIndex]) { |
| intersectionIndicesNew.add(newIndex); |
| intersectionIndicesOld.add(newIndex); |
| newIndex += 1; |
| } |
| |
| // If child lists are identical, do nothing. |
| if (_previousChildrenInTraversalOrder.length == |
| _childrenInTraversalOrder.length && |
| newIndex == _childrenInTraversalOrder.length) { |
| return; |
| } |
| |
| // If child lists are not identical, continue computing the intersection |
| // between the two lists. |
| while (newIndex < _childrenInTraversalOrder.length) { |
| for (int oldIndex = 0; |
| oldIndex < _previousChildrenInTraversalOrder.length; |
| oldIndex += 1) { |
| if (_previousChildrenInTraversalOrder[oldIndex] == |
| _childrenInTraversalOrder[newIndex]) { |
| intersectionIndicesNew.add(newIndex); |
| intersectionIndicesOld.add(oldIndex); |
| break; |
| } |
| } |
| newIndex += 1; |
| } |
| |
| // The longest sub-sequence in the old list maximizes the number of children |
| // that do not need to be moved. |
| final List<int> longestSequence = |
| longestIncreasingSubsequence(intersectionIndicesOld); |
| final List<int> stationaryIds = <int>[]; |
| for (int i = 0; i < longestSequence.length; i += 1) { |
| stationaryIds.add(_previousChildrenInTraversalOrder[ |
| intersectionIndicesOld[longestSequence[i]]]); |
| } |
| |
| // Remove children that are no longer in the list. |
| for (int i = 0; i < _previousChildrenInTraversalOrder.length; i++) { |
| if (!intersectionIndicesOld.contains(i)) { |
| // Child not in the intersection. Must be removed. |
| final int childId = _previousChildrenInTraversalOrder[i]; |
| owner._detachObject(childId); |
| } |
| } |
| |
| html.Element refNode; |
| for (int i = _childrenInTraversalOrder.length - 1; i >= 0; i -= 1) { |
| final int childId = _childrenInTraversalOrder[i]; |
| final SemanticsObject child = owner.getOrCreateObject(childId); |
| if (!stationaryIds.contains(childId)) { |
| if (refNode == null) { |
| containerElement.append(child.element); |
| } else { |
| containerElement.insertBefore(child.element, refNode); |
| } |
| owner._attachObject(parent: this, child: child); |
| } else { |
| assert(child._parent == this); |
| } |
| refNode = child.element; |
| } |
| |
| _previousChildrenInTraversalOrder = _childrenInTraversalOrder; |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| final String children = _childrenInTraversalOrder != null && |
| _childrenInTraversalOrder.isNotEmpty |
| ? '[${_childrenInTraversalOrder.join(', ')}]' |
| : '<empty>'; |
| return '$runtimeType(#$id, children: $children)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| } |
| |
| /// Controls how pointer events and browser-detected gestures are treated by |
| /// the Web Engine. |
| enum AccessibilityMode { |
| /// We are not told whether the assistive technology is enabled or not. |
| /// |
| /// This is the default mode. |
| /// |
| /// In this mode we use a gesture recognition system that deduplicates |
| /// gestures detected by Flutter with gestures detected by the browser. |
| unknown, |
| |
| /// We are told whether the assistive technology is enabled. |
| known, |
| } |
| |
| /// Called when the current [GestureMode] changes. |
| typedef GestureModeCallback = void Function(GestureMode mode); |
| |
| /// The method used to detect user gestures. |
| enum GestureMode { |
| /// Send pointer events to Flutter to detect gestures using framework-level |
| /// gesture recognizers and gesture arenas. |
| pointerEvents, |
| |
| /// Listen to browser-detected gestures and report them to the framework as |
| /// [ui.SemanticsAction]. |
| browserGestures, |
| } |
| |
| /// The top-level service that manages everything semantics-related. |
| class EngineSemanticsOwner { |
| EngineSemanticsOwner._() { |
| registerHotRestartListener(() { |
| _rootSemanticsElement?.remove(); |
| }); |
| } |
| |
| /// The singleton instance that manages semantics. |
| static EngineSemanticsOwner get instance { |
| return _instance ??= EngineSemanticsOwner._(); |
| } |
| |
| static EngineSemanticsOwner _instance; |
| |
| /// Disables semantics and uninitializes the singleton [instance]. |
| /// |
| /// Instances of [EngineSemanticsOwner] are no longer valid after calling this |
| /// method. Using them will lead to undefined behavior. This method is only |
| /// meant to be used for testing. |
| static void debugResetSemantics() { |
| if (_instance == null) { |
| return; |
| } |
| _instance.semanticsEnabled = false; |
| _instance = null; |
| } |
| |
| final Map<int, SemanticsObject> _semanticsTree = <int, SemanticsObject>{}; |
| |
| /// Map [SemanticsObject.id] to parent [SemanticsObject] it was attached to |
| /// this frame. |
| Map<int, SemanticsObject> _attachments = <int, SemanticsObject>{}; |
| |
| /// Declares that the [child] must be attached to the [parent]. |
| /// |
| /// Attachments take precendence over detachments (see [_detachObject]). This |
| /// allows the same node to be detached from one parent in the tree and |
| /// reattached to another parent. |
| void _attachObject({SemanticsObject parent, SemanticsObject child}) { |
| assert(child != null); |
| assert(parent != null); |
| child._parent = parent; |
| _attachments[child.id] = parent; |
| } |
| |
| /// List of objects that were detached this frame. |
| /// |
| /// The objects in this list will be detached permanently unless they are |
| /// reattached via the [_attachObject] method. |
| List<SemanticsObject> _detachments = <SemanticsObject>[]; |
| |
| /// Declares that the [SemanticsObject] with the given [id] was detached from |
| /// its current parent object. |
| /// |
| /// The object will be detached permanently unless it is reattached via the |
| /// [_attachObject] method. |
| void _detachObject(int id) { |
| assert(_semanticsTree.containsKey(id)); |
| final SemanticsObject object = _semanticsTree[id]; |
| _detachments.add(object); |
| } |
| |
| /// Callbacks called after all objects in the tree have their properties |
| /// populated and their sizes and locations computed. |
| /// |
| /// This list is reset to empty after all callbacks are called. |
| List<ui.VoidCallback> _oneTimePostUpdateCallbacks = <ui.VoidCallback>[]; |
| |
| /// Schedules a one-time callback to be called after all objects in the tree |
| /// have their properties populated and their sizes and locations computed. |
| void addOneTimePostUpdateCallback(ui.VoidCallback callback) { |
| _oneTimePostUpdateCallbacks.add(callback); |
| } |
| |
| /// Reconciles [_attachments] and [_detachments], and after that calls all |
| /// the one-time callbacks scheduled via the [addOneTimePostUpdateCallback] |
| /// method. |
| void _finalizeTree() { |
| for (SemanticsObject object in _detachments) { |
| final SemanticsObject parent = _attachments[object.id]; |
| if (parent == null) { |
| // Was not reparented and is removed permanently from the tree. |
| _semanticsTree.remove(object.id); |
| object._parent = null; |
| object.element.remove(); |
| } else { |
| assert(object._parent == parent); |
| assert(object.element.parent == parent._childContainerElement); |
| } |
| } |
| _detachments = <SemanticsObject>[]; |
| _attachments = <int, SemanticsObject>{}; |
| |
| if (_oneTimePostUpdateCallbacks.isNotEmpty) { |
| for (ui.VoidCallback callback in _oneTimePostUpdateCallbacks) { |
| callback(); |
| } |
| _oneTimePostUpdateCallbacks = <ui.VoidCallback>[]; |
| } |
| } |
| |
| /// Returns the entire semantics tree for testing. |
| /// |
| /// Works only in debug mode. |
| Map<int, SemanticsObject> get debugSemanticsTree { |
| Map<int, SemanticsObject> result; |
| assert(() { |
| result = _semanticsTree; |
| return true; |
| }()); |
| return result; |
| } |
| |
| /// The top-level DOM element of the semantics DOM element tree. |
| html.Element _rootSemanticsElement; |
| |
| TimestampFunction _now = () => DateTime.now(); |
| |
| void debugOverrideTimestampFunction(TimestampFunction value) { |
| _now = value; |
| } |
| |
| void debugResetTimestampFunction() { |
| _now = () => DateTime.now(); |
| } |
| |
| final SemanticsHelper semanticsHelper = SemanticsHelper(); |
| |
| /// Whether the user has requested that [updateSemantics] be called when |
| /// the semantic contents of window changes. |
| /// |
| /// The [ui.Window.onSemanticsEnabledChanged] callback is called whenever this |
| /// value changes. |
| /// |
| /// This is separate from accessibility [mode], which controls how gestures |
| /// are interpreted when this value is true. |
| bool get semanticsEnabled => _semanticsEnabled; |
| bool _semanticsEnabled = false; |
| set semanticsEnabled(bool value) { |
| if (value == _semanticsEnabled) { |
| return; |
| } |
| _semanticsEnabled = value; |
| |
| if (!_semanticsEnabled) { |
| // We do not process browser events at all when semantics is explicitly |
| // disabled. All gestures are handled by the framework-level gesture |
| // recognizers from pointer events. |
| if (_gestureMode != GestureMode.pointerEvents) { |
| _gestureMode = GestureMode.pointerEvents; |
| _notifyGestureModeListeners(); |
| } |
| final List<int> keys = _semanticsTree.keys.toList(); |
| final int len = keys.length; |
| for (int i = 0; i < len; i++) { |
| _detachObject(keys[i]); |
| } |
| _finalizeTree(); |
| _rootSemanticsElement?.remove(); |
| _rootSemanticsElement = null; |
| _gestureModeClock?.datetime = null; |
| } |
| |
| if (window._onSemanticsEnabledChanged != null) { |
| window.invokeOnSemanticsEnabledChanged(); |
| } |
| } |
| |
| /// Controls how pointer events and browser-detected gestures are treated by |
| /// the Web Engine. |
| /// |
| /// The default mode is [AccessibilityMode.unknown]. |
| AccessibilityMode get mode => _mode; |
| set mode(AccessibilityMode value) { |
| assert(value != null); |
| _mode = value; |
| } |
| |
| AccessibilityMode _mode = AccessibilityMode.unknown; |
| |
| /// Currently used [GestureMode]. |
| /// |
| /// This value changes automatically depending on the incoming input events. |
| /// Functionality that implements different strategies depending on this mode |
| /// would use [addGestureModeListener] and [removeGestureModeListener] to get |
| /// notifications about when the value of this field changes. |
| GestureMode get gestureMode => _gestureMode; |
| GestureMode _gestureMode = GestureMode.browserGestures; |
| |
| AlarmClock _gestureModeClock; |
| |
| AlarmClock _getGestureModeClock() { |
| if (_gestureModeClock == null) { |
| _gestureModeClock = AlarmClock(_now); |
| _gestureModeClock.callback = () { |
| if (_gestureMode == GestureMode.browserGestures) { |
| return; |
| } |
| |
| _gestureMode = GestureMode.browserGestures; |
| _notifyGestureModeListeners(); |
| }; |
| } |
| return _gestureModeClock; |
| } |
| |
| /// Disables browser gestures temporarily because we have detected pointer |
| /// events. |
| /// |
| /// This is used to deduplicate gestures detected by Flutter and gestures |
| /// detected by the browser. Flutter-detected gestures have higher precedence. |
| void _temporarilyDisableBrowserGestureMode() { |
| const Duration _kDebounceThreshold = Duration(milliseconds: 500); |
| _getGestureModeClock().datetime = _now().add(_kDebounceThreshold); |
| if (_gestureMode != GestureMode.pointerEvents) { |
| _gestureMode = GestureMode.pointerEvents; |
| _notifyGestureModeListeners(); |
| } |
| } |
| |
| /// Receives DOM events from the pointer event system to correlate with the |
| /// semantics events; returns true if the event should be forwarded to the |
| /// framework. |
| /// |
| /// The browser sends us both raw pointer events and gestures from |
| /// [SemanticsObject.element]s. There could be three possibilities: |
| /// |
| /// 1. Assistive technology is enabled and we know that it is. |
| /// 2. Assistive technology is disabled and we know that it isn't. |
| /// 3. We do not know whether an assistive technology is enabled. |
| /// |
| /// If [autoEnableOnTap] was called, this will automatically enable semantics |
| /// if the user requests it. |
| /// |
| /// In the first case we can ignore raw pointer events and only interpret |
| /// high-level gestures, e.g. "click". |
| /// |
| /// In the second case we can ignore high-level gestures and interpret the raw |
| /// pointer events directly. |
| /// |
| /// Finally, in a mode when we do not know if an assistive technology is |
| /// enabled or not we do a best-effort estimate which to respond to, raw |
| /// pointer or high-level gestures. We avoid doing both because that will |
| /// result in double-firing of event listeners, such as `onTap` on a button. |
| /// An approach we use is to measure the distance between the last pointer |
| /// event and a gesture event. If a gesture is receive "soon" after the last |
| /// received pointer event (determined by a heuristic), it is debounced as it |
| /// is likely that the gesture detected from the pointer even will do the |
| /// right thing. However, if we receive a standalone gesture we will map it |
| /// onto a [ui.SemanticsAction] to be processed by the framework. |
| bool receiveGlobalEvent(html.Event event) { |
| // For pointer event reference see: |
| // |
| // https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events |
| const List<String> _pointerEventTypes = <String>[ |
| 'pointerdown', |
| 'pointermove', |
| 'pointerup', |
| 'pointercancel', |
| 'touchstart', |
| 'touchend', |
| 'touchmove', |
| 'touchcancel', |
| 'mousedown', |
| 'mousemove', |
| 'mouseup', |
| 'keyup', |
| 'keydown', |
| ]; |
| |
| if (_pointerEventTypes.contains(event.type)) { |
| _temporarilyDisableBrowserGestureMode(); |
| } |
| |
| return semanticsHelper.shouldEnableSemantics(event); |
| } |
| |
| /// Callbacks called when the [GestureMode] changes. |
| /// |
| /// Callbacks are called synchronously. HTML DOM updates made in a callback |
| /// take effect in the current animation frame and/or the current message loop |
| /// event. |
| List<GestureModeCallback> _gestureModeListeners = <GestureModeCallback>[]; |
| |
| /// Calls the [callback] every time the current [GestureMode] changes. |
| /// |
| /// The callback is called synchronously. HTML DOM updates made in the |
| /// callback take effect in the current animation frame and/or the current |
| /// message loop event. |
| void addGestureModeListener(GestureModeCallback callback) { |
| _gestureModeListeners.add(callback); |
| } |
| |
| /// Stops calling the [callback] when the [GestureMode] changes. |
| /// |
| /// The passed [callback] must be the exact same object as the one passed to |
| /// [addGestureModeListener]. |
| void removeGestureModeListener(GestureModeCallback callback) { |
| assert(_gestureModeListeners.contains(callback)); |
| _gestureModeListeners.remove(callback); |
| } |
| |
| void _notifyGestureModeListeners() { |
| for (int i = 0; i < _gestureModeListeners.length; i++) { |
| _gestureModeListeners[i](_gestureMode); |
| } |
| } |
| |
| /// Whether a gesture event of type [eventType] should be accepted as a |
| /// semantic action. |
| /// |
| /// If [mode] is [AccessibilityMode.known] the gesture is always accepted if |
| /// [semanticsEnabled] is `true`, and it is always rejected if |
| /// [semanticsEnabled] is `false`. |
| /// |
| /// If [mode] is [AccessibilityMode.unknown] the gesture is accepted if it is |
| /// not accompanied by pointer events. In the presence of pointer events we |
| /// delegate to Flutter's gesture detection system to produce gestures. |
| bool shouldAcceptBrowserGesture(String eventType) { |
| if (_mode == AccessibilityMode.known) { |
| // Do not ignore accessibility gestures in known mode, unless semantics |
| // is explicitly disabled. |
| return semanticsEnabled; |
| } |
| |
| const List<String> pointerDebouncedGestures = <String>[ |
| 'click', |
| 'scroll', |
| ]; |
| |
| if (pointerDebouncedGestures.contains(eventType)) { |
| return _gestureMode == GestureMode.browserGestures; |
| } |
| |
| return false; |
| } |
| |
| /// Looks up a [SemanticsObject] in the semantics tree by ID, or creates a new |
| /// instance if it does not exist. |
| SemanticsObject getOrCreateObject(int id) { |
| SemanticsObject object = _semanticsTree[id]; |
| if (object == null) { |
| object = SemanticsObject(id, this); |
| _semanticsTree[id] = object; |
| } |
| return object; |
| } |
| |
| /// Updates the semantics tree from data in the [uiUpdate]. |
| void updateSemantics(ui.SemanticsUpdate/*!*/ uiUpdate) { |
| if (!_semanticsEnabled) { |
| return; |
| } |
| |
| final SemanticsUpdate update = uiUpdate; |
| for (SemanticsNodeUpdate nodeUpdate in update._nodeUpdates) { |
| final SemanticsObject object = getOrCreateObject(nodeUpdate.id); |
| object.updateWith(nodeUpdate); |
| } |
| |
| if (_rootSemanticsElement == null) { |
| final SemanticsObject root = _semanticsTree[0]; |
| _rootSemanticsElement = root.element; |
| // We render semantics inside the glasspane for proper focus and event |
| // handling. If semantics is behind the glasspane, the phone will disable |
| // focusing by touch, only by tabbing around the UI. If semantics is in |
| // front of glasspane, then DOM event won't bubble up to the glasspane so |
| // it can forward events to the framework. |
| // |
| // We insert the semantics root before the scene host. For all widgets |
| // in the scene, except for platform widgets, the scene host will pass the |
| // pointer events through to the semantics tree. However, for platform |
| // views, the pointer events will not pass through, and will be handled |
| // by the platform view. |
| domRenderer.glassPaneElement |
| .insertBefore(_rootSemanticsElement, domRenderer.sceneHostElement); |
| } |
| |
| _finalizeTree(); |
| |
| assert(_semanticsTree.containsKey(0)); // must contain root node |
| assert(() { |
| // Validate tree |
| _semanticsTree.forEach((int id, SemanticsObject object) { |
| assert(id == object.id); |
| // Ensure child ID list is consistent with the parent-child |
| // relationship of the semantics tree. |
| if (object._childrenInTraversalOrder != null) { |
| for (int childId in object._childrenInTraversalOrder) { |
| final SemanticsObject child = _semanticsTree[childId]; |
| if (child == null) { |
| throw AssertionError('Child #$childId is missing in the tree.'); |
| } |
| if (child._parent == null) { |
| throw AssertionError( |
| 'Child #$childId of parent #${object.id} has null parent ' |
| 'reference.'); |
| } |
| if (!identical(child._parent, object)) { |
| throw AssertionError( |
| 'Parent #${object.id} has child #$childId. However, the ' |
| 'child is attached to #${child._parent.id}.'); |
| } |
| } |
| } |
| }); |
| |
| // Validate that all updates were applied |
| for (SemanticsNodeUpdate update in update._nodeUpdates) { |
| // Node was added to the tree. |
| assert(_semanticsTree.containsKey(update.id)); |
| // We created a DOM element for it. |
| assert(_semanticsTree[update.id].element != null); |
| } |
| |
| return true; |
| }()); |
| } |
| } |
| |
| /// Computes the [longest increasing subsequence](http://en.wikipedia.org/wiki/Longest_increasing_subsequence). |
| /// |
| /// Returns list of indices (rather than values) into [list]. |
| /// |
| /// Complexity: n*log(n) |
| List<int> longestIncreasingSubsequence(List<int> list) { |
| final int len = list.length; |
| final List<int> predecessors = <int>[]; |
| final List<int> mins = <int>[0]; |
| int longest = 0; |
| for (int i = 0; i < len; i++) { |
| // Binary search for the largest positive `j ≤ longest` |
| // such that `list[mins[j]] < list[i]` |
| final int elem = list[i]; |
| int lo = 1; |
| int hi = longest; |
| while (lo <= hi) { |
| final int mid = (lo + hi) ~/ 2; |
| if (list[mins[mid]] < elem) { |
| lo = mid + 1; |
| } else { |
| hi = mid - 1; |
| } |
| } |
| // After searching, `lo` is 1 greater than the |
| // length of the longest prefix of `list[i]` |
| final int expansionIndex = lo; |
| // The predecessor of `list[i]` is the last index of |
| // the subsequence of length `newLongest - 1` |
| predecessors.add(mins[expansionIndex - 1]); |
| if (expansionIndex >= mins.length) { |
| mins.add(i); |
| } else { |
| mins[expansionIndex] = i; |
| } |
| if (expansionIndex > longest) { |
| // If we found a subsequence longer than any we've |
| // found yet, update `longest` |
| longest = expansionIndex; |
| } |
| } |
| // Reconstruct the longest subsequence |
| final List<int> seq = List<int>(longest); |
| int k = mins[longest]; |
| for (int i = longest - 1; i >= 0; i--) { |
| seq[i] = k; |
| k = predecessors[k]; |
| } |
| return seq; |
| } |