diff --git a/packages/flame/lib/src/devtools/connectors/component_snapshot_connector.dart b/packages/flame/lib/src/devtools/connectors/component_snapshot_connector.dart new file mode 100644 index 00000000000..93420d152e1 --- /dev/null +++ b/packages/flame/lib/src/devtools/connectors/component_snapshot_connector.dart @@ -0,0 +1,71 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/src/devtools/dev_tools_connector.dart'; + +class ComponentSnapshotConnector extends DevToolsConnector { + @override + void init() { + registerExtension( + 'ext.flame_devtools.getComponentSnapshot', + (method, parameters) async { + Image? image; + final id = int.tryParse(parameters['id'] ?? ''); + game.propagateToChildren( + (c) { + if (c.hashCode == id) { + final pictureRecorder = PictureRecorder(); + + final canvas = Canvas(pictureRecorder); + + // I am not sure how we could calculate the size of a component + // that isn't a PositionComponent, so for now we will just use + // an arbitrary size. + var width = 100; + var height = 100; + + if (c is PositionComponent) { + width = c.width.toInt(); + height = c.height.toInt(); + + // Translate the canvas so that the component is + // drawn at the 0,0 + canvas.translate(-c.x, -c.y); + } + + c.renderTree(canvas); + + final picture = pictureRecorder.endRecording(); + + image = picture.toImageSync(width, height); + + return false; + } + return true; + }, + ); + + if (image != null) { + final byteData = await image!.toByteData(format: ImageByteFormat.png); + final buffer = byteData!.buffer.asUint8List(); + final snapshot = base64Encode(buffer); + return ServiceExtensionResponse.result( + json.encode({ + 'id': id, + 'snapshot': snapshot, + }), + ); + } + + return ServiceExtensionResponse.result( + json.encode({ + 'id': id, + 'snapshot': '', + }), + ); + }, + ); + } +} diff --git a/packages/flame/lib/src/devtools/dev_tools_service.dart b/packages/flame/lib/src/devtools/dev_tools_service.dart index fe11e207401..b73250a16e7 100644 --- a/packages/flame/lib/src/devtools/dev_tools_service.dart +++ b/packages/flame/lib/src/devtools/dev_tools_service.dart @@ -1,5 +1,6 @@ import 'package:flame/game.dart'; import 'package:flame/src/devtools/connectors/component_count_connector.dart'; +import 'package:flame/src/devtools/connectors/component_snapshot_connector.dart'; import 'package:flame/src/devtools/connectors/component_tree_connector.dart'; import 'package:flame/src/devtools/connectors/debug_mode_connector.dart'; import 'package:flame/src/devtools/connectors/game_loop_connector.dart'; @@ -35,6 +36,7 @@ class DevToolsService { ComponentCountConnector(), ComponentTreeConnector(), GameLoopConnector(), + ComponentSnapshotConnector(), ]; /// This method is called every time a new game is set in the service and it diff --git a/packages/flame_devtools/lib/repository.dart b/packages/flame_devtools/lib/repository.dart index c9fc22b54c8..8bb08c98edd 100644 --- a/packages/flame_devtools/lib/repository.dart +++ b/packages/flame_devtools/lib/repository.dart @@ -67,4 +67,13 @@ sealed class Repository { ); return stepResponse.json!['step_time'] as double; } + + static Future snapshot({String? id}) async { + final snapshotResponse = + await serviceManager.callServiceExtensionOnMainIsolate( + 'ext.flame_devtools.getComponentSnapshot', + args: {'id': id}, + ); + return snapshotResponse.json!['snapshot'] as String?; + } } diff --git a/packages/flame_devtools/lib/widgets/component_snapshot.dart b/packages/flame_devtools/lib/widgets/component_snapshot.dart new file mode 100644 index 00000000000..72ed1841c0f --- /dev/null +++ b/packages/flame_devtools/lib/widgets/component_snapshot.dart @@ -0,0 +1,89 @@ +import 'package:flame/flame.dart'; +import 'package:flame/widgets.dart'; +import 'package:flame_devtools/repository.dart'; +import 'package:flutter/material.dart' hide Image; + +class ComponentSnapshot extends StatefulWidget { + const ComponentSnapshot({ + required this.id, + super.key, + }); + + final String id; + + @override + State createState() => _ComponentSnapshotState(); +} + +class _ComponentSnapshotState extends State { + late Future _snapshot; + + @override + void initState() { + super.initState(); + + _snapshot = Repository.snapshot(id: widget.id); + } + + @override + void didUpdateWidget(ComponentSnapshot oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.id != widget.id) { + _snapshot = Repository.snapshot(id: widget.id); + } + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _snapshot, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + return Base64Image( + base64: snapshot.data!, + imageId: widget.id, + ); + } + return const Text('Loading snapshot...'); + }, + ); + } +} + +class Base64Image extends StatelessWidget { + const Base64Image({ + required this.base64, + required this.imageId, + super.key, + }); + + final String base64; + final String imageId; + + @override + Widget build(BuildContext context) { + final imageFuture = Flame.images.fromBase64( + imageId, + base64, + ); + return FutureBuilder( + future: imageFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return SizedBox( + width: 200, + height: 200, + child: SpriteWidget( + sprite: Sprite( + snapshot.data!, + ), + ), + ); + } + return const Text('Loading image...'); + }, + ); + } +} diff --git a/packages/flame_devtools/lib/widgets/component_tree.dart b/packages/flame_devtools/lib/widgets/component_tree.dart index 0e67fd9e7be..687511422e1 100644 --- a/packages/flame_devtools/lib/widgets/component_tree.dart +++ b/packages/flame_devtools/lib/widgets/component_tree.dart @@ -1,5 +1,6 @@ import 'package:animated_tree_view/animated_tree_view.dart'; import 'package:devtools_app_shared/ui.dart' as devtools_ui; +import 'package:flame_devtools/widgets/component_snapshot.dart'; import 'package:flame_devtools/widgets/component_tree_model.dart'; import 'package:flame_devtools/widgets/debug_mode_button.dart'; import 'package:flutter/material.dart'; @@ -128,33 +129,38 @@ class ComponentSection extends ConsumerWidget { ], ), ), - Padding( - padding: const EdgeInsets.all(20), - child: node == null - ? Text( - 'Select a component in the tree', - style: textStyle, - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + Expanded( + child: Padding( + padding: const EdgeInsets.all(20), + child: node == null + ? Text( + 'Select a component in the tree', + style: textStyle, + ) + : SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Id: ${node.id}', style: textStyle), - DebugModeButton(id: node.id), + Row( + children: [ + Text('Id: ${node.id}', style: textStyle), + DebugModeButton(id: node.id), + ].withSpacing(), + ), + Text('Type: ${node.name}', style: textStyle), + Text( + 'Children: ${node.children.length}', + style: textStyle, + ), + Text( + 'toString:\n${node.toStringText}', + style: textStyle, + ), + ComponentSnapshot(id: node.id.toString()), ].withSpacing(), ), - Text('Type: ${node.name}', style: textStyle), - Text( - 'Children: ${node.children.length}', - style: textStyle, - ), - Text( - 'toString:\n${node.toStringText}', - style: textStyle, - ), - ].withSpacing(), - ), + ), + ), ), ], ),