520 lines
14 KiB
C
520 lines
14 KiB
C
/* date = May 7th 2023 7:16 pm */
|
|
|
|
#ifndef VN_TEXT_OP_H
|
|
#define VN_TEXT_OP_H
|
|
|
|
struct text_op
|
|
{
|
|
range1_s64 Range;
|
|
string ReplaceString;
|
|
string CopyString;
|
|
s64 NewCursor;
|
|
s64 NewMark;
|
|
};
|
|
|
|
struct text_edit_state
|
|
{
|
|
s64 Cursor;
|
|
s64 Mark;
|
|
};
|
|
|
|
typedef u32 text_action_flags;
|
|
enum
|
|
{
|
|
TextActionFlag_WordScan = (1<<0),
|
|
TextActionFlag_KeepMark = (1<<1),
|
|
TextActionFlag_Delete = (1<<2),
|
|
TextActionFlag_ZeroDeltaWithSelection = (1<<3),
|
|
TextActionFlag_DeltaPicksSelectionSide = (1<<4),
|
|
TextActionFlag_Copy = (1<<5),
|
|
TextActionFlag_Paste = (1<<6),
|
|
TextActionFlag_OperateOnLine = (1<<7),
|
|
TextActionFlag_StopOnNewline = (1<<8),
|
|
|
|
TextActionFlag_EmptyLineScan = TextActionFlag_WordScan,
|
|
};
|
|
|
|
struct text_action
|
|
{
|
|
text_action_flags Flags;
|
|
s64 Delta;
|
|
u32 Codepoint;
|
|
};
|
|
|
|
inline b32 IsValid(text_action *Action)
|
|
{
|
|
b32 Result = !(Action->Flags == 0 && Action->Delta == 0 && Action->Codepoint == 0);
|
|
return(Result);
|
|
}
|
|
|
|
static text_action SingleLineTextActionFromEvent(platform_event *Event)
|
|
{
|
|
text_action Action = {};
|
|
|
|
if(Event->Type == PlatformEvent_Text)
|
|
{
|
|
if(Event->Codepoint != '\n')
|
|
{
|
|
Action.Codepoint = Event->Codepoint;
|
|
}
|
|
}
|
|
else if(Event->Type == PlatformEvent_Press)
|
|
{
|
|
if(Event->Modifiers & PlatformModifier_Ctrl)
|
|
{
|
|
Action.Flags |= TextActionFlag_WordScan;
|
|
}
|
|
if(Event->Modifiers & PlatformModifier_Shift)
|
|
{
|
|
Action.Flags |= TextActionFlag_KeepMark;
|
|
}
|
|
|
|
switch(Event->Key)
|
|
{
|
|
case Key_Right:
|
|
{
|
|
Action.Delta = +1;
|
|
Action.Flags |= TextActionFlag_DeltaPicksSelectionSide;
|
|
} break;
|
|
|
|
case Key_Left:
|
|
{
|
|
Action.Delta = -1;
|
|
Action.Flags |= TextActionFlag_DeltaPicksSelectionSide;
|
|
} break;
|
|
|
|
case Key_Home: { Action.Delta = S64_Min; } break;
|
|
case Key_End: { Action.Delta = S64_Max; } break;
|
|
|
|
case Key_Backspace:
|
|
{
|
|
Action.Delta = -1;
|
|
Action.Flags |= TextActionFlag_Delete|TextActionFlag_ZeroDeltaWithSelection;
|
|
} break;
|
|
|
|
case Key_Delete:
|
|
{
|
|
Action.Delta = +1;
|
|
Action.Flags |= TextActionFlag_Delete|TextActionFlag_ZeroDeltaWithSelection;
|
|
} break;
|
|
|
|
case Key_C: if(Event->Modifiers & PlatformModifier_Ctrl)
|
|
{
|
|
Action.Flags |= TextActionFlag_Copy;
|
|
} break;
|
|
|
|
case Key_V: if(Event->Modifiers & PlatformModifier_Ctrl)
|
|
{
|
|
Action.Flags |= TextActionFlag_Paste;
|
|
} break;
|
|
|
|
case Key_X: if(Event->Modifiers & PlatformModifier_Ctrl)
|
|
{
|
|
Action.Flags |= TextActionFlag_Copy|TextActionFlag_Delete;
|
|
} break;
|
|
|
|
default: {} break;
|
|
}
|
|
}
|
|
return(Action);
|
|
}
|
|
|
|
static text_action MultiLineTextActionFromEvent(platform_event *Event)
|
|
{
|
|
text_action Action = {};
|
|
|
|
if(Event->Type == PlatformEvent_Text)
|
|
{
|
|
Action.Codepoint = Event->Codepoint;
|
|
}
|
|
else if(Event->Type == PlatformEvent_Press)
|
|
{
|
|
if(Event->Modifiers & PlatformModifier_Ctrl)
|
|
{
|
|
Action.Flags |= TextActionFlag_WordScan;
|
|
}
|
|
if(Event->Modifiers & PlatformModifier_Shift)
|
|
{
|
|
Action.Flags |= TextActionFlag_KeepMark;
|
|
}
|
|
|
|
switch(Event->Key)
|
|
{
|
|
case Key_Right:
|
|
{
|
|
Action.Delta = +1;
|
|
Action.Flags |= TextActionFlag_DeltaPicksSelectionSide;
|
|
} break;
|
|
|
|
case Key_Left:
|
|
{
|
|
Action.Delta = -1;
|
|
Action.Flags |= TextActionFlag_DeltaPicksSelectionSide;
|
|
} break;
|
|
|
|
case Key_Down:
|
|
{
|
|
Action.Delta = +1;
|
|
Action.Flags |= TextActionFlag_OperateOnLine;
|
|
} break;
|
|
|
|
case Key_Up:
|
|
{
|
|
Action.Delta = -1;
|
|
Action.Flags |= TextActionFlag_OperateOnLine;
|
|
} break;
|
|
|
|
case Key_Home:
|
|
{
|
|
Action.Delta = S64_Min;
|
|
Action.Flags |= TextActionFlag_StopOnNewline;
|
|
} break;
|
|
|
|
case Key_End:
|
|
{
|
|
Action.Delta = S64_Max;
|
|
Action.Flags |= TextActionFlag_StopOnNewline;
|
|
} break;
|
|
|
|
case Key_Backspace:
|
|
{
|
|
Action.Delta = -1;
|
|
Action.Flags |= TextActionFlag_Delete|TextActionFlag_ZeroDeltaWithSelection;
|
|
} break;
|
|
|
|
case Key_Delete:
|
|
{
|
|
Action.Delta = +1;
|
|
Action.Flags |= TextActionFlag_Delete|TextActionFlag_ZeroDeltaWithSelection;
|
|
} break;
|
|
|
|
case Key_C: if(Event->Modifiers & PlatformModifier_Ctrl)
|
|
{
|
|
Action.Flags |= TextActionFlag_Copy;
|
|
} break;
|
|
|
|
case Key_V: if(Event->Modifiers & PlatformModifier_Ctrl)
|
|
{
|
|
Action.Flags |= TextActionFlag_Paste;
|
|
} break;
|
|
|
|
case Key_X: if(Event->Modifiers & PlatformModifier_Ctrl)
|
|
{
|
|
Action.Flags |= TextActionFlag_Copy|TextActionFlag_Delete;
|
|
} break;
|
|
|
|
default: {} break;
|
|
}
|
|
}
|
|
return(Action);
|
|
}
|
|
|
|
inline s64 CodepointScan(string String, s64 Index, s64 Delta, b32 StopOnNewline)
|
|
{
|
|
s64 Result = 0;
|
|
if(Delta > 0)
|
|
{
|
|
while(Index < String.Count && Delta)
|
|
{
|
|
u8 Base = String.Data[Index];
|
|
s64 ToMove = 0;
|
|
|
|
if(StopOnNewline && Base == '\n')
|
|
{
|
|
break;
|
|
}
|
|
|
|
if((Base & 0x80) == 0x00)
|
|
{
|
|
ToMove = 1;
|
|
}
|
|
else if((Base & 0xE0) == 0xC0)
|
|
{
|
|
ToMove = 2;
|
|
}
|
|
else if((Base & 0xF0) == 0xE0)
|
|
{
|
|
ToMove = 3;
|
|
}
|
|
else if((Base & 0xF8) == 0xF0)
|
|
{
|
|
ToMove = 4;
|
|
}
|
|
|
|
Result += ToMove;
|
|
Index += ToMove;
|
|
|
|
--Delta;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Index -= 1;
|
|
|
|
while(Index >= 0 && (Delta != 0))
|
|
{
|
|
u8 Base = String.Data[Index];
|
|
|
|
if(StopOnNewline && Base == '\n')
|
|
{
|
|
break;
|
|
}
|
|
|
|
|
|
if(((Base & 0x80) == 0) || !((Base & 0xC0) == 0x80))
|
|
{
|
|
++Delta;
|
|
}
|
|
|
|
--Result;
|
|
--Index;
|
|
}
|
|
}
|
|
|
|
return(Result);
|
|
}
|
|
|
|
inline b32 IsWordBoundary(string String, s64 Index)
|
|
{
|
|
b32 Result;
|
|
if(Index > 0)
|
|
{
|
|
Result = IsWhitespace(String.Data[Index - 1]) && !(IsWhitespace(String.Data[Index]));
|
|
}
|
|
else
|
|
{
|
|
Result = true;
|
|
}
|
|
return(Result);
|
|
}
|
|
|
|
static s64 WordScan(string String, s64 Index, s64 Delta)
|
|
{
|
|
s64 Result = 0;
|
|
|
|
while(Delta)
|
|
{
|
|
if(Delta > 0)
|
|
{
|
|
++Index;
|
|
++Result;
|
|
|
|
while(Index < String.Count && !IsWordBoundary(String, Index))
|
|
{
|
|
++Index;
|
|
++Result;
|
|
}
|
|
|
|
if(Index > String.Count)
|
|
{
|
|
Result -= Index - String.Count;
|
|
Index = String.Count;
|
|
goto End;
|
|
}
|
|
|
|
--Delta;
|
|
}
|
|
else
|
|
{
|
|
--Index;
|
|
--Result;
|
|
|
|
while(Index >= 0 && !IsWordBoundary(String, Index))
|
|
{
|
|
--Index;
|
|
--Result;
|
|
}
|
|
|
|
if(Index < 0)
|
|
{
|
|
Result -= Index;
|
|
Index = 0;
|
|
goto End;
|
|
}
|
|
|
|
++Delta;
|
|
}
|
|
}
|
|
|
|
End:
|
|
return(Result);
|
|
}
|
|
|
|
static b32 StringIsWhitespace(string String)
|
|
{
|
|
b32 Result = true;
|
|
|
|
u8 *StringBegin = String.Data;
|
|
u8 *StringEnd = StringBegin+String.Count;
|
|
|
|
for(u8 *Char = StringBegin; Char < StringEnd; Char += 1)
|
|
{
|
|
if(!IsWhitespace(*Char) && *Char != '\0')
|
|
{
|
|
Result = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return(Result);
|
|
}
|
|
|
|
static text_op TextOpFromAction(arena *Arena, string String,
|
|
text_edit_state *State, text_action *Action,
|
|
range1_s64_array *Lines = 0, s64 LastColumnIndex = 0)
|
|
{
|
|
text_op Op = {};
|
|
|
|
Op.NewCursor = State->Cursor;
|
|
Op.NewMark = State->Mark;
|
|
Op.Range = Range1S64(0, 0);
|
|
Op.ReplaceString = StrLit("");
|
|
|
|
//- sixten: navtigation
|
|
s64 Delta = 0;
|
|
if(Action->Flags & TextActionFlag_OperateOnLine)
|
|
{
|
|
//- sixten: determine what line we are on.
|
|
s64 LineIndex = 0;
|
|
s64 ColumnIndex = 0;
|
|
{
|
|
u8 *TextBegin = String.Data;
|
|
u8 *TextEnd = TextBegin + State->Cursor;
|
|
u8 *Char = TextBegin;
|
|
for(;Char < TextEnd; Char += 1)
|
|
{
|
|
ColumnIndex += 1;
|
|
if(*Char == '\n')
|
|
{
|
|
LineIndex += 1;
|
|
ColumnIndex = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
s64 LineDelta = Action->Delta;
|
|
|
|
if(Action->Flags & TextActionFlag_EmptyLineScan)
|
|
{
|
|
// sixten: if the line we start on is blank, we want to make sure that there has been a text containing non-whitespace before stopping.
|
|
b32 IgnoreFirstWhitespace = StringIsWhitespace(Substring(String, Lines->Ranges[LineIndex]));
|
|
|
|
for(;0 <= LineIndex && LineIndex < Lines->Count; LineIndex += Action->Delta)
|
|
{
|
|
string Line = Substring(String, Lines->Ranges[LineIndex+Action->Delta]);
|
|
if(StringIsWhitespace(Line))
|
|
{
|
|
if(!IgnoreFirstWhitespace)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
IgnoreFirstWhitespace = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
u64 ColumnOffset = Max(LastColumnIndex - ColumnIndex, 0LLU);
|
|
|
|
//- sixten: check that the line we are trying to access is inbounds, else just go to the start or end of the text.
|
|
if(InRange(Range1S64(0, Lines->Count), LineIndex + LineDelta))
|
|
{
|
|
Delta = Lines->Ranges[LineIndex+LineDelta].Min - Lines->Ranges[LineIndex].Min + ColumnOffset;
|
|
if(!InRange(Lines->Ranges[LineIndex+LineDelta], State->Cursor + Delta))
|
|
{
|
|
Delta = Lines->Ranges[LineIndex+LineDelta].Max - State->Cursor - 1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
s64 TempDelta = (LineDelta > 0)?S64_Max:S64_Min;
|
|
Delta = CodepointScan(String, State->Cursor, TempDelta, Action->Flags & TextActionFlag_StopOnNewline);
|
|
}
|
|
}
|
|
|
|
else
|
|
{
|
|
if(Action->Flags & TextActionFlag_WordScan)
|
|
{
|
|
Delta = WordScan(String, State->Cursor, Action->Delta);
|
|
}
|
|
else
|
|
{
|
|
Delta = CodepointScan(String, State->Cursor, Action->Delta, Action->Flags & TextActionFlag_StopOnNewline);
|
|
}
|
|
}
|
|
if(State->Cursor != State->Mark &&
|
|
Action->Flags & TextActionFlag_ZeroDeltaWithSelection)
|
|
{
|
|
Delta = 0;
|
|
}
|
|
|
|
if(State->Cursor != State->Mark &&
|
|
Action->Flags & TextActionFlag_DeltaPicksSelectionSide &&
|
|
!(Action->Flags & TextActionFlag_KeepMark))
|
|
{
|
|
Delta = 0;
|
|
if(Action->Delta > 0)
|
|
{
|
|
Op.NewCursor = Maximum(State->Cursor, State->Mark);
|
|
}
|
|
else if(Action->Delta < 0)
|
|
{
|
|
Op.NewCursor = Minimum(State->Cursor, State->Mark);
|
|
}
|
|
}
|
|
|
|
Op.NewCursor = State->Cursor + Delta;
|
|
|
|
//- sixten: post-navigation
|
|
|
|
if(Action->Flags & TextActionFlag_Copy)
|
|
{
|
|
string CopyString = Substring(String, Range1S64(Op.NewCursor, Op.NewMark));
|
|
Platform.SetClipboard(CopyString);
|
|
}
|
|
|
|
if(Action->Flags & TextActionFlag_Delete)
|
|
{
|
|
Op.Range = Range1S64(Op.NewCursor, Op.NewMark);
|
|
Op.NewCursor = Op.NewMark = Op.Range.Min;
|
|
}
|
|
|
|
if(Action->Codepoint != 0)
|
|
{
|
|
Op.ReplaceString = StringFromCodepoint(Arena, Action->Codepoint);
|
|
|
|
if(State->Cursor == State->Mark)
|
|
{
|
|
Op.NewCursor += Op.ReplaceString.Count;
|
|
Op.Range = Range1S64(State->Cursor, State->Cursor);
|
|
}
|
|
else
|
|
{
|
|
Op.NewCursor += Op.ReplaceString.Count;
|
|
Op.Range = Range1S64(State->Cursor, State->Mark);
|
|
}
|
|
}
|
|
else if(Action->Flags & TextActionFlag_Paste)
|
|
{
|
|
Op.ReplaceString = Platform.GetClipboard(Arena);
|
|
Op.Range = Range1S64(State->Cursor, State->Cursor);
|
|
Op.NewCursor += Op.ReplaceString.Count;
|
|
}
|
|
|
|
if(!(Action->Flags & TextActionFlag_KeepMark))
|
|
{
|
|
Op.NewMark = Op.NewCursor;
|
|
}
|
|
|
|
s64 NewStringCount = String.Count-DimOfRange(Op.Range)+Op.ReplaceString.Count;
|
|
Op.NewCursor = Clamp(Op.NewCursor, 0, NewStringCount);
|
|
Op.NewMark = Clamp(Op.NewMark, 0, NewStringCount);
|
|
|
|
return(Op);
|
|
}
|
|
|
|
#endif //VN_TEXT_OP_H
|