semantics.dart 22.6 KB
Newer Older
Hixie's avatar
Hixie committed
1 2 3 4 5 6 7
// 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.

import 'dart:math' as math;
import 'dart:ui' show Rect;

8
import 'package:flutter/foundation.dart';
Hixie's avatar
Hixie committed
9 10 11 12 13 14 15 16
import 'package:flutter/painting.dart';
import 'package:sky_services/semantics/semantics.mojom.dart' as mojom;
import 'package:vector_math/vector_math_64.dart';

import 'node.dart';

/// The type of function returned by [RenderObject.getSemanticAnnotators()].
///
17
/// These callbacks are called with the [SemanticsNode] object that
Hixie's avatar
Hixie committed
18 19 20 21 22 23 24
/// corresponds to the [RenderObject]. (One [SemanticsNode] can
/// correspond to multiple [RenderObject] objects.)
///
/// See [RenderObject.getSemanticAnnotators()] for details on the
/// contract that semantic annotators must follow.
typedef void SemanticAnnotator(SemanticsNode semantics);

25
/// Interface for [RenderObject]s to implement when they want to support
Hixie's avatar
Hixie committed
26 27 28
/// being tapped, etc.
///
/// These handlers will only be called if the relevant flag is set
29 30 31
/// (e.g. [handleSemanticTap]() will only be called if
/// [SemanticsNode.canBeTapped] is true, [handleSemanticScrollDown]() will only
/// be called if [SemanticsNode.canBeScrolledVertically] is true, etc).
Hixie's avatar
Hixie committed
32
abstract class SemanticActionHandler {
33
  /// Called when the user taps on the render object.
Hixie's avatar
Hixie committed
34
  void handleSemanticTap() { }
35 36

  /// Called when the user presses on the render object for a long period of time.
Hixie's avatar
Hixie committed
37
  void handleSemanticLongPress() { }
38 39

  /// Called when the user scrolls to the left.
Hixie's avatar
Hixie committed
40
  void handleSemanticScrollLeft() { }
41 42

  /// Called when the user scrolls to the right.
Hixie's avatar
Hixie committed
43
  void handleSemanticScrollRight() { }
44 45

  /// Called when the user scrolls up.
Hixie's avatar
Hixie committed
46
  void handleSemanticScrollUp() { }
47 48

  /// Called when the user scrolls down.
Hixie's avatar
Hixie committed
49 50 51 52 53
  void handleSemanticScrollDown() { }
}

enum _SemanticFlags {
  mergeAllDescendantsIntoThisNode,
54
  inheritedMergeAllDescendantsIntoThisNode, // whether an ancestor had mergeAllDescendantsIntoThisNode set
Hixie's avatar
Hixie committed
55 56 57 58 59 60 61 62
  canBeTapped,
  canBeLongPressed,
  canBeScrolledHorizontally,
  canBeScrolledVertically,
  hasCheckedState,
  isChecked,
}

63 64 65
/// Signature for a function that is called for each [SemanticsNode].
///
/// Return false to stop visiting nodes.
Hixie's avatar
Hixie committed
66 67
typedef bool SemanticsNodeVisitor(SemanticsNode node);

68 69 70 71 72 73
/// 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
74
class SemanticsNode extends AbstractNode {
75 76 77 78
  /// Creates a semantic node.
  ///
  /// Each semantic node has a unique identifier that is assigned when the node
  /// is created.
Hixie's avatar
Hixie committed
79 80 81 82 83
  SemanticsNode({
    SemanticActionHandler handler
  }) : _id = _generateNewId(),
       _actionHandler = handler;

84 85 86
  /// 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
87
  SemanticsNode.root({
88 89
    SemanticActionHandler handler,
    Object owner
Hixie's avatar
Hixie committed
90 91
  }) : _id = 0,
       _actionHandler = handler {
92
    attach(owner);
Hixie's avatar
Hixie committed
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
  }

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

