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