//////////////////////////////// //~ sixten: Mutable String Functions static mutable_string MutableStringAllocate(u64 Size) { mutable_string Result = {}; Result.Arena = ArenaAllocate(Size); ArenaSetAlign(Result.Arena, 1); Result.String = MakeString(PushArray(Result.Arena, u8, 1), 0); return(Result); } static void MutableStringRelease(mutable_string *String) { ArenaRelease(String->Arena); } static void MutableStringReplaceRange(mutable_string *MutString, string ReplaceString, range1_s64 Range) { Range = Intersection(Range, Range1S64(0, MutString->String.Count)); s64 NewCount = MutString->String.Count+ReplaceString.Count-DimOfRange(Range); if(NewCount > MutString->String.Count) { s64 ToAllocate = NewCount-MutString->String.Count; PushArray(MutString->Arena, u8, ToAllocate); } Move(MutString->String.Data+Range.Min+ReplaceString.Count, MutString->String.Data+Range.Max, MutString->String.Count-DimOfRange(Range)); Copy(MutString->String.Data+Range.Min, ReplaceString.Data, ReplaceString.Count); MutString->String.Count = NewCount; } //////////////////////////////// //~ sixten: Workspace Text Editor Functions static workspace_text_data Workspace_TextDataFromStringChunkList(memory_arena *Arena, string Text) { temporary_memory Scratch = GetScratch(&Arena, 1); //- sixten: tokenize the text tokenize_result TokenizeResult = T_TokenizeFromText(Arena, StrLit("*scratch*"), Text); token_array Tokens = TokenizeResult.Tokens; //- sixten: gather all line ranges range1_s64_list Lines = {}; { u8 *TextBegin = Text.Data; u8 *TextEnd = TextBegin + Text.Count; range1_s64 Range = Range1S64(0, 0); for(u8 *Char = TextBegin; Char <= TextEnd; Char += 1) { Range.Max += 1; //- sixten: push line range on newline and EOF if(Char == TextEnd || *Char == '\n') { Range1S64ListPush(Scratch.Arena, &Lines, Range); Range = Range1S64(Range.Max, Range.Max); } } } //- sixten: fill & return workspace_text_data Result = {}; { Result.Tokens = Tokens; Result.Lines = Range1S64ArrayFromList(Arena, &Lines);; } ReleaseScratch(Scratch); return(Result); } static UI_CUSTOM_DRAW_CALLBACK(Workspace_TextEditorDrawCallback) { temporary_memory Scratch = GetScratch(); workspace_view_text_editor *Editor = (workspace_view_text_editor *)Data; //- sixten: get dimensions & scroll offset from container ui_box *ContainerBox = Editor->ContainerBox; range2_r32 ParentRect = ContainerBox->Rect; v2 ParentDim = DimOfRange(ParentRect); v2 Offset = Box->Parent->Offset; //- sixten: rendering properties r32 FontSize = 16.0f; r32 LineHeight = FontSize + 4.0f; //- sixten: calculate the dimensions of the glyphs glyph *Glyph = GetGlyph(Atlas, Font_Monospace, 'A', FontSize, 0); r32 GlyphAdvance = Glyph->Advance; //- sixten: find the text point text_point CursorTextP = TextPointFromOffset(Editor->Text.String, Editor->EditState.Cursor); text_point MarkTextP = TextPointFromOffset(Editor->Text.String, Editor->EditState.Mark); //- sixten: get the line count range1_s64_array *Lines = &Editor->Lines; s64 LineCount = Lines->Count; //- sixten: calculate the text dim Editor->TextDim = V2(1900, LineCount*LineHeight); //- sixten: calculate the line margin dim s32 LineMarginDigitsRequired = 6; v2_r32 LineMarginDim = V2((LineMarginDigitsRequired)*GlyphAdvance, ParentRect.Max.y - ParentRect.Min.y); //- sixten: tokenize text tokenize_result TokenizeResult = T_TokenizeFromText(Scratch.Arena, StrLit("nobody cares"), Editor->Text.String); token_array Tokens = TokenizeResult.Tokens; token *TokensBegin = Tokens.Tokens; token *TokensEnd = TokensBegin + Tokens.Count; //- sixten: find the first visible token token *VisibleTokensBegin = TokensBegin; s64 TopMostLine = Min((s64)Floor(-Offset.y / LineHeight), LineCount); for(s64 LinesFound = 0; LinesFound < TopMostLine && VisibleTokensBegin < TokensEnd; VisibleTokensBegin += 1) { if(VisibleTokensBegin->Flags & TokenFlag_Newline) { LinesFound += 1; } } //- sixten: find the last visible token token *VisibleTokensEnd = VisibleTokensBegin; s64 LinesOnScreen = Min((s64)Floor(ParentDim.y / LineHeight)+1, LineCount-TopMostLine); for(s64 LinesFound = 0; LinesFound < LinesOnScreen && VisibleTokensEnd < TokensEnd; VisibleTokensEnd += 1) { if(VisibleTokensEnd->Flags & TokenFlag_Newline) { LinesFound += 1; } } //- sixten: draw line numbers & line highlights { //- sixten: draw the background v4 LineMarginColor = ColorFromHex(0x10203080); range2_r32 LineMarginBox = Range2R32(ParentRect.Min, ParentRect.Min+LineMarginDim); PushQuad(Group, LineMarginBox, LineMarginColor, LineMarginColor, LineMarginColor, LineMarginColor, 0, 0, 0); //- sixten: draw the numbers v2_r32 LineOffset = Box->Rect.Min; for(s64 LineIndex = TopMostLine; LineIndex < TopMostLine + LinesOnScreen; LineIndex += 1) { r32 LineY = LineOffset.y + LineIndex*LineHeight; PushTextF(Group, Atlas, Font_Monospace, V2(0, LineY), FontSize, Color_Grey, "%*.i", LineMarginDigitsRequired, LineIndex+1); if(LineIndex + 1 == CursorTextP.Line) { v4_r32 LineHighlightColor = ColorFromHex(0x10204080); range2_r32 LineHighlightBox = Range2R32(V2(LineMarginBox.Max.x, LineY), V2(Box->Rect.Max.x, LineY+LineHeight)); PushQuad(Group, LineHighlightBox, LineHighlightColor, LineHighlightColor, LineHighlightColor, LineHighlightColor, 0, 0, 0); } } } //- sixten: render tokens v2 BaseTokenP = Box->Rect.Min+V2(LineMarginDim.x, TopMostLine*LineHeight); v2 TokenP = BaseTokenP; for(token *Token = VisibleTokensBegin; Token < VisibleTokensEnd; Token += 1) { string TokenString = T_StringFromToken(Editor->Text.String, *Token); //- sixten: get color from token v4 Color = Color_Magenta; if(Token->Flags & TokenGroup_Comment) { Color = Color_Grey; } else if(Token->Flags & TokenFlag_Reserved) { Color = Color_Grey; } else if(Token->Flags & TokenFlag_Symbol) { Color = ColorFromHex(0xbd2d2dff); } else if(Token->Flags & TokenFlag_StringLiteral) { Color = ColorFromHex(0xffa900ff); } else if(Token->Flags & TokenFlag_Numeric) { Color = ColorFromHex(0xffa900ff); } else if(Token->Flags & TokenFlag_Identifier) { //- sixten: check for keywords if(AreEqual(TokenString, StrLit("var")) || AreEqual(TokenString, StrLit("proc")) || AreEqual(TokenString, StrLit("branch")) || AreEqual(TokenString, StrLit("jump")) || AreEqual(TokenString, StrLit("if")) || AreEqual(TokenString, StrLit("true")) || AreEqual(TokenString, StrLit("false"))) { Color = ColorFromHex(0xf0c674ff); } else { Color = Theme_TextColor; } } //- sixten: render & advance by token if(!(Token->Flags & TokenGroup_Whitespace)) { TokenP.x += PushText(Group, Atlas, Font_Monospace, TokenP, FontSize, Color, TokenString); } else { if(Token->Flags & TokenFlag_Newline) { TokenP.x = BaseTokenP.x; TokenP.y += LineHeight; } else { u8 *StringBegin = TokenString.Data; u8 *StringEnd = StringBegin + TokenString.Count; for(u8 *Char = StringBegin; Char < StringEnd; Char += 1) { if(*Char == ' ' || *Char == '\t') { TokenP.x += GlyphAdvance; } } } } } //- sixten: render cursor { v2 TargetCursorP = Box->Rect.Min+V2(LineMarginDim.x+(CursorTextP.Column-1)*GlyphAdvance,(CursorTextP.Line-1)*LineHeight); v2 CursorP = V2(AnimationCurve_AnimateValueF(TargetCursorP.x, TargetCursorP.x, 0.1, "Workspace Text Editor Cursor X %p", Editor), AnimationCurve_AnimateValueF(TargetCursorP.y, TargetCursorP.y, 0.1, "Workspace Text Editor Cursor Y %p", Editor)); v2 CursorDim = V2(2, LineHeight); range2_r32 CursorRect = Range2R32(CursorP, CursorP+CursorDim); v4 CursorColor = ColorFromHex(0x10FF20FF); PushQuad(Group, CursorRect, CursorColor, CursorColor, CursorColor, CursorColor, 2, 0.4, 0); } //- sixten: render the selection { text_range Selection = TextRange(CursorTextP, MarkTextP); range1_s64 LineRange = Range1S64(Selection.Min.Line, Selection.Max.Line); for(s64 Line = TopMostLine; Line < TopMostLine + LinesOnScreen; Line += 1) { r32 LineY = Box->Rect.Min.y + Line*LineHeight; if(Contains(LineRange, Line + 1)) { range1_s64 ColumnRange = Lines->Ranges[Line]; range1_s64 NormalizedColumnRange = Range1S64(0, DimOfRange(ColumnRange)); if(Line + 1 == LineRange.Min && Line + 1 == LineRange.Max) { NormalizedColumnRange = Range1S64(Editor->EditState.Cursor - ColumnRange.Min, Editor->EditState.Mark - ColumnRange.Min); } else if(Line + 1 == LineRange.Min) { NormalizedColumnRange = Range1S64(Min(Editor->EditState.Mark, Editor->EditState.Cursor) - ColumnRange.Min, DimOfRange(ColumnRange)); } else if(Line + 1 == LineRange.Max) { NormalizedColumnRange = Range1S64(0, Max(Editor->EditState.Mark, Editor->EditState.Cursor) - ColumnRange.Min); } v4_r32 LineHighlightColor = ColorFromHex(0x66B3CC4C); range2_r32 LineHighlightBox = Range2R32(V2(LineMarginDim.x+NormalizedColumnRange.Min*GlyphAdvance, LineY), V2(LineMarginDim.x+NormalizedColumnRange.Max*GlyphAdvance, LineY+LineHeight)); PushQuad(Group, LineHighlightBox, LineHighlightColor, LineHighlightColor, LineHighlightColor, LineHighlightColor, 4, 1.4, 0); } } } ReleaseScratch(Scratch); } static void Workspace_BuildTextEditor(workspace_view *View) { workspace_view_text_editor *Editor = (workspace_view_text_editor *)View->Data; temporary_memory Scratch = GetScratch(); //- sixten: rendering properties r32 FontSize = 16.0f; r32 LineHeight = FontSize + 4.0f; //- sixten: calculate the dimensions of the glyphs glyph *Glyph = GetGlyph(UI_GlyphAtlas(), Font_Monospace, 'A', FontSize, 0); r32 GlyphAdvance = Glyph->Advance; //- sixten: calculate the line margin dim s32 LineMarginDigitsRequired = 6; r32 LineMarginWidth = (LineMarginDigitsRequired)*GlyphAdvance; ui_box *EditorBox = 0; UI_SetNextSize(UI_Percent(1, 1), UI_Percent(1, 1)); UI_Scroll(0, &Editor->Offset.y) { //- sixten: find the container box for the scrollable region Editor->ContainerBox = UI_TopParent()->Parent->Parent; UI_SetNextSize(UI_Pixels(Editor->TextDim.x, 1), UI_Pixels(Editor->TextDim.y, 1)); EditorBox = UI_MakeBoxF(UI_BoxFlag_DrawBackground|UI_BoxFlag_Clickable, "Workspace Text Editor %p", View); EditorBox->DrawCallback = Workspace_TextEditorDrawCallback; EditorBox->DrawCallbackData = Editor; } b32 CursorHasBeenModified = false; //- sixten: keyboard input -> text op plus handling if(Workspace_ViewIsCurrent(View)) { for(platform_event *Event = UI_EventList()->First; Event != 0; Event = Event->Next) { if(Event->Type == PlatformEvent_Press || Event->Type == PlatformEvent_Text) { text_action Action = MultiLineTextActionFromEvent(Event); if(IsValid(&Action)) { text_op Op = TextOpFromAction(Scratch.Arena, Editor->Text.String, &Editor->EditState, &Action, &Editor->Lines, Editor->LastTextPoint.Column - 1); CursorHasBeenModified = true; { MutableStringReplaceRange(&Editor->Text, Op.ReplaceString, Op.Range); ArenaClear(Editor->ProcessingArena); workspace_text_data TextData = Workspace_TextDataFromStringChunkList(Editor->ProcessingArena, Editor->Text.String); Editor->Tokens = TextData.Tokens; Editor->Lines = TextData.Lines; } Editor->EditState.Cursor = Op.NewCursor; Editor->EditState.Mark = Op.NewMark; } } } } ui_signal Signal = UI_SignalFromBox(EditorBox); if(Signal.Dragging) { if(Signal.Pressed) { //- sixten: translate mouse position to text point v2 MouseOffset = Signal.MouseP - EditorBox->Rect.Min - V2(LineMarginWidth, 0); text_point Point = {(s64)(MouseOffset.y / LineHeight) + 1, (s64)(MouseOffset.x / GlyphAdvance) + 1}; Editor->EditState.Cursor = Editor->EditState.Mark = OffsetFromTextPoint(Editor->Text.String, Editor->Lines, Point); } //- sixten: translate mouse position to text point v2 MouseOffset = Signal.MouseP - EditorBox->Rect.Min - V2(LineMarginWidth, 0); text_point Point = {(s64)(MouseOffset.y / LineHeight) + 1, (s64)(MouseOffset.x / GlyphAdvance) + 1}; Editor->EditState.Cursor = OffsetFromTextPoint(Editor->Text.String, Editor->Lines, Point); CursorHasBeenModified = true; } //- sixten: update eventual text point extents if(CursorHasBeenModified) { text_point Point = TextPointFromOffset(Editor->Text.String, Editor->EditState.Cursor); if(Editor->LastTextPoint.Line == Point.Line) { Editor->LastTextPoint = Point; } else { Editor->LastTextPoint.Line = Point.Line; Editor->LastTextPoint.Column = Max(Editor->LastTextPoint.Column, Point.Column); } } ReleaseScratch(Scratch); }