diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml index bbef280f85336..4779b21430011 100644 --- a/lib/web_ui/dev/goldens_lock.yaml +++ b/lib/web_ui/dev/goldens_lock.yaml @@ -1,2 +1,2 @@ repository: https://github.com/flutter/goldens.git -revision: 06e0333b8371965dce5dc05e140e6dfb454f33fa +revision: ac75f12c6e93461369e1391da6cc20bf8cb08829 diff --git a/lib/web_ui/dev/test_runner.dart b/lib/web_ui/dev/test_runner.dart index 872698d20c848..9a67335cf0905 100644 --- a/lib/web_ui/dev/test_runner.dart +++ b/lib/web_ui/dev/test_runner.dart @@ -436,35 +436,39 @@ class TestCommand extends Command with ArgUtils { 'test', )); - if (isUnitTestsScreenshotsAvailable) { - // Separate screenshot tests from unit-tests. Screenshot tests must run - // one at a time. Otherwise, they will end up screenshotting each other. - // This is not an issue for unit-tests. - final FilePath failureSmokeTestPath = FilePath.fromWebUi( - 'test/golden_tests/golden_failure_smoke_test.dart', - ); - final List screenshotTestFiles = []; - final List unitTestFiles = []; - - for (io.File testFile - in testDir.listSync(recursive: true).whereType()) { - final FilePath testFilePath = FilePath.fromCwd(testFile.path); - if (!testFilePath.absolute.endsWith('_test.dart')) { - // Not a test file at all. Skip. - continue; - } - if (testFilePath == failureSmokeTestPath) { - // A smoke test that fails on purpose. Skip. - continue; - } + // Separate screenshot tests from unit-tests. Screenshot tests must run + // one at a time. Otherwise, they will end up screenshotting each other. + // This is not an issue for unit-tests. + final FilePath failureSmokeTestPath = FilePath.fromWebUi( + 'test/golden_tests/golden_failure_smoke_test.dart', + ); + final List screenshotTestFiles = []; + final List unitTestFiles = []; + + for (io.File testFile + in testDir.listSync(recursive: true).whereType()) { + final FilePath testFilePath = FilePath.fromCwd(testFile.path); + if (!testFilePath.absolute.endsWith('_test.dart')) { + // Not a test file at all. Skip. + continue; + } + if (testFilePath == failureSmokeTestPath) { + // A smoke test that fails on purpose. Skip. + continue; + } - if (path.split(testFilePath.relativeToWebUi).contains('golden_tests')) { - screenshotTestFiles.add(testFilePath); - } else { - unitTestFiles.add(testFilePath); - } + // All files under test/golden_tests are considered golden tests. + final bool isUnderGoldenTestsDirectory = path.split(testFilePath.relativeToWebUi).contains('golden_tests'); + // Any file whose name ends with "_golden_test.dart" is run as a golden test. + final bool isGoldenTestFile = path.basename(testFilePath.relativeToWebUi).endsWith('_golden_test.dart'); + if (isUnderGoldenTestsDirectory || isGoldenTestFile) { + screenshotTestFiles.add(testFilePath); + } else { + unitTestFiles.add(testFilePath); } + } + if (isUnitTestsScreenshotsAvailable) { // This test returns a non-zero exit code on purpose. Run it separately. if (io.Platform.environment['CIRRUS_CI'] != 'true') { await _runTestBatch( @@ -474,11 +478,13 @@ class TestCommand extends Command with ArgUtils { ); _checkExitCode(); } + } - // Run all unit-tests as a single batch. - await _runTestBatch(unitTestFiles, concurrency: 10, expectFailure: false); - _checkExitCode(); + // Run all unit-tests as a single batch. + await _runTestBatch(unitTestFiles, concurrency: 10, expectFailure: false); + _checkExitCode(); + if (isUnitTestsScreenshotsAvailable) { // Run screenshot tests one at a time. for (FilePath testFilePath in screenshotTestFiles) { await _runTestBatch( @@ -488,24 +494,6 @@ class TestCommand extends Command with ArgUtils { ); _checkExitCode(); } - } else { - final List unitTestFiles = []; - for (io.File testFile - in testDir.listSync(recursive: true).whereType()) { - final FilePath testFilePath = FilePath.fromCwd(testFile.path); - if (!testFilePath.absolute.endsWith('_test.dart')) { - // Not a test file at all. Skip. - continue; - } - if (!path - .split(testFilePath.relativeToWebUi) - .contains('golden_tests')) { - unitTestFiles.add(testFilePath); - } - } - // Run all unit-tests as a single batch. - await _runTestBatch(unitTestFiles, concurrency: 10, expectFailure: false); - _checkExitCode(); } } diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvas.dart b/lib/web_ui/lib/src/engine/canvaskit/canvas.dart index 5703e6105e8c9..4333a15c80631 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvas.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvas.dart @@ -5,6 +5,10 @@ // @dart = 2.12 part of engine; +/// Memoized value for ClipOp.Intersect, so we don't have to hit JS-interop +/// every time we need it. +final SkClipOp _clipOpIntersect = canvasKit.ClipOp.Intersect; + /// A Dart wrapper around Skia's [SkCanvas]. /// /// This is intentionally not memory-managing the underlying [SkCanvas]. See @@ -20,12 +24,9 @@ class CkCanvas { skCanvas.clear(toSharedSkColor1(color)); } - static final SkClipOp _clipOpIntersect = canvasKit.ClipOp.Intersect; - - void clipPath(ui.Path path, bool doAntiAlias) { - final CkPath ckPath = path as CkPath; + void clipPath(CkPath path, bool doAntiAlias) { skCanvas.clipPath( - ckPath.skiaObject, + path.skiaObject, _clipOpIntersect, doAntiAlias, ); @@ -66,15 +67,14 @@ class CkCanvas { void drawAtlasRaw( CkPaint paint, - ui.Image atlas, + CkImage atlas, Float32List rstTransforms, Float32List rects, List? colors, ui.BlendMode blendMode, ) { - final CkImage skAtlas = atlas as CkImage; skCanvas.drawAtlas( - skAtlas.skImage, + atlas.skImage, rects, rstTransforms, paint.skiaObject, @@ -107,20 +107,18 @@ class CkCanvas { ); } - void drawImage(ui.Image image, ui.Offset offset, CkPaint paint) { - final CkImage skImage = image as CkImage; + void drawImage(CkImage image, ui.Offset offset, CkPaint paint) { skCanvas.drawImage( - skImage.skImage, + image.skImage, offset.dx, offset.dy, paint.skiaObject, ); } - void drawImageRect(ui.Image image, ui.Rect src, ui.Rect dst, CkPaint paint) { - final CkImage skImage = image as CkImage; + void drawImageRect(CkImage image, ui.Rect src, ui.Rect dst, CkPaint paint) { skCanvas.drawImageRect( - skImage.skImage, + image.skImage, toSkRect(src), toSkRect(dst), paint.skiaObject, @@ -129,10 +127,9 @@ class CkCanvas { } void drawImageNine( - ui.Image image, ui.Rect center, ui.Rect dst, CkPaint paint) { - final CkImage skImage = image as CkImage; + CkImage image, ui.Rect center, ui.Rect dst, CkPaint paint) { skCanvas.drawImageNine( - skImage.skImage, + image.skImage, toSkRect(center), toSkRect(dst), paint.skiaObject, @@ -173,7 +170,7 @@ class CkCanvas { } void drawPicture(CkPicture picture) { - skCanvas.drawPicture(picture.skiaObject.skiaObject); + skCanvas.drawPicture(picture.skiaObject); } void drawPoints(CkPaint paint, ui.PointMode pointMode, Float32List points) { @@ -195,17 +192,16 @@ class CkCanvas { skCanvas.drawRect(toSkRect(rect), paint.skiaObject); } - void drawShadow(ui.Path path, ui.Color color, double elevation, + void drawShadow(CkPath path, ui.Color color, double elevation, bool transparentOccluder) { - drawSkShadow(skCanvas, path as CkPath, color, elevation, + drawSkShadow(skCanvas, path, color, elevation, transparentOccluder, ui.window.devicePixelRatio); } void drawVertices( - ui.Vertices vertices, ui.BlendMode blendMode, CkPaint paint) { - CkVertices skVertices = vertices as CkVertices; + CkVertices vertices, ui.BlendMode blendMode, CkPaint paint) { skCanvas.drawVertices( - skVertices.skiaObject, + vertices.skiaObject, toSkBlendMode(blendMode), paint.skiaObject, ); @@ -227,17 +223,17 @@ class CkCanvas { return skCanvas.save(); } - void saveLayer(ui.Rect bounds, CkPaint paint) { + void saveLayer(ui.Rect bounds, CkPaint? paint) { skCanvas.saveLayer( - paint.skiaObject, + paint?.skiaObject, toSkRect(bounds), null, null, ); } - void saveLayerWithoutBounds(CkPaint paint) { - skCanvas.saveLayer(paint.skiaObject, null, null, null); + void saveLayerWithoutBounds(CkPaint? paint) { + skCanvas.saveLayer(paint?.skiaObject, null, null, null); } void saveLayerWithFilter(ui.Rect bounds, ui.ImageFilter filter) { @@ -266,7 +262,811 @@ class CkCanvas { skCanvas.translate(dx, dy); } - void flush() { - skCanvas.flush(); + CkPictureSnapshot? get pictureSnapshot => null; +} + +class RecordingCkCanvas extends CkCanvas { + RecordingCkCanvas(SkCanvas skCanvas, ui.Rect bounds) + : pictureSnapshot = CkPictureSnapshot(bounds), + super(skCanvas); + + @override + final CkPictureSnapshot pictureSnapshot; + + void _addCommand(CkPaintCommand command) { + pictureSnapshot._commands.add(command); + } + + @override + void clear(ui.Color color) { + super.clear(color); + _addCommand(CkClearCommand(color)); + } + + @override + void clipPath(CkPath path, bool doAntiAlias) { + super.clipPath(path, doAntiAlias); + _addCommand(CkClipPathCommand(path, doAntiAlias)); + } + + @override + void clipRRect(ui.RRect rrect, bool doAntiAlias) { + super.clipRRect(rrect, doAntiAlias); + _addCommand(CkClipRRectCommand(rrect, doAntiAlias)); + } + + @override + void clipRect(ui.Rect rect, ui.ClipOp clipOp, bool doAntiAlias) { + super.clipRect(rect, clipOp, doAntiAlias); + _addCommand(CkClipRectCommand(rect, clipOp, doAntiAlias)); + } + + @override + void drawArc( + ui.Rect oval, + double startAngle, + double sweepAngle, + bool useCenter, + CkPaint paint, + ) { + super.drawArc(oval, startAngle, sweepAngle, useCenter, paint); + _addCommand(CkDrawArcCommand(oval, startAngle, sweepAngle, useCenter, paint)); + } + + @override + void drawAtlasRaw( + CkPaint paint, + CkImage atlas, + Float32List rstTransforms, + Float32List rects, + List? colors, + ui.BlendMode blendMode, + ) { + super.drawAtlasRaw(paint, atlas, rstTransforms, rects, colors, blendMode); + _addCommand(CkDrawAtlasCommand(paint, atlas, rstTransforms, rects, colors, blendMode)); + } + + @override + void drawCircle(ui.Offset c, double radius, CkPaint paint) { + super.drawCircle(c, radius, paint); + _addCommand(CkDrawCircleCommand(c, radius, paint)); + } + + @override + void drawColor(ui.Color color, ui.BlendMode blendMode) { + super.drawColor(color, blendMode); + _addCommand(CkDrawColorCommand(color, blendMode)); + } + + @override + void drawDRRect(ui.RRect outer, ui.RRect inner, CkPaint paint) { + super.drawDRRect(outer, inner, paint); + _addCommand(CkDrawDRRectCommand(outer, inner, paint)); + } + + @override + void drawImage(CkImage image, ui.Offset offset, CkPaint paint) { + super.drawImage(image, offset, paint); + _addCommand(CkDrawImageCommand(image, offset, paint)); + } + + @override + void drawImageRect(CkImage image, ui.Rect src, ui.Rect dst, CkPaint paint) { + super.drawImageRect(image, src, dst, paint); + _addCommand(CkDrawImageRectCommand(image, src, dst, paint)); + } + + @override + void drawImageNine( + CkImage image, ui.Rect center, ui.Rect dst, CkPaint paint) { + super.drawImageNine(image, center, dst, paint); + _addCommand(CkDrawImageNineCommand(image, center, dst, paint)); + } + + @override + void drawLine(ui.Offset p1, ui.Offset p2, CkPaint paint) { + super.drawLine(p1, p2, paint); + _addCommand(CkDrawLineCommand(p1, p2, paint)); + } + + @override + void drawOval(ui.Rect rect, CkPaint paint) { + super.drawOval(rect, paint); + _addCommand(CkDrawOvalCommand(rect, paint)); + } + + @override + void drawPaint(CkPaint paint) { + super.drawPaint(paint); + _addCommand(CkDrawPaintCommand(paint)); + } + + @override + void drawParagraph(CkParagraph paragraph, ui.Offset offset) { + super.drawParagraph(paragraph, offset); + _addCommand(CkDrawParagraphCommand(paragraph, offset)); + } + + @override + void drawPath(CkPath path, CkPaint paint) { + super.drawPath(path, paint); + _addCommand(CkDrawPathCommand(path, paint)); + } + + @override + void drawPicture(CkPicture picture) { + super.drawPicture(picture); + _addCommand(CkDrawPictureCommand(picture)); + } + + @override + void drawPoints(CkPaint paint, ui.PointMode pointMode, Float32List points) { + super.drawPoints(paint, pointMode, points); + _addCommand(CkDrawPointsCommand(pointMode, points, paint)); + } + + @override + void drawRRect(ui.RRect rrect, CkPaint paint) { + super.drawRRect(rrect, paint); + _addCommand(CkDrawRRectCommand(rrect, paint)); + } + + @override + void drawRect(ui.Rect rect, CkPaint paint) { + super.drawRect(rect, paint); + _addCommand(CkDrawRectCommand(rect, paint)); + } + + @override + void drawShadow(CkPath path, ui.Color color, double elevation, + bool transparentOccluder) { + super.drawShadow(path, color, elevation, transparentOccluder); + _addCommand(CkDrawShadowCommand(path, color, elevation, transparentOccluder)); + } + + @override + void drawVertices( + CkVertices vertices, ui.BlendMode blendMode, CkPaint paint) { + super.drawVertices(vertices, blendMode, paint); + _addCommand(CkDrawVerticesCommand(vertices, blendMode, paint)); + } + + @override + void restore() { + super.restore(); + _addCommand(const CkRestoreCommand()); + } + + @override + void restoreToCount(int count) { + super.restoreToCount(count); + _addCommand(CkRestoreToCountCommand(count)); + } + + @override + void rotate(double radians) { + super.rotate(radians); + _addCommand(CkRotateCommand(radians)); + } + + @override + int save() { + _addCommand(const CkSaveCommand()); + return super.save(); + } + + @override + void saveLayer(ui.Rect bounds, CkPaint? paint) { + super.saveLayer(bounds, paint); + _addCommand(CkSaveLayerCommand(bounds, paint)); + } + + @override + void saveLayerWithoutBounds(CkPaint? paint) { + super.saveLayerWithoutBounds(paint); + _addCommand(CkSaveLayerWithoutBoundsCommand(paint)); + } + + @override + void saveLayerWithFilter(ui.Rect bounds, ui.ImageFilter filter) { + super.saveLayerWithFilter(bounds, filter); + _addCommand(CkSaveLayerWithFilterCommand(bounds, filter)); + } + + @override + void scale(double sx, double sy) { + super.scale(sx, sy); + _addCommand(CkScaleCommand(sx, sy)); + } + + @override + void skew(double sx, double sy) { + super.skew(sx, sy); + _addCommand(CkSkewCommand(sx, sy)); + } + + @override + void transform(Float32List matrix4) { + super.transform(matrix4); + _addCommand(CkTransformCommand(matrix4)); + } + + @override + void translate(double dx, double dy) { + super.translate(dx, dy); + _addCommand(CkTranslateCommand(dx, dy)); + } +} + +class CkPictureSnapshot { + CkPictureSnapshot(this._bounds); + + final ui.Rect _bounds; + final List _commands = []; + + SkPicture toPicture() { + final SkPictureRecorder recorder = SkPictureRecorder(); + final Float32List skRect = toSkRect(_bounds); + final SkCanvas skCanvas = recorder.beginRecording(skRect); + for (final CkPaintCommand command in _commands) { + command.apply(skCanvas); + } + final SkPicture skPicture = recorder.finishRecordingAsPicture(); + recorder.delete(); + return skPicture; + } + + void dispose() { + for (final CkPaintCommand command in _commands) { + command.dispose(); + } + } +} + +/// A paint command recorded by [RecordingCkCanvas]. +/// +/// # Special rules when drawing images +/// +/// A command painting an image must clone the original image to bump the ref +/// count. Otherwise when the framework decides it doesn't need the image any +/// more it will bump the ref count down and delete the underlying Skia object, +/// leaving the picture that recorded this paint command with a dangling +/// pointer. If we attempt to resurrect the picture we'll hit a use-after-free +/// error. The command must call [CkImage.dispose] in its [dispose] +/// implementation. +abstract class CkPaintCommand { + const CkPaintCommand(); + + /// Applies the command onto the [canvas]. + void apply(SkCanvas canvas); + + /// Frees resources associated with the command. + void dispose() {} +} + +class CkClearCommand extends CkPaintCommand { + const CkClearCommand(this.color); + + final ui.Color color; + + @override + void apply(SkCanvas canvas) { + canvas.clear(toSharedSkColor1(color)); + } +} + +class CkSaveCommand extends CkPaintCommand { + const CkSaveCommand(); + + @override + void apply(SkCanvas canvas) { + canvas.save(); + } +} + +class CkRestoreCommand extends CkPaintCommand { + const CkRestoreCommand(); + + @override + void apply(SkCanvas canvas) { + canvas.restore(); + } +} + +class CkRestoreToCountCommand extends CkPaintCommand { + const CkRestoreToCountCommand(this.count); + + final int count; + + @override + void apply(SkCanvas canvas) { + canvas.restoreToCount(count); + } +} + +class CkTranslateCommand extends CkPaintCommand { + final double dx; + final double dy; + + CkTranslateCommand(this.dx, this.dy); + + @override + void apply(SkCanvas canvas) { + canvas.translate(dx, dy); + } +} + +class CkScaleCommand extends CkPaintCommand { + final double sx; + final double sy; + + CkScaleCommand(this.sx, this.sy); + + @override + void apply(SkCanvas canvas) { + canvas.scale(sx, sy); + } +} + +class CkRotateCommand extends CkPaintCommand { + final double radians; + + CkRotateCommand(this.radians); + + @override + void apply(SkCanvas canvas) { + canvas.rotate(radians * 180.0 / math.pi, 0.0, 0.0); + } +} + +class CkTransformCommand extends CkPaintCommand { + final Float32List matrix4; + + CkTransformCommand(this.matrix4); + + @override + void apply(SkCanvas canvas) { + canvas.concat(toSkMatrixFromFloat32(matrix4)); + } +} + +class CkSkewCommand extends CkPaintCommand { + final double sx; + final double sy; + + CkSkewCommand(this.sx, this.sy); + + @override + void apply(SkCanvas canvas) { + canvas.skew(sx, sy); + } +} + +class CkClipRectCommand extends CkPaintCommand { + final ui.Rect rect; + final ui.ClipOp clipOp; + final bool doAntiAlias; + + CkClipRectCommand(this.rect, this.clipOp, this.doAntiAlias); + + @override + void apply(SkCanvas canvas) { + canvas.clipRect( + toSkRect(rect), + toSkClipOp(clipOp), + doAntiAlias, + ); + } +} + +class CkDrawArcCommand extends CkPaintCommand { + CkDrawArcCommand(this.oval, this.startAngle, this.sweepAngle, this.useCenter, this.paint); + + final ui.Rect oval; + final double startAngle; + final double sweepAngle; + final bool useCenter; + final CkPaint paint; + + @override + void apply(SkCanvas canvas) { + const double toDegrees = 180 / math.pi; + canvas.drawArc( + toSkRect(oval), + startAngle * toDegrees, + sweepAngle * toDegrees, + useCenter, + paint.skiaObject, + ); + } +} + +class CkDrawAtlasCommand extends CkPaintCommand { + CkDrawAtlasCommand(this.paint, this.atlas, this.rstTransforms, this.rects, this.colors, this.blendMode); + + final CkPaint paint; + final CkImage atlas; + final Float32List rstTransforms; + final Float32List rects; + final List? colors; + final ui.BlendMode blendMode; + + @override + void apply(SkCanvas canvas) { + canvas.drawAtlas( + atlas.skImage, + rects, + rstTransforms, + paint.skiaObject, + toSkBlendMode(blendMode), + colors, + ); + } +} + +class CkClipRRectCommand extends CkPaintCommand { + final ui.RRect rrect; + final bool doAntiAlias; + + CkClipRRectCommand(this.rrect, this.doAntiAlias); + + @override + void apply(SkCanvas canvas) { + canvas.clipRRect( + toSkRRect(rrect), + _clipOpIntersect, + doAntiAlias, + ); + } +} + +class CkClipPathCommand extends CkPaintCommand { + final CkPath path; + final bool doAntiAlias; + + CkClipPathCommand(this.path, this.doAntiAlias); + + @override + void apply(SkCanvas canvas) { + canvas.clipPath( + path.skiaObject, + _clipOpIntersect, + doAntiAlias, + ); + } +} + +class CkDrawColorCommand extends CkPaintCommand { + final ui.Color color; + final ui.BlendMode blendMode; + + CkDrawColorCommand(this.color, this.blendMode); + + @override + void apply(SkCanvas canvas) { + canvas.drawColorInt( + color.value, + toSkBlendMode(blendMode), + ); + } +} + +class CkDrawLineCommand extends CkPaintCommand { + final ui.Offset p1; + final ui.Offset p2; + final CkPaint paint; + + CkDrawLineCommand(this.p1, this.p2, this.paint); + + @override + void apply(SkCanvas canvas) { + canvas.drawLine( + p1.dx, + p1.dy, + p2.dx, + p2.dy, + paint.skiaObject, + ); + } +} + +class CkDrawPaintCommand extends CkPaintCommand { + final CkPaint paint; + + CkDrawPaintCommand(this.paint); + + @override + void apply(SkCanvas canvas) { + canvas.drawPaint(paint.skiaObject); + } +} + +class CkDrawVerticesCommand extends CkPaintCommand { + final CkVertices vertices; + final ui.BlendMode blendMode; + final CkPaint paint; + CkDrawVerticesCommand(this.vertices, this.blendMode, this.paint); + + @override + void apply(SkCanvas canvas) { + canvas.drawVertices( + vertices.skiaObject, + toSkBlendMode(blendMode), + paint.skiaObject, + ); + } +} + +class CkDrawPointsCommand extends CkPaintCommand { + final Float32List points; + final ui.PointMode pointMode; + final CkPaint paint; + CkDrawPointsCommand(this.pointMode, this.points, this.paint); + + @override + void apply(SkCanvas canvas) { + canvas.drawPoints( + toSkPointMode(pointMode), + points, + paint.skiaObject, + ); + } +} + +class CkDrawRectCommand extends CkPaintCommand { + final ui.Rect rect; + final CkPaint paint; + + CkDrawRectCommand(this.rect, this.paint); + + @override + void apply(SkCanvas canvas) { + canvas.drawRect(toSkRect(rect), paint.skiaObject); + } +} + +class CkDrawRRectCommand extends CkPaintCommand { + final ui.RRect rrect; + final CkPaint paint; + + CkDrawRRectCommand(this.rrect, this.paint); + + @override + void apply(SkCanvas canvas) { + canvas.drawRRect( + toSkRRect(rrect), + paint.skiaObject, + ); + } +} + +class CkDrawDRRectCommand extends CkPaintCommand { + final ui.RRect outer; + final ui.RRect inner; + final CkPaint paint; + + CkDrawDRRectCommand(this.outer, this.inner, this.paint); + + @override + void apply(SkCanvas canvas) { + canvas.drawDRRect( + toSkRRect(outer), + toSkRRect(inner), + paint.skiaObject, + ); + } +} + +class CkDrawOvalCommand extends CkPaintCommand { + final ui.Rect rect; + final CkPaint paint; + + CkDrawOvalCommand(this.rect, this.paint); + + @override + void apply(SkCanvas canvas) { + canvas.drawOval( + toSkRect(rect), + paint.skiaObject, + ); + } +} + +class CkDrawCircleCommand extends CkPaintCommand { + final ui.Offset c; + final double radius; + final CkPaint paint; + + CkDrawCircleCommand(this.c, this.radius, this.paint); + + @override + void apply(SkCanvas canvas) { + canvas.drawCircle( + c.dx, + c.dy, + radius, + paint.skiaObject, + ); + } +} + +class CkDrawPathCommand extends CkPaintCommand { + final CkPath path; + final CkPaint paint; + + CkDrawPathCommand(this.path, this.paint); + + @override + void apply(SkCanvas canvas) { + canvas.drawPath(path.skiaObject, paint.skiaObject); + } +} + +class CkDrawShadowCommand extends CkPaintCommand { + CkDrawShadowCommand( + this.path, this.color, this.elevation, this.transparentOccluder); + + final CkPath path; + final ui.Color color; + final double elevation; + final bool transparentOccluder; + + @override + void apply(SkCanvas canvas) { + drawSkShadow(canvas, path, color, elevation, transparentOccluder, + ui.window.devicePixelRatio); + } +} + +class CkDrawImageCommand extends CkPaintCommand { + final CkImage image; + final ui.Offset offset; + final CkPaint paint; + + CkDrawImageCommand(CkImage image, this.offset, this.paint) + : this.image = image.clone(); + + @override + void apply(SkCanvas canvas) { + canvas.drawImage( + image.skImage, + offset.dx, + offset.dy, + paint.skiaObject, + ); + } + + @override + void dispose() { + image.dispose(); + } +} + +class CkDrawImageRectCommand extends CkPaintCommand { + final CkImage image; + final ui.Rect src; + final ui.Rect dst; + final CkPaint paint; + + CkDrawImageRectCommand(CkImage image, this.src, this.dst, this.paint) + : this.image = image.clone(); + + @override + void apply(SkCanvas canvas) { + canvas.drawImageRect( + image.skImage, + toSkRect(src), + toSkRect(dst), + paint.skiaObject, + false, + ); + } + + @override + void dispose() { + image.dispose(); + } +} + +class CkDrawImageNineCommand extends CkPaintCommand { + CkDrawImageNineCommand(CkImage image, this.center, this.dst, this.paint) + : this.image = image.clone(); + + final CkImage image; + final ui.Rect center; + final ui.Rect dst; + final CkPaint paint; + + @override + void apply(SkCanvas canvas) { + canvas.drawImageNine( + image.skImage, + toSkRect(center), + toSkRect(dst), + paint.skiaObject, + ); + } + + @override + void dispose() { + image.dispose(); + } +} + +class CkDrawParagraphCommand extends CkPaintCommand { + final CkParagraph paragraph; + final ui.Offset offset; + + CkDrawParagraphCommand(this.paragraph, this.offset); + + @override + void apply(SkCanvas canvas) { + canvas.drawParagraph( + paragraph.skiaObject, + offset.dx, + offset.dy, + ); + } +} + +class CkDrawPictureCommand extends CkPaintCommand { + CkDrawPictureCommand(this.picture); + + final CkPicture picture; + + @override + void apply(SkCanvas canvas) { + canvas.drawPicture(picture.skiaObject); + } +} + +class CkSaveLayerCommand extends CkPaintCommand { + CkSaveLayerCommand(this.bounds, this.paint); + + final ui.Rect bounds; + final CkPaint? paint; + + @override + void apply(SkCanvas canvas) { + canvas.saveLayer( + paint?.skiaObject, + toSkRect(bounds), + null, + null, + ); + } +} + +class CkSaveLayerWithoutBoundsCommand extends CkPaintCommand { + CkSaveLayerWithoutBoundsCommand(this.paint); + + final CkPaint? paint; + + @override + void apply(SkCanvas canvas) { + canvas.saveLayer( + paint?.skiaObject, + null, + null, + null, + ); + } +} + +class CkSaveLayerWithFilterCommand extends CkPaintCommand { + CkSaveLayerWithFilterCommand(this.bounds, this.filter); + + final ui.Rect bounds; + final ui.ImageFilter filter; + + @override + void apply(SkCanvas canvas) { + final _CkManagedSkImageFilterConvertible convertible = filter as _CkManagedSkImageFilterConvertible; + return canvas.saveLayer( + null, + toSkRect(bounds), + convertible._imageFilter.skiaObject, + 0, + ); } } diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart index 31a5e5f366a3e..a60fc57e8d51e 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart @@ -16,8 +16,8 @@ class CanvasKitCanvas implements ui.Canvas { '"recorder" must not already be associated with another Canvas.'); } cullRect ??= ui.Rect.largest; - final CkPictureRecorder skRecorder = recorder as CkPictureRecorder; - return CanvasKitCanvas._(skRecorder.beginRecording(cullRect)); + final CkPictureRecorder ckRecorder = recorder as CkPictureRecorder; + return CanvasKitCanvas._(ckRecorder.beginRecording(cullRect)); } CanvasKitCanvas._(this._canvas); @@ -120,11 +120,7 @@ class CanvasKitCanvas implements ui.Canvas { // ignore: unnecessary_null_comparison assert(path != null); // path is checked on the engine side assert(doAntiAlias != null); // ignore: unnecessary_null_comparison - _clipPath(path, doAntiAlias); - } - - void _clipPath(ui.Path path, bool doAntiAlias) { - _canvas.clipPath(path, doAntiAlias); + _canvas.clipPath(path as CkPath, doAntiAlias); } @override @@ -243,11 +239,7 @@ class CanvasKitCanvas implements ui.Canvas { assert(image != null); // image is checked on the engine side assert(_offsetIsValid(p)); assert(paint != null); // ignore: unnecessary_null_comparison - _drawImage(image, p, paint); - } - - void _drawImage(ui.Image image, ui.Offset p, ui.Paint paint) { - _canvas.drawImage(image, p, paint as CkPaint); + _canvas.drawImage(image as CkImage, p, paint as CkPaint); } @override @@ -257,12 +249,7 @@ class CanvasKitCanvas implements ui.Canvas { assert(rectIsValid(src)); assert(rectIsValid(dst)); assert(paint != null); // ignore: unnecessary_null_comparison - _drawImageRect(image, src, dst, paint); - } - - void _drawImageRect( - ui.Image image, ui.Rect src, ui.Rect dst, ui.Paint paint) { - _canvas.drawImageRect(image, src, dst, paint as CkPaint); + _canvas.drawImageRect(image as CkImage, src, dst, paint as CkPaint); } @override @@ -278,7 +265,7 @@ class CanvasKitCanvas implements ui.Canvas { void _drawImageNine( ui.Image image, ui.Rect center, ui.Rect dst, ui.Paint paint) { - _canvas.drawImageNine(image, center, dst, paint as CkPaint); + _canvas.drawImageNine(image as CkImage, center, dst, paint as CkPaint); } @override @@ -337,12 +324,7 @@ class CanvasKitCanvas implements ui.Canvas { assert(vertices != null); // vertices is checked on the engine side assert(paint != null); // ignore: unnecessary_null_comparison assert(blendMode != null); // ignore: unnecessary_null_comparison - _drawVertices(vertices, blendMode, paint); - } - - void _drawVertices( - ui.Vertices vertices, ui.BlendMode blendMode, ui.Paint paint) { - _canvas.drawVertices(vertices, blendMode, paint as CkPaint); + _canvas.drawVertices(vertices as CkVertices, blendMode, paint as CkPaint); } @override @@ -438,7 +420,14 @@ class CanvasKitCanvas implements ui.Canvas { List? colors, ui.BlendMode blendMode, ) { - _canvas.drawAtlasRaw(paint as CkPaint, atlas, rstTransforms, rects, colors, blendMode); + _canvas.drawAtlasRaw( + paint as CkPaint, + atlas as CkImage, + rstTransforms, + rects, + colors, + blendMode, + ); } @override @@ -453,6 +442,6 @@ class CanvasKitCanvas implements ui.Canvas { void _drawShadow(ui.Path path, ui.Color color, double elevation, bool transparentOccluder) { - _canvas.drawShadow(path, color, elevation, transparentOccluder); + _canvas.drawShadow(path as CkPath, color, elevation, transparentOccluder); } } diff --git a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart index 92351db5e62a1..4cb4eaf8f863b 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -336,7 +336,7 @@ class HtmlViewEmbedder { _overlays[viewId]!.surface.acquireFrame(_frameSize); final CkCanvas canvas = frame.skiaCanvas; canvas.drawPicture( - _pictureRecorders[viewId]!.endRecording() as CkPicture, + _pictureRecorders[viewId]!.endRecording(), ); frame.submit(); } diff --git a/lib/web_ui/lib/src/engine/canvaskit/image.dart b/lib/web_ui/lib/src/engine/canvaskit/image.dart index dbf0f5974296a..749e6d8e2aa8f 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/image.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/image.dart @@ -188,7 +188,7 @@ class CkImage implements ui.Image, StackTraceDebugger { } @override - ui.Image clone() { + CkImage clone() { assert(_debugCheckIsNotDisposed()); return CkImage.cloneOf(box); } diff --git a/lib/web_ui/lib/src/engine/canvaskit/layer.dart b/lib/web_ui/lib/src/engine/canvaskit/layer.dart index 4f2e0547c4ee4..0892a70a18264 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/layer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/layer.dart @@ -128,7 +128,7 @@ class BackdropFilterLayer extends ContainerLayer { /// A layer that clips its child layers by a given [Path]. class ClipPathLayer extends ContainerLayer { /// The path used to clip child layers. - final ui.Path _clipPath; + final CkPath _clipPath; final ui.Clip _clipBehavior; ClipPathLayer(this._clipPath, this._clipBehavior) @@ -265,7 +265,7 @@ class OpacityLayer extends ContainerLayer implements ui.OpacityEngineLayer { void paint(PaintContext paintContext) { assert(needsPainting); - final ui.Paint paint = ui.Paint(); + final CkPaint paint = CkPaint(); paint.color = ui.Color.fromARGB(_alpha, 0, 0, 0); paintContext.internalNodesCanvas.save(); @@ -354,7 +354,7 @@ class ImageFilterLayer extends ContainerLayer implements ui.OpacityEngineLayer { @override void paint(PaintContext paintContext) { assert(needsPainting); - final ui.Paint paint = ui.Paint(); + final CkPaint paint = CkPaint(); paint.imageFilter = _filter; paintContext.internalNodesCanvas.saveLayer(paintBounds, paint); paintChildren(paintContext); @@ -484,9 +484,9 @@ class PhysicalShapeLayer extends ContainerLayer _color.alpha != 0xff); } - final ui.Paint paint = ui.Paint()..color = _color; + final CkPaint paint = CkPaint()..color = _color; if (_clipBehavior != ui.Clip.antiAliasWithSaveLayer) { - paintContext.leafNodesCanvas!.drawPath(_path, paint as CkPaint); + paintContext.leafNodesCanvas!.drawPath(_path, paint); } final int saveCount = paintContext.internalNodesCanvas.save(); @@ -510,7 +510,7 @@ class PhysicalShapeLayer extends ContainerLayer // (https://github.com/flutter/flutter/issues/18057#issue-328003931) // using saveLayer, we have to call drawPaint instead of drawPath as // anti-aliased drawPath will always have such artifacts. - paintContext.leafNodesCanvas!.drawPaint(paint as CkPaint); + paintContext.leafNodesCanvas!.drawPaint(paint); } paintChildren(paintContext); @@ -522,7 +522,7 @@ class PhysicalShapeLayer extends ContainerLayer /// /// The blur of the shadow is decided by the [elevation], and the /// shadow is painted with the given [color]. - static void drawShadow(CkCanvas canvas, ui.Path path, ui.Color color, + static void drawShadow(CkCanvas canvas, CkPath path, ui.Color color, double elevation, bool transparentOccluder) { canvas.drawShadow(path, color, elevation, transparentOccluder); } diff --git a/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart b/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart index e5c7266c010ed..9dfc895784015 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart @@ -86,7 +86,7 @@ class LayerSceneBuilder implements ui.SceneBuilder { } @override - ui.Scene build() { + LayerScene build() { return LayerScene(rootLayer); } @@ -113,7 +113,7 @@ class LayerSceneBuilder implements ui.SceneBuilder { ui.Clip clipBehavior = ui.Clip.antiAlias, ui.EngineLayer? oldLayer, }) { - pushLayer(ClipPathLayer(path, clipBehavior)); + pushLayer(ClipPathLayer(path as CkPath, clipBehavior)); return null; } diff --git a/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart b/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart index 3e1bf0f551306..cf0456fc0a85e 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart @@ -7,6 +7,7 @@ part of engine; /// A virtual canvas that applies operations to multiple canvases at once. class CkNWayCanvas { + // TODO(yjbanov): make this List final List _canvases = []; void addCanvas(CkCanvas? canvas) { @@ -23,9 +24,9 @@ class CkNWayCanvas { } /// Calls [saveLayer] on all canvases. - void saveLayer(ui.Rect bounds, ui.Paint? paint) { + void saveLayer(ui.Rect bounds, CkPaint? paint) { for (int i = 0; i < _canvases.length; i++) { - _canvases[i]!.saveLayer(bounds, paint as CkPaint); + _canvases[i]!.saveLayer(bounds, paint); } } @@ -65,9 +66,9 @@ class CkNWayCanvas { } /// Calls [clipPath] on all canvases. - void clipPath(ui.Path? path, bool doAntiAlias) { + void clipPath(CkPath path, bool doAntiAlias) { for (int i = 0; i < _canvases.length; i++) { - _canvases[i]!.clipPath(path!, doAntiAlias); + _canvases[i]!.clipPath(path, doAntiAlias); } } diff --git a/lib/web_ui/lib/src/engine/canvaskit/picture.dart b/lib/web_ui/lib/src/engine/canvaskit/picture.dart index 2a1cc0b8a4a01..bd2a6d9440bee 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/picture.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/picture.dart @@ -5,18 +5,23 @@ // @dart = 2.12 part of engine; -class CkPicture implements ui.Picture { - final SkiaObject skiaObject; +class CkPicture extends ManagedSkiaObject implements ui.Picture { final ui.Rect? cullRect; + final CkPictureSnapshot? _snapshot; - CkPicture(SkPicture picture, this.cullRect) - : skiaObject = SkPictureSkiaObject(picture); + CkPicture(SkPicture picture, this.cullRect, this._snapshot) : super(picture) { + assert( + browserSupportsFinalizationRegistry && _snapshot == null || _snapshot != null, + 'If the browser does not support FinalizationRegistry (WeakRef), then we must have a picture snapshot to be able to resurrect it.', + ); + } @override int get approximateBytesUsed => 0; @override void dispose() { + _snapshot?.dispose(); skiaObject.delete(); } @@ -24,18 +29,28 @@ class CkPicture implements ui.Picture { Future toImage(int width, int height) async { final SkSurface skSurface = canvasKit.MakeSurface(width, height); final SkCanvas skCanvas = skSurface.getCanvas(); - skCanvas.drawPicture(skiaObject.skiaObject); + skCanvas.drawPicture(skiaObject); final SkImage skImage = skSurface.makeImageSnapshot(); skSurface.dispose(); return CkImage(skImage); } -} -class SkPictureSkiaObject extends OneShotSkiaObject { - SkPictureSkiaObject(SkPicture picture) : super(picture); + @override + bool get isResurrectionExpensive => true; + + @override + SkPicture createDefault() { + // The default object is supplied in the constructor. + throw StateError('Unreachable code'); + } + + @override + SkPicture resurrect() { + return _snapshot!.toPicture(); + } @override void delete() { - rawSkiaObject.delete(); + rawSkiaObject?.delete(); } } diff --git a/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart b/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart index 9069008142734..d19e98f2a1974 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart @@ -15,13 +15,15 @@ class CkPictureRecorder implements ui.PictureRecorder { final SkPictureRecorder recorder = _skRecorder = SkPictureRecorder(); final Float32List skRect = toSkRect(bounds); final SkCanvas skCanvas = recorder.beginRecording(skRect); - return _recordingCanvas = CkCanvas(skCanvas); + return _recordingCanvas = browserSupportsFinalizationRegistry + ? CkCanvas(skCanvas) + : RecordingCkCanvas(skCanvas, bounds); } CkCanvas? get recordingCanvas => _recordingCanvas; @override - ui.Picture endRecording() { + CkPicture endRecording() { final SkPictureRecorder? recorder = _skRecorder; if (recorder == null) { @@ -31,7 +33,7 @@ class CkPictureRecorder implements ui.PictureRecorder { final SkPicture skPicture = recorder.finishRecordingAsPicture(); recorder.delete(); _skRecorder = null; - return CkPicture(skPicture, _cullRect); + return CkPicture(skPicture, _cullRect, _recordingCanvas!.pictureSnapshot); } @override diff --git a/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart b/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart index 8ea1ec27c1a01..664bcf0f77fef 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart @@ -219,54 +219,6 @@ abstract class ManagedSkiaObject extends SkiaObject { bool get isResurrectionExpensive => false; } -// TODO(hterkelsen): [OneShotSkiaObject] is dangerous because it might delete -// the underlying Skia object while the associated Dart object is still in -// use. This issue discusses ways to address this: -// https://github.com/flutter/flutter/issues/60401 -/// A [SkiaObject] which is deleted once and cannot be used again. -/// -/// In browsers that support weak references we use feedback from the garbage -/// collector to determine when it is safe to release the C++ object. Otherwise, -/// we use an LRU cache (see [SkiaObjects.manageOneShot]). -abstract class OneShotSkiaObject extends SkiaObject { - /// Returns the current skia object as is without attempting to - /// resurrect it. - /// - /// If the returned value is `null`, the corresponding C++ object has - /// been deleted. - /// - /// Use this field instead of the [skiaObject] getter when implementing - /// the [delete] method. - T rawSkiaObject; - - bool _isDeleted = false; - - OneShotSkiaObject(T skObject) : this.rawSkiaObject = skObject { - if (browserSupportsFinalizationRegistry) { - Collector.instance.register(this, skObject as SkDeletable); - } else { - SkiaObjects.manageOneShot(this); - } - } - - @override - T get skiaObject { - if (browserSupportsFinalizationRegistry) { - return rawSkiaObject; - } - if (_isDeleted) { - throw StateError('Attempting to use a Skia object that has been freed.'); - } - SkiaObjects.oneShotCache.markUsed(this); - return rawSkiaObject; - } - - @override - void didDelete() { - _isDeleted = true; - } -} - /// Interface that classes wrapping [SkiaObjectBox] must implement. /// /// Used to collect stack traces in debug mode. @@ -468,10 +420,7 @@ class SkiaObjects { []; @visibleForTesting - static int maximumCacheSize = 8192; - - @visibleForTesting - static final SkiaObjectCache oneShotCache = SkiaObjectCache(maximumCacheSize); + static int maximumCacheSize = 1024; @visibleForTesting static final SkiaObjectCache expensiveCache = @@ -499,15 +448,6 @@ class SkiaObjects { resurrectableObjects.add(object); } - /// Starts managing the lifecycle of a one-shot [object]. - /// - /// We should avoid deleting these whenever we can, since we won't - /// be able to resurrect them. - static void manageOneShot(OneShotSkiaObject object) { - registerCleanupCallback(); - oneShotCache.add(object); - } - /// Starts managing the lifecycle of a resurrectable object that is expensive. /// /// Since it's expensive to resurrect, we shouldn't just delete it after every diff --git a/lib/web_ui/lib/src/engine/canvaskit/text.dart b/lib/web_ui/lib/src/engine/canvaskit/text.dart index 36f609f64d83a..bdc403fb70481 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/text.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/text.dart @@ -667,7 +667,7 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { } @override - ui.Paragraph build() { + CkParagraph build() { final builtParagraph = _buildCkParagraph(); return CkParagraph(builtParagraph, _style, _commands); } diff --git a/lib/web_ui/test/canvaskit/canvas_golden_test.dart b/lib/web_ui/test/canvaskit/canvas_golden_test.dart new file mode 100644 index 0000000000000..d7b24b4cba65d --- /dev/null +++ b/lib/web_ui/test/canvaskit/canvas_golden_test.dart @@ -0,0 +1,402 @@ +// 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. + +// @dart = 2.12 +import 'dart:html' as html; +import 'dart:math' as math; +import 'dart:typed_data'; + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +import 'package:web_engine_tester/golden_tester.dart'; + +import 'common.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +const ui.Rect region = const ui.Rect.fromLTRB(0, 0, 500, 250); + +Future matchPictureGolden(String goldenFile, CkPicture picture, { bool write = false }) async { + final EnginePlatformDispatcher dispatcher = ui.window.platformDispatcher as EnginePlatformDispatcher; + final LayerSceneBuilder sb = LayerSceneBuilder(); + sb.pushOffset(0, 0); + sb.addPicture(ui.Offset.zero, picture); + dispatcher.rasterizer!.draw(sb.build().layerTree); + await matchGoldenFile(goldenFile, region: region, write: write); +} + +void testMain() { + group('CkCanvas', () { + setUpCanvasKitTest(); + + test('renders using non-recording canvas if weak refs are supported', () async { + expect(browserSupportsFinalizationRegistry, isTrue, + reason: 'This test specifically tests non-recording canvas, which ' + 'only works if FinalizationRegistry is available.'); + final CkPictureRecorder recorder = CkPictureRecorder(); + final CkCanvas canvas = recorder.beginRecording(region); + expect(canvas.runtimeType, CkCanvas); + drawTestPicture(canvas); + await matchPictureGolden('canvaskit_picture_original.png', recorder.endRecording()); + }); + + test('renders using a recording canvas if weak refs are not supported', () async { + browserSupportsFinalizationRegistry = false; + final CkPictureRecorder recorder = CkPictureRecorder(); + final CkCanvas canvas = recorder.beginRecording(region); + expect(canvas, isA()); + drawTestPicture(canvas); + + final CkPicture originalPicture = recorder.endRecording(); + await matchPictureGolden('canvaskit_picture_original.png', originalPicture); + + final ByteData originalPixels = await (await originalPicture.toImage(50, 50)).toByteData() as ByteData; + + // Test that a picture restored from a snapshot looks the same. + final CkPictureSnapshot? snapshot = canvas.pictureSnapshot; + expect(snapshot, isNotNull); + final SkPicture restoredSkPicture = snapshot!.toPicture(); + expect(restoredSkPicture, isNotNull); + final CkPicture restoredPicture = CkPicture(restoredSkPicture, ui.Rect.fromLTRB(0, 0, 50, 50), snapshot); + final ByteData restoredPixels = await (await restoredPicture.toImage(50, 50)).toByteData() as ByteData; + + await matchPictureGolden('canvaskit_picture_restored.png', restoredPicture); + expect(restoredPixels.buffer.asUint8List(), originalPixels.buffer.asUint8List()); + }); + // TODO: https://github.com/flutter/flutter/issues/60040 + // TODO: https://github.com/flutter/flutter/issues/71520 + }, skip: isIosSafari || isFirefox); +} + +void drawTestPicture(CkCanvas canvas) { + canvas.clear(ui.Color(0xFFFFFFF)); + + canvas.translate(10, 10); + + // Row 1 + canvas.save(); + + canvas.save(); + canvas.clipRect( + ui.Rect.fromLTRB(0, 0, 45, 45), + ui.ClipOp.intersect, + true, + ); + canvas.clipRRect( + ui.RRect.fromLTRBR(5, 5, 50, 50, ui.Radius.circular(8)), + true, + ); + canvas.clipPath( + CkPath() + ..moveTo(5, 5) + ..lineTo(25, 5) + ..lineTo(45, 45) + ..lineTo(5, 45) + ..close(), + true, + ); + canvas.drawColor(ui.Color.fromARGB(255, 100, 100, 0), ui.BlendMode.srcOver); + canvas.restore(); // remove clips + + canvas.translate(60, 0); + canvas.drawCircle( + const ui.Offset(30, 25), + 15, + CkPaint()..color = ui.Color(0xFF0000AA), + ); + + canvas.translate(60, 0); + canvas.drawArc( + ui.Rect.fromLTRB(10, 20, 50, 40), + math.pi / 4, + 3 * math.pi / 2, + true, + CkPaint()..color = ui.Color(0xFF00AA00), + ); + + canvas.translate(60, 0); + canvas.drawImage( + generateTestImage(), + const ui.Offset(20, 20), + CkPaint(), + ); + + canvas.translate(60, 0); + final ui.RSTransform transform = ui.RSTransform.fromComponents( + rotation: 0, + scale: 1, + anchorX: 0, + anchorY: 0, + translateX: 0, + translateY: 0, + ); + canvas.drawAtlasRaw( + CkPaint(), + generateTestImage(), + Float32List(4) + ..[0] = transform.scos + ..[1] = transform.ssin + ..[2] = transform.tx + 20 + ..[3] = transform.ty + 20, + Float32List(4) + ..[0] = 0 + ..[1] = 0 + ..[2] = 15 + ..[3] = 15, + [Float32List(4)], + ui.BlendMode.srcOver, + ); + + canvas.translate(60, 0); + canvas.drawDRRect( + ui.RRect.fromLTRBR(0, 0, 40, 30, ui.Radius.elliptical(16, 8)), + ui.RRect.fromLTRBR(10, 10, 30, 20, ui.Radius.elliptical(4, 8)), + CkPaint(), + ); + + canvas.translate(60, 0); + canvas.drawImageRect( + generateTestImage(), + ui.Rect.fromLTRB(0, 0, 15, 15), + ui.Rect.fromLTRB(10, 10, 40, 40), + CkPaint(), + ); + + canvas.translate(60, 0); + canvas.drawImageNine( + generateTestImage(), + ui.Rect.fromLTRB(5, 5, 15, 15), + ui.Rect.fromLTRB(10, 10, 50, 40), + CkPaint(), + ); + + canvas.restore(); + + // Row 2 + canvas.translate(0, 60); + canvas.save(); + + canvas.drawLine(ui.Offset(0, 0), ui.Offset(40, 30), CkPaint()); + + canvas.translate(60, 0); + canvas.drawOval( + ui.Rect.fromLTRB(0, 0, 40, 30), + CkPaint(), + ); + + canvas.translate(60, 0); + canvas.save(); + canvas.clipRect(ui.Rect.fromLTRB(0, 0, 50, 30), ui.ClipOp.intersect, true); + canvas.drawPaint(CkPaint()..color = ui.Color(0xFF6688AA)); + canvas.restore(); + + canvas.translate(60, 0); + { + final CkPictureRecorder otherRecorder = CkPictureRecorder(); + final CkCanvas otherCanvas = + otherRecorder.beginRecording(ui.Rect.fromLTRB(0, 0, 40, 20)); + otherCanvas.drawCircle( + ui.Offset(30, 15), + 10, + CkPaint()..color = ui.Color(0xFFAABBCC), + ); + canvas.drawPicture(otherRecorder.endRecording()); + } + + canvas.translate(60, 0); + // TODO(yjbanov): CanvasKit.drawPoints is currently broken + // https://github.com/flutter/flutter/issues/71489 + // But keeping this anyway as it's a good test-case that + // will ensure it's fixed when we have the fix. + canvas.drawPoints( + CkPaint() + ..color = ui.Color(0xFF0000FF) + ..strokeWidth = 5 + ..strokeCap = ui.StrokeCap.round, + ui.PointMode.polygon, + offsetListToFloat32List([ + ui.Offset(10, 10), + ui.Offset(20, 10), + ui.Offset(30, 20), + ui.Offset(40, 20) + ]), + ); + + canvas.translate(60, 0); + canvas.drawRRect( + ui.RRect.fromLTRBR(0, 0, 40, 30, ui.Radius.circular(10)), + CkPaint(), + ); + + canvas.translate(60, 0); + canvas.drawRect( + ui.Rect.fromLTRB(0, 0, 40, 30), + CkPaint(), + ); + + canvas.translate(60, 0); + canvas.drawShadow( + CkPath() + ..addRect(ui.Rect.fromLTRB(0, 0, 40, 30)), + ui.Color(0xFF00FF00), + 4, + true, + ); + + canvas.restore(); + + // Row 3 + canvas.translate(0, 60); + canvas.save(); + + canvas.drawVertices( + CkVertices( + ui.VertexMode.triangleFan, + [ + ui.Offset(10, 30), + ui.Offset(30, 50), + ui.Offset(10, 60), + ], + ), + ui.BlendMode.srcOver, + CkPaint(), + ); + + canvas.translate(60, 0); + final int restorePoint = canvas.save(); + for (int i = 0; i < 5; i++) { + canvas.save(); + canvas.translate(10, 10); + canvas.drawCircle(ui.Offset.zero, 5, CkPaint()); + } + canvas.restoreToCount(restorePoint); + canvas.drawCircle(ui.Offset.zero, 7, CkPaint()..color = ui.Color(0xFFFF0000)); + + canvas.translate(60, 0); + canvas.drawLine(ui.Offset.zero, ui.Offset(30, 30), CkPaint()); + canvas.save(); + canvas.rotate(-math.pi / 8); + canvas.drawLine(ui.Offset.zero, ui.Offset(30, 30), CkPaint()); + canvas.drawCircle(ui.Offset(30, 30), 7, CkPaint()..color = ui.Color(0xFF00AA00)); + canvas.restore(); + + canvas.translate(60, 0); + final CkPaint thickStroke = CkPaint() + ..style = ui.PaintingStyle.stroke + ..strokeWidth = 20; + final CkPaint semitransparent = CkPaint()..color = ui.Color(0x66000000); + + canvas.saveLayer(region, semitransparent); + canvas.drawLine(ui.Offset(10, 10), ui.Offset(50, 50), thickStroke); + canvas.drawLine(ui.Offset(50, 10), ui.Offset(10, 50), thickStroke); + canvas.restore(); + + canvas.translate(60, 0); + canvas.saveLayerWithoutBounds(semitransparent); + canvas.drawLine(ui.Offset(10, 10), ui.Offset(50, 50), thickStroke); + canvas.drawLine(ui.Offset(50, 10), ui.Offset(10, 50), thickStroke); + canvas.restore(); + + // To test saveLayerWithFilter we draw three circles with only the middle one + // blurred using the layer image filter. + canvas.translate(60, 0); + canvas.saveLayer(region, CkPaint()); + canvas.drawCircle(ui.Offset(30, 30), 10, CkPaint()); + { + canvas.saveLayerWithFilter(region, ui.ImageFilter.blur(sigmaX: 5, sigmaY: 10)); + canvas.drawCircle(ui.Offset(10, 10), 10, CkPaint()); + canvas.drawCircle(ui.Offset(50, 50), 10, CkPaint()); + canvas.restore(); + } + canvas.restore(); + + canvas.translate(60, 0); + canvas.save(); + canvas.translate(30, 30); + canvas.scale(2, 1.5); + canvas.drawCircle(ui.Offset.zero, 10, CkPaint()); + canvas.restore(); + + canvas.translate(60, 0); + canvas.save(); + canvas.translate(30, 30); + canvas.skew(2, 1.5); + canvas.drawRect(ui.Rect.fromLTRB(-10, -10, 10, 10), CkPaint()); + canvas.restore(); + + canvas.restore(); + + // Row 4 + canvas.translate(0, 60); + canvas.save(); + + canvas.save(); + final Matrix4 matrix = Matrix4.identity(); + matrix.translate(30, 30); + matrix.scale(2, 1.5); + canvas.transform(matrix.storage); + canvas.drawCircle(ui.Offset.zero, 10, CkPaint()); + canvas.restore(); + + canvas.translate(60, 0); + final CkParagraphBuilder pb = CkParagraphBuilder(CkParagraphStyle( + fontFamily: 'Roboto', + fontStyle: ui.FontStyle.normal, + fontWeight: ui.FontWeight.normal, + fontSize: 18, + )); + pb.pushStyle(CkTextStyle( + color: ui.Color(0xFF0000AA), + )); + pb.addText('Hello'); + pb.pop(); + final CkParagraph p = pb.build(); + p.layout(ui.ParagraphConstraints(width: 1000)); + canvas.drawParagraph( + p, + ui.Offset(10, 20), + ); + + canvas.translate(60, 0); + canvas.drawPath( + CkPath() + ..moveTo(30, 20) + ..lineTo(50, 50) + ..lineTo(10, 50) + ..close(), + CkPaint()..color = ui.Color(0xFF0000AA), + ); + + canvas.restore(); +} + +CkImage generateTestImage() { + final html.CanvasElement canvas = html.CanvasElement() + ..width = 20 + ..height = 20; + final html.CanvasRenderingContext2D ctx = canvas.context2D; + ctx.fillStyle = '#FF0000'; + ctx.fillRect(0, 0, 10, 10); + ctx.fillStyle = '#00FF00'; + ctx.fillRect(0, 10, 10, 10); + ctx.fillStyle = '#0000FF'; + ctx.fillRect(10, 0, 10, 10); + ctx.fillStyle = '#FF00FF'; + ctx.fillRect(10, 10, 10, 10); + final Uint8List imageData = ctx.getImageData(0, 0, 20, 20).data.buffer.asUint8List(); + final SkImage skImage = canvasKit.MakeImage( + imageData, + 20, + 20, + canvasKit.AlphaType.Premul, + canvasKit.ColorType.RGBA_8888, + SkColorSpaceSRGB, + ); + return CkImage(skImage); +} diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index 89022a27dfc76..b70182ac85b1d 100644 --- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -1169,6 +1169,10 @@ void _canvasTests() { }); test('toImage.toByteData', () async { + // Pretend that FinalizationRegistry is supported, so we can run this + // test in older browsers (the test will use a TestCollector instead of + // ProductionCollector) + browserSupportsFinalizationRegistry = true; final SkPictureRecorder otherRecorder = SkPictureRecorder(); final SkCanvas otherCanvas = otherRecorder.beginRecording(Float32List.fromList([0, 0, 1, 1])); @@ -1177,7 +1181,7 @@ void _canvasTests() { SkPaint(), ); final CkPicture picture = - CkPicture(otherRecorder.finishRecordingAsPicture(), null); + CkPicture(otherRecorder.finishRecordingAsPicture(), null, null); final CkImage image = await picture.toImage(1, 1); final ByteData rawData = await image.toByteData(format: ui.ImageByteFormat.rawRgba); diff --git a/lib/web_ui/test/canvaskit/common.dart b/lib/web_ui/test/canvaskit/common.dart index 46c319bc9ccfb..1cff72b20cdfb 100644 --- a/lib/web_ui/test/canvaskit/common.dart +++ b/lib/web_ui/test/canvaskit/common.dart @@ -7,11 +7,14 @@ import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -/// Whether we are running on iOS Safari. +/// Whether the current browser is Safari on iOS. // TODO: https://github.com/flutter/flutter/issues/60040 bool get isIosSafari => browserEngine == BrowserEngine.webkit && operatingSystem == OperatingSystem.iOs; +/// Whether the current browser is Firefox. +bool get isFirefox => browserEngine == BrowserEngine.firefox; + /// Used in tests instead of [ProductionCollector] to control Skia object /// collection explicitly, and to prevent leaks across tests. /// @@ -24,7 +27,7 @@ void setUpCanvasKitTest() { expect(useCanvasKit, true, reason: 'This test must run in CanvasKit mode.'); debugResetBrowserSupportsFinalizationRegistry(); - await ui.webOnlyInitializePlatform(); + await ui.webOnlyInitializePlatform(assetManager: WebOnlyMockAssetManager()); }); setUp(() async { diff --git a/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart b/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart index cfdb9e75a35d6..2845c062b41a9 100644 --- a/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart +++ b/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart @@ -117,37 +117,6 @@ void _tests() { }); }); - group(OneShotSkiaObject, () { - test('is added to SkiaObjects cache', () { - TestOneShotSkiaObject.deleteCount = 0; - OneShotSkiaObject object1 = TestOneShotSkiaObject(); - expect(SkiaObjects.oneShotCache.length, 1); - expect(SkiaObjects.oneShotCache.debugContains(object1), isTrue); - - OneShotSkiaObject object2 = TestOneShotSkiaObject(); - expect(SkiaObjects.oneShotCache.length, 2); - expect(SkiaObjects.oneShotCache.debugContains(object2), isTrue); - - SkiaObjects.postFrameCleanUp(); - expect(SkiaObjects.oneShotCache.length, 2); - expect(SkiaObjects.oneShotCache.debugContains(object1), isTrue); - expect(SkiaObjects.oneShotCache.debugContains(object2), isTrue); - - // Add 3 more objects to the cache to overflow it. - TestOneShotSkiaObject(); - TestOneShotSkiaObject(); - TestOneShotSkiaObject(); - expect(SkiaObjects.oneShotCache.length, 5); - expect(SkiaObjects.cachesToResize.length, 1); - - SkiaObjects.postFrameCleanUp(); - expect(TestOneShotSkiaObject.deleteCount, 2); - expect(SkiaObjects.oneShotCache.length, 3); - expect(SkiaObjects.oneShotCache.debugContains(object1), isFalse); - expect(SkiaObjects.oneShotCache.debugContains(object2), isFalse); - }); - }); - group(SkiaObjectBox, () { test('Records stack traces and respects refcounts', () async { TestSkDeletable.deleteCount = 0; @@ -297,27 +266,6 @@ class TestSkDeletable implements SkDeletable { JsConstructor get constructor => TestJsConstructor('TestSkDeletable'); } -class TestOneShotSkiaObject extends OneShotSkiaObject implements SkDeletable { - static int deleteCount = 0; - - TestOneShotSkiaObject() : super(SkPaint()); - - @override - bool isDeleted() => _isDeleted; - bool _isDeleted = false; - - @override - void delete() { - expect(_isDeleted, isFalse, - reason: 'CanvasKit does not allow deleting the same object more than once.'); - rawSkiaObject?.delete(); - deleteCount++; - } - - @override - JsConstructor get constructor => TestJsConstructor('TestOneShotSkiaObject'); -} - class TestJsConstructor implements JsConstructor{ TestJsConstructor(this.name); diff --git a/web_sdk/web_engine_tester/lib/golden_tester.dart b/web_sdk/web_engine_tester/lib/golden_tester.dart index d8cf77059fa59..630607931c779 100644 --- a/web_sdk/web_engine_tester/lib/golden_tester.dart +++ b/web_sdk/web_engine_tester/lib/golden_tester.dart @@ -2,10 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// @dart = 2.12 + import 'dart:async'; import 'dart:convert'; import 'dart:html' as html; +// ignore: implementation_imports import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; @@ -18,7 +21,7 @@ Future _callScreenshotServer(dynamic requestData) async { sendData: json.encode(requestData), ); - return json.decode(request.responseText); + return json.decode(request.responseText!); } /// How to compare pixels within the image. @@ -50,8 +53,8 @@ enum PixelComparison { /// [pixelComparison] determines the algorithm used to compare pixels. Uses /// fuzzy comparison by default. Future matchGoldenFile(String filename, - {bool write = false, Rect region = null, double maxDiffRatePercent = null, PixelComparison pixelComparison = PixelComparison.fuzzy}) async { - Map serverParams = { + {bool write = false, Rect? region, double? maxDiffRatePercent, PixelComparison pixelComparison = PixelComparison.fuzzy}) async { + final Map serverParams = { 'filename': filename, 'write': write, 'region': region == null