Commit 8b5ece7c authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Report semantics geometry in physical units. (#10094)

Previously we used logical pixels. This made the accessibility metrics
tiny on modern devices, since the OS works in physical units.

Also add a bit more debugging info and some docs.
parent 20cc29cd
...@@ -29,6 +29,8 @@ abstract class GestureBinding extends BindingBase implements HitTestable, HitTes ...@@ -29,6 +29,8 @@ abstract class GestureBinding extends BindingBase implements HitTestable, HitTes
static GestureBinding _instance; static GestureBinding _instance;
void _handlePointerDataPacket(ui.PointerDataPacket packet) { void _handlePointerDataPacket(ui.PointerDataPacket packet) {
// We convert pointer data to logical pixels so that e.g. the touch slop can be
// defined in a device-independent manner.
_pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, ui.window.devicePixelRatio)); _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, ui.window.devicePixelRatio));
_flushPointerEventQueue(); _flushPointerEventQueue();
} }
......
...@@ -32,6 +32,10 @@ class _PointerState { ...@@ -32,6 +32,10 @@ class _PointerState {
} }
/// Converts from engine pointer data to framework pointer events. /// Converts from engine pointer data to framework pointer events.
///
/// This takes [PointerDataPacket] objects, as received from the engine via
/// [dart:ui.Window.onPointerDataPacket], and converts them to [PointerEvent]
/// objects.
class PointerEventConverter { class PointerEventConverter {
// Map from platform pointer identifiers to PointerEvent pointer identifiers. // Map from platform pointer identifiers to PointerEvent pointer identifiers.
static final Map<int, _PointerState> _pointers = <int, _PointerState>{}; static final Map<int, _PointerState> _pointers = <int, _PointerState>{};
...@@ -44,6 +48,11 @@ class PointerEventConverter { ...@@ -44,6 +48,11 @@ class PointerEventConverter {
} }
/// Expand the given packet of pointer data into a sequence of framework pointer events. /// Expand the given packet of pointer data into a sequence of framework pointer events.
///
/// The `devicePixelRatio` argument (usually given the value from
/// [dart:ui.Window.devicePixelRatio]) is used to convert the incoming data
/// from physical coordinates to logical pixels. See the discussion at
/// [PointerEvent] for more details on the [PointerEvent] coordinate space.
static Iterable<PointerEvent> expand(Iterable<ui.PointerData> data, double devicePixelRatio) sync* { static Iterable<PointerEvent> expand(Iterable<ui.PointerData> data, double devicePixelRatio) sync* {
for (ui.PointerData datum in data) { for (ui.PointerData datum in data) {
final Offset position = new Offset(datum.physicalX, datum.physicalY) / devicePixelRatio; final Offset position = new Offset(datum.physicalX, datum.physicalY) / devicePixelRatio;
......
...@@ -69,6 +69,26 @@ int nthMouseButton(int number) => (kPrimaryMouseButton << (number - 1)) & kMaxUn ...@@ -69,6 +69,26 @@ int nthMouseButton(int number) => (kPrimaryMouseButton << (number - 1)) & kMaxUn
int nthStylusButton(int number) => (kPrimaryStylusButton << (number - 1)) & kMaxUnsignedSMI; int nthStylusButton(int number) => (kPrimaryStylusButton << (number - 1)) & kMaxUnsignedSMI;
/// Base class for touch, stylus, or mouse events. /// Base class for touch, stylus, or mouse events.
///
/// Pointer events operate in the coordinate space of the screen, scaled to
/// logical pixels. Logical pixels approximate a grid with about 38 pixels per
/// centimeter, or 96 pixels per inch.
///
/// This allows gestures to be recognised independent of the precise hardware
/// characteristics of the device. In particular, features such as touch slop
/// (see [kTouchSlop]) can be defined in terms of roughly physical lengths so
/// that the user can shift their finger by the same distance on a high-density
/// display as on a low-resolution device.
///
/// For similar reasons, pointer events are not affected by any transforms in
/// the rendering layer. This means that deltas may need to be scaled before
/// being applied to movement within the rendering. For example, if a scrolling
/// list is shown scaled by 2x, the pointer deltas will have to be scaled by the
/// inverse amount if the list is to appear to scroll with the user's finger.
///
/// See also:
///
/// * [Window.devicePixelRatio], which defines the device's current resolution.
@immutable @immutable
abstract class PointerEvent { abstract class PointerEvent {
/// Abstract const constructor. This constructor enables subclasses to provide /// Abstract const constructor. This constructor enables subclasses to provide
......
...@@ -13,8 +13,8 @@ import 'basic_types.dart'; ...@@ -13,8 +13,8 @@ import 'basic_types.dart';
class MatrixUtils { class MatrixUtils {
MatrixUtils._(); MatrixUtils._();
/// Returns the given [transform] matrix as Offset, if the matrix is nothing /// Returns the given [transform] matrix as an [Offset], if the matrix is
/// but a 2D translation. /// nothing but a 2D translation.
/// ///
/// Otherwise, returns null. /// Otherwise, returns null.
static Offset getAsTranslation(Matrix4 transform) { static Offset getAsTranslation(Matrix4 transform) {
...@@ -40,6 +40,34 @@ class MatrixUtils { ...@@ -40,6 +40,34 @@ class MatrixUtils {
return null; return null;
} }
/// Returns the given [transform] matrix as a [double] describing a uniform
/// scale, if the matrix is nothing but a symmetric 2D scale transform.
///
/// Otherwise, returns null.
static double getAsScale(Matrix4 transform) {
assert(transform != null);
final Float64List values = transform.storage;
// Values are stored in column-major order.
if (values[1] == 0.0 && // col 1 (value 0 is the scale)
values[2] == 0.0 &&
values[3] == 0.0 &&
values[4] == 0.0 && // col 2 (value 5 is the scale)
values[6] == 0.0 &&
values[7] == 0.0 &&
values[8] == 0.0 && // col 3
values[9] == 0.0 &&
values[10] == 1.0 &&
values[11] == 0.0 &&
values[12] == 0.0 && // col 4
values[13] == 0.0 &&
values[14] == 0.0 &&
values[15] == 1.0 &&
values[0] == values[5]) { // uniform scale
return values[0];
}
return null;
}
/// Returns true if the given matrices are exactly equal, and false /// Returns true if the given matrices are exactly equal, and false
/// otherwise. Null values are assumed to be the identity matrix. /// otherwise. Null values are assumed to be the identity matrix.
static bool matrixEquals(Matrix4 a, Matrix4 b) { static bool matrixEquals(Matrix4 a, Matrix4 b) {
......
...@@ -143,7 +143,7 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding, ...@@ -143,7 +143,7 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
final double devicePixelRatio = ui.window.devicePixelRatio; final double devicePixelRatio = ui.window.devicePixelRatio;
return new ViewConfiguration( return new ViewConfiguration(
size: ui.window.physicalSize / devicePixelRatio, size: ui.window.physicalSize / devicePixelRatio,
devicePixelRatio: devicePixelRatio devicePixelRatio: devicePixelRatio,
); );
} }
......
...@@ -11,6 +11,7 @@ import 'package:flutter/gestures.dart'; ...@@ -11,6 +11,7 @@ import 'package:flutter/gestures.dart';
import 'package:vector_math/vector_math_64.dart'; import 'package:vector_math/vector_math_64.dart';
import 'debug.dart'; import 'debug.dart';
import 'node.dart';
import 'object.dart'; import 'object.dart';
// This class should only be used in debug builds. // This class should only be used in debug builds.
...@@ -1806,9 +1807,19 @@ abstract class RenderBox extends RenderObject { ...@@ -1806,9 +1807,19 @@ abstract class RenderBox extends RenderObject {
/// system of `ancestor`. /// system of `ancestor`.
/// ///
/// If `ancestor` is null, this method returns a matrix that maps from the /// If `ancestor` is null, this method returns a matrix that maps from the
/// local coordinate system to the global coordinate system. /// local coordinate system to the coordinate system of the
/// [PipelineOwner.rootNode]. For the render tree owner by the
/// [RendererBinding] (i.e. for the main render tree displayed on the device)
/// this means that this method maps to the global coordinate system in
/// logical pixels. To get physical pixels, use [applyPaintTransform] from the
/// [RenderView] to further transform the coordinate.
Matrix4 getTransformTo(RenderObject ancestor) { Matrix4 getTransformTo(RenderObject ancestor) {
assert(attached); assert(attached);
if (ancestor == null) {
final AbstractNode rootNode = owner.rootNode;
if (rootNode is RenderObject)
ancestor = rootNode;
}
final List<RenderObject> renderers = <RenderObject>[]; final List<RenderObject> renderers = <RenderObject>[];
for (RenderObject renderer = this; renderer != ancestor; renderer = renderer.parent) { for (RenderObject renderer = this; renderer != ancestor; renderer = renderer.parent) {
assert(renderer != null); // Failed to find ancestor in parent chain. assert(renderer != null); // Failed to find ancestor in parent chain.
...@@ -1820,8 +1831,8 @@ abstract class RenderBox extends RenderObject { ...@@ -1820,8 +1831,8 @@ abstract class RenderBox extends RenderObject {
return transform; return transform;
} }
/// Convert the given point from the global coodinate system to the local /// Convert the given point from the global coodinate system in logical pixels
/// coordinate system for this box. /// to the local coordinate system for this box.
/// ///
/// If the transform from global coordinates to local coordinates is /// If the transform from global coordinates to local coordinates is
/// degenerate, this function returns [Offset.zero]. /// degenerate, this function returns [Offset.zero].
...@@ -1829,6 +1840,8 @@ abstract class RenderBox extends RenderObject { ...@@ -1829,6 +1840,8 @@ abstract class RenderBox extends RenderObject {
/// If `ancestor` is non-null, this function converts the given point from the /// If `ancestor` is non-null, this function converts the given point from the
/// coordinate system of `ancestor` (which must be an ancestor of this render /// coordinate system of `ancestor` (which must be an ancestor of this render
/// object) instead of from the global coordinate system. /// object) instead of from the global coordinate system.
///
/// This method is implemented in terms of [getTransformTo].
Offset globalToLocal(Offset point, { RenderObject ancestor }) { Offset globalToLocal(Offset point, { RenderObject ancestor }) {
final Matrix4 transform = getTransformTo(ancestor); final Matrix4 transform = getTransformTo(ancestor);
final double det = transform.invert(); final double det = transform.invert();
...@@ -1838,11 +1851,13 @@ abstract class RenderBox extends RenderObject { ...@@ -1838,11 +1851,13 @@ abstract class RenderBox extends RenderObject {
} }
/// Convert the given point from the local coordinate system for this box to /// Convert the given point from the local coordinate system for this box to
/// the global coordinate system. /// the global coordinate system in logical pixels.
/// ///
/// If `ancestor` is non-null, this function converts the given point to the /// If `ancestor` is non-null, this function converts the given point to the
/// coordinate system of `ancestor` (which must be an ancestor of this render /// coordinate system of `ancestor` (which must be an ancestor of this render
/// object) instead of to the global coordinate system. /// object) instead of to the global coordinate system.
///
/// This method is implemented in terms of [getTransformTo].
Offset localToGlobal(Offset point, { RenderObject ancestor }) { Offset localToGlobal(Offset point, { RenderObject ancestor }) {
return MatrixUtils.transformPoint(getTransformTo(ancestor), point); return MatrixUtils.transformPoint(getTransformTo(ancestor), point);
} }
......
...@@ -184,7 +184,7 @@ class SemanticsNode extends AbstractNode { ...@@ -184,7 +184,7 @@ class SemanticsNode extends AbstractNode {
Matrix4 _transform; Matrix4 _transform;
set transform(Matrix4 value) { set transform(Matrix4 value) {
if (!MatrixUtils.matrixEquals(_transform, value)) { if (!MatrixUtils.matrixEquals(_transform, value)) {
_transform = value; _transform = MatrixUtils.isIdentity(value) ? null : value;
_markDirty(); _markDirty();
} }
} }
...@@ -569,7 +569,20 @@ class SemanticsNode extends AbstractNode { ...@@ -569,7 +569,20 @@ class SemanticsNode extends AbstractNode {
buffer.write(' (${ owner != null && owner._dirtyNodes.contains(this) ? "dirty" : "STALE; owner=$owner" })'); buffer.write(' (${ owner != null && owner._dirtyNodes.contains(this) ? "dirty" : "STALE; owner=$owner" })');
if (_shouldMergeAllDescendantsIntoThisNode) if (_shouldMergeAllDescendantsIntoThisNode)
buffer.write(' (leaf merge)'); buffer.write(' (leaf merge)');
buffer.write('; $rect'); final Offset offset = transform != null ? MatrixUtils.getAsTranslation(transform) : null;
if (offset != null) {
buffer.write('; ${rect.shift(offset)}');
} else {
final double scale = transform != null ? MatrixUtils.getAsScale(transform) : null;
if (scale != null) {
buffer.write('; $rect scaled by ${scale.toStringAsFixed(1)}x');
} else if (transform != null && !MatrixUtils.isIdentity(transform)) {
final String matrix = transform.toString().split('\n').take(4).map((String line) => line.substring(4)).join('; ');
buffer.write('; $rect with transform [$matrix]');
} else {
buffer.write('; $rect');
}
}
if (wasAffectedByClip) if (wasAffectedByClip)
buffer.write(' (clipped)'); buffer.write(' (clipped)');
for (SemanticsAction action in SemanticsAction.values.values) { for (SemanticsAction action in SemanticsAction.values.values) {
......
...@@ -54,11 +54,14 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> ...@@ -54,11 +54,14 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
/// Creates the root of the render tree. /// Creates the root of the render tree.
/// ///
/// Typically created by the binding (e.g., [RendererBinding]). /// Typically created by the binding (e.g., [RendererBinding]).
///
/// The [configuration] must not be null.
RenderView({ RenderView({
RenderBox child, RenderBox child,
this.timeForRotation: const Duration(microseconds: 83333), this.timeForRotation: const Duration(microseconds: 83333),
ViewConfiguration configuration @required ViewConfiguration configuration,
}) : _configuration = configuration { }) : assert(configuration != null),
_configuration = configuration {
this.child = child; this.child = child;
} }
...@@ -76,26 +79,44 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> ...@@ -76,26 +79,44 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
/// The constraints used for the root layout. /// The constraints used for the root layout.
ViewConfiguration get configuration => _configuration; ViewConfiguration get configuration => _configuration;
ViewConfiguration _configuration; ViewConfiguration _configuration;
/// The configuration is initially set by the `configuration` argument
/// passed to the constructor.
///
/// Always call [scheduleInitialFrame] before changing the configuration.
set configuration(ViewConfiguration value) { set configuration(ViewConfiguration value) {
assert(value != null);
if (configuration == value) if (configuration == value)
return; return;
_configuration = value; _configuration = value;
final ContainerLayer rootLayer = new TransformLayer(transform: configuration.toMatrix()); replaceRootLayer(_updateMatricesAndCreateNewRootLayer());
rootLayer.attach(this); assert(_rootTransform != null);
replaceRootLayer(rootLayer);
markNeedsLayout(); markNeedsLayout();
} }
/// Bootstrap the rendering pipeline by scheduling the first frame. /// Bootstrap the rendering pipeline by scheduling the first frame.
///
/// This should only be called once, and must be called before changing
/// [configuration]. It is typically called immediately after calling the
/// constructor.
void scheduleInitialFrame() { void scheduleInitialFrame() {
assert(owner != null); assert(owner != null);
assert(_rootTransform == null);
scheduleInitialLayout(); scheduleInitialLayout();
final ContainerLayer rootLayer = new TransformLayer(transform: configuration.toMatrix()); scheduleInitialPaint(_updateMatricesAndCreateNewRootLayer());
rootLayer.attach(this); assert(_rootTransform != null);
scheduleInitialPaint(rootLayer);
owner.requestVisualUpdate(); owner.requestVisualUpdate();
} }
Matrix4 _rootTransform;
Layer _updateMatricesAndCreateNewRootLayer() {
_rootTransform = configuration.toMatrix();
final ContainerLayer rootLayer = new TransformLayer(transform: _rootTransform);
rootLayer.attach(this);
assert(_rootTransform != null);
return rootLayer;
}
// We never call layout() on this class, so this should never get // We never call layout() on this class, so this should never get
// checked. (This class is laid out using scheduleInitialLayout().) // checked. (This class is laid out using scheduleInitialLayout().)
@override @override
...@@ -108,6 +129,7 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> ...@@ -108,6 +129,7 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
@override @override
void performLayout() { void performLayout() {
assert(_rootTransform != null);
if (configuration.orientation != _orientation) { if (configuration.orientation != _orientation) {
if (_orientation != null && child != null) if (_orientation != null && child != null)
child.rotate(oldAngle: _orientation, newAngle: configuration.orientation, time: timeForRotation); child.rotate(oldAngle: _orientation, newAngle: configuration.orientation, time: timeForRotation);
...@@ -131,7 +153,10 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> ...@@ -131,7 +153,10 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
/// of its descendants. Adds any render objects that contain the point to the /// of its descendants. Adds any render objects that contain the point to the
/// given hit test result. /// given hit test result.
/// ///
/// The [position] argument is in the coordinate system of the render view. /// The [position] argument is in the coordinate system of the render view,
/// which is to say, in logical pixels. This is not necessarily the same
/// coordinate system as that expected by the root [Layer], which will
/// normally be in physical (device) pixels.
bool hitTest(HitTestResult result, { Offset position }) { bool hitTest(HitTestResult result, { Offset position }) {
if (child != null) if (child != null)
child.hitTest(result, position: position); child.hitTest(result, position: position);
...@@ -148,6 +173,13 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> ...@@ -148,6 +173,13 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
context.paintChild(child, offset); context.paintChild(child, offset);
} }
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
assert(_rootTransform != null);
transform.multiply(_rootTransform);
super.applyPaintTransform(child, transform);
}
/// Uploads the composited layer tree to the engine. /// Uploads the composited layer tree to the engine.
/// ///
/// Actually causes the output of the rendering pipeline to appear on screen. /// Actually causes the output of the rendering pipeline to appear on screen.
...@@ -173,7 +205,10 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> ...@@ -173,7 +205,10 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
Rect get paintBounds => Offset.zero & size; Rect get paintBounds => Offset.zero & size;
@override @override
Rect get semanticBounds => Offset.zero & size; Rect get semanticBounds {
assert(_rootTransform != null);
return MatrixUtils.transformRect(_rootTransform, Offset.zero & size);
}
@override @override
void debugFillDescription(List<String> description) { void debugFillDescription(List<String> description) {
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui' show SemanticsFlags; import 'dart:ui' show SemanticsFlags;
import 'dart:ui' as ui show window;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
...@@ -31,7 +32,7 @@ class SemanticsDebugger extends StatefulWidget { ...@@ -31,7 +32,7 @@ class SemanticsDebugger extends StatefulWidget {
_SemanticsDebuggerState createState() => new _SemanticsDebuggerState(); _SemanticsDebuggerState createState() => new _SemanticsDebuggerState();
} }
class _SemanticsDebuggerState extends State<SemanticsDebugger> { class _SemanticsDebuggerState extends State<SemanticsDebugger> with WidgetsBindingObserver {
_SemanticsClient _client; _SemanticsClient _client;
@override @override
...@@ -43,6 +44,7 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> { ...@@ -43,6 +44,7 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> {
// the BuildContext. // the BuildContext.
_client = new _SemanticsClient(WidgetsBinding.instance.pipelineOwner) _client = new _SemanticsClient(WidgetsBinding.instance.pipelineOwner)
..addListener(_update); ..addListener(_update);
WidgetsBinding.instance.addObserver(this);
} }
@override @override
...@@ -50,9 +52,17 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> { ...@@ -50,9 +52,17 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> {
_client _client
..removeListener(_update) ..removeListener(_update)
..dispose(); ..dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
@override
void didChangeMetrics() {
setState(() {
// The root transform may have changed, we have to repaint.
});
}
void _update() { void _update() {
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
// We want the update to take effect next frame, so to make that // We want the update to take effect next frame, so to make that
...@@ -71,8 +81,10 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> { ...@@ -71,8 +81,10 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> {
Offset _lastPointerDownLocation; Offset _lastPointerDownLocation;
void _handlePointerDown(PointerDownEvent event) { void _handlePointerDown(PointerDownEvent event) {
setState(() { setState(() {
_lastPointerDownLocation = event.position; _lastPointerDownLocation = event.position * ui.window.devicePixelRatio;
}); });
// TODO(ianh): Use a gesture recognizer so that we can reset the
// _lastPointerDownLocation when none of the other gesture recognizers win.
} }
void _handleTap() { void _handleTap() {
...@@ -129,7 +141,8 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> { ...@@ -129,7 +141,8 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> {
foregroundPainter: new _SemanticsDebuggerPainter( foregroundPainter: new _SemanticsDebuggerPainter(
_pipelineOwner, _pipelineOwner,
_client.generation, _client.generation,
_lastPointerDownLocation _lastPointerDownLocation, // in physical pixels
ui.window.devicePixelRatio,
), ),
child: new GestureDetector( child: new GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
...@@ -142,10 +155,10 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> { ...@@ -142,10 +155,10 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> {
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: new IgnorePointer( child: new IgnorePointer(
ignoringSemantics: false, ignoringSemantics: false,
child: widget.child child: widget.child,
) ),
) ),
) ),
); );
} }
} }
...@@ -293,11 +306,12 @@ void _paint(Canvas canvas, SemanticsNode node, int rank) { ...@@ -293,11 +306,12 @@ void _paint(Canvas canvas, SemanticsNode node, int rank) {
} }
class _SemanticsDebuggerPainter extends CustomPainter { class _SemanticsDebuggerPainter extends CustomPainter {
const _SemanticsDebuggerPainter(this.owner, this.generation, this.pointerPosition); const _SemanticsDebuggerPainter(this.owner, this.generation, this.pointerPosition, this.devicePixelRatio);
final PipelineOwner owner; final PipelineOwner owner;
final int generation; final int generation;
final Offset pointerPosition; final Offset pointerPosition; // in physical pixels
final double devicePixelRatio;
SemanticsNode get _rootSemanticsNode { SemanticsNode get _rootSemanticsNode {
return owner.semanticsOwner?.rootSemanticsNode; return owner.semanticsOwner?.rootSemanticsNode;
...@@ -306,13 +320,16 @@ class _SemanticsDebuggerPainter extends CustomPainter { ...@@ -306,13 +320,16 @@ class _SemanticsDebuggerPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final SemanticsNode rootNode = _rootSemanticsNode; final SemanticsNode rootNode = _rootSemanticsNode;
canvas.save();
canvas.scale(1.0 / devicePixelRatio, 1.0 / devicePixelRatio);
if (rootNode != null) if (rootNode != null)
_paint(canvas, rootNode, _findDepth(rootNode)); _paint(canvas, rootNode, _findDepth(rootNode));
if (pointerPosition != null) { if (pointerPosition != null) {
final Paint paint = new Paint(); final Paint paint = new Paint();
paint.color = const Color(0x7F0090FF); paint.color = const Color(0x7F0090FF);
canvas.drawCircle(pointerPosition, 10.0, paint); canvas.drawCircle(pointerPosition, 10.0 * devicePixelRatio, paint);
} }
canvas.restore();
} }
@override @override
......
...@@ -25,10 +25,9 @@ void main() { ...@@ -25,10 +25,9 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics( new TestSemantics.root(
id: 0,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics.rootChild(
id: 1, id: 1,
actions: SemanticsAction.tap.index, actions: SemanticsAction.tap.index,
label: 'ABC', label: 'ABC',
......
...@@ -460,14 +460,14 @@ void main() { ...@@ -460,14 +460,14 @@ void main() {
) )
); );
expect(semantics, hasSemantics(new TestSemantics(id: 0, label: tooltipText))); expect(semantics, hasSemantics(new TestSemantics.root(label: tooltipText)));
// before using "as dynamic" in your code, see note top of file // before using "as dynamic" in your code, see note top of file
(key.currentState as dynamic).ensureTooltipVisible(); // this triggers a rebuild of the semantics because the tree changes (key.currentState as dynamic).ensureTooltipVisible(); // this triggers a rebuild of the semantics because the tree changes
await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
expect(semantics, hasSemantics(new TestSemantics(id: 0, label: tooltipText))); expect(semantics, hasSemantics(new TestSemantics.root(label: tooltipText)));
semantics.dispose(); semantics.dispose();
}); });
......
...@@ -23,7 +23,7 @@ void main() { ...@@ -23,7 +23,7 @@ void main() {
) )
); );
expect(semantics, hasSemantics(new TestSemantics(id: 0, label: 'test1'))); expect(semantics, hasSemantics(new TestSemantics.root(label: 'test1')));
// control for forking // control for forking
await tester.pumpWidget( await tester.pumpWidget(
...@@ -45,7 +45,7 @@ void main() { ...@@ -45,7 +45,7 @@ void main() {
) )
); );
expect(semantics, hasSemantics(new TestSemantics(id: 0, label: 'child1'))); expect(semantics, hasSemantics(new TestSemantics.root(label: 'child1')));
// forking semantics // forking semantics
await tester.pumpWidget( await tester.pumpWidget(
...@@ -68,15 +68,14 @@ void main() { ...@@ -68,15 +68,14 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics( new TestSemantics.root(
id: 0,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics.rootChild(
id: 1, id: 1,
label: 'child1', label: 'child1',
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
), ),
new TestSemantics( new TestSemantics.rootChild(
id: 2, id: 2,
label: 'child2', label: 'child2',
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
...@@ -106,7 +105,7 @@ void main() { ...@@ -106,7 +105,7 @@ void main() {
) )
); );
expect(semantics, hasSemantics(new TestSemantics(id: 0, label: 'child1'))); expect(semantics, hasSemantics(new TestSemantics.root(label: 'child1')));
// toggle a branch back on // toggle a branch back on
await tester.pumpWidget( await tester.pumpWidget(
...@@ -129,15 +128,14 @@ void main() { ...@@ -129,15 +128,14 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics( new TestSemantics.root(
id: 0,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics.rootChild(
id: 3, id: 3,
label: 'child1', label: 'child1',
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
), ),
new TestSemantics( new TestSemantics.rootChild(
id: 2, id: 2,
label: 'child2', label: 'child2',
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
......
...@@ -38,15 +38,14 @@ void main() { ...@@ -38,15 +38,14 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics( new TestSemantics.root(
id: 0,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics.rootChild(
id: 1, id: 1,
label: 'child1', label: 'child1',
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
), ),
new TestSemantics( new TestSemantics.rootChild(
id: 2, id: 2,
label: 'child2', label: 'child2',
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
...@@ -76,7 +75,7 @@ void main() { ...@@ -76,7 +75,7 @@ void main() {
) )
); );
expect(semantics, hasSemantics(new TestSemantics(id: 0, label: 'child1'))); expect(semantics, hasSemantics(new TestSemantics.root(label: 'child1')));
// toggle a branch back on // toggle a branch back on
await tester.pumpWidget( await tester.pumpWidget(
...@@ -99,15 +98,14 @@ void main() { ...@@ -99,15 +98,14 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics( new TestSemantics.root(
id: 0,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics.rootChild(
id: 3, id: 3,
label: 'child1', label: 'child1',
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
), ),
new TestSemantics( new TestSemantics.rootChild(
id: 2, id: 2,
label: 'child2', label: 'child2',
rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
......
...@@ -29,8 +29,7 @@ void main() { ...@@ -29,8 +29,7 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics( new TestSemantics.root(
id: 0,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
label: 'test', label: 'test',
) )
...@@ -48,8 +47,7 @@ void main() { ...@@ -48,8 +47,7 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics( new TestSemantics.root(
id: 0,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
) )
)); ));
...@@ -66,8 +64,7 @@ void main() { ...@@ -66,8 +64,7 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics( new TestSemantics.root(
id: 0,
label: 'test', label: 'test',
) )
)); ));
...@@ -87,8 +84,7 @@ void main() { ...@@ -87,8 +84,7 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics( new TestSemantics.root(
id: 0,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
label: 'test', label: 'test',
) )
......
...@@ -46,24 +46,27 @@ void main() { ...@@ -46,24 +46,27 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics( new TestSemantics.root(
id: 0,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics.rootChild(
id: 1, id: 1,
label: 'L1', label: 'L1',
rect: TestSemantics.fullScreen,
), ),
new TestSemantics( new TestSemantics.rootChild(
id: 2, id: 2,
label: 'L2', label: 'L2',
rect: TestSemantics.fullScreen,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 3, id: 3,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
rect: TestSemantics.fullScreen,
), ),
new TestSemantics( new TestSemantics(
id: 4, id: 4,
flags: SemanticsFlags.hasCheckedState.index, flags: SemanticsFlags.hasCheckedState.index,
rect: TestSemantics.fullScreen,
), ),
] ]
), ),
...@@ -100,17 +103,18 @@ void main() { ...@@ -100,17 +103,18 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics( new TestSemantics.root(
id: 0,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics.rootChild(
id: 1, id: 1,
label: 'L1', label: 'L1',
rect: TestSemantics.fullScreen,
), ),
new TestSemantics( new TestSemantics.rootChild(
id: 2, id: 2,
label: 'L2', label: 'L2',
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
rect: TestSemantics.fullScreen,
), ),
], ],
) )
...@@ -142,8 +146,7 @@ void main() { ...@@ -142,8 +146,7 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics( new TestSemantics.root(
id: 0,
label: 'L2', label: 'L2',
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
) )
......
...@@ -31,15 +31,16 @@ void main() { ...@@ -31,15 +31,16 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics( new TestSemantics.root(
id: 0,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics.rootChild(
id: 1, id: 1,
rect: TestSemantics.fullScreen,
), ),
new TestSemantics( new TestSemantics.rootChild(
id: 2, id: 2,
label: 'label', label: 'label',
rect: TestSemantics.fullScreen,
), ),
] ]
) )
......
...@@ -49,19 +49,20 @@ void main() { ...@@ -49,19 +49,20 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics( new TestSemantics.root(
id: 0,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics.rootChild(
id: 1, id: 1,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
label: label, label: label,
rect: TestSemantics.fullScreen,
), ),
// IDs 2 and 3 are used up by the nodes that get merged in // IDs 2 and 3 are used up by the nodes that get merged in
new TestSemantics( new TestSemantics.rootChild(
id: 4, id: 4,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
label: label, label: label,
rect: TestSemantics.fullScreen,
), ),
// IDs 5 and 6 are used up by the nodes that get merged in // IDs 5 and 6 are used up by the nodes that get merged in
], ],
...@@ -101,19 +102,20 @@ void main() { ...@@ -101,19 +102,20 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics( new TestSemantics.root(
id: 0,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics.rootChild(
id: 1, id: 1,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
label: label, label: label,
rect: TestSemantics.fullScreen,
), ),
// IDs 2 and 3 are used up by the nodes that get merged in // IDs 2 and 3 are used up by the nodes that get merged in
new TestSemantics( new TestSemantics.rootChild(
id: 4, id: 4,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
label: label, label: label,
rect: TestSemantics.fullScreen,
), ),
// IDs 5 and 6 are used up by the nodes that get merged in // IDs 5 and 6 are used up by the nodes that get merged in
], ],
......
...@@ -36,8 +36,7 @@ void main() { ...@@ -36,8 +36,7 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics( new TestSemantics.root(
id: 0,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
label: 'label', label: 'label',
) )
...@@ -66,8 +65,7 @@ void main() { ...@@ -66,8 +65,7 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics( new TestSemantics.root(
id: 0,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
label: 'label', label: 'label',
) )
......
...@@ -12,8 +12,7 @@ void main() { ...@@ -12,8 +12,7 @@ void main() {
testWidgets('Semantics shutdown and restart', (WidgetTester tester) async { testWidgets('Semantics shutdown and restart', (WidgetTester tester) async {
SemanticsTester semantics = new SemanticsTester(tester); SemanticsTester semantics = new SemanticsTester(tester);
final TestSemantics expectedSemantics = new TestSemantics( final TestSemantics expectedSemantics = new TestSemantics.root(
id: 0,
label: 'test1', label: 'test1',
); );
...@@ -59,13 +58,13 @@ void main() { ...@@ -59,13 +58,13 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics( new TestSemantics.root(
id: 0,
label: 'test1', label: 'test1',
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics.rootChild(
id: 1, id: 1,
label: 'test2a', label: 'test2a',
rect: TestSemantics.fullScreen,
) )
] ]
) )
...@@ -90,17 +89,18 @@ void main() { ...@@ -90,17 +89,18 @@ void main() {
); );
expect(semantics, hasSemantics( expect(semantics, hasSemantics(
new TestSemantics( new TestSemantics.root(
id: 0,
label: 'test1', label: 'test1',
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics.rootChild(
id: 2, id: 2,
label: 'middle', label: 'middle',
rect: TestSemantics.fullScreen,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
id: 1, id: 1,
label: 'test2b', label: 'test2b',
rect: TestSemantics.fullScreen,
) )
] ]
) )
......
...@@ -13,20 +13,69 @@ export 'package:flutter/rendering.dart' show SemanticsData; ...@@ -13,20 +13,69 @@ export 'package:flutter/rendering.dart' show SemanticsData;
/// Useful with [hasSemantics] and [SemanticsTester] to test the contents of the /// Useful with [hasSemantics] and [SemanticsTester] to test the contents of the
/// semantics tree. /// semantics tree.
class TestSemantics { class TestSemantics {
/// Creates an object witht some test semantics data. /// Creates an object with some test semantics data.
/// ///
/// If [rect] argument is null, the [rect] field with ve initialized with /// The [id] field is required. The root node has an id of zero. Other nodes
/// `new Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)`, which is the default size of /// are given a unique id when they are created, in a predictable fashion, and
/// the screen during unit testing. /// so these values can be hard-coded.
///
/// The [rect] field is required and has no default. Convenient values are
/// available:
///
/// * [TestSemantics.rootRect]: 2400x1600, the test screen's size in physical
/// pixels, useful for the node with id zero.
///
/// * [TestSemantics.fullScreen] 800x600, the test screen's size in logical
/// pixels, useful for other full-screen widgets.
TestSemantics({ TestSemantics({
this.id, @required this.id,
this.flags: 0, this.flags: 0,
this.actions: 0, this.actions: 0,
this.label: '', this.label: '',
Rect rect, @required this.rect,
this.transform, this.transform,
this.children: const <TestSemantics>[], this.children: const <TestSemantics>[],
}) : rect = rect ?? new Rect.fromLTRB(0.0, 0.0, 800.0, 600.0); }) : assert(id != null),
assert(flags != null),
assert(label != null),
assert(rect != null),
assert(children != null);
/// Creates an object with some test semantics data, with the [id] and [rect]
/// set to the appropriate values for the root node.
TestSemantics.root({
this.flags: 0,
this.actions: 0,
this.label: '',
this.transform,
this.children: const <TestSemantics>[],
}) : id = 0,
assert(flags != null),
assert(label != null),
rect = TestSemantics.rootRect,
assert(children != null);
/// Creates an object with some test semantics data, with the [id] and [rect]
/// set to the appropriate values for direct children of the root node.
///
/// The [transform] is set to a 3.0 scale (to account for the
/// [Window.devicePixelRatio] being 3.0 on the test pseudo-device).
///
/// The [rect] field is required and has no default. The
/// [TestSemantics.fullScreen] property may be useful as a value; it describes
/// an 800x600 rectangle, which is the test screen's size in logical pixels.
TestSemantics.rootChild({
@required this.id,
this.flags: 0,
this.actions: 0,
this.label: '',
@required this.rect,
Matrix4 transform,
this.children: const <TestSemantics>[],
}) : assert(flags != null),
assert(label != null),
transform = _applyRootChildScale(transform),
assert(children != null);
/// The unique identifier for this node. /// The unique identifier for this node.
/// ///
...@@ -45,9 +94,26 @@ class TestSemantics { ...@@ -45,9 +94,26 @@ class TestSemantics {
/// The bounding box for this node in its coordinate system. /// The bounding box for this node in its coordinate system.
/// ///
/// Defaults to filling the screen. /// Convenient values are available:
///
/// * [TestSemantics.rootRect]: 2400x1600, the test screen's size in physical
/// pixels, useful for the node with id zero.
///
/// * [TestSemantics.fullScreen] 800x600, the test screen's size in logical
/// pixels, useful for other full-screen widgets.
final Rect rect; final Rect rect;
/// The test screen's size in physical pixels, typically used as the [rect]
/// for the node with id zero.
///
/// See also [new TestSemantics.root], which uses this value to describe the
/// root node.
static final Rect rootRect = new Rect.fromLTWH(0.0, 0.0, 2400.0, 1800.0);
/// The test screen's size in logical pixels, useful for the [rect] of
/// full-screen widgets other than the root node.
static final Rect fullScreen = new Rect.fromLTWH(0.0, 0.0, 800.0, 600.0);
/// The transform from this node's coordinate system to its parent's coordinate system. /// The transform from this node's coordinate system to its parent's coordinate system.
/// ///
/// By default, the transform is null, which represents the identity /// By default, the transform is null, which represents the identity
...@@ -55,6 +121,13 @@ class TestSemantics { ...@@ -55,6 +121,13 @@ class TestSemantics {
/// parent). /// parent).
final Matrix4 transform; final Matrix4 transform;
static Matrix4 _applyRootChildScale(Matrix4 transform) {
final Matrix4 result = new Matrix4.diagonal3Values(3.0, 3.0, 1.0);
if (transform != null)
result.multiply(transform);
return result;
}
/// The children of this node. /// The children of this node.
final List<TestSemantics> children; final List<TestSemantics> children;
...@@ -154,7 +227,7 @@ class _HasSemantics extends Matcher { ...@@ -154,7 +227,7 @@ class _HasSemantics extends Matcher {
if (testNode.rect != data.rect) if (testNode.rect != data.rect)
return mismatchDescription.add('expected node id ${testNode.id} to have rect ${testNode.rect} but found rect ${data.rect}'); return mismatchDescription.add('expected node id ${testNode.id} to have rect ${testNode.rect} but found rect ${data.rect}');
if (testNode.transform != data.transform) if (testNode.transform != data.transform)
return mismatchDescription.add('expected node id ${testNode.id} to have transform ${testNode.transform} but found transform ${data.transform}'); return mismatchDescription.add('expected node id ${testNode.id} to have transform ${testNode.transform} but found transform:\n${data.transform}');
final int childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount; final int childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount;
if (testNode.children.length != childrenCount) if (testNode.children.length != childrenCount)
return mismatchDescription.add('expected node id ${testNode.id} to have ${testNode.children.length} but found $childrenCount children'); return mismatchDescription.add('expected node id ${testNode.id} to have ${testNode.children.length} but found $childrenCount children');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment