Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 61 additions & 4 deletions packages/material_ui/tool/gen_defaults/templates/template.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -70,17 +73,17 @@ 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),
'The template name "$name" must use spaces and capitalized words (e.g., "Typography" or "Icon Button").',
);
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',
};
}

Expand All @@ -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)})';
}
Comment thread
QuncCccccc marked this conversation as resolved.

/// 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(' ', '_');
Expand Down
99 changes: 96 additions & 3 deletions packages/material_ui/tool/gen_defaults/test/gen_defaults_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<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();
Expand Down Expand Up @@ -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';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)};
}
''';
}
Expand Down