Add image atlas and nine-slice support

This commit is contained in:
gem
2026-06-09 12:30:44 +08:00
parent 409942b4af
commit e2a584d4dc
12 changed files with 453 additions and 24 deletions

View File

@@ -2,6 +2,7 @@
## Unreleased
- Added atlas source-region and nine-slice image rendering fields for image-capable nodes.
- Added Runtime text shadow fields for text-capable nodes.
- Fixed Runtime node color alpha composition so `#AARRGGBB` alpha now multiplies with node/runtime alpha instead of being overwritten.

View File

@@ -54,6 +54,21 @@ Text-capable nodes may use flat shadow fields:
The shadow color alpha is multiplied by `RuntimeNode.alpha` and any runtime animation alpha.
### Image regions and nine-slice
Image-capable nodes (`image`, `sprite`, and image-backed `button`) may draw only part of an asset by setting source region fields in image pixels:
- `sourceX` / `sourceY`: top-left source position inside the loaded image.
- `sourceWidth` / `sourceHeight`: source region size.
When source fields are omitted, the full image is used. This supports atlas-style usage where multiple sprites are packed into one image and nodes reference different source rectangles.
Image-capable nodes may also use nine-slice scaling with source-pixel insets:
- `sliceLeft` / `sliceTop` / `sliceRight` / `sliceBottom`.
Nine-slice keeps corners unscaled, stretches edges on one axis, and stretches the center on both axes. Insets are clamped to the selected source region and destination size.
## RuntimeCommand
Runtime commands request generic side effects owned by Dart/Flame.

View File

@@ -86,6 +86,14 @@
---@field type RuntimeNodeType
---@field parent? string
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
---@field sourceX? number Source atlas region x in image pixels.
---@field sourceY? number Source atlas region y in image pixels.
---@field sourceWidth? number Source atlas region width in image pixels.
---@field sourceHeight? number Source atlas region height in image pixels.
---@field sliceLeft? number Left nine-slice inset in source pixels.
---@field sliceTop? number Top nine-slice inset in source pixels.
---@field sliceRight? number Right nine-slice inset in source pixels.
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
---@field pressedAsset? string Button pressed-state image asset key.
---@field disabledAsset? string Button disabled-state image asset key.
---@field animation? string
@@ -147,6 +155,14 @@
---@field type? RuntimeNodeType
---@field parent? string
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
---@field sourceX? number Source atlas region x in image pixels.
---@field sourceY? number Source atlas region y in image pixels.
---@field sourceWidth? number Source atlas region width in image pixels.
---@field sourceHeight? number Source atlas region height in image pixels.
---@field sliceLeft? number Left nine-slice inset in source pixels.
---@field sliceTop? number Top nine-slice inset in source pixels.
---@field sliceRight? number Right nine-slice inset in source pixels.
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
---@field pressedAsset? string Button pressed-state image asset key.
---@field disabledAsset? string Button disabled-state image asset key.
---@field animation? string

View File

@@ -86,6 +86,14 @@
---@field type RuntimeNodeType
---@field parent? string
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
---@field sourceX? number Source atlas region x in image pixels.
---@field sourceY? number Source atlas region y in image pixels.
---@field sourceWidth? number Source atlas region width in image pixels.
---@field sourceHeight? number Source atlas region height in image pixels.
---@field sliceLeft? number Left nine-slice inset in source pixels.
---@field sliceTop? number Top nine-slice inset in source pixels.
---@field sliceRight? number Right nine-slice inset in source pixels.
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
---@field pressedAsset? string Button pressed-state image asset key.
---@field disabledAsset? string Button disabled-state image asset key.
---@field animation? string
@@ -147,6 +155,14 @@
---@field type? RuntimeNodeType
---@field parent? string
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
---@field sourceX? number Source atlas region x in image pixels.
---@field sourceY? number Source atlas region y in image pixels.
---@field sourceWidth? number Source atlas region width in image pixels.
---@field sourceHeight? number Source atlas region height in image pixels.
---@field sliceLeft? number Left nine-slice inset in source pixels.
---@field sliceTop? number Top nine-slice inset in source pixels.
---@field sliceRight? number Right nine-slice inset in source pixels.
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
---@field pressedAsset? string Button pressed-state image asset key.
---@field disabledAsset? string Button disabled-state image asset key.
---@field animation? string

