Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/genui/lib/src/catalog/core_widgets/column.dart
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ final column = CatalogItem(
componentId: componentId,
dataContext: dataContext,
buildChild: buildChild,
component: getComponent(componentId),
weight: getComponent(componentId)?.weight,
),
)
.toList(),
Expand All @@ -142,7 +142,7 @@ final column = CatalogItem(
DataPath('$dataBinding/$i'),
),
buildChild: itemContext.buildChild,
component: itemContext.getComponent(componentId),
weight: itemContext.getComponent(componentId)?.weight,
),
],
],
Expand Down
13 changes: 11 additions & 2 deletions packages/genui/lib/src/catalog/core_widgets/row.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:json_schema_builder/json_schema_builder.dart';
import '../../model/a2ui_schemas.dart';
import '../../model/catalog_item.dart';
import '../../model/data_model.dart';
import '../../model/ui_models.dart';
import '../../primitives/simple_items.dart';
import 'widget_helpers.dart';

Expand Down Expand Up @@ -123,13 +124,21 @@ final row = CatalogItem(
componentId: componentId,
dataContext: dataContext,
buildChild: buildChild,
component: getComponent(componentId),
weight:
getComponent(componentId)?.weight ??
(getComponent(componentId)?.type == 'TextField'
? 1
: null),
),
)
.toList(),
);
},
templateListWidgetBuilder: (context, list, componentId, dataBinding) {
final Component? component = itemContext.getComponent(componentId);
final int? weight =
component?.weight ?? (component?.type == 'TextField' ? 1 : null);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This LGTM as a practical fix for this now! I think we need a more scalable solution long term, to avoid other leaf components having this issue. We should figure out what circumstances this error happens in - is it for any leaf component that has an unbounded max width or height? Maybe we need them to be identifiable somehow. Either way, this is a great first step I think!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, constraints go down; sizes go up and all that1. Basically any widget that relies on a constraint from their parent to figure out how large they should be. For example, I suspect sliders within rows will fall victim to this exact crash.

Footnotes

  1. https://docs.flutter.dev/ui/layout/constraints

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And yeah, the cleaner solution would be some sort of declaration within the catalog that describe what constraint(s) a widget needs to be able to render. We'd also have to do some plumbing and/or refactoring to make this information available to layout widgets like Row and Column.


return Row(
mainAxisAlignment: _parseMainAxisAlignment(rowData.distribution),
crossAxisAlignment: _parseCrossAxisAlignment(rowData.alignment),
Expand All @@ -142,7 +151,7 @@ final row = CatalogItem(
DataPath('$dataBinding/$i'),
),
buildChild: itemContext.buildChild,
component: itemContext.getComponent(componentId),
weight: weight,
),
],
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import '../../model/ui_models.dart';
import '../../primitives/simple_items.dart';

