vn/code/vn_text_op.h

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;
}
//- sixten: handle insertions
{
b32 InsertedSomething = false;
if(Action->Codepoint != 0)
{
Op.ReplaceString = StringFromCodepoint(Arena, Action->Codepoint);
InsertedSomething = true;
}
else if(Action->Flags & TextActionFlag_Paste)
{
Op.ReplaceString = RemoveAll(Arena, Platform.GetClipboard(Arena), '\r');;
InsertedSomething = true;
}
if(InsertedSomething)
{
range1_s64 Selection = Range1S64(State->Cursor, State->Mark);
Op.Range = Selection;
Op.NewCursor = Op.NewMark = Selection.Min+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