Initial flame_lua_runtime package

This commit is contained in:
gem
2026-06-07 22:53:58 +08:00
commit 733b2fb798
262 changed files with 28439 additions and 0 deletions

View File

@@ -0,0 +1,173 @@
import 'runtime_command.dart';
import 'runtime_node.dart';
import '../protocol/runtime_protocol.dart';
class NodeUpdate {
const NodeUpdate({required this.id, required this.props});
final String id;
final Map<String, Object?> props;
static NodeUpdate fromMap(Map<String, Object?> map) {
RuntimeProtocolSchema.ensureKnownKeys(
map,
allowed: RuntimeProtocolSchema.nodeUpdateFields,
context: 'NodeUpdate',
);
final id = map[RuntimeProtocolField.id];
if (id is! String || id.isEmpty) {
throw const FormatException('NodeUpdate.id must be a string');
}
final props = map[RuntimeProtocolField.props];
if (props is! Map) {
throw const FormatException('NodeUpdate.props must be a map');
}
final typedProps = Map<String, Object?>.from(props);
RuntimeProtocolSchema.ensureKnownKeys(
typedProps,
allowed: RuntimeProtocolSchema.nodePropsFields,
context: 'RuntimeNode.props',
);
return NodeUpdate(id: id, props: typedProps);
}
}
class NodeRemove {
const NodeRemove({required this.id});
final String id;
static NodeRemove fromValue(Object? value) {
if (value is String && value.isNotEmpty) {
return NodeRemove(id: value);
}
if (value is Map) {
RuntimeProtocolSchema.ensureKnownKeys(
value,
allowed: RuntimeProtocolSchema.nodeRemoveFields,
context: 'NodeRemove',
);
final id = value[RuntimeProtocolField.id];
if (id is String && id.isNotEmpty) {
return NodeRemove(id: id);
}
}
throw const FormatException('NodeRemove must be an id string or {id}');
}
}
class NodeDiff {
const NodeDiff({
this.creates = const [],
this.updates = const [],
this.removes = const [],
});
final List<RuntimeNode> creates;
final List<NodeUpdate> updates;
final List<NodeRemove> removes;
static NodeDiff empty = const NodeDiff();
static NodeDiff fromMap(Object? value) {
if (value == null) {
return NodeDiff.empty;
}
if (value is! Map) {
throw const FormatException('NodeDiff must be a map');
}
RuntimeProtocolSchema.ensureKnownKeys(
value,
allowed: RuntimeProtocolSchema.nodeDiffFields,
context: 'NodeDiff',
);
return NodeDiff(
creates: _readList(
value[RuntimeProtocolField.creates],
(item) => RuntimeNode.fromMap(Map<String, Object?>.from(item as Map)),
),
updates: _readList(
value[RuntimeProtocolField.updates],
(item) => NodeUpdate.fromMap(Map<String, Object?>.from(item as Map)),
),
removes: _readList(
value[RuntimeProtocolField.removes],
NodeRemove.fromValue,
),
);
}
static List<T> _readList<T>(Object? value, T Function(Object? value) mapper) {
if (value == null) {
return const [];
}
if (value is List) {
return value.map(mapper).toList(growable: false);
}
if (value is Map && value.isEmpty) {
return const [];
}
if (value is Map && value.keys.every(_isPositiveIntegerKey)) {
final entries = value.entries.toList()
..sort(
(a, b) => int.parse(
a.key.toString(),
).compareTo(int.parse(b.key.toString())),
);
return entries
.map((entry) => mapper(entry.value))
.toList(growable: false);
}
throw const FormatException('Diff field must be a list');
}
static bool _isPositiveIntegerKey(Object? key) {
final value = int.tryParse(key.toString());
return value != null && value > 0;
}
}
class GameDiff {
const GameDiff({
required this.render,
required this.ui,
required this.commands,
});
final NodeDiff render;
final NodeDiff ui;
final List<RuntimeCommand> commands;
static const empty = GameDiff(
render: NodeDiff(),
ui: NodeDiff(),
commands: [],
);
static GameDiff fromMap(Map<String, Object?> map) {
RuntimeProtocolSchema.ensureKnownKeys(
map,
allowed: RuntimeProtocolSchema.gameDiffFields,
context: 'GameDiff',
);
final commandsValue = map[RuntimeProtocolField.commands];
final commands = commandsValue == null
? const <RuntimeCommand>[]
: NodeDiff._readList(
commandsValue,
(item) =>
RuntimeCommand.fromMap(Map<String, Object?>.from(item as Map)),
);
return GameDiff(
render: NodeDiff.fromMap(map[RuntimeProtocolField.render]),
ui: NodeDiff.fromMap(map[RuntimeProtocolField.ui]),
commands: commands,
);
}
}

View File

@@ -0,0 +1,43 @@
import '../protocol/runtime_protocol.dart';
class RuntimeCommand {
const RuntimeCommand({
required this.type,
this.target,
this.payload = const {},
});
final String type;
final String? target;
final Map<String, Object?> payload;
static RuntimeCommand fromMap(Map<String, Object?> map) {
final type = map[RuntimeProtocolField.type];
if (type is! String || type.isEmpty) {
throw const FormatException('RuntimeCommand.type must be a string');
}
if (!RuntimeCommandType.isSupported(type)) {
throw FormatException('RuntimeCommand.type is unsupported: $type');
}
RuntimeProtocolSchema.ensureKnownKeys(
map,
allowed: RuntimeProtocolSchema.allowedCommandFields(type),
context: 'RuntimeCommand.$type',
);
final targetValue = map[RuntimeProtocolField.target];
if (targetValue != null && targetValue is! String) {
throw const FormatException('RuntimeCommand.target must be a string');
}
final payload = Map<String, Object?>.from(map)
..remove(RuntimeProtocolField.type)
..remove(RuntimeProtocolField.target);
return RuntimeCommand(
type: type,
target: targetValue as String?,
payload: payload,
);
}
}

View File

@@ -0,0 +1,64 @@
class RuntimeEvent {
const RuntimeEvent({
required this.type,
this.target,
this.handler,
this.x,
this.y,
this.data = const {},
this.sessionId,
this.scope,
this.targetEpoch,
this.scopeEpoch,
});
final String type;
final String? target;
final String? handler;
final double? x;
final double? y;
final Map<String, Object?> data;
/// Runtime-internal lifecycle session. Not exposed to Lua.
final int? sessionId;
/// Runtime-internal lifecycle scope. Not exposed to Lua.
final String? scope;
/// Runtime-internal target node epoch. Not exposed to Lua.
final int? targetEpoch;
/// Runtime-internal scope node epoch. Not exposed to Lua.
final int? scopeEpoch;
RuntimeEvent withLifecycle({
int? sessionId,
String? scope,
int? targetEpoch,
int? scopeEpoch,
}) {
return RuntimeEvent(
type: type,
target: target,
handler: handler,
x: x,
y: y,
data: data,
sessionId: sessionId ?? this.sessionId,
scope: scope ?? this.scope,
targetEpoch: targetEpoch ?? this.targetEpoch,
scopeEpoch: scopeEpoch ?? this.scopeEpoch,
);
}
Map<String, Object?> toMap() {
return {
'type': type,
if (target != null) 'target': target,
if (handler != null) 'handler': handler,
if (x != null) 'x': x,
if (y != null) 'y': y,
if (data.isNotEmpty) 'data': data,
};
}
}

View File

@@ -0,0 +1,572 @@
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');
}
}