Skip to content

OverflowBox always sets its own size as constraints.biggest, while users frequently needs it to "shrink wrap" #129094

@fzyzcjy

Description

@fzyzcjy

Is there an existing issue for this?

Steps to reproduce

Run the test

Expected results

Actual results

Firstly, when enableOverflowBox=false, the results are expected:

  • When contentSuperLong=false, i.e. content is short, the test passes
  • When contentSuperLong=true, i.e. content is too long and overflows, the test correctly prints an error saying "A RenderFlex overflowed by 9400 pixels on the bottom". That is expected, because our content is really too long.

Secondly, when enableOverflowBox=true, i.e. use OverflowBox, the results are less than optimal:

  • When contentSuperLong=false, i.e. content is short, the test fails. This is the main point the PR is going to solve. Here, OverflowBox always sets its own size as constraints.biggest, while users wants it to "shrink wrap", i.e. when child is small, the OverflowBox should be as small as child.
  • When contentSuperLong=true, i.e. content is too long and overflows, the OverflowBox does its job correctly.

In other words, the enableOverflowBox=true contentSuperLong=false needs to be fixed.

Code Fix

Please copy-paste the following MyOverflowBox, and modify usage of OverflowBox to MyOverflowBox.

Then, as can be seen, the enableOverflowBox=true contentSuperLong=false case now passes test, because it correctly shrink wraps.

Details
class MyOverflowBox extends SingleChildRenderObjectWidget {
  /// Creates a widget that lets its child overflow itself.
  const MyOverflowBox({
    super.key,
    this.alignment = Alignment.center,
    this.minWidth,
    this.maxWidth,
    this.minHeight,
    this.maxHeight,
    super.child,
  });

  /// How to align the child.
  ///
  /// The x and y values of the alignment control the horizontal and vertical
  /// alignment, respectively. An x value of -1.0 means that the left edge of
  /// the child is aligned with the left edge of the parent whereas an x value
  /// of 1.0 means that the right edge of the child is aligned with the right
  /// edge of the parent. Other values interpolate (and extrapolate) linearly.
  /// For example, a value of 0.0 means that the center of the child is aligned
  /// with the center of the parent.
  ///
  /// Defaults to [Alignment.center].
  ///
  /// See also:
  ///
  ///  * [Alignment], a class with convenient constants typically used to
  ///    specify an [AlignmentGeometry].
  ///  * [AlignmentDirectional], like [Alignment] for specifying alignments
  ///    relative to text direction.
  final AlignmentGeometry alignment;

  /// The minimum width constraint to give the child. Set this to null (the
  /// default) to use the constraint from the parent instead.
  final double? minWidth;

  /// The maximum width constraint to give the child. Set this to null (the
  /// default) to use the constraint from the parent instead.
  final double? maxWidth;

  /// The minimum height constraint to give the child. Set this to null (the
  /// default) to use the constraint from the parent instead.
  final double? minHeight;

  /// The maximum height constraint to give the child. Set this to null (the
  /// default) to use the constraint from the parent instead.
  final double? maxHeight;

  @override
  MyRenderConstrainedOverflowBox createRenderObject(BuildContext context) {
    return MyRenderConstrainedOverflowBox(
      alignment: alignment,
      minWidth: minWidth,
      maxWidth: maxWidth,
      minHeight: minHeight,
      maxHeight: maxHeight,
      textDirection: Directionality.maybeOf(context),
    );
  }

  @override
  void updateRenderObject(BuildContext context, MyRenderConstrainedOverflowBox renderObject) {
    renderObject
      ..alignment = alignment
      ..minWidth = minWidth
      ..maxWidth = maxWidth
      ..minHeight = minHeight
      ..maxHeight = maxHeight
      ..textDirection = Directionality.maybeOf(context);
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment));
    properties.add(DoubleProperty('minWidth', minWidth, defaultValue: null));
    properties.add(DoubleProperty('maxWidth', maxWidth, defaultValue: null));
    properties.add(DoubleProperty('minHeight', minHeight, defaultValue: null));
    properties.add(DoubleProperty('maxHeight', maxHeight, defaultValue: null));
  }
}

