Skip to content

Add support for flutter shaders #596

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 0 additions & 1 deletion packages/command/bin/flutter_gen_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import 'package:flutter_gen_core/flutter_generator.dart';
import 'package:flutter_gen_core/utils/cast.dart';
import 'package:flutter_gen_core/version.gen.dart';


void main(List<String> args) {
final parser = ArgParser();
parser.addOption(
Expand Down
1 change: 0 additions & 1 deletion packages/command/test/flutter_gen_command_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import 'package:flutter_gen_core/version.gen.dart';
import 'package:test/test.dart';
import 'package:test_process/test_process.dart';


final separator = Platform.pathSeparator;

void main() {
Expand Down
14 changes: 14 additions & 0 deletions packages/core/lib/flutter_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:dart_style/dart_style.dart';
import 'package:flutter_gen_core/generators/assets_generator.dart';
import 'package:flutter_gen_core/generators/colors_generator.dart';
import 'package:flutter_gen_core/generators/fonts_generator.dart';
import 'package:flutter_gen_core/generators/shaders_generator.dart';
import 'package:flutter_gen_core/settings/config.dart';
import 'package:flutter_gen_core/utils/file.dart';
import 'package:path/path.dart';
Expand All @@ -15,13 +16,15 @@ class FlutterGenerator {
this.assetsName = 'assets.gen.dart',
this.colorsName = 'colors.gen.dart',
this.fontsName = 'fonts.gen.dart',
this.shadersName = 'shaders.gen.dart',
});

final File pubspecFile;
final File? buildFile;
final String assetsName;
final String colorsName;
final String fontsName;
final String shadersName;

Future<void> build({Config? config, FileWriter? writer}) async {
config ??= loadPubspecConfigOrNull(pubspecFile, buildFile: buildFile);
Expand Down Expand Up @@ -80,6 +83,17 @@ class FlutterGenerator {
stdout.writeln('[FlutterGen] Generated: $fontsPath');
}

if (flutterGen.shaders.enabled && flutter.shaders.isNotEmpty) {
final generated = await generateShaders(
ShadersGenConfig.fromConfig(pubspecFile, config),
formatter,
);
final shadersPath =
normalize(join(pubspecFile.parent.path, output, shadersName));
writer(generated, shadersPath);
stdout.writeln('[FlutterGen] Generated: $shadersPath');
}

stdout.writeln('[FlutterGen] Finished generating.');
}
}
602 changes: 602 additions & 0 deletions packages/core/lib/generators/shaders_generator.dart

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions packages/core/lib/settings/config_default.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ flutter_gen:
outputs:
class_name: FontFamily

shaders:
# Optional
enabled: true
# Optional
outputs:
# Optional
# Set to true if you want this package to be a package dependency
# See: https://flutter.dev/docs/development/ui/assets-and-images#from-packages
package_parameter_enabled: false
# Optional
# Available values:
# - camel-case
# - snake-case
# - dot-delimiter
style: dot-delimiter
class_name: Shaders
exclude: []

colors:
# Optional
enabled: true
Expand All @@ -55,4 +73,6 @@ flutter:
assets: []
# See: https://flutter.dev/docs/cookbook/design/fonts#2-declare-the-font-in-the-pubspec
fonts: []
# See: https://docs.flutter.dev/ui/design/graphics/fragment-shaders
shaders: []
''';
19 changes: 19 additions & 0 deletions packages/core/lib/settings/flavored_shader.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class FlavoredShader {
const FlavoredShader({
required this.path,
this.flavors = const {},
});

final String path;
final Set<String> flavors;

FlavoredShader copyWith({String? path, Set<String>? flavors}) {
return FlavoredShader(
path: path ?? this.path,
flavors: flavors ?? this.flavors,
);
}

@override
String toString() => 'FlavoredShader(path: $path, flavors: $flavors)';
}
8 changes: 8 additions & 0 deletions packages/core/lib/settings/pubspec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class Flutter {
Flutter({
required this.assets,
required this.fonts,
required this.shaders,
});

@JsonKey(name: 'assets', required: true)
Expand All @@ -37,6 +38,9 @@ class Flutter {
@JsonKey(name: 'fonts', required: true)
final List<FlutterFonts> fonts;

@JsonKey(name: 'shaders', required: true)
final List<Object> shaders;

factory Flutter.fromJson(Map json) => _$FlutterFromJson(json);
}

Expand All @@ -58,6 +62,7 @@ class FlutterGen {
required this.parseMetadata,
required this.assets,
required this.fonts,
required this.shaders,
required this.integrations,
required this.colors,
});
Expand All @@ -77,6 +82,9 @@ class FlutterGen {
@JsonKey(name: 'fonts', required: true)
final FlutterGenFonts fonts;

@JsonKey(name: 'shaders', required: true)
final FlutterGenAssets shaders;

@JsonKey(name: 'integrations', required: true)
final FlutterGenIntegrations integrations;

Expand Down
7 changes: 6 additions & 1 deletion packages/core/lib/settings/pubspec.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

218 changes: 218 additions & 0 deletions packages/core/lib/settings/shader_type.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import 'package:dartx/dartx.dart';
import 'package:flutter_gen_core/utils/identifer.dart';
import 'package:flutter_gen_core/utils/string.dart';
import 'package:mime/mime.dart' show lookupMimeType;
import 'package:path/path.dart' as p;

/// https://github.com/dart-lang/mime/blob/master/lib/src/default_extension_map.dart
class ShaderType {
ShaderType({
required this.rootPath,
required this.path,
required this.flavors,
});

final String rootPath;
final String path;
final Set<String> flavors;

final List<ShaderType> _children = List.empty(growable: true);

bool get isDefaultShadersDirectory => path == 'shaders' || path == 'shader';

String? get mime => lookupMimeType(path);

bool get isIgnoreFile {
switch (baseName) {
case '.DS_Store':
return true;
}

switch (extension) {
case '.DS_Store':
case '.swp':
return true;
}

return false;
}

bool get isUnKnownMime => mime == null;

/// Returns a name for this shader.
String get name => p.withoutExtension(path);

String get baseName => p.basenameWithoutExtension(path);

String get extension => p.extension(path);

/// Returns the full absolute path for reading the shader file.
String get fullPath => p.join(rootPath, path);

// Replace to Posix style for Windows separator.
String get posixStylePath => path.replaceAll(r'\', r'/');

List<ShaderType> get children => _children.sortedBy((e) => e.path);

void addChild(ShaderType type) {
_children.add(type);
}

@override
String toString() => 'ShaderType('
'rootPath: $rootPath, '
'path: $path, '
'flavors: $flavors'
')';
}

/// Represents a ShaderType with modifiers on it to mutate the [name] to ensure
/// it is unique in a larger list and a valid dart identifer.
///
/// See [ShaderTypeIterable.mapToUniqueShaderType] for the algorithm.
class UniqueShaderType extends ShaderType {
UniqueShaderType({
required ShaderType shaderType,
this.style = camelCase,
this.basenameOnly = false,
this.needExtension = false,
this.suffix = '',
}) : super(
rootPath: shaderType.rootPath,
path: shaderType.path,
flavors: shaderType.flavors,
);

/// Convert the shader name to a correctly styled name, e.g camelCase or
/// snakeCase.
final String Function(String) style;

/// Include just the basename of the shader in the [name],
/// e.g. 'images/image.png' -> 'image'.
final bool basenameOnly;

/// Include the extension in the [name], e.g. 'image.png' -> 'imagePng'.
bool needExtension;

/// Optional suffix to append to the [name] to make it unique. Typically just
/// one or more '_' characters.
String suffix;

/// Returns a identifier name, which is ideally unique and valid.
@override
String get name {
// Omit root directory from the name if it is either shader or shaders.
// TODO(bramp): Maybe move this into the _flatStyleDefinition
String result = path.replaceFirst(RegExp(r'^shader(s)?[/\\]'), '');
if (basenameOnly) {
result = p.basename(result);
}
if (!needExtension) {
result = p.withoutExtension(result);
}
return style(convertToIdentifier(result)) + suffix;
}

@override
String toString() {
return 'UniqueShaderType{'
'rootPath: $rootPath, '
'path: $path, '
'style: $style, '
'needExtension: $needExtension, '
'suffix: $suffix}';
}
}

extension ShaderTypeIterable on Iterable<ShaderType> {
/// Takes a Iterable<ShaderType> and mutates the ShaderType's to ensure each
/// ShaderType has a unique name.
///
/// The strategy is as follows:
///
/// 1) Convert the shader file name, to a valid dart identifier, that is
/// - Replace non ASCII chars with ASCII.
/// - Ensure the name starts with a letter (not number or _).
/// - Style the name per the camelCase or snakeCase rules.
/// 2) Use the shader name without extension. If unique and not a dart reserved
/// word use that.
/// 3) Use the shader name with extension. If unique and not a dart reserved
/// word use that.
/// 4) If there are any collisions, append a underscore suffix to each item
/// until they are unique.
///
/// Because the name change can cause it to clash with an existing name, the
/// code is run iteratively until no collision are found. This can be a little
/// more expensive, but it simplier.
///
Iterable<UniqueShaderType> mapToUniqueShaderType(
String Function(String) style, {
bool justBasename = false,
}) {
List<UniqueShaderType> shaders = map((e) => UniqueShaderType(
shaderType: e,
style: style,
needExtension: false,
suffix: '',
basenameOnly: justBasename,
)).toList();

while (true) {
// Check if we have any name collisions.
final dups = shaders.groupBy((e) => e.name).values;

// No more duplicates, so we can bail.
if (dups.every((list) => list.length == 1)) break;

// Otherwise start to process the list and mutate the shaders as needed.
shaders = dups
.map((list) {
assert(list.isNotEmpty,
'The groupBy list of shaders should not be empty.');

// Check the first element in the list. Since we grouped by each
// list element should have the same name.
final name = list[0].name;
final isValidIdentifer = isValidVariableIdentifier(name);

// TODO(bramp): In future we should also check this name doesn't collide
// with the integration's class name (e.g ShaderGenImage).

// No colissions for this name, carry on.
if (list.length == 1 && isValidIdentifer) {
return list;
}

// We haven't added filename extensions yet, let's try that first
if (!list.every((e) => e.needExtension)) {
for (final e in list) {
e.needExtension = true;
}

return list;
}

// Ok, we must resolve the conflicts by adding suffixes.
String suffix = '';
list.forEachIndexed((shader, index) {
// Shouldn't need to mutate the first item (unless it's an invalid
// identifer).
if (index == 0 && isValidIdentifer) return;

// Append a extra suffixes to each item so they hopefully become unique
suffix = '${suffix}_';
shader.suffix += suffix;
});

return list;
})
.flatten()
.toList();
}

assert(shaders.map((e) => e.name).distinct().length == shaders.length,
'There are duplicate names in the shader list.');

return shaders;
}
}
Loading