diff --git a/dart/a2ui_flutter/README.md b/dart/a2ui_flutter/README.md index 1132851ea3..b86abdee18 100644 --- a/dart/a2ui_flutter/README.md +++ b/dart/a2ui_flutter/README.md @@ -298,6 +298,12 @@ so a v1.0 agent reads it with `version: A2uiProtocolVersion.v1_0`. them goes back to the model. [e2e_test](e2e_test) runs it against the live model. +## Gallery + +[a2ui_explorer](a2ui_explorer) is the gallery app. It renders the v0.9 and +v1.0 basic catalog examples and steps through their messages, next to each +surface's data model and a log of actions and errors. + ## Tests `test/catalog/basic/examples_test.dart` renders every diff --git a/dart/a2ui_flutter/a2ui_explorer/.gitignore b/dart/a2ui_flutter/a2ui_explorer/.gitignore new file mode 100644 index 0000000000..79f7ecabd5 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/.gitignore @@ -0,0 +1,48 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# Widget Preview related +.widget_preview/ diff --git a/dart/a2ui_flutter/a2ui_explorer/.metadata b/dart/a2ui_flutter/a2ui_explorer/.metadata new file mode 100644 index 0000000000..edfa7eb1df --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d3b14c876900e553bc736ca19295fc09e3853e8e" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d3b14c876900e553bc736ca19295fc09e3853e8e + base_revision: d3b14c876900e553bc736ca19295fc09e3853e8e + - platform: macos + create_revision: d3b14c876900e553bc736ca19295fc09e3853e8e + base_revision: d3b14c876900e553bc736ca19295fc09e3853e8e + - platform: web + create_revision: d3b14c876900e553bc736ca19295fc09e3853e8e + base_revision: d3b14c876900e553bc736ca19295fc09e3853e8e + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/dart/a2ui_flutter/a2ui_explorer/README.md b/dart/a2ui_flutter/a2ui_explorer/README.md new file mode 100644 index 0000000000..1987e2a6fc --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/README.md @@ -0,0 +1,39 @@ +# A2UI Flutter Explorer + +The gallery app of the Flutter renderer for protocols v0.9 and v1.0, +following the +[framework adapter blueprint](../../../blueprints/modules/a2ui_framework_adapter.blueprint.md). +The left column lists the basic catalog examples of the selected version: +[v0.9](../../../specification/v0_9/catalogs/basic/examples) or +[v1.0](../../../catalogs/basic/v1/examples). The center column shows the +selected example's surfaces above its messages, each message marked +processed or pending. The right column shows each surface's data model as it +changes, above a log of the actions the surfaces send and the errors the +surfaces and the message processor report. + +## Running + +From this directory: + +```bash +flutter run -d macos +flutter run -d chrome +``` + +Selecting an example processes all of its messages. Reset starts it over with +none processed, Advance processes the next one, and Run all processes the +rest. Selecting the other version opens the example of the same file name in +it, or its first example. + +## Tests + +```bash +flutter test +``` + +## Examples + +`assets/examples/v0_9` and `assets/examples/v1_0` link to each version's +examples, so the app bundles them as they are. On Windows, the links need a +clone made with `git clone -c core.symlinks=true` and Developer Mode turned +on. diff --git a/dart/a2ui_flutter/a2ui_explorer/assets/examples/v0_9 b/dart/a2ui_flutter/a2ui_explorer/assets/examples/v0_9 new file mode 120000 index 0000000000..844e938f39 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/assets/examples/v0_9 @@ -0,0 +1 @@ +../../../../../specification/v0_9/catalogs/basic/examples \ No newline at end of file diff --git a/dart/a2ui_flutter/a2ui_explorer/assets/examples/v1_0 b/dart/a2ui_flutter/a2ui_explorer/assets/examples/v1_0 new file mode 120000 index 0000000000..8f13cb5c49 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/assets/examples/v1_0 @@ -0,0 +1 @@ +../../../../../catalogs/basic/v1/examples \ No newline at end of file diff --git a/dart/a2ui_flutter/a2ui_explorer/lib/main.dart b/dart/a2ui_flutter/a2ui_explorer/lib/main.dart new file mode 100644 index 0000000000..94a9e2534a --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/lib/main.dart @@ -0,0 +1,24 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +import 'src/example.dart'; +import 'src/explorer_app.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + runApp(ExplorerApp(examples: await loadExamples(rootBundle))); +} diff --git a/dart/a2ui_flutter/a2ui_explorer/lib/src/demo_catalog.dart b/dart/a2ui_flutter/a2ui_explorer/lib/src/demo_catalog.dart new file mode 100644 index 0000000000..2e2ed05b99 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/lib/src/demo_catalog.dart @@ -0,0 +1,141 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; +import 'dart:math' as math; + +import 'package:a2ui_core/a2ui_core.dart'; +import 'package:a2ui_flutter/a2ui_flutter.dart'; +import 'package:a2ui_flutter/v0_9/basic_catalog.dart'; +// ignore: deprecated_member_use, see https://github.com/a2ui-project/a2ui/issues/3235 +import 'package:flutter/material.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; + +/// The basic catalog under its own id, with the custom components the +/// examples name: `CustomGrid` and `CustomSlider`. +final CatalogImplementation demoCatalog = basicCatalog().copyWith( + components: [...BasicComponents.all, customGrid, customSlider], +); + +final Schema _dynamicNumber = Schema.fromMap({ + r'$ref': + 'https://a2ui.org/specification/v0_9/common_types.json#/\$defs/DynamicNumber', +}); + +/// A `CustomGrid`: its `title` and `description` over its `children` in two +/// columns, as wide as the screen when the incoming width is unbounded. +final ComponentImplementation customGrid = ComponentImplementation( + name: 'CustomGrid', + schema: Schema.object( + properties: { + 'title': CommonSchemas.dynamicString, + 'description': CommonSchemas.dynamicString, + 'children': CommonSchemas.childList, + }, + ), + builder: (context, node, props, buildChild) { + final TextTheme text = Theme.of(context).textTheme; + final List> children = props + .children('children'); + Widget cell(int index) => index < children.length + ? Expanded( + key: NodeKey(children[index].instanceId), + child: Card.outlined( + child: Padding( + padding: const EdgeInsets.all(8), + child: buildChild(children[index]), + ), + ), + ) + : const Spacer(); + return LimitedBox( + maxWidth: MediaQuery.sizeOf(context).width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (props.string('title') case final String title) + Text(title, style: text.titleMedium), + if (props.string('description') case final String description) + Text(description, style: text.bodySmall), + for (var row = 0; row < children.length; row += 2) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [cell(row), cell(row + 1)], + ), + ], + ), + ); + }, +); + +/// A `CustomSlider`: its `label` and `value` over a [Slider] from `min` to +/// `max` that writes each move to the path `value` is bound to, as a whole +/// number of steps of 1 from `min` that stays within `max`. +final ComponentImplementation customSlider = ComponentImplementation( + name: 'CustomSlider', + schema: Schema.object( + properties: { + 'label': CommonSchemas.dynamicString, + 'value': _dynamicNumber, + 'min': _dynamicNumber, + 'max': _dynamicNumber, + }, + ), + builder: (context, node, props, buildChild) { + final double min = props.number('min') ?? 0; + final double max = math.max(min, props.number('max') ?? 100); + final double value = (props.number('value') ?? min).clamp(min, max); + final WritableBinding? binding = props.writable('value'); + final SurfaceModel surface = A2uiSurface.of( + context, + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${props.string('label') ?? 'Value'}: ' + '${ComponentProps.asString(value)}', + ), + Slider( + value: value, + min: min, + max: max, + onChanged: binding == null + ? null + : (moved) { + final int steps = math.min( + (moved - min).round(), + (max - min).floor(), + ); + final double next = min + steps; + try { + binding.set(next % 1 == 0 ? next.toInt() : next); + } on A2uiDataError catch (error) { + unawaited( + surface.dispatchError( + A2uiRendererError( + code: error.code, + surfaceId: surface.id, + message: error.message, + path: error.path, + ), + ), + ); + } + }, + ), + ], + ); + }, +); diff --git a/dart/a2ui_flutter/a2ui_explorer/lib/src/example.dart b/dart/a2ui_flutter/a2ui_explorer/lib/src/example.dart new file mode 100644 index 0000000000..e5d371ed9d --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/lib/src/example.dart @@ -0,0 +1,92 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:convert'; + +import 'package:a2ui_core/a2ui_core.dart'; +import 'package:flutter/services.dart'; + +/// One of the basic catalog examples of a protocol version. +class Example { + const Example({ + required this.version, + required this.fileName, + required this.name, + required this.description, + required this.json, + }); + + /// The protocol version of the example's messages. + final A2uiProtocolVersion version; + + /// The example's file name, such as `00_simple-text.json`. + final String fileName; + + /// The example's `name`. + final String name; + + /// The example's `description`. + final String description; + + /// The example's file. + final String json; + + /// The example's messages, in order. + List> get messages => [ + for (final Object? message + in (jsonDecode(json) as Map)['messages']! as List) + message! as Map, + ]; +} + +/// The asset directory of each protocol version's examples, a link to that +/// version's basic catalog examples. +const Map examplesDirectories = { + A2uiProtocolVersion.v0_9: 'assets/examples/v0_9/', + A2uiProtocolVersion.v1_0: 'assets/examples/v1_0/', +}; + +/// Every example in [examplesDirectories] of [bundle], by version in the +/// order of [examplesDirectories], then by file name. +Future> loadExamples(AssetBundle bundle) async { + final AssetManifest manifest = await AssetManifest.loadFromAssetBundle( + bundle, + ); + return [ + for (final MapEntry(key: version, value: directory) + in examplesDirectories.entries) + for (final String path + in manifest + .listAssets() + .where((p) => p.startsWith(directory) && p.endsWith('.json')) + .toList() + ..sort()) + _example( + version, + path.substring(directory.length), + await bundle.loadString(path), + ), + ]; +} + +Example _example(A2uiProtocolVersion version, String fileName, String json) { + final document = jsonDecode(json) as Map; + return Example( + version: version, + fileName: fileName, + name: document['name']! as String, + description: document['description']! as String, + json: json, + ); +} diff --git a/dart/a2ui_flutter/a2ui_explorer/lib/src/example_run.dart b/dart/a2ui_flutter/a2ui_explorer/lib/src/example_run.dart new file mode 100644 index 0000000000..b5b88b77b8 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/lib/src/example_run.dart @@ -0,0 +1,133 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:a2ui_core/a2ui_core.dart'; +import 'package:a2ui_flutter/a2ui_flutter.dart'; +import 'package:a2ui_flutter/basic_catalog.dart'; +import 'package:flutter/widgets.dart'; + +import 'demo_catalog.dart'; +import 'example.dart'; + +/// An entry of the action log: a heading and the details under it. +typedef LogEntry = ({String heading, Map details}); + +/// One run of an [Example]: a [MessageProcessor] over [demoCatalog] for +/// v0.9 and the v1.0 basic catalog that processes the example's messages in +/// order on request, the surfaces it creates, and a log of the actions the +/// surfaces send and the errors the surfaces and the processor report. +/// +/// Notifies its listeners when a message is processed, a surface is created +/// or deleted, or an entry is logged. +class ExampleRun extends ChangeNotifier { + ExampleRun(this.example) + : messages = example.messages, + _processor = MessageProcessor( + catalogs: [demoCatalog, basicCatalog()], + defaultVersion: example.version, + validationConfig: const ValidationConfig( + allowOrphanComponents: true, + allowDanglingReferences: true, + allowMissingRoot: true, + ), + ) { + final SurfaceGroupModel group = _processor.model; + group.onAction.subscribe( + (action) => _log((heading: action.name, details: action.toJson())), + ); + group.onSurfaceCreated.subscribe((surface) { + surface.onError.subscribe( + (error) => + _log((heading: 'Error ${error.code}', details: error.toJson())), + ); + _surfaces.add(surface); + _changed(); + }); + group.onSurfaceDeleted.subscribe((id) { + _surfaces.removeWhere((surface) => surface.id == id); + _changed(); + }); + } + + /// The example this run steps through. + final Example example; + + /// The example's messages. + final List> messages; + + /// Checks each message against the surface it leaves behind, which may + /// lack a root, reference components a later message adds, and hold + /// components nothing reaches. + final MessageProcessor _processor; + + final List> _surfaces = []; + final List _entries = []; + int _processed = 0; + bool _disposed = false; + + /// The live surfaces, in creation order. + List> get surfaces => + List.unmodifiable(_surfaces); + + /// The logged actions and errors, oldest first. + List get log => List.unmodifiable(_entries); + + /// How many of [messages] have been processed. + int get processed => _processed; + + /// Whether a message is left to process. + bool get canAdvance => _processed < messages.length; + + /// Processes the next message. + void advance() => _processUntil(_processed + 1); + + /// Processes every message left. + void runAll() => _processUntil(messages.length); + + void _processUntil(int end) { + for (; _processed < end && canAdvance; _processed++) { + try { + _processor.processMessages( + AgentToRendererMessagePayload.fromJson( + messages[_processed], + protocolVersion: example.version, + ), + ); + } on A2uiError catch (error) { + _entries.add(( + heading: 'Error ${error.code}', + details: {'message': error.message, 'messageNumber': _processed + 1}, + )); + } + } + _changed(); + } + + void _log(LogEntry entry) { + _entries.add(entry); + _changed(); + } + + void _changed() { + if (!_disposed) notifyListeners(); + } + + /// Disposes the processor and its surfaces. + @override + void dispose() { + _disposed = true; + _processor.dispose(); + super.dispose(); + } +} diff --git a/dart/a2ui_flutter/a2ui_explorer/lib/src/explorer_app.dart b/dart/a2ui_flutter/a2ui_explorer/lib/src/explorer_app.dart new file mode 100644 index 0000000000..77e3157423 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/lib/src/explorer_app.dart @@ -0,0 +1,443 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:convert'; +import 'dart:math' as math; + +import 'package:a2ui_core/a2ui_core.dart'; +import 'package:a2ui_flutter/a2ui_flutter.dart'; +import 'package:a2ui_flutter/basic_catalog.dart'; +// ignore: deprecated_member_use, see https://github.com/a2ui-project/a2ui/issues/3235 +import 'package:flutter/material.dart'; +import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'example.dart'; +import 'example_run.dart'; + +/// The gallery app of the A2UI Flutter renderer. +class ExplorerApp extends StatelessWidget { + const ExplorerApp({super.key, required this.examples}); + + /// The examples to list. + final List examples; + + @override + Widget build(BuildContext context) => MaterialApp( + title: 'A2UI Explorer', + theme: ThemeData(colorSchemeSeed: Colors.indigo), + home: ExplorerPage(examples: examples), + ); +} + +/// Three columns: the examples of one protocol version, the selected +/// example's surfaces, messages and stepper, and its data models and action +/// log. +/// +/// Selecting an example processes all of its messages. Reset starts it over +/// with none processed. Selecting another version opens the example of the +/// same file name in it, or its first example. Below 1120 logical pixels +/// wide, the columns scroll horizontally. +class ExplorerPage extends StatefulWidget { + const ExplorerPage({super.key, required this.examples}); + + /// The examples to list. Must not be empty. + final List examples; + + @override + State createState() => _ExplorerPageState(); +} + +class _ExplorerPageState extends State { + late ExampleRun _run = ExampleRun(widget.examples.first)..runAll(); + + /// Starts [example] over in a new run, with all of its messages processed + /// when [runAll] is true. The previous run is disposed after the frame that + /// removes its surfaces. + void _start(Example example, {bool runAll = false}) { + final ExampleRun previous = _run; + final run = ExampleRun(example); + if (runAll) run.runAll(); + setState(() => _run = run); + WidgetsBinding.instance.addPostFrameCallback((_) => previous.dispose()); + } + + @override + void dispose() { + _run.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + body: LayoutBuilder( + builder: (context, constraints) => SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: math.max(constraints.maxWidth, 1120), + height: constraints.maxHeight, + child: ListenableBuilder( + listenable: _run, + builder: (context, _) => Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(width: 280, child: _examplesPane()), + const VerticalDivider(width: 1), + Expanded(child: _runPane(context)), + const VerticalDivider(width: 1), + SizedBox( + width: 360, + child: KeyedSubtree( + key: ObjectKey(_run), + child: _inspectorPane(), + ), + ), + ], + ), + ), + ), + ), + ), + ); + + /// Opens the example of [version] with the selected example's file name, + /// or the first example of [version]. + void _selectVersion(A2uiProtocolVersion version) { + final List examples = _examplesOf(version); + _start( + examples.firstWhere( + (e) => e.fileName == _run.example.fileName, + orElse: () => examples.first, + ), + runAll: true, + ); + } + + List _examplesOf(A2uiProtocolVersion version) => [ + for (final example in widget.examples) + if (example.version == version) example, + ]; + + Widget _examplesPane() { + final List versions = { + for (final example in widget.examples) example.version, + }.toList(); + final List examples = _examplesOf(_run.example.version); + return _Pane( + title: 'Examples', + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: SegmentedButton( + key: const Key('version'), + showSelectedIcon: false, + segments: [ + for (final version in versions) + ButtonSegment( + value: version, + label: Text('v${version.major}.${version.minor}'), + ), + ], + selected: {_run.example.version}, + onSelectionChanged: (selected) => _selectVersion(selected.single), + ), + ), + Expanded( + child: KeyedSubtree( + key: ValueKey(_run.example.version), + child: ListView.builder( + key: const Key('examples'), + itemCount: examples.length, + itemBuilder: (context, index) { + final Example example = examples[index]; + return ListTile( + key: ValueKey(example.fileName), + title: Text(example.name), + subtitle: Text(example.description), + selected: identical(example, _run.example), + onTap: () => _start(example, runAll: true), + ); + }, + ), + ), + ), + ], + ), + ); + } + + Widget _runPane(BuildContext context) { + final ExampleRun run = _run; + final List> surfaces = run.surfaces; + final TextTheme text = Theme.of(context).textTheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8, + children: [ + Text(run.example.name, style: text.titleLarge), + Text(run.example.description), + Wrap( + spacing: 8, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + FilledButton( + onPressed: run.canAdvance ? run.advance : null, + child: const Text('Advance'), + ), + OutlinedButton( + onPressed: run.canAdvance ? run.runAll : null, + child: const Text('Run all'), + ), + TextButton( + onPressed: () => _start(run.example), + child: const Text('Reset'), + ), + Text( + '${run.processed} of ${run.messages.length} messages ' + 'processed', + ), + ], + ), + ], + ), + ), + const Divider(height: 1), + Expanded( + flex: 3, + child: KeyedSubtree( + key: ObjectKey(run), + child: surfaces.isEmpty + ? const Center(child: Text('No surface yet.')) + : A2uiMarkdown( + builder: _markdown, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 16, + children: [ + for (final surface in surfaces) + A2uiSurface( + key: ObjectKey(surface), + surface: surface, + ), + ], + ), + ), + ), + ), + ), + const Divider(height: 1), + Expanded( + flex: 2, + child: KeyedSubtree( + key: ObjectKey(run), + child: _Pane( + title: 'Messages', + child: _JsonList( + key: const Key('messages'), + empty: 'No message.', + children: [ + for (final (int index, Map message) + in run.messages.indexed) + _messageCard(index, message), + ], + ), + ), + ), + ), + ], + ); + } + + /// The message at [index], numbered and marked processed or pending. + Widget _messageCard(int index, Map message) { + final bool processed = index < _run.processed; + final String type = message.keys.firstWhere( + (key) => key != 'version', + orElse: () => '?', + ); + return Card.outlined( + color: processed + ? Theme.of(context).colorScheme.secondaryContainer + : null, + child: Padding( + padding: const EdgeInsets.all(12), + child: _JsonView( + heading: + '${index + 1}. $type (${processed ? 'processed' : 'pending'})', + json: message, + ), + ), + ); + } + + Widget _inspectorPane() => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: _Pane( + title: 'Data Model', + child: _JsonList( + key: const Key('data-model'), + empty: 'No surface yet.', + children: [ + for (final surface in _run.surfaces) + _DataModelView(key: ObjectKey(surface), surface: surface), + ], + ), + ), + ), + const Divider(height: 1), + Expanded( + child: _Pane( + title: 'Action Log', + child: _JsonList( + key: const Key('action-log'), + empty: 'No action yet.', + children: [ + for (final LogEntry entry in _run.log.reversed) + _JsonView(heading: entry.heading, json: entry.details), + ], + ), + ), + ), + ], + ); +} + +/// [title] over [child], which fills the rest of the pane. +class _Pane extends StatelessWidget { + const _Pane({required this.title, required this.child}); + + final String title; + final Widget child; + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text(title, style: Theme.of(context).textTheme.titleMedium), + ), + Expanded(child: child), + ], + ); +} + +/// A selectable, scrolling list of [children], or [empty] when there are +/// none. +class _JsonList extends StatelessWidget { + const _JsonList({super.key, required this.children, required this.empty}); + + final List children; + final String empty; + + @override + Widget build(BuildContext context) => SelectionArea( + child: ListView( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + children: children.isEmpty ? [Text(empty)] : children, + ), + ); +} + +/// The data model of [surface], updated as it changes. +class _DataModelView extends StatefulWidget { + const _DataModelView({super.key, required this.surface}); + + final SurfaceModel surface; + + @override + State<_DataModelView> createState() => _DataModelViewState(); +} + +class _DataModelViewState extends State<_DataModelView> { + late final DataSubscription _subscription; + + @override + void initState() { + super.initState(); + _subscription = widget.surface.dataModel.subscribe('/', (_) { + if (mounted) setState(() {}); + }); + } + + @override + void dispose() { + _subscription.unsubscribe(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => _JsonView( + heading: widget.surface.id, + json: widget.surface.dataModel.get('/'), + ); +} + +/// [heading] over [json], pretty-printed. +class _JsonView extends StatelessWidget { + const _JsonView({required this.heading, required this.json}); + + static const JsonEncoder _encoder = JsonEncoder.withIndent(' ', _asString); + + static Object? _asString(Object? value) => '$value'; + + final String heading; + final Object? json; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4, + children: [ + Text(heading, style: Theme.of(context).textTheme.titleSmall), + Text( + _encoder.convert(json), + style: const TextStyle( + fontFamily: 'monospace', + fontFamilyFallback: ['Menlo', 'Courier New'], + fontSize: 12, + ), + ), + ], + ), + ); +} + +/// Renders a Text's [markdown] with `flutter_markdown_plus`, in [style], and +/// opens a tapped link with `url_launcher`. +Widget _markdown(BuildContext context, String markdown, TextStyle style) => + MarkdownBody( + data: markdown, + onTapLink: (text, href, title) { + final Uri? uri = href == null ? null : Uri.tryParse(href); + if (uri != null) launchUrl(uri).ignore(); + }, + styleSheet: MarkdownStyleSheet.fromTheme( + Theme.of(context), + ).copyWith(p: style), + ); diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/.gitignore b/dart/a2ui_flutter/a2ui_explorer/macos/.gitignore new file mode 100644 index 0000000000..746adbb6b9 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Flutter/Flutter-Debug.xcconfig b/dart/a2ui_flutter/a2ui_explorer/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000000..c2efd0b608 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Flutter/Flutter-Release.xcconfig b/dart/a2ui_flutter/a2ui_explorer/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000000..c2efd0b608 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner.xcodeproj/project.pbxproj b/dart/a2ui_flutter/a2ui_explorer/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..fd3421cc4e --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,729 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* a2ui_explorer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "a2ui_explorer.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* a2ui_explorer.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* a2ui_explorer.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.a2uiExplorer.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/a2ui_explorer.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/a2ui_explorer"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.a2uiExplorer.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/a2ui_explorer.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/a2ui_explorer"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.a2uiExplorer.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/a2ui_explorer.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/a2ui_explorer"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/dart/a2ui_flutter/a2ui_explorer/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/dart/a2ui_flutter/a2ui_explorer/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000000..68a4a5c960 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner.xcworkspace/contents.xcworkspacedata b/dart/a2ui_flutter/a2ui_explorer/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..1d526a16ed --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/dart/a2ui_flutter/a2ui_explorer/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner/AppDelegate.swift b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000000..ac49826773 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/AppDelegate.swift @@ -0,0 +1,27 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..60e60c947e --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images": [ + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_16.png", + "scale": "1x" + }, + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "2x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "1x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_64.png", + "scale": "2x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_128.png", + "scale": "1x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "2x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "1x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "2x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "1x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_1024.png", + "scale": "2x" + } + ], + "info": { + "version": 1, + "author": "xcode" + } +} diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000..82b6f9d9a3 Binary files /dev/null and b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000..13b35eba55 Binary files /dev/null and b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000..0a3f5fa40f Binary files /dev/null and b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000000..bdb57226d5 Binary files /dev/null and b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000000..f083318e09 Binary files /dev/null and b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000000..326c0e72c9 Binary files /dev/null and b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000..2f1632cfdd Binary files /dev/null and b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Base.lproj/MainMenu.xib b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000000..78a2a4440f --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Configs/AppInfo.xcconfig b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000000..457523be1d --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = a2ui_explorer + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.a2uiExplorer + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Configs/Debug.xcconfig b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000000..36b0fd9464 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Configs/Release.xcconfig b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000000..dff4f49561 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Configs/Warnings.xcconfig b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000000..42bcbf4780 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner/DebugProfile.entitlements b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000000..08c3ab17cc --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Info.plist b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Info.plist new file mode 100644 index 0000000000..4789daa6a4 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner/MainFlutterWindow.swift b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000000..8ff622f512 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,29 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Release.entitlements b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Release.entitlements new file mode 100644 index 0000000000..ee95ab7e58 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/dart/a2ui_flutter/a2ui_explorer/macos/RunnerTests/RunnerTests.swift b/dart/a2ui_flutter/a2ui_explorer/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000000..201f3f571e --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,26 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/dart/a2ui_flutter/a2ui_explorer/pubspec.yaml b/dart/a2ui_flutter/a2ui_explorer/pubspec.yaml new file mode 100644 index 0000000000..7771b4e1e7 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/pubspec.yaml @@ -0,0 +1,44 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: a2ui_explorer +description: The gallery app of the A2UI Flutter renderer. +publish_to: "none" + +environment: + sdk: ">=3.10.0 <4.0.0" + flutter: ">=3.38.0" + +resolution: workspace + +dependencies: + a2ui_core: + path: ../../a2ui_core + a2ui_flutter: + path: .. + flutter: + sdk: flutter + flutter_markdown_plus: ^1.0.7 + json_schema_builder: ^0.1.7 + url_launcher: ^6.3.2 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true + assets: + - assets/examples/v0_9/ + - assets/examples/v1_0/ diff --git a/dart/a2ui_flutter/a2ui_explorer/test/demo_catalog_test.dart b/dart/a2ui_flutter/a2ui_explorer/test/demo_catalog_test.dart new file mode 100644 index 0000000000..ddacd2dd82 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/test/demo_catalog_test.dart @@ -0,0 +1,137 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:convert'; + +import 'package:a2ui_core/a2ui_core.dart'; +import 'package:a2ui_explorer/src/example.dart'; +import 'package:a2ui_explorer/src/example_run.dart'; +import 'package:a2ui_flutter/a2ui_flutter.dart'; +// ignore: deprecated_member_use, see https://github.com/a2ui-project/a2ui/issues/3235 +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Runs [components] on one surface whose data model is [data], and renders +/// the surface. +Future _render( + WidgetTester tester, + List> components, { + Map data = const {}, +}) async { + const surfaceId = 'test'; + final run = ExampleRun( + Example( + version: A2uiProtocolVersion.v0_9, + fileName: 'test.json', + name: 'Test', + description: '', + json: jsonEncode({ + 'messages': [ + { + 'version': 'v0.9', + 'createSurface': { + 'surfaceId': surfaceId, + 'catalogId': BasicCatalog.v0_9Id, + }, + }, + { + 'version': 'v0.9', + 'updateDataModel': { + 'surfaceId': surfaceId, + 'path': '/', + 'value': data, + }, + }, + { + 'version': 'v0.9', + 'updateComponents': { + 'surfaceId': surfaceId, + 'components': components, + }, + }, + ], + }), + ), + )..runAll(); + addTearDown(run.dispose); + await tester.pumpWidget( + MaterialApp( + home: Scaffold(body: A2uiSurface(surface: run.surfaces.single)), + ), + ); + return run; +} + +void main() { + testWidgets('CustomGrid lays its children out in two columns in a ' + 'horizontal List', (tester) async { + final ExampleRun run = await _render(tester, [ + { + 'id': 'root', + 'component': 'List', + 'direction': 'horizontal', + 'children': ['grid'], + }, + { + 'id': 'grid', + 'component': 'CustomGrid', + 'children': ['a', 'b', 'c'], + }, + {'id': 'a', 'component': 'Text', 'text': 'A'}, + {'id': 'b', 'component': 'Text', 'text': 'B'}, + {'id': 'c', 'component': 'Text', 'text': 'C'}, + ]); + + final Rect a = tester.getRect(find.text('A')); + final Rect b = tester.getRect(find.text('B')); + final Rect c = tester.getRect(find.text('C')); + expect(a.right, lessThan(b.left)); + expect(a.top, b.top); + expect(c.left, a.left); + expect(c.top, greaterThan(a.bottom)); + expect(run.log, isEmpty); + }); + + testWidgets('CustomSlider shows its value and writes whole steps from min ' + 'within its range', (tester) async { + final ExampleRun run = await _render( + tester, + [ + { + 'id': 'root', + 'component': 'CustomSlider', + 'label': 'Level', + 'value': {'path': '/level'}, + 'min': 0.5, + 'max': 2.8, + }, + ], + data: {'level': 0.5}, + ); + expect(find.text('Level: 0.5'), findsOneWidget); + + await tester.tap(find.byType(Slider)); + await tester.pump(); + expect(run.surfaces.single.dataModel.get('/level'), 1.5); + expect(find.text('Level: 1.5'), findsOneWidget); + + await tester.drag(find.byType(Slider), const Offset(1000, 0)); + await tester.pump(); + expect(run.surfaces.single.dataModel.get('/level'), 2.5); + + await tester.drag(find.byType(Slider), const Offset(-1000, 0)); + await tester.pump(); + expect(run.surfaces.single.dataModel.get('/level'), 0.5); + }); +} diff --git a/dart/a2ui_flutter/a2ui_explorer/test/example_run_test.dart b/dart/a2ui_flutter/a2ui_explorer/test/example_run_test.dart new file mode 100644 index 0000000000..fef4df3dea --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/test/example_run_test.dart @@ -0,0 +1,192 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:convert'; + +import 'package:a2ui_core/a2ui_core.dart'; +import 'package:a2ui_explorer/src/example.dart'; +import 'package:a2ui_explorer/src/example_run.dart'; +import 'package:a2ui_flutter/a2ui_flutter.dart'; +// ignore: deprecated_member_use, see https://github.com/a2ui-project/a2ui/issues/3235 +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// A run of an example made of [messages]. +ExampleRun _run(List> messages) { + final run = ExampleRun( + Example( + version: A2uiProtocolVersion.v0_9, + fileName: 'test.json', + name: 'Test', + description: '', + json: jsonEncode({'messages': messages}), + ), + ); + addTearDown(run.dispose); + return run; +} + +const Map _createSurface = { + 'version': 'v0.9', + 'createSurface': {'surfaceId': 'test', 'catalogId': BasicCatalog.v0_9Id}, +}; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late final List examples; + setUpAll(() async => examples = await loadExamples(rootBundle)); + + testWidgets('runs and renders every example without an error', ( + tester, + ) async { + for (final example in examples) { + final run = ExampleRun(example)..runAll(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SingleChildScrollView( + child: Column( + children: [ + for (final surface in run.surfaces) + A2uiSurface(key: ObjectKey(surface), surface: surface), + ], + ), + ), + ), + ), + ); + await tester.pump(); + expect( + run.log, + isEmpty, + reason: '${example.version.jsonValue}/${example.fileName}', + ); + await tester.pumpWidget(const SizedBox()); + run.dispose(); + } + }); + + test('logs a message the processor rejects, with its number', () { + final ExampleRun run = _run([ + { + 'version': 'v0.9', + 'updateDataModel': { + 'surfaceId': 'missing', + 'path': '/', + 'value': {}, + }, + }, + ])..runAll(); + + expect(run.processed, 1); + expect(run.log.single.heading, 'Error INTEGRITY_ERROR'); + expect(run.log.single.details['messageNumber'], 1); + }); + + test('logs an error a surface reports', () async { + final ExampleRun run = _run([_createSurface])..runAll(); + + await run.surfaces.single.dispatchError( + A2uiRendererError( + code: 'TEST', + surfaceId: 'test', + message: 'Failed.', + details: {'reason': 'test'}, + ), + ); + + expect(run.log.single.heading, 'Error TEST'); + expect(run.log.single.details, { + 'code': 'TEST', + 'surfaceId': 'test', + 'message': 'Failed.', + 'details': {'reason': 'test'}, + }); + }); + + testWidgets('logs an error a surface reports when it first appears', ( + tester, + ) async { + final ExampleRun run = _run([ + _createSurface, + { + 'version': 'v0.9', + 'updateDataModel': { + 'surfaceId': 'test', + 'path': '/', + 'value': {'name': 'abc', 'pattern': '('}, + }, + }, + { + 'version': 'v0.9', + 'updateComponents': { + 'surfaceId': 'test', + 'components': [ + { + 'id': 'root', + 'component': 'Text', + 'text': { + 'call': 'formatString', + 'args': { + 'value': r'${regex(value: ${/name}, pattern: ${/pattern})}', + }, + 'returnType': 'string', + }, + }, + ], + }, + }, + ]); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: ListenableBuilder( + listenable: run, + builder: (context, _) => Column( + children: [ + Text('log: ${run.log.length}'), + for (final surface in run.surfaces) + A2uiSurface(surface: surface), + ], + ), + ), + ), + ), + ); + run.runAll(); + await tester.pump(); + await tester.pump(); + expect(tester.takeException(), isNull); + expect(find.text('log: 1'), findsOneWidget); + }); + + test('drops a deleted surface', () { + final ExampleRun run = _run([ + _createSurface, + { + 'version': 'v0.9', + 'deleteSurface': {'surfaceId': 'test'}, + }, + ]); + + run.advance(); + expect(run.surfaces.single.id, 'test'); + + run.advance(); + expect(run.surfaces, isEmpty); + expect(run.log, isEmpty); + }); +} diff --git a/dart/a2ui_flutter/a2ui_explorer/test/examples_test.dart b/dart/a2ui_flutter/a2ui_explorer/test/examples_test.dart new file mode 100644 index 0000000000..4288fcb21d --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/test/examples_test.dart @@ -0,0 +1,61 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:io'; + +import 'package:a2ui_core/a2ui_core.dart'; +import 'package:a2ui_explorer/src/example.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('the bundle holds every example of each version', () async { + final List examples = await loadExamples(rootBundle); + + for (final (A2uiProtocolVersion version, String directory) in [ + ( + A2uiProtocolVersion.v0_9, + '../../../specification/v0_9/catalogs/basic/examples', + ), + (A2uiProtocolVersion.v1_0, '../../../catalogs/basic/v1/examples'), + ]) { + final List specified = [ + for (final FileSystemEntity file in Directory(directory).listSync()) + if (file.path.endsWith('.json')) file.uri.pathSegments.last, + ]..sort(); + + expect(specified, isNotEmpty, reason: directory); + expect([ + for (final e in examples) + if (e.version == version) e.fileName, + ], specified); + } + for (final example in examples) { + expect( + example.messages, + isNotEmpty, + reason: '${example.version.jsonValue}/${example.fileName}', + ); + for (final Map message in example.messages) { + expect( + message['version'], + example.version.jsonValue, + reason: '${example.version.jsonValue}/${example.fileName}', + ); + } + } + }); +} diff --git a/dart/a2ui_flutter/a2ui_explorer/test/explorer_app_test.dart b/dart/a2ui_flutter/a2ui_explorer/test/explorer_app_test.dart new file mode 100644 index 0000000000..68e6f8450e --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/test/explorer_app_test.dart @@ -0,0 +1,353 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:a2ui_explorer/src/example.dart'; +import 'package:a2ui_explorer/src/explorer_app.dart'; +import 'package:a2ui_flutter/a2ui_flutter.dart'; +// ignore: deprecated_member_use, see https://github.com/a2ui-project/a2ui/issues/3235 +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; +import 'package:flutter_test/flutter_test.dart'; + +late final List _examples; + +/// Pumps the app in a window wide enough for its three columns, and selects +/// the example in [fileName], which processes all of its messages. +Future _open(WidgetTester tester, String fileName) async { + tester.view.physicalSize = const Size(1600, 1200); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + await tester.pumpWidget(ExplorerApp(examples: _examples)); + + final Finder item = find.byKey(ValueKey(fileName)); + await tester.scrollUntilVisible( + item, + 200, + scrollable: find.descendant( + of: find.byKey(const Key('examples')), + matching: find.byType(Scrollable), + ), + ); + await tester.pump(); + await tester.tap(item); + await tester.pump(); +} + +/// Taps the control labelled [control] and pumps a frame. +Future _press(WidgetTester tester, String control) async { + await tester.tap(find.text(control)); + await tester.pump(); +} + +/// Selects protocol version [version], such as `v1.0`, and pumps a frame. +Future _selectVersion(WidgetTester tester, String version) async { + await tester.tap( + find.descendant( + of: find.byKey(const Key('version')), + matching: find.text(version), + ), + ); + await tester.pump(); +} + +/// [finder], within the surfaces. +Finder _inSurface(Finder finder) => + find.descendant(of: find.byType(A2uiSurface), matching: finder); + +/// [finder], within the pane keyed [key]. +Finder _inPane(String key, Finder finder) => + find.descendant(of: find.byKey(Key(key)), matching: finder); + +/// Action log entries containing [text]. +Finder _logged(String text) => _inPane('action-log', find.textContaining(text)); + +/// The action log's text while it is empty. +Finder get _noAction => _inPane('action-log', find.text('No action yet.')); + +void main() { + setUpAll(() async { + TestWidgetsFlutterBinding.ensureInitialized(); + _examples = await loadExamples(rootBundle); + }); + + testWidgets('Simple Text renders its text', (tester) async { + await _open(tester, '00_simple-text.json'); + + expect(_inSurface(find.text('Hello, Minimal Catalog!')), findsOneWidget); + expect(_noAction, findsOneWidget); + }); + + testWidgets('Row Layout lays its children out left to right', (tester) async { + await _open(tester, '00_row-layout.json'); + + final Rect left = tester.getRect(_inSurface(find.text('Left Content'))); + final Rect right = tester.getRect(_inSurface(find.text('Right Content'))); + expect(left.right, lessThanOrEqualTo(right.left)); + expect(left.center.dy, moreOrLessEquals(right.center.dy, epsilon: 1)); + }); + + testWidgets('Complex Layout lays its column out top to bottom', ( + tester, + ) async { + await _open(tester, '00_complex-layout.json'); + + final Rect header = tester.getRect( + _inSurface(find.text('User Profile Form')), + ); + final Rect first = tester.getRect( + _inSurface(find.widgetWithText(TextField, 'First Name')), + ); + final Rect last = tester.getRect( + _inSurface(find.widgetWithText(TextField, 'Last Name')), + ); + final Rect footer = tester.getRect( + _inSurface(find.text('Please fill out all fields.')), + ); + expect(header.bottom, lessThanOrEqualTo(first.top)); + expect(first.right, lessThanOrEqualTo(last.left)); + expect(first.top, last.top); + expect(first.bottom, lessThanOrEqualTo(footer.top)); + }); + + testWidgets('typing in a TextField updates it and the data model', ( + tester, + ) async { + await _open(tester, '00_formatted-text.json'); + + await tester.enterText(_inSurface(find.byType(TextField)), 'hello'); + await tester.pump(); + + expect(_inSurface(find.text('hello')), findsOneWidget); + expect( + _inPane('data-model', find.textContaining('"inputValue": "hello"')), + findsOneWidget, + ); + }); + + testWidgets('toggling a CheckBox updates it and the data model', ( + tester, + ) async { + await _open(tester, '07_task-card.json'); + final Finder checkbox = _inSurface(find.byType(Checkbox)); + expect( + _inPane('data-model', find.textContaining('"completed": false')), + findsOneWidget, + ); + + await tester.tap(checkbox); + await tester.pump(); + + expect(tester.widget(checkbox).value, isTrue); + expect( + _inPane('data-model', find.textContaining('"completed": true')), + findsOneWidget, + ); + }); + + testWidgets('a Text bound to a TextField path follows the typing', ( + tester, + ) async { + await _open(tester, '00_formatted-text.json'); + + await tester.enterText(_inSurface(find.byType(TextField)), 'hello'); + await tester.pump(); + + expect(_inSurface(find.text('You typed: hello')), findsOneWidget); + }); + + testWidgets('an action in a template item resolves its item context', ( + tester, + ) async { + await _open(tester, '00_incremental.json'); + final Finder buttons = _inSurface(find.text('Book now')); + expect(buttons, findsNWidgets(4)); + + for (final (int index, String name) in [ + (1, "Ocean's Bounty"), + (3, 'Spice Route'), + ]) { + await tester.ensureVisible(buttons.at(index)); + await tester.tap(buttons.at(index)); + await tester.pump(); + + expect(_logged('"restaurantName": "$name"'), findsOneWidget); + } + expect(_inPane('action-log', find.text('book_now')), findsNWidgets(2)); + expect(_logged('"sourceComponentId": "rc_button"'), findsNWidgets(2)); + expect(_logged('"timestamp"'), findsNWidgets(2)); + }); + + testWidgets('Advance, Run all and Reset step through the messages', ( + tester, + ) async { + await _open(tester, '00_incremental.json'); + expect(find.text('6 of 6 messages processed'), findsOneWidget); + + await _press(tester, 'Reset'); + expect(find.text('0 of 6 messages processed'), findsOneWidget); + expect(find.text('1. createSurface (pending)'), findsOneWidget); + expect(find.byType(A2uiSurface), findsNothing); + + await _press(tester, 'Advance'); + expect(find.text('1 of 6 messages processed'), findsOneWidget); + expect(find.text('1. createSurface (processed)'), findsOneWidget); + expect(find.text('2. updateDataModel (pending)'), findsOneWidget); + expect(find.byType(A2uiSurface), findsOneWidget); + + for (var i = 0; i < 3; i++) { + await _press(tester, 'Advance'); + } + expect(find.text('4 of 6 messages processed'), findsOneWidget); + expect(_inSurface(find.text('The Golden Fork')), findsOneWidget); + expect(_inSurface(find.text('Spice Route')), findsNothing); + + await _press(tester, 'Reset'); + expect(find.text('0 of 6 messages processed'), findsOneWidget); + expect(find.byType(A2uiSurface), findsNothing); + expect(_inPane('data-model', find.text('No surface yet.')), findsOneWidget); + + await _press(tester, 'Run all'); + expect(find.text('6 of 6 messages processed'), findsOneWidget); + expect(_inSurface(find.text('Spice Route')), findsOneWidget); + expect( + tester + .widget(find.widgetWithText(FilledButton, 'Advance')) + .onPressed, + isNull, + ); + expect(_noAction, findsOneWidget); + }); + + testWidgets('a newly selected example starts its lists at the top', ( + tester, + ) async { + double offset(String pane) => tester + .state( + find + .descendant( + of: find.byKey(Key(pane)), + matching: find.byType(Scrollable), + ) + .first, + ) + .position + .pixels; + + await _open(tester, '18_track-list.json'); + for (final pane in ['messages', 'data-model']) { + await tester.drag(find.byKey(Key(pane)), const Offset(0, -3000)); + } + await tester.pump(); + expect(offset('messages'), greaterThan(0)); + expect(offset('data-model'), greaterThan(0)); + + await _open(tester, '24_recipe-card.json'); + + expect(offset('messages'), 0); + expect(offset('data-model'), 0); + }); + + testWidgets('Markdown Text renders its markdown', (tester) async { + await _open(tester, '35_markdown-text.json'); + + expect(_inSurface(find.byType(MarkdownBody)), findsWidgets); + expect( + _inSurface(find.text('Heading 1', findRichText: true)), + findsOneWidget, + ); + expect( + _inSurface(find.textContaining('**', findRichText: true)), + findsNothing, + ); + }); + + testWidgets('Native Grid lays out its custom components in two columns ' + 'and writes back a slider', (tester) async { + await _open(tester, '37_native-grid.json'); + expect(_inSurface(find.byType(Slider)), findsNWidgets(3)); + final Rect volume = tester.getRect( + _inSurface(find.text('Master Volume (Native): 65')), + ); + final Rect card = tester.getRect( + _inSurface(find.text('Universal Web Component: Text & Card')), + ); + final Rect brightness = tester.getRect( + _inSurface(find.text('Brightness Level (Native): 30')), + ); + expect(volume.right, lessThanOrEqualTo(card.left)); + expect(brightness.left, volume.left); + expect(brightness.top, greaterThan(card.bottom)); + + await tester.drag( + _inSurface(find.byType(Slider)).first, + const Offset(-1000, 0), + ); + await tester.pump(); + + expect(_inSurface(find.text('Master Volume (Native): 0')), findsOneWidget); + expect( + _inPane('data-model', find.textContaining('"sliderValue1": 0')), + findsOneWidget, + ); + expect(_noAction, findsOneWidget); + }); + + testWidgets('selecting v1.0 opens the v1.0 example of the same file', ( + tester, + ) async { + await _open(tester, '00_formatted-text.json'); + + await _selectVersion(tester, 'v1.0'); + + expect( + _inPane('messages', find.textContaining('"version": "1.0"')), + findsNWidgets(2), + ); + await tester.enterText(_inSurface(find.byType(TextField)), 'hello'); + await tester.pump(); + expect(_inSurface(find.text('You typed: hello')), findsOneWidget); + expect( + _inPane('data-model', find.textContaining('"inputValue": "hello"')), + findsOneWidget, + ); + expect(_noAction, findsOneWidget); + }); + + testWidgets('a version without the open example opens its first example', ( + tester, + ) async { + await _open(tester, '37_native-grid.json'); + + await _selectVersion(tester, 'v1.0'); + + expect(find.byKey(const ValueKey('37_native-grid.json')), findsNothing); + expect( + tester + .widget( + find.byKey(const ValueKey('00_complex-layout.json')), + ) + .selected, + isTrue, + ); + expect(_inSurface(find.text('User Profile Form')), findsOneWidget); + + await _selectVersion(tester, 'v0.9'); + + expect( + _inPane('messages', find.textContaining('"version": "v0.9"')), + findsWidgets, + ); + }); +} diff --git a/dart/a2ui_flutter/a2ui_explorer/web/favicon.png b/dart/a2ui_flutter/a2ui_explorer/web/favicon.png new file mode 100644 index 0000000000..8aaa46ac1a Binary files /dev/null and b/dart/a2ui_flutter/a2ui_explorer/web/favicon.png differ diff --git a/dart/a2ui_flutter/a2ui_explorer/web/icons/Icon-192.png b/dart/a2ui_flutter/a2ui_explorer/web/icons/Icon-192.png new file mode 100644 index 0000000000..b749bfef07 Binary files /dev/null and b/dart/a2ui_flutter/a2ui_explorer/web/icons/Icon-192.png differ diff --git a/dart/a2ui_flutter/a2ui_explorer/web/icons/Icon-512.png b/dart/a2ui_flutter/a2ui_explorer/web/icons/Icon-512.png new file mode 100644 index 0000000000..88cfd48dff Binary files /dev/null and b/dart/a2ui_flutter/a2ui_explorer/web/icons/Icon-512.png differ diff --git a/dart/a2ui_flutter/a2ui_explorer/web/icons/Icon-maskable-192.png b/dart/a2ui_flutter/a2ui_explorer/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000..eb9b4d76e5 Binary files /dev/null and b/dart/a2ui_flutter/a2ui_explorer/web/icons/Icon-maskable-192.png differ diff --git a/dart/a2ui_flutter/a2ui_explorer/web/icons/Icon-maskable-512.png b/dart/a2ui_flutter/a2ui_explorer/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000..d69c56691f Binary files /dev/null and b/dart/a2ui_flutter/a2ui_explorer/web/icons/Icon-maskable-512.png differ diff --git a/dart/a2ui_flutter/a2ui_explorer/web/index.html b/dart/a2ui_flutter/a2ui_explorer/web/index.html new file mode 100644 index 0000000000..c134c3e4f1 --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/web/index.html @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + A2UI Explorer + + + + + + + diff --git a/dart/a2ui_flutter/a2ui_explorer/web/manifest.json b/dart/a2ui_flutter/a2ui_explorer/web/manifest.json new file mode 100644 index 0000000000..72463dbb3e --- /dev/null +++ b/dart/a2ui_flutter/a2ui_explorer/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "A2UI Explorer", + "short_name": "A2UI Explorer", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "The gallery app of the A2UI Flutter renderer.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/pubspec.yaml b/pubspec.yaml index 81f4025d67..411156c417 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,7 @@ workspace: - dart/a2ui_core - dart/a2ui_cli - dart/a2ui_flutter + - dart/a2ui_flutter/a2ui_explorer - dart/a2ui_flutter/e2e_test - dart/a2ui_flutter/example