semantics.dart 25.8 KB
Newer Older
Hixie's avatar
Hixie committed
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:ui' as ui;
6
import 'dart:ui' show Rect, SemanticsAction, SemanticsFlags;
7
import 'dart:typed_data';
Hixie's avatar
Hixie committed
8

9
import 'package:flutter/foundation.dart';
Hixie's avatar
Hixie committed
10 11 12 13 14
import 'package:flutter/painting.dart';
import 'package:vector_math/vector_math_64.dart';

import 'node.dart';

15
export 'dart:ui' show SemanticsAction;
Hixie's avatar
Hixie committed
16

17
/// Interface for [RenderObject]s to implement when they want to support
Hixie's avatar
Hixie committed
18 19 20
/// being tapped, etc.
///
/// These handlers will only be called if the relevant flag is set
21 22 23
/// (e.g. [handleSemanticTap]() will only be called if
/// [SemanticsNode.canBeTapped] is true, [handleSemanticScrollDown]() will only
/// be called if [SemanticsNode.canBeScrolledVertically] is true, etc).
24
abstract class SemanticsActionHandler { // ignore: one_member_abstracts
25
  /// Called when the object implementing this interface receives a
26
  /// [SemanticsAction]. For example, if the user of an accessibility tool
27 28
  /// instructs their device that they wish to tap a button, the [RenderObject]
  /// behind that button would have its [performAction] method called with the
29 30
  /// [SemanticsAction.tap] action.
  void performAction(SemanticsAction action);
Hixie's avatar
Hixie committed
31 32
}

33
/// The type of function returned by [RenderObject.getSemanticsAnnotators()].
34 35 36 37 38
///
/// These callbacks are called with the [SemanticsNode] object that
/// corresponds to the [RenderObject]. (One [SemanticsNode] can
/// correspond to multiple [RenderObject] objects.)
///
39
/// See [RenderObject.getSemanticsAnnotators()] for details on the
40
/// contract that semantic annotators must follow.
41
typedef void SemanticsAnnotator(SemanticsNode semantics);
42

43 44 45
/// Signature for a function that is called for each [SemanticsNode].
///
/// Return false to stop visiting nodes.
46 47
///
/// Used by [SemanticsNode.visitChildren].
Hixie's avatar
Hixie committed
48 49
typedef bool SemanticsNodeVisitor(SemanticsNode node);

50 51 52 53 54 55 56 57 58 59 60
/// Summary information about a [SemanticsNode] object.
///
/// A semantics node might [SemanticsNode.mergeAllDescendantsIntoThisNode],
/// which means the individual fields on the semantics node don't fully describe
/// the semantics at that node. This data structure contains the full semantics
/// for the node.
///
/// Typically obtained from [SemanticsNode.getSemanticsData].
class SemanticsData {
  /// Creates a semantics data object.
  ///
61 62
  /// The [flags], [actions], [label], and [Rect] arguments must not be null.
  SemanticsData({
63 64 65 66 67
    @required this.flags,
    @required this.actions,
    @required this.label,
    @required this.rect,
    this.transform
68 69 70 71 72 73
  }) {
    assert(flags != null);
    assert(actions != null);
    assert(label != null);
    assert(rect != null);
  }
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98

  /// A bit field of [SemanticsFlags] that apply to this node.
  final int flags;

  /// A bit field of [SemanticsActions] that apply to this node.
  final int actions;

  /// A textual description of this node.
  final String label;

  /// The bounding box for this node in its coordinate system.
  final Rect rect;

  /// The transform from this node's coordinate system to its parent's coordinate system.
  ///
  /// By default, the transform is null, which represents the identity
  /// transformation (i.e., that this node has the same coorinate system as its
  /// parent).
  final Matrix4 transform;

  /// Whether [flags] contains the given flag.
  bool hasFlag(SemanticsFlags flag) => (flags & flag.index) != 0;

  /// Whether [actions] contains the given action.
  bool hasAction(SemanticsAction action) => (actions & action.index) != 0;
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133

  @override
  String toString() {
    StringBuffer buffer = new StringBuffer();
    buffer.write('$runtimeType($rect');
    if (transform != null)
      buffer.write('; $transform');
    for (SemanticsAction action in SemanticsAction.values.values) {
      if ((actions & action.index) != 0)
        buffer.write('; $action');
    }
    for (SemanticsFlags flag in SemanticsFlags.values.values) {
      if ((flags & flag.index) != 0)
        buffer.write('; $flag');
    }
    if (label.isNotEmpty)
      buffer.write('; "$label"');
    buffer.write(')');
    return buffer.toString();
  }

