[material_ui] Add helper methods in gen_defaults template (#12121)
diff --git a/packages/material_ui/tool/gen_defaults/templates/template.dart b/packages/material_ui/tool/gen_defaults/templates/template.dart
index d619a21..b196e8d 100644
--- a/packages/material_ui/tool/gen_defaults/templates/template.dart
+++ b/packages/material_ui/tool/gen_defaults/templates/template.dart
@@ -6,6 +6,9 @@
import 'package:meta/meta.dart';
+import '../data/color_role.dart';
+import '../data/shape_struct.dart';
+
enum _MaterialVersion { material3, material3Expressive }
/// A template for generating Material 3 component defaults.
@@ -70,8 +73,8 @@
/// The Material version this template is for.
_MaterialVersion get _version;
- /// The name of the class that will be generated (e.g. `_M3IconButtonDefaults`
- /// or `_M3EIconButtonDefaults`).
+ /// The name of the class that will be generated (e.g. `_IconButtonDefaultsM3`
+ /// or `_IconButtonDefaultsM3E`).
String get _className {
assert(
_nameRegExp.hasMatch(name),
@@ -79,8 +82,8 @@
);
final String camelName = name.replaceAll(' ', '');
return switch (_version) {
- _MaterialVersion.material3 => '_M3${camelName}Defaults',
- _MaterialVersion.material3Expressive => '_M3E${camelName}Defaults',
+ _MaterialVersion.material3 => '_${camelName}DefaultsM3',
+ _MaterialVersion.material3Expressive => '_${camelName}DefaultsM3E',
};
}
@@ -93,6 +96,60 @@
/// The [className] parameter must be used to declare the class.
String generateContents(String className);
+ /// Generates a Dart number literal for token values.
+ String number(num value) => value.toString();
+
+ /// Generates a [ColorScheme] color expression for the given token.
+ String color(TokenColorRole role, String prefix) => '$prefix.${role.name}';
+
+ /// Generates a color expression with opacity applied.
+ String colorWithOpacity(TokenColorRole role, double opacity, String prefix) {
+ if (opacity == 1.0) {
+ return color(role, prefix);
+ }
+ return '${color(role, prefix)}.withOpacity(${number(opacity)})';
+ }
+
+ /// Generates an [OutlinedBorder] expression for a shape token.
+ ///
+ /// Currently supports:
+ /// - `SHAPE_FAMILY_ROUNDED_CORNERS`, which maps to
+ /// [RoundedRectangleBorder].
+ /// - `SHAPE_FAMILY_CIRCULAR`, which maps to [StadiumBorder].
+ String shape(ShapeStruct shape, [String prefix = 'const ']) {
+ switch (shape.family) {
+ case 'SHAPE_FAMILY_ROUNDED_CORNERS':
+ final ShapeStruct(
+ :double topLeft,
+ :double topRight,
+ :double bottomLeft,
+ :double bottomRight,
+ ) = shape;
+ if (topLeft == topRight && topLeft == bottomLeft && topLeft == bottomRight) {
+ if (topLeft == 0) {
+ return '${prefix}RoundedRectangleBorder()';
+ }
+ return '${prefix}RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(${number(topLeft)})))';
+ }
+ if (topLeft == topRight && bottomLeft == bottomRight) {
+ return '${prefix}RoundedRectangleBorder(borderRadius: BorderRadius.vertical('
+ '${topLeft > 0 ? 'top: Radius.circular(${number(topLeft)})' : ''}'
+ '${topLeft > 0 && bottomLeft > 0 ? ', ' : ''}'
+ '${bottomLeft > 0 ? 'bottom: Radius.circular(${number(bottomLeft)})' : ''}'
+ '))';
+ }
+ return '${prefix}RoundedRectangleBorder(borderRadius: '
+ 'BorderRadius.only('
+ 'topLeft: Radius.circular(${number(topLeft)}), '
+ 'topRight: Radius.circular(${number(topRight)}), '
+ 'bottomLeft: Radius.circular(${number(bottomLeft)}), '
+ 'bottomRight: Radius.circular(${number(bottomRight)})))';
+ case 'SHAPE_FAMILY_CIRCULAR':
+ return '${prefix}StadiumBorder()';
+ }
+ throw UnsupportedError('Unsupported shape family type: ${shape.family}');
+ }
+
/// Generates the file under the target path [materialLib] and formats it.
void generateFile({bool verbose = false}) {
final String snakeName = name.toLowerCase().replaceAll(' ', '_');
diff --git a/packages/material_ui/tool/gen_defaults/test/gen_defaults_test.dart b/packages/material_ui/tool/gen_defaults/test/gen_defaults_test.dart
index 11cef5f..b8676b9 100644
--- a/packages/material_ui/tool/gen_defaults/test/gen_defaults_test.dart
+++ b/packages/material_ui/tool/gen_defaults/test/gen_defaults_test.dart
@@ -5,6 +5,8 @@
import 'dart:io';
import 'package:test/test.dart';
+import '../data/color_role.dart';
+import '../data/shape_struct.dart';
import '../templates/template.dart';
import 'test_fixtures/test_templates.dart';
@@ -66,6 +68,85 @@
});
}
+ test('color generates color expression', () {
+ final template = M3IconButtonTemplate(testPath());
+ expect(template.color(TokenColorRole.onSurface, '_colors'), '_colors.onSurface');
+ });
+
+ test('colorWithOpacity generates color expression with opacity', () {
+ final template = M3IconButtonTemplate(testPath());
+ expect(
+ template.colorWithOpacity(TokenColorRole.onSurface, 0.12, '_colors'),
+ '_colors.onSurface.withOpacity(0.12)',
+ );
+ expect(
+ template.colorWithOpacity(TokenColorRole.onSurface, 1.0, '_colors'),
+ '_colors.onSurface',
+ );
+ });
+
+ test('shape generates shape expressions', () {
+ final template = M3IconButtonTemplate(testPath());
+ expect(
+ template.shape(
+ const ShapeStruct(
+ family: 'SHAPE_FAMILY_ROUNDED_CORNERS',
+ topLeft: 8.0,
+ topRight: 8.0,
+ bottomLeft: 8.0,
+ bottomRight: 8.0,
+ ),
+ ),
+ 'const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0)))',
+ );
+ expect(
+ template.shape(
+ const ShapeStruct(
+ family: 'SHAPE_FAMILY_ROUNDED_CORNERS',
+ topLeft: 8.0,
+ topRight: 8.0,
+ bottomLeft: 4.0,
+ bottomRight: 4.0,
+ ),
+ ),
+ 'const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(8.0), bottom: Radius.circular(4.0)))',
+ );
+ expect(
+ template.shape(
+ const ShapeStruct(
+ family: 'SHAPE_FAMILY_CIRCULAR',
+ topLeft: 0.0,
+ topRight: 0.0,
+ bottomLeft: 0.0,
+ bottomRight: 0.0,
+ ),
+ ),
+ 'const StadiumBorder()',
+ );
+ });
+
+ test('shape throws UnsupportedError for unsupported shape family', () {
+ final template = M3IconButtonTemplate(testPath());
+ expect(
+ () => template.shape(
+ const ShapeStruct(
+ family: 'SHAPE_FAMILY_UNKNOWN',
+ topLeft: 0.0,
+ topRight: 0.0,
+ bottomLeft: 0.0,
+ bottomRight: 0.0,
+ ),
+ ),
+ throwsA(
+ isA<UnsupportedError>().having(
+ (UnsupportedError e) => e.message,
+ 'message',
+ 'Unsupported shape family type: SHAPE_FAMILY_UNKNOWN',
+ ),
+ ),
+ );
+ });
+
test('will run dart format over the generated file', () {
final template = UnformattedTemplate(testPath());
template.generateFile();
@@ -115,21 +196,33 @@
''';
const _buttonExpressiveDefaultsClass = '''
-class _M3EIconButtonDefaults {
+class _IconButtonDefaultsM3E {
static const double height = 40.0;
static const double borderRadius = 8.0;
}
''';
const _buttonDefaultsClass = '''
-class _M3IconButtonDefaults {
+class _IconButtonDefaultsM3 {
+ _IconButtonDefaultsM3(this.context);
+
+ final BuildContext context;
+ late final ColorScheme _colors = Theme.of(context).colorScheme;
+
static const double height = 40.0;
static const double borderRadius = 8.0;
+ Color get iconColor => _colors.onSurfaceVariant;
+ Color get disabledIconColor => _colors.onSurface.withOpacity(0.38);
+ Color get hoveredStateLayerColor =>
+ _colors.onSurfaceVariant.withOpacity(0.08);
+ OutlinedBorder get shape => const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(8.0)),
+ );
}
''';
const formattedClass = '''
-class _M3UnformattedDefaults {
+class _UnformattedDefaultsM3 {
final int x = 1;
final String y = 'hello';
}
diff --git a/packages/material_ui/tool/gen_defaults/test/test_fixtures/icon_button_token_data.dart b/packages/material_ui/tool/gen_defaults/test/test_fixtures/icon_button_token_data.dart
index 8eb7fd0..d400b72 100644
--- a/packages/material_ui/tool/gen_defaults/test/test_fixtures/icon_button_token_data.dart
+++ b/packages/material_ui/tool/gen_defaults/test/test_fixtures/icon_button_token_data.dart
@@ -2,7 +2,22 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import '../../data/color_role.dart';
+import '../../data/shape_struct.dart';
+
class TokenIconButton {
static const double height = 40.0;
static const double borderRadius = 8.0;
+ static const TokenColorRole iconColor = TokenColorRole.onSurfaceVariant;
+ static const TokenColorRole disabledIconColor = TokenColorRole.onSurface;
+ static const double disabledIconOpacity = 0.38;
+ static const TokenColorRole hoveredStateLayerColor = TokenColorRole.onSurfaceVariant;
+ static const double hoveredStateLayerOpacity = 0.08;
+ static const ShapeStruct pressedContainerShape = ShapeStruct(
+ family: 'SHAPE_FAMILY_ROUNDED_CORNERS',
+ topLeft: 8.00,
+ topRight: 8.00,
+ bottomLeft: 8.00,
+ bottomRight: 8.00,
+ );
}
diff --git a/packages/material_ui/tool/gen_defaults/test/test_fixtures/test_templates.dart b/packages/material_ui/tool/gen_defaults/test/test_fixtures/test_templates.dart
index 01442bf..735382c 100644
--- a/packages/material_ui/tool/gen_defaults/test/test_fixtures/test_templates.dart
+++ b/packages/material_ui/tool/gen_defaults/test/test_fixtures/test_templates.dart
@@ -48,8 +48,19 @@
String generateContents(String className) {
return '''
class $className {
+ $className(this.context);
+
+ final BuildContext context;
+ late final ColorScheme _colors = Theme.of(context).colorScheme;
+
static const double height = ${TokenIconButton.height};
static const double borderRadius = ${TokenIconButton.borderRadius};
+ Color get iconColor => ${color(TokenIconButton.iconColor, '_colors')};
+ Color get disabledIconColor =>
+ ${colorWithOpacity(TokenIconButton.disabledIconColor, TokenIconButton.disabledIconOpacity, '_colors')};
+ Color get hoveredStateLayerColor =>
+ ${colorWithOpacity(TokenIconButton.hoveredStateLayerColor, TokenIconButton.hoveredStateLayerOpacity, '_colors')};
+ OutlinedBorder get shape => ${shape(TokenIconButton.pressedContainerShape)};
}
''';
}