Skip to content
Open
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
24 changes: 24 additions & 0 deletions packages/mix_annotations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,30 @@ final Prop<Matrix4>? $transform;
final Prop<List<Shadow>>? $shadows;
```

### `@MixWidget`

Generates a `StatelessWidget` wrapper around a Styler. Apply to a top-level variable or function that produces a Styler; the generated widget's constructor mirrors the source's parameters merged with the Styler's `call(...)` signature.

```dart
// Top-level variable
@MixWidget('Card')
final card = BoxStyler().paddingAll(16).borderRounded(8);

// Top-level function — source parameters become widget parameters
@MixWidget('Card')
BoxStyler card(Widget child, {Color color = const Color(0xFFFFFFFF)}) =>
BoxStyler().color(color).paddingAll(16);
```

Set `stylable: true` to expose a runtime `style` parameter that is merged into the annotated Styler before `call(...)` is invoked:

```dart
@MixWidget('H1', stylable: true)
final _h1 = TextStyler().fontSize(24).fontWeight(FontWeight.bold);

// Usage: H1('Title', style: TextStyler().color(Colors.red));
```

## Generator Flags

Each annotation accepts bitwise flags to control which methods or components are generated:
Expand Down
17 changes: 17 additions & 0 deletions packages/mix_annotations/lib/src/annotations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
final bool ignoreSetter;

/// Optional type override for the setter parameter.
/// If not specified, the type is inferred from the field's Prop<T> type argument.

Check notice on line 68 in packages/mix_annotations/lib/src/annotations.dart

View workflow job for this annotation

GitHub Actions / Test / Run tests for all packages

Angle brackets will be interpreted as HTML.

Try using backticks around the content with angle brackets, or try replacing `<` with `&lt;` and `>` with `&gt;`. See https://dart.dev/diagnostics/unintended_html_in_doc_comment to learn more about this problem.
final Type? setterType;

const MixableField({this.ignoreSetter = false, this.setterType});
Expand Down Expand Up @@ -103,3 +103,20 @@
}

const mixable = Mixable();

/// Generates a [StatelessWidget] wrapper around an annotated Styler.
///
/// Apply to a top-level variable whose value is a Styler, or to a top-level
/// function that returns a Styler. The generated widget exposes a constructor
/// that mirrors the source's parameters merged with the Styler's `call(...)`
/// signature, and its `build` method invokes that `call(...)` with the
/// widget's fields.
///
/// When [stylable] is true, the generated widget also accepts an optional
/// `style` parameter of the matching Styler type and merges it into the
/// annotated Styler before invoking `call(...)`.
class MixWidget {
final String name;
final bool stylable;
const MixWidget(this.name, {this.stylable = false});
}
36 changes: 36 additions & 0 deletions packages/mix_generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,42 @@ final class BoxConstraintsMix extends ConstraintsMix<BoxConstraints>
}
```

### From `@MixWidget` — StatelessWidget wrapper

Generates a `StatelessWidget` subclass named `<Name>` that wraps an annotated Styler source (top-level variable or function). The generated constructor merges the source's parameters with the Styler's `call(...)` signature; `build()` invokes `call(...)` with the widget's fields.

```dart
part 'card.g.dart';

@MixWidget('Card')
final card = BoxStyler().paddingAll(16).borderRounded(8);

// Use the generated widget:
// Card(child: Text('Hello'));
```

Function sources forward their own parameters onto the generated widget, preserving positional vs. named shape:

```dart
@MixWidget('Card')
BoxStyler card(Widget child, {Color color = const Color(0xFFFFFFFF)}) =>
BoxStyler().color(color).paddingAll(16);

// Generated:
// Card(child, color: Colors.blue);
```

Pass `stylable: true` to add an optional `style` parameter of the Styler's type. The generated `build()` merges it into the source Styler before calling `call(...)`:

```dart
@MixWidget('H1', stylable: true)
final _h1 = TextStyler().fontSize(24).fontWeight(FontWeight.bold);

// H1('Title', style: TextStyler().color(Colors.red));
```

`@MixWidget` consumes the `call(...)` method generated by `@MixableStyler`, so the target Styler must not skip `call` via `GeneratedStylerMethods.skipCall`.

### Field-level control with `@MixableField`

