| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart: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 '../common/matchers.dart'; |
| import '../common/test_initialization.dart'; |
| |
| void main() { |
| internalBootstrapBrowserTest(() => testMain); |
| } |
| |
| Future<void> testMain() async { |
| setUpUnitTests( |
| setUpTestViewDimensions: false, |
| ); |
| |
| final bool deviceClipRoundsOut = renderer is! HtmlRenderer; |
| runCanvasTests(deviceClipRoundsOut: deviceClipRoundsOut); |
| } |
| |
| void runCanvasTests({required bool deviceClipRoundsOut}) { |
| setUp(() { |
| EngineSemantics.debugResetSemantics(); |
| }); |
| |
| group('ui.Canvas transform tests', () { |
| void transformsClose(Float64List value, Float64List expected) { |
| expect(expected.length, equals(16)); |
| expect(value.length, equals(16)); |
| for (int r = 0; r < 4; r++) { |
| for (int c = 0; c < 4; c++) { |
| expect(value[r*4 + c], within(from: expected[r*4 + c])); |
| } |
| } |
| } |
| |
| void transformsNotClose(Float64List value, Float64List expected) { |
| // We check the lengths here even though [transformsClose] will |
| // check them so that the [TestFailure] we catch below can only |
| // be due to a difference in matrix values. |
| expect(expected.length, equals(16)); |
| expect(value.length, equals(16)); |
| try { |
| transformsClose(value, expected); |
| } on TestFailure { |
| return; |
| } |
| throw TestFailure('transforms were too close to equal'); // ignore: only_throw_errors |
| } |
| |
| test('ui.Canvas.translate affects canvas.getTransform', () { |
| final ui.PictureRecorder recorder = ui.PictureRecorder(); |
| final ui.Canvas canvas = ui.Canvas(recorder); |
| canvas.translate(12, 14.5); |
| final Float64List matrix = Matrix4.translationValues(12, 14.5, 0).toFloat64(); |
| final Float64List curMatrix = canvas.getTransform(); |
| transformsClose(curMatrix, matrix); |
| canvas.translate(10, 10); |
| final Float64List newCurMatrix = canvas.getTransform(); |
| transformsNotClose(newCurMatrix, matrix); |
| transformsClose(curMatrix, matrix); |
| }); |
| |
| test('ui.Canvas.scale affects canvas.getTransform', () { |
| final ui.PictureRecorder recorder = ui.PictureRecorder(); |
| final ui.Canvas canvas = ui.Canvas(recorder); |
| canvas.scale(12, 14.5); |
| final Float64List matrix = Matrix4.diagonal3Values(12, 14.5, 1).toFloat64(); |
| final Float64List curMatrix = canvas.getTransform(); |
| transformsClose(curMatrix, matrix); |
| canvas.scale(10, 10); |
| final Float64List newCurMatrix = canvas.getTransform(); |
| transformsNotClose(newCurMatrix, matrix); |
| transformsClose(curMatrix, matrix); |
| }); |
| |
| test('Canvas.rotate affects canvas.getTransform', () async { |
| final ui.PictureRecorder recorder = ui.PictureRecorder(); |
| final ui.Canvas canvas = ui.Canvas(recorder); |
| canvas.rotate(pi); |
| final Float64List matrix = Matrix4.rotationZ(pi).toFloat64(); |
| final Float64List curMatrix = canvas.getTransform(); |
| transformsClose(curMatrix, matrix); |
| canvas.rotate(pi / 2); |
| final Float64List newCurMatrix = canvas.getTransform(); |
| transformsNotClose(newCurMatrix, matrix); |
| transformsClose(curMatrix, matrix); |
| }); |
| |
| test('Canvas.skew affects canvas.getTransform', () async { |
| final ui.PictureRecorder recorder = ui.PictureRecorder(); |
| final ui.Canvas canvas = ui.Canvas(recorder); |
| canvas.skew(12, 14.5); |
| final Float64List matrix = (Matrix4.identity()..setEntry(0, 1, 12)..setEntry(1, 0, 14.5)).toFloat64(); |
| final Float64List curMatrix = canvas.getTransform(); |
| transformsClose(curMatrix, matrix); |
| canvas.skew(10, 10); |
| final Float64List newCurMatrix = canvas.getTransform(); |
| transformsNotClose(newCurMatrix, matrix); |
| transformsClose(curMatrix, matrix); |
| }); |
| |
| test('Canvas.transform affects canvas.getTransform', () async { |
| final ui.PictureRecorder recorder = ui.PictureRecorder(); |
| final ui.Canvas canvas = ui.Canvas(recorder); |
| final Float64List matrix = (Matrix4.identity()..translate(12.0, 14.5)..scale(12.0, 14.5)).toFloat64(); |
| canvas.transform(matrix); |
| final Float64List curMatrix = canvas.getTransform(); |
| transformsClose(curMatrix, matrix); |
| canvas.translate(10, 10); |
| final Float64List newCurMatrix = canvas.getTransform(); |
| transformsNotClose(newCurMatrix, matrix); |
| transformsClose(curMatrix, matrix); |
| }); |
| }); |
| |
| void rectsClose(ui.Rect value, ui.Rect expected) { |
| expect(value.left, closeTo(expected.left, 1e-6)); |
| expect(value.top, closeTo(expected.top, 1e-6)); |
| expect(value.right, closeTo(expected.right, 1e-6)); |
| expect(value.bottom, closeTo(expected.bottom, 1e-6)); |
| } |
| |
| void rectsNotClose(ui.Rect value, ui.Rect expected) { |
| try { |
| rectsClose(value, expected); |
| } on TestFailure { |
| return; |
| } |
| throw TestFailure('transforms were too close to equal'); // ignore: only_throw_errors |
| } |
| |
| group('ui.Canvas clip tests', () { |
| test('Canvas.clipRect affects canvas.getClipBounds', () async { |
| final ui.PictureRecorder recorder = ui.PictureRecorder(); |
| final ui.Canvas canvas = ui.Canvas(recorder, const ui.Rect.fromLTRB(0, 0, 100, 100)); |
| const ui.Rect clipRawBounds = ui.Rect.fromLTRB(10.2, 11.3, 20.4, 25.7); |
| const ui.Rect clipExpandedBounds = ui.Rect.fromLTRB(10, 11, 21, 26); |
| final ui.Rect clipDestBounds = deviceClipRoundsOut ? clipExpandedBounds : clipRawBounds; |
| canvas.clipRect(clipRawBounds); |
| |
| // Save initial return values for testing restored values |
| final ui.Rect initialLocalBounds = canvas.getLocalClipBounds(); |
| final ui.Rect initialDestinationBounds = canvas.getDestinationClipBounds(); |
| rectsClose(initialLocalBounds, clipExpandedBounds); |
| rectsClose(initialDestinationBounds, clipDestBounds); |
| |
| canvas.save(); |
| canvas.clipRect(const ui.Rect.fromLTRB(0, 0, 15, 15)); |
| // Both clip bounds have changed |
| rectsNotClose(canvas.getLocalClipBounds(), clipExpandedBounds); |
| rectsNotClose(canvas.getDestinationClipBounds(), clipDestBounds); |
| // Previous return values have not changed |
| rectsClose(initialLocalBounds, clipExpandedBounds); |
| rectsClose(initialDestinationBounds, clipDestBounds); |
| canvas.restore(); |
| |
| // save/restore returned the values to their original values |
| expect(canvas.getLocalClipBounds(), initialLocalBounds); |
| expect(canvas.getDestinationClipBounds(), initialDestinationBounds); |
| |
| canvas.save(); |
| canvas.scale(2, 2); |
| const ui.Rect scaledExpandedBounds = ui.Rect.fromLTRB(5, 5.5, 10.5, 13); |
| rectsClose(canvas.getLocalClipBounds(), scaledExpandedBounds); |
| // Destination bounds are unaffected by transform |
| rectsClose(canvas.getDestinationClipBounds(), clipDestBounds); |
| canvas.restore(); |
| |
| // save/restore returned the values to their original values |
| expect(canvas.getLocalClipBounds(), initialLocalBounds); |
| expect(canvas.getDestinationClipBounds(), initialDestinationBounds); |
| }); |
| |
| test('Canvas.clipRRect affects canvas.getClipBounds', () async { |
| final ui.PictureRecorder recorder = ui.PictureRecorder(); |
| final ui.Canvas canvas = ui.Canvas(recorder, const ui.Rect.fromLTRB(0, 0, 100, 100)); |
| const ui.Rect clipRawBounds = ui.Rect.fromLTRB(10.2, 11.3, 20.4, 25.7); |
| const ui.Rect clipExpandedBounds = ui.Rect.fromLTRB(10, 11, 21, 26); |
| final ui.Rect clipDestBounds = deviceClipRoundsOut ? clipExpandedBounds : clipRawBounds; |
| final ui.RRect clip = ui.RRect.fromRectAndRadius(clipRawBounds, const ui.Radius.circular(3)); |
| canvas.clipRRect(clip); |
| |
| // Save initial return values for testing restored values |
| final ui.Rect initialLocalBounds = canvas.getLocalClipBounds(); |
| final ui.Rect initialDestinationBounds = canvas.getDestinationClipBounds(); |
| rectsClose(initialLocalBounds, clipExpandedBounds); |
| rectsClose(initialDestinationBounds, clipDestBounds); |
| |
| canvas.save(); |
| canvas.clipRect(const ui.Rect.fromLTRB(0, 0, 15, 15)); |
| // Both clip bounds have changed |
| rectsNotClose(canvas.getLocalClipBounds(), clipExpandedBounds); |
| rectsNotClose(canvas.getDestinationClipBounds(), clipDestBounds); |
| // Previous return values have not changed |
| rectsClose(initialLocalBounds, clipExpandedBounds); |
| rectsClose(initialDestinationBounds, clipDestBounds); |
| canvas.restore(); |
| |
| // save/restore returned the values to their original values |
| expect(canvas.getLocalClipBounds(), initialLocalBounds); |
| expect(canvas.getDestinationClipBounds(), initialDestinationBounds); |
| |
| canvas.save(); |
| canvas.scale(2, 2); |
| const ui.Rect scaledExpandedBounds = ui.Rect.fromLTRB(5, 5.5, 10.5, 13); |
| rectsClose(canvas.getLocalClipBounds(), scaledExpandedBounds); |
| // Destination bounds are unaffected by transform |
| rectsClose(canvas.getDestinationClipBounds(), clipDestBounds); |
| canvas.restore(); |
| |
| // save/restore returned the values to their original values |
| expect(canvas.getLocalClipBounds(), initialLocalBounds); |
| expect(canvas.getDestinationClipBounds(), initialDestinationBounds); |
| }); |
| |
| test('Canvas.clipPath affects canvas.getClipBounds', () async { |
| final ui.PictureRecorder recorder = ui.PictureRecorder(); |
| final ui.Canvas canvas = ui.Canvas(recorder, const ui.Rect.fromLTRB(0, 0, 100, 100)); |
| const ui.Rect clipRawBounds = ui.Rect.fromLTRB(10.2, 11.3, 20.4, 25.7); |
| const ui.Rect clipExpandedBounds = ui.Rect.fromLTRB(10, 11, 21, 26); |
| final ui.Rect clipDestBounds = deviceClipRoundsOut ? clipExpandedBounds : clipRawBounds; |
| final ui.Path clip = ui.Path()..addRect(clipRawBounds)..addOval(clipRawBounds); |
| canvas.clipPath(clip); |
| |
| // Save initial return values for testing restored values |
| final ui.Rect initialLocalBounds = canvas.getLocalClipBounds(); |
| final ui.Rect initialDestinationBounds = canvas.getDestinationClipBounds(); |
| rectsClose(initialLocalBounds, clipExpandedBounds); |
| rectsClose(initialDestinationBounds, clipDestBounds); |
| |
| canvas.save(); |
| canvas.clipRect(const ui.Rect.fromLTRB(0, 0, 15, 15)); |
| // Both clip bounds have changed |
| rectsNotClose(canvas.getLocalClipBounds(), clipExpandedBounds); |
| rectsNotClose(canvas.getDestinationClipBounds(), clipDestBounds); |
| // Previous return values have not changed |
| rectsClose(initialLocalBounds, clipExpandedBounds); |
| rectsClose(initialDestinationBounds, clipDestBounds); |
| canvas.restore(); |
| |
| // save/restore returned the values to their original values |
| expect(canvas.getLocalClipBounds(), initialLocalBounds); |
| expect(canvas.getDestinationClipBounds(), initialDestinationBounds); |
| |
| canvas.save(); |
| canvas.scale(2, 2); |
| const ui.Rect scaledExpandedBounds = ui.Rect.fromLTRB(5, 5.5, 10.5, 13); |
| rectsClose(canvas.getLocalClipBounds(), scaledExpandedBounds); |
| // Destination bounds are unaffected by transform |
| rectsClose(canvas.getDestinationClipBounds(), clipDestBounds); |
| canvas.restore(); |
| |
| // save/restore returned the values to their original values |
| expect(canvas.getLocalClipBounds(), initialLocalBounds); |
| expect(canvas.getDestinationClipBounds(), initialDestinationBounds); |
| }); |
| |
| test('Canvas.clipRect(diff) does not affect canvas.getClipBounds', () async { |
| final ui.PictureRecorder recorder = ui.PictureRecorder(); |
| final ui.Canvas canvas = ui.Canvas(recorder, const ui.Rect.fromLTRB(0, 0, 100, 100)); |
| const ui.Rect clipRawBounds = ui.Rect.fromLTRB(10.2, 11.3, 20.4, 25.7); |
| const ui.Rect clipExpandedBounds = ui.Rect.fromLTRB(10, 11, 21, 26); |
| final ui.Rect clipDestBounds = deviceClipRoundsOut ? clipExpandedBounds : clipRawBounds; |
| canvas.clipRect(clipRawBounds); |
| |
| // Save initial return values for testing restored values |
| final ui.Rect initialLocalBounds = canvas.getLocalClipBounds(); |
| final ui.Rect initialDestinationBounds = canvas.getDestinationClipBounds(); |
| rectsClose(initialLocalBounds, clipExpandedBounds); |
| rectsClose(initialDestinationBounds, clipDestBounds); |
| |
| canvas.clipRect(const ui.Rect.fromLTRB(0, 0, 15, 15), clipOp: ui.ClipOp.difference); |
| expect(canvas.getLocalClipBounds(), initialLocalBounds); |
| expect(canvas.getDestinationClipBounds(), initialDestinationBounds); |
| }); |
| }); |
| |
| group('RestoreToCount function tests', () { |
| test('RestoreToCount can work', () async { |
| final ui.PictureRecorder recorder = ui.PictureRecorder(); |
| final ui.Canvas canvas = ui.Canvas(recorder); |
| canvas.save(); |
| canvas.save(); |
| canvas.save(); |
| canvas.save(); |
| canvas.save(); |
| expect(canvas.getSaveCount(), 6); |
| canvas.restoreToCount(2); |
| expect(canvas.getSaveCount(), 2); |
| canvas.restore(); |
| expect(canvas.getSaveCount(), 1); |
| }); |
| |
| test('RestoreToCount count less than 1, the stack should be reset', () async { |
| final ui.PictureRecorder recorder = ui.PictureRecorder(); |
| final ui.Canvas canvas = ui.Canvas(recorder); |
| canvas.save(); |
| canvas.save(); |
| canvas.save(); |
| canvas.save(); |
| canvas.save(); |
| expect(canvas.getSaveCount(), equals(6)); |
| canvas.restoreToCount(0); |
| expect(canvas.getSaveCount(), equals(1)); |
| }); |
| |
| test('RestoreToCount count greater than current [getSaveCount]', () async { |
| final ui.PictureRecorder recorder = ui.PictureRecorder(); |
| final ui.Canvas canvas = ui.Canvas(recorder); |
| canvas.save(); |
| canvas.save(); |
| canvas.save(); |
| canvas.save(); |
| canvas.save(); |
| expect(canvas.getSaveCount(), equals(6)); |
| canvas.restoreToCount(canvas.getSaveCount() + 1); |
| expect(canvas.getSaveCount(), equals(6)); |
| }); |
| }); |
| } |