final _schema = S.object(
description: 'A text input field.',
properties: {
'text': A2uiSchemas.stringReference(
description: 'The initial value of the text field.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import 'package:flutter/material.dart';

import '../../model/catalog_item.dart';
import '../../model/data_model.dart';
import '../../model/ui_models.dart';
import '../../primitives/logging.dart';
import '../../primitives/simple_items.dart';

Expand Down Expand Up @@ -133,9 +132,8 @@ Widget buildWeightedChild({
required String componentId,
required DataContext dataContext,
required ChildBuilderCallback buildChild,
required Component? component,
required int? weight,
}) {
final int? weight = component?.weight;
final Widget childWidget = buildChild(componentId, dataContext);
if (weight != null) {
return Flexible(flex: weight, child: childWidget);
Expand Down
1 change: 1 addition & 0 deletions packages/genui/lib/src/core/ui_tools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class SurfaceUpdateTool extends AiTool<JsonMap> {
return Component(
id: component['id'] as String,
componentProperties: component['component'] as JsonMap,
weight: (component['weight'] as num?)?.toInt(),
);
}).toList();
handleMessage(SurfaceUpdate(surfaceId: surfaceId, components: components));
Expand Down
134 changes: 134 additions & 0 deletions packages/genui/test/catalog/core_widgets/text_field_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright 2025 The Flutter Authors.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:genui/genui.dart';

void main() {
testWidgets('TextField with no weight in Row defaults to weight: 1 '
'and expands', (WidgetTester tester) async {
final a2uiProcessor = A2uiMessageProcessor(
catalogs: [CoreCatalogItems.asCatalog()],
);
const surfaceId = 'testSurface';
final components = [
const Component(
id: 'row',
componentProperties: {
'Row': {
'children': {
'explicitList': ['text_field'],
},
},
},
),
const Component(
id: 'text_field',
componentProperties: {
'TextField': {
'label': {'literalString': 'Input'},
},
},
// "weight" property is left unset.
),
];

a2uiProcessor.handleMessage(
SurfaceUpdate(surfaceId: surfaceId, components: components),
);
a2uiProcessor.handleMessage(
const BeginRendering(
surfaceId: surfaceId,
root: 'row',
catalogId: 'a2ui.org:standard_catalog_0_8_0',
),
);

await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: GenUiSurface(host: a2uiProcessor, surfaceId: surfaceId),
),
),
);

expect(find.byType(TextField), findsOneWidget);

final Flexible flexible = tester.widget(
find.ancestor(
of: find.byType(TextField),
matching: find.byType(Flexible),
),
);
expect(flexible.flex, 1);

final Finder textFieldFinder = find.byType(TextField);
final Size size = tester.getSize(textFieldFinder);
expect(size.width, 800.0);
});

testWidgets('TextField in Row (with weight) expands', (
WidgetTester tester,
) async {
final manager = A2uiMessageProcessor(
catalogs: [CoreCatalogItems.asCatalog()],
);
const surfaceId = 'testSurface';
final components = [
const Component(
id: 'row',
componentProperties: {
'Row': {
'children': {
'explicitList': ['text_field'],
},
},
},
),
const Component(
id: 'text_field',
componentProperties: {
'TextField': {
'label': {'literalString': 'Input'},
},
},
weight: 1,
),
];

manager.handleMessage(
SurfaceUpdate(surfaceId: surfaceId, components: components),
);
manager.handleMessage(
const BeginRendering(
surfaceId: surfaceId,
root: 'row',
catalogId: 'a2ui.org:standard_catalog_0_8_0',
),
);

await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: GenUiSurface(host: manager, surfaceId: surfaceId),
),
),
);

expect(find.byType(TextField), findsOneWidget);

expect(
find.ancestor(
of: find.byType(TextField),
matching: find.byType(Flexible),
),
findsOneWidget,
);

// Default test screen width is 800.
final Size size = tester.getSize(find.byType(TextField));
expect(size.width, 800.0);
});
}
51 changes: 51 additions & 0 deletions packages/genui/test/core/ui_tools_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,57 @@ void main() {
expect(surfaceUpdate.components[0].componentProperties, {
'Text': {'text': 'Hello'},
});
expect(surfaceUpdate.components[0].weight, isNull);
});

test('invoke correctly parses int weight', () async {
final messages = <A2uiMessage>[];
final tool = SurfaceUpdateTool(
handleMessage: messages.add,
catalog: const Catalog([], catalogId: 'test_catalog'),
);

final Map<String, Object> args = {
surfaceIdKey: 'testSurface',
'components': [
{
'id': 'weightedWidget',
'component': {'Text': <Object?, Object?>{}},
'weight': 1,
},
],
};

await tool.invoke(args);

expect(messages.length, 1);
final surfaceUpdate = messages[0] as SurfaceUpdate;
expect(surfaceUpdate.components[0].weight, 1);
});

test('invoke correctly parses double weight', () async {
final messages = <A2uiMessage>[];
final tool = SurfaceUpdateTool(
handleMessage: messages.add,
catalog: const Catalog([], catalogId: 'test_catalog'),
);

final Map<String, Object> args = {
surfaceIdKey: 'testSurface',
'components': [
{
'id': 'weightedWidget',
'component': {'Text': <Object?, Object?>{}},
'weight': 1.0,
},
],
};

await tool.invoke(args);

expect(messages.length, 1);
final surfaceUpdate = messages[0] as SurfaceUpdate;
expect(surfaceUpdate.components[0].weight, 1);
});
});

Expand Down
Loading