```dart
Expand Down
8 changes: 8 additions & 0 deletions packages/mix_generator/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,11 @@ builders:
auto_apply: dependents
build_to: cache
applies_builders: ['source_gen:combining_builder']

mix_widget_generator:
import: 'package:mix_generator/mix_generator.dart'
builder_factories: ['mixWidgetGenerator']
build_extensions: {'.dart': ['.mix_widget_generator.g.part']}
auto_apply: dependents
build_to: cache
applies_builders: ['source_gen:combining_builder']
16 changes: 16 additions & 0 deletions packages/mix_generator/lib/mix_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:dart_style/dart_style.dart';
import 'package:source_gen/source_gen.dart';

import 'src/mix_generator.dart';
import 'src/mix_widget_generator.dart';
import 'src/mixable_generator.dart';
import 'src/styler_generator.dart';

Expand All @@ -25,6 +26,7 @@ export 'src/core/models/styler_field_model.dart';
export 'src/core/registry/mix_type_registry.dart';
export 'src/core/resolvers/index.dart';
export 'src/mix_generator.dart';
export 'src/mix_widget_generator.dart';
export 'src/mixable_generator.dart';
export 'src/styler_generator.dart';

Expand Down Expand Up @@ -69,3 +71,17 @@ Builder mixableGenerator(BuilderOptions _) {
},
);
}

/// Entry point for the mix_widget_generator builder.
///
/// Triggers on @MixWidget annotations and generates a StatelessWidget wrapper
/// that invokes the annotated Styler's `call(...)` method.
Builder mixWidgetGenerator(BuilderOptions _) {
return SharedPartBuilder(
[MixWidgetGenerator()],
'mix_widget_generator',
formatOutput: (code, version) {
return DartFormatter(languageVersion: version).format(code);
},
);
}
1 change: 1 addition & 0 deletions packages/mix_generator/lib/src/core/builders/index.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
library;

export 'mix_mixin_builder.dart';
export 'mix_widget_builder.dart';
export 'spec_mixin_builder.dart';
export 'styler_mixin_builder.dart';
100 changes: 100 additions & 0 deletions packages/mix_generator/lib/src/core/builders/mix_widget_builder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import '../models/mix_widget_param_model.dart';

enum MixWidgetSourceKind { variable, function }

/// Emits the full `class <Name> extends StatelessWidget { ... }` source for a
/// `@MixWidget` annotation. Purely string-driven; no analyzer types.
class MixWidgetBuilder {
final String widgetName;
final MixWidgetSourceKind sourceKind;
final String sourceName;
final List<MixWidgetParam> sourceParams;
final List<MixWidgetParam> callParams;
final bool stylable;
final String stylerTypeDisplay;

const MixWidgetBuilder({
required this.widgetName,
required this.sourceKind,
required this.sourceName,
required this.sourceParams,
required this.callParams,
required this.stylable,
required this.stylerTypeDisplay,
});

String build() {
final merged = mergeMixWidgetParams(source: sourceParams, call: callParams);

final fields = StringBuffer();
for (final p in merged) {
fields.writeln(' final ${p.typeDisplay} ${p.name};');
}
if (stylable) {
fields.writeln(' final $stylerTypeDisplay? style;');
}

// Dart forbids mixing `[positional-optional]` with `{named}`, and the
// wrapper always needs `{super.key}`. Only required-positional params can
// stay positional on the widget ctor; positional-optional degrades to
// named.
final positional = <MixWidgetParam>[];
final named = <MixWidgetParam>[];
for (final p in merged) {
if (p.isPositional && p.isRequired && p.defaultValueCode == null) {
positional.add(p);
} else {
named.add(p);
}
}

final ctorSegments = <String>[for (final p in positional) 'this.${p.name}'];
final namedBuffer = StringBuffer('super.key');
for (final p in named) {
namedBuffer.write(', ');
if (p.isRequired) {
namedBuffer.write('required this.${p.name}');
} else if (p.defaultValueCode != null) {
namedBuffer.write('this.${p.name} = ${p.defaultValueCode}');
} else {
namedBuffer.write('this.${p.name}');
}
}
if (stylable) {
namedBuffer.write(', this.style');
}
ctorSegments.add('{$namedBuffer}');
final ctorParams = ctorSegments.join(', ');

final invocation = _buildInvocation();

return '''
class $widgetName extends StatelessWidget {
${fields.toString().trimRight()}

const $widgetName($ctorParams);

@override
Widget build(BuildContext context) => $invocation;
}
''';
}

String _buildInvocation() {
final head = switch (sourceKind) {
MixWidgetSourceKind.variable => sourceName,
MixWidgetSourceKind.function =>
'$sourceName(${_renderArgs(sourceParams)})',
};
final maybeMerge = stylable ? '.merge(style)' : '';
final callArgs = _renderArgs(callParams, skipKey: true);
return '$head$maybeMerge($callArgs)';
}

String _renderArgs(List<MixWidgetParam> params, {bool skipKey = false}) {
return params
.where((p) => !skipKey || p.name != 'key')
.map((p) => p.isPositional ? p.name : '${p.name}: ${p.name}')
.join(', ');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/// Pure value object describing one parameter of a `@MixWidget` source
/// (function parameter or Styler `call(...)` parameter). Intentionally free of
/// analyzer types so the string builder can be unit-tested in isolation.
class MixWidgetParam {
final String name;
final String typeDisplay;
final bool isPositional;
final bool isRequired;
final String? defaultValueCode;

const MixWidgetParam({
required this.name,
required this.typeDisplay,
required this.isPositional,
required this.isRequired,
required this.defaultValueCode,
});
}

/// Merges the source-function parameter list with the Styler's `call(...)`
/// parameter list by name. On collision, the source entry wins (defaults and
/// required-ness live there). The `key` call parameter is dropped because the
/// generated widget forwards it via `super.key`.
List<MixWidgetParam> mergeMixWidgetParams({
required List<MixWidgetParam> source,
required List<MixWidgetParam> call,
}) {
final byName = <String, MixWidgetParam>{};
for (final p in source) {
byName[p.name] = p;
}
for (final p in call) {
if (p.name == 'key') continue;
byName.putIfAbsent(p.name, () => p);
}
return byName.values.toList(growable: false);
}
Loading
Loading