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