//////////////////////////////// //~ 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), 0LL); 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; PushArrayNoClear(MutString->Arena, u8, ToAllocate); } else if(NewCount < MutString->String.Count) { ArenaPopTo(MutString->Arena, sizeof(memory_arena)+NewCount+1); } Move(MutString->String.Data+Range.Min+ReplaceString.Count, MutString->String.Data+Range.Max, MutString->String.Count-Range.Min-DimOfRange(Range)); Copy(MutString->String.Data+Range.Min, ReplaceString.Data, ReplaceString.Count); MutString->String.Count = NewCount; MutString->String.Data[NewCount] = 0; } //////////////////////////////// //~ sixten: History & Undo Functions static history_entry HistoryEntry(memory_arena *Arena, string ReplaceString, range1_s64 Range) { // sixten(TODO): proper memory management, right now we just keep appending to the arena, which works but never frees any memory history_entry Entry = {}; Entry.ReplaceString = PushString(Arena, ReplaceString); Entry.Range = Range; return(Entry); } static void AppendToHistory(memory_arena *Arena, history_list *List, history_entry Forward, history_entry Backward) { // sixten(TODO): proper memory management, right now we just keep appending to the arena, which works but never frees any memory history_node *Node = PushStructNoClear(Arena, history_node); Node->Forward = Forward; Node->Backward = Backward; SenDLLInsertLast(&List->Sentinel, Node); } //////////////////////////////// //~ sixten: Workspace Text Editing Functions static workspace_text_data W_TextDataFromString(memory_arena *Arena, string Text) { temporary_memory Scratch = GetScratch(&Arena, 1); //- sixten: clear the target arena ArenaClear(Arena); //- sixten: tokenize the text tokenize_result TokenizeResult = T_TokenizeFromText(Arena, 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 void W_TextEditorApplyChanges(workspace_view_text_editor *Editor) { workspace_text_data TextData = W_TextDataFromString(Editor->ProcessingArena, Editor->Text.String); Editor->Tokens = TextData.Tokens; Editor->Lines = TextData.Lines; Editor->Compiled = S_ScriptFromText(Editor->ProcessingArena, Editor->Text.String); if(Editor->Compiled.IsValid) { SV_SetCurrentSource(&Editor->Compiled); } } static void W_SaveTextEditorToFile(workspace_view_text_editor *Editor) { temporary_memory Scratch = GetScratch(); if(Editor->SavePoint != Editor->History.At) { string Path = PushFormat(Scratch.Arena, "%S/%S", Editor->FilePath, Editor->FileName); platform_file_handle Handle = Platform.OpenFile(Path, PlatformAccess_Write); if(Handle.IsValid) { Platform.WriteFile(Handle, Editor->Text.String.Data, 0, Editor->Text.String.Count); Platform.CloseFile(Handle); } Editor->SavePoint = Editor->History.At; } ReleaseScratch(Scratch); } //////////////////////////////// //~ sixten: Workspace Text Editor Builder Functions static UI_CUSTOM_DRAW_CALLBACK(W_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 = Editor->FontSize; 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, 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->Kind == TokenKind_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->Kind == TokenKind_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 & font from token font_id Font = Font_Monospace; v4 Color = Color_Magenta; if(Token->Kind == TokenKind_Comment) { Color = Color_Grey; Font = Font_MonospaceOblique; } else if(Token->Kind > TokenKind_SymbolsBegin && Token->Kind < TokenKind_SymbolsEnd) { Color = Color_Grey; } else if(Token->Kind == TokenKind_StringLiteral) { Color = ColorFromHex(0xffa900ff); } else if(Token->Kind == TokenKind_Numeric) { Color = ColorFromHex(0xffa900ff); } else if(Token->Kind > TokenKind_KeywordsBegin && Token->Kind < TokenKind_KeywordsEnd) { if(Token->Kind == TokenKind_True || Token->Kind == TokenKind_False) { Color = ColorFromHex(0xffa900ff); } else { Color = ColorFromHex(0xf0c674ff); } } else if(Token->Kind == TokenKind_Identifier) { Color = Theme_TextColor; } //- sixten: check for errors b32 ConsideredError = false; for(scene_compile_error *Error = Editor->Compiled.Errors.First; Error != 0; Error = Error->Next) { if(Error->Token.Range.Min == Token->Range.Min && Error->Token.Range.Max == Token->Range.Max) { ConsideredError = true; break; } } //- sixten: render & advance by token if(!(T_IsWhitespace(Token->Kind))) { if(Token->Kind == TokenKind_Comment) { //- sixten: advance to newline and push text // sixten(TODO): proper multiline comment rendering. u8 *TextBegin = TokenString.Data; u8 *TextEnd = TextBegin+TokenString.Count; u8 *Marker = TextBegin; for(u8 *Byte = TextBegin; Byte <= TextEnd; Byte += 1) { if(*Byte == '\n' || Byte == TextEnd) { PushText(Group, Atlas, Font, TokenP, FontSize, Color, MakeString(Marker, Byte-Marker)); Marker = Byte+1; if(*Byte == '\n' && Byte != TextEnd) { TokenP.x = BaseTokenP.x; TokenP.y += LineHeight; } } } } else { r32 TokenWidth = PushText(Group, Atlas, Font, TokenP, FontSize, Color, TokenString); //- sixten: render error highlight if(ConsideredError) { range2_r32 Dest = Range2R32(TokenP+V2R32(0, LineHeight-3), TokenP+V2R32(TokenWidth, LineHeight)); v4_r32 ErrorColor = V4R32(0.9f, 0.3f, 0.3f, 0.8f); PushQuad(Group, Dest, ErrorColor, ErrorColor, ErrorColor, ErrorColor, 3, 0.4, 0); } TokenP.x += TokenWidth; } } else { if(Token->Kind == TokenKind_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 { s64 LineIndex = CursorTextP.Line-1; string Line = Substring(Editor->Text.String, Editor->Lines.Ranges[LineIndex]); s64 ColumnIndex = CursorTextP.Column-1; s64 ColumnOffset = UTF8OffsetFromIndex(Line, ColumnIndex); v2 TargetCursorP = Box->Rect.Min+V2(LineMarginDim.x+ColumnOffset*GlyphAdvance, LineIndex*LineHeight); v2 CursorP = V2(AC_AnimateValueF(TargetCursorP.x, TargetCursorP.x, 0.1, "Workspace Text Editor Cursor X %p", Editor), AC_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 LineIndex = TopMostLine; LineIndex < TopMostLine + LinesOnScreen; LineIndex += 1) { if(Contains(LineRange, LineIndex + 1)) { range1_s64 ColumnRange = Lines->Ranges[LineIndex]; range1_s64 NormalizedColumnRange = Range1S64(0, DimOfRange(ColumnRange)); if(LineIndex+1 == LineRange.Min && LineIndex+1 == LineRange.Max) { NormalizedColumnRange = Range1S64(Editor->EditState.Cursor - ColumnRange.Min, Editor->EditState.Mark - ColumnRange.Min); } else if(LineIndex+1 == LineRange.Min) { NormalizedColumnRange = Range1S64(Min(Editor->EditState.Mark, Editor->EditState.Cursor) - ColumnRange.Min, DimOfRange(ColumnRange)); } else if(LineIndex+1 == LineRange.Max) { NormalizedColumnRange = Range1S64(0, Max(Editor->EditState.Mark, Editor->EditState.Cursor) - ColumnRange.Min); } string Line = Substring(Editor->Text.String, ColumnRange); range1_s64 ColumnOffsetRange = Range1S64(UTF8OffsetFromIndex(Line, NormalizedColumnRange.Min), UTF8OffsetFromIndex(Line, NormalizedColumnRange.Max)); r32 LineY = LineIndex*LineHeight; v4_r32 LineHighlightColor = ColorFromHex(0x66B3CC4C); range2_r32 LineHighlightBox = Range2R32(Box->Rect.Min+V2(LineMarginDim.x+ColumnOffsetRange.Min*GlyphAdvance, LineY), Box->Rect.Min+V2(LineMarginDim.x+ColumnOffsetRange.Max*GlyphAdvance, LineY+LineHeight)); PushQuad(Group, LineHighlightBox, LineHighlightColor, LineHighlightColor, LineHighlightColor, LineHighlightColor, 4, 1.4, 0); } } } ReleaseScratch(Scratch); } UI_CUSTOM_DRAW_CALLBACK(W_TextEditorListerInputCallback) { workspace_view_text_editor *Editor = (workspace_view_text_editor *)Data; s64 ClampedCursor = Clamp(0, Box->String.Count, Editor->ListerInputEditState.Cursor); s64 ClampedMark = Clamp(0, Box->String.Count, Editor->ListerInputEditState.Mark); string ToCursor = MakeString(Box->String.Data, ClampedCursor); string ToMark = MakeString(Box->String.Data, ClampedMark); r32 TargetCursorX = CalculateRasterizedTextWidth(Atlas, Box->Font, Box->FontSize, ToCursor); r32 TargetMarkerX = CalculateRasterizedTextWidth(Atlas, Box->Font, Box->FontSize, ToMark); r32 CursorX = AC_AnimateValueF(TargetCursorX, 0, 0.175, "Workspace View Input Cursor %p", Box); r32 MarkerX = AC_AnimateValueF(TargetMarkerX, 0, 0.175, "Workspace View Input Mark %p", Box); v2 BoxDim = DimOfRange(Box->Rect); // sixten: Draw selection { v2 Offset = V2(7.5, (BoxDim.y - Box->FontSize) / 2); v2 Dim = V2(0, Box->FontSize); if(CursorX > MarkerX) { Offset.x += MarkerX; Dim.x = CursorX - MarkerX; } else { Offset.x += CursorX; Dim.x = MarkerX - CursorX; } v2 P = Box->Rect.Min + Offset; v4 Color = V4(0.4, 0.7, 0.8, 0.3); PushQuad(Group, Range2R32(P, P+Dim), Color, 0, 0, 0); } // sixten: Draw cursor { range1_r32 CursorSpan = Range1R32(CursorX, TargetCursorX); r32 Height = Box->FontSize + 4; v2 Offset = V2(7.5F + CursorSpan.Min, (BoxDim.y - Height) / 2); v2 Dim = V2(1.25F + CursorSpan.Max - CursorSpan.Min, Height); v2 P = Box->Rect.Min + Offset; v4 Color = V4(0.3, 1, 0.3, 0.7); PushQuad(Group, Range2R32(P, P+Dim), Color, 0, 0, 0); } } static b32 W_BuildTextEditorListerItem(string Text, u32 Icon) { b32 Result = false; UI_SetNextLayoutAxis(Axis2_X); UI_SetNextHoverCursor(PlatformCursor_Hand); ui_box *Container = UI_MakeBoxF(UI_BoxFlag_DrawBorder | UI_BoxFlag_DrawBackground | UI_BoxFlag_Clickable | UI_BoxFlag_HotAnimation | UI_BoxFlag_ActiveAnimation| UI_BoxFlag_DrawDropShadow, "File Lister %S", Text); UI_Parent(Container) { UI_Width(UI_Em(2, 1)) UI_Font(Font_Icons) UI_LabelF("%U", Icon); UI_Width(UI_TextContent(0, 1)) UI_Label(Text); } ui_signal Signal = UI_SignalFromBox(Container); if(Signal.Clicked) { Result = true; } return(Result); } static workspace_text_editor_lister_action W_BuildTextEditorLister(workspace_view *View, workspace_view_text_editor *Editor) { workspace_text_editor_lister_action ListerAction = {}; temporary_memory Scratch = GetScratch(); UI_Size(UI_Percent(1, 1), UI_Percent(1, 1)) UI_Scroll(0, &Editor->ListerScroll) { UI_Height(UI_Em(2, 1)) { //- sixten: filename input field if(W_ViewIsCurrent(View)) { for(platform_event *Event = UI_EventList()->First; Event != 0; Event = Event->Next) { if((Event->Type == PlatformEvent_Press || Event->Type == PlatformEvent_Text) && (Event->Codepoint != '/' && Event->Codepoint != '\\')) { text_action Action = SingleLineTextActionFromEvent(Event); if(IsValid(&Action)) { text_op Op = TextOpFromAction(Scratch.Arena, MakeString(Editor->ListerInput, Editor->ListerInputUsed), &Editor->ListerInputEditState, &Action); string Left = MakeString(Editor->ListerInput, Op.Range.Min); string Right = MakeString(Editor->ListerInput + Op.Range.Max, Editor->ListerInputUsed - Op.Range.Max); u64 NewStringSize = Left.Count + Right.Count + Op.ReplaceString.Count; char *NewString = PushArray(Scratch.Arena, char, NewStringSize); Copy(NewString, Left.Data, Left.Count); Copy(NewString + Left.Count, Op.ReplaceString.Data, Op.ReplaceString.Count); Copy(NewString + Left.Count + Op.ReplaceString.Count, Right.Data, Right.Count); Editor->ListerInputUsed = Minimum(ArrayCount(Editor->ListerInput), NewStringSize); Copy(Editor->ListerInput, NewString, Editor->ListerInputUsed); Editor->ListerInputEditState.Cursor = Minimum(Op.NewCursor, Editor->ListerInputUsed); Editor->ListerInputEditState.Mark = Minimum(Op.NewMark, Editor->ListerInputUsed); } } } } //- sixten: build navbar UI_Row(UI_BoxFlag_DrawBorder) { UI_SetNextBackgroundColor(ColorFromHex(0x2D5790FF)); UI_SetNextWidth(UI_TextContent(20, 1)); UI_MakeBox(UI_BoxFlag_DrawBackground|UI_BoxFlag_DrawText, Editor->Path); UI_SetNextWidth(UI_TextContent(15, 1)); ui_box *InputBox = UI_MakeBox(UI_BoxFlag_DrawText, StrLit("Text Editor Lister Input")); UI_EquipBoxText(InputBox, MakeString(Editor->ListerInput, Editor->ListerInputUsed)); UI_EquipBoxCustomDrawCallback(InputBox, W_TextEditorListerInputCallback, Editor); UI_Padding(UI_Percent(1, 0)); UI_SetNextWidth(UI_TextContent(20, 1)); if(UI_ButtonF("Open/Create").Clicked || Platform_KeyPress(UI_EventList(), Key_Return)) { ListerAction.HasRequestedFile = true; ListerAction.Name = MakeString(Editor->ListerInput, Editor->ListerInputUsed); ListerAction.Path = Editor->Path; } } //- sixten: display "parent directory button" s64 LastSlash = LastIndexOf(Editor->Path, '/'); if(LastSlash != -1) { if(W_BuildTextEditorListerItem(StrLit("Parent Directory"), FontIcon_Reply)) { Editor->Path = Prefix(Editor->Path, LastSlash); Editor->ListerInputUsed = 0; } } platform_file_info FileInfo; platform_file_iter *FileIter; string Name = MakeString(Editor->ListerInput, Editor->ListerInputUsed); string FullPath = PushFormat(Scratch.Arena, "%S/%S", Editor->Path, Name); //- sixten: display directories { FileIter = Platform.BeginFileIter(Scratch.Arena, FullPath); for(;Platform.AdvanceFileIter(Scratch.Arena, FileIter, &FileInfo);) { if(FileInfo.IsDirectory) { if(W_BuildTextEditorListerItem(FileInfo.Name, FontIcon_Folder)) { Editor->Path = PushFormat(View->Arena, "%S/%S", Editor->Path, FileInfo.Name); Editor->ListerInputUsed = 0; } } } } Platform.EndFileIter(FileIter); //- sixten: display files { FileIter = Platform.BeginFileIter(Scratch.Arena, FullPath); for(;Platform.AdvanceFileIter(Scratch.Arena, FileIter, &FileInfo);) { if(!FileInfo.IsDirectory) { if(W_BuildTextEditorListerItem(FileInfo.Name, FontIcon_Document)) { ListerAction.HasRequestedFile = true; ListerAction.Name = PushString(View->Arena, FileInfo.Name); ListerAction.Path = Editor->Path; } } } } Platform.EndFileIter(FileIter); } } ReleaseScratch(Scratch); return(ListerAction); } static b32 W_ProcessTextEditorEvent(workspace_view_text_editor *Editor, platform_event *Event) { b32 CursorHasBeenModified = false; temporary_memory Scratch = GetScratch(); 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); if(DimOfRange(Op.Range) != 0 || !AreEqual(StrLit(""), Op.ReplaceString)) { //- sixten: append to the history { history_list *List = &Editor->History; //- sixten: remove the pre-existing history if needed if(List->Sentinel.Prev != List->At) { // sixten(TODO): instead of just removing the links to the old memory, find some way to manage it. List->Sentinel.Prev->Next = List->At; List->Sentinel.Prev = List->At; } range1_s64 Selection = Range1S64(Editor->EditState.Cursor, Editor->EditState.Mark); AppendToHistory(Editor->HistoryArena, List, HistoryEntry(Editor->HistoryArena, Op.ReplaceString, Op.Range), HistoryEntry(Editor->HistoryArena, Substring(Editor->Text.String, Op.Range), Range1S64(Op.Range.Min, Op.Range.Min+Op.ReplaceString.Count))); List->At = List->Sentinel.Prev; } //- sixten: apply the text action string ReplaceString = RemoveAll(Scratch.Arena, Op.ReplaceString, '\r'); MutableStringReplaceRange(&Editor->Text, ReplaceString, Op.Range); W_TextEditorApplyChanges(Editor); } CursorHasBeenModified = true; Editor->EditState.Cursor = Op.NewCursor; Editor->EditState.Mark = Op.NewMark; } ReleaseScratch(Scratch); return(CursorHasBeenModified); } static void W_BuildTextEditorInfoBar(workspace_view_text_editor *Editor) { UI_SetNextLayoutAxis(Axis2_X); UI_WidthFill UI_Height(UI_Em(1.75f, 1)) UI_Parent(UI_MakeBoxF(UI_BoxFlag_DrawDropShadow, "")) UI_Padding(UI_Em(1, 1)) { UI_Width(UI_TextContent(0, 1)) { UI_Font(Font_Icons) UI_LabelF("%U", FontIcon_Attention); UI_Spacer(UI_Em(0.5f, 1)); UI_LabelF("%i", Editor->Compiled.Errors.Count); } } } static void W_BuildTextEditor(workspace_view *View) { workspace_view_text_editor *Editor = (workspace_view_text_editor *)View->Data; temporary_memory Scratch = GetScratch(); //- sixten: rendering properties r32 FontSize = Editor->FontSize = 13.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; b32 InFileListMode = AreEqual(Editor->FileName, StrLit("")); if(InFileListMode) { //- sixten: build & handle file lister workspace_text_editor_lister_action Action = W_BuildTextEditorLister(View, Editor); if(Action.HasRequestedFile) { //- sixten: try to load file string FullPath = PushFormat(Scratch.Arena, "%S/%S", Action.Path, Action.Name); platform_file_handle File = Platform.OpenFile(FullPath, PlatformAccess_Read); if(File.IsValid) { s64 FileSize = Platform.GetFileSize(File); string ReplaceString = MakeString(PushArray(Scratch.Arena, u8, FileSize+1), FileSize); Platform.ReadFile(File, ReplaceString.Data, 0, ReplaceString.Count); ReplaceString = RemoveAll(Scratch.Arena, ReplaceString, '\r'); MutableStringReplaceRange(&Editor->Text, ReplaceString, Range1S64(0, 0)); W_TextEditorApplyChanges(Editor); Editor->FileName = Action.Name; Editor->FilePath = Action.Path; Platform.CloseFile(File); } else { Editor->FileName = Action.Name; Editor->FilePath = Action.Path; } } } else { //- sixten: build & handle the text editor ui_box *EditorBox = 0; UI_SetNextSize(UI_Percent(1, 1), UI_Percent(1, 0)); 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); UI_EquipBoxCustomDrawCallback(EditorBox, W_TextEditorDrawCallback, Editor); } //- sixten: build footer W_BuildTextEditorInfoBar(Editor); r32 TargetFooterHeightEm = 2.25f*Min(Editor->Compiled.Errors.Count, 10LL); UI_Size(UI_Percent(1, 0), UI_Em(AC_AnimateValueF(TargetFooterHeightEm, TargetFooterHeightEm, 0.3, "Error Lister %p", Editor), 1)) UI_Column() UI_Height(UI_TextContent(0, 1)) { s64 Index = 0; for(scene_compile_error *Error = Editor->Compiled.Errors.First; Error != 0; Error = Error->Next, Index += 1) { UI_SetNextHeight(UI_ChildrenSum(1, 1)); UI_SetNextLayoutAxis(Axis2_X); UI_Parent(UI_MakeBoxF(0, "Editor Error Lister Container %p", Error)) UI_Padding(UI_Em(1, 1)) UI_Height(UI_Em(1.75f, 1)) { UI_SetNextBackgroundColor(SetAlpha(Theme_BorderColor, 0.8f)); UI_SetNextCornerRadius(4); UI_SetNextLayoutAxis(Axis2_X); UI_SetNextHoverCursor(PlatformCursor_Hand); ui_box *ContainerBox = UI_MakeBoxF(UI_BoxFlag_Clickable|UI_BoxFlag_DrawDropShadow|UI_BoxFlag_DrawBorder, "Container Box"); UI_Parent(ContainerBox) UI_Padding(UI_Em(1, 1)) UI_Width(UI_TextContent(0, 1)) { UI_Font(Font_Icons) UI_LabelF("%U", FontIcon_Attention); UI_Spacer(UI_Em(0.5f, 1)); // sixten(TODO): this is dumb, slow and downright stupid. text_point Point = TextPointFromOffset(Editor->Text.String, Error->Token.Range.Min); UI_LabelF("%i:%i", Point.Line, Point.Column); UI_Spacer(UI_Em(0.5f, 1)); UI_Label(Error->Message); UI_Spacer(UI_Percent(1, 0)); } ui_signal Signal = UI_SignalFromBox(ContainerBox); if(Signal.Hovering) { UI_TooltipLabel(StrLit("Goto in source"), UI_MouseP()); } } UI_Spacer(UI_Em(0.5, 1)); } } b32 CursorHasBeenModified = false; if(W_ViewIsCurrent(View)) { //- sixten: handle history { history_list *List = &Editor->History; //- sixten: undo if(Platform_KeyPress(UI_EventList(), Key_Z, PlatformModifier_Ctrl)) { history_node *Node = List->At; if(Node != &List->Sentinel) { //- sixten: get entry & apply history_entry Entry = Node->Backward; MutableStringReplaceRange(&Editor->Text, Entry.ReplaceString, Entry.Range); W_TextEditorApplyChanges(Editor); Editor->EditState.Cursor = Editor->EditState.Mark = Entry.Range.Min+Entry.ReplaceString.Count; CursorHasBeenModified = true; List->At = Node->Prev; } } //- sixten: redo if(Platform_KeyPress(UI_EventList(), Key_Y, PlatformModifier_Ctrl)) { history_node *Node = List->At->Next; if(Node != &List->Sentinel) { //- sixten: get entry & apply history_entry Entry = Node->Forward; MutableStringReplaceRange(&Editor->Text, Entry.ReplaceString, Entry.Range); W_TextEditorApplyChanges(Editor); Editor->EditState.Cursor = Editor->EditState.Mark = Entry.Range.Min+Entry.ReplaceString.Count; CursorHasBeenModified = true; List->At = Node; } } } //- sixten: save if(Platform_KeyPress(UI_EventList(), Key_S, PlatformModifier_Ctrl)) { W_SaveTextEditorToFile(Editor); } //- sixten: select all if(Platform_KeyPress(UI_EventList(), Key_A, PlatformModifier_Ctrl)) { Editor->EditState.Mark = 0; Editor->EditState.Cursor = Editor->Text.String.Count; } //- sixten: keyboard input -> text op for(platform_event *Event = UI_EventList()->First; Event != 0; Event = Event->Next) { if(Event->Type == PlatformEvent_Press || Event->Type == PlatformEvent_Text) { //- sixten: auto-indent s64 Indent = 0; if(Event->Codepoint == '\n') { string Line = Substring(Editor->Text.String, Editor->Lines.Ranges[Editor->LastTextPoint.Line-1]); if(Line.Data[Line.Count-1] == '\n') { Line.Count -= 1; } for(u8 *Data = Line.Data; *Data == '\t' && Data < Line.Data+Line.Count; Data += 1) { Indent += 1; } } //- sixten: auto close bracket if(Event->Codepoint == '{') { platform_event FakeEvent = {}; FakeEvent.Codepoint = '}'; FakeEvent.Type = PlatformEvent_Text; CursorHasBeenModified |= W_ProcessTextEditorEvent(Editor, &FakeEvent); FakeEvent.Key = Key_Left;; FakeEvent.Type = PlatformEvent_Press; CursorHasBeenModified |= W_ProcessTextEditorEvent(Editor, &FakeEvent); } //- sixten: auto close string literal if(Event->Codepoint == '"') { platform_event FakeEvent = {}; FakeEvent.Codepoint = '"'; FakeEvent.Type = PlatformEvent_Text; CursorHasBeenModified |= W_ProcessTextEditorEvent(Editor, &FakeEvent); FakeEvent.Key = Key_Left;; FakeEvent.Type = PlatformEvent_Press; CursorHasBeenModified |= W_ProcessTextEditorEvent(Editor, &FakeEvent); } CursorHasBeenModified |= W_ProcessTextEditorEvent(Editor, Event); //- sixten: apply indent { platform_event FakeTab = {}; FakeTab.Codepoint = '\t'; FakeTab.Type = PlatformEvent_Text; for(s64 IndentIndex = 0; IndentIndex < Indent; IndentIndex += 1) { CursorHasBeenModified |= W_ProcessTextEditorEvent(Editor, &FakeTab); } } } } } //- sixten: right-click dropdown { if(Editor->DropdownActive) { UI_Tooltip { UI_SetNextFixedP(Editor->DropdownP); UI_SetNextWidth(UI_Em(20, 1)); UI_SetNextHeight(UI_ChildrenSum(AC_AnimateValueDirect(1, 0.2, &Editor->DropdownTransition), 1)); UI_SetNextCornerRadius(4); UI_Parent(UI_MakeBox(UI_BoxFlag_DrawBackground | UI_BoxFlag_DrawDropShadow | UI_BoxFlag_Clip | UI_BoxFlag_FloatingX | UI_BoxFlag_FloatingY, StrLit("Text Editor Dropdown"))) UI_Width(UI_Percent(1, 1)) UI_Height(UI_Em(1.66, 1)) UI_BackgroundColor(V4(0.25, 0.25, 0.25, 1)) UI_BorderColor(V4(0.45, 0.45, 0.45, 1)) UI_CornerRadius(2) { if(W_BuildMenuItem(FontIcon_Gamepad, "Run in scene view", "").Clicked) { SV_SetCurrentSource(&Editor->Compiled); Editor->DropdownActive = false; } if(Editor->EditState.Cursor != Editor->EditState.Mark) { if(W_BuildMenuItem(FontIcon_Document, "Copy", "Ctrl+C").Clicked) { Editor->DropdownActive = false; } if(W_BuildMenuItem(FontIcon_Cut, "Cut", "Ctrl+X").Clicked) { Editor->DropdownActive = false; } } if(W_BuildMenuItem(FontIcon_Paste, "Paste", "Ctrl+V").Clicked) { Editor->DropdownActive = false; } if(W_BuildMenuItem(FontIcon_Floppy, "Save", "Ctrl+S").Clicked) { W_SaveTextEditorToFile(Editor); Editor->DropdownActive = false; } } } } } ui_signal Signal = UI_SignalFromBox(EditorBox); if(Signal.Pressed || (Signal.PressedRight && (Editor->EditState.Cursor == Editor->EditState.Mark))) { //- sixten: translate mouse position to text point v2 MouseOffset = Signal.MouseP - EditorBox->Rect.Min - V2(LineMarginWidth, 0); s64 LineIndex = Clamp((s64)(MouseOffset.y / LineHeight), 0, Editor->Lines.Count); string Line = Substring(Editor->Text.String, Editor->Lines.Ranges[LineIndex]); s64 ColumnOffset = (s64)(MouseOffset.x / GlyphAdvance); s64 ColumnIndex = UTF8IndexFromOffset(Line, ColumnOffset); text_point Point = {LineIndex + 1, ColumnIndex + 1}; Editor->EditState.Cursor = Editor->EditState.Mark = OffsetFromTextPoint(Editor->Text.String, Editor->Lines, Point); } if(Signal.Dragging) { //- sixten: translate mouse position to text point v2 MouseOffset = Signal.MouseP - EditorBox->Rect.Min - V2(LineMarginWidth, 0); s64 LineIndex = Clamp((s64)(MouseOffset.y / LineHeight), 0, Editor->Lines.Count); string Line = Substring(Editor->Text.String, Editor->Lines.Ranges[LineIndex]); s64 ColumnOffset = (s64)(MouseOffset.x / GlyphAdvance); s64 ColumnIndex = UTF8IndexFromOffset(Line, ColumnOffset); text_point Point = {LineIndex + 1, ColumnIndex + 1}; Editor->EditState.Cursor = OffsetFromTextPoint(Editor->Text.String, Editor->Lines, Point); CursorHasBeenModified = true; Editor->DropdownActive = false; } if(Signal.PressedRight) { Editor->DropdownActive = true; Editor->DropdownP = UI_MouseP(); Editor->DropdownTransition = 0; } //- 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); }