A game framework written with osu! in mind.
at master 19 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4using System.Linq; 5using System.Threading.Tasks; 6using NUnit.Framework; 7using osu.Framework.Graphics.Sprites; 8using osu.Framework.Graphics.Textures; 9using osu.Framework.Text; 10using osuTK; 11 12namespace osu.Framework.Tests.Text 13{ 14 [TestFixture] 15 public class TextBuilderTest 16 { 17 private const float font_size = 1; 18 19 private const float x_offset = 1; 20 private const float y_offset = 2; 21 private const float x_advance = 3; 22 private const float width = 4; 23 private const float height = 5; 24 private const float kerning = -6; 25 26 private const float b_x_offset = 7; 27 private const float b_y_offset = 8; 28 private const float b_x_advance = 9; 29 private const float b_width = 10; 30 private const float b_height = 11; 31 private const float b_kerning = -12; 32 33 private const float m_x_offset = 13; 34 private const float m_y_offset = 14; 35 private const float m_x_advance = 15; 36 private const float m_width = 16; 37 private const float m_height = 17; 38 private const float m_kerning = -18; 39 40 private static readonly Vector2 spacing = new Vector2(19, 20); 41 42 private static readonly TestFontUsage normal_font = new TestFontUsage("test"); 43 private static readonly TestFontUsage fixed_width_font = new TestFontUsage("test-fixedwidth", fixedWidth: true); 44 45 private readonly TestStore fontStore; 46 47 public TextBuilderTest() 48 { 49 fontStore = new TestStore( 50 new GlyphEntry(normal_font, new TestGlyph('a', x_offset, y_offset, x_advance, width, height, kerning)), 51 new GlyphEntry(normal_font, new TestGlyph('b', b_x_offset, b_y_offset, b_x_advance, b_width, b_height, b_kerning)), 52 new GlyphEntry(normal_font, new TestGlyph('m', m_x_offset, m_y_offset, m_x_advance, m_width, m_height, m_kerning)), 53 new GlyphEntry(fixed_width_font, new TestGlyph('a', x_offset, y_offset, x_advance, width, height, kerning)), 54 new GlyphEntry(fixed_width_font, new TestGlyph('b', b_x_offset, b_y_offset, b_x_advance, b_width, b_height, b_kerning)), 55 new GlyphEntry(fixed_width_font, new TestGlyph('m', m_x_offset, m_y_offset, m_x_advance, m_width, m_height, m_kerning)) 56 ); 57 } 58 59 /// <summary> 60 /// Tests that the size of a fresh text builder is zero. 61 /// </summary> 62 [Test] 63 public void TestInitialSizeIsZero() 64 { 65 var builder = new TextBuilder(fontStore, normal_font); 66 67 Assert.That(builder.Bounds, Is.EqualTo(Vector2.Zero)); 68 } 69 70 /// <summary> 71 /// Tests that the first added character is correctly marked as being on a new line. 72 /// </summary> 73 [Test] 74 public void TestFirstCharacterIsOnNewLine() 75 { 76 var builder = new TextBuilder(fontStore, normal_font); 77 78 builder.AddText("a"); 79 80 Assert.That(builder.Characters[0].OnNewLine, Is.True); 81 } 82 83 /// <summary> 84 /// Tests that the first added fixed-width character metrics match the glyph's. 85 /// </summary> 86 [Test] 87 public void TestFirstCharacterRectangleIsCorrect() 88 { 89 var builder = new TextBuilder(fontStore, normal_font); 90 91 builder.AddText("a"); 92 93 Assert.That(builder.Characters[0].DrawRectangle.Left, Is.EqualTo(x_offset)); 94 Assert.That(builder.Characters[0].DrawRectangle.Top, Is.EqualTo(y_offset)); 95 Assert.That(builder.Characters[0].DrawRectangle.Width, Is.EqualTo(width)); 96 Assert.That(builder.Characters[0].DrawRectangle.Height, Is.EqualTo(height)); 97 } 98 99 /// <summary> 100 /// Tests that the first added character metrics match the glyph's. 101 /// </summary> 102 [Test] 103 public void TestFirstFixedWidthCharacterRectangleIsCorrect() 104 { 105 var builder = new TextBuilder(fontStore, fixed_width_font); 106 107 builder.AddText("a"); 108 109 Assert.That(builder.Characters[0].DrawRectangle.Left, Is.EqualTo((m_width - width) / 2)); 110 Assert.That(builder.Characters[0].DrawRectangle.Top, Is.EqualTo(y_offset)); 111 Assert.That(builder.Characters[0].DrawRectangle.Width, Is.EqualTo(width)); 112 Assert.That(builder.Characters[0].DrawRectangle.Height, Is.EqualTo(height)); 113 } 114 115 /// <summary> 116 /// Tests that the current position is advanced after a character is added. 117 /// </summary> 118 [Test] 119 public void TestCurrentPositionAdvancedAfterCharacter() 120 { 121 var builder = new TextBuilder(fontStore, normal_font); 122 123 builder.AddText("a"); 124 builder.AddText("a"); 125 126 Assert.That(builder.Characters[1].DrawRectangle.Left, Is.EqualTo(x_advance + kerning + x_offset)); 127 Assert.That(builder.Characters[1].DrawRectangle.Top, Is.EqualTo(y_offset)); 128 Assert.That(builder.Characters[1].DrawRectangle.Width, Is.EqualTo(width)); 129 Assert.That(builder.Characters[1].DrawRectangle.Height, Is.EqualTo(height)); 130 } 131 132 /// <summary> 133 /// Tests that the current position is advanced after a fixed width character is added. 134 /// </summary> 135 [Test] 136 public void TestCurrentPositionAdvancedAfterFixedWidthCharacter() 137 { 138 var builder = new TextBuilder(fontStore, fixed_width_font); 139 140 builder.AddText("a"); 141 builder.AddText("a"); 142 143 Assert.That(builder.Characters[1].DrawRectangle.Left, Is.EqualTo(m_width + (m_width - width) / 2)); 144 Assert.That(builder.Characters[1].DrawRectangle.Top, Is.EqualTo(y_offset)); 145 Assert.That(builder.Characters[1].DrawRectangle.Width, Is.EqualTo(width)); 146 Assert.That(builder.Characters[1].DrawRectangle.Height, Is.EqualTo(height)); 147 } 148 149 /// <summary> 150 /// Tests that a new line added to an empty builder always uses the font height. 151 /// </summary> 152 [Test] 153 public void TestNewLineOnEmptyBuilderOffsetsPositionByFontSize() 154 { 155 var builder = new TextBuilder(fontStore, normal_font); 156 157 builder.AddNewLine(); 158 builder.AddText("a"); 159 160 Assert.That(builder.Characters[0].DrawRectangle.Top, Is.EqualTo(font_size + y_offset)); 161 } 162 163 /// <summary> 164 /// Tests that a new line added to an empty line always uses the font height. 165 /// </summary> 166 [Test] 167 public void TestNewLineOnEmptyLineOffsetsPositionByFontSize() 168 { 169 var builder = new TextBuilder(fontStore, normal_font); 170 171 builder.AddNewLine(); 172 builder.AddNewLine(); 173 builder.AddText("a"); 174 175 Assert.That(builder.Characters[0].DrawRectangle.Top, Is.EqualTo(y_offset + y_offset)); 176 } 177 178 /// <summary> 179 /// Tests that a new line added to a builder that is using the font height as size offsets the y-position by the font size and not the glyph size. 180 /// </summary> 181 [Test] 182 public void TestNewLineUsesFontHeightWhenUsingFontHeightAsSize() 183 { 184 var builder = new TextBuilder(fontStore, normal_font); 185 186 builder.AddText("a"); 187 builder.AddText("b"); 188 builder.AddNewLine(); 189 builder.AddText("a"); 190 191 Assert.That(builder.Characters[2].DrawRectangle.Top, Is.EqualTo(font_size + y_offset)); 192 } 193 194 /// <summary> 195 /// Tests that a new line added to a builder that is not using the font height as size offsets the y-position by the glyph size and not the font size. 196 /// </summary> 197 [Test] 198 public void TestNewLineUsesGlyphHeightWhenNotUsingFontHeightAsSize() 199 { 200 var builder = new TextBuilder(fontStore, normal_font, useFontSizeAsHeight: false); 201 202 builder.AddText("a"); 203 builder.AddText("b"); 204 builder.AddNewLine(); 205 builder.AddText("a"); 206 207 // b is the larger glyph 208 Assert.That(builder.Characters[2].DrawRectangle.Top, Is.EqualTo(b_y_offset + b_height + y_offset)); 209 } 210 211 /// <summary> 212 /// Tests that the first added character on a new line is correctly marked as being on a new line. 213 /// </summary> 214 [Test] 215 public void TestFirstCharacterOnNewLineIsOnNewLine() 216 { 217 var builder = new TextBuilder(fontStore, normal_font); 218 219 builder.AddText("a"); 220 builder.AddNewLine(); 221 builder.AddText("a"); 222 223 Assert.That(builder.Characters[1].OnNewLine, Is.True); 224 } 225 226 /// <summary> 227 /// Tests that no kerning is added for the first character of a new line. 228 /// </summary> 229 [Test] 230 public void TestFirstCharacterOnNewLineHasNoKerning() 231 { 232 var builder = new TextBuilder(fontStore, normal_font); 233 234 builder.AddText("a"); 235 builder.AddNewLine(); 236 builder.AddText("a"); 237 238 Assert.That(builder.Characters[1].DrawRectangle.Left, Is.EqualTo(x_offset)); 239 } 240 241 /// <summary> 242 /// Tests that the current position is correctly reset when the first character is removed. 243 /// </summary> 244 [Test] 245 public void TestRemoveFirstCharacterResetsCurrentPosition() 246 { 247 var builder = new TextBuilder(fontStore, normal_font, spacing: spacing); 248 249 builder.AddText("a"); 250 builder.RemoveLastCharacter(); 251 252 Assert.That(builder.Bounds, Is.EqualTo(Vector2.Zero)); 253 254 builder.AddText("a"); 255 256 Assert.That(builder.Characters[0].DrawRectangle.Top, Is.EqualTo(y_offset)); 257 Assert.That(builder.Characters[0].DrawRectangle.Left, Is.EqualTo(x_offset)); 258 } 259 260 /// <summary> 261 /// Tests that the current position is moved backwards and the character is removed when a character is removed. 262 /// </summary> 263 [Test] 264 public void TestRemoveCharacterOnSameLineRemovesCharacter() 265 { 266 var builder = new TextBuilder(fontStore, normal_font, spacing: spacing); 267 268 builder.AddText("a"); 269 builder.AddText("a"); 270 builder.RemoveLastCharacter(); 271 272 Assert.That(builder.Bounds, Is.EqualTo(new Vector2(x_advance, font_size))); 273 274 builder.AddText("a"); 275 276 Assert.That(builder.Characters[1].DrawRectangle.Top, Is.EqualTo(y_offset)); 277 Assert.That(builder.Characters[1].DrawRectangle.Left, Is.EqualTo(x_advance + spacing.X + kerning + x_offset)); 278 } 279 280 /// <summary> 281 /// Tests that the current position is moved to the end of the previous line, and that the character + new line is removed when a character is removed. 282 /// </summary> 283 [Test] 284 public void TestRemoveCharacterOnNewLineRemovesCharacterAndLine() 285 { 286 var builder = new TextBuilder(fontStore, normal_font, spacing: spacing); 287 288 builder.AddText("a"); 289 builder.AddNewLine(); 290 builder.AddText("a"); 291 builder.RemoveLastCharacter(); 292 293 Assert.That(builder.Bounds, Is.EqualTo(new Vector2(x_advance, font_size))); 294 295 builder.AddText("a"); 296 297 Assert.That(builder.Characters[1].DrawRectangle.TopLeft, Is.EqualTo(new Vector2(x_advance + spacing.X + kerning + x_offset, y_offset))); 298 Assert.That(builder.Bounds, Is.EqualTo(new Vector2(x_advance + spacing.X + kerning + x_advance, font_size))); 299 } 300 301 /// <summary> 302 /// Tests that the custom user-provided spacing is added for a new character/line. 303 /// </summary> 304 [Test] 305 public void TestSpacingAdded() 306 { 307 var builder = new TextBuilder(fontStore, normal_font, spacing: spacing); 308 309 builder.AddText("a"); 310 builder.AddText("a"); 311 builder.AddNewLine(); 312 builder.AddText("a"); 313 314 Assert.That(builder.Characters[0].DrawRectangle.Left, Is.EqualTo(x_offset)); 315 Assert.That(builder.Characters[1].DrawRectangle.Left, Is.EqualTo(x_advance + spacing.X + kerning + x_offset)); 316 Assert.That(builder.Characters[2].DrawRectangle.Left, Is.EqualTo(x_offset)); 317 Assert.That(builder.Characters[2].DrawRectangle.Top, Is.EqualTo(font_size + spacing.Y + y_offset)); 318 } 319 320 /// <summary> 321 /// Tests that glyph lookup falls back to using the same character with no font name. 322 /// </summary> 323 [Test] 324 public void TestSameCharacterFallsBackWithNoFontName() 325 { 326 var font = new TestFontUsage("test"); 327 var nullFont = new TestFontUsage(null); 328 var builder = new TextBuilder(new TestStore( 329 new GlyphEntry(font, new TestGlyph('b', 0, 0, 0, 0, 0, 0)), 330 new GlyphEntry(nullFont, new TestGlyph('a', 0, 0, 0, 0, 0, 0)), 331 new GlyphEntry(font, new TestGlyph('?', 0, 0, 0, 0, 0, 0)), 332 new GlyphEntry(nullFont, new TestGlyph('?', 0, 0, 0, 0, 0, 0)) 333 ), font); 334 335 builder.AddText("a"); 336 337 Assert.That(builder.Characters[0].Character, Is.EqualTo('a')); 338 } 339 340 /// <summary> 341 /// Tests that glyph lookup falls back to using the fallback character with the provided font name. 342 /// </summary> 343 [Test] 344 public void TestFallBackCharacterFallsBackWithFontName() 345 { 346 var font = new TestFontUsage("test"); 347 var nullFont = new TestFontUsage(null); 348 var builder = new TextBuilder(new TestStore( 349 new GlyphEntry(font, new TestGlyph('b', 0, 0, 0, 0, 0, 0)), 350 new GlyphEntry(nullFont, new TestGlyph('b', 0, 0, 0, 0, 0, 0)), 351 new GlyphEntry(font, new TestGlyph('?', 0, 0, 0, 0, 0, 0)), 352 new GlyphEntry(nullFont, new TestGlyph('?', 1, 0, 0, 0, 0, 0)) 353 ), font); 354 355 builder.AddText("a"); 356 357 Assert.That(builder.Characters[0].Character, Is.EqualTo('?')); 358 Assert.That(builder.Characters[0].XOffset, Is.EqualTo(0)); 359 } 360 361 /// <summary> 362 /// Tests that glyph lookup falls back to using the fallback character with no font name. 363 /// </summary> 364 [Test] 365 public void TestFallBackCharacterFallsBackWithNoFontName() 366 { 367 var font = new TestFontUsage("test"); 368 var nullFont = new TestFontUsage(null); 369 var builder = new TextBuilder(new TestStore( 370 new GlyphEntry(font, new TestGlyph('b', 0, 0, 0, 0, 0, 0)), 371 new GlyphEntry(nullFont, new TestGlyph('b', 0, 0, 0, 0, 0, 0)), 372 new GlyphEntry(font, new TestGlyph('b', 0, 0, 0, 0, 0, 0)), 373 new GlyphEntry(nullFont, new TestGlyph('?', 1, 0, 0, 0, 0, 0)) 374 ), font); 375 376 builder.AddText("a"); 377 378 Assert.That(builder.Characters[0].Character, Is.EqualTo('?')); 379 Assert.That(builder.Characters[0].XOffset, Is.EqualTo(1)); 380 } 381 382 /// <summary> 383 /// Tests that a null glyph is correctly handled. 384 /// </summary> 385 [Test] 386 public void TestFailedCharacterLookup() 387 { 388 var font = new TestFontUsage("test"); 389 var builder = new TextBuilder(new TestStore(), font); 390 391 builder.AddText("a"); 392 393 Assert.That(builder.Bounds, Is.EqualTo(Vector2.Zero)); 394 } 395 396 private readonly struct TestFontUsage 397 { 398 private readonly string family; 399 private readonly string weight; 400 private readonly bool italics; 401 private readonly bool fixedWidth; 402 403 public TestFontUsage(string family = null, string weight = null, bool italics = false, bool fixedWidth = false) 404 { 405 this.family = family; 406 this.weight = weight; 407 this.italics = italics; 408 this.fixedWidth = fixedWidth; 409 } 410 411 public static implicit operator FontUsage(TestFontUsage tfu) 412 => new FontUsage(tfu.family, font_size, tfu.weight, tfu.italics, tfu.fixedWidth); 413 } 414 415 private class TestStore : ITexturedGlyphLookupStore 416 { 417 private readonly GlyphEntry[] glyphs; 418 419 public TestStore(params GlyphEntry[] glyphs) 420 { 421 this.glyphs = glyphs; 422 } 423 424 public ITexturedCharacterGlyph Get(string fontName, char character) 425 { 426 if (string.IsNullOrEmpty(fontName)) 427 { 428 return glyphs.FirstOrDefault(g => g.Glyph.Character == character).Glyph; 429 } 430 431 return glyphs.FirstOrDefault(g => g.Font.FontName == fontName && g.Glyph.Character == character).Glyph; 432 } 433 434 public Task<ITexturedCharacterGlyph> GetAsync(string fontName, char character) => throw new System.NotImplementedException(); 435 } 436 437 private readonly struct GlyphEntry 438 { 439 public readonly FontUsage Font; 440 public readonly ITexturedCharacterGlyph Glyph; 441 442 public GlyphEntry(FontUsage font, ITexturedCharacterGlyph glyph) 443 { 444 Font = font; 445 Glyph = glyph; 446 } 447 } 448 449 private readonly struct TestGlyph : ITexturedCharacterGlyph 450 { 451 public Texture Texture => new Texture(1, 1); 452 public float XOffset { get; } 453 public float YOffset { get; } 454 public float XAdvance { get; } 455 public float Width { get; } 456 public float Height { get; } 457 public char Character { get; } 458 459 private readonly float glyphKerning; 460 461 public TestGlyph(char character, float xOffset, float yOffset, float xAdvance, float width, float height, float kerning) 462 { 463 glyphKerning = kerning; 464 Character = character; 465 XOffset = xOffset; 466 YOffset = yOffset; 467 XAdvance = xAdvance; 468 Width = width; 469 Height = height; 470 } 471 472 public float GetKerning<T>(T lastGlyph) 473 where T : ICharacterGlyph 474 => glyphKerning; 475 } 476 } 477}