Files

171 lines
6.3 KiB
C#

using System.Diagnostics.Contracts;
using System.Text;
using Robust.Client.Graphics;
using Robust.Shared.Utility;
namespace Content.Client._White.UserInterface.Controls;
internal struct WordWrap
{
private readonly float _maxSizeX;
public float MaxUsedWidth;
// Index we put into the LineBreaks list when a line break should occur.
public int BreakIndexCounter;
public int NextBreakIndexCounter;
// If the CURRENT processing word ends up too long, this is the index to put a line break.
public (int index, float lineSize)? WordStartBreakIndex;
// Word size in pixels.
public int WordSizePixels;
// The horizontal position of the text cursor.
public int PosX;
public Rune LastRune;
// If a word is larger than maxSizeX, we split it.
// We need to keep track of some data to split it into two words.
public (int breakIndex, int wordSizePixels)? ForceSplitData = null;
public WordWrap(float maxSizeX)
{
this = default;
_maxSizeX = maxSizeX;
LastRune = new Rune('A');
}
public void NextRune(Rune rune, out int? breakLine, out int? breakNewLine, out bool skip)
{
BreakIndexCounter = NextBreakIndexCounter;
NextBreakIndexCounter += rune.Utf16SequenceLength;
breakLine = null;
breakNewLine = null;
skip = false;
if (IsWordBoundary(LastRune, rune) || rune == new Rune('\n'))
{
// Word boundary means we know where the word ends.
if (PosX > _maxSizeX && LastRune != new Rune(' '))
{
DebugTools.Assert(WordStartBreakIndex.HasValue,
"wordStartBreakIndex can only be null if the word begins at a new line, in which case this branch shouldn't be reached as the word would be split due to being longer than a single line.");
//Ensure the assert had a chance to run and then just return
if (!WordStartBreakIndex.HasValue)
return;
// We ran into a word boundary and the word is too big to fit the previous line.
// So we insert the line break BEFORE the last word.
breakLine = WordStartBreakIndex!.Value.index;
MaxUsedWidth = Math.Max(MaxUsedWidth, WordStartBreakIndex.Value.lineSize);
PosX = WordSizePixels;
}
// Start a new word since we hit a word boundary.
//wordSize = 0;
WordSizePixels = 0;
WordStartBreakIndex = (BreakIndexCounter, PosX);
ForceSplitData = null;
// Just manually handle newlines.
if (rune == new Rune('\n'))
{
MaxUsedWidth = Math.Max(MaxUsedWidth, PosX);
PosX = 0;
WordStartBreakIndex = null;
skip = true;
breakNewLine = BreakIndexCounter;
}
}
LastRune = rune;
}
public void NextMetrics(in CharMetrics metrics, out int? breakLine, out bool abort)
{
abort = false;
breakLine = null;
// Increase word size and such with the current character.
var oldWordSizePixels = WordSizePixels;
WordSizePixels += metrics.Advance;
// TODO: Theoretically, does it make sense to break after the glyph's width instead of its advance?
// It might result in some more tight packing but I doubt it'd be noticeable.
// Also definitely even more complex to implement.
PosX += metrics.Advance;
if (PosX <= _maxSizeX)
return;
if (!ForceSplitData.HasValue)
{
ForceSplitData = (BreakIndexCounter, oldWordSizePixels);
}
// Oh hey we get to break a word that doesn't fit on a single line.
if (WordSizePixels > _maxSizeX)
{
var (breakIndex, splitWordSize) = ForceSplitData.Value;
if (splitWordSize == 0)
{
// Happens if there's literally not enough space for a single character so uh...
// Yeah just don't.
abort = true;
return;
}
// Reset forceSplitData so that we can split again if necessary.
ForceSplitData = null;
breakLine = breakIndex;
WordSizePixels -= splitWordSize;
WordStartBreakIndex = null;
MaxUsedWidth = Math.Max(MaxUsedWidth, _maxSizeX);
PosX = WordSizePixels;
}
}
public int FinalizeText(out int? breakLine)
{
// This needs to happen because word wrapping doesn't get checked for the last word.
if (PosX > _maxSizeX)
{
if (!WordStartBreakIndex.HasValue)
{
Logger.Error(
"Assert fail inside RichTextEntry.Update, " +
"wordStartBreakIndex is null on method end w/ word wrap required. " +
"Dumping relevant stuff. Send this to PJB.");
// Logger.Error($"Message: {Message}");
Logger.Error($"maxSizeX: {_maxSizeX}");
Logger.Error($"maxUsedWidth: {MaxUsedWidth}");
Logger.Error($"breakIndexCounter: {BreakIndexCounter}");
Logger.Error("wordStartBreakIndex: null (duh)");
Logger.Error($"wordSizePixels: {WordSizePixels}");
Logger.Error($"posX: {PosX}");
Logger.Error($"lastChar: {LastRune}");
Logger.Error($"forceSplitData: {ForceSplitData}");
// Logger.Error($"LineBreaks: {string.Join(", ", LineBreaks)}");
throw new Exception(
"wordStartBreakIndex can only be null if the word begins at a new line," +
"in which case this branch shouldn't be reached as" +
"the word would be split due to being longer than a single line.");
}
breakLine = WordStartBreakIndex.Value.index;
MaxUsedWidth = Math.Max(MaxUsedWidth, WordStartBreakIndex.Value.lineSize);
}
else
{
breakLine = null;
MaxUsedWidth = Math.Max(MaxUsedWidth, PosX);
}
return (int)MaxUsedWidth;
}
[Pure]
private static bool IsWordBoundary(Rune a, Rune b)
{
return a == new Rune(' ') || b == new Rune(' ') || a == new Rune('-') || b == new Rune('-');
}
}