Skip to content

Commit e2f91df

Browse files
committed
Formula functions, wrapping lists, style bindings, per-row list previews
- Formulas: padLeft/padRight, substring, replace, split, indexOf, matches, slug, seconds/duration, slice/take/skip/reverse/findIndex, index params in lambdas, multi-key sortBy and sortByDesc, object literals with merge/set, money decimals, luminance. - Lists can wrap their items onto further lines. - StyleBindings: effects, stroke weight, icon, font weight and corner radius follow variables and formulas, on the canvas and at runtime. - Bindings inside a list row read that row's item on the design canvas. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oMokUGNW6txknJs7xxTik
1 parent 0948e93 commit e2f91df

14 files changed

Lines changed: 2186 additions & 90 deletions

‎lib/src/transformers/node_transformers/passive_button_transformer.dart‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ class PassiveButtonWidget extends StatelessWidget {
131131
final List<TextSpan> spans = buttonLabelTemplateOf(node)
132132
.splitMap(
133133
variableSyntaxIdentifierRegex,
134-
onMatch: (match) => VariableSpan(variable: match[0]!, node: node),
134+
onMatch: (match) =>
135+
VariableSpan(variable: match[0]!, node: node, context: context),
135136
onNonMatch: (text) => TextSpan(text: text),
136137
)
137138
.toList();

‎lib/src/transformers/node_transformers/passive_list_view_transformer.dart‎

Lines changed: 165 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -75,21 +75,59 @@ class PassiveListViewWidget extends StatelessWidget {
7575
}
7676

7777
/// Sizes the list by [ViewportLayout] around [builder].
78+
///
79+
/// A wrapping list is sized by [WrapNodeBox] instead, and the flag given
80+
/// to [builder] says whether its runs scroll along the cross axis.
7881
Widget box(ViewportContentBuilder builder, {required bool canShrinkWrap}) =>
79-
ViewportNodeBox(
80-
node: node,
82+
node.properties.isWrapping
83+
? WrapNodeBox(node: node, builder: builder)
84+
: ViewportNodeBox(
85+
node: node,
86+
scrollDirection: node.scrollDirection,
87+
canShrinkWrap: canShrinkWrap,
88+
builder: builder,
89+
);
90+
91+
/// Builds one item, placed as the editor lays it out; see [ListItemBox].
92+
/// The item of a wrapping list keeps its own size; see [WrapItemBox].
93+
Widget buildItem(BaseNode item, BuildContext context) {
94+
final Widget child = manager.buildWidgetByID(
95+
item.id,
96+
context,
97+
settings: settings,
98+
);
99+
if (node.properties.isWrapping) {
100+
return WrapItemBox(
101+
item: item,
81102
scrollDirection: node.scrollDirection,
82-
canShrinkWrap: canShrinkWrap,
83-
builder: builder,
103+
itemExtent: node.properties.itemExtent,
104+
child: child,
84105
);
106+
}
107+
return ListItemBox(
108+
item: item,
109+
scrollDirection: node.scrollDirection,
110+
itemExtent: node.properties.itemExtent,
111+
child: child,
112+
);
113+
}
85114

86-
/// Builds one item, placed as the editor lays it out; see [ListItemBox].
87-
Widget buildItem(BaseNode item, BuildContext context) => ListItemBox(
88-
item: item,
89-
scrollDirection: node.scrollDirection,
90-
itemExtent: node.properties.itemExtent,
91-
child: manager.buildWidgetByID(item.id, context, settings: settings),
92-
);
115+
/// The items of a wrapping list; [scrolls] comes from [WrapNodeBox].
116+
Widget wrapped(bool scrolls, int itemCount, IndexedWidgetBuilder builder) =>
117+
WrappedItems(
118+
itemCount: itemCount,
119+
itemBuilder: builder,
120+
direction: node.scrollDirection.flutterAxis,
121+
spacing: node.properties.wrapSpacing,
122+
runSpacing: node.properties.wrapRunSpacing,
123+
alignment: node.properties.wrapAlignment.flutterWrapAlignment,
124+
padding: node.padding.flutterEdgeInsets,
125+
scrolls: scrolls,
126+
physics: node.physics.flutterScrollPhysics(false),
127+
keyboardDismissBehavior:
128+
node.keyboardDismissBehavior.flutterKeyboardDismissBehavior,
129+
clipBehavior: node.clipsContent ? Clip.hardEdge : Clip.none,
130+
);
93131

94132
@override
95133
Widget build(BuildContext context) {
@@ -170,6 +208,23 @@ class PassiveListViewWidget extends StatelessWidget {
170208
final List<QueryDocumentSnapshot<Map>> docs = snapshot.docs;
171209
final itemCount = docs.length;
172210

211+
// A wrapping list shows the loaded page, without paging on.
212+
if (node.properties.isWrapping) {
213+
return wrapped(shrinkWrap, itemCount, (context, index) {
214+
final doc = docs[index];
215+
return IndexedItemProvider(
216+
key: ValueKey(index),
217+
item: IndexedItem(index, {'id': doc.id, ...doc.data()}),
218+
child: Builder(
219+
builder: (context) => KeyedSubtree(
220+
key: ValueKey(index),
221+
child: buildItem(item, context),
222+
),
223+
),
224+
);
225+
});
226+
}
227+
173228
// Item Count always exists.
174229
return ListViewBuilder(
175230
primary: node.primary,
@@ -294,6 +349,15 @@ class PassiveListViewWidget extends StatelessWidget {
294349
);
295350
}
296351

352+
// A wrapping list flows its items onto further lines, all of them built,
353+
// so an endless one shows nothing.
354+
if (node.properties.isWrapping) {
355+
return box(
356+
canShrinkWrap: true,
357+
(context, scrolls) => wrapped(scrolls, itemCount ?? 0, itemBuilder),
358+
);
359+
}
360+
297361
// An endless list with items of free extent ends after a viewport's
298362
// worth of items that take no space, so they cannot be built without
299363
// limit; see [EndlessList]. A fixed item extent is bounded by Flutter.
@@ -455,7 +519,6 @@ class ListViewItemSeparator extends StatelessWidget {
455519
}
456520
}
457521

458-
459522
/// The items of a stacked list: drawn on top of each other in the list's
460523
/// box, each at its top center, the first item on top.
461524
class StackedItems extends StatelessWidget {
@@ -493,3 +556,93 @@ class StackedItems extends StatelessWidget {
493556
],
494557
);
495558
}
559+
560+
/// The items of a wrapping list: laid out by a [Wrap] along [direction],
561+
/// flowing onto further lines (runs) when a line is full, each item at its
562+
/// own size.
563+
///
564+
/// With [scrolls], the runs scroll along the cross axis when they don't fit
565+
/// the box; otherwise the box grows with them.
566+
class WrappedItems extends StatelessWidget {
567+
/// Creates a [WrappedItems].
568+
const WrappedItems({
569+
super.key,
570+
required this.itemCount,
571+
required this.itemBuilder,
572+
this.direction = Axis.horizontal,
573+
this.spacing = 0,
574+
this.runSpacing = 0,
575+
this.alignment = WrapAlignment.start,
576+
this.padding = EdgeInsets.zero,
577+
this.scrolls = false,
578+
this.physics,
579+
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
580+
this.clipBehavior = Clip.none,
581+
});
582+
583+
/// How many items.
584+
final int itemCount;
585+
586+
/// Builds an item.
587+
final IndexedWidgetBuilder itemBuilder;
588+
589+
/// The wrap direction: the list's scroll direction.
590+
final Axis direction;
591+
592+
/// The gap between two items of a line.
593+
final double spacing;
594+
595+
/// The gap between two lines.
596+
final double runSpacing;
597+
598+
/// How the items of a line are placed along it.
599+
final WrapAlignment alignment;
600+
601+
/// The list's padding, around the runs.
602+
final EdgeInsets padding;
603+
604+
/// Whether the runs scroll along the cross axis.
605+
final bool scrolls;
606+
607+
/// The physics of that scroll.
608+
final ScrollPhysics? physics;
609+
610+
/// See [ScrollView.keyboardDismissBehavior].
611+
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
612+
613+
/// Whether the runs are clipped to the box.
614+
final Clip clipBehavior;
615+
616+
@override
617+
Widget build(BuildContext context) {
618+
final Widget wrap = Wrap(
619+
direction: direction,
620+
spacing: spacing,
621+
runSpacing: runSpacing,
622+
alignment: alignment,
623+
children: [
624+
for (int index = 0; index < itemCount; index++)
625+
itemBuilder(context, index),
626+
],
627+
);
628+
if (scrolls) {
629+
return DraggableScrollConfiguration(
630+
child: SingleChildScrollView(
631+
primary: false,
632+
scrollDirection: direction == Axis.horizontal
633+
? Axis.vertical
634+
: Axis.horizontal,
635+
padding: padding,
636+
physics: physics,
637+
keyboardDismissBehavior: keyboardDismissBehavior,
638+
clipBehavior: clipBehavior,
639+
child: wrap,
640+
),
641+
);
642+
}
643+
final Widget padded = Padding(padding: padding, child: wrap);
644+
return clipBehavior == Clip.none
645+
? padded
646+
: ClipRect(clipBehavior: clipBehavior, child: padded);
647+
}
648+
}

‎lib/src/transformers/passive_transformer_manager.dart‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ class PassiveNodeTransformerManager extends WidgetNodeTransformerManager {
128128
/// Returns [node] with the style properties that conditions, bound
129129
/// variables or formulas, or set value actions give it at runtime: `fill`
130130
/// (a text's color for text nodes), `stroke`, `opacity`, `width` and
131-
/// `height`. Returns [node] itself when
131+
/// `height`, and the properties of [StyleBindings] (effects, stroke
132+
/// weight, corner radius, font weight, icon). Returns [node] itself when
132133
/// nothing changes it.
133134
BaseNode applyStyleOverrides(BuildContext context, BaseNode node) {
134135
final ScopedValues scopedValues = ScopedValues.of(context);
@@ -191,6 +192,9 @@ class PassiveNodeTransformerManager extends WidgetNodeTransformerManager {
191192
).toJson();
192193
}
193194

195+
// Effects, stroke weight, corner radius, font weight and icon.
196+
StyleBindings.addTo(patch, node, scopedValues: scopedValues);
197+
194198
if (patch.isEmpty) return node;
195199
final BaseNode? styled = NodeJsonConverter().fromJson({
196200
...node.toJson(),

‎lib/src/transformers/transformers.dart‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export 'utils/node_geometry.dart';
1414
export 'utils/property_value_delegate.dart';
1515
export 'utils/scoped_values.dart';
1616
export 'utils/stroke_painter.dart';
17+
export 'utils/style_bindings.dart';
1718
export 'utils/text_utils.dart';
1819
export 'utils/variable_span.dart';
1920
export 'utils/viewport_sizing.dart';

0 commit comments

Comments
 (0)