blob: 3ffe8e0ccdf888a568837a5cf4d495974587c0bc [file] [log] [blame]
// 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' 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/rendering.dart';
import '../common/test_initialization.dart';
import 'utils.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
setUpUnitTests(
withImplicitView: true,
emulateTesterEnvironment: false,
setUpTestViewDimensions: false,
);
group('${ui.SceneBuilder}', () {
const ui.Rect region = ui.Rect.fromLTWH(0, 0, 300, 300);
test('Test offset layer', () async {
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
sceneBuilder.pushOffset(150, 150);
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawCircle(
ui.Offset.zero, 50, ui.Paint()..color = const ui.Color(0xFF00FF00));
}));
await renderScene(sceneBuilder.build());
await matchGoldenFile('scene_builder_centered_circle.png',
region: region);
});
test('Test transform layer', () async {
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
final Matrix4 transform = Matrix4.identity();
// The html renderer expects the top-level transform to just be a scaling
// matrix for the device pixel ratio, so just push the identity matrix.
sceneBuilder.pushTransform(transform.toFloat64());
transform.translate(150, 150);
transform.rotate(kUnitZ, math.pi / 3);
sceneBuilder.pushTransform(transform.toFloat64());
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawRRect(
ui.RRect.fromRectAndRadius(
ui.Rect.fromCircle(center: ui.Offset.zero, radius: 50),
const ui.Radius.circular(10)),
ui.Paint()..color = const ui.Color(0xFF0000FF));
}));
await renderScene(sceneBuilder.build());
await matchGoldenFile('scene_builder_rotated_rounded_square.png',
region: region);
});
test('Test clipRect layer', () async {
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
sceneBuilder.pushClipRect(const ui.Rect.fromLTRB(0, 0, 150, 150));
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawCircle(const ui.Offset(150, 150), 50,
ui.Paint()..color = const ui.Color(0xFFFF0000));
}));
await renderScene(sceneBuilder.build());
await matchGoldenFile('scene_builder_circle_clip_rect.png',
region: region);
});
test('Devtools rendering regression test', () async {
// This is a regression test for https://github.com/flutter/devtools/issues/8401
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
sceneBuilder.pushClipRect(const ui.Rect.fromLTRB(12.0, 0.0, 300.0, 27.0));
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawOval(
const ui.Rect.fromLTRB(15.0, 5.0, 64.0, 21.0),
ui.Paint()..color = const ui.Color(0xFF0000FF),
);
}));
await renderScene(sceneBuilder.build());
await matchGoldenFile('scene_builder_oval_clip_rect.png',
region: region);
});
test('Test clipRRect layer', () async {
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
sceneBuilder.pushClipRRect(
ui.RRect.fromRectAndRadius(
const ui.Rect.fromLTRB(0, 0, 150, 150),
const ui.Radius.circular(25),
),
clipBehavior: ui.Clip.antiAlias);
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawCircle(const ui.Offset(150, 150), 50,
ui.Paint()..color = const ui.Color(0xFFFF00FF));
}));
await renderScene(sceneBuilder.build());
await matchGoldenFile('scene_builder_circle_clip_rrect.png',
region: region);
});
test('Test clipPath layer', () async {
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
final ui.Path path = ui.Path();
path.addOval(
ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 60));
sceneBuilder.pushClipPath(path);
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawRect(
ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 50),
ui.Paint()..color = const ui.Color(0xFF00FFFF));
}));
await renderScene(sceneBuilder.build());
await matchGoldenFile('scene_builder_rectangle_clip_circular_path.png',
region: region);
});
test('Test opacity layer', () async {
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawRect(
ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 50),
ui.Paint()..color = const ui.Color(0xFF00FF00));
}));
sceneBuilder.pushOpacity(0x7F, offset: const ui.Offset(150, 150));
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
final ui.Paint paint = ui.Paint()..color = const ui.Color(0xFFFF0000);
canvas.drawCircle(const ui.Offset(-25, 0), 50, paint);
canvas.drawCircle(const ui.Offset(25, 0), 50, paint);
}));
await renderScene(sceneBuilder.build());
await matchGoldenFile('scene_builder_opacity_circles_on_square.png',
region: region);
});
test('shader mask layer', () async {
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
final ui.Paint paint = ui.Paint()..color = const ui.Color(0xFFFF0000);
canvas.drawCircle(const ui.Offset(125, 150), 50, paint);
canvas.drawCircle(const ui.Offset(175, 150), 50, paint);
}));
final ui.Shader shader = ui.Gradient.linear(
ui.Offset.zero, const ui.Offset(50, 50), <ui.Color>[
const ui.Color(0xFFFFFFFF),
const ui.Color(0x00000000),
]);
sceneBuilder.pushShaderMask(shader,
const ui.Rect.fromLTRB(125, 125, 175, 175), ui.BlendMode.srcATop);
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawRect(
ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 50),
ui.Paint()..color = const ui.Color(0xFF00FF00));
}));
await renderScene(sceneBuilder.build());
await matchGoldenFile('scene_builder_shader_mask.png', region: region);
},
skip: isFirefox &&
isHtml); // https://github.com/flutter/flutter/issues/86623
test('backdrop filter layer', () async {
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
// Create a red and blue checkerboard pattern
final ui.Paint redPaint = ui.Paint()
..color = const ui.Color(0xFFFF0000);
final ui.Paint bluePaint = ui.Paint()
..color = const ui.Color(0xFF0000FF);
for (double y = 0; y < 300; y += 10) {
for (double x = 0; x < 300; x += 10) {
final ui.Paint paint = ((x + y) % 20 == 0) ? redPaint : bluePaint;
canvas.drawRect(ui.Rect.fromLTWH(x, y, 10, 10), paint);
}
}
}));
sceneBuilder.pushBackdropFilter(ui.ImageFilter.blur(
sigmaX: 3.0,
sigmaY: 3.0,
));
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawCircle(const ui.Offset(150, 150), 50,
ui.Paint()..color = const ui.Color(0xFF00FF00));
}));
await renderScene(sceneBuilder.build());
await matchGoldenFile('scene_builder_backdrop_filter.png',
region: region);
});
test('empty backdrop filter layer with clip', () async {
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
// Create a red and blue checkerboard pattern
final ui.Paint redPaint = ui.Paint()
..color = const ui.Color(0xFFFF0000);
final ui.Paint bluePaint = ui.Paint()
..color = const ui.Color(0xFF0000FF);
for (double y = 0; y < 300; y += 10) {
for (double x = 0; x < 300; x += 10) {
final ui.Paint paint = ((x + y) % 20 == 0) ? redPaint : bluePaint;
canvas.drawRect(ui.Rect.fromLTWH(x, y, 10, 10), paint);
}
}
}));
sceneBuilder.pushClipRect(const ui.Rect.fromLTRB(100, 100, 200, 200));
sceneBuilder.pushBackdropFilter(ui.ImageFilter.blur(
sigmaX: 3.0,
sigmaY: 3.0,
));
await renderScene(sceneBuilder.build());
await matchGoldenFile('scene_builder_empty_backdrop_filter_with_clip.png',
region: region);
});
test('blur image filter layer', () async {
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
sceneBuilder.pushImageFilter(ui.ImageFilter.blur(
sigmaX: 5.0,
sigmaY: 5.0,
));
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawCircle(const ui.Offset(150, 150), 50,
ui.Paint()..color = const ui.Color(0xFF00FF00));
}));
await renderScene(sceneBuilder.build());
await matchGoldenFile('scene_builder_image_filter.png', region: region);
});
test('matrix image filter layer', () async {
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
sceneBuilder.pushOffset(50.0, 50.0);
final Matrix4 matrix = Matrix4.rotationZ(math.pi / 18);
final ui.ImageFilter matrixFilter = ui.ImageFilter.matrix(toMatrix64(matrix.storage));
sceneBuilder.pushImageFilter(matrixFilter);
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawRect(
region,
ui.Paint()..color = const ui.Color(0xFF00FF00)
);
}));
await renderScene(sceneBuilder.build());
await matchGoldenFile('scene_builder_matrix_image_filter.png', region: region);
});
// Regression test for https://github.com/flutter/flutter/issues/154303
test('image filter layer with offset', () async {
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
sceneBuilder.pushClipRect(const ui.Rect.fromLTWH(100, 100, 100, 100));
sceneBuilder.pushImageFilter(
ui.ImageFilter.blur(
sigmaX: 5.0,
sigmaY: 5.0,
),
offset: const ui.Offset(100, 100),
);
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawCircle(const ui.Offset(50, 50), 25,
ui.Paint()..color = const ui.Color(0xFF00FF00));
}));
await renderScene(sceneBuilder.build());
await matchGoldenFile(
'scene_builder_image_filter_with_offset.png',
region: region,
);
});
test('color filter layer', () async {
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
const ui.ColorFilter sepia = ui.ColorFilter.matrix(<double>[
0.393,
0.769,
0.189,
0,
0,
0.349,
0.686,
0.168,
0,
0,
0.272,
0.534,
0.131,
0,
0,
0,
0,
0,
1,
0,
]);
sceneBuilder.pushColorFilter(sepia);
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawCircle(const ui.Offset(150, 150), 50,
ui.Paint()..color = const ui.Color(0xFF00FF00));
}));
await renderScene(sceneBuilder.build());
await matchGoldenFile('scene_builder_color_filter.png', region: region);
});
test('overlapping pictures in opacity layer', () async {
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
sceneBuilder.pushOpacity(128);
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawCircle(const ui.Offset(100, 150), 100,
ui.Paint()..color = const ui.Color(0xFFFF0000));
}));
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawCircle(const ui.Offset(200, 150), 100,
ui.Paint()..color = const ui.Color(0xFFFF0000));
}));
sceneBuilder.pop();
await renderScene(sceneBuilder.build());
await matchGoldenFile('scene_builder_overlapping_pictures_in_opacity.png',
region: region);
});
test('picture clipped out in final scene', () async {
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
sceneBuilder.pushClipRect(const ui.Rect.fromLTRB(0, 0, 125, 300));
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawCircle(const ui.Offset(50, 150), 50,
ui.Paint()..color = const ui.Color(0xFFFF0000));
}));
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawCircle(const ui.Offset(200, 150), 50,
ui.Paint()..color = const ui.Color(0xFFFF0000));
}));
sceneBuilder.pop();
await renderScene(sceneBuilder.build());
await matchGoldenFile('scene_builder_picture_clipped_out.png',
region: region);
});
test('picture clipped but scrolls back in', () async {
// Frame 1: Clip out the right circle
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
sceneBuilder.pushClipRect(const ui.Rect.fromLTRB(0, 0, 125, 300));
// Save this offsetLayer to add back in so we are using the same
// picture layers on the next scene.
final ui.OffsetEngineLayer offsetLayer = sceneBuilder.pushOffset(0, 0);
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawCircle(const ui.Offset(50, 150), 50,
ui.Paint()..color = const ui.Color(0xFFFF0000));
}));
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawCircle(const ui.Offset(200, 150), 50,
ui.Paint()..color = const ui.Color(0xFFFF0000));
}));
sceneBuilder.pop();
sceneBuilder.pop();
await renderScene(sceneBuilder.build());
// Frame 2: Clip out the left circle
final ui.SceneBuilder sceneBuilder2 = ui.SceneBuilder();
sceneBuilder2.pushClipRect(const ui.Rect.fromLTRB(150, 0, 300, 300));
sceneBuilder2.addRetained(offsetLayer);
sceneBuilder2.pop();
await renderScene(sceneBuilder2.build());
// Frame 3: Clip out the right circle again
final ui.SceneBuilder sceneBuilder3 = ui.SceneBuilder();
sceneBuilder3.pushClipRect(const ui.Rect.fromLTRB(0, 0, 125, 300));
sceneBuilder3.addRetained(offsetLayer);
sceneBuilder3.pop();
await renderScene(sceneBuilder3.build());
await matchGoldenFile(
'scene_builder_picture_clipped_out_then_clipped_in.png',
region: region);
});
test('shader mask parent of clipped out picture', () async {
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
final ui.Shader shader = ui.Gradient.linear(
ui.Offset.zero, const ui.Offset(50, 50), <ui.Color>[
const ui.Color(0xFFFFFFFF),
const ui.Color(0x00000000),
]);
sceneBuilder.pushClipRect(const ui.Rect.fromLTRB(0, 0, 125, 300));
sceneBuilder.pushShaderMask(shader,
const ui.Rect.fromLTRB(25, 125, 75, 175), ui.BlendMode.srcATop);
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawCircle(const ui.Offset(50, 150), 50,
ui.Paint()..color = const ui.Color(0xFFFF0000));
}));
sceneBuilder.pop();
sceneBuilder.pushShaderMask(shader,
const ui.Rect.fromLTRB(175, 125, 225, 175), ui.BlendMode.srcATop);
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawCircle(const ui.Offset(200, 150), 50,
ui.Paint()..color = const ui.Color(0xFFFF0000));
}));
sceneBuilder.pop();
sceneBuilder.pop();
await renderScene(sceneBuilder.build());
await matchGoldenFile('scene_builder_shader_mask_clipped_out.png',
region: region);
},
skip: isFirefox &&
isHtml); // https://github.com/flutter/flutter/issues/86623
test('opacity layer with transformed children', () async {
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
sceneBuilder.pushOffset(0, 0);
sceneBuilder.pushOpacity(128);
// Push some complex transforms
final Float64List transform1 = Float64List.fromList([
1.00,
0.00,
0.00,
0.00,
0.06,
1.00,
-0.88,
0.00,
-0.03,
0.60,
0.47,
-0.00,
-4.58,
257.03,
63.11,
0.81,
]);
final Float64List transform2 = Float64List.fromList([
1.00,
0.00,
0.00,
0.00,
0.07,
0.90,
-0.94,
0.00,
-0.02,
0.75,
0.33,
-0.00,
-3.28,
309.29,
45.20,
0.86,
]);
sceneBuilder
.pushTransform(Matrix4.identity().scaled(0.3, 0.3).toFloat64());
sceneBuilder.pushTransform(transform1);
sceneBuilder.pushTransform(transform2);
sceneBuilder.addPicture(const ui.Offset(20, 20),
drawPicture((ui.Canvas canvas) {
canvas.drawCircle(const ui.Offset(25, 75), 25,
ui.Paint()..color = const ui.Color(0xFFFF0000));
}));
sceneBuilder.pop();
sceneBuilder.pop();
sceneBuilder.pop();
sceneBuilder.pop();
sceneBuilder.pop();
await renderScene(sceneBuilder.build());
await matchGoldenFile(
'scene_builder_opacity_layer_with_transformed_children.png',
region: region);
});
test('backdrop layer with default blur tile mode', () async {
final scene = backdropBlurWithTileMode(null, 10, 50);
await renderScene(scene);
await matchGoldenFile(
'scene_builder_backdrop_filter_blur_default_tile_mode.png',
region: const ui.Rect.fromLTWH(0, 0, 10*50, 10*50));
},
// HTML renderer doesn't have tile modes
skip: isHtml);
test('backdrop layer with clamp blur tile mode', () async {
final scene = backdropBlurWithTileMode(ui.TileMode.clamp, 10, 50);
await renderScene(scene);
await matchGoldenFile(
'scene_builder_backdrop_filter_blur_clamp_tile_mode.png',
region: const ui.Rect.fromLTWH(0, 0, 10*50, 10*50));
},
// HTML renderer doesn't have tile modes
skip: isHtml);
test('backdrop layer with mirror blur tile mode', () async {
final scene = backdropBlurWithTileMode(ui.TileMode.mirror, 10, 50);
await renderScene(scene);
await matchGoldenFile(
'scene_builder_backdrop_filter_blur_mirror_tile_mode.png',
region: const ui.Rect.fromLTWH(0, 0, 10*50, 10*50));
},
// HTML renderer doesn't have tile modes
skip: isHtml);
test('backdrop layer with repeated blur tile mode', () async {
final scene = backdropBlurWithTileMode(ui.TileMode.repeated, 10, 50);
await renderScene(scene);
await matchGoldenFile(
'scene_builder_backdrop_filter_blur_repeated_tile_mode.png',
region: const ui.Rect.fromLTWH(0, 0, 10*50, 10*50));
},
// HTML renderer doesn't have tile modes
skip: isHtml);
test('backdrop layer with decal blur tile mode', () async {
final scene = backdropBlurWithTileMode(ui.TileMode.decal, 10, 50);
await renderScene(scene);
await matchGoldenFile(
'scene_builder_backdrop_filter_blur_decal_tile_mode.png',
region: const ui.Rect.fromLTWH(0, 0, 10*50, 10*50));
},
// HTML renderer doesn't have tile modes
skip: isHtml);
});
}
ui.Picture drawPicture(void Function(ui.Canvas) drawCommands) {
final ui.PictureRecorder recorder = ui.PictureRecorder();
final ui.Canvas canvas = ui.Canvas(recorder);
drawCommands(canvas);
return recorder.endRecording();
}
ui.Scene backdropBlurWithTileMode(ui.TileMode? tileMode,
final double rectSize,
final int count) {
final double imgSize = rectSize * count;
const ui.Color white = ui.Color(0xFFFFFFFF);
const ui.Color purple = ui.Color(0xFFFF00FF);
const ui.Color blue = ui.Color(0xFF0000FF);
const ui.Color green = ui.Color(0xFF00FF00);
const ui.Color yellow = ui.Color(0xFFFFFF00);
const ui.Color red = ui.Color(0xFFFF0000);
final ui.Picture blueGreenGridPicture = drawPicture((ui.Canvas canvas) {
canvas.drawColor(white, ui.BlendMode.src);
for (int i = 0; i < count; i++) {
for (int j = 0; j < count; j++) {
final bool rectOdd = (i + j) & 1 == 0;
final ui.Color fg = (i < count / 2)
? ((j < count / 2) ? green : blue)
: ((j < count / 2) ? yellow : red);
canvas.drawRect(ui.Rect.fromLTWH(i * rectSize, j * rectSize, rectSize, rectSize),
ui.Paint()..color = rectOdd ? fg : white);
}
}
canvas.drawRect(ui.Rect.fromLTWH(0, 0, imgSize, 1), ui.Paint()..color = purple);
canvas.drawRect(ui.Rect.fromLTWH(0, 0, 1, imgSize), ui.Paint()..color = purple);
canvas.drawRect(ui.Rect.fromLTWH(0, imgSize - 1, imgSize, 1), ui.Paint()..color = purple);
canvas.drawRect(ui.Rect.fromLTWH(imgSize - 1, 0, 1, imgSize), ui.Paint()..color = purple);
});
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
// We push a clipRect layer with the SaveLayer behavior so that it creates
// a layer of predetermined size in which the backdrop filter will apply
// its filter to show the edge effects on predictable edges.
sceneBuilder.pushClipRect(ui.Rect.fromLTWH(0, 0, imgSize, imgSize),
clipBehavior: ui.Clip.antiAliasWithSaveLayer);
sceneBuilder.addPicture(ui.Offset.zero, blueGreenGridPicture);
sceneBuilder.pushBackdropFilter(ui.ImageFilter.blur(sigmaX: 20, sigmaY: 20, tileMode: tileMode));
// The following picture prevents the saveLayer in the backdrop filter from
// being completely ignored on the skwasm backend due to an interaction with
// SkPictureRecorder eliminating saveLayer entries with no content even if
// they have a backdrop filter. It draws nothing because the pixels below
// it are opaque and dstOver is a NOP in that case, but it is unlikely that
// a recording process would be able to figure that out without extensive
// analysis between the pictures and layers.
sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) {
canvas.drawRect(ui.Rect.fromLTWH(imgSize * 0.5 - 10, imgSize * 0.5 - 10, 20, 20),
ui.Paint()..color = purple..blendMode = ui.BlendMode.dstOver);
}));
sceneBuilder.pop();
sceneBuilder.pop();
return sceneBuilder.build();
}