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';
7library;
8
9import 'dart:math' as math;
10import 'dart:ui' as ui;
11
12import 'package:flutter/foundation.dart';
13
14import 'object.dart';
15import 'stack.dart';
16
17// Describes which side the region data overflows on.
18enum _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.
27class _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.
94mixin 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