From e0370152e5f4694d52471d04853ba5324b5af48c Mon Sep 17 00:00:00 2001 From: Qun Cheng Date: Mon, 6 Jul 2026 12:57:05 -0700 Subject: [PATCH 1/2] Add gen_defaults template helpers --- .../tool/gen_defaults/templates/template.dart | 63 +++++++++++- .../gen_defaults/test/gen_defaults_test.dart | 99 ++++++++++++++++++- .../test_fixtures/icon_button_token_data.dart | 15 +++ .../test/test_fixtures/test_templates.dart | 11 +++ 4 files changed, 181 insertions(+), 7 deletions(-) diff --git a/packages/material_ui/tool/gen_defaults/templates/template.dart b/packages/material_ui/tool/gen_defaults/templates/template.dart index d619a217766f..c220c52e27be 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,58 @@ 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 double topLeft = shape.topLeft; + final double topRight = shape.topRight; + final double bottomLeft = shape.bottomLeft; + final double bottomRight = shape.bottomRight; + 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)}; } '''; } From 6b230c0d6e9675b9b136dc6334fd7bb68df837fb Mon Sep 17 00:00:00 2001 From: Qun Cheng Date: Tue, 7 Jul 2026 19:06:50 -0700 Subject: [PATCH 2/2] address feedback --- .../tool/gen_defaults/templates/template.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/material_ui/tool/gen_defaults/templates/template.dart b/packages/material_ui/tool/gen_defaults/templates/template.dart index c220c52e27be..b196e8d5f8e8 100644 --- a/packages/material_ui/tool/gen_defaults/templates/template.dart +++ b/packages/material_ui/tool/gen_defaults/templates/template.dart @@ -119,10 +119,12 @@ abstract class TokenTemplate { String shape(ShapeStruct shape, [String prefix = 'const ']) { switch (shape.family) { case 'SHAPE_FAMILY_ROUNDED_CORNERS': - final double topLeft = shape.topLeft; - final double topRight = shape.topRight; - final double bottomLeft = shape.bottomLeft; - final double bottomRight = shape.bottomRight; + final ShapeStruct( + :double topLeft, + :double topRight, + :double bottomLeft, + :double bottomRight, + ) = shape; if (topLeft == topRight && topLeft == bottomLeft && topLeft == bottomRight) { if (topLeft == 0) { return '${prefix}RoundedRectangleBorder()';