semantics.dart 19.4 KB
Newer Older
Hixie's avatar
Hixie committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
// 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;

import 'package:flutter/painting.dart';
import 'package:sky_services/semantics/semantics.mojom.dart' as mojom;
import 'package:vector_math/vector_math_64.dart';

import 'basic_types.dart';
import 'node.dart';

/// The type of function returned by [RenderObject.getSemanticAnnotators()].
///
/// These callbacks are invoked with the [SemanticsNode] object that
/// 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);

/// Interface for RenderObjects to implement when they want to support
/// being tapped, etc.
///
/// These handlers will only be called if the relevant flag is set
/// (e.g. handleSemanticTap() will only be called if canBeTapped is
/// true, handleSemanticScrollDown() will only be called if
/// canBeScrolledVertically is true, etc).
abstract class SemanticActionHandler {
  void handleSemanticTap() { }
  void handleSemanticLongPress() { }
  void handleSemanticScrollLeft() { }
  void handleSemanticScrollRight() { }
  void handleSemanticScrollUp() { }
  void handleSemanticScrollDown() { }
}

enum _SemanticFlags {
  mergeAllDescendantsIntoThisNode,
43
  inheritedMergeAllDescendantsIntoThisNode, // whether an ancestor had mergeAllDescendantsIntoThisNode set
Hixie's avatar
Hixie committed
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
  canBeTapped,
  canBeLongPressed,
  canBeScrolledHorizontally,
  canBeScrolledVertically,
  hasCheckedState,
  isChecked,
}

typedef bool SemanticsNodeVisitor(SemanticsNode node);

class SemanticsNode extends AbstractNode {
  SemanticsNode({
    SemanticActionHandler handler
  }) : _id = _generateNewId(),
       _actionHandler = handler;

  SemanticsNode.root({
61 62
    SemanticActionHandler handler,
    Object owner
Hixie's avatar
Hixie committed
63 64
  }) : _id = 0,
       _actionHandler = handler {
65
    attach(owner);
Hixie's avatar
Hixie committed
66 67 68 69 70 71 72 73 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 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
  }

  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

  Matrix4 get transform => _transform;
  Matrix4 _transform; // defaults to null, which we say means the identity matrix
  void set transform (Matrix4 value) {
    if (!MatrixUtils.matrixEquals(_transform, value)) {
      _transform = value;
      _markDirty();
    }
  }

  Rect get rect => _rect;
  Rect _rect = Rect.zero;
  void set rect (Rect value) {
    assert(value != null);
    if (_rect != value) {
      _rect = value;
      _markDirty();
    }
  }

  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];
  }

  bool get mergeAllDescendantsIntoThisNode => _flags[_SemanticFlags.mergeAllDescendantsIntoThisNode];
  void set mergeAllDescendantsIntoThisNode(bool value) => _setFlag(_SemanticFlags.mergeAllDescendantsIntoThisNode, value);

124 125 126 127 128
  bool get _inheritedMergeAllDescendantsIntoThisNode => _flags[_SemanticFlags.inheritedMergeAllDescendantsIntoThisNode];
  void set _inheritedMergeAllDescendantsIntoThisNode(bool value) => _setFlag(_SemanticFlags.inheritedMergeAllDescendantsIntoThisNode, value);

  bool get _shouldMergeAllDescendantsIntoThisNode => mergeAllDescendantsIntoThisNode || _inheritedMergeAllDescendantsIntoThisNode;

Hixie's avatar
Hixie committed
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
  bool get canBeTapped => _flags[_SemanticFlags.canBeTapped];
  void set canBeTapped(bool value) => _setFlag(_SemanticFlags.canBeTapped, value, needsHandler: true);

  bool get canBeLongPressed => _flags[_SemanticFlags.canBeLongPressed];
  void set canBeLongPressed(bool value) => _setFlag(_SemanticFlags.canBeLongPressed, value, needsHandler: true);

  bool get canBeScrolledHorizontally => _flags[_SemanticFlags.canBeScrolledHorizontally];
  void set canBeScrolledHorizontally(bool value) => _setFlag(_SemanticFlags.canBeScrolledHorizontally, value, needsHandler: true);

  bool get canBeScrolledVertically => _flags[_SemanticFlags.canBeScrolledVertically];
  void set canBeScrolledVertically(bool value) => _setFlag(_SemanticFlags.canBeScrolledVertically, value, needsHandler: true);

  bool get hasCheckedState => _flags[_SemanticFlags.hasCheckedState];
  void set hasCheckedState(bool value) => _setFlag(_SemanticFlags.hasCheckedState, value);

  bool get isChecked => _flags[_SemanticFlags.isChecked];
  void set isChecked(bool value) => _setFlag(_SemanticFlags.isChecked, value);

  String get label => _label;
  String _label = '';
  void set label(String value) {
    assert(value != null);
    if (_label != value) {
      _label = value;
      _markDirty();
    }
  }

  void reset() {
158
    bool hadInheritedMergeAllDescendantsIntoThisNode = _inheritedMergeAllDescendantsIntoThisNode;
Hixie's avatar
Hixie committed
159
    _flags.reset();
160 161
    if (hadInheritedMergeAllDescendantsIntoThisNode)
      _inheritedMergeAllDescendantsIntoThisNode = true;
Hixie's avatar
Hixie committed
162 163 164 165 166 167 168 169 170
    _label = '';
    _markDirty();
  }

  List<SemanticsNode> _newChildren;
  void addChildren(Iterable<SemanticsNode> children) {
    _newChildren ??= <SemanticsNode>[];
    _newChildren.addAll(children);
    // we do the asserts afterwards because children is an Iterable
171
    // and doing the asserts before would mean the behavior is
Hixie's avatar
Hixie committed
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
    // 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;
  bool get hasChildren => _children?.isNotEmpty ?? false;
  bool _dead = false;
  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
221
            // that our child was, in the last pass, a child of one of our
Hixie's avatar
Hixie committed
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
            // 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();
  }