  @override
  bool operator ==(dynamic other) {
    if (other is! SemanticsData)
      return false;
    final SemanticsData typedOther = other;
    return typedOther.flags == flags
        && typedOther.actions == actions
        && typedOther.label == label
        && typedOther.rect == rect
        && typedOther.transform == transform;
  }

  @override
  int get hashCode => hashValues(flags, actions, label, rect, transform);
134 135
}

136 137 138 139 140 141
/// A node that represents some semantic data.
///
/// The semantics tree is maintained during the semantics phase of the pipeline
/// (i.e., during [PipelineOwner.flushSemantics]), which happens after
/// compositing. The semantics tree is then uploaded into the engine for use
/// by assistive technology.
Hixie's avatar
Hixie committed
142
class SemanticsNode extends AbstractNode {
143 144 145 146
  /// Creates a semantic node.
  ///
  /// Each semantic node has a unique identifier that is assigned when the node
  /// is created.
Hixie's avatar
Hixie committed
147
  SemanticsNode({
148
    SemanticsActionHandler handler
149
  }) : id = _generateNewId(),
Hixie's avatar
Hixie committed
150 151
       _actionHandler = handler;

152 153 154
  /// Creates a semantic node to represent the root of the semantics tree.
  ///
  /// The root node is assigned an identifier of zero.
Hixie's avatar
Hixie committed
155
  SemanticsNode.root({
156
    SemanticsActionHandler handler,
157
    SemanticsOwner owner
158
  }) : id = 0,
Hixie's avatar
Hixie committed
159
       _actionHandler = handler {
160
    attach(owner);
Hixie's avatar
Hixie committed
161 162 163 164 165 166 167 168
  }

  static int _lastIdentifier = 0;
  static int _generateNewId() {
    _lastIdentifier += 1;
    return _lastIdentifier;
  }

169 170 171 172 173
  /// The unique identifier for this node.
  ///
  /// The root node has an id of zero. Other nodes are given a unique id when
  /// they are created.
  final int id;
Hixie's avatar
Hixie committed
174

175
  final SemanticsActionHandler _actionHandler;
Hixie's avatar
Hixie committed
176 177 178 179

  // GEOMETRY
  // These are automatically handled by RenderObject's own logic

180 181 182 183 184
  /// The transform from this node's coordinate system to its parent's coordinate system.
  ///
  /// By default, the transform is null, which represents the identity
  /// transformation (i.e., that this node has the same coorinate system as its
  /// parent).
Hixie's avatar
Hixie committed
185
  Matrix4 get transform => _transform;
186
  Matrix4 _transform;
187
  set transform (Matrix4 value) {
Hixie's avatar
Hixie committed
188 189 190 191 192 193
    if (!MatrixUtils.matrixEquals(_transform, value)) {
      _transform = value;
      _markDirty();
    }
  }

194
  /// The bounding box for this node in its coordinate system.
Hixie's avatar
Hixie committed
195 196
  Rect get rect => _rect;
  Rect _rect = Rect.zero;
197
  set rect (Rect value) {
Hixie's avatar
Hixie committed
198 199 200 201 202 203 204
    assert(value != null);
    if (_rect != value) {
      _rect = value;
      _markDirty();
    }
  }

205
  /// Whether [rect] might have been influenced by clips applied by ancestors.
Hixie's avatar
Hixie committed
206 207 208 209
  bool wasAffectedByClip = false;


