debug_overflow_indicator.dart 11 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7
// 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;

8
import 'package:flutter/foundation.dart';
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

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({
25
    required this.rect,
26 27 28
    this.label = '',
    this.labelOffset = Offset.zero,
    this.rotation = 0.0,
29
    required this.side,
30 31 32 33 34 35 36 37 38 39 40 41 42
  });

  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
43
/// how much, their children overflow their containers. These indicators are
44 45 46 47 48 49 50
/// 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.
///
51
/// {@tool snippet}
52 53 54 55
///
/// ```dart
/// class MyRenderObject extends RenderAligningShiftedBox with DebugOverflowIndicatorMixin {
///   MyRenderObject({
56 57 58 59
///     super.alignment = Alignment.center,
///     required super.textDirection,
///     super.child,
///   });
60
///
61 62
///   late Rect _containerRect;
///   late Rect _childRect;
63
///
64 65 66
///   @override
///   void performLayout() {
///     // ...
67
///     final BoxParentData childParentData = child!.parentData! as BoxParentData;
68
///     _containerRect = Offset.zero & size;
69
///     _childRect = childParentData.offset & child!.size;
70
///   }
71
///
72 73 74 75
///   @override
///   void paint(PaintingContext context, Offset offset) {
///     // Do normal painting here...
///     // ...
76
///
77 78 79 80 81 82 83
///     assert(() {
///       paintOverflowIndicator(context, offset, _containerRect, _childRect);
///       return true;
///     }());
///   }
/// }
/// ```
84
/// {@end-tool}
85 86 87
///
/// See also:
///
Kate Lovett's avatar
Kate Lovett committed
88 89
///  * [RenderConstraintsTransformBox] and [RenderFlex] for examples of classes
///    that use this indicator mixin.
90
mixin DebugOverflowIndicatorMixin on RenderObject {
91 92
  static const Color _black = Color(0xBF000000);
  static const Color _yellow = Color(0xBFFFFF00);
93 94 95 96
  // 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;
97 98
  static const TextStyle _indicatorTextStyle = TextStyle(
    color: Color(0xFF900000),
99 100 101
    fontSize: _indicatorFontSizePixels,
    fontWeight: FontWeight.w800,
  );
102 103
  static final Paint _indicatorPaint = Paint()
    ..shader = ui.Gradient.linear(
104
      Offset.zero,
105 106 107 108 109
      const Offset(10.0, 10.0),
      <Color>[_black, _yellow, _yellow, _black],
      <double>[0.25, 0.25, 0.75, 0.75],
      TileMode.repeated,
    );
110
  static final Paint _labelBackgroundPaint = Paint()..color = const Color(0xFFFFFFFF);
111

112
  final List<TextPainter> _indicatorLabel = List<TextPainter>.filled(
113
    _OverflowSide.values.length,
114
    TextPainter(textDirection: TextDirection.ltr), // This label is in English.
115 116 117
  );

  // Set to true to trigger a debug message in the console upon
118
  // the next paint call. Will be reset after each paint.
119 120 121 122
  bool _overflowReportNeeded = true;

  String _formatPixels(double value) {
    assert(value > 0.0);
123
    final String pixels;
124 125 126 127 128 129 130 131 132 133 134 135 136
    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) {
137
      final Rect markerRect = Rect.fromLTWH(
138 139 140 141 142
        0.0,
        0.0,
        containerRect.width * _indicatorFraction,
        containerRect.height,
      );
143
      regions.add(_OverflowRegionData(
144 145 146 147
        rect: markerRect,
        label: 'LEFT OVERFLOWED BY ${_formatPixels(overflow.left)} PIXELS',
        labelOffset: markerRect.centerLeft +
            const Offset(_indicatorFontSizePixels + _indicatorLabelPaddingPixels, 0.0),
148
        rotation: math.pi / 2.0,
149 150 151 152
        side: _OverflowSide.left,
      ));
    }
    if (overflow.right > 0.0) {
153
      final Rect markerRect = Rect.fromLTWH(
154 155 156 157 158
        containerRect.width * (1.0 - _indicatorFraction),
        0.0,
        containerRect.width * _indicatorFraction,
        containerRect.height,
      );
159
      regions.add(_OverflowRegionData(
160 161 162 163
        rect: markerRect,
        label: 'RIGHT OVERFLOWED BY ${_formatPixels(overflow.right)} PIXELS',
        labelOffset: markerRect.centerRight -
            const Offset(_indicatorFontSizePixels + _indicatorLabelPaddingPixels, 0.0),
164
        rotation: -math.pi / 2.0,
165 166 167 168
        side: _OverflowSide.right,
      ));
    }
    if (overflow.top > 0.0) {
169
      final Rect markerRect = Rect.fromLTWH(
170 171 172 173 174
        0.0,
        0.0,
        containerRect.width,
        containerRect.height * _indicatorFraction,
      );
175
      regions.add(_OverflowRegionData(
176 177 178 179 180 181 182
        rect: markerRect,
        label: 'TOP OVERFLOWED BY ${_formatPixels(overflow.top)} PIXELS',
        labelOffset: markerRect.topCenter + const Offset(0.0, _indicatorLabelPaddingPixels),
        side: _OverflowSide.top,
      ));
    }
    if (overflow.bottom > 0.0) {
183
      final Rect markerRect = Rect.fromLTWH(
184 185 186 187 188
        0.0,
        containerRect.height * (1.0 - _indicatorFraction),
        containerRect.width,
        containerRect.height * _indicatorFraction,
      );
189
      regions.add(_OverflowRegionData(
190 191 192 193 194 195 196 197 198 199
        rect: markerRect,
        label: 'BOTTOM OVERFLOWED BY ${_formatPixels(overflow.bottom)} PIXELS',
        labelOffset: markerRect.bottomCenter -
            const Offset(0.0, _indicatorFontSizePixels + _indicatorLabelPaddingPixels),
        side: _OverflowSide.bottom,
      ));
    }
    return regions;
  }

