using System.Globalization; namespace Sandbox.UI; public partial class Label { /// /// Enables multi-line support for editing purposes. /// public bool Multiline { get; set; } = true; // Sol: TODO: unlink this from wrapping, add css text-wrap or something private Vector2 caretScroll; /// /// Replace the currently selected text with given text. /// public void ReplaceSelection( string str ) { var s = Math.Min( SelectionStart, SelectionEnd ); var e = Math.Max( SelectionStart, SelectionEnd ); var len = e - s; if ( CaretPosition > e ) CaretPosition -= len; else if ( CaretPosition > s ) CaretPosition = s; CaretPosition += new StringInfo( str ).LengthInTextElements; InsertText( str, s, e ); SelectionStart = 0; SelectionEnd = 0; } /// /// Sets the text selection. /// public void SetSelection( int start, int end ) { var s = Math.Min( start, end ).Clamp( 0, TextLength ); var e = Math.Max( start, end ).Clamp( 0, TextLength ); if ( s == e ) { s = 0; e = 0; } SelectionStart = s; SelectionEnd = e; } /// /// Set the text caret position to the given index. /// /// Where to move the text caret to within the text. /// Whether to also add the characters we passed by to the selection. public void SetCaretPosition( int pos, bool select = false ) { if ( SelectionEnd == 0 && SelectionStart == 0 && select ) { SelectionStart = CaretPosition.Clamp( 0, TextLength ); } CaretPosition = pos.Clamp( 0, TextLength ); if ( select ) { SelectionEnd = CaretPosition; } else { SelectionEnd = 0; SelectionStart = 0; } ScrollToCaret(); } /// /// Put the caret within the visible region. /// public void ScrollToCaret() { // Sol: not supported right now. think would work totally differently with "proper" scrollbars etc? if ( Multiline ) return; if ( _textBlock is null ) return; _textBlock.ScrollToCaret( CaretPosition, ref caretScroll, Box.RectInner.Size ); } /// /// Move the text caret to the closest word start or end to the left of current position.
/// This simulates holding Control key while pressing left arrow key. ///
/// Whether to also add the characters we passed by to the selection. public void MoveToWordBoundaryLeft( bool select ) { var boundaries = GetWordBoundaryIndices(); var left = boundaries.LastOrDefault( x => x < CaretPosition ); MoveCaretPos( left - CaretPosition, select ); } /// /// Move the text caret to the closest word start or end to the right of current position.
/// This simulates holding Control key while pressing right arrow key. ///
/// Whether to also add the characters we passed by to the selection. public void MoveToWordBoundaryRight( bool select ) { var boundaries = GetWordBoundaryIndices(); var right = boundaries.FirstOrDefault( x => x >= CaretPosition ); if ( right == 0 ) return; MoveCaretPos( right - CaretPosition, select ); } /// /// Move the text caret by given amount. /// /// How many characters to the right to move. Negative values move left. /// Whether to also add the characters we passed by to the selection. public void MoveCaretPos( int delta, bool select = false ) { SetCaretPosition( CaretPosition + delta, select ); } /// /// Insert given text at given position. /// /// Text to insert. /// Position to insert the text at. /// If set, the end position in the current , /// which will be used to replace portion of the existing text with the given . public void InsertText( string text, int pos, int? endpos = null ) { CaretSantity(); pos = Math.Clamp( pos, 0, TextLength ); if ( endpos.HasValue ) endpos = Math.Clamp( endpos.Value, 0, TextLength ); var a = pos > 0 ? StringInfo.SubstringByTextElements( 0, pos ) : ""; var b = ""; if ( endpos.HasValue ) { if ( endpos < TextLength ) b = StringInfo.SubstringByTextElements( endpos.Value ); } else { if ( pos < TextLength ) b = StringInfo.SubstringByTextElements( pos ); } Text = $"{a}{text}{b}"; } /// /// Remove given amount of characters from the label at given position. /// public virtual void RemoveText( int start, int count ) { var a = start > 0 ? StringInfo.SubstringByTextElements( 0, start ) : ""; var b = (start + count < TextLength) ? StringInfo.SubstringByTextElements( start + count ) : ""; Text = a + b; } /// /// Move the text caret to the start of the current line. /// /// Whether to also add the characters we passed by to the selection. public void MoveToLineStart( bool select = false ) { if ( !Multiline ) { SetCaretPosition( 0, select ); return; } int iNewline = 0; var e = StringInfo.GetTextElementEnumerator( Text ); while ( e.MoveNext() ) { if ( e.ElementIndex >= CaretPosition ) break; if ( IsNewline( e.GetTextElement() ) ) iNewline = e.ElementIndex + 1; } SetCaretPosition( iNewline, select ); } /// /// Move the text caret to the end of the current line. /// /// Whether to also add the characters we passed by to the selection. public void MoveToLineEnd( bool select = false ) { if ( !Multiline ) { SetCaretPosition( TextLength, select ); return; } var e = StringInfo.GetTextElementEnumerator( Text ); while ( e.MoveNext() ) { if ( e.ElementIndex < CaretPosition ) continue; if ( IsNewline( e.GetTextElement() ) ) { SetCaretPosition( e.ElementIndex, select ); return; } } SetCaretPosition( TextLength, select ); } /// /// Move the text caret to next or previous line. /// /// How many lines to offset. Negative values move up. /// Whether to also add the characters we passed by to the selection. public void MoveCaretLine( int offset_line, bool select ) { if ( !Multiline ) { if ( offset_line < 0 ) SetCaretPosition( 0, select ); if ( offset_line > 0 ) SetCaretPosition( TextLength, select ); return; } var caret = GetCaretRect( CaretPosition ); var height = caret.Size; height.x = 0; var click = caret.Position + caret.Size * 0.5f + height * offset_line * 1.2f; var pos = GetLetterAtScreenPosition( click ); SetCaretPosition( pos, select ); } /// /// Select a work at given word position. /// public void SelectWord( int wordPos ) { if ( TextLength == 0 ) return; var boundaries = GetWordBoundaryIndices(); SelectionStart = boundaries.LastOrDefault( x => x < wordPos ); SelectionEnd = boundaries.FirstOrDefault( x => x >= wordPos ); CaretPosition = SelectionEnd; } /// /// Returns a list of positions in the text of each side of each word within the .
/// This is used for Control + Arrow Key navigation. ///
public List GetWordBoundaryIndices() { var result = new List() { 0, StringInfo.LengthInTextElements }; var e = StringInfo.GetTextElementEnumerator( Text ); var input = string.Empty; // make it work with graphemes by assuming everything is 1 char long while ( e.MoveNext() ) { input += e.GetTextElement()[0]; } var match = System.Text.RegularExpressions.Regex.Match( input, @"\b" ); while ( match.Success ) { result.Add( match.Index ); match = match.NextMatch(); } result = result.Distinct().ToList(); result.Sort(); return result; } /// /// Returns true if the input string is a 1 or 2 (\r\n) character newline symbol. /// private bool IsNewline( string str ) { if ( str == "\n" ) return true; if ( str == "\r\n" ) return true; if ( str == "\r" ) return true; return false; } }