Unverified Commit b23f207e authored by Pierre-Louis's avatar Pierre-Louis Committed by GitHub

Improve `update_icons.dart` script (#93863)

* Update update_icons.dart

* formatting

* more formatting

* simplify safety checks

* add safety checks tests

* formatting

* update copyright

* naming

* revert copyright

* add `fontFamily` option

* tweak diff messages

* Update update_icons.dart

* don't exit if platform adaptive icon not found

* remove trailing spaces

* remove warning and fix insert_chart (outlined) dartdoc

* x

* add error message when exiting due to failed safety checkes

* fix icon reference

* fix rewrite for onetwothree
parent 1dc458b2
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/test.dart';
import '../update_icons.dart';
Map<String, String> codepointsA = <String, String>{
'airplane': '111',
'boat': '222',
};
Map<String, String> codepointsB = <String, String>{
'airplane': '333',
};
Map<String, String> codepointsC = <String, String>{
'airplane': '111',
'train': '444',
};
void main() {
group('safety checks', () {
test('superset', () {
expect(testIsSuperset(codepointsA, codepointsA), true);
expect(testIsSuperset(codepointsA, codepointsB), true);
expect(testIsSuperset(codepointsB, codepointsA), false);
});
test('stability', () {
expect(testIsStable(codepointsA, codepointsA), true);
expect(testIsStable(codepointsA, codepointsB), false);
expect(testIsStable(codepointsB, codepointsA), false);
expect(testIsStable(codepointsA, codepointsC), true);
expect(testIsStable(codepointsC, codepointsA), true);
});
});
}
...@@ -10,16 +10,21 @@ import 'dart:convert' show LineSplitter; ...@@ -10,16 +10,21 @@ import 'dart:convert' show LineSplitter;
import 'dart:io'; import 'dart:io';
import 'package:args/args.dart'; import 'package:args/args.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
const String _iconsPathOption = 'icons';
const String _iconsTemplatePathOption = 'icons-template';
const String _newCodepointsPathOption = 'new-codepoints'; const String _newCodepointsPathOption = 'new-codepoints';
const String _oldCodepointsPathOption = 'old-codepoints'; const String _oldCodepointsPathOption = 'old-codepoints';
const String _iconsClassPathOption = 'icons'; const String _fontFamilyOption = 'font-family';
const String _enforceSafetyChecks = 'enforce-safety-checks';
const String _dryRunOption = 'dry-run'; const String _dryRunOption = 'dry-run';
const String _defaultIconsPath = 'packages/flutter/lib/src/material/icons.dart';
const String _defaultNewCodepointsPath = 'codepoints'; const String _defaultNewCodepointsPath = 'codepoints';
const String _defaultOldCodepointsPath = 'bin/cache/artifacts/material_fonts/codepoints'; const String _defaultOldCodepointsPath = 'bin/cache/artifacts/material_fonts/codepoints';
const String _defaultIconsPath = 'packages/flutter/lib/src/material/icons.dart'; const String _defaultFontFamily = 'MaterialIcons';
const String _beginGeneratedMark = '// BEGIN GENERATED ICONS'; const String _beginGeneratedMark = '// BEGIN GENERATED ICONS';
const String _endGeneratedMark = '// END GENERATED ICONS'; const String _endGeneratedMark = '// END GENERATED ICONS';
...@@ -38,36 +43,37 @@ const Map<String, List<String>> _platformAdaptiveIdentifiers = <String, List<Str ...@@ -38,36 +43,37 @@ const Map<String, List<String>> _platformAdaptiveIdentifiers = <String, List<Str
// Rewrite certain Flutter IDs (numbers) using prefix matching. // Rewrite certain Flutter IDs (numbers) using prefix matching.
const Map<String, String> identifierPrefixRewrites = <String, String>{ const Map<String, String> identifierPrefixRewrites = <String, String>{
'_1': 'one_', '1': 'one_',
'_2': 'two_', '2': 'two_',
'_3': 'three_', '3': 'three_',
'_4': 'four_', '4': 'four_',
'_5': 'five_', '5': 'five_',
'_6': 'six_', '6': 'six_',
'_7': 'seven_', '7': 'seven_',
'_8': 'eight_', '8': 'eight_',
'_9': 'nine_', '9': 'nine_',
'_10': 'ten_', '10': 'ten_',
'_11': 'eleven_', '11': 'eleven_',
'_12': 'twelve_', '12': 'twelve_',
'_13': 'thirteen_', '13': 'thirteen_',
'_14': 'fourteen_', '14': 'fourteen_',
'_15': 'fifteen_', '15': 'fifteen_',
'_16': 'sixteen_', '16': 'sixteen_',
'_17': 'seventeen_', '17': 'seventeen_',
'_18': 'eighteen_', '18': 'eighteen_',
'_19': 'nineteen_', '19': 'nineteen_',
'_20': 'twenty_', '20': 'twenty_',
'_21': 'twenty_one_', '21': 'twenty_one_',
'_22': 'twenty_two_', '22': 'twenty_two_',
'_23': 'twenty_three_', '23': 'twenty_three_',
'_24': 'twenty_four_', '24': 'twenty_four_',
'_30': 'thirty_', '30': 'thirty_',
'_60': 'sixty_', '60': 'sixty_',
'_360': 'threesixty', '123': 'onetwothree',
'_2d': 'twod', '360': 'threesixty',
'_3d': 'threed', '2d': 'twod',
'_3d_rotation': 'threed_rotation', '3d': 'threed',
'3d_rotation': 'threed_rotation',
}; };
// Rewrite certain Flutter IDs (reserved keywords) using exact matching. // Rewrite certain Flutter IDs (reserved keywords) using exact matching.
...@@ -163,9 +169,14 @@ void main(List<String> args) { ...@@ -163,9 +169,14 @@ void main(List<String> args) {
final ArgResults argResults = _handleArguments(args); final ArgResults argResults = _handleArguments(args);
final File iconClassFile = File(path.normalize(path.absolute(argResults[_iconsClassPathOption] as String))); final File iconsFile = File(path.normalize(path.absolute(argResults[_iconsPathOption] as String)));
if (!iconClassFile.existsSync()) { if (!iconsFile.existsSync()) {
stderr.writeln('Error: Icons file not found: ${iconClassFile.path}'); stderr.writeln('Error: Icons file not found: ${iconsFile.path}');
exit(1);
}
final File iconsTemplateFile = File(path.normalize(path.absolute(argResults[_iconsTemplatePathOption] as String)));
if (!iconsTemplateFile.existsSync()) {
stderr.writeln('Error: Icons template file not found: ${iconsTemplateFile.path}');
exit(1); exit(1);
} }
final File newCodepointsFile = File(argResults[_newCodepointsPathOption] as String); final File newCodepointsFile = File(argResults[_newCodepointsPathOption] as String);
...@@ -185,33 +196,51 @@ void main(List<String> args) { ...@@ -185,33 +196,51 @@ void main(List<String> args) {
final String oldCodepointsString = oldCodepointsFile.readAsStringSync(); final String oldCodepointsString = oldCodepointsFile.readAsStringSync();
final Map<String, String> oldTokenPairMap = _stringToTokenPairMap(oldCodepointsString); final Map<String, String> oldTokenPairMap = _stringToTokenPairMap(oldCodepointsString);
_testIsMapSuperset(newTokenPairMap, oldTokenPairMap); stderr.writeln('Performing safety checks');
final bool isSuperset = testIsSuperset(newTokenPairMap, oldTokenPairMap);
final String iconClassFileData = iconClassFile.readAsStringSync(); final bool isStable = testIsStable(newTokenPairMap, oldTokenPairMap);
if ((!isSuperset || !isStable) && argResults[_enforceSafetyChecks] as bool) {
exit(1);
}
final String iconsTemplateContents = iconsTemplateFile.readAsStringSync();
stderr.writeln('Generating icons file...'); stderr.writeln("Generating icons ${argResults[_dryRunOption] as bool ? '' : 'to ${iconsFile.path}'}");
final String newIconData = _regenerateIconsFile(iconClassFileData, newTokenPairMap); final String newIconsContents = _regenerateIconsFile(
iconsTemplateContents,
newTokenPairMap,
argResults[_fontFamilyOption] as String,
argResults[_enforceSafetyChecks] as bool,
);
if (argResults[_dryRunOption] as bool) { if (argResults[_dryRunOption] as bool) {
stdout.write(newIconData); stdout.write(newIconsContents);
} else { } else {
stderr.writeln('\nWriting to ${iconClassFile.path}.'); iconsFile.writeAsStringSync(newIconsContents);
iconClassFile.writeAsStringSync(newIconData);
_regenerateCodepointsFile(oldCodepointsFile, newTokenPairMap); _regenerateCodepointsFile(oldCodepointsFile, newTokenPairMap);
} }
} }
ArgResults _handleArguments(List<String> args) { ArgResults _handleArguments(List<String> args) {
final ArgParser argParser = ArgParser() final ArgParser argParser = ArgParser()
..addOption(_iconsPathOption,
defaultsTo: _defaultIconsPath,
help: 'Location of the material icons file')
..addOption(_iconsTemplatePathOption,
defaultsTo: _defaultIconsPath,
help:
'Location of the material icons file template. Usually the same as --$_iconsPathOption')
..addOption(_newCodepointsPathOption, ..addOption(_newCodepointsPathOption,
defaultsTo: _defaultNewCodepointsPath, defaultsTo: _defaultNewCodepointsPath,
help: 'Location of the new codepoints directory') help: 'Location of the new codepoints directory')
..addOption(_oldCodepointsPathOption, ..addOption(_oldCodepointsPathOption,
defaultsTo: _defaultOldCodepointsPath, defaultsTo: _defaultOldCodepointsPath,
help: 'Location of the existing codepoints directory') help: 'Location of the existing codepoints directory')
..addOption(_iconsClassPathOption, ..addOption(_fontFamilyOption,
defaultsTo: _defaultIconsPath, defaultsTo: _defaultFontFamily,
help: 'Location of the material icons file') help: 'The font family to use for the IconData constants')
..addFlag(_enforceSafetyChecks,
defaultsTo: true,
help: 'Whether to exit if safety checks fail (e.g. codepoints are missing or unstable')
..addFlag(_dryRunOption); ..addFlag(_dryRunOption);
argParser.addFlag('help', abbr: 'h', negatable: false, callback: (bool help) { argParser.addFlag('help', abbr: 'h', negatable: false, callback: (bool help) {
if (help) { if (help) {
...@@ -227,7 +256,7 @@ Map<String, String> _stringToTokenPairMap(String codepointData) { ...@@ -227,7 +256,7 @@ Map<String, String> _stringToTokenPairMap(String codepointData) {
.map((String line) => line.trim()) .map((String line) => line.trim())
.where((String line) => line.isNotEmpty); .where((String line) => line.isNotEmpty);
final Map<String, String> pairs = <String,String>{}; final Map<String, String> pairs = <String, String>{};
for (final String line in cleanData) { for (final String line in cleanData) {
final List<String> tokens = line.split(' '); final List<String> tokens = line.split(' ');
...@@ -240,40 +269,49 @@ Map<String, String> _stringToTokenPairMap(String codepointData) { ...@@ -240,40 +269,49 @@ Map<String, String> _stringToTokenPairMap(String codepointData) {
return pairs; return pairs;
} }
String _regenerateIconsFile(String iconData, Map<String, String> tokenPairMap) { String _regenerateIconsFile(
String templateFileContents,
Map<String, String> tokenPairMap,
String fontFamily,
bool enforceSafetyChecks,
) {
final List<_Icon> newIcons = tokenPairMap.entries final List<_Icon> newIcons = tokenPairMap.entries
.map((MapEntry<String, String> entry) => _Icon(entry)) .map((MapEntry<String, String> entry) => _Icon(entry, fontFamily))
.toList(); .toList();
newIcons.sort((_Icon a, _Icon b) => a._compareTo(b)); newIcons.sort((_Icon a, _Icon b) => a._compareTo(b));
final StringBuffer buf = StringBuffer(); final StringBuffer buf = StringBuffer();
bool generating = false; bool generating = false;
for (final String line in LineSplitter.split(iconData)) { for (final String line in LineSplitter.split(templateFileContents)) {
if (!generating) { if (!generating) {
buf.writeln(line); buf.writeln(line);
} }
// Generate for _PlatformAdaptiveIcons // Generate for PlatformAdaptiveIcons
if (line.contains(_beginPlatformAdaptiveGeneratedMark)) { if (line.contains(_beginPlatformAdaptiveGeneratedMark)) {
generating = true; generating = true;
final List<String> platformAdaptiveDeclarations = <String>[]; final List<String> platformAdaptiveDeclarations = <String>[];
_platformAdaptiveIdentifiers.forEach((String flutterId, List<String> ids) { _platformAdaptiveIdentifiers.forEach((String flutterId, List<String> ids) {
// Automatically finds and generates styled icon declarations. // Automatically finds and generates all icon declarations.
for (final String style in <String>['', '_outlined', '_rounded', '_sharp']) { for (final String style in <String>['', '_outlined', '_rounded', '_sharp']) {
try { try {
final _Icon agnosticIcon = newIcons.firstWhere( final _Icon agnosticIcon = newIcons.firstWhere(
(_Icon icon) => icon.id == '${ids[0]}$style', (_Icon icon) => icon.id == '${ids[0]}$style',
orElse: () => throw ids[0]); orElse: () => throw ids[0]);
final _Icon iOSIcon = newIcons.firstWhere( final _Icon iOSIcon = newIcons.firstWhere(
(_Icon icon) => icon.id == '${ids[1]}$style', (_Icon icon) => icon.id == '${ids[1]}$style',
orElse: () => throw ids[1]); orElse: () => throw ids[1]);
platformAdaptiveDeclarations.add(_Icon.platformAdaptiveDeclaration('$flutterId$style', agnosticIcon, iOSIcon)); platformAdaptiveDeclarations.add(_Icon.platformAdaptiveDeclaration('$flutterId$style', agnosticIcon, iOSIcon),
);
} catch (e) { } catch (e) {
if (style == '') { if (style == '') {
// Throw an error for regular (unstyled) icons. // Throw an error for baseline icons.
stderr.writeln("Error while generating platformAdaptiveDeclarations: Icon '$e' not found."); stderr.writeln("❌ Platform adaptive icon '$e' not found.");
exit(1); if (enforceSafetyChecks) {
stderr.writeln('Safety checks failed');
exit(1);
}
} else { } else {
// Ignore errors for styled icons since some don't exist. // Ignore errors for styled icons since some don't exist.
} }
...@@ -299,28 +337,50 @@ String _regenerateIconsFile(String iconData, Map<String, String> tokenPairMap) { ...@@ -299,28 +337,50 @@ String _regenerateIconsFile(String iconData, Map<String, String> tokenPairMap) {
return buf.toString(); return buf.toString();
} }
void _testIsMapSuperset(Map<String, String> newCodepoints, Map<String, String> oldCodepoints) { @visibleForTesting
bool testIsSuperset(Map<String, String> newCodepoints, Map<String, String> oldCodepoints) {
final Set<String> newCodepointsSet = newCodepoints.keys.toSet(); final Set<String> newCodepointsSet = newCodepoints.keys.toSet();
final Set<String> oldCodepointsSet = oldCodepoints.keys.toSet(); final Set<String> oldCodepointsSet = oldCodepoints.keys.toSet();
final int diff = newCodepointsSet.length - oldCodepointsSet.length;
if (diff > 0) {
stderr.writeln('🆕 $diff new codepoints: ${newCodepointsSet.difference(oldCodepointsSet)}');
}
if (!newCodepointsSet.containsAll(oldCodepointsSet)) { if (!newCodepointsSet.containsAll(oldCodepointsSet)) {
stderr.writeln(''' stderr.writeln(
Error: New codepoints file does not contain all ${oldCodepointsSet.length} existing codepoints.\n '❌ new codepoints file does not contain all ${oldCodepointsSet.length} '
Missing: ${oldCodepointsSet.difference(newCodepointsSet)} 'existing codepoints. Missing: ${oldCodepointsSet.difference(newCodepointsSet)}');
''', return false;
);
exit(1);
} else { } else {
final int diff = newCodepointsSet.length - oldCodepointsSet.length; stderr.writeln('✅ new codepoints file contains all ${oldCodepointsSet.length} existing codepoints');
stderr.writeln('New codepoints file contains all ${oldCodepointsSet.length} existing codepoints.'); }
if (diff > 0) { return true;
stderr.writeln('It also contains $diff new codepoints: ${newCodepointsSet.difference(oldCodepointsSet)}'); }
@visibleForTesting
bool testIsStable(Map<String, String> newCodepoints, Map<String, String> oldCodepoints) {
final int oldCodepointsCount = oldCodepoints.length;
final List<String> unstable = <String>[];
oldCodepoints.forEach((String key, String value) {
if (newCodepoints.containsKey(key)) {
if (value != newCodepoints[key]) {
unstable.add(key);
}
} }
});
if (unstable.isNotEmpty) {
stderr.writeln('❌ out of $oldCodepointsCount existing codepoints, ${unstable.length} were unstable: $unstable');
return false;
} else {
stderr.writeln('✅ all existing $oldCodepointsCount codepoints are stable');
return true;
} }
} }
void _regenerateCodepointsFile(File oldCodepointsFile, Map<String, String> newTokenPairMap) { void _regenerateCodepointsFile(File oldCodepointsFile, Map<String, String> newTokenPairMap) {
stderr.writeln('Regenerating old codepoints file ${oldCodepointsFile.path}.\n'); stderr.writeln('Regenerating old codepoints file ${oldCodepointsFile.path}');
final StringBuffer buf = StringBuffer(); final StringBuffer buf = StringBuffer();
final SplayTreeMap<String, String> sortedNewTokenPairMap = SplayTreeMap<String, String>.of(newTokenPairMap); final SplayTreeMap<String, String> sortedNewTokenPairMap = SplayTreeMap<String, String>.of(newTokenPairMap);
...@@ -330,7 +390,7 @@ void _regenerateCodepointsFile(File oldCodepointsFile, Map<String, String> newTo ...@@ -330,7 +390,7 @@ void _regenerateCodepointsFile(File oldCodepointsFile, Map<String, String> newTo
class _Icon { class _Icon {
// Parse tokenPair (e.g. {"6_ft_apart_outlined": "e004"}). // Parse tokenPair (e.g. {"6_ft_apart_outlined": "e004"}).
_Icon(MapEntry<String, String> tokenPair) { _Icon(MapEntry<String, String> tokenPair, this.fontFamily) {
id = tokenPair.key; id = tokenPair.key;
hexCodepoint = tokenPair.value; hexCodepoint = tokenPair.value;
...@@ -349,14 +409,15 @@ class _Icon { ...@@ -349,14 +409,15 @@ class _Icon {
htmlSuffix = '-filled'; htmlSuffix = '-filled';
} else { } else {
family = 'material'; family = 'material';
if (id.endsWith('_outlined') && id != 'insert_chart_outlined') { if (id.endsWith('_baseline')) {
id = _removeLast(id, '_baseline');
htmlSuffix = '';
} else if (id.endsWith('_outlined')) {
htmlSuffix = '-outlined'; htmlSuffix = '-outlined';
} else if (id.endsWith('_rounded')) { } else if (id.endsWith('_rounded')) {
htmlSuffix = '-round'; htmlSuffix = '-round';
} else if (id.endsWith('_sharp')) { } else if (id.endsWith('_sharp')) {
htmlSuffix = '-sharp'; htmlSuffix = '-sharp';
} else {
htmlSuffix = '';
} }
} }
...@@ -379,7 +440,8 @@ class _Icon { ...@@ -379,7 +440,8 @@ class _Icon {
late String flutterId; // e.g. five_g, five_g_outlined, five_g_rounded, five_g_sharp late String flutterId; // e.g. five_g, five_g_outlined, five_g_rounded, five_g_sharp
late String family; // e.g. material late String family; // e.g. material
late String hexCodepoint; // e.g. e547 late String hexCodepoint; // e.g. e547
late String htmlSuffix; // The suffix for the 'material-icons' HTML class. late String htmlSuffix = ''; // The suffix for the 'material-icons' HTML class.
String fontFamily; // The IconData font family.
String get name => shortId.replaceAll('_', ' ').trim(); String get name => shortId.replaceAll('_', ' ').trim();
...@@ -393,7 +455,7 @@ class _Icon { ...@@ -393,7 +455,7 @@ class _Icon {
: ''; : '';
String get declaration => String get declaration =>
"static const IconData $flutterId = IconData(0x$hexCodepoint, fontFamily: 'MaterialIcons'$mirroredInRTL);"; "static const IconData $flutterId = IconData(0x$hexCodepoint, fontFamily: '$fontFamily'$mirroredInRTL);";
String get fullDeclaration => ''' String get fullDeclaration => '''
...@@ -419,16 +481,14 @@ class _Icon { ...@@ -419,16 +481,14 @@ class _Icon {
return shortId.compareTo(b.shortId); return shortId.compareTo(b.shortId);
} }
static String _replaceLast(String string, String toReplace) { static String _removeLast(String string, String toReplace) {
return string.replaceAll(RegExp('$toReplace\$'), ''); return string.replaceAll(RegExp('$toReplace\$'), '');
} }
static String _generateShortId(String id) { static String _generateShortId(String id) {
String shortId = id; String shortId = id;
for (final String styleSuffix in _idSuffixes) { for (final String styleSuffix in _idSuffixes) {
if (styleSuffix == '_outlined' && id == 'insert_chart_outlined') shortId = _removeLast(shortId, styleSuffix);
continue;
shortId = _replaceLast(shortId, styleSuffix);
if (shortId != id) { if (shortId != id) {
break; break;
} }
...@@ -440,22 +500,22 @@ class _Icon { ...@@ -440,22 +500,22 @@ class _Icon {
static String generateFlutterId(String id) { static String generateFlutterId(String id) {
String flutterId = id; String flutterId = id;
// Exact identifier rewrites. // Exact identifier rewrites.
for (final MapEntry<String, String> rewritePair for (final MapEntry<String, String> rewritePair in identifierExactRewrites.entries) {
in identifierExactRewrites.entries) {
final String shortId = _Icon._generateShortId(id); final String shortId = _Icon._generateShortId(id);
if (shortId == rewritePair.key) { if (shortId == rewritePair.key) {
flutterId = id.replaceFirst(rewritePair.key, identifierExactRewrites[rewritePair.key]!); flutterId = id.replaceFirst(
rewritePair.key,
identifierExactRewrites[rewritePair.key]!,
);
} }
} }
// Prefix identifier rewrites. // Prefix identifier rewrites.
for (final MapEntry<String, String> rewritePair for (final MapEntry<String, String> rewritePair in identifierPrefixRewrites.entries) {
in identifierPrefixRewrites.entries) {
if (id.startsWith(rewritePair.key)) { if (id.startsWith(rewritePair.key)) {
flutterId = id.replaceFirst(rewritePair.key, identifierPrefixRewrites[rewritePair.key]!); flutterId = id.replaceFirst(
} rewritePair.key,
// TODO(guidezpl): With the next icon update, this won't be necessary, remove it. identifierPrefixRewrites[rewritePair.key]!,
if (id.startsWith(rewritePair.key.replaceFirst('_', ''))) { );
flutterId = id.replaceFirst(rewritePair.key.replaceFirst('_', ''), identifierPrefixRewrites[rewritePair.key]!);
} }
} }
return flutterId; return flutterId;
......
...@@ -97,8 +97,7 @@ class PlatformAdaptiveIcons implements Icons { ...@@ -97,8 +97,7 @@ class PlatformAdaptiveIcons implements Icons {
/// ///
/// Use with the [Icon] class to show specific icons. /// Use with the [Icon] class to show specific icons.
/// ///
/// Icons are identified by their name as listed below. **Do not use codepoints /// Icons are identified by their name as listed below, e.g. [Icons.airplanemode_on].
/// directly, as they are subject to change.**
/// ///
/// To use this class, make sure you set `uses-material-design: true` in your /// To use this class, make sure you set `uses-material-design: true` in your
/// project's `pubspec.yaml` file in the `flutter` section. This ensures that /// project's `pubspec.yaml` file in the `flutter` section. This ensures that
...@@ -10287,7 +10286,7 @@ class Icons { ...@@ -10287,7 +10286,7 @@ class Icons {
/// <i class="material-icons-round md-36">insert_chart</i> &#x2014; material icon named "insert chart" (round). /// <i class="material-icons-round md-36">insert_chart</i> &#x2014; material icon named "insert chart" (round).
static const IconData insert_chart_rounded = IconData(0xf819, fontFamily: 'MaterialIcons'); static const IconData insert_chart_rounded = IconData(0xf819, fontFamily: 'MaterialIcons');
/// <i class="material-icons md-36">insert_chart_outlined</i> &#x2014; material icon named "insert chart outlined". /// <i class="material-icons-outlined md-36">insert_chart</i> &#x2014; material icon named "insert chart" (outlined).
static const IconData insert_chart_outlined = IconData(0xf12a, fontFamily: 'MaterialIcons'); static const IconData insert_chart_outlined = IconData(0xf12a, fontFamily: 'MaterialIcons');
/// <i class="material-icons-sharp md-36">insert_chart_outlined</i> &#x2014; material icon named "insert chart outlined" (sharp). /// <i class="material-icons-sharp md-36">insert_chart_outlined</i> &#x2014; material icon named "insert chart outlined" (sharp).
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment