Client LuaCsForBarotrauma
ScalableFont.cs
2 using Microsoft.Xna.Framework;
3 using Microsoft.Xna.Framework.Graphics;
4 using SharpFont;
5 using System;
6 using System.Collections.Generic;
7 using System.Collections.Immutable;
8 using System.Linq;
9 using System.Threading;
10 using System.Xml.Linq;
11 using Barotrauma.Threading;
12 
13 namespace Barotrauma
14 {
15  public class ScalableFont : IDisposable
16  {
17  private static readonly List<ScalableFont> FontList = new List<ScalableFont>();
18  private static Library Lib = null;
19  private static readonly object globalMutex = new object();
20 
21  private readonly ReaderWriterLockSlim rwl = new ReaderWriterLockSlim();
22 
23  private readonly string filename;
24  private readonly Face face;
25  private uint size;
26  private int baseHeight;
27  private readonly Dictionary<uint, GlyphData> texCoords;
28  private readonly List<Texture2D> textures;
29  private readonly GraphicsDevice graphicsDevice;
30 
31  private Vector2 currentDynamicAtlasCoords;
32  private int currentDynamicAtlasNextY;
33  uint[] currentDynamicPixelBuffer;
34 
35  public bool DynamicLoading
36  {
37  get;
38  private set;
39  }
40 
41  public TextManager.SpeciallyHandledCharCategory SpeciallyHandledCharCategory
42  {
43  get;
44  private set;
45  }
46 
47  public uint Size
48  {
49  get
50  {
51  return size;
52  }
53  set
54  {
55  size = value;
56  if (graphicsDevice != null) { RenderAtlas(graphicsDevice, charRanges, texDims, baseChar); }
57  }
58  }
59 
60  public bool ForceUpperCase = false;
61 
62  public float LineHeight => baseHeight * 1.8f;
63 
64  private uint[] charRanges;
65  private int texDims;
66  private uint baseChar;
67 
68  public readonly record struct GlyphData(
69  int TexIndex = default,
70  Vector2 DrawOffset = default,
71  float Advance = default,
72  Rectangle TexCoords = default);
73 
74  public static TextManager.SpeciallyHandledCharCategory ExtractShccFromXElement(XElement element)
75  => TextManager.SpeciallyHandledCharCategories
76  .Where(category => element.GetAttributeBool($"is{category}", category switch {
77  // CJK and Japanese aren't supported by default
78  TextManager.SpeciallyHandledCharCategory.CJK => false,
79  TextManager.SpeciallyHandledCharCategory.Japanese => false,
80 
81  // For backwards compatibility, we assume that Cyrillic is supported by default
82  TextManager.SpeciallyHandledCharCategory.Cyrillic => true,
83 
84  _ => throw new NotImplementedException($"nameof{category} not implemented.")
85  }))
86  .Aggregate(TextManager.SpeciallyHandledCharCategory.None, (current, category) => current | category);
87 
88  public ScalableFont(ContentXElement element, uint defaultSize = 14, GraphicsDevice gd = null)
89  : this(
90  element.GetAttributeContentPath("file")?.Value,
91  (uint)element.GetAttributeInt("size", (int)defaultSize),
92  gd,
93  element.GetAttributeBool("dynamicloading", false),
94  ExtractShccFromXElement(element))
95  {
96  }
97 
98  public ScalableFont(
99  string filename,
100  uint size,
101  GraphicsDevice gd = null,
102  bool dynamicLoading = false,
103  TextManager.SpeciallyHandledCharCategory speciallyHandledCharCategory = TextManager.SpeciallyHandledCharCategory.None)
104  {
105  lock (globalMutex)
106  {
107  Lib ??= new Library();
108  }
109 
110  this.filename = filename;
111  this.face = null;
112  using (new ReadLock(rwl))
113  {
114  foreach (ScalableFont font in FontList)
115  {
116  if (font.filename == filename)
117  {
118  this.face = font.face;
119  break;
120  }
121  }
122  }
123 
124  this.face ??= new Face(Lib, filename);
125  this.size = size;
126  this.textures = new List<Texture2D>();
127  this.texCoords = new Dictionary<uint, GlyphData>();
128  this.DynamicLoading = dynamicLoading;
129  this.SpeciallyHandledCharCategory = speciallyHandledCharCategory;
130  this.graphicsDevice = gd;
131 
132  if (gd != null && !dynamicLoading)
133  {
134  RenderAtlas(gd);
135  }
136 
137  lock (globalMutex)
138  {
139  FontList.Add(this);
140  }
141  }
142 
151  private void RenderAtlas(GraphicsDevice gd, uint[] charRanges = null, int texDims = 1024, uint baseChar = 0x54)
152  {
153  if (DynamicLoading) { return; }
154 
155  if (charRanges == null)
156  {
157  charRanges = new uint[] { 0x20, 0xFFFF };
158  }
159  this.charRanges = charRanges;
160  this.texDims = texDims;
161  this.baseChar = baseChar;
162 
163  textures.ForEach(t => t.Dispose());
164  textures.Clear();
165  texCoords.Clear();
166 
167  uint[] pixelBuffer = new uint[texDims * texDims];
168  for (int i = 0; i < texDims * texDims; i++)
169  {
170  pixelBuffer[i] = 0;
171  }
172 
173  CrossThread.RequestExecutionOnMainThread(() =>
174  {
175  textures.Add(new Texture2D(gd, texDims, texDims, false, SurfaceFormat.Color));
176  });
177  int texIndex = 0;
178 
179  Vector2 currentCoords = Vector2.Zero;
180  int nextY = 0;
181 
182  using (new WriteLock(rwl))
183  {
184  face.SetPixelSizes(0, size);
185  face.LoadGlyph(face.GetCharIndex(baseChar), LoadFlags.Default, LoadTarget.Normal);
186  baseHeight = face.Glyph.Metrics.Height.ToInt32();
187 
188  for (int i = 0; i < charRanges.Length; i += 2)
189  {
190  uint start = charRanges[i];
191  uint end = charRanges[i + 1];
192  for (uint j = start; j <= end; j++)
193  {
194  uint glyphIndex = face.GetCharIndex(j);
195  if (glyphIndex == 0)
196  {
197  texCoords.Add(j, new GlyphData(
198  Advance: 0,
199  TexIndex: -1));
200  continue;
201  }
202  face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal);
203  if (face.Glyph.Metrics.Width == 0 || face.Glyph.Metrics.Height == 0)
204  {
205  //glyph is empty, but char might still apply advance
206  GlyphData blankData = new GlyphData(
207  Advance: Math.Max((float)face.Glyph.Metrics.HorizontalAdvance, 0f),
208  TexIndex: -1); //indicates no texture because the glyph is empty
209 
210  texCoords.Add(j, blankData);
211  continue;
212  }
213  //stacktrace doesn't really work that well when RenderGlyph throws an exception
214  face.Glyph.RenderGlyph(RenderMode.Normal);
215  byte[] bitmap = face.Glyph.Bitmap.BufferData;
216  int glyphWidth = face.Glyph.Bitmap.Width;
217  int glyphHeight = bitmap.Length / glyphWidth;
218 
219  //if (glyphHeight>lineHeight) lineHeight=glyphHeight;
220 
221  if (glyphWidth > texDims - 1 || glyphHeight > texDims - 1)
222  {
223  throw new Exception(filename + ", " + size.ToString() + ", " + (char)j + "; Glyph dimensions exceed texture atlas dimensions");
224  }
225 
226  nextY = Math.Max(nextY, glyphHeight + 2);
227 
228  if (currentCoords.X + glyphWidth + 2 > texDims - 1)
229  {
230  currentCoords.X = 0;
231  currentCoords.Y += nextY;
232  nextY = 0;
233  }
234  if (currentCoords.Y + glyphHeight + 2 > texDims - 1)
235  {
236  currentCoords.X = 0;
237  currentCoords.Y = 0;
238  CrossThread.RequestExecutionOnMainThread(() =>
239  {
240  textures[texIndex].SetData<uint>(pixelBuffer);
241  textures.Add(new Texture2D(gd, texDims, texDims, false, SurfaceFormat.Color));
242  });
243  texIndex++;
244  for (int k = 0; k < texDims * texDims; k++)
245  {
246  pixelBuffer[k] = 0;
247  }
248  }
249 
250  GlyphData newData = new GlyphData(
251  Advance: (float)face.Glyph.Metrics.HorizontalAdvance,
252  TexIndex: texIndex,
253  TexCoords: new Rectangle((int)currentCoords.X, (int)currentCoords.Y, glyphWidth, glyphHeight),
254  DrawOffset: new Vector2(face.Glyph.BitmapLeft, baseHeight * 14 / 10 - face.Glyph.BitmapTop)
255  );
256  texCoords.Add(j, newData);
257 
258  for (int y = 0; y < glyphHeight; y++)
259  {
260  for (int x = 0; x < glyphWidth; x++)
261  {
262  byte byteColor = bitmap[x + y * glyphWidth];
263  pixelBuffer[((int)currentCoords.X + x) + ((int)currentCoords.Y + y) * texDims] = (uint)(byteColor << 24 | 0x00ffffff);
264  }
265  }
266 
267  currentCoords.X += glyphWidth + 2;
268  }
269  CrossThread.RequestExecutionOnMainThread(() =>
270  {
271  textures[texIndex].SetData<uint>(pixelBuffer);
272  });
273  }
274  }
275  }
276 
277  private void DynamicRenderAtlas(GraphicsDevice gd, uint character, int texDims = 1024, uint baseChar = 0x54)
278  {
279  bool missingCharacterFound = false;
280  using (new ReadLock(rwl))
281  {
282  missingCharacterFound = !texCoords.ContainsKey(character);
283  }
284  if (!missingCharacterFound) { return; }
285  DynamicRenderAtlas(gd, character.ToEnumerable(), texDims, baseChar);
286  }
287 
288  private void DynamicRenderAtlas(GraphicsDevice gd, string str, int texDims = 1024, uint baseChar = 0x54)
289  {
290  bool missingCharacterFound = false;
291  using (new ReadLock(rwl))
292  {
293  foreach (var character in str)
294  {
295  if (texCoords.ContainsKey(character)) { continue; }
296 
297  missingCharacterFound = true;
298  break;
299  }
300  }
301  if (!missingCharacterFound) { return; }
302  DynamicRenderAtlas(gd, str.Select(c => (uint)c), texDims, baseChar);
303  }
304 
305  private void DynamicRenderAtlas(GraphicsDevice gd, IEnumerable<uint> characters, int texDims = 1024, uint baseChar = 0x54)
306  {
307  if (System.Threading.Thread.CurrentThread != GameMain.MainThread)
308  {
309  CrossThread.RequestExecutionOnMainThread(() =>
310  {
311  DynamicRenderAtlas(gd, characters, texDims, baseChar);
312  });
313  return;
314  }
315 
316  byte[] bitmap;
317  int glyphWidth; int glyphHeight;
318  Fixed26Dot6 horizontalAdvance;
319  Vector2 drawOffset;
320 
321  using (new WriteLock(rwl))
322  {
323  if (textures.Count == 0)
324  {
325  this.texDims = texDims;
326  this.baseChar = baseChar;
327  face.SetPixelSizes(0, size);
328  face.LoadGlyph(face.GetCharIndex(baseChar), LoadFlags.Default, LoadTarget.Normal);
329  baseHeight = face.Glyph.Metrics.Height.ToInt32();
330  textures.Add(new Texture2D(gd, texDims, texDims, false, SurfaceFormat.Color));
331  }
332 
333  bool anyChanges = false;
334  bool firstChar = true;
335  foreach (var character in characters)
336  {
337  if (texCoords.ContainsKey(character)) { continue; }
338 
339  uint glyphIndex = face.GetCharIndex(character);
340  if (glyphIndex == 0)
341  {
342  texCoords.Add(character, new GlyphData(
343  Advance: 0,
344  TexIndex: -1));
345  continue;
346  }
347 
348  face.SetPixelSizes(0, size);
349  face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal);
350  if (face.Glyph.Metrics.Width == 0 || face.Glyph.Metrics.Height == 0)
351  {
352  //glyph is empty, but char might still apply advance
353  GlyphData blankData = new GlyphData(
354  Advance: Math.Max((float)face.Glyph.Metrics.HorizontalAdvance, 0f),
355  TexIndex: -1); //indicates no texture because the glyph is empty
356  texCoords.Add(character, blankData);
357  continue;
358  }
359 
360  //stacktrace doesn't really work that well when RenderGlyph throws an exception
361  face.Glyph.RenderGlyph(RenderMode.Normal);
362  bitmap = (byte[])face.Glyph.Bitmap.BufferData.Clone();
363  glyphWidth = face.Glyph.Bitmap.Width;
364  glyphHeight = bitmap.Length / glyphWidth;
365  horizontalAdvance = face.Glyph.Metrics.HorizontalAdvance;
366  drawOffset = new Vector2(face.Glyph.BitmapLeft, baseHeight * 14 / 10 - face.Glyph.BitmapTop);
367 
368  if (glyphWidth > texDims - 1 || glyphHeight > texDims - 1)
369  {
370  throw new Exception(filename + ", " + size.ToString() + ", " + (char)character + "; Glyph dimensions exceed texture atlas dimensions");
371  }
372 
373  currentDynamicAtlasNextY = Math.Max(currentDynamicAtlasNextY, glyphHeight + 2);
374  if (currentDynamicAtlasCoords.X + glyphWidth + 2 > texDims - 1)
375  {
376  currentDynamicAtlasCoords.X = 0;
377  currentDynamicAtlasCoords.Y += currentDynamicAtlasNextY;
378  currentDynamicAtlasNextY = 0;
379  }
380  //no more room in current texture atlas, create a new one
381  if (currentDynamicAtlasCoords.Y + glyphHeight + 2 > texDims - 1)
382  {
383  if (!firstChar) { textures[^1].SetData<uint>(currentDynamicPixelBuffer); }
384  currentDynamicAtlasCoords.X = 0;
385  currentDynamicAtlasCoords.Y = 0;
386  currentDynamicAtlasNextY = 0;
387  textures.Add(new Texture2D(gd, texDims, texDims, false, SurfaceFormat.Color));
388  currentDynamicPixelBuffer = null;
389  }
390 
391  GlyphData newData = new GlyphData(
392  Advance: (float)horizontalAdvance,
393  TexIndex: textures.Count - 1,
394  TexCoords: new Rectangle((int)currentDynamicAtlasCoords.X, (int)currentDynamicAtlasCoords.Y, glyphWidth, glyphHeight),
395  DrawOffset: drawOffset
396  );
397  texCoords.Add(character, newData);
398 
399  if (currentDynamicPixelBuffer == null)
400  {
401  currentDynamicPixelBuffer = new uint[texDims * texDims];
402  textures[newData.TexIndex].GetData<uint>(currentDynamicPixelBuffer, 0, texDims * texDims);
403  }
404 
405  for (int y = 0; y < glyphHeight; y++)
406  {
407  for (int x = 0; x < glyphWidth; x++)
408  {
409  byte byteColor = bitmap[x + y * glyphWidth];
410  currentDynamicPixelBuffer[((int)currentDynamicAtlasCoords.X + x) + ((int)currentDynamicAtlasCoords.Y + y) * texDims] = (uint)(byteColor << 24 | 0x00ffffff);
411  }
412  }
413 
414  currentDynamicAtlasCoords.X += glyphWidth + 2;
415  firstChar = false;
416  anyChanges = true;
417  }
418 
419  if (anyChanges) { textures[^1].SetData<uint>(currentDynamicPixelBuffer); }
420  }
421  }
422 
423  // TODO: refactor this further
424  private void HandleNewLineAndAlignment(
425  string text,
426  in Vector2 advanceUnit,
427  in Vector2 position,
428  in Vector2 scale,
429  Alignment alignment,
430  int i,
431  ref float lineWidth,
432  ref Vector2 currentLineOffset,
433  ref int lineNum,
434  ref Vector2 currentPos,
435  out uint charIndex,
436  out bool shouldContinue)
437  {
438  if ((alignment.HasFlag(Alignment.CenterX) || alignment.HasFlag(Alignment.Right)) && (lineWidth < 0.0f || text[i] == '\n'))
439  {
440  int startIndex = lineWidth < 0.0f ? i : (i + 1);
441  lineWidth = 0.0f;
442  for (int j = startIndex; j < text.Length; j++)
443  {
444  if (text[j] == '\n') { break; }
445  uint chrIndex = text[j];
446 
447  var gd2 = GetGlyphData(chrIndex);
448  lineWidth += gd2.Advance;
449  }
450  currentLineOffset = -lineWidth * advanceUnit * scale.X;
451  if (alignment.HasFlag(Alignment.CenterX)) { currentLineOffset *= 0.5f; }
452 
453  currentLineOffset.X = MathF.Round(currentLineOffset.X);
454  currentLineOffset.Y = MathF.Round(currentLineOffset.Y);
455  }
456  if (text[i] == '\n')
457  {
458  lineNum++;
459  currentPos = position;
460  currentPos.X -= LineHeight * lineNum * advanceUnit.Y * scale.Y;
461  currentPos.Y += LineHeight * lineNum * advanceUnit.X * scale.Y;
462  shouldContinue = true; charIndex = 0; return;
463  }
464 
465  shouldContinue = false;
466  charIndex = text[i];
467  }
468 
469  private GlyphData GetGlyphData(uint charIndex)
470  {
471  const uint DEFAULT_INDEX = 0x25A1; //U+25A1 = white square
472 
473  if (texCoords.TryGetValue(charIndex, out GlyphData gd) ||
474  texCoords.TryGetValue(DEFAULT_INDEX, out gd))
475  {
476  return gd;
477  }
478 
479  return new GlyphData(TexIndex: -1);
480  }
481 
482  public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit)
483  {
484  if (textures.Count == 0 && !DynamicLoading) { return; }
485  text = ApplyUpperCase(text, forceUpperCase);
486  if (DynamicLoading)
487  {
488  DynamicRenderAtlas(graphicsDevice, text);
489  }
490 
491  float lineWidth = -1.0f;
492  Vector2 currentLineOffset = Vector2.Zero;
493 
494  int lineNum = 0;
495  Vector2 currentPos = position;
496  Vector2 advanceUnit = rotation == 0.0f ? Vector2.UnitX : new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation));
497  for (int i = 0; i < text.Length; i++)
498  {
499  HandleNewLineAndAlignment(text, advanceUnit, position, scale, alignment, i,
500  ref lineWidth, ref currentLineOffset, ref lineNum, ref currentPos,
501  out uint charIndex, out bool shouldContinue);
502  if (shouldContinue) { continue; }
503 
504  GlyphData gd = GetGlyphData(charIndex);
505  if (gd.TexIndex >= 0)
506  {
507  if (gd.TexIndex < 0 || gd.TexIndex >= textures.Count)
508  {
509  throw new ArgumentOutOfRangeException($"Error while rendering text. Texture index was out of range. Text: {text}, char: {charIndex} index: {gd.TexIndex}, texture count: {textures.Count}");
510  }
511  Texture2D tex = textures[gd.TexIndex];
512  Vector2 drawOffset;
513  drawOffset.X = gd.DrawOffset.X * advanceUnit.X * scale.X - gd.DrawOffset.Y * advanceUnit.Y * scale.Y;
514  drawOffset.Y = gd.DrawOffset.X * advanceUnit.Y * scale.Y + gd.DrawOffset.Y * advanceUnit.X * scale.X;
515 
516  sb.Draw(tex, currentPos + currentLineOffset + drawOffset, gd.TexCoords, color, rotation, origin, scale, se, layerDepth);
517  }
518  currentPos += gd.Advance * advanceUnit * scale.X;
519  }
520  }
521 
522  public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit)
523  {
524  DrawString(sb, text, position, color, rotation, origin, new Vector2(scale), se, layerDepth, alignment, forceUpperCase);
525  }
526 
527  private string ApplyUpperCase(string text, ForceUpperCase forceUpperCase)
528  => forceUpperCase switch
529  {
530  Barotrauma.ForceUpperCase.Inherit => ForceUpperCase ? text.ToUpperInvariant() : text,
531  Barotrauma.ForceUpperCase.Yes => text.ToUpperInvariant(),
532  Barotrauma.ForceUpperCase.No => text
533  };
534 
535  private readonly static VertexPositionColorTexture[] quadVertices = new VertexPositionColorTexture[4];
536  public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit, bool italics = false)
537  {
538  if (textures.Count == 0 && !DynamicLoading) { return; }
539  text = ApplyUpperCase(text, forceUpperCase);
540  if (DynamicLoading)
541  {
542  DynamicRenderAtlas(graphicsDevice, text);
543  }
544 
545  Vector2 currentPos = position;
546  for (int i = 0; i < text.Length; i++)
547  {
548  if (text[i] == '\n')
549  {
550  currentPos.X = position.X;
551  currentPos.Y += LineHeight;
552  continue;
553  }
554 
555  uint charIndex = text[i];
556 
557  GlyphData gd = GetGlyphData(charIndex);
558  if (gd.TexIndex >= 0)
559  {
560  float halfCharHeight = gd.TexCoords.Height * 0.5f;
561  float slantStrength = 0.35f;
562  float topItalicOffset = italics ? ((halfCharHeight - gd.DrawOffset.Y) * slantStrength) + baseHeight * 0.18f : 0.0f;
563  float bottomItalicOffset = italics ? ((-halfCharHeight - gd.DrawOffset.Y) * slantStrength) + baseHeight * 0.18f : 0.0f;
564 
565  Texture2D tex = textures[gd.TexIndex];
566  quadVertices[0].Position = new Vector3(currentPos + gd.DrawOffset + (bottomItalicOffset, gd.TexCoords.Height), 0.0f);
567  quadVertices[0].TextureCoordinate = ((float)gd.TexCoords.Left / tex.Width, (float)gd.TexCoords.Bottom / tex.Height);
568  quadVertices[0].Color = color;
569 
570  quadVertices[1].Position = new Vector3(currentPos + gd.DrawOffset + (topItalicOffset, 0.0f), 0.0f);
571  quadVertices[1].TextureCoordinate = ((float)gd.TexCoords.Left / tex.Width, (float)gd.TexCoords.Top / tex.Height);
572  quadVertices[1].Color = color;
573 
574  quadVertices[2].Position = new Vector3(currentPos + gd.DrawOffset + (gd.TexCoords.Width + bottomItalicOffset, gd.TexCoords.Height), 0.0f);
575  quadVertices[2].TextureCoordinate = ((float)gd.TexCoords.Right / tex.Width, (float)gd.TexCoords.Bottom / tex.Height);
576  quadVertices[2].Color = color;
577 
578  quadVertices[3].Position = new Vector3(currentPos + gd.DrawOffset + (gd.TexCoords.Width + topItalicOffset, 0.0f), 0.0f);
579  quadVertices[3].TextureCoordinate = ((float)gd.TexCoords.Right / tex.Width, (float)gd.TexCoords.Top / tex.Height);
580  quadVertices[3].Color = color;
581 
582  sb.Draw(tex, quadVertices, 0.0f);
583  }
584  currentPos.X += gd.Advance;
585  }
586  }
587 
588  public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, in ImmutableArray<RichTextData>? richTextData, int rtdOffset = 0, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit)
589  {
590  DrawStringWithColors(sb, text, position, color, rotation, origin, new Vector2(scale), se, layerDepth, richTextData, rtdOffset, alignment, forceUpperCase);
591  }
592 
593  public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth, in ImmutableArray<RichTextData>? richTextData, int rtdOffset = 0, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit)
594  {
595  if (textures.Count == 0 && !DynamicLoading) { return; }
596  if (!richTextData.HasValue || richTextData.Value.Length <= 0) { DrawString(sb, text, position, color, rotation, origin, scale, se, layerDepth, forceUpperCase: forceUpperCase); return; }
597 
598  text = ApplyUpperCase(text, forceUpperCase);
599 
600  float lineWidth = -1.0f;
601  Vector2 currentLineOffset = Vector2.Zero;
602  if (DynamicLoading)
603  {
604  DynamicRenderAtlas(graphicsDevice, text);
605  }
606 
607  int lineNum = 0;
608  Vector2 currentPos = position;
609  Vector2 advanceUnit = rotation == 0.0f ? Vector2.UnitX : new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation));
610 
611  int richTextDataIndex = 0;
612  RichTextData currentRichTextData = richTextData.Value[richTextDataIndex];
613 
614  for (int i = 0; i < text.Length; i++)
615  {
616  HandleNewLineAndAlignment(text, advanceUnit, position, scale, alignment, i,
617  ref lineWidth, ref currentLineOffset, ref lineNum, ref currentPos,
618  out uint charIndex, out bool shouldContinue);
619  if (shouldContinue) { continue; }
620 
621  Color currentTextColor;
622 
623  while (currentRichTextData != null && i + rtdOffset > currentRichTextData.EndIndex + lineNum)
624  {
625  richTextDataIndex++;
626  currentRichTextData = richTextDataIndex < richTextData.Value.Length ? richTextData.Value[richTextDataIndex] : null;
627  }
628 
629  if (currentRichTextData != null && currentRichTextData.StartIndex + lineNum <= i + rtdOffset && i + rtdOffset <= currentRichTextData.EndIndex + lineNum)
630  {
631  currentTextColor = currentRichTextData.Color * currentRichTextData.Alpha ?? color;
632  if (!string.IsNullOrEmpty(currentRichTextData.Metadata))
633  {
634  currentTextColor = Color.Lerp(currentTextColor, Color.White, 0.5f);
635  }
636  }
637  else
638  {
639  currentTextColor = color;
640  }
641 
642  GlyphData gd = GetGlyphData(charIndex);
643  if (gd.TexIndex >= 0)
644  {
645  Texture2D tex = textures[gd.TexIndex];
646  Vector2 drawOffset;
647  drawOffset.X = gd.DrawOffset.X * advanceUnit.X * scale.X - gd.DrawOffset.Y * advanceUnit.Y * scale.Y;
648  drawOffset.Y = gd.DrawOffset.X * advanceUnit.Y * scale.Y + gd.DrawOffset.Y * advanceUnit.X * scale.X;
649 
650  sb.Draw(tex, currentPos + currentLineOffset + drawOffset, gd.TexCoords, currentTextColor, rotation, origin, scale, se, layerDepth);
651  }
652  currentPos += gd.Advance * advanceUnit * scale.X;
653  }
654  }
655 
656  public string WrapText(string text, float width)
657  => WrapText(text, width, requestCharPos: 0, out _, returnAllCharPositions: false, out _);
658 
659  public string WrapText(string text, float width, int requestCharPos, out Vector2 requestedCharPos)
660  => WrapText(text, width, requestCharPos, out requestedCharPos, returnAllCharPositions: false, out _);
661 
662  public string WrapText(string text, float width, out Vector2[] allCharPositions)
663  => WrapText(text, width, requestCharPos: 0, out _, returnAllCharPositions: true, out allCharPositions);
664 
670  private string WrapText(string text,
671  float width,
672  int requestCharPos,
673  out Vector2 requestedCharPos,
674  bool returnAllCharPositions,
675  out Vector2[] allCharPositions)
676  {
677  int currLineStart = 0;
678  Vector2 currentPos = Vector2.Zero;
679  Vector2 foundCharPos = Vector2.Zero;
680  int? lastBreakerIndex = null;
681  string result = "";
682  var allCharPos = returnAllCharPositions ? new Vector2[text.Length+1] : null;
683  for (int i = 0; i < text.Length; i++)
684  {
685  //Records the caret position of the current character
686  void recordCurrentPos()
687  {
688  if (i == requestCharPos) { foundCharPos = currentPos; }
689 
690  if (allCharPos != null) { allCharPos[i] = currentPos; }
691  }
692  recordCurrentPos();
693 
694  //Appends a newline to the result and resets the caret position's X value
695  void nextLine()
696  {
697  result += text[currLineStart..i].Remove("\n") + "\n";
698  lastBreakerIndex = null;
699  currentPos.X = 0.0f;
700  currentPos.Y += LineHeight;
701  currLineStart = i;
702  }
703 
704  //If a newline is found in the source, split immediately
705  if (text[i] == '\n')
706  {
707  nextLine();
708  continue;
709  }
710 
711  //Otherwise, advance based on the width of the current character
712  GlyphData gd = GetGlyphData(text[i]);
713  float advance = gd.Advance;
714  if (currentPos.X + advance >= width)
715  {
716  //Advancing based on the last character
717  //would put us past the max width!
718  if (i > 0 && char.IsWhiteSpace(text[i]) && !char.IsWhiteSpace(text[i - 1]))
719  {
720  //Whitespace immediately after a visible
721  //character can be shrunk down to fit
722  advance = width - currentPos.X;
723  }
724  else
725  {
726  if (lastBreakerIndex.HasValue)
727  {
728  //A breaker (whitespace or CJK) was found earlier
729  //in this line, so let's break the line there
730  i = lastBreakerIndex.Value + 1;
731  gd = GetGlyphData(text[i]);
732  advance = gd.Advance;
733  }
734 
735  nextLine();
736  recordCurrentPos(); //must re-record current caret position since we are on a new line now
737  }
738  }
739  currentPos.X += advance;
740 
741  if (char.IsWhiteSpace(text[i]) || TextManager.IsCJK($"{text[i]}"))
742  {
743  lastBreakerIndex = i;
744  }
745  }
746  if (requestCharPos >= text.Length) { foundCharPos = currentPos; }
747  if (allCharPos != null) { allCharPos[text.Length] = currentPos; }
748  allCharPositions = allCharPos;
749  result += text[currLineStart..].Remove("\n");
750  requestedCharPos = foundCharPos;
751  return result;
752  }
753 
754  public Vector2 MeasureString(LocalizedString str, bool removeExtraSpacing = false)
755  {
756  return MeasureString(str.Value, removeExtraSpacing);
757  }
758 
759  public Vector2 MeasureString(string text, bool removeExtraSpacing = false)
760  {
761  if (text == null)
762  {
763  return Vector2.Zero;
764  }
765 
766  float currentLineX = 0.0f;
767  Vector2 retVal = Vector2.Zero;
768 
769  if (!removeExtraSpacing)
770  {
771  retVal.Y = LineHeight;
772  }
773  else
774  {
775  retVal.Y = baseHeight;
776  }
777  if (DynamicLoading)
778  {
779  DynamicRenderAtlas(graphicsDevice, text);
780  }
781 
782  for (int i = 0; i < text.Length; i++)
783  {
784  if (text[i] == '\n')
785  {
786  currentLineX = 0.0f;
787  retVal.Y += LineHeight;
788  continue;
789  }
790  uint charIndex = text[i];
791 
792  GlyphData gd = GetGlyphData(charIndex);
793  currentLineX += gd.Advance;
794  retVal.X = Math.Max(retVal.X, currentLineX);
795  }
796  return retVal;
797  }
798 
799  public Vector2 MeasureChar(char c)
800  {
801  Vector2 retVal = Vector2.Zero;
802  retVal.Y = LineHeight;
803 
804  var (gd, _) = GetGlyphDataAndTextureForChar(c);
805  retVal.X = gd.Advance;
806  return retVal;
807  }
808 
809  public (GlyphData GlyphData, Texture2D Texture) GetGlyphDataAndTextureForChar(char c)
810  {
811  if (DynamicLoading && !texCoords.ContainsKey(c))
812  {
813  DynamicRenderAtlas(graphicsDevice, c);
814  }
815 
816  GlyphData gd = GetGlyphData(c);
817  var tex = gd.TexIndex >= 0 ? textures[gd.TexIndex] : null;
818  return (gd, tex);
819  }
820 
821  public void Dispose()
822  {
823  FontList.Remove(this);
824  foreach (Texture2D texture in textures)
825  {
826  texture.Dispose();
827  }
828  textures.Clear();
829  }
830  }
831 }
Vector2 MeasureChar(char c)
void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth, in ImmutableArray< RichTextData >? richTextData, int rtdOffset=0, Alignment alignment=Alignment.TopLeft, ForceUpperCase forceUpperCase=Barotrauma.ForceUpperCase.Inherit)
string WrapText(string text, float width)
string WrapText(string text, float width, out Vector2[] allCharPositions)
void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth, Alignment alignment=Alignment.TopLeft, ForceUpperCase forceUpperCase=Barotrauma.ForceUpperCase.Inherit)
static TextManager.SpeciallyHandledCharCategory ExtractShccFromXElement(XElement element)
string WrapText(string text, float width, int requestCharPos, out Vector2 requestedCharPos)
void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, ForceUpperCase forceUpperCase=Barotrauma.ForceUpperCase.Inherit, bool italics=false)
ScalableFont(string filename, uint size, GraphicsDevice gd=null, bool dynamicLoading=false, TextManager.SpeciallyHandledCharCategory speciallyHandledCharCategory=TextManager.SpeciallyHandledCharCategory.None)
Definition: ScalableFont.cs:98
Vector2 MeasureString(string text, bool removeExtraSpacing=false)
void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, in ImmutableArray< RichTextData >? richTextData, int rtdOffset=0, Alignment alignment=Alignment.TopLeft, ForceUpperCase forceUpperCase=Barotrauma.ForceUpperCase.Inherit)
TextManager.SpeciallyHandledCharCategory SpeciallyHandledCharCategory
Definition: ScalableFont.cs:42
void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, Alignment alignment=Alignment.TopLeft, ForceUpperCase forceUpperCase=Barotrauma.ForceUpperCase.Inherit)
Vector2 MeasureString(LocalizedString str, bool removeExtraSpacing=false)
ScalableFont(ContentXElement element, uint defaultSize=14, GraphicsDevice gd=null)
Definition: ScalableFont.cs:88