Skip to content

Commit 656d562

Browse files
committed
Wrap a row or column's children onto new runs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147gqWiq3xoEdAGwWSpq9JK
1 parent f39b9fe commit 656d562

5 files changed

Lines changed: 149 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## Unreleased
22

3+
- `RowColumnNode.wrap`, `spacing` and `runSpacing`: a row or column flows its
4+
children onto new runs when they don't fit, like Flutter's `Wrap`.
35
- `strokeWeight` is a bindable style property of every node with strokes.
46
- A navigate-back (`pop`) action reads from JSON without `destinationId`,
57
which it has no use for. It is still written, as `''`, for readers that

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:json_annotation/json_annotation.dart';
22

3+
import '../field_access.dart';
34
import '../mixins.dart';
45
import '../models/models.dart';
56

@@ -24,6 +25,19 @@ class RowColumnNode extends ParentNode
2425
@override
2526
set scrollDirection(AxisC value) {}
2627

28+
/// Whether the children wrap onto new runs when they don't fit the main
29+
/// axis, like Flutter's [Wrap] widget, instead of being laid out in a single
30+
/// line.
31+
bool wrap;
32+
33+
/// The gap between adjacent children along the main axis within a run.
34+
/// Only used when [wrap] is true.
35+
double spacing;
36+
37+
/// The gap between adjacent runs along the cross axis.
38+
/// Only used when [wrap] is true.
39+
double runSpacing;
40+
2741
/// Creates a [RowColumnNode] with the given data.
2842
RowColumnNode({
2943
required super.id,
@@ -76,6 +90,9 @@ class RowColumnNode extends ParentNode
7690
ScrollViewKeyboardDismissBehaviorC.manual,
7791
bool useFlutterListView = false,
7892
bool shouldAlwaysScroll = true,
93+
this.wrap = false,
94+
this.spacing = 0,
95+
this.runSpacing = 0,
7996
}) {
8097
setCornerMixin(
8198
cornerRadius: cornerRadius,
@@ -111,6 +128,38 @@ class RowColumnNode extends ParentNode
111128
TriggerType.hold,
112129
];
113130

131+
@override
132+
FieldsMap generateFields() => {
133+
...super.generateFields(),
134+
'wrap': BoolFieldAccess(
135+
'Wrap',
136+
'Whether the children wrap onto new runs when they overflow.',
137+
() => wrap,
138+
(value) => wrap = value,
139+
defaultValue: () => false,
140+
requiresLayout: true,
141+
),
142+
'spacing': NumFieldAccess<double>(
143+
'Spacing',
144+
'Gap between children within a run when wrapping.',
145+
() => spacing,
146+
(value) => spacing = value,
147+
min: 0,
148+
defaultValue: () => 0,
149+
requiresLayout: true,
150+
),
151+
'runSpacing': NumFieldAccess<double>(
152+
'Run Spacing',
153+
'Gap between runs when wrapping.',
154+
() => runSpacing,
155+
(value) => runSpacing = value,
156+
min: 0,
157+
defaultValue: () => 0,
158+
requiresLayout: true,
159+
),
160+
};
161+
162+
/// Creates a [RowColumnNode] from [json].
114163
factory RowColumnNode.fromJson(Map json) => _$RowColumnNodeFromJson(json);
115164

116165
@override

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

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

‎test/json_defaults_test.dart‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,18 @@ List<BaseNode> sampleNodes() {
121121
verticalFit: SizeFit.shrinkWrap,
122122
padding: const EdgeInsetsModel.all(8),
123123
),
124+
RowColumnNode(
125+
id: 'wrapRow',
126+
name: 'Wrap Row',
127+
basicBoxLocal: box,
128+
children: ['a', 'b'],
129+
rowColumnType: RowColumnType.row,
130+
mainAxisAlignment: MainAxisAlignmentC.start,
131+
verticalFit: SizeFit.shrinkWrap,
132+
wrap: true,
133+
spacing: 8,
134+
runSpacing: 6,
135+
),
124136
FrameNode(
125137
id: 'frame',
126138
name: 'Frame',
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import 'dart:convert';
2+
3+
import 'package:codelessly_api/codelessly_api.dart';
4+
import 'package:test/test.dart';
5+
6+
Map encodeDecode(Object? json) => jsonDecode(jsonEncode(json)) as Map;
7+
8+
RowColumnNode _row({
9+
bool wrap = false,
10+
double spacing = 0,
11+
double runSpacing = 0,
12+
}) => RowColumnNode(
13+
id: 'chips',
14+
name: 'Chips',
15+
basicBoxLocal: NodeBox(0, 0, 200, 40),
16+
children: ['a', 'b', 'c'],
17+
rowColumnType: RowColumnType.row,
18+
wrap: wrap,
19+
spacing: spacing,
20+
runSpacing: runSpacing,
21+
);
22+
23+
void main() {
24+
group('RowColumnNode wrap', () {
25+
test('defaults to no wrap and zero spacing', () {
26+
final node = _row();
27+
expect(node.wrap, isFalse);
28+
expect(node.spacing, 0);
29+
expect(node.runSpacing, 0);
30+
});
31+
32+
test('default values are left out of JSON', () {
33+
final json = encodeDecode(_row().toJson());
34+
expect(json.containsKey('wrap'), isFalse);
35+
expect(json.containsKey('spacing'), isFalse);
36+
expect(json.containsKey('runSpacing'), isFalse);
37+
});
38+
39+
test('round-trips wrap, spacing and runSpacing', () {
40+
final json = encodeDecode(
41+
_row(wrap: true, spacing: 8, runSpacing: 6.5).toJson(),
42+
);
43+
expect(json['wrap'], isTrue);
44+
expect(json['spacing'], 8);
45+
expect(json['runSpacing'], 6.5);
46+
47+
final back = NodeJsonConverter().fromJson(json)! as RowColumnNode;
48+
expect(back.wrap, isTrue);
49+
expect(back.spacing, 8);
50+
expect(back.runSpacing, 6.5);
51+
expect(encodeDecode(back.toJson()), json);
52+
});
53+
54+
test('reads JSON written without the wrap keys', () {
55+
final json =
56+
encodeDecode(_row(wrap: true, spacing: 8, runSpacing: 6).toJson())
57+
..remove('wrap')
58+
..remove('spacing')
59+
..remove('runSpacing');
60+
final back = RowColumnNode.fromJson(json);
61+
expect(back.wrap, isFalse);
62+
expect(back.spacing, 0);
63+
expect(back.runSpacing, 0);
64+
});
65+
66+
test('exposes wrap fields through generateFields', () {
67+
final node = _row();
68+
final fields = node.generateFields();
69+
fields['wrap']!.setValue(true);
70+
fields['spacing']!.setValue(12.0);
71+
fields['runSpacing']!.setValue(4.0);
72+
expect(node.wrap, isTrue);
73+
expect(node.spacing, 12);
74+
expect(node.runSpacing, 4);
75+
});
76+
});
77+
}

0 commit comments

Comments
 (0)