// Copyright 2014 The Flutter 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:ui'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:vector_math/vector_math_64.dart'; void main() { test('ContainerLayer.findAllAnnotations returns all results from its children', () { final Layer root = _Layers( ContainerLayer(), children: <Object>[ _TestAnnotatedLayer(1, opaque: false), _TestAnnotatedLayer(2, opaque: false), _TestAnnotatedLayer(3, opaque: false), ], ).build(); expect( root.findAllAnnotations<int>(Offset.zero).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 3, localPosition: Offset.zero), const AnnotationEntry<int>(annotation: 2, localPosition: Offset.zero), const AnnotationEntry<int>(annotation: 1, localPosition: Offset.zero), ]), ); }); test('ContainerLayer.find returns the first result from its children', () { final Layer root = _Layers( ContainerLayer(), children: <Object>[ _TestAnnotatedLayer(1, opaque: false), _TestAnnotatedLayer(2, opaque: false), _TestAnnotatedLayer(3, opaque: false), ], ).build(); final int result = root.find<int>(Offset.zero)!; expect(result, 3); }); test('ContainerLayer.findAllAnnotations returns empty result when finding nothing', () { final Layer root = _Layers( ContainerLayer(), children: <Object>[ _TestAnnotatedLayer(1, opaque: false), _TestAnnotatedLayer(2, opaque: false), _TestAnnotatedLayer(3, opaque: false), ], ).build(); expect(root.findAllAnnotations<double>(Offset.zero).entries.isEmpty, isTrue); }); test('ContainerLayer.find returns null when finding nothing', () { final Layer root = _Layers( ContainerLayer(), children: <Object>[ _TestAnnotatedLayer(1, opaque: false), _TestAnnotatedLayer(2, opaque: false), _TestAnnotatedLayer(3, opaque: false), ], ).build(); expect(root.find<double>(Offset.zero), isNull); }); test('ContainerLayer.findAllAnnotations stops at the first opaque child', () { final Layer root = _Layers( ContainerLayer(), children: <Object>[ _TestAnnotatedLayer(1, opaque: false), _TestAnnotatedLayer(2, opaque: true), _TestAnnotatedLayer(3, opaque: false), ], ).build(); expect( root.findAllAnnotations<int>(Offset.zero).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 3, localPosition: Offset.zero), const AnnotationEntry<int>(annotation: 2, localPosition: Offset.zero), ]), ); }); test("ContainerLayer.findAllAnnotations returns children's opacity (true)", () { final Layer root = _withBackgroundAnnotation( 1000, _Layers( ContainerLayer(), children: <Object>[ _TestAnnotatedLayer(2, opaque: true), ], ).build(), ); expect( root.findAllAnnotations<int>(Offset.zero).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 2, localPosition: Offset.zero), ]), ); }); test("ContainerLayer.findAllAnnotations returns children's opacity (false)", () { final Layer root = _withBackgroundAnnotation( 1000, _Layers( ContainerLayer(), children: <Object>[ _TestAnnotatedLayer(2, opaque: false), ], ).build(), ); expect( root.findAllAnnotations<int>(Offset.zero).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 2, localPosition: Offset.zero), const AnnotationEntry<int>(annotation: 1000, localPosition: Offset.zero), ]), ); }); test('ContainerLayer.findAllAnnotations returns false as opacity when finding nothing', () { final Layer root = _withBackgroundAnnotation( 1000, _Layers( ContainerLayer(), children: <Object>[ _TestAnnotatedLayer(2, opaque: false, size: Size.zero), ], ).build(), ); expect( root.findAllAnnotations<int>(Offset.zero).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 1000, localPosition: Offset.zero), ]), ); }); test('OffsetLayer.findAllAnnotations respects offset', () { const Offset insidePosition = Offset(-5, 5); const Offset outsidePosition = Offset(5, 5); final Layer root = _withBackgroundAnnotation( 1000, _Layers( OffsetLayer(offset: const Offset(-10, 0)), children: <Object>[ _TestAnnotatedLayer(1, opaque: true, size: const Size(10, 10)), ], ).build(), ); expect( root.findAllAnnotations<int>(insidePosition).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 1, localPosition: Offset(5, 5)), ]), ); expect( root.findAllAnnotations<int>(outsidePosition).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 1000, localPosition: Offset(5, 5)), ]), ); }); test('ClipRectLayer.findAllAnnotations respects clipRect', () { const Offset insidePosition = Offset(11, 11); const Offset outsidePosition = Offset(19, 19); final Layer root = _withBackgroundAnnotation( 1000, _Layers( ClipRectLayer(clipRect: const Offset(10, 10) & const Size(5, 5)), children: <Object>[ _TestAnnotatedLayer( 1, opaque: true, size: const Size(10, 10), offset: const Offset(10, 10), ), ], ).build(), ); expect( root.findAllAnnotations<int>(insidePosition).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 1, localPosition: insidePosition), ]), ); expect( root.findAllAnnotations<int>(outsidePosition).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 1000, localPosition: outsidePosition), ]), ); }); test('ClipRRectLayer.findAllAnnotations respects clipRRect', () { // For a curve of radius 4 centered at (4, 4), // location (1, 1) is outside, while (2, 2) is inside. // Here we shift this RRect by (10, 10). final RRect rrect = RRect.fromRectAndRadius( const Offset(10, 10) & const Size(10, 10), const Radius.circular(4), ); const Offset insidePosition = Offset(12, 12); const Offset outsidePosition = Offset(11, 11); final Layer root = _withBackgroundAnnotation( 1000, _Layers( ClipRRectLayer(clipRRect: rrect), children: <Object>[ _TestAnnotatedLayer( 1, opaque: true, size: const Size(10, 10), offset: const Offset(10, 10), ), ], ).build(), ); expect( root.findAllAnnotations<int>(insidePosition).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 1, localPosition: insidePosition), ]), ); expect( root.findAllAnnotations<int>(outsidePosition).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 1000, localPosition: outsidePosition), ]), ); }); test('ClipPathLayer.findAllAnnotations respects clipPath', () { // For this triangle, location (1, 1) is inside, while (2, 2) is outside. // 2 // ————— // | / // | / // 2 |/ final Path originalPath = Path(); originalPath.lineTo(2, 0); originalPath.lineTo(0, 2); originalPath.close(); // Shift this clip path by (10, 10). final Path path = originalPath.shift(const Offset(10, 10)); const Offset insidePosition = Offset(11, 11); const Offset outsidePosition = Offset(12, 12); final Layer root = _withBackgroundAnnotation( 1000, _Layers( ClipPathLayer(clipPath: path), children: <Object>[ _TestAnnotatedLayer( 1, opaque: true, size: const Size(10, 10), offset: const Offset(10, 10), ), ], ).build(), ); expect( root.findAllAnnotations<int>(insidePosition).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 1, localPosition: insidePosition), ]), ); expect( root.findAllAnnotations<int>(outsidePosition).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 1000, localPosition: outsidePosition), ]), ); }); test('TransformLayer.findAllAnnotations respects transform', () { // Matrix `transform` enlarges the target by (2x, 4x), then shift it by // (10, 20). final Matrix4 transform = Matrix4.diagonal3Values(2, 4, 1)..setTranslation(Vector3(10, 20, 0)); // The original region is Offset(10, 10) & Size(10, 10) // The transformed region is Offset(30, 60) & Size(20, 40) const Offset insidePosition = Offset(40, 80); const Offset outsidePosition = Offset(20, 40); final Layer root = _withBackgroundAnnotation( 1000, _Layers( TransformLayer(transform: transform), children: <Object>[ _TestAnnotatedLayer( 1, opaque: true, size: const Size(10, 10), offset: const Offset(10, 10), ), ], ).build(), ); expect( root.findAllAnnotations<int>(insidePosition).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 1, localPosition: Offset(15, 15)), ]), ); expect( root.findAllAnnotations<int>(outsidePosition).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 1000, localPosition: outsidePosition), ]), ); }); test('TransformLayer.findAllAnnotations correctly transforms with perspective', () { // Test the 4 corners of a transformed annotated region. final Matrix4 transform = Matrix4.identity() ..setEntry(3, 2, 0.005) ..rotateX(-0.2) ..rotateY(0.2); final Layer root = _withBackgroundAnnotation( 0, _Layers( TransformLayer(transform: transform), children: <Object>[ _TestAnnotatedLayer( 1, opaque: true, size: const Size(30, 40), offset: const Offset(10, 20), ), ], ).build(), ); void expectOneAnnotation({ required Offset globalPosition, required int value, required Offset localPosition, }) { expect( root.findAllAnnotations<int>(globalPosition).entries.toList(), _equalToAnnotationResult<int>( <AnnotationEntry<int>>[ AnnotationEntry<int>(annotation: value, localPosition: localPosition), ], maxCoordinateRelativeDiff: 0.005, ), ); } expectOneAnnotation( globalPosition: const Offset(10.0, 19.7), value: 0, localPosition: const Offset(10.0, 19.7), ); expectOneAnnotation( globalPosition: const Offset(10.1, 19.8), value: 1, localPosition: const Offset(10.0, 20.0), ); expectOneAnnotation( globalPosition: const Offset(10.5, 62.8), value: 0, localPosition: const Offset(10.5, 62.8), ); expectOneAnnotation( globalPosition: const Offset(10.6, 62.7), value: 1, localPosition: const Offset(10.1, 59.9), ); expectOneAnnotation( globalPosition: const Offset(42.6, 40.8), value: 0, localPosition: const Offset(42.6, 40.8), ); expectOneAnnotation( globalPosition: const Offset(42.5, 40.9), value: 1, localPosition: const Offset(39.9, 40.0), ); expectOneAnnotation( globalPosition: const Offset(43.5, 63.5), value: 0, localPosition: const Offset(43.5, 63.5), ); expectOneAnnotation( globalPosition: const Offset(43.4, 63.4), value: 1, localPosition: const Offset(39.9, 59.9), ); }); test('TransformLayer.findAllAnnotations skips when transform is irreversible', () { final Matrix4 transform = Matrix4.diagonal3Values(1, 0, 1); final Layer root = _withBackgroundAnnotation( 1000, _Layers( TransformLayer(transform: transform), children: <Object>[ _TestAnnotatedLayer(1, opaque: true), ], ).build(), ); expect( root.findAllAnnotations<int>(Offset.zero).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 1000, localPosition: Offset.zero), ]), ); }); test('PhysicalModelLayer.findAllAnnotations respects clipPath', () { // For this triangle, location (1, 1) is inside, while (2, 2) is outside. // 2 // ————— // | / // | / // 2 |/ final Path originalPath = Path(); originalPath.lineTo(2, 0); originalPath.lineTo(0, 2); originalPath.close(); // Shift this clip path by (10, 10). final Path path = originalPath.shift(const Offset(10, 10)); const Offset insidePosition = Offset(11, 11); const Offset outsidePosition = Offset(12, 12); final Layer root = _withBackgroundAnnotation( 1000, _Layers( PhysicalModelLayer( clipPath: path, elevation: 10, color: const Color.fromARGB(0, 0, 0, 0), shadowColor: const Color.fromARGB(0, 0, 0, 0), ), children: <Object>[ _TestAnnotatedLayer( 1, opaque: true, size: const Size(10, 10), offset: const Offset(10, 10), ), ], ).build(), ); expect( root.findAllAnnotations<int>(insidePosition).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 1, localPosition: insidePosition), ]), ); expect( root.findAllAnnotations<int>(outsidePosition).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 1000, localPosition: outsidePosition), ]), ); }); test('LeaderLayer.findAllAnnotations respects offset', () { const Offset insidePosition = Offset(-5, 5); const Offset outsidePosition = Offset(5, 5); final Layer root = _withBackgroundAnnotation( 1000, _Layers( LeaderLayer( link: LayerLink(), offset: const Offset(-10, 0), ), children: <Object>[ _TestAnnotatedLayer(1, opaque: true, size: const Size(10, 10)), ], ).build(), ); expect( root.findAllAnnotations<int>(insidePosition).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 1, localPosition: Offset(5, 5)), ]), ); expect( root.findAllAnnotations<int>(outsidePosition).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 1000, localPosition: outsidePosition), ]), ); }); test( 'AnnotatedRegionLayer.findAllAnnotations should append to the list ' 'and return the given opacity (false) during a successful hit', () { const Offset position = Offset(5, 5); final Layer root = _withBackgroundAnnotation( 1000, _Layers( AnnotatedRegionLayer<int>(1), children: <Object>[ _TestAnnotatedLayer(2, opaque: false), ], ).build(), ); expect( root.findAllAnnotations<int>(position).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 2, localPosition: position), const AnnotationEntry<int>(annotation: 1, localPosition: position), const AnnotationEntry<int>(annotation: 1000, localPosition: position), ]), ); }, ); test( 'AnnotatedRegionLayer.findAllAnnotations should append to the list ' 'and return the given opacity (true) during a successful hit', () { const Offset position = Offset(5, 5); final Layer root = _withBackgroundAnnotation( 1000, _Layers( AnnotatedRegionLayer<int>(1, opaque: true), children: <Object>[ _TestAnnotatedLayer(2, opaque: false), ], ).build(), ); expect( root.findAllAnnotations<int>(position).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 2, localPosition: position), const AnnotationEntry<int>(annotation: 1, localPosition: position), ]), ); }, ); test('AnnotatedRegionLayer.findAllAnnotations has default opacity as false', () { const Offset position = Offset(5, 5); final Layer root = _withBackgroundAnnotation( 1000, _Layers( AnnotatedRegionLayer<int>(1), children: <Object>[ _TestAnnotatedLayer(2, opaque: false), ], ).build(), ); expect( root.findAllAnnotations<int>(position).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 2, localPosition: position), const AnnotationEntry<int>(annotation: 1, localPosition: position), const AnnotationEntry<int>(annotation: 1000, localPosition: position), ]), ); }); test( 'AnnotatedRegionLayer.findAllAnnotations should still check children and return ' "children's opacity (false) during a failed hit", () { const Offset position = Offset(5, 5); final Layer root = _withBackgroundAnnotation( 1000, _Layers( AnnotatedRegionLayer<int>(1, opaque: true, size: Size.zero), children: <Object>[ _TestAnnotatedLayer(2, opaque: false), ], ).build(), ); expect( root.findAllAnnotations<int>(position).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 2, localPosition: position), const AnnotationEntry<int>(annotation: 1000, localPosition: position), ]), ); }, ); test( 'AnnotatedRegionLayer.findAllAnnotations should still check children and return ' "children's opacity (true) during a failed hit", () { const Offset position = Offset(5, 5); final Layer root = _withBackgroundAnnotation( 1000, _Layers( AnnotatedRegionLayer<int>(1, size: Size.zero), children: <Object>[ _TestAnnotatedLayer(2, opaque: true), ], ).build(), ); expect( root.findAllAnnotations<int>(position).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 2, localPosition: position), ]), ); }, ); test( "AnnotatedRegionLayer.findAllAnnotations should not add to children's opacity " 'during a successful hit if it is not opaque', () { const Offset position = Offset(5, 5); final Layer root = _withBackgroundAnnotation( 1000, _Layers( AnnotatedRegionLayer<int>(1), children: <Object>[ _TestAnnotatedLayer(2, opaque: false), ], ).build(), ); expect( root.findAllAnnotations<int>(position).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 2, localPosition: position), const AnnotationEntry<int>(annotation: 1, localPosition: position), const AnnotationEntry<int>(annotation: 1000, localPosition: position), ]), ); }, ); test( "AnnotatedRegionLayer.findAllAnnotations should add to children's opacity " 'during a successful hit if it is opaque', () { const Offset position = Offset(5, 5); final Layer root = _withBackgroundAnnotation( 1000, _Layers( AnnotatedRegionLayer<int>(1, opaque: true), children: <Object>[ _TestAnnotatedLayer(2, opaque: false), ], ).build(), ); expect( root.findAllAnnotations<int>(position).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 2, localPosition: position), const AnnotationEntry<int>(annotation: 1, localPosition: position), ]), ); }, ); test( 'AnnotatedRegionLayer.findAllAnnotations should clip its annotation ' 'using size and offset (positive)', () { // The target position would have fallen outside if not for the offset. const Offset position = Offset(100, 100); final Layer root = _withBackgroundAnnotation( 1000, _Layers( AnnotatedRegionLayer<int>( 1, size: const Size(20, 20), offset: const Offset(90, 90), ), children: <Object>[ _TestAnnotatedLayer( 2, opaque: false, // Use this offset to make sure AnnotatedRegionLayer's offset // does not affect its children. offset: const Offset(20, 20), size: const Size(110, 110), ), ], ).build(), ); expect( root.findAllAnnotations<int>(position).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 2, localPosition: position), const AnnotationEntry<int>(annotation: 1, localPosition: Offset(10, 10)), const AnnotationEntry<int>(annotation: 1000, localPosition: position), ]), ); }, ); test( 'AnnotatedRegionLayer.findAllAnnotations should clip its annotation ' 'using size and offset (negative)', () { // The target position would have fallen inside if not for the offset. const Offset position = Offset(10, 10); final Layer root = _withBackgroundAnnotation( 1000, _Layers( AnnotatedRegionLayer<int>( 1, size: const Size(20, 20), offset: const Offset(90, 90), ), children: <Object>[ _TestAnnotatedLayer(2, opaque: false, size: const Size(110, 110)), ], ).build(), ); expect( root.findAllAnnotations<int>(position).entries.toList(), _equalToAnnotationResult<int>(<AnnotationEntry<int>>[ const AnnotationEntry<int>(annotation: 2, localPosition: position), const AnnotationEntry<int>(annotation: 1000, localPosition: position), ]), ); }, ); } /// A [ContainerLayer] that contains a stack of layers: `layer` in the front, /// and another layer annotated with `value` in the back. /// /// It is a utility function that helps checking the opacity returned by /// [Layer.findAnnotations]. Layer _withBackgroundAnnotation(int value, Layer layer) { return _Layers( ContainerLayer(), children: <Object>[ _TestAnnotatedLayer(value, opaque: false), layer, ], ).build(); } // A utility class that helps building a layer tree. class _Layers { _Layers(this.root, {this.children}); final ContainerLayer root; // Each element must be instance of Layer or _Layers. final List<Object>? children; bool _assigned = false; // Build the layer tree by calling each child's `build`, then append children // to [root]. Returns the root. Layer build() { assert(!_assigned); _assigned = true; if (children != null) { for (final Object child in children!) { late Layer layer; if (child is Layer) { layer = child; } else if (child is _Layers) { layer = child.build(); } else { assert(false, 'Element of _Layers.children must be instance of Layer or _Layers'); } root.append(layer); } } return root; } } // This layer's [findAnnotation] can be controlled by the given arguments. class _TestAnnotatedLayer extends Layer { _TestAnnotatedLayer( this.value, { required this.opaque, this.offset = Offset.zero, this.size, }); // The value added to result in [findAnnotations] during a successful hit. final int value; // The return value of [findAnnotations] during a successful hit. final bool opaque; /// The [offset] is optionally used to translate the clip region for the /// hit-testing of [find] by [offset]. /// /// If not provided, offset defaults to [Offset.zero]. /// /// Ignored if [size] is not set. final Offset offset; /// The [size] is optionally used to clip the hit-testing of [find]. /// /// If not provided, all offsets are considered to be contained within this /// layer, unless an ancestor layer applies a clip. /// /// If [offset] is set, then the offset is applied to the size region before /// hit testing in [find]. final Size? size; @override EngineLayer? addToScene(SceneBuilder builder) { return null; } // This implementation is hit when the type is `int` and position is within // [offset] & [size]. If it is hit, it adds [value] to result and returns // [opaque]; otherwise it directly returns false. @override bool findAnnotations<S extends Object>( AnnotationResult<S> result, Offset localPosition, { required bool onlyFirst, }) { if (S != int) return false; if (size != null && !(offset & size!).contains(localPosition)) return false; final Object untypedValue = value; final S typedValue = untypedValue as S; result.add(AnnotationEntry<S>(annotation: typedValue, localPosition: localPosition)); return opaque; } } bool _almostEqual(double a, double b, double maxRelativeDiff) { assert(maxRelativeDiff >= 0); assert(maxRelativeDiff < 1); return (a - b).abs() <= a.abs() * maxRelativeDiff; } Matcher _equalToAnnotationResult<T>( List<AnnotationEntry<int>> list, { double maxCoordinateRelativeDiff = 0, }) { return pairwiseCompare<AnnotationEntry<int>, AnnotationEntry<int>>( list, (AnnotationEntry<int> a, AnnotationEntry<int> b) { return a.annotation == b.annotation && _almostEqual(a.localPosition.dx, b.localPosition.dx, maxCoordinateRelativeDiff) && _almostEqual(a.localPosition.dy, b.localPosition.dy, maxCoordinateRelativeDiff); }, 'equal to', ); }