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.
......
......@@ -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', () {
......
......@@ -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