Skip to content

Commit 32ea950

Browse files
committed
Row/column spacing and wrap
A row/column passes its spacing to the Flex as the gap between children. With wrap on it builds a Wrap instead: mainAxisAlignment places each run, crossAxisAlignment places children within their run and the runs within the box (stretch and baseline fall back to start), spacing and runSpacing give the gaps. Children can't flex in a Wrap, so expanded or flexible children and spacers take their design main-axis size, and a wrapping row/column never scrolls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyH8twyrBoe4wFCTw78Cz5
1 parent 70f1f0a commit 32ea950

2 files changed

Lines changed: 358 additions & 13 deletions

File tree

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

Lines changed: 131 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:math' as math;
2+
13
import 'package:codelessly_api/codelessly_api.dart';
24
import 'package:flutter/material.dart';
35

@@ -59,6 +61,7 @@ class PassiveRowColumnTransformer extends NodeWidgetTransformer<RowColumnNode> {
5961
final SizeFit mainFit = isRow
6062
? rowColumnNode.horizontalFit
6163
: rowColumnNode.verticalFit;
64+
final bool wraps = rowColumnWraps(rowColumnNode);
6265
final bool crossWraps = isRow
6366
? rowColumnNode.isVerticalWrap
6467
: rowColumnNode.isHorizontalWrap;
@@ -77,7 +80,9 @@ class PassiveRowColumnTransformer extends NodeWidgetTransformer<RowColumnNode> {
7780
rowColumnNode.crossAxisAlignment == CrossAxisAlignmentC.stretch;
7881
bool measureCross = false;
7982
bool stretchFallsBack = false;
80-
if (crossMin == null && crossWraps) {
83+
// A Wrap has no stretch, and its children don't align across the run
84+
// (see [wrapChildWithSizeFits]), so it never needs measuring.
85+
if (crossMin == null && crossWraps && !wraps) {
8186
final bool anyChildNeedsAlignment = childrenNodes.any(
8287
(child) => child.alignment != AlignmentModel.none,
8388
);
@@ -90,24 +95,41 @@ class PassiveRowColumnTransformer extends NodeWidgetTransformer<RowColumnNode> {
9095
}
9196
}
9297

93-
Widget res = CodelesslyFlex(
94-
direction: isRow ? Axis.horizontal : Axis.vertical,
95-
mainAxisAlignment: rowColumnNode.mainAxisAlignment.flutterAxis,
96-
crossAxisAlignment: stretchFallsBack
97-
? CrossAxisAlignment.start
98-
: rowColumnNode.crossAxisAlignment.flutterAxis,
99-
mainAxisSize: mainFit == SizeFit.shrinkWrap
100-
? MainAxisSize.min
101-
: MainAxisSize.max,
102-
key: ValueKey(rowColumnNode.id),
103-
children: childrenWidgets,
104-
);
98+
Widget res = wraps
99+
? Wrap(
100+
key: ValueKey(rowColumnNode.id),
101+
direction: isRow ? Axis.horizontal : Axis.vertical,
102+
alignment: wrapAlignment(rowColumnNode.mainAxisAlignment),
103+
crossAxisAlignment: wrapCrossAlignment(
104+
rowColumnNode.crossAxisAlignment,
105+
),
106+
runAlignment: wrapRunAlignment(rowColumnNode.crossAxisAlignment),
107+
spacing: rowColumnSpacing(rowColumnNode),
108+
runSpacing: (rowColumnNode as RowColumnNode).runSpacing,
109+
children: childrenWidgets,
110+
)
111+
: CodelesslyFlex(
112+
direction: isRow ? Axis.horizontal : Axis.vertical,
113+
mainAxisAlignment: rowColumnNode.mainAxisAlignment.flutterAxis,
114+
crossAxisAlignment: stretchFallsBack
115+
? CrossAxisAlignment.start
116+
: rowColumnNode.crossAxisAlignment.flutterAxis,
117+
mainAxisSize: mainFit == SizeFit.shrinkWrap
118+
? MainAxisSize.min
119+
: MainAxisSize.max,
120+
spacing: rowColumnSpacing(rowColumnNode),
121+
key: ValueKey(rowColumnNode.id),
122+
children: childrenWidgets,
123+
);
105124

106125
if (measureCross) {
107126
res = isRow ? IntrinsicHeight(child: res) : IntrinsicWidth(child: res);
108127
}
109128

129+
// A wrapping row/column flows onto new runs instead of scrolling along
130+
// its main axis, so it never scrolls.
110131
if (withScroll &&
132+
!wraps &&
111133
rowColumnNode is ScrollableMixin &&
112134
(rowColumnNode as ScrollableMixin).isScrollable) {
113135
res = wrapWithPaddedScrollable(
@@ -146,6 +168,93 @@ class PassiveRowColumnTransformer extends NodeWidgetTransformer<RowColumnNode> {
146168
);
147169
}
148170

171+
/// Whether [node] is a row/column whose children flow onto new runs when
172+
/// they don't fit ([RowColumnNode.wrap]), built as a [Wrap].
173+
static bool rowColumnWraps(BaseNode node) =>
174+
node is RowColumnNode && node.wrap;
175+
176+
/// The main-axis gap between [node]'s children ([RowColumnNode.spacing]).
177+
static double rowColumnSpacing(BaseNode node) =>
178+
node is RowColumnNode ? math.max(0, node.spacing) : 0;
179+
180+
/// The [Wrap.alignment] of a wrapping row/column: how each run places its
181+
/// children along the main axis, as [MainAxisAlignmentC] does for a Flex.
182+
static WrapAlignment wrapAlignment(MainAxisAlignmentC alignment) =>
183+
switch (alignment) {
184+
MainAxisAlignmentC.start => WrapAlignment.start,
185+
MainAxisAlignmentC.center => WrapAlignment.center,
186+
MainAxisAlignmentC.end => WrapAlignment.end,
187+
MainAxisAlignmentC.spaceBetween => WrapAlignment.spaceBetween,
188+
MainAxisAlignmentC.spaceAround => WrapAlignment.spaceAround,
189+
MainAxisAlignmentC.spaceEvenly => WrapAlignment.spaceEvenly,
190+
};
191+
192+
/// The [Wrap.crossAxisAlignment] of a wrapping row/column: how children
193+
/// sit within their run. A Wrap can't stretch or align baselines; those
194+
/// place children at the start of the run.
195+
static WrapCrossAlignment wrapCrossAlignment(CrossAxisAlignmentC alignment) =>
196+
switch (alignment) {
197+
CrossAxisAlignmentC.center => WrapCrossAlignment.center,
198+
CrossAxisAlignmentC.end => WrapCrossAlignment.end,
199+
CrossAxisAlignmentC.start ||
200+
CrossAxisAlignmentC.stretch ||
201+
CrossAxisAlignmentC.baseline => WrapCrossAlignment.start,
202+
};
203+
204+
/// The [Wrap.runAlignment] of a wrapping row/column. The runs follow the
205+
/// cross-axis alignment too, so a wrapping row whose children fit on one
206+
/// run places them like the same row without wrapping.
207+
static WrapAlignment wrapRunAlignment(CrossAxisAlignmentC alignment) =>
208+
switch (alignment) {
209+
CrossAxisAlignmentC.center => WrapAlignment.center,
210+
CrossAxisAlignmentC.end => WrapAlignment.end,
211+
CrossAxisAlignmentC.start ||
212+
CrossAxisAlignmentC.stretch ||
213+
CrossAxisAlignmentC.baseline => WrapAlignment.start,
214+
};
215+
216+
/// [childWidget] as a child of a wrapping row/column.
217+
///
218+
/// A [Wrap] has no free space to share, so children can't flex in it: a
219+
/// child that is expanded or flexible on the main axis, and a spacer, take
220+
/// their design main-axis size. Children keep their own size across the
221+
/// run (there is no stretch) and their own alignment is not applied.
222+
static Widget wrapChildInWrap(
223+
BaseNode node,
224+
RowColumnMixin parentRowColumn,
225+
Widget childWidget, {
226+
WidgetInserter? flexibleSpaceBackground,
227+
}) {
228+
final bool isRow = parentRowColumn.rowColumnType == RowColumnType.row;
229+
final SizeFit mainFit = isRow ? node.horizontalFit : node.verticalFit;
230+
final double designMain = isRow
231+
? node.outerBoxLocal.width
232+
: node.outerBoxLocal.height;
233+
234+
if (node is SpacerNode) {
235+
return SizedBox(
236+
width: isRow ? designMain : 0,
237+
height: isRow ? 0 : designMain,
238+
);
239+
}
240+
241+
// A transformer may hand back a flex parent-data widget for a Flex.
242+
while (childWidget is Flexible) {
243+
childWidget = childWidget.child;
244+
}
245+
if (flexibleSpaceBackground != null) {
246+
childWidget = flexibleSpaceBackground(childWidget);
247+
}
248+
if (mainFit.isFlex) {
249+
childWidget = SizedBox(
250+
width: isRow ? designMain : null,
251+
height: isRow ? null : designMain,
252+
child: childWidget,
253+
);
254+
}
255+
return childWidget;
256+
}
257+
149258
/// The design extent of a Row/Column's own box on one axis, or null when it
150259
/// hugs its children.
151260
///
@@ -192,6 +301,15 @@ class PassiveRowColumnTransformer extends NodeWidgetTransformer<RowColumnNode> {
192301
}) {
193302
assert(parentRowColumn is RowColumnMixin);
194303

304+
if (rowColumnWraps(parentRowColumn)) {
305+
return wrapChildInWrap(
306+
node,
307+
parentRowColumn as RowColumnMixin,
308+
childWidget,
309+
flexibleSpaceBackground: flexibleSpaceBackground,
310+
);
311+
}
312+
195313
if (node is SpacerNode) {
196314
return childWidget;
197315
}

0 commit comments

Comments
 (0)