390 lines
16 KiB
C#
390 lines
16 KiB
C#
//
|
|
// 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<int> { 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<int, Color>();
|
|
var backgroundColors = specialBackgrounds != null ? specialBackgrounds.ToDictionary(x => x.Key + x.Key / charsOnLine, x => x.Value) : new Dictionary<int, Color>();
|
|
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<int, Color> dictionary)
|
|
{
|
|
if(dictionary == null)
|
|
{
|
|
dictionary = new Dictionary<int, Color>();
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<Tuple<int, int, Color>> GetColorRanges(Dictionary<int, Color> entries)
|
|
{
|
|
var colors = entries.Values.Distinct();
|
|
foreach(var color in colors)
|
|
{
|
|
var entriesThisColor = new HashSet<int>(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<int, Color> specialForegrounds;
|
|
private Dictionary<int, Color> specialBackgrounds;
|
|
}
|
|
}
|
|
|