debug_overflow_indicator.dart 11 KB
Newer Older
1 2 3 4 5 6 7 8
// Copyright 2017 The Chromium 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';
9
import 'package:flutter/foundation.dart';
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

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,
27 28 29
    this.label = '',
    this.labelOffset = Offset.zero,
    this.rotation = 0.0,
30 31 32 33 34 35 36 37 38 39 40 41 42 43
    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
44
/// how much, their children overflow their containers. These indicators are
45 46 47 48 49 50 51
/// 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.
///
52
/// {@tool sample}
53 54 55 56 57 58 59 60
///
/// ```dart
/// class MyRenderObject extends RenderAligningShiftedBox with DebugOverflowIndicatorMixin {
///   MyRenderObject({
///     AlignmentGeometry alignment,
///     TextDirection textDirection,
///     RenderBox child,
///   }) : super.mixin(alignment, textDirection, child);
61
///
62 63
///   Rect _containerRect;
///   Rect _childRect;
64
///
65 66 67 68 69 70 71
///   @override
///   void performLayout() {
///     // ...
///     final BoxParentData childParentData = child.parentData;
///     _containerRect = Offset.zero & size;
///     _childRect = childParentData.offset & child.size;
///   }
72
///
73 74 75 76
///   @override
///   void paint(PaintingContext context, Offset offset) {
///     // Do normal painting here...
///     // ...
77
///
78 79 80 81 82 83 84
///     assert(() {
///       paintOverflowIndicator(context, offset, _containerRect, _childRect);
///       return true;
///     }());
///   }
/// }
/// ```
85
/// {@end-tool}
86 87 88
///
/// See also:
///
89
///  * [RenderUnconstrainedBox] 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 105 106 107 108 109
      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,
    );
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 123 124 125 126 127 128 129 130 131 132 133 134 135 136
  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) {
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 183
        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) {
184
      final Rect markerRect = Rect.fromLTWH(
185 186 187 188 189
        0.0,
        containerRect.height * (1.0 - _indicatorFraction),
        containerRect.width,
        containerRect.height * _indicatorFraction,
      );
190
      regions.add(_OverflowRegionData(
191 192 193 194 195 196 197 198 199 200 201
        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;
  }

202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
  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.'
      ));
    }
219

220 221 222 223 224 225
    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',
    ];
226 227 228 229 230 231 232 233 234 235 236 237 238 239
    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(', ');
    }
240 241
    // TODO(jacobr): add the overflows in pixels as structured data so they can
    // be visualized in debugging tools.
242
    FlutterError.reportError(
243
      FlutterErrorDetailsForRendering(
244
        exception: FlutterError('A $runtimeType overflowed by $overflowText.'),
245
        library: 'rendering library',
246
        context: ErrorDescription('during layout'),
247
        renderObject: this,
248
        informationCollector: () sync* {
249 250
          if (debugCreator != null)
            yield DiagnosticsDebugCreator(debugCreator);
251 252 253 254 255 256
          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);
257
        },
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
      ),
    );
  }

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

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

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