#nullable enable
using System.Diagnostics;
namespace Terminal.Gui;
///
/// Provides text formatting. Supports s, horizontal and vertical alignment, text direction,
/// multiple lines, and word-based line wrap.
///
public class TextFormatter
{
private Key _hotKey = new ();
private int _hotKeyPos = -1;
private List _lines = new ();
private bool _multiLine;
private bool _preserveTrailingSpaces;
private int _tabWidth = 4;
private string? _text;
private Alignment _textAlignment = Alignment.Start;
private TextDirection _textDirection;
private Alignment _textVerticalAlignment = Alignment.Start;
private bool _wordWrap = true;
/// Get or sets the horizontal text alignment.
/// The text alignment.
public Alignment Alignment
{
get => _textAlignment;
set => _textAlignment = EnableNeedsFormat (value);
}
///
/// Gets the cursor position of the . If the is defined, the cursor will
/// be positioned over it.
///
public int CursorPosition { get; internal set; }
/// Gets or sets the text-direction.
/// The text direction.
public TextDirection Direction
{
get => _textDirection;
set => _textDirection = EnableNeedsFormat (value);
}
/// Draws the text held by to using the colors specified.
///
/// Causes the text to be formatted (references ). Sets to
/// false.
///
/// Specifies the screen-relative location and maximum size for drawing the text.
/// The color to use for all text except the hotkey
/// The color to use to draw the hotkey
/// Specifies the screen-relative location and maximum container size.
/// The console driver currently used by the application.
///
public void Draw (
Rectangle screen,
Attribute normalColor,
Attribute hotColor,
Rectangle maximum = default,
IConsoleDriver? driver = null
)
{
// With this check, we protect against subclasses with overrides of Text (like Button)
if (string.IsNullOrEmpty (Text))
{
return;
}
if (driver is null)
{
driver = Application.Driver;
}
driver?.SetAttribute (normalColor);
List linesFormatted = GetLines ();
bool isVertical = IsVerticalDirection (Direction);
Rectangle maxScreen = screen;
if (driver is { })
{
// INTENT: What, exactly, is the intent of this?
maxScreen = maximum == default (Rectangle)
? screen
: new (
Math.Max (maximum.X, screen.X),
Math.Max (maximum.Y, screen.Y),
Math.Max (
Math.Min (maximum.Width, maximum.Right - screen.Left),
0
),
Math.Max (
Math.Min (
maximum.Height,
maximum.Bottom - screen.Top
),
0
)
);
}
if (maxScreen.Width == 0 || maxScreen.Height == 0)
{
return;
}
int lineOffset = !isVertical && screen.Y < 0 ? Math.Abs (screen.Y) : 0;
for (int line = lineOffset; line < linesFormatted.Count; line++)
{
if ((isVertical && line > screen.Width) || (!isVertical && line > screen.Height))
{
continue;
}
if ((isVertical && line >= maxScreen.Left + maxScreen.Width)
|| (!isVertical && line >= maxScreen.Top + maxScreen.Height + lineOffset))
{
break;
}
Rune [] runes = linesFormatted [line].ToRunes ();
// When text is justified, we lost left or right, so we use the direction to align.
int x = 0, y = 0;
// Horizontal Alignment
if (Alignment is Alignment.End)
{
if (isVertical)
{
int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, line, linesFormatted.Count - line, TabWidth);
x = screen.Right - runesWidth;
CursorPosition = screen.Width - runesWidth + (_hotKeyPos > -1 ? _hotKeyPos : 0);
}
else
{
int runesWidth = StringExtensions.ToString (runes).GetColumns ();
x = screen.Right - runesWidth;
CursorPosition = screen.Width - runesWidth + (_hotKeyPos > -1 ? _hotKeyPos : 0);
}
}
else if (Alignment is Alignment.Start)
{
if (isVertical)
{
int runesWidth = line > 0
? GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth)
: 0;
x = screen.Left + runesWidth;
}
else
{
x = screen.Left;
}
CursorPosition = _hotKeyPos > -1 ? _hotKeyPos : 0;
}
else if (Alignment is Alignment.Fill)
{
if (isVertical)
{
int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth);
int prevLineWidth = line > 0 ? GetColumnsRequiredForVerticalText (linesFormatted, line - 1, 1, TabWidth) : 0;
int firstLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, 1, TabWidth);
int lastLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, linesFormatted.Count - 1, 1, TabWidth);
var interval = (int)Math.Round ((double)(screen.Width + firstLineWidth + lastLineWidth) / linesFormatted.Count);
x = line == 0
? screen.Left
: line < linesFormatted.Count - 1
? screen.Width - runesWidth <= lastLineWidth ? screen.Left + prevLineWidth : screen.Left + line * interval
: screen.Right - lastLineWidth;
}
else
{
x = screen.Left;
}
CursorPosition = _hotKeyPos > -1 ? _hotKeyPos : 0;
}
else if (Alignment is Alignment.Center)
{
if (isVertical)
{
int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth);
int linesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth);
x = screen.Left + linesWidth + (screen.Width - runesWidth) / 2;
CursorPosition = (screen.Width - runesWidth) / 2 + (_hotKeyPos > -1 ? _hotKeyPos : 0);
}
else
{
int runesWidth = StringExtensions.ToString (runes).GetColumns ();
x = screen.Left + (screen.Width - runesWidth) / 2;
CursorPosition = (screen.Width - runesWidth) / 2 + (_hotKeyPos > -1 ? _hotKeyPos : 0);
}
}
else
{
Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}");
return;
}
// Vertical Alignment
if (VerticalAlignment is Alignment.End)
{
if (isVertical)
{
y = screen.Bottom - runes.Length;
}
else
{
y = screen.Bottom - linesFormatted.Count + line;
}
}
else if (VerticalAlignment is Alignment.Start)
{
if (isVertical)
{
y = screen.Top;
}
else
{
y = screen.Top + line;
}
}
else if (VerticalAlignment is Alignment.Fill)
{
if (isVertical)
{
y = screen.Top;
}
else
{
var interval = (int)Math.Round ((double)(screen.Height + 2) / linesFormatted.Count);
y = line == 0 ? screen.Top :
line < linesFormatted.Count - 1 ? screen.Height - interval <= 1 ? screen.Top + 1 : screen.Top + line * interval : screen.Bottom - 1;
}
}
else if (VerticalAlignment is Alignment.Center)
{
if (isVertical)
{
int s = (screen.Height - runes.Length) / 2;
y = screen.Top + s;
}
else
{
int s = (screen.Height - linesFormatted.Count) / 2;
y = screen.Top + line + s;
}
}
else
{
Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}");
return;
}
int colOffset = screen.X < 0 ? Math.Abs (screen.X) : 0;
int start = isVertical ? screen.Top : screen.Left;
int size = isVertical ? screen.Height : screen.Width;
int current = start + colOffset;
List lastZeroWidthPos = null!;
Rune rune = default;
int zeroLengthCount = isVertical ? runes.Sum (r => r.GetColumns () == 0 ? 1 : 0) : 0;
for (int idx = (isVertical ? start - y : start - x) + colOffset;
current < start + size + zeroLengthCount;
idx++)
{
Rune lastRuneUsed = rune;
if (lastZeroWidthPos is null)
{
if (idx < 0
|| (isVertical
? VerticalAlignment != Alignment.End && current < 0
: Alignment != Alignment.End && x + current + colOffset < 0))
{
current++;
continue;
}
if (!FillRemaining && idx > runes.Length - 1)
{
break;
}
if ((!isVertical
&& (current - start > maxScreen.Left + maxScreen.Width - screen.X + colOffset
|| (idx < runes.Length && runes [idx].GetColumns () > screen.Width)))
|| (isVertical
&& ((current > start + size + zeroLengthCount && idx > maxScreen.Top + maxScreen.Height - screen.Y)
|| (idx < runes.Length && runes [idx].GetColumns () > screen.Width))))
{
break;
}
}
//if ((!isVertical && idx > maxBounds.Left + maxBounds.Width - viewport.X + colOffset)
// || (isVertical && idx > maxBounds.Top + maxBounds.Height - viewport.Y))
// break;
rune = (Rune)' ';
if (isVertical)
{
if (idx >= 0 && idx < runes.Length)
{
rune = runes [idx];
}
if (lastZeroWidthPos is null)
{
driver?.Move (x, current);
}
else
{
int foundIdx = lastZeroWidthPos.IndexOf (
p =>
p is { } && p.Value.Y == current
);
if (foundIdx > -1)
{
if (rune.IsCombiningMark ())
{
lastZeroWidthPos [foundIdx] =
new Point (
lastZeroWidthPos [foundIdx]!.Value.X + 1,
current
);
driver?.Move (
lastZeroWidthPos [foundIdx]!.Value.X,
current
);
}
else if (!rune.IsCombiningMark () && lastRuneUsed.IsCombiningMark ())
{
current++;
driver?.Move (x, current);
}
else
{
driver?.Move (x, current);
}
}
else
{
driver?.Move (x, current);
}
}
}
else
{
driver?.Move (current, y);
if (idx >= 0 && idx < runes.Length)
{
rune = runes [idx];
}
}
int runeWidth = GetRuneWidth (rune, TabWidth);
if (HotKeyPos > -1 && idx == HotKeyPos)
{
if ((isVertical && VerticalAlignment == Alignment.Fill) || (!isVertical && Alignment == Alignment.Fill))
{
CursorPosition = idx - start;
}
driver?.SetAttribute (hotColor);
driver?.AddRune (rune);
driver?.SetAttribute (normalColor);
}
else
{
if (isVertical)
{
if (runeWidth == 0)
{
if (lastZeroWidthPos is null)
{
lastZeroWidthPos = new ();
}
int foundIdx = lastZeroWidthPos.IndexOf (
p =>
p is { } && p.Value.Y == current
);
if (foundIdx == -1)
{
current--;
lastZeroWidthPos.Add (new Point (x + 1, current));
}
driver?.Move (x + 1, current);
}
}
driver?.AddRune (rune);
}
if (isVertical)
{
if (runeWidth > 0)
{
current++;
}
}
else
{
current += runeWidth;
}
int nextRuneWidth = idx + 1 > -1 && idx + 1 < runes.Length
? runes [idx + 1].GetColumns ()
: 0;
if (!isVertical && idx + 1 < runes.Length && current + nextRuneWidth > start + size)
{
break;
}
}
}
}
///
/// Determines if the viewport width will be used or only the text width will be used,
/// If all the viewport area will be filled with whitespaces and the same background color
/// showing a perfect rectangle.
///
public bool FillRemaining { get; set; }
/// Returns the formatted text, constrained to .
///
/// If is , causes a format, resetting
/// to .
///
/// The formatted text.
public string Format ()
{
var sb = new StringBuilder ();
// Lines_get causes a Format
foreach (string line in GetLines ())
{
sb.AppendLine (line);
}
return sb.ToString ().TrimEnd (Environment.NewLine.ToCharArray ());
}
/// Gets the size required to hold the formatted text, given the constraints placed by .
/// Causes a format, resetting to .
///
/// If provided, will cause the text to be constrained to the provided size instead of and
/// .
///
/// The size required to hold the formatted text.
public Size FormatAndGetSize (Size? constrainSize = null)
{
if (string.IsNullOrEmpty (Text))
{
return System.Drawing.Size.Empty;
}
int? prevWidth = _constrainToWidth;
int? prevHeight = _constrainToHeight;
if (constrainSize is { })
{
_constrainToWidth = constrainSize?.Width;
_constrainToHeight = constrainSize?.Height;
}
// HACK: Fill normally will fill the entire constraint size, but we need to know the actual size of the text.
Alignment prevAlignment = Alignment;
if (Alignment == Alignment.Fill)
{
Alignment = Alignment.Start;
}
Alignment prevVerticalAlignment = VerticalAlignment;
if (VerticalAlignment == Alignment.Fill)
{
VerticalAlignment = Alignment.Start;
}
// This calls Format
List lines = GetLines ();
// Undo hacks
Alignment = prevAlignment;
VerticalAlignment = prevVerticalAlignment;
if (constrainSize is { })
{
_constrainToWidth = prevWidth ?? null;
_constrainToHeight = prevHeight ?? null;
}
if (lines.Count == 0)
{
return System.Drawing.Size.Empty;
}
int width;
int height;
if (IsVerticalDirection (Direction))
{
width = GetColumnsRequiredForVerticalText (lines, 0, lines.Count, TabWidth);
height = lines.Max (static line => line.Length);
}
else
{
width = lines.Max (static line => line.GetColumns ());
height = lines.Count;
}
return new (width, height);
}
///
/// Gets the width or height of the characters
/// in the property.
///
///
/// Only the first HotKey specifier found in is supported.
///
///
/// If (the default) the width required for the HotKey specifier is returned. Otherwise, the
/// height is returned.
///
///
/// The number of characters required for the . If the text
/// direction specified
/// by does not match the parameter, 0 is returned.
///
public int GetHotKeySpecifierLength (bool isWidth = true)
{
if (isWidth)
{
return IsHorizontalDirection (Direction) && Text?.Contains ((char)HotKeySpecifier.Value) == true
? Math.Max (HotKeySpecifier.GetColumns (), 0)
: 0;
}
return IsVerticalDirection (Direction) && Text?.Contains ((char)HotKeySpecifier.Value) == true
? Math.Max (HotKeySpecifier.GetColumns (), 0)
: 0;
}
/// Gets a list of formatted lines, constrained to .
///
///
/// If the text needs to be formatted (if is )
/// will be called and upon return
/// will be .
///
///
/// If either of the dimensions of are zero, the text will not be formatted and no lines will
/// be returned.
///
///
public List GetLines ()
{
string text = _text!.ReplaceLineEndings ();
// With this check, we protect against subclasses with overrides of Text
if (string.IsNullOrEmpty (Text) || ConstrainToWidth is 0 || ConstrainToHeight is 0)
{
_lines = [string.Empty];
NeedsFormat = false;
return _lines;
}
if (!NeedsFormat)
{
return _lines;
}
int width = ConstrainToWidth ?? int.MaxValue;
int height = ConstrainToHeight ?? int.MaxValue;
if (FindHotKey (_text!, HotKeySpecifier, out _hotKeyPos, out Key newHotKey))
{
HotKey = newHotKey;
text = RemoveHotKeySpecifier (Text, _hotKeyPos, HotKeySpecifier);
text = ReplaceHotKeyWithTag (text, _hotKeyPos);
}
if (IsVerticalDirection (Direction))
{
int colsWidth = GetSumMaxCharWidth (text, 0, 1, TabWidth);
_lines = Format (
text,
height,
VerticalAlignment == Alignment.Fill,
width > colsWidth && WordWrap,
PreserveTrailingSpaces,
TabWidth,
Direction,
MultiLine,
this
);
colsWidth = GetMaxColsForWidth (_lines, width, TabWidth);
if (_lines.Count > colsWidth)
{
_lines.RemoveRange (colsWidth, _lines.Count - colsWidth);
}
}
else
{
_lines = Format (
text,
width,
Alignment == Alignment.Fill,
height > 1 && WordWrap,
PreserveTrailingSpaces,
TabWidth,
Direction,
MultiLine,
this
);
if (_lines.Count > height)
{
_lines.RemoveRange (height, _lines.Count - height);
}
}
NeedsFormat = false;
return _lines;
}
private int? _constrainToWidth;
/// Gets or sets the width will be constrained to when formatted.
///
///
/// Does not return the width of the formatted text but the width that will be used to constrain the text when
/// formatted.
///
///
/// If the height will be unconstrained. if both and are the text will be formatted to the size of the text.
///
///
/// Use to get the size of the formatted text.
///
/// When set, is set to .
///
public int? ConstrainToWidth
{
get => _constrainToWidth;
set
{
if (_constrainToWidth == value)
{
return;
}
ArgumentOutOfRangeException.ThrowIfNegative (value.GetValueOrDefault (), nameof (ConstrainToWidth));
_constrainToWidth = EnableNeedsFormat (value);
}
}
private int? _constrainToHeight;
/// Gets or sets the height will be constrained to when formatted.
///
///
/// Does not return the height of the formatted text but the height that will be used to constrain the text when
/// formatted.
///
///
/// If the height will be unconstrained. if both and are the text will be formatted to the size of the text.
///
///
/// Use to get the size of the formatted text.
///
/// When set, is set to .
///
public int? ConstrainToHeight
{
get => _constrainToHeight;
set
{
if (_constrainToHeight == value)
{
return;
}
ArgumentOutOfRangeException.ThrowIfNegative (value.GetValueOrDefault (), nameof (ConstrainToHeight));
_constrainToHeight = EnableNeedsFormat (value);
}
}
/// Gets or sets the width and height will be constrained to when formatted.
///
///
/// Does not return the size of the formatted text but the size that will be used to constrain the text when
/// formatted.
///
///
/// If both the width and height will be unconstrained and text will be formatted to the size of the text.
///
///
/// Setting this property is the same as setting and separately.
///
///
/// Use to get the size of the formatted text.
///
/// When set, is set to .
///
public Size? ConstrainToSize
{
get
{
if (_constrainToWidth is null || _constrainToHeight is null)
{
return null;
}
return new Size (_constrainToWidth.Value, _constrainToHeight.Value);
}
set
{
if (value is null)
{
_constrainToWidth = null;
_constrainToHeight = null;
EnableNeedsFormat (true);
}
else
{
_constrainToWidth = EnableNeedsFormat (value.Value.Width);
_constrainToHeight = EnableNeedsFormat (value.Value.Height);
}
}
}
/// Gets or sets the hot key. Fires the event.
public Key HotKey
{
get => _hotKey;
internal set
{
if (_hotKey != value)
{
Key oldKey = _hotKey;
_hotKey = value;
HotKeyChanged?.Invoke (this, new (oldKey, value));
}
}
}
/// Event invoked when the is changed.
public event EventHandler? HotKeyChanged;
/// The position in the text of the hot key. The hot key will be rendered using the hot color.
public int HotKeyPos
{
get => _hotKeyPos;
internal set => _hotKeyPos = value;
}
///
/// The specifier character for the hot key (e.g. '_'). Set to '\xffff' to disable hot key support for this View
/// instance. The default is '\xffff'.
///
public Rune HotKeySpecifier { get; set; } = (Rune)0xFFFF;
/// Gets or sets a value indicating whether multi line is allowed.
/// Multi line is ignored if is .
public bool MultiLine
{
get => _multiLine;
set => _multiLine = EnableNeedsFormat (value);
}
/// Gets or sets whether the needs to format the text.
///
/// If when Draw is called, the Draw call will be faster.
/// Used by
/// Set to when any of the properties of are set.
/// Set to when the text is formatted (if is accessed).
///
public bool NeedsFormat { get; set; }
///
/// Gets or sets whether trailing spaces at the end of word-wrapped lines are preserved or not when
/// is enabled. If trailing spaces at the end of wrapped
/// lines will be removed when is formatted for display. The default is .
///
public bool PreserveTrailingSpaces
{
get => _preserveTrailingSpaces;
set => _preserveTrailingSpaces = EnableNeedsFormat (value);
}
/// Gets or sets the number of columns used for a tab.
public int TabWidth
{
get => _tabWidth;
set => _tabWidth = EnableNeedsFormat (value);
}
/// The text to be formatted. This string is never modified.
public string Text
{
get => _text!;
set => _text = EnableNeedsFormat (value);
}
/// Gets or sets the vertical text-alignment.
/// The text vertical alignment.
public Alignment VerticalAlignment
{
get => _textVerticalAlignment;
set => _textVerticalAlignment = EnableNeedsFormat (value);
}
/// Gets or sets whether word wrap will be used to fit to .
public bool WordWrap
{
get => _wordWrap;
set => _wordWrap = EnableNeedsFormat (value);
}
/// Sets to and returns the value.
///
///
///
private T EnableNeedsFormat (T value)
{
NeedsFormat = true;
return value;
}
///
/// Calculates and returns a describing the areas where text would be output, based on the
/// formatting rules of .
///
///
/// Uses the same formatting logic as , including alignment, direction, word wrap, and constraints,
/// but does not perform actual drawing to .
///
/// Specifies the screen-relative location and maximum size for drawing the text.
/// Specifies the screen-relative location and maximum container size.
/// A representing the areas where text would be drawn.
public Region GetDrawRegion (Rectangle screen, Rectangle maximum = default)
{
Region drawnRegion = new Region ();
// With this check, we protect against subclasses with overrides of Text (like Button)
if (string.IsNullOrEmpty (Text))
{
return drawnRegion;
}
List linesFormatted = GetLines ();
bool isVertical = IsVerticalDirection (Direction);
Rectangle maxScreen = screen;
// INTENT: What, exactly, is the intent of this?
maxScreen = maximum == default (Rectangle)
? screen
: new (
Math.Max (maximum.X, screen.X),
Math.Max (maximum.Y, screen.Y),
Math.Max (
Math.Min (maximum.Width, maximum.Right - screen.Left),
0
),
Math.Max (
Math.Min (
maximum.Height,
maximum.Bottom - screen.Top
),
0
)
);
if (maxScreen.Width == 0 || maxScreen.Height == 0)
{
return drawnRegion;
}
int lineOffset = !isVertical && screen.Y < 0 ? Math.Abs (screen.Y) : 0;
for (int line = lineOffset; line < linesFormatted.Count; line++)
{
if ((isVertical && line > screen.Width) || (!isVertical && line > screen.Height))
{
continue;
}
if ((isVertical && line >= maxScreen.Left + maxScreen.Width)
|| (!isVertical && line >= maxScreen.Top + maxScreen.Height + lineOffset))
{
break;
}
Rune [] runes = linesFormatted [line].ToRunes ();
// When text is justified, we lost left or right, so we use the direction to align.
int x = 0, y = 0;
switch (Alignment)
{
// Horizontal Alignment
case Alignment.End when isVertical:
{
int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, line, linesFormatted.Count - line, TabWidth);
x = screen.Right - runesWidth;
break;
}
case Alignment.End:
{
int runesWidth = StringExtensions.ToString (runes).GetColumns ();
x = screen.Right - runesWidth;
break;
}
case Alignment.Start when isVertical:
{
int runesWidth = line > 0
? GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth)
: 0;
x = screen.Left + runesWidth;
break;
}
case Alignment.Start:
x = screen.Left;
break;
case Alignment.Fill when isVertical:
{
int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth);
int prevLineWidth = line > 0 ? GetColumnsRequiredForVerticalText (linesFormatted, line - 1, 1, TabWidth) : 0;
int firstLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, 1, TabWidth);
int lastLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, linesFormatted.Count - 1, 1, TabWidth);
var interval = (int)Math.Round ((double)(screen.Width + firstLineWidth + lastLineWidth) / linesFormatted.Count);
x = line == 0
? screen.Left
: line < linesFormatted.Count - 1
? screen.Width - runesWidth <= lastLineWidth ? screen.Left + prevLineWidth : screen.Left + line * interval
: screen.Right - lastLineWidth;
break;
}
case Alignment.Fill:
x = screen.Left;
break;
case Alignment.Center when isVertical:
{
int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth);
int linesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth);
x = screen.Left + linesWidth + (screen.Width - runesWidth) / 2;
break;
}
case Alignment.Center:
{
int runesWidth = StringExtensions.ToString (runes).GetColumns ();
x = screen.Left + (screen.Width - runesWidth) / 2;
break;
}
default:
Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}");
return drawnRegion;
}
switch (VerticalAlignment)
{
// Vertical Alignment
case Alignment.End when isVertical:
y = screen.Bottom - runes.Length;
break;
case Alignment.End:
y = screen.Bottom - linesFormatted.Count + line;
break;
case Alignment.Start when isVertical:
y = screen.Top;
break;
case Alignment.Start:
y = screen.Top + line;
break;
case Alignment.Fill when isVertical:
y = screen.Top;
break;
case Alignment.Fill:
{
var interval = (int)Math.Round ((double)(screen.Height + 2) / linesFormatted.Count);
y = line == 0 ? screen.Top :
line < linesFormatted.Count - 1 ? screen.Height - interval <= 1 ? screen.Top + 1 : screen.Top + line * interval : screen.Bottom - 1;
break;
}
case Alignment.Center when isVertical:
{
int s = (screen.Height - runes.Length) / 2;
y = screen.Top + s;
break;
}
case Alignment.Center:
{
int s = (screen.Height - linesFormatted.Count) / 2;
y = screen.Top + line + s;
break;
}
default:
Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}");
return drawnRegion;
}
int colOffset = screen.X < 0 ? Math.Abs (screen.X) : 0;
int start = isVertical ? screen.Top : screen.Left;
int size = isVertical ? screen.Height : screen.Width;
int current = start + colOffset;
int zeroLengthCount = isVertical ? runes.Sum (r => r.GetColumns () == 0 ? 1 : 0) : 0;
int lineX = x, lineY = y, lineWidth = 0, lineHeight = 1;
for (int idx = (isVertical ? start - y : start - x) + colOffset;
current < start + size + zeroLengthCount;
idx++)
{
if (idx < 0
|| (isVertical
? VerticalAlignment != Alignment.End && current < 0
: Alignment != Alignment.End && x + current + colOffset < 0))
{
current++;
continue;
}
if (!FillRemaining && idx > runes.Length - 1)
{
break;
}
if ((!isVertical
&& (current - start > maxScreen.Left + maxScreen.Width - screen.X + colOffset
|| (idx < runes.Length && runes [idx].GetColumns () > screen.Width)))
|| (isVertical
&& ((current > start + size + zeroLengthCount && idx > maxScreen.Top + maxScreen.Height - screen.Y)
|| (idx < runes.Length && runes [idx].GetColumns () > screen.Width))))
{
break;
}
Rune rune = idx >= 0 && idx < runes.Length ? runes [idx] : (Rune)' ';
int runeWidth = GetRuneWidth (rune, TabWidth);
if (isVertical)
{
if (runeWidth > 0)
{
// Update line height for vertical text (each rune is a column)
lineHeight = Math.Max (lineHeight, current - y + 1);
lineWidth = Math.Max (lineWidth, 1); // Width is 1 per rune in vertical
}
}
else
{
// Update line width and position for horizontal text
lineWidth += runeWidth;
}
current += isVertical && runeWidth > 0 ? 1 : runeWidth;
int nextRuneWidth = idx + 1 > -1 && idx + 1 < runes.Length
? runes [idx + 1].GetColumns ()
: 0;
if (!isVertical && idx + 1 < runes.Length && current + nextRuneWidth > start + size)
{
break;
}
}
// Add the line's drawn region to the overall region
if (lineWidth > 0 && lineHeight > 0)
{
drawnRegion.Union (new Rectangle (lineX, lineY, lineWidth, lineHeight));
}
}
return drawnRegion;
}
#region Static Members
/// Check if it is a horizontal direction
public static bool IsHorizontalDirection (TextDirection textDirection)
{
return textDirection switch
{
TextDirection.LeftRight_TopBottom => true,
TextDirection.LeftRight_BottomTop => true,
TextDirection.RightLeft_TopBottom => true,
TextDirection.RightLeft_BottomTop => true,
_ => false
};
}
/// Check if it is a vertical direction
public static bool IsVerticalDirection (TextDirection textDirection)
{
return textDirection switch
{
TextDirection.TopBottom_LeftRight => true,
TextDirection.TopBottom_RightLeft => true,
TextDirection.BottomTop_LeftRight => true,
TextDirection.BottomTop_RightLeft => true,
_ => false
};
}
/// Check if it is Left to Right direction
public static bool IsLeftToRight (TextDirection textDirection)
{
return textDirection switch
{
TextDirection.LeftRight_TopBottom => true,
TextDirection.LeftRight_BottomTop => true,
_ => false
};
}
/// Check if it is Top to Bottom direction
public static bool IsTopToBottom (TextDirection textDirection)
{
return textDirection switch
{
TextDirection.TopBottom_LeftRight => true,
TextDirection.TopBottom_RightLeft => true,
_ => false
};
}
// TODO: Move to StringExtensions?
private static string StripCRLF (string str, bool keepNewLine = false)
{
List runes = str.ToRuneList ();
for (var i = 0; i < runes.Count; i++)
{
switch ((char)runes [i].Value)
{
case '\n':
if (!keepNewLine)
{
runes.RemoveAt (i);
}
break;
case '\r':
if (i + 1 < runes.Count && runes [i + 1].Value == '\n')
{
runes.RemoveAt (i);
if (!keepNewLine)
{
runes.RemoveAt (i);
}
i++;
}
else
{
if (!keepNewLine)
{
runes.RemoveAt (i);
}
}
break;
}
}
return StringExtensions.ToString (runes);
}
// TODO: Move to StringExtensions?
private static string ReplaceCRLFWithSpace (string str)
{
List runes = str.ToRuneList ();
for (var i = 0; i < runes.Count; i++)
{
switch (runes [i].Value)
{
case '\n':
runes [i] = (Rune)' ';
break;
case '\r':
if (i + 1 < runes.Count && runes [i + 1].Value == '\n')
{
runes [i] = (Rune)' ';
runes.RemoveAt (i + 1);
i++;
}
else
{
runes [i] = (Rune)' ';
}
break;
}
}
return StringExtensions.ToString (runes);
}
// TODO: Move to StringExtensions?
private static string ReplaceTABWithSpaces (string str, int tabWidth)
{
if (tabWidth == 0)
{
return str.Replace ("\t", "");
}
return str.Replace ("\t", new (' ', tabWidth));
}
// TODO: Move to StringExtensions?
///
/// Splits all newlines in the into a list and supports both CRLF and LF, preserving the
/// ending newline.
///
/// The text.
/// A list of text without the newline characters.
public static List SplitNewLine (string text)
{
List runes = text.ToRuneList ();
List lines = new ();
var start = 0;
for (var i = 0; i < runes.Count; i++)
{
int end = i;
switch (runes [i].Value)
{
case '\n':
lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start)));
i++;
start = i;
break;
case '\r':
if (i + 1 < runes.Count && runes [i + 1].Value == '\n')
{
lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start)));
i += 2;
start = i;
}
else
{
lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start)));
i++;
start = i;
}
break;
}
}
switch (runes.Count)
{
case > 0 when lines.Count == 0:
lines.Add (StringExtensions.ToString (runes));
break;
case > 0 when start < runes.Count:
lines.Add (StringExtensions.ToString (runes.GetRange (start, runes.Count - start)));
break;
default:
lines.Add ("");
break;
}
return lines;
}
// TODO: Move to StringExtensions?
///
/// Adds trailing whitespace or truncates so that it fits exactly
/// columns. Note that some unicode characters take 2+ columns
///
///
///
///
public static string ClipOrPad (string text, int width)
{
if (string.IsNullOrEmpty (text))
{
return text;
}
// if value is not wide enough
if (text.EnumerateRunes ().Sum (c => c.GetColumns ()) < width)
{
// pad it out with spaces to the given Alignment
int toPad = width - text.EnumerateRunes ().Sum (c => c.GetColumns ());
return text + new string (' ', toPad);
}
// value is too wide
return new (text.TakeWhile (c => (width -= ((Rune)c).GetColumns ()) >= 0).ToArray ());
}
/// Formats the provided text to fit within the width provided using word wrapping.
/// The text to word wrap
/// The number of columns to constrain the text to
///
/// If trailing spaces at the end of wrapped lines will be
/// preserved. If , trailing spaces at the end of wrapped lines will be trimmed.
///
/// The number of columns used for a tab.
/// The text direction.
/// instance to access any of his objects.
/// A list of word wrapped lines.
///
/// This method does not do any alignment.
/// This method strips Newline ('\n' and '\r\n') sequences before processing.
///
/// If is at most one space will be preserved
/// at the end of the last line.
///
///
/// A list of lines.
public static List WordWrapText (
string text,
int width,
bool preserveTrailingSpaces = false,
int tabWidth = 0,
TextDirection textDirection = TextDirection.LeftRight_TopBottom,
TextFormatter? textFormatter = null
)
{
ArgumentOutOfRangeException.ThrowIfNegative (width, nameof (width));
List lines = new ();
if (string.IsNullOrEmpty (text))
{
return lines;
}
List runes = StripCRLF (text).ToRuneList ();
int start = Math.Max (
!runes.Contains ((Rune)' ') && textFormatter is { VerticalAlignment: Alignment.End } && IsVerticalDirection (textDirection)
? runes.Count - width
: 0,
0);
int end;
if (preserveTrailingSpaces)
{
while ((end = start) < runes.Count)
{
end = GetNextWhiteSpace (start, width, out bool incomplete);
if (end == 0 && incomplete)
{
start = text.GetRuneCount ();
break;
}
lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start)));
start = end;
if (incomplete)
{
start = text.GetRuneCount ();
break;
}
}
}
else
{
if (IsHorizontalDirection (textDirection))
{
while ((end = start
+ GetLengthThatFits (
runes.GetRange (start, runes.Count - start),
width,
tabWidth,
textDirection
))
< runes.Count)
{
while (runes [end].Value != ' ' && end > start)
{
end--;
}
if (end == start)
{
end = start
+ GetLengthThatFits (
runes.GetRange (end, runes.Count - end),
width,
tabWidth,
textDirection
);
}
var str = StringExtensions.ToString (runes.GetRange (start, end - start));
int zeroLength = text.EnumerateRunes ().Sum (r => r.GetColumns () == 0 ? 1 : 0);
if (end > start && GetRuneWidth (str, tabWidth, textDirection) <= width + zeroLength)
{
lines.Add (str);
start = end;
if (runes [end].Value == ' ')
{
start++;
}
}
else
{
end++;
start = end;
}
}
}
else
{
while ((end = start + width) < runes.Count)
{
while (runes [end].Value != ' ' && end > start)
{
end--;
}
if (end == start)
{
end = start + width;
}
var zeroLength = 0;
for (int i = end; i < runes.Count - start; i++)
{
Rune r = runes [i];
if (r.GetColumns () == 0)
{
zeroLength++;
}
else
{
break;
}
}
lines.Add (
StringExtensions.ToString (
runes.GetRange (
start,
end - start + zeroLength
)
)
);
end += zeroLength;
start = end;
if (runes [end].Value == ' ')
{
start++;
}
}
}
}
int GetNextWhiteSpace (int from, int cWidth, out bool incomplete, int cLength = 0)
{
int to = from;
int length = cLength;
incomplete = false;
while (length < cWidth && to < runes.Count)
{
Rune rune = runes [to];
if (IsHorizontalDirection (textDirection))
{
length += rune.GetColumns ();
}
else
{
length++;
}
if (length > cWidth)
{
if (to >= runes.Count || (length > 1 && cWidth <= 1))
{
incomplete = true;
}
return to;
}
switch (rune.Value)
{
case ' ' when length == cWidth:
return to + 1;
case ' ' when length > cWidth:
return to;
case ' ':
return GetNextWhiteSpace (to + 1, cWidth, out incomplete, length);
case '\t':
{
length += tabWidth + 1;
if (length == tabWidth && tabWidth > cWidth)
{
return to + 1;
}
if (length > cWidth && tabWidth > cWidth)
{
return to;
}
return GetNextWhiteSpace (to + 1, cWidth, out incomplete, length);
}
default:
to++;
break;
}
}
return cLength switch
{
> 0 when to < runes.Count && runes [to].Value != ' ' && runes [to].Value != '\t' => from,
> 0 when to < runes.Count && (runes [to].Value == ' ' || runes [to].Value == '\t') => from,
_ => to
};
}
if (start < text.GetRuneCount ())
{
string str = ReplaceTABWithSpaces (
StringExtensions.ToString (runes.GetRange (start, runes.Count - start)),
tabWidth
);
if (IsVerticalDirection (textDirection) || preserveTrailingSpaces || str.GetColumns () <= width)
{
lines.Add (str);
}
}
return lines;
}
/// Justifies text within a specified width.
/// The text to justify.
///
/// The number of columns to clip the text to. Text longer than will be
/// clipped.
///
/// Alignment.
/// The text direction.
/// The number of columns used for a tab.
/// instance to access any of his objects.
/// Justified and clipped text.
public static string ClipAndJustify (
string text,
int width,
Alignment textAlignment,
TextDirection textDirection = TextDirection.LeftRight_TopBottom,
int tabWidth = 0,
TextFormatter? textFormatter = null
)
{
return ClipAndJustify (text, width, textAlignment == Alignment.Fill, textDirection, tabWidth, textFormatter);
}
/// Justifies text within a specified width.
/// The text to justify.
///
/// The number of columns to clip the text to. Text longer than will be
/// clipped.
///
/// Justify.
/// The text direction.
/// The number of columns used for a tab.
/// instance to access any of his objects.
/// Justified and clipped text.
public static string ClipAndJustify (
string text,
int width,
bool justify,
TextDirection textDirection = TextDirection.LeftRight_TopBottom,
int tabWidth = 0,
TextFormatter? textFormatter = null
)
{
ArgumentOutOfRangeException.ThrowIfNegative (width, nameof (width));
if (string.IsNullOrEmpty (text))
{
return text;
}
text = ReplaceTABWithSpaces (text, tabWidth);
List runes = text.ToRuneList ();
int zeroLength = runes.Sum (r => r.GetColumns () == 0 ? 1 : 0);
if (runes.Count - zeroLength > width)
{
if (IsHorizontalDirection (textDirection))
{
if (textFormatter is { Alignment: Alignment.End })
{
return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection);
}
if (textFormatter is { Alignment: Alignment.Center })
{
return GetRangeThatFits (runes, Math.Max ((runes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection);
}
return GetRangeThatFits (runes, 0, text, width, tabWidth, textDirection);
}
if (IsVerticalDirection (textDirection))
{
if (textFormatter is { VerticalAlignment: Alignment.End })
{
return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection);
}
if (textFormatter is { VerticalAlignment: Alignment.Center })
{
return GetRangeThatFits (runes, Math.Max ((runes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection);
}
return GetRangeThatFits (runes, 0, text, width, tabWidth, textDirection);
}
return StringExtensions.ToString (runes.GetRange (0, width + zeroLength));
}
if (justify)
{
return Justify (text, width, ' ', textDirection, tabWidth);
}
if (IsHorizontalDirection (textDirection))
{
if (textFormatter is { Alignment: Alignment.End })
{
if (GetRuneWidth (text, tabWidth, textDirection) > width)
{
return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection);
}
}
else if (textFormatter is { Alignment: Alignment.Center })
{
return GetRangeThatFits (runes, Math.Max ((runes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection);
}
else if (GetRuneWidth (text, tabWidth, textDirection) > width)
{
return GetRangeThatFits (runes, 0, text, width, tabWidth, textDirection);
}
}
if (IsVerticalDirection (textDirection))
{
if (textFormatter is { VerticalAlignment: Alignment.End })
{
if (runes.Count - zeroLength > width)
{
return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection);
}
}
else if (textFormatter is { VerticalAlignment: Alignment.Center })
{
return GetRangeThatFits (runes, Math.Max ((runes.Count - width - zeroLength) / 2, 0), text, width, tabWidth, textDirection);
}
else if (runes.Count - zeroLength > width)
{
return GetRangeThatFits (runes, 0, text, width, tabWidth, textDirection);
}
}
return text;
}
private static string GetRangeThatFits (List runes, int index, string text, int width, int tabWidth, TextDirection textDirection)
{
return StringExtensions.ToString (
runes.GetRange (
Math.Max (index, 0),
GetLengthThatFits (text, width, tabWidth, textDirection)
)
);
}
///
/// Justifies the text to fill the width provided. Space will be added between words to make the text just fit
/// width. Spaces will not be added to the start or end.
///
///
///
/// Character to replace whitespace and pad with. For debugging purposes.
/// The text direction.
/// The number of columns used for a tab.
/// The justified text.
public static string Justify (
string text,
int width,
char spaceChar = ' ',
TextDirection textDirection = TextDirection.LeftRight_TopBottom,
int tabWidth = 0
)
{
ArgumentOutOfRangeException.ThrowIfNegative (width, nameof (width));
if (string.IsNullOrEmpty (text))
{
return text;
}
text = ReplaceTABWithSpaces (text, tabWidth);
string [] words = text.Split (' ');
int textCount;
if (IsHorizontalDirection (textDirection))
{
textCount = words.Sum (arg => GetRuneWidth (arg, tabWidth, textDirection));
}
else
{
textCount = words.Sum (arg => arg.GetRuneCount ()) - text.EnumerateRunes ().Sum (r => r.GetColumns () == 0 ? 1 : 0);
}
int spaces = words.Length > 1 ? (width - textCount) / (words.Length - 1) : 0;
int extras = words.Length > 1 ? (width - textCount) % (words.Length - 1) : 0;
var s = new StringBuilder ();
for (var w = 0; w < words.Length; w++)
{
string x = words [w];
s.Append (x);
if (w + 1 < words.Length)
{
for (var i = 0; i < spaces; i++)
{
s.Append (spaceChar);
}
}
if (extras > 0)
{
for (var i = 0; i < 1; i++)
{
s.Append (spaceChar);
}
extras--;
}
if (w + 1 == words.Length - 1)
{
for (var i = 0; i < extras; i++)
{
s.Append (spaceChar);
}
}
}
return s.ToString ();
}
/// Formats text into lines, applying text alignment and optionally wrapping text to new lines on word boundaries.
///
/// The number of columns to constrain the text to for word wrapping and clipping.
/// Specifies how the text will be aligned horizontally.
///
/// If , the text will be wrapped to new lines no longer than
/// . If , forces text to fit a single line. Line breaks are converted
/// to spaces. The text will be clipped to .
///
///
/// If trailing spaces at the end of wrapped lines will be
/// preserved. If , trailing spaces at the end of wrapped lines will be trimmed.
///
/// The number of columns used for a tab.
/// The text direction.
/// If new lines are allowed.
/// instance to access any of his objects.
/// A list of word wrapped lines.
///
/// An empty string will result in one empty line.
/// If is 0, a single, empty line will be returned.
/// If is int.MaxValue, the text will be formatted to the maximum width possible.
///
public static List Format (
string text,
int width,
Alignment textAlignment,
bool wordWrap,
bool preserveTrailingSpaces = false,
int tabWidth = 0,
TextDirection textDirection = TextDirection.LeftRight_TopBottom,
bool multiLine = false,
TextFormatter? textFormatter = null
)
{
return Format (
text,
width,
textAlignment == Alignment.Fill,
wordWrap,
preserveTrailingSpaces,
tabWidth,
textDirection,
multiLine,
textFormatter
);
}
/// Formats text into lines, applying text alignment and optionally wrapping text to new lines on word boundaries.
///
/// The number of columns to constrain the text to for word wrapping and clipping.
/// Specifies whether the text should be justified.
///
/// If , the text will be wrapped to new lines no longer than
/// . If , forces text to fit a single line. Line breaks are converted
/// to spaces. The text will be clipped to .
///
///
/// If trailing spaces at the end of wrapped lines will be
/// preserved. If , trailing spaces at the end of wrapped lines will be trimmed.
///
/// The number of columns used for a tab.
/// The text direction.
/// If new lines are allowed.
/// instance to access any of his objects.
/// A list of word wrapped lines.
///
/// An empty string will result in one empty line.
/// If is 0, a single, empty line will be returned.
/// If is int.MaxValue, the text will be formatted to the maximum width possible.
///
public static List Format (
string text,
int width,
bool justify,
bool wordWrap,
bool preserveTrailingSpaces = false,
int tabWidth = 0,
TextDirection textDirection = TextDirection.LeftRight_TopBottom,
bool multiLine = false,
TextFormatter? textFormatter = null
)
{
ArgumentOutOfRangeException.ThrowIfNegative (width, nameof (width));
List lineResult = new ();
if (string.IsNullOrEmpty (text) || width == 0)
{
lineResult.Add (string.Empty);
return lineResult;
}
if (!wordWrap)
{
text = ReplaceTABWithSpaces (text, tabWidth);
if (multiLine)
{
// Abhorrent case: Just a new line
if (text == "\n")
{
lineResult.Add (string.Empty);
return lineResult;
}
string []? lines = null;
if (text.Contains ("\r\n"))
{
lines = text.Split ("\r\n");
}
else if (text.Contains ('\n'))
{
lines = text.Split ('\n');
}
lines ??= new [] { text };
foreach (string line in lines)
{
lineResult.Add (
ClipAndJustify (
PerformCorrectFormatDirection (textDirection, line),
width,
justify,
textDirection,
tabWidth,
textFormatter));
}
return PerformCorrectFormatDirection (textDirection, lineResult);
}
text = ReplaceCRLFWithSpace (text);
lineResult.Add (ClipAndJustify (PerformCorrectFormatDirection (textDirection, text), width, justify, textDirection, tabWidth, textFormatter));
return PerformCorrectFormatDirection (textDirection, lineResult);
}
List runes = StripCRLF (text, true).ToRuneList ();
int runeCount = runes.Count;
var lp = 0;
for (var i = 0; i < runeCount; i++)
{
Rune c = runes [i];
if (c.Value == '\n')
{
List wrappedLines =
WordWrapText (
StringExtensions.ToString (PerformCorrectFormatDirection (textDirection, runes.GetRange (lp, i - lp))),
width,
preserveTrailingSpaces,
tabWidth,
textDirection,
textFormatter
);
foreach (string line in wrappedLines)
{
lineResult.Add (ClipAndJustify (line, width, justify, textDirection, tabWidth));
}
if (wrappedLines.Count == 0)
{
lineResult.Add (string.Empty);
}
lp = i + 1;
}
}
foreach (string line in WordWrapText (
StringExtensions.ToString (PerformCorrectFormatDirection (textDirection, runes.GetRange (lp, runeCount - lp))),
width,
preserveTrailingSpaces,
tabWidth,
textDirection,
textFormatter
))
{
lineResult.Add (ClipAndJustify (line, width, justify, textDirection, tabWidth));
}
return PerformCorrectFormatDirection (textDirection, lineResult);
}
private static string PerformCorrectFormatDirection (TextDirection textDirection, string line)
{
return textDirection switch
{
TextDirection.RightLeft_BottomTop
or TextDirection.RightLeft_TopBottom
or TextDirection.BottomTop_LeftRight
or TextDirection.BottomTop_RightLeft => StringExtensions.ToString (line.EnumerateRunes ().Reverse ()),
_ => line
};
}
private static List PerformCorrectFormatDirection (TextDirection textDirection, List runes)
{
return PerformCorrectFormatDirection (textDirection, StringExtensions.ToString (runes)).ToRuneList ();
}
private static List PerformCorrectFormatDirection (TextDirection textDirection, List lines)
{
return textDirection switch
{
TextDirection.TopBottom_RightLeft
or TextDirection.LeftRight_BottomTop
or TextDirection.RightLeft_BottomTop
or TextDirection.BottomTop_RightLeft => lines.ToArray ().Reverse ().ToList (),
_ => lines
};
}
///
/// Returns the number of columns required to render oriented vertically.
///
///
/// This API will return incorrect results if the text includes glyphs whose width is dependent on surrounding
/// glyphs (e.g. Arabic).
///
/// The lines.
/// The line in the list to start with (any lines before will be ignored).
///
/// The number of lines to process (if less than lines.Count, any lines after will be
/// ignored).
///
/// The number of columns used for a tab.
/// The width required.
public static int GetColumnsRequiredForVerticalText (
List lines,
int startLine = -1,
int linesCount = -1,
int tabWidth = 0
)
{
var max = 0;
for (int i = startLine == -1 ? 0 : startLine;
i < (linesCount == -1 ? lines.Count : startLine + linesCount);
i++)
{
string runes = lines [i];
if (runes.Length > 0)
{
max += runes.EnumerateRunes ().Max (r => GetRuneWidth (r, tabWidth));
}
}
return max;
}
///
/// Returns the number of columns in the widest line in the text, without word wrap, accounting for wide-glyphs
/// (uses ). if it contains newlines.
///
///
/// This API will return incorrect results if the text includes glyphs whose width is dependent on surrounding
/// glyphs (e.g. Arabic).
///
/// Text, may contain newlines.
/// The number of columns used for a tab.
/// The length of the longest line.
public static int GetWidestLineLength (string text, int tabWidth = 0)
{
List result = SplitNewLine (text);
return result.Max (x => GetRuneWidth (x, tabWidth));
}
///
/// Gets the maximum number of columns from the text based on the and the
/// .
///
///
/// This API will return incorrect results if the text includes glyphs whose width is dependent on surrounding
/// glyphs (e.g. Arabic).
///
/// The text.
/// The start index.
/// The length.
/// The number of columns used for a tab.
/// The maximum characters width.
public static int GetSumMaxCharWidth (string text, int startIndex = -1, int length = -1, int tabWidth = 0)
{
var max = 0;
Rune [] runes = text.ToRunes ();
for (int i = startIndex == -1 ? 0 : startIndex;
i < (length == -1 ? runes.Length : startIndex + length);
i++)
{
max += GetRuneWidth (runes [i], tabWidth);
}
return max;
}
/// Gets the number of the Runes in the text that will fit in .
///
/// This API will return incorrect results if the text includes glyphs whose width is dependent on surrounding
/// glyphs (e.g. Arabic).
///
/// The text.
/// The width.
/// The width used for a tab.
/// The text direction.
/// The index of the text that fit the width.
public static int GetLengthThatFits (string text, int width, int tabWidth = 0, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
{
return GetLengthThatFits (text?.ToRuneList () ?? [], width, tabWidth, textDirection);
}
/// Gets the number of the Runes in a list of Runes that will fit in .
///
/// This API will return incorrect results if the text includes glyphs whose width is dependent on surrounding
/// glyphs (e.g. Arabic).
///
/// The list of runes.
/// The width.
/// The width used for a tab.
/// The text direction.
/// The index of the last Rune in that fit in .
public static int GetLengthThatFits (List runes, int width, int tabWidth = 0, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
{
if (runes is null || runes.Count == 0)
{
return 0;
}
var runesLength = 0;
var runeIdx = 0;
for (; runeIdx < runes.Count; runeIdx++)
{
int runeWidth = GetRuneWidth (runes [runeIdx], tabWidth, textDirection);
if (runesLength + runeWidth > width)
{
break;
}
runesLength += runeWidth;
}
return runeIdx;
}
private static int GetRuneWidth (string str, int tabWidth, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
{
int runesWidth = 0;
foreach (Rune rune in str.EnumerateRunes ())
{
runesWidth += GetRuneWidth (rune, tabWidth, textDirection);
}
return runesWidth;
}
private static int GetRuneWidth (Rune rune, int tabWidth, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
{
int runeWidth = IsHorizontalDirection (textDirection) ? rune.GetColumns () : rune.GetColumns () == 0 ? 0 : 1;
if (rune.Value == '\t')
{
return tabWidth;
}
if (runeWidth < 0 || runeWidth > 0)
{
return Math.Max (runeWidth, 1);
}
return runeWidth;
}
/// Gets the index position from the list based on the .
///
/// This API will return incorrect results if the text includes glyphs whose width is dependent on surrounding
/// glyphs (e.g. Arabic).
///
/// The lines.
/// The width.
/// The number of columns used for a tab.
/// The index of the list that fit the width.
public static int GetMaxColsForWidth (List lines, int width, int tabWidth = 0)
{
var runesLength = 0;
var lineIdx = 0;
for (; lineIdx < lines.Count; lineIdx++)
{
List runes = lines [lineIdx].ToRuneList ();
int maxRruneWidth = runes.Count > 0
? runes.Max (r => GetRuneWidth (r, tabWidth))
: 1;
if (runesLength + maxRruneWidth > width)
{
break;
}
runesLength += maxRruneWidth;
}
return lineIdx;
}
/// Finds the HotKey and its location in text.
/// The text to look in.
/// The HotKey specifier (e.g. '_') to look for.
/// Outputs the Rune index into text.
/// Outputs the hotKey. if not found.
///
/// If true the legacy behavior of identifying the first upper case character as the
/// HotKey will be enabled. Regardless of the value of this parameter, hotKeySpecifier takes precedence.
/// Defaults to .
///
/// true if a HotKey was found; false otherwise.
public static bool FindHotKey (
string text,
Rune hotKeySpecifier,
out int hotPos,
out Key hotKey,
bool firstUpperCase = false
)
{
if (string.IsNullOrEmpty (text) || hotKeySpecifier == (Rune)0xFFFF)
{
hotPos = -1;
hotKey = Key.Empty;
return false;
}
var curHotKey = (Rune)0;
int curHotPos = -1;
// Use first hot_key char passed into 'hotKey'.
// TODO: Ignore hot_key of two are provided
// TODO: Do not support non-alphanumeric chars that can't be typed
var i = 0;
foreach (Rune c in text.EnumerateRunes ())
{
if ((char)c.Value != 0xFFFD)
{
if (c == hotKeySpecifier)
{
curHotPos = i;
}
else if (curHotPos > -1)
{
curHotKey = c;
break;
}
}
i++;
}
// Legacy support - use first upper case char if the specifier was not found
if (curHotPos == -1 && firstUpperCase)
{
i = 0;
foreach (Rune c in text.EnumerateRunes ())
{
if ((char)c.Value != 0xFFFD)
{
if (Rune.IsUpper (c))
{
curHotKey = c;
curHotPos = i;
break;
}
}
i++;
}
}
if (curHotKey != (Rune)0 && curHotPos != -1)
{
hotPos = curHotPos;
var newHotKey = (KeyCode)curHotKey.Value;
if (newHotKey != KeyCode.Null && !(newHotKey == KeyCode.Space || Rune.IsControl (curHotKey)))
{
if ((newHotKey & ~KeyCode.Space) is >= KeyCode.A and <= KeyCode.Z)
{
newHotKey &= ~KeyCode.Space;
}
hotKey = newHotKey;
//hotKey.Scope = KeyBindingScope.HotKey;
return true;
}
}
hotPos = -1;
hotKey = KeyCode.Null;
return false;
}
///
/// Replaces the Rune at the index specified by the hotPos parameter with a tag identifying it as the
/// hotkey.
///
/// The text to tag the hotkey in.
/// The Rune index of the hotkey in text.
/// The text with the hotkey tagged.
/// The returned string will not render correctly without first un-doing the tag. To undo the tag, search for
public static string ReplaceHotKeyWithTag (string text, int hotPos)
{
// Set the high bit
List runes = text.ToRuneList ();
if (Rune.IsLetterOrDigit (runes [hotPos]))
{
runes [hotPos] = new ((uint)runes [hotPos].Value);
}
return StringExtensions.ToString (runes);
}
/// Removes the hotkey specifier from text.
/// The text to manipulate.
/// The hot-key specifier (e.g. '_') to look for.
/// Returns the position of the hot-key in the text. -1 if not found.
/// The input text with the hotkey specifier ('_') removed.
public static string RemoveHotKeySpecifier (string text, int hotPos, Rune hotKeySpecifier)
{
if (string.IsNullOrEmpty (text))
{
return text;
}
// Scan
var start = string.Empty;
var i = 0;
foreach (Rune c in text.EnumerateRunes ())
{
if (c == hotKeySpecifier && i == hotPos)
{
i++;
continue;
}
start += c;
i++;
}
return start;
}
#endregion // Static Members
}