Skip to content

Commit ca788d8

Browse files
authored
Leader not always dirty (flutter#92598)
1 parent 7f17708 commit ca788d8

File tree

5 files changed

+228
-19
lines changed

5 files changed

+228
-19
lines changed

packages/flutter/lib/src/rendering/editable.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2354,7 +2354,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
23542354
_clipRectLayer.layer = null;
23552355
_paintContents(context, offset);
23562356
}
2357-
_paintHandleLayers(context, getEndpointsForSelection(selection!));
2357+
if (selection!.isValid) {
2358+
_paintHandleLayers(context, getEndpointsForSelection(selection!));
2359+
}
23582360
}
23592361

23602362
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();

packages/flutter/lib/src/rendering/layer.dart

Lines changed: 98 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2104,14 +2104,41 @@ class PhysicalModelLayer extends ContainerLayer {
21042104
/// * [RenderLeaderLayer] and [RenderFollowerLayer], the corresponding
21052105
/// render objects.
21062106
class LayerLink {
2107-
/// The currently-registered [LeaderLayer], if any.
2108-
LeaderLayer? get leader => _leader;
21092107
LeaderLayer? _leader;
21102108

2111-
/// The total size of [leader]'s contents.
2109+
int _connectedFollowers = 0;
2110+
2111+
/// Whether a [LeaderLayer] is currently connected to this link.
2112+
bool get leaderConnected => _leader != null;
2113+
2114+
/// Called by the [FollowerLayer] to establish a link to a [LeaderLayer].
2115+
///
2116+
/// The returned [LayerLinkHandle] provides access to the leader via
2117+
/// [LayerLinkHandle.leader].
2118+
///
2119+
/// When the [FollowerLayer] no longer wants to follow the [LeaderLayer],
2120+
/// [LayerLinkHandle.dispose] must be called to disconnect the link.
2121+
_LayerLinkHandle _registerFollower() {
2122+
assert(_connectedFollowers >= 0);
2123+
_connectedFollowers++;
2124+
return _LayerLinkHandle(this);
2125+
}
2126+
2127+
/// Returns the [LeaderLayer] currently connected to this link.
2128+
///
2129+
/// Valid in debug mode only. Returns null in all other modes.
2130+
LeaderLayer? get debugLeader {
2131+
LeaderLayer? result;
2132+
if (kDebugMode) {
2133+
result = _leader;
2134+
}
2135+
return result;
2136+
}
2137+
2138+
/// The total size of the content of the connected [LeaderLayer].
21122139
///
21132140
/// Generally this should be set by the [RenderObject] that paints on the
2114-
/// registered [leader] layer (for instance a [RenderLeaderLayer] that shares
2141+
/// registered [LeaderLayer] (for instance a [RenderLeaderLayer] that shares
21152142
/// this link with its followers). This size may be outdated before and during
21162143
/// layout.
21172144
Size? leaderSize;
@@ -2120,6 +2147,30 @@ class LayerLink {
21202147
String toString() => '${describeIdentity(this)}(${ _leader != null ? "<linked>" : "<dangling>" })';
21212148
}
21222149

2150+
/// A handle provided by [LayerLink.registerFollower] to a calling
2151+
/// [FollowerLayer] to establish a link between that [FollowerLayer] and a
2152+
/// [LeaderLayer].
2153+
///
2154+
/// If the link is no longer needed, [dispose] must be called to disconnect it.
2155+
class _LayerLinkHandle {
2156+
_LayerLinkHandle(this._link);
2157+
2158+
LayerLink? _link;
2159+
2160+
/// The currently-registered [LeaderLayer], if any.
2161+
LeaderLayer? get leader => _link!._leader;
2162+
2163+
/// Disconnects the link between the [FollowerLayer] owning this handle and
2164+
/// the [leader].
2165+
///
2166+
/// The [LayerLinkHandle] becomes unusable after calling this method.
2167+
void dispose() {
2168+
assert(_link!._connectedFollowers > 0);
2169+
_link!._connectedFollowers--;
2170+
_link = null;
2171+
}
2172+
}
2173+
21232174
/// A composited layer that can be followed by a [FollowerLayer].
21242175
///
21252176
/// This layer collapses the accumulated offset into a transform and passes
@@ -2134,18 +2185,22 @@ class LeaderLayer extends ContainerLayer {
21342185
///
21352186
/// The [offset] property must be non-null before the compositing phase of the
21362187
/// pipeline.
2137-
LeaderLayer({ required LayerLink link, this.offset = Offset.zero }) : assert(link != null), _link = link;
2188+
LeaderLayer({ required LayerLink link, Offset offset = Offset.zero }) : assert(link != null), _link = link, _offset = offset;
21382189

21392190
/// The object with which this layer should register.
21402191
///
21412192
/// The link will be established when this layer is [attach]ed, and will be
21422193
/// cleared when this layer is [detach]ed.
21432194
LayerLink get link => _link;
2195+
LayerLink _link;
21442196
set link(LayerLink value) {
21452197
assert(value != null);
2198+
if (_link == value) {
2199+
return;
2200+
}
2201+
_link._leader = null;
21462202
_link = value;
21472203
}
2148-
LayerLink _link;
21492204

21502205
/// Offset from parent in the parent's coordinate system.
21512206
///
@@ -2154,23 +2209,34 @@ class LeaderLayer extends ContainerLayer {
21542209
///
21552210
/// The [offset] property must be non-null before the compositing phase of the
21562211
/// pipeline.
2157-
Offset offset;
2212+
Offset get offset => _offset;
2213+
Offset _offset;
2214+
set offset(Offset value) {
2215+
assert(value != null);
2216+
if (value == _offset) {
2217+
return;
2218+
}
2219+
_offset = value;
2220+
if (!alwaysNeedsAddToScene) {
2221+
markNeedsAddToScene();
2222+
}
2223+
}
21582224

21592225
/// {@macro flutter.rendering.FollowerLayer.alwaysNeedsAddToScene}
21602226
@override
2161-
bool get alwaysNeedsAddToScene => true;
2227+
bool get alwaysNeedsAddToScene => _link._connectedFollowers > 0;
21622228

21632229
@override
21642230
void attach(Object owner) {
21652231
super.attach(owner);
2166-
assert(link.leader == null);
2232+
assert(link._leader == null);
21672233
_lastOffset = null;
21682234
link._leader = this;
21692235
}
21702236

21712237
@override
21722238
void detach() {
2173-
assert(link.leader == this);
2239+
assert(link._leader == this);
21742240
link._leader = null;
21752241
_lastOffset = null;
21762242
super.detach();
@@ -2256,6 +2322,10 @@ class FollowerLayer extends ContainerLayer {
22562322
LayerLink get link => _link;
22572323
set link(LayerLink value) {
22582324
assert(value != null);
2325+
if (value != _link && _leaderHandle != null) {
2326+
_leaderHandle!.dispose();
2327+
_leaderHandle = value._registerFollower();
2328+
}
22592329
_link = value;
22602330
}
22612331
LayerLink _link;
@@ -2302,6 +2372,21 @@ class FollowerLayer extends ContainerLayer {
23022372
/// * [unlinkedOffset], for when the layer is not linked.
23032373
Offset? linkedOffset;
23042374

2375+
_LayerLinkHandle? _leaderHandle;
2376+
2377+
@override
2378+
void attach(Object owner) {
2379+
super.attach(owner);
2380+
_leaderHandle = _link._registerFollower();
2381+
}
2382+
2383+
@override
2384+
void detach() {
2385+
super.detach();
2386+
_leaderHandle?.dispose();
2387+
_leaderHandle = null;
2388+
}
2389+
23052390
Offset? _lastOffset;
23062391
Matrix4? _lastTransform;
23072392
Matrix4? _invertedTransform;
@@ -2321,7 +2406,7 @@ class FollowerLayer extends ContainerLayer {
23212406

23222407
@override
23232408
bool findAnnotations<S extends Object>(AnnotationResult<S> result, Offset localPosition, { required bool onlyFirst }) {
2324-
if (link.leader == null) {
2409+
if (_leaderHandle!.leader == null) {
23252410
if (showWhenUnlinked!) {
23262411
return super.findAnnotations(result, localPosition - unlinkedOffset!, onlyFirst: onlyFirst);
23272412
}
@@ -2400,7 +2485,7 @@ class FollowerLayer extends ContainerLayer {
24002485
void _establishTransform() {
24012486
assert(link != null);
24022487
_lastTransform = null;
2403-
final LeaderLayer? leader = link.leader;
2488+
final LeaderLayer? leader = _leaderHandle!.leader;
24042489
// Check to see if we are linked.
24052490
if (leader == null)
24062491
return;
@@ -2462,7 +2547,7 @@ class FollowerLayer extends ContainerLayer {
24622547
void addToScene(ui.SceneBuilder builder) {
24632548
assert(link != null);
24642549
assert(showWhenUnlinked != null);
2465-
if (link.leader == null && !showWhenUnlinked!) {
2550+
if (_leaderHandle!.leader == null && !showWhenUnlinked!) {
24662551
_lastTransform = null;
24672552
_lastOffset = null;
24682553
_inverseDirty = true;

packages/flutter/lib/src/rendering/proxy_box.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5287,7 +5287,7 @@ class RenderFollowerLayer extends RenderProxyBox {
52875287
@override
52885288
bool hitTest(BoxHitTestResult result, { required Offset position }) {
52895289
// Disables the hit testing if this render object is hidden.
5290-
if (link.leader == null && !showWhenUnlinked)
5290+
if (!link.leaderConnected && !showWhenUnlinked)
52915291
return false;
52925292
// RenderFollowerLayer objects don't check if they are
52935293
// themselves hit, because it's confusing to think about
@@ -5311,8 +5311,8 @@ class RenderFollowerLayer extends RenderProxyBox {
53115311
void paint(PaintingContext context, Offset offset) {
53125312
final Size? leaderSize = link.leaderSize;
53135313
assert(
5314-
link.leaderSize != null || (link.leader == null || leaderAnchor == Alignment.topLeft),
5315-
'$link: layer is linked to ${link.leader} but a valid leaderSize is not set. '
5314+
link.leaderSize != null || (!link.leaderConnected || leaderAnchor == Alignment.topLeft),
5315+
'$link: layer is linked to ${link.debugLeader} but a valid leaderSize is not set. '
53165316
'leaderSize is required when leaderAnchor is not Alignment.topLeft '
53175317
'(current value is $leaderAnchor).',
53185318
);

packages/flutter/test/material/text_field_test.dart

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9967,4 +9967,73 @@ void main() {
99679967
containsPair('hintText', 'placeholder text'),
99689968
);
99699969
});
9970+
9971+
testWidgets('TextField at rest does not push any layers with alwaysNeedsAddToScene', (WidgetTester tester) async {
9972+
await tester.pumpWidget(
9973+
const MaterialApp(
9974+
home: Material(
9975+
child: Center(
9976+
child: TextField(),
9977+
),
9978+
),
9979+
),
9980+
);
9981+
9982+
expect(tester.layers.any((Layer layer) => layer.debugSubtreeNeedsAddToScene!), isFalse);
9983+
});
9984+
9985+
testWidgets('Focused TextField does not push any layers with alwaysNeedsAddToScene', (WidgetTester tester) async {
9986+
final FocusNode focusNode = FocusNode();
9987+
await tester.pumpWidget(
9988+
MaterialApp(
9989+
home: Material(
9990+
child: Center(
9991+
child: TextField(focusNode: focusNode),
9992+
),
9993+
),
9994+
),
9995+
);
9996+
await tester.showKeyboard(find.byType(TextField));
9997+
9998+
expect(focusNode.hasFocus, isTrue);
9999+
expect(tester.layers.any((Layer layer) => layer.debugSubtreeNeedsAddToScene!), isFalse);
10000+
});
10001+
10002+
testWidgets('TextField does not push any layers with alwaysNeedsAddToScene after toolbar is dismissed', (WidgetTester tester) async {
10003+
final FocusNode focusNode = FocusNode();
10004+
await tester.pumpWidget(
10005+
MaterialApp(
10006+
home: Material(
10007+
child: Center(
10008+
child: TextField(focusNode: focusNode),
10009+
),
10010+
),
10011+
),
10012+
);
10013+
10014+
await tester.showKeyboard(find.byType(TextField));
10015+
10016+
// Bring up the toolbar.
10017+
const String testValue = 'A B C';
10018+
tester.testTextInput.updateEditingValue(
10019+
const TextEditingValue(
10020+
text: testValue,
10021+
),
10022+
);
10023+
await tester.pump();
10024+
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
10025+
state.renderEditable.selectWordsInRange(from: Offset.zero, cause: SelectionChangedCause.tap);
10026+
expect(state.showToolbar(), true);
10027+
await tester.pumpAndSettle();
10028+
await tester.pump(const Duration(seconds: 1));
10029+
expect(find.text('Copy'), findsOneWidget); // Toolbar is visible
10030+
10031+
// Hide the toolbar
10032+
focusNode.unfocus();
10033+
await tester.pumpAndSettle();
10034+
await tester.pump(const Duration(seconds: 1));
10035+
expect(find.text('Copy'), findsNothing); // Toolbar is not visible
10036+
10037+
expect(tester.layers.any((Layer layer) => layer.debugSubtreeNeedsAddToScene!), isFalse);
10038+
}, skip: isContextMenuProvidedByPlatform); // [intended] only applies to platforms where we supply the context menu.
997010039
}

packages/flutter/test/rendering/layers_test.dart

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,18 +153,71 @@ void main() {
153153
}
154154
});
155155

156-
test('leader and follower layers are always dirty', () {
156+
test('follower layers are always dirty', () {
157157
final LayerLink link = LayerLink();
158158
final LeaderLayer leaderLayer = LeaderLayer(link: link);
159159
final FollowerLayer followerLayer = FollowerLayer(link: link);
160160
leaderLayer.debugMarkClean();
161161
followerLayer.debugMarkClean();
162162
leaderLayer.updateSubtreeNeedsAddToScene();
163163
followerLayer.updateSubtreeNeedsAddToScene();
164-
expect(leaderLayer.debugSubtreeNeedsAddToScene, true);
165164
expect(followerLayer.debugSubtreeNeedsAddToScene, true);
166165
});
167166

167+
test('leader layers are always dirty when connected to follower layer', () {
168+
final ContainerLayer root = ContainerLayer()..attach(Object());
169+
170+
final LayerLink link = LayerLink();
171+
final LeaderLayer leaderLayer = LeaderLayer(link: link);
172+
final FollowerLayer followerLayer = FollowerLayer(link: link);
173+
174+
root.append(leaderLayer);
175+
root.append(followerLayer);
176+
177+
leaderLayer.debugMarkClean();
178+
followerLayer.debugMarkClean();
179+
leaderLayer.updateSubtreeNeedsAddToScene();
180+
followerLayer.updateSubtreeNeedsAddToScene();
181+
expect(leaderLayer.debugSubtreeNeedsAddToScene, true);
182+
});
183+
184+
test('leader layers are not dirty when all followers disconnects', () {
185+
final ContainerLayer root = ContainerLayer()..attach(Object());
186+
final LayerLink link = LayerLink();
187+
final LeaderLayer leaderLayer = LeaderLayer(link: link);
188+
root.append(leaderLayer);
189+
190+
// Does not need add to scene when nothing is connected to link.
191+
leaderLayer.debugMarkClean();
192+
leaderLayer.updateSubtreeNeedsAddToScene();
193+
expect(leaderLayer.debugSubtreeNeedsAddToScene, false);
194+
195+
// Connecting a follower requires adding to scene.
196+
final FollowerLayer follower1 = FollowerLayer(link: link);
197+
root.append(follower1);
198+
leaderLayer.debugMarkClean();
199+
leaderLayer.updateSubtreeNeedsAddToScene();
200+
expect(leaderLayer.debugSubtreeNeedsAddToScene, true);
201+
202+
final FollowerLayer follower2 = FollowerLayer(link: link);
203+
root.append(follower2);
204+
leaderLayer.debugMarkClean();
205+
leaderLayer.updateSubtreeNeedsAddToScene();
206+
expect(leaderLayer.debugSubtreeNeedsAddToScene, true);
207+
208+
// Disconnecting one follower, still needs add to scene.
209+
follower2.remove();
210+
leaderLayer.debugMarkClean();
211+
leaderLayer.updateSubtreeNeedsAddToScene();
212+
expect(leaderLayer.debugSubtreeNeedsAddToScene, true);
213+
214+
// Disconnecting all followers goes back to not requiring add to scene.
215+
follower1.remove();
216+
leaderLayer.debugMarkClean();
217+
leaderLayer.updateSubtreeNeedsAddToScene();
218+
expect(leaderLayer.debugSubtreeNeedsAddToScene, false);
219+
});
220+
168221
test('depthFirstIterateChildren', () {
169222
final ContainerLayer a = ContainerLayer();
170223
final ContainerLayer b = ContainerLayer();

0 commit comments

Comments
 (0)