class MyRenderConstrainedOverflowBox extends RenderAligningShiftedBox {
  /// Creates a render object that lets its child overflow itself.
  MyRenderConstrainedOverflowBox({
    super.child,
    double? minWidth,
    double? maxWidth,
    double? minHeight,
    double? maxHeight,
    super.alignment,
    super.textDirection,
  })  : _minWidth = minWidth,
        _maxWidth = maxWidth,
        _minHeight = minHeight,
        _maxHeight = maxHeight;

  /// The minimum width constraint to give the child. Set this to null (the
  /// default) to use the constraint from the parent instead.
  double? get minWidth => _minWidth;
  double? _minWidth;

  set minWidth(double? value) {
    if (_minWidth == value) {
      return;
    }
    _minWidth = value;
    markNeedsLayout();
  }

  /// The maximum width constraint to give the child. Set this to null (the
  /// default) to use the constraint from the parent instead.
  double? get maxWidth => _maxWidth;
  double? _maxWidth;

  set maxWidth(double? value) {
    if (_maxWidth == value) {
      return;
    }
    _maxWidth = value;
    markNeedsLayout();
  }

  /// The minimum height constraint to give the child. Set this to null (the
  /// default) to use the constraint from the parent instead.
  double? get minHeight => _minHeight;
  double? _minHeight;

  set minHeight(double? value) {
    if (_minHeight == value) {
      return;
    }
    _minHeight = value;
    markNeedsLayout();
  }

  /// The maximum height constraint to give the child. Set this to null (the
  /// default) to use the constraint from the parent instead.
  double? get maxHeight => _maxHeight;
  double? _maxHeight;

  set maxHeight(double? value) {
    if (_maxHeight == value) {
      return;
    }
    _maxHeight = value;
    markNeedsLayout();
  }

  BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
    return BoxConstraints(
      minWidth: _minWidth ?? constraints.minWidth,
      maxWidth: _maxWidth ?? constraints.maxWidth,
      minHeight: _minHeight ?? constraints.minHeight,
      maxHeight: _maxHeight ?? constraints.maxHeight,
    );
  }

  @override
  // bool get sizedByParent => true;
  bool get sizedByParent => false; // NOTE MODIFIED

  // NOTE MODIFIED
  // @override
  // Size computeDryLayout(BoxConstraints constraints) {
  //   return constraints.biggest;
  // }

  @override
  void performLayout() {
    if (child != null) {
      child?.layout(_getInnerConstraints(constraints), parentUsesSize: true);
      // alignChild(); // NOTE MODIFIED
    }

    size = child != null ? constraints.constrain(child!.size) : constraints.smallest; // NOTE MODIFIED

    if (child != null) {
      alignChild();
    }

    print('hi performLayout constraints=$constraints size=$size child.size=${child?.size}');
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DoubleProperty('minWidth', minWidth, ifNull: 'use parent minWidth constraint'));
    properties.add(DoubleProperty('maxWidth', maxWidth, ifNull: 'use parent maxWidth constraint'));
    properties.add(DoubleProperty('minHeight', minHeight, ifNull: 'use parent minHeight constraint'));
    properties.add(DoubleProperty('maxHeight', maxHeight, ifNull: 'use parent maxHeight constraint'));
  }
}

Code sample

Code sample
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  for (final contentSuperLong in [false, true]) {
    for (final enableOverflowBox in [false, true]) {
      testWidgets('contentSuperLong=$contentSuperLong enableOverflowBox=$enableOverflowBox',
          (WidgetTester tester) async {
        final key = GlobalKey();

        final child = Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            SizedBox(width: 100, height: contentSuperLong ? 10000 : 100),
          ],
        );

        await tester.pumpWidget(
          MaterialApp(
            home: Stack(
              children: [
                Container(
                  key: key,
                  child: enableOverflowBox
                      ? OverflowBox(
                          maxHeight: 1000000,
                          child: child,
                        )
                      : child,
                ),
              ],
            ),
          ),
        );

        expect(tester.getBottomLeft(find.byKey(key)).dy, contentSuperLong ? 600 : 100);
      });
    }
  }
}

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
[Paste your logs here]

Flutter Doctor output

Doctor output
[Paste your output here]

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Issues that are less important to the Flutter projectfound in release: 3.10Found to occur in 3.10found in release: 3.12Found to occur in 3.12frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onr: fixedIssue is closed as already fixed in a newer versionteam-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions