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