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; + } +}