Skip to content

Commit

Permalink
feat: Adding x,y,width and height inputs to position components on De…
Browse files Browse the repository at this point in the history
…v Tools (#3263)

Adds fields to allow developers to change the x, y, width and height of
position components from the dev tools.



https://github.com/user-attachments/assets/70f6fe15-488c-4eb2-83bf-6cdc050526d8
  • Loading branch information
erickzanardo authored Aug 12, 2024
1 parent 267d680 commit 003ec3a
Show file tree
Hide file tree
Showing 9 changed files with 380 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,24 @@ class ComponentTreeNode {
final int id;
final String name;
final String toStringText;
final bool isPositionComponent;
final List<ComponentTreeNode> children;

ComponentTreeNode(this.id, this.name, this.toStringText, this.children);
ComponentTreeNode(
this.id,
this.name,
this.toStringText,
// ignore: avoid_positional_boolean_parameters
this.isPositionComponent,
this.children,
);

ComponentTreeNode.fromComponent(Component component)
: this(
component.hashCode,
component.runtimeType.toString(),
component.toString(),
component is PositionComponent,
component.children.map(ComponentTreeNode.fromComponent).toList(),
);

Expand All @@ -47,6 +56,7 @@ class ComponentTreeNode {
json['id'] as int,
json['name'] as String,
json['toString'] as String,
json['isPositionComponent'] as bool,
(json['children'] as List)
.map((e) => ComponentTreeNode.fromJson(e as Map<String, dynamic>))
.toList(),
Expand All @@ -57,6 +67,7 @@ class ComponentTreeNode {
'id': id,
'name': name,
'toString': toStringText,
'isPositionComponent': isPositionComponent,
'children': children.map((e) => e.toJson()).toList(),
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import 'dart:convert';
import 'dart:developer';

import 'package:flame/components.dart';
import 'package:flame/src/devtools/dev_tools_connector.dart';

class PositionComponentAttributesConnector extends DevToolsConnector {
@override
void init() {
registerExtension(
'ext.flame_devtools.getPositionComponentAttributes',
(method, parameters) async {
final id = int.tryParse(parameters['id'] ?? '');

final positionComponent = findComponent<PositionComponent>(id);

if (positionComponent != null) {
return ServiceExtensionResponse.result(
json.encode({
'id': id,
'x': positionComponent.x,
'y': positionComponent.y,
'width': positionComponent.width,
'height': positionComponent.height,
}),
);
} else {
return ServiceExtensionResponse.error(
ServiceExtensionResponse.extensionError,
'No PositionComponent found with id: $id',
);
}
},
);

registerExtension(
'ext.flame_devtools.setPositionComponentAttributes',
(method, parameters) async {
final id = int.tryParse(parameters['id'] ?? '');
final attribute = parameters['attribute'];

final positionComponent = findComponent<PositionComponent>(id);

if (positionComponent != null) {
if (attribute == 'x') {
positionComponent.x = double.parse(parameters['value']!);
} else if (attribute == 'y') {
positionComponent.y = double.parse(parameters['value']!);
} else if (attribute == 'width') {
positionComponent.width = double.parse(parameters['value']!);
} else if (attribute == 'height') {
positionComponent.height = double.parse(parameters['value']!);
} else {
return ServiceExtensionResponse.error(
ServiceExtensionResponse.extensionError,
'Invalid attribute: $attribute',
);
}
return ServiceExtensionResponse.result('Success');
} else {
return ServiceExtensionResponse.error(
ServiceExtensionResponse.extensionError,
'No PositionComponent found with id: $id',
);
}
},
);
}
}
20 changes: 20 additions & 0 deletions packages/flame/lib/src/devtools/dev_tools_connector.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:developer';

import 'package:flame/components.dart';
import 'package:flame/debug.dart';
import 'package:flame/game.dart';
import 'package:flutter/foundation.dart';
Expand Down Expand Up @@ -29,4 +30,23 @@ abstract class DevToolsConnector {

/// Here you can do clean-up before a new game is set in the connector.
void disposeGame() {}

/// Finds a component in the game tree by its id.
///
/// Returns the component if found, otherwise null.
T? findComponent<T extends Component>(int? id) {
T? component;
game.propagateToChildren<T>(
(c) {
if (c.hashCode == id) {
component = c;
return false;
}
return true;
},
includeSelf: true,
);

return component;
}
}
2 changes: 2 additions & 0 deletions packages/flame/lib/src/devtools/dev_tools_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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';
import 'package:flame/src/devtools/connectors/position_component_attributes_connector.dart';
import 'package:flame/src/devtools/dev_tools_connector.dart';

/// When [DevToolsService] is initialized by the [FlameGame] it will call
Expand Down Expand Up @@ -37,6 +38,7 @@ class DevToolsService {
ComponentTreeConnector(),
GameLoopConnector(),
ComponentSnapshotConnector(),
PositionComponentAttributesConnector(),
];

/// This method is called every time a new game is set in the service and it
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'package:flame_devtools/repository.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final positionComponentAttributesProvider =
FutureProvider.family<PositionComponentAttributes, int>((ref, id) async {
return Repository.getPositionComponentAttributes(id: id);
});
52 changes: 52 additions & 0 deletions packages/flame_devtools/lib/repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,56 @@ sealed class Repository {
);
return snapshotResponse.json!['snapshot'] as String?;
}

static Future<PositionComponentAttributes> getPositionComponentAttributes({
int? id,
}) async {
final potentialPositionComponentResponse =
await serviceManager.callServiceExtensionOnMainIsolate(
'ext.flame_devtools.getPositionComponentAttributes',
args: {'id': id},
);

return PositionComponentAttributes.fromJson(
potentialPositionComponentResponse.json!,
);
}

static Future<void> setPositionComponentAttribute({
required String attribute,
required dynamic value,
int? id,
}) async {
await serviceManager.callServiceExtensionOnMainIsolate(
'ext.flame_devtools.setPositionComponentAttributes',
args: {
'id': id,
'attribute': attribute,
'value': value,
},
);
}
}

class PositionComponentAttributes {
final double x;
final double y;
final double width;
final double height;

PositionComponentAttributes({
required this.x,
required this.y,
required this.width,
required this.height,
});

factory PositionComponentAttributes.fromJson(Map<String, dynamic> json) {
return PositionComponentAttributes(
x: json['x'] as double,
y: json['y'] as double,
width: json['width'] as double,
height: json['height'] as double,
);
}
}
8 changes: 8 additions & 0 deletions packages/flame_devtools/lib/widgets/component_tree.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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:flame_devtools/widgets/position_component_attributes_form.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

Expand Down Expand Up @@ -152,6 +153,13 @@ class ComponentSection extends ConsumerWidget {
'Children: ${node.children.length}',
style: textStyle,
),
if (node.isPositionComponent)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: PositionComponentAttributesForm(
componentId: node.id,
),
),
Text(
'toString:\n${node.toStringText}',
style: textStyle,
Expand Down
110 changes: 110 additions & 0 deletions packages/flame_devtools/lib/widgets/incremental_number_form_field.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import 'package:flutter/material.dart';

class IncrementalNumberFormField<T extends num> extends StatefulWidget {
const IncrementalNumberFormField({
required this.initialValue,
required this.label,
this.onChanged,
super.key,
});

final String label;
final T initialValue;
final void Function(T)? onChanged;

@override
State<IncrementalNumberFormField<T>> createState() =>
_IncrementalNumberFormFieldState<T>();
}

class _IncrementalNumberFormFieldState<T extends num>
extends State<IncrementalNumberFormField<T>> {
late final _controller = TextEditingController()
..text = widget.initialValue.toString();

String? errorText;

@override
void didUpdateWidget(covariant IncrementalNumberFormField<T> oldWidget) {
super.didUpdateWidget(oldWidget);

if (oldWidget.initialValue != widget.initialValue) {
_controller.text = widget.initialValue.toString();
errorText = null;
}
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

T _parse() {
if (T == double) {
return double.parse(_controller.text) as T;
} else {
return int.parse(_controller.text) as T;
}
}

void _tryUpdate(String value) {
try {
final value = _parse();
_update(value);
} on Exception catch (_) {
setState(() {
errorText = 'Invalid number';
});
}
}

void _update(T v) {
setState(() {
errorText = null;
});

widget.onChanged?.call(v);
}

void _increment() {
final value = _parse() + 1 as T;
_update(value);
_controller.text = value.toString();
}

void _decrement() {
final value = _parse() - 1 as T;
_update(value);
_controller.text = value.toString();
}

@override
Widget build(BuildContext context) {
return Row(
children: [
IconButton(
onPressed: _decrement,
icon: const Icon(Icons.remove),
),
const SizedBox(width: 8),
SizedBox(
width: 100,
child: TextField(
decoration: InputDecoration(
labelText: widget.label,
errorText: errorText,
),
controller: _controller,
onChanged: _tryUpdate,
),
),
const SizedBox(width: 8),
IconButton(
onPressed: _increment,
icon: const Icon(Icons.add),
),
],
);
}
}
Loading

0 comments on commit 003ec3a

Please sign in to comment.