Skip to content

Commit f16a0a2

Browse files
committed
Wrapping lists, bindable style properties, spelled-out padding
- ListViewProperties.wrap with spacing, run spacing and alignment. - effectsVisible, strokeWeight, cornerRadius and fontWeight are property variables Set value actions can change. - EdgeInsetsModel reads {left, top, right, bottom} and {horizontal, vertical}. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oMokUGNW6txknJs7xxTik
1 parent 8079cff commit f16a0a2

7 files changed

Lines changed: 270 additions & 4 deletions

File tree

‎lib/src/api/mixins.dart‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,8 +1045,9 @@ mixin PropertyVariableMixin on BaseNode {
10451045
/// Properties of the node that can be used in actions or can be changed by
10461046
/// an action.
10471047
///
1048-
/// Style properties (fill, stroke, opacity, width, height; a text's fill is
1049-
/// its color) can be set by
1048+
/// Style properties (fill, stroke, opacity, width, height, effectsVisible,
1049+
/// strokeWeight, cornerRadius; a text's fill is its color, and its
1050+
/// fontWeight a number from 100 to 900) can be set by
10501051
/// conditions, bound to variables or formulas, or changed by a set value
10511052
/// action, on any node that has them.
10521053
late final List<ValueModel> propertyVariables = [
@@ -1056,13 +1057,30 @@ mixin PropertyVariableMixin on BaseNode {
10561057
PaintValue(name: 'fill', value: geometry.fills.firstOrNull),
10571058
PaintValue(name: 'stroke', value: geometry.strokes.firstOrNull),
10581059
],
1059-
if (this case final TextNode text)
1060+
if (this case final TextNode text) ...[
10601061
PaintValue(
10611062
name: 'fill',
10621063
value: text.textMixedProps.firstOrNull?.fills.firstOrNull,
10631064
),
1064-
if (this case final BlendMixin blend)
1065+
IntValue(
1066+
name: 'fontWeight',
1067+
value:
1068+
((text.textMixedProps.firstOrNull?.fontName.weight?.index ?? 3) +
1069+
1) *
1070+
100,
1071+
),
1072+
],
1073+
if (this case final BlendMixin blend) ...[
10651074
DoubleValue(name: 'opacity', value: blend.opacity),
1075+
BoolValue(
1076+
name: 'effectsVisible',
1077+
value: blend.effects.every((effect) => effect.visible),
1078+
),
1079+
],
1080+
if (this case final GeometryMixin geometry)
1081+
DoubleValue(name: 'strokeWeight', value: geometry.strokeWeight),
1082+
if (this case final CornerMixin corner)
1083+
DoubleValue(name: 'cornerRadius', value: corner.cornerRadius.tl.x),
10661084
DoubleValue(name: 'width', value: basicBoxLocal.width),
10671085
DoubleValue(name: 'height', value: basicBoxLocal.height),
10681086
];

‎lib/src/api/models/edge_insets.dart‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,18 @@ class EdgeInsetsModel with Equatable, DynamicSerializableMixin {
190190

191191
/// Factory constructor for creating a new [EdgeInsetsModel] instance
192192
/// from JSON data.
193+
static const Set<String> _longSideKeys = <String>{
194+
'left',
195+
'top',
196+
'right',
197+
'bottom',
198+
'horizontal',
199+
'vertical',
200+
};
201+
202+
static double _side(Map<dynamic, dynamic> json, String side, String axis) =>
203+
((json[side] ?? json[axis] ?? 0) as num).toDouble();
204+
193205
factory EdgeInsetsModel.fromJson(dynamic json) {
194206
return switch (json) {
195207
num all => EdgeInsetsModel.all(all.toDouble()),
@@ -203,6 +215,15 @@ class EdgeInsetsModel with Equatable, DynamicSerializableMixin {
203215
right: right.toDouble(),
204216
bottom: bottom.toDouble(),
205217
),
218+
// Spelled-out sides, as Flutter names them; missing sides are 0.
219+
Map<dynamic, dynamic> sides
220+
when sides.keys.any(_longSideKeys.contains) =>
221+
EdgeInsetsModel.only(
222+
left: _side(sides, 'left', 'horizontal'),
223+
top: _side(sides, 'top', 'vertical'),
224+
right: _side(sides, 'right', 'horizontal'),
225+
bottom: _side(sides, 'bottom', 'vertical'),
226+
),
206227
_ => _$EdgeInsetsModelFromJson(json),
207228
};
208229
}

‎lib/src/api/nodes/list_view_node.dart‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,33 @@ class ListViewProperties extends CustomProperties with FieldsHolder {
201201
/// with motion bound to their index (see [MotionModel]).
202202
bool stacked;
203203

204+
/// Whether the items flow onto further lines (runs) when they don't fit,
205+
/// like Flutter's [Wrap], instead of scrolling in one line. The scroll
206+
/// direction is the wrap direction: a horizontal list fills a line from
207+
/// left to right, then starts a new line below; a vertical list fills a
208+
/// column top to bottom, then starts a new column to the right. Items keep
209+
/// their own size (they are not stretched to the list's cross extent).
210+
/// Separators are not drawn; use [wrapSpacing] and [wrapRunSpacing].
211+
/// Ignored when [stacked] is true.
212+
///
213+
/// A wrapping list shows every item, so it needs an item count or a data
214+
/// source. It scrolls along the cross axis when its runs don't fit its box;
215+
/// give it a shrink-wrap fit on the cross axis to grow with its runs.
216+
bool wrap;
217+
218+
/// The gap between two items of the same line when [wrap] is true.
219+
double wrapSpacing;
220+
221+
/// The gap between two lines (runs) when [wrap] is true.
222+
double wrapRunSpacing;
223+
224+
/// How the items of a line are placed along the wrap direction when [wrap]
225+
/// is true. Maps to Flutter's [WrapAlignment] by name.
226+
MainAxisAlignmentC wrapAlignment;
227+
228+
/// Whether this list wraps its items: [wrap] is on and [stacked] is off.
229+
bool get isWrapping => wrap && !stacked;
230+
204231
/// Creates a new [ListViewProperties] instance.
205232
ListViewProperties({
206233
this.itemCount,
@@ -210,6 +237,10 @@ class ListViewProperties extends CustomProperties with FieldsHolder {
210237
this.separatorSpacing = 0,
211238
this.hasSeparator = false,
212239
this.stacked = false,
240+
this.wrap = false,
241+
this.wrapSpacing = 0,
242+
this.wrapRunSpacing = 0,
243+
this.wrapAlignment = MainAxisAlignmentC.start,
213244
DividerProperties? dividerProperties,
214245
}) : dividerProperties = dividerProperties ?? DividerProperties();
215246

@@ -230,10 +261,18 @@ class ListViewProperties extends CustomProperties with FieldsHolder {
230261
double? dividerSpacing,
231262
bool? hasSeparator,
232263
bool? stacked,
264+
bool? wrap,
265+
double? wrapSpacing,
266+
double? wrapRunSpacing,
267+
MainAxisAlignmentC? wrapAlignment,
233268
DividerProperties? dividerProperties,
234269
}) {
235270
return ListViewProperties(
236271
stacked: stacked ?? this.stacked,
272+
wrap: wrap ?? this.wrap,
273+
wrapSpacing: wrapSpacing ?? this.wrapSpacing,
274+
wrapRunSpacing: wrapRunSpacing ?? this.wrapRunSpacing,
275+
wrapAlignment: wrapAlignment ?? this.wrapAlignment,
237276
itemCount: itemCount ?? this.itemCount,
238277
itemExtent: itemExtent ?? this.itemExtent,
239278
cacheExtent: cacheExtent ?? this.cacheExtent,
@@ -255,6 +294,37 @@ class ListViewProperties extends CustomProperties with FieldsHolder {
255294
(value) => stacked = value,
256295
requiresLayout: true,
257296
),
297+
'wrap': BoolFieldAccess(
298+
'Wrap',
299+
'Flow the items onto further lines when they do not fit, instead of '
300+
'scrolling in one line. Ignored when Stacked is on.',
301+
() => wrap,
302+
(value) => wrap = value,
303+
requiresLayout: true,
304+
),
305+
'wrapSpacing': NumFieldAccess<double>(
306+
'Wrap Spacing',
307+
'The gap between two items of the same line of a wrapping list.',
308+
() => wrapSpacing,
309+
(value) => wrapSpacing = value,
310+
requiresLayout: true,
311+
),
312+
'wrapRunSpacing': NumFieldAccess<double>(
313+
'Wrap Run Spacing',
314+
'The gap between two lines of a wrapping list.',
315+
() => wrapRunSpacing,
316+
(value) => wrapRunSpacing = value,
317+
requiresLayout: true,
318+
),
319+
'wrapAlignment': EnumFieldAccess<MainAxisAlignmentC>(
320+
'Wrap Alignment',
321+
'How the items of a line of a wrapping list are placed along it.',
322+
() => wrapAlignment,
323+
(value) => wrapAlignment = value,
324+
defaultValue: () => MainAxisAlignmentC.start,
325+
options: () => MainAxisAlignmentC.values,
326+
requiresLayout: true,
327+
),
258328
'hasSeparator': BoolFieldAccess(
259329
'Has Separator',
260330
'Whether to use a separator between list view items.',
@@ -304,6 +374,10 @@ class ListViewProperties extends CustomProperties with FieldsHolder {
304374
separatorSpacing,
305375
hasSeparator,
306376
stacked,
377+
wrap,
378+
wrapSpacing,
379+
wrapRunSpacing,
380+
wrapAlignment,
307381
dividerProperties,
308382
];
309383
}

‎lib/src/api/nodes/list_view_node.g.dart‎

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎test/edge_insets_json_test.dart‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import 'package:codelessly_api/codelessly_api.dart';
2+
import 'package:test/test.dart';
3+
4+
void main() {
5+
test('padding reads spelled-out sides', () {
6+
expect(
7+
EdgeInsetsModel.fromJson({'left': 1, 'top': 2, 'right': 3, 'bottom': 4}),
8+
EdgeInsetsModel.only(left: 1, top: 2, right: 3, bottom: 4),
9+
);
10+
expect(
11+
EdgeInsetsModel.fromJson({'horizontal': 16, 'vertical': 8}),
12+
EdgeInsetsModel.symmetric(horizontal: 16, vertical: 8),
13+
);
14+
expect(
15+
EdgeInsetsModel.fromJson({'top': 12}),
16+
EdgeInsetsModel.only(top: 12),
17+
);
18+
});
19+
20+
test('the stored forms still read', () {
21+
expect(EdgeInsetsModel.fromJson(8), EdgeInsetsModel.all(8));
22+
expect(
23+
EdgeInsetsModel.fromJson([1, 2, 3, 4]),
24+
EdgeInsetsModel.only(left: 1, top: 2, right: 3, bottom: 4),
25+
);
26+
expect(
27+
EdgeInsetsModel.fromJson({'l': 1, 't': 2, 'r': 3, 'b': 4}),
28+
EdgeInsetsModel.only(left: 1, top: 2, right: 3, bottom: 4),
29+
);
30+
});
31+
}

‎test/list_wrap_test.dart‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import 'package:codelessly_api/codelessly_api.dart';
2+
import 'package:test/test.dart';
3+
4+
void main() {
5+
test('wrap settings survive JSON and are omitted at their defaults', () {
6+
expect(ListViewProperties().toJson().keys,
7+
isNot(anyOf(contains('wrap'), contains('wrapSpacing'),
8+
contains('wrapRunSpacing'), contains('wrapAlignment'))));
9+
10+
final ListViewProperties props = ListViewProperties(
11+
wrap: true,
12+
wrapSpacing: 8,
13+
wrapRunSpacing: 6,
14+
wrapAlignment: MainAxisAlignmentC.center,
15+
);
16+
final Map<String, dynamic> json = props.toJson();
17+
expect(json['wrap'], isTrue);
18+
expect(json['wrapSpacing'], 8);
19+
expect(json['wrapRunSpacing'], 6);
20+
expect(json['wrapAlignment'], 'center');
21+
22+
final ListViewProperties copy = ListViewProperties.fromJson(json);
23+
expect(copy, props);
24+
expect(copy.isWrapping, isTrue);
25+
expect(props.copyWith(), props);
26+
expect(props.copyWith(stacked: true).isWrapping, isFalse);
27+
});
28+
29+
test('a wrapping list node round-trips', () {
30+
final ListViewNode node = ListViewNode(
31+
id: 'list',
32+
name: 'Crates',
33+
basicBoxLocal: NodeBox(0, 0, 300, 40),
34+
properties: ListViewProperties(wrap: true, wrapSpacing: 4),
35+
children: const [],
36+
scrollDirection: AxisC.horizontal,
37+
);
38+
final ListViewNode copy = ListViewNode.fromJson(node.toJson());
39+
expect(copy.properties.wrap, isTrue);
40+
expect(copy.properties.wrapSpacing, 4);
41+
expect(copy.scrollDirection, AxisC.horizontal);
42+
});
43+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import 'package:codelessly_api/codelessly_api.dart';
2+
import 'package:test/test.dart';
3+
4+
void main() {
5+
test('A shape offers its effects, stroke weight and corner radius to set '
6+
'value actions', () {
7+
final RectangleNode node = RectangleNode(
8+
id: 'chip',
9+
name: 'Chip',
10+
basicBoxLocal: NodeBox(0, 0, 80, 32),
11+
strokeWeight: 2,
12+
cornerRadius: const CornerRadius.all(RadiusModel.circular(6)),
13+
effects: [
14+
Effect.dropShadow(
15+
id: 'shadow',
16+
visible: false,
17+
radius: 0,
18+
color: ColorRGBA.black,
19+
offset: const Vec(2, 2),
20+
spread: 0,
21+
),
22+
],
23+
);
24+
final Map<String, Object?> values = {
25+
for (final ValueModel value in node.propertyVariables)
26+
value.name: value.value,
27+
};
28+
expect(values['effectsVisible'], isFalse);
29+
expect(values['strokeWeight'], 2);
30+
expect(values['cornerRadius'], 6);
31+
});
32+
33+
test('A text offers its font weight as a number', () {
34+
final TextNode node = TextNode(
35+
id: 'text',
36+
name: 'Text',
37+
basicBoxLocal: NodeBox(0, 0, 80, 20),
38+
characters: 'Oat',
39+
textMixedProps: [
40+
StartEndProp(
41+
start: 0,
42+
end: 3,
43+
fontName: const FontName(
44+
family: 'Roboto',
45+
weight: FontWeightNumeric.w700,
46+
),
47+
),
48+
],
49+
);
50+
final ValueModel weight = node.propertyVariables.firstWhere(
51+
(value) => value.name == 'fontWeight',
52+
);
53+
expect(weight.value, 700);
54+
});
55+
}

0 commit comments

Comments
 (0)