From 8d51db42a50b8c9d5a05c0bacaf6a5db8e298a6f Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 8 Sep 2026 20:22:10 +0200 Subject: [PATCH] fix: Apply opacity to text shadows and keep the style color of TextComponent --- doc/flame/rendering/text_rendering.md | 7 + .../lib/stories/rendering/text_example.dart | 7 + .../src/components/text_box_component.dart | 17 +- .../lib/src/components/text_component.dart | 24 ++- .../text/renderers/sprite_font_renderer.dart | 16 +- .../lib/src/text/renderers/text_paint.dart | 47 ++++- .../lib/src/text/renderers/text_renderer.dart | 9 + .../components/text_box_component_test.dart | 20 ++ .../test/components/text_component_test.dart | 147 ++++++++++++++ .../test/text/sprite_font_renderer_test.dart | 17 ++ packages/flame/test/text/text_paint_test.dart | 185 ++++++++++++++++++ .../lib/src/debug_text_renderer.dart | 2 +- .../test/debug_text_formatter_test.dart | 23 +++ 13 files changed, 512 insertions(+), 9 deletions(-) diff --git a/doc/flame/rendering/text_rendering.md b/doc/flame/rendering/text_rendering.md index d082e96c8a1..67e281c2d9d 100644 --- a/doc/flame/rendering/text_rendering.md +++ b/doc/flame/rendering/text_rendering.md @@ -67,6 +67,13 @@ class MyGame extends FlameGame { } ``` +`TextComponent` has the `HasPaint` mixin, so effects such as `OpacityEffect` and `ColorEffect` +work on it. The component's `paint` is applied on top of the `TextRenderer`: the opacity of the +paint scales the opacity of the text and of the shadows defined in the style, and a color filter +set on the paint is applied to the glyphs. The renderer you set is never modified, `textRenderer` +always returns it. `TextBoxComponent` applies the `paint` to its whole box instead, including the +background. + You can find all the options under [TextComponent's API](https://pub.dev/documentation/flame/latest/components/TextComponent-class.html). diff --git a/examples/lib/stories/rendering/text_example.dart b/examples/lib/stories/rendering/text_example.dart index 074a26e19ac..9ef51d1be0f 100644 --- a/examples/lib/stories/rendering/text_example.dart +++ b/examples/lib/stories/rendering/text_example.dart @@ -85,6 +85,13 @@ class TextExample extends FlameGame { ), TextComponent( text: 'I fade in and fade out', + textRenderer: TextPaint( + style: const TextStyle( + color: Color(0xFF2E9940), + fontSize: 24, + shadows: [Shadow(color: Color(0x99FFFFFF), blurRadius: 4)], + ), + ), anchor: Anchor.topRight, position: Vector2(size.x - 50, 20), children: [ diff --git a/packages/flame/lib/src/components/text_box_component.dart b/packages/flame/lib/src/components/text_box_component.dart index 519f0003083..a9aed50e7b0 100644 --- a/packages/flame/lib/src/components/text_box_component.dart +++ b/packages/flame/lib/src/components/text_box_component.dart @@ -6,7 +6,6 @@ import 'dart:ui'; import 'package:collection/collection.dart'; import 'package:flame/components.dart'; import 'package:flame/extensions.dart'; -import 'package:flame/palette.dart'; import 'package:flame/text.dart'; import 'package:flutter/widgets.dart' hide Image; import 'package:meta/meta.dart'; @@ -63,8 +62,6 @@ class TextBoxConfig { } class TextBoxComponent extends TextComponent { - static final Paint _imagePaint = BasicPalette.white.paint() - ..filterQuality = FilterQuality.medium; TextBoxConfig _boxConfig; TextBoxConfig get boxConfig => _boxConfig; @@ -361,6 +358,9 @@ class TextBoxComponent extends TextComponent { } } + /// Draws the cached image of the text box with the component's [paint], so + /// that the opacity and the color filter of the paint apply to the whole + /// box, including the background, without re-rendering the text. @override void render(Canvas canvas) { if (cache == null) { @@ -368,10 +368,19 @@ class TextBoxComponent extends TextComponent { } canvas.save(); canvas.scale(1 / pixelRatio); - canvas.drawImage(cache!, Offset.zero, _imagePaint); + canvas.drawImage( + cache!, + Offset.zero, + paint..filterQuality = FilterQuality.medium, + ); canvas.restore(); } + /// The paint is applied when the cached image is drawn in [render], so the + /// text does not need to be formatted again when the paint changes. + @override + void onChanged() {} + Future _fullRenderAsImage(Vector2 size) { final recorder = PictureRecorder(); final scaledSize = size * pixelRatio; diff --git a/packages/flame/lib/src/components/text_component.dart b/packages/flame/lib/src/components/text_component.dart index dcb46eeacab..252a56a1e72 100644 --- a/packages/flame/lib/src/components/text_component.dart +++ b/packages/flame/lib/src/components/text_component.dart @@ -20,6 +20,7 @@ class TextComponent extends PositionComponent super.key, }) : _text = text ?? '', _textRenderer = textRenderer ?? TextRendererFactory.createDefault() { + _paintedTextRenderer = _textRenderer; updateBounds(); } @@ -32,17 +33,35 @@ class TextComponent extends PositionComponent } } + /// The renderer that was set on the component. + /// + /// The text is drawn with [paintedTextRenderer], which is derived from this + /// renderer whenever the [paint] changes, so this renderer is never modified + /// by opacity or color changes. T get textRenderer => _textRenderer; T _textRenderer; set textRenderer(T textRenderer) { _textRenderer = textRenderer; + _paintedTextRenderer = _hasPaintChanged + ? textRenderer.copyWithPaint(paint) as T + : textRenderer; updateBounds(); } + /// [textRenderer] with the [paint] of the component applied, which is what + /// the text is drawn with. + /// + /// It is derived from [textRenderer] again on every paint change, so the + /// paint never compounds across changes. + @internal + T get paintedTextRenderer => _paintedTextRenderer; + late T _paintedTextRenderer; + bool _hasPaintChanged = false; + late InlineTextElement _textElement; void _updateElement() { - _textElement = _textRenderer.format(_text); + _textElement = _paintedTextRenderer.format(_text); _textElement.translate(0, _textElement.metrics.ascent); } @@ -60,7 +79,8 @@ class TextComponent extends PositionComponent @override void onChanged() { - _textRenderer = _textRenderer.copyWithPaint(paint) as T; + _hasPaintChanged = true; + _paintedTextRenderer = _textRenderer.copyWithPaint(paint) as T; _updateElement(); } } diff --git a/packages/flame/lib/src/text/renderers/sprite_font_renderer.dart b/packages/flame/lib/src/text/renderers/sprite_font_renderer.dart index 0f3b127d199..93f73fd16ef 100644 --- a/packages/flame/lib/src/text/renderers/sprite_font_renderer.dart +++ b/packages/flame/lib/src/text/renderers/sprite_font_renderer.dart @@ -64,11 +64,25 @@ class SpriteFontRenderer extends TextRenderer { ); } + /// Returns a copy of this [SpriteFontRenderer] where [paint] is applied on + /// top of the renderer's own [SpriteFontRenderer.paint]. + /// + /// The opacity of [paint] is multiplied into the glyph opacity, and the + /// color filter of [paint] replaces the one set through the `color` + /// argument of [SpriteFontRenderer.fromFont] when present. @override TextRenderer copyWithPaint(Paint paint) { return SpriteFontRenderer.fromPaint( font, - paint: paint, + paint: Paint.from(this.paint) + ..color = this.paint.color.withValues( + alpha: this.paint.color.a * paint.color.a, + ) + ..colorFilter = paint.colorFilter ?? this.paint.colorFilter + ..maskFilter = paint.maskFilter ?? this.paint.maskFilter + ..shader = paint.shader ?? this.paint.shader, + scale: scale, + letterSpacing: letterSpacing, ); } } diff --git a/packages/flame/lib/src/text/renderers/text_paint.dart b/packages/flame/lib/src/text/renderers/text_paint.dart index 1202523cd83..72276cfe15f 100644 --- a/packages/flame/lib/src/text/renderers/text_paint.dart +++ b/packages/flame/lib/src/text/renderers/text_paint.dart @@ -54,17 +54,62 @@ class TextPaint extends TextRenderer { return _textPainterCache.getValue(text)!; } + /// Returns a copy of this [TextPaint] where [paint] is applied on top of + /// the [style]. + /// + /// The opacity of [paint] scales the opacity of the text color, the + /// [TextStyle.shadows], the decoration color and the background, so opacity + /// changes made to the paint of a component affect the whole text without + /// discarding the colors that the [style] defines. The color filter, mask + /// filter and shader of [paint] are applied to the glyphs when set. @override TextRenderer copyWithPaint(Paint paint) { + final opacity = paint.color.a; return copyWith( (style) { return style.copyWith( - foreground: paint, + foreground: _foregroundWithPaint(style, paint), + shadows: style.shadows + ?.map((shadow) => _shadowWithOpacity(shadow, opacity)) + .toList(), + decorationColor: _withOpacity(style.decorationColor, opacity), + backgroundColor: _withOpacity(style.backgroundColor, opacity), + background: _paintWithOpacity(style.background, opacity), ); }, ); } + static Paint _foregroundWithPaint(TextStyle style, Paint paint) { + final foreground = style.foreground; + final color = foreground?.color ?? style.color ?? defaultTextStyle.color!; + return (foreground == null ? Paint() : Paint.from(foreground)) + ..color = color.withValues(alpha: color.a * paint.color.a) + ..colorFilter = paint.colorFilter ?? foreground?.colorFilter + ..maskFilter = paint.maskFilter ?? foreground?.maskFilter + ..shader = paint.shader ?? foreground?.shader; + } + + static Shadow _shadowWithOpacity(Shadow shadow, double opacity) { + return Shadow( + color: shadow.color.withValues(alpha: shadow.color.a * opacity), + offset: shadow.offset, + blurRadius: shadow.blurRadius, + ); + } + + static Color? _withOpacity(Color? color, double opacity) { + return color?.withValues(alpha: color.a * opacity); + } + + static Paint? _paintWithOpacity(Paint? paint, double opacity) { + if (paint == null) { + return null; + } + return Paint.from(paint) + ..color = paint.color.withValues(alpha: paint.color.a * opacity); + } + TextPaint copyWith( TextStyle Function(TextStyle) transform, { TextDirection? textDirection, diff --git a/packages/flame/lib/src/text/renderers/text_renderer.dart b/packages/flame/lib/src/text/renderers/text_renderer.dart index 1d442de8757..b6a324f7f48 100644 --- a/packages/flame/lib/src/text/renderers/text_renderer.dart +++ b/packages/flame/lib/src/text/renderers/text_renderer.dart @@ -13,6 +13,15 @@ abstract class TextRenderer { return format(text).metrics; } + /// Returns a copy of this renderer with [paint] applied on top of its own + /// styling. + /// + /// This is used by `TextComponent` whenever its paint changes, for example + /// by an `OpacityEffect` or a `ColorEffect`. The returned renderer should + /// keep the colors and other properties of this renderer, and scale its + /// opacity by the opacity of [paint]. Implementations are always given the + /// renderer that was originally set on the component, never a previously + /// returned copy, so the opacity does not compound. TextRenderer copyWithPaint(Paint paint); void render( diff --git a/packages/flame/test/components/text_box_component_test.dart b/packages/flame/test/components/text_box_component_test.dart index b4de6881e2b..77354bd9f5c 100644 --- a/packages/flame/test/components/text_box_component_test.dart +++ b/packages/flame/test/components/text_box_component_test.dart @@ -216,6 +216,26 @@ void main() { ); }); + testWithFlameGame('draws the cached image with the component paint', ( + game, + ) async { + final c = TextBoxComponent(text: 'foo bar'); + await game.ensureAdd(c); + + c.setOpacity(0.5); + + final canvas = MockCanvas(); + game.render(canvas); + expect( + canvas, + MockCanvas(mode: AssertionMode.containsAnyOrder)..drawImage( + null, + Offset.zero, + Paint()..color = const Color(0xFFFFFFFF).withValues(alpha: 0.5), + ), + ); + }); + testWithFlameGame( 'internal image is disposed when component is removed', (game) async { diff --git a/packages/flame/test/components/text_component_test.dart b/packages/flame/test/components/text_component_test.dart index 222ddccdefb..2204a47d95d 100644 --- a/packages/flame/test/components/text_component_test.dart +++ b/packages/flame/test/components/text_component_test.dart @@ -1,5 +1,7 @@ import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; import 'package:flame/text.dart'; +import 'package:flame_test/flame_test.dart'; import 'package:flutter/painting.dart'; import 'package:test/test.dart'; @@ -23,6 +25,151 @@ void main() { expect(elements, hasLength(2)); expect(elements.last.translations, [const Offset(0, _ascent)]); }); + + test('draws with the given renderer until the paint changes', () { + final textPaint = TextPaint( + style: const TextStyle(color: Color(0xFF2E9940)), + ); + final component = TextComponent(text: 'foo', textRenderer: textPaint); + expect(component.textRenderer, same(textPaint)); + expect(component.paintedTextRenderer, same(textPaint)); + + component.opacity = 0.5; + expect(component.textRenderer, same(textPaint)); + expect(component.paintedTextRenderer, isNot(same(textPaint))); + expect(textPaint.style.color, const Color(0xFF2E9940)); + }); + + test('a renderer round trip between opacity changes does not compound', () { + const shadowColor = Color(0x99FFFFFF); + final component = TextComponent( + text: 'foo', + textRenderer: TextPaint( + style: const TextStyle( + color: Color(0xFF2E9940), + shadows: [Shadow(color: shadowColor, blurRadius: 4)], + ), + ), + ); + + component.setOpacity(0.5); + component.textRenderer = component.textRenderer.copyWith( + (style) => style.copyWith(fontSize: 30), + ); + component.setOpacity(0.25); + + expect(component.textRenderer.style.color, const Color(0xFF2E9940)); + expect(component.textRenderer.style.shadows!.single.color, shadowColor); + final painted = component.paintedTextRenderer.style; + expect(painted.fontSize, 30); + expectColor( + painted.foreground!.color, + const Color(0xFF2E9940).withValues(alpha: 0.25), + ); + expectColor( + painted.shadows!.single.color, + const Color(0xFFFFFFFF).withValues(alpha: shadowColor.a * 0.25), + ); + }); + + test('applies opacity to the text color and shadows', () { + final component = TextComponent( + text: 'foo', + textRenderer: TextPaint( + style: const TextStyle( + color: Color(0xFF2E9940), + shadows: [Shadow(color: Color(0x99FFFFFF), blurRadius: 4)], + ), + ), + ); + + component.opacity = 0.5; + + final style = component.paintedTextRenderer.style; + expectColor( + style.foreground!.color, + const Color(0xFF2E9940).withValues(alpha: 0.5), + ); + expectColor( + style.shadows!.single.color, + const Color(0xFFFFFFFF).withValues(alpha: (153 / 255) * 0.5), + ); + }); + + test('opacity changes do not compound', () { + final component = TextComponent( + text: 'foo', + textRenderer: TextPaint( + style: const TextStyle( + color: Color(0xFF2E9940), + shadows: [Shadow(color: Color(0xFFFFFFFF), blurRadius: 4)], + ), + ), + ); + + component.opacity = 0.5; + component.opacity = 0.5; + component.opacity = 0.8; + + final style = component.paintedTextRenderer.style; + expectColor( + style.foreground!.color, + const Color(0xFF2E9940).withValues(alpha: 0.8), + ); + expectColor( + style.shadows!.single.color, + const Color(0xFFFFFFFF).withValues(alpha: 0.8), + ); + }); + + test('a new renderer gets the current paint applied', () { + final component = TextComponent(text: 'foo'); + component.opacity = 0.25; + + component.textRenderer = TextPaint( + style: const TextStyle(color: Color(0xFF0000FF)), + ); + + expectColor( + component.paintedTextRenderer.style.foreground!.color, + const Color(0xFF0000FF).withValues(alpha: 0.25), + ); + }); + + testWithFlameGame('fades out together with the shadows', (game) async { + final component = TextComponent( + text: 'foo', + textRenderer: TextPaint( + style: const TextStyle( + color: Color(0xFF2E9940), + shadows: [Shadow(color: Color(0xFFFFFFFF), blurRadius: 4)], + ), + ), + )..add(OpacityEffect.fadeOut(EffectController(duration: 1))); + await game.ensureAdd(component); + + game.update(0.5); + var style = component.paintedTextRenderer.style; + expectColor( + style.foreground!.color, + const Color(0xFF2E9940).withValues(alpha: 0.5), + ); + expectColor( + style.shadows!.single.color, + const Color(0xFFFFFFFF).withValues(alpha: 0.5), + ); + + game.update(0.5); + style = component.paintedTextRenderer.style; + expectColor( + style.foreground!.color, + const Color(0xFF2E9940).withValues(alpha: 0), + ); + expectColor( + style.shadows!.single.color, + const Color(0xFFFFFFFF).withValues(alpha: 0), + ); + }); }); } diff --git a/packages/flame/test/text/sprite_font_renderer_test.dart b/packages/flame/test/text/sprite_font_renderer_test.dart index 185b0a84484..4bad0fc2f5f 100644 --- a/packages/flame/test/text/sprite_font_renderer_test.dart +++ b/packages/flame/test/text/sprite_font_renderer_test.dart @@ -29,6 +29,23 @@ void main() { ); }); + test('copyWithPaint keeps the renderer properties', () async { + final renderer = await _createRenderer(scale: 2, letterSpacing: 1); + const colorFilter = ColorFilter.mode(Color(0xFFFF0000), BlendMode.srcIn); + renderer.paint.colorFilter = colorFilter; + + final copy = + renderer.copyWithPaint(Paint()..color = const Color(0x80FFFFFF)) + as SpriteFontRenderer; + + expect(copy.font, same(renderer.font)); + expect(copy.scale, 2); + expect(copy.letterSpacing, 1); + expect(copy.paint, isNot(same(renderer.paint))); + expect(copy.paint.colorFilter, colorFilter); + expectColorAlpha(copy.paint.color, const Color(0x80FFFFFF)); + }); + testGolden( 'text rendering at different scales', (game, tester) async { diff --git a/packages/flame/test/text/text_paint_test.dart b/packages/flame/test/text/text_paint_test.dart index 26224389451..8bc7ae16ec7 100644 --- a/packages/flame/test/text/text_paint_test.dart +++ b/packages/flame/test/text/text_paint_test.dart @@ -1,11 +1,196 @@ import 'dart:ui'; import 'package:flame/text.dart'; +import 'package:flame_test/flame_test.dart'; import 'package:flutter/rendering.dart' as flutter; import 'package:test/test.dart'; void main() { group('TextPaint', () { + group('copyWithPaint', () { + test('keeps the style color and applies the paint opacity', () { + final textPaint = TextPaint( + style: const flutter.TextStyle(color: Color(0xFF2E9940)), + ); + + final copy = + textPaint.copyWithPaint(Paint()..color = const Color(0x80FFFFFF)) + as TextPaint; + + expect(copy.style.color, isNull); + expectColor( + copy.style.foreground!.color, + const Color(0xFF2E9940).withValues(alpha: 128 / 255), + ); + }); + + test('falls back to the default color when the style has none', () { + final textPaint = TextPaint(style: const flutter.TextStyle()); + + final copy = + textPaint.copyWithPaint(Paint()..color = const Color(0x80FFFFFF)) + as TextPaint; + + expectColor(copy.style.foreground!.color, const Color(0x80FFFFFF)); + }); + + test('applies the paint opacity to the shadows', () { + final textPaint = TextPaint( + style: const flutter.TextStyle( + color: Color(0xFF2E9940), + shadows: [ + Shadow( + color: Color(0xFFFFFFFF), + offset: Offset(1, 2), + blurRadius: 4, + ), + Shadow(color: Color(0x80000000), blurRadius: 1), + ], + ), + ); + + final copy = + textPaint.copyWithPaint(Paint()..color = const Color(0x80FFFFFF)) + as TextPaint; + + final shadows = copy.style.shadows!; + expect(shadows, hasLength(2)); + expectColor( + shadows[0].color, + const Color(0xFFFFFFFF).withValues(alpha: 128 / 255), + ); + expect(shadows[0].offset, const Offset(1, 2)); + expect(shadows[0].blurRadius, 4); + expectColor( + shadows[1].color, + const Color(0xFF000000).withValues(alpha: (128 / 255) * (128 / 255)), + ); + expect(shadows[1].blurRadius, 1); + }); + + test('keeps the properties of an existing foreground paint', () { + final textPaint = TextPaint( + style: flutter.TextStyle( + foreground: Paint() + ..color = const Color(0xFF0000FF) + ..style = PaintingStyle.stroke + ..strokeWidth = 3, + ), + ); + + final copy = + textPaint.copyWithPaint(Paint()..color = const Color(0x80FFFFFF)) + as TextPaint; + + final foreground = copy.style.foreground!; + expect(foreground, isNot(same(textPaint.style.foreground))); + expectColor( + foreground.color, + const Color(0xFF0000FF).withValues(alpha: 128 / 255), + ); + expect(foreground.style, PaintingStyle.stroke); + expect(foreground.strokeWidth, 3); + }); + + test('carries over the color filter of the paint', () { + const colorFilter = ColorFilter.mode( + Color(0xFFFF0000), + BlendMode.srcATop, + ); + final textPaint = TextPaint( + style: const flutter.TextStyle(color: Color(0xFF2E9940)), + ); + + final copy = + textPaint.copyWithPaint( + Paint() + ..color = const Color(0xFFFFFFFF) + ..colorFilter = colorFilter, + ) + as TextPaint; + + expect(copy.style.foreground!.colorFilter, colorFilter); + expectColor(copy.style.foreground!.color, const Color(0xFF2E9940)); + }); + + test('only uses the opacity of the paint color', () { + final textPaint = TextPaint( + style: const flutter.TextStyle(color: Color(0xFF2E9940)), + ); + + final copy = + textPaint.copyWithPaint(Paint()..color = const Color(0x80FF0000)) + as TextPaint; + + expectColor( + copy.style.foreground!.color, + const Color(0xFF2E9940).withValues(alpha: 128 / 255), + ); + }); + + test('applies the paint opacity to the decoration color', () { + final textPaint = TextPaint( + style: const flutter.TextStyle( + color: Color(0xFF2E9940), + decoration: TextDecoration.underline, + decorationColor: Color(0xFFFF0000), + ), + ); + + final copy = + textPaint.copyWithPaint(Paint()..color = const Color(0x80FFFFFF)) + as TextPaint; + + expect(copy.style.decoration, TextDecoration.underline); + expectColor( + copy.style.decorationColor!, + const Color(0xFFFF0000).withValues(alpha: 128 / 255), + ); + }); + + test('applies the paint opacity to the background color', () { + final textPaint = TextPaint( + style: const flutter.TextStyle( + color: Color(0xFF2E9940), + backgroundColor: Color(0xFF000000), + ), + ); + + final copy = + textPaint.copyWithPaint(Paint()..color = const Color(0x80FFFFFF)) + as TextPaint; + + expect(copy.style.background, isNull); + expectColor( + copy.style.backgroundColor!, + const Color(0xFF000000).withValues(alpha: 128 / 255), + ); + }); + + test('applies the paint opacity to the background paint', () { + final textPaint = TextPaint( + style: flutter.TextStyle( + color: const Color(0xFF2E9940), + background: Paint() + ..color = const Color(0xFF000000) + ..style = PaintingStyle.stroke, + ), + ); + + final copy = + textPaint.copyWithPaint(Paint()..color = const Color(0x80FFFFFF)) + as TextPaint; + + final background = copy.style.background!; + expect(background, isNot(same(textPaint.style.background))); + expect(background.style, PaintingStyle.stroke); + expectColor( + background.color, + const Color(0xFF000000).withValues(alpha: 128 / 255), + ); + }); + }); + test('copyWith returns a new instance with the new values', () { const style = flutter.TextStyle(fontSize: 12, fontFamily: 'Times'); final tp = TextPaint( diff --git a/packages/flame_test/lib/src/debug_text_renderer.dart b/packages/flame_test/lib/src/debug_text_renderer.dart index c04f4aa9c54..2a6c5f3351c 100644 --- a/packages/flame_test/lib/src/debug_text_renderer.dart +++ b/packages/flame_test/lib/src/debug_text_renderer.dart @@ -29,7 +29,7 @@ class DebugTextRenderer extends TextRenderer { @override TextRenderer copyWithPaint(Paint paint) { return DebugTextRenderer( - color: paint.color, + color: color.withValues(alpha: color.a * paint.color.a), fontSize: fontSize, lineHeight: lineHeight, fontWeight: fontWeight, diff --git a/packages/flame_test/test/debug_text_formatter_test.dart b/packages/flame_test/test/debug_text_formatter_test.dart index a8c62fb1256..095b911d95e 100644 --- a/packages/flame_test/test/debug_text_formatter_test.dart +++ b/packages/flame_test/test/debug_text_formatter_test.dart @@ -7,6 +7,29 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('DebugTextFormatter', () { + test('copyWithPaint keeps the color and scales the opacity', () { + final renderer = DebugTextRenderer( + color: const Color(0xFFFF0000), + fontSize: 10, + lineHeight: 1.5, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic, + ); + + final copy = + renderer.copyWithPaint(Paint()..color = const Color(0x80FFFFFF)) + as DebugTextRenderer; + + expect(copy.color.r, 1.0); + expect(copy.color.g, 0.0); + expect(copy.color.b, 0.0); + expect(copy.color.a, closeTo(128 / 255, 1e-7)); + expect(copy.fontSize, 10); + expect(copy.lineHeight, 1.5); + expect(copy.fontWeight, FontWeight.bold); + expect(copy.fontStyle, FontStyle.italic); + }); + testGolden( 'Render debug text', (game, tester) async {