| // 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('ContainerLayer.toImageSync 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.toImageSync(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); |
| }); |
| |
| test('Layer types that support rasterization', () { |
| // Supported. |
| final OffsetLayer offsetLayer = OffsetLayer(); |
| final OpacityLayer opacityLayer = OpacityLayer(); |
| final ClipRectLayer clipRectLayer = ClipRectLayer(); |
| final ClipRRectLayer clipRRectLayer = ClipRRectLayer(); |
| final ImageFilterLayer imageFilterLayer = ImageFilterLayer(); |
| final BackdropFilterLayer backdropFilterLayer = BackdropFilterLayer(); |
| final PhysicalModelLayer physicalModelLayer = PhysicalModelLayer(); |
| final ColorFilterLayer colorFilterLayer = ColorFilterLayer(); |
| final ShaderMaskLayer shaderMaskLayer = ShaderMaskLayer(); |
| final TextureLayer textureLayer = TextureLayer(rect: Rect.zero, textureId: 1); |
| expect(offsetLayer.supportsRasterization(), true); |
| expect(opacityLayer.supportsRasterization(), true); |
| expect(clipRectLayer.supportsRasterization(), true); |
| expect(clipRRectLayer.supportsRasterization(), true); |
| expect(imageFilterLayer.supportsRasterization(), true); |
| expect(backdropFilterLayer.supportsRasterization(), true); |
| expect(physicalModelLayer.supportsRasterization(), true); |
| expect(colorFilterLayer.supportsRasterization(), true); |
| expect(shaderMaskLayer.supportsRasterization(), true); |
| expect(textureLayer.supportsRasterization(), true); |
| |
| // Unsupported. |
| final PlatformViewLayer platformViewLayer = PlatformViewLayer(rect: Rect.zero, viewId: 1); |
| |
| expect(platformViewLayer.supportsRasterization(), false); |
| }); |
| } |
| |
| 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 {} |