mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-01 09:37:03 +02:00
179 lines
6.7 KiB
C#
179 lines
6.7 KiB
C#
using System;
|
|
using System.Diagnostics.Contracts;
|
|
using System.Text;
|
|
using Robust.Client.Graphics;
|
|
using Robust.Shared.Log;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Robust.Client.UserInterface;
|
|
|
|
/// <summary>
|
|
/// Helper utility struct for word-wrapping calculations.
|
|
/// </summary>
|
|
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;
|
|
|
|
public WordWrap(float maxSizeX)
|
|
{
|
|
this = default;
|
|
_maxSizeX = maxSizeX;
|
|
LastRune = new Rune('A');
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add <paramref name="rune" /> as the next glyph in the sequence and update internal line break state.
|
|
/// </summary>
|
|
/// <param name="breakLine">If non-null, indicates that a line break needs to be added at this rune
|
|
/// index within the string. This index will refer to the offset of a previously-entered rune</param>
|
|
/// <param name="breakNewLine">If non-null, indicates the rune index of an entered newline</param>
|
|
/// <param name="skip">If true, indicates that the rune should occupy zero space. Currently only used
|
|
/// for newlines.</param>
|
|
public void NextRune(Rune rune, out int? breakLine, out int? breakNewLine, out bool skip)
|
|
{
|
|
BreakIndexCounter = NextBreakIndexCounter;
|
|
NextBreakIndexCounter += 1;
|
|
|
|
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);
|
|
|
|
// 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;
|
|
|
|
// Break the "word" at the last word index
|
|
if (WordStartBreakIndex.HasValue && oldWordSizePixels != 0)
|
|
{
|
|
breakLine = WordStartBreakIndex.Value.index;
|
|
MaxUsedWidth = Math.Max(MaxUsedWidth, WordStartBreakIndex.Value.lineSize);
|
|
PosX = WordSizePixels;
|
|
}
|
|
|
|
// Oh hey we get to break a word that doesn't fit on a single line.
|
|
if (WordSizePixels > _maxSizeX)
|
|
{
|
|
if (oldWordSizePixels == 0)
|
|
{
|
|
// Happens if there's literally not enough space for a single character so uh...
|
|
// Yeah just don't.
|
|
abort = true;
|
|
return;
|
|
}
|
|
|
|
breakLine = BreakIndexCounter;
|
|
WordSizePixels -= oldWordSizePixels;
|
|
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($"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('-');
|
|
}
|
|
|
|
}
|