From 75ca31b0e48d15c97c4cddf68b25bfa0bd3d70f3 Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Wed, 8 Feb 2023 08:52:41 -0800 Subject: [PATCH] Correct Badge interpretation of its alignment parameter (#119853) --- .../gen_defaults/lib/badge_template.dart | 2 +- packages/flutter/lib/src/material/badge.dart | 118 ++++++++++-- .../flutter/lib/src/material/badge_theme.dart | 20 +- .../flutter/test/material/badge_test.dart | 176 +++++++++++++++--- .../test/material/badge_theme_test.dart | 21 ++- 5 files changed, 287 insertions(+), 50 deletions(-) diff --git a/dev/tools/gen_defaults/lib/badge_template.dart b/dev/tools/gen_defaults/lib/badge_template.dart index 09d26bc7f28..38bec9e0764 100644 --- a/dev/tools/gen_defaults/lib/badge_template.dart +++ b/dev/tools/gen_defaults/lib/badge_template.dart @@ -16,7 +16,7 @@ class _${blockName}DefaultsM3 extends BadgeThemeData { smallSize: ${tokens["md.comp.badge.size"]}, largeSize: ${tokens["md.comp.badge.large.size"]}, padding: const EdgeInsets.symmetric(horizontal: 4), - alignment: const AlignmentDirectional(12, -4), + alignment: AlignmentDirectional.topEnd, ); final BuildContext context; diff --git a/packages/flutter/lib/src/material/badge.dart b/packages/flutter/lib/src/material/badge.dart index f1a2bcdd843..e0574be986d 100644 --- a/packages/flutter/lib/src/material/badge.dart +++ b/packages/flutter/lib/src/material/badge.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'badge_theme.dart'; @@ -35,6 +36,7 @@ class Badge extends StatelessWidget { this.textStyle, this.padding, this.alignment, + this.offset, this.label, this.isLabelVisible = true, this.child, @@ -54,6 +56,7 @@ class Badge extends StatelessWidget { this.textStyle, this.padding, this.alignment, + this.offset, required int count, this.isLabelVisible = true, this.child, @@ -106,13 +109,29 @@ class Badge extends StatelessWidget { /// left and right if the theme's value is null. final EdgeInsetsGeometry? padding; - /// The location of the [label] relative to the [child]. + /// Combined with [offset] to determine the location of the [label] + /// relative to the [child]. + /// + /// The alignment positions the label in the same way a child of an + /// [Align] widget is positioned, except that, the alignment is + /// resolved as if the label was a [largeSize] square and [offset] + /// is added to the result. /// /// This value is only used if [label] is non-null. /// - /// Defaults to the [BadgeTheme]'s alignment, or `start = 12` - /// and `top = -4` if the theme's value is null. - final AlignmentDirectional? alignment; + /// Defaults to the [BadgeTheme]'s alignment, or + /// [AlignmentDirectional.topEnd] if the theme's value is null. + final AlignmentGeometry? alignment; + + /// Combined with [alignment] to determine the location of the [label] + /// relative to the [child]. + /// + /// This value is only used if [label] is non-null. + /// + /// Defaults to the [BadgeTheme]'s offset, or + /// if the theme's value is null then `Offset(4, -4)` for + /// [TextDirection.ltr] or `Offset(-4, -4)` for [TextDirection.rtl]. + final Offset? offset; /// The badge's content, typically a [Text] widget that contains 1 to 4 /// characters. @@ -168,24 +187,99 @@ class Badge extends StatelessWidget { return badge; } - final AlignmentDirectional effectiveAlignment = alignment ?? badgeTheme.alignment ?? defaults.alignment!; + final AlignmentGeometry effectiveAlignment = alignment ?? badgeTheme.alignment ?? defaults.alignment!; + final TextDirection textDirection = Directionality.of(context); + final Offset defaultOffset = textDirection == TextDirection.ltr ? const Offset(4, -4) : const Offset(-4, -4); + final Offset effectiveOffset = offset ?? badgeTheme.offset ?? defaultOffset; + return Stack( clipBehavior: Clip.none, children: [ child!, - Positioned.directional( - textDirection: Directionality.of(context), - start: label == null ? null : effectiveAlignment.start, - end: label == null ? 0 : null, - top: label == null ? 0 : effectiveAlignment.y, - child: badge, + Positioned.fill( + child: _Badge( + alignment: effectiveAlignment, + offset: label == null ? Offset.zero : effectiveOffset, + textDirection: textDirection, + child: badge, + ), ), ], ); } } +class _Badge extends SingleChildRenderObjectWidget { + const _Badge({ + required this.alignment, + required this.offset, + required this.textDirection, + super.child, // the badge + }); + + final AlignmentGeometry alignment; + final Offset offset; + final TextDirection textDirection; + + @override + _RenderBadge createRenderObject(BuildContext context) { + return _RenderBadge( + alignment: alignment, + offset: offset, + textDirection: Directionality.maybeOf(context), + ); + } + + @override + void updateRenderObject(BuildContext context, _RenderBadge renderObject) { + renderObject + ..alignment = alignment + ..offset = offset + ..textDirection = Directionality.maybeOf(context); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('alignment', alignment)); + properties.add(DiagnosticsProperty('offset', offset)); + } +} + +class _RenderBadge extends RenderAligningShiftedBox { + _RenderBadge({ + super.textDirection, + super.alignment, + required Offset offset, + }) : _offset = offset; + + Offset get offset => _offset; + Offset _offset; + set offset(Offset value) { + if (_offset == value) { + return; + } + _offset = value; + markNeedsLayout(); + } + + @override + void performLayout() { + final BoxConstraints constraints = this.constraints; + assert(constraints.hasBoundedWidth); + assert(constraints.hasBoundedHeight); + size = constraints.biggest; + + child!.layout(const BoxConstraints(), parentUsesSize: true); + final double badgeSize = child!.size.height; + final Alignment resolvedAlignment = alignment.resolve(textDirection); + final BoxParentData childParentData = child!.parentData! as BoxParentData; + childParentData.offset = offset + resolvedAlignment.alongOffset(Offset(size.width - badgeSize, size.height - badgeSize)); + } +} + + // BEGIN GENERATED TOKEN PROPERTIES - Badge // Do not edit by hand. The code between the "BEGIN GENERATED" and @@ -200,7 +294,7 @@ class _BadgeDefaultsM3 extends BadgeThemeData { smallSize: 6.0, largeSize: 16.0, padding: const EdgeInsets.symmetric(horizontal: 4), - alignment: const AlignmentDirectional(12, -4), + alignment: AlignmentDirectional.topEnd, ); final BuildContext context; diff --git a/packages/flutter/lib/src/material/badge_theme.dart b/packages/flutter/lib/src/material/badge_theme.dart index f56f84999f7..d10dec2e73c 100644 --- a/packages/flutter/lib/src/material/badge_theme.dart +++ b/packages/flutter/lib/src/material/badge_theme.dart @@ -41,6 +41,7 @@ class BadgeThemeData with Diagnosticable { this.textStyle, this.padding, this.alignment, + this.offset, }); /// Overrides the default value for [Badge.backgroundColor]. @@ -62,7 +63,10 @@ class BadgeThemeData with Diagnosticable { final EdgeInsetsGeometry? padding; /// Overrides the default value for [Badge.alignment]. - final AlignmentDirectional? alignment; + final AlignmentGeometry? alignment; + + /// Overrides the default value for [Badge.offset]. + final Offset? offset; /// Creates a copy of this object but with the given fields replaced with the /// new values. @@ -73,7 +77,8 @@ class BadgeThemeData with Diagnosticable { double? largeSize, TextStyle? textStyle, EdgeInsetsGeometry? padding, - AlignmentDirectional? alignment, + AlignmentGeometry? alignment, + Offset? offset, }) { return BadgeThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, @@ -83,6 +88,7 @@ class BadgeThemeData with Diagnosticable { textStyle: textStyle ?? this.textStyle, padding: padding ?? this.padding, alignment: alignment ?? this.alignment, + offset: offset ?? this.offset, ); } @@ -95,7 +101,8 @@ class BadgeThemeData with Diagnosticable { largeSize: lerpDouble(a?.largeSize, b?.largeSize, t), textStyle: TextStyle.lerp(a?.textStyle, b?.textStyle, t), padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t), - alignment: AlignmentDirectional.lerp(a?.alignment, b?.alignment, t), + alignment: AlignmentGeometry.lerp(a?.alignment, b?.alignment, t), + offset: Offset.lerp(a?.offset, b?.offset, t), ); } @@ -108,6 +115,7 @@ class BadgeThemeData with Diagnosticable { textStyle, padding, alignment, + offset, ); @override @@ -125,7 +133,8 @@ class BadgeThemeData with Diagnosticable { && other.largeSize == largeSize && other.textStyle == textStyle && other.padding == padding - && other.alignment == alignment; + && other.alignment == alignment + && other.offset == offset; } @override @@ -137,7 +146,8 @@ class BadgeThemeData with Diagnosticable { properties.add(DoubleProperty('largeSize', largeSize, defaultValue: null)); properties.add(DiagnosticsProperty('textStyle', textStyle, defaultValue: null)); properties.add(DiagnosticsProperty('padding', padding, defaultValue: null)); - properties.add(DiagnosticsProperty('alignment', alignment, defaultValue: null)); + properties.add(DiagnosticsProperty('alignment', alignment, defaultValue: null)); + properties.add(DiagnosticsProperty('offset', offset, defaultValue: null)); } } diff --git a/packages/flutter/test/material/badge_test.dart b/packages/flutter/test/material/badge_test.dart index 6de9f4c9b5b..8f853bf2f3b 100644 --- a/packages/flutter/test/material/badge_test.dart +++ b/packages/flutter/test/material/badge_test.dart @@ -37,7 +37,8 @@ void main() { theme.textTheme.labelSmall!.copyWith(color: theme.colorScheme.onError), ); - // default badge alignment = AlignmentDirectional(12, -4) + // default badge alignment = AlignmentDirection.topEnd + // default offset for LTR = Offset(4, -4) // default padding = EdgeInsets.symmetric(horizontal: 4) // default largeSize = 16 // '0'.width = 12 @@ -46,16 +47,9 @@ void main() { expect(tester.getSize(find.byType(Badge)), const Size(24, 24)); // default Icon size expect(tester.getTopLeft(find.byType(Badge)), Offset.zero); - // x = alignment.start + padding.left - // y = alignment.top expect(tester.getTopLeft(find.text('0')), const Offset(16, -4)); final RenderBox box = tester.renderObject(find.byType(Badge)); - // '0'.width = 12 - // L = alignment.start - // T = alignment.top - // R = L + '0'.width + padding.width - // B = T + largeSize, R = largeSize/2 expect(box, paints..rrect(rrect: RRect.fromLTRBR(12, -4, 32, 12, const Radius.circular(8)), color: theme.colorScheme.error)); }); @@ -89,26 +83,13 @@ void main() { theme.textTheme.labelSmall!.copyWith(color: theme.colorScheme.onError), ); - // default badge alignment = AlignmentDirectional(12, -4) - // default padding = EdgeInsets.symmetric(horizontal: 4) - // default largeSize = 16 - // '0'.width = 12 - // icon.width = 24 - expect(tester.getSize(find.byType(Badge)), const Size(24, 24)); // default Icon size expect(tester.getTopLeft(find.byType(Badge)), Offset.zero); - // x = icon.width - alignment.start - '0'.width - padding.right - // y = alignment.top - expect(tester.getTopLeft(find.text('0')), const Offset(-4, -4)); + expect(tester.getTopLeft(find.text('0')), const Offset(0, -4)); final RenderBox box = tester.renderObject(find.byType(Badge)); - // L = icon.width - alignment.start - '0.width' - padding.width - // T = alignment.top - // R = L + '0.width' + padding.width - // B = T + largeSize - // R = largeSize/2 - expect(box, paints..rrect(rrect: RRect.fromLTRBR(-8, -4, 12, 12, const Radius.circular(8)), color: theme.colorScheme.error)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(-4, -4, 16, 12, const Radius.circular(8)), color: theme.colorScheme.error)); }); // Essentially the same as 'Large Badge defaults' @@ -282,4 +263,153 @@ void main() { final RenderBox box = tester.renderObject(find.byType(Badge)); expect(box, isNot(paints..rrect())); }); + + testWidgets('Large Badge alignment', (WidgetTester tester) async { + const Radius badgeRadius = Radius.circular(8); + + Widget buildFrame(Alignment alignment, [Offset offset = Offset.zero]) { + return MaterialApp( + theme: ThemeData.light(useMaterial3: true), + home: Align( + alignment: Alignment.topLeft, + child: Badge( + // Default largeSize = 16, badge with label is "large". + label: Container(width: 8, height: 8, color: Colors.blue), + alignment: alignment, + offset: offset, + child: Container( + color: const Color(0xFF00FF00), + width: 200, + height: 200, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildFrame(Alignment.topLeft)); + final RenderBox box = tester.renderObject(find.byType(Badge)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(0, 0, 16, 16, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.topCenter)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(100 - 8, 0, 100 + 8, 16, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.topRight)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(200 - 16, 0, 200, 16, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.centerLeft)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(0, 100 - 8, 16, 100 + 8, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.centerRight)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(200 - 16, 100 - 8, 200, 100 + 8, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.bottomLeft)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(0, 200 - 16, 16, 200, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.bottomCenter)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(100 - 8, 200 - 16, 100 + 8, 200, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.bottomRight)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(200 - 16, 200 - 16, 200, 200, badgeRadius))); + + const Offset offset = Offset(5, 10); + + await tester.pumpWidget(buildFrame(Alignment.topLeft, offset)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(0, 0, 16, 16, badgeRadius).shift(offset))); + + await tester.pumpWidget(buildFrame(Alignment.topCenter, offset)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(100 - 8, 0, 100 + 8, 16, badgeRadius).shift(offset))); + + await tester.pumpWidget(buildFrame(Alignment.topRight, offset)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(200 - 16, 0, 200, 16, badgeRadius).shift(offset))); + + await tester.pumpWidget(buildFrame(Alignment.centerLeft, offset)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(0, 100 - 8, 16, 100 + 8, badgeRadius).shift(offset))); + + await tester.pumpWidget(buildFrame(Alignment.centerRight, offset)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(200 - 16, 100 - 8, 200, 100 + 8, badgeRadius).shift(offset))); + + await tester.pumpWidget(buildFrame(Alignment.bottomLeft, offset)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(0, 200 - 16, 16, 200, badgeRadius).shift(offset))); + + await tester.pumpWidget(buildFrame(Alignment.bottomCenter, offset)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(100 - 8, 200 - 16, 100 + 8, 200, badgeRadius).shift(offset))); + + await tester.pumpWidget(buildFrame(Alignment.bottomRight, offset)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(200 - 16, 200 - 16, 200, 200, badgeRadius).shift(offset))); + }); + + testWidgets('Small Badge alignment', (WidgetTester tester) async { + const Radius badgeRadius = Radius.circular(3); + + Widget buildFrame(Alignment alignment, [Offset offset = Offset.zero]) { + return MaterialApp( + theme: ThemeData.light(useMaterial3: true), + home: Align( + alignment: Alignment.topLeft, + child: Badge( + // Default smallSize = 6, badge without label is "small". + alignment: alignment, + offset: offset, // Not used for smallSize badges. + child: Container( + color: const Color(0xFF00FF00), + width: 200, + height: 200, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildFrame(Alignment.topLeft)); + final RenderBox box = tester.renderObject(find.byType(Badge)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(0, 0, 6, 6, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.topCenter)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(100 - 3, 0, 100 + 3, 6, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.topRight)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(200 - 6, 0, 200, 6, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.centerLeft)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(0, 100 - 3, 6, 100 + 3, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.centerRight)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(200 - 6, 100 - 3, 200, 100 + 3, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.bottomLeft)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(0, 200 - 6, 6, 200, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.bottomCenter)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(100 - 3, 200 - 6, 100 + 3, 200, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.bottomRight)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(200 - 6, 200 - 6, 200, 200, badgeRadius))); + + const Offset offset = Offset(5, 10); // Not used for smallSize Badges. + + await tester.pumpWidget(buildFrame(Alignment.topLeft, offset)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(0, 0, 6, 6, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.topCenter, offset)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(100 - 3, 0, 100 + 3, 6, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.topRight, offset)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(200 - 6, 0, 200, 6, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.centerLeft, offset)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(0, 100 - 3, 6, 100 + 3, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.centerRight, offset)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(200 - 6, 100 - 3, 200, 100 + 3, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.bottomLeft, offset)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(0, 200 - 6, 6, 200, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.bottomCenter, offset)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(100 - 3, 200 - 6, 100 + 3, 200, badgeRadius))); + + await tester.pumpWidget(buildFrame(Alignment.bottomRight, offset)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(200 - 6, 200 - 6, 200, 200, badgeRadius))); + }); } diff --git a/packages/flutter/test/material/badge_theme_test.dart b/packages/flutter/test/material/badge_theme_test.dart index ea16156eb2d..8350c62cff5 100644 --- a/packages/flutter/test/material/badge_theme_test.dart +++ b/packages/flutter/test/material/badge_theme_test.dart @@ -23,6 +23,7 @@ void main() { expect(themeData.textStyle, null); expect(themeData.padding, null); expect(themeData.alignment, null); + expect(themeData.offset, null); }); testWidgets('Default BadgeThemeData debugFillProperties', (WidgetTester tester) async { @@ -47,6 +48,7 @@ void main() { textStyle: TextStyle(fontSize: 4), padding: EdgeInsets.all(5), alignment: AlignmentDirectional(6, 7), + offset: Offset.zero, ).debugFillProperties(builder); final List description = builder.properties @@ -61,7 +63,8 @@ void main() { 'largeSize: 2.0', 'textStyle: TextStyle(inherit: true, size: 4.0)', 'padding: EdgeInsets.all(5.0)', - 'alignment: AlignmentDirectional(6.0, 7.0)' + 'alignment: AlignmentDirectional(6.0, 7.0)', + 'offset: Offset(0.0, 0.0)' ]); }); @@ -75,7 +78,8 @@ void main() { largeSize: 20, textStyle: TextStyle(fontSize: 12), padding: EdgeInsets.symmetric(horizontal: 5), - alignment: AlignmentDirectional(24, 0), + alignment: Alignment.topRight, + offset: Offset(24, 0), ); await tester.pumpWidget( @@ -95,8 +99,7 @@ void main() { // text width = 48 = fontSize * 4, text height = fontSize expect(tester.getSize(find.text('1234')), const Size(48, 12)); - // x = 29 = alignment.start + padding.left, y = 4 = (largeSize - fontSize) / 2 - expect(tester.getTopLeft(find.text('1234')), const Offset(29, 4)); + expect(tester.getTopLeft(find.text('1234')), const Offset(33, 4)); expect(tester.getSize(find.byType(Badge)), const Size(24, 24)); // default Icon size @@ -107,8 +110,7 @@ void main() { expect(textStyle.color, black); final RenderBox box = tester.renderObject(find.byType(Badge)); - // L = alignment.start, T = alignment.top, R = L + fontSize * 4 + padding.width, B = largeSize R = largeSize/2 - expect(box, paints..rrect(rrect: RRect.fromLTRBR(24, 0, 82, 20, const Radius.circular(10)), color: green)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(28, 0, 86, 20, const Radius.circular(10)), color: green)); }); @@ -125,7 +127,8 @@ void main() { largeSize: 20, textStyle: TextStyle(fontSize: 12), padding: EdgeInsets.symmetric(horizontal: 5), - alignment: AlignmentDirectional(24, 0), + alignment: Alignment.topRight, + offset: Offset(24, 0), ); await tester.pumpWidget( @@ -143,13 +146,13 @@ void main() { ); expect(tester.getSize(find.text('1234')), const Size(48, 12)); - expect(tester.getTopLeft(find.text('1234')), const Offset(29, 4)); + expect(tester.getTopLeft(find.text('1234')), const Offset(33, 4)); expect(tester.getSize(find.byType(Badge)), const Size(24, 24)); // default Icon size expect(tester.getTopLeft(find.byType(Badge)), Offset.zero); final TextStyle textStyle = tester.renderObject(find.text('1234')).text.style!; expect(textStyle.fontSize, 12); expect(textStyle.color, black); final RenderBox box = tester.renderObject(find.byType(Badge)); - expect(box, paints..rrect(rrect: RRect.fromLTRBR(24, 0, 82, 20, const Radius.circular(10)), color: green)); + expect(box, paints..rrect(rrect: RRect.fromLTRBR(28, 0, 86, 20, const Radius.circular(10)), color: green)); }); }