A game framework written with osu! in mind.
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}