diff --git a/osu.Framework.Font.Tests/Graphics/Sprites/LyricSpriteTextTest.cs b/osu.Framework.Font.Tests/Graphics/Sprites/LyricSpriteTextTest.cs index a838576..91e34cc 100644 --- a/osu.Framework.Font.Tests/Graphics/Sprites/LyricSpriteTextTest.cs +++ b/osu.Framework.Font.Tests/Graphics/Sprites/LyricSpriteTextTest.cs @@ -9,6 +9,18 @@ namespace osu.Framework.Font.Tests.Graphics.Sprites { public class LyricSpriteTextTest { + [TestCase("カラオケ", new[] { "[0,1]:か" }, new[] { "[0,1]:か" })] + [TestCase("カラオケ", new[] { "[0,1]:", "[0,1]:" }, new string[] { })] // will filter those empty text time-tags. + [TestCase("カラオケ", new[] { "[0,1]:か", "[0,1]:か" }, new[] { "[0,1]:か" })] // will filter the duplicated + [TestCase("カラオケ", new[] { "[0,1]:か", "[0,1]:ら" }, new[] { "[0,1]:か", "[0,1]:ら" })] // will not filter even if index are same. + [TestCase("カラオケ", new[] { "[0,1]:か", "[1,0]:か" }, new[] { "[0,1]:か" })] // will give it a fix and filter the duplicated. + public void TestGetFixedPositionTexts(string lyric, string[] positionTexts, string[] fixedPositionTexts) + { + var expected = TestCaseTagHelper.ParsePositionTexts(fixedPositionTexts); + var actual = LyricSpriteText.GetFixedPositionTexts(TestCaseTagHelper.ParsePositionTexts(positionTexts), lyric); + Assert.AreEqual(expected, actual); + } + [TestCase("カラオケ", "[0,1]:か", "[0,1]:か")] [TestCase("カラオケ", "[3,4]:か", "[3,4]:か")] [TestCase("カラオケ", "[-1,1]:か", "[0,1]:か")] // fix out of range issue. diff --git a/osu.Framework.Font.Tests/Helper/TestCaseTagHelper.cs b/osu.Framework.Font.Tests/Helper/TestCaseTagHelper.cs index 430143d..ad295a9 100644 --- a/osu.Framework.Font.Tests/Helper/TestCaseTagHelper.cs +++ b/osu.Framework.Font.Tests/Helper/TestCaseTagHelper.cs @@ -62,7 +62,7 @@ public static Tuple ParseTimeTag(string str) return new Tuple(time, new TextIndex(index, state)); } - public static PositionText[] ParseParsePositionTexts(IEnumerable strings) + public static PositionText[] ParsePositionTexts(IEnumerable strings) => strings?.Select(ParsePositionText).ToArray(); public static IReadOnlyDictionary ParseTimeTags(IEnumerable strings) diff --git a/osu.Framework.Font.Tests/Text/PositionTextBuilderTest.cs b/osu.Framework.Font.Tests/Text/PositionTextBuilderTest.cs deleted file mode 100644 index d7c2be2..0000000 --- a/osu.Framework.Font.Tests/Text/PositionTextBuilderTest.cs +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright (c) karaoke.dev . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using NUnit.Framework; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; -using osu.Framework.Text; -using osuTK; - -namespace osu.Framework.Font.Tests.Text -{ - [TestFixture] - public class PositionTextBuilderTest - { - private const float font_size = 1; - - private const float x_offset = 1; - private const float y_offset = 2; - private const float x_advance = 3; - private const float width = 4; - private const float baseline = 5; - private const float height = 6; - private const float kerning = -7; - - private const float b_x_offset = 8; - private const float b_y_offset = 9; - private const float b_x_advance = 10; - private const float b_width = 11; - private const float b_baseline = 12; - private const float b_height = 13; - private const float b_kerning = -14; - - private const float m_x_offset = 15; - private const float m_y_offset = 16; - private const float m_x_advance = 17; - private const float m_width = 18; - private const float m_baseline = 19; - private const float m_height = 20; - private const float m_kerning = -21; - - private static readonly Vector2 spacing = new(19, 20); - - private static readonly TestFontUsage normal_font = new("test"); - private static readonly TestFontUsage fixed_width_font = new("test-fixedwidth", fixedWidth: true); - - private readonly TestStore fontStore; - - public PositionTextBuilderTest() - { - fontStore = new TestStore( - new GlyphEntry(normal_font, new TestGlyph('カ', x_offset, y_offset, x_advance, width, baseline, height, kerning)), - new GlyphEntry(normal_font, new TestGlyph('ラ', b_x_offset, b_y_offset, b_x_advance, b_width, b_baseline, b_height, b_kerning)), - new GlyphEntry(normal_font, new TestGlyph('オ', m_x_offset, m_y_offset, m_x_advance, m_width, m_baseline, m_height, m_kerning)), - new GlyphEntry(fixed_width_font, new TestGlyph('ケ', x_offset, y_offset, x_advance, width, baseline, height, kerning)), - new GlyphEntry(fixed_width_font, new TestGlyph('か', b_x_offset, b_y_offset, b_x_advance, b_width, b_baseline, b_height, b_kerning)), - new GlyphEntry(fixed_width_font, new TestGlyph('ら', m_x_offset, m_y_offset, m_x_advance, m_width, m_baseline, m_height, m_kerning)), - new GlyphEntry(fixed_width_font, new TestGlyph('お', x_offset, y_offset, x_advance, width, baseline, height, kerning)), - new GlyphEntry(fixed_width_font, new TestGlyph('け', b_x_offset, b_y_offset, b_x_advance, b_width, b_baseline, b_height, b_kerning)), - new GlyphEntry(fixed_width_font, new TestGlyph('k', m_x_offset, m_y_offset, m_x_advance, m_width, m_baseline, m_height, m_kerning)), - new GlyphEntry(fixed_width_font, new TestGlyph('a', x_offset, y_offset, x_advance, width, baseline, height, kerning)), - new GlyphEntry(fixed_width_font, new TestGlyph('r', b_x_offset, b_y_offset, b_x_advance, b_width, b_baseline, b_height, b_kerning)), - new GlyphEntry(fixed_width_font, new TestGlyph('o', m_x_offset, m_y_offset, m_x_advance, m_width, m_baseline, m_height, m_kerning)), - new GlyphEntry(fixed_width_font, new TestGlyph('e', x_offset, y_offset, x_advance, width, baseline, height, kerning)) - ); - } - - private List characterList; - - [SetUp] - public void SetUp() - { - var mainTextBuilder = new TextBuilder(fontStore, normal_font); - mainTextBuilder.AddText("カラオケ"); - mainTextBuilder.AddText("karaoke"); - characterList = mainTextBuilder.Characters; - } - - [TestCase('か', true)] - [TestCase('A', false)] - public void TestAddPositionTextHasChar(char c, bool equal) - { - var builder = new PositionTextBuilder(fontStore, normal_font, characterList: characterList); - builder.AddText(new PositionText(c.ToString(), 0, 1)); - - var character = builder.Characters.LastOrDefault(); - - if (equal) - { - Assert.AreEqual(character.Character, c); - } - else - { - // will not render character if not contain this char in font store. - Assert.AreNotEqual(character.Character, c); - } - } - - [TestCase('か', 5.5f, 12.5f)] - [TestCase('ら', 9.0f, 12.5f)] - public void TestAddPositionTextPosition(char c, float x, float y) - { - var builder = new PositionTextBuilder(fontStore, normal_font, characterList: characterList); - builder.AddText(new PositionText(c.ToString(), 0, 1)); - - var character = builder.Characters.LastOrDefault(); - var topLeftPosition = character.DrawRectangle.TopLeft; - Assert.AreEqual(topLeftPosition.X, x); - Assert.AreEqual(topLeftPosition.Y, y); - } - - private readonly struct TestFontUsage - { - private readonly string family; - private readonly string weight; - private readonly bool italics; - private readonly bool fixedWidth; - - public TestFontUsage(string family = null, string weight = null, bool italics = false, bool fixedWidth = false) - { - this.family = family; - this.weight = weight; - this.italics = italics; - this.fixedWidth = fixedWidth; - } - - public static implicit operator FontUsage(TestFontUsage tfu) - => new(tfu.family, font_size, tfu.weight, tfu.italics, tfu.fixedWidth); - } - - private class TestStore : ITexturedGlyphLookupStore - { - private readonly GlyphEntry[] glyphs; - - public TestStore(params GlyphEntry[] glyphs) - { - this.glyphs = glyphs; - } - - public ITexturedCharacterGlyph Get(string fontName, char character) - { - if (string.IsNullOrEmpty(fontName)) - { - return glyphs.FirstOrDefault(g => g.Glyph.Character == character).Glyph; - } - - return glyphs.FirstOrDefault(g => g.Font.FontName == fontName && g.Glyph.Character == character).Glyph; - } - - public Task GetAsync(string fontName, char character) => throw new NotImplementedException(); - } - - private readonly struct GlyphEntry - { - public readonly FontUsage Font; - public readonly ITexturedCharacterGlyph Glyph; - - public GlyphEntry(FontUsage font, ITexturedCharacterGlyph glyph) - { - Font = font; - Glyph = glyph; - } - } - - private readonly struct TestGlyph : ITexturedCharacterGlyph - { - public Texture Texture => new(1, 1); - public float XOffset { get; } - public float YOffset { get; } - public float XAdvance { get; } - public float Width { get; } - - public float Baseline { get; } - public float Height { get; } - - public char Character { get; } - - private readonly float glyphKerning; - - public TestGlyph(char character, float xOffset, float yOffset, float xAdvance, float width, float baseline, float height, float kerning) - { - glyphKerning = kerning; - Character = character; - XOffset = xOffset; - YOffset = yOffset; - XAdvance = xAdvance; - Width = width; - Baseline = baseline; - Height = height; - } - - public float GetKerning(T lastGlyph) - where T : ICharacterGlyph - => glyphKerning; - } - } -} diff --git a/osu.Framework.Font.Tests/Visual/Sprites/TestSceneKaraokeSpriteText.cs b/osu.Framework.Font.Tests/Visual/Sprites/TestSceneKaraokeSpriteText.cs index 8af9a4d..1b35990 100644 --- a/osu.Framework.Font.Tests/Visual/Sprites/TestSceneKaraokeSpriteText.cs +++ b/osu.Framework.Font.Tests/Visual/Sprites/TestSceneKaraokeSpriteText.cs @@ -36,8 +36,8 @@ public TestSceneKaraokeSpriteText() Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = "カラオケ!", - Rubies = TestCaseTagHelper.ParseParsePositionTexts(new[] { "[0,1]:か", "[2,3]:お" }), - Romajies = TestCaseTagHelper.ParseParsePositionTexts(new[] { "[1,2]:ra", "[3,4]:ke" }), + Rubies = TestCaseTagHelper.ParsePositionTexts(new[] { "[0,1]:か", "[2,3]:お" }), + Romajies = TestCaseTagHelper.ParsePositionTexts(new[] { "[1,2]:ra", "[3,4]:ke" }), LeftTextColour = Color4.Green, RightTextColour = Color4.Red, Scale = new Vector2(2), @@ -96,7 +96,7 @@ public void TestRuby(string[] rubyTags, bool boo) { AddStep("Change ruby", () => { - var ruby = TestCaseTagHelper.ParseParsePositionTexts(rubyTags); + var ruby = TestCaseTagHelper.ParsePositionTexts(rubyTags); karaokeSpriteText.Rubies = ruby; }); } @@ -106,7 +106,7 @@ public void TestRomaji(string[] romajiTags, bool boo) { AddStep("Change romaji", () => { - var romajies = TestCaseTagHelper.ParseParsePositionTexts(romajiTags); + var romajies = TestCaseTagHelper.ParsePositionTexts(romajiTags); karaokeSpriteText.Romajies = romajies; }); } diff --git a/osu.Framework.Font.Tests/Visual/Sprites/TestSceneKaraokeSpriteTextWithShader.cs b/osu.Framework.Font.Tests/Visual/Sprites/TestSceneKaraokeSpriteTextWithShader.cs index 541f410..957e67d 100644 --- a/osu.Framework.Font.Tests/Visual/Sprites/TestSceneKaraokeSpriteTextWithShader.cs +++ b/osu.Framework.Font.Tests/Visual/Sprites/TestSceneKaraokeSpriteTextWithShader.cs @@ -26,8 +26,8 @@ public TestSceneKaraokeSpriteTextWithShader() Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = "カラオケ!", - Rubies = TestCaseTagHelper.ParseParsePositionTexts(new[] { "[0,1]:か", "[1,2]:ら", "[2,3]:お", "[3,4]:け" }), - Romajies = TestCaseTagHelper.ParseParsePositionTexts(new[] { "[0,1]:ka", "[1,2]:ra", "[2,3]:o", "[3,4]:ke" }), + Rubies = TestCaseTagHelper.ParsePositionTexts(new[] { "[0,1]:か", "[1,2]:ら", "[2,3]:お", "[3,4]:け" }), + Romajies = TestCaseTagHelper.ParsePositionTexts(new[] { "[0,1]:ka", "[1,2]:ra", "[2,3]:o", "[3,4]:ke" }), TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { "[0,start]:500", "[1,start]:600", "[2,start]:1000", "[3,start]:1500", "[4,start]:2000" }), Scale = new Vector2(2), LeftTextColour = Color4.Green, diff --git a/osu.Framework.Font.Tests/Visual/Sprites/TestSceneLyricSpriteText.cs b/osu.Framework.Font.Tests/Visual/Sprites/TestSceneLyricSpriteText.cs index d1d48a0..72252aa 100644 --- a/osu.Framework.Font.Tests/Visual/Sprites/TestSceneLyricSpriteText.cs +++ b/osu.Framework.Font.Tests/Visual/Sprites/TestSceneLyricSpriteText.cs @@ -29,8 +29,8 @@ public void TestText(string text, string[] rubyTags, string[] romajiTags) AddStep("Create lyric", () => setContents(() => new LyricSpriteText { Text = text, - Rubies = TestCaseTagHelper.ParseParsePositionTexts(rubyTags), - Romajies = TestCaseTagHelper.ParseParsePositionTexts(romajiTags), + Rubies = TestCaseTagHelper.ParsePositionTexts(rubyTags), + Romajies = TestCaseTagHelper.ParsePositionTexts(romajiTags), })); } @@ -238,12 +238,12 @@ public DefaultLyricSpriteText(bool ruby = true, bool romaji = true) if (ruby) { - Rubies = TestCaseTagHelper.ParseParsePositionTexts(new[] { "[0,1]:か", "[1,2]:ら", "[2,3]:お", "[3,4]:け" }); + Rubies = TestCaseTagHelper.ParsePositionTexts(new[] { "[0,1]:か", "[1,2]:ら", "[2,3]:お", "[3,4]:け" }); } if (romaji) { - Romajies = TestCaseTagHelper.ParseParsePositionTexts(new[] { "[0,1]:ka", "[1,2]:ra", "[2,3]:o", "[3,4]:ke" }); + Romajies = TestCaseTagHelper.ParsePositionTexts(new[] { "[0,1]:ka", "[1,2]:ra", "[2,3]:o", "[3,4]:ke" }); } } } diff --git a/osu.Framework.Font.Tests/Visual/Sprites/TestSceneLyricSpriteTextCharacterPosition.cs b/osu.Framework.Font.Tests/Visual/Sprites/TestSceneLyricSpriteTextCharacterPosition.cs index 7815e5f..1fb0def 100644 --- a/osu.Framework.Font.Tests/Visual/Sprites/TestSceneLyricSpriteTextCharacterPosition.cs +++ b/osu.Framework.Font.Tests/Visual/Sprites/TestSceneLyricSpriteTextCharacterPosition.cs @@ -40,8 +40,8 @@ public TestSceneLyricSpriteTextCharacterPosition() Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = "カラオケyo-", - Rubies = TestCaseTagHelper.ParseParsePositionTexts(new[] { "[0,1]:か", "[1,2]:ら", "[2,3]:お", "[3,4]:け", "[4,5]:-" }), - Romajies = TestCaseTagHelper.ParseParsePositionTexts(new[] { "[0,1]:ka", "[1,2]:ra", "[2,3]:o", "[3,4]:ke", "[4,5]:yo" }), + Rubies = TestCaseTagHelper.ParsePositionTexts(new[] { "[0,1]:か", "[1,2]:ら", "[2,3]:お", "[3,4]:け", "[4,5]:-" }), + Romajies = TestCaseTagHelper.ParsePositionTexts(new[] { "[0,1]:ka", "[1,2]:ra", "[2,3]:o", "[3,4]:ke", "[4,5]:yo" }), }, } }; diff --git a/osu.Framework.Font.Tests/Visual/Sprites/TestSceneLyricSpriteTextWithColour.cs b/osu.Framework.Font.Tests/Visual/Sprites/TestSceneLyricSpriteTextWithColour.cs index f8c3292..67de0e9 100644 --- a/osu.Framework.Font.Tests/Visual/Sprites/TestSceneLyricSpriteTextWithColour.cs +++ b/osu.Framework.Font.Tests/Visual/Sprites/TestSceneLyricSpriteTextWithColour.cs @@ -27,8 +27,8 @@ public TestSceneLyricSpriteTextWithColour() Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = "カラオケ", - Rubies = TestCaseTagHelper.ParseParsePositionTexts(new[] { "[0,1]:か", "[1,2]:ら", "[2,3]:お", "[3,4]:け" }), - Romajies = TestCaseTagHelper.ParseParsePositionTexts(new[] { "[0,1]:ka", "[1,2]:ra", "[2,3]:o", "[3,4]:ke" }), + Rubies = TestCaseTagHelper.ParsePositionTexts(new[] { "[0,1]:か", "[1,2]:ら", "[2,3]:お", "[3,4]:け" }), + Romajies = TestCaseTagHelper.ParsePositionTexts(new[] { "[0,1]:ka", "[1,2]:ra", "[2,3]:o", "[3,4]:ke" }), Y = 60, }, new BufferedContainer diff --git a/osu.Framework.Font.Tests/Visual/Sprites/TestSceneLyricSpriteTextWithShader.cs b/osu.Framework.Font.Tests/Visual/Sprites/TestSceneLyricSpriteTextWithShader.cs index cbbd801..df9b4b9 100644 --- a/osu.Framework.Font.Tests/Visual/Sprites/TestSceneLyricSpriteTextWithShader.cs +++ b/osu.Framework.Font.Tests/Visual/Sprites/TestSceneLyricSpriteTextWithShader.cs @@ -23,8 +23,8 @@ public TestSceneLyricSpriteTextWithShader() Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = "カラオケ", - Rubies = TestCaseTagHelper.ParseParsePositionTexts(new[] { "[0,1]:か", "[1,2]:ら", "[2,3]:お", "[3,4]:け" }), - Romajies = TestCaseTagHelper.ParseParsePositionTexts(new[] { "[0,1]:ka", "[1,2]:ra", "[2,3]:o", "[3,4]:ke" }), + Rubies = TestCaseTagHelper.ParsePositionTexts(new[] { "[0,1]:か", "[1,2]:ら", "[2,3]:お", "[3,4]:け" }), + Romajies = TestCaseTagHelper.ParsePositionTexts(new[] { "[0,1]:ka", "[1,2]:ra", "[2,3]:o", "[3,4]:ke" }), Scale = new Vector2(2) }; } diff --git a/osu.Framework.Font/Graphics/Sprites/LyricSpriteText.cs b/osu.Framework.Font/Graphics/Sprites/LyricSpriteText.cs index a7f8c04..7dfa4d3 100644 --- a/osu.Framework.Font/Graphics/Sprites/LyricSpriteText.cs +++ b/osu.Framework.Font/Graphics/Sprites/LyricSpriteText.cs @@ -43,6 +43,8 @@ public LyricSpriteText() AddLayout(parentScreenSpaceCache); AddLayout(localScreenSpaceCache); AddLayout(textBuilderCache); + AddLayout(rubyTextBuilderCache); + AddLayout(romajiTextBuilderCache); } [BackgroundDependencyLoader] diff --git a/osu.Framework.Font/Graphics/Sprites/LyricSpriteText_Characters.cs b/osu.Framework.Font/Graphics/Sprites/LyricSpriteText_Characters.cs index baeb234..d0f9240 100644 --- a/osu.Framework.Font/Graphics/Sprites/LyricSpriteText_Characters.cs +++ b/osu.Framework.Font/Graphics/Sprites/LyricSpriteText_Characters.cs @@ -33,11 +33,18 @@ public partial class LyricSpriteText protected virtual char FallbackCharacter => '?'; private readonly LayoutValue textBuilderCache = new LayoutValue(Invalidation.DrawSize, InvalidationSource.Parent); + private readonly LayoutValue rubyTextBuilderCache = new LayoutValue(Invalidation.DrawSize, InvalidationSource.Parent); + private readonly LayoutValue romajiTextBuilderCache = new LayoutValue(Invalidation.DrawSize, InvalidationSource.Parent); /// /// Invalidates the current , causing a new one to be created next time it's required via . /// - protected void InvalidateTextBuilder() => textBuilderCache.Invalidate(); + protected void InvalidateTextBuilder() + { + textBuilderCache.Invalidate(); + rubyTextBuilderCache.Invalidate(); + romajiTextBuilderCache.Invalidate(); + } /// /// Creates a to generate the character layout for this . @@ -59,32 +66,36 @@ protected virtual TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) if (AllowMultiline) { - return new MultilineTextBuilder(store, Font, builderMaxWidth, UseFullGlyphHeight, startOffset, spacing, charactersBacking, + return new MultilineTextBuilder(store, Font, builderMaxWidth, UseFullGlyphHeight, startOffset, spacing, null, excludeCharacters, FallbackCharacter, FixedWidthReferenceCharacter); } if (Truncate) { - return new TruncatingTextBuilder(store, Font, builderMaxWidth, ellipsisString, UseFullGlyphHeight, startOffset, spacing, charactersBacking, + return new TruncatingTextBuilder(store, Font, builderMaxWidth, ellipsisString, UseFullGlyphHeight, startOffset, spacing, null, excludeCharacters, FallbackCharacter, FixedWidthReferenceCharacter); } - return new TextBuilder(store, Font, builderMaxWidth, UseFullGlyphHeight, startOffset, spacing, charactersBacking, + return new TextBuilder(store, Font, builderMaxWidth, UseFullGlyphHeight, startOffset, spacing, null, excludeCharacters, FallbackCharacter, FixedWidthReferenceCharacter); } - protected virtual PositionTextBuilder CreateRubyTextBuilder(ITexturedGlyphLookupStore store) + protected virtual TextBuilder CreateRubyTextBuilder(ITexturedGlyphLookupStore store) { const int builder_max_width = int.MaxValue; - return new PositionTextBuilder(store, RubyFont, builder_max_width, UseFullGlyphHeight, - new Vector2(0, -rubyMargin), rubySpacing, charactersBacking, FixedWidthExcludeCharacters, FallbackCharacter, FixedWidthReferenceCharacter, RelativePosition.Top, rubyAlignment); + var excludeCharacters = FixedWidthExcludeCharacters ?? default_never_fixed_width_characters; + + return new TextBuilder(store, RubyFont, builder_max_width, UseFullGlyphHeight, + new Vector2(), rubySpacing, null, excludeCharacters, FallbackCharacter, FixedWidthReferenceCharacter); } - protected virtual PositionTextBuilder CreateRomajiTextBuilder(ITexturedGlyphLookupStore store) + protected virtual TextBuilder CreateRomajiTextBuilder(ITexturedGlyphLookupStore store) { const int builder_max_width = int.MaxValue; - return new PositionTextBuilder(store, RomajiFont, builder_max_width, UseFullGlyphHeight, - new Vector2(0, romajiMargin), romajiSpacing, charactersBacking, FixedWidthExcludeCharacters, FallbackCharacter, FixedWidthReferenceCharacter, RelativePosition.Bottom, romajiAlignment); + var excludeCharacters = FixedWidthExcludeCharacters ?? default_never_fixed_width_characters; + + return new TextBuilder(store, RomajiFont, builder_max_width, UseFullGlyphHeight, + new Vector2(), romajiSpacing, null, excludeCharacters, FallbackCharacter, FixedWidthReferenceCharacter); } private TextBuilder getTextBuilder() @@ -95,6 +106,22 @@ private TextBuilder getTextBuilder() return textBuilderCache.Value; } + private TextBuilder getRubyTextBuilder() + { + if (!rubyTextBuilderCache.IsValid) + rubyTextBuilderCache.Value = CreateRubyTextBuilder(store); + + return rubyTextBuilderCache.Value; + } + + private TextBuilder getRomajiTextBuilder() + { + if (!romajiTextBuilderCache.IsValid) + romajiTextBuilderCache.Value = CreateRomajiTextBuilder(store); + + return romajiTextBuilderCache.Value; + } + public float LineBaseHeight { get @@ -127,6 +154,40 @@ private IReadOnlyList characters } } + /// + /// Glyph list to be passed to . + /// + private readonly Dictionary rubyCharactersBacking = new Dictionary(); + + /// + /// The characters in local space. + /// + private IReadOnlyDictionary rubyCharacters + { + get + { + computeCharacters(); + return rubyCharactersBacking; + } + } + + /// + /// Glyph list to be passed to . + /// + private readonly Dictionary romajiCharactersBacking = new Dictionary(); + + /// + /// The characters in local space. + /// + private IReadOnlyDictionary romajiCharacters + { + get + { + computeCharacters(); + return romajiCharactersBacking; + } + } + /// /// Compute character textures and positions. /// @@ -143,6 +204,8 @@ private void computeCharacters() return; charactersBacking.Clear(); + rubyCharactersBacking.Clear(); + romajiCharactersBacking.Clear(); // Todo: Re-enable this assert after autosize is split into two passes. // Debug.Assert(!isComputingCharacters, "Cyclic invocation of computeCharacters()!"); @@ -154,26 +217,29 @@ private void computeCharacters() if (string.IsNullOrEmpty(displayedText)) return; - TextBuilder textBuilder = getTextBuilder(); + // Main text + var textBuilder = getTextBuilder(); + charactersBacking.AddRange(applyTextToBuilder(textBuilder, displayedText)); - textBuilder.Reset(); - textBuilder.AddText(displayedText); - textBounds = textBuilder.Bounds; - - var fixedRubies = getFixedPositionTexts(rubies, displayedText); - var fixedRomajies = getFixedPositionTexts(romajies, displayedText); + // Ruby + var rubyTextBuilder = getRubyTextBuilder(); + var rubyTextFormatter = new PositionTextFormatter(charactersBacking, RelativePosition.Top, rubyAlignment, rubySpacing, rubyMargin); - if (fixedRubies.Any()) + foreach (var (positionText, textBuilderGlyphs) in applyPositionTextToBuilder(rubyTextBuilder, displayedText, rubies)) { - var rubyTextBuilder = CreateRubyTextBuilder(store); - fixedRubies.ForEach(x => rubyTextBuilder.AddText(x)); + rubyCharactersBacking.Add(positionText, rubyTextFormatter.Calculate(positionText, textBuilderGlyphs)); } - if (fixedRomajies.Any()) + // Romaji + var romajiTextBuilder = getRomajiTextBuilder(); + var romajiTextFormatter = new PositionTextFormatter(charactersBacking, RelativePosition.Bottom, romajiAlignment, romajiSpacing, romajiMargin); + + foreach (var (positionText, textBuilderGlyphs) in applyPositionTextToBuilder(romajiTextBuilder, displayedText, romajies)) { - var romajiTextBuilder = CreateRomajiTextBuilder(store); - fixedRomajies.ForEach(x => romajiTextBuilder.AddText(x)); + romajiCharactersBacking.Add(positionText, romajiTextFormatter.Calculate(positionText, textBuilderGlyphs)); } + + textBounds = textBuilder.Bounds; } finally { @@ -190,14 +256,40 @@ private void computeCharacters() charactersCache.Validate(); } + } - static List getFixedPositionTexts(IEnumerable positionTexts, string lyricText) - => positionTexts - .Where(x => !string.IsNullOrEmpty(x.Text)) - .Select(x => GetFixedPositionText(x, lyricText)) - .ToList(); + private static IEnumerable applyTextToBuilder(TextBuilder textBuilder, string text) + { + textBuilder.Reset(); + textBuilder.AddText(text); + + return textBuilder.Characters; } + private static Dictionary applyPositionTextToBuilder(TextBuilder textBuilder, string text, IEnumerable positionTexts) + { + var fixedPositionTexts = GetFixedPositionTexts(positionTexts, text); + + var texts = new Dictionary(); + + foreach (var positionText in fixedPositionTexts) + { + textBuilder.Reset(); + textBuilder.AddText(positionText.Text); + + texts.Add(positionText, textBuilder.Characters.ToArray()); + } + + return texts; + } + + internal static List GetFixedPositionTexts(IEnumerable positionTexts, string lyricText) + => positionTexts + .Where(x => !string.IsNullOrEmpty(x.Text)) + .Select(x => GetFixedPositionText(x, lyricText)) + .Distinct() + .ToList(); + internal static PositionText GetFixedPositionText(PositionText positionText, string lyricText) { var startIndex = Math.Clamp(positionText.StartIndex, 0, lyricText.Length); @@ -251,6 +343,20 @@ private void computeScreenSpaceCharacters() }); } + var positionCharacters = new List() + .Concat(rubyCharacters.SelectMany(x => x.Value)) + .Concat(romajiCharacters.SelectMany(x => x.Value)); + + foreach (var character in positionCharacters) + { + screenSpaceCharactersBacking.Add(new ScreenSpaceCharacterPart + { + DrawQuad = ToScreenSpace(character.DrawRectangle.Inflate(inflationAmount)), + InflationPercentage = Vector2.Divide(inflationAmount, character.DrawRectangle.Size), + Texture = character.Texture + }); + } + localScreenSpaceCache.Validate(); } @@ -277,31 +383,21 @@ public RectangleF GetCharacterDrawRectangle(int index, bool drawSizeOnly = false public RectangleF GetRubyTagDrawRectangle(PositionText rubyTag, bool drawSizeOnly = false) { - int rubyIndex = Rubies.ToList().IndexOf(rubyTag); - if (rubyIndex < 0) - throw new ArgumentOutOfRangeException(nameof(rubyIndex)); - - int startCharacterIndex = Text.Length + skinIndex(Rubies, rubyIndex); - int count = rubyTag.Text.Length; - var drawRectangle = characters.ToList() - .GetRange(startCharacterIndex, count) - .Select(x => TextBuilderGlyphUtils.GetCharacterRectangle(x, drawSizeOnly)) - .Aggregate(RectangleF.Union); + if (!rubyCharactersBacking.TryGetValue(rubyTag, out var glyphs)) + throw new ArgumentOutOfRangeException(nameof(rubyTag)); + + var drawRectangle = glyphs.Select(x => TextBuilderGlyphUtils.GetCharacterRectangle(x, drawSizeOnly)) + .Aggregate(RectangleF.Union); return getComputeCharacterDrawRectangle(drawRectangle); } public RectangleF GetRomajiTagDrawRectangle(PositionText romajiTag, bool drawSizeOnly = false) { - int romajiIndex = Romajies.ToList().IndexOf(romajiTag); - if (romajiIndex < 0) - throw new ArgumentOutOfRangeException(nameof(romajiIndex)); - - int startCharacterIndex = Text.Length + skinIndex(Rubies, Rubies.Count) + skinIndex(Romajies, romajiIndex); - int count = romajiTag.Text.Length; - var drawRectangle = characters.ToList() - .GetRange(startCharacterIndex, count) - .Select(x => TextBuilderGlyphUtils.GetCharacterRectangle(x, drawSizeOnly)) - .Aggregate(RectangleF.Union); + if (!romajiCharactersBacking.TryGetValue(romajiTag, out var glyphs)) + throw new ArgumentOutOfRangeException(nameof(romajiTag)); + + var drawRectangle = glyphs.Select(x => TextBuilderGlyphUtils.GetCharacterRectangle(x, drawSizeOnly)) + .Aggregate(RectangleF.Union); return getComputeCharacterDrawRectangle(drawRectangle); } diff --git a/osu.Framework.Font/Text/PositionTextBuilder.cs b/osu.Framework.Font/Text/PositionTextBuilder.cs deleted file mode 100644 index d7e6def..0000000 --- a/osu.Framework.Font/Text/PositionTextBuilder.cs +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright (c) karaoke.dev . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Utils; -using osuTK; - -namespace osu.Framework.Text -{ - public class PositionTextBuilder : TextBuilder - { - private readonly ITexturedGlyphLookupStore store; - private readonly FontUsage font; - private readonly Vector2 startOffset; - private readonly Vector2 spacing; - private readonly char fallbackCharacter; - - private readonly RelativePosition relativePosition; - private readonly LyricTextAlignment alignment; - - /// - /// Creates a new . - /// - /// The store from which glyphs are to be retrieved from. - /// The font to use for glyph lookups from . - /// True to use the provided size as the height for each line. False if the height of each individual glyph should be used. - /// The offset at which characters should begin being added at. - /// The spacing between characters. - /// The maximum width of the resulting text bounds. - /// That list to contain all resulting s. - /// The characters for which fixed width should never be applied. - /// The character to use if a glyph lookup fails. - /// The character to use to calculate the fixed width width. Defaults to 'm'. - /// Should be added into top or bottom. - /// Lyric text alignment. - public PositionTextBuilder(ITexturedGlyphLookupStore store, FontUsage font, float maxWidth = int.MaxValue, bool useFontSizeAsHeight = true, - Vector2 startOffset = default, - Vector2 spacing = default, List characterList = null, char[] neverFixedWidthCharacters = null, - char fallbackCharacter = '?', char fixedWidthReferenceCharacter = 'm', RelativePosition relativePosition = RelativePosition.Top, - LyricTextAlignment alignment = LyricTextAlignment.Auto) - : base(store, font, maxWidth, useFontSizeAsHeight, startOffset, spacing, characterList, neverFixedWidthCharacters, fallbackCharacter, fixedWidthReferenceCharacter) - { - this.store = store; - this.font = font; - this.startOffset = startOffset; - this.spacing = spacing; - this.fallbackCharacter = fallbackCharacter; - - this.relativePosition = relativePosition; - this.alignment = alignment; - } - - /// - /// Appends text to this . - /// - /// The text to append. - public void AddText(PositionText positionText) - { - var text = positionText.Text; - if (string.IsNullOrEmpty(text)) - return; - - // get some position relative params. - var mainCharacterRect = getMainCharacterRectangleF(positionText.StartIndex, positionText.EndIndex); - var subTextWidth = getSubTextWidth(text); - var subTextHeight = getSubTextHeight(text); - - // calculate the start draw position. - var position = getPositionTextDrawPosition(mainCharacterRect, new Vector2(subTextWidth, subTextHeight), relativePosition); - - // set start render position - setCurrentPosition(position + startOffset); - - // render the chars. - foreach (var c in text) - { - if (!AddCharacter(c)) - break; - } - } - - private void setCurrentPosition(Vector2 position) - { - // force change the start print text position. - var prop = typeof(TextBuilder).GetField("currentPos", BindingFlags.Instance | BindingFlags.NonPublic); - if (prop == null) - throw new NullReferenceException(); - - prop.SetValue(this, position); - } - - private static Vector2 getPositionTextDrawPosition(RectangleF mainTextPosition, Vector2 positionTextSize, RelativePosition relativePosition) - { - var subTextWidth = positionTextSize.X; - var subTextHeight = positionTextSize.Y; - - return relativePosition switch - { - RelativePosition.Top => new Vector2(mainTextPosition.Centre.X, mainTextPosition.Top) + new Vector2(-subTextWidth / 2, -subTextHeight), - RelativePosition.Bottom => new Vector2(mainTextPosition.Centre.X, mainTextPosition.Bottom) + new Vector2(-subTextWidth / 2, 0), - _ => throw new ArgumentOutOfRangeException(nameof(relativePosition), relativePosition, null) - }; - } - - private RectangleF getMainCharacterRectangleF(int startCharIndex, int endCharIndex) - { - var starCharacter = Characters[startCharIndex]; - var endCharacter = Characters[endCharIndex - 1]; - var startCharacterRectangle = TextBuilderGlyphUtils.GetCharacterRectangle(starCharacter, false); - var endCharacterRectangle = TextBuilderGlyphUtils.GetCharacterRectangle(endCharacter, false); - - var position = startCharacterRectangle.TopLeft; - - // if center position is between two lines, then should let canter position in the first line. - var leftX = startCharacterRectangle.Left; - var rightX = endCharacterRectangle.Right > leftX - ? endCharacterRectangle.Right - : Characters.Max(c => c.DrawRectangle.Right); - - // because each character has different height, so we need to get base text height from here. - var width = rightX - leftX; - var height = Math.Max(startCharacterRectangle.Height, endCharacterRectangle.Height); - - // return center position. - return new RectangleF(position, new Vector2(width, height)); - } - - private float getSubTextWidth(string text) - { - if (string.IsNullOrEmpty(text)) - return 0; - - return text.Sum(c => getCharWidth(c, font)) + spacing.X * (text.Length - 1); - } - - private float getSubTextHeight(string text) - { - if (string.IsNullOrEmpty(text)) - return 0; - - return getCharHeight(text.First(), font); - } - - private float getCharWidth(char c, FontUsage fontUsage) - { - var texture = getTexturedGlyph(c, fontUsage); - if (texture == null) - return 0; - - return texture.Width * fontUsage.Size; - } - - private float getCharHeight(char c, FontUsage fontUsage) - { - var texture = getTexturedGlyph(c, fontUsage); - if (texture == null) - return 0; - - return texture.Baseline * fontUsage.Size; - } - - private ITexturedCharacterGlyph getTexturedGlyph(char character, FontUsage font) - { - return store.Get(font.FontName, character) - ?? store.Get(null, character) - ?? store.Get(font.FontName, fallbackCharacter) - ?? store.Get(null, fallbackCharacter); - } - } - - public enum RelativePosition - { - Top, - - Bottom - } -} diff --git a/osu.Framework.Font/Text/PositionTextBuilderGlyph.cs b/osu.Framework.Font/Text/PositionTextBuilderGlyph.cs new file mode 100644 index 0000000..d59fccf --- /dev/null +++ b/osu.Framework.Font/Text/PositionTextBuilderGlyph.cs @@ -0,0 +1,49 @@ +// Copyright (c) karaoke.dev . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Runtime.CompilerServices; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osuTK; + +namespace osu.Framework.Text +{ + /// + /// A provided as final position conversion of . + /// + public readonly struct PositionTextBuilderGlyph : ITexturedCharacterGlyph + { + public Texture Texture => glyph.Texture; + public readonly float XOffset => glyph.XOffset; + + public readonly float YOffset => glyph.YOffset; + public readonly float XAdvance => glyph.XAdvance; + + public readonly float Baseline => glyph.Baseline; + public float Width => glyph.Width; + + public float Height => glyph.Height; + public readonly char Character => glyph.Character; + + private readonly TextBuilderGlyph glyph; + + /// + /// The rectangle for the character to be drawn in. + /// + public RectangleF DrawRectangle { get; } + + internal PositionTextBuilderGlyph(TextBuilderGlyph glyph, Vector2 position) + { + this = default; + + this.glyph = glyph; + + DrawRectangle = new RectangleF(position, glyph.DrawRectangle.Size); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float GetKerning(T lastGlyph) where T : ICharacterGlyph + => glyph.GetKerning(lastGlyph); + } +} diff --git a/osu.Framework.Font/Text/PositionTextFormatter.cs b/osu.Framework.Font/Text/PositionTextFormatter.cs new file mode 100644 index 0000000..74c5e51 --- /dev/null +++ b/osu.Framework.Font/Text/PositionTextFormatter.cs @@ -0,0 +1,106 @@ +// Copyright (c) karaoke.dev . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; +using osuTK; + +namespace osu.Framework.Text +{ + public class PositionTextFormatter + { + private readonly List characterList; + private readonly RelativePosition relativePosition; + private readonly LyricTextAlignment alignment; + private readonly Vector2 spacing; + private readonly int margin; + + public PositionTextFormatter(List characterList, + RelativePosition relativePosition, + LyricTextAlignment alignment = LyricTextAlignment.Auto, + Vector2 spacing = new Vector2(), + int margin = 0) + { + this.characterList = characterList; + this.relativePosition = relativePosition; + this.alignment = alignment; + this.spacing = spacing; + this.margin = margin; + } + + public PositionTextBuilderGlyph[] Calculate(PositionText positionText, TextBuilderGlyph[] glyphs) + { + if (glyphs == null || glyphs.Length == 0) + throw new ArgumentException($"{nameof(glyphs)} cannot be empty"); + + // get some position related params. + var mainCharacterRect = getMainCharacterRectangleF(positionText.StartIndex, positionText.EndIndex); + var subTextSize = getPositionTextSize(glyphs); + + // calculate the start draw position. + var startPosition = getPositionTextStartPosition(mainCharacterRect, subTextSize.X, glyphs.First().Baseline, relativePosition, margin); + + // then it's time to shift the position. + return createPositionTextBuilderGlyphs(glyphs, startPosition); + } + + private static PositionTextBuilderGlyph[] createPositionTextBuilderGlyphs(TextBuilderGlyph[] glyphs, Vector2 startPosition) + { + return glyphs.Select(x => + { + var newPosition = startPosition + x.DrawRectangle.TopLeft; + return new PositionTextBuilderGlyph(x, newPosition); + }).ToArray(); + } + + private static Vector2 getPositionTextStartPosition(RectangleF mainTextRect, float subTextWidth, float baseLine, RelativePosition relativePosition, int margin) + { + var drawXPosition = mainTextRect.Centre.X - subTextWidth / 2; + + return relativePosition switch + { + RelativePosition.Top => new Vector2(drawXPosition, mainTextRect.Top - margin - baseLine), + RelativePosition.Bottom => new Vector2(drawXPosition, mainTextRect.Bottom + margin), + _ => throw new ArgumentOutOfRangeException(nameof(relativePosition), relativePosition, null) + }; + } + + private static Vector2 getPositionTextSize(TextBuilderGlyph[] glyphs) + => glyphs.Select(x => TextBuilderGlyphUtils.GetCharacterRectangle(x, false)) + .Aggregate(RectangleF.Union).Size; + + private RectangleF getMainCharacterRectangleF(int startCharIndex, int endCharIndex) + { + var starCharacter = characterList[startCharIndex]; + var endCharacter = characterList[endCharIndex - 1]; + var startCharacterRectangle = TextBuilderGlyphUtils.GetCharacterRectangle(starCharacter, false); + var endCharacterRectangle = TextBuilderGlyphUtils.GetCharacterRectangle(endCharacter, false); + + var position = startCharacterRectangle.TopLeft; + + // if center position is between two lines, then should let canter position in the first line. + var leftX = startCharacterRectangle.Left; + var rightX = endCharacterRectangle.Right > leftX + ? endCharacterRectangle.Right + : characterList.Max(c => c.DrawRectangle.Right); + + // because each character has different height, so we need to get base text height from here. + var width = rightX - leftX; + var height = Math.Max(startCharacterRectangle.Height, endCharacterRectangle.Height); + + // return center position. + return new RectangleF(position, new Vector2(width, height)); + } + } + + public enum RelativePosition + { + Top, + + Bottom + } +} diff --git a/osu.Framework.Font/Utils/TextBuilderGlyphUtils.cs b/osu.Framework.Font/Utils/TextBuilderGlyphUtils.cs index 055eca0..0e8fe9f 100644 --- a/osu.Framework.Font/Utils/TextBuilderGlyphUtils.cs +++ b/osu.Framework.Font/Utils/TextBuilderGlyphUtils.cs @@ -23,5 +23,20 @@ public static RectangleF GetCharacterRectangle(TextBuilderGlyph character, bool Bottom = character.Baseline - character.Height - character.YOffset + bottomIncrease, }); } + + public static RectangleF GetCharacterRectangle(PositionTextBuilderGlyph character, bool drawSizeOnly) + { + if (drawSizeOnly) + return character.DrawRectangle; + + // todo: should get the real value. + var topReduce = character.Baseline * 0.3f; + var bottomIncrease = character.Baseline * 0.2f; + return character.DrawRectangle.Inflate(new MarginPadding + { + Top = character.YOffset - topReduce, + Bottom = character.Baseline - character.Height - character.YOffset + bottomIncrease, + }); + } } }