diff --git a/src/Explorer.csproj b/src/Explorer.csproj
index d452a9e..173ba96 100644
--- a/src/Explorer.csproj
+++ b/src/Explorer.csproj
@@ -256,6 +256,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ExplorerCore.cs b/src/ExplorerCore.cs
index 0995ad1..e4be609 100644
--- a/src/ExplorerCore.cs
+++ b/src/ExplorerCore.cs
@@ -97,10 +97,6 @@ namespace ExplorerBeta
m_doneUIInit = true;
}
}
- else
- {
- UIManager.Update();
- }
if (InputManager.GetKeyDown(ModConfig.Instance.Main_Menu_Toggle))
{
diff --git a/src/UI/Main/DebugConsole.cs b/src/UI/Main/DebugConsole.cs
index 38d8c7a..f0aa950 100644
--- a/src/UI/Main/DebugConsole.cs
+++ b/src/UI/Main/DebugConsole.cs
@@ -94,14 +94,6 @@ namespace ExplorerBeta.UI.Main
tmpInput.verticalScrollbar = scroller;
m_textInput = input.GetComponent();
-
- for (int i = 0; i < 100; i++)
- {
- Log("hello " + i);
- }
-
- Log("hello", Color.red);
- Log("hello", Color.yellow);
}
public static void Log(string message)
diff --git a/src/UI/Main/MainMenu.cs b/src/UI/Main/MainMenu.cs
index c3e1c33..39a482d 100644
--- a/src/UI/Main/MainMenu.cs
+++ b/src/UI/Main/MainMenu.cs
@@ -49,6 +49,7 @@ namespace ExplorerBeta.UI.Main
foreach (var page in Pages)
{
page.Init();
+ page.Content?.SetActive(false);
}
SetPage(Pages[0]);
@@ -56,7 +57,7 @@ namespace ExplorerBeta.UI.Main
public void Update()
{
- // todo
+ m_activePage?.Update();
}
// todo
diff --git a/src/UI/Main/Pages/Console/Editor/AutoIndent.cs b/src/UI/Main/Pages/Console/Editor/AutoIndent.cs
new file mode 100644
index 0000000..dd66ddb
--- /dev/null
+++ b/src/UI/Main/Pages/Console/Editor/AutoIndent.cs
@@ -0,0 +1,133 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Explorer.UI.Main.Pages.Console
+{
+ public class AutoIndent
+ {
+ // Enum
+ public enum IndentMode
+ {
+ None,
+ AutoTab,
+ AutoTabContextual,
+ }
+
+ // Private
+ private static readonly StringBuilder indentBuilder = new StringBuilder();
+
+ private static string indentDecreaseString = null;
+
+ // Public
+ public static IndentMode autoIndentMode = IndentMode.AutoTabContextual;
+
+ ///
+ /// Should auto indent be used for this language.
+ ///
+ public static bool allowAutoIndent = true;
+ ///
+ /// The character that causes the indent level to increase.
+ ///
+ public static char indentIncreaseCharacter = '{';
+ ///
+ /// The character that causes the indent level to decrease.
+ ///
+ public static char indentDecreaseCharacter = '}';
+
+ // Properties
+ ///
+ /// Get the string representation of the indent character.
+ ///
+ public static string IndentDecreaseString
+ {
+ get
+ {
+ if (indentDecreaseString == null)
+ {
+ indentDecreaseString = new string(indentDecreaseCharacter, 1);
+ }
+ return indentDecreaseString;
+ }
+ }
+
+ // Methods
+ public static string GetAutoIndentedFormattedString(string indentSection, int currentIndent, out int caretPosition)
+ {
+ // Add indent level
+ int indent = currentIndent + 1;
+
+ // Append characters
+ for (int i = 0; i < indentSection.Length; i++)
+ {
+ if (indentSection[i] == '\n')
+ {
+ indentBuilder.Append('\n');
+ AppendIndentString(indent);
+ }
+ else if (indentSection[i] == '\t')
+ {
+ // We will add tabs manually
+ continue;
+ }
+ else if (indentSection[i] == indentIncreaseCharacter)
+ {
+ indentBuilder.Append(indentIncreaseCharacter);
+ indent++;
+ }
+ else if (indentSection[i] == indentDecreaseCharacter)
+ {
+ indentBuilder.Append(indentDecreaseCharacter);
+ indent--;
+ }
+ else
+ {
+ indentBuilder.Append(indentSection[i]);
+ }
+ }
+
+ // Build the string
+ string formattedSection = indentBuilder.ToString();
+ indentBuilder.Length = 0;
+
+ // Default caret position
+ caretPosition = formattedSection.Length - 1;
+
+ // Find the caret position
+ for (int i = formattedSection.Length - 1; i >= 0; i--)
+ {
+ if (formattedSection[i] == '\n')
+ continue;
+
+ caretPosition = i;
+ break;
+ }
+
+ return formattedSection;
+ }
+
+ public static int GetAutoIndentLevel(string inputString, int startIndex, int endIndex)
+ {
+ int indent = 0;
+
+ for (int i = startIndex; i < endIndex; i++)
+ {
+ if (inputString[i] == '\t')
+ indent++;
+
+ // Check for end line or other characters
+ if (inputString[i] == '\n' || inputString[i] != ' ')
+ break;
+ }
+
+ return indent;
+ }
+
+ private static void AppendIndentString(int amount)
+ {
+ for (int i = 0; i < amount; i++)
+ indentBuilder.Append("\t");
+ }
+ }
+}
diff --git a/src/UI/Main/Pages/Console/Editor/CodeEditor.cs b/src/UI/Main/Pages/Console/Editor/CodeEditor.cs
new file mode 100644
index 0000000..828cf7e
--- /dev/null
+++ b/src/UI/Main/Pages/Console/Editor/CodeEditor.cs
@@ -0,0 +1,552 @@
+using UnityEngine;
+using UnityEngine.UI;
+using TMPro;
+using System;
+using System.Text;
+using System.Reflection;
+using ExplorerBeta.Input;
+using Explorer.UI.Main.Pages.Console.Lexer;
+using ExplorerBeta;
+
+namespace Explorer.UI.Main.Pages.Console
+{
+ public class CodeEditor
+ {
+ public CodeEditor(TMP_InputField inputField, TextMeshProUGUI inputText, TextMeshProUGUI inputHighlightText, TextMeshProUGUI lineText,
+ Image background, Image lineHighlight, Image lineNumberBackground, Image scrollbar)
+ {
+ this.InputField = inputField;
+ this.inputText = inputText;
+ this.inputHighlightText = inputHighlightText;
+ this.lineText = lineText;
+ this.background = background;
+ this.lineHighlight = lineHighlight;
+ this.lineNumberBackground = lineNumberBackground;
+ this.scrollbar = scrollbar;
+
+ var highlightTextRect = inputHighlightText.GetComponent();
+ highlightTextRect.anchorMin = Vector2.zero;
+ highlightTextRect.anchorMax = Vector2.one;
+ highlightTextRect.offsetMin = Vector2.zero;
+ highlightTextRect.offsetMax = Vector2.zero;
+
+ if (!AllReferencesAssigned())
+ {
+ throw new Exception("CodeEditor: Components are missing!");
+ }
+
+ this.inputTextTransform = inputText.GetComponent();
+ this.lineHighlightTransform = lineHighlight.GetComponent();
+
+ ApplyTheme();
+ ApplyLanguage();
+
+ // subscribe to text input changing
+ InputField.onValueChanged.AddListener(new Action((string s) => { Refresh(); }));
+ }
+
+ private static readonly KeyCode[] lineChangeKeys =
+ {
+ KeyCode.Return, KeyCode.Backspace, KeyCode.UpArrow,
+ KeyCode.DownArrow, KeyCode.LeftArrow, KeyCode.RightArrow
+ };
+
+ private static readonly StringBuilder highlightedBuilder = new StringBuilder(4096);
+ private static readonly StringBuilder lineBuilder = new StringBuilder();
+
+ private readonly InputStringLexer lexer = new InputStringLexer();
+ private readonly RectTransform inputTextTransform;
+ private readonly RectTransform lineHighlightTransform;
+ private string lastText;
+ private bool lineHighlightLocked;
+
+ public readonly TMP_InputField InputField;
+ private readonly TextMeshProUGUI inputText;
+ private readonly TextMeshProUGUI inputHighlightText;
+ private readonly TextMeshProUGUI lineText;
+ private readonly Image background;
+ private readonly Image lineHighlight;
+ private readonly Image lineNumberBackground;
+ private readonly Image scrollbar;
+
+ private bool lineNumbers = true;
+ private int lineNumbersSize = 20;
+
+ public int LineCount { get; private set; } = 0;
+ public int CurrentLine { get; private set; } = 0;
+ public int CurrentColumn { get; private set; } = 0;
+ public int CurrentIndent { get; private set; } = 0;
+
+ public string Text
+ {
+ get { return InputField.text; }
+ set
+ {
+ if (!string.IsNullOrEmpty(value))
+ {
+ InputField.text = value;
+ inputHighlightText.text = value;
+ }
+ else
+ {
+ InputField.text = string.Empty;
+ inputHighlightText.text = string.Empty;
+ }
+
+ inputText.ForceMeshUpdate(false);
+ }
+ }
+
+ public string HighlightedText => inputHighlightText.text;
+
+ public bool LineNumbers
+ {
+ get { return lineNumbers; }
+ set
+ {
+ lineNumbers = value;
+
+ //RectTransform inputFieldTransform = InputField.transform as RectTransform;
+ //RectTransform lineNumberBackgroudTransform = lineNumberBackground.transform as RectTransform;
+
+ //// Check for line numbers
+ //if (lineNumbers == true)
+ //{
+ // // Enable line numbers
+ // lineNumberBackground.gameObject.SetActive(true);
+ // lineText.gameObject.SetActive(true);
+
+ // // Set left value
+ // inputFieldTransform.offsetMin = new Vector2(lineNumbersSize, inputFieldTransform.offsetMin.y);
+ // lineNumberBackgroudTransform.sizeDelta = new Vector2(lineNumbersSize + 15, lineNumberBackgroudTransform.sizeDelta.y);
+ //}
+ //else
+ //{
+ // // Disable line numbers
+ // lineNumberBackground.gameObject.SetActive(false);
+ // lineText.gameObject.SetActive(false);
+
+ // // Set left value
+ // inputFieldTransform.offsetMin = new Vector2(0, inputFieldTransform.offsetMin.y);
+ //}
+ }
+ }
+
+ // todo maybe not needed
+ public int LineNumbersSize
+ {
+ get { return lineNumbersSize; }
+ set
+ {
+ lineNumbersSize = value;
+
+ // Update the line numbers
+ LineNumbers = lineNumbers;
+ }
+ }
+
+ public void Update()
+ {
+ // Auto indent
+ if (AutoIndent.autoIndentMode != AutoIndent.IndentMode.None)
+ {
+ // Check for new line
+ if (InputManager.GetKeyDown(KeyCode.Return))
+ {
+ AutoIndentCaret();
+ }
+ }
+
+ bool focusKeyPressed = false;
+
+ // Check for any focus key pressed
+ foreach (KeyCode key in lineChangeKeys)
+ {
+ if (InputManager.GetKeyDown(key))
+ {
+ focusKeyPressed = true;
+ break;
+ }
+ }
+
+ // Update line highlight
+ if (focusKeyPressed || InputManager.GetMouseButton(0))
+ {
+ UpdateCurrentLineHighlight();
+ }
+ }
+
+ public void Refresh(bool forceUpdate = false)
+ {
+ // Trigger a content change event
+ DisplayedContentChanged(InputField.text, forceUpdate);
+ }
+
+ public void SetLineHighlight(int lineNumber, bool lockLineHighlight)
+ {
+ // Check if code editor is not active
+ if (lineNumber < 1 || lineNumber > LineCount)
+ return;
+
+ //int lineOffset = 0;
+ //int lineIndex = lineNumber - 1;
+
+ // Highlight the current line
+ lineHighlightTransform.anchoredPosition = new Vector2(5,
+ (inputText.textInfo.lineInfo[inputText.textInfo.characterInfo[0].lineNumber].lineHeight *
+ -(lineNumber - 1)) - 4f +
+ inputTextTransform.anchoredPosition.y);
+
+ // Lock the line highlight so it cannot be moved
+ if (lockLineHighlight == true)
+ LockLineHighlight();
+ else
+ UnlockLineHighlight();
+ }
+
+ public void LockLineHighlight()
+ {
+ lineHighlightLocked = true;
+ }
+
+ public void UnlockLineHighlight()
+ {
+ lineHighlightLocked = false;
+ }
+
+ private void DisplayedContentChanged(string newText, bool forceUpdate)
+ {
+ // Update caret position
+ UpdateCurrentLineColumnIndent();
+
+ // Check for change
+ if ((!forceUpdate && lastText == newText) || string.IsNullOrEmpty(newText))
+ {
+ if (string.IsNullOrEmpty(newText))
+ {
+ inputHighlightText.text = string.Empty;
+ }
+
+ // Its possible the text was cleared so we need to sync numbers and highlighter
+ UpdateCurrentLineNumbers();
+ UpdateCurrentLineHighlight();
+ return;
+ }
+
+ inputHighlightText.text = SyntaxHighlightContent(newText);
+
+ // Sync line numbers and update the line highlight
+ UpdateCurrentLineNumbers();
+ UpdateCurrentLineHighlight();
+
+ this.lastText = newText;
+ }
+
+ private void UpdateCurrentLineNumbers()
+ {
+ // Get the line count
+ int currentLineCount = inputText.textInfo.lineCount;
+
+ int currentLineNumber = 1;
+
+ // Check for a change in line
+ if (currentLineCount != LineCount)
+ {
+ try
+ {
+ // Update line numbers
+ lineBuilder.Length = 0;
+
+ // Build line numbers string
+ for (int i = 1; i < currentLineCount + 2; i++)
+ {
+ if (i - 1 > 0 && i - 1 < currentLineCount - 1)
+ {
+ int characterStart = inputText.textInfo.lineInfo[i - 1].firstCharacterIndex;
+ int characterCount = inputText.textInfo.lineInfo[i - 1].characterCount;
+
+ if (characterStart >= 0 && characterStart < inputText.text.Length &&
+ characterCount != 0 && !inputText.text.Substring(characterStart, characterCount).Contains("\n"))
+ {
+ lineBuilder.Append("\n");
+ continue;
+ }
+ }
+
+ lineBuilder.Append(currentLineNumber);
+ lineBuilder.Append('\n');
+
+ currentLineNumber++;
+
+ if (i - 1 == 0 && i - 1 < currentLineCount - 1)
+ {
+ int characterStart = inputText.textInfo.lineInfo[i - 1].firstCharacterIndex;
+ int characterCount = inputText.textInfo.lineInfo[i - 1].characterCount;
+
+ if (characterStart >= 0 && characterStart < inputText.text.Length &&
+ characterCount != 0 && !inputText.text.Substring(characterStart, characterCount).Contains("\n"))
+ {
+ lineBuilder.Append("\n");
+ continue;
+ }
+ }
+ }
+
+ // Update displayed line numbers
+ lineText.text = lineBuilder.ToString();
+ LineCount = currentLineCount;
+ }
+ catch { }
+ }
+ }
+
+ private void UpdateCurrentLineColumnIndent()
+ {
+ // Get the current line number
+ int caret = InputField.caretPosition;
+
+ if (caret < 0 || caret >= inputText.textInfo.characterInfo.Count)
+ {
+ while (caret >= 0 && caret >= inputText.textInfo.characterInfo.Count)
+ caret--;
+
+ if (caret < 0 || caret >= inputText.textInfo.characterInfo.Count)
+ {
+ return;
+ }
+ }
+
+ CurrentLine = inputText.textInfo.characterInfo[caret].lineNumber;
+
+ // Get the total character count
+ int charCount = 0;
+ for (int i = 0; i < CurrentLine; i++)
+ charCount += inputText.textInfo.lineInfo[i].characterCount;
+
+ // Get the column position
+ CurrentColumn = caret - charCount;
+
+ CurrentIndent = 0;
+
+ // Check for auto indent allowed
+ if (AutoIndent.allowAutoIndent)
+ {
+ for (int i = 0; i < caret && i < InputField.text.Length; i++)
+ {
+ char character = InputField.text[i];
+
+ // Check for opening indents
+ if (character == AutoIndent.indentIncreaseCharacter)
+ CurrentIndent++;
+
+ // Check for closing indents
+ if (character == AutoIndent.indentDecreaseCharacter)
+ CurrentIndent--;
+ }
+
+ // Dont allow negative indents
+ if (CurrentIndent < 0)
+ CurrentIndent = 0;
+ }
+ }
+
+ private void UpdateCurrentLineHighlight()
+ {
+ if (lineHighlightLocked)
+ return;
+
+ try
+ {
+ // unity 2018.2 and older may need lineOffset as 0? not sure
+ //int lineOffset = 1;
+
+ int caret = InputField.caretPosition - 1;
+
+ var lineHeight = inputText.textInfo.lineInfo[inputText.textInfo.characterInfo[0].lineNumber].lineHeight;
+ var lineNumber = inputText.textInfo.characterInfo[caret].lineNumber;
+ var offset = lineNumber + inputTextTransform.anchoredPosition.y;
+
+ lineHighlightTransform.anchoredPosition = new Vector2(5, -(offset * lineHeight));
+ }
+ catch //(Exception e)
+ {
+ //ExplorerCore.LogWarning("Exception on Update Line Highlight: " + e);
+ }
+ }
+
+ private const string CLOSE_COLOR_TAG = "";
+
+ private string SyntaxHighlightContent(string inputText)
+ {
+ if (!InputTheme.allowSyntaxHighlighting)
+ return inputText;
+
+ int offset = 0;
+
+ highlightedBuilder.Length = 0;
+
+ foreach (var match in lexer.LexInputString(inputText))
+ {
+ // Copy text before the match
+ for (int i = offset; i < match.startIndex; i++)
+ highlightedBuilder.Append(inputText[i]);
+
+ // Add the opening color tag
+ highlightedBuilder.Append(match.htmlColor);
+
+ // Copy text inbetween the match boundaries
+ for (int i = match.startIndex; i < match.endIndex; i++)
+ highlightedBuilder.Append(inputText[i]);
+
+ // Add the closing color tag
+ highlightedBuilder.Append(CLOSE_COLOR_TAG);
+
+ // Update offset
+ offset = match.endIndex;
+ }
+
+ // Copy remaining text
+ for (int i = offset; i < inputText.Length; i++)
+ highlightedBuilder.Append(inputText[i]);
+
+ // Convert to string
+ inputText = highlightedBuilder.ToString();
+
+ return inputText;
+ }
+
+ // todo param is probably pointless
+ private void AutoIndentCaret()
+ {
+ if (CurrentIndent > 0)
+ {
+ var indent = GetAutoIndentTab(CurrentIndent);
+
+ if (indent.Length > 0)
+ {
+ var caretPos = InputField.caretPosition;
+
+ var indentMinusOne = indent.Substring(0, indent.Length - 1);
+
+ // get last index of {
+ // check it on the next line if its not already
+ var text = InputField.text;
+ var sub = InputField.text.Substring(0, InputField.caretPosition);
+ var lastIndex = sub.LastIndexOf("{");
+ var offset = lastIndex - 1;
+ if (offset >= 0 && text[offset] != '\n' && text[offset] != '\t')
+ {
+ var open = "\n" + indentMinusOne;
+
+ InputField.text = text.Insert(offset + 1, open);
+
+ caretPos += open.Length;
+ }
+
+ // check if should add auto-close }
+ int numOpen = 0;
+ int numClose = 0;
+ char prevChar = default;
+ foreach (var _char in InputField.text)
+ {
+ if (_char == '{')
+ {
+ if (prevChar != default && (prevChar == '\\' || prevChar == '{'))
+ {
+ if (prevChar == '{')
+ numOpen--;
+ }
+ else
+ {
+ numOpen++;
+ }
+ }
+ else if (_char == '}')
+ {
+ if (prevChar != default && (prevChar == '\\' || prevChar == '}'))
+ {
+ if (prevChar == '}')
+ numClose--;
+ }
+ else
+ {
+ numClose++;
+ }
+ }
+ prevChar = _char;
+ }
+ if (numOpen > numClose)
+ {
+ // add auto-indent closing
+ indentMinusOne = $"\n{indentMinusOne}}}";
+ InputField.text = InputField.text.Insert(caretPos, indentMinusOne);
+ }
+
+ // insert the actual auto indent now
+ InputField.text = InputField.text.Insert(caretPos, indent);
+
+ InputField.stringPosition = caretPos + indent.Length;
+ }
+ }
+
+ // Update line column and indent positions
+ UpdateCurrentLineColumnIndent();
+
+ inputText.text = InputField.text;
+ inputText.SetText(InputField.text, true);
+ inputText.Rebuild(CanvasUpdate.Prelayout);
+ InputField.ForceLabelUpdate();
+ InputField.Rebuild(CanvasUpdate.Prelayout);
+ Refresh(true);
+ }
+
+ private string GetAutoIndentTab(int amount)
+ {
+ string tab = string.Empty;
+
+ for (int i = 0; i < amount; i++)
+ tab += "\t";
+
+ return tab;
+ }
+
+ private void ApplyTheme()
+ {
+ // Check for missing references
+ if (!AllReferencesAssigned())
+ throw new Exception("Cannot apply theme because one or more required component references are missing. ");
+
+ // Apply theme colors
+ InputField.caretColor = InputTheme.caretColor;
+ inputText.color = InputTheme.textColor;
+ inputHighlightText.color = InputTheme.textColor;
+ background.color = InputTheme.backgroundColor;
+ lineHighlight.color = InputTheme.lineHighlightColor;
+ lineNumberBackground.color = InputTheme.lineNumberBackgroundColor;
+ lineText.color = InputTheme.lineNumberTextColor;
+ scrollbar.color = InputTheme.scrollbarColor;
+ }
+
+ private void ApplyLanguage()
+ {
+ lexer.UseMatchers(CodeTheme.DelimiterSymbols, CodeTheme.Matchers);
+ }
+
+ private bool AllReferencesAssigned()
+ {
+ if (!InputField ||
+ !inputText ||
+ !inputHighlightText ||
+ !lineText ||
+ !background ||
+ !lineHighlight ||
+ !lineNumberBackground ||
+ !scrollbar)
+ {
+ // One or more references are not assigned
+ return false;
+ }
+ return true;
+ }
+ }
+}
diff --git a/src/UI/Main/Pages/Console/Editor/CodeTheme.cs b/src/UI/Main/Pages/Console/Editor/CodeTheme.cs
new file mode 100644
index 0000000..a2875f0
--- /dev/null
+++ b/src/UI/Main/Pages/Console/Editor/CodeTheme.cs
@@ -0,0 +1,164 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using UnityEngine;
+using Explorer.UI.Main.Pages.Console.Lexer;
+using System.Runtime.InteropServices;
+
+namespace Explorer.UI.Main.Pages.Console
+{
+ public static class CodeTheme
+ {
+ internal static readonly StringBuilder sharedBuilder = new StringBuilder();
+
+ private static char[] delimiterSymbolCache = null;
+ private static MatchLexer[] matchers = null;
+
+ public static string languageName = "C#";
+
+ public static string delimiterSymbols = "[ ] ( ) { } ; : , .";
+
+ public static KeywordGroupMatch[] keywordGroups = new KeywordGroupMatch[]
+ {
+ // VALID KEYWORDS
+
+ new KeywordGroupMatch()
+ {
+ highlightColor = new Color(0.33f, 0.61f, 0.83f, 1.0f),
+ caseSensitive = true,
+ keywords = @"add as ascending await base bool break by byte
+ case catch char checked const continue decimal default descending do dynamic
+ else enum equals false finally fixed float for foreach from global goto group
+ if in int into is join let lock long new null object on orderby out params
+ partial ref remove return sbyte sealed select short sizeof stackalloc string
+ struct switch this throw true try typeof uint ulong unchecked unsafe ushort
+ using value var void where while yield"
+ },
+
+ // INVALID KEYWORDS (cannot use inside method scope)
+
+ new KeywordGroupMatch()
+ {
+ highlightColor = new Color(0.95f, 0.10f, 0.10f, 1.0f),
+ caseSensitive = true,
+ keywords = @"abstract async class delegate explicit extern get
+ implicit interface internal namespace operator override private protected public
+ readonly set static virtual volatile"
+ }
+ };
+
+ ///
+ /// A symbol group used to specify which symbols should be highlighted.
+ ///
+ public static SymbolGroupMatch symbolGroup = new SymbolGroupMatch
+ {
+ symbols = @"[ ] ( ) . ? : + - * / % & | ^ ~ = < > ++ -- && || << >> == != <= >=
+ += -= *= /= %= &= |= ^= <<= >>= -> ?? =>",
+ highlightColor = new Color(0.58f, 0.47f, 0.37f, 1.0f),
+ };
+
+ ///
+ /// A number group used to specify whether numbers should be highlighted.
+ ///
+ public static NumberGroupMatch numberGroup = new NumberGroupMatch
+ {
+ highlightNumbers = true,
+ highlightColor = new Color(0.58f, 0.33f, 0.33f, 1.0f)
+ };
+
+ ///
+ /// A comment group used to specify which strings open and close comments.
+ ///
+ public static CommentGroupMatch commentGroup = new CommentGroupMatch
+ {
+ blockCommentEnd = @"*/",
+ blockCommentStart = @"/*",
+ lineCommentStart = @"//",
+ lineCommentHasPresedence = true,
+ highlightColor = new Color(0.34f, 0.65f, 0.29f, 1.0f),
+ };
+
+ ///
+ /// A literal group used to specify whether quote strings should be highlighted.
+ ///
+ public static LiteralGroupMatch literalGroup = new LiteralGroupMatch
+ {
+ highlightLiterals = true,
+ highlightColor = new Color(0.79f, 0.52f, 0.32f, 1.0f)
+ };
+
+ /////
+ ///// Options group for all auto indent related settings.
+ /////
+ //public static AutoIndent autoIndent;
+
+ // Properties
+ internal static char[] DelimiterSymbols
+ {
+ get
+ {
+ if (delimiterSymbolCache == null)
+ {
+ // Split by space
+ string[] symbols = delimiterSymbols.Split(' ');
+
+ int count = 0;
+
+ // Count the number of valid symbols
+ for (int i = 0; i < symbols.Length; i++)
+ if (symbols[i].Length == 1)
+ count++;
+
+ // Allocate array
+ delimiterSymbolCache = new char[count];
+
+ // Copy symbols
+ for (int i = 0, index = 0; i < symbols.Length; i++)
+ {
+ // Require only 1 character
+ if (symbols[i].Length == 1)
+ {
+ // Get the first character for the string
+ delimiterSymbolCache[index] = symbols[i][0];
+ index++;
+ }
+ }
+ }
+ return delimiterSymbolCache;
+ }
+ }
+
+ internal static MatchLexer[] Matchers
+ {
+ get
+ {
+ if (matchers == null)
+ {
+ List matcherList = new List
+ {
+ commentGroup,
+ symbolGroup,
+ numberGroup,
+ literalGroup
+ };
+ matcherList.AddRange(keywordGroups);
+
+ matchers = matcherList.ToArray();
+ }
+ return matchers;
+ }
+ }
+
+ // Methods
+ internal static void Invalidate()
+ {
+ foreach (KeywordGroupMatch group in keywordGroups)
+ group.Invalidate();
+
+ symbolGroup.Invalidate();
+ commentGroup.Invalidate();
+ numberGroup.Invalidate();
+ literalGroup.Invalidate();
+ }
+ }
+}
diff --git a/src/UI/Main/Pages/Console/Editor/InputTheme.cs b/src/UI/Main/Pages/Console/Editor/InputTheme.cs
new file mode 100644
index 0000000..9299579
--- /dev/null
+++ b/src/UI/Main/Pages/Console/Editor/InputTheme.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using UnityEngine;
+
+namespace Explorer.UI.Main.Pages.Console
+{
+ public static class InputTheme
+ {
+ public static bool allowSyntaxHighlighting = true;
+
+ public static Color caretColor = new Color32(255, 255, 255, 255);
+ public static Color textColor = new Color32(255, 255, 255, 255);
+ public static Color backgroundColor = new Color32(37, 37, 37, 255);
+ public static Color lineHighlightColor = new Color32(50, 50, 50, 255);
+ public static Color lineNumberBackgroundColor = new Color32(25, 25, 25, 255);
+ public static Color lineNumberTextColor = new Color32(180, 180, 180, 255);
+ public static Color scrollbarColor = new Color32(45, 50, 50, 255);
+ }
+}
diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/CommentGroupMatch.cs b/src/UI/Main/Pages/Console/Editor/Lexer/CommentGroupMatch.cs
new file mode 100644
index 0000000..8ba8b40
--- /dev/null
+++ b/src/UI/Main/Pages/Console/Editor/Lexer/CommentGroupMatch.cs
@@ -0,0 +1,221 @@
+using System;
+using System.Collections.Generic;
+using Explorer.Unstrip.ColorUtility;
+using UnityEngine;
+
+namespace Explorer.UI.Main.Pages.Console.Lexer
+{
+ ///
+ /// Used to match line and block comments.
+ ///
+ public sealed class CommentGroupMatch : MatchLexer
+ {
+ [NonSerialized]
+ private string htmlColor = null;
+
+ // Public
+ ///
+ /// The string that denotes the start of a line comment.
+ /// Leave this value empty if line comments should not be highlighted.
+ ///
+ public string lineCommentStart;
+ ///
+ /// The string that denotes the start of a block comment.
+ /// Leave this value empty if block comments should not be highlighted.
+ ///
+ public string blockCommentStart;
+ ///
+ /// The string that denotes the end of a block comment.
+ ///
+ public string blockCommentEnd;
+ ///
+ /// The color that comments will be highlighted with.
+ ///
+ public Color highlightColor = Color.black;
+
+ public bool lineCommentHasPresedence = true;
+
+ // Properties
+ ///
+ /// Retrusn a value indicating whether any comment highlighting is enabled.
+ /// A valid line or block comment start string must be specified in order for comment highlighting to be enabled.
+ ///
+ public bool HasCommentHighlighting
+ {
+ get
+ {
+ return string.IsNullOrEmpty(lineCommentStart) == false ||
+ string.IsNullOrEmpty(blockCommentStart) == false;
+ }
+ }
+
+ ///
+ /// Get the html tag color that comments will be highlighted with.
+ ///
+ public override string HTMLColor
+ {
+ get
+ {
+ // Build html color string
+ if (htmlColor == null)
+ htmlColor = "<#" + highlightColor.ToHex() + ">";
+
+ return htmlColor;
+ }
+ }
+
+ ///
+ /// Returns an enumerable collection of characters from this group that can act as delimiter symbols when they appear after a keyword.
+ ///
+ public override IEnumerable SpecialStartCharacters
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(lineCommentStart) == false)
+ yield return lineCommentStart[0];
+
+ if (string.IsNullOrEmpty(blockCommentEnd) == false)
+ yield return blockCommentEnd[0];
+ }
+ }
+
+ ///
+ /// Returns an enumerable collection of characters from this group that can act as delimiter symbols when they appear before a keyword.
+ ///
+ public override IEnumerable SpecialEndCharacters
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(blockCommentEnd) == false)
+ yield return blockCommentEnd[blockCommentEnd.Length - 1];
+ }
+ }
+
+ // Methods
+ ///
+ /// Causes the cached values to be reloaded.
+ /// Useful for editor visualisation.
+ ///
+ public override void Invalidate()
+ {
+ this.htmlColor = null;
+ }
+
+ ///
+ /// Returns true if the lexer input contains a valid comment format as the next character sequence.
+ ///
+ /// The input lexer
+ /// True if a comment was found or false if not
+ public override bool IsImplicitMatch(ILexer lexer)
+ {
+ if (lineCommentHasPresedence == true)
+ {
+ // Parse line comments then block comments
+ if (IsLineCommentMatch(lexer) == true ||
+ IsBlockCommentMatch(lexer) == true)
+ return true;
+ }
+ else
+ {
+ // Parse block comments then line coments
+ if (IsBlockCommentMatch(lexer) == true ||
+ IsLineCommentMatch(lexer) == true)
+ return true;
+ }
+
+ // Not a comment
+ return false;
+ }
+
+ private bool IsLineCommentMatch(ILexer lexer)
+ {
+ // Check for line comment
+ if (string.IsNullOrEmpty(lineCommentStart) == false)
+ {
+ lexer.Rollback();
+
+ bool match = true;
+
+ for (int i = 0; i < lineCommentStart.Length; i++)
+ {
+ if (lineCommentStart[i] != lexer.ReadNext())
+ {
+ match = false;
+ break;
+ }
+ }
+
+ // Check for valid match
+ if (match == true)
+ {
+ // Read until end
+ while (IsEndLineOrEndFile(lexer, lexer.ReadNext()) == false) ;
+
+ // Matched a single line comment
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private bool IsBlockCommentMatch(ILexer lexer)
+ {
+ // Check for block comment
+ if (string.IsNullOrEmpty(blockCommentStart) == false)
+ {
+ lexer.Rollback();
+
+ bool match = true;
+
+ for (int i = 0; i < blockCommentStart.Length; i++)
+ {
+ if (blockCommentStart[i] != lexer.ReadNext())
+ {
+ match = false;
+ break;
+ }
+ }
+
+ // Check for valid match
+ if (match == true)
+ {
+ // Read until end or closing block
+ while (IsEndLineOrString(lexer, blockCommentEnd) == false) ;
+
+ // Matched a multi-line block commend
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private bool IsEndLineOrEndFile(ILexer lexer, char character)
+ {
+ if (lexer.EndOfStream == true ||
+ character == '\n' ||
+ character == '\r')
+ {
+ // Line end or file end
+ return true;
+ }
+ return false;
+ }
+
+ private bool IsEndLineOrString(ILexer lexer, string endString)
+ {
+ for (int i = 0; i < endString.Length; i++)
+ {
+ // Check for end of stream
+ if (lexer.EndOfStream == true)
+ return true;
+
+ // Check for matching end string
+ if (endString[i] != lexer.ReadNext())
+ return false;
+ }
+
+ // We matched the end string
+ return true;
+ }
+ }
+}
diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/ILexer.cs b/src/UI/Main/Pages/Console/Editor/Lexer/ILexer.cs
new file mode 100644
index 0000000..112528f
--- /dev/null
+++ b/src/UI/Main/Pages/Console/Editor/Lexer/ILexer.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Explorer.UI.Main.Pages.Console.Lexer
+{
+ // Types
+ ///
+ /// Represents a keyword position where a special character may appear.
+ ///
+ public enum SpecialCharacterPosition
+ {
+ ///
+ /// The special character may appear before a keyword.
+ ///
+ Start,
+ ///
+ /// The special character may appear after a keyword.
+ ///
+ End,
+ };
+
+ ///
+ /// Represents a streamable lexer input which can be examined by matchers.
+ ///
+ public interface ILexer
+ {
+ // Properties
+ ///
+ /// Returns true if there are no more characters to read.
+ ///
+ bool EndOfStream { get; }
+
+ ///
+ /// Returns the previously read character or '\0' if there is no previous character.
+ ///
+ char Previous { get; }
+
+ // Methods
+ ///
+ /// Attempt to read the next character.
+ ///
+ /// The next character in the stream or '\0' if the end of stream is reached
+ char ReadNext();
+
+ ///
+ /// Causes the lexer to return its state to a previously commited state.
+ ///
+ /// Use -1 to return to the last commited state or a positive number to represent the number of characters to rollback
+ void Rollback(int amount = -1);
+
+ ///
+ /// Causes all read characters to be commited meaning that rollback will return to this lexer state.
+ ///
+ void Commit();
+
+ ///
+ /// Determines whether the specified character is considered a special symbol by the lexer meaning that it is able to act as a delimiter.
+ ///
+ /// The character to check
+ /// The character position to check. This determines whether the character may appear at the start or end of a keyword
+ /// True if the character is a valid delimiter or false if not
+ bool IsSpecialSymbol(char character, SpecialCharacterPosition position = SpecialCharacterPosition.Start);
+ }
+}
diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/InputStringLexer.cs b/src/UI/Main/Pages/Console/Editor/Lexer/InputStringLexer.cs
new file mode 100644
index 0000000..6afc22c
--- /dev/null
+++ b/src/UI/Main/Pages/Console/Editor/Lexer/InputStringLexer.cs
@@ -0,0 +1,207 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using UnityEngine;
+
+namespace Explorer.UI.Main.Pages.Console.Lexer
+{
+ internal struct InputStringMatchInfo
+ {
+ // Public
+ public int startIndex;
+ public int endIndex;
+ public string htmlColor;
+ }
+
+ internal class InputStringLexer : ILexer
+ {
+ // Private
+ private string inputString = null;
+ private MatchLexer[] matchers = null;
+ private readonly HashSet specialStartSymbols = new HashSet();
+ private readonly HashSet specialEndSymbols = new HashSet();
+ private char current = ' ';
+ private char previous = ' ';
+ private int currentIndex = 0;
+ private int currentLookaheadIndex = 0;
+
+ // Properties
+ public bool EndOfStream
+ {
+ get { return currentLookaheadIndex >= inputString.Length; }
+ }
+
+ public char Previous
+ {
+ get { return previous; }
+ }
+
+ // Methods
+ public void UseMatchers(char[] delimiters, MatchLexer[] matchers)
+ {
+ // Store matchers
+ this.matchers = matchers;
+
+ // Clear old symbols
+ specialStartSymbols.Clear();
+ specialEndSymbols.Clear();
+
+ // Check for any delimiter characters
+ if (delimiters != null)
+ {
+ // Add delimiters
+ foreach (char character in delimiters)
+ {
+ // Add to start
+ if (specialStartSymbols.Contains(character) == false)
+ specialStartSymbols.Add(character);
+
+ // Add to end
+ if (specialEndSymbols.Contains(character) == false)
+ specialEndSymbols.Add(character);
+ }
+ }
+
+ // Check for any matchers
+ if (matchers != null)
+ {
+ // Add all special symbols which can act as a delimiter
+ foreach (MatchLexer lexer in matchers)
+ {
+ foreach (char special in lexer.SpecialStartCharacters)
+ if (specialStartSymbols.Contains(special) == false)
+ specialStartSymbols.Add(special);
+
+ foreach (char special in lexer.SpecialEndCharacters)
+ if (specialEndSymbols.Contains(special) == false)
+ specialEndSymbols.Add(special);
+ }
+ }
+ }
+
+ public IEnumerable LexInputString(string input)
+ {
+ if (input == null || matchers == null || matchers.Length == 0)
+ yield break;
+
+ // Store the input string
+ this.inputString = input;
+ this.current = ' ';
+ this.previous = ' ';
+ this.currentIndex = 0;
+ this.currentLookaheadIndex = 0;
+
+ // Process the input string
+ while (EndOfStream == false)
+ {
+ bool didMatchLexer = false;
+
+ // Read any white spaces
+ ReadWhiteSpace();
+
+ // Process each matcher
+ foreach (MatchLexer matcher in matchers)
+ {
+ // Get the current index
+ int startIndex = currentIndex;
+
+ // Try to match
+ bool isMatched = matcher.IsMatch(this);
+
+ if (isMatched == true)
+ {
+ // Get the end index of the match
+ int endIndex = currentIndex;
+
+ // Set matched flag
+ didMatchLexer = true;
+
+ // Register the match
+ yield return new InputStringMatchInfo
+ {
+ startIndex = startIndex,
+ endIndex = endIndex,
+ htmlColor = matcher.HTMLColor,
+ };
+
+ // Move to next character
+ break;
+ }
+ }
+
+ if (didMatchLexer == false)
+ {
+ // Move to next
+ ReadNext();
+ Commit();
+ }
+ }
+ }
+
+ public char ReadNext()
+ {
+ // Check for end of stream
+ if (EndOfStream == true)
+ return '\0';
+
+ // Update previous character
+ previous = current;
+
+ // Get the character
+ current = inputString[currentLookaheadIndex];
+ currentLookaheadIndex++;
+
+ return current;
+ }
+
+ public void Rollback(int amount = -1)
+ {
+ if (amount == -1)
+ {
+ // Revert to index
+ currentLookaheadIndex = currentIndex;
+ }
+ else
+ {
+ if (currentLookaheadIndex > currentIndex)
+ currentLookaheadIndex -= amount;
+ }
+
+ int previousIndex = currentLookaheadIndex - 1;
+
+ if (previousIndex >= inputString.Length)
+ previous = inputString[inputString.Length - 1];
+ else if (previousIndex >= 0)
+ previous = inputString[previousIndex];
+ else
+ previous = ' ';
+ }
+
+ public void Commit()
+ {
+ currentIndex = currentLookaheadIndex;
+ }
+
+ public bool IsSpecialSymbol(char character, SpecialCharacterPosition position = SpecialCharacterPosition.Start)
+ {
+ if (position == SpecialCharacterPosition.Start)
+ return specialStartSymbols.Contains(character);
+
+ return specialEndSymbols.Contains(character);
+ }
+
+ private void ReadWhiteSpace()
+ {
+ // Read until white space
+ while (char.IsWhiteSpace(ReadNext()) == true)
+ {
+ // Consume the char
+ Commit();
+ }
+
+ // Return lexer state
+ Rollback();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/KeywordGroupMatch.cs b/src/UI/Main/Pages/Console/Editor/Lexer/KeywordGroupMatch.cs
new file mode 100644
index 0000000..1f8254c
--- /dev/null
+++ b/src/UI/Main/Pages/Console/Editor/Lexer/KeywordGroupMatch.cs
@@ -0,0 +1,208 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using Explorer.Unstrip.ColorUtility;
+using UnityEngine;
+
+namespace Explorer.UI.Main.Pages.Console.Lexer
+{
+ ///
+ /// A matcher that checks for a number of predefined keywords in the lexer stream.
+ ///
+ public sealed class KeywordGroupMatch : MatchLexer
+ {
+ // Private
+ private static readonly HashSet shortlist = new HashSet();
+ private static readonly Stack removeList = new Stack();
+ private string[] keywordCache = null;
+ private string htmlColor = null;
+
+ // Public
+ ///
+ /// Used for editor gui only. Has no purpose other than to give the inspector foldout a nice name
+ ///
+ public string group = "Keyword Group"; // This value is not used - it just gives the inspector foldout a nice name
+ ///
+ /// A string containing one or more keywords separated by a space character that will be used by this matcher.
+ ///
+ public string keywords;
+ ///
+ /// The color that any matched keywords will be highlighted.
+ ///
+ public Color highlightColor = Color.black;
+ ///
+ /// Should keyword matching be case sensitive.
+ ///
+ public bool caseSensitive = true;
+
+ // Properties
+ ///
+ /// Get a value indicating whether keyword highlighting is enabled based upon the number of valid keywords found.
+ ///
+ public bool HasKeywordHighlighting
+ {
+ get
+ {
+ // Check for valid keyword
+ if (string.IsNullOrEmpty(keywords) == false)
+ return true;
+
+ return false;
+ }
+ }
+
+ ///
+ /// Get the html formatted color tag that any matched keywords will be highlighted with.
+ ///
+ public override string HTMLColor
+ {
+ get
+ {
+ // Get html color
+ if (htmlColor == null)
+ htmlColor = "<#" + highlightColor.ToHex() + ">";
+
+ return htmlColor;
+ }
+ }
+
+ // Methods
+ ///
+ /// Causes any cached data to be reloaded.
+ ///
+ public override void Invalidate()
+ {
+ this.htmlColor = null;
+ }
+
+ ///
+ /// Check whether the specified lexer has a valid keyword at its current position.
+ ///
+ /// The input lexer to check
+ /// True if the stream has a keyword or false if not
+ public override bool IsImplicitMatch(ILexer lexer)
+ {
+ // Make sure cache is built
+ BuildKeywordCache();
+
+ // Require whitespace before character
+ if (char.IsWhiteSpace(lexer.Previous) == false &&
+ lexer.IsSpecialSymbol(lexer.Previous, SpecialCharacterPosition.End) == false)
+ return false;
+
+ // Clear old data
+ shortlist.Clear();
+
+ // Read the first character
+ int currentIndex = 0;
+ char currentChar = lexer.ReadNext();
+
+ // Add to shortlist
+ for (int i = 0; i < keywordCache.Length; i++)
+ if (CompareChar(keywordCache[i][0], currentChar) == true)
+ shortlist.Add(keywordCache[i]);
+
+ // Check for no matches we can skip the heavy work quickly
+ if (shortlist.Count == 0)
+ return false;
+
+ do
+ {
+ // Check for end of stream
+ if (lexer.EndOfStream == true)
+ {
+ RemoveLongStrings(currentIndex + 1);
+ break;
+ }
+
+ // Read next character
+ currentChar = lexer.ReadNext();
+ currentIndex++;
+
+ // Check for end of word
+ if (char.IsWhiteSpace(currentChar) == true ||
+ lexer.IsSpecialSymbol(currentChar, SpecialCharacterPosition.Start) == true)
+ {
+ // Finalize any matching candidates and undo the reading of the space or special character
+ RemoveLongStrings(currentIndex);
+ lexer.Rollback(1);
+ break;
+ }
+
+ // Check for shortlist match
+ foreach (string keyword in shortlist)
+ {
+ if (currentIndex >= keyword.Length ||
+ CompareChar(keyword[currentIndex], currentChar) == false)
+ {
+ removeList.Push(keyword);
+ }
+ }
+
+ // Remove from short list
+ while (removeList.Count > 0)
+ shortlist.Remove(removeList.Pop());
+ }
+ while (shortlist.Count > 0);
+
+ // Check for any words in the shortlist
+ return shortlist.Count > 0;
+ }
+
+ private void RemoveLongStrings(int length)
+ {
+ foreach (string keyword in shortlist)
+ {
+ if (keyword.Length > length)
+ {
+ removeList.Push(keyword);
+ }
+ }
+
+ // Remove from short list
+ while (removeList.Count > 0)
+ shortlist.Remove(removeList.Pop());
+ }
+
+ private void BuildKeywordCache()
+ {
+ // Check if we need to build the cache
+ if (keywordCache == null)
+ {
+ // Get keyowrds and insert them into a cache array for quick reference
+ var kwSplit = keywords.Split(' ');
+
+ var list = new List();
+ foreach (var kw in kwSplit)
+ {
+ if (!string.IsNullOrEmpty(kw) && kw.Length > 0)
+ {
+ list.Add(kw);
+ }
+ }
+ keywordCache = list.ToArray();
+ }
+ }
+
+ private bool CompareChar(char a, char b)
+ {
+ // Check for direct match
+ if (a == b)
+ return true;
+
+ // Check for case sensitive
+ if (caseSensitive == false)
+ {
+ if (char.ToUpper(a, CultureInfo.CurrentCulture) ==
+ char.ToUpper(b, CultureInfo.CurrentCulture))
+ {
+ // Check for match ignoring case
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/LiteralGroupMatch.cs b/src/UI/Main/Pages/Console/Editor/Lexer/LiteralGroupMatch.cs
new file mode 100644
index 0000000..4601148
--- /dev/null
+++ b/src/UI/Main/Pages/Console/Editor/Lexer/LiteralGroupMatch.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Explorer.Unstrip.ColorUtility;
+using UnityEngine;
+
+namespace Explorer.UI.Main.Pages.Console.Lexer
+{
+ ///
+ /// A matcher that checks for quote strings in the lexer stream.
+ ///
+ [Serializable]
+ public sealed class LiteralGroupMatch : MatchLexer
+ {
+ // Private
+ private string htmlColor = null;
+
+ // Public
+ ///
+ /// Should literal be highlighted.
+ /// When true, any text surrounded by double quotes will be highlighted.
+ ///
+ public bool highlightLiterals = true;
+ ///
+ /// The color that any matched literals will be highlighted.
+ ///
+ public Color highlightColor = Color.black;
+
+ // Properties
+ ///
+ /// Get a value indicating whether literal highlighting is enabled.
+ ///
+ public bool HasLiteralHighlighting
+ {
+ get { return highlightLiterals; }
+ }
+
+ ///
+ /// Get the html formatted color tag that any matched literals will be highlighted with.
+ ///
+ public override string HTMLColor
+ {
+ get
+ {
+ if (htmlColor == null)
+ htmlColor = "<#" + highlightColor.ToHex() + ">";
+
+ return htmlColor;
+ }
+ }
+
+ ///
+ /// Returns special symbols that can act as delimiters when appearing before a word.
+ /// In this case '"' will be returned.
+ ///
+ public override IEnumerable SpecialStartCharacters
+ {
+ get
+ {
+ yield return '"';
+ }
+ }
+
+ ///
+ /// Returns special symbols that can act as delimiters when appearing after a word.
+ /// In this case '"' will be returned.
+ ///
+ public override IEnumerable SpecialEndCharacters
+ {
+ get
+ {
+ yield return '"';
+ }
+ }
+
+ // Methods
+ ///
+ /// Causes any cached data to be reloaded.
+ ///
+ public override void Invalidate()
+ {
+ this.htmlColor = null;
+ }
+
+ ///
+ /// Check whether the specified lexer has a valid literal at its current position.
+ ///
+ /// The input lexer to check
+ /// True if the stream has a literal or false if not
+ public override bool IsImplicitMatch(ILexer lexer)
+ {
+ // Skip highlighting
+ if (highlightLiterals == false)
+ return false;
+
+ // Check for quote
+ if (lexer.ReadNext() == '"')
+ {
+ // Read all characters inside the quote
+ while (IsClosingQuoteOrEndFile(lexer, lexer.ReadNext()) == false) ;
+
+ // Found a valid literal
+ return true;
+ }
+ return false;
+ }
+
+ private bool IsClosingQuoteOrEndFile(ILexer lexer, char character)
+ {
+ if (lexer.EndOfStream == true ||
+ character == '"')
+ {
+ // We have found the end of the file or quote
+ return true;
+ }
+ return false;
+ }
+ }
+}
diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/MatchLexer.cs b/src/UI/Main/Pages/Console/Editor/Lexer/MatchLexer.cs
new file mode 100644
index 0000000..97560c2
--- /dev/null
+++ b/src/UI/Main/Pages/Console/Editor/Lexer/MatchLexer.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace Explorer.UI.Main.Pages.Console.Lexer
+{
+ public abstract class MatchLexer
+ {
+ ///
+ /// Get the html formatted color tag that any matched text will be highlighted with.
+ ///
+ public abstract string HTMLColor { get; }
+
+ ///
+ /// Get an enumerable collection of special characters that can act as delimiter symbols when they appear before a word.
+ ///
+ public virtual IEnumerable SpecialStartCharacters { get { yield break; } }
+
+ ///
+ /// Get an enumerable collection of special characters that can act as delimiter symbols when they appear after a word.
+ ///
+ public virtual IEnumerable SpecialEndCharacters { get { yield break; } }
+
+ // Methods
+ ///
+ /// Checks the specified lexers current position for a certain sequence of characters as defined by the inheriting matcher.
+ ///
+ ///
+ ///
+ public abstract bool IsImplicitMatch(ILexer lexer);
+
+ ///
+ /// Causes the matcher to invalidate any cached data forcing it to be regenerated or reloaded.
+ ///
+ public virtual void Invalidate() { }
+
+ ///
+ /// Attempts to check for a match in the specified lexer.
+ ///
+ /// The lexer that will be checked
+ /// True if a match was found or false if not
+ public bool IsMatch(ILexer lexer)
+ {
+ // Check for implicit match
+ bool match = IsImplicitMatch(lexer);
+
+ if (match == true)
+ {
+ // Consume read tokens
+ lexer.Commit();
+ }
+ else
+ {
+ // Revert lexer state
+ lexer.Rollback();
+ }
+
+ return match;
+ }
+ }
+}
diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/NumberGroupMatch.cs b/src/UI/Main/Pages/Console/Editor/Lexer/NumberGroupMatch.cs
new file mode 100644
index 0000000..2effc0a
--- /dev/null
+++ b/src/UI/Main/Pages/Console/Editor/Lexer/NumberGroupMatch.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using UnityEngine;
+using Explorer.Unstrip.ColorUtility;
+
+namespace Explorer.UI.Main.Pages.Console.Lexer
+{
+ ///
+ /// A matcher that checks for any numbers that appear in the lexer stream.
+ ///
+ public sealed class NumberGroupMatch : MatchLexer
+ {
+ // Private
+ private string htmlColor = null;
+
+ // Public
+ ///
+ /// Should number highlighting be used.
+ /// When false, numbers will appear in the default text color as defined by the current editor theme.
+ ///
+ public bool highlightNumbers = true;
+ ///
+ /// The color that any matched numbers will be highlighted.
+ ///
+ public Color highlightColor = Color.black;
+
+ // Properties
+ ///
+ /// Get a value indicating whether keyword highlighting is enabled.
+ ///
+ public bool HasNumberHighlighting
+ {
+ get { return highlightNumbers; }
+ }
+
+ ///
+ /// Get the html formatted color tag that any matched numbers will be highlighted with.
+ ///
+ public override string HTMLColor
+ {
+ get
+ {
+ if (htmlColor == null)
+ htmlColor = "<#" + highlightColor.ToHex() + ">";
+
+ return htmlColor;
+ }
+ }
+
+ // Methods
+ ///
+ /// Causes any cached data to be reloaded.
+ ///
+ public override void Invalidate()
+ {
+ this.htmlColor = null;
+ }
+
+ ///
+ /// Check whether the specified lexer has a valid number sequence at its current position.
+ ///
+ /// The input lexer to check
+ /// True if the stream has a number sequence or false if not
+ public override bool IsImplicitMatch(ILexer lexer)
+ {
+ // Skip highlighting
+ if (highlightNumbers == false)
+ return false;
+
+ // Require whitespace or symbols before numbers
+ if (char.IsWhiteSpace(lexer.Previous) == false &&
+ lexer.IsSpecialSymbol(lexer.Previous, SpecialCharacterPosition.End) == false)
+ {
+ // There is some other character before the potential number
+ return false;
+ }
+
+ bool matchedNumber = false;
+
+ // Consume the number characters
+ while (lexer.EndOfStream == false)
+ {
+ // Check for valid numerical character
+ if (IsNumberOrDecimalPoint(lexer.ReadNext()) == true)
+ {
+ // We have found a number or decimal
+ matchedNumber = true;
+ lexer.Commit();
+ }
+ else
+ {
+ lexer.Rollback();
+ break;
+ }
+ }
+
+ return matchedNumber;
+ }
+
+ private bool IsNumberOrDecimalPoint(char character) => char.IsNumber(character) || character == '.';
+ }
+
+}
diff --git a/src/UI/Main/Pages/Console/Editor/Lexer/SymbolGroupMatch.cs b/src/UI/Main/Pages/Console/Editor/Lexer/SymbolGroupMatch.cs
new file mode 100644
index 0000000..53dbced
--- /dev/null
+++ b/src/UI/Main/Pages/Console/Editor/Lexer/SymbolGroupMatch.cs
@@ -0,0 +1,223 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Explorer.Unstrip.ColorUtility;
+using ExplorerBeta;
+using UnityEngine;
+
+namespace Explorer.UI.Main.Pages.Console.Lexer
+{
+ ///
+ /// A matcher that checks for a number of predefined symbols in the lexer stream.
+ ///
+ [Serializable]
+ public sealed class SymbolGroupMatch : MatchLexer
+ {
+ // Private
+ private static readonly List shortlist = new List();
+ private static readonly Stack removeList = new Stack();
+ [NonSerialized]
+ private string[] symbolCache = null;
+ [NonSerialized]
+ private string htmlColor = null;
+
+ // Public
+ ///
+ /// A string containing one or more symbols separated by a space character that will be used by the matcher.
+ /// Symbols can be one or more characters long and should not contain numbers or letters.
+ ///
+ public string symbols;
+ ///
+ /// The color that any matched symbols will be highlighted.
+ ///
+ public Color highlightColor = Color.black;
+
+ // Properties
+ ///
+ /// Get a value indicating whether symbol highlighting is enabled based upon the number of valid symbols found.
+ ///
+ public bool HasSymbolHighlighting
+ {
+ get { return symbols.Length > 0; }
+ }
+
+ ///
+ /// Get the html formatted color tag that any matched symbols will be highlighted with.
+ ///
+ public override string HTMLColor
+ {
+ get
+ {
+ // Get html color
+ if (htmlColor == null)
+ htmlColor = "<#" + highlightColor.ToHex() + ">";
+
+ return htmlColor;
+ }
+ }
+
+ ///
+ /// Returns special symbols that can act as delimiters when appearing before a word.
+ ///
+ public override IEnumerable SpecialStartCharacters
+ {
+ get
+ {
+ // Make sure cahce is created
+ BuildSymbolCache();
+
+ // Get the first character of each symbol
+ foreach (string symbol in symbolCache.Where(x => x.Length > 0))
+ yield return symbol[0];
+ }
+ }
+
+ ///
+ /// Returns special symbols that can act as delimiters when appearing after a word.
+ /// In this case '"' will be returned.
+ ///
+ public override IEnumerable SpecialEndCharacters
+ {
+ get
+ {
+ // Make sure cahce is created
+ BuildSymbolCache();
+
+ // Get the first character of each symbol
+ foreach (string symbol in symbolCache.Where(x => x.Length > 0))
+ yield return symbol[0];
+ }
+ }
+
+ // Methods
+ ///
+ /// Causes any cached data to be reloaded.
+ ///
+ public override void Invalidate()
+ {
+ this.htmlColor = null;
+ }
+
+ ///
+ /// Checks whether the specified lexer has a valid symbol at its current posiiton.
+ ///
+ /// The input lexer to check
+ /// True if the stream has a symbol or false if not
+ public override bool IsImplicitMatch(ILexer lexer)
+ {
+ // Make sure cache is created
+ BuildSymbolCache();
+
+ if (lexer == null)
+ {
+ return false;
+ }
+
+ // Require whitespace, letter or digit before symbol
+ if (char.IsWhiteSpace(lexer.Previous) == false &&
+ char.IsLetter(lexer.Previous) == false &&
+ char.IsDigit(lexer.Previous) == false &&
+ lexer.IsSpecialSymbol(lexer.Previous, SpecialCharacterPosition.End) == false)
+ return false;
+
+ // Clear old data
+ shortlist.Clear();
+
+ // Read the first character
+ int currentIndex = 0;
+ char currentChar = lexer.ReadNext();
+
+ // Add to shortlist
+ for (int i = symbolCache.Length - 1; i >= 0; i--)
+ {
+ if (symbolCache[i][0] == currentChar)
+ shortlist.Add(symbolCache[i]);
+ }
+
+ // No potential matches
+ if (shortlist.Count == 0)
+ return false;
+
+ do
+ {
+ // Check for end of stream
+ if (lexer.EndOfStream == true)
+ {
+ RemoveLongStrings(currentIndex + 1);
+ break;
+ }
+
+ // Read next character
+ currentChar = lexer.ReadNext();
+ currentIndex++;
+
+ if (char.IsWhiteSpace(currentChar) == true ||
+ char.IsLetter(currentChar) == true ||
+ char.IsDigit(currentChar) == true ||
+ lexer.IsSpecialSymbol(currentChar, SpecialCharacterPosition.Start) == true)
+ {
+ RemoveLongStrings(currentIndex);
+ lexer.Rollback(1);
+ break;
+ }
+
+ // Check for shortlist match
+ foreach (string symbol in shortlist)
+ {
+ if (currentIndex >= symbol.Length ||
+ symbol[currentIndex] != currentChar)
+ {
+ removeList.Push(symbol);
+ }
+ }
+
+ // Remove from short list
+ while (removeList.Count > 0)
+ shortlist.Remove(removeList.Pop());
+ }
+ while (shortlist.Count > 0);
+
+ // Check for any words in the shortlist
+ return shortlist.Count > 0;
+ }
+
+ private void RemoveLongStrings(int length)
+ {
+ foreach (string keyword in shortlist)
+ {
+ if (keyword.Length > length)
+ {
+ removeList.Push(keyword);
+ }
+ }
+
+ // Remove from short list
+ while (removeList.Count > 0)
+ shortlist.Remove(removeList.Pop());
+ }
+
+ private void BuildSymbolCache()
+ {
+ if (string.IsNullOrEmpty(symbols))
+ {
+ ExplorerCore.LogWarning("Symbol cache is null!");
+ symbolCache = new string[0];
+ }
+ else
+ {
+ // Get symbols and insert them into a cache array for quick reference
+ var symSplit = symbols.Split(' ');
+ var list = new List();
+ foreach (var sym in symSplit)
+ {
+ if (!string.IsNullOrEmpty(sym) && sym.Length > 0)
+ {
+ list.Add(sym);
+ }
+ }
+ symbolCache = list.ToArray();
+ }
+ }
+ }
+}
diff --git a/src/UI/Main/Pages/Console/ScriptEvaluator/AutoComplete.cs b/src/UI/Main/Pages/Console/ScriptEvaluator/AutoComplete.cs
new file mode 100644
index 0000000..dbc22f0
--- /dev/null
+++ b/src/UI/Main/Pages/Console/ScriptEvaluator/AutoComplete.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using ExplorerBeta;
+using UnityEngine;
+
+// Thanks to ManlyMarco for this
+
+namespace Explorer.UI.Main.Pages.Console
+{
+ public struct AutoComplete
+ {
+ public string Full => Prefix + Addition;
+
+ public readonly string Prefix;
+ public readonly string Addition;
+ public readonly Contexts Context;
+
+ public Color TextColor => Context == Contexts.Namespace
+ ? Color.gray
+ : Color.white;
+
+ public AutoComplete(string addition, string prefix, Contexts type)
+ {
+ Addition = addition;
+ Prefix = prefix;
+ Context = type;
+ }
+
+ public enum Contexts
+ {
+ Namespace,
+ Other
+ }
+ }
+
+ public static class AutoCompleteHelpers
+ {
+ public static HashSet Namespaces => _namespaces ?? GetNamespaces();
+ private static HashSet _namespaces;
+
+ private static HashSet GetNamespaces()
+ {
+ var set = new HashSet(
+ AppDomain.CurrentDomain.GetAssemblies()
+ .SelectMany(GetTypes)
+ .Where(x => x.IsPublic && !string.IsNullOrEmpty(x.Namespace))
+ .Select(x => x.Namespace));
+
+ return _namespaces = set;
+
+ IEnumerable GetTypes(Assembly asm) => asm.TryGetTypes();
+ }
+ }
+}
diff --git a/src/UI/Main/Pages/Console/ScriptEvaluator/ScriptEvaluator.cs b/src/UI/Main/Pages/Console/ScriptEvaluator/ScriptEvaluator.cs
new file mode 100644
index 0000000..86c4390
--- /dev/null
+++ b/src/UI/Main/Pages/Console/ScriptEvaluator/ScriptEvaluator.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using Mono.CSharp;
+
+// Thanks to ManlyMarco for this
+
+namespace Explorer.UI.Main.Pages.Console
+{
+ internal class ScriptEvaluator : Evaluator, IDisposable
+ {
+ private static readonly HashSet StdLib = new HashSet(StringComparer.InvariantCultureIgnoreCase)
+ {
+ "mscorlib", "System.Core", "System", "System.Xml"
+ };
+
+ private readonly TextWriter _logger;
+
+ public ScriptEvaluator(TextWriter logger) : base(BuildContext(logger))
+ {
+ _logger = logger;
+
+ ImportAppdomainAssemblies(ReferenceAssembly);
+ AppDomain.CurrentDomain.AssemblyLoad += OnAssemblyLoad;
+ }
+
+ public void Dispose()
+ {
+ AppDomain.CurrentDomain.AssemblyLoad -= OnAssemblyLoad;
+ _logger.Dispose();
+ }
+
+ private void OnAssemblyLoad(object sender, AssemblyLoadEventArgs args)
+ {
+ string name = args.LoadedAssembly.GetName().Name;
+ if (StdLib.Contains(name))
+ return;
+ ReferenceAssembly(args.LoadedAssembly);
+ }
+
+ private static CompilerContext BuildContext(TextWriter tw)
+ {
+ var reporter = new StreamReportPrinter(tw);
+
+ var settings = new CompilerSettings
+ {
+ Version = LanguageVersion.Experimental,
+ GenerateDebugInfo = false,
+ StdLib = true,
+ Target = Target.Library,
+ WarningLevel = 0,
+ EnhancedWarnings = false
+ };
+
+ return new CompilerContext(settings, reporter);
+ }
+
+ private static void ImportAppdomainAssemblies(Action import)
+ {
+ foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
+ {
+ string name = assembly.GetName().Name;
+ if (StdLib.Contains(name))
+ continue;
+ import(assembly);
+ }
+ }
+ }
+}
diff --git a/src/UI/Main/Pages/Console/ScriptEvaluator/ScriptInteraction.cs b/src/UI/Main/Pages/Console/ScriptEvaluator/ScriptInteraction.cs
new file mode 100644
index 0000000..73fc83e
--- /dev/null
+++ b/src/UI/Main/Pages/Console/ScriptEvaluator/ScriptInteraction.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Mono.CSharp;
+using UnityEngine;
+using ExplorerBeta;
+
+namespace Explorer.UI.Main
+{
+ public class ScriptInteraction : InteractiveBase
+ {
+ public static void Log(object message)
+ {
+ ExplorerCore.Log(message);
+ }
+
+ public static object CurrentTarget()
+ {
+ throw new NotImplementedException("TODO");
+ }
+
+ public static object[] AllTargets()
+ {
+ throw new NotImplementedException("TODO");
+ }
+
+ public static void Inspect(object obj)
+ {
+ throw new NotImplementedException("TODO");
+ }
+
+ public static void Inspect(Type type)
+ {
+ throw new NotImplementedException("TODO");
+ }
+
+ public static void Help()
+ {
+ ExplorerCore.Log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
+ ExplorerCore.Log(" C# Console Help ");
+ ExplorerCore.Log("");
+ ExplorerCore.Log("The following helper methods are available:");
+ ExplorerCore.Log("");
+ ExplorerCore.Log("void Log(object message)");
+ ExplorerCore.Log(" prints a message to the console window and debug log");
+ ExplorerCore.Log(" usage: Log(\"hello world\");");
+ ExplorerCore.Log("");
+ ExplorerCore.Log("object CurrentTarget()");
+ ExplorerCore.Log(" returns the target object of the current tab (in tab view mode only)");
+ ExplorerCore.Log(" usage: var target = CurrentTarget();");
+ ExplorerCore.Log("");
+ ExplorerCore.Log("object[] AllTargets()");
+ ExplorerCore.Log(" returns an object[] array containing all currently inspected objects");
+ ExplorerCore.Log(" usage: var targets = AllTargets();");
+ ExplorerCore.Log("");
+ ExplorerCore.Log("void Inspect(object obj)");
+ ExplorerCore.Log(" inspects the provided object in a new window.");
+ ExplorerCore.Log(" usage: Inspect(Camera.main);");
+ ExplorerCore.Log("");
+ ExplorerCore.Log("void Inspect(Type type)");
+ ExplorerCore.Log(" attempts to inspect the provided type with static-only reflection.");
+ ExplorerCore.Log(" usage: Inspect(typeof(Camera));");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/UI/Main/Pages/ConsolePage.cs b/src/UI/Main/Pages/ConsolePage.cs
index fb90d31..bd0b42e 100644
--- a/src/UI/Main/Pages/ConsolePage.cs
+++ b/src/UI/Main/Pages/ConsolePage.cs
@@ -1,7 +1,17 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Text;
+using Explorer.UI.Main.Pages.Console;
+using ExplorerBeta;
+using ExplorerBeta.UI;
+using ExplorerBeta.UI.Main;
+using ExplorerBeta.Unstrip.Resources;
+using TMPro;
+using UnhollowerRuntimeLib;
+using UnityEngine;
+using UnityEngine.UI;
namespace Explorer.UI.Main.Pages
{
@@ -9,14 +19,350 @@ namespace Explorer.UI.Main.Pages
{
public override string Name => "C# Console";
+ public static ConsolePage Instance { get; private set; }
+
+ private CodeEditor codeEditor;
+
+ private ScriptEvaluator m_evaluator;
+
+ public static List AutoCompletes = new List();
+ public static List UsingDirectives;
+
+ public static readonly string[] DefaultUsing = new string[]
+ {
+ "System",
+ "UnityEngine",
+ "System.Linq",
+ "System.Collections",
+ "System.Collections.Generic",
+ "System.Reflection"
+ };
+
public override void Init()
{
+ Instance = this;
+ try
+ {
+ ResetConsole();
+
+ foreach (var use in DefaultUsing)
+ {
+ AddUsing(use);
+ }
+
+ ConstructUI();
+ }
+ catch (Exception e)
+ {
+ ExplorerCore.LogWarning($"Error setting up console!\r\nMessage: {e.Message}");
+ // TODO
+ }
}
public override void Update()
{
+ codeEditor?.Update();
+ }
+ internal string AsmToUsing(string asm, bool richText = false)
+ {
+ if (richText)
+ {
+ return $"using {asm};";
+ }
+ return $"using {asm};";
+ }
+
+ public void AddUsing(string asm)
+ {
+ if (!UsingDirectives.Contains(asm))
+ {
+ UsingDirectives.Add(asm);
+ Evaluate(AsmToUsing(asm), true);
+ }
+ }
+
+ public object Evaluate(string str, bool suppressWarning = false)
+ {
+ object ret = VoidType.Value;
+
+ m_evaluator.Compile(str, out var compiled);
+
+ try
+ {
+ if (compiled == null)
+ {
+ throw new Exception("Mono.Csharp Service was unable to compile the code provided.");
+ }
+
+ compiled.Invoke(ref ret);
+ }
+ catch (Exception e)
+ {
+ if (!suppressWarning)
+ {
+ ExplorerCore.LogWarning(e.GetType() + ", " + e.Message);
+ }
+ }
+
+ return ret;
+ }
+
+ public void ResetConsole()
+ {
+ if (m_evaluator != null)
+ {
+ m_evaluator.Dispose();
+ }
+
+ m_evaluator = new ScriptEvaluator(new StringWriter(new StringBuilder())) { InteractiveBaseClass = typeof(ScriptInteraction) };
+
+ UsingDirectives = new List();
+ }
+
+ #region UI Construction
+
+ public void ConstructUI()
+ {
+ Content = UIFactory.CreateUIObject("C# Console", MainMenu.Instance.PageViewport);
+
+ var mainLayout = Content.AddComponent();
+ mainLayout.preferredHeight = 300;
+ mainLayout.flexibleHeight = 4;
+
+ var mainGroup = Content.AddComponent();
+ mainGroup.childControlHeight = true;
+ mainGroup.childControlWidth = true;
+ mainGroup.childForceExpandHeight = true;
+ mainGroup.childForceExpandWidth = true;
+
+ var topBarObj = UIFactory.CreateHorizontalGroup(Content);
+ var topBarLayout = topBarObj.AddComponent();
+ topBarLayout.preferredHeight = 50;
+ topBarLayout.flexibleHeight = 0;
+
+ var topBarGroup = topBarObj.GetComponent();
+ topBarGroup.padding.left = 30;
+ topBarGroup.padding.right = 30;
+ topBarGroup.padding.top = 8;
+ topBarGroup.padding.bottom = 8;
+ topBarGroup.spacing = 10;
+ topBarGroup.childForceExpandHeight = true;
+ topBarGroup.childForceExpandWidth = true;
+ topBarGroup.childControlWidth = true;
+ topBarGroup.childControlHeight = true;
+
+ var topBarLabel = UIFactory.CreateLabel(topBarObj, TextAnchor.MiddleLeft);
+ var topBarLabelLayout = topBarLabel.AddComponent();
+ topBarLabelLayout.preferredWidth = 800;
+ topBarLabelLayout.flexibleWidth = 10;
+ var topBarText = topBarLabel.GetComponent();
+ topBarText.text = "C# Console";
+ topBarText.fontSize = 20;
+
+ var compileBtnObj = UIFactory.CreateButton(topBarObj);
+ var compileBtnLayout = compileBtnObj.AddComponent();
+ compileBtnLayout.preferredWidth = 80;
+ compileBtnLayout.flexibleWidth = 0;
+ var compileButton = compileBtnObj.GetComponent