  final int _id;
  final SemanticActionHandler _actionHandler;


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

108 109 110 111 112
  /// 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
113
  Matrix4 get transform => _transform;
114
  Matrix4 _transform;
115
  set transform (Matrix4 value) {
Hixie's avatar
Hixie committed
116 117 118 119 120 121
    if (!MatrixUtils.matrixEquals(_transform, value)) {
      _transform = value;
      _markDirty();
    }
  }

122
  /// The bounding box for this node in its coordinate system.
Hixie's avatar
Hixie committed
123 124
  Rect get rect => _rect;
  Rect _rect = Rect.zero;
125
  set rect (Rect value) {
Hixie's avatar
Hixie committed
126 127 128 129 130 131 132
    assert(value != null);
    if (_rect != value) {
      _rect = value;
      _markDirty();
    }
  }

133
  /// Whether [rect] might have been influenced by clips applied by ancestors.
Hixie's avatar
Hixie committed
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
  bool wasAffectedByClip = false;


  // FLAGS AND LABELS
  // These are supposed to be set by SemanticAnnotator obtained from getSemanticAnnotators

  BitField<_SemanticFlags> _flags = new BitField<_SemanticFlags>.filled(_SemanticFlags.values.length, false);

  void _setFlag(_SemanticFlags flag, bool value, { bool needsHandler: false }) {
    assert(value != null);
    assert((!needsHandler) || (_actionHandler != null) || (value == false));
    if (_flags[flag] != value) {
      _flags[flag] = value;
      _markDirty();
    }
  }

  bool _canHandle(_SemanticFlags flag) {
    return _actionHandler != null && _flags[flag];
  }

155
  /// Whether all this node and all of its descendants should be treated as one logical entity.
Hixie's avatar
Hixie committed
156
  bool get mergeAllDescendantsIntoThisNode => _flags[_SemanticFlags.mergeAllDescendantsIntoThisNode];
157
  set mergeAllDescendantsIntoThisNode(bool value) => _setFlag(_SemanticFlags.mergeAllDescendantsIntoThisNode, value);
Hixie's avatar
Hixie committed
158

159
  bool get _inheritedMergeAllDescendantsIntoThisNode => _flags[_SemanticFlags.inheritedMergeAllDescendantsIntoThisNode];
160
  set _inheritedMergeAllDescendantsIntoThisNode(bool value) => _setFlag(_SemanticFlags.inheritedMergeAllDescendantsIntoThisNode, value);
161 162 163

  bool get _shouldMergeAllDescendantsIntoThisNode => mergeAllDescendantsIntoThisNode || _inheritedMergeAllDescendantsIntoThisNode;

164
  /// Whether this node responds to tap gestures.
Hixie's avatar
Hixie committed
165
  bool get canBeTapped => _flags[_SemanticFlags.canBeTapped];
166
  set canBeTapped(bool value) => _setFlag(_SemanticFlags.canBeTapped, value, needsHandler: true);
Hixie's avatar
Hixie committed
167

168
  /// Whether this node responds to long-press gestures.
Hixie's avatar
Hixie committed
169
  bool get canBeLongPressed => _flags[_SemanticFlags.canBeLongPressed];
170
  set canBeLongPressed(bool value) => _setFlag(_SemanticFlags.canBeLongPressed, value, needsHandler: true);
Hixie's avatar
Hixie committed
171

172
  /// Whether this node responds to horizontal scrolling.
Hixie's avatar
Hixie committed
173
  bool get canBeScrolledHorizontally => _flags[_SemanticFlags.canBeScrolledHorizontally];
174
  set canBeScrolledHorizontally(bool value) => _setFlag(_SemanticFlags.canBeScrolledHorizontally, value, needsHandler: true);
Hixie's avatar
Hixie committed
175

176
  /// Whether this node responds to vertical scrolling.
Hixie's avatar
Hixie committed
177
  bool get canBeScrolledVertically => _flags[_SemanticFlags.canBeScrolledVertically];
178
  set canBeScrolledVertically(bool value) => _setFlag(_SemanticFlags.canBeScrolledVertically, value, needsHandler: true);
Hixie's avatar
Hixie committed
179

180
  /// Whether this node has Boolean state that can be controlled by the user.
Hixie's avatar
Hixie committed
181
  bool get hasCheckedState => _flags[_SemanticFlags.hasCheckedState];
182
  set hasCheckedState(bool value) => _setFlag(_SemanticFlags.hasCheckedState, value);
Hixie's avatar
Hixie committed
183

184
  /// If this node has Boolean state that can be controlled by the user, whether that state is on or off, cooresponding to `true` and `false`, respectively.
Hixie's avatar
Hixie committed
185
  bool get isChecked => _flags[_SemanticFlags.isChecked];
186
  set isChecked(bool value) => _setFlag(_SemanticFlags.isChecked, value);
Hixie's avatar
Hixie committed
187

188
  /// A textual description of this node.
Hixie's avatar
Hixie committed
189 190
  String get label => _label;
  String _label = '';
191
  set label(String value) {
Hixie's avatar
Hixie committed
192 193 194 195 196 197 198
    assert(value != null);
    if (_label != value) {
      _label = value;
      _markDirty();
    }
  }

199
  /// Restore this node to its default state.
Hixie's avatar
Hixie committed
200
  void reset() {
201
    bool hadInheritedMergeAllDescendantsIntoThisNode = _inheritedMergeAllDescendantsIntoThisNode;
Hixie's avatar
Hixie committed
202
    _flags.reset();
203 204
    if (hadInheritedMergeAllDescendantsIntoThisNode)
      _inheritedMergeAllDescendantsIntoThisNode = true;
Hixie's avatar
Hixie committed
205 206 207 208 209
    _label = '';
    _markDirty();
  }

