Unverified Commit 34c69265 authored by Yegor's avatar Yegor Committed by GitHub

Teach render objects to reuse engine layers (#36402)

Teach Layer and its implementations, RenderObject and its implementations, and PaintingContext to reuse engine layers. The idea is that a concrete RenderObject creates a Layer and holds on to it as long as it needs it (i.e. when it is composited, and the layer type does not change). In return, each Layer object holds on to an EngineLayer and reports it to the engine via addRetained and oldLayer. This allows the Web engine to reuse DOM elements across frames. Without it, each frame drops all previously rendered HTML and regenerates it from scratch.
parent 1a5e4a5d
......@@ -242,12 +242,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
// Creates a [MouseTracker] which manages state about currently connected
// mice, for hover notification.
MouseTracker _createMouseTracker() {
return MouseTracker(pointerRouter, (Offset offset) {
// Layer hit testing is done using device pixels, so we have to convert
// the logical coordinates of the event location back to device pixels
// here.
return renderView.layer.findAll<MouseTrackerAnnotation>(offset * window.devicePixelRatio);
});
return MouseTracker(pointerRouter, renderView.hitTestMouseTrackers);
}
void _handleSemanticsEnabledChanged() {
......
......@@ -7,6 +7,7 @@ import 'dart:io' show Platform;
import 'dart:ui' as ui show Scene, SceneBuilder, Window;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show MouseTrackerAnnotation;
import 'package:flutter/services.dart';
import 'package:vector_math/vector_math_64.dart';
......@@ -173,6 +174,19 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
return true;
}
/// Determines the set of mouse tracker annotations at the given position.
///
/// See also:
///
/// * [Layer.findAll], which is used by this method to find all
/// [AnnotatedRegionLayer]s annotated for mouse tracking.
Iterable<MouseTrackerAnnotation> hitTestMouseTrackers(Offset position) {
// Layer hit testing is done using device pixels, so we have to convert
// the logical coordinates of the event location back to device pixels
// here.
return layer.findAll<MouseTrackerAnnotation>(position * configuration.devicePixelRatio);
}
@override
bool get isRepaintBoundary => true;
......
......@@ -55,6 +55,6 @@ class _ColorFilterRenderObject extends RenderProxyBox {
@override
void paint(PaintingContext context, Offset offset) {
context.pushColorFilter(offset, colorFilter, super.paint);
layer = context.pushColorFilter(offset, colorFilter, super.paint, oldLayer: layer);
}
}
......@@ -10,7 +10,6 @@ import 'dart:typed_data';
import 'dart:ui' as ui
show
ClipOp,
EngineLayer,
Image,
ImageByteFormat,
Paragraph,
......@@ -53,8 +52,8 @@ class _ProxyLayer extends Layer {
final Layer _layer;
@override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
return _layer.addToScene(builder, layerOffset);
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
_layer.addToScene(builder, layerOffset);
}
@override
......@@ -314,9 +313,8 @@ Rect _calculateSubtreeBounds(RenderObject object) {
/// screenshots render to the scene in the local coordinate system of the layer.
class _ScreenshotContainerLayer extends OffsetLayer {
@override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
addChildrenToScene(builder, layerOffset);
return null; // this does not have an engine layer.
}
}
......@@ -556,9 +554,10 @@ class _ScreenshotPaintingContext extends PaintingContext {
// Painting the existing repaint boundary to the screenshot is sufficient.
// We don't just take a direct screenshot of the repaint boundary as we
// want to capture debugPaint information as well.
data.containerLayer.append(_ProxyLayer(repaintBoundary.layer));
data.containerLayer.append(_ProxyLayer(repaintBoundary.debugLayer));
data.foundTarget = true;
data.screenshotOffset = repaintBoundary.layer.offset;
final OffsetLayer offsetLayer = repaintBoundary.debugLayer;
data.screenshotOffset = offsetLayer.offset;
} else {
// Repaint everything under the repaint boundary.
// We call debugInstrumentRepaintCompositedChild instead of paintChild as
......@@ -591,7 +590,7 @@ class _ScreenshotPaintingContext extends PaintingContext {
// We must build the regular scene before we can build the screenshot
// scene as building the screenshot scene assumes addToScene has already
// been called successfully for all layers in the regular scene.
repaintBoundary.layer.buildScene(ui.SceneBuilder());
repaintBoundary.debugLayer.buildScene(ui.SceneBuilder());
return data.containerLayer.toImage(renderBounds, pixelRatio: pixelRatio);
}
......@@ -2504,9 +2503,9 @@ class _InspectorOverlayLayer extends Layer {
double _textPainterMaxWidth;
@override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
if (!selection.active)
return null;
return;
final RenderObject selected = selection.current;
final List<_TransformedRect> candidates = <_TransformedRect>[];
......@@ -2529,7 +2528,6 @@ class _InspectorOverlayLayer extends Layer {
_picture = _buildPicture(state);
}
builder.addPicture(layerOffset, _picture);
return null; // this does not have an engine layer.
}
ui.Picture _buildPicture(_InspectorOverlayRenderState state) {
......
......@@ -99,11 +99,6 @@ void main() {
});
test('RenderAspectRatio: Unbounded', () {
bool hadError = false;
final FlutterExceptionHandler oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
hadError = true;
};
final RenderBox box = RenderConstrainedOverflowBox(
maxWidth: double.infinity,
maxHeight: double.infinity,
......@@ -112,10 +107,16 @@ void main() {
child: RenderSizedBox(const Size(90.0, 70.0)),
),
);
expect(hadError, false);
layout(box);
expect(hadError, true);
FlutterError.onError = oldHandler;
final List<String> errorMessages = <String>[];
layout(box, onErrors: () {
errorMessages.addAll(
renderer.takeAllFlutterErrorDetails().map((FlutterErrorDetails details) => '${details.exceptionAsString()}'),
);
});
expect(errorMessages, hasLength(2));
expect(errorMessages[0], contains('RenderAspectRatio has unbounded constraints.'));
// The second error message is a generic message generated by the Dart VM. Not worth testing.
});
test('RenderAspectRatio: Sizing', () {
......
......@@ -365,10 +365,6 @@ void main() {
});
test('MainAxisSize.min inside unconstrained', () {
final List<dynamic> exceptions = <dynamic>[];
FlutterError.onError = (FlutterErrorDetails details) {
exceptions.add(details.exception);
};
const BoxConstraints square = BoxConstraints.tightFor(width: 100.0, height: 100.0);
final RenderConstrainedBox box1 = RenderConstrainedBox(additionalConstraints: square);
final RenderConstrainedBox box2 = RenderConstrainedBox(additionalConstraints: square);
......@@ -387,17 +383,15 @@ void main() {
flex.addAll(<RenderBox>[box1, box2, box3]);
final FlexParentData box2ParentData = box2.parentData;
box2ParentData.flex = 1;
expect(exceptions, isEmpty);
layout(parent);
final List<dynamic> exceptions = <dynamic>[];
layout(parent, onErrors: () {
exceptions.addAll(renderer.takeAllFlutterExceptions());
});
expect(exceptions, isNotEmpty);
expect(exceptions.first, isInstanceOf<FlutterError>());
});
test('MainAxisSize.min inside unconstrained', () {
final List<dynamic> exceptions = <dynamic>[];
FlutterError.onError = (FlutterErrorDetails details) {
exceptions.add(details.exception);
};
const BoxConstraints square = BoxConstraints.tightFor(width: 100.0, height: 100.0);
final RenderConstrainedBox box1 = RenderConstrainedBox(additionalConstraints: square);
final RenderConstrainedBox box2 = RenderConstrainedBox(additionalConstraints: square);
......@@ -417,8 +411,10 @@ void main() {
final FlexParentData box2ParentData = box2.parentData;
box2ParentData.flex = 1;
box2ParentData.fit = FlexFit.loose;
expect(exceptions, isEmpty);
layout(parent);
final List<dynamic> exceptions = <dynamic>[];
layout(parent, onErrors: () {
exceptions.addAll(renderer.takeAllFlutterExceptions());
});
expect(exceptions, isNotEmpty);
expect(exceptions.first, isInstanceOf<FlutterError>());
});
......
......@@ -22,29 +22,75 @@ void main() {
);
layout(root, phase: EnginePhase.paint);
expect(inner.isRepaintBoundary, isFalse);
expect(() => inner.layer, throwsAssertionError);
expect(inner.debugLayer, null);
expect(boundary.isRepaintBoundary, isTrue);
expect(boundary.layer, isNotNull);
expect(boundary.layer.attached, isTrue); // this time it painted...
expect(boundary.debugLayer, isNotNull);
expect(boundary.debugLayer.attached, isTrue); // this time it painted...
root.opacity = 0.0;
pumpFrame(phase: EnginePhase.paint);
expect(inner.isRepaintBoundary, isFalse);
expect(() => inner.layer, throwsAssertionError);
expect(inner.debugLayer, null);
expect(boundary.isRepaintBoundary, isTrue);
expect(boundary.layer, isNotNull);
expect(boundary.layer.attached, isFalse); // this time it did not.
expect(boundary.debugLayer, isNotNull);
expect(boundary.debugLayer.attached, isFalse); // this time it did not.
root.opacity = 0.5;
pumpFrame(phase: EnginePhase.paint);
expect(inner.isRepaintBoundary, isFalse);
expect(() => inner.layer, throwsAssertionError);
expect(inner.debugLayer, null);
expect(boundary.isRepaintBoundary, isTrue);
expect(boundary.layer, isNotNull);
expect(boundary.layer.attached, isTrue); // this time it did again!
expect(boundary.debugLayer, isNotNull);
expect(boundary.debugLayer.attached, isTrue); // this time it did again!
});
test('layer subtree dirtiness is correctly computed', () {
test('updateSubtreeNeedsAddToScene propagates Layer.alwaysNeedsAddToScene up the tree', () {
final ContainerLayer a = ContainerLayer();
final ContainerLayer b = ContainerLayer();
final ContainerLayer c = ContainerLayer();
final _TestAlwaysNeedsAddToSceneLayer d = _TestAlwaysNeedsAddToSceneLayer();
final ContainerLayer e = ContainerLayer();
final ContainerLayer f = ContainerLayer();
// Tree structure:
// a
// / \
// b c
// / \
// (x)d e
// /
// f
a.append(b);
a.append(c);
b.append(d);
b.append(e);
d.append(f);
a.debugMarkClean();
b.debugMarkClean();
c.debugMarkClean();
d.debugMarkClean();
e.debugMarkClean();
f.debugMarkClean();
expect(a.debugSubtreeNeedsAddToScene, false);
expect(b.debugSubtreeNeedsAddToScene, false);
expect(c.debugSubtreeNeedsAddToScene, false);
expect(d.debugSubtreeNeedsAddToScene, false);
expect(e.debugSubtreeNeedsAddToScene, false);
expect(f.debugSubtreeNeedsAddToScene, false);
a.updateSubtreeNeedsAddToScene();
expect(a.debugSubtreeNeedsAddToScene, true);
expect(b.debugSubtreeNeedsAddToScene, true);
expect(c.debugSubtreeNeedsAddToScene, false);
expect(d.debugSubtreeNeedsAddToScene, true);
expect(e.debugSubtreeNeedsAddToScene, false);
expect(f.debugSubtreeNeedsAddToScene, false);
});
test('updateSubtreeNeedsAddToScene propagates Layer._needsAddToScene up the tree', () {
final ContainerLayer a = ContainerLayer();
final ContainerLayer b = ContainerLayer();
final ContainerLayer c = ContainerLayer();
......@@ -52,53 +98,59 @@ void main() {
final ContainerLayer e = ContainerLayer();
final ContainerLayer f = ContainerLayer();
final ContainerLayer g = ContainerLayer();
final PictureLayer h = PictureLayer(Rect.zero);
final PictureLayer i = PictureLayer(Rect.zero);
final PictureLayer j = PictureLayer(Rect.zero);
final List<ContainerLayer> allLayers = <ContainerLayer>[a, b, c, d, e, f, g];
// The tree is like the following where b and j are dirty:
// a____
// / \
// (x)b___ c
// / \ \ |
// d e f g
// / \ |
// h i j(x)
// d e f g(x)
a.append(b);
a.append(c);
b.append(d);
b.append(e);
b.append(f);
d.append(h);
d.append(i);
c.append(g);
g.append(j);
a.debugMarkClean();
for (ContainerLayer layer in allLayers) {
expect(layer.debugSubtreeNeedsAddToScene, true);
}
for (ContainerLayer layer in allLayers) {
layer.debugMarkClean();
}
for (ContainerLayer layer in allLayers) {
expect(layer.debugSubtreeNeedsAddToScene, false);
}
b.markNeedsAddToScene();
c.debugMarkClean();
d.debugMarkClean();
e.debugMarkClean();
f.debugMarkClean();
g.debugMarkClean();
h.debugMarkClean();
i.debugMarkClean();
j.markNeedsAddToScene();
a.updateSubtreeNeedsAddToScene();
expect(a.debugSubtreeNeedsAddToScene, true);
expect(b.debugSubtreeNeedsAddToScene, true);
expect(c.debugSubtreeNeedsAddToScene, false);
expect(d.debugSubtreeNeedsAddToScene, false);
expect(e.debugSubtreeNeedsAddToScene, false);
expect(f.debugSubtreeNeedsAddToScene, false);
expect(g.debugSubtreeNeedsAddToScene, false);
g.markNeedsAddToScene();
a.updateSubtreeNeedsAddToScene();
expect(a.debugSubtreeNeedsAddToScene, true);
expect(b.debugSubtreeNeedsAddToScene, true);
expect(c.debugSubtreeNeedsAddToScene, true);
expect(g.debugSubtreeNeedsAddToScene, true);
expect(j.debugSubtreeNeedsAddToScene, true);
expect(d.debugSubtreeNeedsAddToScene, false);
expect(e.debugSubtreeNeedsAddToScene, false);
expect(f.debugSubtreeNeedsAddToScene, false);
expect(h.debugSubtreeNeedsAddToScene, false);
expect(i.debugSubtreeNeedsAddToScene, false);
expect(g.debugSubtreeNeedsAddToScene, true);
a.buildScene(SceneBuilder());
for (ContainerLayer layer in allLayers) {
expect(layer.debugSubtreeNeedsAddToScene, false);
}
});
test('leader and follower layers are always dirty', () {
......@@ -465,4 +517,27 @@ void main() {
_testConflicts(layerA, layerB, expectedErrorCount: 1);
});
}, skip: isBrowser);
test('ContainerLayer.toImage can render interior layer', () {
final OffsetLayer parent = OffsetLayer();
final OffsetLayer child = OffsetLayer();
final OffsetLayer grandChild = OffsetLayer();
child.append(grandChild);
parent.append(child);
// This renders the layers and generates engine layers.
parent.buildScene(SceneBuilder());
// Causes grandChild to pass its engine layer as `oldLayer`
grandChild.toImage(const Rect.fromLTRB(0, 0, 10, 10));
// Ensure we can render the same scene again after rendering an interior
// layer.
parent.buildScene(SceneBuilder());
});
}
class _TestAlwaysNeedsAddToSceneLayer extends ContainerLayer {
@override
bool get alwaysNeedsAddToScene => true;
}
......@@ -5,13 +5,14 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/src/binding.dart' show TestWidgetsFlutterBinding;
import 'package:flutter_test/flutter_test.dart';
import 'rendering_tester.dart';
void main() {
test('ensure frame is scheduled for markNeedsSemanticsUpdate', () {
// Initialize all bindings because owner.flushSemantics() requires a window
TestWidgetsFlutterBinding.ensureInitialized();
renderer;
final TestRenderObject renderObject = TestRenderObject();
int onNeedVisualUpdateCallCount = 0;
......@@ -98,6 +99,81 @@ void main() {
..nextSibling = RenderOpacity();
expect(() => data3.detach(), throwsAssertionError);
});
test('PaintingContext.pushClipRect reuses the layer', () {
_testPaintingContextLayerReuse<ClipRectLayer>((PaintingContextCallback painter, PaintingContext context, Offset offset, Layer oldLayer) {
return context.pushClipRect(true, offset, Rect.zero, painter, oldLayer: oldLayer);
});
});
test('PaintingContext.pushClipRRect reuses the layer', () {
_testPaintingContextLayerReuse<ClipRRectLayer>((PaintingContextCallback painter, PaintingContext context, Offset offset, Layer oldLayer) {
return context.pushClipRRect(true, offset, Rect.zero, RRect.fromRectAndRadius(Rect.zero, const Radius.circular(1.0)), painter, oldLayer: oldLayer);
});
});
test('PaintingContext.pushClipPath reuses the layer', () {
_testPaintingContextLayerReuse<ClipPathLayer>((PaintingContextCallback painter, PaintingContext context, Offset offset, Layer oldLayer) {
return context.pushClipPath(true, offset, Rect.zero, Path(), painter, oldLayer: oldLayer);
});
});
test('PaintingContext.pushColorFilter reuses the layer', () {
_testPaintingContextLayerReuse<ColorFilterLayer>((PaintingContextCallback painter, PaintingContext context, Offset offset, Layer oldLayer) {
return context.pushColorFilter(offset, const ColorFilter.mode(Color.fromRGBO(0, 0, 0, 1.0), BlendMode.clear), painter, oldLayer: oldLayer);
});
});
test('PaintingContext.pushTransform reuses the layer', () {
_testPaintingContextLayerReuse<TransformLayer>((PaintingContextCallback painter, PaintingContext context, Offset offset, Layer oldLayer) {
return context.pushTransform(true, offset, Matrix4.identity(), painter, oldLayer: oldLayer);
});
});
test('PaintingContext.pushOpacity reuses the layer', () {
_testPaintingContextLayerReuse<OpacityLayer>((PaintingContextCallback painter, PaintingContext context, Offset offset, Layer oldLayer) {
return context.pushOpacity(offset, 100, painter, oldLayer: oldLayer);
});
});
}
// Tests the create-update cycle by pumping two frames. The first frame has no
// prior layer and forces the painting context to create a new one. The second
// frame reuses the layer painted on the first frame.
void _testPaintingContextLayerReuse<L extends Layer>(_LayerTestPaintCallback painter) {
final _TestCustomLayerBox box = _TestCustomLayerBox(painter);
layout(box, phase: EnginePhase.paint);
// Force a repaint. Otherwise, pumpFrame is a noop.
box.markNeedsPaint();
pumpFrame(phase: EnginePhase.paint);
expect(box.paintedLayers, hasLength(2));
expect(box.paintedLayers[0], isInstanceOf<L>());
expect(box.paintedLayers[0], same(box.paintedLayers[1]));
}
typedef _LayerTestPaintCallback = Layer Function(PaintingContextCallback painter, PaintingContext context, Offset offset, Layer oldLayer);
class _TestCustomLayerBox extends RenderBox {
_TestCustomLayerBox(this.painter);
final _LayerTestPaintCallback painter;
final List<Layer> paintedLayers = <Layer>[];
@override
bool get isRepaintBoundary => false;
@override
void performLayout() {
size = constraints.smallest;
}
@override
void paint(PaintingContext context, Offset offset) {
final Layer paintedLayer = painter(super.paint, context, offset, layer);
paintedLayers.add(paintedLayer);
layer = paintedLayer;
}
}
class TestParentData extends ParentData with ContainerParentDataMixin<RenderBox> { }
......
......@@ -16,17 +16,11 @@ void main() {
// compatible with existing tests in object_test.dart.
test('reentrant paint error', () {
FlutterErrorDetails errorDetails;
final FlutterExceptionHandler oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
errorDetails = details;
};
final RenderBox root = TestReentrantPaintingErrorRenderBox();
try {
layout(root);
pumpFrame(phase: EnginePhase.paint);
} finally {
FlutterError.onError = oldHandler;
}
layout(root, onErrors: () {
errorDetails = renderer.takeFlutterErrorDetails();
});
pumpFrame(phase: EnginePhase.paint);
expect(errorDetails, isNotNull);
expect(errorDetails.stack, isNotNull);
......
......@@ -3,7 +3,7 @@
// found in the LICENSE file.
import 'dart:typed_data';
import 'dart:ui' as ui show Image;
import 'dart:ui' as ui show Gradient, Image, ImageFilter;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
......@@ -218,7 +218,7 @@ void main() {
expect(getPixel(0, 0), equals(0x00000080));
expect(getPixel(image.width - 1, 0 ), equals(0xffffffff));
final OffsetLayer layer = boundary.layer;
final OffsetLayer layer = boundary.debugLayer;
image = await layer.toImage(Offset.zero & const Size(20.0, 20.0));
expect(image.width, equals(20));
......@@ -268,6 +268,13 @@ void main() {
expect(renderOpacity.needsCompositing, false);
});
test('RenderOpacity reuses its layer', () {
_testLayerReuse<OpacityLayer>(RenderOpacity(
opacity: 0.5, // must not be 0 or 1.0. Otherwise, it won't create a layer
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
));
});
test('RenderAnimatedOpacity does not composite if it is transparent', () async {
final Animation<double> opacityAnimation = AnimationController(
vsync: _FakeTickerProvider(),
......@@ -297,6 +304,183 @@ void main() {
layout(renderAnimatedOpacity, phase: EnginePhase.composite);
expect(renderAnimatedOpacity.needsCompositing, false);
});
test('RenderAnimatedOpacity reuses its layer', () {
final Animation<double> opacityAnimation = AnimationController(
vsync: _FakeTickerProvider(),
)..value = 0.5; // must not be 0 or 1.0. Otherwise, it won't create a layer
_testLayerReuse<OpacityLayer>(RenderAnimatedOpacity(
opacity: opacityAnimation,
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
));
});
test('RenderShaderMask reuses its layer', () {
_testLayerReuse<ShaderMaskLayer>(RenderShaderMask(
shaderCallback: (Rect rect) {
return ui.Gradient.radial(
rect.center,
rect.shortestSide / 2.0,
const <Color>[Color.fromRGBO(0, 0, 0, 1.0), Color.fromRGBO(255, 255, 255, 1.0)],
);
},
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
));
});
test('RenderBackdropFilter reuses its layer', () {
_testLayerReuse<BackdropFilterLayer>(RenderBackdropFilter(
filter: ui.ImageFilter.blur(),
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
));
});
test('RenderClipRect reuses its layer', () {
_testLayerReuse<ClipRectLayer>(RenderClipRect(
clipper: _TestRectClipper(),
// Inject opacity under the clip to force compositing.
child: RenderOpacity(
opacity: 0.5,
child: RenderSizedBox(const Size(1.0, 1.0)),
), // size doesn't matter
));
});
test('RenderClipRRect reuses its layer', () {
_testLayerReuse<ClipRRectLayer>(RenderClipRRect(
clipper: _TestRRectClipper(),
// Inject opacity under the clip to force compositing.
child: RenderOpacity(
opacity: 0.5,
child: RenderSizedBox(const Size(1.0, 1.0)),
), // size doesn't matter
));
});
test('RenderClipOval reuses its layer', () {
_testLayerReuse<ClipPathLayer>(RenderClipOval(
clipper: _TestRectClipper(),
// Inject opacity under the clip to force compositing.
child: RenderOpacity(
opacity: 0.5,
child: RenderSizedBox(const Size(1.0, 1.0)),
), // size doesn't matter
));
});
test('RenderClipPath reuses its layer', () {
_testLayerReuse<ClipPathLayer>(RenderClipPath(
clipper: _TestPathClipper(),
// Inject opacity under the clip to force compositing.
child: RenderOpacity(
opacity: 0.5,
child: RenderSizedBox(const Size(1.0, 1.0)),
), // size doesn't matter
));
});
test('RenderPhysicalModel reuses its layer', () {
_testLayerReuse<PhysicalModelLayer>(RenderPhysicalModel(
color: const Color.fromRGBO(0, 0, 0, 1.0),
// Inject opacity under the clip to force compositing.
child: RenderOpacity(
opacity: 0.5,
child: RenderSizedBox(const Size(1.0, 1.0)),
), // size doesn't matter
));
});
test('RenderPhysicalShape reuses its layer', () {
_testLayerReuse<PhysicalModelLayer>(RenderPhysicalShape(
clipper: _TestPathClipper(),
color: const Color.fromRGBO(0, 0, 0, 1.0),
// Inject opacity under the clip to force compositing.
child: RenderOpacity(
opacity: 0.5,
child: RenderSizedBox(const Size(1.0, 1.0)),
), // size doesn't matter
));
});
test('RenderTransform reuses its layer', () {
_testLayerReuse<TransformLayer>(RenderTransform(
// Use a 3D transform to force compositing.
transform: Matrix4.rotationX(0.1),
// Inject opacity under the clip to force compositing.
child: RenderOpacity(
opacity: 0.5,
child: RenderSizedBox(const Size(1.0, 1.0)),
), // size doesn't matter
));
});
void _testFittedBoxWithClipRectLayer() {
_testLayerReuse<ClipRectLayer>(RenderFittedBox(
alignment: Alignment.center,
fit: BoxFit.cover,
// Inject opacity under the clip to force compositing.
child: RenderOpacity(
opacity: 0.5,
child: RenderSizedBox(const Size(100.0, 200.0)),
), // size doesn't matter
));
}
void _testFittedBoxWithTransformLayer() {
_testLayerReuse<TransformLayer>(RenderFittedBox(
alignment: Alignment.center,
fit: BoxFit.fill,
// Inject opacity under the clip to force compositing.
child: RenderOpacity(
opacity: 0.5,
child: RenderSizedBox(const Size(1, 1)),
), // size doesn't matter
));
}
test('RenderFittedBox reuses ClipRectLayer', () {
_testFittedBoxWithClipRectLayer();
});
test('RenderFittedBox reuses TransformLayer', () {
_testFittedBoxWithTransformLayer();
});
test('RenderFittedBox switches between ClipRectLayer and TransformLayer, and reuses them', () {
_testFittedBoxWithClipRectLayer();
// clip -> transform
_testFittedBoxWithTransformLayer();
// transform -> clip
_testFittedBoxWithClipRectLayer();
});
}
class _TestRectClipper extends CustomClipper<Rect> {
@override
Rect getClip(Size size) {
return Rect.zero;
}
@override
Rect getApproximateClipRect(Size size) => getClip(size);
@override
bool shouldReclip(_TestRectClipper oldClipper) => true;
}
class _TestRRectClipper extends CustomClipper<RRect> {
@override
RRect getClip(Size size) {
return RRect.zero;
}
@override
Rect getApproximateClipRect(Size size) => getClip(size).outerRect;
@override
bool shouldReclip(_TestRRectClipper oldClipper) => true;
}
class _FakeTickerProvider implements TickerProvider {
......@@ -348,3 +532,32 @@ class _FakeTicker implements Ticker {
@override
String toString({ bool debugIncludeStack = false }) => super.toString();
}
// Forces two frames and checks that:
// - a layer is created on the first frame
// - the layer is reused on the second frame
void _testLayerReuse<L extends Layer>(RenderObject renderObject) {
expect(L, isNot(Layer));
expect(renderObject.debugLayer, null);
layout(renderObject, phase: EnginePhase.paint, constraints: BoxConstraints.tight(const Size(10, 10)));
final Layer layer = renderObject.debugLayer;
expect(layer, isInstanceOf<L>());
expect(layer, isNotNull);
// Mark for repaint otherwise pumpFrame is a noop.
renderObject.markNeedsPaint();
expect(renderObject.debugNeedsPaint, true);
pumpFrame(phase: EnginePhase.paint);
expect(renderObject.debugNeedsPaint, false);
expect(renderObject.debugLayer, same(layer));
}
class _TestPathClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
return Path()
..addRect(const Rect.fromLTWH(50.0, 50.0, 100.0, 100.0));
}
@override
bool shouldReclip(_TestPathClipper oldClipper) => false;
}
......@@ -4,6 +4,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/src/rendering/layer.dart';
/// An [Invocation] and the [stack] trace that led to it.
///
......@@ -103,38 +104,49 @@ class TestRecordingPaintingContext extends ClipContext implements PaintingContex
}
@override
void pushClipRect(bool needsCompositing, Offset offset, Rect clipRect, PaintingContextCallback painter, { Clip clipBehavior = Clip.hardEdge }) {
ClipRectLayer pushClipRect(bool needsCompositing, Offset offset, Rect clipRect,
PaintingContextCallback painter, { Clip clipBehavior = Clip.hardEdge, ClipRectLayer oldLayer }) {
clipRectAndPaint(clipRect.shift(offset), clipBehavior, clipRect.shift(offset), () => painter(this, offset));
return null;
}
@override
void pushClipRRect(bool needsCompositing, Offset offset, Rect bounds, RRect clipRRect, PaintingContextCallback painter, { Clip clipBehavior = Clip.antiAlias }) {
ClipRRectLayer pushClipRRect(bool needsCompositing, Offset offset, Rect bounds, RRect clipRRect,
PaintingContextCallback painter, { Clip clipBehavior = Clip.antiAlias, ClipRRectLayer oldLayer }) {
assert(clipBehavior != null);
clipRRectAndPaint(clipRRect.shift(offset), clipBehavior, bounds.shift(offset), () => painter(this, offset));
return null;
}
@override
void pushClipPath(bool needsCompositing, Offset offset, Rect bounds, Path clipPath, PaintingContextCallback painter, { Clip clipBehavior = Clip.antiAlias }) {
ClipPathLayer pushClipPath(bool needsCompositing, Offset offset, Rect bounds, Path clipPath,
PaintingContextCallback painter, { Clip clipBehavior = Clip.antiAlias, ClipPathLayer oldLayer }) {
clipPathAndPaint(clipPath.shift(offset), clipBehavior, bounds.shift(offset), () => painter(this, offset));
return null;
}
@override
void pushTransform(bool needsCompositing, Offset offset, Matrix4 transform, PaintingContextCallback painter) {
TransformLayer pushTransform(bool needsCompositing, Offset offset, Matrix4 transform,
PaintingContextCallback painter, { TransformLayer oldLayer }) {
canvas.save();
canvas.transform(transform.storage);
painter(this, offset);
canvas.restore();
return null;
}
@override
void pushOpacity(Offset offset, int alpha, PaintingContextCallback painter) {
OpacityLayer pushOpacity(Offset offset, int alpha, PaintingContextCallback painter,
{ OpacityLayer oldLayer }) {
canvas.saveLayer(null, null); // TODO(ianh): Expose the alpha somewhere.
painter(this, offset);
canvas.restore();
return null;
}
@override
void pushLayer(Layer childLayer, PaintingContextCallback painter, Offset offset, { Rect childPaintBounds }) {
void pushLayer(Layer childLayer, PaintingContextCallback painter, Offset offset,
{ Rect childPaintBounds }) {
painter(this, offset);
}
......
......@@ -7,33 +7,111 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart' show EnginePhase, fail;
import 'package:flutter_test/flutter_test.dart' show EnginePhase;
export 'package:flutter/foundation.dart' show FlutterError, FlutterErrorDetails;
export 'package:flutter_test/flutter_test.dart' show EnginePhase;
class TestRenderingFlutterBinding extends BindingBase with ServicesBinding, GestureBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding {
/// Creates a binding for testing rendering library functionality.
///
/// If [onErrors] is not null, it is called if [FlutterError] caught any errors
/// while drawing the frame. If [onErrors] is null and [FlutterError] caught at least
/// one error, this function fails the test. A test may override [onErrors] and
/// inspect errors using [takeFlutterErrorDetails].
TestRenderingFlutterBinding({ this.onErrors });
final List<FlutterErrorDetails> _errors = <FlutterErrorDetails>[];
/// A function called after drawing a frame if [FlutterError] caught any errors.
///
/// This function is expected to inspect these errors and decide whether they
/// are expected or not. Use [takeFlutterErrorDetails] to take one error at a
/// time, or [takeAllFlutterErrorDetails] to iterate over all errors.
VoidCallback onErrors;
/// Returns the error least recently caught by [FlutterError] and removes it
/// from the list of captured errors.
///
/// Returns null if no errors were captures, or if the list was exhausted by
/// calling this method repeatedly.
FlutterErrorDetails takeFlutterErrorDetails() {
if (_errors.isEmpty) {
return null;
}
return _errors.removeAt(0);
}
/// Returns all error details caught by [FlutterError] from least recently caught to
/// most recently caught, and removes them from the list of captured errors.
///
/// The returned iterable takes errors lazily. If, for example, you iterate over 2
/// errors, but there are 5 errors total, this binding will still fail the test.
/// Tests are expected to take and inspect all errors.
Iterable<FlutterErrorDetails> takeAllFlutterErrorDetails() sync* {
// sync* and yield are used for lazy evaluation. Otherwise, the list would be
// drained eagerly and allow a test pass with unexpected errors.
while (_errors.isNotEmpty) {
yield _errors.removeAt(0);
}
}
/// Returns all exceptions caught by [FlutterError] from least recently caught to
/// most recently caught, and removes them from the list of captured errors.
///
/// The returned iterable takes errors lazily. If, for example, you iterate over 2
/// errors, but there are 5 errors total, this binding will still fail the test.
/// Tests are expected to take and inspect all errors.
Iterable<dynamic> takeAllFlutterExceptions() sync* {
// sync* and yield are used for lazy evaluation. Otherwise, the list would be
// drained eagerly and allow a test pass with unexpected errors.
while (_errors.isNotEmpty) {
yield _errors.removeAt(0).exception;
}
}
EnginePhase phase = EnginePhase.composite;
@override
void drawFrame() {
assert(phase != EnginePhase.build, 'rendering_tester does not support testing the build phase; use flutter_test instead');
pipelineOwner.flushLayout();
if (phase == EnginePhase.layout)
return;
pipelineOwner.flushCompositingBits();
if (phase == EnginePhase.compositingBits)
return;
pipelineOwner.flushPaint();
if (phase == EnginePhase.paint)
return;
renderView.compositeFrame();
if (phase == EnginePhase.composite)
return;
pipelineOwner.flushSemantics();
if (phase == EnginePhase.flushSemantics)
return;
assert(phase == EnginePhase.flushSemantics ||
phase == EnginePhase.sendSemanticsUpdate);
final FlutterExceptionHandler oldErrorHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
_errors.add(details);
};
try {
pipelineOwner.flushLayout();
if (phase == EnginePhase.layout)
return;
pipelineOwner.flushCompositingBits();
if (phase == EnginePhase.compositingBits)
return;
pipelineOwner.flushPaint();
if (phase == EnginePhase.paint)
return;
renderView.compositeFrame();
if (phase == EnginePhase.composite)
return;
pipelineOwner.flushSemantics();
if (phase == EnginePhase.flushSemantics)
return;
assert(phase == EnginePhase.flushSemantics ||
phase == EnginePhase.sendSemanticsUpdate);
} finally {
FlutterError.onError = oldErrorHandler;
if (_errors.isNotEmpty) {
if (onErrors != null) {
onErrors();
if (_errors.isNotEmpty) {
_errors.forEach(FlutterError.dumpErrorToConsole);
fail('There are more errors than the test inspected using TestRenderingFlutterBinding.takeFlutterErrorDetails.');
}
} else {
_errors.forEach(FlutterError.dumpErrorToConsole);
fail('Caught error while rendering frame. See preceding logs for details.');
}
}
}
}
}
......@@ -55,11 +133,14 @@ TestRenderingFlutterBinding get renderer {
///
/// The EnginePhase must not be [EnginePhase.build], since the rendering layer
/// has no build phase.
///
/// If `onErrors` is not null, it is set as [TestRenderingFlutterBinding.onError].
void layout(
RenderBox box, {
BoxConstraints constraints,
Alignment alignment = Alignment.center,
EnginePhase phase = EnginePhase.layout,
VoidCallback onErrors,
}) {
assert(box != null); // If you want to just repump the last box, call pumpFrame().
assert(box.parent == null); // We stick the box in another, so you can't reuse it easily, sorry.
......@@ -76,13 +157,21 @@ void layout(
}
renderer.renderView.child = box;
pumpFrame(phase: phase);
pumpFrame(phase: phase, onErrors: onErrors);
}
void pumpFrame({ EnginePhase phase = EnginePhase.layout }) {
/// Pumps a single frame.
///
/// If `onErrors` is not null, it is set as [TestRenderingFlutterBinding.onError].
void pumpFrame({ EnginePhase phase = EnginePhase.layout, VoidCallback onErrors }) {
assert(renderer != null);
assert(renderer.renderView != null);
assert(renderer.renderView.child != null); // call layout() first!
if (onErrors != null) {
renderer.onErrors = onErrors;
}
renderer.phase = phase;
renderer.drawFrame();
}
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import '../flutter_test_alternative.dart';
......@@ -51,4 +52,102 @@ void main() {
padding.child = repaintBoundary;
pumpFrame(phase: EnginePhase.flushSemantics);
});
test('Framework creates an OffsetLayer for a repaint boundary', () {
final _TestRepaintBoundary repaintBoundary = _TestRepaintBoundary();
final RenderOpacity opacity = RenderOpacity(
opacity: 1.0,
child: repaintBoundary,
);
layout(opacity, phase: EnginePhase.flushSemantics);
expect(repaintBoundary.debugLayer, isInstanceOf<OffsetLayer>());
});
test('Framework does not create an OffsetLayer for a non-repaint boundary', () {
final _TestNonCompositedBox nonCompositedBox = _TestNonCompositedBox();
final RenderOpacity opacity = RenderOpacity(
opacity: 1.0,
child: nonCompositedBox,
);
layout(opacity, phase: EnginePhase.flushSemantics);
expect(nonCompositedBox.debugLayer, null);
});
test('Framework allows a non-repaint boundary to create own layer', () {
final _TestCompositedBox compositedBox = _TestCompositedBox();
final RenderOpacity opacity = RenderOpacity(
opacity: 1.0,
child: compositedBox,
);
layout(opacity, phase: EnginePhase.flushSemantics);
expect(compositedBox.debugLayer, isInstanceOf<OpacityLayer>());
});
test('Framework ensures repaint boundary layer is not overwritten', () {
final _TestRepaintBoundaryThatOverwritesItsLayer faultyRenderObject = _TestRepaintBoundaryThatOverwritesItsLayer();
final RenderOpacity opacity = RenderOpacity(
opacity: 1.0,
child: faultyRenderObject,
);
FlutterErrorDetails error;
layout(opacity, phase: EnginePhase.flushSemantics, onErrors: () {
error = renderer.takeFlutterErrorDetails();
});
expect('${error.exception}', contains('Attempted to set a layer to a repaint boundary render object.'));
});
}
// A plain render object that's a repaint boundary.
class _TestRepaintBoundary extends RenderBox {
@override
bool get isRepaintBoundary => true;
@override
void performLayout() {
size = constraints.smallest;
}
}
// A render object that's a repaint boundary and (incorrectly) creates its own layer.
class _TestRepaintBoundaryThatOverwritesItsLayer extends RenderBox {
@override
bool get isRepaintBoundary => true;
@override
void performLayout() {
size = constraints.smallest;
}
@override
void paint(PaintingContext context, Offset offset) {
layer = OpacityLayer(alpha: 50);
}
}
// A render object that's neither a repaint boundary nor creates its own layer.
class _TestNonCompositedBox extends RenderBox {
@override
bool get isRepaintBoundary => false;
@override
void performLayout() {
size = constraints.smallest;
}
}
// A render object that's not a repaint boundary but creates its own layer.
class _TestCompositedBox extends RenderBox {
@override
bool get isRepaintBoundary => false;
@override
void performLayout() {
size = constraints.smallest;
}
@override
void paint(PaintingContext context, Offset offset) {
layer = OpacityLayer(alpha: 50);
}
}
......@@ -31,12 +31,12 @@ void main() {
),
),
);
int result = RendererBinding.instance.renderView.layer.find<int>(Offset(
int result = RendererBinding.instance.renderView.debugLayer.find<int>(Offset(
10.0 * window.devicePixelRatio,
10.0 * window.devicePixelRatio,
));
expect(result, null);
result = RendererBinding.instance.renderView.layer.find<int>(Offset(
result = RendererBinding.instance.renderView.debugLayer.find<int>(Offset(
50.0 * window.devicePixelRatio,
50.0 * window.devicePixelRatio,
));
......
......@@ -4,6 +4,7 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
testWidgets('Color filter - red', (WidgetTester tester) async {
......@@ -64,4 +65,26 @@ void main() {
),
);
});
}
\ No newline at end of file
testWidgets('Color filter - reuses its layer', (WidgetTester tester) async {
Future<void> pumpWithColor(Color color) async {
await tester.pumpWidget(
RepaintBoundary(
child: ColorFiltered(
colorFilter: ColorFilter.mode(color, BlendMode.color),
child: const Placeholder(),
),
),
);
}
await pumpWithColor(Colors.red);
final RenderObject renderObject = tester.firstRenderObject(find.byType(ColorFiltered));
final ColorFilterLayer originalLayer = renderObject.debugLayer;
expect(originalLayer, isNotNull);
// Change color to force a repaint.
await pumpWithColor(Colors.green);
expect(renderObject.debugLayer, same(originalLayer));
});
}
......@@ -5,6 +5,7 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import '../rendering/mock_canvas.dart';
import 'semantics_tester.dart';
......@@ -191,6 +192,7 @@ void main() {
final Element element = find.byType(RepaintBoundary).first.evaluate().single;
// The following line will send the layer to engine and cause crash if an
// empty opacity layer is sent.
await element.renderObject.layer.toImage(const Rect.fromLTRB(0.0, 0.0, 1.0, 1.0));
final OffsetLayer offsetLayer = element.renderObject.debugLayer;
await offsetLayer.toImage(const Rect.fromLTRB(0.0, 0.0, 1.0, 1.0));
}, skip: isBrowser);
}
......@@ -196,7 +196,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
Future<Evaluation> evaluate(WidgetTester tester) async {
final SemanticsNode root = tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode;
final RenderView renderView = tester.binding.renderView;
final OffsetLayer layer = renderView.layer;
final OffsetLayer layer = renderView.debugLayer;
ui.Image image;
final ByteData byteData = await tester.binding.runAsync<ByteData>(() async {
// Needs to be the same pixel ratio otherwise our dimensions won't match the
......
......@@ -231,7 +231,7 @@ abstract class WidgetController {
}
/// Returns a list of all the [Layer] objects in the rendering.
List<Layer> get layers => _walkLayers(binding.renderView.layer).toList();
List<Layer> get layers => _walkLayers(binding.renderView.debugLayer).toList();
Iterable<Layer> _walkLayers(Layer layer) sync* {
TestAsyncUtils.guardSync();
yield layer;
......
......@@ -1620,7 +1620,7 @@ Future<ui.Image> _captureImage(Element element) {
assert(renderObject != null);
}
assert(!renderObject.debugNeedsPaint);
final OffsetLayer layer = renderObject.layer;
final OffsetLayer layer = renderObject.debugLayer;
return layer.toImage(renderObject.paintBounds);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment