Editor/HotCodeEditor/CodeView/TextLayout.cs
using System.Collections.Generic;
/// <summary>
/// Column maths for monospaced text: tab expansion and word wrapping.
/// A line is shown as one or more rows; each row starts at a character index.
/// Columns inside a row count from the start of that row.
/// </summary>
public static class TextLayout
{
/// <summary>
/// Columns per tab. Set from HotCode's settings.
/// </summary>
public static int TabSize { get; set; } = 4;
private static readonly int[] SingleRow = { 0 };
private static int Advance( char c, int col ) => c == '\t' ? (col / TabSize + 1) * TabSize : col + 1;
/// <summary>
/// Column that character <paramref name="index"/> lands on, counting from <paramref name="rowStart"/>.
/// </summary>
public static int ColumnOf( string line, int rowStart, int index )
{
int col = 0;
for ( int i = rowStart; i < index && i < line.Length; i++ )
col = Advance( line[i], col );
return col;
}
public static int Width( string line ) => ColumnOf( line, 0, line.Length );
/// <summary>
/// Character index in [rowStart, rowEnd] nearest to a (possibly fractional) column.
/// </summary>
public static int IndexAtColumn( string line, int rowStart, int rowEnd, float column )
{
int col = 0;
for ( int i = rowStart; i < rowEnd && i < line.Length; i++ )
{
var next = Advance( line[i], col );
if ( column < (col + next) / 2f ) return i;
col = next;
}
return rowEnd;
}
/// <summary>
/// Where each row of a line starts when wrapped to <paramref name="columns"/>. Breaks after
/// whitespace when it can, mid-word when a word is longer than a row. Columns <= 0 means no wrapping.
/// </summary>
public static int[] Wrap( string line, int columns )
{
if ( columns <= 0 || line.Length == 0 || Width( line ) <= columns )
return SingleRow;
var starts = new List<int> { 0 };
int rowStart = 0;
int col = 0;
int lastBreak = -1;
var rowHasText = false;
for ( int i = 0; i < line.Length; i++ )
{
var next = Advance( line[i], col );
// Loops at most twice: breaking at the last space can still leave this character
// (say, a tab) too wide, and then it moves to a row of its own start
while ( next > columns && i > rowStart )
{
// Whitespace stays at the end of the row it follows, so a row never starts with the break
var breakAt = lastBreak > rowStart && lastBreak <= i ? lastBreak : i;
starts.Add( breakAt );
rowStart = breakAt;
lastBreak = -1;
rowHasText = false;
col = ColumnOf( line, rowStart, i );
next = Advance( line[i], col );
}
col = next;
var isSpace = line[i] == ' ' || line[i] == '\t';
if ( !isSpace ) rowHasText = true;
// Leading indentation isn't a break point, or we'd get rows of nothing but indent
else if ( rowHasText ) lastBreak = i + 1;
}
return starts.ToArray();
}
}