997 lines
32 KiB
C#
997 lines
32 KiB
C#
|
|
//
|
|||
|
|
// 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;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|