Unverified Commit 2df7244f authored by Dan Field's avatar Dan Field Committed by GitHub

Do not hold on to stale canvas reference in _RenderSegmentedButton (#136658)

Fixes https://github.com/flutter/flutter/issues/135747

Before this change, the render object was storing the `context.canvas` and using it after painting children. If those children were composited (e.g. because of an `Opacity` layer), the canvas it stored was disposed and not valid for use anymore.
parent c03dac9a
......@@ -654,7 +654,6 @@ class _RenderSegmentedButton<T> extends RenderBox with
@override
void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas;
final Rect borderRect = offset & size;
final Path borderClipPath = enabledBorder.getInnerPath(borderRect, textDirection: textDirection);
RenderBox? child = firstChild;
......@@ -663,14 +662,14 @@ class _RenderSegmentedButton<T> extends RenderBox with
Path? enabledClipPath;
Path? disabledClipPath;
canvas..save()..clipPath(borderClipPath);
context.canvas..save()..clipPath(borderClipPath);
while (child != null) {
final _SegmentedButtonContainerBoxParentData childParentData = child.parentData! as _SegmentedButtonContainerBoxParentData;
final Rect childRect = childParentData.surroundingRect!.outerRect.shift(offset);
canvas..save()..clipRect(childRect);
context.canvas..save()..clipRect(childRect);
context.paintChild(child, childParentData.offset + offset);
canvas.restore();
context.canvas.restore();
// Compute a clip rect for the outer border of the child.
late final double segmentLeft;
......@@ -705,14 +704,14 @@ class _RenderSegmentedButton<T> extends RenderBox with
: disabledBorder.side.copyWith(strokeAlign: 0.0);
final Offset top = Offset(dividerPos, childRect.top);
final Offset bottom = Offset(dividerPos, childRect.bottom);
canvas.drawLine(top, bottom, divider.toPaint());
context.canvas.drawLine(top, bottom, divider.toPaint());
}
previousChild = child;
child = childAfter(child);
index += 1;
}
canvas.restore();
context.canvas.restore();
// Paint the outer border for both disabled and enabled clip rect if needed.
if (disabledClipPath == null) {
......@@ -723,11 +722,11 @@ class _RenderSegmentedButton<T> extends RenderBox with
disabledBorder.paint(context.canvas, borderRect, textDirection: textDirection);
} else {
// Paint both of them clipped appropriately for the children segments.
canvas..save()..clipPath(enabledClipPath);
context.canvas..save()..clipPath(enabledClipPath);
enabledBorder.paint(context.canvas, borderRect, textDirection: textDirection);
canvas..restore()..save()..clipPath(disabledClipPath);
context.canvas..restore()..save()..clipPath(disabledClipPath);
disabledBorder.paint(context.canvas, borderRect, textDirection: textDirection);
canvas.restore();
context.canvas.restore();
}
}
......
......@@ -21,6 +21,37 @@ Widget boilerplate({required Widget child}) {
}
void main() {
testWidgetsWithLeakTracking('SegmentsButton when compositing does not crash', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/135747
// If the render object holds on to a stale canvas reference, this will
// throw an exception.
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SegmentedButton<int>(
segments: const <ButtonSegment<int>>[
ButtonSegment<int>(
value: 0,
label: Opacity(
opacity: 0.5,
child: Text('option'),
),
icon: Opacity(
opacity: 0.5,
child: Icon(Icons.add),
),
),
],
selected: const <int>{0},
),
),
),
);
expect(find.byType(SegmentedButton<int>), findsOneWidget);
expect(tester.takeException(), isNull);
});
testWidgetsWithLeakTracking('SegmentedButton releases state controllers for deleted segments', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
final Key key = UniqueKey();
......
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