Files
simulation_core/lib/termsharp/Terminal.cs

997 lines
32 KiB
C#
Raw Normal View History

//
// Copyright (c) Antmicro
//
// Full license details are defined in the 'LICENSE' file.
//
#define REMOVE_DUMMY_ROWS
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using TermSharp.Misc;
using TermSharp.Rows;
using Xwt;
using Xwt.Drawing;
namespace TermSharp
{
public class Terminal : HBox
{
public Terminal(Func<bool> focusProvider = null)
{
if(focusProvider == null)
{
this.focusProvider = () => canvas.HasFocus;
}
else
{
this.focusProvider = () => focusProvider() && canvas.HasFocus;
}
rows = new List<IRow>();
heightMap = new List<double>();
layoutParameters = new LayoutParameters(Font, DefaultGray, Colors.Black, Colors.LightSlateGray);
layoutParameters.Font = Font.SystemMonospaceFont;
canvas = new TerminalCanvas(this);
cursor = new Cursor(this, canvas);
PackStart(canvas, true, true);
scrollbar = new VScrollbar();
scrollbar.Sensitive = false;
PackEnd(scrollbar);
canvas.MouseScrolled += OnCanvasMouseScroll;
canvas.BoundsChanged += OnCanvasBoundsChanged;
canvas.ButtonPressed += OnCanvasButtonPressed;
canvas.ButtonReleased += OnCanvasButtonReleased;
canvas.MouseMoved += OnCanvasMouseMoved;
scrollbar.ValueChanged += OnScrollbarValueChanged;
autoscrollEnabled = new TaskCompletionSource<bool>();
HandleAutoscrollAsync();
canvas.CanGetFocus = true;
scrollbar.StepIncrement = Utilities.GetLineSizeFromLayoutParams(layoutParameters).Height;
}
public void Close()
{
scrollbar.ValueChanged -= OnScrollbarValueChanged;
canvas.MouseScrolled -= OnCanvasMouseScroll;
canvas.BoundsChanged -= OnCanvasBoundsChanged;
canvas.ButtonPressed -= OnCanvasButtonPressed;
canvas.ButtonReleased -= OnCanvasButtonReleased;
canvas.MouseMoved -= OnCanvasMouseMoved;
cursor.Dispose();
base.Dispose(disposing: true);
}
public void AppendRow(IRow row, bool treatRowAsNonDummy = false)
{
var weWereAtEnd = scrollbar.Value == GetMaximumScrollbarValue();
var heightMapNeedsRebuilding = false;
if(treatRowAsNonDummy)
{
var position = GetScreenRowIndex(Cursor.Position.Y);
var rowId = Math.Min(position, rows.Count - 1);
if(rowId < rows.Count - 1)
{
rows.RemoveRange(rowId + 1, rows.Count - rowId - 1);
heightMapNeedsRebuilding = true;
}
// this counts in the row we are about to add
lastNonDummyRow = rows.Count;
}
rows.Add(row);
row.PrepareForDrawing(layoutParameters);
if(heightMapNeedsRebuilding)
{
// this must be done after adding the row, otherwise the cursor will be wrongly placed
RebuildHeightMap(true);
}
else
{
AddToHeightMap(row.PrepareForDrawing(layoutParameters));
}
RefreshInner(weWereAtEnd);
}
public new void Clear()
{
Cursor.Position = new IntegerPosition();
rows.Clear();
RebuildHeightMap(true);
}
public void Refresh()
{
var weWereAtEnd = scrollbar.Value == GetMaximumScrollbarValue();
RebuildHeightMap(true);
RefreshInner(weWereAtEnd);
}
public void Redraw()
{
canvas.Redraw();
}
public void PageUp()
{
SetScrollbarValue(scrollbar.Value - scrollbar.PageSize);
}
public void PageDown()
{
SetScrollbarValue(scrollbar.Value + scrollbar.PageSize);
}
public void LineUp()
{
ScrollRows(-1);
}
public void LineDown()
{
ScrollRows(1);
}
public void ClearSelection()
{
canvas.SelectedArea = default(Rectangle);
currentScrollStart = null;
RefreshSelection();
}
public ClipboardData CollectClipboardData()
{
var result = new ClipboardData();
foreach(var row in rows)
{
row.FillClipboardData(result);
}
return result;
}
public IRow GetScreenRow(int screenPosition, bool treatRowAsNonDummy = false)
{
var position = GetScreenRowIndex(screenPosition);
if(treatRowAsNonDummy)
{
lastNonDummyRow = Math.Max(lastNonDummyRow, position);
}
return rows[Math.Min(position, rows.Count - 1)];
}
public IRow GetFirstScreenRow(out double hiddenHeight)
{
double rowStart;
var result = rows[FindRowIndexAtPosition(GetMaximumScrollbarValue(), out rowStart)];
hiddenHeight = GetMaximumScrollbarValue() - rowStart;
return result;
}
public void EraseScreen(IntegerPosition from, IntegerPosition to, Color? background)
{
for(var rowScreenPosition = from.Y; rowScreenPosition <= to.Y; rowScreenPosition++)
{
var row = GetScreenRow(rowScreenPosition);
row.Erase(rowScreenPosition == from.Y ? from.X : 0, rowScreenPosition == to.Y ? to.X : row.CurrentMaximalCursorPosition, background);
}
canvas.Redraw();
}
public void MoveScrollbarToBeginning()
{
SetScrollbarValue(scrollbar.LowerValue);
}
public void MoveScrollbarToEnd()
{
SetScrollbarValue(scrollbar.UpperValue - canvas.CachedBounds.Height);
}
public Font CurrentFont
{
get
{
return layoutParameters.Font;
}
set
{
bool fontSizeDecreased = layoutParameters.Font.Size > value.Size;
layoutParameters.Font = value;
Redraw();
if(rows.Count > 0)
{
if(fontSizeDecreased)
{
RebuildHeightMap();
}
OnCanvasBoundsChanged(null, null);
}
}
}
public int RowCount
{
get
{
return rows.Count;
}
}
public int ScreenRowCount
{
get
{
double unused;
var firstRowNo = FindRowIndexAtPosition(GetMaximumScrollbarValue(), out unused);
return RowCount - firstRowNo;
}
}
public double ScreenSize
{
get
{
return canvas.CachedBounds.Height;
}
}
public SelectionMode SelectionMode
{
get
{
return canvas.SelectionMode;
}
set
{
canvas.SelectionMode = value;
}
}
public new Cursor Cursor
{
get
{
return cursor;
}
}
public Color DefaultForeground
{
get
{
return layoutParameters.DefaultForeground;
}
set
{
layoutParameters.DefaultForeground = value;
}
}
public Color DefaultBackground
{
get
{
return layoutParameters.DefaultBackground;
}
set
{
layoutParameters.DefaultBackground = value;
}
}
public Color SelectionColor
{
get
{
return layoutParameters.SelectionColor;
}
set
{
layoutParameters.SelectionColor = value;
}
}
public double InnerMarginLeft
{
get
{
return canvas.MarginLeft;
}
set
{
canvas.MarginLeft = value;
}
}
public double InnerMarginRight
{
get
{
return canvas.MarginRight;
}
set
{
canvas.MarginRight = value;
}
}
public double InnerMarginBottom
{
get
{
return canvas.MarginBottom;
}
set
{
canvas.MarginBottom = value;
}
}
public double InnerMarginTop
{
get
{
return canvas.MarginTop;
}
set
{
canvas.MarginTop = value;
}
}
public WidgetSpacing InnerMargin
{
get
{
return canvas.Margin;
}
set
{
canvas.Margin = value;
}
}
public Menu ContextMenu { get; set; }
public new event EventHandler<KeyEventArgs> KeyPressed
{
add
{
canvas.KeyPressed += value;
}
remove
{
canvas.KeyPressed -= value;
}
}
public event Action Initialized;
static internal Color DefaultGray = new Color(0.75, 0.75, 0.75);
private void OnScrollbarValueChanged(object sender, EventArgs e)
{
ScrollbarValueChanged();
}
private void ScrollbarValueChanged()
{
double rowOffset;
canvas.FirstRowToDisplay = FindRowIndexAtPosition(scrollbar.Value, out rowOffset);
canvas.FirstRowHeight = rowOffset;
canvas.OffsetFromFirstRow = scrollbar.Value - rowOffset;
RefreshSelection();
}
private void OnCanvasButtonPressed(object sender, ButtonEventArgs e)
{
if(e.Button == PointerButton.Left)
{
canvas.SetFocus();
var position = e.Position;
lastMousePosition = position;
position.Y += scrollbar.Value;
currentScrollStart = position;
foreach(var row in rows)
{
row.ResetSelection();
}
HandleMultiplePresses(e);
RefreshSelection();
}
if(e.Button == PointerButton.Right)
{
var contextMenu = ContextMenu;
if(contextMenu != null)
{
contextMenu.Popup();
}
}
}
private Rectangle GetWordRect(int x, int y)
{
var row = rows[FindRowIndexAtPosition(y, out var yStart)];
var rowText = row.TextContent;
var colIdx = row.GetColumnIndex(x);
var wrappedRow = (row.LineHeight == 0.0) ? 0 : (int)((y - yStart) / row.LineHeight);
var selectedWordStart = colIdx + (row.MaximalColumn + 1) * wrappedRow;
while(selectedWordStart > 0 && selectedWordStart < rowText.Length)
{
if(char.IsWhiteSpace(rowText[selectedWordStart - 1]))
{
break;
}
selectedWordStart--;
}
var selectedWordEnd = colIdx + (row.MaximalColumn + 1) * wrappedRow;
while(selectedWordEnd > 0 && selectedWordEnd < rowText.Length - 1)
{
if(char.IsWhiteSpace(rowText[selectedWordEnd + 1]))
{
break;
}
selectedWordEnd++;
}
var startX = row.IndexToColumn(selectedWordStart % (row.MaximalColumn + 1));
var endX = row.IndexToColumn(selectedWordEnd % (row.MaximalColumn + 1));
var startWrapRow = (int)(selectedWordStart / (row.MaximalColumn + 1));
var endWrapRow = (int)(selectedWordEnd / (row.MaximalColumn + 1));
var wrapRowDiff = endWrapRow - startWrapRow;
var width = endX - startX;
// The required y range is exclusive so we need it to be bigger by an epsilon, since the +0.001
var rectY = yStart + 0.001 + (startWrapRow*row.LineHeight);
return new Rectangle(startX, rectY, width, wrapRowDiff * row.LineHeight);
}
private Rectangle GetLineRect(int x, int y)
{
var row = rows[FindRowIndexAtPosition(y, out var yStart)];
var rowText = row.TextContent;
// See comment in GetWordRect
return new Rectangle(0, yStart + 0.001, row.IndexToColumn(rowText.Length), (row.SublineCount - 1) * row.LineHeight);
}
private void HandleMultiplePresses(ButtonEventArgs e)
{
switch(e.MultiplePress)
{
case 1:
SelectionMode = SelectionMode.Normal;
break;
case 2:
SelectionMode = SelectionMode.Word;
break;
case 3:
SelectionMode = SelectionMode.Line;
break;
}
}
private void OnCanvasButtonReleased(object sender, ButtonEventArgs e)
{
if(e.Button == PointerButton.Left)
{
SetAutoscrollValue(0);
var mousePosition = e.Position;
mousePosition.Y += scrollbar.Value;
if(mousePosition == (currentScrollStart ?? default(Point)) && SelectionMode == SelectionMode.Normal)
{
canvas.SelectedArea = default(Rectangle);
}
currentScrollStart = null;
RefreshSelection();
if(canvas.SelectedArea != default(Rectangle))
{
Clipboard.SetPrimaryText(CollectClipboardData().Text);
}
}
}
private void OnCanvasMouseMoved(object sender, MouseMovedEventArgs e)
{
if(!currentScrollStart.HasValue)
{
return;
}
lastMousePosition = e.Position;
if(e.Position.Y < 0)
{
SetAutoscrollValue((int)e.Position.Y);
}
else if(e.Position.Y > canvas.CachedBounds.Height)
{
SetAutoscrollValue((int)(e.Position.Y - canvas.CachedBounds.Height));
}
else
{
SetAutoscrollValue(0);
}
RefreshSelection();
}
private void OnCanvasMouseScroll(object sender, MouseScrolledEventArgs e)
{
int modifier;
switch(e.Direction)
{
case ScrollDirection.Up:
modifier = -1;
break;
case ScrollDirection.Down:
modifier = 1;
break;
default:
modifier = 0;
break;
}
SetScrollbarValue(scrollbar.Value + scrollbar.StepIncrement * modifier);
}
#if DEBUG
private async void OnCanvasBoundsChanged(object sender, EventArgs e)
#else
private void OnCanvasBoundsChanged(object sender, EventArgs e)
#endif
{
canvas.SelectedArea = default(Rectangle);
// refresh cached bounds value
canvas.CachedBounds = canvas.Bounds;
double oldPosition;
var firstDisplayedRowIndex = FindRowIndexAtPosition(scrollbar.Value, out oldPosition);
var oldScrollbarValue = scrollbar.Value;
var weWereAtEnd = scrollbar.Value == GetMaximumScrollbarValue();
layoutParameters.Width = canvas.Size.Width;
#if REMOVE_DUMMY_ROWS
var numberOfVisibleLines = (int)Math.Floor(ScreenSize / rows[0].LineHeight);
if(heightMap.Count > lastNonDummyRow + 1 && lastNonDummyRow < numberOfVisibleLines)
{
rows.RemoveRange(lastNonDummyRow + 1, heightMap.Count - lastNonDummyRow - 2);
}
#endif
#if DEBUG
if(!RebuildHeightMap(false))
{
var boundChangedGeneration = ++canvasBoundChangedGeneration;
await Task.Delay(TimeSpan.FromMilliseconds(200));
if(boundChangedGeneration != canvasBoundChangedGeneration)
{
return;
}
RebuildHeightMap(true);
}
#else
RebuildHeightMap(true);
#endif
scrollbar.UpperValue = GetMaximumHeight();
if(!weWereAtEnd)
{
// difference between old and new position of the first displayed row:
var diff = GetPositionOfTheRow(firstDisplayedRowIndex) - oldPosition;
SetScrollbarValue(oldScrollbarValue + diff);
}
else
{
MoveScrollbarToEnd();
}
canvas.Redraw();
if(!isInitialized)
{
isInitialized = true;
CallInitializedEvent();
}
}
private void CallInitializedEvent()
{
var initialized = Initialized;
if(initialized != null)
{
initialized();
}
}
private void RefreshInner(bool weWereAtEnd)
{
canvas.Redraw();
scrollbar.Sensitive = GetMaximumHeight() > canvas.CachedBounds.Height;
scrollbar.UpperValue = GetMaximumHeight();
if(weWereAtEnd)
{
MoveScrollbarToEnd();
}
}
private void SetAutoscrollValue(int value)
{
autoscrollStep = value;
if(value != 0)
{
autoscrollEnabled.TrySetResult(true);
}
else
{
if(autoscrollEnabled.Task.IsCompleted)
{
autoscrollEnabled = new TaskCompletionSource<bool>();
}
}
}
private void SetScrollbarValue(double value)
{
var finalValue = Math.Max(0, value);
finalValue = Math.Min(finalValue, GetMaximumScrollbarValue());
scrollbar.Value = finalValue;
}
// This is not the same as `Rectangle.Union`. It's possible for the width to be
// a negative value and it's intentional because later we use it to derive the
// 'selection direction' in `MonospaceTextRow`. This is for handling single lines with
// multiple line breaks. There are two possible cases (not 4 because of the swapping):
//
// 1) First one is the 'normal one' when a.X < b.X, where we get a union
// a
// +---+-----+
// | | |
// +---+ b |
// | +---+
// | | |
// +-----+---+
//
// 2) Second is the 'negative width one' when a.X > b.X, where we go
// from a's top left to b's bottom right
// a
// +----+---+
// | | |
// b | +---+
// +---+ |
// | | |
// +---+----+
//
private Rectangle CombineTwoRects(Rectangle a, Rectangle b)
{
if(a.Y > b.Y || (a.Y == b.Y && a.X > b.X) || (a.X == b.X && a.Y == b.Y && a.Width > b.Width))
{
Utilities.Swap(ref a, ref b);
}
return new Rectangle(a.X, a.Y, b.X - a.X + b.Width, b.Y - a.Y + b.Height);
}
private bool ShouldNotHighlight(Rectangle selectedArea, SelectionMode selectionMode)
{
if(selectionMode != SelectionMode.Normal)
{
return false;
}
const int boxSize = 3;
return Math.Abs(canvas.SelectedArea.Width) < boxSize && Math.Abs(canvas.SelectedArea.Height) < boxSize;
}
private void RefreshSelection()
{
if(currentScrollStart.HasValue)
{
var scrollStart = currentScrollStart.Value;
if(SelectionMode == SelectionMode.Normal)
{
canvas.SelectedArea = new Rectangle(scrollStart.X, scrollStart.Y, lastMousePosition.X - scrollStart.X, lastMousePosition.Y + scrollbar.Value - scrollStart.Y);
}
else if(SelectionMode == SelectionMode.Word)
{
var startRect = GetWordRect((int)scrollStart.X, (int)scrollStart.Y);
var endRect = GetWordRect((int)lastMousePosition.X, (int)(lastMousePosition.Y + scrollbar.Value));
canvas.SelectedArea = CombineTwoRects(startRect, endRect);
}
else if(SelectionMode == SelectionMode.Line)
{
Rectangle startRect = GetLineRect((int)scrollStart.X, (int)scrollStart.Y);
Rectangle endRect = GetLineRect((int)lastMousePosition.X, (int)(lastMousePosition.Y + scrollbar.Value));
canvas.SelectedArea = CombineTwoRects(startRect, endRect);
}
if(ShouldNotHighlight(canvas.SelectedArea, SelectionMode))
{
canvas.SelectedArea = default(Rectangle);
}
}
canvas.Redraw();
}
private async void HandleAutoscrollAsync()
{
while(true)
{
await Task.Delay(TimeSpan.FromMilliseconds(40));
if(autoscrollStep != 0)
{
if(Math.Abs(autoscrollStep) > scrollbar.PageSize / 2)
{
autoscrollStep = (int)(Math.Sign(autoscrollStep) * scrollbar.PageSize / 2);
}
SetScrollbarValue(scrollbar.Value + autoscrollStep);
}
await autoscrollEnabled.Task;
}
}
private bool RebuildHeightMap(bool continueEvenIfLongTask = true)
{
if(rows.Count == 0)
{
heightMap = new List<double>();
return true;
}
var oldFirstScreenRow = GetScreenRowIndex(0);
var stopwatch = new Stopwatch();
stopwatch.Start();
List<double> newHeightMap;
if(unfinishedHeightMap != null && unfinishedHeightMap.Count == rows.Count)
{
newHeightMap = unfinishedHeightMap;
}
else
{
newHeightMap = new List<double>(rows.Count);
unfinishedHeightMap = newHeightMap;
}
var heightSoFar = 0.0;
for(var i = 0; i < rows.Count; i++)
{
heightSoFar += rows[i].PrepareForDrawing(layoutParameters);
newHeightMap.Add(heightSoFar);
if(!continueEvenIfLongTask && (i % HeightMapCheckTimeoutEveryNthRow) == 1 && stopwatch.Elapsed > HeightMapRebuildTimeout)
{
return false;
}
}
heightMap = newHeightMap;
unfinishedHeightMap = null;
scrollbar.PageSize = canvas.CachedBounds.Height; // we update it here to get new value on GetScreenRowId (it depends on height map and this value)
var firstScreenRow = GetScreenRowIndex(0);
var diff = firstScreenRow - oldFirstScreenRow;
cursor.Position = cursor.Position.ShiftedByY(-diff);
return true;
}
private void AddToHeightMap(double value)
{
if(heightMap.Count == 0)
{
heightMap.Add(value);
return;
}
heightMap.Add(value + heightMap[heightMap.Count - 1]);
}
private double GetPositionOfTheRow(int rowIndex)
{
return rowIndex > 0 ? heightMap[rowIndex - 1] : 0.0;
}
private double GetMaximumScrollbarValue()
{
return Math.Max(0, GetMaximumHeight() - scrollbar.PageSize);
}
private double GetMaximumHeight()
{
if(heightMap.Count == 0)
{
return 0;
}
return heightMap[heightMap.Count - 1];
}
private int FindRowIndexAtPosition(double position, out double rowStart)
{
var result = heightMap.BinarySearch(position);
if(result < 0)
{
result = ~result;
}
else
{
result++; // because heightMap[i] shows where ith row *ends* and therefore where (i+1)th starts
}
if(result == heightMap.Count)
{
result--;
}
rowStart = GetPositionOfTheRow(result);
return result;
}
private int GetScreenRowIndex(int screenPosition)
{
double unused;
return FindRowIndexAtPosition(GetMaximumScrollbarValue(), out unused) + screenPosition;
}
private void ScrollRows(int offset)
{
var firstDisplayedRowIndex = FindRowIndexAtPosition(scrollbar.Value, out _);
var newPosition = GetPositionOfTheRow(firstDisplayedRowIndex + offset);
SetScrollbarValue(newPosition);
}
private List<double> heightMap;
private List<double> unfinishedHeightMap;
#if DEBUG
private int canvasBoundChangedGeneration;
#endif
private Point? currentScrollStart;
private Point lastMousePosition;
private int autoscrollStep;
private TaskCompletionSource<bool> autoscrollEnabled;
private int lastNonDummyRow;
private bool isInitialized;
private readonly List<IRow> rows;
private readonly LayoutParameters layoutParameters;
private readonly VScrollbar scrollbar;
private readonly TerminalCanvas canvas;
private readonly Cursor cursor;
private readonly Func<bool> focusProvider;
private static readonly TimeSpan HeightMapRebuildTimeout = TimeSpan.FromMilliseconds(30);
private const int HeightMapCheckTimeoutEveryNthRow = 1000;
internal sealed class TerminalCanvas : Canvas
{
public TerminalCanvas(Terminal parent)
{
this.parent = parent;
Cursor = CursorType.IBeam;
}
public void Redraw()
{
if(drawn)
{
QueueDraw();
drawn = false;
}
}
public Rectangle CachedBounds
{
get
{
if(!cachedBounds.HasValue)
{
cachedBounds = Bounds;
}
return cachedBounds.Value;
}
set
{
cachedBounds = value;
}
}
public int FirstRowToDisplay { get; set; }
public double FirstRowHeight { get; set; }
public double OffsetFromFirstRow { get; set; }
public Rectangle SelectedArea { get; set; }
public SelectionMode SelectionMode { get; set; }
protected override void OnDraw(Context ctx, Rectangle dirtyRect)
{
var screenSelectedArea = SelectedArea;
var selectionDirection = SelectionDirection.SE;
if(screenSelectedArea.Width < 0)
{
selectionDirection = (SelectionDirection)((int)selectionDirection + 1);
screenSelectedArea.X += screenSelectedArea.Width;
screenSelectedArea.Width = -screenSelectedArea.Width;
}
if(screenSelectedArea.Height < 0)
{
selectionDirection = (SelectionDirection)((int)selectionDirection + 2);
screenSelectedArea.Y += screenSelectedArea.Height;
screenSelectedArea.Height = -screenSelectedArea.Height;
}
screenSelectedArea.Y -= FirstRowHeight;
var heightSoFar = 0.0;
ctx.Save();
ctx.SetColor(parent.DefaultBackground);
ctx.Rectangle(new Rectangle(0, 0, CachedBounds.Width, CachedBounds.Height));
ctx.Fill();
ctx.Restore();
ctx.Translate(0, -OffsetFromFirstRow);
ctx.Save();
var i = FirstRowToDisplay;
var cursorRow = parent.GetScreenRowIndex(parent.Cursor.Position.Y);
while(i < parent.rows.Count && heightSoFar - OffsetFromFirstRow < CachedBounds.Height)
{
var height = parent.rows[i].PrepareForDrawing(parent.layoutParameters);
var rowRectangle = new Rectangle(0, heightSoFar, parent.layoutParameters.Width, height);
var selectedAreaInRow = rowRectangle.Intersect(screenSelectedArea);
if(SelectionMode != SelectionMode.Block && selectedAreaInRow != default(Rectangle) &&
(screenSelectedArea.Y <= rowRectangle.Y || screenSelectedArea.Y + screenSelectedArea.Height >= rowRectangle.Y + rowRectangle.Height))
{
if(rowRectangle.Y < screenSelectedArea.Y)
{
// I'm the first row (and there is a second row)
selectedAreaInRow.X = SelectedArea.Height > 0 ? SelectedArea.X : SelectedArea.X + SelectedArea.Width;
selectedAreaInRow.Width = parent.layoutParameters.Width - selectedAreaInRow.X;
}
else if(rowRectangle.Y + rowRectangle.Height > screenSelectedArea.Y + screenSelectedArea.Height)
{
// I'm the last row (and there is some other row)
selectedAreaInRow.Width = SelectedArea.Height > 0 ? SelectedArea.X + SelectedArea.Width : SelectedArea.X;
selectedAreaInRow.X = 0;
}
else
{
// nor the first neither the last one - must be one of the middle rows
selectedAreaInRow.X = 0;
selectedAreaInRow.Width = parent.layoutParameters.Width;
}
}
if(selectedAreaInRow != default(Rectangle))
{
selectedAreaInRow.Y -= heightSoFar;
}
ctx.Save();
var hasFocus = parent.focusProvider();
parent.rows[i].Draw(ctx, selectedAreaInRow, selectionDirection, parent.SelectionMode);
if(parent.Cursor.Enabled && i == cursorRow && (parent.Cursor.BlinkState || !hasFocus))
{
parent.rows[i].DrawCursor(ctx, parent.Cursor.Position.X, hasFocus);
}
ctx.Restore();
heightSoFar += height;
ctx.Translate(0, height);
i++;
}
ctx.Restore();
drawn = true;
}
private bool drawn;
private Rectangle? cachedBounds;
private readonly Terminal parent;
}
}
}