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.frame, this.pressedFrame, this.disabledFrame, this.sourceX, this.sourceY, this.sourceWidth, this.sourceHeight, this.sliceLeft, this.sliceTop, this.sliceRight, this.sliceBottom, 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.textShadowColor, this.textShadowOffsetX, this.textShadowOffsetY, this.textShadowBlur, 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? frame; final String? pressedFrame; final String? disabledFrame; final double? sourceX; final double? sourceY; final double? sourceWidth; final double? sourceHeight; final double? sliceLeft; final double? sliceTop; final double? sliceRight; final double? sliceBottom; 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 Color? textShadowColor; final double? textShadowOffsetX; final double? textShadowOffsetY; final double? textShadowBlur; 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 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, frame: _stringProp(props, RuntimeProtocolField.frame) ?? frame, pressedFrame: _stringProp(props, RuntimeProtocolField.pressedFrame) ?? pressedFrame, disabledFrame: _stringProp(props, RuntimeProtocolField.disabledFrame) ?? disabledFrame, sourceX: _nonNegativeDoubleProp(props, RuntimeProtocolField.sourceX) ?? sourceX, sourceY: _nonNegativeDoubleProp(props, RuntimeProtocolField.sourceY) ?? sourceY, sourceWidth: _positiveDoubleProp(props, RuntimeProtocolField.sourceWidth) ?? sourceWidth, sourceHeight: _positiveDoubleProp(props, RuntimeProtocolField.sourceHeight) ?? sourceHeight, sliceLeft: _nonNegativeDoubleProp(props, RuntimeProtocolField.sliceLeft) ?? sliceLeft, sliceTop: _nonNegativeDoubleProp(props, RuntimeProtocolField.sliceTop) ?? sliceTop, sliceRight: _nonNegativeDoubleProp(props, RuntimeProtocolField.sliceRight) ?? sliceRight, sliceBottom: _nonNegativeDoubleProp(props, RuntimeProtocolField.sliceBottom) ?? sliceBottom, 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, textShadowColor: _colorProp(props, RuntimeProtocolField.textShadowColor) ?? textShadowColor, textShadowOffsetX: _doubleProp(props, RuntimeProtocolField.textShadowOffsetX) ?? textShadowOffsetX, textShadowOffsetY: _doubleProp(props, RuntimeProtocolField.textShadowOffsetY) ?? textShadowOffsetY, textShadowBlur: _nonNegativeDoubleProp(props, RuntimeProtocolField.textShadowBlur) ?? textShadowBlur, 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 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), frame: _stringProp(map, RuntimeProtocolField.frame), pressedFrame: _stringProp(map, RuntimeProtocolField.pressedFrame), disabledFrame: _stringProp(map, RuntimeProtocolField.disabledFrame), sourceX: _nonNegativeDoubleProp(map, RuntimeProtocolField.sourceX), sourceY: _nonNegativeDoubleProp(map, RuntimeProtocolField.sourceY), sourceWidth: _positiveDoubleProp(map, RuntimeProtocolField.sourceWidth), sourceHeight: _positiveDoubleProp(map, RuntimeProtocolField.sourceHeight), sliceLeft: _nonNegativeDoubleProp(map, RuntimeProtocolField.sliceLeft), sliceTop: _nonNegativeDoubleProp(map, RuntimeProtocolField.sliceTop), sliceRight: _nonNegativeDoubleProp(map, RuntimeProtocolField.sliceRight), sliceBottom: _nonNegativeDoubleProp( map, RuntimeProtocolField.sliceBottom, ), 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, textShadowColor: _colorProp(map, RuntimeProtocolField.textShadowColor), textShadowOffsetX: _doubleProp( map, RuntimeProtocolField.textShadowOffsetX, ), textShadowOffsetY: _doubleProp( map, RuntimeProtocolField.textShadowOffsetY, ), textShadowBlur: _nonNegativeDoubleProp( map, RuntimeProtocolField.textShadowBlur, ), 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 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 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 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 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 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 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 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? _positiveDoubleProp(Map 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 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 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 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 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'); } }