241
  @override
Hixie's avatar
Hixie committed
242
  SemanticsNode get parent => super.parent;
243 244

  @override
Hixie's avatar
Hixie committed
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
  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>();

268
  @override
269 270
  void attach(Object owner) {
    super.attach(owner);
Hixie's avatar
Hixie committed
271 272 273
    assert(!_nodes.containsKey(_id));
    _nodes[_id] = this;
    _detachedNodes.remove(this);
274 275
    if (parent != null)
      _inheritedMergeAllDescendantsIntoThisNode = parent._shouldMergeAllDescendantsIntoThisNode;
Hixie's avatar
Hixie committed
276 277
    if (_children != null) {
      for (SemanticsNode child in _children)
278
        child.attach(owner);
Hixie's avatar
Hixie committed
279 280
    }
  }
281 282

  @override
Hixie's avatar
Hixie committed
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
  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>[];
329
      if (_shouldMergeAllDescendantsIntoThisNode) {
Hixie's avatar
Hixie committed
330 331 332 333 334 335 336 337 338
        _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;
339
          node._dirty = false;
Hixie's avatar
Hixie committed
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
          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;
  }

355 356 357 358
  static List<mojom.SemanticsListener> _listeners;
  static bool get hasListeners => _listeners != null && _listeners.length > 0;
  static VoidCallback onSemanticsEnabled; // set by the binding
  static void addListener(mojom.SemanticsListener listener) {
359 360
    if (!hasListeners) {
      assert(onSemanticsEnabled != null); // initialise the binding _before_ adding listeners
361
      onSemanticsEnabled();
362
    }
363 364 365 366 367 368
    _listeners ??= <mojom.SemanticsListener>[];
    _listeners.add(listener);
  }

  static void sendSemanticsTree() {
    assert(hasListeners);
Hixie's avatar
Hixie committed
369 370 371 372 373 374 375 376 377 378 379 380 381 382
    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);
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
      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
          }
        }
408
      }
Hixie's avatar
Hixie committed
409
      assert(_dirtyNodes[index] == node); // make sure nothing went in front of us in the list
410
    }
Hixie's avatar
Hixie committed
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
    _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());
    }
428 429
    for (mojom.SemanticsListener listener in _listeners)
      listener.updateSemanticsTree(updatedNodes);
Hixie's avatar
Hixie committed
430 431 432 433 434 435
    _dirtyNodes.clear();
  }

  static SemanticActionHandler getSemanticActionHandlerForId(int id, { _SemanticFlags neededFlag }) {
    assert(neededFlag != null);
    SemanticsNode result = _nodes[id];
436
    if (result != null && result._shouldMergeAllDescendantsIntoThisNode && !result._canHandle(neededFlag)) {
Hixie's avatar
Hixie committed
437 438 439 440 441 442 443 444 445 446 447 448 449
      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;
  }

450
  @override
Hixie's avatar
Hixie committed
451 452
  String toString() {
    return '$runtimeType($_id'
453 454
             '${_dirty ? " (${ _dirtyNodes.contains(this) ? 'dirty' : 'STALE' })" : ""}'
             '${_shouldMergeAllDescendantsIntoThisNode ? " (leaf merge)" : ""}'
Hixie's avatar
Hixie committed
455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
             '; $rect'
             '${wasAffectedByClip ? " (clipped)" : ""}'
             '${canBeTapped ? "; canBeTapped" : ""}'
             '${canBeLongPressed ? "; canBeLongPressed" : ""}'
             '${canBeScrolledHorizontally ? "; canBeScrolledHorizontally" : ""}'
             '${canBeScrolledVertically ? "; canBeScrolledVertically" : ""}'
             '${hasCheckedState ? (isChecked ? "; checked" : "; unchecked") : ""}'
             '${label != "" ? "; \"$label\"" : ""}'
           ')';
  }

  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;
  }
}

class SemanticsServer extends mojom.SemanticsServer {
480
  @override
481 482
  void addSemanticsListener(mojom.SemanticsListenerProxy listener) {
    SemanticsNode.addListener(listener.ptr);
483
  }
484 485

  @override
Hixie's avatar
Hixie committed
486 487 488
  void tap(int nodeID) {
    SemanticsNode.getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeTapped)?.handleSemanticTap();
  }
489 490

  @override
Hixie's avatar
Hixie committed
491 492 493
  void longPress(int nodeID) {
    SemanticsNode.getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeLongPressed)?.handleSemanticLongPress();
  }
494 495

  @override
Hixie's avatar
Hixie committed
496 497 498
  void scrollLeft(int nodeID) {
    SemanticsNode.getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledHorizontally)?.handleSemanticScrollLeft();
  }
499 500

  @override
Hixie's avatar
Hixie committed
501 502 503
  void scrollRight(int nodeID) {
    SemanticsNode.getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledHorizontally)?.handleSemanticScrollRight();
  }
504 505

  @override
Hixie's avatar
Hixie committed
506 507 508
  void scrollUp(int nodeID) {
    SemanticsNode.getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledVertically)?.handleSemanticScrollUp();
  }
509 510

  @override
Hixie's avatar
Hixie committed
511 512 513 514
  void scrollDown(int nodeID) {
    SemanticsNode.getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledVertically)?.handleSemanticScrollDown();
  }
}