From 9d1c9e0ba628318c646c076f59fe56d3033d04cb Mon Sep 17 00:00:00 2001 From: Andrew Kolos Date: Mon, 15 Dec 2025 14:03:48 -0500 Subject: [PATCH 1/2] fix(genui): add missing `weight` property to `Component` constructor; default `TextField` `width` to 1 when nested in a `Row` --- .../lib/src/catalog/core_widgets/column.dart | 4 +- .../lib/src/catalog/core_widgets/row.dart | 12 +- .../src/catalog/core_widgets/text_field.dart | 1 + .../catalog/core_widgets/widget_helpers.dart | 4 +- packages/genui/lib/src/core/ui_tools.dart | 1 + .../catalog/core_widgets/text_field_test.dart | 134 ++++++++++++++++++ packages/genui/test/core/ui_tools_test.dart | 51 +++++++ 7 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 packages/genui/test/catalog/core_widgets/text_field_test.dart diff --git a/packages/genui/lib/src/catalog/core_widgets/column.dart b/packages/genui/lib/src/catalog/core_widgets/column.dart index 47922a387..8c00e9492 100644 --- a/packages/genui/lib/src/catalog/core_widgets/column.dart +++ b/packages/genui/lib/src/catalog/core_widgets/column.dart @@ -123,7 +123,7 @@ final column = CatalogItem( componentId: componentId, dataContext: dataContext, buildChild: buildChild, - component: getComponent(componentId), + weight: getComponent(componentId)?.weight, ), ) .toList(), @@ -142,7 +142,7 @@ final column = CatalogItem( DataPath('$dataBinding/$i'), ), buildChild: itemContext.buildChild, - component: itemContext.getComponent(componentId), + weight: itemContext.getComponent(componentId)?.weight, ), ], ], diff --git a/packages/genui/lib/src/catalog/core_widgets/row.dart b/packages/genui/lib/src/catalog/core_widgets/row.dart index b9beb9b30..71949e7f2 100644 --- a/packages/genui/lib/src/catalog/core_widgets/row.dart +++ b/packages/genui/lib/src/catalog/core_widgets/row.dart @@ -123,7 +123,11 @@ final row = CatalogItem( componentId: componentId, dataContext: dataContext, buildChild: buildChild, - component: getComponent(componentId), + weight: + getComponent(componentId)?.weight ?? + (getComponent(componentId)?.type == 'TextField' + ? 1 + : null), ), ) .toList(), @@ -142,7 +146,11 @@ final row = CatalogItem( DataPath('$dataBinding/$i'), ), buildChild: itemContext.buildChild, - component: itemContext.getComponent(componentId), + weight: + itemContext.getComponent(componentId)?.weight ?? + (itemContext.getComponent(componentId)?.type == 'TextField' + ? 1 + : null), ), ], ], diff --git a/packages/genui/lib/src/catalog/core_widgets/text_field.dart b/packages/genui/lib/src/catalog/core_widgets/text_field.dart index f92bd2b39..0953b8002 100644 --- a/packages/genui/lib/src/catalog/core_widgets/text_field.dart +++ b/packages/genui/lib/src/catalog/core_widgets/text_field.dart @@ -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.', diff --git a/packages/genui/lib/src/catalog/core_widgets/widget_helpers.dart b/packages/genui/lib/src/catalog/core_widgets/widget_helpers.dart index 5b18ce40e..80052745b 100644 --- a/packages/genui/lib/src/catalog/core_widgets/widget_helpers.dart +++ b/packages/genui/lib/src/catalog/core_widgets/widget_helpers.dart @@ -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'; @@ -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); diff --git a/packages/genui/lib/src/core/ui_tools.dart b/packages/genui/lib/src/core/ui_tools.dart index 6612ee155..2b74abef6 100644 --- a/packages/genui/lib/src/core/ui_tools.dart +++ b/packages/genui/lib/src/core/ui_tools.dart @@ -35,6 +35,7 @@ class SurfaceUpdateTool extends AiTool { 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)); diff --git a/packages/genui/test/catalog/core_widgets/text_field_test.dart b/packages/genui/test/catalog/core_widgets/text_field_test.dart new file mode 100644 index 000000000..827c088bb --- /dev/null +++ b/packages/genui/test/catalog/core_widgets/text_field_test.dart @@ -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); + }); +} diff --git a/packages/genui/test/core/ui_tools_test.dart b/packages/genui/test/core/ui_tools_test.dart index b3087a8eb..0893054d8 100644 --- a/packages/genui/test/core/ui_tools_test.dart +++ b/packages/genui/test/core/ui_tools_test.dart @@ -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 = []; + final tool = SurfaceUpdateTool( + handleMessage: messages.add, + catalog: const Catalog([], catalogId: 'test_catalog'), + ); + + final Map args = { + surfaceIdKey: 'testSurface', + 'components': [ + { + 'id': 'weightedWidget', + 'component': {'Text': {}}, + '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 = []; + final tool = SurfaceUpdateTool( + handleMessage: messages.add, + catalog: const Catalog([], catalogId: 'test_catalog'), + ); + + final Map args = { + surfaceIdKey: 'testSurface', + 'components': [ + { + 'id': 'weightedWidget', + 'component': {'Text': {}}, + 'weight': 1.0, + }, + ], + }; + + await tool.invoke(args); + + expect(messages.length, 1); + final surfaceUpdate = messages[0] as SurfaceUpdate; + expect(surfaceUpdate.components[0].weight, 1); }); }); From b2ebc54978fed9f5886e0e0f18cdcabdabbce803 Mon Sep 17 00:00:00 2001 From: Andrew Kolos Date: Mon, 15 Dec 2025 16:57:00 -0500 Subject: [PATCH 2/2] avoid re-fetching weight/type when looping over each child --- packages/genui/lib/src/catalog/core_widgets/row.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/genui/lib/src/catalog/core_widgets/row.dart b/packages/genui/lib/src/catalog/core_widgets/row.dart index 71949e7f2..3190eb689 100644 --- a/packages/genui/lib/src/catalog/core_widgets/row.dart +++ b/packages/genui/lib/src/catalog/core_widgets/row.dart @@ -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'; @@ -134,6 +135,10 @@ final row = CatalogItem( ); }, templateListWidgetBuilder: (context, list, componentId, dataBinding) { + final Component? component = itemContext.getComponent(componentId); + final int? weight = + component?.weight ?? (component?.type == 'TextField' ? 1 : null); + return Row( mainAxisAlignment: _parseMainAxisAlignment(rowData.distribution), crossAxisAlignment: _parseCrossAxisAlignment(rowData.alignment), @@ -146,11 +151,7 @@ final row = CatalogItem( DataPath('$dataBinding/$i'), ), buildChild: itemContext.buildChild, - weight: - itemContext.getComponent(componentId)?.weight ?? - (itemContext.getComponent(componentId)?.type == 'TextField' - ? 1 - : null), + weight: weight, ), ], ],