  // FLAGS AND LABELS
210
  // These are supposed to be set by SemanticsAnnotator obtained from getSemanticsAnnotators
Hixie's avatar
Hixie committed
211

212
  int _actions = 0;
213

214 215 216
  /// Adds the given action to the set of semantic actions.
  ///
  /// If the user chooses to perform an action,
217
  /// [SemanticsActionHandler.performAction] will be called with the chosen
218
  /// action.
219
  void addAction(SemanticsAction action) {
220 221 222
    final int index = action.index;
    if ((_actions & index) == 0) {
      _actions |= index;
223
      _markDirty();
224
    }
225 226
  }

227
  /// Adds the [SemanticsAction.scrollLeft] and [SemanticsAction.scrollRight] actions.
228
  void addHorizontalScrollingActions() {
229 230
    addAction(SemanticsAction.scrollLeft);
    addAction(SemanticsAction.scrollRight);
231 232
  }

233
  /// Adds the [SemanticsAction.scrollUp] and [SemanticsAction.scrollDown] actions.
234
  void addVerticalScrollingActions() {
235 236
    addAction(SemanticsAction.scrollUp);
    addAction(SemanticsAction.scrollDown);
237 238
  }

239
  /// Adds the [SemanticsAction.increase] and [SemanticsAction.decrease] actions.
240
  void addAdjustmentActions() {
241 242
    addAction(SemanticsAction.increase);
    addAction(SemanticsAction.decrease);
243 244
  }

245
  bool _canPerformAction(SemanticsAction action) {
246
    return _actionHandler != null && (_actions & action.index) != 0;
247 248
  }

249 250 251 252
  /// Whether all this node and all of its descendants should be treated as one logical entity.
  bool get mergeAllDescendantsIntoThisNode => _mergeAllDescendantsIntoThisNode;
  bool _mergeAllDescendantsIntoThisNode = false;
  set mergeAllDescendantsIntoThisNode(bool value) {
Hixie's avatar
Hixie committed
253
    assert(value != null);
254 255 256 257
    if (_mergeAllDescendantsIntoThisNode == value)
      return;
    _mergeAllDescendantsIntoThisNode = value;
    _markDirty();
Hixie's avatar
Hixie committed
258 259
  }

260 261 262 263 264 265 266 267 268
  bool get _inheritedMergeAllDescendantsIntoThisNode => _inheritedMergeAllDescendantsIntoThisNodeValue;
  bool _inheritedMergeAllDescendantsIntoThisNodeValue = false;
  set _inheritedMergeAllDescendantsIntoThisNode(bool value) {
    assert(value != null);
    if (_inheritedMergeAllDescendantsIntoThisNodeValue == value)
      return;
    _inheritedMergeAllDescendantsIntoThisNodeValue = value;
    _markDirty();
  }
269 270 271

  bool get _shouldMergeAllDescendantsIntoThisNode => mergeAllDescendantsIntoThisNode || _inheritedMergeAllDescendantsIntoThisNode;

272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
  int _flags = 0;
  void _setFlag(SemanticsFlags flag, bool value) {
    final int index = flag.index;
    if (value) {
      if ((_flags & index) == 0) {
        _flags |= index;
        _markDirty();
      }
    } else {
      if ((_flags & index) != 0) {
        _flags &= ~index;
        _markDirty();
      }
    }
  }

288
  /// Whether this node has Boolean state that can be controlled by the user.
289 290
  bool get hasCheckedState => (_flags & SemanticsFlags.hasCheckedState.index) != 0;
  set hasCheckedState(bool value) => _setFlag(SemanticsFlags.hasCheckedState, value);
Hixie's avatar
Hixie committed
291

292 293
  /// If this node has Boolean state that can be controlled by the user, whether
  /// that state is on or off, corresponding to true and false, respectively.
294 295
  bool get isChecked => (_flags & SemanticsFlags.isChecked.index) != 0;
  set isChecked(bool value) => _setFlag(SemanticsFlags.isChecked, value);
Hixie's avatar
Hixie committed
296

297
  /// A textual description of this node.
Hixie's avatar
Hixie committed
298 299
  String get label => _label;
  String _label = '';
300
  set label(String value) {
Hixie's avatar
Hixie committed
301 302 303 304 305 306 307
    assert(value != null);
    if (_label != value) {
      _label = value;
      _markDirty();
    }
  }

308
  /// Restore this node to its default state.
Hixie's avatar
Hixie committed
309
  void reset() {
310
    bool hadInheritedMergeAllDescendantsIntoThisNode = _inheritedMergeAllDescendantsIntoThisNode;
311 312
    _actions = 0;
    _flags = 0;
313
    if (hadInheritedMergeAllDescendantsIntoThisNode)
314
      _inheritedMergeAllDescendantsIntoThisNodeValue = true;
Hixie's avatar
Hixie committed
315 316 317 318 319
    _label = '';
    _markDirty();
  }

  List<SemanticsNode> _newChildren;
320 321

