using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; using System.Threading; using Beefy.events; using Beefy.gfx; using Beefy.utils; using System.IO; //#define INCLUDE_CHARDATA_CHARID namespace Beefy.widgets { public struct EditSelection { public int32 mStartPos; public int32 mEndPos; public this(int startPos = 0, int endPos = 0) { mStartPos = (int32)startPos; mEndPos = (int32)endPos; } public EditSelection Clone() { EditSelection aSelection = EditSelection(); aSelection.mStartPos = mStartPos; aSelection.mEndPos = mEndPos; return aSelection; } public bool HasSelection { get { return (mStartPos != mEndPos); } } public bool IsForwardSelect { get { return (mStartPos < mEndPos); } } public int MinPos { get { return (IsForwardSelect) ? mStartPos : mEndPos; } } public int MaxPos { get { return (IsForwardSelect) ? mEndPos : mStartPos; } } public int Length { get { return Math.Abs(mEndPos - mStartPos); } } public void GetAsForwardSelect(out int startPos, out int endPos) { if (IsForwardSelect) { startPos = mStartPos; endPos = mEndPos; } else { startPos = mEndPos; endPos = mStartPos; } } public void MakeForwardSelect() mut { if (mEndPos < mStartPos) { int32 swap = mStartPos; mStartPos = mEndPos; mEndPos = swap; } } } public delegate void EditWidgetEventHandler(EditEvent theEvent); public class EditWidgetContent : Widget { public enum TextFlags { Wrap = 1 }; //[StructLayout(LayoutKind.Explicit) ] public struct CharData { public char8 mChar; public uint8 mDisplayPassId; public uint8 mDisplayTypeId; // For colorization, for example public uint8 mDisplayFlags; } public class TextAction : UndoAction { public EditWidgetContent.Data mEditWidgetContentData; public EditSelection? mSelection; public bool mRestoreSelectionOnUndo; public bool mMoveCursor = true; public int32 mPrevTextVersionId; public int32 mCursorTextPos; public LineAndColumn? mVirtualCursorPos; // Return either the focused edit widget content, or the last one added public EditWidgetContent EditWidgetContent { get { EditWidgetContent editWidgetContent = null; for (var user in mEditWidgetContentData.mUsers) { editWidgetContent = user; if (editWidgetContent.mEditWidget.mHasFocus) break; } return editWidgetContent; } } public this(EditWidgetContent editWidget) { mPrevTextVersionId = editWidget.mData.mCurTextVersionId; mEditWidgetContentData = editWidget.mData; if (editWidget.HasSelection()) mSelection = editWidget.mSelection; mCursorTextPos = (int32)editWidget.CursorTextPos; mVirtualCursorPos = editWidget.mVirtualCursorPos; } public void SetPreviousState(bool force) { var editWidgetContent = EditWidgetContent; mEditWidgetContentData.mCurTextVersionId = mPrevTextVersionId; if ((mRestoreSelectionOnUndo) || (force)) editWidgetContent.mSelection = mSelection; else editWidgetContent.mSelection = null; editWidgetContent.mCursorTextPos = mCursorTextPos; editWidgetContent.mVirtualCursorPos = mVirtualCursorPos; if (mMoveCursor) editWidgetContent.EnsureCursorVisible(); } } public class SetCursorAction : TextAction { public this(EditWidgetContent editWidget) : base(editWidget) { mRestoreSelectionOnUndo = true; mMoveCursor = false; } public override bool Undo() { SetPreviousState(false); return true; } public override bool Redo() { SetPreviousState(true); return true; } } public class TextUndoBatchStart : TextAction, IUndoBatchStart { public String mName; public UndoBatchEnd mBatchEnd; public String Name { get { return mName; } } public this(EditWidgetContent editWidget, String name) : base(editWidget) { mName = name; mBatchEnd = new UndoBatchEnd(); mBatchEnd.mBatchStart = this; } public override bool Undo() { SetPreviousState(false); return true; } public IUndoBatchEnd BatchEnd { get { return mBatchEnd; } } } public class DeleteSelectionAction : TextAction { public String mSelectionText; public this(EditWidgetContent editWidgetContent) : base(editWidgetContent) { if (mSelection != null) { mSelectionText = new String(); editWidgetContent.GetSelectionText(mSelectionText); } } ~this() { delete mSelectionText; } public override bool Undo() { var editWidgetContent = EditWidgetContent; if (mSelection != null) { bool wantSnapScroll = mEditWidgetContentData.mTextLength == 0; editWidgetContent.CursorTextPos = mSelection.Value.MinPos; editWidgetContent.PhysInsertAtCursor(mSelectionText, mMoveCursor); if (wantSnapScroll) editWidgetContent.mEditWidget.FinishScroll(); } SetPreviousState(false); return true; } public override bool Redo() { SetPreviousState(true); if (mSelection != null) EditWidgetContent.PhysDeleteSelection(mMoveCursor); return true; } } public class IndentTextAction : TextAction { // InsertCharList is for block indent, RemoveCharList is for unindent (shift-tab) public List> mRemoveCharList = new List>() ~ delete _; public List> mInsertCharList = new List>() ~ delete _; public EditSelection mNewSelection; public this(EditWidgetContent editWidget) : base(editWidget) { } public override bool Undo() { var editWidgetContent = EditWidgetContent; for (int idx = mRemoveCharList.Count - 1; idx >= 0; idx--) { var idxChar = mRemoveCharList[idx]; editWidgetContent.InsertText(idxChar.Item1, ToStackString!(idxChar.Item2)); } for (int idx = mInsertCharList.Count - 1; idx >= 0; idx--) { var idxChar = mInsertCharList[idx]; editWidgetContent.RemoveText(idxChar.Item1, 1); } editWidgetContent.ContentChanged(); SetPreviousState(false); return true; } public override bool Redo() { var editWidgetContent = EditWidgetContent; SetPreviousState(true); for (var idxChar in mRemoveCharList) editWidgetContent.RemoveText(idxChar.Item1, 1); var charStr = scope String(' ', 1); for (var idxChar in mInsertCharList) { charStr[0] = idxChar.Item2; editWidgetContent.InsertText(idxChar.Item1, charStr); } editWidgetContent.ContentChanged(); editWidgetContent.mSelection = mNewSelection; editWidgetContent.CursorTextPos = mNewSelection.mEndPos; return true; } } public class InsertTextAction : DeleteSelectionAction { public String mText ~ delete _; public this(EditWidgetContent editWidget, String text, InsertFlags insertFlags = .NoMoveCursor) : base(editWidget) { mText = new String(text); mRestoreSelectionOnUndo = !insertFlags.HasFlag(.NoRestoreSelectionOnUndo); } public override bool Merge(UndoAction nextAction) { InsertTextAction insertTextAction = nextAction as InsertTextAction; if (insertTextAction == null) return false; if (insertTextAction.mSelection != null) { if (mSelection == null) return false; if (mSelection.Value.mEndPos != insertTextAction.mSelection.Value.mStartPos) return false; mSelection.ValueRef.mEndPos = insertTextAction.mSelection.Value.mEndPos; mSelectionText.Append(insertTextAction.mSelectionText); } int curIdx = mCursorTextPos; int nextIdx = insertTextAction.mCursorTextPos; mRestoreSelectionOnUndo &= insertTextAction.mRestoreSelectionOnUndo; if ((nextIdx != curIdx + mText.Length) || (mText.EndsWith("\n")) || (insertTextAction.mText == "\n")) return false; mText.Append(insertTextAction.mText); return true; } public override bool Undo() { var editWidgetContent = EditWidgetContent; int startIdx = (mSelection != null) ? mSelection.Value.MinPos : mCursorTextPos; editWidgetContent.RemoveText(startIdx, (int32)mText.Length); editWidgetContent.ContentChanged(); base.Undo(); editWidgetContent.ContentChanged(); editWidgetContent.mData.mCurTextVersionId = mPrevTextVersionId; //mEditWidget.mVirtualCursorPos = mPrevVirtualCursorPos; return true; } public override bool Redo() { var editWidgetContent = EditWidgetContent; base.Redo(); bool wantSnapScroll = mEditWidgetContentData.mTextLength == 0; editWidgetContent.PhysInsertAtCursor(mText, mMoveCursor); if (wantSnapScroll) editWidgetContent.mEditWidget.FinishScroll(); return true; } } public class DeleteCharAction : TextAction { String mText ~ delete _; int32 mOffset; public this(EditWidgetContent editWidget, int offset, int count) : base(editWidget) { var editWidgetContent = EditWidgetContent; mOffset = (int32)offset; int32 textPos = mCursorTextPos; mText = new String(count); editWidgetContent.ExtractString(textPos + offset, count, mText); } public override bool Merge(UndoAction nextAction) { DeleteCharAction deleteCharAction = nextAction as DeleteCharAction; if (deleteCharAction == null) return false; if ((deleteCharAction.mOffset < 0) != (mOffset < 0)) return false; int32 curIdx = mCursorTextPos; int32 nextIdx = deleteCharAction.mCursorTextPos; if (nextIdx != curIdx + mOffset) return false; mOffset += deleteCharAction.mOffset; if (mOffset < 0) { //mText = deleteCharAction.mText + mText; mText.Insert(0, deleteCharAction.mText); } else { //mText = mText + deleteCharAction.mText; mText.Append(deleteCharAction.mText); } return true; } public override bool Undo() { int32 textPos = mCursorTextPos; var editWidgetContent = EditWidgetContent; editWidgetContent.CursorTextPos = textPos + mOffset; editWidgetContent.PhysInsertAtCursor(mText, mMoveCursor); editWidgetContent.ContentChanged(); SetPreviousState(false); return true; } public override bool Redo() { var editWidgetContent = EditWidgetContent; SetPreviousState(true); editWidgetContent.PhysDeleteChars(mOffset, (int32)mText.Length); editWidgetContent.ContentChanged(); return true; } } public struct LineAndColumn { public int32 mLine; public int32 mColumn; public this(int line, int column) { mLine = (int32)line; mColumn = (int32)column; } } public class Data { public CharData[] mText = new CharData[1] ~ delete _; public int32 mTextLength; public IdSpan mTextIdData = IdSpan.Empty ~ _.Dispose(); public uint8[] mTextFlags ~ delete _; public List mLineStarts ~ delete _; public UndoManager mUndoManager = new UndoManager() ~ delete _; public int32 mNextCharId = 1; // public int32 mCurTextVersionId = 1; // Changes when text is modified //public int mCurComplexChangeId = 1; // Changes when text is modified by more than a single-char8acter insertion or deletion public List mUsers = new List() ~ delete _; public ~this() { } public void Ref(EditWidgetContent content) { mUsers.Add(content); } public void Deref(EditWidgetContent content) { mUsers.Remove(content); if (mUsers.Count == 0) delete this; } public char8 SafeGetChar(int idx) { if ((idx < 0) || (idx >= mTextLength)) return 0; return mText[idx].mChar; } public char32 GetChar32(int idx) { int checkIdx = idx; char32 c = (char32)SafeGetChar(checkIdx++); if (c < (char32)0x80) return c; int8 trailingBytes = UTF8.sTrailingBytesForUTF8[c]; switch (trailingBytes) { case 3: c <<= 6; c += (int)SafeGetChar(checkIdx++); fallthrough; case 2: c <<= 6; c += (int)SafeGetChar(checkIdx++); fallthrough; case 1: c <<= 6; c += (int)SafeGetChar(checkIdx++); fallthrough; } return c - (int32)UTF8.sOffsetsFromUTF8[trailingBytes]; } public char32 GetChar32(ref int idx) { char32 c = (char32)SafeGetChar(idx++); if (c < (char32)0x80) return c; int8 trailingBytes = UTF8.sTrailingBytesForUTF8[c]; switch (trailingBytes) { case 3: c <<= 6; c += (int)SafeGetChar(idx++); fallthrough; case 2: c <<= 6; c += (int)SafeGetChar(idx++); fallthrough; case 1: c <<= 6; c += (int)SafeGetChar(idx++); fallthrough; } return c - (int32)UTF8.sOffsetsFromUTF8[trailingBytes]; } } public Data mData ~ _.Deref(this); public Insets mTextInsets = new Insets() ~ delete _; public float mHorzJumpSize = 1; public bool mAutoHorzScroll = true; public float mShowLineBottomPadding; public bool mContentChanged; public EditWidget mEditWidget; public float mTabSize; public float mCharWidth = -1; public int32 mTabLength = 4; public uint8 mExtendDisplayFlags; public uint8 mInsertDisplayFlags; public int32 mCursorBlinkTicks; public bool mCursorImplicitlyMoved; public bool mJustInsertedCharPair; // Pressing backspace will delete last char8, even though cursor is between char8 pairs (ie: for brace pairs 'speculatively' inserted) public EditSelection? mSelection; public EditSelection? mDragSelectionUnion; // For double-clicking a word and then "dragging" the selection public bool mIsReadOnly = false; public bool mWantsUndo = true; public bool mIsMultiline = false; public bool mWordWrap = false; public float mCursorWantX; // For keyboard cursor selection, accounting for when we truncate to line end public bool mOverTypeMode = false; public int32 mCursorTextPos; public bool mShowCursorAtLineEnd; public LineAndColumn? mVirtualCursorPos; public bool mEnsureCursorVisibleOnModify = true; public bool mAllowVirtualCursor; public bool mAllowMaximalScroll = true; // Allows us to scroll down such that edit widget is blank except for one line of content at the top public float mMaximalScrollAddedHeight; // 0 is mAllowMaximalScroll is false public int CursorTextPos { get { if (mCursorTextPos == -1) { float x; float y; GetTextCoordAtLineAndColumn(mVirtualCursorPos.Value.mLine, mVirtualCursorPos.Value.mColumn, out x, out y); int line; int lineChar; float overflowX; GetLineCharAtCoord(x, y, out line, out lineChar, out overflowX); mCursorTextPos = (int32)GetTextIdx(line, lineChar); } return mCursorTextPos; } set { Debug.Assert(value >= 0); mVirtualCursorPos = null; mCursorTextPos = (int32)value; } } public LineAndColumn CursorLineAndColumn { get { if (mVirtualCursorPos.HasValue) return mVirtualCursorPos.Value; LineAndColumn lineAndColumn; int line; int lineChar; GetLineCharAtIdx(mCursorTextPos, out line, out lineChar); float x; float y; GetTextCoordAtLineChar(line, lineChar, out x, out y); int coordLine; int coordLineColumn; GetLineAndColumnAtCoord(x, y, out coordLine, out coordLineColumn); lineAndColumn.mLine = (int32)coordLine; lineAndColumn.mColumn = (int32)coordLineColumn; return lineAndColumn; } set { mVirtualCursorPos = value; mCursorTextPos = -1; Debug.Assert(mAllowVirtualCursor); Debug.Assert(mVirtualCursorPos.Value.mColumn >= 0); } } public bool WantsUndo { get { return (!mIsReadOnly) && (mWantsUndo); } } public this(EditWidgetContent refContent = null) { if (refContent != null) mData = refContent.mData; else mData = CreateEditData(); mData.Ref(this); mContentChanged = true; } protected virtual Data CreateEditData() { return new Data(); } public uint8[] mTempCopyArr; public virtual bool CheckReadOnly() { return mIsReadOnly; } public bool TryGetCursorTextPos(out int textPos, out float overflowX) { if (mVirtualCursorPos.HasValue) { float x; float y; GetTextCoordAtLineAndColumn(mVirtualCursorPos.Value.mLine, mVirtualCursorPos.Value.mColumn, out x, out y); int line; int lineChar; bool success = GetLineCharAtCoord(x, y, out line, out lineChar, out overflowX); textPos = GetTextIdx(line, lineChar); return success; } textPos = mCursorTextPos; overflowX = 0; return true; } public bool TryGetCursorTextPos(out int textPos) { float overflowX; return TryGetCursorTextPos(out textPos, out overflowX); } public (char32, int) GetChar32(int idx) { char32 c = mData.mText[idx].mChar; if (c < '\x80') return (c, 1); EditWidgetContent.CharData* readPtr = &mData.mText[idx + 1]; int trailingBytes = UTF8.sTrailingBytesForUTF8[c]; switch (trailingBytes) { case 4: c <<= 6; c += (uint8)(readPtr++).mChar; fallthrough; case 3: c <<= 6; c += (uint8)(readPtr++).mChar; fallthrough; case 2: c <<= 6; c += (uint8)(readPtr++).mChar; fallthrough; case 1: c <<= 6; c += (uint8)(readPtr++).mChar; fallthrough; } c -= (int32)UTF8.sOffsetsFromUTF8[trailingBytes]; return (c, trailingBytes + 1); } public void ExtractString(int startIdx, int length, String outStr) { Debug.Assert(length >= 0); char8* ptr = outStr.PrepareBuffer(length); CharData* char8DataPtr = mData.mText.CArray(); for (int32 idx = 0; idx < length; idx++) ptr[idx] = char8DataPtr[idx + startIdx].mChar; } public void ExtractLine(int line, String outStr) { GetLinePosition(line, var lineStart, var lineEnd); ExtractString(lineStart, lineEnd - lineStart, outStr); } public char8 SafeGetChar(int idx) { if ((idx < 0) || (idx >= mData.mTextLength)) return 0; return mData.mText[idx].mChar; } static bool IsNonBreakingChar(char8 c) { // Assume any 'high' codepoints are non-breaking return (c.IsLetterOrDigit) || (c == '_') || (c >= '\x80'); } public override void MouseDown(float x, float y, int32 btn, int32 btnCount) { base.MouseDown(x, y, btn, btnCount); mEditWidget.SetFocus(); if ((mSelection == null) && (mWidgetWindow.IsKeyDown(KeyCode.Shift))) StartSelection(); MoveCursorToCoord(x, y); //PrintF("~TestStruct() %d\n", mInner.mVal1); if ((btnCount > 1) && (!mWidgetWindow.IsKeyDown(KeyCode.Shift))) { // Select word StartSelection(); int cursorTextPos = CursorTextPos; int textPos = cursorTextPos; if (mData.mTextLength == 0) { //Nothing } else if ((textPos < mData.mTextLength) && (!IsNonBreakingChar((char8)mData.mText[textPos].mChar))) { mSelection.ValueRef.mEndPos++; } else { while ((textPos > 0) && (IsNonBreakingChar((char8)mData.mText[textPos - 1].mChar))) { mSelection.ValueRef.mStartPos--; textPos--; } textPos = cursorTextPos + 1; while ((textPos <= mData.mTextLength) && ((textPos == mData.mTextLength) || (mData.mText[textPos - 1].mChar != '\n')) && (IsNonBreakingChar((char8)mData.mText[textPos - 1].mChar))) { mSelection.ValueRef.mEndPos++; textPos++; } } mDragSelectionUnion = mSelection; CursorTextPos = mSelection.Value.MaxPos; } else if (!mWidgetWindow.IsKeyDown(KeyCode.Shift)) { if ((btn != 0) && (mSelection != null) && (CursorTextPos >= mSelection.Value.MinPos) && (CursorTextPos <= mSelection.Value.MaxPos)) { // Leave selection } else StartSelection(); } else SelectToCursor(); } public override void MouseUp(float x, float y, int32 btn) { base.MouseUp(x, y, btn); if (mMouseFlags == 0) { mDragSelectionUnion = null; } } public override void MouseMove(float x, float y) { base.MouseMove(x, y); if ((mMouseDown) && (mMouseFlags == .Left)) { MoveCursorToCoord(x, y); SelectToCursor(); } } public override void MouseEnter() { base.MouseEnter(); BFApp.sApp.SetCursor(Cursor.Text); } public override void MouseLeave() { base.MouseLeave(); BFApp.sApp.SetCursor(Cursor.Pointer); } public virtual void ClearText() { SelectAll(); DeleteSelection(); } public void PhysDeleteSelection(bool moveCursor = true) { int startIdx = 0; int endIdx = 0; mSelection.Value.GetAsForwardSelect(out startIdx, out endIdx); RemoveText(startIdx, endIdx - startIdx); mSelection = null; CursorTextPos = startIdx; if (endIdx != startIdx) ContentChanged(); if (moveCursor) EnsureCursorVisible(); } public virtual void DeleteSelection(bool moveCursor = true) { if (mSelection != null) { var action = new DeleteSelectionAction(this); action.mMoveCursor = moveCursor; mData.mUndoManager.Add(action); PhysDeleteSelection(); } } public bool GetSelectionText(String outStr) { if (mSelection == null) return false; int startIdx = 0; int endIdx = 0; mSelection.Value.GetAsForwardSelect(out startIdx, out endIdx); ExtractString(startIdx, endIdx - startIdx, outStr); return true; } void VerifyTextIds() { #if INCLUDE_CHARDATA_CHARID /*if (mUpdateCnt > 0) Debug.WriteLine("IdLen: {0}", mTextIdDataLength);*/ int curTextIdx = 0; int encodeIdx = 0; int curTextId = 1; while (true) { int cmd = Utils.DecodeInt(mTextIdData, ref encodeIdx); ; if (cmd == 0) { Debug.Assert(encodeIdx == mTextIdDataLength); break; } if (cmd > 0) { curTextId = cmd; } else { for (int i = 0; i < -cmd; i++) { Debug.Assert(mText[curTextIdx].mCharId == curTextId); curTextIdx++; curTextId++; } } } Debug.Assert(curTextIdx == mTextLength); #endif } protected virtual void AdjustCursorsAfterExternalEdit(int index, int ofs) { #unwarn int cursorPos = CursorTextPos; if (mCursorTextPos >= index) mCursorTextPos += (int32)ofs; if (HasSelection()) { if (((ofs > 0) && (mSelection.mValue.mStartPos >= index)) || ((ofs < 0) && (mSelection.mValue.mStartPos > index))) mSelection.mValue.mStartPos += (int32)ofs; if (mSelection.mValue.mEndPos > index) mSelection.mValue.mEndPos += (int32)ofs; } } public virtual void ApplyTextFlags(int index, String text, uint8 typeNum, uint8 flags) { uint8 curTypeNum = typeNum; for (int i = 0; i < text.Length; i++) { char8 c = text[i]; mData.mText[i + index].mChar = (char8)c; #if INCLUDE_CHARDATA_CHARID mText[i + index].mCharId = mNextCharId; #endif mData.mNextCharId++; mData.mText[i + index].mDisplayTypeId = curTypeNum; mData.mText[i + index].mDisplayFlags = flags; if (c.IsWhiteSpace) curTypeNum = 0; } } public virtual void GetInsertFlags(int index, ref uint8 typeId, ref uint8 flags) { // Leave it } public virtual void InsertText(int index, String text) { scope AutoBeefPerf("EWC.InsertText"); int wantLen = mData.mTextLength + text.Length + 1; while (wantLen > mData.mText.Count) { scope AutoBeefPerf("EWC.InsertText:Resize"); CharData[] oldText = mData.mText; int allocSize = Math.Max(mData.mText.Count * 3 / 2, wantLen); mData.mText = new CharData[allocSize]; oldText.CopyTo(mData.mText, 0, 0, mData.mTextLength); delete oldText; } mData.mText.CopyTo(mData.mText, index, index + text.Length, mData.mTextLength - index); int32 curId = mData.mNextCharId; mData.mTextIdData.Insert(index, (int32)text.Length, ref curId); // We break attribute copying on spaces and revert back to zero uint8 curTypeNum = 0; uint8 curFlags = mInsertDisplayFlags; GetInsertFlags(index, ref curTypeNum, ref curFlags); ApplyTextFlags(index, text, curTypeNum, curFlags); mData.mTextLength += (int32)text.Length; mData.mCurTextVersionId++; TextChanged(); VerifyTextIds(); for (var user in mData.mUsers) { if (user != this) { user.AdjustCursorsAfterExternalEdit(index, (int32)text.Length); user.ContentChanged(); } } } public virtual void RemoveText(int index, int length) { for (var user in mData.mUsers) { if (user != this) { user.AdjustCursorsAfterExternalEdit(index, -length); user.ContentChanged(); } } if (length == 0) return; mData.mText.CopyTo(mData.mText, index + length, index, mData.mTextLength - (index + length)); mData.mTextIdData.Remove(index, length); mData.mTextLength -= (int32)length; mData.mCurTextVersionId++; TextChanged(); VerifyTextIds(); } /*public void PhysInsertAtCursor(String theString, bool moveCursor = true) { int textPos = CursorTextPos; bool atEnd = textPos == mData.mTextLength; String str = theString; int strStartIdx = 0; while (true) { int crPos = str.IndexOf('\n', strStartIdx); if (crPos == -1) { if (strStartIdx != 0) str = stack String(theString, strStartIdx); break; } if (mIsMultiline) { //string lineString = aString.Substring(strStartIdx, crPos - strStartIdx + 1); String lineString = scope String(); str.Substring(strStartIdx, crPos - strStartIdx + 1, lineString); strStartIdx = crPos + 1; InsertText((int32)textPos, lineString); textPos += lineString.Length; } else { // Only allow the first line in //aString = aString.Substring(0, crPos); //aString.RemoveToEnd(crPos); str = stack String(theString, 0, crPos); } } InsertText((int32)textPos, str); //if (atEnd) //ContentAppended(str); //else ContentChanged(); if (moveCursor) { textPos += str.Length; MoveCursorToIdx((int32)textPos); if (mEnsureCursorVisibleOnModify) EnsureCursorVisible(); } }*/ public void PhysInsertAtCursor(String text, bool moveCursor = true) { int textPos = CursorTextPos; bool atEnd = textPos == mData.mTextLength; String insertStr = text; if (!mIsMultiline) { int crPos = insertStr.IndexOf('\n'); if (crPos != -1) insertStr = scope:: String(text, 0, crPos); } InsertText((int32)textPos, insertStr); if (atEnd) TextAppended(insertStr); else ContentChanged(); if (moveCursor) { textPos += insertStr.Length; MoveCursorToIdx((int32)textPos); if (mEnsureCursorVisibleOnModify) EnsureCursorVisible(); } } public void AppendText(String text) { scope AutoBeefPerf("EWC.AppendText"); if (mAllowVirtualCursor) { let prevCursorPos = CursorLineAndColumn; CursorTextPos = mData.mTextLength; PhysInsertAtCursor(text); CursorLineAndColumn = prevCursorPos; } else { int prevCursorPos = CursorTextPos; CursorTextPos = mData.mTextLength; PhysInsertAtCursor(text); CursorTextPos = prevCursorPos; } } public enum InsertFlags { None, NoRestoreSelectionOnUndo = 1, NoMoveCursor = 2, IsGroupPart = 4, IsGroupStart = 8 } public virtual void InsertAtCursor(String theString, InsertFlags insertFlags = .None) { scope AutoBeefPerf("EWC.InsertAtCursor"); //String insertText = scope String(); String insertString = theString; bool doCheck = true; // { scope AutoBeefPerf("FilterCheck"); if (theString.Length > 32*1024) { bool[128] hasChar = default; doCheck = false; for (char32 c in theString.DecodedChars) { if (c <= (char32)128) hasChar[(int)c] = true; else if (!AllowChar(c)) { doCheck = true; break; } } if (!doCheck) { for (int i < 128) { if ((hasChar[i]) && (!AllowChar((char32)i))) { doCheck = true; break; } } } } if (doCheck) { //TODO: Make this use DecodedChars for (int32 i = 0; i < insertString.Length; i++) { if (!AllowChar(insertString[i])) { if (insertString == (Object)theString) { insertString = scope:: String(); insertString.Set(theString); } insertString.Remove(i, 1); i--; } } } } Debug.Assert(!theString.StartsWith("\x06")); if ((!HasSelection()) && (mVirtualCursorPos.HasValue) && (theString != "\n")) { int textPos; TryGetCursorTextPos(out textPos); int line; int lineChar; GetLineCharAtIdx(textPos, out line, out lineChar); float textX; float textY; GetTextCoordAtLineChar(line, lineChar, out textX, out textY); float cursorX; float cursorY; GetTextCoordAtLineAndColumn(mVirtualCursorPos.Value.mLine, mVirtualCursorPos.Value.mColumn, out cursorX, out cursorY); if (cursorX > textX) { int lineStart; int lineEnd; GetLinePosition(line, out lineStart, out lineEnd); if (textPos == lineEnd) { int prevTabCount = (int)((textX + 0.5f) / mTabSize); int wantTabCount = (int)((cursorX + 0.5f) / mTabSize); int wantAddTabs = wantTabCount - prevTabCount; if (wantAddTabs > 0) textX = wantTabCount * mTabSize + mTextInsets.mLeft; int wantSpaces = (int)((cursorX - textX + 0.5f) / mCharWidth); //theString = new string(' ', wantSpaces) + theString; let prevString = insertString; insertString = scope:: String(); insertString.Append('\t', wantAddTabs); insertString.Append(' ', wantSpaces); insertString.Append(prevString); } } //if (wantAddTabs > 0) //theString = new string('\t', wantAddTabs) + theString; } if ((insertString.Length == 0) && (!HasSelection())) return; if (WantsUndo) { var action = new InsertTextAction(this, insertString, insertFlags); action.mMoveCursor = !insertFlags.HasFlag(.NoMoveCursor); mData.mUndoManager.Add(action); } if (HasSelection()) PhysDeleteSelection(!insertFlags.HasFlag(.NoMoveCursor)); //StringBuilder insertText = new StringBuilder(theString); /*for (int32 i = 0; i < insertText.Length; i++) { if (!AllowChar(insertText[i])) { insertText.Remove(i, 1); i--; } }*/ /*int curIdx = 0; for (var c in insertText.DecodedChars) { if (!AllowChar(c)) { insertText.Remove(curIdx, @c.NextIndex - curIdx); @c.NextIndex = curIdx; continue; } curIdx = @c.NextIndex; }*/ //theString = insertText.ToString(); PhysInsertAtCursor(insertString, !insertFlags.HasFlag(.NoMoveCursor)); } public virtual void ReplaceAtCursor(char8 c) { } public virtual void ClampCursor() { if (mVirtualCursorPos.HasValue) { var cursorPos = mVirtualCursorPos.Value; cursorPos.mLine = Math.Min(GetLineCount() - 1, cursorPos.mLine); mVirtualCursorPos = cursorPos; } mCursorTextPos = Math.Min(mCursorTextPos, mData.mTextLength); } public virtual void ContentChanged() { //TODO: //mCursorPos = GetTextIdx(mCursorLine, mCursorCharIdx); MarkDirty(); mContentChanged = true; delete mData.mTextFlags; mData.mTextFlags = null; delete mData.mLineStarts; mData.mLineStarts = null; mData.mCurTextVersionId++; mDragSelectionUnion = null; mEditWidget.ContentChanged(); TextChanged(); } public virtual void TextAppended(String str) { if ((mWordWrap) || (mData.mLineStarts == null)) { ContentChanged(); return; } scope AutoBeefPerf("EWC.TextAppended"); int textIdx = mData.mTextLength - str.Length; mData.mLineStarts.PopBack(); for (char8 c in str.RawChars) { textIdx++; if (c == '\n') { mData.mLineStarts.Add((int32)textIdx); } } mData.mLineStarts.Add(mData.mTextLength); mContentChanged = true; mData.mCurTextVersionId++; mDragSelectionUnion = null; //Debugging //ContentChanged(); //GetTextData(); mEditWidget.ContentChanged(); } //Thread mCheckThread; public virtual void GetTextData() { // Generate line starts and text flags if we need to if (mData.mLineStarts == null) { scope AutoBeefPerf("EWC.GetTextData"); CharData* char8DataPtr = mData.mText.CArray(); uint8* textFlagsPtr = null; if (mData.mTextFlags != null) textFlagsPtr = mData.mTextFlags.CArray(); int32 lineIdx = 0; if (textFlagsPtr != null) { for (int32 i < mData.mTextLength) { if ((char8DataPtr[i].mChar == '\n') || ((textFlagsPtr != null) && ((textFlagsPtr[i] & ((int32)TextFlags.Wrap)) != 0))) lineIdx++; } } else { for (int32 i < mData.mTextLength) { if (char8DataPtr[i].mChar == '\n') lineIdx++; } } mData.mLineStarts = new List(); mData.mLineStarts.GrowUnitialized(lineIdx + 2); int32* lineStartsPtr = mData.mLineStarts.Ptr; lineStartsPtr[0] = 0; lineIdx = 0; if (textFlagsPtr != null) { for (int32 i < mData.mTextLength) { if ((textFlagsPtr != null) && ((textFlagsPtr[i] & ((int32)TextFlags.Wrap)) != 0)) { lineIdx++; lineStartsPtr[lineIdx] = i; } else if ((char8)char8DataPtr[i].mChar == '\n') { lineIdx++; lineStartsPtr[lineIdx] = i + 1; } } } else { for (int32 i < mData.mTextLength) { if ((char8)char8DataPtr[i].mChar == '\n') { lineIdx++; lineStartsPtr[lineIdx] = i + 1; } } } mData.mLineStarts[lineIdx + 1] = mData.mTextLength; } } public virtual void Backspace() { if (mJustInsertedCharPair) { CursorTextPos++; mJustInsertedCharPair = false; } if (HasSelection()) { DeleteSelection(); return; } mSelection = null; int textPos = 0; if (!TryGetCursorTextPos(out textPos)) { var lineAndColumn = CursorLineAndColumn; mCursorBlinkTicks = 0; CursorLineAndColumn = LineAndColumn(lineAndColumn.mLine, lineAndColumn.mColumn - 1); CursorMoved(); return; } if (textPos == 0) return; int32 removeNum = -1; if (mData.mText[CursorTextPos - 1].mChar == '\n') { // Pulling up line - delete trailing whitespace if the line has no content int lineIdx = 0; int lineCharIdx = 0; GetLineCharAtIdx(CursorTextPos, out lineIdx, out lineCharIdx); String lineText = scope String(); GetLineText(lineIdx, lineText); String trimmedLineText = scope String(lineText); trimmedLineText.Trim(); if ((lineText.Length > 0) && (trimmedLineText.Length == 0)) { CursorTextPos += (int32)lineText.Length; removeNum = (int32)-lineText.Length - 1; } } // Roll back past UTF8 data if necessary while (true) { char8 c = mData.SafeGetChar(mCursorTextPos + removeNum); if ((uint8)c & 0xC0 != 0x80) break; removeNum--; } mData.mUndoManager.Add(new DeleteCharAction(this, removeNum, -removeNum)); PhysDeleteChars(removeNum, -removeNum); } public virtual void PhysDeleteChars(int32 offset, int32 length = 1) { int textPos = CursorTextPos; RemoveText(textPos + offset, length); textPos += offset; ContentChanged(); if (offset != 0) { MoveCursorToIdx(textPos); EnsureCursorVisible(); } } public virtual void ClearLine() { int line; int lineChar; GetCursorLineChar(out line, out lineChar); String lineText = scope String(); GetLineText(line, lineText); var prevCursorLineAndCol = CursorLineAndColumn; mSelection = EditSelection(); mSelection.ValueRef.mStartPos = (int32)(CursorTextPos - lineChar); mSelection.ValueRef.mEndPos = mSelection.Value.mStartPos + (int32)lineText.Length; DeleteSelection(); CursorLineAndColumn = prevCursorLineAndCol; } public virtual void DeleteLine() { int line; int lineChar; GetCursorLineChar(out line, out lineChar); String lineText = scope String(); GetLineText(line, lineText); var prevCursorLineAndCol = CursorLineAndColumn; mSelection = EditSelection(); mSelection.ValueRef.mStartPos = (int32)(CursorTextPos - lineChar); mSelection.ValueRef.mEndPos = mSelection.ValueRef.mStartPos + (int32)lineText.Length + 1; DeleteSelection(); CursorLineAndColumn = prevCursorLineAndCol; } public virtual void DeleteChar() { if (HasSelection()) { DeleteSelection(); return; } mSelection = null; int textPos = CursorTextPos; if (textPos >= mData.mTextLength) return; int line; int lineChar; GetLineCharAtIdx(textPos, out line, out lineChar); bool wasLineEnd = mData.mText[textPos].mChar == '\n'; String lineText = scope String(); GetLineText(line, lineText); if (wasLineEnd) { if (lineText.IsWhiteSpace) { DeleteLine(); GetLineCharAtIdx(CursorTextPos, out line, out lineChar); lineText.Clear(); GetLineText(line, lineText); int32 contentIdx; for (contentIdx = 0; contentIdx < lineText.Length; contentIdx++) { if (!lineText[contentIdx].IsWhiteSpace) { CursorTextPos = GetTextIdx(line, contentIdx); break; } } return; } UndoBatchStart undoBatchStart = new UndoBatchStart("PullUpLine"); mData.mUndoManager.Add(undoBatchStart); // Add in any required virtual spaces InsertAtCursor(""); textPos = CursorTextPos; while ((textPos < mData.mTextLength) && (lineChar != 0)) { mData.mUndoManager.Add(new DeleteCharAction(this, 0, 1)); PhysDeleteChars(0); char8 c = (char8)mData.mText[textPos].mChar; if ((c == '\n') || (!c.IsWhiteSpace)) break; } mData.mUndoManager.Add(undoBatchStart.mBatchEnd); } else { int32 char8Count = 1; int checkIdx = textPos + 1; while (true) { char8 c = mData.SafeGetChar(checkIdx); if (((uint8)c & 0xC0) != 0x80) { // Delete all the combining marks after the char8 var checkChar = mData.GetChar32(checkIdx); if (!checkChar.IsCombiningMark) break; } char8Count++; checkIdx++; } mData.mUndoManager.Add(new DeleteCharAction(this, 0, char8Count)); PhysDeleteChars(0, char8Count); } } public virtual bool AllowChar(char32 c) { if ((c == '\t') && (!mIsMultiline)) return false; return true; } public void InsertCharPair(String char8Pair) { InsertAtCursor(char8Pair); MoveCursorToIdx(CursorTextPos - 1); mJustInsertedCharPair = true; } public virtual bool WantsInsertCharPair(char8 theChar) { return false; } public virtual int32 GetTabSpaceCount() { return (int32)Math.Round(mTabSize / mCharWidth); } public override void KeyChar(char32 theChar) { base.KeyChar(theChar); char32 useChar = theChar; mCursorBlinkTicks = 0; if (useChar == '\b') { if (!CheckReadOnly()) { if (HasSelection()) { DeleteSelection(); return; } int32 column = CursorLineAndColumn.mColumn; int32 tabSpaceCount = GetTabSpaceCount(); if ((mAllowVirtualCursor) && (column != 0) && ((column % tabSpaceCount) == 0)) { int lineStart; int lineEnd; GetLinePosition(CursorLineAndColumn.mLine, out lineStart, out lineEnd); bool doBlockUnindent = false; int cursorTextPos = 0; float overflowX = 0; if (!TryGetCursorTextPos(out cursorTextPos, out overflowX)) { cursorTextPos += (int32)Math.Round(overflowX / mCharWidth); for (int32 i = 0; i < tabSpaceCount; i++) { if (cursorTextPos - 1 < lineEnd) { if (!((char8)mData.mText[cursorTextPos - 1].mChar).IsWhiteSpace) break; Backspace(); } else { var virtualCursorPos = CursorLineAndColumn; virtualCursorPos.mColumn--; CursorLineAndColumn = virtualCursorPos; } cursorTextPos--; } return; } else if (cursorTextPos > 0) { Debug.Assert((cursorTextPos >= lineStart) && (cursorTextPos <= lineEnd)); if (((char8)mData.mText[cursorTextPos - 1].mChar).IsWhiteSpace) { doBlockUnindent = true; // Cursor has to be at line end or only have whitespace infront of it (line start) if (cursorTextPos < lineEnd) { for (int i = lineStart; i < cursorTextPos; i++) { if (!((char8)mData.mText[i].mChar).IsWhiteSpace) { doBlockUnindent = false; break; } } } } } if (doBlockUnindent) { BlockIndentSelection(true); return; } } Backspace(); } return; } mJustInsertedCharPair = false; switch (useChar) { case '\r': useChar = '\n'; break; case '\t': if (AllowChar(useChar)) BlockIndentSelection(mWidgetWindow.IsKeyDown(KeyCode.Shift)); return; } if (useChar == '\n') { if ((!mIsMultiline) || (mWidgetWindow.IsKeyDown(KeyCode.Control))) { // Submit is handled in KeyDown //mEditWidget.Submit(); return; } } if ((AllowChar(useChar)) && (!CheckReadOnly())) { if ((useChar == '\n') && (!mAllowVirtualCursor)) { int lineIdx; int lineCharIdx; GetLineCharAtIdx(CursorTextPos, out lineIdx, out lineCharIdx); // Duplicate the tab start from the previous line if (lineIdx > 0) { int32 indentCount = 0; String aLine = scope String(); GetLineText(lineIdx, aLine); for (int32 i = 0; i < Math.Min(aLine.Length, lineCharIdx); i++) { if (aLine[i] != '\t') break; indentCount++; } aLine.Trim(); UndoBatchStart undoBatchStart = new UndoBatchStart("newline"); mData.mUndoManager.Add(undoBatchStart); if (aLine.Length == 0) { // Remove previous line's tabs (since that line was blank) for (int32 i = 0; i < indentCount; i++) Backspace(); } InsertAtCursor(ToStackString!(useChar)); for (int32 i = 0; i < indentCount; i++) InsertAtCursor("\t"); if (aLine.Length > 0) { char8 c = aLine[aLine.Length - 1]; if ((c == '(') || (c == '[') || (c == '{')) InsertAtCursor("\t"); } if (WantsUndo) mData.mUndoManager.Add(undoBatchStart.mBatchEnd); return; } } bool restoreSelectionOnUndo = true; if ((mOverTypeMode) && (!HasSelection())) { int cursorPos = CursorTextPos; if (cursorPos < mData.mTextLength) { char8 c = (char8)mData.mText[cursorPos].mChar; if ((c != '\n') && (c != '\r')) { mSelection = EditSelection(cursorPos, cursorPos + 1); restoreSelectionOnUndo = false; } } } if ((useChar >= '\x20') || (useChar == '\n')) { InsertAtCursor(ToStackString!(useChar), restoreSelectionOnUndo ? .None : .NoRestoreSelectionOnUndo); } } mCursorImplicitlyMoved = true; } public virtual float GetPageScrollTextHeight() { return mEditWidget.mScrollContentContainer.mHeight; } public virtual float AdjustPageScrollPos(float newY, int dir) { return Math.Max(0, Math.Min(newY, mHeight - mEditWidget.mScrollContentContainer.mHeight - mMaximalScrollAddedHeight)); } int32 GetCharType(char8 theChar) { if (theChar == '\n') return 2; if (theChar.IsWhiteSpace) return 0; if (IsNonBreakingChar(theChar)) return 1; // We break on each instance of these switch (theChar) { case '<', '>', '(', ')', '[', ']', '{', '}': return 3; } return 4; } public void GetTextCoordAtCursor(out float x, out float y) { if (mVirtualCursorPos.HasValue) { GetTextCoordAtLineAndColumn(mVirtualCursorPos.Value.mLine, mVirtualCursorPos.Value.mColumn, out x, out y); } else { int line; int lineChar; GetLineCharAtIdx(mCursorTextPos, out line, out lineChar); GetTextCoordAtLineChar(line, lineChar, out x, out y); } } public bool GetCursorLineChar(out int line, out int lineChar) { if (mCursorTextPos != -1) { GetLineCharAtIdx_Fast(mCursorTextPos, true, out line, out lineChar); return true; } line = mVirtualCursorPos.Value.mLine; float x; float y; GetTextCoordAtLineAndColumn(mVirtualCursorPos.Value.mLine, mVirtualCursorPos.Value.mColumn, out x, out y); float overflowX; return GetLineCharAtCoord(x, y, out line, out lineChar, out overflowX); } public virtual bool PrepareForCursorMove(int dir = 0) { if (mWidgetWindow.IsKeyDown(KeyCode.Shift)) return false; if ((mSelection == null) || (!mSelection.Value.HasSelection)) return false; if (dir < 0) CursorTextPos = mSelection.Value.MinPos; else if (dir > 0) CursorTextPos = mSelection.Value.MaxPos; mSelection = null; return true; } public virtual void Undo() { scope AutoBeefPerf("EWC.Undo"); //Profiler.StartSampling(); if (WantsUndo) mData.mUndoManager.Undo(); //Profiler.StopSampling(); } public virtual void Redo() { scope AutoBeefPerf("EWC.Redo"); if (WantsUndo) mData.mUndoManager.Redo(); } public virtual void PasteText(String text) { InsertAtCursor(text); } public override void KeyDown(KeyCode keyCode, bool isRepeat) { base.KeyDown(keyCode, isRepeat); if (keyCode == KeyCode.Escape) { mEditWidget.Cancel(); if (mWidgetWindow == null) return; } int lineIdx; int lineChar; GetCursorLineChar(out lineIdx, out lineChar); bool wasMoveKey = false; int prevCursorPos; bool gotCursorPos = TryGetCursorTextPos(out prevCursorPos); if (mWidgetWindow.GetKeyFlags() == KeyFlags.Ctrl) { switch (keyCode) { case (KeyCode)'A': SelectAll(); break; case (KeyCode)'C': String selText = scope String(); GetSelectionText(selText); if (!selText.IsWhiteSpace) BFApp.sApp.SetClipboardText(selText); break; case (KeyCode)'X': if (!CheckReadOnly()) { String selText = scope String(); GetSelectionText(selText); if (!selText.IsWhiteSpace) { BFApp.sApp.SetClipboardText(selText); DeleteSelection(); } } break; case (KeyCode)'V': String aText = scope String(); BFApp.sApp.GetClipboardText(aText); aText.Replace("\r", ""); if ((aText != null) && (!CheckReadOnly())) PasteText(aText); break; case (KeyCode)'Z': Undo(); break; case (KeyCode)'Y': Redo(); break; case .Return: if (mIsMultiline) mEditWidget.Submit(); default: } } switch (keyCode) { case .Return: if (!mIsMultiline) mEditWidget.Submit(); case KeyCode.Left: { if (!PrepareForCursorMove(-1)) { PrepareForCursorMove(-1); if (mAllowVirtualCursor) { bool doVirtualMove = true; if ((mWidgetWindow.IsKeyDown(KeyCode.Shift)) || (mWidgetWindow.IsKeyDown(KeyCode.Control))) doVirtualMove = false; else { int lineStart; int lineEnd; GetLinePosition(lineIdx, out lineStart, out lineEnd); //if (gotCursorPos) if (gotCursorPos) doVirtualMove = false; } if (doVirtualMove) { var lineAndColumn = CursorLineAndColumn; if (lineAndColumn.mColumn > 0) { /*mSelection = null; mCursorBlinkTicks = 0; CursorLineAndColumn = LineAndColumn(lineAndColumn.mLine, lineAndColumn.mColumn - 1); EnsureCursorVisible(true, false, false); CursorMoved();*/ float x; float y; GetTextCoordAtLineAndColumn(lineAndColumn.mLine, lineAndColumn.mColumn - 1, out x, out y); MoveCursorToCoord(x, y); break; } } } wasMoveKey = true; int anIndex = GetTextIdx(lineIdx, lineChar); int prevCharType = (anIndex > 0) ? GetCharType((char8)mData.mText[anIndex - 1].mChar) : -1; while (true) { if (lineChar > 0) MoveCursorTo(lineIdx, lineChar - 1); else if (lineIdx > 0) { int cursorIdx = mCursorTextPos; String lineText = scope String(); GetLineText(lineIdx - 1, lineText); MoveCursorTo(lineIdx - 1, (int32)lineText.Length); if ((!mAllowVirtualCursor) && (cursorIdx == mCursorTextPos)) MoveCursorTo(lineIdx - 1, (int32)lineText.Length - 1); break; } if (!mWidgetWindow.IsKeyDown(KeyCode.Control)) break; //mInner.mVal1 = inVal; //PrintF("TestStruct() %d\n", inVal); GetLineCharAtIdx(CursorTextPos, out lineIdx, out lineChar); anIndex = CursorTextPos; if (anIndex == 0) break; char8 aChar = (char8)mData.mText[anIndex - 1].mChar; int32 char8Type = GetCharType(aChar); if (prevCharType == 3) break; if (char8Type != prevCharType) { if ((char8Type == 0) && (prevCharType != 0)) break; if ((prevCharType == 2) || (prevCharType == 1) || (prevCharType == 4)) break; } prevCharType = char8Type; } } } break; case KeyCode.Right: { if (!PrepareForCursorMove(1)) { if (mAllowVirtualCursor) { bool doVirtualMove = true; if ((mWidgetWindow.IsKeyDown(KeyCode.Shift)) || (mWidgetWindow.IsKeyDown(KeyCode.Control))) doVirtualMove = false; else { int lineStart; int lineEnd; GetLinePosition(lineIdx, out lineStart, out lineEnd); if (prevCursorPos < lineEnd) doVirtualMove = false; } if (doVirtualMove) { mCursorBlinkTicks = 0; mSelection = null; var lineAndColumn = CursorLineAndColumn; CursorLineAndColumn = LineAndColumn(lineAndColumn.mLine, lineAndColumn.mColumn + 1); EnsureCursorVisible(true, false, false); CursorMoved(); break; } } wasMoveKey = true; int anIndex = GetTextIdx(lineIdx, lineChar); int prevCharType = (anIndex < mData.mTextLength) ? GetCharType((char8)mData.mText[anIndex].mChar) : -1; while (true) { int lineStart; int lineEnd; GetLinePosition(lineIdx, out lineStart, out lineEnd); int lineLen = lineEnd - lineStart; int nextLineChar = lineChar + 1; bool isWithinLine = nextLineChar < lineLen; if (nextLineChar == lineLen) { GetTextData(); if ((mData.mTextFlags == null) || ((mData.mTextFlags[lineEnd] & (int32)TextFlags.Wrap) == 0)) { isWithinLine = true; } } if (isWithinLine) MoveCursorTo(lineIdx, lineChar + 1, false, 1); else if (lineIdx < GetLineCount() - 1) MoveCursorTo(lineIdx + 1, 0); if (!mWidgetWindow.IsKeyDown(KeyCode.Control)) break; GetLineCharAtIdx(CursorTextPos, out lineIdx, out lineChar); anIndex = GetTextIdx(lineIdx, lineChar); if (anIndex == mData.mTextLength) break; char8 aChar = (char8)mData.mText[anIndex].mChar; int32 char8Type = GetCharType(aChar); if (char8Type == 3) break; if (char8Type != prevCharType) { if ((char8Type != 0) && (prevCharType == 0)) break; if ((char8Type == 2) || (char8Type == 1) || (char8Type == 4)) break; } prevCharType = char8Type; } } } break; case KeyCode.Up: fallthrough; case KeyCode.Down: { int32 aDir = (keyCode == KeyCode.Up) ? -1 : 1; bool didSelectionMove = PrepareForCursorMove(aDir); GetCursorLineChar(out lineIdx, out lineChar); if (mWidgetWindow.IsKeyDown(KeyCode.Control)) { //mEditWidget.Resize(mEditWidget.mX, mEditWidget.mY, mEditWidget.mWidth, mEditWidget.mHeight + aDir); mEditWidget.VertScrollTo(mEditWidget.mVertPos.mDest + aDir * mEditWidget.mScrollContentContainer.mHeight * 0.25f); EnsureCursorVisible(false); return; } wasMoveKey = true; if ((lineIdx + aDir >= 0) && (lineIdx + aDir < GetLineCount())) { if (mAllowVirtualCursor) { float cursorX; float cursorY; GetTextCoordAtCursor(out cursorX, out cursorY); mCursorWantX = cursorX; } //if (!mAllowVirtualCursor) { lineIdx += aDir; float wantedX = mCursorWantX; float aX; float aY; GetTextCoordAtLineChar(lineIdx, 0, out aX, out aY); MoveCursorToCoord(mCursorWantX, aY); // Restore old desired X mCursorWantX = wantedX; } /*else { mCursorBlinkTicks = 0; var prevLineAndColumn = CursorLineAndColumn; CursorLineAndColumn = LineAndColumn(Math.Max(0, prevLineAndColumn.mLine + aDir), prevLineAndColumn.mColumn); CursorMoved(); }*/ } if (didSelectionMove) CursorToLineStart(false); } break; case KeyCode.Home: PrepareForCursorMove(-1); wasMoveKey = true; if (mWidgetWindow.IsKeyDown(KeyCode.Control)) CursorToStart(); else CursorToLineStart(true); //mCursorImplicitlyMoved = true; break; case KeyCode.End: PrepareForCursorMove(1); wasMoveKey = true; if (mWidgetWindow.IsKeyDown(KeyCode.Control)) { CursorToEnd(); } else { CursorToLineEnd(); if ((!mAllowVirtualCursor) && (mWordWrap)) mShowCursorAtLineEnd = true; } break; case KeyCode.PageUp: fallthrough; case KeyCode.PageDown: { if (!mIsMultiline) break; wasMoveKey = true; int32 aDir = (keyCode == KeyCode.PageUp) ? -1 : 1; PrepareForCursorMove(aDir); float cursorX; float cursorY; if (mAllowVirtualCursor) { var lineAndCol = CursorLineAndColumn; GetTextCoordAtLineAndColumn(lineAndCol.mLine, lineAndCol.mColumn, out cursorX, out cursorY); mCursorWantX = cursorX; } else { GetTextCoordAtLineChar(lineIdx, lineChar, out cursorX, out cursorY); } cursorY -= (float)mEditWidget.mVertPos.mDest; float pageScrollTextSize = GetPageScrollTextHeight(); float adjustY = pageScrollTextSize * aDir; float scrollToVal = (float)(mEditWidget.mVertPos.mDest + adjustY); float newScrollToVal = AdjustPageScrollPos(scrollToVal, aDir); cursorY += scrollToVal - newScrollToVal; scrollToVal = newScrollToVal; if (mEditWidget.VertScrollTo(scrollToVal)) { cursorY += (float)mEditWidget.mVertPos.mDest; MoveCursorToCoord(cursorX, cursorY); // Restore old desired X mCursorWantX = cursorX; } else { if (aDir == -1) { lineIdx = 0; CursorTextPos = 0; } else { lineIdx = GetLineCount() - 1; CursorTextPos = GetTextIdx(lineIdx, lineChar); } float wantedX = mCursorWantX; float aX; float aY; GetTextCoordAtLineChar(lineIdx, 0, out aX, out aY); MoveCursorToCoord(mCursorWantX, aY); // Restore old desired X mCursorWantX = wantedX; } } break; case KeyCode.Insert: mOverTypeMode = !mOverTypeMode; break; case KeyCode.Delete: if (!CheckReadOnly()) DeleteChar(); mCursorImplicitlyMoved = true; break; default: } if (wasMoveKey) { if (mWidgetWindow.IsKeyDown(KeyCode.Shift)) { if (!HasSelection()) { StartSelection(); mSelection.ValueRef.mStartPos = (int32)prevCursorPos; } SelectToCursor(); } else mSelection = null; EnsureCursorVisible(); } } public float GetCursorScreenRelY() { float cursorX; float cursorY; GetTextCoordAtCursor(out cursorX, out cursorY); return (float)(cursorY - mEditWidget.mVertScrollbar.mContentPos); } public void SetCursorScreenRelY(float scrollTopDelta) { float cursorX; float cursorY; GetTextCoordAtCursor(out cursorX, out cursorY); mEditWidget.mVertScrollbar.ScrollTo(cursorY - scrollTopDelta); } public override bool Contains(float x, float y) { return true; } public override void Update() { Debug.Assert((mCursorTextPos != -1) || (mVirtualCursorPos != null)); base.Update(); if (mContentChanged) RecalcSize(); mCursorBlinkTicks++; if (mEditWidget.mHasFocus) MarkDirty(); } public virtual void RecalcSize() { mContentChanged = false; mEditWidget.UpdateScrollbars(); } public virtual void TextChanged() { } // Returns true if we have text at that location // If we return 'false' then line/theChar are set to closest positions public virtual bool GetLineCharAtCoord(float x, float y, out int line, out int theChar, out float overflowX) { line = -1; theChar = -1; overflowX = 0; return false; } public virtual bool GetLineAndColumnAtCoord(float x, float y, out int line, out int column) { line = -1; column = -1; return false; } public void GetColumnAtLineChar(int line, int lineChar, out int column) { float x; float y; GetTextCoordAtLineChar(line, lineChar, out x, out y); int line2; GetLineAndColumnAtCoord(x, y, out line2, out column); } public virtual void GetLineCharAtIdx(int idx, out int line, out int theChar) { int lineA; int char8A; GetLineCharAtIdx_Fast(idx, false, out lineA, out char8A); /*int32 lineB; int32 char8B; GetLineCharAtIdx_Slow(idx, out lineB, out char8B); Debug.Assert((lineA == lineB) && (char8A == char8B));*/ line = lineA; theChar = char8A; } public virtual void GetLineCharAtIdx_Fast(int idx, bool checkCursorAtLineEnd, out int line, out int theChar) { GetTextData(); //bool isCurCursor = true; if (idx == mData.mTextLength) { int32 lastLine = GetLineCount() - 1; line = (int32)lastLine; theChar = idx - mData.mLineStarts[lastLine]; return; } int lo = 0; int hi = mData.mLineStarts.Count - 2; if (idx < 0) { line = 0; theChar = 0; } while (lo <= hi) { int i = (lo + hi) / 2; var midVal = mData.mLineStarts[i]; int c = midVal - idx; if (c == 0) { if ((checkCursorAtLineEnd) && (mShowCursorAtLineEnd) && (i > 0)) { i--; midVal = mData.mLineStarts[i]; } line = (int32)i; theChar = idx - midVal; return; } if (c < 0) lo = i + 1; else hi = i - 1; } if (hi < GetLineCount()) { line = (int32)hi; theChar = idx - mData.mLineStarts[hi]; } else { line = (int32)mData.mLineStarts.Count - 2; theChar = mData.mLineStarts[mData.mLineStarts.Count - 1] - mData.mLineStarts[mData.mLineStarts.Count - 2]; } } public void GetLineCharAtIdx_Slow(int32 idx, out int32 line, out int32 theChar) { GetTextData(); for (int32 lineIdx = 0; lineIdx < mData.mLineStarts.Count; lineIdx++) { int32 lineStart = mData.mLineStarts[lineIdx]; if ((idx < lineStart) || ((idx == lineStart) && (lineIdx > 0) && (mCursorTextPos == lineStart) && (mShowCursorAtLineEnd))) { line = lineIdx - 1; theChar = idx - mData.mLineStarts[lineIdx - 1]; return; } } if (idx < 0) { line = 0; theChar = 0; } else { line = (int32)mData.mLineStarts.Count - 2; theChar = mData.mLineStarts[mData.mLineStarts.Count - 1] - mData.mLineStarts[mData.mLineStarts.Count - 2]; } } public virtual float GetLineHeight(int line) { return 0; } public virtual int32 GetLineCount() { GetTextData(); return (int32)mData.mLineStarts.Count - 1; } public virtual void GetLinePosition(int line, out int lineStart, out int lineEnd) { GetTextData(); if (line >= mData.mLineStarts.Count - 1) { lineStart = Math.Max(0, mData.mTextLength); lineEnd = mData.mTextLength; return; } lineStart = mData.mLineStarts[line]; lineEnd = mData.mLineStarts[line + 1]; if ((lineEnd > lineStart) && (mData.mText[lineEnd - 1].mChar == '\n')) lineEnd--; } public virtual void GetLineText(int line, String outStr) { GetTextData(); int lineStart = mData.mLineStarts[line]; int lineEnd = mData.mLineStarts[line + 1]; if ((lineEnd > lineStart) && (mData.mText[lineEnd - 1].mChar == '\n')) ExtractString(lineStart, lineEnd - lineStart - 1, outStr); // Remove last char8 (it's a '\n') else ExtractString(lineStart, lineEnd - lineStart, outStr); // Full line } public int GetTextIdx(int line, int char8Idx) { GetTextData(); int useLine = Math.Min(line, mData.mLineStarts.Count - 1); return mData.mLineStarts[useLine] + char8Idx; } public int GetCharIdIdx(int32 findCharId) { mData.mTextIdData.Prepare(); return mData.mTextIdData.GetIndexFromId(findCharId); } public int32 GetSourceCharIdAtLineChar(int line, int column) { int encodeIdx = 0; int spanLeft = 0; int32 char8Id = 0; int curLine = 0; int curColumn = 0; int char8Idx = 0; mData.mTextIdData.Prepare(); while (true) { while (spanLeft == 0) { if (mData.mTextIdData.mData == null) { spanLeft = mData.mTextLength; continue; } int32 cmd = Utils.DecodeInt(mData.mTextIdData.mData, ref encodeIdx); if (cmd > 0) char8Id = cmd; else spanLeft = -cmd; if (cmd == 0) return 0; } if ((curLine == line) && (curColumn == column)) return char8Id; char8 c = (char8)mData.mText[char8Idx++].mChar; if (c == '\n') { if (curLine == line) return char8Id; curLine++; curColumn = 0; } else curColumn++; spanLeft--; char8Id++; } } public virtual void GetTextCoordAtLineChar(int line, int char8Idx, out float x, out float y) { x = 0; y = 0; } public virtual void GetTextCoordAtLineAndColumn(int line, int column, out float x, out float y) { x = 0; y = 0; } // We used to have a split between PhysCursorMoved and CursorMoved. CursorMoved has a "ResetWantX" and was non-virtual... uh- // so what was taht for? public virtual void PhysCursorMoved() { mJustInsertedCharPair = false; mShowCursorAtLineEnd = false; mCursorBlinkTicks = 0; mCursorImplicitlyMoved = false; // ResetWantX(); } public void CursorMoved() { PhysCursorMoved(); /*mJustInsertedCharPair = false; mShowCursorAtLineEnd = false; mCursorBlinkTicks = 0; ResetWantX();*/ } public virtual void CursorToStart() { MoveCursorTo(0, 0); } public virtual void CursorToEnd() { String lineStr = scope String(); GetLineText(GetLineCount() - 1, lineStr); MoveCursorTo(GetLineCount() - 1, (int32)lineStr.Length); //mCursorWantX = GetDrawPos; } public virtual void CursorToLineStart(bool allowToScreenStart) { int lineIdx; int lineChar; GetCursorLineChar(out lineIdx, out lineChar); String lineText = scope String(); GetLineText(lineIdx, lineText); bool hasNonWhitespace = false; int32 contentIdx; for (contentIdx = 0; contentIdx < lineText.Length; contentIdx++) { if (!lineText[contentIdx].IsWhiteSpace) { hasNonWhitespace = true; break; } } if ((mAllowVirtualCursor) && (!hasNonWhitespace)) { var prevLineAndColumn = CursorLineAndColumn; CursorToLineEnd(); if ((!allowToScreenStart) || (CursorLineAndColumn.mColumn < prevLineAndColumn.mColumn)) return; } if ((allowToScreenStart) && (lineChar <= contentIdx)) MoveCursorTo(lineIdx, 0); else MoveCursorTo(lineIdx, contentIdx); } public virtual void CursorToLineEnd() { int line; int lineChar; GetCursorLineChar(out line, out lineChar); String curLineStr = scope String(); GetLineText(line, curLineStr); int32 lineLen = (int32)curLineStr.Length; MoveCursorTo(line, lineLen); } public void StartSelection() { mSelection = EditSelection(); int textPos; TryGetCursorTextPos(out textPos); mSelection.ValueRef.mEndPos = mSelection.ValueRef.mStartPos = (int32)textPos; } public void SelectToCursor() { /*int textPos; TryGetCursorTextPos(out textPos); if (mSelection != null) { mSelection.Value.mEndPos = textPos; }*/ int textPos; TryGetCursorTextPos(out textPos); if (mDragSelectionUnion != null) { mSelection = EditSelection(mDragSelectionUnion.Value.MinPos, mDragSelectionUnion.Value.MaxPos); if (textPos <= mSelection.Value.mStartPos) { mSelection.ValueRef.mStartPos = (int32)Math.Max(0, textPos - 1); while ((textPos > 0) && (IsNonBreakingChar((char8)mData.mText[textPos - 1].mChar))) { textPos--; mSelection.ValueRef.mStartPos = (int32)textPos; } CursorTextPos = mSelection.Value.mStartPos; } else { if (textPos > mSelection.Value.mEndPos) { mSelection.ValueRef.mEndPos = (int32)textPos; while ((textPos <= mData.mTextLength) && ((textPos == mData.mTextLength) || (mData.mText[textPos - 1].mChar != '\n')) && (IsNonBreakingChar((char8)mData.mText[textPos - 1].mChar))) { mSelection.ValueRef.mEndPos = (int32)textPos; textPos++; } } CursorTextPos = mSelection.Value.mEndPos; } } else if (mSelection != null) { mSelection.ValueRef.mEndPos = (int32)textPos; } } public virtual bool IsLineVisible(int line, bool useDestScrollPostion = true) { if (!mIsMultiline) return true; float aX; float aY; GetTextCoordAtLineChar(line, 0, out aX, out aY); float lineHeight = GetLineHeight(line); double scrollPos = useDestScrollPostion ? mEditWidget.mVertPos.mDest : mEditWidget.mVertPos.mSrc; if (aY + lineHeight*0.9f < scrollPos + mTextInsets.mTop) { return false; } else if (aY + lineHeight*0.1f + mShowLineBottomPadding > scrollPos + mEditWidget.mScrollContentContainer.mHeight) { return false; } return true; } public bool IsCursorVisible(bool useDestScrollPostion = true) { return IsLineVisible(CursorLineAndColumn.mLine, useDestScrollPostion); } public virtual void EnsureCursorVisible(bool scrollView = true, bool centerView = false, bool doHorzJump = true) { if (mEditWidget.mScrollContentContainer.mWidth <= 0) return; // Not sized yet if (mContentChanged) RecalcSize(); float horzJumpSize = doHorzJump ? mHorzJumpSize : 0; float aX; float aY; GetTextCoordAtCursor(out aX, out aY); int line; int lineChar; GetCursorLineChar(out line, out lineChar); float lineHeight = GetLineHeight(line); if (mIsMultiline) { if (aY < mEditWidget.mVertPos.mDest + mTextInsets.mTop) { if (scrollView) { float scrollPos = aY - mTextInsets.mTop; if (centerView) { scrollPos -= mEditWidget.mScrollContentContainer.mHeight * 0.50f; scrollPos = (float)Math.Round(scrollPos / lineHeight) * lineHeight; } mEditWidget.VertScrollTo(scrollPos); } else { int aLine; int aCharIdx; float overflowX; GetLineCharAtCoord(aX, (float)mEditWidget.mVertPos.mDest + mTextInsets.mTop, out aLine, out aCharIdx, out overflowX); float newX; float newY; GetTextCoordAtLineChar(aLine, aCharIdx, out newX, out newY); if (aY < mEditWidget.mVertPos.mDest + mTextInsets.mTop - 0.01f) GetLineCharAtCoord(newX, newY + GetLineHeight(aLine), out aLine, out aCharIdx, out overflowX); MoveCursorTo(aLine, aCharIdx); } } else if (aY + lineHeight + mShowLineBottomPadding > mEditWidget.mVertPos.mDest + mEditWidget.mScrollContentContainer.mHeight) { if (scrollView) { float scrollPos = aY + lineHeight + mShowLineBottomPadding - mEditWidget.mScrollContentContainer.mHeight; if (centerView) { // Show slightly more content on bottom scrollPos += mEditWidget.mScrollContentContainer.mHeight * 0.50f; scrollPos = (float)Math.Round(scrollPos / lineHeight) * lineHeight; } mEditWidget.VertScrollTo(scrollPos); } else MoveCursorToCoord(aX, (float)mEditWidget.mVertPos.mDest + mEditWidget.mScrollContentContainer.mHeight - lineHeight); } } if (mAutoHorzScroll) { if (aX < mEditWidget.mHorzPos.mDest + mTextInsets.mLeft) mEditWidget.HorzScrollTo(Math.Max(0, aX - mTextInsets.mLeft - horzJumpSize)); else if (aX >= mEditWidget.mHorzPos.mDest + mEditWidget.mScrollContentContainer.mWidth - mTextInsets.mRight) { float wantScrollPos = aX - mEditWidget.mScrollContentContainer.mWidth + horzJumpSize + mTextInsets.mRight; if (mAllowVirtualCursor) TrySetHorzScroll(wantScrollPos); mEditWidget.HorzScrollTo(Math.Min(mWidth - mEditWidget.mScrollContentContainer.mWidth, wantScrollPos)); } } } protected virtual void TrySetHorzScroll(float horzScroll) { mWidth = Math.Max(mWidth, horzScroll + mEditWidget.mScrollContentContainer.mWidth); mEditWidget.UpdateScrollbars(); } public void BlockIndentSelection(bool unIndent = false) { if (CheckReadOnly()) return; int minLineIdx = 0; int minLineCharIdx = 0; int maxLineIdx = 0; int maxLineCharIdx = 0; bool isMultilineSelection = HasSelection(); if (isMultilineSelection) { GetLineCharAtIdx(mSelection.Value.MinPos, out minLineIdx, out minLineCharIdx); GetLineCharAtIdx(mSelection.Value.MaxPos, out maxLineIdx, out maxLineCharIdx); isMultilineSelection = maxLineIdx != minLineIdx; } if (isMultilineSelection) { var indentTextAction = new EditWidgetContent.IndentTextAction(this); EditSelection newSel = mSelection.Value; mSelection.ValueRef.MakeForwardSelect(); mSelection.ValueRef.mStartPos -= (int32)minLineCharIdx; //int32 minPos = newSel.MinPos; int32 startAdjust = 0; int32 endAdjust = 0; if (unIndent) { //bool hadWsLeft = (minPos > 0) && (Char8.IsWhiteSpace((char8)mData.mText[minPos - 1].mChar)); for (int lineIdx = minLineIdx; lineIdx <= maxLineIdx; lineIdx++) { int lineStart; int lineEnd; GetLinePosition(lineIdx, out lineStart, out lineEnd); //lineStart += endAdjust; for (int32 i = 0; i < mTabLength; i++) { char8 c = (char8)mData.mText[lineStart + i].mChar; if (((c == '\t') && (i == 0)) || (c == ' ')) { indentTextAction.mRemoveCharList.Add(Tuple((int32)lineStart + i + endAdjust, c)); //RemoveText(lineStart, 1); endAdjust--; } if (c != ' ') break; } if ((lineIdx == minLineIdx) && (endAdjust != 0)) startAdjust = endAdjust; } indentTextAction.Redo(); } else { startAdjust++; for (int lineIdx = minLineIdx; lineIdx <= maxLineIdx; lineIdx++) { int lineStart; int lineEnd; GetLinePosition(lineIdx, out lineStart, out lineEnd); lineStart += endAdjust; //InsertText(lineStart, "\t"); indentTextAction.mInsertCharList.Add(Tuple((int32)lineStart, '\t')); endAdjust++; } indentTextAction.Redo(); } ContentChanged(); if (newSel.IsForwardSelect) { newSel.mStartPos = Math.Max(newSel.mStartPos + startAdjust, 0); newSel.mEndPos += endAdjust; CursorTextPos = newSel.mEndPos; } else { newSel.mEndPos = Math.Max(newSel.mEndPos + startAdjust, 0); newSel.mStartPos += endAdjust; CursorTextPos = newSel.mEndPos; } indentTextAction.mNewSelection = newSel; if ((indentTextAction.mInsertCharList.Count > 0) || (indentTextAction.mRemoveCharList.Count > 0)) { mData.mUndoManager.Add(indentTextAction); } else delete indentTextAction; mSelection = newSel; return; } else // Non-block { int lineIdx; int lineCharIdx; GetLineCharAtIdx(CursorTextPos, out lineIdx, out lineCharIdx); if (unIndent) { var prevSelection = mSelection; mSelection = null; if (lineCharIdx > 0) { String aLine = scope String(); GetLineText(lineIdx, aLine); for (int32 i = 0; i < mTabLength; i++) { if (lineCharIdx == 0) break; char8 c = aLine[lineCharIdx - 1]; if (!c.IsWhiteSpace) break; lineCharIdx--; if (prevSelection != null) { prevSelection.mValue.mStartPos--; prevSelection.mValue.mEndPos--; } Backspace(); if (c == '\t') break; } } else if (mAllowVirtualCursor) { var cursorPos = CursorLineAndColumn; cursorPos.mColumn = Math.Max(0, (cursorPos.mColumn - 1) / mTabLength * mTabLength); CursorLineAndColumn = cursorPos; } mSelection = prevSelection; return; } String curLineText = scope String(); GetLineText(lineIdx, curLineText); int32 indentCount = 1; String trimmedCurLineText = scope String(curLineText); trimmedCurLineText.Trim(); if ((lineCharIdx == 0) && (trimmedCurLineText.Length == 0)) // Smart indent { if (mAllowVirtualCursor) { if (HasSelection()) DeleteSelection(); var prevCursorPos = CursorLineAndColumn; CursorToLineEnd(); if (prevCursorPos.mColumn >= CursorLineAndColumn.mColumn) { var cursorPos = prevCursorPos; cursorPos.mColumn = (prevCursorPos.mColumn + mTabLength) / mTabLength * mTabLength; CursorLineAndColumn = cursorPos; } return; } int checkLineIdx = lineIdx - 1; while (checkLineIdx >= 0) { String lineText = scope String(); GetLineText(checkLineIdx, lineText); lineText.TrimEnd(); int32 checkIndentCount = 0; for (; checkIndentCount < lineText.Length; checkIndentCount++) { if (lineText[checkIndentCount] != '\t') break; } if ((checkIndentCount < lineText.Length) && (lineText[checkIndentCount] == '{')) checkIndentCount++; if (checkIndentCount > 0) { indentCount = checkIndentCount; break; } checkLineIdx--; } } if (HasSelection()) { GetLinePosition(minLineIdx, var lineStart, var lineEnd); String str = scope .(); ExtractString(lineStart, CursorTextPos - lineStart, str); if (str.IsWhiteSpace) { let prevSelection = mSelection.Value; mSelection = null; for (int32 i = 0; i < indentCount; i++) InsertAtCursor("\t"); GetLinePosition(minLineIdx, out lineStart, out lineEnd); mSelection = EditSelection(prevSelection.mStartPos + indentCount, prevSelection.mEndPos + indentCount); } else InsertAtCursor("\t"); } else { for (int32 i = 0; i < indentCount; i++) InsertAtCursor("\t"); } } } public void SelectAll() { CursorToStart(); StartSelection(); CursorToEnd(); SelectToCursor(); Debug.Assert(mSelection.Value.mEndPos <= mData.mTextLength); } //public void MoveCursorTo public void MoveCursorTo(int line, int char8Idx, bool centerCursor = false, int movingDir = 0) { int useCharIdx = char8Idx; mShowCursorAtLineEnd = false; CursorTextPos = GetTextIdx(line, char8Idx); // Skip over UTF8 parts AND unicode combining marks (ie: when we have a letter with an accent mark following it) while (true) { char8 c = mData.SafeGetChar(mCursorTextPos); if (c < (char8)0x80) break; if ((uint8)c & 0xC0 != 0x80) { var checkChar = mData.GetChar32(mCursorTextPos); if (!checkChar.IsCombiningMark) break; } if (movingDir > 0) { useCharIdx++; mCursorTextPos++; } else { if (mCursorTextPos == 0) break; useCharIdx--; mCursorTextPos--; } } float aX; float aY; GetTextCoordAtLineChar(line, useCharIdx, out aX, out aY); mCursorWantX = aX; mCursorBlinkTicks = 0; if (mEnsureCursorVisibleOnModify) EnsureCursorVisible(true, centerCursor); PhysCursorMoved(); } public void ResetWantX() { int line; int col; GetLineCharAtIdx(CursorTextPos, out line, out col); float x; float y; GetTextCoordAtLineChar(line, col, out x, out y); mCursorWantX = x; } public void MoveCursorToIdx(int index, bool centerCursor = false) { int aLine; int aCharIdx; GetLineCharAtIdx(index, out aLine, out aCharIdx); MoveCursorTo(aLine, aCharIdx, centerCursor); } public void MoveCursorToCoord(float x, float y) { bool failed = false; int aLine; int aCharIdx; float overflowX; failed = !GetLineCharAtCoord(x, y, out aLine, out aCharIdx, out overflowX); if ((mAllowVirtualCursor) && (failed)) { int line; int column; GetLineAndColumnAtCoord(x, y, out line, out column); CursorLineAndColumn = LineAndColumn(line, column); mCursorBlinkTicks = 0; PhysCursorMoved(); mShowCursorAtLineEnd = false; } else { if ((mWordWrap) && (failed)) mShowCursorAtLineEnd = true; MoveCursorTo(aLine, aCharIdx); int lineStart; int lineEnd; GetLinePosition(aLine, out lineStart, out lineEnd); if ((mWordWrap) && (aCharIdx == lineEnd - lineStart) && (lineEnd != lineStart)) mShowCursorAtLineEnd = true; } } public bool HasSelection() { return (mSelection != null) && (mSelection.Value.HasSelection); } public override void RehupScale(float oldScale, float newScale) { base.RehupScale(oldScale, newScale); mTextInsets.Scale(newScale / oldScale); //ContentChanged } //public virtual void DrawContent( public void ClearUndoData() { if (mData.mUndoManager != null) { mData.mUndoManager.Clear(); } } public bool HasUndoData() { if (mData.mUndoManager != null) { return mData.mUndoManager.GetActionCount() > 0; } return false; } } public abstract class EditWidget : ScrollableWidget { public EditWidgetContent mEditWidgetContent; public Event mOnSubmit ~ _.Dispose(); public Event mOnCancel ~ _.Dispose(); public Event mOnContentChanged ~ _.Dispose(); public EditWidgetContent Content { get { return mEditWidgetContent; } } public void GetText(String outStr) { mEditWidgetContent.ExtractString(0, mEditWidgetContent.mData.mTextLength, outStr); } public bool IsWhiteSpace() { for (int i < mEditWidgetContent.mData.mTextLength) { char8 c = mEditWidgetContent.mData.mText[i].mChar; if (!c.IsWhiteSpace) return false; } return true; } public virtual void Submit() { if (mOnSubmit.HasListeners) { EditEvent anEvent = scope EditEvent(); anEvent.mSender = this; mOnSubmit(anEvent); } } public void Cancel() { if (mOnCancel.HasListeners) { EditEvent editEvent = scope EditEvent(); editEvent.mSender = this; mOnCancel(editEvent); return; } } public void ContentChanged() { if (mOnContentChanged.HasListeners) { EditEvent editEvent = scope EditEvent(); editEvent.mSender = this; mOnContentChanged(editEvent); return; } } public virtual void SetText(String text) { scope AutoBeefPerf("EditWidget.SetText"); var prevLineAndColumn = mEditWidgetContent.CursorLineAndColumn; float cursorPos = (float)mVertPos.mDest; EditWidgetContent.TextUndoBatchStart undoBatchStart = null; if (mEditWidgetContent.WantsUndo) { undoBatchStart = new EditWidgetContent.TextUndoBatchStart(mEditWidgetContent, "setText"); mEditWidgetContent.mData.mUndoManager.Add(undoBatchStart); } mEditWidgetContent.CursorToStart(); mEditWidgetContent.SelectAll(); mEditWidgetContent.InsertAtCursor(text); if (mEditWidgetContent.mAllowVirtualCursor) mEditWidgetContent.CursorLineAndColumn = prevLineAndColumn; mEditWidgetContent.ClampCursor(); mVertPos.Set(cursorPos, true); if (mEditWidgetContent.WantsUndo) mEditWidgetContent.mData.mUndoManager.Add(undoBatchStart.mBatchEnd); if ((!mHasFocus) && (mHorzPos != null)) mHorzPos.Set(0, true); //TextChanged(); //mEditWidgetContent.mUndoManager.Clear(); } public IdSpan DuplicateTextIdData() { return mEditWidgetContent.mData.mTextIdData.Duplicate(); } public override void MouseDown(float x, float y, int32 btn, int32 btnCount) { base.MouseDown(x, y, btn, btnCount); // If we get clicked on ourselves then just translate into editwidget space to get the focus float childX; float childY; mEditWidgetContent.ParentToSelfTranslate(x, y, out childX, out childY); mEditWidgetContent.MouseDown(childX, childY, btn, btnCount); } public override void SetFocus() { mWidgetWindow.SetFocus(this); } public override void GotFocus() { base.GotFocus(); MouseEventHandler handler = new => HandleWindowMouseDown; //Debug.WriteLine("Adding handler {0}", handler); mWidgetWindow.mOnMouseDown.Add(handler); } public override void LostFocus() { MarkDirty(); var widgetWindow = mWidgetWindow; base.LostFocus(); widgetWindow.mOnMouseDown.Remove(scope => HandleWindowMouseDown, true); //Debug.WriteLine("Removing handler (LostFocus)"); } protected virtual bool WantsUnfocus() { return (!mMouseOver) && (mWidgetWindow != null) && (!ContainsWidget(mWidgetWindow.mOverWidget)); } protected virtual void HandleWindowMouseDown(MouseEvent theEvent) { if (WantsUnfocus()) { // Someone else got clicked on, remove focus //mWidgetWindow.mMouseDownHandler.Remove(scope => HandleWindowMouseDown, true); //Debug.WriteLine("Removing handler (HandleWindowMouseDown)"); mWidgetWindow.SetFocus(null); } } public override void RemovedFromParent(Widget previousParent, WidgetWindow previousWidgetWindow) { base.RemovedFromParent(previousParent, previousWidgetWindow); //TODO: Does this cause problems if we didn't have the handler added? if (previousWidgetWindow != null) { //previousWidgetWindow.mMouseDownHandler.Remove(scope => HandleWindowMouseDown, true); //Debug.WriteLine("Removing handler (RemovedFromParent)"); } } public override void KeyChar(char32 theChar) { base.KeyChar(theChar); mEditWidgetContent.KeyChar(theChar); } public override void Resize(float x, float y, float width, float height) { base.Resize(x, y, width, height); if (mEditWidgetContent.mWordWrap) // Need to recalculate word wrapping mEditWidgetContent.ContentChanged(); } public override void KeyDown(KeyCode keyCode, bool isRepeat) { base.KeyDown(keyCode, isRepeat); if (mWidgetWindow != null) mEditWidgetContent.KeyDown(keyCode, isRepeat); } public void FinishScroll() { mVertPos.mPct = 1.0f; UpdateContentPosition(); } } }