debug_overflow_indicator.dart 10.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
// 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';

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,
26 27 28
    this.label = '',
    this.labelOffset = Offset.zero,
    this.rotation = 0.0,
29 30 31 32 33 34 35 36 37 38 39 40 41 42
    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
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 sample}
52 53 54 55 56 57 58 59
///
/// ```dart
/// class MyRenderObject extends RenderAligningShiftedBox with DebugOverflowIndicatorMixin {
///   MyRenderObject({
///     AlignmentGeometry alignment,
///     TextDirection textDirection,
///     RenderBox child,
///   }) : super.mixin(alignment, textDirection, child);
60
///
61 62
///   Rect _containerRect;
///   Rect _childRect;
63
///
64 65 66 67 68 69 70
///   @override
///   void performLayout() {
///     // ...
///     final BoxParentData childParentData = child.parentData;
///     _containerRect = Offset.zero & size;
///     _childRect = childParentData.offset & child.size;
///   }
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:
///
88
///  * [RenderUnconstrainedBox] and [RenderFlex] for examples of classes that use this indicator mixin.
89
mixin DebugOverflowIndicatorMixin on RenderObject {
90 91
  static const Color _black = Color(0xBF000000);
  static const Color _yellow = Color(0xBFFFFF00);
92 93 94 95
  // 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;
96 97
  static const TextStyle _indicatorTextStyle = TextStyle(
    color: Color(0xFF900000),
98 99 100
    fontSize: _indicatorFontSizePixels,
    fontWeight: FontWeight.w800,
  );
101 102
  static final Paint _indicatorPaint = Paint()
    ..shader = ui.Gradient.linear(
103 104 105 106 107 108
      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,
    );
109
  static final Paint _labelBackgroundPaint = Paint()..color = const Color(0xFFFFFFFF);
110

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

  // Set to true to trigger a debug message in the console upon
117
  // the next paint call. Will be reset after each paint.
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
  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) {
136
      final Rect markerRect = Rect.fromLTWH(
137 138 139 140 141
        0.0,
        0.0,
        containerRect.width * _indicatorFraction,
        containerRect.height,
      );
142
      regions.add(_OverflowRegionData(
143 144 145 146
        rect: markerRect,
        label: 'LEFT OVERFLOWED BY ${_formatPixels(overflow.left)} PIXELS',
        labelOffset: markerRect.centerLeft +
            const Offset(_indicatorFontSizePixels + _indicatorLabelPaddingPixels, 0.0),
147
        rotation: math.pi / 2.0,
148 149 150 151
        side: _OverflowSide.left,
      ));
    }
    if (overflow.right > 0.0) {
152
      final Rect markerRect = Rect.fromLTWH(
153 154 155 156 157
        containerRect.width * (1.0 - _indicatorFraction),
        0.0,
        containerRect.width * _indicatorFraction,
        containerRect.height,
      );
158
      regions.add(_OverflowRegionData(
159 160 161 162
        rect: markerRect,
        label: 'RIGHT OVERFLOWED BY ${_formatPixels(overflow.right)} PIXELS',
        labelOffset: markerRect.centerRight -
            const Offset(_indicatorFontSizePixels + _indicatorLabelPaddingPixels, 0.0),
163
        rotation: -math.pi / 2.0,
164 165 166 167
        side: _OverflowSide.right,
      ));
    }
    if (overflow.top > 0.0) {
168
      final Rect markerRect = Rect.fromLTWH(
169 170 171 172 173
        0.0,
        0.0,
        containerRect.width,
        containerRect.height * _indicatorFraction,
      );
174
      regions.add(_OverflowRegionData(
175 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),
        rotation: 0.0,
        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 200
        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;
  }

201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
  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.'
      ));
    }
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241

    final List<String> overflows = <String>[];
    if (overflow.left > 0.0)
      overflows.add('${_formatPixels(overflow.left)} pixels on the left');
    if (overflow.top > 0.0)
      overflows.add('${_formatPixels(overflow.top)} pixels on the top');
    if (overflow.bottom > 0.0)
      overflows.add('${_formatPixels(overflow.bottom)} pixels on the bottom');
    if (overflow.right > 0.0)
      overflows.add('${_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(', ');
    }
242 243
    // TODO(jacobr): add the overflows in pixels as structured data so they can
    // be visualized in debugging tools.
244
    FlutterError.reportError(
245
      FlutterErrorDetailsForRendering(
246
        exception: FlutterError('A $runtimeType overflowed by $overflowText.'),
247
        library: 'rendering library',
248
        context: ErrorDescription('during layout'),
249
        renderObject: this,
250 251 252 253 254 255 256 257
        informationCollector: () sync* {
          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);
        }
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 287 288

    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);

      if (_indicatorLabel[region.side.index].text?.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;
    }());
  }
}