Mozart: Add hit testing interfaces.
Define Mojo interfaces for hit testing functionality modeled after
Flutter's hit test traversal process.
Each node can supply a hit test behavior to control how hit testing
should occur within its bounds. The key properties are visibility,
pruning, and a hit rect.
Visibility is one of |INVISIBLE|, |TRANSLUCENT|, and |OPAQUE|.
This property determines whether a node can be hit and its effect
upon subsequent traversals once hit.
Pruning determines whether children of a node will be hit tested
at all. This can be used to suppress hit testing for a subgraph.
And the hit rect is just a finer specification of the hittable region
within a node's bounds. In the future, this will likely expand
to describe more complex regions.
The results of a hit test are described structurally as a tree which
records the event dispatch order for nodes which are hit as well as
the path by which they were reached.
In general, the dispatch order goes from the most specific hit node
outwards which can be significant for certain operations.
BUG=
R=abarth@google.com
Review URL: https://codereview.chromium.org/1748363002 .
diff --git a/mojo/dart/packages/mojo_services/lib/mojo/gfx/composition/hit_tests.mojom.dart b/mojo/dart/packages/mojo_services/lib/mojo/gfx/composition/hit_tests.mojom.dart
index acf982b..59bcd8b 100644
--- a/mojo/dart/packages/mojo_services/lib/mojo/gfx/composition/hit_tests.mojom.dart
+++ b/mojo/dart/packages/mojo_services/lib/mojo/gfx/composition/hit_tests.mojom.dart
@@ -9,23 +9,81 @@
import 'package:mojo/mojo/bindings/types/service_describer.mojom.dart' as service_describer;
import 'package:mojo_services/mojo/geometry.mojom.dart' as geometry_mojom;
import 'package:mojo_services/mojo/gfx/composition/scene_token.mojom.dart' as scene_token_mojom;
-const int kHitIdNone = 0;
-class Hit extends bindings.Struct {
- static const List<bindings.StructDataHeader> kVersions = const [
- const bindings.StructDataHeader(40, 0)
+class HitTestBehaviorVisibility extends bindings.MojoEnum {
+ static const HitTestBehaviorVisibility opaque = const HitTestBehaviorVisibility._(0);
+ static const HitTestBehaviorVisibility translucent = const HitTestBehaviorVisibility._(1);
+ static const HitTestBehaviorVisibility invisible = const HitTestBehaviorVisibility._(2);
+
+ const HitTestBehaviorVisibility._(int v) : super(v);
+
+ static const Map<String, HitTestBehaviorVisibility> valuesMap = const {
+ "opaque": opaque,
+ "translucent": translucent,
+ "invisible": invisible,
+ };
+ static const List<HitTestBehaviorVisibility> values = const [
+ opaque,
+ translucent,
+ invisible,
];
- scene_token_mojom.SceneToken sceneToken = null;
- int sceneVersion = 0;
- int nodeId = 0;
- int hitId = 0;
- geometry_mojom.Point intersection = null;
- Hit() : super(kVersions.last.size);
+ static HitTestBehaviorVisibility valueOf(String name) => valuesMap[name];
- static Hit deserialize(bindings.Message message) {
+ factory HitTestBehaviorVisibility(int v) {
+ switch (v) {
+ case 0:
+ return HitTestBehaviorVisibility.opaque;
+ case 1:
+ return HitTestBehaviorVisibility.translucent;
+ case 2:
+ return HitTestBehaviorVisibility.invisible;
+ default:
+ return null;
+ }
+ }
+
+ static HitTestBehaviorVisibility decode(bindings.Decoder decoder0, int offset) {
+ int v = decoder0.decodeUint32(offset);
+ HitTestBehaviorVisibility result = new HitTestBehaviorVisibility(v);
+ if (result == null) {
+ throw new bindings.MojoCodecError(
+ 'Bad value $v for enum HitTestBehaviorVisibility.');
+ }
+ return result;
+ }
+
+ String toString() {
+ switch(this) {
+ case opaque:
+ return 'HitTestBehaviorVisibility.opaque';
+ case translucent:
+ return 'HitTestBehaviorVisibility.translucent';
+ case invisible:
+ return 'HitTestBehaviorVisibility.invisible';
+ default:
+ return null;
+ }
+ }
+
+ int toJson() => mojoEnumValue;
+}
+
+
+
+class HitTestBehavior extends bindings.Struct {
+ static const List<bindings.StructDataHeader> kVersions = const [
+ const bindings.StructDataHeader(24, 0)
+ ];
+ HitTestBehaviorVisibility visibility = new HitTestBehaviorVisibility(0);
+ bool prune = false;
+ geometry_mojom.Rect hitRect = null;
+
+ HitTestBehavior() : super(kVersions.last.size);
+
+ static HitTestBehavior deserialize(bindings.Message message) {
var decoder = new bindings.Decoder(message);
var result = decode(decoder);
if (decoder.excessHandles != null) {
@@ -34,11 +92,11 @@
return result;
}
- static Hit decode(bindings.Decoder decoder0) {
+ static HitTestBehavior decode(bindings.Decoder decoder0) {
if (decoder0 == null) {
return null;
}
- Hit result = new Hit();
+ HitTestBehavior result = new HitTestBehavior();
var mainDataHeader = decoder0.decodeStructDataHeader();
if (mainDataHeader.version <= kVersions.last.version) {
@@ -60,25 +118,20 @@
}
if (mainDataHeader.version >= 0) {
- var decoder1 = decoder0.decodePointer(8, false);
- result.sceneToken = scene_token_mojom.SceneToken.decode(decoder1);
+ result.visibility = HitTestBehaviorVisibility.decode(decoder0, 8);
+ if (result.visibility == null) {
+ throw new bindings.MojoCodecError(
+ 'Trying to decode null union for non-nullable HitTestBehaviorVisibility.');
+ }
}
if (mainDataHeader.version >= 0) {
- result.sceneVersion = decoder0.decodeUint32(16);
+ result.prune = decoder0.decodeBool(12, 0);
}
if (mainDataHeader.version >= 0) {
- result.nodeId = decoder0.decodeUint32(20);
- }
- if (mainDataHeader.version >= 0) {
-
- result.hitId = decoder0.decodeUint32(24);
- }
- if (mainDataHeader.version >= 0) {
-
- var decoder1 = decoder0.decodePointer(32, false);
- result.intersection = geometry_mojom.Point.decode(decoder1);
+ var decoder1 = decoder0.decodePointer(16, true);
+ result.hitRect = geometry_mojom.Rect.decode(decoder1);
}
return result;
}
@@ -86,58 +139,40 @@
void encode(bindings.Encoder encoder) {
var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
try {
- encoder0.encodeStruct(sceneToken, 8, false);
+ encoder0.encodeEnum(visibility, 8);
} on bindings.MojoCodecError catch(e) {
e.message = "Error encountered while encoding field "
- "sceneToken of struct Hit: $e";
+ "visibility of struct HitTestBehavior: $e";
rethrow;
}
try {
- encoder0.encodeUint32(sceneVersion, 16);
+ encoder0.encodeBool(prune, 12, 0);
} on bindings.MojoCodecError catch(e) {
e.message = "Error encountered while encoding field "
- "sceneVersion of struct Hit: $e";
+ "prune of struct HitTestBehavior: $e";
rethrow;
}
try {
- encoder0.encodeUint32(nodeId, 20);
+ encoder0.encodeStruct(hitRect, 16, true);
} on bindings.MojoCodecError catch(e) {
e.message = "Error encountered while encoding field "
- "nodeId of struct Hit: $e";
- rethrow;
- }
- try {
- encoder0.encodeUint32(hitId, 24);
- } on bindings.MojoCodecError catch(e) {
- e.message = "Error encountered while encoding field "
- "hitId of struct Hit: $e";
- rethrow;
- }
- try {
- encoder0.encodeStruct(intersection, 32, false);
- } on bindings.MojoCodecError catch(e) {
- e.message = "Error encountered while encoding field "
- "intersection of struct Hit: $e";
+ "hitRect of struct HitTestBehavior: $e";
rethrow;
}
}
String toString() {
- return "Hit("
- "sceneToken: $sceneToken" ", "
- "sceneVersion: $sceneVersion" ", "
- "nodeId: $nodeId" ", "
- "hitId: $hitId" ", "
- "intersection: $intersection" ")";
+ return "HitTestBehavior("
+ "visibility: $visibility" ", "
+ "prune: $prune" ", "
+ "hitRect: $hitRect" ")";
}
Map toJson() {
Map map = new Map();
- map["sceneToken"] = sceneToken;
- map["sceneVersion"] = sceneVersion;
- map["nodeId"] = nodeId;
- map["hitId"] = hitId;
- map["intersection"] = intersection;
+ map["visibility"] = visibility;
+ map["prune"] = prune;
+ map["hitRect"] = hitRect;
return map;
}
}
@@ -149,7 +184,7 @@
static const List<bindings.StructDataHeader> kVersions = const [
const bindings.StructDataHeader(16, 0)
];
- List<Hit> hits = null;
+ SceneHit root = null;
HitTestResult() : super(kVersions.last.size);
@@ -188,14 +223,103 @@
}
if (mainDataHeader.version >= 0) {
+ var decoder1 = decoder0.decodePointer(8, true);
+ result.root = SceneHit.decode(decoder1);
+ }
+ return result;
+ }
+
+ void encode(bindings.Encoder encoder) {
+ var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
+ try {
+ encoder0.encodeStruct(root, 8, true);
+ } on bindings.MojoCodecError catch(e) {
+ e.message = "Error encountered while encoding field "
+ "root of struct HitTestResult: $e";
+ rethrow;
+ }
+ }
+
+ String toString() {
+ return "HitTestResult("
+ "root: $root" ")";
+ }
+
+ Map toJson() {
+ Map map = new Map();
+ map["root"] = root;
+ return map;
+ }
+}
+
+
+
+
+class SceneHit extends bindings.Struct {
+ static const List<bindings.StructDataHeader> kVersions = const [
+ const bindings.StructDataHeader(32, 0)
+ ];
+ scene_token_mojom.SceneToken sceneToken = null;
+ int sceneVersion = 0;
+ List<Hit> hits = null;
+
+ SceneHit() : super(kVersions.last.size);
+
+ static SceneHit deserialize(bindings.Message message) {
+ var decoder = new bindings.Decoder(message);
+ var result = decode(decoder);
+ if (decoder.excessHandles != null) {
+ decoder.excessHandles.forEach((h) => h.close());
+ }
+ return result;
+ }
+
+ static SceneHit decode(bindings.Decoder decoder0) {
+ if (decoder0 == null) {
+ return null;
+ }
+ SceneHit result = new SceneHit();
+
+ var mainDataHeader = decoder0.decodeStructDataHeader();
+ if (mainDataHeader.version <= kVersions.last.version) {
+ // Scan in reverse order to optimize for more recent versions.
+ for (int i = kVersions.length - 1; i >= 0; --i) {
+ if (mainDataHeader.version >= kVersions[i].version) {
+ if (mainDataHeader.size == kVersions[i].size) {
+ // Found a match.
+ break;
+ }
+ throw new bindings.MojoCodecError(
+ 'Header size doesn\'t correspond to known version size.');
+ }
+ }
+ } else if (mainDataHeader.size < kVersions.last.size) {
+ throw new bindings.MojoCodecError(
+ 'Message newer than the last known version cannot be shorter than '
+ 'required by the last known version.');
+ }
+ if (mainDataHeader.version >= 0) {
+
var decoder1 = decoder0.decodePointer(8, false);
+ result.sceneToken = scene_token_mojom.SceneToken.decode(decoder1);
+ }
+ if (mainDataHeader.version >= 0) {
+
+ result.sceneVersion = decoder0.decodeUint32(16);
+ }
+ if (mainDataHeader.version >= 0) {
+
+ var decoder1 = decoder0.decodePointer(24, false);
{
- var si1 = decoder1.decodeDataHeaderForPointerArray(bindings.kUnspecifiedArrayLength);
+ var si1 = decoder1.decodeDataHeaderForUnionArray(bindings.kUnspecifiedArrayLength);
result.hits = new List<Hit>(si1.numElements);
for (int i1 = 0; i1 < si1.numElements; ++i1) {
- var decoder2 = decoder1.decodePointer(bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i1, false);
- result.hits[i1] = Hit.decode(decoder2);
+ result.hits[i1] = Hit.decode(decoder1, bindings.ArrayDataHeader.kHeaderSize + bindings.kUnionSize * i1);
+ if (result.hits[i1] == null) {
+ throw new bindings.MojoCodecError(
+ 'Trying to decode null union for non-nullable Hit.');
+ }
}
}
}
@@ -205,28 +329,46 @@
void encode(bindings.Encoder encoder) {
var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
try {
+ encoder0.encodeStruct(sceneToken, 8, false);
+ } on bindings.MojoCodecError catch(e) {
+ e.message = "Error encountered while encoding field "
+ "sceneToken of struct SceneHit: $e";
+ rethrow;
+ }
+ try {
+ encoder0.encodeUint32(sceneVersion, 16);
+ } on bindings.MojoCodecError catch(e) {
+ e.message = "Error encountered while encoding field "
+ "sceneVersion of struct SceneHit: $e";
+ rethrow;
+ }
+ try {
if (hits == null) {
- encoder0.encodeNullPointer(8, false);
+ encoder0.encodeNullPointer(24, false);
} else {
- var encoder1 = encoder0.encodePointerArray(hits.length, 8, bindings.kUnspecifiedArrayLength);
+ var encoder1 = encoder0.encodeUnionArray(hits.length, 24, bindings.kUnspecifiedArrayLength);
for (int i0 = 0; i0 < hits.length; ++i0) {
- encoder1.encodeStruct(hits[i0], bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i0, false);
+ encoder1.encodeUnion(hits[i0], bindings.ArrayDataHeader.kHeaderSize + bindings.kUnionSize * i0, false);
}
}
} on bindings.MojoCodecError catch(e) {
e.message = "Error encountered while encoding field "
- "hits of struct HitTestResult: $e";
+ "hits of struct SceneHit: $e";
rethrow;
}
}
String toString() {
- return "HitTestResult("
+ return "SceneHit("
+ "sceneToken: $sceneToken" ", "
+ "sceneVersion: $sceneVersion" ", "
"hits: $hits" ")";
}
Map toJson() {
Map map = new Map();
+ map["sceneToken"] = sceneToken;
+ map["sceneVersion"] = sceneVersion;
map["hits"] = hits;
return map;
}
@@ -235,6 +377,95 @@
+class NodeHit extends bindings.Struct {
+ static const List<bindings.StructDataHeader> kVersions = const [
+ const bindings.StructDataHeader(24, 0)
+ ];
+ int nodeId = 0;
+ geometry_mojom.Point intersection = null;
+
+ NodeHit() : super(kVersions.last.size);
+
+ static NodeHit deserialize(bindings.Message message) {
+ var decoder = new bindings.Decoder(message);
+ var result = decode(decoder);
+ if (decoder.excessHandles != null) {
+ decoder.excessHandles.forEach((h) => h.close());
+ }
+ return result;
+ }
+
+ static NodeHit decode(bindings.Decoder decoder0) {
+ if (decoder0 == null) {
+ return null;
+ }
+ NodeHit result = new NodeHit();
+
+ var mainDataHeader = decoder0.decodeStructDataHeader();
+ if (mainDataHeader.version <= kVersions.last.version) {
+ // Scan in reverse order to optimize for more recent versions.
+ for (int i = kVersions.length - 1; i >= 0; --i) {
+ if (mainDataHeader.version >= kVersions[i].version) {
+ if (mainDataHeader.size == kVersions[i].size) {
+ // Found a match.
+ break;
+ }
+ throw new bindings.MojoCodecError(
+ 'Header size doesn\'t correspond to known version size.');
+ }
+ }
+ } else if (mainDataHeader.size < kVersions.last.size) {
+ throw new bindings.MojoCodecError(
+ 'Message newer than the last known version cannot be shorter than '
+ 'required by the last known version.');
+ }
+ if (mainDataHeader.version >= 0) {
+
+ result.nodeId = decoder0.decodeUint32(8);
+ }
+ if (mainDataHeader.version >= 0) {
+
+ var decoder1 = decoder0.decodePointer(16, false);
+ result.intersection = geometry_mojom.Point.decode(decoder1);
+ }
+ return result;
+ }
+
+ void encode(bindings.Encoder encoder) {
+ var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
+ try {
+ encoder0.encodeUint32(nodeId, 8);
+ } on bindings.MojoCodecError catch(e) {
+ e.message = "Error encountered while encoding field "
+ "nodeId of struct NodeHit: $e";
+ rethrow;
+ }
+ try {
+ encoder0.encodeStruct(intersection, 16, false);
+ } on bindings.MojoCodecError catch(e) {
+ e.message = "Error encountered while encoding field "
+ "intersection of struct NodeHit: $e";
+ rethrow;
+ }
+ }
+
+ String toString() {
+ return "NodeHit("
+ "nodeId: $nodeId" ", "
+ "intersection: $intersection" ")";
+ }
+
+ Map toJson() {
+ Map map = new Map();
+ map["nodeId"] = nodeId;
+ map["intersection"] = intersection;
+ return map;
+ }
+}
+
+
+
+
class _HitTesterHitTestParams extends bindings.Struct {
static const List<bindings.StructDataHeader> kVersions = const [
const bindings.StructDataHeader(16, 0)
@@ -385,6 +616,113 @@
+
+enum HitTag {
+ scene,
+ node,
+ unknown
+}
+
+class Hit extends bindings.Union {
+ static final _tag_to_int = const {
+ HitTag.scene: 0,
+ HitTag.node: 1,
+ };
+
+ static final _int_to_tag = const {
+ 0: HitTag.scene,
+ 1: HitTag.node,
+ };
+
+ var _data;
+ HitTag _tag = HitTag.unknown;
+
+ HitTag get tag => _tag;
+ SceneHit get scene {
+ if (_tag != HitTag.scene) {
+ throw new bindings.UnsetUnionTagError(_tag, HitTag.scene);
+ }
+ return _data;
+ }
+
+ set scene(SceneHit value) {
+ _tag = HitTag.scene;
+ _data = value;
+ }
+ NodeHit get node {
+ if (_tag != HitTag.node) {
+ throw new bindings.UnsetUnionTagError(_tag, HitTag.node);
+ }
+ return _data;
+ }
+
+ set node(NodeHit value) {
+ _tag = HitTag.node;
+ _data = value;
+ }
+
+ static Hit decode(bindings.Decoder decoder0, int offset) {
+ int size = decoder0.decodeUint32(offset);
+ if (size == 0) {
+ return null;
+ }
+ Hit result = new Hit();
+
+
+ HitTag tag = _int_to_tag[decoder0.decodeUint32(offset + 4)];
+ switch (tag) {
+ case HitTag.scene:
+
+ var decoder1 = decoder0.decodePointer(offset + 8, false);
+ result.scene = SceneHit.decode(decoder1);
+ break;
+ case HitTag.node:
+
+ var decoder1 = decoder0.decodePointer(offset + 8, false);
+ result.node = NodeHit.decode(decoder1);
+ break;
+ default:
+ throw new bindings.MojoCodecError("Bad union tag: $tag");
+ }
+
+ return result;
+ }
+
+ void encode(bindings.Encoder encoder0, int offset) {
+
+ encoder0.encodeUint32(16, offset);
+ encoder0.encodeUint32(_tag_to_int[_tag], offset + 4);
+ switch (_tag) {
+ case HitTag.scene:
+ encoder0.encodeStruct(scene, offset + 8, false);
+ break;
+ case HitTag.node:
+ encoder0.encodeStruct(node, offset + 8, false);
+ break;
+ default:
+ throw new bindings.MojoCodecError("Bad union tag: $_tag");
+ }
+ }
+
+ String toString() {
+ String result = "Hit(";
+ switch (_tag) {
+ case HitTag.scene:
+ result += "scene";
+ break;
+ case HitTag.node:
+ result += "node";
+ break;
+ default:
+ result += "unknown";
+ }
+ result += ": $_data)";
+ return result;
+ }
+}
+
+
+
const int _HitTester_hitTestName = 0;
diff --git a/mojo/dart/packages/mojo_services/lib/mojo/gfx/composition/nodes.mojom.dart b/mojo/dart/packages/mojo_services/lib/mojo/gfx/composition/nodes.mojom.dart
index 74ccb1d..cbf2277 100644
--- a/mojo/dart/packages/mojo_services/lib/mojo/gfx/composition/nodes.mojom.dart
+++ b/mojo/dart/packages/mojo_services/lib/mojo/gfx/composition/nodes.mojom.dart
@@ -73,12 +73,12 @@
class Node extends bindings.Struct {
static const List<bindings.StructDataHeader> kVersions = const [
- const bindings.StructDataHeader(56, 0)
+ const bindings.StructDataHeader(64, 0)
];
geometry_mojom.Transform contentTransform = null;
geometry_mojom.Rect contentClip = null;
- int hitId = 0;
NodeCombinator combinator = new NodeCombinator(0);
+ hit_tests_mojom.HitTestBehavior hitTestBehavior = null;
List<int> childNodeIds = null;
NodeOp op = null;
@@ -129,11 +129,7 @@
}
if (mainDataHeader.version >= 0) {
- result.hitId = decoder0.decodeUint32(24);
- }
- if (mainDataHeader.version >= 0) {
-
- result.combinator = NodeCombinator.decode(decoder0, 28);
+ result.combinator = NodeCombinator.decode(decoder0, 24);
if (result.combinator == null) {
throw new bindings.MojoCodecError(
'Trying to decode null union for non-nullable NodeCombinator.');
@@ -141,11 +137,16 @@
}
if (mainDataHeader.version >= 0) {
- result.childNodeIds = decoder0.decodeUint32Array(32, bindings.kArrayNullable, bindings.kUnspecifiedArrayLength);
+ var decoder1 = decoder0.decodePointer(32, true);
+ result.hitTestBehavior = hit_tests_mojom.HitTestBehavior.decode(decoder1);
}
if (mainDataHeader.version >= 0) {
- result.op = NodeOp.decode(decoder0, 40);
+ result.childNodeIds = decoder0.decodeUint32Array(40, bindings.kArrayNullable, bindings.kUnspecifiedArrayLength);
+ }
+ if (mainDataHeader.version >= 0) {
+
+ result.op = NodeOp.decode(decoder0, 48);
}
return result;
}
@@ -167,28 +168,28 @@
rethrow;
}
try {
- encoder0.encodeUint32(hitId, 24);
- } on bindings.MojoCodecError catch(e) {
- e.message = "Error encountered while encoding field "
- "hitId of struct Node: $e";
- rethrow;
- }
- try {
- encoder0.encodeEnum(combinator, 28);
+ encoder0.encodeEnum(combinator, 24);
} on bindings.MojoCodecError catch(e) {
e.message = "Error encountered while encoding field "
"combinator of struct Node: $e";
rethrow;
}
try {
- encoder0.encodeUint32Array(childNodeIds, 32, bindings.kArrayNullable, bindings.kUnspecifiedArrayLength);
+ encoder0.encodeStruct(hitTestBehavior, 32, true);
+ } on bindings.MojoCodecError catch(e) {
+ e.message = "Error encountered while encoding field "
+ "hitTestBehavior of struct Node: $e";
+ rethrow;
+ }
+ try {
+ encoder0.encodeUint32Array(childNodeIds, 40, bindings.kArrayNullable, bindings.kUnspecifiedArrayLength);
} on bindings.MojoCodecError catch(e) {
e.message = "Error encountered while encoding field "
"childNodeIds of struct Node: $e";
rethrow;
}
try {
- encoder0.encodeUnion(op, 40, true);
+ encoder0.encodeUnion(op, 48, true);
} on bindings.MojoCodecError catch(e) {
e.message = "Error encountered while encoding field "
"op of struct Node: $e";
@@ -200,8 +201,8 @@
return "Node("
"contentTransform: $contentTransform" ", "
"contentClip: $contentClip" ", "
- "hitId: $hitId" ", "
"combinator: $combinator" ", "
+ "hitTestBehavior: $hitTestBehavior" ", "
"childNodeIds: $childNodeIds" ", "
"op: $op" ")";
}
@@ -210,8 +211,8 @@
Map map = new Map();
map["contentTransform"] = contentTransform;
map["contentClip"] = contentClip;
- map["hitId"] = hitId;
map["combinator"] = combinator;
+ map["hitTestBehavior"] = hitTestBehavior;
map["childNodeIds"] = childNodeIds;
map["op"] = op;
return map;
diff --git a/mojo/services/gfx/composition/cpp/formatting.cc b/mojo/services/gfx/composition/cpp/formatting.cc
index 6aefebe..4666619 100644
--- a/mojo/services/gfx/composition/cpp/formatting.cc
+++ b/mojo/services/gfx/composition/cpp/formatting.cc
@@ -91,8 +91,8 @@
d.Append() << "content_transform=" << value.content_transform;
if (value.content_clip)
d.Append() << "content_clip=" << value.content_clip;
- if (value.hit_id != mojo::gfx::composition::kHitIdNone)
- d.Append() << "hit_id=" << value.hit_id;
+ if (value.hit_test_behavior)
+ d.Append() << "hit_test_behavior=" << value.hit_test_behavior;
if (value.op)
d.Append() << "op=" << value.op;
d.Append() << "combinator=" << &value.combinator;
@@ -179,6 +179,58 @@
<< ", frame_deadline=" << value.frame_deadline << "}";
}
+std::ostream& operator<<(std::ostream& os,
+ const mojo::gfx::composition::HitTestBehavior& value) {
+ return os << "{visibility=" << &value.visibility << ", prune" << value.prune
+ << ", hit_rect=" << value.hit_rect << "}";
+}
+
+std::ostream& operator<<(
+ std::ostream& os,
+ const mojo::gfx::composition::HitTestBehavior::Visibility* value) {
+ switch (*value) {
+ case mojo::gfx::composition::HitTestBehavior::Visibility::OPAQUE:
+ return os << "OPAQUE";
+ case mojo::gfx::composition::HitTestBehavior::Visibility::TRANSLUCENT:
+ return os << "TRANSLUCENT";
+ case mojo::gfx::composition::HitTestBehavior::Visibility::INVISIBLE:
+ return os << "INVISIBLE";
+ default:
+ return os << "???";
+ }
+}
+
+std::ostream& operator<<(std::ostream& os,
+ const mojo::gfx::composition::HitTestResult& value) {
+ return os << "{root=" << value.root << "}";
+}
+
+std::ostream& operator<<(std::ostream& os,
+ const mojo::gfx::composition::Hit& value) {
+ os << "{";
+ if (value.is_scene()) {
+ os << "scene=" << value.get_scene();
+ } else if (value.is_node()) {
+ os << "node=" << value.get_node();
+ } else {
+ os << "???";
+ }
+ return os << "}";
+}
+
+std::ostream& operator<<(std::ostream& os,
+ const mojo::gfx::composition::SceneHit& value) {
+ return os << "{scene_token=" << value.scene_token
+ << ", scene_version=" << value.scene_version
+ << ", hits=" << value.hits << "}";
+}
+
+std::ostream& operator<<(std::ostream& os,
+ const mojo::gfx::composition::NodeHit& value) {
+ return os << "{node_id=" << value.node_id
+ << ", intersection=" << value.intersection << "}";
+}
+
} // namespace composition
} // namespace gfx
} // namespace mojo
diff --git a/mojo/services/gfx/composition/cpp/formatting.h b/mojo/services/gfx/composition/cpp/formatting.h
index 0f4df77..baf8eec 100644
--- a/mojo/services/gfx/composition/cpp/formatting.h
+++ b/mojo/services/gfx/composition/cpp/formatting.h
@@ -57,6 +57,20 @@
std::ostream& operator<<(std::ostream& os,
const mojo::gfx::composition::FrameInfo& value);
+std::ostream& operator<<(std::ostream& os,
+ const mojo::gfx::composition::HitTestBehavior& value);
+std::ostream& operator<<(
+ std::ostream& os,
+ const mojo::gfx::composition::HitTestBehavior::Visibility* value);
+std::ostream& operator<<(std::ostream& os,
+ const mojo::gfx::composition::HitTestResult& value);
+std::ostream& operator<<(std::ostream& os,
+ const mojo::gfx::composition::Hit& value);
+std::ostream& operator<<(std::ostream& os,
+ const mojo::gfx::composition::SceneHit& value);
+std::ostream& operator<<(std::ostream& os,
+ const mojo::gfx::composition::NodeHit& value);
+
} // namespace composition
} // namespace gfx
} // namespace mojo
diff --git a/mojo/services/gfx/composition/interfaces/hit_tests.mojom b/mojo/services/gfx/composition/interfaces/hit_tests.mojom
index d18d868..49c055b 100644
--- a/mojo/services/gfx/composition/interfaces/hit_tests.mojom
+++ b/mojo/services/gfx/composition/interfaces/hit_tests.mojom
@@ -8,42 +8,100 @@
import "mojo/services/geometry/interfaces/geometry.mojom";
import "mojo/services/gfx/composition/interfaces/scene_token.mojom";
-// Indicates that hit testing is not needed for a given node.
-const uint32 kHitIdNone = 0;
+// Determines how a node behaves during hit testing.
+//
+// See |Node| for more details of the hit testing traversal process.
+struct HitTestBehavior {
+ // The visibility specifies how a node itself is hit and its effect on
+ // the hit testing traversal.
+ enum Visibility {
+ // The node can be hit and prevents other targets visually behind the
+ // node from being hit. This is the default.
+ //
+ // Opaque targets are useful for UI elements like buttons which cover and
+ // block interaction with content visually behind them.
+ OPAQUE,
-// Provides information about the point of intersection of a hit test with
-// a node in a scene graph.
-struct Hit {
- // The scene token of the scene which was hit.
- SceneToken scene_token;
+ // The node can be hit and allows other targets visually behind the node
+ // to also be hit.
+ //
+ // Translucent targets are useful for UI elements like dimissed drawers
+ // which may not obscure content visually behind them but which do need
+ // to intercept gestures in that area (perhaps to reveal themselves).
+ TRANSLUCENT,
- // The version of the scene which was consulted as part of evaluating the
- // hit test.
- uint32 scene_version;
+ // The node cannot be hit.
+ //
+ // Invisible targets are useful for explicitly suppressing hit testing
+ // for a node and its children when combined with the |prune| flag.
+ INVISIBLE,
+ };
- // The node id of the node which was hit.
- uint32 node_id;
+ // Specifies the visibility of the node for hit testing purposes.
+ // The default is opaque.
+ Visibility visibility = Visibility.OPAQUE;
- // The hit test id of the node which was hit.
- uint32 hit_id;
+ // When set to true, prevents a node's children from being hit tested.
+ bool prune = false;
- // The coordinates of the hit within the node's content space.
- mojo.Point intersection;
-};
-
-// The result of a hit test operation.
-struct HitTestResult {
- // A sorted list of hits in dispatch order from the top-most hit node
- // to the nodes underneath it and containing it, or an empty array if none.
- // Omits nodes for which the hit_id was |kHitIdNone|.
- array<Hit> hits;
+ // The rectangle within the node's content space to test for hits.
+ // If null, the node's entire clip region is tested for hits.
+ //
+ // TODO(jeffbrown): Support more complex hit test regions and masks.
+ mojo.Rect? hit_rect;
};
// A hit testing service for scene graphs.
interface HitTester {
// Performs a hit test on the specified point.
//
- // TODO(jeffbrown): Specify a timestamp to allow for hit-tests of geometry
- // as it appeared in the recent past.
+ // TODO(jeffbrown): Specify a presentation timestamp to allow for
+ // hit-tests of geometry as it appeared to the user in the recent past.
HitTest(mojo.Point point) => (HitTestResult result);
};
+
+// The result of a hit test operation.
+//
+// Hit test results form a hit tree with information about the nodes which
+// were hit and the scenes which contain them. The structure of the hit
+// tree reflects the path that the scene graph was traversed during the
+// hit test.
+//
+// To walk the hit nodes in dispatch order, perform a post-order traversal
+// of the hit tree starting at |root| and processing children before their
+// parents.
+struct HitTestResult {
+ // A tree of hits beginning at the root scene, or null if nothing was hit.
+ SceneHit? root;
+};
+
+// An element in a hit tree.
+// This either contains a reference to another scene which contains hits or
+// to a specific node within the containing scene which was hit.
+union Hit {
+ SceneHit scene;
+ NodeHit node;
+};
+
+// Describes a collection of hits within a scene.
+struct SceneHit {
+ // The scene token of the scene which contains hits.
+ SceneToken scene_token;
+
+ // The version of the scene which was consulted at the time the hit test
+ // was performed.
+ uint32 scene_version;
+
+ // The array of hits within this scene, in dispatch order.
+ // This list always contains at least one entry.
+ array<Hit> hits;
+};
+
+// Describes the point of intersection of a hit test with a node.
+struct NodeHit {
+ // The node id of the node which was hit.
+ uint32 node_id;
+
+ // The coordinates of the hit within the node's content space.
+ mojo.Point intersection;
+};
diff --git a/mojo/services/gfx/composition/interfaces/nodes.mojom b/mojo/services/gfx/composition/interfaces/nodes.mojom
index 602ec44..31420a9 100644
--- a/mojo/services/gfx/composition/interfaces/nodes.mojom
+++ b/mojo/services/gfx/composition/interfaces/nodes.mojom
@@ -55,6 +55,46 @@
// missing content for an earlier version of that content or for a placeholder
// if not available.
//
+// HIT TESTING
+//
+// Hit testing is the process of determining which nodes within a scene graph
+// should be responsible for handling events which occur within their visual
+// space on the screen.
+//
+// For example, when the user touches objects on a touch screen, the input
+// system asks the compositor to performs a hit test at the contact point to
+// find the nodes which represent the objects the user wants to interact with.
+// The result of the hit test is a list of nodes, in dispatch order, which
+// have asked to participate in handling events related to the contact point.
+//
+// Nodes may be opaque, translucent, or invisible to the hit testing
+// process depending on whether they prevent or allow targets visually
+// behind them from being hit and whether they can actually be hit,
+// as specified by |HitTestBehavior.visibility|.
+//
+// Nodes are added to the hit test result whenever one of their opaque children
+// is hit. This is useful for scrolling containers which may need to intercept
+// certain gestures within the space of their children and therefore need to
+// be added to the hit test result themselves.
+//
+// Nodes can also request to prune hit testing for their children, which
+// prevents their children from being hit.
+//
+// Hit testing proceeds recursively in post-order traversal (the reverse of
+// the drawing order). Intuitively, this means that the most specific
+// (deepest) nodes of the tree are tested before their ancestors.
+//
+// Starting from the root, the compositor transforms the point of interest
+// into the node's coordinate system, rejects the node if the point is
+// outside of the node's clip region, otherwise recursively tests the
+// node's children (those which were selected by the combinator rule)
+// until the first opaque target hit is found, then evaluates the node's
+// |HitTestBehavior| to determine whether the node was hit. Nodes are
+// accumulated into a hit test result in the order in which they were
+// determined to have been hit.
+//
+// See |HitTestBehavior| for more details.
+//
// INSTANCING
//
// The compositor allows nodes to be referenced and reused multiple times
@@ -128,15 +168,13 @@
// If null, the node does not apply any clipping of its own.
mojo.Rect? content_clip;
- // The hit test id to report if anything within the node is hit.
- // Use |kHitIdNone| if the node should not be hit tested.
- //
- // TODO(jeffbrown): This scheme is probably too naive.
- uint32 hit_id = kHitIdNone;
-
// The Combinator to apply when processing the children of this node.
Combinator combinator = Combinator.MERGE;
+ // The hit testing behavior of the node.
+ // If null, the node is considered invisible for hit testing.
+ HitTestBehavior? hit_test_behavior;
+
// The ids of the children of this node.
// It is an error to specify a node id that does not refer to a valid
// node or which creates a cycle in the graph; the compositor will close