  List<SemanticsNode> _newChildren;
210 211

  /// Append the given children as children of this node.
Hixie's avatar
Hixie committed
212 213 214 215
  void addChildren(Iterable<SemanticsNode> children) {
    _newChildren ??= <SemanticsNode>[];
    _newChildren.addAll(children);
    // we do the asserts afterwards because children is an Iterable
216
    // and doing the asserts before would mean the behavior is
Hixie's avatar
Hixie committed
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
    // 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;
237 238

  /// Whether this node has a non-zero number of children.
Hixie's avatar
Hixie committed
239 240
  bool get hasChildren => _children?.isNotEmpty ?? false;
  bool _dead = false;
241 242 243 244 245 246

  /// 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
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
  void finalizeChildren() {
    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
274
            // that our child was, in the last pass, a child of one of our
Hixie's avatar
Hixie committed
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
            // 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();
  }

294
  @override
Hixie's avatar
Hixie committed
295
  SemanticsNode get parent => super.parent;
296 297

  @override
Hixie's avatar
Hixie committed
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
  void redepthChildren() {
    if (_children != null) {
      for (SemanticsNode child in _children)
        redepthChild(child);
    }
  }

  // Visits all the descendants of this node, calling visitor for each one, until
  // visitor returns false. Returns true if all the visitor calls returned true,
  // otherwise returns false.
  bool _visitDescendants(SemanticsNodeVisitor visitor) {
    if (_children != null) {
      for (SemanticsNode child in _children) {
        if (!visitor(child) || !child._visitDescendants(visitor))
          return false;
      }
    }
    return true;
  }

  static Map<int, SemanticsNode> _nodes = <int, SemanticsNode>{};
  static Set<SemanticsNode> _detachedNodes = new Set<SemanticsNode>();

321
  @override
322 323
  void attach(Object owner) {
    super.attach(owner);
Hixie's avatar
Hixie committed
324 325 326
    assert(!_nodes.containsKey(_id));
    _nodes[_id] = this;
    _detachedNodes.remove(this);
327 328
    if (parent != null)
      _inheritedMergeAllDescendantsIntoThisNode = parent._shouldMergeAllDescendantsIntoThisNode;
Hixie's avatar
Hixie committed
329 330
    if (_children != null) {
      for (SemanticsNode child in _children)
331
        child.attach(owner);
Hixie's avatar
Hixie committed
332 333
    }
  }
334 335

  @override
Hixie's avatar
Hixie committed
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381
  void detach() {
    super.detach();
    assert(_nodes.containsKey(_id));
    assert(!_detachedNodes.contains(this));
    _nodes.remove(_id);
    _detachedNodes.add(this);
    if (_children != null) {
      for (SemanticsNode child in _children)
        child.detach();
    }
  }

  static List<SemanticsNode> _dirtyNodes = <SemanticsNode>[];
  bool _dirty = false;
  void _markDirty() {
    if (_dirty)
      return;
    _dirty = true;
    assert(!_dirtyNodes.contains(this));
    assert(!_detachedNodes.contains(this));
    _dirtyNodes.add(this);
  }

  mojom.SemanticsNode _serialize() {
    mojom.SemanticsNode result = new mojom.SemanticsNode();
    result.id = _id;
    if (_dirty) {
      // We could be even more efficient about not sending data here, by only
      // sending the bits that are dirty (tracking the geometry, flags, strings,
      // and children separately). For now, we send all or nothing.
      result.geometry = new mojom.SemanticGeometry();
      result.geometry.transform = transform?.storage;
      result.geometry.top = rect.top;
      result.geometry.left = rect.left;
      result.geometry.width = math.max(rect.width, 0.0);
      result.geometry.height = math.max(rect.height, 0.0);
      result.flags = new mojom.SemanticFlags();
      result.flags.canBeTapped = canBeTapped;
      result.flags.canBeLongPressed = canBeLongPressed;
      result.flags.canBeScrolledHorizontally = canBeScrolledHorizontally;
      result.flags.canBeScrolledVertically = canBeScrolledVertically;
      result.flags.hasCheckedState = hasCheckedState;
      result.flags.isChecked = isChecked;
      result.strings = new mojom.SemanticStrings();
      result.strings.label = label;
      List<mojom.SemanticsNode> children = <mojom.SemanticsNode>[];
382
      if (_shouldMergeAllDescendantsIntoThisNode) {
Hixie's avatar
Hixie committed
383 384 385 386 387 388 389 390 391
        _visitDescendants((SemanticsNode node) {
          result.flags.canBeTapped = result.flags.canBeTapped || node.canBeTapped;
          result.flags.canBeLongPressed = result.flags.canBeLongPressed || node.canBeLongPressed;
          result.flags.canBeScrolledHorizontally = result.flags.canBeScrolledHorizontally || node.canBeScrolledHorizontally;
          result.flags.canBeScrolledVertically = result.flags.canBeScrolledVertically || node.canBeScrolledVertically;
          result.flags.hasCheckedState = result.flags.hasCheckedState || node.hasCheckedState;
          result.flags.isChecked = result.flags.isChecked || node.isChecked;
          if (node.label != '')
            result.strings.label = result.strings.label.isNotEmpty ? '${result.strings.label}\n${node.label}' : node.label;
392
          node._dirty = false;
Hixie's avatar
Hixie committed
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
          return true; // continue walk
        });
        // and we pretend to have no children
      } else {
        if (_children != null) {
          for (SemanticsNode child in _children)
            children.add(child._serialize());
        }
      }
      result.children = children;
      _dirty = false;
    }
    return result;
  }

408
  static List<mojom.SemanticsListener> _listeners;
409 410 411 412 413

  /// Whether there are currently any consumers of semantic data.
  ///
  /// If there are no consumers of semantic data, there is no need to compile
  /// semantic data into a [SemanticsNode] tree.
414
  static bool get hasListeners => _listeners != null && _listeners.length > 0;
415 416 417 418 419 420 421 422 423 424 425

  /// Called when the first consumer of semantic data arrives.
  ///
  /// Typically set by [RendererBinding].
  static VoidCallback onSemanticsEnabled;

  /// Add a consumer of semantic data.
  ///
  /// After the [PipelineOwner] updates the semantic data for a given frame, it
  /// calls [sendSemanticsTree], which uploads the data to each listener
  /// registered with this function.
426
  static void addListener(mojom.SemanticsListener listener) {
427 428
    if (!hasListeners) {
      assert(onSemanticsEnabled != null); // initialise the binding _before_ adding listeners
429
      onSemanticsEnabled();
430
    }
431 432 433 434
    _listeners ??= <mojom.SemanticsListener>[];
    _listeners.add(listener);
  }

435
  /// Uploads the semantics tree to the listeners registered with [addListener].
436 437
  static void sendSemanticsTree() {
    assert(hasListeners);
Hixie's avatar
Hixie committed
438 439 440 441 442 443 444 445 446 447 448 449 450 451
    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;
    _dirtyNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
    for (int index = 0; index < _dirtyNodes.length; index += 1) {
      // we mutate the list as we walk it here, which is why we use an index instead of an iterator
      SemanticsNode node = _dirtyNodes[index];
      assert(node._dirty);
452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476
      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
          }
        }
477
      }
Hixie's avatar
Hixie committed
478
      assert(_dirtyNodes[index] == node); // make sure nothing went in front of us in the list
479
    }
