forked from Pathoschild/StardewMods
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDrawHelper.cs
More file actions
231 lines (198 loc) · 8.86 KB
/
DrawHelper.cs
File metadata and controls
231 lines (198 loc) · 8.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.Common;
using Pathoschild.Stardew.LookupAnything.Framework;
using StardewValley;
using StardewValley.Extensions;
namespace Pathoschild.Stardew.LookupAnything;
/// <summary>Provides utility methods for drawing to the screen.</summary>
internal static class DrawTextHelper
{
/*********
** Fields
*********/
/// <summary>The last language for which the helper was initialized.</summary>
private static string? LastLanguage;
/// <summary>The characters after which we can line-wrap text, but which are still included in the string.</summary>
private static readonly HashSet<char> SoftBreakCharacters = [];
/*********
** Public methods
*********/
/****
** Drawing
****/
/// <summary>Draw a block of text to the screen with the specified wrap width.</summary>
/// <param name="batch">The sprite batch.</param>
/// <param name="font">The sprite font.</param>
/// <param name="text">The block of text to write.</param>
/// <param name="position">The position at which to draw the text.</param>
/// <param name="wrapWidth">The width at which to wrap the text.</param>
/// <param name="color">The text color.</param>
/// <param name="bold">Whether to draw bold text.</param>
/// <param name="scale">The font scale.</param>
/// <returns>Returns the text dimensions.</returns>
public static Vector2 DrawTextBlock(this SpriteBatch batch, SpriteFont font, string? text, Vector2 position, float wrapWidth, Color? color = null, bool bold = false, float scale = 1)
{
return batch.DrawTextBlock(font, [new FormattedText(text, color, bold)], position, wrapWidth, scale);
}
/// <summary>Draw a block of text to the screen with the specified wrap width.</summary>
/// <param name="batch">The sprite batch.</param>
/// <param name="font">The sprite font.</param>
/// <param name="text">The block of text to write.</param>
/// <param name="position">The position at which to draw the text.</param>
/// <param name="wrapWidth">The width at which to wrap the text.</param>
/// <param name="scale">The font scale.</param>
/// <returns>Returns the text dimensions.</returns>
public static Vector2 DrawTextBlock(this SpriteBatch batch, SpriteFont font, IEnumerable<IFormattedText?>? text, Vector2 position, float wrapWidth, float scale = 1)
{
if (text == null)
return new Vector2(0, 0);
DrawTextHelper.InitIfNeeded();
// track draw values
float xOffset = 0;
float yOffset = 0;
float lineHeight = font.MeasureString("ABC").Y * scale;
float spaceWidth = DrawHelper.GetSpaceWidth(font) * scale;
float blockWidth = 0;
float blockHeight = lineHeight;
// draw text snippets
DrawTextHelper.InitIfNeeded();
foreach (IFormattedText? snippet in text)
{
if (snippet?.Text == null)
continue;
// build word list
string[] rawWords = snippet.Text.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (rawWords.Length > 0)
{
// keep surrounding spaces, since this snippet may be drawn before or after another
if (snippet.Text.StartsWith(" "))
rawWords[0] = $" {rawWords[0]}";
if (snippet.Text.EndsWith(" "))
rawWords[^1] += " ";
}
//for (int i = 0; i < rawWords.Length; i++) rawWords[i] = '[' + rawWords[i] + ']';
// draw words
bool isFirstOfLine = true;
foreach (string rawWord in rawWords)
{
string[] explicitLineBreaks = rawWord.Split('\n');
if (explicitLineBreaks.Length > 1)
{
for (int i = 0; i < explicitLineBreaks.Length; i++)
explicitLineBreaks[i] = explicitLineBreaks[i].TrimEnd('\r');
}
for (int i = 0; i < explicitLineBreaks.Length; i++)
{
// apply explicit \n
if (i > 0)
{
xOffset = 0;
yOffset += lineHeight;
blockHeight += lineHeight;
isFirstOfLine = true;
}
// split within words if needed (e.g. list separators)
bool isStartOfWord = true;
foreach (string wordPart in DrawTextHelper.SplitWithinWordForLineWrapping(explicitLineBreaks[i]))
{
// check wrap width
float wordWidth = font.MeasureString(wordPart).X * scale;
float prependSpace = isStartOfWord && !isFirstOfLine
? spaceWidth
: 0; // no space around soft breaks or start of line
if (wordPart == Environment.NewLine || ((wordWidth + xOffset + prependSpace) > wrapWidth && (int)xOffset != 0))
{
xOffset = 0;
yOffset += lineHeight;
blockHeight += lineHeight;
isFirstOfLine = true;
}
if (wordPart == Environment.NewLine)
continue;
// draw text
Vector2 wordPosition = new Vector2(position.X + xOffset + prependSpace, position.Y + yOffset);
if (snippet.Bold)
Utility.drawBoldText(batch, wordPart, font, wordPosition, snippet.Color ?? Color.Black, scale);
else
batch.DrawString(font, wordPart, wordPosition, snippet.Color ?? Color.Black, 0, Vector2.Zero, scale, SpriteEffects.None, 1);
// update draw values
if (xOffset + wordWidth + prependSpace > blockWidth)
blockWidth = xOffset + wordWidth + prependSpace;
xOffset += wordWidth + prependSpace;
isFirstOfLine = false;
isStartOfWord = false;
}
}
}
}
// return text position & dimensions
return new Vector2(blockWidth, blockHeight);
}
/*********
** Private methods
*********/
/// <summary>Initialize for the current language if needed.</summary>
public static void InitIfNeeded()
{
string language = LocalizedContentManager.CurrentLanguageString;
if (DrawTextHelper.LastLanguage != language)
{
string characters = I18n.GetByKey(I18n.Keys.Generic_LineWrapOn).UsePlaceholder(false);
DrawTextHelper.SoftBreakCharacters.Clear();
if (!string.IsNullOrEmpty(characters))
DrawTextHelper.SoftBreakCharacters.AddRange(characters);
DrawTextHelper.LastLanguage = language;
}
}
/// <summary>Split a word into segments based on newlines and soft-break characters.</summary>
/// <param name="text">The text to split.</param>
private static IList<string> SplitWithinWordForLineWrapping(string text)
{
HashSet<char> splitChars = DrawTextHelper.SoftBreakCharacters;
string newLine = Environment.NewLine;
// handle soft breaks within word
List<string> words = [];
int start = 0;
for (int i = 0; i < text.Length; i++)
{
char ch = text[i];
// newline marker
if (ch == newLine[0] && DrawTextHelper.IsNewlineAt(text, i))
{
if (i > start)
words.Add(text.Substring(start, i - start));
words.Add(newLine);
i += newLine.Length;
start = i;
}
// soft break character
else if (splitChars.Contains(ch))
{
words.Add(text.Substring(start, i - start + 1));
start = i + 1;
}
}
// add any remainder
if (start == 0)
words.Add(text);
else if (start < text.Length - 1)
words.Add(text.Substring(start));
return words;
}
/// <summary>Get whether there's a newline sequence at a given text position.</summary>
/// <param name="text">The text to search.</param>
/// <param name="index">The index to check.</param>
private static bool IsNewlineAt(string text, int index)
{
string newline = Environment.NewLine;
for (int i = index, n = 0; i < text.Length && n < newline.Length; i++, n++)
{
if (text[i] != newline[n])
return false;
}
return true;
}
}