1
0
Fork 0
mirror of https://github.com/beefytech/Beef.git synced 2025-07-04 23:36:00 +02:00

Collapsible regions (aka outlining aka code folding)

This commit is contained in:
Brian Fiete 2022-02-28 11:27:12 -08:00
parent 3dd4212ccd
commit 90735e3bf8
21 changed files with 2518 additions and 277 deletions

View file

@ -346,6 +346,7 @@ namespace Beefy
return list;
}*/
[DisableChecks]
public static int64 DecodeInt64(ref uint8* ptr)
{
int64 value = 0;
@ -364,6 +365,7 @@ namespace Beefy
return value;
}
[DisableChecks]
public static int32 DecodeInt(uint8[] buf, ref int idx)
{
int32 value = 0;
@ -383,6 +385,7 @@ namespace Beefy
return value;
}
[DisableChecks]
public static void EncodeInt(uint8[] buf, ref int idx, int value)
{
int curValue = value;

View file

@ -5,11 +5,49 @@ using System.Text;
using Beefy.widgets;
using Beefy.gfx;
using Beefy.utils;
using Beefy.geom;
namespace Beefy.theme.dark
{
public class DarkEditWidgetContent : EditWidgetContent
{
public class Embed
{
public enum Kind
{
LineStart,
HideLine,
LineEnd,
}
public Kind mKind;
public ~this()
{
}
public virtual void Draw(Graphics g, Rect rect, bool hideLine)
{
}
public virtual void MouseDown(float x, float y, int btn, int btnCount)
{
}
public virtual float GetWidth(bool hideLine)
{
return GS!(24);
}
}
public class Data : EditWidgetContent.Data
{
}
public Font mFont;
public uint32[] mTextColors = sDefaultColors;
public uint32 mHiliteColor = 0xFF2f5c88;
@ -23,6 +61,7 @@ namespace Beefy.theme.dark
public uint32 mViewWhiteSpaceColor;
public bool mScrollToStartOnLostFocus;
public bool mHiliteCurrentLine;
public Dictionary<int, Embed> mEmbeds = new .() ~ DeleteDictionaryAndValues!(_);
protected static uint32[] sDefaultColors = new uint32[] ( Color.White ) ~ delete _;
@ -39,6 +78,48 @@ namespace Beefy.theme.dark
mFont = DarkTheme.sDarkTheme?.mSmallFont;
}
protected override EditWidgetContent.Data CreateEditData()
{
return new Data();
}
public virtual void CheckLineCoords()
{
if (mLineCoordTextVersionId == mData.mCurTextVersionId)
return;
mLineCoordTextVersionId = mData.mCurTextVersionId;
if (mLineCoords == null)
mLineCoords = new .();
if (mLineCoordJumpTable == null)
mLineCoordJumpTable = new .();
mLineCoords.Clear();
mLineCoords.GrowUnitialized(mData.mLineStarts.Count);
mLineCoordJumpTable.Clear();
float fontHeight = mFont.GetLineSpacing();
int prevJumpIdx = -1;
float jumpCoordSpacing = GetJumpCoordSpacing();
double curY = 0;
for (int line < mData.mLineStarts.Count)
{
float lineHeight = fontHeight;
mLineCoords[line] = (float)curY;
int jumpIdx = (.)(curY / jumpCoordSpacing);
while (prevJumpIdx < jumpIdx)
{
mLineCoordJumpTable.Add(((int32)line, (int32)line + 1));
prevJumpIdx++;
}
mLineCoordJumpTable[jumpIdx].max = (.)line + 1;
curY += lineHeight;
}
}
public override void GetTextData()
{
// Generate text flags if we need to...
@ -97,8 +178,56 @@ namespace Beefy.theme.dark
}
base.GetTextData();
CheckLineCoords();
}
public int32 mLineCoordTextVersionId = -1;
public List<float> mLineCoords ~ delete _;
public List<(int32 min, int32 max)> mLineCoordJumpTable ~ delete _;
public bool IsLineCollapsed(int line)
{
if (mLineCoords == null)
return false;
if ((line >= 0) && (line < mLineCoords.Count - 1))
return (mLineCoords[line + 1] - mLineCoords[line]) < 0.1f;
return false;
}
public float GetLineHeight(int line, float defaultVal)
{
if ((line >= 0) && (line < mLineCoords.Count - 1))
return mLineCoords[line + 1] - mLineCoords[line];
return defaultVal;
}
public float GetLineY(int line, float defaultVal)
{
if ((line >= 0) && (line < mLineCoords.Count))
return mLineCoords[line];
return defaultVal;
}
public int FindUncollapsedLine(int line)
{
var line;
while ((line > 0) && (IsLineCollapsed(line)))
line--;
return line;
}
public bool IsInCollapseGroup(int anchorLine, int checkLine)
{
if (checkLine < anchorLine)
return false;
if (checkLine == anchorLine)
return true;
if (checkLine == anchorLine + 1)
return IsLineCollapsed(checkLine);
return mLineCoords[anchorLine + 1] == mLineCoords[checkLine + 1];
}
protected override void AdjustCursorsAfterExternalEdit(int index, int ofs)
{
base.AdjustCursorsAfterExternalEdit(index, ofs);
@ -322,6 +451,8 @@ namespace Beefy.theme.dark
Utils.RoundScale(ref mTabSize, newScale / oldScale);
SetFont(mFont, mCharWidth != -1, mAllowVirtualCursor);
mContentChanged = true; // Defer calling of RecalcSize
GetTextData();
LineStartsChanged();
}
public virtual float DrawText(Graphics g, String str, float x, float y, uint16 typeIdAndFlags)
@ -335,15 +466,52 @@ namespace Beefy.theme.dark
return mEditWidget.mHasFocus ? mHiliteColor : mUnfocusedHiliteColor;
}
protected Rect GetEmbedRect(int lineIdx, DarkEditWidgetContent.Embed embed)
{
GetLinePosition(lineIdx, var lineStart, var lineEnd);
bool hideLine = false;
if ((embed.mKind == .HideLine) &&
((!mEditWidget.mHasFocus) || (!IsInCollapseGroup(lineIdx, CursorLine))))
hideLine = true;
int wsEnd = lineEnd;
if ((hideLine) || (embed.mKind == .LineStart))
{
for (int i in lineStart..<lineEnd)
{
var checkEnd = ref mData.mText[i];
if (!checkEnd.mChar.IsWhiteSpace)
{
wsEnd = i;
break;
}
}
}
String str = scope .(256);
ExtractString(lineStart, wsEnd - lineStart, str);
float selStartX = GetTabbedWidth(str, 0);
if ((embed.mKind == .LineEnd) ||
((embed.mKind == .HideLine) && (!hideLine)))
selStartX += GS!(4);
Rect rect = .(selStartX, mLineCoords[lineIdx] - GS!(2), embed.GetWidth(hideLine), mFont.GetLineSpacing() + GS!(4));
if (rect.mY < 0)
rect.mY = 0;
return rect;
}
public override void Draw(Graphics g)
{
base.Draw(g);
#unwarn
int lineCount = GetLineCount();
float lineSpacing = GetLineHeight(0);
g.SetFont(mFont);
float lineSpacing = mFont.GetLineSpacing();
float offsetY = mTextInsets.mTop;
if (mHeight < lineSpacing)
@ -376,7 +544,50 @@ namespace Beefy.theme.dark
float lastOverflowX;
GetLineCharAtCoord(0, -mY + mEditWidget.mScrollContentContainer.mHeight, out lastLine, out lastCharIdx, out lastOverflowX);
bool drewCursor = false;
bool drewCursor = false;
void DrawCursor(float x, float y)
{
if (mHiliteCurrentLine && selStartIdx == selEndIdx)
{
float thickness = 2 * (lineSpacing / 18);
// This isn't quite the right value, but I'm not sure how to get this
// to properly highlight the whole line without getting cut off - this works well for now.
float totalLineWidth = mEditWidget.mScrollContentContainer.mWidth - thickness;
float hiliteX = (int)mEditWidget.mHorzPos.v; // If we don't round to int we get jitter while scrolling.
using (g.PushColor(DarkTheme.COLOR_CURRENT_LINE_HILITE))
g.OutlineRect(hiliteX, y, totalLineWidth, lineSpacing + thickness, thickness);
}
float brightness = (float)Math.Cos(Math.Max(0.0f, mCursorBlinkTicks - 20) / 9.0f);
brightness = Math.Clamp(brightness * 2.0f + 1.6f, 0, 1);
if (mEditWidget.mVertPos.IsMoving)
brightness = 0; // When we animate a pgup or pgdn, it's weird seeing the cursor scrolling around
Color cursorColor = mTextColors[0];
if (mOverTypeMode)
{
if (mCharWidth <= 2)
{
using (g.PushColor(Color.Mult(cursorColor, Color.Get(brightness * 0.75f))))
g.FillRect(x, y, GS!(2), lineSpacing);
}
else
{
using (g.PushColor(Color.Mult(cursorColor, Color.Get(brightness * 0.30f))))
g.FillRect(x, y, mCharWidth, lineSpacing);
}
}
else
{
using (g.PushColor(Color.Mult(cursorColor, Color.Get(brightness))))
g.FillRect(x, y, Math.Max(1.0f, GS!(1)), lineSpacing);
}
drewCursor = true;
}
String sectionText = scope String(256);
for (int lineIdx = firstLine; lineIdx <= lastLine; lineIdx++)
{
@ -387,7 +598,26 @@ namespace Beefy.theme.dark
int lineDrawStart = lineStart;
float curX = 0;
float curY = lineIdx * lineSpacing;
float curY = mLineCoords[lineIdx];
float height = mLineCoords[lineIdx + 1] - curY;
if (height <= 1.0f)
continue;
DarkEditWidgetContent.Embed embed = null;
if (mEmbeds.GetValue(lineIdx) case .Ok(out embed))
{
if ((embed.mKind == .HideLine) &&
((!IsInCollapseGroup(lineIdx, CursorLine)) || (!mEditWidget.mHasFocus)))
{
var embedRect = GetEmbedRect(lineIdx, embed);
embed.Draw(g, embedRect, true);
g.SetFont(mFont);
continue;
}
}
g.SetFont(mFont);
while (true)
{
int lineDrawEnd = lineDrawStart;
@ -481,46 +711,7 @@ namespace Beefy.theme.dark
}
if (aX != -1)
{
if (mHiliteCurrentLine && selStartIdx == selEndIdx)
{
float thickness = 2 * (lineSpacing / 18);
// This isn't quite the right value, but I'm not sure how to get this
// to properly highlight the whole line without getting cut off - this works well for now.
float totalLineWidth = mEditWidget.mScrollContentContainer.mWidth - thickness;
float x = (int)mEditWidget.mHorzPos.v; // If we don't round to int we get jitter while scrolling.
using (g.PushColor(DarkTheme.COLOR_CURRENT_LINE_HILITE))
g.OutlineRect(x, curY, totalLineWidth, lineSpacing + thickness, thickness);
}
float brightness = (float)Math.Cos(Math.Max(0.0f, mCursorBlinkTicks - 20) / 9.0f);
brightness = Math.Clamp(brightness * 2.0f + 1.6f, 0, 1);
if (mEditWidget.mVertPos.IsMoving)
brightness = 0; // When we animate a pgup or pgdn, it's weird seeing the cursor scrolling around
Color cursorColor = mTextColors[0];
if (mOverTypeMode)
{
if (mCharWidth <= 2)
{
using (g.PushColor(Color.Mult(cursorColor, Color.Get(brightness * 0.75f))))
g.FillRect(aX, curY, GS!(2), lineSpacing);
}
else
{
using (g.PushColor(Color.Mult(cursorColor, Color.Get(brightness * 0.30f))))
g.FillRect(aX, curY, mCharWidth, lineSpacing);
}
}
else
{
using (g.PushColor(Color.Mult(cursorColor, Color.Get(brightness))))
g.FillRect(aX, curY, Math.Max(1.0f, GS!(1)), lineSpacing);
}
drewCursor = true;
}
DrawCursor(aX, curY);
}
lineDrawStart = lineDrawEnd;
@ -528,7 +719,18 @@ namespace Beefy.theme.dark
if (lineDrawStart >= lineEnd)
break;
}
}
if (embed != null)
{
var embedRect = GetEmbedRect(lineIdx, embed);
embed.Draw(g, embedRect, false);
g.SetFont(mFont);
if ((!drewCursor) && (IsLineCollapsed(CursorLineAndColumn.mLine)) &&
(GetLineY(CursorLineAndColumn.mLine, -1) == GetLineY(lineIdx + 1, -1)))
DrawCursor(embedRect.Right + GS!(2), curY);
}
}
g.PopMatrix();
@ -561,6 +763,8 @@ namespace Beefy.theme.dark
public override void GetTextCoordAtLineChar(int line, int lineChar, out float x, out float y)
{
GetTextData();
String lineText = scope String(256);
GetLineText(line, lineText);
if (lineChar > lineText.Length)
@ -571,74 +775,95 @@ namespace Beefy.theme.dark
subText.Append(lineText, 0, lineChar);
x = GetTabbedWidth(subText, 0, true) + mTextInsets.mLeft;
}
y = mTextInsets.mTop + line * mFont.GetLineSpacing();
y = mTextInsets.mTop + mLineCoords[line];
}
public override void GetTextCoordAtLineAndColumn(int line, int column, out float x, out float y)
{
GetTextData();
Debug.Assert((mCharWidth != -1) || (column == 0));
String lineText = scope String(256);
GetLineText(line, lineText);
x = mTextInsets.mLeft + column * mCharWidth;
y = mTextInsets.mTop + line * mFont.GetLineSpacing();
y = mTextInsets.mTop + mLineCoords[line];
}
public override bool GetLineCharAtCoord(float x, float y, out int line, out int char8Idx, out float overflowX)
protected int GetLineAt(float y)
{
if (y < 0)
return 0;
GetTextData();
int lineCount = GetLineCount();
var checkY = y - mTextInsets.mTop + 0.001f;
var jumpEntry = mLineCoordJumpTable[Math.Clamp((int)(y / GetJumpCoordSpacing()), 0, mLineCoordJumpTable.Count - 1)];
int line = jumpEntry.min - 1;
for (int checkLine in jumpEntry.min ..< jumpEntry.max)
{
if (checkY >= mLineCoords[checkLine])
line = checkLine;
}
if (line < 0)
line = 0;
if (line >= lineCount)
line = lineCount - 1;
return line;
}
public override bool GetLineCharAtCoord(float x, float y, out int line, out int lineChar, out float overflowX)
{
line = (int) ((y - mTextInsets.mTop) / mFont.GetLineSpacing() + 0.001f);
int lineCount = GetLineCount();
line = GetLineAt(y);
return GetLineCharAtCoord(line, x, out lineChar, out overflowX);
}
if (line < 0)
line = 0;
if (line >= lineCount)
line = lineCount - 1;
public override bool GetLineAndColumnAtCoord(float x, float y, out int line, out int column)
{
line = GetLineAt(y);
column = Math.Max(0, (int32)((x - mTextInsets.mLeft + 1) / mCharWidth + 0.6f));
return mCharWidth != -1;
}
String lineText = scope String(256);
GetLineText(line, lineText);
int32 char8Count = GetTabbedCharCountToLength(lineText, x - mTextInsets.mLeft);
char8Idx = char8Count;
public override bool GetLineCharAtCoord(int line, float x, out int lineChar, out float overflowX)
{
String lineText = scope String(256);
GetLineText(line, lineText);
int32 char8Count = GetTabbedCharCountToLength(lineText, x - mTextInsets.mLeft);
lineChar = char8Count;
if (char8Count < lineText.Length)
{
if (char8Count < lineText.Length)
{
String subString = new:ScopedAlloc! String(char8Count);
subString.Append(lineText, 0, char8Count);
float subWidth = GetTabbedWidth(subString, 0);
float subWidth = GetTabbedWidth(subString, 0);
var utf8enumerator = lineText.DecodedChars(char8Count);
if (utf8enumerator.MoveNext())
{
char32 c = utf8enumerator.Current;
float checkCharWidth = 0;
if (c == '\t')
checkCharWidth = mTabSize * 0.5f;
else
float checkCharWidth = 0;
if (c == '\t')
checkCharWidth = mTabSize * 0.5f;
else
{
checkCharWidth = mFont.GetWidth(c) * 0.5f;
}
if (x >= subWidth + mTextInsets.mLeft + checkCharWidth)
char8Idx = (int32)utf8enumerator.NextIndex;
if (x >= subWidth + mTextInsets.mLeft + checkCharWidth)
lineChar = (int32)utf8enumerator.NextIndex;
}
}
else
{
overflowX = (x - mTextInsets.mLeft) - (GetTabbedWidth(lineText, 0) + 0.001f);
return overflowX <= 0;
}
}
else
{
overflowX = (x - mTextInsets.mLeft) - (GetTabbedWidth(lineText, 0) + 0.001f);
return overflowX <= 0;
}
overflowX = 0;
return true;
}
public override bool GetLineAndColumnAtCoord(float x, float y, out int line, out int column)
{
line = (int32)((y - mTextInsets.mTop) / mFont.GetLineSpacing() + 0.001f);
if (line >= GetLineCount())
line = GetLineCount() - 1;
line = Math.Max(0, line);
column = Math.Max(0, (int32)((x - mTextInsets.mLeft + 1) / mCharWidth + 0.6f));
return mCharWidth != -1;
}
overflowX = 0;
return true;
}
void RecalcSize(int32 startLineNum, int32 endLineNum, bool forceAccurate = false)
{
@ -670,6 +895,53 @@ namespace Beefy.theme.dark
base.CursorToLineEnd();
}
public override void GetTextCoordAtCursor(out float x, out float y)
{
int32 line = CursorLine;
if (IsLineCollapsed(line))
{
line = (.)FindUncollapsedLine(line);
GetTextCoordAtLineAndColumn(line, CursorLineAndColumn.mColumn, out x, out y);
return;
}
base.GetTextCoordAtCursor(out x, out y);
}
public override void MoveCursorToCoord(float x, float y)
{
base.MoveCursorToCoord(x, y);
if ((!mEmbeds.IsEmpty) && (mEmbeds.GetValue(CursorLineAndColumn.mLine) case .Ok(let embed)))
{
var embedRect = GetEmbedRect(CursorLineAndColumn.mLine, embed);
if (x > embedRect.Right)
{
int endLine = CursorLineAndColumn.mLine;
while (true)
{
if (!IsLineCollapsed(endLine + 1))
break;
endLine++;
}
if (endLine != CursorLineAndColumn.mLine)
{
GetLinePosition(endLine, var endLineStart, var endLineEnd);
GetLineAndColumnAtLineChar(endLine, endLineEnd - endLineStart, var column);
CursorLineAndColumn = .(endLine, column);
}
}
}
}
public override void CursorToLineStart(bool allowToScreenStart)
{
while (IsLineCollapsed(CursorLineAndColumn.mLine))
CursorLineAndColumn = .(CursorLineAndColumn.mLine - 1, 1);
base.CursorToLineStart(allowToScreenStart);
}
public void RecalcSize(bool forceAccurate = false)
{
mMaximalScrollAddedHeight = 0;
@ -707,7 +979,9 @@ namespace Beefy.theme.dark
Debug.Assert(!mWidth.IsNaN);
}
mHeight = GetLineCount() * mFont.GetLineSpacing() + mTextInsets.mTop + mTextInsets.mBottom;
GetTextData();
mHeight = mLineCoords.Back + mTextInsets.mTop + mTextInsets.mBottom;
Debug.Assert(mHeight > 0);
UpdateMaximalScroll();
base.RecalcSize();
}
@ -721,6 +995,21 @@ namespace Beefy.theme.dark
{
base.ContentChanged();
mRecalcSizeLineNum = -1;
mLineCoordTextVersionId = -1;
DeleteAndNullify!(mLineCoords);
DeleteAndNullify!(mLineCoordJumpTable);
}
public float GetJumpCoordSpacing()
{
return mFont.GetLineSpacing() * 4;
}
public override void LineStartsChanged()
{
base.LineStartsChanged();
mLineCoordTextVersionId = -1;
}
public override void TextAppended(String str)
@ -734,7 +1023,7 @@ namespace Beefy.theme.dark
base.TextAppended(str);
}
void UpdateMaximalScroll()
protected void UpdateMaximalScroll()
{
if (mAllowMaximalScroll)
{
@ -744,6 +1033,8 @@ namespace Beefy.theme.dark
mMaximalScrollAddedHeight = mEditWidget.mScrollContentContainer.mHeight - mFont.GetLineSpacing();
mHeight += mMaximalScrollAddedHeight;
Debug.Assert(mHeight >= 0);
if (mHeight != prevHeight)
mEditWidget.UpdateScrollbars();
}
@ -757,7 +1048,9 @@ namespace Beefy.theme.dark
public override float GetLineHeight(int line)
{
return mFont.GetLineSpacing();
if (mLineCoords == null)
GetTextData();
return mLineCoords[line + 1] - mLineCoords[line];
}
public override float GetPageScrollTextHeight()
@ -843,6 +1136,27 @@ namespace Beefy.theme.dark
CheckRecordScrollTop();
}
public override void MouseMove(float x, float y)
{
base.MouseMove(x, y);
int line = GetLineAt(y);
bool isOverEmbed = false;
if (mEmbeds.GetValue(line) case .Ok(let embed))
{
Rect embedRect = GetEmbedRect(line, embed);
if (embedRect.Contains(x, y))
isOverEmbed = true;
}
if (isOverEmbed)
BFApp.sApp.SetCursor(Cursor.Pointer);
else
BFApp.sApp.SetCursor(Cursor.Text);
}
}
public class DarkEditWidget : EditWidget