View File

@@ -86,6 +86,14 @@
---@field type RuntimeNodeType
---@field parent? string
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
---@field sourceX? number Source atlas region x in image pixels.
---@field sourceY? number Source atlas region y in image pixels.
---@field sourceWidth? number Source atlas region width in image pixels.
---@field sourceHeight? number Source atlas region height in image pixels.
---@field sliceLeft? number Left nine-slice inset in source pixels.
---@field sliceTop? number Top nine-slice inset in source pixels.
---@field sliceRight? number Right nine-slice inset in source pixels.
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
---@field pressedAsset? string Button pressed-state image asset key.
---@field disabledAsset? string Button disabled-state image asset key.
---@field animation? string
@@ -147,6 +155,14 @@
---@field type? RuntimeNodeType
---@field parent? string
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
---@field sourceX? number Source atlas region x in image pixels.
---@field sourceY? number Source atlas region y in image pixels.
---@field sourceWidth? number Source atlas region width in image pixels.
---@field sourceHeight? number Source atlas region height in image pixels.
---@field sliceLeft? number Left nine-slice inset in source pixels.
---@field sliceTop? number Top nine-slice inset in source pixels.
---@field sliceRight? number Right nine-slice inset in source pixels.
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
---@field pressedAsset? string Button pressed-state image asset key.
---@field disabledAsset? string Button disabled-state image asset key.
---@field animation? string

View File

@@ -86,6 +86,14 @@
---@field type RuntimeNodeType
---@field parent? string
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
---@field sourceX? number Source atlas region x in image pixels.
---@field sourceY? number Source atlas region y in image pixels.
---@field sourceWidth? number Source atlas region width in image pixels.
---@field sourceHeight? number Source atlas region height in image pixels.
---@field sliceLeft? number Left nine-slice inset in source pixels.
---@field sliceTop? number Top nine-slice inset in source pixels.
---@field sliceRight? number Right nine-slice inset in source pixels.
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
---@field pressedAsset? string Button pressed-state image asset key.
---@field disabledAsset? string Button disabled-state image asset key.
---@field animation? string
@@ -147,6 +155,14 @@
---@field type? RuntimeNodeType
---@field parent? string
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
---@field sourceX? number Source atlas region x in image pixels.
---@field sourceY? number Source atlas region y in image pixels.
---@field sourceWidth? number Source atlas region width in image pixels.
---@field sourceHeight? number Source atlas region height in image pixels.
---@field sliceLeft? number Left nine-slice inset in source pixels.
---@field sliceTop? number Top nine-slice inset in source pixels.
---@field sliceRight? number Right nine-slice inset in source pixels.
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
---@field pressedAsset? string Button pressed-state image asset key.
---@field disabledAsset? string Button disabled-state image asset key.
---@field animation? string

View File

