/* 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(memory_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; } #if 0 u8 *LineBegin = Line.Data; u8 *LineEnd = LineBegin+Line.Count; u8 *Char = (Action->Delta > 0)?LineBegin:LineEnd; if(Action->Delta > 0) { for(;Char <= LineEnd; Char += 1) { if(Char == LineEnd) { goto FoundLine; } if(!IsWhitespace(*Char)) { break; } } } else if(Action->Delta < 0) { for(;LineBegin <= Char; Char -= 1) { if(!IsWhitespace(*Char)) { break; } if(Char == LineBegin) { goto FoundLine; } } } #endif } } 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