Unverified Commit 4cf89cc2 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Remove LiveTestRenderView (#127882)

In the multi view world, `RenderViews` are created by the `View` widget and no longer owned by the binding. Prior to this change, the `LiveTestWidgetsFlutterBinding` owned and managed a special subclass of `RenderView`, the `_LiveTestRenderView`. In the new world, where `RenderView`s can be created anywhere in the widget tree where a `View` widget is used, this setup is no longer feasible. This change removes this special `_LiveTestRenderView` and instead adds debug hocks to `RenderView` to allow the `LiveTestWidgetsFlutterBinding` to draw a debug overlay on top of the content of any `RenderView`.
parent 6f8ef1a1
......@@ -33,6 +33,10 @@ class ViewConfiguration {
final double devicePixelRatio;
/// Creates a transformation matrix that applies the [devicePixelRatio].
///
/// The matrix translates points from the local coordinate system of the
/// app (in logical pixels) to the global coordinate system of the
/// [FlutterView] (in physical pixels).
Matrix4 toMatrix() {
return Matrix4.diagonal3Values(devicePixelRatio, devicePixelRatio, 1.0);
}
......@@ -212,6 +216,15 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
if (child != null) {
context.paintChild(child!, offset);
}
assert(() {
final List<DebugPaintCallback> localCallbacks = _debugPaintCallbacks.toList();
for (final DebugPaintCallback paintCallback in localCallbacks) {
if (_debugPaintCallbacks.contains(paintCallback)) {
paintCallback(context, offset, this);
}
}
return true;
}());
}
@override
......@@ -381,4 +394,47 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
properties.add(DiagnosticsNode.message('semantics enabled'));
}
}
static final List<DebugPaintCallback> _debugPaintCallbacks = <DebugPaintCallback>[];
/// Registers a [DebugPaintCallback] that is called every time a [RenderView]
/// repaints in debug mode.
///
/// The callback may paint a debug overlay on top of the content of the
/// [RenderView] provided to the callback. Callbacks are invoked in the
/// order they were registered in.
///
/// Neither registering a callback nor the continued presence of a callback
/// changes how often [RenderView]s are repainted. It is up to the owner of
/// the callback to call [markNeedsPaint] on any [RenderView] for which it
/// wants to update the painted overlay.
///
/// Does nothing in release mode.
static void debugAddPaintCallback(DebugPaintCallback callback) {
assert(() {
_debugPaintCallbacks.add(callback);
return true;
}());
}
/// Removes a callback registered with [debugAddPaintCallback].
///
/// It does not schedule a frame to repaint the [RenderView]s without the
/// overlay painted by the removed callback. It is up to the owner of the
/// callback to call [markNeedsPaint] on the relevant [RenderView]s to
/// repaint them without the overlay.
///
/// Does nothing in release mode.
static void debugRemovePaintCallback(DebugPaintCallback callback) {
assert(() {
_debugPaintCallbacks.remove(callback);
return true;
}());
}
}
/// A callback for painting a debug overlay on top of the provided [RenderView].
///
/// Used by [RenderView.debugAddPaintCallback] and
/// [RenderView.debugRemovePaintCallback].
typedef DebugPaintCallback = void Function(PaintingContext context, Offset offset, RenderView renderView);
......@@ -5,6 +5,7 @@
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'mock_canvas.dart';
import 'rendering_tester.dart';
void main() {
......@@ -69,4 +70,76 @@ void main() {
expect(viewConfigurationA.hashCode, viewConfigurationB.hashCode);
expect(viewConfigurationA.hashCode != viewConfigurationC.hashCode, true);
});
test('invokes DebugPaintCallback', () {
final PaintPattern paintsOrangeRect = paints..rect(
color: orange,
rect: orangeRect,
);
final PaintPattern paintsGreenRect = paints..rect(
color: green,
rect: greenRect,
);
final PaintPattern paintOrangeAndGreenRect = paints
..rect(
color: orange,
rect: orangeRect,
)
..rect(
color: green,
rect: greenRect,
);
void paintCallback(PaintingContext context, Offset offset, RenderView renderView) {
context.canvas.drawRect(
greenRect,
Paint()..color = green,
);
}
layout(TestRenderObject());
expect(
TestRenderingFlutterBinding.instance.renderView,
paintsOrangeRect,
);
expect(
TestRenderingFlutterBinding.instance.renderView,
isNot(paintsGreenRect),
);
RenderView.debugAddPaintCallback(paintCallback);
expect(
TestRenderingFlutterBinding.instance.renderView,
paintOrangeAndGreenRect,
);
RenderView.debugRemovePaintCallback(paintCallback);
expect(
TestRenderingFlutterBinding.instance.renderView,
paintsOrangeRect,
);
expect(
TestRenderingFlutterBinding.instance.renderView,
isNot(paintsGreenRect),
);
});
}
const Color orange = Color(0xFFFF9000);
const Color green = Color(0xFF0FF900);
const Rect orangeRect = Rect.fromLTWH(10, 10, 50, 75);
const Rect greenRect = Rect.fromLTWH(20, 20, 100, 150);
class TestRenderObject extends RenderBox {
@override
void performLayout() {
size = constraints.biggest;
}
@override
void paint(PaintingContext context, Offset offset) {
context.canvas.drawRect(
orangeRect,
Paint()..color = orange,
);
}
}
......@@ -563,17 +563,24 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
});
}
/// Convert the given point from the global coordinate space to the local
/// one.
/// Convert the given point from the global coordinate space of the provided
/// [RenderView] to its local one.
///
/// This method operates in logical pixels for both coordinate spaces. It does
/// not apply the device pixel ratio (used to translate to/from physical
/// pixels).
///
/// For definitions for coordinate spaces, see [TestWidgetsFlutterBinding].
Offset globalToLocal(Offset point) => point;
Offset globalToLocal(Offset point, RenderView view) => point;
/// Convert the given point from the local coordinate space to the global
/// one.
/// coordinate space of the [RenderView].
///
/// This method operates in logical pixels for both coordinate spaces. It does
/// not apply the device pixel ratio to translate to physical pixels.
///
/// For definitions for coordinate spaces, see [TestWidgetsFlutterBinding].
Offset localToGlobal(Offset point) => point;
Offset localToGlobal(Offset point, RenderView view) => point;
/// The source of the current pointer event.
///
......@@ -1654,6 +1661,8 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
void initInstances() {
super.initInstances();
_instance = this;
RenderView.debugAddPaintCallback(_handleRenderViewPaint);
}
/// The current [LiveTestWidgetsFlutterBinding], if one has been created.
......@@ -1782,21 +1791,68 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
}
}
@override
void initRenderView() {
renderView = _LiveTestRenderView(
configuration: createViewConfiguration(),
onNeedPaint: _handleViewNeedsPaint,
view: platformDispatcher.implicitView!,
);
renderView.prepareInitialFrame();
void _markViewNeedsPaint() {
_viewNeedsPaint = true;
renderView.markNeedsPaint();
}
_LiveTestRenderView get _liveTestRenderView => super.renderView as _LiveTestRenderView;
TextPainter? _label;
static const TextStyle _labelStyle = TextStyle(
fontFamily: 'sans-serif',
fontSize: 10.0,
);
void _handleViewNeedsPaint() {
_viewNeedsPaint = true;
renderView.markNeedsPaint();
void _setDescription(String value) {
if (value.isEmpty) {
_label = null;
return;
}
// TODO(ianh): Figure out if the test name is actually RTL.
_label ??= TextPainter(textAlign: TextAlign.left, textDirection: TextDirection.ltr);
_label!.text = TextSpan(text: value, style: _labelStyle);
_label!.layout();
_markViewNeedsPaint();
}
final Map<int, _LiveTestPointerRecord> _pointerIdToPointerRecord = <int, _LiveTestPointerRecord>{};
void _handleRenderViewPaint(PaintingContext context, Offset offset, RenderView renderView) {
assert(offset == Offset.zero);
if (_pointerIdToPointerRecord.isNotEmpty) {
final double radius = renderView.configuration.size.shortestSide * 0.05;
final Path path = Path()
..addOval(Rect.fromCircle(center: Offset.zero, radius: radius))
..moveTo(0.0, -radius * 2.0)
..lineTo(0.0, radius * 2.0)
..moveTo(-radius * 2.0, 0.0)
..lineTo(radius * 2.0, 0.0);
final Canvas canvas = context.canvas;
final Paint paint = Paint()
..strokeWidth = radius / 10.0
..style = PaintingStyle.stroke;
bool dirty = false;
for (final _LiveTestPointerRecord record in _pointerIdToPointerRecord.values) {
paint.color = record.color.withOpacity(record.decay < 0 ? (record.decay / (_kPointerDecay - 1)) : 1.0);
canvas.drawPath(path.shift(record.position), paint);
if (record.decay < 0) {
dirty = true;
}
record.decay += 1;
}
_pointerIdToPointerRecord
.keys
.where((int pointer) => _pointerIdToPointerRecord[pointer]!.decay == 0)
.toList()
.forEach(_pointerIdToPointerRecord.remove);
if (dirty) {
scheduleMicrotask(() {
_markViewNeedsPaint();
});
}
}
_label?.paint(context.canvas, offset - const Offset(0.0, 10.0));
}
/// An object to which real device events should be routed.
......@@ -1822,19 +1878,19 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
void handlePointerEvent(PointerEvent event) {
switch (pointerEventSource) {
case TestBindingEventSource.test:
final _LiveTestPointerRecord? record = _liveTestRenderView._pointers[event.pointer];
final _LiveTestPointerRecord? record = _pointerIdToPointerRecord[event.pointer];
if (record != null) {
record.position = event.position;
if (!event.down) {
record.decay = _kPointerDecay;
}
_handleViewNeedsPaint();
_markViewNeedsPaint();
} else if (event.down) {
_liveTestRenderView._pointers[event.pointer] = _LiveTestPointerRecord(
_pointerIdToPointerRecord[event.pointer] = _LiveTestPointerRecord(
event.pointer,
event.position,
);
_handleViewNeedsPaint();
_markViewNeedsPaint();
}
super.handlePointerEvent(event);
case TestBindingEventSource.device:
......@@ -1846,7 +1902,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
// The pointer events received with this source has a global position
// (see [handlePointerEventForSource]). Transform it to the local
// coordinate space used by the testing widgets.
final PointerEvent localEvent = event.copyWith(position: globalToLocal(event.position));
final PointerEvent localEvent = event.copyWith(position: globalToLocal(event.position, renderView));
withPointerEventSource(TestBindingEventSource.device,
() => super.handlePointerEvent(localEvent)
);
......@@ -1945,7 +2001,7 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
}) {
assert(!inTest);
_inTest = true;
_liveTestRenderView._setDescription(description);
_setDescription(description);
return _runTest(testBody, invariantTester, description);
}
......@@ -1981,24 +2037,42 @@ class LiveTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
}
@override
Offset globalToLocal(Offset point) {
final Matrix4 transform = _liveTestRenderView.configuration.toHitTestMatrix();
Offset globalToLocal(Offset point, RenderView view) {
// The method is expected to translate the given point expressed in logical
// pixels in the global coordinate space to the local coordinate space (also
// expressed in logical pixels).
// The inverted transform translates from the global coordinate space in
// physical pixels to the local coordinate space in logical pixels.
final Matrix4 transform = view.configuration.toMatrix();
final double det = transform.invert();
assert(det != 0.0);
final Offset result = MatrixUtils.transformPoint(transform, point);
return result;
// In order to use the transform, we need to translate the point first into
// the physical coordinate space by applying the device pixel ratio.
return MatrixUtils.transformPoint(
transform,
point * view.configuration.devicePixelRatio,
);
}
@override
Offset localToGlobal(Offset point) {
final Matrix4 transform = _liveTestRenderView.configuration.toHitTestMatrix();
return MatrixUtils.transformPoint(transform, point);
Offset localToGlobal(Offset point, RenderView view) {
// The method is expected to translate the given point expressed in logical
// pixels in the local coordinate space to the global coordinate space (also
// expressed in logical pixels).
// The transform translates from the local coordinate space in logical
// pixels to the global coordinate space in physical pixels.
final Matrix4 transform = view.configuration.toMatrix();
final Offset pointInPhysicalPixels = MatrixUtils.transformPoint(transform, point);
// We need to apply the device pixel ratio to get back to logical pixels.
return pointInPhysicalPixels / view.configuration.devicePixelRatio;
}
}
/// A [ViewConfiguration] that pretends the display is of a particular size. The
/// size is in logical pixels. The resulting ViewConfiguration maps the given
/// size onto the actual display using the [BoxFit.contain] algorithm.
/// A [ViewConfiguration] that pretends the display is of a particular size (in
/// logical pixels).
///
/// The resulting ViewConfiguration maps the given size onto the actual display
/// using the [BoxFit.contain] algorithm.
class TestViewConfiguration extends ViewConfiguration {
/// Deprecated. Will be removed in a future version of Flutter.
///
......@@ -2023,7 +2097,6 @@ class TestViewConfiguration extends ViewConfiguration {
/// The [size] defaults to 800x600.
TestViewConfiguration.fromView({required ui.FlutterView view, super.size = _kDefaultTestViewportSize})
: _paintMatrix = _getMatrix(size, view.devicePixelRatio, view),
_hitTestMatrix = _getMatrix(size, 1.0, view),
super(devicePixelRatio: view.devicePixelRatio);
static Matrix4 _getMatrix(Size size, double devicePixelRatio, ui.FlutterView window) {
......@@ -2051,21 +2124,10 @@ class TestViewConfiguration extends ViewConfiguration {
}
final Matrix4 _paintMatrix;
final Matrix4 _hitTestMatrix;
@override
Matrix4 toMatrix() => _paintMatrix.clone();
/// Provides the transformation matrix that converts coordinates in the test
/// coordinate space to coordinates in logical pixels on the real display.
///
/// This is essentially the same as [toMatrix] but ignoring the device pixel
/// ratio.
///
/// This is useful because pointers are described in logical pixels, as
/// opposed to graphics which are expressed in physical pixels.
Matrix4 toHitTestMatrix() => _hitTestMatrix.clone();
@override
String toString() => 'TestViewConfiguration';
}
......@@ -2083,75 +2145,3 @@ class _LiveTestPointerRecord {
Offset position;
int decay; // >0 means down, <0 means up, increases by one each time, removed at 0
}
class _LiveTestRenderView extends RenderView {
_LiveTestRenderView({
required super.configuration,
required this.onNeedPaint,
required super.view,
});
@override
TestViewConfiguration get configuration => super.configuration as TestViewConfiguration;
@override
set configuration(covariant TestViewConfiguration value) { super.configuration = value; }
final VoidCallback onNeedPaint;
final Map<int, _LiveTestPointerRecord> _pointers = <int, _LiveTestPointerRecord>{};
TextPainter? _label;
static const TextStyle _labelStyle = TextStyle(
fontFamily: 'sans-serif',
fontSize: 10.0,
);
void _setDescription(String value) {
if (value.isEmpty) {
_label = null;
return;
}
// TODO(ianh): Figure out if the test name is actually RTL.
_label ??= TextPainter(textAlign: TextAlign.left, textDirection: TextDirection.ltr);
_label!.text = TextSpan(text: value, style: _labelStyle);
_label!.layout();
onNeedPaint();
}
@override
void paint(PaintingContext context, Offset offset) {
assert(offset == Offset.zero);
super.paint(context, offset);
if (_pointers.isNotEmpty) {
final double radius = configuration.size.shortestSide * 0.05;
final Path path = Path()
..addOval(Rect.fromCircle(center: Offset.zero, radius: radius))
..moveTo(0.0, -radius * 2.0)
..lineTo(0.0, radius * 2.0)
..moveTo(-radius * 2.0, 0.0)
..lineTo(radius * 2.0, 0.0);
final Canvas canvas = context.canvas;
final Paint paint = Paint()
..strokeWidth = radius / 10.0
..style = PaintingStyle.stroke;
bool dirty = false;
for (final int pointer in _pointers.keys) {
final _LiveTestPointerRecord record = _pointers[pointer]!;
paint.color = record.color.withOpacity(record.decay < 0 ? (record.decay / (_kPointerDecay - 1)) : 1.0);
canvas.drawPath(path.shift(record.position), paint);
if (record.decay < 0) {
dirty = true;
}
record.decay += 1;
}
_pointers
.keys
.where((int pointer) => _pointers[pointer]!.decay == 0)
.toList()
.forEach(_pointers.remove);
if (dirty) {
scheduleMicrotask(onNeedPaint);
}
}
_label?.paint(context.canvas, offset - const Offset(0.0, 10.0));
}
}
......@@ -834,7 +834,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
@override
HitTestResult hitTestOnBinding(Offset location) {
location = binding.localToGlobal(location);
location = binding.localToGlobal(location, binding.renderView);
return super.hitTestOnBinding(location);
}
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
final LiveTestWidgetsFlutterBinding binding = LiveTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('localToGlobal and globalToLocal calculate correct results', (WidgetTester tester) async {
tester.view.physicalSize = const Size(2400, 1800);
tester.view.devicePixelRatio = 3.0;
final RenderView renderView = RenderView(
view: tester.view,
configuration: TestViewConfiguration.fromView(
view: tester.view,
size: const Size(400, 200),
)
);
// The configuration above defines a view with a resolution of 2400x1800
// physical pixels. With a device pixel ratio of 3x, this yields a
// resolution of 800x600 logical pixels. In this view, a RenderView sized
// 400x200 (in logical pixels) is fitted using the BoxFit.contain
// algorithm (as documented on TestViewConfiguration. To fit 400x200 into
// 800x600 the RenderView is scaled up by 2 to fill the full width and then
// vertically positioned in the middle. The origin of the RenderView is
// located at (0, 100) in the logical coordinate space of the view:
//
// View: 800 logical pixels wide (or 2400 physical pixels)
// +---------------------------------------+
// | |
// | 100px |
// | |
// +---------------------------------------+
// | |
// | RenderView (400x200px) |
// | 400px scaled to 800x400px | View: 600 logical pixels high (or 1800 physical pixels)
// | |
// | |
// +---------------------------------------+
// | |
// | 200px |
// | |
// +---------------------------------------+
//
// All values in logical pixels until otherwise noted.
//
// A point can be translated from the local coordinate space of the
// RenderView (in logical pixels) to the global coordinate space of the View
// (in logical pixels) by multiplying each coordinate by 2 and adding 100 to
// the y coordinate. This is what the localToGlobal/globalToLocal methods
// do:
expect(binding.localToGlobal(const Offset(0, -50), renderView), Offset.zero);
expect(binding.localToGlobal(Offset.zero, renderView), const Offset(0, 100));
expect(binding.localToGlobal(const Offset(200, 100), renderView), const Offset(400, 300));
expect(binding.localToGlobal(const Offset(150, 75), renderView), const Offset(300, 250));
expect(binding.localToGlobal(const Offset(400, 200), renderView), const Offset(800, 500));
expect(binding.localToGlobal(const Offset(400, 400), renderView), const Offset(800, 900));
expect(binding.globalToLocal(Offset.zero, renderView), const Offset(0, -50));
expect(binding.globalToLocal(const Offset(0, 100), renderView), Offset.zero);
expect(binding.globalToLocal(const Offset(400, 300), renderView), const Offset(200, 100));
expect(binding.globalToLocal(const Offset(300, 250), renderView), const Offset(150, 75));
expect(binding.globalToLocal(const Offset(800, 500), renderView), const Offset(400, 200));
expect(binding.globalToLocal(const Offset(800, 900), renderView), const Offset(400, 400));
});
}
......@@ -126,7 +126,7 @@ class _MockLiveTestWidgetsFlutterBinding extends LiveTestWidgetsFlutterBinding {
// real devices touches sends event in the global coordinate system.
// See the documentation of [handlePointerEventForSource] for details.
if (source == TestBindingEventSource.test) {
final PointerEvent globalEvent = event.copyWith(position: localToGlobal(event.position));
final PointerEvent globalEvent = event.copyWith(position: localToGlobal(event.position, renderView));
return super.handlePointerEventForSource(globalEvent);
}
return super.handlePointerEventForSource(event, source: source);
......
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