Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion Blish HUD/Controls/MultilineTextBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,27 @@ public bool HideBackground {
/// </summary>
public int[] DisplayNewLineIndices => _displayNewLineIndices;

private bool _disableWordWrap;

/// <summary>
/// Determines whether the automatic word-wrap will be disabled.
/// </summary>
public bool DisableWordWrap {
get => _disableWordWrap;
set => SetProperty(ref _disableWordWrap, value);
}

private char[] _wrapCharacters;

/// <summary>
/// The characters, that are used to wrap a word, if it does not fit the current line
/// it's on.
/// </summary>
public char[] WrapCharacters {
get => _wrapCharacters ?? Array.Empty<char>();
set => SetProperty(ref _wrapCharacters, value);
}

public MultilineTextBox() {
_multiline = true;
_maxLength = 524288;
Expand Down Expand Up @@ -105,7 +126,12 @@ protected override string ProcessDisplayText(string value) {
/// Applies word-wrap to the <paramref name="value"/>.
/// </summary>
protected string ApplyWordWrap(string value) {
string displayText = DrawUtil.WrapText(_font, value, this._textRegion.Width, out int[] newLineIndices);
if (DisableWordWrap) {
_displayNewLineIndices = Array.Empty<int>();
return value;
}

string displayText = DrawUtil.WrapText(_font, value, this._textRegion.Width, WrapCharacters, out int[] newLineIndices);
_displayNewLineIndices = newLineIndices;

return displayText;
Expand Down
173 changes: 151 additions & 22 deletions Blish HUD/_Utils/DrawUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,48 +43,171 @@ public static void DrawAlignedText(SpriteBatch sb, BitmapFont sf, string text, R
sb.DrawString(sf, text, new Vector2(xPos, yPos), clr);
}

/// <summary>
/// Wraps a <paramref name="word"/>, if it does not fit into the <paramref name="maxLineWidth"/>
/// accounting for the given <paramref name="offset"/>.
/// </summary>
/// <remarks>
/// Will prioritize wrapping a word at any of the given <paramref name="preferredWrapCharacters"/>,
/// but will wrap in the middle of the word if none of them occur.
/// </remarks>
/// <param name="spriteFont"></param>
/// <param name="word"></param>
/// <param name="offset"></param>
/// <param name="maxLineWidth"></param>
/// <param name="preferredWrapCharacters"></param>
/// <param name="newLineIndices"></param>
/// <returns>The <paramref name="word"/> with new line characters at appropriate
/// positions to make it fit into the <paramref name="maxLineWidth"/>.</returns>
private static string WrapWord(BitmapFont spriteFont, string word, float offset, float maxLineWidth, char[] preferredWrapCharacters, out int[] newLineIndices) {
newLineIndices = Array.Empty<int>();
if (string.IsNullOrEmpty(word)) return string.Empty;

if (offset + spriteFont.MeasureString(word).Width <= maxLineWidth) return word;

StringBuilder resultBuilder = new StringBuilder();

List<int> indices = new List<int>();
bool didSplitCharacterOccur = false;

StringBuilder partBuilder = new StringBuilder();

// this is neccessary, because measuring each character individually and
// adding them up, results in a significant higher value that measuring the whole line
float currentLineWithNewCharacterWidth;
string currentLineCharacters = string.Empty;

for (int i = 0; i < word.Length; i++) {
if (indices.Any()) {
offset = 0;
}

char character = word[i];
currentLineCharacters += character;
currentLineWithNewCharacterWidth = spriteFont.MeasureString(currentLineCharacters).Width + offset;

if (currentLineWithNewCharacterWidth < maxLineWidth) {
partBuilder.Append(character);

if (preferredWrapCharacters.Contains(character)) {
resultBuilder.Append(partBuilder);
partBuilder.Clear();
didSplitCharacterOccur = true;
}
} else {

int characterOffset = 0;

if (!didSplitCharacterOccur) {
resultBuilder.Append(partBuilder);
resultBuilder.Append('\n');
currentLineCharacters = string.Empty;
}
else {
resultBuilder.Append('\n');
resultBuilder.Append(partBuilder);
currentLineCharacters = partBuilder.ToString();

characterOffset = partBuilder.Length;
}

indices.Add(i + indices.Count() - characterOffset);

partBuilder.Clear();
partBuilder.Append(character);
currentLineCharacters += character;

didSplitCharacterOccur = false;
}
}

if (partBuilder.Length != 0) {
resultBuilder.Append(partBuilder);
}

newLineIndices = indices.ToArray();
return resultBuilder.ToString();
}

private static string WrapTextSegment(BitmapFont spriteFont, string text, float maxLineWidth) {
return WrapTextSegment(spriteFont, text, maxLineWidth, out _);
}

private static string WrapTextSegment(BitmapFont spriteFont, string text, float maxLineWidth, out int[] newLineIndices) {
return WrapTextSegment(spriteFont, text, maxLineWidth, Array.Empty<char>(), out newLineIndices);
}

/// <remarks>
/// Source: https://stackoverflow.com/a/15987581/595437
/// (slightly modified)
/// Original source: https://stackoverflow.com/a/15987581/595437
/// (modified)
/// </remarks>
private static string WrapTextSegment(BitmapFont spriteFont, string text, float maxLineWidth, out int[] newLineIndices) {
private static string WrapTextSegment(BitmapFont spriteFont, string text, float maxLineWidth, char[] preferredWrapCharacters, out int[] newLineIndices) {
newLineIndices = Array.Empty<int>();
if (string.IsNullOrEmpty(text)) return string.Empty;

string[] words = text.Split(' ');
var sb = new StringBuilder();
float lineWidth = 0f;
float currentLineWidth = 0f;
float spaceWidth = spriteFont.MeasureString(" ").Width;

List<int> indices = new List<int>();
int characterIndex = 0;
int processedCharacters = 0;

for (int i = 0; i < words.Length; i++) {
string word = words[i];
Vector2 size = spriteFont.MeasureString(word);
float wordWidth = spriteFont.MeasureString(word).Width;

if (lineWidth + size.X < maxLineWidth) {
if (currentLineWidth + wordWidth < maxLineWidth) {
sb.Append(word);
lineWidth += size.X;
currentLineWidth += wordWidth;
if (i < words.Length - 1) {
sb.Append(" ");
lineWidth += spaceWidth;
currentLineWidth += spaceWidth;
processedCharacters++;
}
} else {
sb.Append("\n" + word);
lineWidth = size.X;
string wrappedWord = WrapWord(spriteFont, word, currentLineWidth, maxLineWidth, preferredWrapCharacters, out int[] wordNewLineIndices);

string firstPart = wrappedWord;
string lastPart = wrappedWord;

if (wordNewLineIndices.Length != 0) {
firstPart = wrappedWord.Substring(0, wordNewLineIndices.First());
lastPart = wrappedWord.Substring(wordNewLineIndices.Last() + 1);
}

// words should only every be broken in the middle of the word (no wrap character
// in the first part), if they started on their own line.
if (preferredWrapCharacters.Any(character => firstPart.Contains(character)) || currentLineWidth == 0) {
sb.Append(wrappedWord);
currentLineWidth = spriteFont.MeasureString(lastPart).Width;

int indexOffset = processedCharacters + indices.Count();

foreach (int wordIndex in wordNewLineIndices) {
indices.Add(wordIndex + indexOffset);
}
} else {
string wrappedWordOnNextLine = WrapWord(spriteFont, word, 0, maxLineWidth, preferredWrapCharacters, out int[] wordOnNextLineNewLineIndices);
sb.Append('\n');
indices.Add(processedCharacters + indices.Count());
sb.Append(wrappedWordOnNextLine);
currentLineWidth = spriteFont.MeasureString(wrappedWordOnNextLine.Split('\n').Last()).Width;

int indexOffset = processedCharacters + indices.Count();

foreach (int wordIndex in wordOnNextLineNewLineIndices) {
indices.Add(wordIndex + indexOffset);
}
}

if (i < words.Length - 1) {
sb.Append(" ");
lineWidth += spaceWidth;
processedCharacters++;
currentLineWidth += spaceWidth;
}
indices.Add(characterIndex);
characterIndex++;
}
characterIndex += word.Length + 1;
processedCharacters += word.Length;
}

newLineIndices = indices.ToArray();
Expand All @@ -96,31 +219,37 @@ public static string WrapText(BitmapFont spriteFont, string text, float maxLineW
}

public static string WrapText(BitmapFont spriteFont, string text, float maxLineWidth, out int[] newLineIndices) {
return WrapText(spriteFont, text, maxLineWidth, Array.Empty<char>(), out newLineIndices);
}

public static string WrapText(BitmapFont spriteFont, string text, float maxLineWidth, char[] preferredWrapCharacters, out int[] newLineIndices) {
newLineIndices = Array.Empty<int>();
if (string.IsNullOrEmpty(text)) return "";

var sb = new StringBuilder();
List<int> indices = new List<int>();
int lineStart = 0;
int processedCharacters = 0;

string[] lines = text.Split('\n');
for (int i = 0; i < lines.Length; i++) {
sb.Append(WrapTextSegment(spriteFont, lines[i], maxLineWidth, out int[] localNewLineIndices));
foreach (int localIndex in localNewLineIndices) {
indices.Add(localIndex + lineStart);
sb.Append(WrapTextSegment(spriteFont, lines[i], maxLineWidth, preferredWrapCharacters, out int[] segmentNewLineIndices));

int indexOffset = processedCharacters + indices.Count();

foreach (int segmentIndex in segmentNewLineIndices) {
indices.Add(segmentIndex + indexOffset);
}

lineStart += lines[i].Length;
processedCharacters += lines[i].Length;

if (i < lines.Length - 1) {
sb.Append('\n');
lineStart++;
processedCharacters++;
}
}

newLineIndices = indices.ToArray();
return sb.ToString();
}

}
}