View file

@ -181,6 +181,8 @@ namespace Beefy.theme.dark
PanelHeader,
ExtMethod,
CollapseClosed,
CollapseOpened,
COUNT
};

View file

@ -6,8 +6,129 @@ using System.Threading.Tasks;
namespace Beefy.utils
{
public struct IdSpan
public struct IdSpan : IFormattable
{
public class LookupContext
{
public enum SortKind
{
None,
Id,
Index
}
public struct Entry
{
public int32 mIdStart;
public int32 mIndexStart;
public int32 mLength;
}
public List<Entry> mEntries = new .() ~ delete _;
public SortKind mSortKind;
public this(IdSpan idSpan)
{
int encodeIdx = 0;
int charId = 1;
int charIdx = 0;
while (true)
{
int cmd = Utils.DecodeInt(idSpan.mData, ref encodeIdx);
if (cmd > 0)
charId = cmd;
else
{
int spanSize = -cmd;
Entry entry;
entry.mIdStart = (.)charId;
entry.mIndexStart = (.)charIdx;
entry.mLength = (.)spanSize;
mEntries.Add(entry);
charId += spanSize;
charIdx += spanSize;
if (cmd == 0)
break;
}
}
}
public int GetIndexFromId(int32 findCharId)
{
if (mSortKind != .Id)
{
mEntries.Sort(scope (lhs, rhs) => lhs.mIdStart <=> rhs.mIdStart);
mSortKind = .Id;
}
int lo = 0;
int hi = mEntries.Count - 1;
while (lo <= hi)
{
int i = (lo + hi) / 2;
var midVal = ref mEntries[i];
if ((findCharId >= midVal.mIdStart) && (findCharId < midVal.mIdStart + midVal.mLength))
return midVal.mIndexStart + (findCharId - midVal.mIdStart);
if (findCharId > midVal.mIdStart)
lo = i + 1;
else
hi = i - 1;
}
return -1;
}
public int32 GetIdAtIndex(int32 findCharId)
{
if (mSortKind != .Index)
{
mEntries.Sort(scope (lhs, rhs) => lhs.mIndexStart <=> rhs.mIndexStart);
mSortKind = .Index;
}
int lo = 0;
int hi = mEntries.Count - 1;
while (lo <= hi)
{
int i = (lo + hi) / 2;
var midVal = ref mEntries[i];
if ((findCharId >= midVal.mIndexStart) && (findCharId < midVal.mIndexStart + midVal.mLength))
return midVal.mIdStart + (findCharId - midVal.mIndexStart);
if (findCharId > midVal.mIndexStart)
lo = i + 1;
else
hi = i - 1;
}
return -1;
}
public override void ToString(String strBuffer)
{
if (mSortKind != .Index)
{
mEntries.Sort(scope (lhs, rhs) => lhs.mIndexStart <=> rhs.mIndexStart);
mSortKind = .Index;
}
strBuffer.AppendF("IdSpan.LookupCtx(");
for (var entry in mEntries)
{
if (@entry.Index > 0)
strBuffer.Append(' ');
strBuffer.AppendF($"{entry.mIndexStart}:{entry.mLength}={entry.mIdStart}");
}
strBuffer.AppendF(")");
}
}
enum Change
{
case Insert(int32 index, int32 id, int16 length);
@ -671,7 +792,6 @@ namespace Beefy.utils
public IdSpan Duplicate()
{
Debug.Assert(!HasChangeList);
IdSpan idSpan = IdSpan();
if (mData != null)
{
@ -679,6 +799,12 @@ namespace Beefy.utils
mData.CopyTo(idSpan.mData, 0, 0, mLength);
idSpan.mLength = mLength;
}
if (mChangeList != null)
{
idSpan.mChangeList = new .();
for (var change in mChangeList)
idSpan.mChangeList.Add(change);
}
return idSpan;
}
@ -866,6 +992,11 @@ namespace Beefy.utils
return idSpan;
}
public override void ToString(String strBuffer)
{
ToString(strBuffer, "", null);
}
public void Dump() mut
{
Prepare();
@ -895,5 +1026,49 @@ namespace Beefy.utils
}
}
}
public void ToString(String outString, String format, IFormatProvider formatProvider)
{
if (HasChangeList)
{
IdSpan span = Duplicate();
span.ToString(outString, format, formatProvider);
return;
}
outString.AppendF($"Span(Length:{mLength} ChangeList:{(mChangeList?.Count).GetValueOrDefault()})");
if (format == "D")
{
outString.Append("{");
int encodeIdx = 0;
int charId = 1;
int charIdx = 0;
while (true)
{
int32 cmd = Utils.DecodeInt(mData, ref encodeIdx);
if (cmd > 0)
{
charId = cmd;
outString.AppendF($" #{charId}");
}
else
{
int32 spanSize = -cmd;
charId += spanSize;
charIdx += spanSize;
if (cmd == 0)
{
outString.Append("}");
return;
}
outString.AppendF($":{spanSize}");
}
}
}
}
}
}