  /// Append the given children as children of this node.
Hixie's avatar
Hixie committed
322 323 324 325
  void addChildren(Iterable<SemanticsNode> children) {
    _newChildren ??= <SemanticsNode>[];
    _newChildren.addAll(children);
    // we do the asserts afterwards because children is an Iterable
326
    // and doing the asserts before would mean the behavior is
Hixie's avatar
Hixie committed
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
    // different in checked mode vs release mode (if you walk an
    // iterator after having reached the end, it'll just start over;
    // the values are not cached).
    assert(!_newChildren.any((SemanticsNode child) => child == this));
    assert(() {
      SemanticsNode ancestor = this;
      while (ancestor.parent is SemanticsNode)
        ancestor = ancestor.parent;
      assert(!_newChildren.any((SemanticsNode child) => child == ancestor));
      return true;
    });
    assert(() {
      Set<SemanticsNode> seenChildren = new Set<SemanticsNode>();
      for (SemanticsNode child in _newChildren)
        assert(seenChildren.add(child)); // check for duplicate adds
      return true;
    });
  }

  List<SemanticsNode> _children;
347 348

  /// Whether this node has a non-zero number of children.
Hixie's avatar
Hixie committed
349 350
  bool get hasChildren => _children?.isNotEmpty ?? false;
  bool _dead = false;
351

352 353 354
  /// The number of children this node has.
  int get childrenCount => hasChildren ? _children.length : 0;

355 356 357 358 359 360 361 362 363 364 365 366 367 368
  /// Visits the immediate children of this node.
  ///
  /// This function calls visitor for each child in a pre-order travseral
  /// until visitor returns false. Returns true if all the visitor calls
  /// returned true, otherwise returns false.
  void visitChildren(SemanticsNodeVisitor visitor) {
    if (_children != null) {
      for (SemanticsNode child in _children) {
        if (!visitor(child))
          return;
      }
    }
  }

369 370 371 372 373
  /// Called during the compilation phase after all the children of this node have been compiled.
  ///
  /// This function lets the semantic node respond to all the changes to its
  /// child list for the given frame at once instead of needing to process the
  /// changes incrementally as new children are compiled.
Hixie's avatar
Hixie committed
374
  void finalizeChildren() {
375
    // The goal of this function is updating sawChange.
Hixie's avatar
Hixie committed
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
    if (_children != null) {
      for (SemanticsNode child in _children)
        child._dead = true;
    }
    if (_newChildren != null) {
      for (SemanticsNode child in _newChildren)
        child._dead = false;
    }
    bool sawChange = false;
    if (_children != null) {
      for (SemanticsNode child in _children) {
        if (child._dead) {
          if (child.parent == this) {
            // we might have already had our child stolen from us by
            // another node that is deeper in the tree.
            dropChild(child);
          }
          sawChange = true;
        }
      }
    }
    if (_newChildren != null) {
      for (SemanticsNode child in _newChildren) {
        if (child.parent != this) {
          if (child.parent != null) {
            // we're rebuilding the tree from the bottom up, so it's possible
402
            // that our child was, in the last pass, a child of one of our
Hixie's avatar
Hixie committed
403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
            // ancestors. In that case, we drop the child eagerly here.
            // TODO(ianh): Find a way to assert that the same node didn't
            // actually appear in the tree in two places.
            child.parent?.dropChild(child);
          }
          assert(!child.attached);
          adoptChild(child);
          sawChange = true;
        }
      }
    }
    List<SemanticsNode> oldChildren = _children;
    _children = _newChildren;
    oldChildren?.clear();
    _newChildren = oldChildren;
    if (sawChange)
      _markDirty();
  }

422 423 424
  @override
  SemanticsOwner get owner => super.owner;

425
  @override
Hixie's avatar
Hixie committed
426
  SemanticsNode get parent => super.parent;
427 428

