2023-06-02 21:54:15 +03:00
using System.Diagnostics.Contracts ;
using System.Text ;
using Robust.Client.Graphics ;
using Robust.Shared.Utility ;
2024-01-28 17:32:55 +07:00
namespace Content.Client._White.UserInterface.Controls ;
2023-06-02 21:54:15 +03:00
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 ( '-' ) ;
}
}