mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Rebase ios-experimental branch onto main. This will make the PRs experimenting with newer versions of Xcode (like https://github.com/flutter/flutter/pull/173123) smaller and easier to reason about. Rebases #168860 and #170274 ``` $ git rebase main -Xtheirs ``` --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Co-authored-by: Siva <a-siva@users.noreply.github.com> Co-authored-by: engine-flutter-autoroll <engine-flutter-autoroll@skia.org> Co-authored-by: Jamil Saadeh <jssaadeh@outlook.com> Co-authored-by: Dara Adedeji <76637177+SunkenInTime@users.noreply.github.com> Co-authored-by: Greg Price <gnprice@gmail.com> Co-authored-by: Ben Konyi <bkonyi@google.com> Co-authored-by: Ricardo Dalarme <ricardodalarme@outlook.com> Co-authored-by: Flutter GitHub Bot <fluttergithubbot@gmail.com> Co-authored-by: Justin McCandless <jmccandless@google.com> Co-authored-by: Alex Talebi <31685655+SalehTZ@users.noreply.github.com> Co-authored-by: Qun Cheng <36861262+QuncCccccc@users.noreply.github.com> Co-authored-by: Mouad Debbar <mdebbar@google.com> Co-authored-by: Zuckjet <1083941774@qq.com> Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com> Co-authored-by: auto-submit[bot] <98614782+auto-submit[bot]@users.noreply.github.com> Co-authored-by: auto-submit[bot] <flutter-engprod-team@google.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: yim <ybz975218925@gmail.com> Co-authored-by: bufffun <chenmingding.cmd@alibaba-inc.com> Co-authored-by: Chinmay Garde <chinmaygarde@google.com> Co-authored-by: Hannah Jin <jhy03261997@gmail.com> Co-authored-by: Kate Lovett <katelovett@google.com> Co-authored-by: Valentin Vignal <32538273+ValentinVignal@users.noreply.github.com> Co-authored-by: Derek Xu <derekx@google.com> Co-authored-by: Yash Dhrangdhariya <72062416+Yash-Dhrangdhariya@users.noreply.github.com> Co-authored-by: bungeman <bungeman@chromium.org> Co-authored-by: Ahmed Mohamed Sameh <ahmedsameha1@gmail.com> Co-authored-by: John "codefu" McDole <codefu@google.com> Co-authored-by: Dmitry Grand <dmgr@google.com> Co-authored-by: Kostia Sokolovskyi <sokolovskyi.konstantin@gmail.com> Co-authored-by: Reid Baker <1063596+reidbaker@users.noreply.github.com> Co-authored-by: Matthew Kosarek <matt.kosarek@canonical.com> Co-authored-by: Jason Simmons <jason-simmons@users.noreply.github.com> Co-authored-by: Jim Graham <flar@google.com> Co-authored-by: Michael Goderbauer <goderbauer@google.com> Co-authored-by: Gray Mackall <34871572+gmackall@users.noreply.github.com> Co-authored-by: Gray Mackall <mackall@google.com> Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com> Co-authored-by: Jon Ihlas <jon.i@hotmail.fr> Co-authored-by: Micael Cid <micaelcid10@gmail.com> Co-authored-by: Alexander Aprelev <aam@google.com> Co-authored-by: hellohuanlin <41930132+hellohuanlin@users.noreply.github.com> Co-authored-by: Luke Memet <1598289+lukemmtt@users.noreply.github.com> Co-authored-by: Victoria Ashworth <15619084+vashworth@users.noreply.github.com> Co-authored-by: Mairramer <50643541+Mairramer@users.noreply.github.com> Co-authored-by: Florin Malita <fmalita@gmail.com> Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com> Co-authored-by: Salem Iranloye <127918074+salemiranloye@users.noreply.github.com> Co-authored-by: Kevin Moore <kevmoo@google.com> Co-authored-by: Sydney Bao <sydneybao@google.com> Co-authored-by: Wdestroier <Wdestroier@gmail.com> Co-authored-by: Matt Boetger <matt.boetger@gmail.com> Co-authored-by: Reid Baker <reidbaker@google.com> Co-authored-by: Victor Sanni <victorsanniay@gmail.com> Co-authored-by: Jessy Yameogo <jessy.yameogo@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: romain.gyh <11901536+romaingyh@users.noreply.github.com> Co-authored-by: Robert Ancell <robert.ancell@canonical.com> Co-authored-by: TheLastFlame <131446187+TheLastFlame@users.noreply.github.com> Co-authored-by: masato <returnymgstokh@icloud.com> Co-authored-by: Albin PK <56157868+albinpk@users.noreply.github.com> Co-authored-by: Huy <huy@nevercode.io> Co-authored-by: Matan Lurey <matanlurey@users.noreply.github.com> Co-authored-by: Azat Chorekliyev <azat24680@gmail.com> Co-authored-by: EdwynZN <edwinzn9@gmail.com> Co-authored-by: Bruno Leroux <bruno.leroux@gmail.com> Co-authored-by: Dev TtangKong <ttankkeo112@gmail.com> Co-authored-by: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Co-authored-by: Houssem Eddine Fadhli <houssemeddinefadhli81@gmail.com>
731 lines
24 KiB
Dart
731 lines
24 KiB
Dart
// 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 'dart:ui' as ui;
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
|
|
|
|
import 'rendering_tester.dart';
|
|
|
|
void main() {
|
|
TestRenderingFlutterBinding.ensureInitialized();
|
|
|
|
test('PipelineOwner dispatches memory events', () async {
|
|
await expectLater(
|
|
await memoryEvents(() => PipelineOwner().dispose(), PipelineOwner),
|
|
areCreateAndDispose,
|
|
);
|
|
});
|
|
|
|
test('nodesNeedingLayout updated with layout changes', () {
|
|
final _TestPipelineOwner owner = _TestPipelineOwner();
|
|
final TestRenderObject renderObject = TestRenderObject()..isRepaintBoundary = true;
|
|
renderObject.attach(owner);
|
|
expect(owner.needLayout, isEmpty);
|
|
|
|
renderObject.layout(const BoxConstraints.tightForFinite());
|
|
renderObject.markNeedsLayout();
|
|
expect(owner.needLayout, contains(renderObject));
|
|
|
|
owner.flushLayout();
|
|
expect(owner.needLayout, isEmpty);
|
|
});
|
|
|
|
test('nodesNeedingPaint updated with paint changes', () {
|
|
final _TestPipelineOwner owner = _TestPipelineOwner();
|
|
final TestRenderObject renderObject = TestRenderObject(allowPaintBounds: true)
|
|
..isRepaintBoundary = true;
|
|
final OffsetLayer layer = OffsetLayer();
|
|
layer.attach(owner);
|
|
renderObject.attach(owner);
|
|
expect(owner.needPaint, isEmpty);
|
|
|
|
renderObject.markNeedsPaint();
|
|
renderObject.scheduleInitialPaint(layer);
|
|
expect(owner.needPaint, contains(renderObject));
|
|
|
|
owner.flushPaint();
|
|
expect(owner.needPaint, isEmpty);
|
|
});
|
|
|
|
test('ensure frame is scheduled for markNeedsSemanticsUpdate', () {
|
|
// Initialize all bindings because owner.flushSemantics() requires a window
|
|
final TestRenderObject renderObject = TestRenderObject();
|
|
int onNeedVisualUpdateCallCount = 0;
|
|
final PipelineOwner owner = PipelineOwner(
|
|
onNeedVisualUpdate: () {
|
|
onNeedVisualUpdateCallCount += 1;
|
|
},
|
|
onSemanticsUpdate: (ui.SemanticsUpdate update) {},
|
|
);
|
|
owner.ensureSemantics();
|
|
owner.rootNode = renderObject;
|
|
renderObject.layout(
|
|
const BoxConstraints.tightForFinite(),
|
|
); // semantics are only calculated if layout information is up to date.
|
|
owner.flushSemantics();
|
|
|
|
expect(onNeedVisualUpdateCallCount, 1);
|
|
renderObject.markNeedsSemanticsUpdate();
|
|
expect(onNeedVisualUpdateCallCount, 2);
|
|
});
|
|
|
|
test('onSemanticsUpdate is called during flushSemantics.', () {
|
|
int onSemanticsUpdateCallCount = 0;
|
|
final PipelineOwner owner = PipelineOwner(
|
|
onSemanticsUpdate: (ui.SemanticsUpdate update) {
|
|
onSemanticsUpdateCallCount += 1;
|
|
},
|
|
);
|
|
owner.ensureSemantics();
|
|
|
|
expect(onSemanticsUpdateCallCount, 0);
|
|
|
|
final TestRenderObject renderObject = TestRenderObject();
|
|
owner.rootNode = renderObject;
|
|
renderObject.layout(const BoxConstraints.tightForFinite());
|
|
owner.flushSemantics();
|
|
|
|
expect(onSemanticsUpdateCallCount, 1);
|
|
});
|
|
|
|
test('Enabling semantics without configuring onSemanticsUpdate is invalid.', () {
|
|
final PipelineOwner pipelineOwner = PipelineOwner();
|
|
expect(() => pipelineOwner.ensureSemantics(), throwsAssertionError);
|
|
});
|
|
|
|
test('onSemanticsUpdate during sendSemanticsUpdate.', () {
|
|
int onSemanticsUpdateCallCount = 0;
|
|
final SemanticsOwner owner = SemanticsOwner(
|
|
onSemanticsUpdate: (ui.SemanticsUpdate update) {
|
|
onSemanticsUpdateCallCount += 1;
|
|
},
|
|
);
|
|
|
|
final SemanticsNode node = SemanticsNode.root(owner: owner);
|
|
node.rect = Rect.largest;
|
|
|
|
expect(onSemanticsUpdateCallCount, 0);
|
|
|
|
owner.sendSemanticsUpdate();
|
|
|
|
expect(onSemanticsUpdateCallCount, 1);
|
|
});
|
|
|
|
test('detached RenderObject does not do semantics', () {
|
|
final TestRenderObject renderObject = TestRenderObject();
|
|
expect(renderObject.attached, isFalse);
|
|
expect(renderObject.describeSemanticsConfigurationCallCount, 0);
|
|
|
|
renderObject.markNeedsSemanticsUpdate();
|
|
expect(renderObject.describeSemanticsConfigurationCallCount, 0);
|
|
});
|
|
|
|
test('ensure errors processing render objects are well formatted', () {
|
|
late FlutterErrorDetails errorDetails;
|
|
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
|
|
FlutterError.onError = (FlutterErrorDetails details) {
|
|
errorDetails = details;
|
|
};
|
|
final PipelineOwner owner = PipelineOwner();
|
|
final TestThrowingRenderObject renderObject = TestThrowingRenderObject();
|
|
try {
|
|
renderObject.attach(owner);
|
|
renderObject.layout(const BoxConstraints());
|
|
} finally {
|
|
FlutterError.onError = oldHandler;
|
|
}
|
|
|
|
expect(errorDetails, isNotNull);
|
|
expect(errorDetails.stack, isNotNull);
|
|
// Check the ErrorDetails without the stack trace
|
|
final List<String> lines = errorDetails.toString().split('\n');
|
|
// The lines in the middle of the error message contain the stack trace
|
|
// which will change depending on where the test is run.
|
|
expect(lines.length, greaterThan(8));
|
|
expect(
|
|
lines.take(4).join('\n'),
|
|
equalsIgnoringHashCodes(
|
|
'══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞══════════════════════\n'
|
|
'The following assertion was thrown during performLayout():\n'
|
|
'TestThrowingRenderObject does not support performLayout.\n',
|
|
),
|
|
);
|
|
|
|
expect(
|
|
lines.getRange(lines.length - 8, lines.length).join('\n'),
|
|
equalsIgnoringHashCodes(
|
|
'\n'
|
|
'The following RenderObject was being processed when the exception was fired:\n'
|
|
' TestThrowingRenderObject#00000 NEEDS-PAINT:\n'
|
|
' parentData: MISSING\n'
|
|
' constraints: BoxConstraints(unconstrained)\n'
|
|
'This RenderObject has no descendants.\n'
|
|
'═════════════════════════════════════════════════════════════════\n',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('ContainerParentDataMixin requires nulled out pointers to siblings before detach', () {
|
|
expect(() => TestParentData().detach(), isNot(throwsAssertionError));
|
|
|
|
final TestParentData data1 = TestParentData()
|
|
..nextSibling = RenderOpacity()
|
|
..previousSibling = RenderOpacity();
|
|
expect(() => data1.detach(), throwsAssertionError);
|
|
|
|
final TestParentData data2 = TestParentData()..previousSibling = RenderOpacity();
|
|
expect(() => data2.detach(), throwsAssertionError);
|
|
|
|
final TestParentData data3 = TestParentData()..nextSibling = RenderOpacity();
|
|
expect(() => data3.detach(), throwsAssertionError);
|
|
});
|
|
|
|
test('RenderObject.getTransformTo asserts if target not in the same render tree', () {
|
|
final PipelineOwner owner = PipelineOwner();
|
|
final TestRenderObject renderObject1 = TestRenderObject();
|
|
renderObject1.attach(owner);
|
|
final TestRenderObject renderObject2 = TestRenderObject();
|
|
renderObject2.attach(owner);
|
|
expect(() => renderObject1.getTransformTo(renderObject2), throwsAssertionError);
|
|
});
|
|
|
|
test('RenderObject.getTransformTo works for siblings and descendants', () {
|
|
final PipelineOwner owner = PipelineOwner();
|
|
final TestRenderObject renderObject1 = TestRenderObject()..attach(owner);
|
|
final TestRenderObject renderObject11 = TestRenderObject();
|
|
final TestRenderObject renderObject12 = TestRenderObject();
|
|
|
|
renderObject1
|
|
..add(renderObject11)
|
|
..add(renderObject12);
|
|
expect(renderObject11.getTransformTo(renderObject12), equals(Matrix4.identity()));
|
|
expect(renderObject1.getTransformTo(renderObject11), equals(Matrix4.identity()));
|
|
expect(renderObject1.getTransformTo(renderObject12), equals(Matrix4.identity()));
|
|
expect(renderObject11.getTransformTo(renderObject1), equals(Matrix4.identity()));
|
|
expect(renderObject12.getTransformTo(renderObject1), equals(Matrix4.identity()));
|
|
|
|
expect(renderObject1.getTransformTo(renderObject1), equals(Matrix4.identity()));
|
|
expect(renderObject11.getTransformTo(renderObject11), equals(Matrix4.identity()));
|
|
expect(renderObject12.getTransformTo(renderObject12), equals(Matrix4.identity()));
|
|
});
|
|
|
|
test('RenderObject.getTransformTo gets the correct paint transform', () {
|
|
final PipelineOwner owner = PipelineOwner();
|
|
final TestRenderObject renderObject0 = TestRenderObject()..attach(owner);
|
|
final TestRenderObject renderObject1 = TestRenderObject();
|
|
final TestRenderObject renderObject2 = TestRenderObject();
|
|
renderObject0
|
|
..add(renderObject1)
|
|
..add(renderObject2)
|
|
..paintTransform = Matrix4.diagonal3Values(9, 4, 1);
|
|
|
|
final TestRenderObject renderObject11 = TestRenderObject();
|
|
final TestRenderObject renderObject21 = TestRenderObject();
|
|
renderObject1
|
|
..add(renderObject11)
|
|
..paintTransform = Matrix4.translationValues(8, 16, 32);
|
|
renderObject2
|
|
..add(renderObject21)
|
|
..paintTransform = Matrix4.translationValues(32, 64, 128);
|
|
|
|
expect(
|
|
renderObject11.getTransformTo(renderObject21),
|
|
equals(Matrix4.translationValues((8 - 32) * 9, (16 - 64) * 4, 32 - 128)),
|
|
);
|
|
// Turn one of the paint transforms into a singular matrix and getTransformTo
|
|
// should return Matrix4.zero().
|
|
renderObject0.paintTransform = Matrix4(
|
|
1,
|
|
1,
|
|
1,
|
|
1,
|
|
2,
|
|
2,
|
|
2,
|
|
2,
|
|
3,
|
|
3,
|
|
3,
|
|
3,
|
|
4,
|
|
4,
|
|
4,
|
|
4,
|
|
); // Not a full rank matrix, so it has to be singular.
|
|
expect(renderObject11.getTransformTo(renderObject21), equals(Matrix4.zero()));
|
|
});
|
|
|
|
test('PaintingContext.pushClipRect reuses the layer', () {
|
|
_testPaintingContextLayerReuse<ClipRectLayer>((
|
|
PaintingContextCallback painter,
|
|
PaintingContext context,
|
|
Offset offset,
|
|
Layer? oldLayer,
|
|
) {
|
|
return context.pushClipRect(
|
|
true,
|
|
offset,
|
|
Rect.zero,
|
|
painter,
|
|
oldLayer: oldLayer as ClipRectLayer?,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('PaintingContext.pushClipRRect reuses the layer', () {
|
|
_testPaintingContextLayerReuse<ClipRRectLayer>((
|
|
PaintingContextCallback painter,
|
|
PaintingContext context,
|
|
Offset offset,
|
|
Layer? oldLayer,
|
|
) {
|
|
return context.pushClipRRect(
|
|
true,
|
|
offset,
|
|
Rect.zero,
|
|
RRect.fromRectAndRadius(Rect.zero, const Radius.circular(1.0)),
|
|
painter,
|
|
oldLayer: oldLayer as ClipRRectLayer?,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('PaintingContext.pushClipRSuperellipse reuses the layer', () {
|
|
_testPaintingContextLayerReuse<ClipRSuperellipseLayer>((
|
|
PaintingContextCallback painter,
|
|
PaintingContext context,
|
|
Offset offset,
|
|
Layer? oldLayer,
|
|
) {
|
|
return context.pushClipRSuperellipse(
|
|
true,
|
|
offset,
|
|
Rect.zero,
|
|
RSuperellipse.fromRectAndRadius(Rect.zero, const Radius.circular(1.0)),
|
|
painter,
|
|
oldLayer: oldLayer as ClipRSuperellipseLayer?,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('PaintingContext.pushClipPath reuses the layer', () {
|
|
_testPaintingContextLayerReuse<ClipPathLayer>((
|
|
PaintingContextCallback painter,
|
|
PaintingContext context,
|
|
Offset offset,
|
|
Layer? oldLayer,
|
|
) {
|
|
return context.pushClipPath(
|
|
true,
|
|
offset,
|
|
Rect.zero,
|
|
Path(),
|
|
painter,
|
|
oldLayer: oldLayer as ClipPathLayer?,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('PaintingContext.pushColorFilter reuses the layer', () {
|
|
_testPaintingContextLayerReuse<ColorFilterLayer>((
|
|
PaintingContextCallback painter,
|
|
PaintingContext context,
|
|
Offset offset,
|
|
Layer? oldLayer,
|
|
) {
|
|
return context.pushColorFilter(
|
|
offset,
|
|
const ColorFilter.mode(Color.fromRGBO(0, 0, 0, 1.0), BlendMode.clear),
|
|
painter,
|
|
oldLayer: oldLayer as ColorFilterLayer?,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('PaintingContext.pushTransform reuses the layer', () {
|
|
_testPaintingContextLayerReuse<TransformLayer>((
|
|
PaintingContextCallback painter,
|
|
PaintingContext context,
|
|
Offset offset,
|
|
Layer? oldLayer,
|
|
) {
|
|
return context.pushTransform(
|
|
true,
|
|
offset,
|
|
Matrix4.identity(),
|
|
painter,
|
|
oldLayer: oldLayer as TransformLayer?,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('PaintingContext.pushOpacity reuses the layer', () {
|
|
_testPaintingContextLayerReuse<OpacityLayer>((
|
|
PaintingContextCallback painter,
|
|
PaintingContext context,
|
|
Offset offset,
|
|
Layer? oldLayer,
|
|
) {
|
|
return context.pushOpacity(offset, 100, painter, oldLayer: oldLayer as OpacityLayer?);
|
|
});
|
|
});
|
|
|
|
test('RenderObject.dispose sets debugDisposed to true', () {
|
|
final TestRenderObject renderObject = TestRenderObject();
|
|
expect(renderObject.debugDisposed, false);
|
|
renderObject.dispose();
|
|
expect(renderObject.debugDisposed, true);
|
|
expect(renderObject.toStringShort(), contains('DISPOSED'));
|
|
});
|
|
|
|
test('Leader layer can switch to a different render object within one frame', () {
|
|
List<FlutterErrorDetails?>? caughtErrors;
|
|
TestRenderingFlutterBinding.instance.onErrors = () {
|
|
caughtErrors = TestRenderingFlutterBinding.instance.takeAllFlutterErrorDetails().toList();
|
|
};
|
|
|
|
final LayerLink layerLink = LayerLink();
|
|
// renderObject1 paints the leader layer first.
|
|
final LeaderLayerRenderObject renderObject1 = LeaderLayerRenderObject();
|
|
renderObject1.layerLink = layerLink;
|
|
renderObject1.attach(TestRenderingFlutterBinding.instance.pipelineOwner);
|
|
final OffsetLayer rootLayer1 = OffsetLayer();
|
|
rootLayer1.attach(renderObject1);
|
|
renderObject1.scheduleInitialPaint(rootLayer1);
|
|
renderObject1.layout(const BoxConstraints.tightForFinite());
|
|
|
|
final LeaderLayerRenderObject renderObject2 = LeaderLayerRenderObject();
|
|
final OffsetLayer rootLayer2 = OffsetLayer();
|
|
rootLayer2.attach(renderObject2);
|
|
renderObject2.attach(TestRenderingFlutterBinding.instance.pipelineOwner);
|
|
renderObject2.scheduleInitialPaint(rootLayer2);
|
|
renderObject2.layout(const BoxConstraints.tightForFinite());
|
|
TestRenderingFlutterBinding.instance.pumpCompleteFrame();
|
|
|
|
// Swap the layer link to renderObject2 in the same frame
|
|
renderObject1.layerLink = null;
|
|
renderObject1.markNeedsPaint();
|
|
renderObject2.layerLink = layerLink;
|
|
renderObject2.markNeedsPaint();
|
|
TestRenderingFlutterBinding.instance.pumpCompleteFrame();
|
|
|
|
// Swap the layer link to renderObject1 in the same frame
|
|
renderObject1.layerLink = layerLink;
|
|
renderObject1.markNeedsPaint();
|
|
renderObject2.layerLink = null;
|
|
renderObject2.markNeedsPaint();
|
|
TestRenderingFlutterBinding.instance.pumpCompleteFrame();
|
|
|
|
TestRenderingFlutterBinding.instance.onErrors = null;
|
|
expect(caughtErrors, isNull);
|
|
});
|
|
|
|
test('Leader layer append to two render objects does crash', () {
|
|
List<FlutterErrorDetails?>? caughtErrors;
|
|
TestRenderingFlutterBinding.instance.onErrors = () {
|
|
caughtErrors = TestRenderingFlutterBinding.instance.takeAllFlutterErrorDetails().toList();
|
|
};
|
|
final LayerLink layerLink = LayerLink();
|
|
// renderObject1 paints the leader layer first.
|
|
final LeaderLayerRenderObject renderObject1 = LeaderLayerRenderObject();
|
|
renderObject1.layerLink = layerLink;
|
|
renderObject1.attach(TestRenderingFlutterBinding.instance.pipelineOwner);
|
|
final OffsetLayer rootLayer1 = OffsetLayer();
|
|
rootLayer1.attach(renderObject1);
|
|
renderObject1.scheduleInitialPaint(rootLayer1);
|
|
renderObject1.layout(const BoxConstraints.tightForFinite());
|
|
|
|
final LeaderLayerRenderObject renderObject2 = LeaderLayerRenderObject();
|
|
renderObject2.layerLink = layerLink;
|
|
final OffsetLayer rootLayer2 = OffsetLayer();
|
|
rootLayer2.attach(renderObject2);
|
|
renderObject2.attach(TestRenderingFlutterBinding.instance.pipelineOwner);
|
|
renderObject2.scheduleInitialPaint(rootLayer2);
|
|
renderObject2.layout(const BoxConstraints.tightForFinite());
|
|
TestRenderingFlutterBinding.instance.pumpCompleteFrame();
|
|
|
|
TestRenderingFlutterBinding.instance.onErrors = null;
|
|
expect(caughtErrors!.isNotEmpty, isTrue);
|
|
});
|
|
|
|
test('RenderObject.dispose null the layer on repaint boundaries', () {
|
|
final TestRenderObject renderObject = TestRenderObject(allowPaintBounds: true);
|
|
// Force a layer to get set.
|
|
renderObject.isRepaintBoundary = true;
|
|
PaintingContext.repaintCompositedChild(renderObject, debugAlsoPaintedParent: true);
|
|
expect(renderObject.debugLayer, isA<OffsetLayer>());
|
|
|
|
// Dispose with repaint boundary still being true.
|
|
renderObject.dispose();
|
|
expect(renderObject.debugLayer, null);
|
|
});
|
|
|
|
test('RenderObject.dispose nulls the layer on non-repaint boundaries', () {
|
|
final TestRenderObject renderObject = TestRenderObject(allowPaintBounds: true);
|
|
// Force a layer to get set.
|
|
renderObject.isRepaintBoundary = true;
|
|
PaintingContext.repaintCompositedChild(renderObject, debugAlsoPaintedParent: true);
|
|
|
|
// Dispose with repaint boundary being false.
|
|
renderObject.isRepaintBoundary = false;
|
|
renderObject.dispose();
|
|
expect(renderObject.debugLayer, null);
|
|
});
|
|
|
|
test('Add composition callback works', () {
|
|
final ContainerLayer root = ContainerLayer();
|
|
final PaintingContext context = PaintingContext(root, Rect.zero);
|
|
bool calledBack = false;
|
|
final TestObservingRenderObject object = TestObservingRenderObject((Layer layer) {
|
|
expect(layer, root);
|
|
calledBack = true;
|
|
});
|
|
|
|
expect(calledBack, false);
|
|
object.paint(context, Offset.zero);
|
|
expect(calledBack, false);
|
|
|
|
root.buildScene(ui.SceneBuilder()).dispose();
|
|
expect(calledBack, true);
|
|
});
|
|
|
|
test('Change isRepaintBoundary after both markNeedsCompositedLayerUpdate and markNeedsPaint', () {
|
|
List<FlutterErrorDetails?>? caughtErrors;
|
|
TestRenderingFlutterBinding.instance.onErrors = () {
|
|
caughtErrors = TestRenderingFlutterBinding.instance.takeAllFlutterErrorDetails().toList();
|
|
};
|
|
final TestRenderObject object = TestRenderObject(allowPaintBounds: true);
|
|
object.isRepaintBoundary = true;
|
|
object.attach(TestRenderingFlutterBinding.instance.pipelineOwner);
|
|
object.layout(const BoxConstraints.tightForFinite());
|
|
PaintingContext.repaintCompositedChild(object, debugAlsoPaintedParent: true);
|
|
|
|
object.markNeedsCompositedLayerUpdate();
|
|
object.markNeedsPaint();
|
|
object.isRepaintBoundary = false;
|
|
object.markNeedsCompositingBitsUpdate();
|
|
TestRenderingFlutterBinding.instance.pumpCompleteFrame();
|
|
expect(caughtErrors, isNull);
|
|
});
|
|
|
|
test('ContainerParentDataMixin asserts parentData type', () {
|
|
final TestRenderObject renderObject = TestRenderObjectWithoutSetupParentData();
|
|
final TestRenderObject child = TestRenderObject();
|
|
expect(
|
|
() => renderObject.add(child),
|
|
throwsA(
|
|
isA<AssertionError>().having(
|
|
(AssertionError error) => error.toString(),
|
|
'description',
|
|
contains(
|
|
'A child of TestRenderObjectWithoutSetupParentData has parentData of type ParentData, '
|
|
'which does not conform to TestRenderObjectParentData. Class using ContainerRenderObjectMixin '
|
|
'should override setupParentData() to set parentData to type TestRenderObjectParentData.',
|
|
),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
test('PictureRecorder getter', () {
|
|
final PaintingContext context = PaintingContext(ContainerLayer(), Rect.zero);
|
|
expect(context.recorder.isRecording, isTrue);
|
|
});
|
|
}
|
|
|
|
class TestObservingRenderObject extends RenderBox {
|
|
TestObservingRenderObject(this.callback);
|
|
|
|
final CompositionCallback callback;
|
|
|
|
@override
|
|
bool get sizedByParent => true;
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
context.addCompositionCallback(callback);
|
|
}
|
|
}
|
|
|
|
// Tests the create-update cycle by pumping two frames. The first frame has no
|
|
// prior layer and forces the painting context to create a new one. The second
|
|
// frame reuses the layer painted on the first frame.
|
|
void _testPaintingContextLayerReuse<L extends Layer>(_LayerTestPaintCallback painter) {
|
|
final _TestCustomLayerBox box = _TestCustomLayerBox(painter);
|
|
layout(box, phase: EnginePhase.paint);
|
|
|
|
// Force a repaint. Otherwise, pumpFrame is a noop.
|
|
box.markNeedsPaint();
|
|
pumpFrame(phase: EnginePhase.paint);
|
|
expect(box.paintedLayers, hasLength(2));
|
|
expect(box.paintedLayers[0], isA<L>());
|
|
expect(box.paintedLayers[0], same(box.paintedLayers[1]));
|
|
}
|
|
|
|
typedef _LayerTestPaintCallback =
|
|
Layer? Function(
|
|
PaintingContextCallback painter,
|
|
PaintingContext context,
|
|
Offset offset,
|
|
Layer? oldLayer,
|
|
);
|
|
|
|
class _TestCustomLayerBox extends RenderBox {
|
|
_TestCustomLayerBox(this.painter);
|
|
|
|
final _LayerTestPaintCallback painter;
|
|
final List<Layer> paintedLayers = <Layer>[];
|
|
|
|
@override
|
|
bool get isRepaintBoundary => false;
|
|
|
|
@override
|
|
void performLayout() {
|
|
size = constraints.smallest;
|
|
}
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
final Layer paintedLayer = painter(super.paint, context, offset, layer)!;
|
|
paintedLayers.add(paintedLayer);
|
|
layer = paintedLayer as ContainerLayer;
|
|
}
|
|
}
|
|
|
|
class TestParentData extends ParentData with ContainerParentDataMixin<RenderBox> {}
|
|
|
|
class TestRenderObjectParentData extends ParentData
|
|
with ContainerParentDataMixin<TestRenderObject> {}
|
|
|
|
class TestRenderObject extends RenderObject
|
|
with ContainerRenderObjectMixin<TestRenderObject, TestRenderObjectParentData> {
|
|
TestRenderObject({this.allowPaintBounds = false});
|
|
|
|
final bool allowPaintBounds;
|
|
|
|
@override
|
|
bool isRepaintBoundary = false;
|
|
|
|
@override
|
|
void debugAssertDoesMeetConstraints() {}
|
|
|
|
@override
|
|
Rect get paintBounds {
|
|
assert(allowPaintBounds); // For some tests, this should not get called.
|
|
return Rect.zero;
|
|
}
|
|
|
|
Matrix4 paintTransform = Matrix4.identity();
|
|
@override
|
|
void applyPaintTransform(covariant RenderObject child, Matrix4 transform) {
|
|
super.applyPaintTransform(child, transform);
|
|
transform.multiply(paintTransform);
|
|
}
|
|
|
|
@override
|
|
void setupParentData(RenderObject child) {
|
|
if (child.parentData is! TestRenderObjectParentData) {
|
|
child.parentData = TestRenderObjectParentData();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void performLayout() {}
|
|
|
|
@override
|
|
void performResize() {}
|
|
|
|
@override
|
|
Rect get semanticBounds => const Rect.fromLTWH(0.0, 0.0, 10.0, 20.0);
|
|
|
|
int describeSemanticsConfigurationCallCount = 0;
|
|
|
|
@override
|
|
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
|
super.describeSemanticsConfiguration(config);
|
|
config.isSemanticBoundary = true;
|
|
describeSemanticsConfigurationCallCount++;
|
|
}
|
|
}
|
|
|
|
class TestRenderObjectWithoutSetupParentData extends TestRenderObject {
|
|
@override
|
|
void setupParentData(RenderObject child) {
|
|
// Use a mismatched parent data type.
|
|
if (child.parentData is! ParentData) {
|
|
child.parentData = ParentData();
|
|
}
|
|
}
|
|
}
|
|
|
|
class LeaderLayerRenderObject extends RenderObject {
|
|
LeaderLayerRenderObject();
|
|
|
|
LayerLink? layerLink;
|
|
|
|
@override
|
|
bool isRepaintBoundary = true;
|
|
|
|
@override
|
|
void debugAssertDoesMeetConstraints() {}
|
|
|
|
@override
|
|
Rect get paintBounds {
|
|
return Rect.zero;
|
|
}
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
if (layerLink != null) {
|
|
context.pushLayer(LeaderLayer(link: layerLink!), super.paint, offset);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void performLayout() {}
|
|
|
|
@override
|
|
void performResize() {}
|
|
|
|
@override
|
|
Rect get semanticBounds => const Rect.fromLTWH(0.0, 0.0, 10.0, 20.0);
|
|
}
|
|
|
|
class TestThrowingRenderObject extends RenderObject {
|
|
@override
|
|
void performLayout() {
|
|
throw FlutterError('TestThrowingRenderObject does not support performLayout.');
|
|
}
|
|
|
|
@override
|
|
void debugAssertDoesMeetConstraints() {}
|
|
|
|
@override
|
|
Rect get paintBounds {
|
|
assert(false); // The test shouldn't call this.
|
|
return Rect.zero;
|
|
}
|
|
|
|
@override
|
|
void performResize() {}
|
|
|
|
@override
|
|
Rect get semanticBounds {
|
|
assert(false); // The test shouldn't call this.
|
|
return Rect.zero;
|
|
}
|
|
}
|
|
|
|
final class _TestPipelineOwner extends PipelineOwner {
|
|
// Make these protected fields visible for testing.
|
|
Iterable<RenderObject> get needLayout => super.nodesNeedingLayout;
|
|
|
|
Iterable<RenderObject> get needPaint => super.nodesNeedingPaint;
|
|
}
|