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