Hixie's avatar
Hixie committed
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496
    _dirtyNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
    List<mojom.SemanticsNode> updatedNodes = <mojom.SemanticsNode>[];
    for (SemanticsNode node in _dirtyNodes) {
      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)
        updatedNodes.add(node._serialize());
    }
497 498
    for (mojom.SemanticsListener listener in _listeners)
      listener.updateSemanticsTree(updatedNodes);
Hixie's avatar
Hixie committed
499 500 501
    _dirtyNodes.clear();
  }

502
  static SemanticActionHandler _getSemanticActionHandlerForId(int id, { _SemanticFlags neededFlag }) {
Hixie's avatar
Hixie committed
503 504
    assert(neededFlag != null);
    SemanticsNode result = _nodes[id];
505
    if (result != null && result._shouldMergeAllDescendantsIntoThisNode && !result._canHandle(neededFlag)) {
Hixie's avatar
Hixie committed
506 507 508 509 510 511 512 513 514 515 516 517 518
      result._visitDescendants((SemanticsNode node) {
        if (node._actionHandler != null && node._flags[neededFlag]) {
          result = node;
          return false; // found node, abort walk
        }
        return true; // continue walk
      });
    }
    if (result == null || !result._canHandle(neededFlag))
      return null;
    return result._actionHandler;
  }

