Unverified Commit e17a1858 authored by chunhtai's avatar chunhtai Committed by GitHub

LayerLink can temporary allow multiple leaders (#95977)

parent db5c71f4
......@@ -7,6 +7,7 @@ import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/scheduler.dart';
import 'package:vector_math/vector_math_64.dart';
import 'debug.dart';
......@@ -2110,6 +2111,55 @@ class PhysicalModelLayer extends ContainerLayer {
class LayerLink {
LeaderLayer? _leader;
void _registerLeader(LeaderLayer leader) {
assert(_leader != leader);
assert((){
if (_leader != null) {
_debugPreviousLeaders ??= <LeaderLayer>{};
_debugPreviousLeaders!.add(_leader!);
_debugScheduleLeadersCleanUpCheck();
}
return true;
}());
_leader = leader;
}
void _unregisterLeader(LeaderLayer leader) {
assert(_leader != null);
if (_leader == leader) {
_leader = null;
} else {
assert((){
_debugPreviousLeaders!.remove(leader);
return true;
}());
}
}
/// Stores the previous leaders that were replaced by the current [_leader]
/// in the current frame.
///
/// These leaders need to give up their leaderships of this link by the end of
/// the current frame.
Set<LeaderLayer>? _debugPreviousLeaders;
bool _debugLeaderCheckScheduled = false;
/// Schedules the check as post frame callback to make sure the
/// [_debugPreviousLeaders] is empty.
void _debugScheduleLeadersCleanUpCheck() {
assert(_debugPreviousLeaders != null);
assert(() {
if (_debugLeaderCheckScheduled)
return true;
_debugLeaderCheckScheduled = true;
SchedulerBinding.instance!.addPostFrameCallback((Duration timeStamp) {
_debugLeaderCheckScheduled = false;
assert(_debugPreviousLeaders!.isEmpty);
});
return true;
}());
}
int _connectedFollowers = 0;
/// Whether a [LeaderLayer] is currently connected to this link.
......@@ -2202,7 +2252,10 @@ class LeaderLayer extends ContainerLayer {
if (_link == value) {
return;
}
_link._leader = null;
if (attached) {
_link._unregisterLeader(this);
value._registerLeader(this);
}
_link = value;
}
......@@ -2233,16 +2286,14 @@ class LeaderLayer extends ContainerLayer {
@override
void attach(Object owner) {
super.attach(owner);
assert(link._leader == null);
assert(_debugSetLastOffset(null));
link._leader = this;
_link._registerLeader(this);
}
@override
void detach() {
assert(link._leader == this);
link._leader = null;
assert(_debugSetLastOffset(null));
_link._unregisterLeader(this);
super.detach();
}
......
......@@ -164,6 +164,18 @@ void main() {
expect(followerLayer.debugSubtreeNeedsAddToScene, true);
});
test('switching layer link of an attached leader layer should not crash', () {
final LayerLink link = LayerLink();
final LeaderLayer leaderLayer = LeaderLayer(link: link);
final RenderView view = RenderView(configuration: const ViewConfiguration(), window: window);
leaderLayer.attach(view);
final LayerLink link2 = LayerLink();
leaderLayer.link = link2;
// This should not crash.
leaderLayer.detach();
expect(leaderLayer.link, link2);
});
test('leader layers are always dirty when connected to follower layer', () {
final ContainerLayer root = ContainerLayer()..attach(Object());
......
......@@ -153,6 +153,76 @@ void main() {
expect(renderObject.toStringShort(), contains('DISPOSED'));
});
test('Leader layer can switch to a different render object within one frame', () {
List<FlutterErrorDetails?>? caughtErrors;
renderer.onErrors = () {
caughtErrors = renderer.takeAllFlutterErrorDetails().toList();
};
final LayerLink layerLink = LayerLink();
// renderObject1 paints the leader layer first.
final LeaderLayerRenderObject renderObject1 = LeaderLayerRenderObject();
renderObject1.layerLink = layerLink;
renderObject1.attach(renderer.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(renderer.pipelineOwner);
renderObject2.scheduleInitialPaint(rootLayer2);
renderObject2.layout(const BoxConstraints.tightForFinite());
renderer.pumpCompleteFrame();
// Swap the layer link to renderObject2 in the same frame
renderObject1.layerLink = null;
renderObject1.markNeedsPaint();
renderObject2.layerLink = layerLink;
renderObject2.markNeedsPaint();
renderer.pumpCompleteFrame();
// Swap the layer link to renderObject1 in the same frame
renderObject1.layerLink = layerLink;
renderObject1.markNeedsPaint();
renderObject2.layerLink = null;
renderObject2.markNeedsPaint();
renderer.pumpCompleteFrame();
renderer.onErrors = null;
expect(caughtErrors, isNull);
});
test('Leader layer append to two render objects does crash', () {
List<FlutterErrorDetails?>? caughtErrors;
renderer.onErrors = () {
caughtErrors = renderer.takeAllFlutterErrorDetails().toList();
};
final LayerLink layerLink = LayerLink();
// renderObject1 paints the leader layer first.
final LeaderLayerRenderObject renderObject1 = LeaderLayerRenderObject();
renderObject1.layerLink = layerLink;
renderObject1.attach(renderer.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(renderer.pipelineOwner);
renderObject2.scheduleInitialPaint(rootLayer2);
renderObject2.layout(const BoxConstraints.tightForFinite());
renderer.pumpCompleteFrame();
renderer.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.
......@@ -255,6 +325,39 @@ class TestRenderObject extends RenderObject {
}
}
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() {
......
......@@ -82,6 +82,35 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser
EnginePhase phase = EnginePhase.composite;
/// Pumps a frame and runs its entire life cycle.
///
/// This method runs all of the [SchedulerPhase]s in a frame, this is useful
/// to test [SchedulerPhase.postFrameCallbacks].
void pumpCompleteFrame() {
final FlutterExceptionHandler? oldErrorHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
_errors.add(details);
};
try {
renderer.handleBeginFrame(null);
renderer.handleDrawFrame();
} finally {
FlutterError.onError = oldErrorHandler;
if (_errors.isNotEmpty) {
if (onErrors != null) {
onErrors!();
if (_errors.isNotEmpty) {
_errors.forEach(FlutterError.dumpErrorToConsole);
fail('There are more errors than the test inspected using TestRenderingFlutterBinding.takeFlutterErrorDetails.');
}
} else {
_errors.forEach(FlutterError.dumpErrorToConsole);
fail('Caught error while rendering frame. See preceding logs for details.');
}
}
}
}
@override
void drawFrame() {
assert(phase != EnginePhase.build, 'rendering_tester does not support testing the build phase; use flutter_test instead');
......
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