// 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/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'rendering_tester.dart'; void main() { TestRenderingFlutterBinding.ensureInitialized(); test('non-painted layers are detached', () { RenderObject boundary, inner; final RenderOpacity root = RenderOpacity( child: boundary = RenderRepaintBoundary( child: inner = RenderDecoratedBox( decoration: const BoxDecoration(), ), ), ); layout(root, phase: EnginePhase.paint); expect(inner.isRepaintBoundary, isFalse); expect(inner.debugLayer, null); expect(boundary.isRepaintBoundary, isTrue); expect(boundary.debugLayer, isNotNull); expect(boundary.debugLayer!.attached, isTrue); // this time it painted... root.opacity = 0.0; pumpFrame(phase: EnginePhase.paint); expect(inner.isRepaintBoundary, isFalse); expect(inner.debugLayer, null); expect(boundary.isRepaintBoundary, isTrue); expect(boundary.debugLayer, isNotNull); expect(boundary.debugLayer!.attached, isFalse); // this time it did not. root.opacity = 0.5; pumpFrame(phase: EnginePhase.paint); expect(inner.isRepaintBoundary, isFalse); expect(inner.debugLayer, null); expect(boundary.isRepaintBoundary, isTrue); expect(boundary.debugLayer, isNotNull); expect(boundary.debugLayer!.attached, isTrue); // this time it did again! }); test('updateSubtreeNeedsAddToScene propagates Layer.alwaysNeedsAddToScene up the tree', () { final ContainerLayer a = ContainerLayer(); final ContainerLayer b = ContainerLayer(); final ContainerLayer c = ContainerLayer(); final _TestAlwaysNeedsAddToSceneLayer d = _TestAlwaysNeedsAddToSceneLayer(); final ContainerLayer e = ContainerLayer(); final ContainerLayer f = ContainerLayer(); // Tree structure: // a // / \ // b c // / \ // (x)d e // / // f a.append(b); a.append(c); b.append(d); b.append(e); d.append(f); a.debugMarkClean(); b.debugMarkClean(); c.debugMarkClean(); d.debugMarkClean(); e.debugMarkClean(); f.debugMarkClean(); expect(a.debugSubtreeNeedsAddToScene, false); expect(b.debugSubtreeNeedsAddToScene, false); expect(c.debugSubtreeNeedsAddToScene, false); expect(d.debugSubtreeNeedsAddToScene, false); expect(e.debugSubtreeNeedsAddToScene, false); expect(f.debugSubtreeNeedsAddToScene, false); a.updateSubtreeNeedsAddToScene(); expect(a.debugSubtreeNeedsAddToScene, true); expect(b.debugSubtreeNeedsAddToScene, true); expect(c.debugSubtreeNeedsAddToScene, false); expect(d.debugSubtreeNeedsAddToScene, true); expect(e.debugSubtreeNeedsAddToScene, false); expect(f.debugSubtreeNeedsAddToScene, false); }); test('updateSubtreeNeedsAddToScene propagates Layer._needsAddToScene up the tree', () { final ContainerLayer a = ContainerLayer(); final ContainerLayer b = ContainerLayer(); final ContainerLayer c = ContainerLayer(); final ContainerLayer d = ContainerLayer(); final ContainerLayer e = ContainerLayer(); final ContainerLayer f = ContainerLayer(); final ContainerLayer g = ContainerLayer(); final List<ContainerLayer> allLayers = <ContainerLayer>[a, b, c, d, e, f, g]; // The tree is like the following where b and j are dirty: // a____ // / \ // (x)b___ c // / \ \ | // d e f g(x) a.append(b); a.append(c); b.append(d); b.append(e); b.append(f); c.append(g); for (final ContainerLayer layer in allLayers) { expect(layer.debugSubtreeNeedsAddToScene, true); } for (final ContainerLayer layer in allLayers) { layer.debugMarkClean(); } for (final ContainerLayer layer in allLayers) { expect(layer.debugSubtreeNeedsAddToScene, false); } b.markNeedsAddToScene(); a.updateSubtreeNeedsAddToScene(); expect(a.debugSubtreeNeedsAddToScene, true); expect(b.debugSubtreeNeedsAddToScene, true); expect(c.debugSubtreeNeedsAddToScene, false); expect(d.debugSubtreeNeedsAddToScene, false); expect(e.debugSubtreeNeedsAddToScene, false); expect(f.debugSubtreeNeedsAddToScene, false); expect(g.debugSubtreeNeedsAddToScene, false); g.markNeedsAddToScene(); a.updateSubtreeNeedsAddToScene(); expect(a.debugSubtreeNeedsAddToScene, true); expect(b.debugSubtreeNeedsAddToScene, true); expect(c.debugSubtreeNeedsAddToScene, true); expect(d.debugSubtreeNeedsAddToScene, false); expect(e.debugSubtreeNeedsAddToScene, false); expect(f.debugSubtreeNeedsAddToScene, false); expect(g.debugSubtreeNeedsAddToScene, true); a.buildScene(SceneBuilder()); for (final ContainerLayer layer in allLayers) { expect(layer.debugSubtreeNeedsAddToScene, false); } }); test('follower layers are always dirty', () { final LayerLink link = LayerLink(); final LeaderLayer leaderLayer = LeaderLayer(link: link); final FollowerLayer followerLayer = FollowerLayer(link: link); leaderLayer.debugMarkClean(); followerLayer.debugMarkClean(); leaderLayer.updateSubtreeNeedsAddToScene(); followerLayer.updateSubtreeNeedsAddToScene(); expect(followerLayer.debugSubtreeNeedsAddToScene, true); }); test('switching layer link of an attached leader layer should not crash', () { final LayerLink link = LayerLink(); final LeaderLayer leaderLayer = LeaderLayer(link: link); final RenderView view = RenderView(configuration: const ViewConfiguration(), window: RendererBinding.instance.window); leaderLayer.attach(view); final LayerLink link2 = LayerLink(); leaderLayer.link = link2; // This should not crash. leaderLayer.detach(); expect(leaderLayer.link, link2); }); test('layer link attach/detach order should not crash app.', () { final LayerLink link = LayerLink(); final LeaderLayer leaderLayer1 = LeaderLayer(link: link); final LeaderLayer leaderLayer2 = LeaderLayer(link: link); final RenderView view = RenderView(configuration: const ViewConfiguration(), window: RendererBinding.instance.window); leaderLayer1.attach(view); leaderLayer2.attach(view); leaderLayer2.detach(); leaderLayer1.detach(); expect(link.leader, isNull); }); test('leader layers not dirty when connected to follower layer', () { final ContainerLayer root = ContainerLayer()..attach(Object()); final LayerLink link = LayerLink(); final LeaderLayer leaderLayer = LeaderLayer(link: link); final FollowerLayer followerLayer = FollowerLayer(link: link); root.append(leaderLayer); root.append(followerLayer); leaderLayer.debugMarkClean(); followerLayer.debugMarkClean(); leaderLayer.updateSubtreeNeedsAddToScene(); followerLayer.updateSubtreeNeedsAddToScene(); expect(leaderLayer.debugSubtreeNeedsAddToScene, false); }); test('leader layers are not dirty when all followers disconnects', () { final ContainerLayer root = ContainerLayer()..attach(Object()); final LayerLink link = LayerLink(); final LeaderLayer leaderLayer = LeaderLayer(link: link); root.append(leaderLayer); // Does not need add to scene when nothing is connected to link. leaderLayer.debugMarkClean(); leaderLayer.updateSubtreeNeedsAddToScene(); expect(leaderLayer.debugSubtreeNeedsAddToScene, false); // Connecting a follower does not require adding to scene final FollowerLayer follower1 = FollowerLayer(link: link); root.append(follower1); leaderLayer.debugMarkClean(); leaderLayer.updateSubtreeNeedsAddToScene(); expect(leaderLayer.debugSubtreeNeedsAddToScene, false); final FollowerLayer follower2 = FollowerLayer(link: link); root.append(follower2); leaderLayer.debugMarkClean(); leaderLayer.updateSubtreeNeedsAddToScene(); expect(leaderLayer.debugSubtreeNeedsAddToScene, false); // Disconnecting one follower, still does not needs add to scene. follower2.remove(); leaderLayer.debugMarkClean(); leaderLayer.updateSubtreeNeedsAddToScene(); expect(leaderLayer.debugSubtreeNeedsAddToScene, false); // Disconnecting all followers goes back to not requiring add to scene. follower1.remove(); leaderLayer.debugMarkClean(); leaderLayer.updateSubtreeNeedsAddToScene(); expect(leaderLayer.debugSubtreeNeedsAddToScene, false); }); test('LeaderLayer.applyTransform can be called after retained rendering', () { void expectTransform(RenderObject leader) { final LeaderLayer leaderLayer = leader.debugLayer! as LeaderLayer; final Matrix4 expected = Matrix4.identity() ..translate(leaderLayer.offset.dx, leaderLayer.offset.dy); final Matrix4 transformed = Matrix4.identity(); leaderLayer.applyTransform(null, transformed); expect(transformed, expected); } final LayerLink link = LayerLink(); late RenderLeaderLayer leader; final RenderRepaintBoundary root = RenderRepaintBoundary( child:RenderRepaintBoundary( child: leader = RenderLeaderLayer(link: link), ), ); layout(root, phase: EnginePhase.composite); expectTransform(leader); // Causes a repaint, but the LeaderLayer of RenderLeaderLayer will be added // as retained and LeaderLayer.addChildrenToScene will not be called. root.markNeedsPaint(); pumpFrame(phase: EnginePhase.composite); // The LeaderLayer.applyTransform call shouldn't crash. expectTransform(leader); }); test('depthFirstIterateChildren', () { final ContainerLayer a = ContainerLayer(); final ContainerLayer b = ContainerLayer(); final ContainerLayer c = ContainerLayer(); final ContainerLayer d = ContainerLayer(); final ContainerLayer e = ContainerLayer(); final ContainerLayer f = ContainerLayer(); final ContainerLayer g = ContainerLayer(); final PictureLayer h = PictureLayer(Rect.zero); final PictureLayer i = PictureLayer(Rect.zero); final PictureLayer j = PictureLayer(Rect.zero); // The tree is like the following: // a____ // / \ // b___ c // / \ \ | // d e f g // / \ | // h i j a.append(b); a.append(c); b.append(d); b.append(e); b.append(f); d.append(h); d.append(i); c.append(g); g.append(j); expect( a.depthFirstIterateChildren(), <Layer>[b, d, h, i, e, f, c, g, j], ); d.remove(); // a____ // / \ // b___ c // \ \ | // e f g // | // j expect( a.depthFirstIterateChildren(), <Layer>[b, e, f, c, g, j], ); }); void checkNeedsAddToScene(Layer layer, void Function() mutateCallback) { layer.debugMarkClean(); layer.updateSubtreeNeedsAddToScene(); expect(layer.debugSubtreeNeedsAddToScene, false); mutateCallback(); layer.updateSubtreeNeedsAddToScene(); expect(layer.debugSubtreeNeedsAddToScene, true); } List<String> getDebugInfo(Layer layer) { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); layer.debugFillProperties(builder); return builder.properties .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) .map((DiagnosticsNode node) => node.toString()).toList(); } test('ClipRectLayer prints clipBehavior in debug info', () { expect(getDebugInfo(ClipRectLayer()), contains('clipBehavior: Clip.hardEdge')); expect( getDebugInfo(ClipRectLayer(clipBehavior: Clip.antiAliasWithSaveLayer)), contains('clipBehavior: Clip.antiAliasWithSaveLayer'), ); }); test('ClipRRectLayer prints clipBehavior in debug info', () { expect(getDebugInfo(ClipRRectLayer()), contains('clipBehavior: Clip.antiAlias')); expect( getDebugInfo(ClipRRectLayer(clipBehavior: Clip.antiAliasWithSaveLayer)), contains('clipBehavior: Clip.antiAliasWithSaveLayer'), ); }); test('ClipPathLayer prints clipBehavior in debug info', () { expect(getDebugInfo(ClipPathLayer()), contains('clipBehavior: Clip.antiAlias')); expect( getDebugInfo(ClipPathLayer(clipBehavior: Clip.antiAliasWithSaveLayer)), contains('clipBehavior: Clip.antiAliasWithSaveLayer'), ); }); test('BackdropFilterLayer prints filter and blendMode in debug info', () { final ImageFilter filter = ImageFilter.blur(sigmaX: 1.0, sigmaY: 1.0, tileMode: TileMode.repeated); final BackdropFilterLayer layer = BackdropFilterLayer(filter: filter, blendMode: BlendMode.clear); final List<String> info = getDebugInfo(layer); expect(info, contains(isBrowser ? 'filter: ImageFilter.blur(1, 1, TileMode.repeated)' : 'filter: ImageFilter.blur(1.0, 1.0, repeated)')); expect(info, contains('blendMode: clear')); }); test('PictureLayer prints picture, raster cache hints in debug info', () { final PictureRecorder recorder = PictureRecorder(); final Canvas canvas = Canvas(recorder); canvas.drawPaint(Paint()); final Picture picture = recorder.endRecording(); final PictureLayer layer = PictureLayer(const Rect.fromLTRB(0, 0, 1, 1)); layer.picture = picture; layer.isComplexHint = true; layer.willChangeHint = false; final List<String> info = getDebugInfo(layer); expect(info, contains('picture: ${describeIdentity(picture)}')); expect(info, isNot(contains('engine layer: ${describeIdentity(null)}'))); expect(info, contains('raster cache hints: isComplex = true, willChange = false')); }); test('Layer prints engineLayer if it is not null in debug info', () { final ConcreteLayer layer = ConcreteLayer(); List<String> info = getDebugInfo(layer); expect(info, isNot(contains('engine layer: ${describeIdentity(null)}'))); layer.engineLayer = FakeEngineLayer(); info = getDebugInfo(layer); expect(info, contains('engine layer: ${describeIdentity(layer.engineLayer)}')); }); test('mutating PictureLayer fields triggers needsAddToScene', () { final PictureLayer pictureLayer = PictureLayer(Rect.zero); checkNeedsAddToScene(pictureLayer, () { final PictureRecorder recorder = PictureRecorder(); Canvas(recorder); pictureLayer.picture = recorder.endRecording(); }); pictureLayer.isComplexHint = false; checkNeedsAddToScene(pictureLayer, () { pictureLayer.isComplexHint = true; }); pictureLayer.willChangeHint = false; checkNeedsAddToScene(pictureLayer, () { pictureLayer.willChangeHint = true; }); }); const Rect unitRect = Rect.fromLTRB(0, 0, 1, 1); test('mutating PerformanceOverlayLayer fields triggers needsAddToScene', () { final PerformanceOverlayLayer layer = PerformanceOverlayLayer( overlayRect: Rect.zero, optionsMask: 0, rasterizerThreshold: 0, checkerboardRasterCacheImages: false, checkerboardOffscreenLayers: false, ); checkNeedsAddToScene(layer, () { layer.overlayRect = unitRect; }); }); test('mutating OffsetLayer fields triggers needsAddToScene', () { final OffsetLayer layer = OffsetLayer(); checkNeedsAddToScene(layer, () { layer.offset = const Offset(1, 1); }); }); test('mutating ClipRectLayer fields triggers needsAddToScene', () { final ClipRectLayer layer = ClipRectLayer(clipRect: Rect.zero); checkNeedsAddToScene(layer, () { layer.clipRect = unitRect; }); checkNeedsAddToScene(layer, () { layer.clipBehavior = Clip.antiAliasWithSaveLayer; }); }); test('mutating ClipRRectLayer fields triggers needsAddToScene', () { final ClipRRectLayer layer = ClipRRectLayer(clipRRect: RRect.zero); checkNeedsAddToScene(layer, () { layer.clipRRect = RRect.fromRectAndRadius(unitRect, Radius.zero); }); checkNeedsAddToScene(layer, () { layer.clipBehavior = Clip.antiAliasWithSaveLayer; }); }); test('mutating ClipPath fields triggers needsAddToScene', () { final ClipPathLayer layer = ClipPathLayer(clipPath: Path()); checkNeedsAddToScene(layer, () { final Path newPath = Path(); newPath.addRect(unitRect); layer.clipPath = newPath; }); checkNeedsAddToScene(layer, () { layer.clipBehavior = Clip.antiAliasWithSaveLayer; }); }); test('mutating OpacityLayer fields triggers needsAddToScene', () { final OpacityLayer layer = OpacityLayer(alpha: 0); checkNeedsAddToScene(layer, () { layer.alpha = 1; }); checkNeedsAddToScene(layer, () { layer.offset = const Offset(1, 1); }); }); test('mutating ColorFilterLayer fields triggers needsAddToScene', () { final ColorFilterLayer layer = ColorFilterLayer( colorFilter: const ColorFilter.mode(Color(0xFFFF0000), BlendMode.color), ); checkNeedsAddToScene(layer, () { layer.colorFilter = const ColorFilter.mode(Color(0xFF00FF00), BlendMode.color); }); }); test('mutating ShaderMaskLayer fields triggers needsAddToScene', () { const Gradient gradient = RadialGradient(colors: <Color>[Color(0x00000000), Color(0x00000001)]); final Shader shader = gradient.createShader(Rect.zero); final ShaderMaskLayer layer = ShaderMaskLayer(shader: shader, maskRect: Rect.zero, blendMode: BlendMode.clear); checkNeedsAddToScene(layer, () { layer.maskRect = unitRect; }); checkNeedsAddToScene(layer, () { layer.blendMode = BlendMode.color; }); checkNeedsAddToScene(layer, () { layer.shader = gradient.createShader(unitRect); }); }); test('mutating BackdropFilterLayer fields triggers needsAddToScene', () { final BackdropFilterLayer layer = BackdropFilterLayer(filter: ImageFilter.blur()); checkNeedsAddToScene(layer, () { layer.filter = ImageFilter.blur(sigmaX: 1.0); }); }); test('mutating PhysicalModelLayer fields triggers needsAddToScene', () { final PhysicalModelLayer layer = PhysicalModelLayer( clipPath: Path(), elevation: 0, color: const Color(0x00000000), shadowColor: const Color(0x00000000), ); checkNeedsAddToScene(layer, () { final Path newPath = Path(); newPath.addRect(unitRect); layer.clipPath = newPath; }); checkNeedsAddToScene(layer, () { layer.elevation = 1; }); checkNeedsAddToScene(layer, () { layer.color = const Color(0x00000001); }); checkNeedsAddToScene(layer, () { layer.shadowColor = const Color(0x00000001); }); }); test('ContainerLayer.toImage can render interior layer', () { final OffsetLayer parent = OffsetLayer(); final OffsetLayer child = OffsetLayer(); final OffsetLayer grandChild = OffsetLayer(); child.append(grandChild); parent.append(child); // This renders the layers and generates engine layers. parent.buildScene(SceneBuilder()); // Causes grandChild to pass its engine layer as `oldLayer` grandChild.toImage(const Rect.fromLTRB(0, 0, 10, 10)); // Ensure we can render the same scene again after rendering an interior // layer. parent.buildScene(SceneBuilder()); }, skip: isBrowser); // TODO(yjbanov): `toImage` doesn't work on the Web: https://github.com/flutter/flutter/issues/49857 test('PictureLayer does not let you call dispose unless refcount is 0', () { PictureLayer layer = PictureLayer(Rect.zero); expect(layer.debugHandleCount, 0); layer.dispose(); expect(layer.debugDisposed, true); layer = PictureLayer(Rect.zero); final LayerHandle<PictureLayer> handle = LayerHandle<PictureLayer>(layer); expect(layer.debugHandleCount, 1); expect(() => layer.dispose(), throwsAssertionError); handle.layer = null; expect(layer.debugHandleCount, 0); expect(layer.debugDisposed, true); expect(() => layer.dispose(), throwsAssertionError); // already disposed. }); test('Layer append/remove increases/decreases handle count', () { final PictureLayer layer = PictureLayer(Rect.zero); final ContainerLayer parent = ContainerLayer(); expect(layer.debugHandleCount, 0); expect(layer.debugDisposed, false); parent.append(layer); expect(layer.debugHandleCount, 1); expect(layer.debugDisposed, false); layer.remove(); expect(layer.debugHandleCount, 0); expect(layer.debugDisposed, true); }); test('Layer.dispose disposes the engineLayer', () { final Layer layer = ConcreteLayer(); final FakeEngineLayer engineLayer = FakeEngineLayer(); layer.engineLayer = engineLayer; expect(engineLayer.disposed, false); layer.dispose(); expect(engineLayer.disposed, true); expect(layer.engineLayer, null); }); test('Layer.engineLayer (set) disposes the engineLayer', () { final Layer layer = ConcreteLayer(); final FakeEngineLayer engineLayer = FakeEngineLayer(); layer.engineLayer = engineLayer; expect(engineLayer.disposed, false); layer.engineLayer = null; expect(engineLayer.disposed, true); }); test('PictureLayer.picture (set) disposes the picture', () { final PictureLayer layer = PictureLayer(Rect.zero); final FakePicture picture = FakePicture(); layer.picture = picture; expect(picture.disposed, false); layer.picture = null; expect(picture.disposed, true); }); test('PictureLayer disposes the picture', () { final PictureLayer layer = PictureLayer(Rect.zero); final FakePicture picture = FakePicture(); layer.picture = picture; expect(picture.disposed, false); layer.dispose(); expect(picture.disposed, true); }); test('LayerHandle disposes the layer', () { final ConcreteLayer layer = ConcreteLayer(); final ConcreteLayer layer2 = ConcreteLayer(); expect(layer.debugHandleCount, 0); expect(layer2.debugHandleCount, 0); final LayerHandle<ConcreteLayer> holder = LayerHandle<ConcreteLayer>(layer); expect(layer.debugHandleCount, 1); expect(layer.debugDisposed, false); expect(layer2.debugHandleCount, 0); expect(layer2.debugDisposed, false); holder.layer = layer; expect(layer.debugHandleCount, 1); expect(layer.debugDisposed, false); expect(layer2.debugHandleCount, 0); expect(layer2.debugDisposed, false); holder.layer = layer2; expect(layer.debugHandleCount, 0); expect(layer.debugDisposed, true); expect(layer2.debugHandleCount, 1); expect(layer2.debugDisposed, false); holder.layer = null; expect(layer.debugHandleCount, 0); expect(layer.debugDisposed, true); expect(layer2.debugHandleCount, 0); expect(layer2.debugDisposed, true); expect(() => holder.layer = layer, throwsAssertionError); }); test('OpacityLayer does not push an OffsetLayer if there are no children', () { final OpacityLayer layer = OpacityLayer(alpha: 128); final FakeSceneBuilder builder = FakeSceneBuilder(); layer.addToScene(builder); expect(builder.pushedOpacity, false); expect(builder.pushedOffset, false); expect(builder.addedPicture, false); expect(layer.engineLayer, null); layer.append(PictureLayer(Rect.largest)..picture = FakePicture()); builder.reset(); layer.addToScene(builder); expect(builder.pushedOpacity, true); expect(builder.pushedOffset, false); expect(builder.addedPicture, true); expect(layer.engineLayer, isA<FakeOpacityEngineLayer>()); builder.reset(); layer.alpha = 200; expect(layer.engineLayer, isA<FakeOpacityEngineLayer>()); layer.alpha = 255; expect(layer.engineLayer, null); builder.reset(); layer.addToScene(builder); expect(builder.pushedOpacity, false); expect(builder.pushedOffset, true); expect(builder.addedPicture, true); expect(layer.engineLayer, isA<FakeOffsetEngineLayer>()); layer.alpha = 200; expect(layer.engineLayer, null); builder.reset(); layer.addToScene(builder); expect(builder.pushedOpacity, true); expect(builder.pushedOffset, false); expect(builder.addedPicture, true); expect(layer.engineLayer, isA<FakeOpacityEngineLayer>()); }); test('OpacityLayer dispose its engineLayer if there are no children', () { final OpacityLayer layer = OpacityLayer(alpha: 128); final FakeSceneBuilder builder = FakeSceneBuilder(); layer.addToScene(builder); expect(layer.engineLayer, null); layer.append(PictureLayer(Rect.largest)..picture = FakePicture()); layer.addToScene(builder); expect(layer.engineLayer, isA<FakeOpacityEngineLayer>()); layer.removeAllChildren(); layer.addToScene(builder); expect(layer.engineLayer, null); }); test('Layers describe clip bounds', () { ContainerLayer layer = ContainerLayer(); expect(layer.describeClipBounds(), null); const Rect bounds = Rect.fromLTRB(10, 10, 20, 20); final RRect rbounds = RRect.fromRectXY(bounds, 2, 2); layer = ClipRectLayer(clipRect: bounds); expect(layer.describeClipBounds(), bounds); layer = ClipRRectLayer(clipRRect: rbounds); expect(layer.describeClipBounds(), rbounds.outerRect); layer = ClipPathLayer(clipPath: Path()..addRect(bounds)); expect(layer.describeClipBounds(), bounds); }); test('Subtree has composition callbacks', () { final ContainerLayer root = ContainerLayer(); expect(root.subtreeHasCompositionCallbacks, false); final List<VoidCallback> cancellationCallbacks = <VoidCallback>[]; cancellationCallbacks.add(root.addCompositionCallback((_) {})); expect(root.subtreeHasCompositionCallbacks, true); final ContainerLayer a1 = ContainerLayer(); final ContainerLayer a2 = ContainerLayer(); final ContainerLayer b1 = ContainerLayer(); root.append(a1); root.append(a2); a1.append(b1); expect(root.subtreeHasCompositionCallbacks, true); expect(a1.subtreeHasCompositionCallbacks, false); expect(a2.subtreeHasCompositionCallbacks, false); expect(b1.subtreeHasCompositionCallbacks, false); cancellationCallbacks.add(b1.addCompositionCallback((_) {})); expect(root.subtreeHasCompositionCallbacks, true); expect(a1.subtreeHasCompositionCallbacks, true); expect(a2.subtreeHasCompositionCallbacks, false); expect(b1.subtreeHasCompositionCallbacks, true); cancellationCallbacks.removeAt(0)(); expect(root.subtreeHasCompositionCallbacks, true); expect(a1.subtreeHasCompositionCallbacks, true); expect(a2.subtreeHasCompositionCallbacks, false); expect(b1.subtreeHasCompositionCallbacks, true); cancellationCallbacks.removeAt(0)(); expect(root.subtreeHasCompositionCallbacks, false); expect(a1.subtreeHasCompositionCallbacks, false); expect(a2.subtreeHasCompositionCallbacks, false); expect(b1.subtreeHasCompositionCallbacks, false); }); test('Subtree has composition callbacks - removeChild', () { final ContainerLayer root = ContainerLayer(); expect(root.subtreeHasCompositionCallbacks, false); final ContainerLayer a1 = ContainerLayer(); final ContainerLayer a2 = ContainerLayer(); final ContainerLayer b1 = ContainerLayer(); root.append(a1); root.append(a2); a1.append(b1); expect(b1.subtreeHasCompositionCallbacks, false); expect(a1.subtreeHasCompositionCallbacks, false); expect(root.subtreeHasCompositionCallbacks, false); expect(a2.subtreeHasCompositionCallbacks, false); b1.addCompositionCallback((_) { }); expect(b1.subtreeHasCompositionCallbacks, true); expect(a1.subtreeHasCompositionCallbacks, true); expect(root.subtreeHasCompositionCallbacks, true); expect(a2.subtreeHasCompositionCallbacks, false); b1.remove(); expect(b1.subtreeHasCompositionCallbacks, true); expect(a1.subtreeHasCompositionCallbacks, false); expect(root.subtreeHasCompositionCallbacks, false); expect(a2.subtreeHasCompositionCallbacks, false); }); test('No callback if removed', () { final ContainerLayer root = ContainerLayer(); final ContainerLayer a1 = ContainerLayer(); final ContainerLayer a2 = ContainerLayer(); final ContainerLayer b1 = ContainerLayer(); root.append(a1); root.append(a2); a1.append(b1); // Add and immediately remove the callback. b1.addCompositionCallback((Layer layer) { fail('Should not have called back'); })(); root.buildScene(SceneBuilder()).dispose(); }); test('Observe layer tree composition - not retained', () { final ContainerLayer root = ContainerLayer(); final ContainerLayer a1 = ContainerLayer(); final ContainerLayer a2 = ContainerLayer(); final ContainerLayer b1 = ContainerLayer(); root.append(a1); root.append(a2); a1.append(b1); bool compositedB1 = false; b1.addCompositionCallback((Layer layer) { expect(layer, b1); compositedB1 = true; }); expect(compositedB1, false); root.buildScene(SceneBuilder()).dispose(); expect(compositedB1, true); }); test('Observe layer tree composition - retained', () { final ContainerLayer root = ContainerLayer(); final ContainerLayer a1 = ContainerLayer(); final ContainerLayer a2 = ContainerLayer(); final ContainerLayer b1 = ContainerLayer(); root.append(a1); root.append(a2); a1.append(b1); // Actually build the retained layer so that the engine sees it as real and // reusable. SceneBuilder builder = SceneBuilder(); b1.engineLayer = builder.pushOffset(0, 0); builder.build().dispose(); builder = SceneBuilder(); // Force the layer to appear clean and have an engine layer for retained // rendering. expect(b1.engineLayer, isNotNull); b1.debugMarkClean(); expect(b1.debugSubtreeNeedsAddToScene, false); bool compositedB1 = false; b1.addCompositionCallback((Layer layer) { expect(layer, b1); compositedB1 = true; }); expect(compositedB1, false); root.buildScene(builder).dispose(); expect(compositedB1, true); }); test('Observe layer tree composition - asserts on mutation', () { final ContainerLayer root = ContainerLayer(); final ContainerLayer a1 = ContainerLayer(); final ContainerLayer a2 = ContainerLayer(); final ContainerLayer b1 = ContainerLayer(); root.append(a1); root.append(a2); a1.append(b1); bool compositedB1 = false; b1.addCompositionCallback((Layer layer) { expect(layer, b1); expect(() => layer.remove(), throwsAssertionError); expect(() => layer.dispose(), throwsAssertionError); expect(() => layer.markNeedsAddToScene(), throwsAssertionError); expect(() => layer.debugMarkClean(), throwsAssertionError); expect(() => layer.updateSubtreeNeedsAddToScene(), throwsAssertionError); expect(() => layer.dropChild(ContainerLayer()), throwsAssertionError); expect(() => layer.adoptChild(ContainerLayer()), throwsAssertionError); expect(() => (layer as ContainerLayer).append(ContainerLayer()), throwsAssertionError); expect(() => layer.engineLayer = null, throwsAssertionError); compositedB1 = true; }); expect(compositedB1, false); root.buildScene(SceneBuilder()).dispose(); expect(compositedB1, true); }); test('Observe layer tree composition - detach triggers callback', () { final ContainerLayer root = ContainerLayer(); final ContainerLayer a1 = ContainerLayer(); final ContainerLayer a2 = ContainerLayer(); final ContainerLayer b1 = ContainerLayer(); root.append(a1); root.append(a2); a1.append(b1); bool compositedB1 = false; b1.addCompositionCallback((Layer layer) { expect(layer, b1); compositedB1 = true; }); root.attach(Object()); expect(compositedB1, false); root.detach(); expect(compositedB1, true); }); test('Observe layer tree composition - observer count correctly maintained', () { final ContainerLayer root = ContainerLayer(); final ContainerLayer a1 = ContainerLayer(); root.append(a1); expect(root.subtreeHasCompositionCallbacks, false); expect(a1.subtreeHasCompositionCallbacks, false); final VoidCallback remover1 = a1.addCompositionCallback((_) { }); final VoidCallback remover2 = a1.addCompositionCallback((_) { }); expect(root.subtreeHasCompositionCallbacks, true); expect(a1.subtreeHasCompositionCallbacks, true); remover1(); expect(root.subtreeHasCompositionCallbacks, true); expect(a1.subtreeHasCompositionCallbacks, true); remover2(); expect(root.subtreeHasCompositionCallbacks, false); expect(a1.subtreeHasCompositionCallbacks, false); }); test('Double removing a observe callback throws', () { final ContainerLayer root = ContainerLayer(); final VoidCallback callback = root.addCompositionCallback((_) { }); callback(); expect(() => callback(), throwsAssertionError); }); test('Removing an observe callback on a disposed layer does not throw', () { final ContainerLayer root = ContainerLayer(); final VoidCallback callback = root.addCompositionCallback((_) { }); root.dispose(); expect(() => callback(), returnsNormally); }); } class FakeEngineLayer extends Fake implements EngineLayer { bool disposed = false; @override void dispose() { assert(!disposed); disposed = true; } } class FakePicture extends Fake implements Picture { bool disposed = false; @override void dispose() { assert(!disposed); disposed = true; } } class ConcreteLayer extends Layer { @override void addToScene(SceneBuilder builder) {} } class _TestAlwaysNeedsAddToSceneLayer extends ContainerLayer { @override bool get alwaysNeedsAddToScene => true; } class FakeSceneBuilder extends Fake implements SceneBuilder { void reset() { pushedOpacity = false; pushedOffset = false; addedPicture = false; } bool pushedOpacity = false; bool pushedOffset = false; bool addedPicture = false; @override dynamic noSuchMethod(Invocation invocation) { // Use noSuchMethod forwarding instead of override these methods to make it easier // for these methods to add new optional arguments in the future. switch (invocation.memberName) { case #pushOpacity: pushedOpacity = true; return FakeOpacityEngineLayer(); case #pushOffset: pushedOffset = true; return FakeOffsetEngineLayer(); case #addPicture: addedPicture = true; return; case #pop: return; } super.noSuchMethod(invocation); } } class FakeOpacityEngineLayer extends FakeEngineLayer implements OpacityEngineLayer {} class FakeOffsetEngineLayer extends FakeEngineLayer implements OffsetEngineLayer {}