519
  @override
Hixie's avatar
Hixie committed
520 521
  String toString() {
    return '$runtimeType($_id'
522 523
             '${_dirty ? " (${ _dirtyNodes.contains(this) ? 'dirty' : 'STALE' })" : ""}'
             '${_shouldMergeAllDescendantsIntoThisNode ? " (leaf merge)" : ""}'
Hixie's avatar
Hixie committed
524 525 526 527 528 529 530 531 532 533 534
             '; $rect'
             '${wasAffectedByClip ? " (clipped)" : ""}'
             '${canBeTapped ? "; canBeTapped" : ""}'
             '${canBeLongPressed ? "; canBeLongPressed" : ""}'
             '${canBeScrolledHorizontally ? "; canBeScrolledHorizontally" : ""}'
             '${canBeScrolledVertically ? "; canBeScrolledVertically" : ""}'
             '${hasCheckedState ? (isChecked ? "; checked" : "; unchecked") : ""}'
             '${label != "" ? "; \"$label\"" : ""}'
           ')';
  }

535
  /// Returns a string representation of this node and its descendants.
Hixie's avatar
Hixie committed
536 537 538 539 540 541 542 543 544 545 546 547 548
  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;
  }
}

549
/// Exposes the [SemanticsNode] tree to the underlying platform.
Hixie's avatar
Hixie committed
550
class SemanticsServer extends mojom.SemanticsServer {
551
  @override
552
  void addSemanticsListener(mojom.SemanticsListenerProxy listener) {
553 554
    // TODO(abarth): We should remove the listener when this pipe closes.
    // See <https://github.com/flutter/flutter/issues/3342>.
555
    SemanticsNode.addListener(listener);
556
  }
557 558

  @override
Hixie's avatar
Hixie committed
559
  void tap(int nodeID) {
560
    SemanticsNode._getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeTapped)?.handleSemanticTap();
Hixie's avatar
Hixie committed
561
  }
562 563

  @override
Hixie's avatar
Hixie committed
564
  void longPress(int nodeID) {
565
    SemanticsNode._getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeLongPressed)?.handleSemanticLongPress();
Hixie's avatar
Hixie committed
566
  }
567 568

  @override
Hixie's avatar
Hixie committed
569
  void scrollLeft(int nodeID) {
570
    SemanticsNode._getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledHorizontally)?.handleSemanticScrollLeft();
Hixie's avatar
Hixie committed
571
  }
572 573

  @override
Hixie's avatar
Hixie committed
574
  void scrollRight(int nodeID) {
575
    SemanticsNode._getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledHorizontally)?.handleSemanticScrollRight();
Hixie's avatar
Hixie committed
576
  }
577 578

  @override
Hixie's avatar
Hixie committed
579
  void scrollUp(int nodeID) {
580
    SemanticsNode._getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledVertically)?.handleSemanticScrollUp();
Hixie's avatar
Hixie committed
581
  }
582 583

  @override
Hixie's avatar
Hixie committed
584
  void scrollDown(int nodeID) {
585
    SemanticsNode._getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledVertically)?.handleSemanticScrollDown();
Hixie's avatar
Hixie committed
586 587
  }
}