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
5import 'dart:math' as math;
6import 'dart:ui' as ui;
7
8import 'package:flutter/foundation.dart';
9
10import 'object.dart';
11import 'stack.dart';
12
13// Describes which side the region data overflows on.
14enum _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.
23class _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.
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 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