blob: d2473d4261780868079d1018e69d3f65cb1b2e1d [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.
// @dart = 2.6
import 'dart:html' as html;
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:test/test.dart';
import 'matchers.dart';
void main() {
group('SceneBuilder', () {
test('pushOffset implements surface lifecycle', () {
testLayerLifeCycle((SceneBuilder sceneBuilder, EngineLayer oldLayer) {
return sceneBuilder.pushOffset(10, 20, oldLayer: oldLayer);
}, () {
return '''<s><flt-offset></flt-offset></s>''';
});
});
test('pushTransform implements surface lifecycle', () {
testLayerLifeCycle((SceneBuilder sceneBuilder, EngineLayer oldLayer) {
return sceneBuilder.pushTransform(
Matrix4.translationValues(10, 20, 0).storage,
oldLayer: oldLayer);
}, () {
return '''<s><flt-transform></flt-transform></s>''';
});
});
test('pushClipRect implements surface lifecycle', () {
testLayerLifeCycle((SceneBuilder sceneBuilder, EngineLayer oldLayer) {
return sceneBuilder.pushClipRect(const Rect.fromLTRB(10, 20, 30, 40),
oldLayer: oldLayer);
}, () {
return '''
<s>
<clip><clip-i></clip-i></clip>
</s>
''';
});
});
test('pushClipRRect implements surface lifecycle', () {
testLayerLifeCycle((SceneBuilder sceneBuilder, EngineLayer oldLayer) {
return sceneBuilder.pushClipRRect(
RRect.fromLTRBR(10, 20, 30, 40, const Radius.circular(3)),
oldLayer: oldLayer);
}, () {
return '''
<s>
<rclip><clip-i></clip-i></rclip>
</s>
''';
});
});
test('pushClipPath implements surface lifecycle', () {
testLayerLifeCycle((SceneBuilder sceneBuilder, EngineLayer oldLayer) {
final Path path = Path()..addRect(const Rect.fromLTRB(10, 20, 30, 40));
return sceneBuilder.pushClipPath(path, oldLayer: oldLayer);
}, () {
return '''
<s>
<flt-clippath>
<svg><defs><clipPath><path></path></clipPath></defs></svg>
</flt-clippath>
</s>
''';
});
});
test('pushOpacity implements surface lifecycle', () {
testLayerLifeCycle((SceneBuilder sceneBuilder, EngineLayer oldLayer) {
return sceneBuilder.pushOpacity(10, oldLayer: oldLayer);
}, () {
return '''<s><o></o></s>''';
});
});
test('pushPhysicalShape implements surface lifecycle', () {
testLayerLifeCycle((SceneBuilder sceneBuilder, EngineLayer oldLayer) {
final Path path = Path()..addRect(const Rect.fromLTRB(10, 20, 30, 40));
return sceneBuilder.pushPhysicalShape(
path: path,
elevation: 2,
color: const Color.fromRGBO(0, 0, 0, 1),
shadowColor: const Color.fromRGBO(0, 0, 0, 1),
oldLayer: oldLayer,
);
}, () {
return '''<s><pshape><clip-i></clip-i></pshape></s>''';
});
});
test('pushBackdropFilter implements surface lifecycle', () {
testLayerLifeCycle((SceneBuilder sceneBuilder, EngineLayer oldLayer) {
return sceneBuilder.pushBackdropFilter(
ImageFilter.blur(sigmaX: 1.0, sigmaY: 1.0),
oldLayer: oldLayer,
);
}, () {
return '<s><flt-backdrop>'
'<flt-backdrop-filter></flt-backdrop-filter>'
'<flt-backdrop-interior></flt-backdrop-interior>'
'</flt-backdrop></s>';
});
});
});
group('parent child lifecycle', () {
test(
'build, retain, update, and applyPaint are called the right number of times',
() {
final PersistedScene scene1 = PersistedScene(null);
final PersistedClipRect clip1 =
PersistedClipRect(null, const Rect.fromLTRB(10, 10, 20, 20));
final PersistedOpacity opacity = PersistedOpacity(null, 100, Offset.zero);
final MockPersistedPicture picture = MockPersistedPicture();
scene1.appendChild(clip1);
clip1.appendChild(opacity);
opacity.appendChild(picture);
expect(picture.retainCount, 0);
expect(picture.buildCount, 0);
expect(picture.updateCount, 0);
expect(picture.applyPaintCount, 0);
scene1.preroll();
scene1.build();
commitScene(scene1);
expect(picture.retainCount, 0);
expect(picture.buildCount, 1);
expect(picture.updateCount, 0);
expect(picture.applyPaintCount, 1);
// The second scene graph retains the opacity, but not the clip. However,
// because the clip didn't change no repaints should happen.
final PersistedScene scene2 = PersistedScene(scene1);
final PersistedClipRect clip2 =
PersistedClipRect(clip1, const Rect.fromLTRB(10, 10, 20, 20));
clip1.state = PersistedSurfaceState.pendingUpdate;
scene2.appendChild(clip2);
opacity.state = PersistedSurfaceState.pendingRetention;
clip2.appendChild(opacity);
scene2.preroll();
scene2.update(scene1);
commitScene(scene2);
expect(picture.retainCount, 1);
expect(picture.buildCount, 1);
expect(picture.updateCount, 0);
expect(picture.applyPaintCount, 1);
// The third scene graph retains the opacity, and produces a new clip.
// This should cause the picture to repaint despite being retained.
final PersistedScene scene3 = PersistedScene(scene2);
final PersistedClipRect clip3 =
PersistedClipRect(clip2, const Rect.fromLTRB(10, 10, 50, 50));
clip2.state = PersistedSurfaceState.pendingUpdate;
scene3.appendChild(clip3);
opacity.state = PersistedSurfaceState.pendingRetention;
clip3.appendChild(opacity);
scene3.preroll();
scene3.update(scene2);
commitScene(scene3);
expect(picture.retainCount, 2);
expect(picture.buildCount, 1);
expect(picture.updateCount, 0);
expect(picture.applyPaintCount, 2);
}, // TODO(nurhan): https://github.com/flutter/flutter/issues/46638
skip: (browserEngine == BrowserEngine.firefox));
});
}
typedef TestLayerBuilder = EngineLayer Function(
SceneBuilder sceneBuilder, EngineLayer oldLayer);
typedef ExpectedHtmlGetter = String Function();
void testLayerLifeCycle(
TestLayerBuilder layerBuilder, ExpectedHtmlGetter expectedHtmlGetter) {
// Force scene builder to start from scratch. This guarantees that the first
// scene starts from the "build" phase.
SurfaceSceneBuilder.debugForgetFrameScene();
// Build: builds a brand new layer.
SceneBuilder sceneBuilder = SceneBuilder();
final EngineLayer layer1 = layerBuilder(sceneBuilder, null);
final Type surfaceType = layer1.runtimeType;
sceneBuilder.pop();
SceneTester tester = SceneTester(sceneBuilder.build());
tester.expectSceneHtml(expectedHtmlGetter());
PersistedSurface findSurface() {
return enumerateSurfaces()
.where((PersistedSurface s) => s.runtimeType == surfaceType)
.single;
}
final PersistedSurface surface1 = findSurface();
final html.Element surfaceElement1 = surface1.rootElement;
// Retain: reuses a layer as is along with its DOM elements.
sceneBuilder = SceneBuilder();
sceneBuilder.addRetained(layer1);
tester = SceneTester(sceneBuilder.build());
tester.expectSceneHtml(expectedHtmlGetter());
final PersistedSurface surface2 = findSurface();
final html.Element surfaceElement2 = surface2.rootElement;
expect(surface2, same(surface1));
expect(surfaceElement2, same(surfaceElement1));
// Reuse: reuses a layer's DOM elements by matching it.
sceneBuilder = SceneBuilder();
final EngineLayer layer3 = layerBuilder(sceneBuilder, layer1);
sceneBuilder.pop();
expect(layer3, isNot(same(layer1)));
tester = SceneTester(sceneBuilder.build());
tester.expectSceneHtml(expectedHtmlGetter());
final PersistedSurface surface3 = findSurface();
expect(surface3, same(layer3));
final html.Element surfaceElement3 = surface3.rootElement;
expect(surface3, isNot(same(surface2)));
expect(surfaceElement3, isNotNull);
expect(surfaceElement3, same(surfaceElement2));
// Recycle: discards all the layers.
sceneBuilder = SceneBuilder();
tester = SceneTester(sceneBuilder.build());
tester.expectSceneHtml('<s></s>');
expect(surface3.rootElement, isNull); // offset3 should be recycled.
// Retain again: the framework should be able to request that a layer is added
// as retained even after it has been recycled. In this case the
// engine would "rehydrate" the layer with new DOM elements.
sceneBuilder = SceneBuilder();
sceneBuilder.addRetained(layer3);
tester = SceneTester(sceneBuilder.build());
tester.expectSceneHtml(expectedHtmlGetter());
expect(surface3.rootElement, isNotNull); // offset3 should be rehydrated.
// Make sure we clear retained surface list.
expect(debugRetainedSurfaces, isEmpty);
}
class MockPersistedPicture extends PersistedPicture {
factory MockPersistedPicture() {
final EnginePictureRecorder recorder = PictureRecorder();
// Use the largest cull rect so that layer clips are effective. The tests
// rely on this.
recorder.beginRecording(Rect.largest)..drawPaint(Paint());
return MockPersistedPicture._(recorder.endRecording());
}
MockPersistedPicture._(Picture picture) : super(0, 0, picture, 0);
int retainCount = 0;
int buildCount = 0;
int updateCount = 0;
int applyPaintCount = 0;
final BitmapCanvas _fakeCanvas = BitmapCanvas(const Rect.fromLTRB(0, 0, 10, 10));
@override
EngineCanvas get debugCanvas {
return _fakeCanvas;
}
@override
double matchForUpdate(PersistedPicture existingSurface) {
return identical(existingSurface.picture, picture) ? 0.0 : 1.0;
}
@override
Matrix4 get localTransformInverse => null;
@override
void build() {
super.build();
buildCount++;
}
@override
void retain() {
super.retain();
retainCount++;
}
@override
void applyPaint(EngineCanvas oldCanvas) {
applyPaintCount++;
}
@override
void update(PersistedPicture oldSurface) {
super.update(oldSurface);
updateCount++;
}
@override
int get bitmapPixelCount => 0;
}