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;
6
7import 'package:flutter/foundation.dart';
8
9import 'basic.dart';
10import 'debug.dart';
11import 'framework.dart';
12
13const double _kOffset = 40.0; // distance to bottom of banner, at a 45 degree angle inwards
14const double _kHeight = 12.0; // height of banner
15const double _kBottomOffset = _kOffset + 0.707 * _kHeight; // offset plus sqrt(2)/2 * banner height
16const Rect _kRect = Rect.fromLTWH(-_kOffset, _kOffset - _kHeight, _kOffset * 2.0, _kHeight);
17
18const Color _kColor = Color(0xA0B71C1C);
19const TextStyle _kTextStyle = TextStyle(
20 color: Color(0xFFFFFFFF),
21 fontSize: _kHeight * 0.85,
22 fontWeight: FontWeight.w900,
23 height: 1.0,
24);
25
26const String _flutterWidgetsLibrary = 'package:flutter/widgets.dart';
27
28/// Where to show a [Banner].
29///
30/// The start and end locations are relative to the ambient [Directionality]
31/// (which can be overridden by [Banner.layoutDirection]).
32enum BannerLocation {
33 /// Show the banner in the top-right corner when the ambient [Directionality]
34 /// (or [Banner.layoutDirection]) is [TextDirection.rtl] and in the top-left
35 /// corner when the ambient [Directionality] is [TextDirection.ltr].
36 topStart,
37
38 /// Show the banner in the top-left corner when the ambient [Directionality]
39 /// (or [Banner.layoutDirection]) is [TextDirection.rtl] and in the top-right
40 /// corner when the ambient [Directionality] is [TextDirection.ltr].
41 topEnd,
42
43 /// Show the banner in the bottom-right corner when the ambient
44 /// [Directionality] (or [Banner.layoutDirection]) is [TextDirection.rtl] and
45 /// in the bottom-left corner when the ambient [Directionality] is
46 /// [TextDirection.ltr].
47 bottomStart,
48
49 /// Show the banner in the bottom-left corner when the ambient
50 /// [Directionality] (or [Banner.layoutDirection]) is [TextDirection.rtl] and
51 /// in the bottom-right corner when the ambient [Directionality] is
52 /// [TextDirection.ltr].
53 bottomEnd,
54}
55
56/// Paints a [Banner].
57class BannerPainter extends CustomPainter {
58 /// Creates a banner painter.
59 BannerPainter({
60 required this.message,
61 required this.textDirection,
62 required this.location,
63 required this.layoutDirection,
64 this.color = _kColor,
65 this.textStyle = _kTextStyle,
66 }) : super(repaint: PaintingBinding.instance.systemFonts) {
67 // TODO(polina-c): stop duplicating code across disposables
68 // https://github.com/flutter/flutter/issues/137435
69 if (kFlutterMemoryAllocationsEnabled) {
70 FlutterMemoryAllocations.instance.dispatchObjectCreated(
71 library: _flutterWidgetsLibrary,
72 className: '$BannerPainter',
73 object: this,
74 );
75 }
76 }
77
78 /// The message to show in the banner.
79 final String message;
80
81 /// The directionality of the text.
82 ///
83 /// This value is used to disambiguate how to render bidirectional text. For
84 /// example, if the message is an English phrase followed by a Hebrew phrase,
85 /// in a [TextDirection.ltr] context the English phrase will be on the left
86 /// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
87 /// context, the English phrase will be on the right and the Hebrew phrase on
88 /// its left.
89 ///
90 /// See also:
91 ///
92 /// * [layoutDirection], which controls the interpretation of values in
93 /// [location].
94 final TextDirection textDirection;
95
96 /// Where to show the banner (e.g., the upper right corner).
97 final BannerLocation location;
98
99 /// The directionality of the layout.
100 ///
101 /// This value is used to interpret the [location] of the banner.
102 ///
103 /// See also:
104 ///
105 /// * [textDirection], which controls the reading direction of the [message].
106 final TextDirection layoutDirection;
107
108 /// The color to paint behind the [message].
109 ///
110 /// Defaults to a dark red.
111 final Color color;
112
113 /// The text style to use for the [message].
114 ///
115 /// Defaults to bold, white text.
116 final TextStyle textStyle;
117
118 static const BoxShadow _shadow = BoxShadow(
119 color: Color(0x7F000000),
120 blurRadius: 6.0,
121 );
122
123 bool _prepared = false;
124 TextPainter? _textPainter;
125 late Paint _paintShadow;
126 late Paint _paintBanner;
127
128 /// Release resources held by this painter.
129 ///
130 /// After calling this method, this object is no longer usable.
131 void dispose() {
132 // TODO(polina-c): stop duplicating code across disposables
133 // https://github.com/flutter/flutter/issues/137435
134 if (kFlutterMemoryAllocationsEnabled) {
135 FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
136 }
137 _textPainter?.dispose();
138 _textPainter = null;
139 }
140
141 void _prepare() {
142 _paintShadow = _shadow.toPaint();
143 _paintBanner = Paint()
144 ..color = color;
145 _textPainter?.dispose();
146 _textPainter = TextPainter(
147 text: TextSpan(style: textStyle, text: message),
148 textAlign: TextAlign.center,
149 textDirection: textDirection,
150 );
151 _prepared = true;
152 }
153
154 @override
155 void paint(Canvas canvas, Size size) {
156 if (!_prepared) {
157 _prepare();
158 }
159 canvas
160 ..translate(_translationX(size.width), _translationY(size.height))
161 ..rotate(_rotation)
162 ..drawRect(_kRect, _paintShadow)
163 ..drawRect(_kRect, _paintBanner);
164 const double width = _kOffset * 2.0;
165 _textPainter!.layout(minWidth: width, maxWidth: width);
166 _textPainter!.paint(canvas, _kRect.topLeft + Offset(0.0, (_kRect.height - _textPainter!.height) / 2.0));
167 }
168
169 @override
170 bool shouldRepaint(BannerPainter oldDelegate) {
171 return message != oldDelegate.message
172 || location != oldDelegate.location
173 || color != oldDelegate.color
174 || textStyle != oldDelegate.textStyle;
175 }
176
177 @override
178 bool hitTest(Offset position) => false;
179
180 double _translationX(double width) {
181 switch (layoutDirection) {
182 case TextDirection.rtl:
183 switch (location) {
184 case BannerLocation.bottomEnd:
185 return _kBottomOffset;
186 case BannerLocation.topEnd:
187 return 0.0;
188 case BannerLocation.bottomStart:
189 return width - _kBottomOffset;
190 case BannerLocation.topStart:
191 return width;
192 }
193 case TextDirection.ltr:
194 switch (location) {
195 case BannerLocation.bottomEnd:
196 return width - _kBottomOffset;
197 case BannerLocation.topEnd:
198 return width;
199 case BannerLocation.bottomStart:
200 return _kBottomOffset;
201 case BannerLocation.topStart:
202 return 0.0;
203 }
204 }
205 }
206
207 double _translationY(double height) {
208 switch (location) {
209 case BannerLocation.bottomStart:
210 case BannerLocation.bottomEnd:
211 return height - _kBottomOffset;
212 case BannerLocation.topStart:
213 case BannerLocation.topEnd:
214 return 0.0;
215 }
216 }
217
218 double get _rotation {
219 switch (layoutDirection) {
220 case TextDirection.rtl:
221 switch (location) {
222 case BannerLocation.bottomStart:
223 case BannerLocation.topEnd:
224 return -math.pi / 4.0;
225 case BannerLocation.bottomEnd:
226 case BannerLocation.topStart:
227 return math.pi / 4.0;
228 }
229 case TextDirection.ltr:
230 switch (location) {
231 case BannerLocation.bottomStart:
232 case BannerLocation.topEnd:
233 return math.pi / 4.0;
234 case BannerLocation.bottomEnd:
235 case BannerLocation.topStart:
236 return -math.pi / 4.0;
237 }
238 }
239 }
240}
241
242/// Displays a diagonal message above the corner of another widget.
243///
244/// Useful for showing the execution mode of an app (e.g., that asserts are
245/// enabled.)
246///
247/// See also:
248///
249/// * [CheckedModeBanner], which the [WidgetsApp] widget includes by default in
250/// debug mode, to show a banner that says "DEBUG".
251class Banner extends StatefulWidget {
252 /// Creates a banner.
253 const Banner({
254 super.key,
255 this.child,
256 required this.message,
257 this.textDirection,
258 required this.location,
259 this.layoutDirection,
260 this.color = _kColor,
261 this.textStyle = _kTextStyle,
262 });
263
264 /// The widget to show behind the banner.
265 ///
266 /// {@macro flutter.widgets.ProxyWidget.child}
267 final Widget? child;
268
269 /// The message to show in the banner.
270 final String message;
271
272 /// The directionality of the text.
273 ///
274 /// This is used to disambiguate how to render bidirectional text. For
275 /// example, if the message is an English phrase followed by a Hebrew phrase,
276 /// in a [TextDirection.ltr] context the English phrase will be on the left
277 /// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
278 /// context, the English phrase will be on the right and the Hebrew phrase on
279 /// its left.
280 ///
281 /// Defaults to the ambient [Directionality], if any.
282 ///
283 /// See also:
284 ///
285 /// * [layoutDirection], which controls the interpretation of the [location].
286 final TextDirection? textDirection;
287
288 /// Where to show the banner (e.g., the upper right corner).
289 final BannerLocation location;
290
291 /// The directionality of the layout.
292 ///
293 /// This is used to resolve the [location] values.
294 ///
295 /// Defaults to the ambient [Directionality], if any.
296 ///
297 /// See also:
298 ///
299 /// * [textDirection], which controls the reading direction of the [message].
300 final TextDirection? layoutDirection;
301
302 /// The color of the banner.
303 final Color color;
304
305 /// The style of the text shown on the banner.
306 final TextStyle textStyle;
307
308 @override
309 State<Banner> createState() => _BannerState();
310}
311
312class _BannerState extends State<Banner> {
313 BannerPainter? _painter;
314
315 @override
316 void dispose() {
317 _painter?.dispose();
318 super.dispose();
319 }
320
321 @override
322 Widget build(BuildContext context) {
323 assert((widget.textDirection != null && widget.layoutDirection != null) || debugCheckHasDirectionality(context));
324
325 _painter?.dispose();
326 _painter = BannerPainter(
327 message: widget.message,
328 textDirection: widget.textDirection ?? Directionality.of(context),
329 location: widget.location,
330 layoutDirection: widget.layoutDirection ?? Directionality.of(context),
331 color: widget.color,
332 textStyle: widget.textStyle,
333 );
334
335 return CustomPaint(
336 foregroundPainter: _painter,
337 child: widget.child,
338 );
339 }
340
341 @override
342 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
343 super.debugFillProperties(properties);
344 properties.add(StringProperty('message', widget.message, showName: false));
345 properties.add(EnumProperty<TextDirection>('textDirection', widget.textDirection, defaultValue: null));
346 properties.add(EnumProperty<BannerLocation>('location', widget.location));
347 properties.add(EnumProperty<TextDirection>('layoutDirection', widget.layoutDirection, defaultValue: null));
348 properties.add(ColorProperty('color', widget.color, showName: false));
349 widget.textStyle.debugFillProperties(properties, prefix: 'text ');
350 }
351}
352
353/// Displays a [Banner] saying "DEBUG" when running in debug mode.
354/// [MaterialApp] builds one of these by default.
355///
356/// Does nothing in release mode.
357class CheckedModeBanner extends StatelessWidget {
358 /// Creates a const debug mode banner.
359 const CheckedModeBanner({
360 super.key,
361 required this.child,
362 });
363
364 /// The widget to show behind the banner.
365 ///
366 /// {@macro flutter.widgets.ProxyWidget.child}
367 final Widget child;
368
369 @override
370 Widget build(BuildContext context) {
371 Widget result = child;
372 assert(() {
373 result = Banner(
374 message: 'DEBUG',
375 textDirection: TextDirection.ltr,
376 location: BannerLocation.topEnd,
377 child: result,
378 );
379 return true;
380 }());
381 return result;
382 }
383
384 @override
385 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
386 super.debugFillProperties(properties);
387 String message = 'disabled';
388 assert(() {
389 message = '"DEBUG"';
390 return true;
391 }());
392 properties.add(DiagnosticsNode.message(message));
393 }
394}
395