| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:math' as math; |
| import 'dart:ui' as ui; |
| |
| import 'package:flutter/painting.dart'; |
| import 'package:flutter/foundation.dart'; |
| |
| import 'object.dart'; |
| import 'stack.dart'; |
| |
| // Describes which side the region data overflows on. |
| enum _OverflowSide { |
| left, |
| top, |
| bottom, |
| right, |
| } |
| |
| // Data used by the DebugOverflowIndicator to manage the regions and labels for |
| // the indicators. |
| class _OverflowRegionData { |
| const _OverflowRegionData({ |
| this.rect, |
| this.label = '', |
| this.labelOffset = Offset.zero, |
| this.rotation = 0.0, |
| this.side, |
| }); |
| |
| final Rect rect; |
| final String label; |
| final Offset labelOffset; |
| final double rotation; |
| final _OverflowSide side; |
| } |
| |
| /// An mixin indicator that is drawn when a [RenderObject] overflows its |
| /// container. |
| /// |
| /// This is used by some RenderObjects that are containers to show where, and by |
| /// how much, their children overflow their containers. These indicators are |
| /// typically only shown in a debug build (where the call to |
| /// [paintOverflowIndicator] is surrounded by an assert). |
| /// |
| /// This class will also print a debug message to the console when the container |
| /// overflows. It will print on the first occurrence, and once after each time that |
| /// [reassemble] is called. |
| /// |
| /// {@tool snippet} |
| /// |
| /// ```dart |
| /// class MyRenderObject extends RenderAligningShiftedBox with DebugOverflowIndicatorMixin { |
| /// MyRenderObject({ |
| /// AlignmentGeometry alignment, |
| /// TextDirection textDirection, |
| /// RenderBox child, |
| /// }) : super.mixin(alignment, textDirection, child); |
| /// |
| /// Rect _containerRect; |
| /// Rect _childRect; |
| /// |
| /// @override |
| /// void performLayout() { |
| /// // ... |
| /// final BoxParentData childParentData = child.parentData; |
| /// _containerRect = Offset.zero & size; |
| /// _childRect = childParentData.offset & child.size; |
| /// } |
| /// |
| /// @override |
| /// void paint(PaintingContext context, Offset offset) { |
| /// // Do normal painting here... |
| /// // ... |
| /// |
| /// assert(() { |
| /// paintOverflowIndicator(context, offset, _containerRect, _childRect); |
| /// return true; |
| /// }()); |
| /// } |
| /// } |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [RenderUnconstrainedBox] and [RenderFlex] for examples of classes that use this indicator mixin. |
| mixin DebugOverflowIndicatorMixin on RenderObject { |
| static const Color _black = Color(0xBF000000); |
| static const Color _yellow = Color(0xBFFFFF00); |
| // The fraction of the container that the indicator covers. |
| static const double _indicatorFraction = 0.1; |
| static const double _indicatorFontSizePixels = 7.5; |
| static const double _indicatorLabelPaddingPixels = 1.0; |
| static const TextStyle _indicatorTextStyle = TextStyle( |
| color: Color(0xFF900000), |
| fontSize: _indicatorFontSizePixels, |
| fontWeight: FontWeight.w800, |
| ); |
| static final Paint _indicatorPaint = Paint() |
| ..shader = ui.Gradient.linear( |
| const Offset(0.0, 0.0), |
| const Offset(10.0, 10.0), |
| <Color>[_black, _yellow, _yellow, _black], |
| <double>[0.25, 0.25, 0.75, 0.75], |
| TileMode.repeated, |
| ); |
| static final Paint _labelBackgroundPaint = Paint()..color = const Color(0xFFFFFFFF); |
| |
| final List<TextPainter> _indicatorLabel = List<TextPainter>.filled( |
| _OverflowSide.values.length, |
| TextPainter(textDirection: TextDirection.ltr), // This label is in English. |
| ); |
| |
| // Set to true to trigger a debug message in the console upon |
| // the next paint call. Will be reset after each paint. |
| bool _overflowReportNeeded = true; |
| |
| String _formatPixels(double value) { |
| assert(value > 0.0); |
| String pixels; |
| if (value > 10.0) { |
| pixels = value.toStringAsFixed(0); |
| } else if (value > 1.0) { |
| pixels = value.toStringAsFixed(1); |
| } else { |
| pixels = value.toStringAsPrecision(3); |
| } |
| return pixels; |
| } |
| |
| List<_OverflowRegionData> _calculateOverflowRegions(RelativeRect overflow, Rect containerRect) { |
| final List<_OverflowRegionData> regions = <_OverflowRegionData>[]; |
| if (overflow.left > 0.0) { |
| final Rect markerRect = Rect.fromLTWH( |
| 0.0, |
| 0.0, |
| containerRect.width * _indicatorFraction, |
| containerRect.height, |
| ); |
| regions.add(_OverflowRegionData( |
| rect: markerRect, |
| label: 'LEFT OVERFLOWED BY ${_formatPixels(overflow.left)} PIXELS', |
| labelOffset: markerRect.centerLeft + |
| const Offset(_indicatorFontSizePixels + _indicatorLabelPaddingPixels, 0.0), |
| rotation: math.pi / 2.0, |
| side: _OverflowSide.left, |
| )); |
| } |
| if (overflow.right > 0.0) { |
| final Rect markerRect = Rect.fromLTWH( |
| containerRect.width * (1.0 - _indicatorFraction), |
| 0.0, |
| containerRect.width * _indicatorFraction, |
| containerRect.height, |
| ); |
| regions.add(_OverflowRegionData( |
| rect: markerRect, |
| label: 'RIGHT OVERFLOWED BY ${_formatPixels(overflow.right)} PIXELS', |
| labelOffset: markerRect.centerRight - |
| const Offset(_indicatorFontSizePixels + _indicatorLabelPaddingPixels, 0.0), |
| rotation: -math.pi / 2.0, |
| side: _OverflowSide.right, |
| )); |
| } |
| if (overflow.top > 0.0) { |
| final Rect markerRect = Rect.fromLTWH( |
| 0.0, |
| 0.0, |
| containerRect.width, |
| containerRect.height * _indicatorFraction, |
| ); |
| regions.add(_OverflowRegionData( |
| rect: markerRect, |
| label: 'TOP OVERFLOWED BY ${_formatPixels(overflow.top)} PIXELS', |
| labelOffset: markerRect.topCenter + const Offset(0.0, _indicatorLabelPaddingPixels), |
| rotation: 0.0, |
| side: _OverflowSide.top, |
| )); |
| } |
| if (overflow.bottom > 0.0) { |
| final Rect markerRect = Rect.fromLTWH( |
| 0.0, |
| containerRect.height * (1.0 - _indicatorFraction), |
| containerRect.width, |
| containerRect.height * _indicatorFraction, |
| ); |
| regions.add(_OverflowRegionData( |
| rect: markerRect, |
| label: 'BOTTOM OVERFLOWED BY ${_formatPixels(overflow.bottom)} PIXELS', |
| labelOffset: markerRect.bottomCenter - |
| const Offset(0.0, _indicatorFontSizePixels + _indicatorLabelPaddingPixels), |
| rotation: 0.0, |
| side: _OverflowSide.bottom, |
| )); |
| } |
| return regions; |
| } |
| |
| void _reportOverflow(RelativeRect overflow, List<DiagnosticsNode> overflowHints) { |
| overflowHints ??= <DiagnosticsNode>[]; |
| if (overflowHints.isEmpty) { |
| overflowHints.add(ErrorDescription( |
| 'The edge of the $runtimeType that is ' |
| 'overflowing has been marked in the rendering with a yellow and black ' |
| 'striped pattern. This is usually caused by the contents being too big ' |
| 'for the $runtimeType.' |
| )); |
| overflowHints.add(ErrorHint( |
| 'This is considered an error condition because it indicates that there ' |
| 'is content that cannot be seen. If the content is legitimately bigger ' |
| 'than the available space, consider clipping it with a ClipRect widget ' |
| 'before putting it in the $runtimeType, or using a scrollable ' |
| 'container, like a ListView.' |
| )); |
| } |
| |
| final List<String> overflows = <String>[ |
| if (overflow.left > 0.0) '${_formatPixels(overflow.left)} pixels on the left', |
| if (overflow.top > 0.0) '${_formatPixels(overflow.top)} pixels on the top', |
| if (overflow.bottom > 0.0) '${_formatPixels(overflow.bottom)} pixels on the bottom', |
| if (overflow.right > 0.0) '${_formatPixels(overflow.right)} pixels on the right', |
| ]; |
| String overflowText = ''; |
| assert(overflows.isNotEmpty, |
| "Somehow $runtimeType didn't actually overflow like it thought it did."); |
| switch (overflows.length) { |
| case 1: |
| overflowText = overflows.first; |
| break; |
| case 2: |
| overflowText = '${overflows.first} and ${overflows.last}'; |
| break; |
| default: |
| overflows[overflows.length - 1] = 'and ${overflows[overflows.length - 1]}'; |
| overflowText = overflows.join(', '); |
| } |
| // TODO(jacobr): add the overflows in pixels as structured data so they can |
| // be visualized in debugging tools. |
| FlutterError.reportError( |
| FlutterErrorDetailsForRendering( |
| exception: FlutterError('A $runtimeType overflowed by $overflowText.'), |
| library: 'rendering library', |
| context: ErrorDescription('during layout'), |
| renderObject: this, |
| informationCollector: () sync* { |
| if (debugCreator != null) |
| yield DiagnosticsDebugCreator(debugCreator); |
| yield* overflowHints; |
| yield describeForError('The specific $runtimeType in question is'); |
| // TODO(jacobr): this line is ascii art that it would be nice to |
| // handle a little more generically in GUI debugging clients in the |
| // future. |
| yield DiagnosticsNode.message('◢◤' * (FlutterError.wrapWidth ~/ 2), allowWrap: false); |
| }, |
| ), |
| ); |
| } |
| |
| /// To be called when the overflow indicators should be painted. |
| /// |
| /// Typically only called if there is an overflow, and only from within a |
| /// debug build. |
| /// |
| /// See example code in [DebugOverflowIndicatorMixin] documentation. |
| void paintOverflowIndicator( |
| PaintingContext context, |
| Offset offset, |
| Rect containerRect, |
| Rect childRect, { |
| List<DiagnosticsNode> overflowHints, |
| }) { |
| final RelativeRect overflow = RelativeRect.fromRect(containerRect, childRect); |
| |
| if (overflow.left <= 0.0 && |
| overflow.right <= 0.0 && |
| overflow.top <= 0.0 && |
| overflow.bottom <= 0.0) { |
| return; |
| } |
| |
| final List<_OverflowRegionData> overflowRegions = _calculateOverflowRegions(overflow, containerRect); |
| for (final _OverflowRegionData region in overflowRegions) { |
| context.canvas.drawRect(region.rect.shift(offset), _indicatorPaint); |
| final TextSpan textSpan = _indicatorLabel[region.side.index].text as TextSpan; |
| if (textSpan?.text != region.label) { |
| _indicatorLabel[region.side.index].text = TextSpan( |
| text: region.label, |
| style: _indicatorTextStyle, |
| ); |
| _indicatorLabel[region.side.index].layout(); |
| } |
| |
| final Offset labelOffset = region.labelOffset + offset; |
| final Offset centerOffset = Offset(-_indicatorLabel[region.side.index].width / 2.0, 0.0); |
| final Rect textBackgroundRect = centerOffset & _indicatorLabel[region.side.index].size; |
| context.canvas.save(); |
| context.canvas.translate(labelOffset.dx, labelOffset.dy); |
| context.canvas.rotate(region.rotation); |
| context.canvas.drawRect(textBackgroundRect, _labelBackgroundPaint); |
| _indicatorLabel[region.side.index].paint(context.canvas, centerOffset); |
| context.canvas.restore(); |
| } |
| |
| if (_overflowReportNeeded) { |
| _overflowReportNeeded = false; |
| _reportOverflow(overflow, overflowHints); |
| } |
| } |
| |
| @override |
| void reassemble() { |
| super.reassemble(); |
| // Users expect error messages to be shown again after hot reload. |
| assert(() { |
| _overflowReportNeeded = true; |
| return true; |
| }()); |
| } |
| } |