-
Notifications
You must be signed in to change notification settings - Fork 3
feat(text): 添加富文本效果系统和颜色标记功能 #251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
e5ad293
22882f6
1c2a813
f3d50c6
1665e72
5cb5a22
1145f45
11515ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| using GFramework.Godot.Text; | ||
| using Array = Godot.Collections.Array; | ||
|
|
||
| namespace GFramework.Godot.Tests.Text; | ||
|
|
||
| /// <summary> | ||
| /// <see cref="RichTextEffectsController" /> 的纯托管行为测试。 | ||
| /// </summary> | ||
| [TestFixture] | ||
| public sealed class RichTextEffectsControllerTests | ||
| { | ||
| /// <summary> | ||
| /// 验证启用框架效果时会开启宿主 BBCode,并在 Profile 为空时回退到内置默认配置。 | ||
| /// </summary> | ||
| [Test] | ||
| public void RefreshEffects_Should_Enable_Bbcode_And_Use_BuiltIn_Default_Profile() | ||
| { | ||
| var host = new FakeRichTextEffectHost(); | ||
| var registry = new RecordingRegistry(); | ||
| var controller = new RichTextEffectsController( | ||
| host, | ||
| () => registry, | ||
| () => null, | ||
| () => true, | ||
| () => false); | ||
|
|
||
| controller.RefreshEffects(); | ||
|
|
||
| Assert.That(host.BbcodeEnabled, Is.True); | ||
| Assert.That(registry.CapturedAnimatedEffectsEnabled, Has.Count.EqualTo(1)); | ||
| Assert.That(registry.CapturedAnimatedEffectsEnabled[0], Is.False); | ||
| Assert.That(registry.CapturedProfiles, Has.Count.EqualTo(1)); | ||
| Assert.That(registry.CapturedProfiles[0].Effects.Select(static entry => entry.Key), Is.EqualTo(new[] | ||
| { | ||
| "green", | ||
| "red", | ||
| "gold", | ||
| "blue", | ||
| "fade_in", | ||
| "sine", | ||
| "jitter", | ||
| "fly_in" | ||
| })); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// 验证关闭框架效果时不会调用注册表,并会清空宿主的自定义效果集合。 | ||
| /// </summary> | ||
| [Test] | ||
| public void RefreshEffects_Should_Clear_CustomEffects_When_Framework_Effects_Are_Disabled() | ||
| { | ||
| var existingEffects = new Array(); | ||
| existingEffects.Add("placeholder"); | ||
|
|
||
| var host = new FakeRichTextEffectHost | ||
| { | ||
| BbcodeEnabled = true, | ||
| CustomEffects = existingEffects | ||
| }; | ||
| var registry = new RecordingRegistry(); | ||
| var controller = new RichTextEffectsController( | ||
| host, | ||
| () => registry, | ||
| () => RichTextProfile.CreateBuiltInDefault(), | ||
| () => false, | ||
| () => true); | ||
|
|
||
| controller.RefreshEffects(); | ||
|
|
||
| Assert.That(host.BbcodeEnabled, Is.True); | ||
| Assert.That(host.CustomEffects.Count, Is.EqualTo(0)); | ||
| Assert.That(registry.CapturedProfiles, Is.Empty); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// 验证控制器会在每次刷新时读取最新的注册表访问器结果,避免缓存旧注册表。 | ||
| /// </summary> | ||
| [Test] | ||
| public void RefreshEffects_Should_Use_The_Current_Registry_From_Accessor() | ||
| { | ||
| var host = new FakeRichTextEffectHost(); | ||
| var firstRegistry = new RecordingRegistry(); | ||
| var secondRegistry = new RecordingRegistry(); | ||
| IRichTextEffectRegistry currentRegistry = firstRegistry; | ||
|
|
||
| var controller = new RichTextEffectsController( | ||
| host, | ||
| () => currentRegistry, | ||
| () => RichTextProfile.CreateBuiltInDefault(), | ||
| () => true, | ||
| () => true); | ||
|
|
||
| controller.RefreshEffects(); | ||
| currentRegistry = secondRegistry; | ||
| controller.RefreshEffects(); | ||
|
|
||
| Assert.That(firstRegistry.CapturedProfiles, Has.Count.EqualTo(1)); | ||
| Assert.That(secondRegistry.CapturedProfiles, Has.Count.EqualTo(1)); | ||
| } | ||
|
|
||
| private sealed class FakeRichTextEffectHost : IRichTextEffectHost | ||
| { | ||
| public bool BbcodeEnabled { get; set; } | ||
|
|
||
| public Array CustomEffects { get; set; } = new(); | ||
| } | ||
|
|
||
| private sealed class RecordingRegistry : IRichTextEffectRegistry | ||
|
Check failure on line 108 in GFramework.Godot.Tests/Text/RichTextEffectsControllerTests.cs
|
||
| { | ||
| public List<RichTextProfile> CapturedProfiles { get; } = []; | ||
|
|
||
| public List<bool> CapturedAnimatedEffectsEnabled { get; } = []; | ||
|
|
||
| public IReadOnlyList<RichTextEffect> CreateEffects(RichTextProfile profile, bool animatedEffectsEnabled) | ||
|
Check failure on line 114 in GFramework.Godot.Tests/Text/RichTextEffectsControllerTests.cs
|
||
| { | ||
| CapturedProfiles.Add(profile); | ||
| CapturedAnimatedEffectsEnabled.Add(animatedEffectsEnabled); | ||
| return Array.Empty<RichTextEffect>(); | ||
| } | ||
|
|
||
| public RichTextEffect? CreateEffect(string key, bool animatedEffectsEnabled) | ||
|
Check failure on line 121 in GFramework.Godot.Tests/Text/RichTextEffectsControllerTests.cs
|
||
| { | ||
| return null; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| using GFramework.Godot.Text; | ||
|
|
||
| namespace GFramework.Godot.Tests.Text; | ||
|
|
||
| /// <summary> | ||
| /// <see cref="RichTextMarkup" /> 的测试。 | ||
| /// </summary> | ||
| [TestFixture] | ||
| public sealed class RichTextMarkupTests | ||
| { | ||
| /// <summary> | ||
| /// 验证颜色快捷方法会输出预期标签。 | ||
| /// </summary> | ||
| [Test] | ||
| public void Green_Should_Wrap_Text_With_Green_Tag() | ||
| { | ||
| var result = RichTextMarkup.Green("Ready"); | ||
|
|
||
| Assert.That(result, Is.EqualTo("[green]Ready[/green]")); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// 验证效果方法会按稳定顺序拼接环境参数。 | ||
| /// </summary> | ||
| [Test] | ||
| public void Effect_Should_Sort_Environment_Parameters_By_Key() | ||
| { | ||
| var env = new Dictionary<string, object?> | ||
| { | ||
| ["tick"] = 0.1f, | ||
| ["speed"] = 4 | ||
| }; | ||
|
|
||
| var result = RichTextMarkup.Effect("Hello", "fade_in", env); | ||
|
|
||
| Assert.That(result, Is.EqualTo("[fade_in speed=4 tick=0.1]Hello[/fade_in]")); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// 验证非法标签 token 会被拒绝,避免生成损坏的 BBCode。 | ||
| /// </summary> | ||
| [Test] | ||
| public void Effect_Should_Reject_Invalid_Tag_Tokens() | ||
| { | ||
| var exception = Assert.Throws<ArgumentException>(() => RichTextMarkup.Effect("Hello", "fade=in")); | ||
|
|
||
| Assert.That(exception!.ParamName, Is.EqualTo("tag")); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// 验证非法环境参数键会被拒绝,避免注入无效的 BBCode token。 | ||
| /// </summary> | ||
| [Test] | ||
| public void Effect_Should_Reject_Invalid_Environment_Key_Tokens() | ||
| { | ||
| var env = new Dictionary<string, object?> | ||
| { | ||
| ["bad key"] = 1 | ||
| }; | ||
|
|
||
| var exception = Assert.Throws<ArgumentException>(() => RichTextMarkup.Effect("Hello", "fade_in", env)); | ||
|
|
||
| Assert.That(exception!.ParamName, Is.EqualTo("env")); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| using GFramework.Godot.Text; | ||
|
|
||
| namespace GFramework.Godot.Tests.Text; | ||
|
|
||
| /// <summary> | ||
| /// <see cref="RichTextProfile" /> 的测试。 | ||
| /// </summary> | ||
| [TestFixture] | ||
| public sealed class RichTextProfileTests | ||
| { | ||
| /// <summary> | ||
| /// 验证默认内置配置会暴露完整的第一阶段效果键集合。 | ||
| /// </summary> | ||
| [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" | ||
| })); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| using GFramework.Godot.Text.Effects; | ||
|
|
||
| namespace GFramework.Godot.Text; | ||
|
|
||
| /// <summary> | ||
| /// 默认的富文本效果注册表。 | ||
| /// 该实现仅负责内置效果键的解析,不处理业务层文本构建或配置持久化。 | ||
| /// </summary> | ||
| public sealed class DefaultRichTextEffectRegistry : IRichTextEffectRegistry | ||
| { | ||
| /// <summary> | ||
| /// 创建当前配置对应的全部效果实例。 | ||
| /// </summary> | ||
| /// <param name="profile">效果组合配置。</param> | ||
| /// <param name="animatedEffectsEnabled">当前是否允许字符级动态效果生效。</param> | ||
| /// <returns>内置效果实例集合。</returns> | ||
| /// <exception cref="ArgumentNullException"> | ||
| /// 当 <paramref name="profile" /> 为 <see langword="null" /> 时抛出。 | ||
| /// </exception> | ||
| public IReadOnlyList<RichTextEffect> CreateEffects(RichTextProfile profile, bool animatedEffectsEnabled) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(profile); | ||
|
|
||
| var effects = new List<RichTextEffect>(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; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// 根据效果键创建单个效果实例。 | ||
| /// </summary> | ||
| /// <param name="key">效果键。</param> | ||
| /// <param name="animatedEffectsEnabled">当前是否允许字符级动态效果生效。</param> | ||
| /// <returns>解析成功时返回效果实例;否则返回 <see langword="null" />。</returns> | ||
| 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 | ||
| }; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| namespace GFramework.Godot.Text.Effects; | ||
|
|
||
| /// <summary> | ||
| /// 为文本应用蓝色语义高亮。 | ||
| /// </summary> | ||
| [GlobalClass] | ||
| [Tool] | ||
| public partial class RichTextBlueEffect : RichTextEffectBase | ||
| { | ||
| private static readonly Color BlueColor = new(0.44f, 0.72f, 0.98f, 1.0f); | ||
|
|
||
| /// <summary> | ||
| /// 获取标签名。 | ||
| /// </summary> | ||
| protected override string TagName => "blue"; | ||
|
|
||
| /// <summary> | ||
| /// 应用蓝色颜色效果。 | ||
| /// </summary> | ||
| /// <param name="charFx">当前字符上下文。</param> | ||
| /// <returns>始终返回 <see langword="true" />。</returns> | ||
| public override bool _ProcessCustomFX(CharFXTransform charFx) | ||
| { | ||
| charFx.Color = BlueColor; | ||
| return true; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.