@@ -8,6 +8,14 @@ class RuntimeNode {
required this.type,
this.parent,
this.asset,
this.sourceX,
this.sourceY,
this.sourceWidth,
this.sourceHeight,
this.sliceLeft,
this.sliceTop,
this.sliceRight,
this.sliceBottom,
this.pressedAsset,
this.disabledAsset,
this.animation,
@@ -70,6 +78,14 @@ class RuntimeNode {
final String type;
final String? parent;
final String? asset;
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;
@@ -213,6 +229,26 @@ class RuntimeNode {
type: nextType,
parent: _parentProp(props, currentParent: parent, nodeId: id),
asset: _stringProp(props, RuntimeProtocolField.asset) ?? asset,
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:
@@ -345,6 +381,20 @@ class RuntimeNode {
nodeId: _requiredString(map, RuntimeProtocolField.id),
),
asset: _stringProp(map, RuntimeProtocolField.asset),
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),
@@ -530,6 +580,17 @@ class RuntimeNode {
return value;
}
static double? _positiveDoubleProp(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, {

View File

@@ -138,6 +138,14 @@ class RuntimeProtocolField {
static const target = 'target';
static const parent = 'parent';
static const asset = 'asset';
static const sourceX = 'sourceX';
static const sourceY = 'sourceY';
static const sourceWidth = 'sourceWidth';
static const sourceHeight = 'sourceHeight';
static const sliceLeft = 'sliceLeft';
static const sliceTop = 'sliceTop';
static const sliceRight = 'sliceRight';
static const sliceBottom = 'sliceBottom';
static const pressedAsset = 'pressedAsset';
static const disabledAsset = 'disabledAsset';
static const animation = 'animation';
@@ -225,6 +233,14 @@ class RuntimeProtocolSchema {
RuntimeProtocolField.type,
RuntimeProtocolField.parent,
RuntimeProtocolField.asset,
RuntimeProtocolField.sourceX,
RuntimeProtocolField.sourceY,
RuntimeProtocolField.sourceWidth,
RuntimeProtocolField.sourceHeight,
RuntimeProtocolField.sliceLeft,
RuntimeProtocolField.sliceTop,
RuntimeProtocolField.sliceRight,
RuntimeProtocolField.sliceBottom,
RuntimeProtocolField.pressedAsset,
RuntimeProtocolField.disabledAsset,
RuntimeProtocolField.animation,
@@ -294,6 +310,14 @@ class RuntimeProtocolSchema {
RuntimeProtocolField.type,
RuntimeProtocolField.parent,
RuntimeProtocolField.asset,
RuntimeProtocolField.sourceX,
RuntimeProtocolField.sourceY,
RuntimeProtocolField.sourceWidth,
RuntimeProtocolField.sourceHeight,
RuntimeProtocolField.sliceLeft,
RuntimeProtocolField.sliceTop,
RuntimeProtocolField.sliceRight,
RuntimeProtocolField.sliceBottom,
RuntimeProtocolField.pressedAsset,
RuntimeProtocolField.disabledAsset,
RuntimeProtocolField.animation,

View File

@@ -17,6 +17,102 @@ Color composeRuntimeColorAlpha(Color color, double alpha) {
return color.withValues(alpha: color.a * alpha.clamp(0.0, 1.0));
}
@visibleForTesting
Rect runtimeImageSourceRect({
required double imageWidth,
required double imageHeight,
double? sourceX,
double? sourceY,
double? sourceWidth,
double? sourceHeight,
}) {
final x = (sourceX ?? 0).clamp(0.0, imageWidth).toDouble();
final y = (sourceY ?? 0).clamp(0.0, imageHeight).toDouble();
final maxWidth = imageWidth - x;
final maxHeight = imageHeight - y;
final width = (sourceWidth ?? maxWidth).clamp(0.0, maxWidth).toDouble();
final height = (sourceHeight ?? maxHeight).clamp(0.0, maxHeight).toDouble();
return Rect.fromLTWH(x, y, width, height);
}
@visibleForTesting
List<({Rect source, Rect destination})> runtimeNineSliceRects({
required Rect source,
required Rect destination,
double sliceLeft = 0,
double sliceTop = 0,
double sliceRight = 0,
double sliceBottom = 0,
}) {
if (source.width <= 0 ||
source.height <= 0 ||
destination.width <= 0 ||
destination.height <= 0) {
return const [];
}
final left = sliceLeft.clamp(0.0, source.width).toDouble();
final top = sliceTop.clamp(0.0, source.height).toDouble();
final right = sliceRight.clamp(0.0, source.width - left).toDouble();
final bottom = sliceBottom.clamp(0.0, source.height - top).toDouble();
final destLeft = left.clamp(0.0, destination.width).toDouble();
final destTop = top.clamp(0.0, destination.height).toDouble();
final destRight = right.clamp(0.0, destination.width - destLeft).toDouble();
final destBottom = bottom
.clamp(0.0, destination.height - destTop)
.toDouble();
final sourceXs = [
source.left,
source.left + left,
source.right - right,
source.right,
];
final sourceYs = [
source.top,
source.top + top,
source.bottom - bottom,
source.bottom,
];
final destXs = [
destination.left,
destination.left + destLeft,
destination.right - destRight,
destination.right,
];
final destYs = [
destination.top,
destination.top + destTop,
destination.bottom - destBottom,
destination.bottom,
];
final parts = <({Rect source, Rect destination})>[];
for (var y = 0; y < 3; y++) {
for (var x = 0; x < 3; x++) {
final sourcePart = Rect.fromLTRB(
sourceXs[x],
sourceYs[y],
sourceXs[x + 1],
sourceYs[y + 1],
);
final destPart = Rect.fromLTRB(
destXs[x],
destYs[y],
destXs[x + 1],
destYs[y + 1],
);
if (sourcePart.width <= 0 ||
sourcePart.height <= 0 ||
destPart.width <= 0 ||
destPart.height <= 0) {
continue;
}
parts.add((source: sourcePart, destination: destPart));
}
}
return parts;
}
class RuntimeComponent extends PositionComponent
with HasVisibility, TapCallbacks {
RuntimeComponent({
@@ -431,12 +527,14 @@ class RuntimeComponent extends PositionComponent
(_node.type == RuntimeNodeType.sprite ||
_node.type == RuntimeNodeType.image ||
_node.type == RuntimeNodeType.button)) {
canvas.drawImageRect(
image,
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
rect,
Paint()..color = composeRuntimeColorAlpha(Colors.white, renderAlpha),
);
final imagePaint = Paint()
..color = composeRuntimeColorAlpha(Colors.white, renderAlpha);
final source = _imageSourceRect(image);
if (_usesNineSlice(source, rect)) {
_drawNineSliceImage(canvas, image, source, rect, imagePaint);
} else {
canvas.drawImageRect(image, source, rect, imagePaint);
}
return;
}
@@ -448,6 +546,50 @@ class RuntimeComponent extends PositionComponent
);
}
Rect _imageSourceRect(ui.Image image) {
return runtimeImageSourceRect(
imageWidth: image.width.toDouble(),
imageHeight: image.height.toDouble(),
sourceX: _node.sourceX,
sourceY: _node.sourceY,
sourceWidth: _node.sourceWidth,
sourceHeight: _node.sourceHeight,
);
}
bool _usesNineSlice(Rect source, Rect destination) {
if (source.width <= 0 ||
source.height <= 0 ||
destination.width <= 0 ||
destination.height <= 0) {
return false;
}
return (_node.sliceLeft ?? 0) > 0 ||
(_node.sliceTop ?? 0) > 0 ||
(_node.sliceRight ?? 0) > 0 ||
(_node.sliceBottom ?? 0) > 0;
}
void _drawNineSliceImage(
Canvas canvas,
ui.Image image,
Rect source,
Rect destination,
Paint paint,
) {
final parts = runtimeNineSliceRects(
source: source,
destination: destination,
sliceLeft: _node.sliceLeft ?? 0,
sliceTop: _node.sliceTop ?? 0,
sliceRight: _node.sliceRight ?? 0,
sliceBottom: _node.sliceBottom ?? 0,
);
for (final part in parts) {
canvas.drawImageRect(image, part.source, part.destination, paint);
}
}
void _applyBase(RuntimeNode node) {
_syncVisibility();
size = Vector2(node.width ?? 40, node.height ?? 40);

View File

@@ -10,6 +10,14 @@ void main() {
'type': 'button',
'parent': 'top_bar',
'asset': 'dice_normal',
'sourceX': 4,
'sourceY': 5,
'sourceWidth': 64,
'sourceHeight': 32,
'sliceLeft': 6,
'sliceTop': 7,
'sliceRight': 8,
'sliceBottom': 9,
'pressedAsset': 'dice_pressed',
'disabledAsset': 'dice_disabled',
'animation': 'idle',
@@ -72,6 +80,14 @@ void main() {
expect(node.type, 'button');
expect(node.parent, 'top_bar');
expect(node.asset, 'dice_normal');
expect(node.sourceX, 4);
expect(node.sourceY, 5);
expect(node.sourceWidth, 64);
expect(node.sourceHeight, 32);
expect(node.sliceLeft, 6);
expect(node.sliceTop, 7);
expect(node.sliceRight, 8);
expect(node.sliceBottom, 9);
expect(node.pressedAsset, 'dice_pressed');
expect(node.disabledAsset, 'dice_disabled');
expect(node.animation, 'idle');
@@ -147,6 +163,14 @@ void main() {
expect(node.textShadowOffsetX, isNull);
expect(node.textShadowOffsetY, isNull);
expect(node.textShadowBlur, isNull);
expect(node.sourceX, isNull);
expect(node.sourceY, isNull);
expect(node.sourceWidth, isNull);
expect(node.sourceHeight, isNull);
expect(node.sliceLeft, isNull);
expect(node.sliceTop, isNull);
expect(node.sliceRight, isNull);
expect(node.sliceBottom, isNull);
expect(node.scrollbarVisible, isTrue);
expect(node.paddingLeft, 0);
expect(node.paddingTop, 0);
@@ -182,6 +206,14 @@ void main() {
'paddingBottom': 11,
'contentWidth': 120,
'contentHeight': 100,
'sourceX': 3,
'sourceY': 4,
'sourceWidth': 40,
'sourceHeight': 41,
'sliceLeft': 5,
'sliceTop': 6,
'sliceRight': 7,
'sliceBottom': 8,
'pressedAsset': 'button_pressed',
'disabledAsset': 'button_disabled',
'scrollX': 90,
@@ -213,6 +245,14 @@ void main() {
expect(updated.paddingBottom, 11);
expect(updated.contentWidth, 120);
expect(updated.contentHeight, 100);
expect(updated.sourceX, 3);
expect(updated.sourceY, 4);
expect(updated.sourceWidth, 40);
expect(updated.sourceHeight, 41);
expect(updated.sliceLeft, 5);
expect(updated.sliceTop, 6);
expect(updated.sliceRight, 7);
expect(updated.sliceBottom, 8);
expect(updated.pressedAsset, 'button_pressed');
expect(updated.disabledAsset, 'button_disabled');
expect(updated.scrollX, 68);
@@ -255,6 +295,22 @@ void main() {
() => RuntimeNode.fromMap({'id': 'a', 'type': 'progress', 'value': 2}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({
'id': 'a',
'type': 'image',
'sourceWidth': 0,
}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({
'id': 'a',
'type': 'image',
'sliceLeft': -1,
}),
throwsFormatException,
);
expect(
() => RuntimeNode.fromMap({
'id': 'a',

View File

@@ -4,6 +4,7 @@ import 'package:flame_lua_runtime/runtime/protocol/runtime_protocol.dart';
import 'package:flame_lua_runtime/runtime/rendering/runtime_component.dart';
import 'package:flame_lua_runtime/runtime/resources/game_resource_manager.dart';
import 'package:flutter/material.dart' show Color;
import 'package:flutter/rendering.dart' show Rect;
import 'package:flutter_test/flutter_test.dart';
void main() {
@@ -138,6 +139,50 @@ void main() {
);
});
test('computes atlas source region and clamps to image bounds', () {
expect(
runtimeImageSourceRect(
imageWidth: 100,
imageHeight: 80,
sourceX: 10,
sourceY: 12,
sourceWidth: 30,
sourceHeight: 20,
),
const Rect.fromLTWH(10, 12, 30, 20),
);
expect(
runtimeImageSourceRect(
imageWidth: 100,
imageHeight: 80,
sourceX: 90,
sourceY: 70,
sourceWidth: 30,
sourceHeight: 20,
),
const Rect.fromLTWH(90, 70, 10, 10),
);
});
test('computes nine-slice source and destination rects', () {
final parts = runtimeNineSliceRects(
source: const Rect.fromLTWH(10, 20, 30, 40),
destination: const Rect.fromLTWH(0, 0, 90, 120),
sliceLeft: 5,
sliceTop: 6,
sliceRight: 7,
sliceBottom: 8,
);
expect(parts, hasLength(9));
expect(parts.first.source, const Rect.fromLTRB(10, 20, 15, 26));
expect(parts.first.destination, const Rect.fromLTRB(0, 0, 5, 6));
expect(parts[4].source, const Rect.fromLTRB(15, 26, 33, 52));
expect(parts[4].destination, const Rect.fromLTRB(5, 6, 83, 112));
expect(parts.last.source, const Rect.fromLTRB(33, 52, 40, 60));
expect(parts.last.destination, const Rect.fromLTRB(83, 112, 90, 120));
});
test('updates text alpha style without rebuilding text component', () {
final component = RuntimeComponent(
node: const RuntimeNode(
@@ -161,7 +206,9 @@ void main() {
expect(((updatedText.textRenderer as TextPaint).style.color!).a, 1);
});
test('updates button text alpha style without rebuilding text component', () {
test(
'updates button text alpha style without rebuilding text component',
() {
final component = RuntimeComponent(
node: const RuntimeNode(
id: 'button',
@@ -178,10 +225,13 @@ void main() {
component.setRuntimeAlpha(1);
final updatedText = component.children.whereType<TextComponent>().single;
final updatedText = component.children
.whereType<TextComponent>()
.single;
expect(identical(updatedText, text), isTrue);
expect(((updatedText.textRenderer as TextPaint).style.color!).a, 1);
});
},
);
test('applies text shadow style', () {
final component = RuntimeComponent(

View File

@@ -86,6 +86,14 @@
---@field type RuntimeNodeType
---@field parent? string
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
---@field sourceX? number Source atlas region x in image pixels.
---@field sourceY? number Source atlas region y in image pixels.
---@field sourceWidth? number Source atlas region width in image pixels.
---@field sourceHeight? number Source atlas region height in image pixels.
---@field sliceLeft? number Left nine-slice inset in source pixels.
---@field sliceTop? number Top nine-slice inset in source pixels.
---@field sliceRight? number Right nine-slice inset in source pixels.
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
---@field pressedAsset? string Button pressed-state image asset key.
---@field disabledAsset? string Button disabled-state image asset key.
---@field animation? string
@@ -147,6 +155,14 @@
---@field type? RuntimeNodeType
---@field parent? string
---@field asset? string Normal image/sprite/spine asset key. For button nodes this is the normal-state image.
---@field sourceX? number Source atlas region x in image pixels.
---@field sourceY? number Source atlas region y in image pixels.
---@field sourceWidth? number Source atlas region width in image pixels.
---@field sourceHeight? number Source atlas region height in image pixels.
---@field sliceLeft? number Left nine-slice inset in source pixels.
---@field sliceTop? number Top nine-slice inset in source pixels.
---@field sliceRight? number Right nine-slice inset in source pixels.
---@field sliceBottom? number Bottom nine-slice inset in source pixels.
---@field pressedAsset? string Button pressed-state image asset key.
---@field disabledAsset? string Button disabled-state image asset key.
---@field animation? string