Unverified Commit ee8488e8 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Remove warnings when `UnconstrainedBox` and `ConstraintsTransformBox` are clipped (#110393)

parent 45570b13
......@@ -656,9 +656,9 @@ class RenderConstrainedOverflowBox extends RenderAligningShiftedBox {
}
}
/// A [RenderBox] that applies an arbitrary transform to its [constraints]
/// before sizing its child using the new constraints, treating any overflow as
/// error.
/// A [RenderBox] that applies an arbitrary transform to its constraints,
/// and sizes its child using the resulting [BoxConstraints], optionally
/// clipping, or treating the overflow as an error.
///
/// This [RenderBox] sizes its child using a [BoxConstraints] created by
/// applying [constraintsTransform] to this [RenderBox]'s own [constraints].
......@@ -668,9 +668,9 @@ class RenderConstrainedOverflowBox extends RenderAligningShiftedBox {
/// the entire child, the child will be clipped if [clipBehavior] is not
/// [Clip.none].
///
/// In debug mode, if the child overflows the box, a warning will be printed on
/// the console, and black and yellow striped areas will appear where the
/// overflow occurs.
/// In debug mode, if [clipBehavior] is [Clip.none] and the child overflows the
/// container, a warning will be printed on the console, and black and yellow
/// striped areas will appear where the overflow occurs.
///
/// When [child] is null, this [RenderBox] takes the smallest possible size and
/// never overflows.
......@@ -732,7 +732,9 @@ class RenderConstraintsTransformBox extends RenderAligningShiftedBox with DebugO
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.none], and must not be null.
/// {@macro flutter.widgets.ConstraintsTransformBox.clipBehavior}
///
/// Defaults to [Clip.none].
Clip get clipBehavior => _clipBehavior;
Clip _clipBehavior;
set clipBehavior(Clip value) {
......@@ -830,9 +832,17 @@ class RenderConstraintsTransformBox extends RenderAligningShiftedBox with DebugO
oldLayer: _clipRectLayer.layer,
);
// Display the overflow indicator.
// Display the overflow indicator if clipBehavior is Clip.none.
assert(() {
switch (clipBehavior) {
case Clip.none:
paintOverflowIndicator(context, offset, _overflowContainerRect, _overflowChildRect);
break;
case Clip.hardEdge:
case Clip.antiAlias:
case Clip.antiAliasWithSaveLayer:
break;
}
return true;
}());
}
......
......@@ -2513,8 +2513,8 @@ class ConstrainedBox extends SingleChildRenderObjectWidget {
}
/// A container widget that applies an arbitrary transform to its constraints,
/// and sizes its child using the resulting [BoxConstraints], treating any
/// overflow as error.
/// and sizes its child using the resulting [BoxConstraints], optionally
/// clipping, or treating the overflow as an error.
///
/// This container sizes its child using a [BoxConstraints] created by applying
/// [constraintsTransform] to its own constraints. This container will then
......@@ -2523,12 +2523,12 @@ class ConstrainedBox extends SingleChildRenderObjectWidget {
/// [alignment]. If the container cannot expand enough to accommodate the entire
/// child, the child will be clipped if [clipBehavior] is not [Clip.none].
///
/// In debug mode, if the child overflows the container, a warning will be
/// printed on the console, and black and yellow striped areas will appear where
/// the overflow occurs.
/// In debug mode, if [clipBehavior] is [Clip.none] and the child overflows the
/// container, a warning will be printed on the console, and black and yellow
/// striped areas will appear where the overflow occurs.
///
/// When [child] is null, this widget becomes as small as possible and never
/// overflows
/// overflows.
///
/// This widget can be used to ensure some of [child]'s natural dimensions are
/// honored, and get an early warning otherwise during development. For
......@@ -2564,7 +2564,7 @@ class ConstrainedBox extends SingleChildRenderObjectWidget {
/// * [OverflowBox], a widget that imposes additional constraints on its child,
/// and allows the child to overflow itself.
/// * [UnconstrainedBox] which allows its children to render themselves
/// unconstrained, expands to fit them, and considers overflow to be an error.
/// unconstrained and expands to fit them.
class ConstraintsTransformBox extends SingleChildRenderObjectWidget {
/// Creates a widget that uses a function to transform the constraints it
/// passes to its child. If the child overflows the parent's constraints, a
......@@ -2686,6 +2686,13 @@ class ConstraintsTransformBox extends SingleChildRenderObjectWidget {
/// {@macro flutter.material.Material.clipBehavior}
///
/// {@template flutter.widgets.ConstraintsTransformBox.clipBehavior}
/// In debug mode, if `clipBehavior` is [Clip.none], and the child overflows
/// its constraints, a warning will be printed on the console, and black and
/// yellow striped areas will appear where the overflow occurs. For other
/// values of `clipBehavior`, the contents are clipped accordingly.
/// {@endtemplate}
///
/// Defaults to [Clip.none].
final Clip clipBehavior;
......
......@@ -410,16 +410,65 @@ void main() {
expect(firstErrorDetails?.toString(), contains('is not normalized'));
});
test('overflow is reported when insufficient size is given', () {
final RenderConstrainedBox child = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: double.maxFinite));
final RenderConstraintsTransformBox box = RenderConstraintsTransformBox(
test('overflow is reported when insufficient size is given and clipBehavior is Clip.none', () {
bool hadErrors = false;
void expectOverflowedErrors() {
absorbOverflowedErrors();
hadErrors = true;
}
final TestClipPaintingContext context = TestClipPaintingContext();
for (final Clip? clip in <Clip?>[null, ...Clip.values]) {
final RenderConstraintsTransformBox box;
switch (clip) {
case Clip.none:
case Clip.hardEdge:
case Clip.antiAlias:
case Clip.antiAliasWithSaveLayer:
box = RenderConstraintsTransformBox(
alignment: Alignment.center,
textDirection: TextDirection.ltr,
constraintsTransform: (BoxConstraints constraints) => constraints.copyWith(maxWidth: double.infinity),
child: child,
clipBehavior: clip!,
child: RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(
width: double.maxFinite,
height: double.maxFinite,
),
),
);
break;
case null:
box = RenderConstraintsTransformBox(
alignment: Alignment.center,
textDirection: TextDirection.ltr,
constraintsTransform: (BoxConstraints constraints) => constraints.copyWith(maxWidth: double.infinity),
child: RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(
width: double.maxFinite,
height: double.maxFinite,
),
),
);
break;
}
layout(box, constraints: const BoxConstraints(), phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
context.paintChild(box, Offset.zero);
// By default, clipBehavior should be Clip.none
expect(context.clipBehavior, equals(clip ?? Clip.none));
switch (clip) {
case null:
case Clip.none:
expect(hadErrors, isTrue, reason: 'Should have had overflow errors for $clip');
break;
case Clip.hardEdge:
case Clip.antiAlias:
case Clip.antiAliasWithSaveLayer:
expect(hadErrors, isFalse, reason: 'Should not have had overflow errors for $clip');
break;
}
hadErrors = false;
}
});
test('handles flow layout', () {
......@@ -642,26 +691,50 @@ void main() {
const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 100.0);
final TestClipPaintingContext context = TestClipPaintingContext();
// By default, clipBehavior should be Clip.none
final RenderUnconstrainedBox defaultBox = RenderUnconstrainedBox(
bool hadErrors = false;
void expectOverflowedErrors() {
absorbOverflowedErrors();
hadErrors = true;
}
for (final Clip? clip in <Clip?>[null, ...Clip.values]) {
final RenderUnconstrainedBox box;
switch (clip) {
case Clip.none:
case Clip.hardEdge:
case Clip.antiAlias:
case Clip.antiAliasWithSaveLayer:
box = RenderUnconstrainedBox(
alignment: Alignment.center,
textDirection: TextDirection.ltr,
child: box200x200,
clipBehavior: clip!,
);
layout(defaultBox, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
context.paintChild(defaultBox, Offset.zero);
expect(context.clipBehavior, equals(Clip.none));
for (final Clip clip in Clip.values) {
final RenderUnconstrainedBox box = RenderUnconstrainedBox(
break;
case null:
box = RenderUnconstrainedBox(
alignment: Alignment.center,
textDirection: TextDirection.ltr,
child: box200x200,
clipBehavior: clip,
);
break;
}
layout(box, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
switch (clip) {
case null:
case Clip.none:
expect(hadErrors, isTrue, reason: 'Should have had overflow errors for $clip');
break;
case Clip.hardEdge:
case Clip.antiAlias:
case Clip.antiAliasWithSaveLayer:
expect(hadErrors, isFalse, reason: 'Should not have had overflow errors for $clip');
break;
}
hadErrors = false;
context.paintChild(box, Offset.zero);
expect(context.clipBehavior, equals(clip));
// By default, clipBehavior should be Clip.none
expect(context.clipBehavior, equals(clip ?? Clip.none), reason: 'for $clip');
}
});
......
......@@ -54,13 +54,18 @@ void main() {
TestRenderingFlutterBinding.ensureInitialized();
test('RenderEditable respects clipBehavior', () {
const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 100.0);
final TestClipPaintingContext context = TestClipPaintingContext();
const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 1.0);
final String longString = 'a' * 10000;
// By default, clipBehavior should be Clip.none
final RenderEditable defaultEditable = RenderEditable(
for (final Clip? clip in <Clip?>[null, ...Clip.values]) {
final TestClipPaintingContext context = TestClipPaintingContext();
final RenderEditable editable;
switch (clip) {
case Clip.none:
case Clip.hardEdge:
case Clip.antiAlias:
case Clip.antiAliasWithSaveLayer:
editable = RenderEditable(
text: TextSpan(text: longString),
textDirection: TextDirection.ltr,
startHandleLayerLink: LayerLink(),
......@@ -68,14 +73,11 @@ void main() {
offset: ViewportOffset.zero(),
textSelectionDelegate: _FakeEditableTextState(),
selection: const TextSelection(baseOffset: 0, extentOffset: 0),
clipBehavior: clip!,
);
layout(defaultEditable, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
context.paintChild(defaultEditable, Offset.zero);
expect(context.clipBehavior, equals(Clip.hardEdge));
context.clipBehavior = Clip.none; // Reset as Clip.none won't write into clipBehavior.
for (final Clip clip in Clip.values) {
final RenderEditable editable = RenderEditable(
break;
case null:
editable = RenderEditable(
text: TextSpan(text: longString),
textDirection: TextDirection.ltr,
startHandleLayerLink: LayerLink(),
......@@ -83,11 +85,13 @@ void main() {
offset: ViewportOffset.zero(),
textSelectionDelegate: _FakeEditableTextState(),
selection: const TextSelection(baseOffset: 0, extentOffset: 0),
clipBehavior: clip,
);
layout(editable, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
break;
}
layout(editable, constraints: viewport, phase: EnginePhase.composite, onErrors: expectNoFlutterErrors);
context.paintChild(editable, Offset.zero);
expect(context.clipBehavior, equals(clip));
// By default, clipBehavior is Clip.hardEdge.
expect(context.clipBehavior, equals(clip ?? Clip.hardEdge), reason: 'for $clip');
}
});
......
......@@ -60,18 +60,30 @@ void main() {
test('Clip behavior is respected', () {
const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 100.0);
final TestClipPaintingContext context = TestClipPaintingContext();
// By default, clipBehavior should be Clip.none
final RenderFlex defaultFlex = RenderFlex(direction: Axis.vertical, children: <RenderBox>[box200x200]);
layout(defaultFlex, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
context.paintChild(defaultFlex, Offset.zero);
expect(context.clipBehavior, equals(Clip.none));
for (final Clip clip in Clip.values) {
final RenderFlex flex = RenderFlex(direction: Axis.vertical, children: <RenderBox>[box200x200], clipBehavior: clip);
layout(flex, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
bool hadErrors = false;
for (final Clip? clip in <Clip?>[null, ...Clip.values]) {
final RenderFlex flex;
switch (clip) {
case Clip.none:
case Clip.hardEdge:
case Clip.antiAlias:
case Clip.antiAliasWithSaveLayer:
flex = RenderFlex(direction: Axis.vertical, children: <RenderBox>[box200x200], clipBehavior: clip!);
break;
case null:
flex = RenderFlex(direction: Axis.vertical, children: <RenderBox>[box200x200]);
break;
}
layout(flex, constraints: viewport, phase: EnginePhase.composite, onErrors: () {
absorbOverflowedErrors();
hadErrors = true;
});
context.paintChild(flex, Offset.zero);
expect(context.clipBehavior, equals(clip));
// By default, clipBehavior should be Clip.none
expect(context.clipBehavior, equals(clip ?? Clip.none));
expect(hadErrors, isTrue);
hadErrors = false;
}
});
......
......@@ -536,19 +536,24 @@ void main() {
test('RenderFittedBox respects clipBehavior', () {
const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 100.0);
for (final Clip? clip in <Clip?>[null, ...Clip.values]) {
final TestClipPaintingContext context = TestClipPaintingContext();
// By default, clipBehavior should be Clip.none
final RenderFittedBox defaultBox = RenderFittedBox(child: box200x200, fit: BoxFit.none);
layout(defaultBox, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
defaultBox.paint(context, Offset.zero);
expect(context.clipBehavior, equals(Clip.none));
for (final Clip clip in Clip.values) {
final RenderFittedBox box = RenderFittedBox(child: box200x200, fit: BoxFit.none, clipBehavior: clip);
layout(box, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
final RenderFittedBox box;
switch (clip) {
case Clip.none:
case Clip.hardEdge:
case Clip.antiAlias:
case Clip.antiAliasWithSaveLayer:
box = RenderFittedBox(child: box200x200, fit: BoxFit.none, clipBehavior: clip!);
break;
case null:
box = RenderFittedBox(child: box200x200, fit: BoxFit.none);
break;
}
layout(box, constraints: viewport, phase: EnginePhase.composite, onErrors: expectNoFlutterErrors);
box.paint(context, Offset.zero);
expect(context.clipBehavior, equals(clip));
// By default, clipBehavior should be Clip.none
expect(context.clipBehavior, equals(clip ?? Clip.none));
}
});
......
......@@ -382,13 +382,22 @@ class TestPushLayerPaintingContext extends PaintingContext {
}
}
void expectOverflowedErrors() {
final FlutterErrorDetails errorDetails = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails()!;
final bool overflowed = errorDetails.toString().contains('overflowed');
if (!overflowed) {
FlutterError.reportError(errorDetails);
// Absorbs errors that don't have "overflowed" in their error details.
void absorbOverflowedErrors() {
final Iterable<FlutterErrorDetails> errorDetails = TestRenderingFlutterBinding.instance.takeAllFlutterErrorDetails();
final Iterable<FlutterErrorDetails> filtered = errorDetails.where((FlutterErrorDetails details) {
return !details.toString().contains('overflowed');
});
if (filtered.isNotEmpty) {
filtered.forEach(FlutterError.reportError);
}
}
// Reports any FlutterErrors.
void expectNoFlutterErrors() {
final Iterable<FlutterErrorDetails> errorDetails = TestRenderingFlutterBinding.instance.takeAllFlutterErrorDetails();
errorDetails.forEach(FlutterError.reportError);
}
RenderConstrainedBox get box200x200 =>
RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(height: 200.0, width: 200.0));
......@@ -63,30 +63,39 @@ void main() {
expect(stack.size.height, equals(100.0));
});
test('Stack respects clipBehavior', () {
test('Stack has correct clipBehavior', () {
const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 100.0);
final TestClipPaintingContext context = TestClipPaintingContext();
// By default, clipBehavior should be Clip.none
final RenderStack defaultStack = RenderStack(textDirection: TextDirection.ltr, children: <RenderBox>[box200x200]);
layout(defaultStack, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
defaultStack.paint(context, Offset.zero);
expect(context.clipBehavior, equals(Clip.none));
for (final Clip clip in Clip.values) {
for (final Clip? clip in <Clip?>[null, ...Clip.values]) {
final TestClipPaintingContext context = TestClipPaintingContext();
final RenderBox child = box200x200;
final RenderStack stack = RenderStack(
final RenderStack stack;
switch(clip){
case Clip.none:
case Clip.hardEdge:
case Clip.antiAlias:
case Clip.antiAliasWithSaveLayer:
stack = RenderStack(
textDirection: TextDirection.ltr,
children: <RenderBox>[child],
clipBehavior: clip,
clipBehavior: clip!,
);
break;
case null:
stack = RenderStack(
textDirection: TextDirection.ltr,
children: <RenderBox>[child],
);
break;
}
{ // Make sure that the child is positioned so the stack will consider it as overflowed.
final StackParentData parentData = child.parentData! as StackParentData;
parentData.left = parentData.right = 0;
}
layout(stack, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
layout(stack, constraints: viewport, phase: EnginePhase.composite, onErrors: expectNoFlutterErrors);
context.paintChild(stack, Offset.zero);
expect(context.clipBehavior, equals(clip));
// By default, clipBehavior should be Clip.hardEdge
expect(context.clipBehavior, equals(clip ?? Clip.hardEdge), reason: 'for $clip');
}
});
......
......@@ -208,17 +208,23 @@ void main() {
const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 100.0);
final TestClipPaintingContext context = TestClipPaintingContext();
// By default, clipBehavior should be Clip.none
final RenderWrap defaultWrap = RenderWrap(textDirection: TextDirection.ltr, children: <RenderBox>[box200x200]);
layout(defaultWrap, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
context.paintChild(defaultWrap, Offset.zero);
expect(context.clipBehavior, equals(Clip.none));
for (final Clip clip in Clip.values) {
final RenderWrap wrap = RenderWrap(textDirection: TextDirection.ltr, children: <RenderBox>[box200x200], clipBehavior: clip);
layout(wrap, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
for (final Clip? clip in <Clip?>[null, ...Clip.values]) {
final RenderWrap wrap;
switch(clip){
case Clip.none:
case Clip.hardEdge:
case Clip.antiAlias:
case Clip.antiAliasWithSaveLayer:
wrap = RenderWrap(textDirection: TextDirection.ltr, children: <RenderBox>[box200x200], clipBehavior: clip!);
break;
case null:
wrap = RenderWrap(textDirection: TextDirection.ltr, children: <RenderBox>[box200x200]);
break;
}
layout(wrap, constraints: viewport, phase: EnginePhase.composite, onErrors: expectNoFlutterErrors);
context.paintChild(wrap, Offset.zero);
expect(context.clipBehavior, equals(clip));
// By default, clipBehavior should be Clip.none
expect(context.clipBehavior, equals(clip ?? Clip.none));
}
});
}
......@@ -578,6 +578,55 @@ void main() {
expect(renderObject.clipBehavior, equals(Clip.antiAlias));
});
testWidgets('UnconstrainedBox warns only when clipBehavior is Clip.none', (WidgetTester tester) async {
for (final Clip? clip in <Clip?>[null, ...Clip.values]) {
// Clear any render objects that were there before so that we can see more
// than one error. Otherwise, it just throws the first one and skips the
// rest, since the render objects haven't changed.
await tester.pumpWidget(const SizedBox());
await tester.pumpWidget(
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200, maxWidth: 200),
child: clip == null
? const UnconstrainedBox(child: SizedBox(width: 400, height: 400))
: UnconstrainedBox(
clipBehavior: clip,
child: const SizedBox(width: 400, height: 400),
),
),
),
);
final RenderConstraintsTransformBox renderObject = tester.allRenderObjects.whereType<RenderConstraintsTransformBox>().first;
// Defaults to Clip.none
expect(renderObject.clipBehavior, equals(clip ?? Clip.none), reason: 'for clip = $clip');
switch(clip) {
case null:
case Clip.none:
// the UnconstrainedBox overflows.
final dynamic exception = tester.takeException();
expect(exception, isFlutterError, reason: 'for clip = $clip');
// ignore: avoid_dynamic_calls
expect(exception.diagnostics.first.level, DiagnosticLevel.summary, reason: 'for clip = $clip');
expect(
// ignore: avoid_dynamic_calls
exception.diagnostics.first.toString(),
startsWith('A RenderConstraintsTransformBox overflowed'),
reason: 'for clip = $clip',
);
break;
case Clip.hardEdge:
case Clip.antiAlias:
case Clip.antiAliasWithSaveLayer:
expect(tester.takeException(), isNull, reason: 'for clip = $clip');
break;
}
}
});
group('ConstraintsTransformBox', () {
test('toString', () {
expect(
......
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