// // Copyright (c) Antmicro // // Full license details are defined in the 'LICENSE' file. // using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text; using TermSharp.Misc; using Xwt; using Xwt.Drawing; namespace TermSharp.Rows { public class MonospaceTextRow : IRow { public MonospaceTextRow(string content) { Debug.Assert(!content.Contains("\n")); this.content = content; lengthInTextElements = new StringInfo(content).LengthInTextElements; } public virtual double PrepareForDrawing(ILayoutParameters parameters) { cursorInRow = null; defaultForeground = parameters.DefaultForeground; defaultBackground = parameters.DefaultBackground; selectionColor = parameters.SelectionColor; textLayout = RowUtils.TextLayoutCache.GetValue(parameters); lineSize = RowUtils.LineSizeCache.GetValue(parameters); charWidth = RowUtils.CharSizeCache.GetValue(parameters).Width; if(lineSize.Width == 0) { return 0; } // Math.Max used to prevent division by zero errors on windows narrower than one character MaximalColumn = Math.Max(((int)(lineSize.Width / charWidth)) - 1, 0); var charsOnLine = MaximalColumn + 1; var lengthInTextElementsAtLeastOne = lengthInTextElements == 0 ? 1 : lengthInTextElements; // because even empty line has height of one line SublineCount = Math.Max(minimalSublineCount, DivisionWithCeiling(lengthInTextElementsAtLeastOne, charsOnLine)); return lineSize.Height * SublineCount; } public virtual void Draw(Context ctx, Rectangle selectedArea, SelectionDirection selectionDirection, SelectionMode selectionMode) { ctx.SetColor(defaultForeground); var newLinesAt = new List { 0 }; // contains indices of line wraps (i.e. \n) var charsOnLine = MaximalColumn + 1; var result = new StringBuilder(); var enumerator = StringInfo.GetTextElementEnumerator(content); var textElementsThisLine = 0; while(enumerator.MoveNext()) { textElementsThisLine++; result.Append(enumerator.GetTextElement()); if(textElementsThisLine == charsOnLine) { result.Append('\n'); newLinesAt.Add(enumerator.ElementIndex + newLinesAt.Count + 1); textElementsThisLine = 0; } } textLayout.Text = result.ToString(); var foregroundColors = specialForegrounds != null ? specialForegrounds.ToDictionary(x => x.Key + x.Key / charsOnLine, x => x.Value) : new Dictionary(); var backgroundColors = specialBackgrounds != null ? specialBackgrounds.ToDictionary(x => x.Key + x.Key / charsOnLine, x => x.Value) : new Dictionary(); if(selectedArea != default(Rectangle) && lengthInTextElements > 0) { var textWithNewLines = textLayout.Text; var firstSubrow = (int)Math.Min(newLinesAt.Count - 1, Math.Floor(selectedArea.Y / lineSize.Height)); var lastSubrow = (int)Math.Min(newLinesAt.Count - 1, Math.Floor((selectedArea.Y + selectedArea.Height) / lineSize.Height)); var firstColumn = (int)Math.Round(selectedArea.X / charWidth); var lastColumn = (int)Math.Floor((selectedArea.X + selectedArea.Width) / charWidth); if(selectionMode == SelectionMode.Block) { for(var i = firstSubrow; i <= lastSubrow; i++) { for(var j = firstColumn; j <= lastColumn; j++) { foregroundColors[(charsOnLine + 1) * i + j] = GetSelectionForegroundColor(i, charsOnLine); backgroundColors[(charsOnLine + 1) * i + j] = selectionColor; } } } else { var hasWrapping = firstSubrow != lastSubrow; var endsInThisRow = selectedArea.BottomRight.Y < LineHeight * SublineCount; var startsBeforeThisRow = selectedArea.TopLeft.X == 0 && selectedArea.TopLeft.Y == 0; if(hasWrapping && endsInThisRow && !startsBeforeThisRow) { // In those cases we want to start at the top right of the selected area rect, not the top left if(selectionDirection == SelectionDirection.SW || selectionDirection == SelectionDirection.NE) { Utilities.Swap(ref firstColumn, ref lastColumn); } } if(selectionDirection == SelectionDirection.NW) { Utilities.Swap(ref firstColumn, ref lastColumn); Utilities.Swap(ref firstSubrow, ref lastSubrow); } var firstIndex = firstColumn + newLinesAt[firstSubrow]; var lastIndex = lastColumn + newLinesAt[lastSubrow]; if(lastIndex < firstIndex) { Utilities.Swap(ref firstIndex, ref lastIndex); } var textWithNewLinesStringInfo = new StringInfo(textWithNewLines); if(firstIndex > textWithNewLinesStringInfo.LengthInTextElements - 1) { selectedContent = null; } else { firstIndex = Math.Max(0, Math.Min(textWithNewLinesStringInfo.LengthInTextElements - 1, firstIndex)); lastIndex = Math.Max(0, Math.Min(textWithNewLinesStringInfo.LengthInTextElements - 1, lastIndex)); for(var i = firstIndex; i <= lastIndex; i++) { foregroundColors[i] = GetSelectionForegroundColor(i, charsOnLine); backgroundColors[i] = selectionColor; } selectedContent = textWithNewLinesStringInfo.SubstringByTextElements(firstIndex, lastIndex - firstIndex + 1); } } } else { selectedContent = null; } textLayout.SetForeground(defaultForeground, 0, textLayout.Text.Length); foreach(var entry in GetColorRanges(foregroundColors)) { textLayout.SetForeground(entry.Item3, entry.Item1, entry.Item2); } textLayout.SetBackground(defaultBackground, 0, textLayout.Text.Length); foreach(var entry in GetColorRanges(backgroundColors)) { textLayout.SetBackground(entry.Item3, entry.Item1, entry.Item2); } if(cursorInRow.HasValue) { // we draw a rectangle AND set background so that one can see cursor in a row without character textLayout.SetForeground(defaultBackground, cursorInRow.Value, 1); } ctx.DrawTextLayout(textLayout, 0, 0); textLayout.ClearAttributes(); } public void ResetSelection() { selectedContent = null; } public virtual void DrawCursor(Context ctx, int offset, bool focused) { var maxColumn = MaximalColumn + 1; var column = offset % maxColumn; var row = offset / maxColumn; var transparentForeground = defaultForeground; // Color is a struct transparentForeground.Alpha = cursorTransparency; ctx.SetColor(transparentForeground); ctx.Rectangle(new Rectangle(column * charWidth, row * lineSize.Height, charWidth, lineSize.Height)); if(focused) { ctx.Fill(); //We add the row number (0-based) to account for \n characters added in subrows. cursorInRow = offset + row; } else { ctx.Stroke(); } } public void FillClipboardData(ClipboardData data) { if(selectedContent != null) { data.AppendText(selectedContent); } } public void Erase(int from, int to, Color? background = null) { // due to TrimEnd() (near the end of this method) the number of sublines can go down - but this in fact // cannot happen during erase operation, so we make sure that the minimal subline count is the current count minimalSublineCount = SublineCount; var builder = new StringBuilder(); var stringInfo = new StringInfo(content); from = Math.Max(0, Math.Min(from, stringInfo.LengthInTextElements)); if(from > 0) { builder.Append(stringInfo.SubstringByTextElements(0, from)); } if(to > from) { builder.Append(' ', to - from); } if(to < stringInfo.LengthInTextElements) { builder.Append(stringInfo.SubstringByTextElements(to)); } for(var i = from; i <= to; i++) { if(background.HasValue) { CheckDictionary(ref specialBackgrounds); specialBackgrounds[i] = background.Value; } else { if(specialBackgrounds != null) { specialBackgrounds.Remove(i); } } if(specialForegrounds != null) { specialForegrounds.Remove(i); } } content = builder.ToString().TrimEnd().PadRight(from, ' '); lengthInTextElements = new StringInfo(content).LengthInTextElements; } public bool PutCharacterAt(int position, string what, Color? foreground = null, Color? background = null) { Debug.Assert(new StringInfo(what).LengthInTextElements == 1); if(foreground.HasValue) { CheckDictionary(ref specialForegrounds); specialForegrounds[position] = foreground.Value; } else { if(specialForegrounds != null) { specialForegrounds.Remove(position); } } if(background.HasValue) { CheckDictionary(ref specialBackgrounds); specialBackgrounds[position] = background.Value; } else { if(specialBackgrounds != null) { specialBackgrounds.Remove(position); } } var stringInfo = new StringInfo(content); StringBuilder builder; if(position > 0) { if(lengthInTextElements <= position) // append at the end of the current string, possibly enlarging it { builder = new StringBuilder(content); builder.Append(' ', position - lengthInTextElements).Append(what); } else // insert in the middle of the current string { builder = new StringBuilder(stringInfo.SubstringByTextElements(0, position)).Append(what); } } else // insert at the beginning of the current string { builder = new StringBuilder(what); } // append the rest of the current string if(lengthInTextElements > position + 1) { builder.Append(stringInfo.SubstringByTextElements(position + 1)); } content = builder.ToString(); var oldLengthInTextElements = lengthInTextElements; lengthInTextElements = new StringInfo(content).LengthInTextElements; // Math.Max used to prevent division by zero errors on windows narrower than one character var charsOnLine = Math.Max((int)Math.Floor(lineSize.Width / charWidth), 1); var result = DivisionWithCeiling(oldLengthInTextElements == 0 ? 1 : oldLengthInTextElements, charsOnLine) != DivisionWithCeiling(lengthInTextElements == 0 ? 1 : lengthInTextElements, charsOnLine); return result; } public int GetColumnIndex(double x) => (int)(x / charWidth); public double IndexToColumn(int idx) => idx * charWidth; public int SublineCount { get; private set; } public string TextContent => content; public double LineHeight { get { return lineSize.Height; } } public int CurrentMaximalCursorPosition { get { return MaximalColumn * SublineCount; } } public int MaximalColumn { get; private set; } private void CheckDictionary(ref Dictionary dictionary) { if(dictionary == null) { dictionary = new Dictionary(); } } private static IEnumerable> GetColorRanges(Dictionary entries) { var colors = entries.Values.Distinct(); foreach(var color in colors) { var entriesThisColor = new HashSet(entries.Where(x => x.Value == color).Select(x => x.Key)); var begins = entriesThisColor.Where(x => !entriesThisColor.Contains(x - 1)).OrderBy(x => x).ToArray(); var ends = entriesThisColor.Where(x => !entriesThisColor.Contains(x + 1)).OrderBy(x => x).ToArray(); for(var i = 0; i < begins.Length; i++) { yield return Tuple.Create(begins[i], ends[i] - begins[i] + 1, color); } } } private static int DivisionWithCeiling(int dividend, int divisor) { Debug.Assert(divisor > 0 && dividend > 0); return (dividend + divisor - 1) / divisor; } private Color GetSelectionForegroundColor(int index, int charsOnLine) { // the index may be shifted by new line characters in wrapped lines, so we subtract the amount of subrows. index -= index / charsOnLine; return (specialForegrounds != null && specialForegrounds.ContainsKey(index)) ? specialForegrounds[index].WithIncreasedLight(0.2) : defaultBackground; } public override string ToString() { return $"[MonospaceTextRow: content={content}]"; } private double charWidth; private Size lineSize; private TextLayout textLayout; private string selectedContent; private string content; private const double cursorTransparency = 0.6; private Color defaultForeground; private Color defaultBackground; private Color selectionColor; private int lengthInTextElements; private int? cursorInRow; private int minimalSublineCount; private Dictionary specialForegrounds; private Dictionary specialBackgrounds; } }