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