| // Copyright 2013 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:typed_data'; |
| import 'dart:ui'; |
| |
| import 'package:litetest/litetest.dart'; |
| |
| void main() { |
| test('Scene.toImageSync succeeds', () async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| const Color color = Color(0xFF123456); |
| canvas.drawPaint(Paint()..color = color); |
| final Picture picture = recorder.endRecording(); |
| final SceneBuilder builder = SceneBuilder(); |
| builder.pushOffset(10, 10); |
| builder.addPicture(const Offset(5, 5), picture); |
| final Scene scene = builder.build(); |
| |
| final Image image = scene.toImageSync(6, 8); |
| picture.dispose(); |
| scene.dispose(); |
| |
| expect(image.width, 6); |
| expect(image.height, 8); |
| |
| final ByteData? data = await image.toByteData(); |
| |
| expect(data, isNotNull); |
| expect(data!.lengthInBytes, 6 * 8 * 4); |
| expect(data.buffer.asUint8List()[0], 0x12); |
| expect(data.buffer.asUint8List()[1], 0x34); |
| expect(data.buffer.asUint8List()[2], 0x56); |
| expect(data.buffer.asUint8List()[3], 0xFF); |
| }); |
| |
| test('Scene.toImageSync succeeds with texture layer', () async { |
| final SceneBuilder builder = SceneBuilder(); |
| builder.pushOffset(10, 10); |
| builder.addTexture(0, width: 10, height: 10); |
| |
| final Scene scene = builder.build(); |
| final Image image = scene.toImageSync(20, 20); |
| scene.dispose(); |
| |
| expect(image.width, 20); |
| expect(image.height, 20); |
| |
| final ByteData? data = await image.toByteData(); |
| |
| expect(data, isNotNull); |
| expect(data!.lengthInBytes, 20 * 20 * 4); |
| expect(data.buffer.asUint8List()[0], 0); |
| expect(data.buffer.asUint8List()[1], 0); |
| expect(data.buffer.asUint8List()[2], 0); |
| expect(data.buffer.asUint8List()[3], 0); |
| }); |
| |
| test('addPicture with disposed picture does not crash', () { |
| bool assertsEnabled = false; |
| assert(() { |
| assertsEnabled = true; |
| return true; |
| }()); |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| canvas.drawPaint(Paint()); |
| final Picture picture = recorder.endRecording(); |
| picture.dispose(); |
| |
| assert(picture.debugDisposed); |
| |
| final SceneBuilder builder = SceneBuilder(); |
| if (assertsEnabled) { |
| expect( |
| () => builder.addPicture(Offset.zero, picture), |
| throwsA(isInstanceOf<AssertionError>()), |
| ); |
| } else { |
| builder.addPicture(Offset.zero, picture); |
| } |
| |
| final Scene scene = builder.build(); |
| scene.dispose(); |
| }); |
| |
| test('pushTransform validates the matrix', () { |
| final SceneBuilder builder = SceneBuilder(); |
| final Float64List matrix4 = Float64List.fromList(<double>[ |
| 1, 0, 0, 0, |
| 0, 1, 0, 0, |
| 0, 0, 1, 0, |
| 0, 0, 0, 1, |
| ]); |
| expect(builder.pushTransform(matrix4), isNotNull); |
| |
| final Float64List matrix4WrongLength = Float64List.fromList(<double>[ |
| 1, 0, 0, 0, |
| 0, 1, 0, |
| 0, 0, 1, 0, |
| 0, 0, 0, |
| ]); |
| assert(() { |
| expect( |
| () => builder.pushTransform(matrix4WrongLength), |
| expectAssertion, |
| ); |
| return true; |
| }()); |
| |
| final Float64List matrix4NaN = Float64List.fromList(<double>[ |
| 1, 0, 0, 0, |
| 0, 1, 0, 0, |
| 0, 0, 1, 0, |
| 0, 0, 0, double.nan, |
| ]); |
| assert(() { |
| expect( |
| () => builder.pushTransform(matrix4NaN), |
| expectAssertion, |
| ); |
| return true; |
| }()); |
| |
| final Float64List matrix4Infinity = Float64List.fromList(<double>[ |
| 1, 0, 0, 0, |
| 0, 1, 0, 0, |
| 0, 0, 1, 0, |
| 0, 0, 0, double.infinity, |
| ]); |
| assert(() { |
| expect( |
| () => builder.pushTransform(matrix4Infinity), |
| expectAssertion, |
| ); |
| return true; |
| }()); |
| }); |
| |
| test('SceneBuilder accepts typed layers', () { |
| final SceneBuilder builder1 = SceneBuilder(); |
| final OpacityEngineLayer opacity1 = builder1.pushOpacity(100); |
| expect(opacity1, isNotNull); |
| builder1.pop(); |
| builder1.build(); |
| |
| final SceneBuilder builder2 = SceneBuilder(); |
| final OpacityEngineLayer opacity2 = builder2.pushOpacity(200, oldLayer: opacity1); |
| expect(opacity2, isNotNull); |
| builder2.pop(); |
| builder2.build(); |
| }); |
| |
| // Attempts to use the same layer first as `oldLayer` then in `addRetained`. |
| void testPushThenIllegalRetain(_TestNoSharingFunction pushFunction) { |
| final SceneBuilder builder1 = SceneBuilder(); |
| final EngineLayer layer = pushFunction(builder1, null); |
| builder1.pop(); |
| builder1.build(); |
| |
| final SceneBuilder builder2 = SceneBuilder(); |
| pushFunction(builder2, layer); |
| builder2.pop(); |
| assert(() { |
| try { |
| builder2.addRetained(layer); |
| fail('Expected addRetained to throw AssertionError but it returned successully'); |
| } on AssertionError catch (error) { |
| expect(error.toString(), contains('The layer is already being used')); |
| } |
| return true; |
| }()); |
| builder2.build(); |
| } |
| |
| // Attempts to use the same layer first in `addRetained` then as `oldLayer`. |
| void testAddRetainedThenIllegalPush(_TestNoSharingFunction pushFunction) { |
| final SceneBuilder builder1 = SceneBuilder(); |
| final EngineLayer layer = pushFunction(builder1, null); |
| builder1.pop(); |
| builder1.build(); |
| |
| final SceneBuilder builder2 = SceneBuilder(); |
| builder2.addRetained(layer); |
| assert(() { |
| try { |
| pushFunction(builder2, layer); |
| fail('Expected push to throw AssertionError but it returned successully'); |
| } on AssertionError catch (error) { |
| expect(error.toString(), contains('The layer is already being used')); |
| } |
| return true; |
| }()); |
| builder2.build(); |
| } |
| |
| // Attempts to retain the same layer twice in the same scene. |
| void testDoubleAddRetained(_TestNoSharingFunction pushFunction) { |
| final SceneBuilder builder1 = SceneBuilder(); |
| final EngineLayer layer = pushFunction(builder1, null); |
| builder1.pop(); |
| builder1.build(); |
| |
| final SceneBuilder builder2 = SceneBuilder(); |
| builder2.addRetained(layer); |
| assert(() { |
| try { |
| builder2.addRetained(layer); |
| fail('Expected second addRetained to throw AssertionError but it returned successully'); |
| } on AssertionError catch (error) { |
| expect(error.toString(), contains('The layer is already being used')); |
| } |
| return true; |
| }()); |
| builder2.build(); |
| } |
| |
| // Attempts to use the same layer as `oldLayer` twice in the same scene. |
| void testPushOldLayerTwice(_TestNoSharingFunction pushFunction) { |
| final SceneBuilder builder1 = SceneBuilder(); |
| final EngineLayer layer = pushFunction(builder1, null); |
| builder1.pop(); |
| builder1.build(); |
| |
| final SceneBuilder builder2 = SceneBuilder(); |
| pushFunction(builder2, layer); |
| assert(() { |
| try { |
| pushFunction(builder2, layer); |
| fail('Expected push to throw AssertionError but it returned successully'); |
| } on AssertionError catch (error) { |
| expect(error.toString(), contains('was previously used as oldLayer')); |
| } |
| return true; |
| }()); |
| builder2.build(); |
| } |
| |
| // Attempts to use a child of a retained layer as an `oldLayer`. |
| void testPushChildLayerOfRetainedLayer(_TestNoSharingFunction pushFunction) { |
| final SceneBuilder builder1 = SceneBuilder(); |
| final EngineLayer layer = pushFunction(builder1, null); |
| final OpacityEngineLayer childLayer = builder1.pushOpacity(123); |
| builder1.pop(); |
| builder1.pop(); |
| builder1.build(); |
| |
| final SceneBuilder builder2 = SceneBuilder(); |
| builder2.addRetained(layer); |
| assert(() { |
| try { |
| builder2.pushOpacity(321, oldLayer: childLayer); |
| fail('Expected pushOpacity to throw AssertionError but it returned successully'); |
| } on AssertionError catch (error) { |
| expect(error.toString(), contains('The layer is already being used')); |
| } |
| return true; |
| }()); |
| builder2.build(); |
| } |
| |
| // Attempts to retain a layer whose child is already used as `oldLayer` elsewhere in the scene. |
| void testRetainParentLayerOfPushedChild(_TestNoSharingFunction pushFunction) { |
| final SceneBuilder builder1 = SceneBuilder(); |
| final EngineLayer layer = pushFunction(builder1, null); |
| final OpacityEngineLayer childLayer = builder1.pushOpacity(123); |
| builder1.pop(); |
| builder1.pop(); |
| builder1.build(); |
| |
| final SceneBuilder builder2 = SceneBuilder(); |
| builder2.pushOpacity(234, oldLayer: childLayer); |
| builder2.pop(); |
| assert(() { |
| try { |
| builder2.addRetained(layer); |
| fail('Expected addRetained to throw AssertionError but it returned successully'); |
| } on AssertionError catch (error) { |
| expect(error.toString(), contains('The layer is already being used')); |
| } |
| return true; |
| }()); |
| builder2.build(); |
| } |
| |
| // Attempts to retain a layer that has been used as `oldLayer` in a previous frame. |
| void testRetainOldLayer(_TestNoSharingFunction pushFunction) { |
| final SceneBuilder builder1 = SceneBuilder(); |
| final EngineLayer layer = pushFunction(builder1, null); |
| builder1.pop(); |
| builder1.build(); |
| |
| final SceneBuilder builder2 = SceneBuilder(); |
| pushFunction(builder2, layer); |
| builder2.pop(); |
| assert(() { |
| try { |
| final SceneBuilder builder3 = SceneBuilder(); |
| builder3.addRetained(layer); |
| fail('Expected addRetained to throw AssertionError but it returned successully'); |
| } on AssertionError catch (error) { |
| expect(error.toString(), contains('was previously used as oldLayer')); |
| } |
| return true; |
| }()); |
| builder2.build(); |
| } |
| |
| // Attempts to pass layer as `oldLayer` that has been used as `oldLayer` in a previous frame. |
| void testPushOldLayer(_TestNoSharingFunction pushFunction) { |
| final SceneBuilder builder1 = SceneBuilder(); |
| final EngineLayer layer = pushFunction(builder1, null); |
| builder1.pop(); |
| builder1.build(); |
| |
| final SceneBuilder builder2 = SceneBuilder(); |
| pushFunction(builder2, layer); |
| builder2.pop(); |
| assert(() { |
| try { |
| final SceneBuilder builder3 = SceneBuilder(); |
| pushFunction(builder3, layer); |
| fail('Expected addRetained to throw AssertionError but it returned successully'); |
| } on AssertionError catch (error) { |
| expect(error.toString(), contains('was previously used as oldLayer')); |
| } |
| return true; |
| }()); |
| builder2.build(); |
| } |
| |
| // Attempts to retain a parent of a layer used as `oldLayer` in a previous frame. |
| void testRetainsParentOfOldLayer(_TestNoSharingFunction pushFunction) { |
| final SceneBuilder builder1 = SceneBuilder(); |
| final EngineLayer parentLayer = pushFunction(builder1, null); |
| final OpacityEngineLayer childLayer = builder1.pushOpacity(123); |
| builder1.pop(); |
| builder1.pop(); |
| builder1.build(); |
| |
| final SceneBuilder builder2 = SceneBuilder(); |
| builder2.pushOpacity(321, oldLayer: childLayer); |
| builder2.pop(); |
| assert(() { |
| try { |
| final SceneBuilder builder3 = SceneBuilder(); |
| builder3.addRetained(parentLayer); |
| fail('Expected addRetained to throw AssertionError but it returned successully'); |
| } on AssertionError catch (error) { |
| expect(error.toString(), contains('was previously used as oldLayer')); |
| } |
| return true; |
| }()); |
| builder2.build(); |
| } |
| |
| void testNoSharing(_TestNoSharingFunction pushFunction) { |
| testPushThenIllegalRetain(pushFunction); |
| testAddRetainedThenIllegalPush(pushFunction); |
| testDoubleAddRetained(pushFunction); |
| testPushOldLayerTwice(pushFunction); |
| testPushChildLayerOfRetainedLayer(pushFunction); |
| testRetainParentLayerOfPushedChild(pushFunction); |
| testRetainOldLayer(pushFunction); |
| testPushOldLayer(pushFunction); |
| testRetainsParentOfOldLayer(pushFunction); |
| } |
| |
| test('SceneBuilder does not share a layer between addRetained and push*', () { |
| testNoSharing((SceneBuilder builder, EngineLayer? oldLayer) { |
| return builder.pushOffset(0, 0, oldLayer: oldLayer as OffsetEngineLayer?); |
| }); |
| testNoSharing((SceneBuilder builder, EngineLayer? oldLayer) { |
| return builder.pushTransform(Float64List(16), oldLayer: oldLayer as TransformEngineLayer?); |
| }); |
| testNoSharing((SceneBuilder builder, EngineLayer? oldLayer) { |
| return builder.pushClipRect(Rect.zero, oldLayer: oldLayer as ClipRectEngineLayer?); |
| }); |
| testNoSharing((SceneBuilder builder, EngineLayer? oldLayer) { |
| return builder.pushClipRRect(RRect.zero, oldLayer: oldLayer as ClipRRectEngineLayer?); |
| }); |
| testNoSharing((SceneBuilder builder, EngineLayer? oldLayer) { |
| return builder.pushClipPath(Path(), oldLayer: oldLayer as ClipPathEngineLayer?); |
| }); |
| testNoSharing((SceneBuilder builder, EngineLayer? oldLayer) { |
| return builder.pushOpacity(100, oldLayer: oldLayer as OpacityEngineLayer?); |
| }); |
| testNoSharing((SceneBuilder builder, EngineLayer? oldLayer) { |
| return builder.pushBackdropFilter(ImageFilter.blur(), oldLayer: oldLayer as BackdropFilterEngineLayer?); |
| }); |
| testNoSharing((SceneBuilder builder, EngineLayer? oldLayer) { |
| return builder.pushShaderMask( |
| Gradient.radial( |
| Offset.zero, |
| 10, |
| const <Color>[Color.fromARGB(0, 0, 0, 0), Color.fromARGB(0, 255, 255, 255)], |
| ), |
| Rect.zero, |
| BlendMode.color, |
| oldLayer: oldLayer as ShaderMaskEngineLayer?, |
| ); |
| }); |
| testNoSharing((SceneBuilder builder, EngineLayer? oldLayer) { |
| return builder.pushColorFilter( |
| const ColorFilter.mode( |
| Color.fromARGB(0, 0, 0, 0), |
| BlendMode.color, |
| ), |
| oldLayer: oldLayer as ColorFilterEngineLayer?, |
| ); |
| }); |
| testNoSharing((SceneBuilder builder, EngineLayer? oldLayer) { |
| return builder.pushColorFilter( |
| const ColorFilter.matrix(<double>[ |
| 1, 0, 0, 0, 0, |
| 0, 1, 0, 0, 0, |
| 0, 0, 1, 0, 0, |
| 0, 0, 0, 1, 0, |
| ]), |
| oldLayer: oldLayer as ColorFilterEngineLayer?, |
| ); |
| }); |
| testNoSharing((SceneBuilder builder, EngineLayer? oldLayer) { |
| return builder.pushColorFilter( |
| const ColorFilter.linearToSrgbGamma(), |
| oldLayer: oldLayer as ColorFilterEngineLayer?, |
| ); |
| }); |
| testNoSharing((SceneBuilder builder, EngineLayer? oldLayer) { |
| return builder.pushColorFilter( |
| const ColorFilter.srgbToLinearGamma(), |
| oldLayer: oldLayer as ColorFilterEngineLayer?, |
| ); |
| }); |
| testNoSharing((SceneBuilder builder, EngineLayer? oldLayer) { |
| return builder.pushImageFilter( |
| ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), |
| oldLayer: oldLayer as ImageFilterEngineLayer?, |
| ); |
| }); |
| testNoSharing((SceneBuilder builder, EngineLayer? oldLayer) { |
| return builder.pushImageFilter( |
| ImageFilter.dilate(radiusX: 10.0, radiusY: 10.0), |
| oldLayer: oldLayer as ImageFilterEngineLayer?, |
| ); |
| }); |
| testNoSharing((SceneBuilder builder, EngineLayer? oldLayer) { |
| return builder.pushImageFilter( |
| ImageFilter.erode(radiusX: 10.0, radiusY: 10.0), |
| oldLayer: oldLayer as ImageFilterEngineLayer?, |
| ); |
| }); |
| testNoSharing((SceneBuilder builder, EngineLayer? oldLayer) { |
| return builder.pushImageFilter( |
| ImageFilter.matrix(Float64List.fromList(<double>[ |
| 1, 0, 0, 0, |
| 0, 1, 0, 0, |
| 0, 0, 1, 0, |
| 0, 0, 0, 1, |
| ])), |
| oldLayer: oldLayer as ImageFilterEngineLayer?, |
| ); |
| }); |
| }); |
| } |
| |
| typedef _TestNoSharingFunction = EngineLayer Function(SceneBuilder builder, EngineLayer? oldLayer); |