From e5ad29314e7ba1ebbfc08055f536805c9a9ac894 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Sat, 18 Apr 2026 11:58:40 +0800
Subject: [PATCH] =?UTF-8?q?feat(text):=20=E6=B7=BB=E5=8A=A0=E5=AF=8C?=
=?UTF-8?q?=E6=96=87=E6=9C=AC=E6=95=88=E6=9E=9C=E7=B3=BB=E7=BB=9F=E5=92=8C?=
=?UTF-8?q?=E9=A2=9C=E8=89=B2=E6=A0=87=E8=AE=B0=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现 RichTextEffectBase 基类提供统一的标签命名和参数读取逻辑
- 添加 RichTextBlueEffect、RichTextGoldEffect、RichTextGreenEffect 和 RichTextRedEffect 颜色标记效果
- 添加 RichTextFadeInEffect、RichTextFlyInEffect、RichTextJitterEffect 和 RichTextSineEffect 动画效果
- 实现 DefaultRichTextEffectRegistry 默认效果注册表
- 创建 GfRichTextLabel 富文本标签宿主组件
- 添加 IRichTextEffectRegistry 效果注册表接口
- 实现 RichTextEffectEntry、RichTextEffectsController 和 RichTextProfile 配置管理类
- 添加 RichTextMarkup 语义化标签构建辅助方法
- 创建 RichTextMarkupTests 和 RichTextProfileTests 单元测试
---
.../Text/RichTextMarkupTests.cs | 38 ++++++
.../Text/RichTextProfileTests.cs | 31 +++++
.../Text/DefaultRichTextEffectRegistry.cs | 65 +++++++++
.../Text/Effects/RichTextBlueEffect.cs | 27 ++++
.../Text/Effects/RichTextEffectBase.cs | 81 +++++++++++
.../Text/Effects/RichTextFadeInEffect.cs | 47 +++++++
.../Text/Effects/RichTextFlyInEffect.cs | 64 +++++++++
.../Text/Effects/RichTextGoldEffect.cs | 27 ++++
.../Text/Effects/RichTextGreenEffect.cs | 27 ++++
.../Text/Effects/RichTextJitterEffect.cs | 57 ++++++++
.../Text/Effects/RichTextRedEffect.cs | 27 ++++
.../Text/Effects/RichTextSineEffect.cs | 47 +++++++
GFramework.Godot/Text/GfRichTextLabel.cs | 83 ++++++++++++
.../Text/IRichTextEffectRegistry.cs | 23 ++++
GFramework.Godot/Text/RichTextEffectEntry.cs | 22 +++
.../Text/RichTextEffectsController.cs | 70 ++++++++++
GFramework.Godot/Text/RichTextMarkup.cs | 128 ++++++++++++++++++
GFramework.Godot/Text/RichTextProfile.cs | 37 +++++
18 files changed, 901 insertions(+)
create mode 100644 GFramework.Godot.Tests/Text/RichTextMarkupTests.cs
create mode 100644 GFramework.Godot.Tests/Text/RichTextProfileTests.cs
create mode 100644 GFramework.Godot/Text/DefaultRichTextEffectRegistry.cs
create mode 100644 GFramework.Godot/Text/Effects/RichTextBlueEffect.cs
create mode 100644 GFramework.Godot/Text/Effects/RichTextEffectBase.cs
create mode 100644 GFramework.Godot/Text/Effects/RichTextFadeInEffect.cs
create mode 100644 GFramework.Godot/Text/Effects/RichTextFlyInEffect.cs
create mode 100644 GFramework.Godot/Text/Effects/RichTextGoldEffect.cs
create mode 100644 GFramework.Godot/Text/Effects/RichTextGreenEffect.cs
create mode 100644 GFramework.Godot/Text/Effects/RichTextJitterEffect.cs
create mode 100644 GFramework.Godot/Text/Effects/RichTextRedEffect.cs
create mode 100644 GFramework.Godot/Text/Effects/RichTextSineEffect.cs
create mode 100644 GFramework.Godot/Text/GfRichTextLabel.cs
create mode 100644 GFramework.Godot/Text/IRichTextEffectRegistry.cs
create mode 100644 GFramework.Godot/Text/RichTextEffectEntry.cs
create mode 100644 GFramework.Godot/Text/RichTextEffectsController.cs
create mode 100644 GFramework.Godot/Text/RichTextMarkup.cs
create mode 100644 GFramework.Godot/Text/RichTextProfile.cs
diff --git a/GFramework.Godot.Tests/Text/RichTextMarkupTests.cs b/GFramework.Godot.Tests/Text/RichTextMarkupTests.cs
new file mode 100644
index 00000000..72009309
--- /dev/null
+++ b/GFramework.Godot.Tests/Text/RichTextMarkupTests.cs
@@ -0,0 +1,38 @@
+using GFramework.Godot.Text;
+
+namespace GFramework.Godot.Tests.Text;
+
+///
+/// 的测试。
+///
+[TestFixture]
+public sealed class RichTextMarkupTests
+{
+ ///
+ /// 验证颜色快捷方法会输出预期标签。
+ ///
+ [Test]
+ public void Green_Should_Wrap_Text_With_Green_Tag()
+ {
+ var result = RichTextMarkup.Green("Ready");
+
+ Assert.That(result, Is.EqualTo("[green]Ready[/green]"));
+ }
+
+ ///
+ /// 验证效果方法会按稳定顺序拼接环境参数。
+ ///
+ [Test]
+ public void Effect_Should_Append_Environment_Parameters()
+ {
+ var env = new Dictionary
+ {
+ ["speed"] = 4,
+ ["tick"] = 0.1f
+ };
+
+ var result = RichTextMarkup.Effect("Hello", "fade_in", env);
+
+ Assert.That(result, Is.EqualTo("[fade_in speed=4 tick=0.1]Hello[/fade_in]"));
+ }
+}
diff --git a/GFramework.Godot.Tests/Text/RichTextProfileTests.cs b/GFramework.Godot.Tests/Text/RichTextProfileTests.cs
new file mode 100644
index 00000000..666289b5
--- /dev/null
+++ b/GFramework.Godot.Tests/Text/RichTextProfileTests.cs
@@ -0,0 +1,31 @@
+using GFramework.Godot.Text;
+
+namespace GFramework.Godot.Tests.Text;
+
+///
+/// 的测试。
+///
+[TestFixture]
+public sealed class RichTextProfileTests
+{
+ ///
+ /// 验证默认内置配置会暴露完整的第一阶段效果键集合。
+ ///
+ [Test]
+ public void CreateBuiltInDefault_Should_Contain_The_First_Phase_Effect_Keys()
+ {
+ var profile = RichTextProfile.CreateBuiltInDefault();
+
+ Assert.That(profile.Effects.Select(static entry => entry.Key), Is.EqualTo(new[]
+ {
+ "green",
+ "red",
+ "gold",
+ "blue",
+ "fade_in",
+ "sine",
+ "jitter",
+ "fly_in"
+ }));
+ }
+}
diff --git a/GFramework.Godot/Text/DefaultRichTextEffectRegistry.cs b/GFramework.Godot/Text/DefaultRichTextEffectRegistry.cs
new file mode 100644
index 00000000..590e36b3
--- /dev/null
+++ b/GFramework.Godot/Text/DefaultRichTextEffectRegistry.cs
@@ -0,0 +1,65 @@
+using GFramework.Godot.Text.Effects;
+
+namespace GFramework.Godot.Text;
+
+///
+/// 默认的富文本效果注册表。
+/// 该实现仅负责内置效果键的解析,不处理业务层文本构建或配置持久化。
+///
+public sealed class DefaultRichTextEffectRegistry : IRichTextEffectRegistry
+{
+ ///
+ /// 创建当前配置对应的全部效果实例。
+ ///
+ /// 效果组合配置。
+ /// 当前是否允许字符级动态效果生效。
+ /// 内置效果实例集合。
+ public IReadOnlyList CreateEffects(RichTextProfile profile, bool animatedEffectsEnabled)
+ {
+ ArgumentNullException.ThrowIfNull(profile);
+
+ var effects = new List(profile.Effects.Length);
+ foreach (var entry in profile.Effects)
+ {
+ if (entry is null || !entry.Enabled || string.IsNullOrWhiteSpace(entry.Key))
+ {
+ continue;
+ }
+
+ var effect = CreateEffect(entry.Key, animatedEffectsEnabled);
+ if (effect is not null)
+ {
+ effects.Add(effect);
+ }
+ }
+
+ return effects;
+ }
+
+ ///
+ /// 根据效果键创建单个效果实例。
+ ///
+ /// 效果键。
+ /// 当前是否允许字符级动态效果生效。
+ /// 解析成功时返回效果实例;否则返回 。
+ public RichTextEffect? CreateEffect(string key, bool animatedEffectsEnabled)
+ {
+ if (string.IsNullOrWhiteSpace(key))
+ {
+ return null;
+ }
+
+ return key.Trim().ToLowerInvariant() switch
+ {
+ "green" => new RichTextGreenEffect(),
+ "red" => new RichTextRedEffect(),
+ "gold" => new RichTextGoldEffect(),
+ "blue" => new RichTextBlueEffect(),
+ "fade_in" => new RichTextFadeInEffect(animatedEffectsEnabled),
+ "sine" => new RichTextSineEffect(animatedEffectsEnabled),
+ "jitter" => new RichTextJitterEffect(animatedEffectsEnabled),
+ "fly_in" => new RichTextFlyInEffect(animatedEffectsEnabled),
+ _ => null
+ };
+ }
+}
diff --git a/GFramework.Godot/Text/Effects/RichTextBlueEffect.cs b/GFramework.Godot/Text/Effects/RichTextBlueEffect.cs
new file mode 100644
index 00000000..c7319616
--- /dev/null
+++ b/GFramework.Godot/Text/Effects/RichTextBlueEffect.cs
@@ -0,0 +1,27 @@
+namespace GFramework.Godot.Text.Effects;
+
+///
+/// 为文本应用蓝色语义高亮。
+///
+[GlobalClass]
+[Tool]
+public partial class RichTextBlueEffect : RichTextEffectBase
+{
+ private static readonly Color BlueColor = new(0.44f, 0.72f, 0.98f, 1.0f);
+
+ ///
+ /// 获取标签名。
+ ///
+ protected override string TagName => "blue";
+
+ ///
+ /// 应用蓝色颜色效果。
+ ///
+ /// 当前字符上下文。
+ /// 始终返回 。
+ public override bool _ProcessCustomFX(CharFXTransform charFx)
+ {
+ charFx.Color = BlueColor;
+ return true;
+ }
+}
diff --git a/GFramework.Godot/Text/Effects/RichTextEffectBase.cs b/GFramework.Godot/Text/Effects/RichTextEffectBase.cs
new file mode 100644
index 00000000..41d7ca8e
--- /dev/null
+++ b/GFramework.Godot/Text/Effects/RichTextEffectBase.cs
@@ -0,0 +1,81 @@
+namespace GFramework.Godot.Text.Effects;
+
+///
+/// 富文本效果基类,提供统一的标签命名和环境参数读取辅助逻辑。
+/// 该基类只负责 Godot 适配细节,不承载业务语义分层。
+///
+[Tool]
+public abstract partial class RichTextEffectBase : RichTextEffect
+{
+ ///
+ /// 获取当前效果对应的 BBCode 标签名。
+ ///
+ protected abstract string TagName { get; }
+
+ ///
+ /// 获取 Godot 识别当前效果所需的 `bbcode` 属性。
+ ///
+ public string bbcode => TagName;
+
+ ///
+ /// 尝试从字符环境参数中读取布尔值。
+ ///
+ /// 当前字符变换上下文。
+ /// 参数键。
+ /// 读取失败时使用的默认值。
+ /// 最终布尔值。
+ protected bool GetBool(CharFXTransform transform, string key, bool defaultValue = false)
+ {
+ if (transform.Env.TryGetValue(Variant.From(key), out var value))
+ {
+ return value.AsBool();
+ }
+
+ return defaultValue;
+ }
+
+ ///
+ /// 尝试从字符环境参数中读取浮点值。
+ ///
+ /// 当前字符变换上下文。
+ /// 参数键。
+ /// 读取失败时使用的默认值。
+ /// 最终浮点值。
+ protected float GetFloat(CharFXTransform transform, string key, float defaultValue)
+ {
+ if (transform.Env.TryGetValue(Variant.From(key), out var value))
+ {
+ return (float)value.AsDouble();
+ }
+
+ return defaultValue;
+ }
+
+ ///
+ /// 尝试从字符环境参数中读取颜色值。
+ ///
+ /// 当前字符变换上下文。
+ /// 参数键。
+ /// 读取失败时使用的默认值。
+ /// 最终颜色值。
+ protected Color GetColor(CharFXTransform transform, string key, Color defaultValue)
+ {
+ if (transform.Env.TryGetValue(Variant.From(key), out var value) &&
+ value.VariantType == Variant.Type.Color)
+ {
+ return (Color)value;
+ }
+
+ return defaultValue;
+ }
+
+ ///
+ /// 从字符环境参数中应用可见性开关。
+ ///
+ /// 当前字符变换上下文。
+ /// 默认可见性。
+ protected void ApplyVisibility(CharFXTransform transform, bool defaultValue = true)
+ {
+ transform.Visible = GetBool(transform, "visible", defaultValue);
+ }
+}
diff --git a/GFramework.Godot/Text/Effects/RichTextFadeInEffect.cs b/GFramework.Godot/Text/Effects/RichTextFadeInEffect.cs
new file mode 100644
index 00000000..3af25719
--- /dev/null
+++ b/GFramework.Godot/Text/Effects/RichTextFadeInEffect.cs
@@ -0,0 +1,47 @@
+namespace GFramework.Godot.Text.Effects;
+
+///
+/// 为文本提供逐字符淡入效果。
+///
+[GlobalClass]
+[Tool]
+public partial class RichTextFadeInEffect : RichTextEffectBase
+{
+ private readonly bool _animatedEffectsEnabled;
+
+ ///
+ /// 初始化淡入效果。
+ ///
+ /// 是否允许动态效果实际生效。
+ public RichTextFadeInEffect(bool animatedEffectsEnabled = true)
+ {
+ _animatedEffectsEnabled = animatedEffectsEnabled;
+ }
+
+ ///
+ /// 获取标签名。
+ ///
+ protected override string TagName => "fade_in";
+
+ ///
+ /// 应用淡入动画。
+ ///
+ /// 当前字符上下文。
+ /// 始终返回 。
+ public override bool _ProcessCustomFX(CharFXTransform charFx)
+ {
+ if (!_animatedEffectsEnabled)
+ {
+ return true;
+ }
+
+ var speed = GetFloat(charFx, "speed", 4.0f);
+ var tick = GetFloat(charFx, "tick", 0.01f);
+ var progress = (float)(charFx.ElapsedTime * speed - charFx.RelativeIndex * tick);
+ var color = charFx.Color;
+ color.A = Mathf.Clamp(progress, 0f, 1f);
+ charFx.Color = color;
+ ApplyVisibility(charFx);
+ return true;
+ }
+}
diff --git a/GFramework.Godot/Text/Effects/RichTextFlyInEffect.cs b/GFramework.Godot/Text/Effects/RichTextFlyInEffect.cs
new file mode 100644
index 00000000..efb6f893
--- /dev/null
+++ b/GFramework.Godot/Text/Effects/RichTextFlyInEffect.cs
@@ -0,0 +1,64 @@
+namespace GFramework.Godot.Text.Effects;
+
+///
+/// 为文本提供逐字符飞入效果。
+///
+[GlobalClass]
+[Tool]
+public partial class RichTextFlyInEffect : RichTextEffectBase
+{
+ private readonly bool _animatedEffectsEnabled;
+
+ ///
+ /// 初始化飞入效果。
+ ///
+ /// 是否允许动态效果实际生效。
+ public RichTextFlyInEffect(bool animatedEffectsEnabled = true)
+ {
+ _animatedEffectsEnabled = animatedEffectsEnabled;
+ }
+
+ ///
+ /// 获取标签名。
+ ///
+ protected override string TagName => "fly_in";
+
+ ///
+ /// 应用飞入动画。
+ ///
+ /// 当前字符上下文。
+ /// 始终返回 。
+ public override bool _ProcessCustomFX(CharFXTransform charFx)
+ {
+ if (!_animatedEffectsEnabled)
+ {
+ return true;
+ }
+
+ var startOffset = new Vector2(
+ GetFloat(charFx, "offset_x", 12f),
+ GetFloat(charFx, "offset_y", 0f));
+ var speed = GetFloat(charFx, "speed", 3.0f);
+ var tick = GetFloat(charFx, "tick", 0.015f);
+ var progress = Mathf.Clamp((float)(charFx.ElapsedTime * speed - charFx.RelativeIndex * tick), 0f, 1f);
+ var eased = EaseOutQuad(progress);
+
+ charFx.Offset += startOffset * (1f - eased);
+
+ var color = charFx.Color;
+ color.A = eased;
+ charFx.Color = color;
+ ApplyVisibility(charFx);
+ return true;
+ }
+
+ ///
+ /// 计算二次缓出值。
+ ///
+ /// 归一化进度。
+ /// 缓出后的进度。
+ private static float EaseOutQuad(float value)
+ {
+ return 1f - (1f - value) * (1f - value);
+ }
+}
diff --git a/GFramework.Godot/Text/Effects/RichTextGoldEffect.cs b/GFramework.Godot/Text/Effects/RichTextGoldEffect.cs
new file mode 100644
index 00000000..68ee952d
--- /dev/null
+++ b/GFramework.Godot/Text/Effects/RichTextGoldEffect.cs
@@ -0,0 +1,27 @@
+namespace GFramework.Godot.Text.Effects;
+
+///
+/// 为文本应用金色语义高亮。
+///
+[GlobalClass]
+[Tool]
+public partial class RichTextGoldEffect : RichTextEffectBase
+{
+ private static readonly Color GoldColor = new(0.96f, 0.79f, 0.34f, 1.0f);
+
+ ///
+ /// 获取标签名。
+ ///
+ protected override string TagName => "gold";
+
+ ///
+ /// 应用金色颜色效果。
+ ///
+ /// 当前字符上下文。
+ /// 始终返回 。
+ public override bool _ProcessCustomFX(CharFXTransform charFx)
+ {
+ charFx.Color = GoldColor;
+ return true;
+ }
+}
diff --git a/GFramework.Godot/Text/Effects/RichTextGreenEffect.cs b/GFramework.Godot/Text/Effects/RichTextGreenEffect.cs
new file mode 100644
index 00000000..f4f3db0e
--- /dev/null
+++ b/GFramework.Godot/Text/Effects/RichTextGreenEffect.cs
@@ -0,0 +1,27 @@
+namespace GFramework.Godot.Text.Effects;
+
+///
+/// 为文本应用绿色语义高亮。
+///
+[GlobalClass]
+[Tool]
+public partial class RichTextGreenEffect : RichTextEffectBase
+{
+ private static readonly Color GreenColor = new(0.46f, 0.91f, 0.49f, 1.0f);
+
+ ///
+ /// 获取标签名。
+ ///
+ protected override string TagName => "green";
+
+ ///
+ /// 应用绿色颜色效果。
+ ///
+ /// 当前字符上下文。
+ /// 始终返回 。
+ public override bool _ProcessCustomFX(CharFXTransform charFx)
+ {
+ charFx.Color = GreenColor;
+ return true;
+ }
+}
diff --git a/GFramework.Godot/Text/Effects/RichTextJitterEffect.cs b/GFramework.Godot/Text/Effects/RichTextJitterEffect.cs
new file mode 100644
index 00000000..b5b3fa16
--- /dev/null
+++ b/GFramework.Godot/Text/Effects/RichTextJitterEffect.cs
@@ -0,0 +1,57 @@
+namespace GFramework.Godot.Text.Effects;
+
+///
+/// 为文本提供抖动效果。
+///
+[GlobalClass]
+[Tool]
+public partial class RichTextJitterEffect : RichTextEffectBase
+{
+ private readonly bool _animatedEffectsEnabled;
+ private readonly FastNoiseLite _noise;
+
+ ///
+ /// 初始化抖动效果。
+ ///
+ /// 是否允许动态效果实际生效。
+ public RichTextJitterEffect(bool animatedEffectsEnabled = true)
+ {
+ _animatedEffectsEnabled = animatedEffectsEnabled;
+ _noise = new FastNoiseLite
+ {
+ NoiseType = FastNoiseLite.NoiseTypeEnum.Perlin,
+ FractalOctaves = 8,
+ FractalGain = 0.8f
+ };
+ }
+
+ ///
+ /// 获取标签名。
+ ///
+ protected override string TagName => "jitter";
+
+ ///
+ /// 应用抖动位移。
+ ///
+ /// 当前字符上下文。
+ /// 始终返回 。
+ public override bool _ProcessCustomFX(CharFXTransform charFx)
+ {
+ if (!_animatedEffectsEnabled)
+ {
+ return true;
+ }
+
+ var amplitude = GetFloat(charFx, "amplitude", 3.0f);
+ var speed = GetFloat(charFx, "speed", 600.0f);
+
+ _noise.Seed = (charFx.RelativeIndex + 1) * 131;
+ var x = _noise.GetNoise1D((float)charFx.ElapsedTime * speed);
+ _noise.Seed = (charFx.RelativeIndex + 1) * 737;
+ var y = _noise.GetNoise1D((float)charFx.ElapsedTime * speed);
+
+ charFx.Offset += new Vector2(x, y) * amplitude;
+ ApplyVisibility(charFx);
+ return true;
+ }
+}
diff --git a/GFramework.Godot/Text/Effects/RichTextRedEffect.cs b/GFramework.Godot/Text/Effects/RichTextRedEffect.cs
new file mode 100644
index 00000000..af4afa4e
--- /dev/null
+++ b/GFramework.Godot/Text/Effects/RichTextRedEffect.cs
@@ -0,0 +1,27 @@
+namespace GFramework.Godot.Text.Effects;
+
+///
+/// 为文本应用红色语义高亮。
+///
+[GlobalClass]
+[Tool]
+public partial class RichTextRedEffect : RichTextEffectBase
+{
+ private static readonly Color RedColor = new(0.96f, 0.35f, 0.35f, 1.0f);
+
+ ///
+ /// 获取标签名。
+ ///
+ protected override string TagName => "red";
+
+ ///
+ /// 应用红色颜色效果。
+ ///
+ /// 当前字符上下文。
+ /// 始终返回 。
+ public override bool _ProcessCustomFX(CharFXTransform charFx)
+ {
+ charFx.Color = RedColor;
+ return true;
+ }
+}
diff --git a/GFramework.Godot/Text/Effects/RichTextSineEffect.cs b/GFramework.Godot/Text/Effects/RichTextSineEffect.cs
new file mode 100644
index 00000000..10530bf9
--- /dev/null
+++ b/GFramework.Godot/Text/Effects/RichTextSineEffect.cs
@@ -0,0 +1,47 @@
+namespace GFramework.Godot.Text.Effects;
+
+///
+/// 为文本提供正弦波形的上下漂浮效果。
+///
+[GlobalClass]
+[Tool]
+public partial class RichTextSineEffect : RichTextEffectBase
+{
+ private readonly bool _animatedEffectsEnabled;
+
+ ///
+ /// 初始化正弦效果。
+ ///
+ /// 是否允许动态效果实际生效。
+ public RichTextSineEffect(bool animatedEffectsEnabled = true)
+ {
+ _animatedEffectsEnabled = animatedEffectsEnabled;
+ }
+
+ ///
+ /// 获取标签名。
+ ///
+ protected override string TagName => "sine";
+
+ ///
+ /// 应用正弦位移。
+ ///
+ /// 当前字符上下文。
+ /// 始终返回 。
+ public override bool _ProcessCustomFX(CharFXTransform charFx)
+ {
+ if (!_animatedEffectsEnabled)
+ {
+ return true;
+ }
+
+ var amplitude = GetFloat(charFx, "amplitude", 0.8f);
+ var frequency = GetFloat(charFx, "frequency", 0.5f);
+ var speed = GetFloat(charFx, "speed", 1.5f);
+ var phase = (float)(charFx.ElapsedTime * speed + charFx.RelativeIndex * 0.1f);
+ var offsetY = amplitude * Mathf.Sin(phase * Mathf.Pi * 2f * frequency);
+ charFx.Offset += new Vector2(0f, offsetY);
+ ApplyVisibility(charFx);
+ return true;
+ }
+}
diff --git a/GFramework.Godot/Text/GfRichTextLabel.cs b/GFramework.Godot/Text/GfRichTextLabel.cs
new file mode 100644
index 00000000..6715ec45
--- /dev/null
+++ b/GFramework.Godot/Text/GfRichTextLabel.cs
@@ -0,0 +1,83 @@
+namespace GFramework.Godot.Text;
+
+///
+/// GFramework 提供的组合式富文本标签宿主。
+/// 该类型只负责桥接 Godot 的 与框架的效果装配逻辑,不承载具体效果实现。
+///
+[GlobalClass]
+[Tool]
+public partial class GfRichTextLabel : RichTextLabel
+{
+ private IRichTextEffectRegistry? _effectRegistry;
+ private RichTextEffectsController? _effectsController;
+
+ ///
+ /// 获取或设置当前标签使用的效果配置。
+ /// 为空时将回退到内置默认配置。
+ ///
+ [Export]
+ public RichTextProfile? Profile { get; set; }
+
+ ///
+ /// 获取或设置是否启用框架管理的富文本效果装配。
+ ///
+ [Export]
+ public bool EnableFrameworkEffects { get; set; } = true;
+
+ ///
+ /// 获取或设置是否允许字符级动态效果实际生效。
+ /// 关闭后仍然会安装对应标签,使富文本内容保持可解析。
+ ///
+ [Export]
+ public bool AnimatedEffectsEnabled { get; set; } = true;
+
+ ///
+ /// 获取当前使用的效果注册表。
+ ///
+ internal IRichTextEffectRegistry EffectRegistry
+ {
+ get => _effectRegistry ??= new DefaultRichTextEffectRegistry();
+ set => _effectRegistry = value ?? throw new ArgumentNullException(nameof(value));
+ }
+
+ ///
+ /// 节点就绪时初始化控制器并安装效果集合。
+ ///
+ public override void _Ready()
+ {
+ if (EnableFrameworkEffects && !BbcodeEnabled)
+ {
+ BbcodeEnabled = true;
+ }
+
+ EnsureController().Initialize();
+ }
+
+ ///
+ /// 手动刷新框架效果集合。
+ /// 当调用方在运行时替换配置或切换动画开关时,可通过该方法同步宿主状态。
+ ///
+ public void RefreshFrameworkEffects()
+ {
+ if (EnableFrameworkEffects && !BbcodeEnabled)
+ {
+ BbcodeEnabled = true;
+ }
+
+ EnsureController().RefreshEffects();
+ }
+
+ ///
+ /// 获取或创建控制器实例。
+ ///
+ /// 组合式装配控制器。
+ private RichTextEffectsController EnsureController()
+ {
+ return _effectsController ??= new RichTextEffectsController(
+ this,
+ EffectRegistry,
+ () => Profile,
+ () => EnableFrameworkEffects,
+ () => AnimatedEffectsEnabled);
+ }
+}
diff --git a/GFramework.Godot/Text/IRichTextEffectRegistry.cs b/GFramework.Godot/Text/IRichTextEffectRegistry.cs
new file mode 100644
index 00000000..8d5a29c1
--- /dev/null
+++ b/GFramework.Godot/Text/IRichTextEffectRegistry.cs
@@ -0,0 +1,23 @@
+namespace GFramework.Godot.Text;
+
+///
+/// 富文本效果注册表,负责把配置中的效果键解析为可安装的 实例。
+///
+public interface IRichTextEffectRegistry
+{
+ ///
+ /// 根据指定配置创建完整的效果实例集合。
+ ///
+ /// 效果组合配置。
+ /// 当前是否允许字符级动态效果生效。
+ /// 可直接写入 的效果实例集合。
+ IReadOnlyList CreateEffects(RichTextProfile profile, bool animatedEffectsEnabled);
+
+ ///
+ /// 根据单个效果键创建对应效果实例。
+ ///
+ /// 效果键。
+ /// 当前是否允许字符级动态效果生效。
+ /// 解析成功时返回效果实例;否则返回 。
+ RichTextEffect? CreateEffect(string key, bool animatedEffectsEnabled);
+}
diff --git a/GFramework.Godot/Text/RichTextEffectEntry.cs b/GFramework.Godot/Text/RichTextEffectEntry.cs
new file mode 100644
index 00000000..63e30d3d
--- /dev/null
+++ b/GFramework.Godot/Text/RichTextEffectEntry.cs
@@ -0,0 +1,22 @@
+namespace GFramework.Godot.Text;
+
+///
+/// 描述一条富文本效果配置项。
+/// 该资源只负责声明需要启用的效果键与开关状态,不承担实例创建逻辑。
+///
+[GlobalClass]
+public partial class RichTextEffectEntry : Resource
+{
+ ///
+ /// 获取或设置效果键。
+ /// 键值由 解析为具体的 实例。
+ ///
+ [Export]
+ public string Key { get; set; } = string.Empty;
+
+ ///
+ /// 获取或设置该配置项是否启用。
+ ///
+ [Export]
+ public bool Enabled { get; set; } = true;
+}
diff --git a/GFramework.Godot/Text/RichTextEffectsController.cs b/GFramework.Godot/Text/RichTextEffectsController.cs
new file mode 100644
index 00000000..2a9eeb78
--- /dev/null
+++ b/GFramework.Godot/Text/RichTextEffectsController.cs
@@ -0,0 +1,70 @@
+using Array = Godot.Collections.Array;
+
+namespace GFramework.Godot.Text;
+
+///
+/// 负责把配置、开关和注册表装配为宿主标签的实际效果集合。
+/// 该控制器是组合式扩展的装配中心,使 保持轻量。
+///
+internal sealed class RichTextEffectsController
+{
+ private readonly Func _animatedEffectsEnabledAccessor;
+ private readonly Func _frameworkEffectsEnabledAccessor;
+ private readonly RichTextLabel _host;
+ private readonly Func _profileAccessor;
+ private readonly IRichTextEffectRegistry _registry;
+
+ ///
+ /// 初始化控制器实例。
+ ///
+ /// 目标富文本标签。
+ /// 效果注册表。
+ /// 当前配置访问器。
+ /// 框架效果总开关访问器。
+ /// 字符动画开关访问器。
+ public RichTextEffectsController(
+ RichTextLabel host,
+ IRichTextEffectRegistry registry,
+ Func profileAccessor,
+ Func frameworkEffectsEnabledAccessor,
+ Func animatedEffectsEnabledAccessor)
+ {
+ _host = host ?? throw new ArgumentNullException(nameof(host));
+ _registry = registry ?? throw new ArgumentNullException(nameof(registry));
+ _profileAccessor = profileAccessor ?? throw new ArgumentNullException(nameof(profileAccessor));
+ _frameworkEffectsEnabledAccessor = frameworkEffectsEnabledAccessor
+ ?? throw new ArgumentNullException(nameof(frameworkEffectsEnabledAccessor));
+ _animatedEffectsEnabledAccessor = animatedEffectsEnabledAccessor
+ ?? throw new ArgumentNullException(nameof(animatedEffectsEnabledAccessor));
+ }
+
+ ///
+ /// 初始化并立即刷新宿主标签的效果集合。
+ ///
+ public void Initialize()
+ {
+ RefreshEffects();
+ }
+
+ ///
+ /// 根据当前配置和开关重建宿主标签上的 。
+ ///
+ public void RefreshEffects()
+ {
+ if (!_frameworkEffectsEnabledAccessor())
+ {
+ _host.CustomEffects = new Array();
+ return;
+ }
+
+ var profile = _profileAccessor() ?? RichTextProfile.CreateBuiltInDefault();
+ var effects = _registry.CreateEffects(profile, _animatedEffectsEnabledAccessor());
+ var customEffects = new Array();
+ foreach (var effect in effects)
+ {
+ customEffects.Add(effect);
+ }
+
+ _host.CustomEffects = customEffects;
+ }
+}
diff --git a/GFramework.Godot/Text/RichTextMarkup.cs b/GFramework.Godot/Text/RichTextMarkup.cs
new file mode 100644
index 00000000..e08ec667
--- /dev/null
+++ b/GFramework.Godot/Text/RichTextMarkup.cs
@@ -0,0 +1,128 @@
+using System.Globalization;
+using System.Text;
+
+namespace GFramework.Godot.Text;
+
+///
+/// 提供语义化的富文本标签构建辅助方法。
+/// 该工具层用于减少业务代码直接手写原始 BBCode 字符串的重复工作。
+///
+public static class RichTextMarkup
+{
+ ///
+ /// 使用指定标签包裹文本。
+ ///
+ /// 原始文本。
+ /// 标签名。
+ /// 包裹后的 BBCode 文本。
+ public static string Color(string text, string tag)
+ {
+ return Wrap(text, tag);
+ }
+
+ ///
+ /// 使用 `green` 标签包裹文本。
+ ///
+ /// 原始文本。
+ /// 包裹后的 BBCode 文本。
+ public static string Green(string text)
+ {
+ return Wrap(text, "green");
+ }
+
+ ///
+ /// 使用 `red` 标签包裹文本。
+ ///
+ /// 原始文本。
+ /// 包裹后的 BBCode 文本。
+ public static string Red(string text)
+ {
+ return Wrap(text, "red");
+ }
+
+ ///
+ /// 使用 `gold` 标签包裹文本。
+ ///
+ /// 原始文本。
+ /// 包裹后的 BBCode 文本。
+ public static string Gold(string text)
+ {
+ return Wrap(text, "gold");
+ }
+
+ ///
+ /// 使用 `blue` 标签包裹文本。
+ ///
+ /// 原始文本。
+ /// 包裹后的 BBCode 文本。
+ public static string Blue(string text)
+ {
+ return Wrap(text, "blue");
+ }
+
+ ///
+ /// 使用指定效果标签包裹文本,并可附带参数环境。
+ ///
+ /// 原始文本。
+ /// 标签名。
+ /// 可选的标签参数集合。
+ /// 包裹后的 BBCode 文本。
+ public static string Effect(string text, string tag, IReadOnlyDictionary? env = null)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(tag);
+
+ var builder = new StringBuilder();
+ builder.Append('[');
+ builder.Append(tag);
+
+ if (env is not null)
+ {
+ foreach (var pair in env)
+ {
+ if (string.IsNullOrWhiteSpace(pair.Key) || pair.Value is null)
+ {
+ continue;
+ }
+
+ builder.Append(' ');
+ builder.Append(pair.Key);
+ builder.Append('=');
+ builder.Append(FormatValue(pair.Value));
+ }
+ }
+
+ builder.Append(']');
+ builder.Append(text ?? string.Empty);
+ builder.Append("[/");
+ builder.Append(tag);
+ builder.Append(']');
+ return builder.ToString();
+ }
+
+ ///
+ /// 使用指定标签包裹文本。
+ ///
+ /// 原始文本。
+ /// 标签名。
+ /// 包裹后的 BBCode 文本。
+ private static string Wrap(string text, string tag)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(tag);
+ return $"[{tag}]{text ?? string.Empty}[/{tag}]";
+ }
+
+ ///
+ /// 将标签参数值格式化为稳定的 BBCode 字符串表示。
+ ///
+ /// 待格式化的值。
+ /// 适用于 BBCode 参数的字符串。
+ private static string FormatValue(object value)
+ {
+ return value switch
+ {
+ string text => text,
+ IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
+ _ => value.ToString() ?? string.Empty
+ };
+ }
+}
diff --git a/GFramework.Godot/Text/RichTextProfile.cs b/GFramework.Godot/Text/RichTextProfile.cs
new file mode 100644
index 00000000..d61d3f56
--- /dev/null
+++ b/GFramework.Godot/Text/RichTextProfile.cs
@@ -0,0 +1,37 @@
+namespace GFramework.Godot.Text;
+
+///
+/// 描述一个富文本效果组合配置。
+/// 该资源是组合式扩展的核心载体,用于声明宿主标签需要安装的效果集合。
+///
+[GlobalClass]
+public partial class RichTextProfile : Resource
+{
+ ///
+ /// 获取或设置当前配置启用的效果条目集合。
+ ///
+ [Export]
+ public RichTextEffectEntry[] Effects { get; set; } = [];
+
+ ///
+ /// 创建包含全部内置效果的默认配置。
+ /// 该方法为第一阶段提供零配置可用的回退组合。
+ ///
+ /// 包含全部内置效果键的默认配置。
+ public static RichTextProfile CreateBuiltInDefault()
+ {
+ var profile = new RichTextProfile();
+ profile.Effects =
+ [
+ new RichTextEffectEntry { Key = "green" },
+ new RichTextEffectEntry { Key = "red" },
+ new RichTextEffectEntry { Key = "gold" },
+ new RichTextEffectEntry { Key = "blue" },
+ new RichTextEffectEntry { Key = "fade_in" },
+ new RichTextEffectEntry { Key = "sine" },
+ new RichTextEffectEntry { Key = "jitter" },
+ new RichTextEffectEntry { Key = "fly_in" }
+ ];
+ return profile;
+ }
+}