diff --git a/dev/tools/update_icons.dart b/dev/tools/update_icons.dart index 41a5c4c7bd4..cc0bed70b40 100644 --- a/dev/tools/update_icons.dart +++ b/dev/tools/update_icons.dart @@ -20,8 +20,20 @@ const String _defaultNewCodepointsPath = 'codepoints'; const String _defaultOldCodepointsPath = 'bin/cache/artifacts/material_fonts/codepoints'; const String _defaultIconsPath = 'packages/flutter/lib/src/material/icons.dart'; -const String _beginGeneratedMark = '// BEGIN GENERATED'; -const String _endGeneratedMark = '// END GENERATED'; +const String _beginGeneratedMark = '// BEGIN GENERATED ICONS'; +const String _endGeneratedMark = '// END GENERATED ICONS'; +const String _beginPlatformAdaptiveGeneratedMark = '// BEGIN GENERATED PLATFORM ADAPTIVE ICONS'; +const String _endPlatformAdaptiveGeneratedMark = '// END GENERATED PLATFORM ADAPTIVE ICONS'; + +const Map> _platformAdaptiveIdentifiers = >{ + // Mapping of Flutter IDs to an Android/agnostic ID and an iOS ID. + // Flutter IDs can be anything, but should be chosen to be agnostic. + 'arrow_back': ['arrow_back', 'arrow_back_ios'], + 'arrow_forward': ['arrow_forward', 'arrow_forward_ios'], + 'flip_camera': ['flip_camera_android', 'flip_camera_ios'], + 'more': ['more_vert', 'more_horiz'], + 'share': ['share', 'ios_share'], +}; const Map _identifierRewrites = { '360': 'threesixty', @@ -231,20 +243,54 @@ Map stringToTokenPairMap(String codepointData) { // Do not make this method private as it is used by g3 roll. String regenerateIconsFile(String iconData, Map tokenPairMap) { + final Iterable<_Icon> newIcons = tokenPairMap.entries.map((MapEntry entry) => _Icon(entry)); final StringBuffer buf = StringBuffer(); bool generating = false; + for (final String line in LineSplitter.split(iconData)) { if (!generating) { buf.writeln(line); } - if (line.contains(_beginGeneratedMark)) { + + // Generate for _PlatformAdaptiveIcons + if (line.contains(_beginPlatformAdaptiveGeneratedMark)) { generating = true; - final String iconDeclarationsString = [ - for (MapEntry entry in tokenPairMap.entries) - _Icon(entry).fullDeclaration - ].join(); + final List platformAdaptiveDeclarations = []; + _platformAdaptiveIdentifiers.forEach((String flutterId, List ids) { + // Automatically finds and generates styled icon declarations. + for (final IconStyle iconStyle in IconStyle.values) { + final String style = iconStyle.idSuffix(); + try { + final _Icon agnosticIcon = newIcons.firstWhere( + (_Icon icon) => icon.id == '${ids[0]}$style', + orElse: () => throw ids[0]); + final _Icon iOSIcon = newIcons.firstWhere( + (_Icon icon) => icon.id == '${ids[1]}$style', + orElse: () => throw ids[1]); + platformAdaptiveDeclarations.add(_Icon.platformAdaptiveDeclaration('$flutterId$style', agnosticIcon, iOSIcon)); + } catch (e) { + if (iconStyle == IconStyle.regular) { + stderr.writeln("Error while generating platformAdaptiveDeclarations: Icon '$e' not found."); + exit(1); + } else { + // Ignore errors for styled icons since some don't exist. + } + } + } + }); + + buf.write(platformAdaptiveDeclarations.join()); + } else if (line.contains(_endPlatformAdaptiveGeneratedMark)) { + generating = false; + buf.writeln(line); + } + + // Generate for Icons + if (line.contains(_beginGeneratedMark)) { + generating = true; + final String iconDeclarationsString = newIcons.map((_Icon icon) => icon.fullDeclaration).join(''); buf.write(iconDeclarationsString); } else if (line.contains(_endGeneratedMark)) { generating = false; @@ -275,9 +321,9 @@ enum IconStyle { sharp, } -extension IconStyleSuffix on IconStyle { +extension IconStyleExtension on IconStyle { // The suffix for the 'material-icons' HTML class. - String suffix() { + String htmlSuffix() { switch (this) { case IconStyle.outlined: return '-outlined'; case IconStyle.rounded: return '-round'; @@ -285,6 +331,17 @@ extension IconStyleSuffix on IconStyle { default: return ''; } } + + // The suffix for icon ids. + String idSuffix() { + switch (this) { + case IconStyle.outlined: + case IconStyle.rounded: + case IconStyle.sharp: + return '_' + toString().split('.').last; + default: return ''; + } + } } class _Icon { @@ -332,12 +389,25 @@ class _Icon { String get name => id.replaceAll('_', ' '); String get dartDoc => - '/// $shortId — material icon named "$name".'; + '$shortId — material icon named "$name"'; String get declaration => "static const IconData $flutterId = IconData(0x$hexCodepoint, fontFamily: 'MaterialIcons'$mirroredInRTL);"; - String get fullDeclaration => '''\n $dartDoc\n $declaration\n'''; + String get fullDeclaration => ''' + + /// $dartDoc. + $declaration +'''; + + static String platformAdaptiveDeclaration(String flutterId, _Icon agnosticIcon, _Icon iOSIcon) => ''' + + /// Platform-adaptive icon for ${agnosticIcon.dartDoc} and ${iOSIcon.dartDoc}.; + IconData get $flutterId => !_isCupertino() ? Icons.${agnosticIcon.flutterId} : Icons.${iOSIcon.flutterId}; +'''; + + @override + String toString() => id; } // Replace the old codepoints file with the new. diff --git a/packages/flutter/lib/src/material/icons.dart b/packages/flutter/lib/src/material/icons.dart index f39bc80db34..65d58802079 100644 --- a/packages/flutter/lib/src/material/icons.dart +++ b/packages/flutter/lib/src/material/icons.dart @@ -2,8 +2,81 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart' show defaultTargetPlatform; import 'package:flutter/widgets.dart'; +// ignore_for_file: non_constant_identifier_names +class _PlatformAdaptiveIcons { + static bool _isCupertino() { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + return false; + case TargetPlatform.iOS: + case TargetPlatform.macOS: + return true; + } + } + + // Generated code: do not hand-edit. + // See https://github.com/flutter/flutter/wiki/Updating-Material-Design-Fonts + // BEGIN GENERATED PLATFORM ADAPTIVE ICONS + + /// Platform-adaptive icon for arrow_back — material icon named "arrow back" and arrow_back_ios — material icon named "arrow back ios".; + IconData get arrow_back => !_isCupertino() ? Icons.arrow_back : Icons.arrow_back_ios; + + /// Platform-adaptive icon for arrow_back — material icon named "arrow back outlined" and arrow_back_ios — material icon named "arrow back ios outlined".; + IconData get arrow_back_outlined => !_isCupertino() ? Icons.arrow_back_outlined : Icons.arrow_back_ios_outlined; + + /// Platform-adaptive icon for arrow_back — material icon named "arrow back rounded" and arrow_back_ios — material icon named "arrow back ios rounded".; + IconData get arrow_back_rounded => !_isCupertino() ? Icons.arrow_back_rounded : Icons.arrow_back_ios_rounded; + + /// Platform-adaptive icon for arrow_back — material icon named "arrow back sharp" and arrow_back_ios — material icon named "arrow back ios sharp".; + IconData get arrow_back_sharp => !_isCupertino() ? Icons.arrow_back_sharp : Icons.arrow_back_ios_sharp; + + /// Platform-adaptive icon for arrow_forward — material icon named "arrow forward" and arrow_forward_ios — material icon named "arrow forward ios".; + IconData get arrow_forward => !_isCupertino() ? Icons.arrow_forward : Icons.arrow_forward_ios; + + /// Platform-adaptive icon for arrow_forward — material icon named "arrow forward outlined" and arrow_forward_ios — material icon named "arrow forward ios outlined".; + IconData get arrow_forward_outlined => !_isCupertino() ? Icons.arrow_forward_outlined : Icons.arrow_forward_ios_outlined; + + /// Platform-adaptive icon for arrow_forward — material icon named "arrow forward rounded" and arrow_forward_ios — material icon named "arrow forward ios rounded".; + IconData get arrow_forward_rounded => !_isCupertino() ? Icons.arrow_forward_rounded : Icons.arrow_forward_ios_rounded; + + /// Platform-adaptive icon for arrow_forward — material icon named "arrow forward sharp" and arrow_forward_ios — material icon named "arrow forward ios sharp".; + IconData get arrow_forward_sharp => !_isCupertino() ? Icons.arrow_forward_sharp : Icons.arrow_forward_ios_sharp; + + /// Platform-adaptive icon for flip_camera_android — material icon named "flip camera android" and flip_camera_ios — material icon named "flip camera ios".; + IconData get flip_camera => !_isCupertino() ? Icons.flip_camera_android : Icons.flip_camera_ios; + + /// Platform-adaptive icon for flip_camera_android — material icon named "flip camera android outlined" and flip_camera_ios — material icon named "flip camera ios outlined".; + IconData get flip_camera_outlined => !_isCupertino() ? Icons.flip_camera_android_outlined : Icons.flip_camera_ios_outlined; + + /// Platform-adaptive icon for flip_camera_android — material icon named "flip camera android rounded" and flip_camera_ios — material icon named "flip camera ios rounded".; + IconData get flip_camera_rounded => !_isCupertino() ? Icons.flip_camera_android_rounded : Icons.flip_camera_ios_rounded; + + /// Platform-adaptive icon for flip_camera_android — material icon named "flip camera android sharp" and flip_camera_ios — material icon named "flip camera ios sharp".; + IconData get flip_camera_sharp => !_isCupertino() ? Icons.flip_camera_android_sharp : Icons.flip_camera_ios_sharp; + + /// Platform-adaptive icon for more_vert — material icon named "more vert" and more_horiz — material icon named "more horiz".; + IconData get more => !_isCupertino() ? Icons.more_vert : Icons.more_horiz; + + /// Platform-adaptive icon for more_vert — material icon named "more vert outlined" and more_horiz — material icon named "more horiz outlined".; + IconData get more_outlined => !_isCupertino() ? Icons.more_vert_outlined : Icons.more_horiz_outlined; + + /// Platform-adaptive icon for more_vert — material icon named "more vert rounded" and more_horiz — material icon named "more horiz rounded".; + IconData get more_rounded => !_isCupertino() ? Icons.more_vert_rounded : Icons.more_horiz_rounded; + + /// Platform-adaptive icon for more_vert — material icon named "more vert sharp" and more_horiz — material icon named "more horiz sharp".; + IconData get more_sharp => !_isCupertino() ? Icons.more_vert_sharp : Icons.more_horiz_sharp; + + /// Platform-adaptive icon for share — material icon named "share" and ios_share — material icon named "ios share".; + IconData get share => !_isCupertino() ? Icons.share : Icons.ios_share; + // END GENERATED PLATFORM ADAPTIVE ICONS +} + /// Identifiers for the supported material design icons. /// /// Use with the [Icon] class to show specific icons. @@ -64,9 +137,35 @@ class Icons { // ignore: unused_element Icons._(); + /// A set of platform-adaptive material design icons. + /// + /// Provides a convenient way to show a certain set of platform-appropriate + /// icons on Apple platforms. + /// + /// Use with the [Icon] class to show specific icons. + /// + /// {@tool snippet} + /// This example shows how to create a share icon that uses the material icon + /// named "share" on non-Apple platforms, and the icon named "ios share" on + /// Apple platforms. + /// + /// ```dart + /// Icon( + /// Icons.adaptive.share, + /// ) + /// ``` + /// {@end-tool} + /// + /// See also: + /// + /// * [Icon] + /// * [IconButton] + /// * + static _PlatformAdaptiveIcons get adaptive => _PlatformAdaptiveIcons(); + // Generated code: do not hand-edit. // See https://github.com/flutter/flutter/wiki/Updating-Material-Design-Fonts - // BEGIN GENERATED + // BEGIN GENERATED ICONS /// 10k — material icon named "10k". static const IconData ten_k = IconData(0xe52a, fontFamily: 'MaterialIcons'); @@ -17029,5 +17128,5 @@ class Icons { /// zoom_out — material icon named "zoom out sharp". static const IconData zoom_out_sharp = IconData(0xf02d, fontFamily: 'MaterialIcons'); - // END GENERATED + // END GENERATED ICONS } diff --git a/packages/flutter/test/material/icons_test.dart b/packages/flutter/test/material/icons_test.dart index 06a30a7f375..6ba39895244 100644 --- a/packages/flutter/test/material/icons_test.dart +++ b/packages/flutter/test/material/icons_test.dart @@ -16,4 +16,29 @@ void main() { expect(Icons.clear.fontFamily, 'MaterialIcons'); expect(Icons.search.fontFamily, 'MaterialIcons'); }); + + testWidgets('Adaptive icons are correct on cupertino platforms', + (WidgetTester tester) async { + expect(Icons.adaptive.arrow_back, Icons.arrow_back_ios); + expect(Icons.adaptive.arrow_back_outlined, Icons.arrow_back_ios_outlined); + }, + variant: const TargetPlatformVariant({ + TargetPlatform.iOS, + TargetPlatform.macOS, + }), + ); + + testWidgets( + 'Adaptive icons are correct on non-cupertino platforms', + (WidgetTester tester) async { + expect(Icons.adaptive.arrow_back, Icons.arrow_back); + expect(Icons.adaptive.arrow_back_outlined, Icons.arrow_back_outlined); + }, + variant: const TargetPlatformVariant({ + TargetPlatform.android, + TargetPlatform.fuchsia, + TargetPlatform.windows, + TargetPlatform.linux, + }), + ); }