Skip to content

Commit 67611ab

Browse files
committed
Rows and columns lay out itemSpacing between children
Formula-hidden children take no gap; children keep their state while siblings show and hide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013oMokUGNW6txknJs7xxTik
1 parent 778f86f commit 67611ab

2 files changed

Lines changed: 476 additions & 1 deletion

File tree

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

Lines changed: 172 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,124 @@ typedef WidgetInserter = Widget Function(Widget child);
88
class PassiveRowColumnTransformer extends NodeWidgetTransformer<RowColumnNode> {
99
PassiveRowColumnTransformer(super.getNode, super.manager);
1010

11+
/// The gap a row/column puts between consecutive children along its main
12+
/// axis: [RowColumnNode.itemSpacing], or 0 for other row/column-like nodes.
13+
static double itemSpacingOf(BaseNode node) => switch (node) {
14+
RowColumnNode(:final double itemSpacing)
15+
when itemSpacing.isFinite && itemSpacing > 0 =>
16+
itemSpacing,
17+
_ => 0,
18+
};
19+
20+
/// Spaces [childrenWidgets] [spacing] apart along [rowColumnNode]'s main
21+
/// axis, leaving out the children [collapsed] marks: a child a visibility
22+
/// formula hides (or that is disabled) takes no space, so it takes no gap
23+
/// either, like CSS `display: none` in a flex box with a `gap`.
24+
///
25+
/// When [collapsed] is null no child can collapse and the gap is Flutter's
26+
/// own `Flex.spacing` (returned as `spacing`). Otherwise the gaps are built
27+
/// into the children and the Flex gets no spacing:
28+
///
29+
/// - A child that doesn't flex on the main axis gets the gap before it as
30+
/// main-axis padding (or the one after it, when the next child flexes).
31+
/// A fixed extent next to a child is exactly what `Flex.spacing` adds, so
32+
/// placement is the same for every main-axis alignment and the flex
33+
/// children share the same free space.
34+
/// - Between two flexing children the gap is a box of its own. A flexing
35+
/// child takes the free space, so alignment spreads nothing and the box
36+
/// lands where `Flex.spacing` would put the gap.
37+
///
38+
/// Every child keeps its wrapper whether it gets a gap or not, and every
39+
/// child is keyed by its node id, so a child keeps its state while its
40+
/// siblings collapse and show again.
41+
static ({List<Widget> children, double spacing}) spaceChildren(
42+
BaseNode rowColumnNode, {
43+
required List<Widget> childrenWidgets,
44+
required List<BaseNode> childrenNodes,
45+
required double spacing,
46+
List<bool>? collapsed,
47+
}) {
48+
if (spacing <= 0 || childrenWidgets.length < 2) {
49+
return (children: childrenWidgets, spacing: 0);
50+
}
51+
if (collapsed == null) {
52+
return (children: childrenWidgets, spacing: spacing);
53+
}
54+
assert(childrenWidgets.length == childrenNodes.length);
55+
assert(collapsed.length == childrenNodes.length);
56+
57+
final bool isRow =
58+
(rowColumnNode as RowColumnMixin).rowColumnType == RowColumnType.row;
59+
bool flexes(BaseNode child) =>
60+
(isRow ? child.horizontalFit : child.verticalFit).isFlex;
61+
62+
final int count = childrenNodes.length;
63+
final List<double> leading = List<double>.filled(count, 0);
64+
final List<double> trailing = List<double>.filled(count, 0);
65+
// Index of a flexing child -> whether a gap box goes before it.
66+
final Set<int> boxBefore = {};
67+
68+
int? previous;
69+
for (int i = 0; i < count; i++) {
70+
if (collapsed[i]) continue;
71+
if (previous != null) {
72+
if (!flexes(childrenNodes[i])) {
73+
leading[i] = spacing;
74+
} else if (!flexes(childrenNodes[previous])) {
75+
trailing[previous] = spacing;
76+
} else {
77+
boxBefore.add(i);
78+
}
79+
}
80+
previous = i;
81+
}
82+
83+
final List<Widget> children = [];
84+
for (int i = 0; i < count; i++) {
85+
final BaseNode child = childrenNodes[i];
86+
Widget widget = childrenWidgets[i];
87+
if (boxBefore.contains(i)) {
88+
children.add(
89+
SizedBox(
90+
key: ValueKey('${child.id}-gap'),
91+
width: isRow ? spacing : null,
92+
height: isRow ? null : spacing,
93+
),
94+
);
95+
}
96+
if (!flexes(child)) {
97+
widget = Padding(
98+
padding: isRow
99+
? EdgeInsets.only(left: leading[i], right: trailing[i])
100+
: EdgeInsets.only(top: leading[i], bottom: trailing[i]),
101+
child: widget,
102+
);
103+
}
104+
children.add(
105+
KeyedSubtree(key: ValueKey('${child.id}-spaced'), child: widget),
106+
);
107+
}
108+
return (children: children, spacing: 0);
109+
}
110+
11111
static Widget buildRowColumnWidget(
12112
BaseNode rowColumnNode, {
13113
required List<Widget> childrenWidgets,
14114
required List<BaseNode> childrenNodes,
15115
required bool withScroll,
16116
required GetNode getNode,
117+
List<bool>? collapsedChildren,
17118
}) {
18119
assert(rowColumnNode is RowColumnMixin);
19120

121+
final spaced = spaceChildren(
122+
rowColumnNode,
123+
childrenWidgets: childrenWidgets,
124+
childrenNodes: childrenNodes,
125+
spacing: itemSpacingOf(rowColumnNode),
126+
collapsed: collapsedChildren,
127+
);
128+
20129
final bool isRow =
21130
(rowColumnNode as RowColumnMixin).rowColumnType == RowColumnType.row;
22131

@@ -100,7 +209,8 @@ class PassiveRowColumnTransformer extends NodeWidgetTransformer<RowColumnNode> {
100209
? MainAxisSize.min
101210
: MainAxisSize.max,
102211
key: ValueKey(rowColumnNode.id),
103-
children: childrenWidgets,
212+
spacing: spaced.spacing,
213+
children: spaced.children,
104214
);
105215

106216
if (measureCross) {
@@ -379,8 +489,67 @@ class PassiveRowColumnWidget extends StatelessWidget {
379489
);
380490
}
381491

492+
/// Whether [child] may take no space at runtime: a visibility formula or
493+
/// condition decides whether it shows, or it is disabled.
494+
bool _mayCollapse(BuildContext context, BaseNode child) {
495+
final NodeTransformerManager manager = this.manager;
496+
if (!child.enabled) return true;
497+
if (manager is! PassiveNodeTransformerManager) return false;
498+
return manager.runtimeVisibility(context, child) != null ||
499+
PropertyValueDelegate.getPropertyValue<bool>(
500+
child,
501+
'node_enabled',
502+
scopedValues: ScopedValues.of(context),
503+
) !=
504+
null;
505+
}
506+
507+
/// Whether [child] takes no space now (see [_mayCollapse]).
508+
bool _isCollapsed(BuildContext context, BaseNode child) {
509+
final NodeTransformerManager manager = this.manager;
510+
if (manager is! PassiveNodeTransformerManager) return !child.enabled;
511+
final bool enabled =
512+
PropertyValueDelegate.getPropertyValue<bool>(
513+
child,
514+
'node_enabled',
515+
scopedValues: ScopedValues.of(context),
516+
) ??
517+
child.enabled;
518+
return !enabled || manager.runtimeVisibility(context, child) == false;
519+
}
520+
382521
@override
383522
Widget build(BuildContext context) {
523+
// With a gap, a child that collapses at runtime must give up its gaps
524+
// too, so the row/column rebuilds when one of them shows or hides.
525+
final bool spaced = PassiveRowColumnTransformer.itemSpacingOf(node) > 0;
526+
final List<BaseNode> collapsible = spaced
527+
? children.where((child) => _mayCollapse(context, child)).toList()
528+
: const [];
529+
if (collapsible.isEmpty) return _build(context, null);
530+
531+
List<bool> collapsedNow(BuildContext context) => [
532+
for (final BaseNode child in children)
533+
collapsible.contains(child) && _isCollapsed(context, child),
534+
];
535+
536+
final NodeTransformerManager manager = this.manager;
537+
final List<Listenable> listenables =
538+
manager is PassiveNodeTransformerManager
539+
? [
540+
for (final BaseNode child in collapsible)
541+
...manager.listenablesFor(context, child),
542+
]
543+
: const [];
544+
if (listenables.isEmpty) return _build(context, collapsedNow(context));
545+
return ManagedListenableBuilder(
546+
key: ValueKey('${node.id}-spacing'),
547+
listenables: listenables,
548+
builder: (context) => _build(context, collapsedNow(context)),
549+
);
550+
}
551+
552+
Widget _build(BuildContext context, List<bool>? collapsedChildren) {
384553
final List<Widget> widgetChildren = children.map((child) {
385554
final Widget builtWidget = manager.buildWidgetByID(
386555
child.id,
@@ -411,6 +580,7 @@ class PassiveRowColumnWidget extends StatelessWidget {
411580
childrenNodes: children,
412581
withScroll: true,
413582
getNode: manager.getNode,
583+
collapsedChildren: collapsedChildren,
414584
),
415585
],
416586
settings: settings,
@@ -421,6 +591,7 @@ class PassiveRowColumnWidget extends StatelessWidget {
421591
childrenNodes: children,
422592
withScroll: true,
423593
getNode: manager.getNode,
594+
collapsedChildren: collapsedChildren,
424595
);
425596

426597
if (kIsTestLayout) {

0 commit comments

Comments
 (0)