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 / Platform.DpiScale) * Scale.Y); } struct LineInfo { public int lineIndex; public int lineOffset; public int lineLength; } 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 / Platform.DpiScale)); // number of fully visible lines int numLines = 0; int lineIndex = lines.Count - 1; List lineInfos = new List(lineMaxLines+1); foreach (string line in lines.Reverse()) { int numChars = 0; int startIndex = lineInfos.Count; while (numChars < line.Length) { LineInfo li = new LineInfo(); li.lineIndex = lineIndex; li.lineOffset = numChars; li.lineLength = Math.Min(line.Length - numChars, lineMaxChars); lineInfos.Add(li); numChars += lineMaxChars; } if (lineInfos.Count - startIndex > 1) lineInfos.Reverse(startIndex, lineInfos.Count - startIndex); 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 / Platform.DpiScale; float accumHeight = wrappedLines.Length * lineHeight; // selection in line-space, wrapping ignored 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++) foreach (LineInfo li in wrappedLines) { var lineIndex = li.lineIndex; string fullLine = lines.ElementAt(lineIndex); string line = fullLine.Substring(li.lineOffset, li.lineLength); int leftChar = selectionLeftChar; int rightChar = selectionRightChar; Rectangle selectionRect = new Rectangle(layout.Bounds.X, layout.Bounds.Y, 0f, 0f); // apply selection if (lineIndex >= selectionLeftLine && lineIndex <= selectionRightLine) { if (lineIndex > selectionLeftLine && lineIndex < selectionRightLine) { // whole line is selected Vector2 lineSize = font.MeasureText(line); selectionRect.Width = lineSize.X; selectionRect.Height = lineSize.Y; } else if (lineIndex == selectionLeftLine) { if (lineIndex < selectionRightLine) { // right side of the line is selected Vector2 leftSize = font.MeasureText(fullLine.Substring(0, leftChar)); Vector2 rightSize = font.MeasureText(fullLine.Substring(leftChar)); 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 (lineIndex == selectionRightLine && leftChar != rightChar) { // selecting middle of the one line Vector2 lineSize = font.MeasureText(fullLine); Vector2 leftSize = font.MeasureText(fullLine.Substring(0, leftChar)); Vector2 midSize = font.MeasureText(fullLine.Substring(leftChar, rightChar-leftChar)); selectionRect.X += leftSize.X; selectionRect.Width = midSize.X; selectionRect.Height = lineSize.Y; //int diff = selectionRightChar - selectionLeftChar; //line = line.Substring(0, selectionLeftChar) + (diff > 0 ? new string('X', diff) : "") + line.Substring(selectionRightChar); } } else if (lineIndex == selectionRightLine) { // left side of the line is selected Vector2 leftSize = font.MeasureText(fullLine.Substring(0, rightChar)); 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; foreach (LineInfo li in wrappedLines) { var lineIndex = li.lineIndex; string line = lines.ElementAt(lineIndex).Substring(li.lineOffset, li.lineLength); 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 lineHeightNormalized = font.Height; float lineHeight = lineHeightNormalized / Platform.DpiScale; float visibleHeight = wrappedLines.Length * lineHeight; float top = (layout.Bounds.Bottom - visibleHeight) / Platform.DpiScale; // UI coordinate space remains normalized int lineMaxLines = (int)(Height / lineHeight); 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).Substring(wrappedLines[hitWrappedLine].lineOffset, wrappedLines[hitWrappedLine].lineLength); layout.Bounds.Y = top + ((hitWrappedLine) * lineHeight); layout.Bounds.Height = top + 9999;//(visibleHeight / Platform.DpiScale); /*if (layout.Bounds.Y < 0) { layout.Bounds.Y = 1; }*/ hitChar = font.HitTestText(line, location, ref layout); hitChar += wrappedLines[hitWrappedLine].lineOffset; //FlaxEngine.Debug.Log(string.Format("hit line {0}/{1}, char {2}", hitWrappedLine, wrappedLines.Length, hitChar)); return true; } /*public int CharIndexAtPoint(ref Vector2 location) { return HitTestText(location + _viewOffset); }*/ public override bool OnKeyDown(KeyboardKeys key) { bool shiftDown = Root.GetKey(KeyboardKeys.Shift); bool ctrlDown = Root.GetKey(KeyboardKeys.Control); if ((shiftDown && key == KeyboardKeys.Delete) || (ctrlDown && key == KeyboardKeys.Insert) || (ctrlDown && key == KeyboardKeys.C) || (ctrlDown && key == KeyboardKeys.X)) { Copy(); return true; } //else if (ctrlDown && key == KeyboardKeys.A) // SelectAll(); return base.OnKeyDown(key); } 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; FlaxEngine.Debug.Log(string.Format("start line {0} char {1}", hitLine, 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; FlaxEngine.Debug.Log(string.Format("end line {0} char {1}", hitLine, hitChar)); } } } /// public override bool OnMouseUp(Vector2 location, MouseButton button) { if (button == MouseButton.Left) { OnSelectingEnd(); Focus(inputBox); return true; } return false; } protected void Copy() { if (!selectionActive) return; // selection in line-space, wrapping ignored 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; } var lines = Console.GetLines(); CalculateVisibleLines(lines, out int startLine, out int lastLine, out LineInfo[] wrappedLines); StringBuilder selectedText = new StringBuilder(); int lastLineIndex = -1; foreach (LineInfo li in wrappedLines) { var lineIndex = li.lineIndex; if (lineIndex < selectionLeftLine || lineIndex > selectionRightLine) continue; if (lastLineIndex != lineIndex && lastLineIndex != -1) selectedText.AppendLine(); lastLineIndex = lineIndex; string fullLine = lines.ElementAt(lineIndex); string line = fullLine.Substring(li.lineOffset, li.lineLength); int leftChar = selectionLeftChar; int rightChar = selectionRightChar; if (lineIndex >= selectionLeftLine && lineIndex <= selectionRightLine) { if (lineIndex > selectionLeftLine && lineIndex < selectionRightLine) { // whole line is selected selectedText.Append(line); } else if (lineIndex == selectionLeftLine) { if (lineIndex < selectionRightLine) { // right side of the line is selected selectedText.Append(fullLine.Substring(leftChar)); } else if (lineIndex == selectionRightLine && leftChar != rightChar) { // selecting middle of the one line selectedText.Append(fullLine.Substring(leftChar, rightChar - leftChar)); } } else if (lineIndex == selectionRightLine) { // left side of the line is selected selectedText.Append(fullLine.Substring(0, rightChar)); } } } if (selectedText.Length > 0) Clipboard.Text = selectedText.ToString(); } } }