Skip to content

feat(mix): add MixWidget wrapper generation#903

Draft
leoafarias wants to merge 7 commits intomainfrom
feat/mix-widget-builders
Draft

feat(mix): add MixWidget wrapper generation#903
leoafarias wants to merge 7 commits intomainfrom
feat/mix-widget-builders

Conversation

@leoafarias
Copy link
Copy Markdown
Collaborator

@leoafarias leoafarias commented Apr 15, 2026

Related issue

N/A

Description

This PR adds @MixWidget, a generator that turns top-level Mix stylers into thin public widgets with almost no handwritten wrapper code. The generated widgets mirror the styler's call() signature, construct the mapped Mix widget directly, and keep the wrapper API aligned with the style definition as it evolves. That makes it easier to promote reusable Mix styles into public components without maintaining a second handwritten constructor surface.

Changes

  • Added the MixWidget, MixWidgetBuilder, and MixWidgetBuilderKind annotation API and exposed the MixWidget surface from package:mix/mix.dart.
  • Added the mix_widget_generator builder, descriptor registry, and direct widget-wrapper generation for built-in Mix widget families like Box, Text, FlexBox, RowBox, ColumnBox, Icon, Image, and StackBox.
  • Made generated wrapper constructors mirror the styler call() signature, with optional style overrides only when styleable: true is explicitly enabled.
  • Added file-backed golden snapshots, refresh tooling, and validation coverage for generator output, naming, mapping drift, default propagation, and constructor compatibility failures.
  • Updated the export-generation scripts and package READMEs so the feature is documented and available through the curated public barrel.

API examples

Basic BoxStyler example:

import 'package:mix/mix.dart';

part 'card_styles.g.dart';

@MixWidget()
final cardStyle = BoxStyler()
    .paddingAll(16)
    .borderRounded(12);

This generates a thin widget wrapper roughly equivalent to:

class Card extends StatelessWidget {
  final Widget? child;

  const Card({super.key, this.child});

  @override
  Widget build(BuildContext context) {
    return Box(child: child, key: key, style: cardStyle);
  }
}

Basic TextStyler example:

@MixWidget()
final headingStyle = TextStyler()
    .fontSize(24)
    .fontWeight(FontWeight.w700);

This generates a wrapper with positional text, because that is the surface exposed by TextStyler.call():

class Heading extends StatelessWidget {
  final String text;

  const Heading(this.text, {super.key});

  @override
  Widget build(BuildContext context) {
    return StyledText(text, key: key, style: headingStyle);
  }
}

The generated wrapper API is not hardcoded per widget type. It mirrors the styler's call() signature, then constructs the mapped Mix widget directly, which is why Box wrappers expose child while Text wrappers expose positional text.

If a generated component should accept style overrides, opt in with styleable: true:

@MixWidget(styleable: true)
final chipStyle = BoxStyler()
    .paddingAll(8)
    .borderRounded(999);

That adds one generated style parameter and merges it with the base style:

class Chip extends StatelessWidget {
  final Widget? child;
  final BoxStyler? style;

  const Chip({super.key, this.child, this.style});

  @override
  Widget build(BuildContext context) {
    final baseStyle = chipStyle;
    final effectiveStyle = baseStyle.merge(style);
    return Box(child: child, key: key, style: effectiveStyle);
  }
}

For style families with multiple valid widget targets, use widgetBuilder:

@MixWidget(widgetBuilder: MixWidgetBuilder.rowBox())
final toolbarStyle = FlexBoxStyler();

This keeps the FlexBoxStyler.call()-shaped API but generates a wrapper that constructs RowBox.

Review Checklist

  • Testing: Have you tested your changes, including unit tests and integration tests for affected code?
  • Breaking Changes: Does this change introduce breaking changes affecting existing code or users?
  • Documentation Updates: Are all relevant documentation files (e.g. README, API docs) updated to reflect the changes in this PR?
  • Website Updates: Is the website containing the updates you make on documentation?

Additional Information (optional)

Verified with melos exec --scope="mix_generator" -- dart test, melos run goldens:mix_widget:update, melos exec --scope="mix" -- flutter pub run build_runner build --delete-conflicting-outputs, dart analyze packages/mix packages/mix_generator packages/mix_annotations, and dart format --output=none --set-exit-if-changed ..

@docs-page
Copy link
Copy Markdown

docs-page bot commented Apr 15, 2026

To view this pull requests documentation preview, visit the following URL:

docs.page/btwld/mix~903

Documentation is deployed and generated using docs.page.

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 15, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mix-docs Ready Ready Preview, Comment Apr 16, 2026 3:00am

@leoafarias leoafarias changed the title Add MixWidget wrapper generation feat(mix): add MixWidget wrapper generation Apr 15, 2026
Refactor @MixWidget codegen to dispatch through extensible MixWidgetBuilder<TSpec>
builders instead of a closed MixWidgetBuilderKind enum + descriptor registry.
Adds per-spec builder files (box, flexbox, icon, image, stackbox, text) so
third-party packages and user widgets can register their own defaults.
…ation

- Added `Equatable` mixin to provide standard equality and hash code implementation based on properties.
- Introduced utility functions for deep comparison of collections and property differences.
- Created tests for `Equatable` to validate equality, hash code, and string representation for nested classes.

feat: Generate widget classes with MixWidget support

- Developed `buildWidgetClass` function to emit generated wrapper classes for `@MixWidget` targets.
- Implemented field, constructor, and method builders for widget classes.
- Added support for custom widget builders and default Mix widget builders.

feat: Centralize type checkers for Mix generators

- Created `checkers.dart` to hold centralized `TypeChecker` constants for Mix annotations and classes.

fix: Standardize error reporting in Mix generators

- Introduced shared error-reporting utilities to maintain consistent error shapes across generators.

feat: Model widget targets for Mix code generation

- Defined models for `@MixWidget` annotated targets, including configuration and parameter specifications.

feat: Resolve widget builder dispatch strategies

- Implemented logic to resolve which `MixWidgetBuilder` subclass to use based on annotation and target specifications.

test: Add golden tests for custom widget builders

- Created golden tests for custom widget builders to ensure correct generation of expected output.
namedArgs.add('${parameter.name}: ${_parameterReference(parameter)}');
}

return 'const $builderClassName().build($styleArgument, ${namedArgs.join(', ')})';
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This emits the inferred builder as a bare identifier. A valid annotated library can import Mix with a prefix:

import 'package:mix/mix.dart' as mix;

@MixWidget()
final cardStyle = mix.BoxStyler();

The generated part then contains const BoxBuilder().build(...), but BoxBuilder is only visible as mix.BoxBuilder, so the output does not compile. The same resolver issue applies to styleable fields that use target.stylerType.getDisplayString() and emit PrefixedStyler? when only custom.PrefixedStyler is visible. Please resolve emitted builder/style references through the annotated library's import scope, like the direct-widget fallback already does for widget classes.

if (!parameter.isNamed) _parameterReference(parameter),
];
final named = [
for (final parameter in mirroredParameters)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The direct fallback assumes every mirrored call() named parameter is also a same-named parameter on the returned widget constructor, but that is not guaranteed by the styler API. For example:

ButtonWidget call({required String label}) => ButtonWidget(text: label, style: this);

This passes validateDirectFallbackWidget() because the widget has key and style, but generation emits ButtonWidget(label: label, key: key, style: buttonStyle), which does not compile because the constructor takes text, not label. Either validate that the constructor can accept every forwarded parameter by name/type, or dispatch through the styler's call()/a builder so the styler's mapping logic is preserved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant