diff --git a/packages/material_ui/tool/gen_defaults/templates/template.dart b/packages/material_ui/tool/gen_defaults/templates/template.dart index d619a217766f..b196e8d5f8e8 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 'dart:io'; 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 @@ abstract class TokenTemplate { /// 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 @@ abstract class TokenTemplate { ); 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 @@ abstract class TokenTemplate { /// 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 11cef5f15769..b8676b9cbae7 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 @@ void main() { }); } + 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().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 _fileHeader = ''' '''; 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 8eb7fd0d466a..d400b72840a6 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 01442bf46fb1..735382ce5c41 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 @@ class M3IconButtonTemplate extends M3TokenTemplate { 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)}; } '''; }