View file

@ -571,16 +571,16 @@ namespace Beefy.widgets
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);
GetLineCharAtCoord(mVirtualCursorPos.Value.mLine, x, out lineChar, out overflowX);
mCursorTextPos = (int32)GetTextIdx(mVirtualCursorPos.Value.mLine, lineChar);
}
return mCursorTextPos;
@ -606,14 +606,9 @@ namespace Beefy.widgets
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;
GetLineAndColumnAtLineChar(line, lineChar, out coordLineColumn);
lineAndColumn.mLine = (int32)line;
lineAndColumn.mColumn = (int32)coordLineColumn;
return lineAndColumn;
}
@ -627,6 +622,20 @@ namespace Beefy.widgets
}
}
public int32 CursorLine
{
get
{
if (mVirtualCursorPos.HasValue)
return mVirtualCursorPos.Value.mLine;
int line;
int lineChar;
GetLineCharAtIdx(mCursorTextPos, out line, out lineChar);
return (.)line;
}
}
public bool WantsUndo
{
get
@ -644,7 +653,8 @@ namespace Beefy.widgets
mData.Ref(this);
mContentChanged = true;
}
protected virtual Data CreateEditData()
{
return new Data();
@ -661,13 +671,13 @@ namespace Beefy.widgets
{
if (mVirtualCursorPos.HasValue)
{
int32 line = mVirtualCursorPos.Value.mLine;
float x;
float y;
GetTextCoordAtLineAndColumn(mVirtualCursorPos.Value.mLine, mVirtualCursorPos.Value.mColumn, out x, out y);
GetTextCoordAtLineAndColumn(line, mVirtualCursorPos.Value.mColumn, out x, out y);
int line;
int lineChar;
bool success = GetLineCharAtCoord(x, y, out line, out lineChar, out overflowX);
bool success = GetLineCharAtCoord(line, x, out lineChar, out overflowX);
textPos = GetTextIdx(line, lineChar);
return success;
@ -851,7 +861,7 @@ namespace Beefy.widgets
public override void MouseLeave()
{
base.MouseLeave();
BFApp.sApp.SetCursor(Cursor.Pointer);
BFApp.sApp.SetCursor(Cursor.Pointer);
}
public virtual void ClearText()
@ -1344,6 +1354,11 @@ namespace Beefy.widgets
TextChanged();
}
public virtual void LineStartsChanged()
{
}
public virtual void TextAppended(String str)
{
if ((mWordWrap) || (mData.mLineStarts == null))
@ -1366,6 +1381,7 @@ namespace Beefy.widgets
}
}
mData.mLineStarts.Add(mData.mTextLength);
LineStartsChanged();
mContentChanged = true;
@ -1385,69 +1401,70 @@ namespace Beefy.widgets
{
// Generate line starts and text flags if we need to
if (mData.mLineStarts == null)
{
scope AutoBeefPerf("EWC.GetTextData");
if (mData.mLineStarts != null)
return;
scope AutoBeefPerf("EWC.GetTextData");
CharData* char8DataPtr = mData.mText.CArray();
uint8* textFlagsPtr = null;
if (mData.mTextFlags != null)
textFlagsPtr = mData.mTextFlags.CArray();
CharData* char8DataPtr = mData.mText.CArray();
uint8* textFlagsPtr = null;
if (mData.mTextFlags != null)
textFlagsPtr = mData.mTextFlags.CArray();
int32 lineIdx = 0;
if (textFlagsPtr != null)
int32 lineIdx = 0;
if (textFlagsPtr != null)
{
for (int32 i < mData.mTextLength)
{
for (int32 i < mData.mTextLength)
{
if ((char8DataPtr[i].mChar == '\n') || ((textFlagsPtr != null) && ((textFlagsPtr[i] & ((int32)TextFlags.Wrap)) != 0)))
lineIdx++;
}
if ((char8DataPtr[i].mChar == '\n') || ((textFlagsPtr != null) && ((textFlagsPtr[i] & ((int32)TextFlags.Wrap)) != 0)))
lineIdx++;
}
else
}
else
{
for (int32 i < mData.mTextLength)
{
for (int32 i < mData.mTextLength)
{
if (char8DataPtr[i].mChar == '\n')
lineIdx++;
}
if (char8DataPtr[i].mChar == '\n')
lineIdx++;
}
}
mData.mLineStarts = new List<int32>();
mData.mLineStarts.GrowUnitialized(lineIdx + 2);
int32* lineStartsPtr = mData.mLineStarts.Ptr;
lineStartsPtr[0] = 0;
lineIdx = 0;
if (textFlagsPtr != null)
mData.mLineStarts = new List<int32>();
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)
{
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;
}
}
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;
}
}
mData.mLineStarts[lineIdx + 1] = mData.mTextLength;
LineStartsChanged();
}
public virtual void Backspace()
@ -1519,7 +1536,7 @@ namespace Beefy.widgets
ContentChanged();
if (offset != 0)
{
MoveCursorToIdx(textPos, false, .FromTyping);
MoveCursorToIdx(textPos, false, .FromTyping_Deleting);
EnsureCursorVisible();
}
}
@ -1648,7 +1665,7 @@ namespace Beefy.widgets
}
else
{
int32 char8Count = 1;
int32 charCount = 1;
int checkIdx = textPos + 1;
while (true)
{
@ -1660,12 +1677,12 @@ namespace Beefy.widgets
if (!checkChar.IsCombiningMark)
break;
}
char8Count++;
charCount++;
checkIdx++;
}
mData.mUndoManager.Add(new DeleteCharAction(this, 0, char8Count));
PhysDeleteChars(0, char8Count);
mData.mUndoManager.Add(new DeleteCharAction(this, 0, charCount));
PhysDeleteChars(0, charCount);
}
}
@ -1934,7 +1951,7 @@ namespace Beefy.widgets
return .Other;
}
public void GetTextCoordAtCursor(out float x, out float y)
public virtual void GetTextCoordAtCursor(out float x, out float y)
{
if (mVirtualCursorPos.HasValue)
{
@ -1963,8 +1980,9 @@ namespace Beefy.widgets
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);
float overflowX;
return GetLineCharAtCoord(line, x, out lineChar, out overflowX);
}
public virtual bool PrepareForCursorMove(int dir = 0)
@ -2064,6 +2082,7 @@ namespace Beefy.widgets
if (var insertTextAction = mData.mUndoManager.GetLastUndoAction() as InsertTextAction)
insertTextAction.mVirtualCursorPos = origPosition;
CursorLineAndColumn = lineStartPosition;
CursorToLineStart(false);
// Adjust to requested column
@ -2110,15 +2129,15 @@ namespace Beefy.widgets
while (true)
{
if (lineChar > 0)
MoveCursorTo(lineIdx, lineChar - 1);
MoveCursorTo(lineIdx, lineChar - 1, false, 0, .SelectLeft);
else if (lineIdx > 0)
{
int cursorIdx = mCursorTextPos;
String lineText = scope String();
GetLineText(lineIdx - 1, lineText);
MoveCursorTo(lineIdx - 1, (int32)lineText.Length);
MoveCursorTo(lineIdx - 1, (int32)lineText.Length, false, 0, .SelectLeft);
if ((!mAllowVirtualCursor) && (cursorIdx == mCursorTextPos))
MoveCursorTo(lineIdx - 1, (int32)lineText.Length - 1);
MoveCursorTo(lineIdx - 1, (int32)lineText.Length - 1, false, 0, .SelectLeft);
break;
}
@ -2180,9 +2199,9 @@ namespace Beefy.widgets
}
if (isWithinLine)
MoveCursorTo(lineIdx, lineChar + 1, false, 1);
MoveCursorTo(lineIdx, lineChar + 1, false, 1, .SelectRight);
else if (lineIdx < GetLineCount() - 1)
MoveCursorTo(lineIdx + 1, 0);
MoveCursorTo(lineIdx + 1, 0, false, 0, .SelectRight);
if (!mWidgetWindow.IsKeyDown(KeyCode.Control))
break;
@ -2274,7 +2293,7 @@ namespace Beefy.widgets
mEditWidget.Submit();
case KeyCode.Left:
{
if (!PrepareForCursorMove(-1))
if (!PrepareForCursorMove(-1))
{
PrepareForCursorMove(-1);
@ -2349,7 +2368,7 @@ namespace Beefy.widgets
var lineAndColumn = CursorLineAndColumn;
CursorLineAndColumn = LineAndColumn(lineAndColumn.mLine, lineAndColumn.mColumn + 1);
EnsureCursorVisible(true, false, false);
CursorMoved();
PhysCursorMoved(.SelectRight);
ClampCursor();
if (lineAndColumn != CursorLineAndColumn)
@ -2393,22 +2412,40 @@ namespace Beefy.widgets
wasMoveKey = true;
if ((lineIdx + aDir >= 0) && (lineIdx + aDir < GetLineCount()))
{
float wantedX = mCursorWantX;
float wantY = 0;
if (mAllowVirtualCursor)
{
float cursorX;
float cursorY;
GetTextCoordAtCursor(out cursorX, out cursorY);
GetLineAndColumnAtCoord(cursorX, cursorY, var virtLine, ?);
/*GetTextCoordAtLineAndColumn(lineIdx, 0, ?, var lineY);
Debug.WriteLine($"Line:{lineIdx} LineY:{lineY} Cursor:{cursorX},{cursorY}");*/
if (aDir < 0)
{
wantY = cursorY - 0.1f;
}
else
{
wantY = cursorY + GetLineHeight(virtLine) + 0.1f;
}
//mCursorWantX = cursorX;
}
else
{
lineIdx += aDir;
lineIdx += aDir;
float wantedX = mCursorWantX;
float aX;
float aY;
GetTextCoordAtLineChar(lineIdx, 0, out aX, out aY);
MoveCursorToCoord(mCursorWantX, aY);
float aX;
float aY;
GetTextCoordAtLineChar(lineIdx, 0, out aX, out aY);
wantY = aY;
}
MoveCursorToCoord(mCursorWantX, wantY);
ClampCursor();
@ -2645,6 +2682,12 @@ namespace Beefy.widgets
return false;
}
public virtual bool GetLineCharAtCoord(int line, float x, out int theChar, out float overflowX)
{
GetTextCoordAtLineAndColumn(line, 0, ?, var y);
return GetLineCharAtCoord(x, y, ?, out theChar, out overflowX);
}
public virtual bool GetLineAndColumnAtCoord(float x, float y, out int line, out int column)
{
line = -1;
@ -2725,7 +2768,7 @@ namespace Beefy.widgets
return;
}
if (c < 0)
lo = i + 1;
lo = i + 1;
else
hi = i - 1;
}
@ -2818,11 +2861,11 @@ namespace Beefy.widgets
ExtractString(lineStart, lineEnd - lineStart, outStr); // Full line
}
public int GetTextIdx(int line, int char8Idx)
public int GetTextIdx(int line, int charIdx)
{
GetTextData();
int useLine = Math.Min(line, mData.mLineStarts.Count - 1);
return mData.mLineStarts[useLine] + char8Idx;
return mData.mLineStarts[useLine] + charIdx;
}
public int GetCharIdIdx(int32 findCharId)
@ -2839,7 +2882,7 @@ namespace Beefy.widgets
int curLine = 0;
int curColumn = 0;
int char8Idx = 0;
int charIdx = 0;
mData.mTextIdData.Prepare();
while (true)
{
@ -2863,7 +2906,7 @@ namespace Beefy.widgets
if ((curLine == line) && (curColumn == column))
return char8Id;
char8 c = (char8)mData.mText[char8Idx++].mChar;
char8 c = (char8)mData.mText[charIdx++].mChar;
if (c == '\n')
{
if (curLine == line)
@ -2878,7 +2921,7 @@ namespace Beefy.widgets
}
}
public virtual void GetTextCoordAtLineChar(int line, int char8Idx, out float x, out float y)
public virtual void GetTextCoordAtLineChar(int line, int charIdx, out float x, out float y)
{
x = 0;
y = 0;
@ -2892,8 +2935,13 @@ namespace Beefy.widgets
public enum CursorMoveKind
{
FromTyping,
Unknown
case FromTyping;
case FromTyping_Deleting;
case Unknown;
case SelectRight;
case SelectLeft;
public bool IsFromTyping => (this == FromTyping) || (this == FromTyping_Deleting);
}
// We used to have a split between PhysCursorMoved and CursorMoved. CursorMoved has a "ResetWantX" and was non-virtual... uh-
@ -2936,7 +2984,7 @@ namespace Beefy.widgets
{
int lineIdx;
int lineChar;
GetCursorLineChar(out lineIdx, out lineChar);
GetCursorLineChar(out lineIdx, out lineChar);
String lineText = scope String();
GetLineText(lineIdx, lineText);
@ -3379,14 +3427,22 @@ namespace Beefy.widgets
Debug.Assert(mSelection.Value.mEndPos <= mData.mTextLength);
}
//public void MoveCursorTo
public virtual void GetLineAndColumnAtLineChar(int line, int lineChar, out int lineColumn)
{
float x;
float y;
GetTextCoordAtLineChar(line, lineChar, out x, out y);
public void MoveCursorTo(int line, int char8Idx, bool centerCursor = false, int movingDir = 0, CursorMoveKind cursorMoveKind = .Unknown)
int coordLine;
GetLineAndColumnAtCoord(x, y, out coordLine, out lineColumn);
}
public virtual void MoveCursorTo(int line, int charIdx, bool centerCursor = false, int movingDir = 0, CursorMoveKind cursorMoveKind = .Unknown)
{
int useCharIdx = char8Idx;
int useCharIdx = charIdx;
mShowCursorAtLineEnd = false;
CursorTextPos = GetTextIdx(line, char8Idx);
CursorTextPos = GetTextIdx(line, charIdx);
// Skip over UTF8 parts AND unicode combining marks (ie: when we have a letter with an accent mark following it)
while (true)
@ -3433,13 +3489,13 @@ namespace Beefy.widgets
public void MoveCursorToIdx(int index, bool centerCursor = false, CursorMoveKind cursorMoveKind = .Unknown)
{
int aLine;
int aCharIdx;
GetLineCharAtIdx(index, out aLine, out aCharIdx);
MoveCursorTo(aLine, aCharIdx, centerCursor, 0, cursorMoveKind);
int line;
int charIdx;
GetLineCharAtIdx(index, out line, out charIdx);
MoveCursorTo(line, charIdx, centerCursor, 0, cursorMoveKind);
}
public void MoveCursorToCoord(float x, float y)
public virtual void MoveCursorToCoord(float x, float y)
{
bool failed = false;