  @override
Hixie's avatar
Hixie committed
429 430 431 432 433 434 435
  void redepthChildren() {
    if (_children != null) {
      for (SemanticsNode child in _children)
        redepthChild(child);
    }
  }

436 437 438 439 440
  /// Visit all the descendants of this node.
  ///
  /// This function calls visitor for each descendant in a pre-order travseral
  /// until visitor returns false. Returns true if all the visitor calls
  /// returned true, otherwise returns false.
Hixie's avatar
Hixie committed
441 442 443 444 445 446 447 448 449 450
  bool _visitDescendants(SemanticsNodeVisitor visitor) {
    if (_children != null) {
      for (SemanticsNode child in _children) {
        if (!visitor(child) || !child._visitDescendants(visitor))
          return false;
      }
    }
    return true;
  }

451
  @override
452
  void attach(SemanticsOwner owner) {
453
    super.attach(owner);
454 455
    assert(!owner._nodes.containsKey(id));
    owner._nodes[id] = this;
456 457 458 459 460
    owner._detachedNodes.remove(this);
    if (_dirty) {
      _dirty = false;
      _markDirty();
    }
461 462
    if (parent != null)
      _inheritedMergeAllDescendantsIntoThisNode = parent._shouldMergeAllDescendantsIntoThisNode;
Hixie's avatar
Hixie committed
463 464
    if (_children != null) {
      for (SemanticsNode child in _children)
465
        child.attach(owner);
Hixie's avatar
Hixie committed
466 467
    }
  }
468 469

  @override
Hixie's avatar
Hixie committed
470
  void detach() {
471
    assert(owner._nodes.containsKey(id));
472
    assert(!owner._detachedNodes.contains(this));
473
    owner._nodes.remove(id);
474
    owner._detachedNodes.add(this);
Hixie's avatar
Hixie committed
475 476
    super.detach();
    if (_children != null) {
477 478 479 480 481 482
      for (SemanticsNode child in _children) {
        // The list of children may be stale and may contain nodes that have
        // been assigned to a different parent.
        if (child.parent == this)
          child.detach();
      }
Hixie's avatar
Hixie committed
483 484 485 486 487 488 489 490
    }
  }

  bool _dirty = false;
  void _markDirty() {
    if (_dirty)
      return;
    _dirty = true;
491 492 493 494
    if (attached) {
      assert(!owner._detachedNodes.contains(this));
      owner._dirtyNodes.add(this);
    }
Hixie's avatar
Hixie committed
495 496
  }

497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529
  /// Returns a summary of the semantics for this node.
  ///
  /// If this node has [mergeAllDescendantsIntoThisNode], then the returned data
  /// includes the information from this node's descendants. Otherwise, the
  /// returned data matches the data on this node.
  SemanticsData getSemanticsData() {
    int flags = _flags;
    int actions = _actions;
    String label = _label;

    if (mergeAllDescendantsIntoThisNode) {
      _visitDescendants((SemanticsNode node) {
        flags |= node._flags;
        actions |= node._actions;
        if (node.label.isNotEmpty) {
          if (label.isEmpty)
            label = node.label;
          else
            label = '$label\n${node.label}';
        }
        return true;
      });
    }

    return new SemanticsData(
      flags: flags,
      actions: actions,
      label: label,
      rect: rect,
      transform: transform
    );
  }

530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547
  static Float64List _initIdentityTransform() {
    return new Matrix4.identity().storage;
  }

  static final Int32List _kEmptyChildList = new Int32List(0);
  static final Float64List _kIdentityTransform = _initIdentityTransform();

  void _addToUpdate(ui.SemanticsUpdateBuilder builder) {
    assert(_dirty);
    final SemanticsData data = getSemanticsData();
    Int32List children;
    if (!hasChildren || mergeAllDescendantsIntoThisNode) {
      children = _kEmptyChildList;
    } else {
      final int childCount = _children.length;
      children = new Int32List(childCount);
      for (int i = 0; i < childCount; ++i)
        children[i] = _children[i].id;
Hixie's avatar
Hixie committed
548
    }
549 550 551 552 553
    builder.updateNode(
      id: id,
      flags: data.flags,
      actions: data.actions,
      rect: data.rect,
554
      label: data.label,
555
      transform: data.transform?.storage ?? _kIdentityTransform,
556
      children: children,
557 558
    );
    _dirty = false;
Hixie's avatar
Hixie committed
559 560
  }

561 562 563
  @override
  String toString() {
    StringBuffer buffer = new StringBuffer();
564
    buffer.write('$runtimeType($id');
565 566 567 568 569 570 571
    if (_dirty)
      buffer.write(" (${ owner != null && owner._dirtyNodes.contains(this) ? 'dirty' : 'STALE' })");
    if (_shouldMergeAllDescendantsIntoThisNode)
      buffer.write(' (leaf merge)');
    buffer.write('; $rect');
    if (wasAffectedByClip)
      buffer.write(' (clipped)');
572 573 574
    for (SemanticsAction action in SemanticsAction.values.values) {
      if ((_actions & action.index) != 0)
        buffer.write('; $action');
575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
    }
    if (hasCheckedState) {
      if (isChecked)
        buffer.write('; checked');
      else
        buffer.write('; unchecked');
    }
    if (label.isNotEmpty)
      buffer.write('; "$label"');
    buffer.write(')');
    return buffer.toString();
  }

