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 { struct LineInfo { public int lineIndex; public int lineOffset; public int lineLength; } [HideInEditor] public ConsoleInputTextBox inputBox; protected TextLayoutOptions _layout; private FontReference Font; public int FontHeight { get { return Font.GetFont().Height; } } 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; public int ScrollOffset = 0; public int ScrollMouseLines = 3; private float heightMultiplier = 1.0f; public float HeightMultiplier { get => heightMultiplier; set { heightMultiplier = value; UpdateHeight(); } } 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(FontReference font, ConsoleInputTextBox inputBox, float x, float y, float width, float height) : base( x, y, width, height) { this.inputBox = inputBox; Height = height; Font = font; _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; } private int GetFontCharacterWidth() { var font = Font.GetFont(); if (!font) return 0; return (int) font.MeasureText("a").X; // hacky, but works for fixed-size fonts... } public int GetFontHeight() { var font = Font.GetFont(); if (font == null) return (int) Height; return (int) Mathf.Round(LineSpacing * (font.Height / Platform.DpiScale) * Scale.Y); } private int GetHeightInLines() { var font = Font.GetFont(); if (!font) return 0; return (int) (Height / (font.Height / Platform.DpiScale)); // number of fully visible lines } protected override void OnParentChangedInternal() { base.OnParentChangedInternal(); if (Parent != null) OnParentResized(); } public override void OnParentResized() { UpdateHeight(); base.OnParentResized(); } private void UpdateHeight() { if (Parent != null && Parent.Parent != null) Height = (Parent.Parent.Size.Y * HeightMultiplier) - GetFontHeight(); } 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 = GetFontCharacterWidth(); int lineMaxChars = (int) (Width / fontWidth); int lineMaxLines = GetHeightInLines(); int numLines = 0; int lineIndex = lines.Count; List lineInfos = new List(lineMaxLines + 1); int linesSkipped = 0; foreach (string line in lines.Reverse()) { lineIndex--; if (linesSkipped < ScrollOffset) { linesSkipped++; continue; } 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++; if (lineInfos.Count > lineMaxLines) break; } lineInfos.Reverse(); wrappedLines = lineInfos.ToArray(); //return lines[lines.Count - numLines .. lines.Count]; // C# 8.0... lastVisibleLine = lineIndex; firstVisibleLine = lastVisibleLine - numLines; } 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.Lines; if (lines.Count > 0) { // Apply view offset and clip mask var textClipRectangle = new Rectangle(1, 1, Width - 2, Height - 2); Render2D.PushClip(textClipRectangle); // 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 = rect; 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 = rect; layout.Bounds.Y -= accumHeight - Height; foreach (LineInfo li in wrappedLines) { var lineIndex = li.lineIndex; string line = lines.ElementAt(lineIndex).content.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); }*/ 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.Lines; CalculateVisibleLines(lines, out int startLine, out int lastLine, out LineInfo[] wrappedLines); TextLayoutOptions layout = _layout; layout.Bounds = new Rectangle(0, 0, Size); 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).content.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 (key == KeyboardKeys.PageUp) { ScrollOffset += GetHeightInLines() / 2; var maxOffset = Console.Lines.Count - GetHeightInLines(); if (ScrollOffset > maxOffset) ScrollOffset = maxOffset; } else if (key == KeyboardKeys.PageDown) { ScrollOffset -= GetHeightInLines() / 2; if (ScrollOffset < 0) ScrollOffset = 0; } //else if (ctrlDown && key == KeyboardKeys.A) // SelectAll(); return base.OnKeyDown(key); } public override bool OnMouseWheel(Vector2 location, float delta) { if (delta < 0) { ScrollOffset -= ScrollMouseLines; if (ScrollOffset < 0) ScrollOffset = 0; } else if (delta > 0) { ScrollOffset += ScrollMouseLines; var maxOffset = Console.Lines.Count - GetHeightInLines(); if (ScrollOffset > maxOffset) ScrollOffset = maxOffset; } return false; } public override bool OnMouseDown(Vector2 location, MouseButton button) { bool ret = false; if (button == MouseButton.Left && !IsFocused) { Focus(); ret = true; } if (button == MouseButton.Left && Console.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.Lines; 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(); } } }