Make CkPicture resurrectable (#22807)

* Make CkPicture resurrectable

* disable goldens on Firefox

* add non-recording canvas test; fix Firefox test
This commit is contained in:
Yegor 2020-12-03 13:13:44 -08:00 committed by GitHub
parent e71c6f4d7e
commit 37738353cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1346 additions and 251 deletions

View File

@ -1,2 +1,2 @@
repository: https://github.com/flutter/goldens.git
revision: 06e0333b8371965dce5dc05e140e6dfb454f33fa
revision: ac75f12c6e93461369e1391da6cc20bf8cb08829

View File

@ -436,35 +436,39 @@ class TestCommand extends Command<bool> 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<FilePath> screenshotTestFiles = <FilePath>[];
final List<FilePath> unitTestFiles = <FilePath>[];
// 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<FilePath> screenshotTestFiles = <FilePath>[];
final List<FilePath> unitTestFiles = <FilePath>[];
for (io.File testFile
in testDir.listSync(recursive: true).whereType<io.File>()) {
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);
}
for (io.File testFile
in testDir.listSync(recursive: true).whereType<io.File>()) {
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;
}
// 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<bool> 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<bool> with ArgUtils {
);
_checkExitCode();
}
} else {
final List<FilePath> unitTestFiles = <FilePath>[];
for (io.File testFile
in testDir.listSync(recursive: true).whereType<io.File>()) {
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();
}
}

View File

@ -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<Float32List>? 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<Float32List>? 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<CkPaintCommand> _commands = <CkPaintCommand>[];
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<Float32List>? 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,
);
}
}

View File

@ -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<Float32List>? 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);
}
}

View File

@ -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();
}

View File

@ -188,7 +188,7 @@ class CkImage implements ui.Image, StackTraceDebugger {
}
@override
ui.Image clone() {
CkImage clone() {
assert(_debugCheckIsNotDisposed());
return CkImage.cloneOf(box);
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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<CkCanvas>
final List<CkCanvas?> _canvases = <CkCanvas?>[];
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);
}
}

View File

@ -5,18 +5,23 @@
// @dart = 2.12
part of engine;
class CkPicture implements ui.Picture {
final SkiaObject<SkPicture> skiaObject;
class CkPicture extends ManagedSkiaObject<SkPicture> 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<ui.Image> 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<SkPicture> {
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();
}
}

View File

@ -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

View File

@ -219,54 +219,6 @@ abstract class ManagedSkiaObject<T extends Object> extends SkiaObject<T> {
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<T extends Object> extends SkiaObject<T> {
/// 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 {
<ManagedSkiaObject>[];
@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

View File

@ -667,7 +667,7 @@ class CkParagraphBuilder implements ui.ParagraphBuilder {
}
@override
ui.Paragraph build() {
CkParagraph build() {
final builtParagraph = _buildCkParagraph();
return CkParagraph(builtParagraph, _style, _commands);
}

View File

@ -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<void> 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<RecordingCkCanvas>());
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>[
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>[
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);
}

View File

@ -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);

View File

@ -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 {

View File

@ -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<SkPaint> 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);

View File

@ -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<dynamic> _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<void> matchGoldenFile(String filename,
{bool write = false, Rect region = null, double maxDiffRatePercent = null, PixelComparison pixelComparison = PixelComparison.fuzzy}) async {
Map<String, dynamic> serverParams = <String, dynamic>{
{bool write = false, Rect? region, double? maxDiffRatePercent, PixelComparison pixelComparison = PixelComparison.fuzzy}) async {
final Map<String, dynamic> serverParams = <String, dynamic>{
'filename': filename,
'write': write,
'region': region == null