// ClassicalSharp copyright 2014-2016 UnknownShadow200 | Licensed under MIT using System; using System.Drawing; using OpenTK.Input; #if ANDROID using Android.Graphics; #endif namespace ClassicalSharp.Gui.Widgets { public abstract class InputWidget : Widget { public InputWidget( Game game, Font font ) : base( game ) { Text = new WrappableStringBuffer( Utils.StringLength * MaxLines ); lines = new string[MaxLines]; lineSizes = new Size[MaxLines]; DrawTextArgs args = new DrawTextArgs( "_", font, true ); caretTex = game.Drawer2D.MakeChatTextTexture( ref args, 0, 0 ); caretTex.Width = (short)((caretTex.Width * 3) / 4); caretWidth = caretTex.Width; caretHeight = caretTex.Height; this.font = font; if( Prefix == null ) return; args = new DrawTextArgs( Prefix, font, true ); Size size = game.Drawer2D.MeasureChatSize( ref args ); prefixWidth = Width = size.Width; prefixHeight = Height = size.Height; } public InputWidget SetLocation( Anchor horAnchor, Anchor verAnchor, int xOffset, int yOffset ) { HorizontalAnchor = horAnchor; VerticalAnchor = verAnchor; XOffset = xOffset; YOffset = yOffset; CalculatePosition(); return this; } protected int caret = -1; protected Texture inputTex, caretTex, prefixTex; protected readonly Font font; protected int caretWidth, caretHeight, prefixWidth, prefixHeight; protected FastColour caretColour; /// The raw text entered. /// You should Append() to add more text, as that also updates the caret position and texture. public WrappableStringBuffer Text; /// The maximum number of lines that may be entered. public abstract int MaxLines { get; } /// The maximum number of characters that can fit on one line. public abstract int MaxCharsPerLine { get; } /// The prefix string that is always shown before the input text. Can be null. public abstract string Prefix { get; } /// The horizontal offset (in pixels) from the start of the box background /// to the beginning of the input texture. public abstract int Padding { get; } /// Whether a caret should be drawn at the position characters /// are inserted/deleted from the input text. public bool ShowCaret; protected string[] lines; // raw text of each line protected Size[] lineSizes; // size of each line in pixels protected int caretCol, caretRow; // coordinates of caret protected double caretAccumulator; public override void Init() { if( lines.Length > 1 ) { Text.WordWrap( game.Drawer2D, lines, MaxCharsPerLine ); } else { lines[0] = Text.ToString(); } CalculateLineSizes(); RemakeTexture(); UpdateCaret(); } public override void Dispose() { gfx.DeleteTexture( ref inputTex ); } public void DisposeFully() { Dispose(); gfx.DeleteTexture( ref caretTex ); gfx.DeleteTexture( ref prefixTex ); } public override void CalculatePosition() { int oldX = X, oldY = Y; base.CalculatePosition(); caretTex.X1 += X - oldX; caretTex.Y1 += Y - oldY; inputTex.X1 += X - oldX; inputTex.Y1 += Y - oldY; } /// Calculates the sizes of each line in the text buffer. public void CalculateLineSizes() { for( int y = 0; y < lineSizes.Length; y++ ) lineSizes[y] = Size.Empty; lineSizes[0].Width = prefixWidth; DrawTextArgs args = new DrawTextArgs( null, font, true ); for( int y = 0; y < MaxLines; y++ ) { args.Text = lines[y]; lineSizes[y] += game.Drawer2D.MeasureChatSize( ref args ); } if( lineSizes[0].Height == 0 ) lineSizes[0].Height = prefixHeight; } /// Calculates the location and size of the caret character public void UpdateCaret() { if( caret >= Text.Length ) caret = -1; Text.GetCoords( caret, lines, out caretCol, out caretRow ); DrawTextArgs args = new DrawTextArgs( null, font, true ); IDrawer2D drawer = game.Drawer2D; caretAccumulator = 0; if( caretCol == MaxCharsPerLine ) { caretTex.X1 = X + Padding + lineSizes[caretRow].Width; caretColour = FastColour.Yellow; caretTex.Width = (short)caretWidth; } else { args.Text = lines[caretRow].Substring( 0, caretCol ); Size trimmedSize = drawer.MeasureChatSize( ref args ); if( caretRow == 0 ) trimmedSize.Width += prefixWidth; caretTex.X1 = X + Padding + trimmedSize.Width; caretColour = FastColour.Scale( FastColour.White, 0.8f ); string line = lines[caretRow]; if( caretCol < line.Length ) { args.Text = new String( line[caretCol], 1 ); caretTex.Width = (short)drawer.MeasureChatSize( ref args ).Width; } else { caretTex.Width = (short)caretWidth; } } caretTex.Y1 = lineSizes[0].Height * caretRow + inputTex.Y1 + 2; // Update the colour of the caret char code = GetLastColour( caretCol, caretRow ); if( code != '\0' ) caretColour = drawer.Colours[code]; } protected void RenderCaret( double delta ) { if( !ShowCaret ) return; caretAccumulator += delta; if( (caretAccumulator % 1) < 0.5 ) caretTex.Render( gfx, caretColour ); } /// Remakes the raw texture containg all the chat lines. /// Also updates the dimensions of the widget. public virtual void RemakeTexture() { int totalHeight = 0, maxWidth = 0; for( int i = 0; i < MaxLines; i++ ) { totalHeight += lineSizes[i].Height; maxWidth = Math.Max( maxWidth, lineSizes[i].Width ); } Size size = new Size( maxWidth, totalHeight ); caretAccumulator = 0; int realHeight = 0; using( Bitmap bmp = IDrawer2D.CreatePow2Bitmap( size ) ) using( IDrawer2D drawer = game.Drawer2D ) { drawer.SetBitmap( bmp ); DrawTextArgs args = new DrawTextArgs( null, font, true ); if( Prefix != null ) { args.Text = Prefix; drawer.DrawChatText( ref args, 0, 0 ); } for( int i = 0; i < lines.Length; i++ ) { if( lines[i] == null ) break; args.Text = lines[i]; char lastCol = GetLastColour( 0, i ); if( !IDrawer2D.IsWhiteColour( lastCol ) ) args.Text = "&" + lastCol + args.Text; int offset = i == 0 ? prefixWidth : 0; drawer.DrawChatText( ref args, offset, realHeight ); realHeight += lineSizes[i].Height; } inputTex = drawer.Make2DTexture( bmp, size, 0, 0 ); } Width = size.Width; Height = realHeight == 0 ? prefixHeight : realHeight; CalculatePosition(); inputTex.X1 = X + Padding; inputTex.Y1 = Y; } protected char GetLastColour( int indexX, int indexY ) { int x = indexX; IDrawer2D drawer = game.Drawer2D; for( int y = indexY; y >= 0; y-- ) { string part = lines[y]; char code = drawer.LastColour( part, x ); if( code != '\0' ) return code; if( y > 0 ) x = lines[y - 1].Length; } return '\0'; } /// Invoked when the user presses enter. public virtual void EnterInput() { Clear(); Height = prefixHeight; } /// Clears all the characters from the text buffer /// Deletes the native texture. public void Clear() { Text.Clear(); for( int i = 0; i < lines.Length; i++ ) lines[i] = null; caret = -1; Dispose(); } /// Appends a sequence of characters to current text buffer. /// Potentially recreates the native texture. public void Append( string text ) { int appended = 0; foreach( char c in text ) { if( TryAppendChar( c ) ) appended++; } if( appended == 0 ) return; Recreate(); } /// Appends a single character to current text buffer. /// Potentially recreates the native texture. public void Append( char c ) { if( !TryAppendChar( c ) ) return; Recreate(); } bool TryAppendChar( char c ) { int totalChars = MaxCharsPerLine * lines.Length; if( Text.Length == totalChars ) return false; if( !AllowedChar( c ) ) return false; AppendChar( c ); return true; } protected virtual bool AllowedChar( char c ) { return Utils.IsValidInputChar( c, game ); } protected void AppendChar( char c ) { if( caret == -1 ) { Text.InsertAt( Text.Length, c ); } else { Text.InsertAt( caret, c ); caret++; if( caret >= Text.Length ) caret = -1; } } protected void DeleteChar() { if( Text.Length == 0 ) return; if( caret == -1 ) { Text.DeleteAt( Text.Length - 1 ); } else { caret--; Text.DeleteAt( caret ); } } #region Input handling protected bool ControlDown() { return OpenTK.Configuration.RunningOnMacOS ? (game.IsKeyDown( Key.WinLeft ) || game.IsKeyDown( Key.WinRight )) : (game.IsKeyDown( Key.ControlLeft ) || game.IsKeyDown( Key.ControlRight )); } public override bool HandlesKeyPress( char key ) { if( !game.HideGui ) Append( key ); return true; } public override bool HandlesKeyDown( Key key ) { if( game.HideGui ) return key < Key.F1 || key > Key.F35; bool clipboardDown = ControlDown(); if( key == Key.Left ) LeftKey( clipboardDown ); else if( key == Key.Right ) RightKey( clipboardDown ); else if( key == Key.BackSpace ) BackspaceKey( clipboardDown ); else if( key == Key.Delete ) DeleteKey(); else if( key == Key.Home ) HomeKey(); else if( key == Key.End ) EndKey(); else if( clipboardDown && !OtherKey( key ) ) return false; return true; } public override bool HandlesKeyUp( Key key ) { return true; } public override bool HandlesMouseClick( int mouseX, int mouseY, MouseButton button ) { if( button == MouseButton.Left ) SetCaretToCursor( mouseX, mouseY ); return true; } void BackspaceKey( bool controlDown ) { if( controlDown ) { if( caret == -1 ) caret = Text.Length - 1; int len = Text.GetBackLength( caret ); if( len == 0 ) return; caret -= len; if( caret < 0 ) caret = 0; for( int i = 0; i <= len; i++ ) Text.DeleteAt( caret ); if( caret >= Text.Length ) caret = -1; if( caret == -1 && Text.Length > 0 ) { Text.value[Text.Length] = ' '; } else if( caret >= 0 && Text.value[caret] != ' ' ) { Text.InsertAt( caret, ' ' ); } Recreate(); } else if( !Text.Empty && caret != 0 ) { int index = caret == -1 ? Text.Length - 1 : caret; if( CheckColour( index - 1 ) ) { DeleteChar(); // backspace XYZ%e to XYZ } else if( CheckColour( index - 2 ) ) { DeleteChar(); DeleteChar(); // backspace XYZ%eH to XYZ } DeleteChar(); Recreate(); } } bool CheckColour( int index ) { if( index < 0 ) return false; char code = Text.value[index], col = Text.value[index + 1]; return (code == '%' || code == '&') && game.Drawer2D.ValidColour( col ); } void DeleteKey() { if( !Text.Empty && caret != -1 ) { Text.DeleteAt( caret ); if( caret >= Text.Length ) caret = -1; Recreate(); } } void LeftKey( bool controlDown ) { if( controlDown ) { if( caret == -1 ) caret = Text.Length - 1; caret -= Text.GetBackLength( caret ); UpdateCaret(); return; } if( !Text.Empty ) { if( caret == -1 ) caret = Text.Length; caret--; if( caret < 0 ) caret = 0; UpdateCaret(); } } void RightKey( bool controlDown ) { if( controlDown ) { caret += Text.GetForwardLength( caret ); if( caret >= Text.Length ) caret = -1; UpdateCaret(); return; } if( !Text.Empty && caret != -1 ) { caret++; if( caret >= Text.Length ) caret = -1; UpdateCaret(); } } void HomeKey() { if( Text.Empty ) return; caret = 0; UpdateCaret(); } void EndKey() { caret = -1; UpdateCaret(); } static char[] trimChars = {'\r', '\n', '\v', '\f', ' ', '\t', '\0'}; bool OtherKey( Key key ) { int totalChars = MaxCharsPerLine * lines.Length; if( key == Key.V && Text.Length < totalChars ) { string text = null; try { text = game.window.ClipboardText.Trim( trimChars ); } catch( Exception ex ) { ErrorHandler.LogError( "Paste from clipboard", ex ); const string warning = "&cError while trying to paste from clipboard."; game.Chat.Add( warning, MessageType.ClientStatus4 ); return true; } if( String.IsNullOrEmpty( text ) ) return true; Append( text ); return true; } else if( key == Key.C ) { if( Text.Empty ) return true; try { game.window.ClipboardText = Text.ToString(); } catch( Exception ex ) { ErrorHandler.LogError( "Copy to clipboard", ex ); const string warning = "&cError while trying to copy to clipboard."; game.Chat.Add( warning, MessageType.ClientStatus4 ); } return true; } return false; } protected unsafe void SetCaretToCursor( int mouseX, int mouseY ) { mouseX -= inputTex.X1; mouseY -= inputTex.Y1; DrawTextArgs args = new DrawTextArgs( null, font, true ); IDrawer2D drawer = game.Drawer2D; int offset = 0, elemHeight = caretHeight; string oneChar = new String( 'A', 1 ); for( int y = 0; y < lines.Length; y++ ) { string line = lines[y]; int xOffset = y == 0 ? prefixWidth : 0; if( line == null ) continue; for( int x = 0; x < line.Length; x++ ) { args.Text = line.Substring( 0, x ); int trimmedWidth = drawer.MeasureChatSize( ref args ).Width + xOffset; // avoid allocating an unnecessary string fixed( char* ptr = oneChar ) ptr[0] = line[x]; args.Text = oneChar; int elemWidth = drawer.MeasureChatSize( ref args ).Width; if( GuiElement.Contains( trimmedWidth, y * elemHeight, elemWidth, elemHeight, mouseX, mouseY ) ) { caret = offset + x; UpdateCaret(); return; } } offset += line.Length; } caret = -1; UpdateCaret(); } #endregion } }