  /// Returns a string representation of this node and its descendants.
  String toStringDeep([String prefixLineOne = '', String prefixOtherLines = '']) {
    String result = '$prefixLineOne$this\n';
    if (_children != null && _children.isNotEmpty) {
      for (int index = 0; index < _children.length - 1; index += 1) {
        SemanticsNode child = _children[index];
        result += '${child.toStringDeep("$prefixOtherLines \u251C", "$prefixOtherLines \u2502")}';
      }
      result += '${_children.last.toStringDeep("$prefixOtherLines \u2514", "$prefixOtherLines  ")}';
    }
    return result;
  }
}

602 603 604 605 606
/// Owns [SemanticsNode] objects and notifies listeners of changes to the
/// render tree semantics.
///
/// To listen for semantic updates, call [PipelineOwner.addSemanticsListener],
/// which will create a [SemanticsOwner] if necessary.
607
class SemanticsOwner extends ChangeNotifier {
608
  final Set<SemanticsNode> _dirtyNodes = new Set<SemanticsNode>();
609 610 611
  final Map<int, SemanticsNode> _nodes = <int, SemanticsNode>{};
  final Set<SemanticsNode> _detachedNodes = new Set<SemanticsNode>();

612 613 614 615 616
  /// The root node of the semantics tree, if any.
  ///
  /// If the semantics tree is empty, returns null.
  SemanticsNode get rootSemanticsNode => _nodes[0];

617
  @override
618 619 620 621
  void dispose() {
    _dirtyNodes.clear();
    _nodes.clear();
    _detachedNodes.clear();
622
    super.dispose();
623
  }
624

625 626
  /// Update the semantics using [ui.window.updateSemantics].
  void sendSemanticsUpdate() {
Hixie's avatar
Hixie committed
627 628 629 630 631 632 633 634 635
    for (SemanticsNode oldNode in _detachedNodes) {
      // The other side will have forgotten this node if we even send
      // it again, so make sure to mark it dirty so that it'll get
      // sent if it is resurrected.
      oldNode._dirty = true;
    }
    _detachedNodes.clear();
    if (_dirtyNodes.isEmpty)
      return;
636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667
    List<SemanticsNode> visitedNodes = <SemanticsNode>[];
    while (_dirtyNodes.isNotEmpty) {
      List<SemanticsNode> localDirtyNodes = _dirtyNodes.toList();
      _dirtyNodes.clear();
      localDirtyNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
      visitedNodes.addAll(localDirtyNodes);
      for (SemanticsNode node in localDirtyNodes) {
        assert(node._dirty);
        assert(node.parent == null || !node.parent._shouldMergeAllDescendantsIntoThisNode || node._inheritedMergeAllDescendantsIntoThisNode);
        if (node._shouldMergeAllDescendantsIntoThisNode) {
          assert(node.mergeAllDescendantsIntoThisNode || node.parent != null);
          if (node.mergeAllDescendantsIntoThisNode ||
              node.parent != null && node.parent._shouldMergeAllDescendantsIntoThisNode) {
            // if we're merged into our parent, make sure our parent is added to the list
            if (node.parent != null && node.parent._shouldMergeAllDescendantsIntoThisNode)
              node.parent._markDirty(); // this can add the node to the dirty list
            // make sure all the descendants are also marked, so that if one gets marked dirty later we know to walk up then too
            if (node._children != null) {
              for (SemanticsNode child in node._children)
                child._inheritedMergeAllDescendantsIntoThisNode = true; // this can add the node to the dirty list
            }
          } else {
            // we previously were being merged but aren't any more
            // update our bits and all our descendants'
            assert(node._inheritedMergeAllDescendantsIntoThisNode);
            assert(!node.mergeAllDescendantsIntoThisNode);
            assert(node.parent == null || !node.parent._shouldMergeAllDescendantsIntoThisNode);
            node._inheritedMergeAllDescendantsIntoThisNode = false;
            if (node._children != null) {
              for (SemanticsNode child in node._children)
                child._inheritedMergeAllDescendantsIntoThisNode = false; // this can add the node to the dirty list
            }
668 669
          }
        }
670
      }
671
    }
672
    visitedNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
673
    ui.SemanticsUpdateBuilder builder = new ui.SemanticsUpdateBuilder();
674
    for (SemanticsNode node in visitedNodes) {
Hixie's avatar
Hixie committed
675 676 677 678 679 680 681 682 683 684 685 686
      assert(node.parent?._dirty != true); // could be null (no parent) or false (not dirty)
      // The _serialize() method marks the node as not dirty, and
      // recurses through the tree to do a deep serialization of all
      // contiguous dirty nodes. This means that when we return here,
      // it's quite possible that subsequent nodes are no longer
      // dirty. We skip these here.
      // We also skip any nodes that were reset and subsequently
      // dropped entirely (RenderObject.markNeedsSemanticsUpdate()
      // calls reset() on its SemanticsNode if onlyChanges isn't set,
      // which happens e.g. when the node is no longer contributing
      // semantics).
      if (node._dirty && node.attached)
687
        node._addToUpdate(builder);
Hixie's avatar
Hixie committed
688 689
    }
    _dirtyNodes.clear();
690 691
    ui.window.updateSemantics(builder.build());
    notifyListeners();
Hixie's avatar
Hixie committed
692 693
  }

694
  SemanticsActionHandler _getSemanticsActionHandlerForId(int id, SemanticsAction action) {
Hixie's avatar
Hixie committed
695
    SemanticsNode result = _nodes[id];
696
    if (result != null && result._shouldMergeAllDescendantsIntoThisNode && !result._canPerformAction(action)) {
Hixie's avatar
Hixie committed
697
      result._visitDescendants((SemanticsNode node) {
698
        if (node._canPerformAction(action)) {
Hixie's avatar
Hixie committed
699 700 701 702 703 704
          result = node;
          return false; // found node, abort walk
        }
        return true; // continue walk
      });
    }
705
    if (result == null || !result._canPerformAction(action))
Hixie's avatar
Hixie committed
706 707 708 709
      return null;
    return result._actionHandler;
  }

710 711 712 713
  /// Asks the [SemanticsNode] with the given id to perform the given action.
  ///
  /// If the [SemanticsNode] has not indicated that it can perform the action,
  /// this function does nothing.
714
  void performAction(int id, SemanticsAction action) {
715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759
    assert(action != null);
    SemanticsActionHandler handler = _getSemanticsActionHandlerForId(id, action);
    handler?.performAction(action);
  }

