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 { ...@@ -50,6 +50,8 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
Layer _previousSibling; Layer _previousSibling;
/// Removes this layer from its parent layer's child list. /// Removes this layer from its parent layer's child list.
///
/// This has no effect if the layer's parent is already null.
@mustCallSuper @mustCallSuper
void remove() { void remove() {
parent?._removeChild(this); parent?._removeChild(this);
...@@ -588,7 +590,11 @@ class OffsetLayer extends ContainerLayer { ...@@ -588,7 +590,11 @@ class OffsetLayer extends ContainerLayer {
assert(bounds != null); assert(bounds != null);
assert(pixelRatio != null); assert(pixelRatio != null);
final ui.SceneBuilder builder = new ui.SceneBuilder(); 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); transform.scale(pixelRatio, pixelRatio);
builder.pushTransform(transform.storage); builder.pushTransform(transform.storage);
addToScene(builder, Offset.zero); addToScene(builder, Offset.zero);
......
...@@ -60,7 +60,13 @@ typedef void PaintingContextCallback(PaintingContext context, Offset offset); ...@@ -60,7 +60,13 @@ typedef void PaintingContextCallback(PaintingContext context, Offset offset);
/// New [PaintingContext] objects are created automatically when using /// New [PaintingContext] objects are created automatically when using
/// [PaintingContext.repaintCompositedChild] and [pushLayer]. /// [PaintingContext.repaintCompositedChild] and [pushLayer].
class PaintingContext extends ClipContext { 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(_containerLayer != null),
assert(estimatedBounds != null); assert(estimatedBounds != null);
...@@ -86,8 +92,19 @@ class PaintingContext extends ClipContext { ...@@ -86,8 +92,19 @@ class PaintingContext extends ClipContext {
/// * [RenderObject.isRepaintBoundary], which determines if a [RenderObject] /// * [RenderObject.isRepaintBoundary], which determines if a [RenderObject]
/// has a composited layer. /// has a composited layer.
static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) { static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) {
assert(child.isRepaintBoundary);
assert(child._needsPaint); assert(child._needsPaint);
_repaintCompositedChild(
child,
debugAlsoPaintedParent: debugAlsoPaintedParent,
);
}
static void _repaintCompositedChild(
RenderObject child, {
bool debugAlsoPaintedParent = false,
PaintingContext childContext,
}) {
assert(child.isRepaintBoundary);
assert(() { assert(() {
// register the call for RepaintBoundary metrics // register the call for RepaintBoundary metrics
child.debugRegisterRepaintBoundaryPaint( child.debugRegisterRepaintBoundaryPaint(
...@@ -107,9 +124,32 @@ class PaintingContext extends ClipContext { ...@@ -107,9 +124,32 @@ class PaintingContext extends ClipContext {
child._layer.debugCreator = child.debugCreator ?? child.runtimeType; child._layer.debugCreator = child.debugCreator ?? child.runtimeType;
return true; return true;
}()); }());
final PaintingContext childContext = new PaintingContext._(child._layer, child.paintBounds); childContext ??= new PaintingContext(child._layer, child.paintBounds);
child._paintWithContext(childContext, Offset.zero); 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]. /// Paint a child [RenderObject].
...@@ -125,7 +165,7 @@ class PaintingContext extends ClipContext { ...@@ -125,7 +165,7 @@ class PaintingContext extends ClipContext {
}()); }());
if (child.isRepaintBoundary) { if (child.isRepaintBoundary) {
_stopRecordingIfNeeded(); stopRecordingIfNeeded();
_compositeChild(child, offset); _compositeChild(child, offset);
} else { } else {
child._paintWithContext(this, offset); child._paintWithContext(this, offset);
...@@ -159,10 +199,20 @@ class PaintingContext extends ClipContext { ...@@ -159,10 +199,20 @@ class PaintingContext extends ClipContext {
}()); }());
} }
child._layer.offset = offset; 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); assert(!_isRecording);
layer.remove(); layer.remove();
_containerLayer.append(layer); _containerLayer.append(layer);
...@@ -210,7 +260,19 @@ class PaintingContext extends ClipContext { ...@@ -210,7 +260,19 @@ class PaintingContext extends ClipContext {
_containerLayer.append(_currentLayer); _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) if (!_isRecording)
return; return;
assert(() { assert(() {
...@@ -271,8 +333,8 @@ class PaintingContext extends ClipContext { ...@@ -271,8 +333,8 @@ class PaintingContext extends ClipContext {
/// * [pushLayer], for adding a layer and using its canvas to paint with that /// * [pushLayer], for adding a layer and using its canvas to paint with that
/// layer. /// layer.
void addLayer(Layer layer) { void addLayer(Layer layer) {
_stopRecordingIfNeeded(); stopRecordingIfNeeded();
_appendLayer(layer); appendLayer(layer);
} }
/// Appends the given layer to the recording, and calls the `painter` callback /// Appends the given layer to the recording, and calls the `painter` callback
...@@ -295,15 +357,21 @@ class PaintingContext extends ClipContext { ...@@ -295,15 +357,21 @@ class PaintingContext extends ClipContext {
/// See also: /// See also:
/// ///
/// * [addLayer], for pushing a leaf layer whose canvas is not used. /// * [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.attached);
assert(childLayer.parent == null); assert(childLayer.parent == null);
assert(painter != null); assert(painter != null);
_stopRecordingIfNeeded(); stopRecordingIfNeeded();
_appendLayer(childLayer); appendLayer(childLayer);
final PaintingContext childContext = new PaintingContext._(childLayer, childPaintBounds ?? estimatedBounds); final PaintingContext childContext = createChildContext(childLayer, childPaintBounds ?? estimatedBounds);
painter(childContext, offset); 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. /// Clip further painting using a rectangle.
...@@ -2036,7 +2104,6 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2036,7 +2104,6 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
Rect get paintBounds; Rect get paintBounds;
/// Override this method to paint debugging information. /// Override this method to paint debugging information.
@protected
void debugPaint(PaintingContext context, Offset offset) { } void debugPaint(PaintingContext context, Offset offset) { }
/// Paint this render object into the given context at the given offset. /// Paint this render object into the given context at the given offset.
......
...@@ -512,7 +512,7 @@ void main() { ...@@ -512,7 +512,7 @@ void main() {
// If you add a service extension... TEST IT! :-) // If you add a service extension... TEST IT! :-)
// ...then increment this number. // ...then increment this number.
expect(binding.extensions.length, 37); expect(binding.extensions.length, 38);
expect(console, isEmpty); expect(console, isEmpty);
debugPrint = debugPrintThrottled; debugPrint = debugPrintThrottled;
......
...@@ -185,12 +185,43 @@ void main() { ...@@ -185,12 +185,43 @@ void main() {
image = await boundary.toImage(); image = await boundary.toImage();
expect(image.width, equals(20)); expect(image.width, equals(20));
expect(image.height, 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.lengthInBytes, equals(20 * 20 * 4));
expect(data.elementSizeInBytes, equals(1)); expect(data.elementSizeInBytes, equals(1));
const int stride = 20 * 4; expect(getPixel(0, 0), equals(0x00000080));
expect(data.getUint32(0), equals(0x00000080)); expect(getPixel(image.width - 1, 0 ), equals(0xffffffff));
expect(data.getUint32(stride - 4), 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', () { test('RenderOpacity does not composite if it is transparent', () {
......
...@@ -251,8 +251,12 @@ Matcher isMethodCall(String name, {@required dynamic arguments}) { ...@@ -251,8 +251,12 @@ Matcher isMethodCall(String name, {@required dynamic arguments}) {
Matcher coversSameAreaAs(Path expectedPath, {@required Rect areaToCompare, int sampleSize = 20}) Matcher coversSameAreaAs(Path expectedPath, {@required Rect areaToCompare, int sampleSize = 20})
=> new _CoversSameAreaAs(expectedPath, areaToCompare: areaToCompare, sampleSize: sampleSize); => new _CoversSameAreaAs(expectedPath, areaToCompare: areaToCompare, sampleSize: sampleSize);
/// Asserts that a [Finder] matches exactly one widget whose rendered image /// Asserts that a [Finder], [Future<ui.Image>], or [ui.Image] matches the
/// matches the golden image file identified by [key]. /// 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. /// [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 ...@@ -264,6 +268,8 @@ Matcher coversSameAreaAs(Path expectedPath, {@required Rect areaToCompare, int s
/// ///
/// ```dart /// ```dart
/// await expectLater(find.text('Save'), matchesGoldenFile('save.png')); /// await expectLater(find.text('Save'), matchesGoldenFile('save.png'));
/// await expectLater(image, matchesGoldenFile('save.png'));
/// await expectLater(imageFuture, matchesGoldenFile('save.png'));
/// ``` /// ```
/// ///
/// See also: /// See also:
...@@ -1496,6 +1502,17 @@ class _CoversSameAreaAs extends Matcher { ...@@ -1496,6 +1502,17 @@ class _CoversSameAreaAs extends Matcher {
description.add('covers expected area and only expected area'); 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 { class _MatchesGoldenFile extends AsyncMatcher {
const _MatchesGoldenFile(this.key); const _MatchesGoldenFile(this.key);
...@@ -1504,23 +1521,22 @@ class _MatchesGoldenFile extends AsyncMatcher { ...@@ -1504,23 +1521,22 @@ class _MatchesGoldenFile extends AsyncMatcher {
final Uri key; final Uri key;
@override @override
Future<String> matchAsync(covariant Finder finder) async { Future<String> matchAsync(dynamic item) async {
final Iterable<Element> elements = finder.evaluate(); Future<ui.Image> imageFuture;
if (elements.isEmpty) { if (item is Future<ui.Image>) {
return 'could not be rendered because no widget was found'; imageFuture = item;
} else if (elements.length > 1) { } else if (item is ui.Image) {
return 'matched too many widgets'; imageFuture = new Future<ui.Image>.value(item);
} } else {
final Element element = elements.single; final Finder finder = item;
final Iterable<Element> elements = finder.evaluate();
RenderObject renderObject = element.renderObject; if (elements.isEmpty) {
while (!renderObject.isRepaintBoundary) { return 'could not be rendered because no widget was found';
renderObject = renderObject.parent; } else if (elements.length > 1) {
assert(renderObject != null); 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(); final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
return binding.runAsync<String>(() async { 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