Unverified Commit 3306fc10 authored by Jacob Richman's avatar Jacob Richman Committed by GitHub

Enable taking screenshots of arbitrary RenderObjects from a running a… (#20637)

Enable taking screenshots of arbitrary RenderObjects from a running application from within the inspector.

Key functionality is in the added _ScreenshotPaintingContext class.
parent ac8b906c
1a999092d10a22bc700214b257cd4890c5800078
da615501c1032cb803b2a5623b07d7f4834d9640
......@@ -50,6 +50,8 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
Layer _previousSibling;
/// Removes this layer from its parent layer's child list.
///
/// This has no effect if the layer's parent is already null.
@mustCallSuper
void remove() {
parent?._removeChild(this);
......@@ -588,7 +590,11 @@ class OffsetLayer extends ContainerLayer {
assert(bounds != null);
assert(pixelRatio != null);
final ui.SceneBuilder builder = new ui.SceneBuilder();
final Matrix4 transform = new Matrix4.translationValues(bounds.left - offset.dx, bounds.top - offset.dy, 0.0);
final Matrix4 transform = new Matrix4.translationValues(
(-bounds.left - offset.dx) * pixelRatio,
(-bounds.top - offset.dy) * pixelRatio,
0.0,
);
transform.scale(pixelRatio, pixelRatio);
builder.pushTransform(transform.storage);
addToScene(builder, Offset.zero);
......
......@@ -60,7 +60,13 @@ typedef void PaintingContextCallback(PaintingContext context, Offset offset);
/// New [PaintingContext] objects are created automatically when using
/// [PaintingContext.repaintCompositedChild] and [pushLayer].
class PaintingContext extends ClipContext {
PaintingContext._(this._containerLayer, this.estimatedBounds)
/// Creates a painting context.
///
/// Typically only called by [PaintingContext.repaintCompositedChild]
/// and [pushLayer].
@protected
PaintingContext(this._containerLayer, this.estimatedBounds)
: assert(_containerLayer != null),
assert(estimatedBounds != null);
......@@ -86,8 +92,19 @@ class PaintingContext extends ClipContext {
/// * [RenderObject.isRepaintBoundary], which determines if a [RenderObject]
/// has a composited layer.
static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) {
assert(child.isRepaintBoundary);
assert(child._needsPaint);
_repaintCompositedChild(
child,
debugAlsoPaintedParent: debugAlsoPaintedParent,
);
}
static void _repaintCompositedChild(
RenderObject child, {
bool debugAlsoPaintedParent = false,
PaintingContext childContext,
}) {
assert(child.isRepaintBoundary);
assert(() {
// register the call for RepaintBoundary metrics
child.debugRegisterRepaintBoundaryPaint(
......@@ -107,9 +124,32 @@ class PaintingContext extends ClipContext {
child._layer.debugCreator = child.debugCreator ?? child.runtimeType;
return true;
}());
final PaintingContext childContext = new PaintingContext._(child._layer, child.paintBounds);
childContext ??= new PaintingContext(child._layer, child.paintBounds);
child._paintWithContext(childContext, Offset.zero);
childContext._stopRecordingIfNeeded();
childContext.stopRecordingIfNeeded();
}
/// In debug mode, repaint the given render object using a custom painting
/// context that can record the results of the painting operation in addition
/// to performing the regular paint of the child.
///
/// See also:
///
/// * [repaintCompositedChild], for repainting a composited child without
/// instrumentation.
static void debugInstrumentRepaintCompositedChild(
RenderObject child, {
bool debugAlsoPaintedParent = false,
@required PaintingContext customContext,
}) {
assert(() {
_repaintCompositedChild(
child,
debugAlsoPaintedParent: debugAlsoPaintedParent,
childContext: customContext,
);
return true;
}());
}
/// Paint a child [RenderObject].
......@@ -125,7 +165,7 @@ class PaintingContext extends ClipContext {
}());
if (child.isRepaintBoundary) {
_stopRecordingIfNeeded();
stopRecordingIfNeeded();
_compositeChild(child, offset);
} else {
child._paintWithContext(this, offset);
......@@ -159,10 +199,20 @@ class PaintingContext extends ClipContext {
}());
}
child._layer.offset = offset;
_appendLayer(child._layer);
appendLayer(child._layer);
}
void _appendLayer(Layer layer) {
/// Adds a layer to the recording requiring that the recording is already
/// stopped.
///
/// Do not call this function directly: call [addLayer] or [pushLayer]
/// instead. This function is called internally when all layers not
/// generated from the [canvas] are added.
///
/// Subclasses that need to customize how layers are added should override
/// this method.
@protected
void appendLayer(Layer layer) {
assert(!_isRecording);
layer.remove();
_containerLayer.append(layer);
......@@ -210,7 +260,19 @@ class PaintingContext extends ClipContext {
_containerLayer.append(_currentLayer);
}
void _stopRecordingIfNeeded() {
/// Stop recording to a canvas if recording has started.
///
/// Do not call this function directly: functions in this class will call
/// this method as needed. This function is called internally to ensure that
/// recording is stopped before adding layers or finalizing the results of a
/// paint.
///
/// Subclasses that need to customize how recording to a canvas is performed
/// should override this method to save the results of the custom canvas
/// recordings.
@protected
@mustCallSuper
void stopRecordingIfNeeded() {
if (!_isRecording)
return;
assert(() {
......@@ -271,8 +333,8 @@ class PaintingContext extends ClipContext {
/// * [pushLayer], for adding a layer and using its canvas to paint with that
/// layer.
void addLayer(Layer layer) {
_stopRecordingIfNeeded();
_appendLayer(layer);
stopRecordingIfNeeded();
appendLayer(layer);
}
/// Appends the given layer to the recording, and calls the `painter` callback
......@@ -295,15 +357,21 @@ class PaintingContext extends ClipContext {
/// See also:
///
/// * [addLayer], for pushing a leaf layer whose canvas is not used.
void pushLayer(Layer childLayer, PaintingContextCallback painter, Offset offset, { Rect childPaintBounds }) {
void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter, Offset offset, { Rect childPaintBounds }) {
assert(!childLayer.attached);
assert(childLayer.parent == null);
assert(painter != null);
_stopRecordingIfNeeded();
_appendLayer(childLayer);
final PaintingContext childContext = new PaintingContext._(childLayer, childPaintBounds ?? estimatedBounds);
stopRecordingIfNeeded();
appendLayer(childLayer);
final PaintingContext childContext = createChildContext(childLayer, childPaintBounds ?? estimatedBounds);
painter(childContext, offset);
childContext._stopRecordingIfNeeded();
childContext.stopRecordingIfNeeded();
}
/// Creates a compatible painting context to paint onto [childLayer].
@protected
PaintingContext createChildContext(ContainerLayer childLayer, Rect bounds) {
return new PaintingContext(childLayer, bounds);
}
/// Clip further painting using a rectangle.
......@@ -2036,7 +2104,6 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
Rect get paintBounds;
/// Override this method to paint debugging information.
@protected
void debugPaint(PaintingContext context, Offset offset) { }
/// Paint this render object into the given context at the given offset.
......
......@@ -7,13 +7,26 @@ import 'dart:collection';
import 'dart:convert';
import 'dart:developer' as developer;
import 'dart:math' as math;
import 'dart:ui' as ui show window, Picture, SceneBuilder, PictureRecorder;
import 'dart:ui' show Offset;
import 'dart:typed_data';
import 'dart:ui' as ui
show
window,
ClipOp,
Image,
ImageByteFormat,
Paragraph,
Picture,
PictureRecorder,
PointMode,
SceneBuilder,
Vertices;
import 'dart:ui' show Canvas, Offset;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:vector_math/vector_math_64.dart';
import 'app.dart';
import 'basic.dart';
......@@ -31,6 +44,556 @@ typedef void _RegisterServiceExtensionCallback({
@required ServiceExtensionCallback callback
});
/// A layer that mimics the behavior of another layer.
///
/// A proxy layer is used for cases where a layer needs to be placed into
/// multiple trees of layers.
class _ProxyLayer extends Layer {
final Layer _layer;
_ProxyLayer(this._layer);
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
_layer.addToScene(builder, layerOffset);
}
@override
S find<S>(Offset regionOffset) => _layer.find(regionOffset);
}
/// A [Canvas] that multicasts all method calls to a main canvas and a
/// secondary screenshot canvas so that a screenshot can be recorded at the same
/// time as performing a normal paint.
class _MulticastCanvas implements Canvas {
final Canvas _main;
final Canvas _screenshot;
_MulticastCanvas({
@required Canvas main,
@required Canvas screenshot,
}) : assert(main != null),
assert(screenshot != null),
_main = main,
_screenshot = screenshot;
@override
void clipPath(Path path, {bool doAntiAlias = true}) {
_main.clipPath(path, doAntiAlias: doAntiAlias);
_screenshot.clipPath(path, doAntiAlias: doAntiAlias);
}
@override
void clipRRect(RRect rrect, {bool doAntiAlias = true}) {
_main.clipRRect(rrect, doAntiAlias: doAntiAlias);
_screenshot.clipRRect(rrect, doAntiAlias: doAntiAlias);
}
@override
void clipRect(Rect rect, {ui.ClipOp clipOp = ui.ClipOp.intersect, bool doAntiAlias = true}) {
_main.clipRect(rect, clipOp: clipOp, doAntiAlias: doAntiAlias);
_screenshot.clipRect(rect, clipOp: clipOp, doAntiAlias: doAntiAlias);
}
@override
void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint) {
_main.drawArc(rect, startAngle, sweepAngle, useCenter, paint);
_screenshot.drawArc(rect, startAngle, sweepAngle, useCenter, paint);
}
@override
void drawAtlas(ui.Image atlas, List<RSTransform> transforms, List<Rect> rects, List<Color> colors, BlendMode blendMode, Rect cullRect, Paint paint) {
_main.drawAtlas(atlas, transforms, rects, colors, blendMode, cullRect, paint);
_screenshot.drawAtlas(atlas, transforms, rects, colors, blendMode, cullRect, paint);
}
@override
void drawCircle(Offset c, double radius, Paint paint) {
_main.drawCircle(c, radius, paint);
_screenshot.drawCircle(c, radius, paint);
}
@override
void drawColor(Color color, BlendMode blendMode) {
_main.drawColor(color, blendMode);
_screenshot.drawColor(color, blendMode);
}
@override
void drawDRRect(RRect outer, RRect inner, Paint paint) {
_main.drawDRRect(outer, inner, paint);
_screenshot.drawDRRect(outer, inner, paint);
}
@override
void drawImage(ui.Image image, Offset p, Paint paint) {
_main.drawImage(image, p, paint);
_screenshot.drawImage(image, p, paint);
}
@override
void drawImageNine(ui.Image image, Rect center, Rect dst, Paint paint) {
_main.drawImageNine(image, center, dst, paint);
_screenshot.drawImageNine(image, center, dst, paint);
}
@override
void drawImageRect(ui.Image image, Rect src, Rect dst, Paint paint) {
_main.drawImageRect(image, src, dst, paint);
_screenshot.drawImageRect(image, src, dst, paint);
}
@override
void drawLine(Offset p1, Offset p2, Paint paint) {
_main.drawLine(p1, p2, paint);
_screenshot.drawLine(p1, p2, paint);
}
@override
void drawOval(Rect rect, Paint paint) {
_main.drawOval(rect, paint);
_screenshot.drawOval(rect, paint);
}
@override
void drawPaint(Paint paint) {
_main.drawPaint(paint);
_screenshot.drawPaint(paint);
}
@override
void drawParagraph(ui.Paragraph paragraph, Offset offset) {
_main.drawParagraph(paragraph, offset);
_screenshot.drawParagraph(paragraph, offset);
}
@override
void drawPath(Path path, Paint paint) {
_main.drawPath(path, paint);
_screenshot.drawPath(path, paint);
}
@override
void drawPicture(ui.Picture picture) {
_main.drawPicture(picture);
_screenshot.drawPicture(picture);
}
@override
void drawPoints(ui.PointMode pointMode, List<Offset> points, Paint paint) {
_main.drawPoints(pointMode, points, paint);
_screenshot.drawPoints(pointMode, points, paint);
}
@override
void drawRRect(RRect rrect, Paint paint) {
_main.drawRRect(rrect, paint);
_screenshot.drawRRect(rrect, paint);
}
@override
void drawRawAtlas(ui.Image atlas, Float32List rstTransforms, Float32List rects, Int32List colors, BlendMode blendMode, Rect cullRect, Paint paint) {
_main.drawRawAtlas(atlas, rstTransforms, rects, colors, blendMode, cullRect, paint);
_screenshot.drawRawAtlas(atlas, rstTransforms, rects, colors, blendMode, cullRect, paint);
}
@override
void drawRawPoints(ui.PointMode pointMode, Float32List points, Paint paint) {
_main.drawRawPoints(pointMode, points, paint);
_screenshot.drawRawPoints(pointMode, points, paint);
}
@override
void drawRect(Rect rect, Paint paint) {
_main.drawRect(rect, paint);
_screenshot.drawRect(rect, paint);
}
@override
void drawShadow(Path path, Color color, double elevation, bool transparentOccluder) {
_main.drawShadow(path, color, elevation, transparentOccluder);
_screenshot.drawShadow(path, color, elevation, transparentOccluder);
}
@override
void drawVertices(ui.Vertices vertices, BlendMode blendMode, Paint paint) {
_main.drawVertices(vertices, blendMode, paint);
_screenshot.drawVertices(vertices, blendMode, paint);
}
@override
int getSaveCount() {
// The main canvas is used instead of the screenshot canvas as the main
// canvas is guaranteed to be consistent with the canvas expected by the
// normal paint pipeline so any logic depending on getSaveCount() will
// behave the same as for the regular paint pipeline.
return _main.getSaveCount();
}
@override
void restore() {
_main.restore();
_screenshot.restore();
}
@override
void rotate(double radians) {
_main.rotate(radians);
_screenshot.rotate(radians);
}
@override
void save() {
_main.save();
_screenshot.save();
}
@override
void saveLayer(Rect bounds, Paint paint) {
_main.saveLayer(bounds, paint);
_screenshot.saveLayer(bounds, paint);
}
@override
void scale(double sx, [double sy]) {
_main.scale(sx, sy);
_screenshot.scale(sx, sy);
}
@override
void skew(double sx, double sy) {
_main.skew(sx, sy);
_screenshot.skew(sx, sy);
}
@override
void transform(Float64List matrix4) {
_main.transform(matrix4);
_screenshot.transform(matrix4);
}
@override
void translate(double dx, double dy) {
_main.translate(dx, dy);
_screenshot.translate(dx, dy);
}
}
Rect _calculateSubtreeBoundsHelper(RenderObject object, Matrix4 transform) {
Rect bounds = MatrixUtils.transformRect(transform, object.semanticBounds);
object.visitChildren((RenderObject child) {
final Matrix4 childTransform = transform.clone();
object.applyPaintTransform(child, childTransform);
Rect childBounds = _calculateSubtreeBoundsHelper(child, childTransform);
final Rect paintClip = object.describeApproximatePaintClip(child);
if (paintClip != null) {
final Rect transformedPaintClip = MatrixUtils.transformRect(
transform,
paintClip,
);
childBounds = childBounds.intersect(transformedPaintClip);
}
if (childBounds.isFinite && !childBounds.isEmpty) {
bounds = bounds.isEmpty ? childBounds : bounds.expandToInclude(childBounds);
}
});
return bounds;
}
/// Calculate bounds for a render object and all of its descendants.
Rect _calculateSubtreeBounds(RenderObject object) {
return _calculateSubtreeBoundsHelper(object, new Matrix4.identity());
}
/// A layer that omits its own offset when adding children to the scene so that
/// screenshots render to the scene in the local coordinate system of the layer.
class _ScreenshotContainerLayer extends OffsetLayer {
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
addChildrenToScene(builder, layerOffset);
}
}
/// Data shared between nested [_ScreenshotPaintingContext] objects recording
/// a screenshot.
class _ScreenshotData {
_ScreenshotData({
@required this.target,
}) : assert(target != null),
containerLayer = new _ScreenshotContainerLayer();
/// Target to take a screenshot of.
final RenderObject target;
/// Root of the layer tree containing the screenshot.
final OffsetLayer containerLayer;
/// Whether the screenshot target has already been found in the render tree.
bool foundTarget = false;
/// Whether paint operations should record to the screenshot.
///
/// At least one of [includeInScreenshot] and [includeInRegularContext] must
/// be true.
bool includeInScreenshot = false;
/// Whether paint operations should record to the regular context.
///
/// This should only be set to false before paint operations that should only
/// apply to the screenshot such rendering debug information about the
/// [target].
///
/// At least one of [includeInScreenshot] and [includeInRegularContext] must
/// be true.
bool includeInRegularContext = true;
/// Offset of the screenshot corresponding to the offset [target] was given as
/// part of the regular paint.
Offset get screenshotOffset {
assert(foundTarget);
return containerLayer.offset;
}
set screenshotOffset(Offset offset) {
containerLayer.offset = offset;
}
}
/// A place to paint to build screenshots of [RenderObject]s.
///
/// Requires that the render objects have already painted successfully as part
/// of the regular rendering pipeline.
/// This painting context behaves the same as standard [PaintingContext] with
/// instrumentation added to compute a screenshot of a specified [RenderObject]
/// added. To correctly mimic the behavor of the regular rendering pipeline, the
/// full subtree of the first [RepaintBoundary] ancestor of the specified
/// [RenderObject] will also be rendered rather than just the subtree of the
/// render object.
class _ScreenshotPaintingContext extends PaintingContext {
_ScreenshotPaintingContext({
@required ContainerLayer containerLayer,
@required Rect estimatedBounds,
@required _ScreenshotData screenshotData,
}) : _data = screenshotData,
super(containerLayer, estimatedBounds);
final _ScreenshotData _data;
// Recording state
PictureLayer _screenshotCurrentLayer;
ui.PictureRecorder _screenshotRecorder;
Canvas _screenshotCanvas;
_MulticastCanvas _multicastCanvas;
@override
Canvas get canvas {
if (_data.includeInScreenshot) {
if (_screenshotCanvas == null) {
_startRecordingScreenshot();
}
assert(_screenshotCanvas != null);
return _data.includeInRegularContext ? _multicastCanvas : _screenshotCanvas;
} else {
assert(_data.includeInRegularContext);
return super.canvas;
}
}
bool get _isScreenshotRecording {
final bool hasScreenshotCanvas = _screenshotCanvas != null;
assert(() {
if (hasScreenshotCanvas) {
assert(_screenshotCurrentLayer != null);
assert(_screenshotRecorder != null);
assert(_screenshotCanvas != null);
} else {
assert(_screenshotCurrentLayer == null);
assert(_screenshotRecorder == null);
assert(_screenshotCanvas == null);
}
return true;
}());
return hasScreenshotCanvas;
}
void _startRecordingScreenshot() {
assert(_data.includeInScreenshot);
assert(!_isScreenshotRecording);
_screenshotCurrentLayer = new PictureLayer(estimatedBounds);
_screenshotRecorder = new ui.PictureRecorder();
_screenshotCanvas = new Canvas(_screenshotRecorder);
_data.containerLayer.append(_screenshotCurrentLayer);
if (_data.includeInRegularContext) {
_multicastCanvas = new _MulticastCanvas(
main: super.canvas,
screenshot: _screenshotCanvas,
);
} else {
_multicastCanvas = null;
}
}
@override
void stopRecordingIfNeeded() {
super.stopRecordingIfNeeded();
_stopRecordingScreenshotIfNeeded();
}
void _stopRecordingScreenshotIfNeeded() {
if (!_isScreenshotRecording)
return;
// There is no need to ever draw repaint rainbows as part of the screenshot.
_screenshotCurrentLayer.picture = _screenshotRecorder.endRecording();
_screenshotCurrentLayer = null;
_screenshotRecorder = null;
_multicastCanvas = null;
_screenshotCanvas = null;
}
@override
void appendLayer(Layer layer) {
if (_data.includeInRegularContext) {
super.appendLayer(layer);
if (_data.includeInScreenshot) {
assert(!_isScreenshotRecording);
// We must use a proxy layer here as the layer is already attached to
// the regular layer tree.
_data.containerLayer.append(new _ProxyLayer(layer));
}
} else {
// Only record to the screenshot.
assert(!_isScreenshotRecording);
assert(_data.includeInScreenshot);
layer.remove();
_data.containerLayer.append(layer);
return;
}
}
@override
PaintingContext createChildContext(ContainerLayer childLayer, Rect bounds) {
if (_data.foundTarget) {
// We have already found the screenshotTarget in the layer tree
// so we can optimize and use a standard PaintingContext.
return super.createChildContext(childLayer, bounds);
} else {
return new _ScreenshotPaintingContext(
containerLayer: childLayer,
estimatedBounds: bounds,
screenshotData: _data,
);
}
}
@override
void paintChild(RenderObject child, Offset offset) {
final bool isScreenshotTarget = identical(child, _data.target);
if (isScreenshotTarget) {
assert(!_data.includeInScreenshot);
assert(!_data.foundTarget);
_data.foundTarget = true;
_data.screenshotOffset = offset;
_data.includeInScreenshot = true;
}
super.paintChild(child, offset);
if (isScreenshotTarget) {
_stopRecordingScreenshotIfNeeded();
_data.includeInScreenshot = false;
}
}
/// Captures an image of the current state of [renderObject] and its children.
///
/// The returned [ui.Image] has uncompressed raw RGBA bytes, will be offset
/// by the top-left corner of [renderBounds], and have dimensions equal to the
/// size of [renderBounds] multiplied by [pixelRatio].
///
/// To use [toImage], the render object must have gone through the paint phase
/// (i.e. [debugNeedsPaint] must be false).
///
/// The [pixelRatio] describes the scale between the logical pixels and the
/// size of the output image. It is independent of the
/// [window.devicePixelRatio] for the device, so specifying 1.0 (the default)
/// will give you a 1:1 mapping between logical pixels and the output pixels
// / in the image.
///
/// The [debugPaint] argument specifies whether the image should include the
/// output of [RenderObject.debugPaint] for [renderObject] with
/// [debugPaintSizeEnabled] set to `true`. Debug paint information is not
/// included for the children of [renderObject] so that it is clear precisely
/// which object the debug paint information references.
///
/// See also:
///
/// * [RenderRepaintBoundary.toImage] for a similar API for [RenderObject]s
/// that are repaint boundaries that can be used outside of the inspector.
/// * [OffsetLayer.toImage] for a similar API at the layer level.
/// * [dart:ui.Scene.toImage] for more information about the image returned.
static Future<ui.Image> toImage(
RenderObject renderObject,
Rect renderBounds, {
double pixelRatio = 1.0,
bool debugPaint = false,
}) {
RenderObject repaintBoundary = renderObject;
while (repaintBoundary != null && !repaintBoundary.isRepaintBoundary) {
repaintBoundary = repaintBoundary.parent;
}
assert(repaintBoundary != null);
final _ScreenshotData data = new _ScreenshotData(target: renderObject);
final _ScreenshotPaintingContext context = new _ScreenshotPaintingContext(
containerLayer: repaintBoundary.debugLayer,
estimatedBounds: repaintBoundary.paintBounds,
screenshotData: data,
);
if (identical(renderObject, repaintBoundary)) {
// 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(new _ProxyLayer(repaintBoundary.layer));
data.foundTarget = true;
data.screenshotOffset = repaintBoundary.layer.offset;
} else {
// Repaint everything under the repaint boundary.
// We call debugInstrumentRepaintCompositedChild instead of paintChild as
// we need to force everything under the repaint boundary to repaint.
PaintingContext.debugInstrumentRepaintCompositedChild(
repaintBoundary,
customContext: context,
);
}
// The check that debugPaintSizeEnabled is false exists to ensure we only
// call debugPaint when it wasn't already called.
if (debugPaint && !debugPaintSizeEnabled) {
data.includeInRegularContext = false;
// Existing recording may be to a canvas that draws to both the normal and
// screenshot canvases.
context.stopRecordingIfNeeded();
assert(data.foundTarget);
data.includeInScreenshot = true;
debugPaintSizeEnabled = true;
try {
renderObject.debugPaint(context, data.screenshotOffset);
} finally {
debugPaintSizeEnabled = false;
context.stopRecordingIfNeeded();
}
}
// 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.addToScene(new ui.SceneBuilder(), Offset.zero);
return data.containerLayer.toImage(renderBounds, pixelRatio: pixelRatio);
}
}
/// A class describing a step along a path through a tree of [DiagnosticsNode]
/// objects.
///
......@@ -464,6 +1027,33 @@ class WidgetInspectorService {
name: 'isWidgetCreationTracked',
callback: isWidgetCreationTracked,
);
registerServiceExtension(
name: 'screenshot',
callback: (Map<String, String> parameters) async {
assert(parameters.containsKey('id'));
assert(parameters.containsKey('width'));
assert(parameters.containsKey('height'));
final ui.Image image = await screenshot(
toObject(parameters['id']),
width: double.parse(parameters['width']),
height: double.parse(parameters['height']),
margin: parameters.containsKey('margin') ?
double.parse(parameters['margin']) : 0.0,
maxPixelRatio: parameters.containsKey('maxPixelRatio') ?
double.parse(parameters['maxPixelRatio']) : 1.0,
debugPaint: parameters['debugPaint'] == 'true',
);
if (image == null) {
return <String, Object>{'result': null};
}
final ByteData byteData = await image.toByteData(format:ui.ImageByteFormat.png);
return <String, Object>{
'result': base64.encoder.convert(new Uint8List.view(byteData.buffer)),
};
},
);
}
/// Clear all InspectorService object references.
......@@ -1036,6 +1626,77 @@ class WidgetInspectorService {
return _safeJsonEncode(_getSelectedWidget(previousSelectionId, groupName));
}
/// Captures an image of the current state of an [object] that is a
/// [RenderObject] or [Element].
///
/// The returned [ui.Image] has uncompressed raw RGBA bytes and will be scaled
/// to be at most [width] pixels wide and [height] pixels tall. The returned
/// image will never have a scale between logical pixels and the
/// size of the output image larger than maxPixelRatio.
/// [margin] indicates the number of pixels relative to the unscaled size of
/// the [object] to include as a margin to include around the bounds of the
/// [object] in the screenshot. Including a margin can be useful to capture
/// areas that are slightly outside of the normal bounds of an object such as
/// some debug paint information.
@protected
Future<ui.Image> screenshot(
Object object, {
@required double width,
@required double height,
double margin = 0.0,
double maxPixelRatio = 1.0,
bool debugPaint = false,
}) async {
if (object is! Element && object is! RenderObject) {
return null;
}
final RenderObject renderObject = object is Element ? object.renderObject : object;
if (renderObject == null || !renderObject.attached) {
return null;
}
if (renderObject.debugNeedsLayout) {
final PipelineOwner owner = renderObject.owner;
assert(owner != null);
assert(!owner.debugDoingLayout);
owner
..flushLayout()
..flushCompositingBits()
..flushPaint();
// If we still need layout, then that means that renderObject was skipped
// in the layout phase and therefore can't be painted. It is clearer to
// return null indicating that a screenshot is unavailable than to return
// an empty image.
if (renderObject.debugNeedsLayout) {
return null;
}
}
Rect renderBounds = _calculateSubtreeBounds(renderObject);
if (margin != 0.0) {
renderBounds = renderBounds.inflate(margin);
}
if (renderBounds.isEmpty) {
return null;
}
final double pixelRatio = math.min(
maxPixelRatio,
math.min(
width / renderBounds.width,
height / renderBounds.height,
),
);
return _ScreenshotPaintingContext.toImage(
renderObject,
renderBounds,
pixelRatio: pixelRatio,
debugPaint: debugPaint,
);
}
Map<String, Object> _getSelectedWidget(String previousSelectionId, String groupName) {
final DiagnosticsNode previousSelection = toObject(previousSelectionId);
final Element current = selection?.currentElement;
......
......@@ -512,7 +512,7 @@ void main() {
// If you add a service extension... TEST IT! :-)
// ...then increment this number.
expect(binding.extensions.length, 37);
expect(binding.extensions.length, 38);
expect(console, isEmpty);
debugPrint = debugPrintThrottled;
......
......@@ -185,12 +185,43 @@ void main() {
image = await boundary.toImage();
expect(image.width, equals(20));
expect(image.height, equals(20));
final ByteData data = await image.toByteData();
ByteData data = await image.toByteData();
int getPixel(int x, int y) => data.getUint32((x + y * image.width) * 4);
expect(data.lengthInBytes, equals(20 * 20 * 4));
expect(data.elementSizeInBytes, equals(1));
const int stride = 20 * 4;
expect(data.getUint32(0), equals(0x00000080));
expect(data.getUint32(stride - 4), equals(0xffffffff));
expect(getPixel(0, 0), equals(0x00000080));
expect(getPixel(image.width - 1, 0 ), equals(0xffffffff));
final OffsetLayer layer = boundary.layer;
image = await layer.toImage(Offset.zero & const Size(20.0, 20.0));
expect(image.width, equals(20));
expect(image.height, equals(20));
data = await image.toByteData();
expect(getPixel(0, 0), equals(0x00000080));
expect(getPixel(image.width - 1, 0 ), equals(0xffffffff));
// non-zero offsets.
image = await layer.toImage(const Offset(-10.0, -10.0) & const Size(30.0, 30.0));
expect(image.width, equals(30));
expect(image.height, equals(30));
data = await image.toByteData();
expect(getPixel(0, 0), equals(0x00000000));
expect(getPixel(10, 10), equals(0x00000080));
expect(getPixel(image.width - 1, 0), equals(0x00000000));
expect(getPixel(image.width - 1, 10), equals(0xffffffff));
// offset combined with a custom pixel ratio.
image = await layer.toImage(const Offset(-10.0, -10.0) & const Size(30.0, 30.0), pixelRatio: 2.0);
expect(image.width, equals(60));
expect(image.height, equals(60));
data = await image.toByteData();
expect(getPixel(0, 0), equals(0x00000000));
expect(getPixel(20, 20), equals(0x00000080));
expect(getPixel(image.width - 1, 0), equals(0x00000000));
expect(getPixel(image.width - 1, 20), equals(0xffffffff));
});
test('RenderOpacity does not composite if it is transparent', () {
......
......@@ -4,6 +4,8 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io' show Platform;
import 'dart:ui' as ui show PictureRecorder;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
......@@ -12,6 +14,80 @@ import 'package:flutter_test/flutter_test.dart';
typedef FutureOr<Map<String, Object>> InspectorServiceExtensionCallback(Map<String, String> parameters);
class RenderRepaintBoundaryWithDebugPaint extends RenderRepaintBoundary {
@override
void debugPaintSize(PaintingContext context, Offset offset) {
super.debugPaintSize(context, offset);
assert(() {
// Draw some debug paint UI interleaving creating layers and drawing
// directly to the context's canvas.
final Paint paint = new Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.0
..color = Colors.red;
{
final PictureLayer pictureLayer = new PictureLayer(Offset.zero & size);
final ui.PictureRecorder recorder = new ui.PictureRecorder();
final Canvas pictureCanvas = new Canvas(recorder);
pictureCanvas.drawCircle(Offset.zero, 20.0, paint);
pictureLayer.picture = recorder.endRecording();
context.addLayer(
new OffsetLayer()
..offset = offset
..append(pictureLayer),
);
}
context.canvas.drawLine(
offset,
offset.translate(size.width, size.height),
paint,
);
{
final PictureLayer pictureLayer = new PictureLayer(Offset.zero & size);
final ui.PictureRecorder recorder = new ui.PictureRecorder();
final Canvas pictureCanvas = new Canvas(recorder);
pictureCanvas.drawCircle(const Offset(20.0, 20.0), 20.0, paint);
pictureLayer.picture = recorder.endRecording();
context.addLayer(
new OffsetLayer()
..offset = offset
..append(pictureLayer),
);
}
paint.color = Colors.blue;
context.canvas.drawLine(
offset,
offset.translate(size.width * 0.5, size.height * 0.5),
paint,
);
return true;
}());
}
}
class RepaintBoundaryWithDebugPaint extends RepaintBoundary {
/// Creates a widget that isolates repaints.
const RepaintBoundaryWithDebugPaint({
Key key,
Widget child,
}) : super(key: key, child: child);
@override
RenderRepaintBoundary createRenderObject(BuildContext context) {
return new RenderRepaintBoundaryWithDebugPaint();
}
}
int getChildLayerCount(OffsetLayer layer) {
Layer child = layer.firstChild;
int count = 0;
while (child != null) {
count++;
child = child.nextSibling;
}
return count;
}
void main() {
TestWidgetInspectorService.runTests();
}
......@@ -1239,5 +1315,483 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
expect(service.rebuildCount, equals(2));
expect(WidgetsApp.debugShowWidgetInspectorOverride, isFalse);
});
testWidgets('ext.flutter.inspector.screenshot',
(WidgetTester tester) async {
final GlobalKey outerContainerKey = new GlobalKey();
final GlobalKey paddingKey = new GlobalKey();
final GlobalKey redContainerKey = new GlobalKey();
final GlobalKey whiteContainerKey = new GlobalKey();
final GlobalKey sizedBoxKey = new GlobalKey();
// Complex widget tree intended to exercise features such as children
// with rotational transforms and clipping without introducing platform
// specific behavior as text rendering would.
await tester.pumpWidget(
new Center(
child: new RepaintBoundaryWithDebugPaint(
child: new Container(
key: outerContainerKey,
color: Colors.white,
child: new Padding(
key: paddingKey,
padding: const EdgeInsets.all(100.0),
child: new SizedBox(
key: sizedBoxKey,
height: 100.0,
width: 100.0,
child: new Transform.rotate(
angle: 1.0, // radians
child: new ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.elliptical(10.0, 20.0),
topRight: Radius.elliptical(5.0, 30.0),
bottomLeft: Radius.elliptical(2.5, 12.0),
bottomRight: Radius.elliptical(15.0, 6.0),
),
child: new Container(
key: redContainerKey,
color: Colors.red,
child: new Container(
key: whiteContainerKey,
color: Colors.white,
child: new RepaintBoundary(
child: new Center(
child: new Container(
color: Colors.black,
height: 10.0,
width: 10.0,
),
),
),
),
),
),
),
),
),
),
),
),
);
final Element repaintBoundary =
find.byType(RepaintBoundaryWithDebugPaint).evaluate().single;
final RenderRepaintBoundary renderObject = repaintBoundary.renderObject;
final OffsetLayer layer = renderObject.debugLayer;
final int expectedChildLayerCount = getChildLayerCount(layer);
expect(expectedChildLayerCount, equals(2));
await expectLater(
layer.toImage(renderObject.semanticBounds.inflate(50.0)),
matchesGoldenFile('inspector.repaint_boundary_margin.png'),
);
// Regression test for how rendering with a pixel scale other than 1.0
// was handled.
await expectLater(
layer.toImage(
renderObject.semanticBounds.inflate(50.0),
pixelRatio: 0.5,
),
matchesGoldenFile('inspector.repaint_boundary_margin_small.png'),
);
await expectLater(
layer.toImage(
renderObject.semanticBounds.inflate(50.0),
pixelRatio: 2.0,
),
matchesGoldenFile('inspector.repaint_boundary_margin_large.png'),
);
final Layer layerParent = layer.parent;
final Layer firstChild = layer.firstChild;
expect(layerParent, isNotNull);
expect(firstChild, isNotNull);
await expectLater(
service.screenshot(
repaintBoundary,
width: 300.0,
height: 300.0,
),
matchesGoldenFile('inspector.repaint_boundary.png'),
);
// Verify that taking a screenshot didn't change the layers associated with
// the renderObject.
expect(renderObject.debugLayer, equals(layer));
// Verify that taking a screenshot did not change the number of children
// of the layer.
expect(getChildLayerCount(layer), equals(expectedChildLayerCount));
await expectLater(
service.screenshot(
repaintBoundary,
width: 500.0,
height: 500.0,
margin: 50.0,
),
matchesGoldenFile('inspector.repaint_boundary_margin.png'),
);
// Verify that taking a screenshot didn't change the layers associated with
// the renderObject.
expect(renderObject.debugLayer, equals(layer));
// Verify that taking a screenshot did not change the number of children
// of the layer.
expect(getChildLayerCount(layer), equals(expectedChildLayerCount));
// Make sure taking a screenshot didn't change the parent of the layer.
expect(layer.parent, equals(layerParent));
await expectLater(
service.screenshot(
repaintBoundary,
width: 300.0,
height: 300.0,
debugPaint: true,
),
matchesGoldenFile('inspector.repaint_boundary_debugPaint.png'),
);
// Verify that taking a screenshot with debug paint on did not change
// the number of children the layer has.
expect(getChildLayerCount(layer), equals(expectedChildLayerCount));
// Ensure that creating screenshots including ones with debug paint
// hasn't changed the regular render of the widget.
await expectLater(
find.byType(RepaintBoundaryWithDebugPaint),
matchesGoldenFile('inspector.repaint_boundary.png'),
);
expect(renderObject.debugLayer, equals(layer));
expect(layer.attached, isTrue);
// Full size image
await expectLater(
service.screenshot(
find.byKey(outerContainerKey).evaluate().single,
width: 100.0,
height: 100.0,
),
matchesGoldenFile('inspector.container.png'),
);
await expectLater(
service.screenshot(
find.byKey(outerContainerKey).evaluate().single,
width: 100.0,
height: 100.0,
debugPaint: true,
),
matchesGoldenFile('inspector.container_debugPaint.png'),
);
{
// Verify calling the screenshot method still works if the RenderObject
// needs to be laid out again.
final RenderObject container =
find.byKey(outerContainerKey).evaluate().single.renderObject;
container
..markNeedsLayout()
..markNeedsPaint();
expect(container.debugNeedsLayout, isTrue);
await expectLater(
service.screenshot(
find.byKey(outerContainerKey).evaluate().single,
width: 100.0,
height: 100.0,
debugPaint: true,
),
matchesGoldenFile('inspector.container_debugPaint.png'),
);
expect(container.debugNeedsLayout, isFalse);
}
// Small image
await expectLater(
service.screenshot(
find.byKey(outerContainerKey).evaluate().single,
width: 50.0,
height: 100.0,
),
matchesGoldenFile('inspector.container_small.png'),
);
await expectLater(
service.screenshot(
find.byKey(outerContainerKey).evaluate().single,
width: 400.0,
height: 400.0,
maxPixelRatio: 3.0,
),
matchesGoldenFile('inspector.container_large.png'),
);
// This screenshot will show the clip rect debug paint but no other
// debug paint.
await expectLater(
service.screenshot(
find.byType(ClipRRect).evaluate().single,
width: 100.0,
height: 100.0,
debugPaint: true,
),
matchesGoldenFile('inspector.clipRect_debugPaint.png'),
);
// Add a margin so that the clip icon shows up in the screenshot.
// This golden image is platform dependent due to the clip icon.
await expectLater(
service.screenshot(
find.byType(ClipRRect).evaluate().single,
width: 100.0,
height: 100.0,
margin: 20.0,
debugPaint: true,
),
matchesGoldenFile('inspector.clipRect_debugPaint_margin.png'),
skip: !Platform.isLinux
);
// Test with a very visible debug paint
await expectLater(
service.screenshot(
find.byKey(paddingKey).evaluate().single,
width: 300.0,
height: 300.0,
debugPaint: true,
),
matchesGoldenFile('inspector.padding_debugPaint.png'),
);
// The bounds for this box crop its rendered content.
await expectLater(
service.screenshot(
find.byKey(sizedBoxKey).evaluate().single,
width: 300.0,
height: 300.0,
debugPaint: true,
),
matchesGoldenFile('inspector.sizedBox_debugPaint.png'),
);
// Verify that setting a margin includes the previously cropped content.
await expectLater(
service.screenshot(
find.byKey(sizedBoxKey).evaluate().single,
width: 300.0,
height: 300.0,
margin: 50.0,
debugPaint: true,
),
matchesGoldenFile('inspector.sizedBox_debugPaint_margin.png'),
);
});
testWidgets('Screenshot of composited transforms - only offsets', (WidgetTester tester) async {
// Composited transforms are challenging to take screenshots of as the
// LeaderLayer and FollowerLayer classes used by CompositedTransformTarget
// and CompositedTransformFollower depend on traversing ancestors of the
// layer tree and mutating a [LayerLink] object when attaching layers to
// the tree so that the FollowerLayer knows about the LeaderLayer.
// 1. Finding the correct position for the follower layers requires
// traversing the ancestors of the follow layer to find a common ancestor
// with the leader layer.
// 2. Creating a LeaderLayer and attaching it to a layer tree has side
// effects as the leader layer will attempt to modify the mutable
// LeaderLayer object shared by the LeaderLayer and FollowerLayer.
// These tests verify that screenshots can still be taken and look correct
// when the leader and follower layer are both in the screenshots and when
// only the leader or follower layer is in the screenshot.
final LayerLink link = new LayerLink();
final GlobalKey key = new GlobalKey();
final GlobalKey mainStackKey = new GlobalKey();
final GlobalKey transformTargetParent = new GlobalKey();
final GlobalKey stackWithTransformFollower = new GlobalKey();
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new RepaintBoundary(
child: new Stack(
key: mainStackKey,
children: <Widget>[
new Stack(
key: transformTargetParent,
children: <Widget>[
new Positioned(
left: 123.0,
top: 456.0,
child: new CompositedTransformTarget(
link: link,
child: new Container(height: 20.0, width: 20.0, color: const Color.fromARGB(128, 255, 0, 0)),
),
),
],
),
new Positioned(
left: 787.0,
top: 343.0,
child: new Stack(
key: stackWithTransformFollower,
children: <Widget>[
// Container so we can see how the follower layer was
// transformed relative to its initial location.
new Container(height: 15.0, width: 15.0, color: const Color.fromARGB(128, 0, 0, 255)),
new CompositedTransformFollower(
link: link,
child: new Container(key: key, height: 10.0, width: 10.0, color: const Color.fromARGB(128, 0, 255, 0)),
),
],
),
),
],
),
),
),
);
final RenderBox box = key.currentContext.findRenderObject();
expect(box.localToGlobal(Offset.zero), const Offset(123.0, 456.0));
await expectLater(
find.byKey(mainStackKey),
matchesGoldenFile('inspector.composited_transform.only_offsets.png'),
);
await expectLater(
WidgetInspectorService.instance.screenshot(
find.byKey(stackWithTransformFollower).evaluate().first,
width: 5000.0,
height: 500.0,
),
matchesGoldenFile('inspector.composited_transform.only_offsets_follower.png'),
);
await expectLater(
WidgetInspectorService.instance.screenshot(find.byType(Stack).evaluate().first, width: 300.0, height: 300.0),
matchesGoldenFile('inspector.composited_transform.only_offsets_small.png'),
);
await expectLater(
WidgetInspectorService.instance.screenshot(
find.byKey(transformTargetParent).evaluate().first,
width: 500.0,
height: 500.0,
),
matchesGoldenFile('inspector.composited_transform.only_offsets_target.png'),
);
});
testWidgets('Screenshot composited transforms - with rotations', (WidgetTester tester) async {
final LayerLink link = new LayerLink();
final GlobalKey key1 = new GlobalKey();
final GlobalKey key2 = new GlobalKey();
final GlobalKey rotate1 = new GlobalKey();
final GlobalKey rotate2 = new GlobalKey();
final GlobalKey mainStackKey = new GlobalKey();
final GlobalKey stackWithTransformTarget = new GlobalKey();
final GlobalKey stackWithTransformFollower = new GlobalKey();
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
key: mainStackKey,
children: <Widget>[
new Stack(
key: stackWithTransformTarget,
children: <Widget>[
new Positioned(
top: 123.0,
left: 456.0,
child: new Transform.rotate(
key: rotate1,
angle: 1.0, // radians
child: new CompositedTransformTarget(
link: link,
child: new Container(key: key1, height: 20.0, width: 20.0, color: const Color.fromARGB(128, 255, 0, 0)),
),
),
),
],
),
new Positioned(
top: 487.0,
left: 243.0,
child: new Stack(
key: stackWithTransformFollower,
children: <Widget>[
new Container(height: 15.0, width: 15.0, color: const Color.fromARGB(128, 0, 0, 255)),
new Transform.rotate(
key: rotate2,
angle: -0.3, // radians
child: new CompositedTransformFollower(
link: link,
child: new Container(key: key2, height: 10.0, width: 10.0, color: const Color.fromARGB(128, 0, 255, 0)),
),
),
],
),
),
],
),
),
);
final RenderBox box1 = key1.currentContext.findRenderObject();
final RenderBox box2 = key2.currentContext.findRenderObject();
// Snapshot the positions of the two relevant boxes to ensure that taking
// screenshots doesn't impact their positions.
final Offset position1 = box1.localToGlobal(Offset.zero);
final Offset position2 = box2.localToGlobal(Offset.zero);
expect(position1.dx, moreOrLessEquals(position2.dx));
expect(position1.dy, moreOrLessEquals(position2.dy));
// Image of the full scene to use as reference to help validate that the
// screenshots of specific subtrees are reasonable.
await expectLater(
find.byKey(mainStackKey),
matchesGoldenFile('inspector.composited_transform.with_rotations.png'),
);
await expectLater(
WidgetInspectorService.instance.screenshot(
find.byKey(mainStackKey).evaluate().first,
width: 500.0,
height: 500.0,
),
matchesGoldenFile('inspector.composited_transform.with_rotations_small.png'),
);
await expectLater(
WidgetInspectorService.instance.screenshot(
find.byKey(stackWithTransformTarget).evaluate().first,
width: 500.0,
height: 500.0,
),
matchesGoldenFile('inspector.composited_transform.with_rotations_target.png'),
);
await expectLater(
WidgetInspectorService.instance.screenshot(
find.byKey(stackWithTransformFollower).evaluate().first,
width: 500.0,
height: 500.0,
),
matchesGoldenFile('inspector.composited_transform.with_rotations_follower.png'),
);
// Make sure taking screenshots hasn't modified the positions of the
// TransformTarget or TransformFollower layers.
expect(identical(key1.currentContext.findRenderObject(), box1), isTrue);
expect(identical(key2.currentContext.findRenderObject(), box2), isTrue);
expect(box1.localToGlobal(Offset.zero), equals(position1));
expect(box2.localToGlobal(Offset.zero), equals(position2));
});
}
}
......@@ -251,8 +251,12 @@ Matcher isMethodCall(String name, {@required dynamic arguments}) {
Matcher coversSameAreaAs(Path expectedPath, {@required Rect areaToCompare, int sampleSize = 20})
=> new _CoversSameAreaAs(expectedPath, areaToCompare: areaToCompare, sampleSize: sampleSize);
/// Asserts that a [Finder] matches exactly one widget whose rendered image
/// matches the golden image file identified by [key].
/// Asserts that a [Finder], [Future<ui.Image>], or [ui.Image] matches the
/// golden image file identified by [key].
///
/// For the case of a [Finder], the [Finder] must match exactly one widget and
/// the rendered image of the first [RepaintBoundary] ancestor of the widget is
/// treated as the image for the widget.
///
/// [key] may be either a [Uri] or a [String] representation of a URI.
///
......@@ -264,6 +268,8 @@ Matcher coversSameAreaAs(Path expectedPath, {@required Rect areaToCompare, int s
///
/// ```dart
/// await expectLater(find.text('Save'), matchesGoldenFile('save.png'));
/// await expectLater(image, matchesGoldenFile('save.png'));
/// await expectLater(imageFuture, matchesGoldenFile('save.png'));
/// ```
///
/// See also:
......@@ -1496,6 +1502,17 @@ class _CoversSameAreaAs extends Matcher {
description.add('covers expected area and only expected area');
}
Future<ui.Image> _captureImage(Element element) {
RenderObject renderObject = element.renderObject;
while (!renderObject.isRepaintBoundary) {
renderObject = renderObject.parent;
assert(renderObject != null);
}
assert(!renderObject.debugNeedsPaint);
final OffsetLayer layer = renderObject.layer;
return layer.toImage(renderObject.paintBounds);
}
class _MatchesGoldenFile extends AsyncMatcher {
const _MatchesGoldenFile(this.key);
......@@ -1504,23 +1521,22 @@ class _MatchesGoldenFile extends AsyncMatcher {
final Uri key;
@override
Future<String> matchAsync(covariant Finder finder) async {
final Iterable<Element> elements = finder.evaluate();
if (elements.isEmpty) {
return 'could not be rendered because no widget was found';
} else if (elements.length > 1) {
return 'matched too many widgets';
}
final Element element = elements.single;
RenderObject renderObject = element.renderObject;
while (!renderObject.isRepaintBoundary) {
renderObject = renderObject.parent;
assert(renderObject != null);
Future<String> matchAsync(dynamic item) async {
Future<ui.Image> imageFuture;
if (item is Future<ui.Image>) {
imageFuture = item;
} else if (item is ui.Image) {
imageFuture = new Future<ui.Image>.value(item);
} else {
final Finder finder = item;
final Iterable<Element> elements = finder.evaluate();
if (elements.isEmpty) {
return 'could not be rendered because no widget was found';
} else if (elements.length > 1) {
return 'matched too many widgets';
}
imageFuture = _captureImage(elements.single);
}
assert(!renderObject.debugNeedsPaint);
final OffsetLayer layer = renderObject.layer;
final Future<ui.Image> imageFuture = layer.toImage(renderObject.paintBounds);
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
return binding.runAsync<String>(() async {
......
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