Skip to content

Commit 50ce058

Browse files
committed
Bindable flex for row and column children
A child's flex can be bound to a variable or formula (a bar segment sized by a count); zero or less takes no space, like a hidden child. The slot rebuilds when the bound value changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012anUJ6o6K6kjUXur8Ak8XX
1 parent 45533a7 commit 50ce058

2 files changed

Lines changed: 95 additions & 2 deletions

File tree

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,8 @@ class PassiveRowColumnWidget extends StatelessWidget {
343343
final WidgetBuildSettings settings;
344344

345345
/// A flexing child hidden at runtime gives up its share of the free space,
346+
/// and one with a bound `flex` variable takes that share instead of its
347+
/// design flex.
346348
/// so a visible sibling (an empty state next to a hidden spinner) can take
347349
/// all of it. The flex drops to zero rather than removing the [Flexible] so
348350
/// the child keeps its state when it shows again.
@@ -357,12 +359,23 @@ class PassiveRowColumnWidget extends StatelessWidget {
357359
}
358360

359361
Widget buildFlexible(BuildContext context) {
360-
final bool hidden = manager.runtimeVisibility(context, child) == false;
362+
// A bound flex (e.g. a bar segment sized by a count) replaces the
363+
// design flex; a flex of zero or less takes no space, like hidden.
364+
final int? boundFlex = child.variables['flex'] == null
365+
? null
366+
: PropertyValueDelegate.getPropertyValue<num>(
367+
child,
368+
'flex',
369+
scopedValues: ScopedValues.of(context),
370+
)?.round();
371+
final bool hidden =
372+
manager.runtimeVisibility(context, child) == false ||
373+
(boundFlex != null && boundFlex <= 0);
361374
// Without flex the hidden child would be laid out with unbounded
362375
// main-axis constraints, which a scrollable inside it can't take
363376
// (a hidden grid computed NaN offsets), so it gets no space at all.
364377
return Flexible(
365-
flex: hidden ? 0 : sizedWidget.flex,
378+
flex: hidden ? 0 : boundFlex ?? sizedWidget.flex,
366379
fit: sizedWidget.fit,
367380
child: SizedBox(
368381
width: hidden ? 0 : null,

‎test/bound_flex_test.dart‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import 'package:codelessly_api/codelessly_api.dart';
2+
import 'package:codelessly_sdk/codelessly_sdk.dart';
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_test/flutter_test.dart';
5+
import 'package:provider/provider.dart';
6+
7+
/// A status bar: segments whose flex is bound to counts.
8+
void main() {
9+
RectangleNode segment(String id, String flexPath) => RectangleNode.fromJson({
10+
...RectangleNode(
11+
id: id,
12+
name: id,
13+
basicBoxLocal: NodeBox(0, 0, 10, 8),
14+
horizontalFit: SizeFit.expanded,
15+
).toJson(),
16+
'variables': {'flex': flexPath},
17+
})..parentID = 'bar';
18+
19+
testWidgets('segments take shares of the width from their bound flex', (
20+
tester,
21+
) async {
22+
final RowColumnNode bar = RowColumnNode(
23+
id: 'bar',
24+
name: 'bar',
25+
basicBoxLocal: NodeBox(0, 0, 400, 8),
26+
rowColumnType: RowColumnType.row,
27+
children: ['a', 'b', 'c'],
28+
)..parentID = kRootNode;
29+
final nodes = [
30+
bar,
31+
segment('a', r'${counts.closed}'),
32+
segment('b', r'${= counts.open * 1}'),
33+
segment('c', r'${counts.review}'),
34+
];
35+
final Observable<VariableData> counts = Observable(
36+
VariableData(
37+
id: 'counts',
38+
name: 'counts',
39+
type: VariableType.map,
40+
value: {'closed': 3, 'open': 1, 'review': 0},
41+
),
42+
);
43+
final registry = NodeRegistry()..setNodes({for (final n in nodes) n.id: n});
44+
final manager = PassiveNodeTransformerManager(
45+
registry.getNodeByID,
46+
(context, bounds, pageID, layoutID, canvasID) => const SizedBox(),
47+
);
48+
await tester.pumpWidget(
49+
ChangeNotifierProvider<CodelesslyContext>.value(
50+
value: CodelesslyContext.empty()..variables['counts'] = counts,
51+
child: MaterialApp(
52+
home: Align(
53+
alignment: Alignment.topLeft,
54+
child: Builder(
55+
builder: (context) => manager.buildWidgetFromNode(
56+
bar,
57+
context,
58+
settings: const WidgetBuildSettings(debugLabel: 'test'),
59+
),
60+
),
61+
),
62+
),
63+
),
64+
);
65+
expect(tester.takeException(), isNull);
66+
double width(String id) => tester
67+
.getSize(find.byKey(ValueKey('Rectangle Transformer of $id [$id]')))
68+
.width;
69+
expect(width('a'), 300);
70+
expect(width('b'), 100);
71+
expect(width('c'), 0);
72+
73+
counts.set(
74+
counts.value.copyWith(value: {'closed': 1, 'open': 1, 'review': 2}),
75+
);
76+
await tester.pump();
77+
expect(width('a'), 100);
78+
expect(width('c'), 200);
79+
});
80+
}

0 commit comments

Comments
 (0)