Initial flame_lua_runtime package
This commit is contained in:
489
lib/runtime/rendering/render_tree_controller.dart
Normal file
489
lib/runtime/rendering/render_tree_controller.dart
Normal file
@@ -0,0 +1,489 @@
|
||||
import 'package:flame/components.dart';
|
||||
|
||||
import '../models/game_diff.dart';
|
||||
import '../models/runtime_event.dart';
|
||||
import '../models/runtime_node.dart';
|
||||
import '../protocol/runtime_protocol.dart';
|
||||
import '../resources/game_resource_manager.dart';
|
||||
import 'runtime_component.dart';
|
||||
|
||||
class RenderTreeController {
|
||||
RenderTreeController({
|
||||
required Component root,
|
||||
required GameResourceManager resources,
|
||||
required void Function(RuntimeEvent event) eventSink,
|
||||
this.onScopeRemoved,
|
||||
}) : _root = root,
|
||||
_resources = resources,
|
||||
_eventSink = eventSink;
|
||||
|
||||
final Component _root;
|
||||
final GameResourceManager _resources;
|
||||
final void Function(RuntimeEvent event) _eventSink;
|
||||
final Map<String, RuntimeComponent> _components = {};
|
||||
final Map<String, int> _epochs = {};
|
||||
final Map<String, Vector2> _scrollVelocities = {};
|
||||
void Function(String id)? onScopeRemoved;
|
||||
|
||||
RuntimeComponent? componentById(String id) => _components[id];
|
||||
|
||||
bool contains(String id) => _components.containsKey(id);
|
||||
|
||||
String? listViewAt(Vector2 canvasPosition) {
|
||||
final hits =
|
||||
_components.values
|
||||
.where(
|
||||
(component) =>
|
||||
component.node.type == RuntimeNodeType.listView &&
|
||||
component.containsVisualPoint(canvasPosition),
|
||||
)
|
||||
.toList(growable: false)
|
||||
..sort((a, b) => b.priority.compareTo(a.priority));
|
||||
return hits.isEmpty ? null : hits.first.node.id;
|
||||
}
|
||||
|
||||
bool scrollListViewAt(
|
||||
Vector2 canvasPosition, {
|
||||
double deltaX = 0,
|
||||
double deltaY = 0,
|
||||
String source = 'wheel',
|
||||
}) {
|
||||
final id = listViewAt(canvasPosition);
|
||||
if (id == null) {
|
||||
return false;
|
||||
}
|
||||
return scrollListView(id, deltaX: deltaX, deltaY: deltaY, source: source);
|
||||
}
|
||||
|
||||
bool scrollListView(
|
||||
String id, {
|
||||
double deltaX = 0,
|
||||
double deltaY = 0,
|
||||
String source = 'program',
|
||||
}) {
|
||||
final component = _components[id];
|
||||
if (component == null || component.node.type != RuntimeNodeType.listView) {
|
||||
return false;
|
||||
}
|
||||
final node = component.node;
|
||||
final viewport = _listViewContentViewport(node);
|
||||
final nextScrollX = _clampScroll(
|
||||
node.scrollX + deltaX,
|
||||
viewportExtent: viewport.x,
|
||||
contentExtent: node.contentWidth,
|
||||
);
|
||||
final nextScrollY = _clampScroll(
|
||||
node.scrollY + deltaY,
|
||||
viewportExtent: viewport.y,
|
||||
contentExtent: node.contentHeight,
|
||||
);
|
||||
if (nextScrollX == node.scrollX && nextScrollY == node.scrollY) {
|
||||
return false;
|
||||
}
|
||||
component.updateNode(
|
||||
node.copyWithProps({'scrollX': nextScrollX, 'scrollY': nextScrollY}),
|
||||
);
|
||||
_reattachChildrenOf(id);
|
||||
_emitScrollEvent(component, source: source);
|
||||
return true;
|
||||
}
|
||||
|
||||
void setListViewVelocity(String id, Vector2 velocity) {
|
||||
final component = _components[id];
|
||||
if (component == null || component.node.type != RuntimeNodeType.listView) {
|
||||
return;
|
||||
}
|
||||
if (!component.node.inertia) {
|
||||
_scrollVelocities.remove(id);
|
||||
return;
|
||||
}
|
||||
if (velocity.length2 < 1) {
|
||||
_scrollVelocities.remove(id);
|
||||
return;
|
||||
}
|
||||
_scrollVelocities[id] = velocity;
|
||||
}
|
||||
|
||||
void stopListViewVelocity(String id) {
|
||||
_scrollVelocities.remove(id);
|
||||
}
|
||||
|
||||
void updateListViewInertia(double dt) {
|
||||
if (_scrollVelocities.isEmpty || dt <= 0) {
|
||||
return;
|
||||
}
|
||||
for (final entry in _scrollVelocities.entries.toList(growable: false)) {
|
||||
final id = entry.key;
|
||||
final velocity = entry.value;
|
||||
final consumed = scrollListView(
|
||||
id,
|
||||
deltaX: velocity.x * dt,
|
||||
deltaY: velocity.y * dt,
|
||||
source: 'inertia',
|
||||
);
|
||||
final next = velocity * 0.88;
|
||||
if (!consumed || next.length2 < 25) {
|
||||
_scrollVelocities.remove(id);
|
||||
} else {
|
||||
_scrollVelocities[id] = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int epochOf(String id) => _epochs[id] ?? 0;
|
||||
|
||||
bool isNodeEpochAlive(String id, int epoch) {
|
||||
return _components.containsKey(id) && epochOf(id) == epoch;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
final ids = _components.keys.toList(growable: false);
|
||||
for (final component in _components.values) {
|
||||
component.removeFromParent();
|
||||
}
|
||||
_components.clear();
|
||||
for (final id in ids) {
|
||||
_bumpEpoch(id);
|
||||
onScopeRemoved?.call(id);
|
||||
}
|
||||
}
|
||||
|
||||
void removeById(String id) {
|
||||
final removedIds = <String>[];
|
||||
for (final childId in _descendantIdsOf(id)) {
|
||||
final child = _components.remove(childId);
|
||||
if (child != null) {
|
||||
removedIds.add(childId);
|
||||
_bumpEpoch(childId);
|
||||
child.removeFromParent();
|
||||
}
|
||||
}
|
||||
|
||||
final component = _components.remove(id);
|
||||
if (component != null) {
|
||||
removedIds.add(id);
|
||||
_bumpEpoch(id);
|
||||
component.removeFromParent();
|
||||
}
|
||||
|
||||
for (final removedId in removedIds) {
|
||||
onScopeRemoved?.call(removedId);
|
||||
}
|
||||
}
|
||||
|
||||
void apply(NodeDiff diff) {
|
||||
_validateDiff(diff);
|
||||
|
||||
for (final remove in diff.removes) {
|
||||
removeById(remove.id);
|
||||
}
|
||||
|
||||
for (final create in diff.creates) {
|
||||
_createOrReplace(create);
|
||||
}
|
||||
|
||||
for (final update in diff.updates) {
|
||||
final component = _components[update.id];
|
||||
if (component == null) {
|
||||
continue;
|
||||
}
|
||||
final nextNode = component.node.copyWithProps(update.props);
|
||||
component.updateNode(nextNode);
|
||||
_attachToParent(component);
|
||||
_reattachChildrenOf(component.node.id);
|
||||
}
|
||||
}
|
||||
|
||||
void _validateDiff(NodeDiff diff) {
|
||||
final nodes = <String, RuntimeNode>{
|
||||
for (final entry in _components.entries) entry.key: entry.value.node,
|
||||
};
|
||||
|
||||
for (final remove in diff.removes) {
|
||||
_removeNodeSnapshot(nodes, remove.id);
|
||||
}
|
||||
|
||||
for (final create in diff.creates) {
|
||||
nodes[create.id] = create;
|
||||
}
|
||||
|
||||
for (final update in diff.updates) {
|
||||
final current = nodes[update.id];
|
||||
if (current == null) {
|
||||
continue;
|
||||
}
|
||||
nodes[update.id] = current.copyWithProps(update.props);
|
||||
}
|
||||
|
||||
_validateParentGraph(nodes);
|
||||
}
|
||||
|
||||
void _removeNodeSnapshot(Map<String, RuntimeNode> nodes, String id) {
|
||||
final descendants = _descendantIdsOfSnapshot(nodes, id);
|
||||
for (final childId in descendants) {
|
||||
nodes.remove(childId);
|
||||
}
|
||||
nodes.remove(id);
|
||||
}
|
||||
|
||||
List<String> _descendantIdsOfSnapshot(
|
||||
Map<String, RuntimeNode> nodes,
|
||||
String parentId,
|
||||
) {
|
||||
final descendants = <String>[];
|
||||
for (final node in nodes.values) {
|
||||
if (node.parent == parentId) {
|
||||
descendants.add(node.id);
|
||||
descendants.addAll(_descendantIdsOfSnapshot(nodes, node.id));
|
||||
}
|
||||
}
|
||||
return descendants;
|
||||
}
|
||||
|
||||
void _validateParentGraph(Map<String, RuntimeNode> nodes) {
|
||||
for (final node in nodes.values) {
|
||||
final parentId = node.parent;
|
||||
if (parentId == null) {
|
||||
continue;
|
||||
}
|
||||
if (parentId == node.id) {
|
||||
throw const FormatException(
|
||||
'RuntimeNode.parent cannot reference itself',
|
||||
);
|
||||
}
|
||||
|
||||
final seen = <String>{node.id};
|
||||
var currentId = parentId;
|
||||
while (true) {
|
||||
if (!seen.add(currentId)) {
|
||||
throw FormatException(
|
||||
'RuntimeNode.parent would create a cycle: ${node.id} -> $parentId',
|
||||
);
|
||||
}
|
||||
final parent = nodes[currentId];
|
||||
final nextId = parent?.parent;
|
||||
if (nextId == null) {
|
||||
break;
|
||||
}
|
||||
currentId = nextId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _createOrReplace(RuntimeNode node) {
|
||||
_validateParent(node);
|
||||
|
||||
final existing = _components.remove(node.id);
|
||||
existing?.removeFromParent();
|
||||
final epoch = _bumpEpoch(node.id);
|
||||
|
||||
late final RuntimeComponent component;
|
||||
component = RuntimeComponent(
|
||||
node: node,
|
||||
resources: _resources,
|
||||
onNodeTap: (tappedNode, localPosition) {
|
||||
if (_components[tappedNode.id] != component) {
|
||||
return;
|
||||
}
|
||||
_eventSink(
|
||||
RuntimeEvent(
|
||||
type: RuntimeEventType.tap,
|
||||
target: tappedNode.id,
|
||||
handler: tappedNode.onTap,
|
||||
x: localPosition.x,
|
||||
y: localPosition.y,
|
||||
scope: tappedNode.id,
|
||||
targetEpoch: epoch,
|
||||
scopeEpoch: epoch,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
_components[node.id] = component;
|
||||
_attachToParent(component);
|
||||
_reattachChildrenOf(node.id);
|
||||
}
|
||||
|
||||
int _bumpEpoch(String id) {
|
||||
final next = (_epochs[id] ?? 0) + 1;
|
||||
_epochs[id] = next;
|
||||
return next;
|
||||
}
|
||||
|
||||
void _attachToParent(RuntimeComponent component) {
|
||||
final parentId = component.node.parent;
|
||||
final parent = parentId == null ? null : _components[parentId];
|
||||
final target = parent ?? _root;
|
||||
final parentIsListView = parent?.node.type == RuntimeNodeType.listView;
|
||||
final parentScrollX = parentIsListView ? parent!.node.scrollX : 0.0;
|
||||
final parentScrollY = parentIsListView ? parent!.node.scrollY : 0.0;
|
||||
final parentContentOffset = parentIsListView
|
||||
? parent!.listViewContentOffset()
|
||||
: Vector2.zero();
|
||||
component.setParentScroll(
|
||||
x: parentScrollX,
|
||||
y: parentScrollY,
|
||||
contentOffsetX: parentContentOffset.x,
|
||||
contentOffsetY: parentContentOffset.y,
|
||||
);
|
||||
component.setViewportCulled(_isCulledByParentListView(component, parent));
|
||||
if (component.parent == target) {
|
||||
return;
|
||||
}
|
||||
|
||||
component.removeFromParent();
|
||||
target.add(component);
|
||||
}
|
||||
|
||||
void _validateParent(RuntimeNode node) {
|
||||
final parentId = node.parent;
|
||||
if (parentId != null && _wouldCreateCycle(node.id, parentId)) {
|
||||
throw FormatException(
|
||||
'RuntimeNode.parent would create a cycle: ${node.id} -> $parentId',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _reattachChildrenOf(String parentId) {
|
||||
for (final component in _components.values.toList(growable: false)) {
|
||||
if (component.node.parent == parentId) {
|
||||
_attachToParent(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _descendantIdsOf(String parentId) {
|
||||
final descendants = <String>[];
|
||||
for (final component in _components.values) {
|
||||
if (component.node.parent == parentId) {
|
||||
descendants.add(component.node.id);
|
||||
descendants.addAll(_descendantIdsOf(component.node.id));
|
||||
}
|
||||
}
|
||||
return descendants;
|
||||
}
|
||||
|
||||
bool _isCulledByParentListView(
|
||||
RuntimeComponent component,
|
||||
RuntimeComponent? parent,
|
||||
) {
|
||||
final parentNode = parent?.node;
|
||||
if (parentNode == null ||
|
||||
parentNode.type != RuntimeNodeType.listView ||
|
||||
!parentNode.virtualized) {
|
||||
return false;
|
||||
}
|
||||
final cache = parentNode.cacheExtent;
|
||||
final viewportLeft = parentNode.scrollX - cache;
|
||||
final viewportTop = parentNode.scrollY - cache;
|
||||
final viewport = parent?.listViewContentViewport() ?? Vector2.zero();
|
||||
final viewportRight = parentNode.scrollX + viewport.x + cache;
|
||||
final viewportBottom = parentNode.scrollY + viewport.y + cache;
|
||||
final node = component.node;
|
||||
final childLeft = node.x;
|
||||
final childTop = node.y;
|
||||
final childRight = node.x + (node.width ?? 0);
|
||||
final childBottom = node.y + (node.height ?? 0);
|
||||
return childRight < viewportLeft ||
|
||||
childLeft > viewportRight ||
|
||||
childBottom < viewportTop ||
|
||||
childTop > viewportBottom;
|
||||
}
|
||||
|
||||
Vector2 _listViewContentViewport(RuntimeNode node) {
|
||||
final width = node.width ?? 0;
|
||||
final height = node.height ?? 0;
|
||||
final left = node.paddingLeft.clamp(0.0, width).toDouble();
|
||||
final top = node.paddingTop.clamp(0.0, height).toDouble();
|
||||
if (!node.scrollbarVisible) {
|
||||
return Vector2(
|
||||
(width - left - node.paddingRight).clamp(0.0, width).toDouble(),
|
||||
(height - top - node.paddingBottom).clamp(0.0, height).toDouble(),
|
||||
);
|
||||
}
|
||||
final thickness = (node.scrollbarThickness ?? 5).clamp(1.0, 16.0);
|
||||
final gutter = thickness + 8;
|
||||
var vertical = (node.contentHeight ?? 0) > height && height > 0;
|
||||
var horizontal = (node.contentWidth ?? 0) > width && width > 0;
|
||||
for (var i = 0; i < 2; i += 1) {
|
||||
final viewportWidth =
|
||||
width - left - node.paddingRight - (vertical ? gutter : 0);
|
||||
final viewportHeight =
|
||||
height - top - node.paddingBottom - (horizontal ? gutter : 0);
|
||||
vertical =
|
||||
(node.contentHeight ?? 0) > viewportHeight && viewportHeight > 0;
|
||||
horizontal =
|
||||
(node.contentWidth ?? 0) > viewportWidth && viewportWidth > 0;
|
||||
}
|
||||
return Vector2(
|
||||
(width - left - node.paddingRight - (vertical ? gutter : 0))
|
||||
.clamp(0.0, width)
|
||||
.toDouble(),
|
||||
(height - top - node.paddingBottom - (horizontal ? gutter : 0))
|
||||
.clamp(0.0, height)
|
||||
.toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
double _clampScroll(
|
||||
double value, {
|
||||
required double? viewportExtent,
|
||||
required double? contentExtent,
|
||||
}) {
|
||||
final maxScroll = (contentExtent ?? 0) - (viewportExtent ?? 0);
|
||||
if (maxScroll <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return value.clamp(0, maxScroll).toDouble();
|
||||
}
|
||||
|
||||
void _emitScrollEvent(RuntimeComponent component, {required String source}) {
|
||||
final node = component.node;
|
||||
final handler = node.onScroll;
|
||||
if (handler == null || handler.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final id = node.id;
|
||||
_eventSink(
|
||||
RuntimeEvent(
|
||||
type: RuntimeEventType.scroll,
|
||||
target: id,
|
||||
handler: handler,
|
||||
scope: id,
|
||||
targetEpoch: epochOf(id),
|
||||
scopeEpoch: epochOf(id),
|
||||
data: {
|
||||
'scrollX': node.scrollX,
|
||||
'scrollY': node.scrollY,
|
||||
'maxScrollX': _clampScroll(
|
||||
double.infinity,
|
||||
viewportExtent: node.width,
|
||||
contentExtent: node.contentWidth,
|
||||
),
|
||||
'maxScrollY': _clampScroll(
|
||||
double.infinity,
|
||||
viewportExtent: node.height,
|
||||
contentExtent: node.contentHeight,
|
||||
),
|
||||
'source': source,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _wouldCreateCycle(String nodeId, String parentId) {
|
||||
var currentId = parentId;
|
||||
while (true) {
|
||||
if (currentId == nodeId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final parent = _components[currentId];
|
||||
final nextId = parent?.node.parent;
|
||||
if (nextId == null) {
|
||||
return false;
|
||||
}
|
||||
currentId = nextId;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user