Unverified Commit a2acc6a3 authored by Jacob Richman's avatar Jacob Richman Committed by GitHub

Revert "Revert "Enable taking screenshots of arbitrary RenderObjects from a...

Revert "Revert "Enable taking screenshots of arbitrary RenderObjects from a running a… (#20637)" (#21395)" (#21448)

This reverts commit 5b5a5b82.
parent de9f775f
......@@ -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