using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using FlaxEditor; using FlaxEngine; using FlaxEngine.GUI; namespace Cabrito { public class ConsoleContentTextBox : Control { [HideInEditor] public ConsoleInputTextBox inputBox; protected TextLayoutOptions _layout; public FontReference Font; public float LineSpacing = 1.0f; public TextWrapping Wrapping; public Color SelectionColor = new Color(0x00, 0x7A, 0xCC, 0xFF); public Color BackgroundSelectedColor = Color.Transparent; public float BackgroundSelectedFlashSpeed = 0; public Color BorderSelectedColor = Color.Transparent; public float CaretFlashSpeed = 0; public Color BorderColor; public Color TextColor = Color.White; public int DefaultMargin = 1; private Vector2 scrollOffset = new Vector2(0); private int selectionStartLine = 0; private int selectionStartChar = 0; private int selectionEndLine = 0; private int selectionEndChar = 0; private bool selectionActive; public bool HasSelection => !(selectionStartLine == selectionEndLine && selectionStartChar == selectionEndChar); public ConsoleContentTextBox() : base() { } public ConsoleContentTextBox(ConsoleInputTextBox inputBox, float x, float y, float width, float height) : base(x, y, width, height) { this.inputBox = inputBox; Height = height; _layout = TextLayoutOptions.Default; _layout.VerticalAlignment = TextAlignment.Near; _layout.TextWrapping = TextWrapping.WrapChars; _layout.Bounds = new Rectangle(DefaultMargin, 1, Width - 2 * DefaultMargin, Height - 2); //IsMultiline = true; //IsReadOnly = true; //CaretColor = new Color(0f, 0f, 0f, 0f); AutoFocus = false; } public int GetFontHeight() { var font = Font.GetFont(); if (font == null) return (int)Height; return (int)Mathf.Round(LineSpacing * font.Height * Scale.Y); } struct LineInfo { public int lineIndex; public int lineOffset; } private void CalculateVisibleLines(IReadOnlyCollection lines, out int firstVisibleLine, out int lastVisibleLine, out LineInfo[] wrappedLines) { wrappedLines = null; firstVisibleLine = 0; lastVisibleLine = 0; var font = Font.GetFont(); if (!font) return; float fontWidth = (int)font.MeasureText("a").X; // hacky, but works for fixed-size fonts... int lineMaxChars = (int)(Width / fontWidth); int lineMaxLines = (int)(Height / font.Height); int numLines = 0; int lineIndex = lines.Count - 1; List lineInfos = new List(lineMaxLines); foreach (string line in lines.Reverse()) { int numChars = 0; while (numChars < line.Length) { LineInfo li = new LineInfo(); li.lineIndex = lineIndex; li.lineOffset = numChars; lineInfos.Add(li); numChars += lineMaxChars; } numLines++; lineIndex--; if (lineInfos.Count > lineMaxLines) break; } lineInfos.Reverse(); wrappedLines = lineInfos.ToArray(); //return lines[lines.Count - numLines .. lines.Count]; // C# 8.0... firstVisibleLine = lines.Count - numLines; lastVisibleLine = lines.Count; } public static double accumDrawTime = 0.0; public static long accumDrawTimes = 0; public override void Draw() { // Cache data var rect = new Rectangle(Vector2.Zero, Size); var font = Font.GetFont(); if (!font) return; Profiler.BeginEvent("ConsoleContetTextBoxDraw"); Stopwatch sw = Stopwatch.StartNew(); // Background Color backColor = BackgroundColor; if (IsMouseOver) backColor = BackgroundSelectedColor; if (backColor.A > 0.0f) Render2D.FillRectangle(rect, backColor); Color borderColor = IsFocused ? BorderSelectedColor : BorderColor; if (borderColor.A > 0.0f) Render2D.DrawRectangle(rect, borderColor); var lines = Console.GetLines(); if (lines.Count > 0) { // Apply view offset and clip mask var textClipRectangle = new Rectangle(1, 1, Width - 2, Height - 2); Render2D.PushClip(textClipRectangle); /*bool useViewOffset = !_viewOffset.IsZero; if (useViewOffset) Render2D.PushTransform(Matrix3x3.Translation2D(-_viewOffset));*/ // Check if any text is selected to draw selection //if (HasSelection) { /*Vector2 leftEdge = font.GetCharPosition(text, SelectionLeft + TextPrefix.Length, ref _layout); Vector2 rightEdge = font.GetCharPosition(text, SelectionRight + TextPrefix.Length, ref _layout); float fontHeight = GetFontHeight(); float spacing = GetRealLineSpacing(); // Draw selection background float alpha = Mathf.Min(1.0f, Mathf.Cos(_animateTime * BackgroundSelectedFlashSpeed) * 0.5f + 1.3f); alpha = alpha * alpha; Color selectionColor = SelectionColor * alpha; int selectedLinesCount = 1 + Mathf.FloorToInt((rightEdge.Y - leftEdge.Y) / fontHeight); if (selectedLinesCount == 1) // Selected is part of single line { Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, rightEdge.X - leftEdge.X, fontHeight); Render2D.FillRectangle(r1, selectionColor); } else // Selected is more than one line { float leftMargin = _layout.Bounds.Location.X; Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, 1000000000, fontHeight); Render2D.FillRectangle(r1, selectionColor); for (int i = 3; i <= selectedLinesCount; i++) { leftEdge.Y += fontHeight; Rectangle r = new Rectangle(leftMargin, leftEdge.Y, 1000000000, fontHeight); Render2D.FillRectangle(r, selectionColor); } Rectangle r2 = new Rectangle(leftMargin, rightEdge.Y, rightEdge.X - leftMargin, fontHeight); Render2D.FillRectangle(r2, selectionColor); }*/ } // Make sure lengthy lines are split CalculateVisibleLines(lines, out int startLine, out int lastLine, out LineInfo[] wrappedLines); float lineHeight = font.Height; float accumHeight = wrappedLines.Length * lineHeight; int selectionLeftLine = selectionStartLine; int selectionLeftChar = selectionStartChar; int selectionRightLine = selectionEndLine; int selectionRightChar = selectionEndChar; if (selectionLeftLine > selectionRightLine || (selectionLeftLine == selectionRightLine && selectionLeftChar > selectionRightChar)) { selectionLeftLine = selectionEndLine; selectionLeftChar = selectionEndChar; selectionRightLine = selectionStartLine; selectionRightChar = selectionStartChar; } // render selection if (selectionActive) { //float alpha = Mathf.Min(1.0f, Mathf.Cos(_animateTime * BackgroundSelectedFlashSpeed) * 0.5f + 1.3f); //alpha = alpha * alpha; Color selectionColor = SelectionColor;// * alpha; TextLayoutOptions layout = _layout; layout.Bounds.Y -= accumHeight - Height; for (int i = startLine; i < lastLine; i++) { string line = lines.ElementAt(i); Rectangle selectionRect = new Rectangle(layout.Bounds.X, layout.Bounds.Y, 0f, 0f); // apply selection if (i >= selectionLeftLine && i <= selectionRightLine) { if (i > selectionLeftLine && i < selectionRightLine) { // whole line is selected Vector2 lineSize = font.MeasureText(line); selectionRect.Width = lineSize.X; selectionRect.Height = lineSize.Y; } else if (i == selectionLeftLine) { if (i < selectionRightLine) { // right side of the line is selected Vector2 leftSize = font.MeasureText(line.Substring(0, selectionLeftChar)); Vector2 rightSize = font.MeasureText(line.Substring(selectionLeftChar)); selectionRect.X += leftSize.X; selectionRect.Width = rightSize.X; selectionRect.Height = rightSize.Y; //int diff = line.Length - selectionLeftChar; //line = line.Substring(0, selectionLeftChar) + (diff > 0 ? new string('X', diff) : ""); } else if (i == selectionRightLine) { // selecting middle of the one line Vector2 lineSize = font.MeasureText(line); Vector2 leftSize = font.MeasureText(line.Substring(0, selectionLeftChar)); Vector2 rightSize = font.MeasureText(line.Substring(selectionRightChar)); selectionRect.X += leftSize.X; selectionRect.Width = lineSize.X - (leftSize.X + rightSize.Y); selectionRect.Height = lineSize.Y; //int diff = selectionRightChar - selectionLeftChar; //line = line.Substring(0, selectionLeftChar) + (diff > 0 ? new string('X', diff) : "") + line.Substring(selectionRightChar); } } else if (i == selectionRightLine) { // left side of the line is selected Vector2 leftSize = font.MeasureText(line.Substring(0, selectionRightChar)); selectionRect.Width = leftSize.X; selectionRect.Height = leftSize.Y; //line = (selectionRightChar > 0 ? new string('X', selectionRightChar) : "") + line.Substring(selectionRightChar); } } Render2D.FillRectangle(selectionRect, selectionColor); layout.Bounds.Y += lineHeight; } } // render lines { TextLayoutOptions layout = _layout; layout.Bounds.Y -= accumHeight - Height; for (int i = startLine; i < lastLine; i++) { string line = lines.ElementAt(i); Render2D.DrawText(font, line, TextColor, ref layout); layout.Bounds.Y += lineHeight; } } /*if (CaretPosition > -1) { var prefixSize = TextPrefix != "" ? font.MeasureText(TextPrefix) : new Vector2(); var caretBounds = CaretBounds; caretBounds.X += prefixSize.X; float alpha = Mathf.Saturate(Mathf.Cos(_animateTime * CaretFlashSpeed) * 0.5f + 0.7f); alpha = alpha * alpha * alpha * alpha * alpha * alpha; Render2D.FillRectangle(caretBounds, CaretColor * alpha); }*/ // Restore rendering state //if (useViewOffset) // Render2D.PopTransform(); Render2D.PopClip(); } sw.Stop(); accumDrawTime += sw.Elapsed.TotalSeconds; accumDrawTimes++; Profiler.EndEvent(); } private void OnSelectingBegin() { if (selectionActive) return; selectionActive = true; StartMouseCapture(); } private void OnSelectingEnd() { if (!selectionActive) return; selectionActive = false; EndMouseCapture(); } public bool HitTestText(Vector2 location, out int hitLine, out int hitChar) { hitLine = 0; hitChar = 0; var font = Font.GetFont(); if (font == null) return false; var lines = Console.GetLines(); CalculateVisibleLines(lines, out int startLine, out int lastLine, out LineInfo[] wrappedLines); TextLayoutOptions layout = _layout; float lineHeight = font.Height; float visibleHeight = wrappedLines.Length * lineHeight; float top = layout.Bounds.Bottom - visibleHeight; int lineMaxLines = (int)(Height / font.Height); int hiddenLines = 0; if (wrappedLines.Length > lineMaxLines) hiddenLines = wrappedLines.Length - lineMaxLines; //if (top < layout.Bounds.Top) // hiddenLines = (int)Math.Ceiling((layout.Bounds.Top - top) / (float)lineHeight); int hitWrappedLine = (int)((location.Y - top) / lineHeight) + hiddenLines; if (hitWrappedLine < 0 || hitWrappedLine >= wrappedLines.Length) return false; hitLine = wrappedLines[hitWrappedLine].lineIndex; string line = lines.ElementAt(hitLine); layout.Bounds.Y = top + ((hitWrappedLine) * lineHeight) + 22.5f; layout.Bounds.Height = top + visibleHeight; /*if (layout.Bounds.Y < 0) { layout.Bounds.Y = 1; }*/ hitChar = font.HitTestText(line, location, ref layout); return true; } /*public int CharIndexAtPoint(ref Vector2 location) { return HitTestText(location + _viewOffset); }*/ public override bool OnMouseDown(Vector2 location, MouseButton button) { bool ret = false; if (button == MouseButton.Left && !IsFocused) { Focus(); ret = true; } var lines = Console.GetLines(); if (button == MouseButton.Left && lines.Count > 0) { bool selectionStarted = !selectionActive; Focus(); OnSelectingBegin(); //FlaxEngine.Debug.Log("mousedown, started: " + selectionStarted.ToString()); if (HitTestText(location, out int hitLine, out int hitChar)) { selectionStartLine = hitLine; selectionStartChar = hitChar; selectionEndLine = hitLine; selectionEndChar = hitChar; } else throw new Exception("no???"); // Select range with shift /*if (_selectionStart != -1 && RootWindow.GetKey(KeyboardKeys.Shift) && SelectionLength == 0) { if (hitPos < _selectionStart) SetSelection(hitPos, _selectionStart); else SetSelection(_selectionStart, hitPos); } else { SetSelection(hitPos); }*/ return true; } return ret; } public override void OnMouseMove(Vector2 location) { if (selectionActive) { if (HitTestText(location, out int hitLine, out int hitChar)) { selectionEndLine = hitLine; selectionEndChar = hitChar; } } } /// public override bool OnMouseUp(Vector2 location, MouseButton button) { if (button == MouseButton.Left) { OnSelectingEnd(); return true; } return false; } } public class ConsoleContentTextBox_Old : ConsoleTextBoxBase { [HideInEditor] public ConsoleInputTextBox inputBox; private string _textCache; private long _textCacheLines; public override string Text { get { var lines = Console.GetLines(); if (_textCache == null || _textCacheLines != lines.Count) { //Deselect(); //ResetViewOffset(); _textCache = string.Join("\n", lines); _textCacheLines = lines.Count; OnTextChanged(); } return _textCache; } set => _textCache = value; } public ConsoleContentTextBox_Old(ConsoleInputTextBox inputBox, float x, float y, float width, float height) : base(x, y, width, height) { this.inputBox = inputBox; Height = height; IsMultiline = true; IsReadOnly = true; CaretColor = new Color(0f, 0f, 0f, 0f); AutoFocus = false; } public override void OnGotFocus() { base.OnGotFocus(); } public override void OnLostFocus() { ClearSelection(); base.OnLostFocus(); } protected override void OnTextChanged() { base.OnTextChanged(); ScrollToEnd(); } public override bool OnKeyDown(KeyboardKeys key) { bool ret; switch (key) { case KeyboardKeys.Escape: ret = true; // disable text restoration break; case KeyboardKeys.Home: case KeyboardKeys.End: // TODO: scroll top and scroll bottom ret = true; break; case KeyboardKeys.ArrowUp: case KeyboardKeys.ArrowDown: case KeyboardKeys.ArrowLeft: case KeyboardKeys.ArrowRight: ret = true; // input box has priority break; case KeyboardKeys.PageUp: case KeyboardKeys.PageDown: return true; default: ret = base.OnKeyDown(key); break; } if (inputBox == null) return ret; return inputBox.OnKeyDown(key); } public override void OnKeyUp(KeyboardKeys key) { base.OnKeyUp(key); if (inputBox != null) inputBox.OnKeyUp(key); } public override bool OnCharInput(char c) { if (inputBox == null) return base.OnCharInput(c); Focus(inputBox); return inputBox.OnCharInput(c); } } }