Files
flutter_lua_runtime/lib/runtime/models/runtime_node.dart
2026-06-07 22:53:58 +08:00

573 lines
19 KiB
Dart

import 'package:flutter/material.dart';
import '../protocol/runtime_protocol.dart';
class RuntimeNode {
const RuntimeNode({
required this.id,
required this.type,
this.parent,
this.asset,
this.pressedAsset,
this.disabledAsset,
this.animation,
this.skin,
this.loop = true,
this.text,
this.x = 0,
this.y = 0,
this.width,
this.height,
this.paddingLeft = 0,
this.paddingTop = 0,
this.paddingRight = 0,
this.paddingBottom = 0,
this.anchor = RuntimeAnchorValue.topLeft,
this.layer = 0,
this.visible = true,
this.alpha = 1,
this.scale = 1,
this.rotation = 0,
this.color,
this.fontSize,
this.textAlign = RuntimeTextAlignValue.center,
this.radius,
this.strokeWidth,
this.value,
this.scrollX = 0,
this.scrollY = 0,
this.contentWidth,
this.contentHeight,
this.virtualized = false,
this.cacheExtent = 0,
this.inertia = true,
this.scrollbarThumbColor,
this.scrollbarTrackColor,
this.scrollbarThickness,
this.scrollbarVisible = true,
this.interactive = false,
this.onTap,
this.onScroll,
this.preset,
this.count,
this.duration,
this.speedMin,
this.speedMax,
this.gravityX,
this.gravityY,
this.spread,
this.colorTo,
this.radiusTo,
this.autoRemove = true,
this.fadeOut = true,
});
final String id;
final String type;
final String? parent;
final String? asset;
final String? pressedAsset;
final String? disabledAsset;
final String? animation;
final String? skin;
final bool loop;
final String? text;
final double x;
final double y;
final double? width;
final double? height;
final double paddingLeft;
final double paddingTop;
final double paddingRight;
final double paddingBottom;
final String anchor;
final int layer;
final bool visible;
final double alpha;
final double scale;
final double rotation;
final Color? color;
final double? fontSize;
final String textAlign;
final double? radius;
final double? strokeWidth;
final double? value;
final double scrollX;
final double scrollY;
final double? contentWidth;
final double? contentHeight;
final bool virtualized;
final double cacheExtent;
final bool inertia;
final Color? scrollbarThumbColor;
final Color? scrollbarTrackColor;
final double? scrollbarThickness;
final bool scrollbarVisible;
final bool interactive;
final String? onTap;
final String? onScroll;
final String? preset;
final int? count;
final double? duration;
final double? speedMin;
final double? speedMax;
final double? gravityX;
final double? gravityY;
final double? spread;
final Color? colorTo;
final double? radiusTo;
final bool autoRemove;
final bool fadeOut;
RuntimeNode copyWithProps(Map<String, Object?> props) {
RuntimeProtocolSchema.ensureKnownKeys(
props,
allowed: RuntimeProtocolSchema.nodePropsFields,
context: 'RuntimeNode.props',
);
final nextType = _stringProp(props, RuntimeProtocolField.type) ?? type;
if (!RuntimeNodeType.isSupported(nextType)) {
throw FormatException('RuntimeNode.type is unsupported: $nextType');
}
final nextAnchor =
_stringProp(props, RuntimeProtocolField.anchor) ?? anchor;
if (!RuntimeAnchorValue.isSupported(nextAnchor)) {
throw FormatException('RuntimeNode.anchor is unsupported: $nextAnchor');
}
final nextTextAlign =
_stringProp(props, RuntimeProtocolField.textAlign) ?? textAlign;
if (!RuntimeTextAlignValue.isSupported(nextTextAlign)) {
throw FormatException(
'RuntimeNode.textAlign is unsupported: $nextTextAlign',
);
}
final nextPreset =
_stringProp(props, RuntimeProtocolField.preset) ?? preset;
_validateParticlePreset(nextPreset);
final nextWidth = _doubleProp(props, RuntimeProtocolField.width) ?? width;
final nextHeight =
_doubleProp(props, RuntimeProtocolField.height) ?? height;
final nextContentWidth =
_doubleProp(props, RuntimeProtocolField.contentWidth) ?? contentWidth;
final nextContentHeight =
_doubleProp(props, RuntimeProtocolField.contentHeight) ?? contentHeight;
final nextPaddingLeft =
_nonNegativeDoubleProp(props, RuntimeProtocolField.paddingLeft) ??
paddingLeft;
final nextPaddingTop =
_nonNegativeDoubleProp(props, RuntimeProtocolField.paddingTop) ??
paddingTop;
final nextPaddingRight =
_nonNegativeDoubleProp(props, RuntimeProtocolField.paddingRight) ??
paddingRight;
final nextPaddingBottom =
_nonNegativeDoubleProp(props, RuntimeProtocolField.paddingBottom) ??
paddingBottom;
final nextViewportWidth = nextWidth == null
? null
: (nextWidth - nextPaddingLeft - nextPaddingRight)
.clamp(0.0, nextWidth)
.toDouble();
final nextViewportHeight = nextHeight == null
? null
: (nextHeight - nextPaddingTop - nextPaddingBottom)
.clamp(0.0, nextHeight)
.toDouble();
final nextScrollX = props.containsKey(RuntimeProtocolField.scrollX)
? _scrollProp(
props,
RuntimeProtocolField.scrollX,
contentExtent: nextContentWidth,
viewportExtent: nextViewportWidth,
)!
: _clampScroll(
scrollX,
contentExtent: nextContentWidth,
viewportExtent: nextViewportWidth,
);
final nextScrollY = props.containsKey(RuntimeProtocolField.scrollY)
? _scrollProp(
props,
RuntimeProtocolField.scrollY,
contentExtent: nextContentHeight,
viewportExtent: nextViewportHeight,
)!
: _clampScroll(
scrollY,
contentExtent: nextContentHeight,
viewportExtent: nextViewportHeight,
);
return RuntimeNode(
id: id,
type: nextType,
parent: _parentProp(props, currentParent: parent, nodeId: id),
asset: _stringProp(props, RuntimeProtocolField.asset) ?? asset,
pressedAsset:
_stringProp(props, RuntimeProtocolField.pressedAsset) ?? pressedAsset,
disabledAsset:
_stringProp(props, RuntimeProtocolField.disabledAsset) ??
disabledAsset,
animation:
_stringProp(props, RuntimeProtocolField.animation) ?? animation,
skin: _stringProp(props, RuntimeProtocolField.skin) ?? skin,
loop: _boolProp(props, RuntimeProtocolField.loop) ?? loop,
text: _stringProp(props, RuntimeProtocolField.text) ?? text,
x: _doubleProp(props, RuntimeProtocolField.x) ?? x,
y: _doubleProp(props, RuntimeProtocolField.y) ?? y,
width: nextWidth,
height: nextHeight,
paddingLeft: nextPaddingLeft,
paddingTop: nextPaddingTop,
paddingRight: nextPaddingRight,
paddingBottom: nextPaddingBottom,
anchor: nextAnchor,
layer: _intProp(props, RuntimeProtocolField.layer) ?? layer,
visible: _boolProp(props, RuntimeProtocolField.visible) ?? visible,
alpha: _doubleProp(props, RuntimeProtocolField.alpha) ?? alpha,
scale: _doubleProp(props, RuntimeProtocolField.scale) ?? scale,
rotation: _doubleProp(props, RuntimeProtocolField.rotation) ?? rotation,
color: _colorProp(props, RuntimeProtocolField.color) ?? color,
fontSize: _doubleProp(props, RuntimeProtocolField.fontSize) ?? fontSize,
textAlign: nextTextAlign,
radius: _doubleProp(props, RuntimeProtocolField.radius) ?? radius,
strokeWidth:
_doubleProp(props, RuntimeProtocolField.strokeWidth) ?? strokeWidth,
value: _normalizedValueProp(props, RuntimeProtocolField.value) ?? value,
scrollX: nextScrollX,
scrollY: nextScrollY,
contentWidth: nextContentWidth,
contentHeight: nextContentHeight,
virtualized:
_boolProp(props, RuntimeProtocolField.virtualized) ?? virtualized,
cacheExtent:
_nonNegativeDoubleProp(props, RuntimeProtocolField.cacheExtent) ??
cacheExtent,
inertia: _boolProp(props, RuntimeProtocolField.inertia) ?? inertia,
scrollbarThumbColor:
_colorProp(props, RuntimeProtocolField.scrollbarThumbColor) ??
scrollbarThumbColor,
scrollbarTrackColor:
_colorProp(props, RuntimeProtocolField.scrollbarTrackColor) ??
scrollbarTrackColor,
scrollbarThickness:
_nonNegativeDoubleProp(
props,
RuntimeProtocolField.scrollbarThickness,
) ??
scrollbarThickness,
scrollbarVisible:
_boolProp(props, RuntimeProtocolField.scrollbarVisible) ??
scrollbarVisible,
interactive:
_boolProp(props, RuntimeProtocolField.interactive) ?? interactive,
onTap: _stringProp(props, RuntimeProtocolField.onTap) ?? onTap,
onScroll: _stringProp(props, RuntimeProtocolField.onScroll) ?? onScroll,
preset: nextPreset,
count: _positiveIntProp(props, RuntimeProtocolField.count) ?? count,
duration:
_nonNegativeDoubleProp(props, RuntimeProtocolField.duration) ??
duration,
speedMin:
_nonNegativeDoubleProp(props, RuntimeProtocolField.speedMin) ??
speedMin,
speedMax:
_nonNegativeDoubleProp(props, RuntimeProtocolField.speedMax) ??
speedMax,
gravityX: _doubleProp(props, RuntimeProtocolField.gravityX) ?? gravityX,
gravityY: _doubleProp(props, RuntimeProtocolField.gravityY) ?? gravityY,
spread:
_nonNegativeDoubleProp(props, RuntimeProtocolField.spread) ?? spread,
colorTo: _colorProp(props, RuntimeProtocolField.colorTo) ?? colorTo,
radiusTo:
_nonNegativeDoubleProp(props, RuntimeProtocolField.radiusTo) ??
radiusTo,
autoRemove:
_boolProp(props, RuntimeProtocolField.autoRemove) ?? autoRemove,
fadeOut: _boolProp(props, RuntimeProtocolField.fadeOut) ?? fadeOut,
);
}
static RuntimeNode fromMap(Map<String, Object?> map) {
RuntimeProtocolSchema.ensureKnownKeys(
map,
allowed: RuntimeProtocolSchema.nodeFields,
context: 'RuntimeNode',
);
final type = _requiredString(map, RuntimeProtocolField.type);
if (!RuntimeNodeType.isSupported(type)) {
throw FormatException('RuntimeNode.type is unsupported: $type');
}
final anchor =
_stringProp(map, RuntimeProtocolField.anchor) ??
RuntimeAnchorValue.topLeft;
if (!RuntimeAnchorValue.isSupported(anchor)) {
throw FormatException('RuntimeNode.anchor is unsupported: $anchor');
}
final textAlign =
_stringProp(map, RuntimeProtocolField.textAlign) ??
RuntimeTextAlignValue.center;
if (!RuntimeTextAlignValue.isSupported(textAlign)) {
throw FormatException('RuntimeNode.textAlign is unsupported: $textAlign');
}
final preset = _stringProp(map, RuntimeProtocolField.preset);
_validateParticlePreset(preset);
return RuntimeNode(
id: _requiredString(map, RuntimeProtocolField.id),
type: type,
parent: _parentProp(
map,
currentParent: null,
nodeId: _requiredString(map, RuntimeProtocolField.id),
),
asset: _stringProp(map, RuntimeProtocolField.asset),
pressedAsset: _stringProp(map, RuntimeProtocolField.pressedAsset),
disabledAsset: _stringProp(map, RuntimeProtocolField.disabledAsset),
animation: _stringProp(map, RuntimeProtocolField.animation),
skin: _stringProp(map, RuntimeProtocolField.skin),
loop: _boolProp(map, RuntimeProtocolField.loop) ?? true,
text: _stringProp(map, RuntimeProtocolField.text),
x: _doubleProp(map, RuntimeProtocolField.x) ?? 0,
y: _doubleProp(map, RuntimeProtocolField.y) ?? 0,
width: _doubleProp(map, RuntimeProtocolField.width),
height: _doubleProp(map, RuntimeProtocolField.height),
paddingLeft:
_nonNegativeDoubleProp(map, RuntimeProtocolField.paddingLeft) ?? 0,
paddingTop:
_nonNegativeDoubleProp(map, RuntimeProtocolField.paddingTop) ?? 0,
paddingRight:
_nonNegativeDoubleProp(map, RuntimeProtocolField.paddingRight) ?? 0,
paddingBottom:
_nonNegativeDoubleProp(map, RuntimeProtocolField.paddingBottom) ?? 0,
anchor: anchor,
layer: _intProp(map, RuntimeProtocolField.layer) ?? 0,
visible: _boolProp(map, RuntimeProtocolField.visible) ?? true,
alpha: _doubleProp(map, RuntimeProtocolField.alpha) ?? 1,
scale: _doubleProp(map, RuntimeProtocolField.scale) ?? 1,
rotation: _doubleProp(map, RuntimeProtocolField.rotation) ?? 0,
color: _colorProp(map, RuntimeProtocolField.color),
fontSize: _doubleProp(map, RuntimeProtocolField.fontSize),
textAlign: textAlign,
radius: _doubleProp(map, RuntimeProtocolField.radius),
strokeWidth: _doubleProp(map, RuntimeProtocolField.strokeWidth),
value: _normalizedValueProp(map, RuntimeProtocolField.value),
scrollX:
_scrollProp(
map,
RuntimeProtocolField.scrollX,
contentExtent: _doubleProp(map, RuntimeProtocolField.contentWidth),
viewportExtent: _doubleProp(map, RuntimeProtocolField.width),
) ??
0,
scrollY:
_scrollProp(
map,
RuntimeProtocolField.scrollY,
contentExtent: _doubleProp(map, RuntimeProtocolField.contentHeight),
viewportExtent: _doubleProp(map, RuntimeProtocolField.height),
) ??
0,
contentWidth: _doubleProp(map, RuntimeProtocolField.contentWidth),
contentHeight: _doubleProp(map, RuntimeProtocolField.contentHeight),
virtualized: _boolProp(map, RuntimeProtocolField.virtualized) ?? false,
cacheExtent:
_nonNegativeDoubleProp(map, RuntimeProtocolField.cacheExtent) ?? 0,
inertia: _boolProp(map, RuntimeProtocolField.inertia) ?? true,
scrollbarThumbColor: _colorProp(
map,
RuntimeProtocolField.scrollbarThumbColor,
),
scrollbarTrackColor: _colorProp(
map,
RuntimeProtocolField.scrollbarTrackColor,
),
scrollbarThickness: _nonNegativeDoubleProp(
map,
RuntimeProtocolField.scrollbarThickness,
),
scrollbarVisible:
_boolProp(map, RuntimeProtocolField.scrollbarVisible) ?? true,
interactive: _boolProp(map, RuntimeProtocolField.interactive) ?? false,
onTap: _stringProp(map, RuntimeProtocolField.onTap),
onScroll: _stringProp(map, RuntimeProtocolField.onScroll),
preset: preset,
count: _positiveIntProp(map, RuntimeProtocolField.count),
duration: _nonNegativeDoubleProp(map, RuntimeProtocolField.duration),
speedMin: _nonNegativeDoubleProp(map, RuntimeProtocolField.speedMin),
speedMax: _nonNegativeDoubleProp(map, RuntimeProtocolField.speedMax),
gravityX: _doubleProp(map, RuntimeProtocolField.gravityX),
gravityY: _doubleProp(map, RuntimeProtocolField.gravityY),
spread: _nonNegativeDoubleProp(map, RuntimeProtocolField.spread),
colorTo: _colorProp(map, RuntimeProtocolField.colorTo),
radiusTo: _nonNegativeDoubleProp(map, RuntimeProtocolField.radiusTo),
autoRemove: _boolProp(map, RuntimeProtocolField.autoRemove) ?? true,
fadeOut: _boolProp(map, RuntimeProtocolField.fadeOut) ?? true,
);
}
static String _requiredString(Map<String, Object?> map, String key) {
final value = map[key];
if (value is String && value.isNotEmpty) {
return value;
}
throw FormatException('RuntimeNode.$key must be a non-empty string');
}
static void _validateParticlePreset(String? preset) {
if (preset != null && !RuntimeParticlePresetValue.isSupported(preset)) {
throw FormatException('RuntimeNode.preset is unsupported: $preset');
}
}
static String? _stringProp(Map<String, Object?> map, String key) {
final value = map[key];
if (value == null) {
return null;
}
if (value is String) {
return value;
}
throw FormatException('RuntimeNode.$key must be a string');
}
static String? _parentProp(
Map<String, Object?> map, {
required String? currentParent,
required String nodeId,
}) {
if (!map.containsKey(RuntimeProtocolField.parent)) {
return currentParent;
}
final value = _stringProp(map, RuntimeProtocolField.parent);
if (value == null || value.isEmpty) {
return null;
}
if (value == nodeId) {
throw const FormatException('RuntimeNode.parent cannot reference itself');
}
return value;
}
static bool? _boolProp(Map<String, Object?> map, String key) {
final value = map[key];
if (value == null) {
return null;
}
if (value is bool) {
return value;
}
throw FormatException('RuntimeNode.$key must be a boolean');
}
static double? _doubleProp(Map<String, Object?> map, String key) {
final value = map[key];
if (value == null) {
return null;
}
if (value is num) {
return value.toDouble();
}
throw FormatException('RuntimeNode.$key must be a number');
}
static double? _normalizedValueProp(Map<String, Object?> map, String key) {
final value = _doubleProp(map, key);
if (value == null) {
return null;
}
if (value < 0 || value > 1) {
throw FormatException('RuntimeNode.$key must be between 0 and 1');
}
return value;
}
static double? _nonNegativeDoubleProp(Map<String, Object?> map, String key) {
final value = _doubleProp(map, key);
if (value == null) {
return null;
}
if (value < 0) {
throw FormatException('RuntimeNode.$key must be >= 0');
}
return value;
}
static double? _scrollProp(
Map<String, Object?> map,
String key, {
required double? contentExtent,
required double? viewportExtent,
}) {
final value = _doubleProp(map, key);
if (value == null) {
return null;
}
if (value < 0) {
throw FormatException('RuntimeNode.$key must be >= 0');
}
return _clampScroll(
value,
contentExtent: contentExtent,
viewportExtent: viewportExtent,
);
}
static double _clampScroll(
double value, {
required double? contentExtent,
required double? viewportExtent,
}) {
final maxScroll = (contentExtent ?? 0) - (viewportExtent ?? 0);
if (maxScroll <= 0) {
return 0;
}
return value.clamp(0, maxScroll).toDouble();
}
static int? _positiveIntProp(Map<String, Object?> map, String key) {
final value = _intProp(map, key);
if (value == null) {
return null;
}
if (value <= 0) {
throw FormatException('RuntimeNode.$key must be > 0');
}
return value;
}
static int? _intProp(Map<String, Object?> map, String key) {
final value = map[key];
if (value == null) {
return null;
}
if (value is num) {
return value.toInt();
}
throw FormatException('RuntimeNode.$key must be an integer');
}
static Color? _colorProp(Map<String, Object?> map, String key) {
final value = map[key];
if (value == null) {
return null;
}
if (value is! String || !value.startsWith('#')) {
throw FormatException('RuntimeNode.$key must be a hex color');
}
final hex = value.substring(1);
if (hex.length == 6) {
return Color(int.parse('ff$hex', radix: 16));
}
if (hex.length == 8) {
return Color(int.parse(hex, radix: 16));
}
throw FormatException('RuntimeNode.$key must be #RRGGBB or #AARRGGBB');
}
}