Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Add support for widget builders.
  • Loading branch information
tugorez committed Mar 6, 2024
commit 93d05539c5afde8bb7e8616bfeff52b18c77311b
23 changes: 23 additions & 0 deletions packages/rfw/lib/src/dart/binary.dart
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,8 @@ const int _msEvent = 0x0E;
const int _msSwitch = 0x0F;
const int _msDefault = 0x10;
const int _msSetState = 0x11;
const int _msWidgetBuilder = 0x12;
const int _msWidgetBuilderArgReference = 0x13;

/// API for decoding Remote Flutter Widgets binary blobs.
///
Expand Down Expand Up @@ -453,6 +455,10 @@ class _BlobDecoder {
return _readSwitch();
case _msSetState:
return SetStateHandler(StateReference(_readPartList()), _readArgument());
case _msWidgetBuilder:
return _readWidgetBuilder();
case _msWidgetBuilderArgReference:
return WidgetBuilderArgReference(_readString(), _readPartList());
default:
return _parseValue(type, _readArgument);
}
Expand All @@ -468,6 +474,14 @@ class _BlobDecoder {
return ConstructorCall(name, _readMap(_readArgument)!);
}

WidgetBuilderDeclaration _readWidgetBuilder() {
final String argumentName = _readString();
final int type = _readByte();
assert(type == _msWidget || type == _msSwitch);
final BlobNode widget = type == _msWidget ? _readWidget() : _readSwitch();
return WidgetBuilderDeclaration(argumentName, widget);
}

