• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2017 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:math' as math;
6import 'dart:ui' as ui;
7
8import 'package:flutter/painting.dart';
9import 'package:flutter/foundation.dart';
10
11import 'object.dart';
12import 'stack.dart';
13
14// Describes which side the region data overflows on.
15enum _OverflowSide {
16  left,
17  top,
18  bottom,
19  right,
20}
21
22// Data used by the DebugOverflowIndicator to manage the regions and labels for
23// the indicators.
24class _OverflowRegionData {
25  const _OverflowRegionData({
26    this.rect,
27    this.label = '',
28    this.labelOffset = Offset.zero,
29    this.rotation = 0.0,
30    this.side,
31  });
32
33  final Rect rect;
34  final String label;
35  final Offset labelOffset;
36  final double rotation;
37  final _OverflowSide side;
38}
39
40/// An mixin indicator that is drawn when a [RenderObject] overflows its
41/// container.
42///
43/// 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/// typically only shown in a debug build (where the call to
46/// [paintOverflowIndicator] is surrounded by an assert).
47///
48/// This class will also print a debug message to the console when the container
49/// overflows. It will print on the first occurrence, and once after each time that
50/// [reassemble] is called.
51///
52/// {@tool sample}
53///
54/// ```dart
55/// class MyRenderObject extends RenderAligningShiftedBox with DebugOverflowIndicatorMixin {
56///   MyRenderObject({
57///     AlignmentGeometry alignment,
58///     TextDirection textDirection,
59///     RenderBox child,
60///   }) : super.mixin(alignment, textDirection, child);
61///
62///   Rect _containerRect;
63///   Rect _childRect;
64///
65///   @override
66///   void performLayout() {
67///     // ...
68///     final BoxParentData childParentData = child.parentData;
69///     _containerRect = Offset.zero & size;
70///     _childRect = childParentData.offset & child.size;
71///   }
72///
73///   @override
74///   void paint(PaintingContext context, Offset offset) {
75///     // Do normal painting here...
76///     // ...
77///
78///     assert(() {
79///       paintOverflowIndicator(context, offset, _containerRect, _childRect);
80///       return true;
81///     }());
82///   }
83/// }
84/// ```
85/// {@end-tool}
86///
87/// See also:
88///
89///  * [RenderUnconstrainedBox] and [RenderFlex] for examples of classes that use this indicator mixin.
90mixin DebugOverflowIndicatorMixin on RenderObject {
91  static const Color _black = Color(0xBF000000);
92  static const Color _yellow = Color(0xBFFFFF00);
93  // The fraction of the container that the indicator covers.
94  static const double _indicatorFraction = 0.1;
95  static const double _indicatorFontSizePixels = 7.5;
96  static const double _indicatorLabelPaddingPixels = 1.0;
97  static const TextStyle _indicatorTextStyle = TextStyle(
98    color: Color(0xFF900000),
99    fontSize: _indicatorFontSizePixels,
100    fontWeight: FontWeight.w800,
101  );
102  static final Paint _indicatorPaint = Paint()
103    ..shader = ui.Gradient.linear(
104      const Offset(0.0, 0.0),
105      const Offset(10.0, 10.0),
106      <Color>[_black, _yellow, _yellow, _black],
107      <double>[0.25, 0.25, 0.75, 0.75],
108      TileMode.repeated,
109    );
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  bool _overflowReportNeeded = true;
120
121  String _formatPixels(double value) {
122    assert(value > 0.0);
123    String pixels;
124    if (value > 10.0) {
125      pixels = value.toStringAsFixed(0);
126    } else if (value > 1.0) {
127      pixels = value.toStringAsFixed(1);
128    } else {
129      pixels = value.toStringAsPrecision(3);
130    }
131    return pixels;
132  }
133
134  List<_OverflowRegionData> _calculateOverflowRegions(RelativeRect overflow, Rect containerRect) {
135    final List<_OverflowRegionData> regions = <_OverflowRegionData>[];
136    if (overflow.left > 0.0) {
137      final Rect markerRect = Rect.fromLTWH(
138        0.0,
139        0.0,
140        containerRect.width * _indicatorFraction,
141        containerRect.height,
142      );
143      regions.add(_OverflowRegionData(
144        rect: markerRect,
145        label: 'LEFT OVERFLOWED BY ${_formatPixels(overflow.left)} PIXELS',
146        labelOffset: markerRect.centerLeft +
147            const Offset(_indicatorFontSizePixels + _indicatorLabelPaddingPixels, 0.0),
148        rotation: math.pi / 2.0,
149        side: _OverflowSide.left,
150      ));
151    }
152    if (overflow.right > 0.0) {
153      final Rect markerRect = Rect.fromLTWH(
154        containerRect.width * (1.0 - _indicatorFraction),
155        0.0,
156        containerRect.width * _indicatorFraction,
157        containerRect.height,
158      );
159      regions.add(_OverflowRegionData(
160        rect: markerRect,
161        label: 'RIGHT OVERFLOWED BY ${_formatPixels(overflow.right)} PIXELS',
162        labelOffset: markerRect.centerRight -
163            const Offset(_indicatorFontSizePixels + _indicatorLabelPaddingPixels, 0.0),
164        rotation: -math.pi / 2.0,
165        side: _OverflowSide.right,
166      ));
167    }
168    if (overflow.top > 0.0) {
169      final Rect markerRect = Rect.fromLTWH(
170        0.0,
171        0.0,
172        containerRect.width,
173        containerRect.height * _indicatorFraction,
174      );
175      regions.add(_OverflowRegionData(
176        rect: markerRect,
177        label: 'TOP OVERFLOWED BY ${_formatPixels(overflow.top)} PIXELS',
178        labelOffset: markerRect.topCenter + const Offset(0.0, _indicatorLabelPaddingPixels),
179        rotation: 0.0,
180        side: _OverflowSide.top,
181      ));
182    }
183    if (overflow.bottom > 0.0) {
184      final Rect markerRect = Rect.fromLTWH(
185        0.0,
186        containerRect.height * (1.0 - _indicatorFraction),
187        containerRect.width,
188        containerRect.height * _indicatorFraction,
189      );
190      regions.add(_OverflowRegionData(
191        rect: markerRect,
192        label: 'BOTTOM OVERFLOWED BY ${_formatPixels(overflow.bottom)} PIXELS',
193        labelOffset: markerRect.bottomCenter -
194            const Offset(0.0, _indicatorFontSizePixels + _indicatorLabelPaddingPixels),
195        rotation: 0.0,
196        side: _OverflowSide.bottom,
197      ));
198    }
199    return regions;
200  }
201
202  void _reportOverflow(RelativeRect overflow, List<DiagnosticsNode> overflowHints) {
203    overflowHints ??= <DiagnosticsNode>[];
204    if (overflowHints.isEmpty) {
205      overflowHints.add(ErrorDescription(
206        'The edge of the $runtimeType that is '
207        'overflowing has been marked in the rendering with a yellow and black '
208        'striped pattern. This is usually caused by the contents being too big '
209        'for the $runtimeType.'
210      ));
211      overflowHints.add(ErrorHint(
212        'This is considered an error condition because it indicates that there '
213        'is content that cannot be seen. If the content is legitimately bigger '
214        'than the available space, consider clipping it with a ClipRect widget '
215        'before putting it in the $runtimeType, or using a scrollable '
216        'container, like a ListView.'
217      ));
218    }
219
220    final List<String> overflows = <String>[];
221    if (overflow.left > 0.0)
222      overflows.add('${_formatPixels(overflow.left)} pixels on the left');
223    if (overflow.top > 0.0)
224      overflows.add('${_formatPixels(overflow.top)} pixels on the top');
225    if (overflow.bottom > 0.0)
226      overflows.add('${_formatPixels(overflow.bottom)} pixels on the bottom');
227    if (overflow.right > 0.0)
228      overflows.add('${_formatPixels(overflow.right)} pixels on the right');
229    String overflowText = '';
230    assert(overflows.isNotEmpty,
231        "Somehow $runtimeType didn't actually overflow like it thought it did.");
232    switch (overflows.length) {
233      case 1:
234        overflowText = overflows.first;
235        break;
236      case 2:
237        overflowText = '${overflows.first} and ${overflows.last}';
238        break;
239      default:
240        overflows[overflows.length - 1] = 'and ${overflows[overflows.length - 1]}';
241        overflowText = overflows.join(', ');
242    }
243    // TODO(jacobr): add the overflows in pixels as structured data so they can
244    // be visualized in debugging tools.
245    FlutterError.reportError(
246      FlutterErrorDetailsForRendering(
247        exception: FlutterError('A $runtimeType overflowed by $overflowText.'),
248        library: 'rendering library',
249        context: ErrorDescription('during layout'),
250        renderObject: this,
251        informationCollector: () sync* {
252          if (debugCreator != null)
253            yield DiagnosticsDebugCreator(debugCreator);
254          yield* overflowHints;
255          yield describeForError('The specific $runtimeType in question is');
256          // TODO(jacobr): this line is ascii art that it would be nice to
257          // handle a little more generically in GUI debugging clients in the
258          // future.
259          yield DiagnosticsNode.message('◢◤' * (FlutterError.wrapWidth ~/ 2), allowWrap: false);
260        }
261      ),
262    );
263  }
264
265  /// To be called when the overflow indicators should be painted.
266  ///
267  /// Typically only called if there is an overflow, and only from within a
268  /// debug build.
269  ///
270  /// See example code in [DebugOverflowIndicatorMixin] documentation.
271  void paintOverflowIndicator(
272    PaintingContext context,
273    Offset offset,
274    Rect containerRect,
275    Rect childRect, {
276    List<DiagnosticsNode> overflowHints,
277  }) {
278    final RelativeRect overflow = RelativeRect.fromRect(containerRect, childRect);
279
280    if (overflow.left <= 0.0 &&
281        overflow.right <= 0.0 &&
282        overflow.top <= 0.0 &&
283        overflow.bottom <= 0.0) {
284      return;
285    }
286
287    final List<_OverflowRegionData> overflowRegions = _calculateOverflowRegions(overflow, containerRect);
288    for (_OverflowRegionData region in overflowRegions) {
289      context.canvas.drawRect(region.rect.shift(offset), _indicatorPaint);
290      final TextSpan textSpan = _indicatorLabel[region.side.index].text;
291      if (textSpan?.text != region.label) {
292        _indicatorLabel[region.side.index].text = TextSpan(
293          text: region.label,
294          style: _indicatorTextStyle,
295        );
296        _indicatorLabel[region.side.index].layout();
297      }
298
299      final Offset labelOffset = region.labelOffset + offset;
300      final Offset centerOffset = Offset(-_indicatorLabel[region.side.index].width / 2.0, 0.0);
301      final Rect textBackgroundRect = centerOffset & _indicatorLabel[region.side.index].size;
302      context.canvas.save();
303      context.canvas.translate(labelOffset.dx, labelOffset.dy);
304      context.canvas.rotate(region.rotation);
305      context.canvas.drawRect(textBackgroundRect, _labelBackgroundPaint);
306      _indicatorLabel[region.side.index].paint(context.canvas, centerOffset);
307      context.canvas.restore();
308    }
309
310    if (_overflowReportNeeded) {
311      _overflowReportNeeded = false;
312      _reportOverflow(overflow, overflowHints);
313    }
314  }
315
316  @override
317  void reassemble() {
318    super.reassemble();
319    // Users expect error messages to be shown again after hot reload.
320    assert(() {
321      _overflowReportNeeded = true;
322      return true;
323    }());
324  }
325}
326