Adaug proiectul Unity

This commit is contained in:
2026-11-01 23:39:55 +02:00
parent cb14d9d37d
commit 863eac8f2c
23463 changed files with 5412601 additions and 0 deletions
@@ -0,0 +1,515 @@
using System.Threading;
using UnityEngine;
using UnityEditor;
using NUnit.Framework;
using UnityEngine.TestTools;
using UnityEngine.TextCore;
using UnityEngine.TextCore.LowLevel;
namespace TMPro
{
[Category("Text Parsing & Layout")]
[Ignore("Unstable tests // UUM-133195")]
internal class FontEngineTests
{
[OneTimeSetUp]
public void Setup()
{
// Check if "TextMesh Pro" folder is present in project
string folderPath = AssetDatabase.GUIDToAssetPath("f54d1bd14bd3ca042bd867b519fee8cc");
if (string.IsNullOrEmpty(folderPath))
{
// Import TMP Essential Resources and TMP Examples & Extras
TMP_PackageResourceImporter.ImportResources(true, true, false);
}
}
[TestCase("e3265ab4bf004d28a9537516768c1c75", "Liberation Sans", "Regular")]
[TestCase("4beb055f07aaff244873dec698d0363e", "Roboto", "Bold")]
[TestCase("997a43b767814dd0a7642ec9b78cba41", "Anton", "Regular")]
public void CreateFontAsset_from_FilePath(string fontFileGUID, string familyName, string styleName)
{
string filePath = AssetDatabase.GUIDToAssetPath(fontFileGUID);
TMP_FontAsset fontAsset = TMP_FontAsset.CreateFontAsset(filePath, 0, 90, 9, GlyphRenderMode.SDFAA, 512, 512);
Assert.NotNull(fontAsset);
Assert.AreEqual(fontAsset.faceInfo.familyName, familyName);
Assert.AreEqual(fontAsset.faceInfo.styleName, styleName);
}
[TestCase("e3265ab4bf004d28a9537516768c1c75", "Liberation Sans", "Regular")]
[TestCase("4beb055f07aaff244873dec698d0363e", "Roboto", "Bold")]
[TestCase("997a43b767814dd0a7642ec9b78cba41", "Anton", "Regular")]
public void CreateFontAsset_from_FontObject(string fontFileGUID, string familyName, string styleName)
{
string filePath = AssetDatabase.GUIDToAssetPath(fontFileGUID);
Font font = AssetDatabase.LoadAssetAtPath<Font>(filePath);
TMP_FontAsset fontAsset = TMP_FontAsset.CreateFontAsset(font, 90, 9, GlyphRenderMode.SDFAA, 512, 512);
Assert.NotNull(fontAsset);
Assert.AreEqual(fontAsset.faceInfo.familyName, familyName);
Assert.AreEqual(fontAsset.faceInfo.styleName, styleName);
}
[TestCase("e3265ab4bf004d28a9537516768c1c75", "Liberation Sans", "Regular")]
public void TryAddCharacters_SanityCheck(string fontFileGUID, string familyName, string styleName)
{
string filePath = AssetDatabase.GUIDToAssetPath(fontFileGUID);
TMP_FontAsset fontAsset = TMP_FontAsset.CreateFontAsset(filePath, 0, 90, 9, GlyphRenderMode.SDFAA, 512, 512);
Assert.NotNull(fontAsset);
fontAsset.TryAddCharacters("abc");
Assert.IsTrue(fontAsset.HasCharacters("abc"));
}
// =============================================
// FONT ENGINE - OPENTYPE TESTS
// =============================================
#if TEXTCORE_FONT_ENGINE_1_5_OR_NEWER
[TestCase("e3265ab4bf004d28a9537516768c1c75", 1)]
[TestCase("4beb055f07aaff244873dec698d0363e", 623)]
[TestCase("24007ea0bd4d6b2418f4caf1b06e2cb4", 43)]
public void GetSingleSubstitutionRecords(string fontFileGUID, int recordCount)
{
string filePath = AssetDatabase.GUIDToAssetPath(fontFileGUID);
if (FontEngine.LoadFontFace(filePath, 0, 0, out FontFaceHandle faceHandle) != FontEngineError.Success)
return;
UnityEngine.TextCore.LowLevel.SingleSubstitutionRecord[] records = FontEngine.GetAllSingleSubstitutionRecords(faceHandle);
if (records == null)
return;
Assert.AreEqual(recordCount, records.Length);
}
[TestCase("47a9b34e6f77bbd4d94f512d266bcd0c", 77)] // Inter - Regular
public void GetAlternateSubstitutionRecords(string fontFileGUID, int recordCount)
{
string filePath = AssetDatabase.GUIDToAssetPath(fontFileGUID);
if (FontEngine.LoadFontFace(filePath, 0, 0, out FontFaceHandle faceHandle) != FontEngineError.Success)
return;
UnityEngine.TextCore.LowLevel.AlternateSubstitutionRecord[] records = FontEngine.GetAllAlternateSubstitutionRecords(faceHandle);
if (records == null)
return;
Assert.AreEqual(recordCount, records.Length);
}
[TestCase("e3265ab4bf004d28a9537516768c1c75", 184)]
[TestCase("4beb055f07aaff244873dec698d0363e", 177)]
[TestCase("c9f6d0e7bc8541498c9a4799ba184ede", 5)]
public void GetLigatures(string fontFileGUID, int recordCount)
{
string filePath = AssetDatabase.GUIDToAssetPath(fontFileGUID);
if (FontEngine.LoadFontFace(filePath, 0, 0, out FontFaceHandle faceHandle) != FontEngineError.Success)
return;
UnityEngine.TextCore.LowLevel.LigatureSubstitutionRecord[] records = FontEngine.GetAllLigatureSubstitutionRecords(faceHandle);
if (records == null)
return;
Assert.AreEqual(recordCount, records.Length);
}
[TestCase("e3265ab4bf004d28a9537516768c1c75", 2016)]
[TestCase("4beb055f07aaff244873dec698d0363e", 70027)]
[TestCase("c9f6d0e7bc8541498c9a4799ba184ede", 1940)]
public void GetPairAdjustmentRecords(string fontFileGUID, int recordCount)
{
string filePath = AssetDatabase.GUIDToAssetPath(fontFileGUID);
if (FontEngine.LoadFontFace(filePath, 0, 0, out FontFaceHandle faceHandle) != FontEngineError.Success)
return;
GlyphPairAdjustmentRecord[] records = FontEngine.GetAllPairAdjustmentRecords(faceHandle);
if (records == null)
return;
Assert.AreEqual(recordCount, records.Length);
}
[TestCase("e3265ab4bf004d28a9537516768c1c75", 8911)]
[TestCase("4beb055f07aaff244873dec698d0363e", 0)]
[TestCase("24007ea0bd4d6b2418f4caf1b06e2cb4", 432)]
public void GetMarkToBaseAdjustmentRecords(string fontFileGUID, int recordCount)
{
string filePath = AssetDatabase.GUIDToAssetPath(fontFileGUID);
if (FontEngine.LoadFontFace(filePath, 0, 0, out FontFaceHandle faceHandle) != FontEngineError.Success)
return;
UnityEngine.TextCore.LowLevel.MarkToBaseAdjustmentRecord[] records = FontEngine.GetAllMarkToBaseAdjustmentRecords(faceHandle);
if (records == null)
return;
Assert.AreEqual(recordCount, records.Length);
}
[TestCase("e3265ab4bf004d28a9537516768c1c75", 0)]
[TestCase("4beb055f07aaff244873dec698d0363e", 0)]
[TestCase("24007ea0bd4d6b2418f4caf1b06e2cb4", 324)]
public void GetMarkToMarkAdjustmentRecords(string fontFileGUID, int recordCount)
{
string filePath = AssetDatabase.GUIDToAssetPath(fontFileGUID);
if (FontEngine.LoadFontFace(filePath, 0, 0, out FontFaceHandle faceHandle) != FontEngineError.Success)
return;
UnityEngine.TextCore.LowLevel.MarkToMarkAdjustmentRecord[] records = FontEngine.GetAllMarkToMarkAdjustmentRecords(faceHandle);
if (records == null)
return;
Assert.AreEqual(recordCount, records.Length);
}
// GetOpenTypeFontFeatureList throws NotImplementedException with new FontEngine changes to support FontFeature (TEXTCORE_FONT_ENGINE_1_5_OR_NEWER)
/*
[TestCase("e3265ab4bf004d28a9537516768c1c75", 0)]
[TestCase("4beb055f07aaff244873dec698d0363e", 0)]
[TestCase("24007ea0bd4d6b2418f4caf1b06e2cb4", 324)]
[TestCase("47a9b34e6f77bbd4d94f512d266bcd0c", 324)]
[TestCase("d01f31227e1b4cd49bc293f44aab2253", 324)]
public void GetFontFeatureList(string fontFileGUID, int recordCount)
{
string filePath = AssetDatabase.GUIDToAssetPath(fontFileGUID);
if (FontEngine.LoadFontFace(filePath) != FontEngineError.Success)
return;
OpenTypeFeature[] fontFeatureList = FontEngine.GetOpenTypeFontFeatureList();
if (fontFeatureList == null)
return;
Assert.AreEqual(recordCount, fontFeatureList.Length);
} */
#endif
}
#if TEXTCORE_FONT_ENGINE_1_6_OR_NEWER
[Category("Text Parsing & Layout")]
internal class FontFaceHandleTests
{
[OneTimeSetUp]
public void Setup()
{
// These tests load fonts from the TMP Essential Resources and Examples & Extras. Import
// whichever is missing; the prebuild step may already have imported the essentials alone.
bool importEssentials = string.IsNullOrEmpty(AssetDatabase.GUIDToAssetPath("f54d1bd14bd3ca042bd867b519fee8cc"));
bool importExamples = string.IsNullOrEmpty(AssetDatabase.GUIDToAssetPath("4beb055f07aaff244873dec698d0363e"));
if (importEssentials || importExamples)
TMP_PackageResourceImporter.ImportResources(importEssentials, importExamples, false);
// If the font files these tests depend on still cannot be resolved (resources unavailable
// in this environment), skip the whole fixture rather than failing — but log why so the
// skip is visible in the test output.
if (string.IsNullOrEmpty(AssetDatabase.GUIDToAssetPath("e3265ab4bf004d28a9537516768c1c75")))
{
Debug.LogWarning("FontFaceHandleTests skipped: required TMP font resources are not present in the project.");
Assert.Ignore("Required TMP font resources are not present; skipping FontFaceHandle tests.");
}
}
[TestCase("e3265ab4bf004d28a9537516768c1c75")]
[TestCase("4beb055f07aaff244873dec698d0363e")]
public void LoadFontFace_ReturnsValidHandle(string fontFileGUID)
{
string filePath = AssetDatabase.GUIDToAssetPath(fontFileGUID);
FontEngineError error = FontEngine.LoadFontFace(filePath, 90, 0, out FontFaceHandle faceHandle);
Assert.AreEqual(FontEngineError.Success, error);
Assert.IsTrue(faceHandle.IsValid);
}
[Test]
public void LoadFontFace_WithInvalidPath_ReturnsErrorAndDefaultHandle()
{
FontEngineError error = FontEngine.LoadFontFace("invalid/path/does_not_exist.ttf", 90, 0, out FontFaceHandle faceHandle);
Assert.AreNotEqual(FontEngineError.Success, error);
Assert.IsFalse(faceHandle.IsValid);
}
[TestCase("e3265ab4bf004d28a9537516768c1c75", "Liberation Sans", "Regular")]
[TestCase("4beb055f07aaff244873dec698d0363e", "Roboto", "Bold")]
public void GetFaceInfo_via_Handle_MatchesGlobalGetFaceInfo(string fontFileGUID, string familyName, string styleName)
{
string filePath = AssetDatabase.GUIDToAssetPath(fontFileGUID);
FontEngineError error = FontEngine.LoadFontFace(filePath, 90, 0, out FontFaceHandle faceHandle);
Assert.AreEqual(FontEngineError.Success, error);
FaceInfo infoViaHandle = FontEngine.GetFaceInfo(faceHandle);
FaceInfo infoGlobal = FontEngine.GetFaceInfo();
Assert.AreEqual(familyName, infoViaHandle.familyName);
Assert.AreEqual(styleName, infoViaHandle.styleName);
Assert.AreEqual(infoGlobal.familyName, infoViaHandle.familyName);
Assert.AreEqual(infoGlobal.pointSize, infoViaHandle.pointSize);
}
// Loads two fonts, stashes their handles, then uses each handle to restore the correct face
// without a fresh LoadFontFace call — the core correctness guarantee of FontFaceHandle.
[TestCase("e3265ab4bf004d28a9537516768c1c75", "Liberation Sans", "4beb055f07aaff244873dec698d0363e", "Roboto")]
public void FontFaceHandle_RestoresCorrectFace(string guidA, string familyA, string guidB, string familyB)
{
string pathA = AssetDatabase.GUIDToAssetPath(guidA);
string pathB = AssetDatabase.GUIDToAssetPath(guidB);
Assert.AreEqual(FontEngineError.Success, FontEngine.LoadFontFace(pathA, 90, 0, out FontFaceHandle handleA));
Assert.AreEqual(FontEngineError.Success, FontEngine.LoadFontFace(pathB, 72, 0, out FontFaceHandle handleB));
// Active face is now B — restore A via its handle.
FaceInfo infoA = FontEngine.GetFaceInfo(handleA);
Assert.AreEqual(familyA, infoA.familyName);
Assert.AreEqual(90, infoA.pointSize);
// Restore B via its handle.
FaceInfo infoB = FontEngine.GetFaceInfo(handleB);
Assert.AreEqual(familyB, infoB.familyName);
Assert.AreEqual(72, infoB.pointSize);
}
[TestCase("e3265ab4bf004d28a9537516768c1c75", 2016)]
[TestCase("4beb055f07aaff244873dec698d0363e", 70027)]
public void GetPairAdjustmentRecords_via_Handle(string fontFileGUID, int recordCount)
{
string filePath = AssetDatabase.GUIDToAssetPath(fontFileGUID);
FontEngineError error = FontEngine.LoadFontFace(filePath, 90, 0, out FontFaceHandle faceHandle);
Assert.AreEqual(FontEngineError.Success, error);
GlyphPairAdjustmentRecord[] records = FontEngine.GetAllPairAdjustmentRecords(faceHandle);
Assert.IsNotNull(records);
Assert.AreEqual(recordCount, records.Length);
}
// Exercises the single-pair GPOS lookup whose loop previously used end() as its condition
// (iterating past the end of the records vector) and indexed the cached map via operator[]
// (default-inserting a missing key). Derives a real pair from the font's own records, then
// probes a first glyph against an impossible partner to drive the no-match path that ran
// past the end of the vector.
[TestCase("e3265ab4bf004d28a9537516768c1c75")] // Liberation Sans (has kerning)
public void GetPairAdjustmentRecord_via_Handle(string fontFileGUID)
{
string filePath = AssetDatabase.GUIDToAssetPath(fontFileGUID);
Assert.AreEqual(FontEngineError.Success, FontEngine.LoadFontFace(filePath, 90, 0, out FontFaceHandle faceHandle));
GlyphPairAdjustmentRecord[] all = FontEngine.GetAllPairAdjustmentRecords(faceHandle);
Assert.IsNotNull(all);
Assert.Greater(all.Length, 0);
// A real (first, second) pair taken from the font's own data must resolve to itself.
uint first = all[0].firstAdjustmentRecord.glyphIndex;
uint second = all[0].secondAdjustmentRecord.glyphIndex;
GlyphPairAdjustmentRecord match = FontEngine.GetPairAdjustmentRecord(faceHandle, first, second);
Assert.AreEqual(first, match.firstAdjustmentRecord.glyphIndex);
Assert.AreEqual(second, match.secondAdjustmentRecord.glyphIndex);
// A first glyph that has records, paired with an impossible partner, must scan to the end
// and return an empty record rather than reading past it.
GlyphPairAdjustmentRecord noMatch = FontEngine.GetPairAdjustmentRecord(faceHandle, first, 0xFFFFFFFE);
Assert.AreEqual(0u, noMatch.secondAdjustmentRecord.glyphIndex);
}
[TestCase("e3265ab4bf004d28a9537516768c1c75")]
public void UnloadFontFace_via_Handle_InvalidatesHandle(string fontFileGUID)
{
string filePath = AssetDatabase.GUIDToAssetPath(fontFileGUID);
Assert.AreEqual(FontEngineError.Success, FontEngine.LoadFontFace(filePath, 90, 0, out FontFaceHandle faceHandle));
Assert.IsTrue(faceHandle.IsValid);
Assert.AreEqual(FontEngineError.Success, FontEngine.UnloadFontFace(faceHandle));
// After unloading, operations using the stale handle should fail with InvalidFontFaceHandle.
// The native layer logs a warning when it cannot resolve the stale handle — that is expected here.
LogAssert.Expect(LogType.Warning, "FontEngine: Font face handle no longer resolves to a cached font face.");
FontEngineError result = FontEngine.SetFaceSize(ref faceHandle, 90);
Assert.AreEqual(FontEngineError.Invalid_FontFaceHandle, result);
}
[TestCase("e3265ab4bf004d28a9537516768c1c75")]
public void StaleHandle_OperationsReturnGracefully(string fontFileGUID)
{
string filePath = AssetDatabase.GUIDToAssetPath(fontFileGUID);
Assert.AreEqual(FontEngineError.Success, FontEngine.LoadFontFace(filePath, 90, 0, out FontFaceHandle faceHandle));
Assert.AreEqual(FontEngineError.Success, FontEngine.UnloadFontFace(faceHandle));
// GetFaceInfo — returns a zeroed FaceInfo rather than throwing.
LogAssert.Expect(LogType.Warning, "FontEngine: Font face handle no longer resolves to a cached font face.");
FaceInfo faceInfo = FontEngine.GetFaceInfo(faceHandle);
Assert.AreEqual(0, faceInfo.pointSize);
// TryGetGlyphIndex — returns false rather than throwing.
LogAssert.Expect(LogType.Warning, "FontEngine: Font face handle no longer resolves to a cached font face.");
bool found = FontEngine.TryGetGlyphIndex(faceHandle, 65, out uint glyphIndex);
Assert.IsFalse(found);
Assert.AreEqual(0u, glyphIndex);
// GetGlyphIndex — returns 0 rather than throwing.
LogAssert.Expect(LogType.Warning, "FontEngine: Font face handle no longer resolves to a cached font face.");
uint index = FontEngine.GetGlyphIndex(faceHandle, 65);
Assert.AreEqual(0u, index);
}
// A scalable font requested at two different point sizes resolves to the SAME cached FT_Face
// (MakeFontID excludes pixel size for scalable fonts), so both handles share one face — and
// one per-face lock. Each handle must still operate at, and report, its own size: the handle
// path resolves the face locally and re-applies the requested size under the lock rather than
// relying on whatever size the previous caller left on the shared face.
[TestCase("e3265ab4bf004d28a9537516768c1c75", "Liberation Sans")]
public void SameScalableFont_DifferentSizes_TrackSizePerHandle(string fontFileGUID, string familyName)
{
string filePath = AssetDatabase.GUIDToAssetPath(fontFileGUID);
Assert.AreEqual(FontEngineError.Success, FontEngine.LoadFontFace(filePath, 90, 0, out FontFaceHandle handle90));
Assert.AreEqual(FontEngineError.Success, FontEngine.LoadFontFace(filePath, 30, 0, out FontFaceHandle handle30));
// Interleave repeatedly; each handle must re-assert its own size every time.
for (int i = 0; i < 4; i++)
{
FaceInfo info90 = FontEngine.GetFaceInfo(handle90);
Assert.AreEqual(familyName, info90.familyName);
Assert.AreEqual(90, info90.pointSize, "handle90 should report size 90 regardless of interleaving");
FaceInfo info30 = FontEngine.GetFaceInfo(handle30);
Assert.AreEqual(familyName, info30.familyName);
Assert.AreEqual(30, info30.pointSize, "handle30 should report size 30 regardless of interleaving");
}
}
// Two distinct faces accessed concurrently must not corrupt each other: with face-threading
// the handle path never touches a shared global current-face, so each thread keeps seeing its
// own font/size. (Distinct FT_Faces are independently safe per the FreeType contract.)
// Faces are loaded on the main thread up front (face creation is not yet concurrency-safe);
// the workers only read through their already-valid handles, which the per-face lock guards.
[TestCase("e3265ab4bf004d28a9537516768c1c75", "Liberation Sans", "4beb055f07aaff244873dec698d0363e", "Roboto")]
public void ConcurrentHandleAccess_DistinctFaces_RemainCorrect(string guidA, string familyA, string guidB, string familyB)
{
string pathA = AssetDatabase.GUIDToAssetPath(guidA);
string pathB = AssetDatabase.GUIDToAssetPath(guidB);
Assert.AreEqual(FontEngineError.Success, FontEngine.LoadFontFace(pathA, 90, 0, out FontFaceHandle handleA));
Assert.AreEqual(FontEngineError.Success, FontEngine.LoadFontFace(pathB, 72, 0, out FontFaceHandle handleB));
const int iterations = 200;
string failureA = null;
string failureB = null;
var tA = new Thread(() => { failureA = HammerFaceInfo(handleA, familyA, 90, iterations); });
var tB = new Thread(() => { failureB = HammerFaceInfo(handleB, familyB, 72, iterations); });
tA.Start();
tB.Start();
tA.Join();
tB.Join();
Assert.IsNull(failureA, failureA);
Assert.IsNull(failureB, failureB);
}
// Two handles to the SAME scalable font at different sizes contend on one shared FT_Face.
// The per-face lock plus per-handle size re-application must keep each thread's view at its
// own size; without the lock the threads would stomp the shared face's active size and read
// each other's metrics.
[TestCase("e3265ab4bf004d28a9537516768c1c75", "Liberation Sans")]
public void ConcurrentHandleAccess_SameFontDifferentSizes_RemainCorrect(string fontFileGUID, string familyName)
{
string filePath = AssetDatabase.GUIDToAssetPath(fontFileGUID);
Assert.AreEqual(FontEngineError.Success, FontEngine.LoadFontFace(filePath, 90, 0, out FontFaceHandle handle90));
Assert.AreEqual(FontEngineError.Success, FontEngine.LoadFontFace(filePath, 30, 0, out FontFaceHandle handle30));
const int iterations = 200;
string failure90 = null;
string failure30 = null;
var t90 = new Thread(() => { failure90 = HammerFaceInfo(handle90, familyName, 90, iterations); });
var t30 = new Thread(() => { failure30 = HammerFaceInfo(handle30, familyName, 30, iterations); });
t90.Start();
t30.Start();
t90.Join();
t30.Join();
Assert.IsNull(failure90, failure90);
Assert.IsNull(failure30, failure30);
}
// Exercises FontFaceCacheLock: a worker thread repeatedly resolves a stable handle while the
// main thread churns the font face cache by loading and unloading a DIFFERENT face (insert /
// erase, and the rehash they can trigger). Without the cache lock, the worker's lookup inside
// ResolveFaceLocked would race the main-thread structural mutation of the shared cache. The
// stable face is never unloaded, so the worker must keep reading its correct family.
[TestCase("e3265ab4bf004d28a9537516768c1c75", "Liberation Sans", "4beb055f07aaff244873dec698d0363e")]
public void ConcurrentResolve_WhileCacheChurns_RemainsStable(string stableGuid, string stableFamily, string churnGuid)
{
string stablePath = AssetDatabase.GUIDToAssetPath(stableGuid);
string churnPath = AssetDatabase.GUIDToAssetPath(churnGuid);
Assert.AreEqual(FontEngineError.Success, FontEngine.LoadFontFace(stablePath, 90, 0, out FontFaceHandle stableHandle));
const int iterations = 200;
string failure = null;
var reader = new Thread(() =>
{
for (int i = 0; i < iterations && failure == null; i++)
{
FaceInfo info = FontEngine.GetFaceInfo(stableHandle);
if (info.familyName != stableFamily)
{
failure = $"Expected {stableFamily}, got '{info.familyName}' on iteration {i}";
return;
}
}
});
reader.Start();
// Main-thread cache churn: load + unload another face repeatedly (writers take the cache lock).
for (int i = 0; i < iterations; i++)
{
if (FontEngine.LoadFontFace(churnPath, 72, 0, out FontFaceHandle churnHandle) == FontEngineError.Success)
FontEngine.UnloadFontFace(churnHandle);
}
reader.Join();
Assert.IsNull(failure, failure);
}
// Repeatedly resolves the handle and checks the face reports the expected family/size.
// Returns a description of the first mismatch, or null on success (asserted on the main
// thread after Join — NUnit assertions don't propagate from worker threads).
static string HammerFaceInfo(FontFaceHandle handle, string expectedFamily, int expectedSize, int iterations)
{
for (int i = 0; i < iterations; i++)
{
FaceInfo info = FontEngine.GetFaceInfo(handle);
if (info.familyName != expectedFamily || info.pointSize != expectedSize)
return $"Expected {expectedFamily}@{expectedSize}, got '{info.familyName}'@{info.pointSize} on iteration {i}";
}
return null;
}
}
#endif
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ec4b2559effa72e42a09339d71efef3d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,323 @@
using System.Collections.Generic;
using System.IO;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TextCore;
using Object = UnityEngine.Object;
namespace TMPro
{
/// <summary>
/// Coverage for the source string mapping that SetArraySizes writes into
/// TMP_CharacterInfo.index and TMP_CharacterInfo.stringLength when several code points are
/// folded into a single rendered character - a "cluster". Two constructs produce one:
/// a base character followed by a variation selector, and a ligature.
///
/// The regression these guard against: the VARIATION SELECTOR and LIGATURES blocks advance
/// the text processing index past the elements they consume, and index / stringLength used
/// to be read from textProcessingArray[i] *after* that advance. A cluster therefore
/// reported the span of its LAST consumed element instead of the span of the whole cluster,
/// so index pointed into the middle of the cluster. TMP_InputField derives every caret
/// position and every delete range from those two fields, which is why the first Backspace
/// after typing an Ideographic Variation Sequence appeared to do nothing. The input field
/// half of this is covered by TMP_InputFieldClusterTests.
///
/// Both renderers carry a duplicated copy of SetArraySizes, so every case here runs against
/// TextMeshPro and TextMeshProUGUI.
/// </summary>
[Category("Text Parsing & Layout")]
internal class TMP_CharacterClusterTests: TMPTestSuiteWithEssentialResources
{
// U+FE0F VARIATION SELECTOR-16. A BMP selector: one UTF-16 code unit.
const string k_VariationSelector16 = "\uFE0F";
// U+E0100 VARIATION SELECTOR-17, the first Ideographic Variation Sequence selector.
// It lives in plane 14, so it is encoded as a surrogate pair: two UTF-16 code units.
// This is the construct from the original bug report.
const string k_VariationSelector17 = "\U000E0100";
// U+1F600 GRINNING FACE. A plain surrogate pair with no variation selector, used as the
// regression guard for characters the cluster accounting must leave alone.
const string k_AstralCharacter = "\U0001F600";
// Copy of the default font asset carrying a synthesized 'f' + 'i' ligature record, so the
// ligature case does not depend on which font the test project happens to ship.
TMP_FontAsset m_LigatureFontAsset;
TextMeshPro m_TextComponent;
TextMeshProUGUI m_TextComponentUGUI;
GameObject m_CanvasObject;
[OneTimeSetUp]
public void OneTimeSetUp()
{
GameObject textObject = new GameObject("Text Object");
m_TextComponent = textObject.AddComponent<TextMeshPro>();
m_TextComponent.fontSize = 18;
m_CanvasObject = new GameObject("Canvas Object");
m_CanvasObject.AddComponent<Canvas>();
GameObject textObjectUGUI = new GameObject("Text Object UGUI");
textObjectUGUI.transform.SetParent(m_CanvasObject.transform);
m_TextComponentUGUI = textObjectUGUI.AddComponent<TextMeshProUGUI>();
m_TextComponentUGUI.fontSize = 18;
// The default font asset is static and ships with no ligature records, so no font in this
// project can produce an 'fi' substitution. The accounting under test is pure span arithmetic
// and does not care whether the ligature glyph is the real one, so the fixture synthesizes a
// record on a private copy rather than depending on a font file that may not be imported.
m_LigatureFontAsset = Object.Instantiate(TMP_Settings.defaultFontAsset);
m_LigatureFontAsset.name = "Ligature Test Font Asset";
m_LigatureFontAsset.ReadFontAssetDefinition();
Assert.IsTrue(m_LigatureFontAsset.characterLookupTable.ContainsKey('f') &&
m_LigatureFontAsset.characterLookupTable.ContainsKey('i') &&
m_LigatureFontAsset.characterLookupTable.ContainsKey('x'),
"The default font asset must contain 'f', 'i' and 'x' for the ligature fixture.");
uint fGlyphIndex = m_LigatureFontAsset.characterLookupTable['f'].glyphIndex;
uint iGlyphIndex = m_LigatureFontAsset.characterLookupTable['i'].glyphIndex;
// Reusing a glyph already in the atlas keeps TryAddGlyphInternal on its early-out path
// (TMP_FontAsset.cs:2414), which is what lets a static font asset perform the substitution.
uint ligatureGlyphIndex = m_LigatureFontAsset.characterLookupTable['x'].glyphIndex;
m_LigatureFontAsset.fontFeatureTable.ligatureRecords.Add(new LigatureSubstitutionRecord
{
componentGlyphIDs = new[] { fGlyphIndex, iGlyphIndex },
ligatureGlyphID = ligatureGlyphIndex
});
m_LigatureFontAsset.InitializeLigatureSubstitutionLookupDictionary();
}
[OneTimeTearDown]
public void OneTimeTearDown()
{
if (m_TextComponentUGUI != null)
{
Object.DestroyImmediate(m_TextComponentUGUI.gameObject);
m_TextComponentUGUI = null;
}
if (m_CanvasObject != null)
{
Object.DestroyImmediate(m_CanvasObject);
m_CanvasObject = null;
}
if (m_TextComponent != null)
{
Object.DestroyImmediate(m_TextComponent.gameObject);
m_TextComponent = null;
}
if (m_LigatureFontAsset != null)
{
m_LigatureFontAsset.atlasTextures = null;
m_LigatureFontAsset.m_AtlasTexture = null;
m_LigatureFontAsset.material = null;
Object.DestroyImmediate(m_LigatureFontAsset);
m_LigatureFontAsset = null;
}
}
/// <summary>
/// The two renderers hold independent copies of SetArraySizes, so each case is asserted
/// against both.
/// </summary>
TMP_Text[] textComponents => new TMP_Text[] { m_TextComponent, m_TextComponentUGUI };
static void AssertCharacterSpan(TMP_Text textComponent, int characterIndex, int expectedIndex, int expectedStringLength)
{
TMP_CharacterInfo characterInfo = textComponent.textInfo.characterInfo[characterIndex];
Assert.AreEqual(expectedIndex, characterInfo.index,
$"[{textComponent.GetType().Name}] characterInfo[{characterIndex}].index should be the first code unit of the cluster.");
Assert.AreEqual(expectedStringLength, characterInfo.stringLength,
$"[{textComponent.GetType().Name}] characterInfo[{characterIndex}].stringLength should cover every code unit of the cluster.");
}
[Test]
public void VariationSelector16_IsFoldedIntoTheSpanOfItsBaseCharacter()
{
// "a" + VS16 renders as one character built from two code units.
const string sourceText = "a" + k_VariationSelector16;
foreach (TMP_Text textComponent in textComponents)
{
textComponent.text = sourceText;
textComponent.ForceMeshUpdate();
Assert.AreEqual(1, textComponent.textInfo.characterCount,
$"[{textComponent.GetType().Name}] A variation selector should not be counted as a character of its own.");
// Before the fix this reported (index 1, stringLength 1) - the selector's own span.
AssertCharacterSpan(textComponent, 0, 0, 2);
}
}
[Test]
public void VariationSelector17_IsFoldedIntoTheSpanOfItsBaseCharacter()
{
// "a" + VS17 renders as one character built from three code units, because the
// selector is a surrogate pair. This is the shape from the original bug report.
const string sourceText = "a" + k_VariationSelector17;
foreach (TMP_Text textComponent in textComponents)
{
textComponent.text = sourceText;
textComponent.ForceMeshUpdate();
Assert.AreEqual(1, textComponent.textInfo.characterCount,
$"[{textComponent.GetType().Name}] A variation selector should not be counted as a character of its own.");
// Before the fix this reported (index 1, stringLength 2) - the selector's own
// span - so TMP_InputField deleted only the surrogate pair and left the base
// character behind, or indexed a stale characterInfo slot entirely.
AssertCharacterSpan(textComponent, 0, 0, 3);
}
}
[Test]
public void VariationSelectorCluster_KeepsTheSpansOfSurroundingCharacters()
{
// 'a' | 'b' + VS17 | 'c' -> three characters over five code units.
const string sourceText = "ab" + k_VariationSelector17 + "c";
foreach (TMP_Text textComponent in textComponents)
{
textComponent.text = sourceText;
textComponent.ForceMeshUpdate();
Assert.AreEqual(3, textComponent.textInfo.characterCount,
$"[{textComponent.GetType().Name}] Unexpected character count.");
AssertCharacterSpan(textComponent, 0, 0, 1);
AssertCharacterSpan(textComponent, 1, 1, 3);
AssertCharacterSpan(textComponent, 2, 4, 1);
}
}
[Test]
public void CharacterSpans_AreContiguousAndCoverTheWholeSourceString()
{
// The invariant TMP_InputField's caret arithmetic depends on: consecutive characters
// describe adjacent, non-overlapping spans that together cover the source string.
// A cluster used to leave a hole here, because its index skipped the base character.
const string sourceText = "ab" + k_VariationSelector17 + "c" + k_VariationSelector16 + "d";
foreach (TMP_Text textComponent in textComponents)
{
textComponent.text = sourceText;
textComponent.ForceMeshUpdate();
TMP_TextInfo textInfo = textComponent.textInfo;
int characterCount = textInfo.characterCount;
Assert.AreEqual(0, textInfo.characterInfo[0].index,
$"[{textComponent.GetType().Name}] The first character should start at the beginning of the source string.");
for (int i = 0; i < characterCount - 1; i++)
{
Assert.AreEqual(textInfo.characterInfo[i].index + textInfo.characterInfo[i].stringLength,
textInfo.characterInfo[i + 1].index,
$"[{textComponent.GetType().Name}] characterInfo[{i}] should end exactly where characterInfo[{i + 1}] begins.");
}
TMP_CharacterInfo lastCharacter = textInfo.characterInfo[characterCount - 1];
Assert.AreEqual(sourceText.Length, lastCharacter.index + lastCharacter.stringLength,
$"[{textComponent.GetType().Name}] The characters together should cover the whole source string.");
}
}
[Test]
public void PlainCharacters_SpansAreUnchanged()
{
// Regression guard: text with no cluster must map one character to one code unit.
const string sourceText = "abc";
foreach (TMP_Text textComponent in textComponents)
{
textComponent.text = sourceText;
textComponent.ForceMeshUpdate();
Assert.AreEqual(3, textComponent.textInfo.characterCount,
$"[{textComponent.GetType().Name}] Unexpected character count.");
AssertCharacterSpan(textComponent, 0, 0, 1);
AssertCharacterSpan(textComponent, 1, 1, 1);
AssertCharacterSpan(textComponent, 2, 2, 1);
}
}
[Test]
public void SurrogatePairWithoutVariationSelector_SpanIsUnchanged()
{
// Regression guard: a surrogate pair is already one character over two code units
// and no cluster block runs for it, so the accounting must not touch it. The default
// font asset does not necessarily carry this glyph, but the span is captured before
// the missing glyph substitution region, so the mapping holds either way.
const string sourceText = "a" + k_AstralCharacter;
foreach (TMP_Text textComponent in textComponents)
{
textComponent.text = sourceText;
textComponent.ForceMeshUpdate();
Assert.AreEqual(2, textComponent.textInfo.characterCount,
$"[{textComponent.GetType().Name}] Unexpected character count.");
AssertCharacterSpan(textComponent, 0, 0, 1);
AssertCharacterSpan(textComponent, 1, 1, 2);
}
}
[Test]
public void Ligature_IsFoldedIntoTheSpanOfItsFirstComponent()
{
const string sourceText = "fi";
foreach (TMP_Text textComponent in textComponents)
{
TMP_FontAsset originalFont = textComponent.font;
List<OTL_FeatureTag> originalFontFeatures = new List<OTL_FeatureTag>(textComponent.fontFeatures);
try
{
textComponent.font = m_LigatureFontAsset;
List<OTL_FeatureTag> fontFeatures = new List<OTL_FeatureTag>(originalFontFeatures);
if (!fontFeatures.Contains(OTL_FeatureTag.liga))
{
// Assign through the property rather than mutating the list in place, so
// the text object is marked dirty the way it would be from user code.
fontFeatures.Add(OTL_FeatureTag.liga);
textComponent.fontFeatures = fontFeatures;
}
textComponent.text = sourceText;
textComponent.ForceMeshUpdate();
Assert.AreEqual(1, textComponent.textInfo.characterCount,
$"[{textComponent.GetType().Name}] 'fi' should be substituted by a single ligature character.");
// Before the fix this reported (index 1, stringLength 1) - the span of the
// last component consumed - so the start of the cluster was lost.
AssertCharacterSpan(textComponent, 0, 0, 2);
}
finally
{
// Leave the shared fixture components as the other tests expect them.
textComponent.fontFeatures = originalFontFeatures;
textComponent.font = originalFont;
textComponent.text = string.Empty;
}
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4bb24419713edc74180cebbba7460c96
@@ -0,0 +1,37 @@
using NUnit.Framework;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine.SceneManagement;
namespace TMPro
{
internal class TMP_ControlTests
{
Scene scene;
[SetUp]
public void Setup()
{
// Create a new scene and open it
scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene);
}
[TestCase("GameObject/UI (Canvas)/Dropdown - TextMeshPro")]
[TestCase("GameObject/UI (Canvas)/Button - TextMeshPro")]
[TestCase("GameObject/UI (Canvas)/Input Field - TextMeshPro")]
[TestCase("GameObject/UI (Canvas)/Text - TextMeshPro")]
public void TMPControlCreationAndUndoTest(string menuItem)
{
Assert.AreEqual(0, scene.rootCount);
EditorApplication.ExecuteMenuItem(menuItem);
// After creating a TMP control, objects in the scene should be Canvas, EventSystem, and the TMP control
Assert.AreEqual(2, scene.rootCount);
Undo.PerformUndo();
// After undoing, the scene should be back to its original state
Assert.AreEqual(0, scene.rootCount);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5c221dffa89042038c946376c731c918
timeCreated: 1705430351
@@ -0,0 +1,628 @@
using UnityEngine;
using UnityEditor;
using NUnit.Framework;
using System;
using System.IO;
using System.Text;
using Object = UnityEngine.Object;
namespace TMPro
{
[Category("Text Parsing & Layout")]
internal class TMP_EditorTests
{
private TextMeshPro m_TextComponent;
// Characters: 22 Spaces: 4 Words: 5 Lines:
private const string m_TextBlock_00 = "A simple line of text.";
// Characters: 104 Spaces: 14 Words: 15 Lines:
private const string m_TextBlock_01 = "Unity 2017 introduces new features that help teams of artists and developers build experiences together.";
// Characters: 1500 Spaces: 228 Words: 241
private const string m_TextBlock_02 = "The European languages are members of the same family. Their separate existence is a myth. For science, music, sport, etc, Europe uses the same vocabulary. The languages only differ in their grammar, their pronunciation and their most common words." +
"Everyone realizes why a new common language would be desirable: one could refuse to pay expensive translators.To achieve this, it would be necessary to have uniform grammar, pronunciation and more common words.If several languages coalesce, the grammar of the resulting language is more simple and regular than that of the individual languages." +
"The new common language will be more simple and regular than the existing European languages.It will be as simple as Occidental; in fact, it will be Occidental.To an English person, it will seem like simplified English, as a skeptical Cambridge friend of mine told me what Occidental is. The European languages are members of the same family." +
"Their separate existence is a myth. For science, music, sport, etc, Europe uses the same vocabulary.The languages only differ in their grammar, their pronunciation and their most common words.Everyone realizes why a new common language would be desirable: one could refuse to pay expensive translators.To achieve this, it would be necessary to" +
"have uniform grammar, pronunciation and more common words.If several languages coalesce, the grammar of the resulting language is more simple and regular than that of the individual languages.The new common language will be";
// Characters: 2500 Spaces: 343 Words: 370
private const string m_TextBlock_03 = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. " +
"Nullam dictum felis eu pede mollis pretium.Integer tincidunt.Cras dapibus.Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim.Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus.Phasellus viverra nulla ut metus varius laoreet.Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue.Curabitur ullamcorper ultricies nisi. " +
"Nam eget dui.Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum.Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem.Maecenas nec odio et ante tincidunt tempus.Donec vitae sapien ut libero venenatis faucibus.Nullam quis ante.Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. " +
"Donec sodales sagittis magna. Sed consequat, leo eget bibendum sodales, augue velit cursus nunc, quis gravida magna mi a libero. Fusce vulputate eleifend sapien. Vestibulum purus quam, scelerisque ut, mollis sed, nonummy id, metus.Nullam accumsan lorem in dui.Cras ultricies mi eu turpis hendrerit fringilla.Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; In ac dui quis mi consectetuer lacinia. Nam pretium turpis et arcu. " +
"Duis arcu tortor, suscipit eget, imperdiet nec, imperdiet iaculis, ipsum. Sed aliquam ultrices mauris.Integer ante arcu, accumsan a, consectetuer eget, posuere ut, mauris.Praesent adipiscing. Phasellus ullamcorper ipsum rutrum nunc.Nunc nonummy metus.Vestibulum volutpat pretium libero. Cras id dui.Aenean ut eros et nisl sagittis vestibulum.Nullam nulla eros, ultricies sit amet, nonummy id, imperdiet feugiat, pede.Sed lectus. Donec mollis hendrerit risus. Phasellus nec sem in justo pellentesque facilisis. " +
"Etiam imperdiet imperdiet orci. Nunc nec neque.Phasellus leo dolor, tempus non, auctor et, hendrerit quis, nisi.Curabitur ligula sapien, tincidunt non, euismod vitae, posuere imperdiet, leo.Maecenas malesuada. Praesent nan. The end of this of this long block of text.";
// Characters: 3423 Spaces: 453 Words: 500
private const string m_TextBlock_04 = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.Aenean commodo ligula eget dolor.Aenean massa.Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem.Nulla consequat massa quis enim.Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu.In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo.Nullam dictum felis eu pede mollis pretium.Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus." +
"Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim.Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus.Phasellus viverra nulla ut metus varius laoreet.Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue.Curabitur ullamcorper ultricies nisi. Nam eget dui.Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum.Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem.Maecenas nec odio et ante tincidunt tempus.Donec vitae sapien ut libero venenatis faucibus.Nullam quis ante." +
"Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed consequat, leo eget bibendum sodales, augue velit cursus nunc, quis gravida magna mi a libero. Fusce vulputate eleifend sapien. Vestibulum purus quam, scelerisque ut, mollis sed, nonummy id, metus.Nullam accumsan lorem in dui.Cras ultricies mi eu turpis hendrerit fringilla.Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; In ac dui quis mi consectetuer lacinia.Nam pretium turpis et arcu." +
"Duis arcu tortor, suscipit eget, imperdiet nec, imperdiet iaculis, ipsum. Sed aliquam ultrices mauris.Integer ante arcu, accumsan a, consectetuer eget, posuere ut, mauris.Praesent adipiscing. Phasellus ullamcorper ipsum rutrum nunc.Nunc nonummy metus.Vestibulum volutpat pretium libero. Cras id dui.Aenean ut eros et nisl sagittis vestibulum.Nullam nulla eros, ultricies sit amet, nonummy id, imperdiet feugiat, pede.Sed lectus. Donec mollis hendrerit risus. Phasellus nec sem in justo pellentesque facilisis.Etiam imperdiet imperdiet orci. Nunc nec neque." +
"Phasellus leo dolor, tempus non, auctor et, hendrerit quis, nisi.Curabitur ligula sapien, tincidunt non, euismod vitae, posuere imperdiet, leo.Maecenas malesuada. Praesent congue erat at massa.Sed cursus turpis vitae tortor.Donec posuere vulputate arcu. Phasellus accumsan cursus velit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed aliquam, nisi quis porttitor congue, elit erat euismod orci, ac placerat dolor lectus quis orci.Phasellus consectetuer vestibulum elit.Aenean tellus metus, bibendum sed, posuere ac, mattis non, nunc.Vestibulum fringilla pede sit amet augue." +
"In turpis. Pellentesque posuere. Praesent turpis. Aenean posuere, tortor sed cursus feugiat, nunc augue blandit nunc, eu sollicitudin urna dolor sagittis lacus. Donec elit libero, sodales nec, volutpat a, suscipit non, turpis.Nullam sagittis. Suspendisse pulvinar, augue ac venenatis condimentum, sem libero volutpat nibh, nec pellentesque velit pede quis nunc. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Fusce id purus.Ut varius tincidunt libero.Phasellus dolor.Maecenas vestibulum mollis";
//
private const string m_TextBlock_05 = "This block of text contains <b>bold</b> and <i>italicized</i> characters.";
private const string m_TextBlock_06 = "<align=center><style=H1><#ffffff><u>Multiple<#80f0ff> Alignment</color> per text object</u></color></style></align><line-height=2em>\n" +
"</line-height> The <<#ffffa0>align</color>> tag in TextMesh<#40a0ff>Pro</color> provides the ability to control the alignment of lines and paragraphs which is essential when working with text.\n" +
"<align=left> You may want some block of text to be<#80f0ff>left aligned</color> <<#ffffa0>align=<#80f0ff>left</color></color>> which is sort of the standard.</align>\n" +
"<style=Quote><#ffffa0>\"Using <#80f0ff>Center Alignment</color> <<#ffffa0>align=<#80f0ff>center</color></color>> for a title or displaying a quote is another good example of text alignment.\"</color></style>\n" +
"<align=right><#80f0ff>Right Alignment</color> <<#ffffa0>align=<#80f0ff>right</color></color>> can be useful to create contrast between lines and paragraphs of text.\n" +
"<align=justified><#80f0ff>Justified Alignment</color> <<#ffffa0>align=<#80f0ff>justified</color></color>> results in text that is flush on both the left and right margins. Used well, justified type can look clean and classy.\n" +
"<style=Quote><align=left><#ffffa0>\"Text formatting and alignment has a huge impact on how people will read and perceive your text.\"</color>\n" +
"<size=65%><align=right> -Stephan Bouchard</style>";
private readonly string[] testStrings = new string[] { m_TextBlock_00, m_TextBlock_01, m_TextBlock_02, m_TextBlock_03, m_TextBlock_04, m_TextBlock_05, m_TextBlock_06 };
private void AssertRenderedText(string expected)
{
expected ??= string.Empty;
m_TextComponent.ForceMeshUpdate();
Assert.AreEqual(expected.Length, m_TextComponent.textInfo.characterCount);
for (int i = 0; i < expected.Length; i++)
Assert.AreEqual(expected[i], m_TextComponent.textInfo.characterInfo[i].character, $"Character mismatch at index {i}.");
}
private void AssertCharacterStyle(int index, FontStyles style, bool expected)
{
FontStyles actualStyle = m_TextComponent.textInfo.characterInfo[index].style;
bool hasStyle = (actualStyle & style) == style;
Assert.AreEqual(expected, hasStyle, $"Expected character at index {index} to {(expected ? "have" : "not have")} style {style}.");
}
private void AssertCharacterStyleRange(int startIndex, int length, FontStyles style, bool expected)
{
for (int i = startIndex; i < startIndex + length; i++)
AssertCharacterStyle(i, style, expected);
}
[OneTimeSetUp]
public void OneTimeSetup()
{
if (Directory.Exists(Path.GetFullPath("Assets/TextMesh Pro")) || Directory.Exists(Path.GetFullPath("Packages/com.unity.textmeshpro.tests/TextMesh Pro")))
{
GameObject textObject = new GameObject("Text Object");
m_TextComponent = textObject.AddComponent<TextMeshPro>();
m_TextComponent.fontSize = 18;
}
else
{
Debug.Log("Skipping over Editor tests as TMP Essential Resources are missing from the current test project.");
Assert.Ignore();
}
}
[Test]
[TestCase("/Package Resources/TMP Essential Resources.unitypackage", "ce4ff17ca867d2b48b5c8a4181611901")]
[TestCase("/Package Resources/TMP Examples & Extras.unitypackage", "bc00e25696e4132499f56528d3fed2e3")]
[TestCase("/PackageConversionData.json", "05f5bfd584002f948982a1498890f9a9")]
public void InternalResourceCheck(string filePath, string guid)
{
string packageRelativePath = EditorUtilities.TMP_EditorUtility.packageRelativePath;
string packageFullPath = EditorUtilities.TMP_EditorUtility.packageFullPath;
Assert.AreEqual(AssetDatabase.AssetPathToGUID(packageRelativePath + filePath), guid);
Assert.IsTrue(File.Exists(packageFullPath + filePath));
}
// =============================================
// Font Asset Creation Tests
// =============================================
// =============================================
// Text Parsing and Layout Tests
// =============================================
[Test]
[TestCase(4, 3423, 453, 500, 1)]
[TestCase(3, 2500, 343, 370, 1)]
[TestCase(2, 1500, 228, 241, 1)]
[TestCase(1, 104, 14, 15, 1)]
[TestCase(0, 22, 4, 5, 1)]
public void TextParsing_TextInfoTest_WordWrappingDisabled(int sourceTextIndex, int characterCount, int spaceCount, int wordCount, int lineCount)
{
m_TextComponent.text = testStrings[sourceTextIndex];
m_TextComponent.textWrappingMode = TextWrappingModes.NoWrap;
m_TextComponent.alignment = TextAlignmentOptions.TopLeft;
// Size the RectTransform
m_TextComponent.rectTransform.sizeDelta = new Vector2(50, 5);
// Force text generation to populate the TextInfo data structure.
m_TextComponent.ForceMeshUpdate();
Assert.AreEqual(m_TextComponent.textInfo.characterCount, characterCount);
Assert.AreEqual(m_TextComponent.textInfo.spaceCount, spaceCount);
Assert.AreEqual(m_TextComponent.textInfo.wordCount, wordCount);
Assert.AreEqual(m_TextComponent.textInfo.lineCount, lineCount);
}
[Test]
[TestCase(4, 3423, 453, 500, 29)]
[TestCase(3, 2500, 343, 370, 21)]
[TestCase(2, 1500, 228, 241, 13)]
[TestCase(1, 104, 14, 15, 1)]
[TestCase(0, 22, 4, 5, 1)]
public void TextParsing_TextInfoTest_WordWrappingEnabled(int sourceTextIndex, int characterCount, int spaceCount, int wordCount, int lineCount)
{
m_TextComponent.text = testStrings[sourceTextIndex];
m_TextComponent.textWrappingMode = TextWrappingModes.Normal;
m_TextComponent.alignment = TextAlignmentOptions.TopLeft;
// Size the RectTransform
m_TextComponent.rectTransform.sizeDelta = new Vector2(100, 50);
// Force text generation to populate the TextInfo data structure.
m_TextComponent.ForceMeshUpdate();
Assert.AreEqual(m_TextComponent.textInfo.characterCount, characterCount);
Assert.AreEqual(m_TextComponent.textInfo.spaceCount, spaceCount);
Assert.AreEqual(m_TextComponent.textInfo.wordCount, wordCount);
Assert.AreEqual(m_TextComponent.textInfo.lineCount, lineCount);
}
[Test]
[TestCase(4, 3423, 453, 500, 27)]
[TestCase(3, 2500, 343, 370, 20)]
[TestCase(2, 1500, 228, 241, 13)]
public void TextParsing_TextInfoTest_TopJustifiedAlignment(int sourceTextIndex, int characterCount, int spaceCount, int wordCount, int lineCount)
{
m_TextComponent.text = testStrings[sourceTextIndex];
m_TextComponent.textWrappingMode = TextWrappingModes.Normal;
m_TextComponent.alignment = TextAlignmentOptions.TopJustified;
// Size the RectTransform
m_TextComponent.rectTransform.sizeDelta = new Vector2(100, 50);
// Force text generation to populate the TextInfo data structure.
m_TextComponent.ForceMeshUpdate();
Assert.AreEqual(m_TextComponent.textInfo.characterCount, characterCount);
Assert.AreEqual(m_TextComponent.textInfo.spaceCount, spaceCount);
Assert.AreEqual(m_TextComponent.textInfo.wordCount, wordCount);
Assert.AreEqual(m_TextComponent.textInfo.lineCount, lineCount);
}
[Test]
[TestCase(6, 768, 124, 126, 14)]
[TestCase(5, 59, 8, 9, 1)]
public void TextParsing_TextInfoTest_RichText(int sourceTextIndex, int characterCount, int spaceCount, int wordCount, int lineCount)
{
m_TextComponent.text = testStrings[sourceTextIndex];
m_TextComponent.textWrappingMode = TextWrappingModes.Normal;
m_TextComponent.alignment = TextAlignmentOptions.TopLeft;
// Size the RectTransform
m_TextComponent.rectTransform.sizeDelta = new Vector2(70, 35);
// Force text generation to populate the TextInfo data structure.
m_TextComponent.ForceMeshUpdate();
Assert.AreEqual(m_TextComponent.textInfo.characterCount, characterCount);
Assert.AreEqual(m_TextComponent.textInfo.spaceCount, spaceCount);
Assert.AreEqual(m_TextComponent.textInfo.wordCount, wordCount);
Assert.AreEqual(m_TextComponent.textInfo.lineCount, lineCount);
}
// =============================================
// Markup tag specific tests
// =============================================
[Test]
[TestCase("<scale=1.0>ABC</scale>", -35.0f, -33.8069763f, -33.8069763f, -32.6139526f, -32.6139526f, -31.3162785f)]
[TestCase("<scale=0.8>ABC</scale>", -35.0f, -34.0455818f, -34.0455818f, -33.0911636f, -33.0911636f, -32.0530243f)]
[TestCase("<scale=1.2>ABC</scale>", -35.0f, -33.5683708f, -33.5683708f, -32.1367455f, -32.1367455f, -30.5795345f)]
public void MarkupTag_Scale(string sourceText, float origin1, float advance1, float origin2, float advance2, float origin3, float advance3)
{
m_TextComponent.text = sourceText;
m_TextComponent.textWrappingMode = TextWrappingModes.Normal;
m_TextComponent.alignment = TextAlignmentOptions.TopLeft;
// Size the RectTransform
m_TextComponent.rectTransform.sizeDelta = new Vector2(70, 35);
// Force text generation to populate the TextInfo data structure.
m_TextComponent.ForceMeshUpdate();
Assert.AreEqual(origin1, m_TextComponent.textInfo.characterInfo[0].origin);
Assert.AreEqual(advance1, m_TextComponent.textInfo.characterInfo[0].xAdvance);
Assert.AreEqual(origin2, m_TextComponent.textInfo.characterInfo[1].origin);
Assert.AreEqual(advance2, m_TextComponent.textInfo.characterInfo[1].xAdvance);
Assert.AreEqual(origin3, m_TextComponent.textInfo.characterInfo[2].origin);
Assert.AreEqual(advance3, m_TextComponent.textInfo.characterInfo[2].xAdvance);
}
[Test]
[TestCase("<size=12>ABC</size>", -35.0f, -34.2046509f, -34.2046509f, -33.4093018f, -33.4093018f, -32.5441856f)]
[TestCase("<size=-6>ABC</size>", -35.0f, -34.2046509f, -34.2046509f, -33.4093018f, -33.4093018f, -32.5441856f)]
[TestCase("<size=+6>ABC</size>", -35.0f, -33.4093018f, -33.4093018f, -31.8186054f, -31.8186054f, -30.0883713f)]
[TestCase("<size=50%>ABC</size>", -35.0f, -34.4034882f, -34.4034882f, -33.8069763f, -33.8069763f, -33.1581383f)]
[TestCase("<size=150%>DEF</size>", -35.0f, -33.0534897f, -33.0534897f, -31.2639542f, -31.2639542f, -29.6000004f)]
[TestCase("<size=0.5em>ABC</size>", -35.0f, -34.4034882f, -34.4034882f, -33.8069763f, -33.8069763f, -33.1581383f)]
[TestCase("<size=1.5em>DEF</size>", -35.0f, -33.0534897f, -33.0534897f, -31.2639542f, -31.2639542f, -29.6000004f)]
public void MarkupTag_Size(string sourceText, float origin1, float advance1, float origin2, float advance2, float origin3, float advance3)
{
m_TextComponent.text = sourceText;
m_TextComponent.textWrappingMode = TextWrappingModes.Normal;
m_TextComponent.alignment = TextAlignmentOptions.TopLeft;
// Size the RectTransform
m_TextComponent.rectTransform.sizeDelta = new Vector2(70, 35);
// Force text generation to populate the TextInfo data structure.
m_TextComponent.ForceMeshUpdate();
Assert.AreEqual(origin1, m_TextComponent.textInfo.characterInfo[0].origin);
Assert.AreEqual(advance1, m_TextComponent.textInfo.characterInfo[0].xAdvance);
Assert.AreEqual(origin2, m_TextComponent.textInfo.characterInfo[1].origin);
Assert.AreEqual(advance2, m_TextComponent.textInfo.characterInfo[1].xAdvance);
Assert.AreEqual(origin3, m_TextComponent.textInfo.characterInfo[2].origin);
Assert.AreEqual(advance3, m_TextComponent.textInfo.characterInfo[2].xAdvance);
}
[Test]
[TestCase("<cspace=30>ABC</cspace>", -35.0f, -30.8069763f, -30.8069763f, -26.6139526f, -26.6139526f, -25.3162804f)]
[TestCase("<cspace=2em>ABC</cspace>", -35.0f, -30.2069759f, -30.2069759f, -25.4139519f, -25.4139519f, -24.1162796f)]
public void MarkupTag_Cspace(string sourceText, float origin1, float advance1, float origin2, float advance2, float origin3, float advance3)
{
m_TextComponent.text = sourceText;
m_TextComponent.textWrappingMode = TextWrappingModes.Normal;
m_TextComponent.alignment = TextAlignmentOptions.TopLeft;
// Size the RectTransform
m_TextComponent.rectTransform.sizeDelta = new Vector2(70, 35);
// Force text generation to populate the TextInfo data structure.
m_TextComponent.ForceMeshUpdate();
Assert.AreEqual(origin1, m_TextComponent.textInfo.characterInfo[0].origin);
Assert.AreEqual(advance1, m_TextComponent.textInfo.characterInfo[0].xAdvance);
Assert.AreEqual(origin2, m_TextComponent.textInfo.characterInfo[1].origin);
Assert.AreEqual(advance2, m_TextComponent.textInfo.characterInfo[1].xAdvance);
Assert.AreEqual(origin3, m_TextComponent.textInfo.characterInfo[2].origin);
Assert.AreEqual(advance3, m_TextComponent.textInfo.characterInfo[2].xAdvance);
}
[Test]
[TestCase("<mspace=30>ABC</mspace>", -34.0965118f, -32.0f, -31.1279068f, -29.0f, -28.1593018f, -26.0f)]
[TestCase("<mspace=2em>ABC</mspace>", -33.7965126f, -31.3999996f, -30.2279072f, -27.7999992f, -26.6593018f, -24.2000008f)]
public void MarkupTag_Mspace(string sourceText, float origin1, float advance1, float origin2, float advance2, float origin3, float advance3)
{
m_TextComponent.text = sourceText;
m_TextComponent.textWrappingMode = TextWrappingModes.Normal;
m_TextComponent.alignment = TextAlignmentOptions.TopLeft;
// Size the RectTransform
m_TextComponent.rectTransform.sizeDelta = new Vector2(70, 35);
// Force text generation to populate the TextInfo data structure.
m_TextComponent.ForceMeshUpdate();
Assert.AreEqual(origin1, m_TextComponent.textInfo.characterInfo[0].origin);
Assert.AreEqual(advance1, m_TextComponent.textInfo.characterInfo[0].xAdvance);
Assert.AreEqual(origin2, m_TextComponent.textInfo.characterInfo[1].origin);
Assert.AreEqual(advance2, m_TextComponent.textInfo.characterInfo[1].xAdvance);
Assert.AreEqual(origin3, m_TextComponent.textInfo.characterInfo[2].origin);
Assert.AreEqual(advance3, m_TextComponent.textInfo.characterInfo[2].xAdvance);
}
[Test]
[TestCase("A<space=18>B<space=18>C", -35.0f, -33.8069763f, -32.0069771f, -30.8139534f, -29.0139542f, -27.7162781f)]
[TestCase("A<space=1em>B<space=1em>C", -35.0f, -33.8069763f, -32.0069771f, -30.8139534f, -29.0139542f, -27.7162781f)]
public void MarkupTag_Space(string sourceText, float origin1, float advance1, float origin2, float advance2, float origin3, float advance3)
{
m_TextComponent.text = sourceText;
m_TextComponent.textWrappingMode = TextWrappingModes.Normal;
m_TextComponent.alignment = TextAlignmentOptions.TopLeft;
// Size the RectTransform
m_TextComponent.rectTransform.sizeDelta = new Vector2(70, 35);
// Force text generation to populate the TextInfo data structure.
m_TextComponent.ForceMeshUpdate();
Assert.AreEqual(origin1, m_TextComponent.textInfo.characterInfo[0].origin);
Assert.AreEqual(advance1, m_TextComponent.textInfo.characterInfo[0].xAdvance);
Assert.AreEqual(origin2, m_TextComponent.textInfo.characterInfo[1].origin);
Assert.AreEqual(advance2, m_TextComponent.textInfo.characterInfo[1].xAdvance);
Assert.AreEqual(origin3, m_TextComponent.textInfo.characterInfo[2].origin);
Assert.AreEqual(advance3, m_TextComponent.textInfo.characterInfo[2].xAdvance);
}
[Test]
[TestCase("A<pos=10%>B<pos=20%>C", -35.0f, -33.8069763f, -28.0f, -26.8069763f, -21.0f, -19.7023258f)]
[TestCase("A<pos=70>B<pos=140>C", -35.0f, -33.8069763f, -28.0f, -26.8069763f, -21.0f, -19.7023258f)]
[TestCase("A<pos=1.5em>B<pos=3em>CC", -35.0f, -33.8069763f, -32.2999992f, -31.1069775f, -29.6000004f, -28.3023262f)]
public void MarkupTag_Pos(string sourceText, float origin1, float advance1, float origin2, float advance2, float origin3, float advance3)
{
m_TextComponent.text = sourceText;
m_TextComponent.textWrappingMode = TextWrappingModes.Normal;
m_TextComponent.alignment = TextAlignmentOptions.TopLeft;
// Size the RectTransform
m_TextComponent.rectTransform.sizeDelta = new Vector2(70, 35);
// Force text generation to populate the TextInfo data structure.
m_TextComponent.ForceMeshUpdate();
Assert.AreEqual(origin1, m_TextComponent.textInfo.characterInfo[0].origin);
Assert.AreEqual(advance1, m_TextComponent.textInfo.characterInfo[0].xAdvance);
Assert.AreEqual(origin2, m_TextComponent.textInfo.characterInfo[1].origin);
Assert.AreEqual(advance2, m_TextComponent.textInfo.characterInfo[1].xAdvance);
Assert.AreEqual(origin3, m_TextComponent.textInfo.characterInfo[2].origin);
Assert.AreEqual(advance3, m_TextComponent.textInfo.characterInfo[2].xAdvance);
}
[Test]
[TestCase("<indent=18>ABC", -33.2000008f, -32.0069771f, -32.0069771f, -30.8139534f, -30.8139534f, -29.5162792f)]
[TestCase("<indent=1em>ABC", -33.2000008f, -32.0069771f, -32.0069771f, -30.8139534f, -30.8139534f, -29.5162792f)]
[TestCase("<indent=2.5%>ABC", -33.25f, -32.0569763f, -32.0569763f, -30.8639526f, -30.8639526f, -29.5662804f)]
public void MarkupTag_Indent(string sourceText, float origin1, float advance1, float origin2, float advance2, float origin3, float advance3)
{
m_TextComponent.text = sourceText;
m_TextComponent.textWrappingMode = TextWrappingModes.Normal;
m_TextComponent.alignment = TextAlignmentOptions.TopLeft;
// Size the RectTransform
m_TextComponent.rectTransform.sizeDelta = new Vector2(70, 35);
// Force text generation to populate the TextInfo data structure.
m_TextComponent.ForceMeshUpdate();
Assert.AreEqual(origin1, m_TextComponent.textInfo.characterInfo[0].origin);
Assert.AreEqual(advance1, m_TextComponent.textInfo.characterInfo[0].xAdvance);
Assert.AreEqual(origin2, m_TextComponent.textInfo.characterInfo[1].origin);
Assert.AreEqual(advance2, m_TextComponent.textInfo.characterInfo[1].xAdvance);
Assert.AreEqual(origin3, m_TextComponent.textInfo.characterInfo[2].origin);
Assert.AreEqual(advance3, m_TextComponent.textInfo.characterInfo[2].xAdvance);
}
#if TMP_TEST_RESOURCES
const string k_SpriteAssetPath = "Assets/TextMesh Pro/Resources/Sprite Assets/MixedIndexTest.asset";
[Test]
public void SpriteAssetIndexAreValidAfterReordering()
{
var spriteAsset = AssetDatabase.LoadAssetAtPath<TMP_SpriteAsset>(k_SpriteAssetPath);
if (spriteAsset == null)
{
Debug.LogError("Failed to load Sprite Asset at path: " + k_SpriteAssetPath);
return;
}
string text = $"<sprite name=\"cta_obsidianjade\">";
m_TextComponent.spriteAsset = spriteAsset;
m_TextComponent.text = text;
m_TextComponent.ForceMeshUpdate();
Assert.AreEqual(203, m_TextComponent.textInfo.characterInfo[0].textElement.glyphIndex, $"Mismatch between sprite index. Expected 203 but was {m_TextComponent.textInfo.characterInfo[0].textElement.glyphIndex}");
}
#endif
// =============================================
// SetText Tests
// =============================================
[Test]
public void SetText_String_Null_ClearsText()
{
m_TextComponent.SetText("Previously assigned text.");
m_TextComponent.ForceMeshUpdate();
Assert.Greater(m_TextComponent.textInfo.characterCount, 0);
m_TextComponent.SetText((string)null);
AssertRenderedText(string.Empty);
}
[Test]
public void SetText_CharArray_Null_ClearsText()
{
m_TextComponent.SetText("Previously assigned text.");
m_TextComponent.ForceMeshUpdate();
Assert.Greater(m_TextComponent.textInfo.characterCount, 0);
m_TextComponent.SetText((char[])null);
AssertRenderedText(string.Empty);
}
[Test]
public void SetText_StringBuilder_Null_ClearsText()
{
m_TextComponent.SetText("Previously assigned text.");
m_TextComponent.ForceMeshUpdate();
Assert.Greater(m_TextComponent.textInfo.characterCount, 0);
m_TextComponent.SetText((StringBuilder)null);
AssertRenderedText(string.Empty);
}
[Test]
public void SetText_ReadOnlySpan_Empty_ClearsText()
{
m_TextComponent.SetText("Previously assigned text.");
m_TextComponent.ForceMeshUpdate();
Assert.Greater(m_TextComponent.textInfo.characterCount, 0);
m_TextComponent.SetText(ReadOnlySpan<char>.Empty);
AssertRenderedText(string.Empty);
}
[Test]
[TestCase("")]
[TestCase("A")]
[TestCase("A simple line of text.")]
[TestCase("The quick brown fox jumps over the lazy dog while TextMesh Pro lays out the generated characters.")]
public void SetText_String_PlainText_RendersExpectedCharacters(string sourceText)
{
m_TextComponent.SetText(sourceText);
AssertRenderedText(sourceText);
}
[Test]
[TestCase("")]
[TestCase("A")]
[TestCase("Character array input with enough length to exercise more than a trivial copy.")]
public void SetText_CharArray_PlainText_RendersExpectedCharacters(string sourceText)
{
m_TextComponent.SetText(sourceText.ToCharArray());
AssertRenderedText(sourceText);
}
[Test]
[TestCase("")]
[TestCase("A")]
[TestCase("StringBuilder input with a longer sentence to validate characterInfo output.")]
public void SetText_StringBuilder_PlainText_RendersExpectedCharacters(string sourceText)
{
m_TextComponent.SetText(new StringBuilder(sourceText));
AssertRenderedText(sourceText);
}
[Test]
[TestCase("")]
[TestCase("A")]
[TestCase("ReadOnlySpan input with a longer sentence to validate characterInfo output.")]
public void SetText_ReadOnlySpan_PlainText_RendersExpectedCharacters(string sourceText)
{
m_TextComponent.SetText(sourceText.AsSpan());
AssertRenderedText(sourceText);
}
[Test]
public void SetText_ReadOnlySpan_Range_CopiesExpectedSubstring()
{
const string sourceText = "0123456789";
m_TextComponent.SetText(sourceText.AsSpan(), 2, 4);
AssertRenderedText("2345");
}
[Test]
public void SetText_ReadOnlySpan_Range_ClampsStartAndLength()
{
const string sourceText = "abcdef";
m_TextComponent.SetText(sourceText.AsSpan(), 4, 10);
AssertRenderedText("ef");
}
[Test]
[TestCase("This block contains <b>bold</b> and <i>italicized</i> characters.", "This block contains bold and italicized characters.")]
[TestCase("A <u>underlined</u> word and a <s>struck</s> word.", "A underlined word and a struck word.")]
[TestCase("Nested <b><i>bold italic</i></b> text returns to normal.", "Nested bold italic text returns to normal.")]
[TestCase("A <color=#ff0000>red</color> word renders without color tags.", "A red word renders without color tags.")]
public void SetText_String_RichTextTags_RendersExpectedCharacters(string sourceText, string expectedText)
{
m_TextComponent.SetText(sourceText);
AssertRenderedText(expectedText);
}
[Test]
[TestCase("A <style=H1>heading</style> B", "A *heading* B")]
[TestCase("A <style=Quote>quote</style> B", "A quote B")]
public void SetText_String_StyleTags_RendersExpectedCharacters(string sourceText, string expectedText)
{
m_TextComponent.SetText(sourceText);
AssertRenderedText(expectedText);
}
[Test]
public void SetText_String_BoldTag_SetsAndClearsBoldStyle()
{
m_TextComponent.SetText("A <b>bold</b> B");
AssertRenderedText("A bold B");
AssertCharacterStyleRange(0, 2, FontStyles.Bold, false);
AssertCharacterStyleRange(2, 4, FontStyles.Bold, true);
AssertCharacterStyleRange(6, 2, FontStyles.Bold, false);
}
[Test]
public void SetText_String_ItalicTag_SetsAndClearsItalicStyle()
{
m_TextComponent.SetText("A <i>italic</i> B");
AssertRenderedText("A italic B");
AssertCharacterStyleRange(0, 2, FontStyles.Italic, false);
AssertCharacterStyleRange(2, 6, FontStyles.Italic, true);
AssertCharacterStyleRange(8, 2, FontStyles.Italic, false);
}
[Test]
public void SetText_String_NestedBoldItalicTags_SetAndClearExpectedStyles()
{
m_TextComponent.SetText("A <b><i>BI</i></b> C");
AssertRenderedText("A BI C");
AssertCharacterStyleRange(0, 2, FontStyles.Bold, false);
AssertCharacterStyleRange(0, 2, FontStyles.Italic, false);
AssertCharacterStyleRange(2, 2, FontStyles.Bold, true);
AssertCharacterStyleRange(2, 2, FontStyles.Italic, true);
AssertCharacterStyleRange(4, 2, FontStyles.Bold, false);
AssertCharacterStyleRange(4, 2, FontStyles.Italic, false);
}
[Test]
public void SetText_String_UnderlineAndStrikethroughTags_SetAndClearExpectedStyles()
{
m_TextComponent.SetText("A <u>U</u> <s>S</s> Z");
AssertRenderedText("A U S Z");
AssertCharacterStyle(2, FontStyles.Underline, true);
AssertCharacterStyle(4, FontStyles.Strikethrough, true);
AssertCharacterStyleRange(0, 2, FontStyles.Underline, false);
AssertCharacterStyleRange(3, 4, FontStyles.Underline, false);
AssertCharacterStyleRange(0, 4, FontStyles.Strikethrough, false);
AssertCharacterStyleRange(5, 2, FontStyles.Strikethrough, false);
}
// Add tests that check position of individual characters in a complex block of text.
// These test also use the data contained inside the TMP_TextInfo class.
[OneTimeTearDown]
public void OneTimeTearDown()
{
if (m_TextComponent != null)
{
Object.DestroyImmediate(m_TextComponent.gameObject);
m_TextComponent = null;
}
// // Remove TMP Essential Resources if they were imported in the project as a result of running tests.
// if (TMPro_EventManager.temporaryResourcesImported == true)
// {
// if (Directory.Exists(Path.GetFullPath("Assets/TextMesh Pro")))
// {
// AssetDatabase.DeleteAsset("Assets/TextMesh Pro");
// TMPro_EventManager.temporaryResourcesImported = false;
// }
// }
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 592f7288ed0df2c4b884e2cd9baac023
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,117 @@
using UnityEngine;
using UnityEngine.TestTools;
using NUnit.Framework;
using System.IO;
using Object = UnityEngine.Object;
namespace TMPro
{
[Category("Text Parsing & Layout")]
internal class TMP_TextTests
{
private TextMeshPro m_TextComponent;
GameObject m_canvasObject;
private TextMeshProUGUI m_TextComponentUGUI;
[OneTimeSetUp]
public void OneTimeSetUp()
{
if (Directory.Exists(Path.GetFullPath("Assets/TextMesh Pro")) || Directory.Exists(Path.GetFullPath("Packages/com.unity.textmeshpro.tests/TextMesh Pro")))
{
GameObject textObject = new GameObject("Text Object");
m_TextComponent = textObject.AddComponent<TextMeshPro>();
m_canvasObject = new GameObject("Canvas Object");
m_canvasObject.AddComponent<Canvas>();
GameObject textObjectUGUI = new GameObject("Text Object UGUI");
textObjectUGUI.transform.SetParent(m_canvasObject.transform);
m_TextComponentUGUI = textObjectUGUI.AddComponent<TextMeshProUGUI>();
}
else
{
Debug.Log("Skipping over Editor tests as TMP Essential Resources are missing from the current test project.");
Assert.Ignore();
}
}
[OneTimeTearDown]
public void OneTimeTearDown()
{
if (m_TextComponent != null)
{
Object.DestroyImmediate(m_TextComponent.gameObject);
Object.DestroyImmediate(m_TextComponentUGUI.gameObject);
Object.DestroyImmediate(m_canvasObject);
m_TextComponent = null;
m_TextComponentUGUI = null;
}
}
[Test]
public void Text3D_EllipsisOverflow_DoesNotThrow_WhenMaterialAtQuadCap()
{
// (UUM-134477) Reproduction for the IndexOutOfRangeException thrown when a TMP
// text with Overflow=Ellipsis is rendered and the ellipsis font
// asset's material has already filled its 16383-quad submesh.
m_TextComponent.rectTransform.sizeDelta = new Vector2(10, 15);
m_TextComponent.text = new string('A', 20000);
m_TextComponent.fontSize = 1f;
m_TextComponent.textWrappingMode = TextWrappingModes.Normal;
m_TextComponent.overflowMode = TextOverflowModes.Ellipsis;
Assert.DoesNotThrow(() => m_TextComponent.ForceMeshUpdate(ignoreActiveState: true, forceTextReparsing: true));
LogAssert.NoUnexpectedReceived();
// Sanity check: the fixture must produce more than 16383 visible
// characters before truncation — otherwise the test passes for the
// wrong reason (the cap was never reached and the bug not exercised).
Assert.IsTrue(m_TextComponent.isTextOverflowing, "Test fixture did not cause text to overflow. Tune rect/font/text to ensure overflow occurs and ellipsis insertion is triggered.");
Assert.Greater(m_TextComponent.textInfo.characterCount, 16384, "Test fixture did not push enough characters into the mesh to exercise the 16383-quad cap. Tune rect/font/text to fit more characters.");
}
[Test]
public void TextUGUI_EllipsisOverflow_DoesNotThrow_WhenMaterialAtQuadCap()
{
// (UUM-134477) Reproduction for the IndexOutOfRangeException thrown when a TMP
// text with Overflow=Ellipsis is rendered and the ellipsis font
// asset's material has already filled its 16383-quad submesh.
m_TextComponentUGUI.rectTransform.sizeDelta = new Vector2(100, 150);
m_TextComponentUGUI.text = new string('A', 20000);
m_TextComponentUGUI.fontSize = 1f;
m_TextComponentUGUI.textWrappingMode = TextWrappingModes.Normal;
m_TextComponentUGUI.overflowMode = TextOverflowModes.Ellipsis;
Assert.DoesNotThrow(() => m_TextComponentUGUI.ForceMeshUpdate(ignoreActiveState: true, forceTextReparsing: true));
LogAssert.NoUnexpectedReceived();
// Sanity check: the fixture must produce more than 16383 visible
// characters before truncation — otherwise the test passes for the
// wrong reason (the cap was never reached and the bug not exercised).
Assert.IsTrue(m_TextComponentUGUI.isTextOverflowing, "Test fixture did not cause text to overflow. Tune rect/font/text to ensure overflow occurs and ellipsis insertion is triggered.");
Assert.Greater(m_TextComponentUGUI.textInfo.characterCount, 16384, "Test fixture did not push enough characters into the mesh to exercise the 16383-quad cap. Tune rect/font/text to fit more characters.");
}
[Test]
public void TextUGUI_GetPreferredValues_HyphenPrecededByWhitespaceAtNarrowWidth_Terminates()
{
// (UUM-152921) Measure read line numbers left over from the previous layout and looped forever on " -".
TextOverflowModes previousOverflowMode = m_TextComponentUGUI.overflowMode;
try
{
m_TextComponentUGUI.fontSize = 36;
m_TextComponentUGUI.textWrappingMode = TextWrappingModes.Normal;
m_TextComponentUGUI.overflowMode = TextOverflowModes.Overflow;
m_TextComponentUGUI.rectTransform.sizeDelta = new Vector2(25, 50);
m_TextComponentUGUI.text = "New Text";
m_TextComponentUGUI.ForceMeshUpdate(ignoreActiveState: true, forceTextReparsing: true);
m_TextComponentUGUI.text = "a -N";
Assert.Greater(m_TextComponentUGUI.GetPreferredValues(25, 0).y, 0);
m_TextComponentUGUI.ForceMeshUpdate(ignoreActiveState: true, forceTextReparsing: true);
Assert.Greater(m_TextComponentUGUI.textInfo.lineCount, 1);
}
finally
{
m_TextComponentUGUI.overflowMode = previousOverflowMode;
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d082183c35deb87489e26f9c7ba8e41f
@@ -0,0 +1,18 @@
{
"name": "Unity.TextMeshPro.Editor.Tests",
"references": [
"Unity.TextMeshPro",
"Unity.TextMeshPro.Editor",
"Unity.TextMeshPro.Tests"
],
"optionalUnityReferences": [
"TestAssemblies"
],
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": []
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 656e461844099ae43a609ff6109b0877
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: