diff --git a/osu.Framework.Benchmarks/BenchmarkTextBuilder.cs b/osu.Framework.Benchmarks/BenchmarkTextBuilder.cs index 61df335fa4..031154983d 100644 --- a/osu.Framework.Benchmarks/BenchmarkTextBuilder.cs +++ b/osu.Framework.Benchmarks/BenchmarkTextBuilder.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; using osu.Framework.Text; namespace osu.Framework.Benchmarks @@ -50,6 +52,8 @@ private class TestStore : ITexturedGlyphLookupStore public ITexturedCharacterGlyph Get(string fontName, char character) => new TexturedCharacterGlyph(new CharacterGlyph(character, character, character, character, character, null), Texture.WhitePixel); public Task GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character)); + + public IGlyphStore GetFont(string name) => throw new NotImplementedException(); } } } diff --git a/osu.Framework.Tests/Text/TextBuilderTest.cs b/osu.Framework.Tests/Text/TextBuilderTest.cs index 826d6c2b29..7b4104bce6 100644 --- a/osu.Framework.Tests/Text/TextBuilderTest.cs +++ b/osu.Framework.Tests/Text/TextBuilderTest.cs @@ -2,11 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; using osu.Framework.Text; using osuTK; @@ -25,6 +30,8 @@ public class TextBuilderTest private const float height = 6; private const float kerning = -7; + private static readonly FontMetrics metrics = new FontMetrics(1000, 2000, 1000); + private const float b_x_offset = 8; private const float b_y_offset = 9; private const float b_x_advance = 10; @@ -33,6 +40,8 @@ public class TextBuilderTest private const float b_height = 13; private const float b_kerning = -14; + private static readonly FontMetrics b_metrics = new FontMetrics(3000, 4000, 1000); + private const float m_x_offset = 15; private const float m_y_offset = 16; private const float m_x_advance = 17; @@ -41,6 +50,8 @@ public class TextBuilderTest private const float m_height = 20; private const float m_kerning = -21; + private static readonly FontMetrics m_metrics = new FontMetrics(5000, 6000, 1000); + private static readonly Vector2 spacing = new Vector2(22, 23); private static readonly TestFontUsage normal_font = new TestFontUsage("test"); @@ -50,13 +61,13 @@ public class TextBuilderTest public TextBuilderTest() { - fontStore = new TestStore( - new GlyphEntry(normal_font, new TestGlyph('a', x_offset, y_offset, x_advance, width, baseline, height, kerning)), - new GlyphEntry(normal_font, new TestGlyph('b', b_x_offset, b_y_offset, b_x_advance, b_width, b_baseline, b_height, b_kerning)), - new GlyphEntry(normal_font, new TestGlyph('m', 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('b', 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', m_x_offset, m_y_offset, m_x_advance, m_width, m_baseline, m_height, m_kerning)) + fontStore = new TestStore(null, + new GlyphEntry(normal_font, new TestGlyph('a', metrics, x_offset, y_offset, x_advance, width, baseline, height, kerning)), + new GlyphEntry(normal_font, new TestGlyph('b', b_metrics, b_x_offset, b_y_offset, b_x_advance, b_width, b_baseline, b_height, b_kerning)), + new GlyphEntry(normal_font, new TestGlyph('m', m_metrics, 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', metrics, x_offset, y_offset, x_advance, width, baseline, height, kerning)), + new GlyphEntry(fixed_width_font, new TestGlyph('b', b_metrics, 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', m_metrics, m_x_offset, m_y_offset, m_x_advance, m_width, m_baseline, m_height, m_kerning)) ); } @@ -201,7 +212,7 @@ public void TestNewLineUsesFontHeightWhenUsingFontHeightAsSize() [Test] public void TestNewLineUsesGlyphHeightWhenNotUsingFontHeightAsSize() { - var builder = new TextBuilder(fontStore, normal_font, useFontSizeAsHeight: false); + var builder = new TextBuilder(fontStore, normal_font, useFullGlyphHeight: false); builder.AddText("a"); builder.AddText("b"); @@ -339,6 +350,24 @@ public void TestLineBaseHeightThrowsOnMultiline() Assert.Throws(() => _ = builder.LineBaseHeight); } + /// + /// Tests setting the (i.e. metrics-based scaling) on a text builder. + /// + [Test] + public void TestCssScaling() + { + var builder = new TextBuilder(fontStore, normal_font.WithCssScaling()); + + builder.AddText("a"); + + Assert.That(builder.Characters[0].Size, Is.EqualTo(font_size * metrics.GlyphScale)); + + builder.AddNewLine(); + builder.AddText("b"); + + Assert.That(builder.Characters[1].DrawRectangle.Top, Is.EqualTo(font_size * metrics.GlyphScale + b_y_offset * b_metrics.GlyphScale)); + } + /// /// Tests that the current position and "line base height" are correctly reset when the first character is removed. /// @@ -495,11 +524,11 @@ public void TestSameCharacterFallsBackWithNoFontName() { var font = new TestFontUsage("test"); var nullFont = new TestFontUsage(null); - var builder = new TextBuilder(new TestStore( - new GlyphEntry(font, new TestGlyph('b', 0, 0, 0, 0, 0, 0, 0)), - new GlyphEntry(nullFont, new TestGlyph('a', 0, 0, 0, 0, 0, 0, 0)), - new GlyphEntry(font, new TestGlyph('?', 0, 0, 0, 0, 0, 0, 0)), - new GlyphEntry(nullFont, new TestGlyph('?', 0, 0, 0, 0, 0, 0, 0)) + var builder = new TextBuilder(new TestStore(null, + new GlyphEntry(font, new TestGlyph('b', default, 0, 0, 0, 0, 0, 0, 0)), + new GlyphEntry(nullFont, new TestGlyph('a', default, 0, 0, 0, 0, 0, 0, 0)), + new GlyphEntry(font, new TestGlyph('?', default, 0, 0, 0, 0, 0, 0, 0)), + new GlyphEntry(nullFont, new TestGlyph('?', default, 0, 0, 0, 0, 0, 0, 0)) ), font); builder.AddText("a"); @@ -515,11 +544,11 @@ public void TestFallBackCharacterFallsBackWithFontName() { var font = new TestFontUsage("test"); var nullFont = new TestFontUsage(null); - var builder = new TextBuilder(new TestStore( - new GlyphEntry(font, new TestGlyph('b', 0, 0, 0, 0, 0, 0, 0)), - new GlyphEntry(nullFont, new TestGlyph('b', 0, 0, 0, 0, 0, 0, 0)), - new GlyphEntry(font, new TestGlyph('?', 0, 0, 0, 0, 0, 0, 0)), - new GlyphEntry(nullFont, new TestGlyph('?', 1, 0, 0, 0, 0, 0, 0)) + var builder = new TextBuilder(new TestStore(null, + new GlyphEntry(font, new TestGlyph('b', default, 0, 0, 0, 0, 0, 0, 0)), + new GlyphEntry(nullFont, new TestGlyph('b', default, 0, 0, 0, 0, 0, 0, 0)), + new GlyphEntry(font, new TestGlyph('?', default, 0, 0, 0, 0, 0, 0, 0)), + new GlyphEntry(nullFont, new TestGlyph('?', default, 1, 0, 0, 0, 0, 0, 0)) ), font); builder.AddText("a"); @@ -536,11 +565,11 @@ public void TestFallBackCharacterFallsBackWithNoFontName() { var font = new TestFontUsage("test"); var nullFont = new TestFontUsage(null); - var builder = new TextBuilder(new TestStore( - new GlyphEntry(font, new TestGlyph('b', 0, 0, 0, 0, 0, 0, 0)), - new GlyphEntry(nullFont, new TestGlyph('b', 0, 0, 0, 0, 0, 0, 0)), - new GlyphEntry(font, new TestGlyph('b', 0, 0, 0, 0, 0, 0, 0)), - new GlyphEntry(nullFont, new TestGlyph('?', 1, 0, 0, 0, 0, 0, 0)) + var builder = new TextBuilder(new TestStore(null, + new GlyphEntry(font, new TestGlyph('b', default, 0, 0, 0, 0, 0, 0, 0)), + new GlyphEntry(nullFont, new TestGlyph('b', default, 0, 0, 0, 0, 0, 0, 0)), + new GlyphEntry(font, new TestGlyph('b', default, 0, 0, 0, 0, 0, 0, 0)), + new GlyphEntry(nullFont, new TestGlyph('?', default, 1, 0, 0, 0, 0, 0, 0)) ), font); builder.AddText("a"); @@ -563,31 +592,70 @@ public void TestFailedCharacterLookup() Assert.That(builder.Bounds, Is.EqualTo(Vector2.Zero)); } + /// + /// Tests that adding a new line after an empty line has a height with the CSS scaling of the exact font specified in the applied to it. + /// + [Test] + public void TestNewLineAfterEmptyLineWithCssScaling() + { + var one = new FontMetrics(1000, 2000, 1000); + var two = new FontMetrics(3000, 4000, 1000); + var font = new TestFontUsage("one", css: true); + + var builder = new TextBuilder(new TestStore(new[] + { + new TestStore.TestGlyphStore("one", one, 0), + new TestStore.TestGlyphStore("two", two, 0), + }, + new GlyphEntry(new TestFontUsage("one"), new TestGlyph('1', one, 0, 0, 0, 0, 0, 0, 0)), + new GlyphEntry(new TestFontUsage("two"), new TestGlyph('2', two, 0, 0, 0, 0, 0, 0, 0)) + ), font); + + builder.AddText("2"); + builder.AddNewLine(); + builder.AddNewLine(); + builder.AddText("2"); + + // The first line would be font_size multiplied by the glyph scale of the glyph residing there ("two"). + // The second line would be font_size multiplied by the glyph scale of the font specified in the FontUsage ("one"), as the line doesn't have any glyphs. + Assert.That(builder.Characters[1].DrawRectangle.Top, Is.EqualTo(font_size * two.GlyphScale + + font_size * one.GlyphScale)); + } + private readonly struct TestFontUsage { private readonly string family; private readonly string weight; private readonly bool italics; private readonly bool fixedWidth; + private readonly bool css; - public TestFontUsage(string family = null, string weight = null, bool italics = false, bool fixedWidth = false) + public TestFontUsage(string family = null, string weight = null, bool italics = false, bool fixedWidth = false, bool css = false) { this.family = family; this.weight = weight; this.italics = italics; this.fixedWidth = fixedWidth; + this.css = css; } + public TestFontUsage WithCssScaling() + => new TestFontUsage(family, weight, italics, fixedWidth, true); + public static implicit operator FontUsage(TestFontUsage tfu) - => new FontUsage(tfu.family, font_size, tfu.weight, tfu.italics, tfu.fixedWidth); + => new FontUsage(tfu.family, font_size, tfu.weight, tfu.italics, tfu.fixedWidth, tfu.css); } private class TestStore : ITexturedGlyphLookupStore { + [CanBeNull] + private readonly TestGlyphStore[] stores; + private readonly GlyphEntry[] glyphs; - public TestStore(params GlyphEntry[] glyphs) + public TestStore(TestGlyphStore[] stores = null, params GlyphEntry[] glyphs) { + this.stores = stores; this.glyphs = glyphs; } @@ -600,6 +668,34 @@ public ITexturedCharacterGlyph Get(string fontName, char character) } public Task GetAsync(string fontName, char character) => throw new NotImplementedException(); + + public IGlyphStore GetFont(string name) => stores?.FirstOrDefault(s => s.FontName == name); + + public class TestGlyphStore : IGlyphStore + { + public string FontName { get; } + + public FontMetrics? Metrics { get; } + + public float? Baseline { get; } + + public TestGlyphStore(string name, FontMetrics metrics, float baseline) + { + FontName = name; + Metrics = metrics; + Baseline = baseline; + } + + public Task LoadFontAsync() => throw new NotImplementedException(); + public bool HasGlyph(char c) => throw new NotImplementedException(); + public CharacterGlyph Get(char character) => throw new NotImplementedException(); + public CharacterGlyph Get(string name) => throw new NotImplementedException(); + public Task GetAsync(string name, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Stream GetStream(string name) => throw new NotImplementedException(); + public IEnumerable GetAvailableResources() => throw new NotImplementedException(); + public int GetKerning(char left, char right) => throw new NotImplementedException(); + public void Dispose() => throw new NotImplementedException(); + } } private readonly struct GlyphEntry @@ -624,13 +720,15 @@ public GlyphEntry(FontUsage font, ITexturedCharacterGlyph glyph) public float Baseline { get; } public float Height { get; } public char Character { get; } + public FontMetrics? Metrics { get; } private readonly float glyphKerning; - public TestGlyph(char character, float xOffset, float yOffset, float xAdvance, float width, float baseline, float height, float kerning) + public TestGlyph(char character, FontMetrics? metrics, float xOffset, float yOffset, float xAdvance, float width, float baseline, float height, float kerning) { glyphKerning = kerning; Character = character; + Metrics = metrics; XOffset = xOffset; YOffset = yOffset; XAdvance = xAdvance;