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 { left, top, bottom, right }
19
20// Data used by the DebugOverflowIndicator to manage the regions and labels for
21// the indicators.
22class _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.
89mixin 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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com