@@ -8,15 +8,124 @@ typedef WidgetInserter = Widget Function(Widget child);
88class 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