WidgetDeclaration _readDeclaration() {
final String name = _readString();
final DynamicMap? initialState = _readMap(readValue, nullIfEmpty: true);
Expand Down Expand Up @@ -613,6 +627,10 @@ class _BlobEncoder {
bytes.addByte(_msWidget);
_writeString(value.name);
_writeMap(value.arguments, _writeArgument);
} else if (value is WidgetBuilderDeclaration) {
bytes.addByte(_msWidgetBuilder);
_writeString(value.argumentName);
_writeArgument(value.widget);
} else if (value is ArgsReference) {
bytes.addByte(_msArgsReference);
_writeInt64(value.parts.length);
Expand All @@ -621,6 +639,11 @@ class _BlobEncoder {
bytes.addByte(_msDataReference);
_writeInt64(value.parts.length);
value.parts.forEach(_writePart);
} else if (value is WidgetBuilderArgReference) {
bytes.addByte(_msWidgetBuilderArgReference);
_writeString(value.argumentName);
_writeInt64(value.parts.length);
value.parts.forEach(_writePart);
} else if (value is LoopReference) {
bytes.addByte(_msLoopReference);
_writeInt64(value.loop);
Expand Down
45 changes: 45 additions & 0 deletions packages/rfw/lib/src/dart/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,26 @@ class ConstructorCall extends BlobNode {
String toString() => '$name($arguments)';
}

/// Representation of functions that return widgets in Remote Flutter library blobs.
class WidgetBuilderDeclaration extends BlobNode {
/// Creates a [WidgetBuilderDeclaration].
Copy link
Contributor

Choose a reason for hiding this comment

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

this documentation is insufficient per our style guide which says that documentation needs to have more information than can be guessed from just the name of the member being documented.
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#documentation-dartdocs-javadocs-etc

Copy link
Contributor Author

Choose a reason for hiding this comment

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

PTAL

const WidgetBuilderDeclaration(this.argumentName, this.widget);

/// The name associated with the passed [DynamicMap].
final String argumentName;

/// The widget that will be returned when the builder is called.
///
/// This is usually a [ConstructorCall], but may be a [Switch] (so long as
/// that [Switch] resolves to a [ConstructorCall]. Other values (or a [Switch]
/// that does not resolve to a constructor call) will result in an
/// [ErrorWidget] being used.
final BlobNode widget;

@override
String toString() => '($argumentName) => $widget';
}

/// Base class for various kinds of references in the RFW data structures.
abstract class Reference extends BlobNode {
/// Abstract const constructor. This constructor enables subclasses to provide
Expand Down Expand Up @@ -534,6 +554,31 @@ class DataReference extends Reference {
String toString() => 'data.${parts.join(".")}';
}

/// Reference to the [DynamicMap] passed into the widget builder.
///
/// This class is used to represent references to a function argument.
/// In "(scope) => Container(width: scope.width)" this represents "scope.width".
///
/// See also:
///
/// * [WidgetBuilderDeclaration] which represents a widget builder definition.
class WidgetBuilderArgReference extends Reference {
/// Wraps the given [parts] associated to the [argumentName] as an [WidgetBuilderArgReference].
///
/// The parts must not be mutated after the object is created.
const WidgetBuilderArgReference(this.argumentName, super.parts);

/// References the function argument name.
final String argumentName;

WidgetBuilderArgReference constructReference(List<Object> moreParts) {
return WidgetBuilderArgReference(argumentName, parts + moreParts);
}

@override
String toString() => '$argumentName.${parts.join('.')}';
}

/// Unbound reference to a [Loop].
class LoopReference extends Reference {
/// Wraps the given [loop] and [parts] as a [LoopReference].
Expand Down
147 changes: 123 additions & 24 deletions packages/rfw/lib/src/dart/text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,8 @@ DynamicMap parseDataFile(String file) {
/// declaration, along with its arguments. Arguments are a map of key-value
/// pairs, where the values can be any of the types in the data model defined
/// above plus any of the types defined below in this section, such as
/// references to arguments, the data model, loops, state, switches, or
/// event handlers.
/// references to arguments, the data model, widget builders, loops, state,
/// switches or event handlers.
///
/// In this example, several constructor calls are nested together:
///
Expand All @@ -283,6 +283,9 @@ DynamicMap parseDataFile(String file) {
/// Container(
/// child: Text(text: "Hello"),
/// ),
/// Builder(
/// builder: (scope) => Text(text: scope.world),
/// ),
/// ],
/// );
/// ```
Expand All @@ -293,6 +296,35 @@ DynamicMap parseDataFile(String file) {
/// constructor call also has only one argument, `child`, whose value, again, is
/// a constructor call, in this case creating a `Text` widget.
///
/// ### Widget Builders
///
/// Widget builders take a single argument and return a widget.
/// The [DynamicMap] argument consists of key-value pairs where values
/// can be of any types in the data model. Widget builders arguments are lexically
/// scoped so a given constructor call has access to any arguments where it is
/// defined plus arguments defined by its parents (if any).
///
/// In this example several widget builders are nested together:
///
/// ```
/// widget Foo {text: 'this is cool'} = Builder(
/// builder: (foo) => Builder(
/// builder: (bar) => Builder(
/// builder: (baz) => Text(
/// text: [
/// args.text,
/// state.text,
/// data.text,
/// foo.text,
/// bar.text,
/// baz.text,
/// ],
/// ),
/// ),
/// ),
/// );
/// ```
///
/// ### References
///
/// Remote widget libraries typically contain _references_, e.g. to the
Expand Down Expand Up @@ -610,6 +642,12 @@ const Set<String> _reservedWords = <String>{
'true',
};

void _checkIsNotReservedWord(String identifier, _Token identifierToken) {
if (_reservedWords.contains(identifier)) {
throw ParserException._fromToken('$identifier is a reserved word', identifierToken);
}
}

sealed class _Token {
_Token(this.line, this.column, this.start, this.end);
final int line;
Expand All @@ -630,6 +668,7 @@ class _SymbolToken extends _Token {
static const int colon = 0x3A; // U+003A COLON character (:)
static const int semicolon = 0x3B; // U+003B SEMICOLON character (;)
static const int equals = 0x3D; // U+003D EQUALS SIGN character (=)
static const int greatherThan = 0x3E; // U+003D GREATHER THAN character (>)
static const int openBracket = 0x5B; // U+005B LEFT SQUARE BRACKET character ([)
static const int closeBracket = 0x5D; // U+005D RIGHT SQUARE BRACKET character (])
static const int openBrace = 0x7B; // U+007B LEFT CURLY BRACKET character ({)
Expand Down Expand Up @@ -812,6 +851,7 @@ Iterable<_Token> _tokenize(String file) sync* {
case 0x3A: // U+003A COLON character (:)
case 0x3B: // U+003B SEMICOLON character (;)
case 0x3D: // U+003D EQUALS SIGN character (=)
case 0x3E: // U+003E GREATHER THAN SIGN character (>)
case 0x5B: // U+005B LEFT SQUARE BRACKET character ([)
case 0x5D: // U+005D RIGHT SQUARE BRACKET character (])
case 0x7B: // U+007B LEFT CURLY BRACKET character ({)
Expand Down Expand Up @@ -2132,22 +2172,35 @@ class _Parser {
return _readString();
}

DynamicMap _readMap({ required bool extended }) {
DynamicMap _readMap({
required bool extended,
List<String> widgetBuilderScope = const <String>[],
}) {
_expectSymbol(_SymbolToken.openBrace);
final DynamicMap results = _readMapBody(extended: extended);
final DynamicMap results = _readMapBody(
widgetBuilderScope: widgetBuilderScope,
extended: extended,
);
_expectSymbol(_SymbolToken.closeBrace);
return results;
}

DynamicMap _readMapBody({ required bool extended }) {
DynamicMap _readMapBody({
required bool extended,
List<String> widgetBuilderScope = const <String>[],
}) {
final DynamicMap results = DynamicMap(); // ignore: prefer_collection_literals
while (_source.current is! _SymbolToken) {
final String key = _readKey();
if (results.containsKey(key)) {
throw ParserException._fromToken('Duplicate key "$key" in map', _source.current);
}
_expectSymbol(_SymbolToken.colon);
final Object value = _readValue(extended: extended, nullOk: true);
final Object value = _readValue(
extended: extended,
nullOk: true,
widgetBuilderScope: widgetBuilderScope,
);
if (value != missing) {
results[key] = value;
}
Expand All @@ -2162,7 +2215,10 @@ class _Parser {

final List<String> _loopIdentifiers = <String>[];

DynamicList _readList({ required bool extended }) {
DynamicList _readList({
required bool extended,
List<String> widgetBuilderScope = const <String>[],
}) {
final DynamicList results = DynamicList.empty(growable: true);
_expectSymbol(_SymbolToken.openBracket);
while (!_foundSymbol(_SymbolToken.closeBracket)) {
Expand All @@ -2172,19 +2228,26 @@ class _Parser {
_expectIdentifier('for');
final _Token loopIdentifierToken = _source.current;
final String loopIdentifier = _readIdentifier();
if (_reservedWords.contains(loopIdentifier)) {
throw ParserException._fromToken('$loopIdentifier is a reserved word', loopIdentifierToken);
}
_checkIsNotReservedWord(loopIdentifier, loopIdentifierToken);
_expectIdentifier('in');
final Object collection = _readValue(extended: true);
final Object collection = _readValue(
widgetBuilderScope: widgetBuilderScope,
extended: true,
);
_expectSymbol(_SymbolToken.colon);
_loopIdentifiers.add(loopIdentifier);
final Object template = _readValue(extended: extended);
final Object template = _readValue(
widgetBuilderScope: widgetBuilderScope,
extended: extended,
);
assert(_loopIdentifiers.last == loopIdentifier);
_loopIdentifiers.removeLast();
results.add(_withSourceRange(Loop(collection, template), start));
} else {
final Object value = _readValue(extended: extended);
final Object value = _readValue(
widgetBuilderScope: widgetBuilderScope,
extended: extended,
);
results.add(value);
}
if (_foundSymbol(_SymbolToken.comma)) {
Expand All @@ -2197,8 +2260,10 @@ class _Parser {
return results;
}

Switch _readSwitch(SourceLocation? start) {
final Object value = _readValue(extended: true);
Switch _readSwitch(SourceLocation? start, {
List<String> widgetBuilderScope = const <String>[],
}) {
final Object value = _readValue(extended: true, widgetBuilderScope: widgetBuilderScope);
final Map<Object?, Object> cases = <Object?, Object>{};
_expectSymbol(_SymbolToken.openBrace);
while (_source.current is! _SymbolToken) {
Expand All @@ -2210,13 +2275,13 @@ class _Parser {
key = null;
_advance();
} else {
key = _readValue(extended: true);
key = _readValue(extended: true, widgetBuilderScope: widgetBuilderScope);
if (cases.containsKey(key)) {
throw ParserException._fromToken('Switch has duplicate cases for key $key', _source.current);
}
}
_expectSymbol(_SymbolToken.colon);
final Object value = _readValue(extended: true);
final Object value = _readValue(extended: true, widgetBuilderScope: widgetBuilderScope);
cases[key] = value;
if (_foundSymbol(_SymbolToken.comma)) {
_advance();
Expand Down Expand Up @@ -2249,13 +2314,19 @@ class _Parser {
return results;
}

Object _readValue({ required bool extended, bool nullOk = false }) {
Object _readValue({
required bool extended,
bool nullOk = false,
List<String> widgetBuilderScope = const <String>[],
}) {
if (_source.current is _SymbolToken) {
switch ((_source.current as _SymbolToken).symbol) {
case _SymbolToken.openBracket:
return _readList(extended: extended);
return _readList(widgetBuilderScope: widgetBuilderScope, extended: extended);
case _SymbolToken.openBrace:
return _readMap(extended: extended);
return _readMap(widgetBuilderScope: widgetBuilderScope, extended: extended);
case _SymbolToken.openParen:
return _readWidgetBuilderDeclaration(widgetBuilderScope: widgetBuilderScope);
}
} else if (_source.current is _IntegerToken) {
final Object result = (_source.current as _IntegerToken).value;
Expand Down Expand Up @@ -2306,10 +2377,15 @@ class _Parser {
_advance();
return _withSourceRange(StateReference(_readParts()), start);
}
if (widgetBuilderScope.contains(identifier)) {
final SourceLocation? start = _getSourceLocation();
_advance();
return _withSourceRange(WidgetBuilderArgReference(identifier, _readParts()), start);
}
if (identifier == 'switch') {
final SourceLocation? start = _getSourceLocation();
_advance();
return _readSwitch(start);
return _readSwitch(start, widgetBuilderScope: widgetBuilderScope);
}
if (identifier == 'set') {
final SourceLocation? start = _getSourceLocation();
Expand All @@ -2327,16 +2403,39 @@ class _Parser {
_advance();
return _withSourceRange(LoopReference(_loopIdentifiers.length - index, _readParts(optional: true)), start);
}
return _readConstructorCall();
return _readConstructorCall(widgetBuilderScope: widgetBuilderScope);
}
throw ParserException._unexpected(_source.current);
}

ConstructorCall _readConstructorCall() {
WidgetBuilderDeclaration _readWidgetBuilderDeclaration({
List<String> widgetBuilderScope = const <String>[],
}) {
_expectSymbol(_SymbolToken.openParen);
final _Token argumentNameToken = _source.current;
final String argumentName = _readIdentifier();
_checkIsNotReservedWord(argumentName, argumentNameToken);
_expectSymbol(_SymbolToken.closeParen);
_expectSymbol(_SymbolToken.equals);
_expectSymbol(_SymbolToken.greatherThan);
final Object widget = _readValue(
extended: true,
widgetBuilderScope: <String>[...widgetBuilderScope, argumentName],
);
assert(widget is ConstructorCall || widget is Switch);
return WidgetBuilderDeclaration(argumentName, widget as BlobNode);
}

ConstructorCall _readConstructorCall({
List<String> widgetBuilderScope = const <String>[],
}) {
final SourceLocation? start = _getSourceLocation();
final String name = _readIdentifier();
_expectSymbol(_SymbolToken.openParen);
final DynamicMap arguments = _readMapBody(extended: true);
final DynamicMap arguments = _readMapBody(
extended: true,
widgetBuilderScope: widgetBuilderScope,
);
_expectSymbol(_SymbolToken.closeParen);
return _withSourceRange(ConstructorCall(name, arguments), start);
}
Expand Down
Loading