  SemanticsActionHandler _getSemanticsActionHandlerForPosition(SemanticsNode node, Point position, SemanticsAction action) {
    if (node.transform != null) {
      Matrix4 inverse = new Matrix4.identity();
      if (inverse.copyInverse(node.transform) == 0.0)
        return null;
      position = MatrixUtils.transformPoint(inverse, position);
    }
    if (!node.rect.contains(position))
      return null;
    if (node.mergeAllDescendantsIntoThisNode) {
      SemanticsNode result;
      node._visitDescendants((SemanticsNode child) {
        if (child._canPerformAction(action)) {
          result = child;
          return false;
        }
        return true;
      });
      return result?._actionHandler;
    }
    if (node.hasChildren) {
      for (SemanticsNode child in node._children.reversed) {
        SemanticsActionHandler handler = _getSemanticsActionHandlerForPosition(child, position, action);
        if (handler != null)
          return handler;
      }
    }
    return node._canPerformAction(action) ? node._actionHandler : null;
  }

  /// Asks the [SemanticsNode] with at the given position to perform the given action.
  ///
  /// If the [SemanticsNode] has not indicated that it can perform the action,
  /// this function does nothing.
  void performActionAt(Point position, SemanticsAction action) {
    assert(action != null);
    final SemanticsNode node = rootSemanticsNode;
    if (node == null)
      return;
    SemanticsActionHandler handler = _getSemanticsActionHandlerForPosition(node, position, action);
760
    handler?.performAction(action);
Hixie's avatar
Hixie committed
761 762
  }
}