PageRenderTime 60ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/ContentSystem/Rendering/FontData.cs

#
C# | 1441 lines | 826 code | 144 blank | 471 comment | 90 complexity | 36f42db1212527fad6e6864f395aab3a MD5 | raw file
Possible License(s): Apache-2.0
  1. using System;
  2. using System.Collections.Generic;
  3. using Delta.ContentSystem.Rendering.Helpers;
  4. using Delta.ContentSystem.Xml;
  5. using Delta.Engine;
  6. using Delta.Utilities;
  7. using Delta.Utilities.Datatypes;
  8. using Delta.Utilities.Datatypes.Advanced;
  9. using Delta.Utilities.Graphics;
  10. using Delta.Utilities.Helpers;
  11. using Delta.Utilities.Xml;
  12. using NUnit.Framework;
  13. namespace Delta.ContentSystem.Rendering
  14. {
  15. /// <summary>
  16. /// That class takes care about the correct loading of the requested bitmap
  17. /// font based on the given parameters in the constructor. The font bitmap
  18. /// textures itself (font maps) will be created by the FontGenerator of the
  19. /// ContentServer.
  20. /// </summary>
  21. public class FontData : XmlData
  22. {
  23. #region Constants
  24. /// <summary>
  25. /// The minimal size (in points) that a font is allowed to have.
  26. /// </summary>
  27. public const int MinFontSize = 5;
  28. /// <summary>
  29. /// The maximal size (in points) that a font is allowed to have.
  30. /// </summary>
  31. public const int MaxFontSize = 72;
  32. /// <summary>
  33. /// Helper constant to initialize the "char" variables
  34. /// </summary>
  35. private const char NoChar = '\0';
  36. /// <summary>
  37. /// The fallback character which is used if a character isn't supported by
  38. /// a font.
  39. /// </summary>
  40. /// <remarks>
  41. /// Is used for parsing a text.
  42. /// </remarks>
  43. private const char FallbackChar = '?';
  44. #endregion
  45. #region Get (Static)
  46. /// <summary>
  47. /// Get and load content based on the content name. This method makes sure
  48. /// we do not load the same content twice (the constructor is protected).
  49. /// </summary>
  50. /// <param name="contentName">
  51. /// Content name we want to load, this is passed onto the Content System,
  52. /// which will do the actual loading with help of the Load method in this
  53. /// class.
  54. /// </param>
  55. /// <returns>
  56. /// The loaded Content object, always unique for the same name, this helps
  57. /// comparing data.
  58. /// </returns>
  59. public new static FontData Get(string contentName)
  60. {
  61. return Get<FontData>(contentName, ContentType.Font);
  62. }
  63. /// <summary>
  64. /// Get or load a font without the content name, but instead by searching
  65. /// for all the meta data: Font Family Name, Font Size and Font Style.
  66. /// Please note that other values like Font Tracking is ignored, you have
  67. /// to use the Get(contentName) overload for that.
  68. /// <para />
  69. /// This method will always return a font. If just a style or size was not
  70. /// found a content entry with the same font family is returned. If that
  71. /// is also not found the default font is returned (which is part of each
  72. /// project because the Engine content is always available as fallback
  73. /// content).
  74. /// </summary>
  75. /// <param name="fontFamilyName">
  76. /// Font family name (like "Verdana", "Arial", etc.).
  77. /// </param>
  78. /// <param name="fontSize">Font size (like 9pt, 12pt, 24pt, etc.).</param>
  79. /// <param name="fontStyle">Font style flags (bold, italic, sharp, etc.)
  80. /// </param>
  81. /// <returns>
  82. /// The loaded FontData content object. If this content was loaded before
  83. /// the same copy is used again.
  84. /// </returns>
  85. public static FontData Get(string fontFamilyName, int fontSize,
  86. FontStyle fontStyle)
  87. {
  88. ContentMetaData bestMatch = null;
  89. Dictionary<string, ContentMetaData> allFontContent =
  90. ContentManager.GetAllContentMetaData(ContentType.Font);
  91. foreach (ContentMetaData fontContent in allFontContent.Values)
  92. {
  93. // Note: ShaderFlags is used for ShaderFeatureFlags.
  94. if (fontContent.FontFamilyName.Compare(fontFamilyName))
  95. {
  96. // Did we find a matching font with the correct size and style?
  97. if (fontContent.FontSize == fontSize &&
  98. fontContent.FontStyle == fontStyle)
  99. {
  100. // Thats good. Get out of here and load the FontData!
  101. return Get(fontContent.Name);
  102. }
  103. // Just name matches, then only set this if we have nothing yet.
  104. if (bestMatch == null)
  105. {
  106. bestMatch = fontContent;
  107. }
  108. // If the font size matches too, replace the entry.
  109. if (fontContent.FontSize == fontSize)
  110. {
  111. if (bestMatch.FontSize != fontSize)
  112. {
  113. bestMatch = fontContent;
  114. }
  115. }
  116. }
  117. } // foreach
  118. // Did we find something? Then load that, but report that this is not
  119. // a perfect match.
  120. if (bestMatch != null)
  121. {
  122. Log.Warning(
  123. "Found a font with the font family name '" + fontFamilyName +
  124. "', but the size='" + fontSize + "' or style='" + fontStyle +
  125. "' did not match: " + bestMatch);
  126. // Return and load it anyway.
  127. return Get(bestMatch.Name);
  128. }
  129. Log.Warning(
  130. "Found no font with the font family name '" + fontFamilyName +
  131. "' (in any size, but the size='" + fontSize + "' and style='" +
  132. fontStyle + "' was requested).");
  133. // Nothing found? Then load the fallback font (will throw more warnings)!
  134. return Get("");
  135. }
  136. #endregion
  137. #region Default (Static)
  138. /// <summary>
  139. /// The font data which is chosen by default if a concrete one is not
  140. /// available (yet). This is always Verdana, 12pt, with an outline.
  141. /// </summary>
  142. public static FontData Default
  143. {
  144. get
  145. {
  146. if (defaultData == null)
  147. {
  148. defaultData = Get("Verdana", 12, FontStyle.AddOutline);
  149. } // if
  150. return defaultData;
  151. } // get
  152. }
  153. #endregion
  154. #region ResolutionFonts (Public)
  155. /// <summary>
  156. /// Font content entries can either be concrete font settings with an
  157. /// xml file for all the glyph data or just a parent entry with children
  158. /// that contain the resolution specific font data (4 children for the 4
  159. /// default resolutions, see Font.DetermineBestFont, which switches fonts
  160. /// at 320p, 480p, 640p and 960p).
  161. /// </summary>
  162. public FontData[] ResolutionFonts;
  163. #endregion
  164. #region FamilyName (Public)
  165. /// <summary>
  166. /// The family name of the font, e.g. Verdana, Consolas, etc.
  167. /// </summary>
  168. public string FamilyName
  169. {
  170. get
  171. {
  172. return data.FontFamilyName;
  173. }
  174. }
  175. #endregion
  176. #region SizeInPoints (Public)
  177. /// <summary>
  178. /// The size of the font in points, e.g. 12, 14, etc.
  179. /// </summary>
  180. public int SizeInPoints
  181. {
  182. get
  183. {
  184. return data.FontSize;
  185. }
  186. }
  187. #endregion
  188. #region Style (Public)
  189. /// <summary>
  190. /// The style how the characters will be shown, e.g. normal, bold, italic,
  191. /// etc.
  192. /// </summary>
  193. public FontStyle Style
  194. {
  195. get
  196. {
  197. return data.FontStyle;
  198. }
  199. }
  200. #endregion
  201. #region Tracking (Public)
  202. /// <summary>
  203. /// Defines the distance (in normalized percentage) which is added between
  204. /// 2 characters. This is only used for this FontData and set by the
  205. /// content system (cannot be changed by the user, unlike
  206. /// Font.TrackingMultiplier, which is just a multiplier to adjust spacing).
  207. /// <para/>
  208. /// 0.0 means no additional space (default)
  209. /// <para/>
  210. /// 1.0 means 100% of the AdvanceWidth of the character as additional space
  211. /// to the right.
  212. /// <para/>
  213. /// -1.0 means 100% of the AdvanceWidth of the character as "reverse" space
  214. /// to the left.
  215. /// </summary>
  216. public float Tracking
  217. {
  218. get
  219. {
  220. return data.FontTracking / 100.0f;
  221. }
  222. }
  223. #endregion
  224. #region PixelLineHeight (Public)
  225. /// <summary>
  226. /// The height of the font in pixel space based on the set font size.
  227. /// <para/>
  228. /// CAUTION: That is not the final height of the font on the screen,
  229. /// it is multiplied by the font meta data line height multiplier and then
  230. /// converted to quad space for rendering (see Font class). Finally it can
  231. /// also be modified by the Font class with the LineHeightMultiplier.
  232. /// </summary>
  233. public int PixelLineHeight
  234. {
  235. get;
  236. private set;
  237. }
  238. #endregion
  239. #region LeftOffset
  240. /// <summary>
  241. /// When each font character glyph is drawn by the FontGenerator several
  242. /// factors might offset the character like the distance from the baseline
  243. /// to the actual first pixel drawn and also margins added and effects
  244. /// like outlining and adding shadow might change the render size. In oder
  245. /// to still position the character to render correctly this offset must be
  246. /// added to the render position to correctly position each glyph when
  247. /// rendering a font.
  248. /// </summary>
  249. public int LeftOffset
  250. {
  251. get;
  252. private set;
  253. }
  254. #endregion
  255. #region TopOffset
  256. /// <summary>
  257. /// Similar to RenderMarginLeft each glyph might have been offseted because
  258. /// of margins used in FontGenerator plus outlining and shadow effects.
  259. /// In oder to still position the character to render correctly this offset
  260. /// must be added to the render position to correctly position each glyph
  261. /// when rendering a font.
  262. /// </summary>
  263. public int TopOffset
  264. {
  265. get;
  266. private set;
  267. }
  268. #endregion
  269. #region FontMapNames (Public)
  270. /// <summary>
  271. /// The list of the names of font maps where all characters are stored.
  272. /// </summary>
  273. public string[] FontMapNames
  274. {
  275. get;
  276. private set;
  277. }
  278. #endregion
  279. #region Internal
  280. #region glyphDictionary (Internal)
  281. /// <summary>
  282. /// Dictionary of each unicode character we have in the font maps and can
  283. /// use for drawing.
  284. /// </summary>
  285. internal Dictionary<char, Glyph> glyphDictionary;
  286. #endregion
  287. #endregion
  288. #region Private
  289. #region defaultData (Private)
  290. /// <summary>
  291. /// Default data
  292. /// </summary>
  293. private static FontData defaultData;
  294. #endregion
  295. #region kernDictionary (Private)
  296. /// <summary>
  297. /// Character spacing with kerning dictionary, which tells us how much
  298. /// spacing is needed between two characters. Most character combinations
  299. /// have no extra values in here, but some have (like W, I, etc.)
  300. /// </summary>
  301. private Dictionary<char, Dictionary<char, int>> kernDictionary;
  302. #endregion
  303. #endregion
  304. #region Constructors
  305. /// <summary>
  306. /// Creates a font from the content data. All font data is stored in xml
  307. /// files (in the Fonts content directory). Will also load all bitmaps
  308. /// required for this font.
  309. /// </summary>
  310. /// <param name="contentName">Font content name to load. If this is empty
  311. /// no content will be loaded (just fallback data will be set).</param>
  312. protected FontData(string contentName)
  313. // For the content we need to make sure that each font content name
  314. // is unique and therefore we need to add the size, etc. to the name!
  315. : base(contentName, ContentType.Font)
  316. {
  317. }
  318. #endregion
  319. #region GetGlyphDrawInfos (Public)
  320. /// <summary>
  321. /// Return the draw info of all glyphs that are needed to show the text
  322. /// on the screen (in pixel space).
  323. /// <para/>
  324. /// ASCII reference:
  325. /// http://www.tcp-ip-info.de/tcp_ip_und_internet/ascii.htm
  326. /// <para/>
  327. /// Note: The glyph info exists as single array but the data itself
  328. /// represents the text inclusive line breaks, so in other words the
  329. /// several text lines are "flattened" to a single-dimensioned array.
  330. /// </summary>
  331. /// <param name="text">Text</param>
  332. /// <param name="lineSpacing">Line spacing</param>
  333. /// <param name="textAlignment">Horizontal text alignment mode</param>
  334. /// <returns>
  335. /// List of GlyphDrawInfos ready for drawing.
  336. /// </returns>
  337. public GlyphDrawInfo[] GetGlyphDrawInfos(string text, float lineSpacing,
  338. HorizontalAlignment textAlignment)
  339. {
  340. Size infiniteSize = Size.Zero;
  341. return GetGlyphDrawInfos(text, lineSpacing, textAlignment, false, false,
  342. ref infiniteSize);
  343. }
  344. /// <summary>
  345. /// Return the draw info of all glyphs that are needed to show the text
  346. /// on the screen (in pixel space).
  347. /// <para/>
  348. /// ASCII reference:
  349. /// http://www.tcp-ip-info.de/tcp_ip_und_internet/ascii.htm
  350. /// <para/>
  351. /// Note: The glyph info exists as single array but the data itself
  352. /// represents the text inclusive line breaks, so in other words the
  353. /// several text lines are "flattened" to a single-dimensioned array.
  354. /// </summary>
  355. /// <param name="text">Text</param>
  356. /// <param name="lineSpacing">Line spacing</param>
  357. /// <param name="textAlignment">Horizontal text alignment mode</param>
  358. /// <param name="isClippingOn">Is clipping check required</param>
  359. /// <param name="maxTextSize">Max. available size in Pixel Space</param>
  360. /// <param name="isWordWrapOn">
  361. /// Indicates if the words of the given text should be wrapped or clipped
  362. /// (if enabled) at the end of a text line.
  363. /// </param>
  364. /// <returns>
  365. /// List of GlyphDrawInfos ready for drawing.
  366. /// </returns>
  367. public GlyphDrawInfo[] GetGlyphDrawInfos(string text, float lineSpacing,
  368. HorizontalAlignment textAlignment, bool isClippingOn, bool isWordWrapOn,
  369. ref Size maxTextSize)
  370. {
  371. if (FontMapNames.Length == 0)
  372. {
  373. // This warning is kind of a duplicate because we already got a warning
  374. // from the font data loading, but this one is useful because it
  375. // happens when trying to render some text, which will be aborted!
  376. return new GlyphDrawInfo[0];
  377. } // if
  378. // Define the list of glyphs which represents the drawing info for
  379. // each character of the text
  380. List<GlyphDrawInfo> glyphInfos = new List<GlyphDrawInfo>();
  381. // At first split the given text in several text lines
  382. // (if there are line breaks)
  383. List<float> textlineWidths;
  384. float maxTextlineWidth;
  385. List<List<char>> textlines = GetTextLines(text, lineSpacing, maxTextSize,
  386. textAlignment, isClippingOn, isWordWrapOn,
  387. out textlineWidths, out maxTextlineWidth);
  388. // The total height for every line
  389. float finalLineHeight = PixelLineHeight * lineSpacing;
  390. // The values of the drawing info of the last character
  391. GlyphDrawInfo lastDrawInfo = new GlyphDrawInfo
  392. {
  393. DrawArea = new Rectangle(Point.Zero, new Size(0, finalLineHeight)),
  394. UV = new Rectangle(Point.Zero, new Size(0, finalLineHeight)),
  395. };
  396. for (int lineId = 0; lineId < textlines.Count; lineId++)
  397. {
  398. List<char> textline = textlines[lineId];
  399. // Sanity check, because we only need to align and handle characters
  400. // if we have some
  401. if (textline.Count > 0)
  402. {
  403. switch (textAlignment)
  404. {
  405. // For left-aligned texts (that goes left-to-right) we need to
  406. // subtract the LeftSideBearing to make sure that every first
  407. // character of a text line starts really at the same left border
  408. // -> 'abcde' instead of 'abcde'
  409. // 'bcd' ' bcd'
  410. // 'cdef' 'cdef'
  411. case HorizontalAlignment.Left:
  412. char firstChar = textline[0];
  413. lastDrawInfo.DrawArea.X = MathHelper.Round(
  414. glyphDictionary[firstChar].LeftSideBearing);
  415. break;
  416. // For center-aligned text, we add here the LeftSideBearing too
  417. // like in the left-aligned mode, but additionally we still make
  418. // sure that the every text line is centered (based on the
  419. // longest text line)
  420. case HorizontalAlignment.Centered:
  421. firstChar = textline[0];
  422. lastDrawInfo.DrawArea.X = MathHelper.Round(
  423. (maxTextlineWidth - textlineWidths[lineId]) * 0.5f +
  424. glyphDictionary[firstChar].LeftSideBearing);
  425. break;
  426. // For right-aligned texts (that goes left-to-right) we need to
  427. // add the RightSideBearing of the last character of a text line
  428. // at the beginning the text line to make sure that every last
  429. // character of a text line ends really at the same right border
  430. // (because we all add the chars from left-to-right)
  431. // -> 'abcde' instead of 'abcde'
  432. // 'bcd' 'bcd '
  433. // 'cdef' 'cdef'
  434. case HorizontalAlignment.Right:
  435. char lastChar = textline[textline.Count - 1];
  436. lastDrawInfo.DrawArea.X = MathHelper.Round(
  437. (maxTextlineWidth - textlineWidths[lineId]) -
  438. glyphDictionary[lastChar].RightSideBearing);
  439. break;
  440. default:
  441. lastDrawInfo.DrawArea.X = 0;
  442. // Nothing to do for us
  443. break;
  444. } // switch
  445. // Also apply the offset for each letter
  446. lastDrawInfo.DrawArea.X -= LeftOffset;
  447. lastDrawInfo.DrawArea.Y -= TopOffset;
  448. // Now iterate through all characters of the text
  449. for (int charIndex = 0; charIndex < textline.Count; charIndex++)
  450. {
  451. char textChar = textline[charIndex];
  452. // Figure out which is the next character, because we need it for
  453. // the kerning to it
  454. char nextChar =
  455. charIndex + 1 < textline.Count
  456. ? textline[charIndex + 1]
  457. : NoChar;
  458. // Get now the character we have to handle here.
  459. // Note: We don't need to care about sanity checks, because that
  460. // has happen in the "GetTextlines()" already which means that we
  461. // get here only supported characters
  462. Glyph characterGlyph;
  463. glyphDictionary.TryGetValue(textChar, out characterGlyph);
  464. if (characterGlyph == null)
  465. {
  466. Log.Warning(
  467. "The glyph for the character '" + textChar +
  468. "' doesn't exists yet in the FontData '" + Name + "', will " +
  469. "use the '?' glyph instead.", false);
  470. characterGlyph = glyphDictionary['?'];
  471. } // if
  472. // Get the width of the current glyph that is used know the
  473. // required distance to the next character
  474. int glyphWidth = characterGlyph.GetDrawWidth(nextChar, Tracking);
  475. // and build the drawing info for the current character now
  476. GlyphDrawInfo newDrawInfo = new GlyphDrawInfo
  477. {
  478. DrawArea = new Rectangle(lastDrawInfo.DrawArea.TopLeft,
  479. characterGlyph.UV.Size),
  480. UV = characterGlyph.FontMapUV,
  481. FontMapId = characterGlyph.FontMapId,
  482. };
  483. // that we add the glyph info into the list
  484. glyphInfos.Add(newDrawInfo);
  485. // and update the last glyph drawing info with it
  486. lastDrawInfo = newDrawInfo;
  487. // with the position for the next character
  488. lastDrawInfo.DrawArea.X += glyphWidth;
  489. } // for
  490. } // if
  491. // Go to the next text line
  492. lastDrawInfo.DrawArea.Y += finalLineHeight;
  493. } // for
  494. return glyphInfos.ToArray();
  495. }
  496. #endregion
  497. #region ToString (Public)
  498. /// <summary>
  499. /// To String method, will just extend the Content.ToString method by
  500. /// some extra font meta information.
  501. /// </summary>
  502. /// <returns>A info string about this object instance.</returns>
  503. public override string ToString()
  504. {
  505. return base.ToString() + ", Font Family Name=" + data.FontFamilyName +
  506. ", Font Size=" + data.FontSize + ", Font Style=" + data.FontStyle;
  507. }
  508. #endregion
  509. #region Methods (Private)
  510. #region Load
  511. /// <summary>
  512. /// Native load method, will just load the xml data.
  513. /// </summary>
  514. /// <param name="alreadyLoadedNativeData">
  515. /// The first instance that has already loaded the required content data
  516. /// of this content class or just 'null' if there is none loaded yet (or
  517. /// anymore).
  518. /// </param>
  519. protected override void Load(Content alreadyLoadedNativeData)
  520. {
  521. // Make sure FontMapNames is not null, else we might crash later.
  522. FontMapNames = new string[0];
  523. // If this entry has no RelativeFilePath, it should have children.
  524. if (String.IsNullOrEmpty(RelativeFilePath))
  525. {
  526. // Just load all the children entries (only up to 4, ignore the rest)
  527. List<FontData> childFonts = new List<FontData>();
  528. foreach (ContentMetaData child in data.Children)
  529. {
  530. // Grab all Font children content entries
  531. if (child.Type == ContentType.Font)
  532. {
  533. childFonts.Add(Get(child.Name));
  534. }
  535. }
  536. // If we have none, there is something wrong with the content!
  537. if (childFonts.Count == 0)
  538. {
  539. // Only warn if the application is still running and was not aborted.
  540. if (Application.IsShuttingDown == false)
  541. {
  542. Log.Warning(
  543. "Unable to load font data, because the font content '" +
  544. Name + "' has no file data and also no font children!", false);
  545. }
  546. return;
  547. }
  548. // Warnings if we have too many fonts are in the Font constructor!
  549. ResolutionFonts = childFonts.ToArray();
  550. // We don't need to load anything more
  551. return;
  552. } // if
  553. // First load all the xml font data.
  554. base.Load(alreadyLoadedNativeData);
  555. // If that failed, we can't do anything, font rendering won't work.
  556. if (RootNode == null ||
  557. String.IsNullOrEmpty(RootNode.Name))
  558. {
  559. Log.Warning(
  560. "Unable to load font data '" + Name + "', because the content " +
  561. "couldn't be found and it seems that there is no working fallback " +
  562. "either. Tried to load file: " + RelativeFilePath, false);
  563. return;
  564. } // if
  565. // Okay, do all the loading now!
  566. try
  567. {
  568. #region Validation
  569. // First find out if this is a font at all
  570. if (RootNode.Name != "Font")
  571. {
  572. // Note: Exceptions are handled below. This will not crash caller.
  573. Log.Warning(
  574. "This content file '" + RootNode.FilePath + "' is not a font, " +
  575. "unable to load the font data! (RootNode=" + RootNode.Name + ")",
  576. false);
  577. return;
  578. } // if
  579. #endregion
  580. // Grab the version to determinate what data we can expect.
  581. Version version = new Version(0, 9, 0);
  582. string versionText = RootNode.GetAttribute("Version");
  583. if (String.IsNullOrEmpty(versionText) == false)
  584. {
  585. Version.TryParse(versionText, out version);
  586. }
  587. string bitmapNameAttribute = "Name";
  588. string characterAttribute = "Character";
  589. string bitmapIndexAttribute = "BitmapIndex";
  590. string uvAttribute = "UV";
  591. string advanceWidthAttribute = "AdvanceWidth";
  592. string leftBearingAttribute = "LeftBearing";
  593. string rightBearingAttribute = "RightBearing";
  594. string firstAttribute = "First";
  595. string secondAttribute = "Second";
  596. string distanceAttribute = "Distance";
  597. // Still support fonts from v0.9.0, which used the version 0.1. Skip
  598. // all the validation checks for v0.1
  599. if (version > new Version(0, 1))
  600. {
  601. // Load some global data for this font and validate in debug mode
  602. LeftOffset = RootNode.GetAttributeAs("LeftOffset", 0);
  603. TopOffset = RootNode.GetAttributeAs("TopOffset", 0);
  604. }
  605. else
  606. {
  607. // The old version had slightly different names for some attributes
  608. bitmapNameAttribute = "Filename";
  609. characterAttribute = "char";
  610. uvAttribute = "uv";
  611. advanceWidthAttribute = "advanceWidth";
  612. leftBearingAttribute = "leftBearing";
  613. rightBearingAttribute = "rightBearing";
  614. bitmapIndexAttribute = "bitmap";
  615. firstAttribute = "first";
  616. secondAttribute = "second";
  617. distanceAttribute = "distance";
  618. }
  619. PixelLineHeight = RootNode.GetAttributeAs("LineHeight", SizeInPoints);
  620. // and make sure we have no other data loaded yet
  621. glyphDictionary = new Dictionary<char, Glyph>();
  622. kernDictionary = new Dictionary<char, Dictionary<char, int>>();
  623. List<string> fontMapNames = new List<string>();
  624. // Little helper to remember bitmap sizes, which are also known
  625. // by the material once it is loaded, but to keep delayed loading
  626. // we do not need to load it here right away (only when it is used
  627. // for the first time and we actually need the material for drawing).
  628. List<Size> fontMapSizes = new List<Size>();
  629. // Helper to make xml attribute warnings more descriptive.
  630. string fontWarningText = "Font " + Name;
  631. // Get the used bitmaps, glyphs and kernings, thats all we need!
  632. foreach (XmlNode childNode in RootNode.Children)
  633. {
  634. switch (childNode.Name)
  635. {
  636. #region 1. step - load all fontmaps
  637. case "Bitmap":
  638. // Load the font map data, currently we only support a single one
  639. string fontMapName = childNode.GetAttribute(bitmapNameAttribute);
  640. int fontMapWidth = childNode.GetAttributeAs("Width", 1);
  641. int fontMapHeight = childNode.GetAttributeAs("Height", 1);
  642. fontMapNames.Add(fontMapName);
  643. fontMapSizes.Add(new Size(fontMapWidth, fontMapHeight));
  644. break;
  645. #endregion
  646. #region 2. step - load all glyphs
  647. case "Glyphs":
  648. // Load all glyphs
  649. foreach (XmlNode glyphNode in childNode.Children)
  650. {
  651. #region Validation
  652. // We cannot load glyphs without having font maps!
  653. if (fontMapNames.Count == 0)
  654. {
  655. throw new NotSupportedException(
  656. "We cannot add and calculate font glyphs with no bitmap " +
  657. "fonts defined. The font xml data is not complete!");
  658. } // if
  659. #endregion
  660. char character =
  661. glyphNode.GetAttributeAs(characterAttribute, ' ');
  662. Glyph glyph = new Glyph
  663. {
  664. FontMapId =
  665. glyphNode.GetAttributeAs(bitmapIndexAttribute, 0),
  666. UV = Rectangle.FromColladaString(
  667. glyphNode.GetAttribute(uvAttribute)),
  668. LeftSideBearing = (int)Math.Round(glyphNode.GetAttributeAs(
  669. leftBearingAttribute, 0.0f, fontWarningText)),
  670. RightSideBearing = (int)Math.Round(glyphNode.GetAttributeAs(
  671. rightBearingAttribute, 0.0f, fontWarningText)),
  672. Kernings = null,
  673. };
  674. if (glyph.FontMapId >=
  675. fontMapNames.Count)
  676. {
  677. Log.Warning(
  678. "Unable to use FontMapNumber '" + glyph.FontMapId +
  679. "' for this glyph because we only have '" +
  680. fontMapNames.Count + "' font maps. The FontMapNumber " +
  681. "will resseted now to '" + (fontMapNames.Count - 1) + "'");
  682. glyph.FontMapId = fontMapNames.Count - 1;
  683. } // if
  684. // Default fallback is width minus some small offset,
  685. // this will never be used except advanceWidth is missing
  686. glyph.AdvanceWidth =
  687. glyphNode.GetAttributeAs(advanceWidthAttribute,
  688. glyph.UV.Width - 2.0f, fontWarningText);
  689. // Calculate the FontMapUV with help of font map sizes
  690. // remembered above. Error checking is done above already.
  691. Size fontMapSize = fontMapSizes[glyph.FontMapId];
  692. // Build the UV's with the usual halfpixel offset, etc.
  693. glyph.FontMapUV = Rectangle.BuildUVRectangle(glyph.UV,
  694. fontMapSize);
  695. // And finally add it to our glyph dictionary
  696. glyphDictionary.Add(character, glyph);
  697. } // foreach
  698. break;
  699. #endregion
  700. #region 3. step - load all kernings
  701. case "Kernings":
  702. // Load all kernings
  703. foreach (XmlNode kernNode in childNode.Children)
  704. {
  705. char firstChar = kernNode.GetAttributeAs(firstAttribute, ' ',
  706. fontWarningText);
  707. char secondChar = kernNode.GetAttributeAs(secondAttribute, ' ',
  708. fontWarningText);
  709. int kerningDistance = kernNode.GetAttributeAs(distanceAttribute,
  710. 0, fontWarningText);
  711. // Try to find the character for that a kerning with an other
  712. // exists
  713. Glyph glyph;
  714. if (glyphDictionary.TryGetValue(firstChar, out glyph))
  715. {
  716. // If it exists (what should be usually the case)
  717. Dictionary<char, int> glyphKernings;
  718. // look if we have already added some kernings
  719. if (glyph.Kernings != null)
  720. {
  721. glyphKernings = glyph.Kernings;
  722. } // if
  723. // or do we add the first one
  724. else
  725. {
  726. // in that case initialize the dictionary
  727. glyphKernings = new Dictionary<char, int>();
  728. // and link it to the glyph
  729. glyph.Kernings = glyphKernings;
  730. } // else
  731. // finally still add the new distance value
  732. glyphKernings.Add(secondChar, kerningDistance);
  733. } // if
  734. else
  735. {
  736. Log.Warning(
  737. "The font content data contains a kerning for the " +
  738. "character '" + firstChar + "' to the character '" +
  739. secondChar + "' but there is no glyph information for '" +
  740. firstChar + "'.", false);
  741. } // else
  742. } // foreach
  743. break;
  744. #endregion
  745. } // switch
  746. } // foreach
  747. FontMapNames = fontMapNames.ToArray();
  748. } // try
  749. catch (Exception ex)
  750. {
  751. Log.Warning("Failed to load font data for '" + Name + "': " + ex);
  752. // Loading failed (for Font to know), report new error for every font
  753. FailedToLoad = true;
  754. } // catch
  755. }
  756. #endregion
  757. #region ParseText
  758. /// <summary>
  759. /// Parses the given text by analyzing every character to decide if the
  760. /// current is supported or not. Every character that is not drawable will
  761. /// be replaced by the "fallback character" (or skipped if the font doesn't
  762. /// support even that). Addtionally by checking the characters also (every
  763. /// tyoe of) line breaks will be detected and teh text splitted by them.
  764. /// </summary>
  765. /// <param name="text">Text</param>
  766. /// <remarks>
  767. /// This method has no validation checks because every caller makes already
  768. /// sure that the text is valid.
  769. /// </remarks>
  770. internal List<List<char>> ParseText(string text)
  771. {
  772. // Our text lines we will return after parsing
  773. List<List<char>> finalTextlines = new List<List<char>>();
  774. // The parsed characters of the current text line
  775. List<char> textline = new List<char>();
  776. char[] textChars = text.ToCharArray();
  777. for (int charIndex = 0; charIndex < textChars.Length; charIndex++)
  778. {
  779. // Grab the current character we have to check now
  780. char textChar = textChars[charIndex];
  781. // Also compute the index of the next char which is used to "detect"
  782. // a Windows line break and when the text has ended
  783. int nextCharIndex = charIndex + 1;
  784. // and indicate if we have find a line break
  785. bool isLineBreak = false;
  786. #region Newlines
  787. // At first check for new-lines, possible values are:
  788. // - Windows = \r\n
  789. // - Unix = \n
  790. // - Macintosh = \r
  791. // Unix
  792. if (textChar == '\n')
  793. {
  794. isLineBreak = true;
  795. } // if
  796. // Macintosh or the first part of the Windows new-line
  797. else if (textChar == '\r')
  798. {
  799. // In both cases it's we need to go to the next line
  800. isLineBreak = true;
  801. // but also need to check for the second part of the Windows new-line
  802. if (nextCharIndex < textChars.Length &&
  803. textChars[nextCharIndex] == '\n')
  804. {
  805. // that we can ignore for the next loop-iteration
  806. charIndex++;
  807. } // if
  808. } // else if
  809. #endregion
  810. #region Tabs
  811. // Just convert a tab into 2 spaces
  812. else if (textChar == '\t')
  813. {
  814. for (int num = 0; num < 2; num++)
  815. {
  816. textline.Add(' ');
  817. } // for
  818. } // else if
  819. #endregion
  820. #region All normal characters (including space)
  821. // Ignore all other special characters (EOT, BackSpace, etc.)
  822. // and the newline that we maybe have handled above already
  823. else if (textChar >= ' ')
  824. {
  825. // Check now if the current text character is supported by the font
  826. // so we can allow it
  827. if (glyphDictionary.ContainsKey(textChar))
  828. {
  829. textline.Add(textChar);
  830. } // if
  831. // in the case it isn't supported then log a warning and use the
  832. // "fallback" character to indicate the character in the text which
  833. // is wrong
  834. else if (glyphDictionary.ContainsKey(FallbackChar))
  835. {
  836. Log.Warning("Sorry the current font '" + this + "' doesn't " +
  837. "support the character '" + textChar + "', so will use the '" +
  838. FallbackChar + "' as fallback character instead.");
  839. textline.Add(FallbackChar);
  840. } // else if
  841. // if even the "fallback" character isn't supported by the font then
  842. // log too but we have to skip this character then
  843. else
  844. {
  845. Log.Warning("Oops, can't even use a '?' in the text to indicate" +
  846. " that the character '" + textChar + "' is not available in" +
  847. " the font '" + this + "', the character will be skipped now.");
  848. continue;
  849. } // else
  850. } // else if
  851. #endregion
  852. // If we have detected a line break now
  853. if (isLineBreak ||
  854. // or have reached the last character of the text
  855. nextCharIndex == textChars.Length)
  856. {
  857. // then the parsing of the current text line is complete and we can
  858. // go to the next one
  859. finalTextlines.Add(textline);
  860. textline = new List<char>();
  861. } // if
  862. } // for
  863. return finalTextlines;
  864. }
  865. #endregion
  866. #region GetTextLines
  867. /// <summary>
  868. /// Gets the single text lines of multi line text by determining the line
  869. /// breaks. Each of the detected text line will be represented as a list of
  870. /// chars whereby only "known" characters will be listed here or at least
  871. /// represented by a question mark. All other characters will skipped and
  872. /// logged out.
  873. /// </summary>
  874. /// <remarks>
  875. /// This method will also handle clipping and word-wrapping.
  876. /// </remarks>
  877. /// <param name="text">Text</param>
  878. /// <param name="lineSpacing">Line spacing</param>
  879. /// <param name="maxTextSize">Maximum text size</param>
  880. /// <param name="textAlignment">Text alignment</param>
  881. /// <param name="isClippingOn">Is clipping on</param>
  882. /// <param name="isWordWrapOn">Is word wrap on</param>
  883. /// <param name="textlineWidths">Textline widths</param>
  884. /// <param name="maxTextlineWidth">Maximum textline width</param>
  885. /// <returns>
  886. /// The list of final text lines where the words contains only allowed
  887. /// characters.
  888. /// </returns>
  889. internal List<List<char>> GetTextLines(string text, float lineSpacing,
  890. Size maxTextSize, HorizontalAlignment textAlignment, bool isClippingOn,
  891. bool isWordWrapOn,
  892. out List<float> textlineWidths, out float maxTextlineWidth)
  893. {
  894. // At first initialize the 'out' values
  895. textlineWidths = new List<float>();
  896. maxTextlineWidth = 0.0f;
  897. // Next the list which contains the final text lines with characters for
  898. // each.
  899. List<List<char>> finalTextlines = new List<List<char>>();
  900. // Without a valid text we immediately stop here
  901. if (String.IsNullOrEmpty(text))
  902. {
  903. return finalTextlines;
  904. } // if
  905. // Compute now the total height which the font would need for every text
  906. // line
  907. float maxLineHeight = PixelLineHeight * lineSpacing;
  908. // In the case text clipping is enabled we check now if the available the
  909. // height of given text area is smaller than a character of the font
  910. // because then no text line will fit into it and we can also stop here
  911. // directly
  912. if (isClippingOn &&
  913. maxTextSize.Height < maxLineHeight)
  914. {
  915. return finalTextlines;
  916. } // if
  917. // Parse the given text now and get the text lines of it with all
  918. // supported characters
  919. List<List<char>> textlines = ParseText(text);
  920. // and iterate them all now
  921. for (int lineIndex = 0; lineIndex < textlines.Count; lineIndex++)
  922. {
  923. List<char> textlineChars = textlines[lineIndex];
  924. // Initialize here the list that will contain all characters of the
  925. // current text line
  926. List<char> textline = new List<char>();
  927. // and width (in pixels) of it which needs to start at 0
  928. float textlineWidth = 0.0f;
  929. // For the word-wrap feature we also need a container which collects
  930. // the characters of the current word we are building
  931. List<char> word = new List<char>();
  932. // and its width (in pixels)
  933. float wordWidth = 0.0f;
  934. // Last we still need the number of words we have already parsed to
  935. // "detect" if a single word exceeds the max. allowed text width and we
  936. // need to put it into the next text line separately
  937. int wordNumber = 0;
  938. for (int charId = 0; charId < textlineChars.Count; charId++)
  939. {
  940. // Get the current char of the current text line
  941. char textChar = textlineChars[charId];
  942. // and also figure out which is the next character which we need for
  943. // kerning, bearing and word- or line-ending detection
  944. int nextCharId = charId + 1;
  945. char nextChar = (nextCharId < textlineChars.Count)
  946. ? textlineChars[nextCharId]
  947. : NoChar;
  948. // Get now the width of the character which it will need to draw it
  949. // Note:
  950. // There is no need to check if a glyph exists because the method
  951. // "ParseText()" makes already sure that our text lines here contain
  952. // only supported characters (unsupported are converted in '?' or
  953. // skipped completely)
  954. Glyph glyph = glyphDictionary[textChar];
  955. int glyphWidth = glyph.GetDrawWidth(nextChar, Tracking);
  956. // and look if it's the first one of the current text line
  957. bool isFirstChar = charId == 0;
  958. // or the last one
  959. bool isLastChar = nextCharId == textlineChars.Count;
  960. // Unlike GetGlyphDrawInfos above we do not care about positioning,
  961. // just the width needed for this line (bearing reduces width).
  962. // But only take care about the bearing if we have here still a
  963. // character at the right of us
  964. if (nextChar != NoChar)
  965. {
  966. if (isFirstChar &&
  967. textAlignment == HorizontalAlignment.Left)
  968. {
  969. glyphWidth -= MathHelper.Round(
  970. glyphDictionary[nextChar].LeftSideBearing);
  971. } // if
  972. if (isLastChar &&
  973. textAlignment == HorizontalAlignment.Right)
  974. {
  975. glyphWidth += MathHelper.Round(
  976. glyphDictionary[nextChar].RightSideBearing);
  977. } // if
  978. } // if
  979. #region Word-Wrap based clipping
  980. // The word-wrap feature makes only sense with clipping otherwise
  981. // there would be no need to wrap text lines
  982. if (isClippingOn &&
  983. isWordWrapOn)
  984. {
  985. bool isSpace = textChar == ' ';
  986. // "Flag" to know when a word ist fully parsed
  987. bool isWordFinished = false;
  988. #region Word separating by a white space
  989. if (isSpace)
  990. {
  991. // Currently add the space character always at the end of the
  992. // word independend by the set text alignment
  993. // Decide if and where to add the space depending on the current
  994. // set text alignment
  995. word.Add(textChar);
  996. // We will add the width of the space later after checking if the
  997. // word has still fit in the currently available text line space
  998. // because it's only a whitespace and doesn't belongs to the word
  999. //wordLength += glyphWidth;
  1000. isWordFinished = true;
  1001. } // if
  1002. #endregion
  1003. #region Last word of the current line has ended
  1004. else if (isLastChar)
  1005. {
  1006. word.Add(textChar);
  1007. wordWidth += glyphWidth;
  1008. isWordFinished = true;
  1009. } // else if
  1010. #endregion
  1011. #region Building the current word
  1012. else
  1013. {
  1014. word.Add(textChar);
  1015. wordWidth += glyphWidth;
  1016. } // else
  1017. #endregion
  1018. // If a word is fully parsed now
  1019. if (isWordFinished)
  1020. {
  1021. // then check if the current text line has still enough space to
  1022. // add it
  1023. if (textlineWidth + wordWidth <= maxTextSize.Width ||
  1024. // and ignore clipping if even the first word of the current
  1025. // text line wouldn't fit into it (to avoid that the word would
  1026. // "push" the rest of the text down and out of the text area
  1027. wordNumber == 0)
  1028. {
  1029. // Update the parsed result of the current text line now
  1030. textline.AddRange(word);
  1031. textlineWidth += wordWidth;
  1032. // and also add the width of the space now that we have skipped
  1033. // above
  1034. if (isSpace)
  1035. {
  1036. textlineWidth += glyphWidth;
  1037. } // if
  1038. wordNumber++;
  1039. } // if
  1040. // In the case the next word would not fit anymore
  1041. else
  1042. {
  1043. // we also grab the remaining text line part
  1044. while (nextCharId < textlineChars.Count)
  1045. {
  1046. word.Add(textlineChars[nextCharId]);
  1047. nextCharId++;
  1048. } // while
  1049. // and insert it as next text line (-> wrapping)
  1050. textlines.Insert(lineIndex + 1, word);
  1051. // Last indicate to the character loop of the current text line
  1052. // that we have "jumped" to the end
  1053. charId = textlineChars.Count;
  1054. } // else
  1055. // before setting the initial values again for the word building
  1056. // so that parsing of the next text line can be started now
  1057. word = new List<char>();
  1058. wordWidth = 0.0f;
  1059. } // if
  1060. } // if
  1061. #endregion
  1062. #region Character based clipping and non-clipping
  1063. // If no clipping is required
  1064. else if (isClippingOn == false ||
  1065. // or in the case that (horizontal) clipping is enabled and we
  1066. // check if the current character would still fit in the remaining
  1067. // space of the text line
  1068. // (-> vertical clipping will be handled more below)
  1069. (textlineWidth + glyphWidth) <= maxTextSize.Width)
  1070. {
  1071. // Add the character to the current text line characters
  1072. textline.Add(textChar);
  1073. // increase the width of current text line by it
  1074. textlineWidth += glyphWidth;
  1075. } // else if
  1076. // if the available space limit of the current text line is reached
  1077. else
  1078. {
  1079. // we can abort the parsing of this line and go to the next one
  1080. break;
  1081. } // else
  1082. #endregion
  1083. } // for
  1084. // Every time a text line is parsed add it as final one
  1085. finalTextlines.Add(textline);
  1086. // inclusive the measured width of it
  1087. textlineWidths.Add(textlineWidth);
  1088. // and additionally check if the current text line was the longest
  1089. // that we had so far
  1090. if (maxTextlineWidth < textlineWidth)
  1091. {
  1092. maxTextlineWidth = textlineWidth;
  1093. } // if
  1094. #region Vertical text clipping check
  1095. // Now check if there is still enough space for the next text line if
  1096. // (vertical) clipping is wished
  1097. if (isClippingOn)
  1098. {
  1099. float requiredHeightForNextLine =
  1100. (finalTextlines.Count + 1) * maxLineHeight;
  1101. // If no more text line would fit
  1102. if (requiredHeightForNextLine > maxTextSize.Height)
  1103. {
  1104. // we can stop here completely and are done now with parsing of
  1105. // the text lines
  1106. break;
  1107. } // if
  1108. // Note:
  1109. // Horizontal clipping was already handled above
  1110. } // if
  1111. #endregion
  1112. // start the text line parsing at the beginning (again)
  1113. textline = new List<char>();
  1114. textlineWidth = 0.0f;
  1115. } // for
  1116. return finalTextlines;
  1117. }
  1118. #endregion
  1119. #endregion
  1120. /// <summary>
  1121. /// Tests
  1122. /// </summary>
  1123. internal class FontDataTests
  1124. {
  1125. #region Helpers
  1126. /// <summary>
  1127. /// Get font data which is used for all tests where the tests values are
  1128. /// tweaked for.
  1129. /// </summary>
  1130. private static FontData GetTestFont()
  1131. {
  1132. // We use the Verdana 12 pt font
  1133. return FontData.Get("Verdana", 12, FontStyle.AddOutline).
  1134. ResolutionFonts[2];
  1135. }
  1136. #endregion
  1137. #region LoadDefaultFontData (LongRunning)
  1138. /// <summary>
  1139. /// Load default font data
  1140. /// </summary>
  1141. [Test, Category("LongRunning")]
  1142. public void LoadDefaultFontData()
  1143. {
  1144. FontData fontData = GetTestFont();
  1145. Assert.NotNull(fontData.data);
  1146. Assert.Equal(fontData.glyphDictionary.Count, 95);
  1147. Assert.Equal(fontData.FontMapNames.Length, 1);
  1148. Assert.Equal(fontData.FontMapNames[0], "Verdana_12_16");
  1149. }
  1150. #endregion
  1151. #region ParseTextLines (LongRunning)
  1152. /// <summary>
  1153. /// Parse text lines
  1154. /// </summary>
  1155. [Test, Category("LongRunning")]
  1156. public void ParseTextLines()
  1157. {
  1158. FontData fontData = GetTestFont();
  1159. List<List<char>> lines = fontData.ParseText(
  1160. "long loong looong loooong text");
  1161. Assert.NotNull(lines);
  1162. Assert.Equal(lines.Count, 1);
  1163. Assert.Equal(lines[0].ToText(), "long loong looong loooong text");
  1164. lines = fontData.ParseText(
  1165. "long loong looong loooong text" + Environment.NewLine +
  1166. "Newline");
  1167. Assert.NotNull(lines);
  1168. Assert.Equal(lines.Count, 2);
  1169. Assert.Equal(lines[0].ToText(), "long loong looong loooong text");
  1170. Assert.Equal(lines[1].ToText(), "Newline");
  1171. }
  1172. #endregion
  1173. #region GetTextLines (LongRunning)
  1174. /// <summary>
  1175. /// Get text lines
  1176. /// </summary>
  1177. [Test, Category("LongRunning")]
  1178. public void GetTextLines()
  1179. {
  1180. string multilineText =
  1181. "This" + "\n" +
  1182. "is a" + "\r\n" +
  1183. "multiline" + "\r" +
  1184. "text.";
  1185. FontData fontData = GetTestFont();
  1186. List<float> textlineWidths;
  1187. float maxTextlineWidth;
  1188. List<List<char>> textlines = fontData.GetTextLines(multilineText,
  1189. 1.0f, Size.Zero, HorizontalAlignment.Left, false, false,
  1190. out textlineWidths, out maxTextlineWidth);
  1191. Assert.Equal(textlineWidths.Count, 4);
  1192. Assert.Equal(textlines[0].ToText(), "This");
  1193. Assert.Equal(textlines[1].ToText(), "is a");
  1194. Assert.Equal(textlines[2].ToText(), "multiline");
  1195. Assert.Equal(textlines[3].ToText(), "text.");
  1196. Assert.Equal(textlines.Count, textlineWidths.Count);
  1197. Dictionary<char, Glyph> glyphs = fontData.glyphDictionary;
  1198. // (8-0) + 8 + 3 + 6 = 25
  1199. //Assert.Equal(textlineWidths[0],
  1200. Assert.Equal(textlineWidths[0] + 1,
  1201. MathHelper.Round(glyphs['T'].AdvanceWidth -
  1202. glyphs['T'].LeftSideBearing) +
  1203. MathHelper.Round(glyphs['h'].AdvanceWidth) +
  1204. MathHelper.Round(glyphs['i'].AdvanceWidth) +
  1205. MathHelper.Round(glyphs['s'].AdvanceWidth));
  1206. Assert.Equal(textlineWidths[0], 24);
  1207. // (3-1) + 6 + 4 + 7 = 19
  1208. Assert.Equal(textlineWidths[1],
  1209. MathHelper.Round(glyphs['i'].AdvanceWidth -
  1210. glyphs['i'].LeftSideBearing) +
  1211. MathHelper.Round(glyphs['s'].AdvanceWidth) +
  1212. MathHelper.Round(glyphs[' '].AdvanceWidth) +
  1213. MathHelper.Round(glyphs['a'].AdvanceWidth));
  1214. Assert.Equal(textlineWidths[1], 19);
  1215. // (12-1) + 8 + 3 + 5 + 3 + 3 + 3 + 8 + 7 = 51
  1216. Assert.Equal(textlineWidths[2],
  1217. MathHelper.Round(glyphs['m'].AdvanceWidth -
  1218. glyphs['m'].LeftSideBearing) +
  1219. MathHelper.Round(glyphs['u'].AdvanceWidth) +
  1220. MathHelper.Round(glyphs['l'].AdvanceWidth) +
  1221. MathHelper.Round(glyphs['t'].AdvanceWidth) +
  1222. MathHelper.Round(glyphs['i'].AdvanceWidth) +
  1223. MathHelper.Round(glyphs['l'].AdvanceWidth) +
  1224. MathHelper.Round(glyphs['i'].AdvanceWidth) +
  1225. MathHelper.Round(glyphs['n'].AdvanceWidth) +
  1226. MathHelper.Round(glyphs['e'].AdvanceWidth));
  1227. Assert.Equal(textlineWidths[2], 51);
  1228. // (4 - 0) + 7 + 7 + 5 + 4 = 27
  1229. Assert.Equal(textlineWidths[3],
  1230. MathHelper.Round(glyphs['t'].AdvanceWidth -
  1231. glyphs['t'].LeftSideBearing) +
  1232. MathHelper.Round(glyphs['e'].AdvanceWidth) +
  1233. MathHelper.Round(glyphs['x'].AdvanceWidth) +
  1234. MathHelper.Round(glyphs['t'].AdvanceWidth) +
  1235. MathHelper.Round(glyphs['.'].AdvanceWidth));
  1236. Assert.Equal(textlineWidths[3], 27);
  1237. Assert.Equal(maxTextlineWidth, 51);
  1238. }
  1239. #endregion
  1240. #region GetTextlineWrapped (LongRunning)
  1241. /// <summary>
  1242. /// Get text line wrapped
  1243. /// </summary>
  1244. /// Caution: For this unit test are the following modules required:
  1245. /// - Graphic
  1246. /// - Platforms.IWindow
  1247. /// - ContentManager.Client
  1248. [Test, Category("LongRunning")]
  1249. public void GetTextlineWrapped()
  1250. {
  1251. string text =
  1252. "long loong looong loooong text" + Environment.NewLine + "Newline";
  1253. FontData fontData = GetTestFont();
  1254. Size textAreaSize = new Size(138, fontData.PixelLineHeight * 3);
  1255. List<float> textlineWidths;
  1256. float maxTextlineWidth;
  1257. List<List<char>> lines = fontData.GetTextLines(text, 1.0f,
  1258. textAreaSize, HorizontalAlignment.Left, true, true,
  1259. out textlineWidths, out maxTextlineWidth);
  1260. Assert.NotNull(lines);
  1261. Assert.Equal(lines.Count, 3);
  1262. Assert.Equal(lines[0].ToText(), "long loong looong ");
  1263. Assert.Equal(lines[1].ToText(), "loooong text");
  1264. Assert.Equal(lines[2].ToText(), "Newline");
  1265. }
  1266. #endregion
  1267. #region GetGlyphDrawInfos
  1268. /// <summary>
  1269. /// Get glyph draw infos
  1270. /// </summary>
  1271. [Test, Category("LongRunning")]
  1272. public void GetGlyphDrawInfos()
  1273. {
  1274. FontData testFont = FontData.Default;
  1275. GlyphDrawInfo[] drawGlyphs = testFont.GetGlyphDrawInfos("", 1.0f,
  1276. HorizontalAlignment.Left);
  1277. Assert.Equal(0, drawGlyphs.Length);
  1278. drawGlyphs = testFont.GetGlyphDrawInfos("\n", 1.0f,
  1279. HorizontalAlignment.Left);
  1280. Assert.Equal(drawGlyphs.Length, 1);
  1281. Rectangle newLineArea = drawGlyphs[0].DrawArea;
  1282. Assert.Equal(newLineArea.X, 0);
  1283. Assert.Equal(newLineArea.Y, testFont.PixelLineHeight);
  1284. Assert.Equal(newLineArea.Width, 0);
  1285. Assert.Equal(newLineArea.Height, testFont.PixelLineHeight);
  1286. drawGlyphs = testFont.GetGlyphDrawInfos("A", 1.0f,
  1287. HorizontalAlignment.Left);
  1288. Assert.Equal(drawGlyphs.Length, 1);
  1289. GlyphDrawInfo glyphA = drawGlyphs[0];
  1290. Assert.Equal(glyphA.UV, Rectangle.BuildUVRectangle(
  1291. new Rectangle(67, 32, 9, 16), new Size(128)));
  1292. Assert.Equal(glyphA.FontMapId, 0);
  1293. }
  1294. #endregion
  1295. }
  1296. }
  1297. }