200
  void _reportOverflow(RelativeRect overflow, List<DiagnosticsNode>? overflowHints) {
201 202 203 204 205 206
    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 '
207
        'for the $runtimeType.',
208 209 210 211 212 213
      ));
      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 '
214
        'container, like a ListView.',
215 216
      ));
    }
217

218 219 220 221 222 223
    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',
    ];
224
    String overflowText = '';
225
    assert(overflows.isNotEmpty, "Somehow $runtimeType didn't actually overflow like it thought it did.");
226 227 228 229 230 231 232 233 234 235 236
    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(', ');
    }
237 238
    // TODO(jacobr): add the overflows in pixels as structured data so they can
    // be visualized in debugging tools.
239
    FlutterError.reportError(
240
      FlutterErrorDetails(
241
        exception: FlutterError('A $runtimeType overflowed by $overflowText.'),
242
        library: 'rendering library',
243
        context: ErrorDescription('during layout'),
244 245 246 247 248 249 250
        informationCollector: () => <DiagnosticsNode>[
          // debugCreator should only be set in DebugMode, but we want the
          // treeshaker to know that.
          if (kDebugMode && debugCreator != null)
            DiagnosticsDebugCreator(debugCreator!),
          ...overflowHints!,
          describeForError('The specific $runtimeType in question is'),
251 252 253
          // 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.
254 255
          DiagnosticsNode.message('◢◤' * (FlutterError.wrapWidth ~/ 2), allowWrap: false),
        ],
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
      ),
    );
  }

  /// 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, {
271
    List<DiagnosticsNode>? overflowHints,
272
  }) {
273
    final RelativeRect overflow = RelativeRect.fromRect(containerRect, childRect);
274 275 276 277 278 279 280 281 282

    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);
283
    for (final _OverflowRegionData region in overflowRegions) {
284
      context.canvas.drawRect(region.rect.shift(offset), _indicatorPaint);
285
      final TextSpan? textSpan = _indicatorLabel[region.side.index].text as TextSpan?;
286
      if (textSpan?.text != region.label) {
287
        _indicatorLabel[region.side.index].text = TextSpan(
288 289 290 291 292 293 294
          text: region.label,
          style: _indicatorTextStyle,
        );
        _indicatorLabel[region.side.index].layout();
      }

      final Offset labelOffset = region.labelOffset + offset;
295
      final Offset centerOffset = Offset(-_indicatorLabel[region.side.index].width / 2.0, 0.0);
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
      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;
    }());
  }
}