Fixes #3011. TextFormatter doesn't handle combining and tab runes. (#3012)

* Add horizontal and vertical support for combining glyphs.

* Fix text and auto size behavior.

* Add TabWidth property.

* Add unit test for WordWrap.

* Add MultiLine property and improve more code.

* Fix word wrap on MessageBox.

* Fix label unit test.

* Rename to GetTextFormatterSizeNeededForTextAndHotKey

* Proves that TextFormatter.Size not must to have the same View.Bounds.Size.

* Fix fails unit tests.

* Updates AutoSize document.

* Updates MultiLine document.

* Removes Application dependency from the TextFormatter class.

* Fix Draw XML comment.
This commit is contained in:
BDisp
2023-11-26 22:41:54 +00:00
committed by GitHub
parent 8ed1b16ee1
commit 3f4d96bec7
8 changed files with 829 additions and 245 deletions

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
@@ -169,6 +168,15 @@ namespace Terminal.Gui {
return StringExtensions.ToString (runes);
}
static string ReplaceTABWithSpaces (string str, int tabWidth)
{
if (tabWidth == 0) {
return str.Replace ("\t", "");
}
return str.Replace ("\t", new string (' ', tabWidth));
}
/// <summary>
/// Splits all newlines in the <paramref name="text"/> into a list
/// and supports both CRLF and LF, preserving the ending newline.
@@ -342,13 +350,13 @@ namespace Terminal.Gui {
// }
//}
while ((end = start + Math.Max (GetLengthThatFits (runes.GetRange (start, runes.Count - start), width), 1)) < runes.Count) {
while ((end = start + GetLengthThatFits (runes.GetRange (start, runes.Count - start), width, tabWidth)) < runes.Count) {
while (runes [end].Value != ' ' && end > start)
end--;
if (end == start)
end = start + GetLengthThatFits (runes.GetRange (end, runes.Count - end), width);
end = start + GetLengthThatFits (runes.GetRange (end, runes.Count - end), width, tabWidth);
var str = StringExtensions.ToString (runes.GetRange (start, end - start));
if (end > start && str.GetColumns () <= width) {
if (end > start && GetRuneWidth (str, tabWidth) <= width) {
lines.Add (str);
start = end;
if (runes [end].Value == ' ') {
@@ -368,7 +376,17 @@ namespace Terminal.Gui {
if (end == start) {
end = start + width;
}
lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start)));
var zeroLength = 0;
for (int i = end; i < runes.Count - start; i++) {
var 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++;
@@ -427,7 +445,7 @@ namespace Terminal.Gui {
}
if (start < text.GetRuneCount ()) {
var str = StringExtensions.ToString (runes.GetRange (start, runes.Count - start));
var str = ReplaceTABWithSpaces (StringExtensions.ToString (runes.GetRange (start, runes.Count - start)), tabWidth);
if (IsVerticalDirection (textDirection) || preserveTrailingSpaces || (!preserveTrailingSpaces && str.GetColumns () <= width)) {
lines.Add (str);
}
@@ -443,10 +461,11 @@ namespace Terminal.Gui {
/// <param name="width">The number of columns to clip the text to. Text longer than <paramref name="width"/> will be clipped.</param>
/// <param name="talign">Alignment.</param>
/// <param name="textDirection">The text direction.</param>
/// <param name="tabWidth">The number of columns used for a tab.</param>
/// <returns>Justified and clipped text.</returns>
public static string ClipAndJustify (string text, int width, TextAlignment talign, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
public static string ClipAndJustify (string text, int width, TextAlignment talign, TextDirection textDirection = TextDirection.LeftRight_TopBottom, int tabWidth = 0)
{
return ClipAndJustify (text, width, talign == TextAlignment.Justified, textDirection);
return ClipAndJustify (text, width, talign == TextAlignment.Justified, textDirection, tabWidth);
}
/// <summary>
@@ -456,8 +475,9 @@ namespace Terminal.Gui {
/// <param name="width">The number of columns to clip the text to. Text longer than <paramref name="width"/> will be clipped.</param>
/// <param name="justify">Justify.</param>
/// <param name="textDirection">The text direction.</param>
/// <param name="tabWidth">The number of columns used for a tab.</param>
/// <returns>Justified and clipped text.</returns>
public static string ClipAndJustify (string text, int width, bool justify, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
public static string ClipAndJustify (string text, int width, bool justify, TextDirection textDirection = TextDirection.LeftRight_TopBottom, int tabWidth = 0)
{
if (width < 0) {
throw new ArgumentOutOfRangeException ("Width cannot be negative.");
@@ -466,19 +486,21 @@ namespace Terminal.Gui {
return text;
}
text = ReplaceTABWithSpaces (text, tabWidth);
var runes = text.ToRuneList ();
int slen = runes.Count;
if (slen > width) {
if (IsHorizontalDirection (textDirection)) {
return StringExtensions.ToString (runes.GetRange (0, GetLengthThatFits (text, width)));
return StringExtensions.ToString (runes.GetRange (0, GetLengthThatFits (text, width, tabWidth)));
} else {
return StringExtensions.ToString (runes.GetRange (0, width));
var zeroLength = runes.Sum (r => r.GetColumns () == 0 ? 1 : 0);
return StringExtensions.ToString (runes.GetRange (0, width + zeroLength));
}
} else {
if (justify) {
return Justify (text, width, ' ', textDirection);
} else if (IsHorizontalDirection (textDirection) && text.GetColumns () > width) {
return StringExtensions.ToString (runes.GetRange (0, GetLengthThatFits (text, width)));
return Justify (text, width, ' ', textDirection, tabWidth);
} else if (IsHorizontalDirection (textDirection) && GetRuneWidth (text, tabWidth) > width) {
return StringExtensions.ToString (runes.GetRange (0, GetLengthThatFits (text, width, tabWidth)));
}
return text;
}
@@ -492,8 +514,9 @@ namespace Terminal.Gui {
/// <param name="width"></param>
/// <param name="spaceChar">Character to replace whitespace and pad with. For debugging purposes.</param>
/// <param name="textDirection">The text direction.</param>
/// <param name="tabWidth">The number of columns used for a tab.</param>
/// <returns>The justified text.</returns>
public static string Justify (string text, int width, char spaceChar = ' ', TextDirection textDirection = TextDirection.LeftRight_TopBottom)
public static string Justify (string text, int width, char spaceChar = ' ', TextDirection textDirection = TextDirection.LeftRight_TopBottom, int tabWidth = 0)
{
if (width < 0) {
throw new ArgumentOutOfRangeException ("Width cannot be negative.");
@@ -502,10 +525,11 @@ namespace Terminal.Gui {
return text;
}
text = ReplaceTABWithSpaces (text, tabWidth);
var words = text.Split (' ');
int textCount;
if (IsHorizontalDirection (textDirection)) {
textCount = words.Sum (arg => arg.GetColumns ());
textCount = words.Sum (arg => GetRuneWidth (arg, tabWidth));
} else {
textCount = words.Sum (arg => arg.GetRuneCount ());
}
@@ -532,7 +556,7 @@ namespace Terminal.Gui {
return s.ToString ();
}
static char [] whitespace = new char [] { ' ', '\t' };
//static char [] whitespace = new char [] { ' ', '\t' };
/// <summary>
/// Reformats text into lines, applying text alignment and optionally wrapping text to new lines on word boundaries.
@@ -546,6 +570,7 @@ namespace Terminal.Gui {
/// If <see langword="false"/>, trailing spaces at the end of wrapped lines will be trimmed.</param>
/// <param name="tabWidth">The number of columns used for a tab.</param>
/// <param name="textDirection">The text direction.</param>
/// <param name="multiLine">If <see langword="true"/> new lines are allowed.</param>
/// <returns>A list of word wrapped lines.</returns>
/// <remarks>
/// <para>
@@ -558,9 +583,9 @@ namespace Terminal.Gui {
/// If <paramref name="width"/> is int.MaxValue, the text will be formatted to the maximum width possible.
/// </para>
/// </remarks>
public static List<string> Format (string text, int width, TextAlignment talign, bool wordWrap, bool preserveTrailingSpaces = false, int tabWidth = 0, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
public static List<string> Format (string text, int width, TextAlignment talign, bool wordWrap, bool preserveTrailingSpaces = false, int tabWidth = 0, TextDirection textDirection = TextDirection.LeftRight_TopBottom, bool multiLine = false)
{
return Format (text, width, talign == TextAlignment.Justified, wordWrap, preserveTrailingSpaces, tabWidth, textDirection);
return Format (text, width, talign == TextAlignment.Justified, wordWrap, preserveTrailingSpaces, tabWidth, textDirection, multiLine);
}
/// <summary>
@@ -575,6 +600,7 @@ namespace Terminal.Gui {
/// If <see langword="false"/>, trailing spaces at the end of wrapped lines will be trimmed.</param>
/// <param name="tabWidth">The number of columns used for a tab.</param>
/// <param name="textDirection">The text direction.</param>
/// <param name="multiLine">If <see langword="true"/> new lines are allowed.</param>
/// <returns>A list of word wrapped lines.</returns>
/// <remarks>
/// <para>
@@ -588,7 +614,7 @@ namespace Terminal.Gui {
/// </para>
/// </remarks>
public static List<string> Format (string text, int width, bool justify, bool wordWrap,
bool preserveTrailingSpaces = false, int tabWidth = 0, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
bool preserveTrailingSpaces = false, int tabWidth = 0, TextDirection textDirection = TextDirection.LeftRight_TopBottom, bool multiLine = false)
{
if (width < 0) {
throw new ArgumentOutOfRangeException ("width cannot be negative");
@@ -600,10 +626,27 @@ namespace Terminal.Gui {
return lineResult;
}
if (wordWrap == false) {
text = ReplaceCRLFWithSpace (text);
lineResult.Add (ClipAndJustify (text, width, justify, textDirection));
return lineResult;
if (!wordWrap) {
text = ReplaceTABWithSpaces (text, tabWidth);
if (multiLine) {
string [] lines = null;
if (text.Contains ("\r\n")) {
lines = text.Split ("\r\n");
} else if (text.Contains ('\n')) {
lines = text.Split ('\n');
}
if (lines == null) {
lines = new [] { text };
}
foreach (var line in lines) {
lineResult.Add (ClipAndJustify (line, width, justify, textDirection, tabWidth));
}
return lineResult;
} else {
text = ReplaceCRLFWithSpace (text);
lineResult.Add (ClipAndJustify (text, width, justify, textDirection, tabWidth));
return lineResult;
}
}
var runes = StripCRLF (text, true).ToRuneList ();
@@ -614,7 +657,7 @@ namespace Terminal.Gui {
if (c.Value == '\n') {
var wrappedLines = WordWrapText (StringExtensions.ToString (runes.GetRange (lp, i - lp)), width, preserveTrailingSpaces, tabWidth, textDirection);
foreach (var line in wrappedLines) {
lineResult.Add (ClipAndJustify (line, width, justify, textDirection));
lineResult.Add (ClipAndJustify (line, width, justify, textDirection, tabWidth));
}
if (wrappedLines.Count == 0) {
lineResult.Add (string.Empty);
@@ -623,7 +666,7 @@ namespace Terminal.Gui {
}
}
foreach (var line in WordWrapText (StringExtensions.ToString (runes.GetRange (lp, runeCount - lp)), width, preserveTrailingSpaces, tabWidth, textDirection)) {
lineResult.Add (ClipAndJustify (line, width, justify, textDirection));
lineResult.Add (ClipAndJustify (line, width, justify, textDirection, tabWidth));
}
return lineResult;
@@ -648,13 +691,14 @@ namespace Terminal.Gui {
/// <returns>Width of the longest line after formatting the text constrained by <paramref name="maxColumns"/>.</returns>
/// <param name="text">Text, may contain newlines.</param>
/// <param name="maxColumns">The number of columns to constrain the text to for formatting.</param>
public static int MaxWidth (string text, int maxColumns)
/// <param name="tabWidth">The number of columns used for a tab.</param>
public static int MaxWidth (string text, int maxColumns, int tabWidth = 0)
{
var result = TextFormatter.Format (text: text, width: maxColumns, justify: false, wordWrap: true);
var max = 0;
result.ForEach (s => {
var m = 0;
s.ToRuneList ().ForEach (r => m += Math.Max (r.GetColumns (), 1));
s.ToRuneList ().ForEach (r => m += GetRuneWidth (r, tabWidth));
if (m > max) {
max = m;
}
@@ -667,11 +711,12 @@ namespace Terminal.Gui {
/// <paramref name="text"/> if it contains newlines.
/// </summary>
/// <param name="text">Text, may contain newlines.</param>
/// <param name="tabWidth">The number of columns used for a tab.</param>
/// <returns>The length of the longest line.</returns>
public static int MaxWidthLine (string text)
public static int MaxWidthLine (string text, int tabWidth = 0)
{
var result = TextFormatter.SplitNewLine (text);
return result.Max (x => x.GetColumns ());
return result.Max (x => GetRuneWidth (x, tabWidth));
}
/// <summary>
@@ -681,14 +726,15 @@ namespace Terminal.Gui {
/// <param name="lines">The lines.</param>
/// <param name="startIndex">The start index.</param>
/// <param name="length">The length.</param>
/// <param name="tabWidth">The number of columns used for a tab.</param>
/// <returns>The maximum characters width.</returns>
public static int GetSumMaxCharWidth (List<string> lines, int startIndex = -1, int length = -1)
public static int GetSumMaxCharWidth (List<string> lines, int startIndex = -1, int length = -1, int tabWidth = 0)
{
var max = 0;
for (int i = (startIndex == -1 ? 0 : startIndex); i < (length == -1 ? lines.Count : startIndex + length); i++) {
var runes = lines [i];
if (runes.Length > 0)
max += runes.EnumerateRunes ().Max (r => Math.Max (r.GetColumns (), 1));
max += runes.EnumerateRunes ().Max (r => GetRuneWidth (r, tabWidth));
}
return max;
}
@@ -700,13 +746,14 @@ namespace Terminal.Gui {
/// <param name="text">The text.</param>
/// <param name="startIndex">The start index.</param>
/// <param name="length">The length.</param>
/// <param name="tabWidth">The number of columns used for a tab.</param>
/// <returns>The maximum characters width.</returns>
public static int GetSumMaxCharWidth (string text, int startIndex = -1, int length = -1)
public static int GetSumMaxCharWidth (string text, int startIndex = -1, int length = -1, int tabWidth = 0)
{
var max = 0;
var runes = text.ToRunes ();
for (int i = (startIndex == -1 ? 0 : startIndex); i < (length == -1 ? runes.Length : startIndex + length); i++) {
max += Math.Max (runes [i].GetColumns (), 1);
max += GetRuneWidth (runes [i], tabWidth);
}
return max;
}
@@ -716,16 +763,18 @@ namespace Terminal.Gui {
/// </summary>
/// <param name="text">The text.</param>
/// <param name="columns">The width.</param>
/// <param name="tabWidth">The number of columns used for a tab.</param>
/// <returns>The index of the text that fit the width.</returns>
public static int GetLengthThatFits (string text, int columns) => GetLengthThatFits (text?.ToRuneList (), columns);
public static int GetLengthThatFits (string text, int columns, int tabWidth = 0) => GetLengthThatFits (text?.ToRuneList (), columns, tabWidth);
/// <summary>
/// Gets the number of the Runes in a list of Runes that will fit in <paramref name="columns"/>.
/// </summary>
/// <param name="runes">The list of runes.</param>
/// <param name="columns">The width.</param>
/// <param name="tabWidth">The number of columns used for a tab.</param>
/// <returns>The index of the last Rune in <paramref name="runes"/> that fit in <paramref name="columns"/>.</returns>
public static int GetLengthThatFits (List<Rune> runes, int columns)
public static int GetLengthThatFits (List<Rune> runes, int columns, int tabWidth = 0)
{
if (runes == null || runes.Count == 0) {
return 0;
@@ -734,7 +783,7 @@ namespace Terminal.Gui {
var runesLength = 0;
var runeIdx = 0;
for (; runeIdx < runes.Count; runeIdx++) {
var runeWidth = Math.Max (runes [runeIdx].GetColumns (), 1);
var runeWidth = GetRuneWidth (runes [runeIdx], tabWidth);
if (runesLength + runeWidth > columns) {
break;
}
@@ -743,20 +792,44 @@ namespace Terminal.Gui {
return runeIdx;
}
private static int GetRuneWidth (string str, int tabWidth)
{
return GetRuneWidth (str.EnumerateRunes ().ToList (), tabWidth);
}
private static int GetRuneWidth (List<Rune> runes, int tabWidth)
{
return runes.Sum (r => GetRuneWidth (r, tabWidth));
}
private static int GetRuneWidth (Rune rune, int tabWidth)
{
var runeWidth = rune.GetColumns ();
if (rune.Value == '\t') {
return tabWidth;
}
if (runeWidth < 0 || runeWidth > 0) {
return Math.Max (runeWidth, 1);
}
return runeWidth;
}
/// <summary>
/// Gets the index position from the list based on the <paramref name="width"/>.
/// </summary>
/// <param name="lines">The lines.</param>
/// <param name="width">The width.</param>
/// <param name="tabWidth">The number of columns used for a tab.</param>
/// <returns>The index of the list that fit the width.</returns>
public static int GetMaxColsForWidth (List<string> lines, int width)
public static int GetMaxColsForWidth (List<string> lines, int width, int tabWidth = 0)
{
var runesLength = 0;
var lineIdx = 0;
for (; lineIdx < lines.Count; lineIdx++) {
var runes = lines [lineIdx].ToRuneList ();
var maxRruneWidth = runes.Count > 0
? runes.Max (r => Math.Max (r.GetColumns (), 1)) : 1;
? runes.Max (r => GetRuneWidth (r, tabWidth)) : 1;
if (runesLength + maxRruneWidth > width) {
break;
}
@@ -772,8 +845,9 @@ namespace Terminal.Gui {
/// <param name="y">The y location of the rectangle</param>
/// <param name="text">The text to measure</param>
/// <param name="direction">The text direction.</param>
/// <param name="tabWidth">The number of columns used for a tab.</param>
/// <returns></returns>
public static Rect CalcRect (int x, int y, string text, TextDirection direction = TextDirection.LeftRight_TopBottom)
public static Rect CalcRect (int x, int y, string text, TextDirection direction = TextDirection.LeftRight_TopBottom, int tabWidth = 0)
{
if (string.IsNullOrEmpty (text)) {
return new Rect (new Point (x, y), Size.Empty);
@@ -795,9 +869,16 @@ namespace Terminal.Gui {
cols = 0;
} else if (rune.Value != '\r') {
cols++;
var rw = ((Rune)rune).GetColumns ();
if (rw > 0) {
rw--;
var rw = 0;
if (rune.Value == '\t') {
rw += tabWidth - 1;
} else {
rw = ((Rune)rune).GetColumns ();
if (rw > 0) {
rw--;
} else if (rw == 0) {
cols--;
}
}
cols += rw;
}
@@ -822,10 +903,18 @@ namespace Terminal.Gui {
cw = 1;
} else if (rune.Value != '\r') {
rows++;
var rw = ((Rune)rune).GetColumns ();
if (cw < rw) {
cw = rw;
vw++;
var rw = 0;
if (rune.Value == '\t') {
rw += tabWidth - 1;
rows += rw;
} else {
rw = ((Rune)rune).GetColumns ();
if (rw == 0) {
rows--;
} else if (cw < rw) {
cw = rw;
vw++;
}
}
}
}
@@ -954,13 +1043,18 @@ namespace Terminal.Gui {
#endregion // Static Members
List<string> _lines = new List<string> ();
string _text;
string _text = null;
TextAlignment _textAlignment;
VerticalTextAlignment _textVerticalAlignment;
TextDirection _textDirection;
Key _hotKey;
int _hotKeyPos = -1;
Size _size;
private bool _autoSize;
private bool _preserveTrailingSpaces;
private int _tabWidth = 4;
private bool _wordWrap = true;
private bool _multiLine;
/// <summary>
/// Event invoked when the <see cref="HotKey"/> is changed.
@@ -973,15 +1067,18 @@ namespace Terminal.Gui {
public virtual string Text {
get => _text;
set {
_text = value;
var textWasNull = _text == null && value != null;
_text = EnableNeedsFormat (value);
if (_text != null && _text.GetRuneCount () > 0 && (Size.Width == 0 || Size.Height == 0 || Size.Width != _text.GetColumns ())) {
// Provide a default size (width = length of longest line, height = 1)
// TODO: It might makes more sense for the default to be width = length of first line?
Size = new Size (TextFormatter.MaxWidth (Text, int.MaxValue), 1);
if ((AutoSize && Alignment != TextAlignment.Justified && VerticalAlignment != VerticalTextAlignment.Justified) || (textWasNull && Size.IsEmpty)) {
Size = CalcRect (0, 0, _text, _textDirection, TabWidth).Size;
}
NeedsFormat = true;
//if (_text != null && _text.GetRuneCount () > 0 && (Size.Width == 0 || Size.Height == 0 || Size.Width != _text.GetColumns ())) {
// // Provide a default size (width = length of longest line, height = 1)
// // TODO: It might makes more sense for the default to be width = length of first line?
// Size = new Size (TextFormatter.MaxWidth (Text, int.MaxValue), 1);
//}
}
}
@@ -991,7 +1088,18 @@ namespace Terminal.Gui {
/// <see cref="LayoutStyle.Absolute"/> values and doesn't work with <see cref="LayoutStyle.Computed"/> layout,
/// to avoid breaking the <see cref="Pos"/> and <see cref="Dim"/> settings.
/// </summary>
public bool AutoSize { get; set; }
/// <remarks>
/// Auto size is ignored if the <see cref="TextAlignment.Justified"/> and <see cref="VerticalTextAlignment.Justified"/> are used.
/// </remarks>
public bool AutoSize {
get => _autoSize;
set {
_autoSize = EnableNeedsFormat (value);
if (_autoSize && Alignment != TextAlignment.Justified && VerticalAlignment != VerticalTextAlignment.Justified) {
Size = CalcRect (0, 0, Text, _textDirection, TabWidth).Size;
}
}
}
/// <summary>
/// Gets or sets whether trailing spaces at the end of word-wrapped lines are preserved
@@ -999,7 +1107,10 @@ namespace Terminal.Gui {
/// If <see langword="true"/> trailing spaces at the end of wrapped lines will be removed when
/// <see cref="Text"/> is formatted for display. The default is <see langword="false"/>.
/// </summary>
public bool PreserveTrailingSpaces { get; set; }
public bool PreserveTrailingSpaces {
get => _preserveTrailingSpaces;
set => _preserveTrailingSpaces = EnableNeedsFormat (value);
}
/// <summary>
/// Controls the horizontal text-alignment property.
@@ -1007,10 +1118,7 @@ namespace Terminal.Gui {
/// <value>The text alignment.</value>
public TextAlignment Alignment {
get => _textAlignment;
set {
_textAlignment = value;
NeedsFormat = true;
}
set => _textAlignment = EnableNeedsFormat (value);
}
/// <summary>
@@ -1019,10 +1127,7 @@ namespace Terminal.Gui {
/// <value>The text vertical alignment.</value>
public VerticalTextAlignment VerticalAlignment {
get => _textVerticalAlignment;
set {
_textVerticalAlignment = value;
NeedsFormat = true;
}
set => _textVerticalAlignment = EnableNeedsFormat (value);
}
/// <summary>
@@ -1031,10 +1136,7 @@ namespace Terminal.Gui {
/// <value>The text vertical alignment.</value>
public TextDirection Direction {
get => _textDirection;
set {
_textDirection = value;
NeedsFormat = true;
}
set => _textDirection = EnableNeedsFormat (value);
}
/// <summary>
@@ -1097,11 +1199,13 @@ namespace Terminal.Gui {
}
}
// TODO: This is not implemented!
/// <summary>
///
/// Allows word wrap the to fit the available container width.
/// </summary>
public bool WordWrap { get; set; } = false;
public bool WordWrap {
get => _wordWrap;
set => _wordWrap = EnableNeedsFormat (value);
}
/// <summary>
/// Gets or sets the size of the area the text will be constrained to when formatted.
@@ -1110,12 +1214,13 @@ namespace Terminal.Gui {
/// Does not return the size the formatted text; just the value that was set.
/// </remarks>
public Size Size {
get {
return _size;
}
get => _size;
set {
_size = value;
NeedsFormat = true;
if (AutoSize && Alignment != TextAlignment.Justified && VerticalAlignment != VerticalTextAlignment.Justified) {
_size = EnableNeedsFormat (CalcRect (0, 0, Text, _textDirection, TabWidth).Size);
} else {
_size = EnableNeedsFormat (value);
}
}
}
@@ -1127,7 +1232,7 @@ namespace Terminal.Gui {
/// <summary>
/// The position in the text of the hotkey. The hotkey will be rendered using the hot color.
/// </summary>
public int HotKeyPos { get => _hotKeyPos; set => _hotKeyPos = value; }
public int HotKeyPos { get => _hotKeyPos; internal set => _hotKeyPos = value; }
/// <summary>
/// Gets the hotkey. Will be an upper case letter or digit.
@@ -1146,7 +1251,7 @@ namespace Terminal.Gui {
/// <summary>
/// Gets the cursor position from <see cref="HotKey"/>. If the <see cref="HotKey"/> is defined, the cursor will be positioned over it.
/// </summary>
public int CursorPosition { get; set; }
public int CursorPosition { get; internal set; }
/// <summary>
/// Gets the size required to hold the formatted text, given the constraints placed by <see cref="Size"/>.
@@ -1169,7 +1274,7 @@ namespace Terminal.Gui {
/// <remarks>
/// <para>
/// Upon a 'get' of this property, if the text needs to be formatted (if <see cref="NeedsFormat"/> is <c>true</c>)
/// <see cref="Format(string, int, bool, bool, bool, int, TextDirection)"/> will be called internally.
/// <see cref="Format(string, int, bool, bool, bool, int, TextDirection, bool)"/> will be called internally.
/// </para>
/// </remarks>
public List<string> Lines {
@@ -1192,18 +1297,18 @@ namespace Terminal.Gui {
}
if (IsVerticalDirection (_textDirection)) {
var colsWidth = GetSumMaxCharWidth (shown_text, 0, 1);
_lines = Format (shown_text, Size.Height, _textVerticalAlignment == VerticalTextAlignment.Justified, Size.Width > colsWidth,
PreserveTrailingSpaces, 0, _textDirection);
var colsWidth = GetSumMaxCharWidth (shown_text, 0, 1, TabWidth);
_lines = Format (shown_text, Size.Height, VerticalAlignment == VerticalTextAlignment.Justified, Size.Width > colsWidth && WordWrap,
PreserveTrailingSpaces, TabWidth, Direction, MultiLine);
if (!AutoSize) {
colsWidth = GetMaxColsForWidth (_lines, Size.Width);
colsWidth = GetMaxColsForWidth (_lines, Size.Width, TabWidth);
if (_lines.Count > colsWidth) {
_lines.RemoveRange (colsWidth, _lines.Count - colsWidth);
}
}
} else {
_lines = Format (shown_text, Size.Width, _textAlignment == TextAlignment.Justified, Size.Height > 1,
PreserveTrailingSpaces, 0, _textDirection);
_lines = Format (shown_text, Size.Width, Alignment == TextAlignment.Justified, Size.Height > 1 && WordWrap,
PreserveTrailingSpaces, TabWidth, Direction, MultiLine);
if (!AutoSize && _lines.Count > Size.Height) {
_lines.RemoveRange (Size.Height, _lines.Count - Size.Height);
}
@@ -1216,7 +1321,7 @@ namespace Terminal.Gui {
}
/// <summary>
/// Gets or sets whether the <see cref="TextFormatter"/> needs to format the text when <see cref="Draw(Rect, Attribute, Attribute, Rect, bool)"/> is called.
/// Gets or sets whether the <see cref="TextFormatter"/> needs to format the text when <see cref="Draw(Rect, Attribute, Attribute, Rect, bool, ConsoleDriver)"/> is called.
/// If it is <c>false</c> when Draw is called, the Draw call will be faster.
/// </summary>
/// <remarks>
@@ -1226,6 +1331,33 @@ namespace Terminal.Gui {
/// </remarks>
public bool NeedsFormat { get; set; }
/// <summary>
/// Gets or sets the number of columns used for a tab.
/// </summary>
public int TabWidth {
get => _tabWidth;
set => _tabWidth = EnableNeedsFormat (value);
}
/// <summary>
/// Gets or sets a value indicating whether multi line is allowed.
/// </summary>
/// <remarks>
/// Multi line is ignored if <see cref="WordWrap"/> is <see langword="true"/>.
/// </remarks>
public bool MultiLine {
get => _multiLine;
set {
_multiLine = EnableNeedsFormat (value);
}
}
private T EnableNeedsFormat<T> (T value)
{
NeedsFormat = true;
return value;
}
/// <summary>
/// Causes the <see cref="TextFormatter"/> to reformat the text.
/// </summary>
@@ -1235,27 +1367,31 @@ namespace Terminal.Gui {
var sb = new StringBuilder ();
// Lines_get causes a Format
foreach (var line in Lines) {
sb.AppendLine (line.ToString ());
sb.AppendLine (line);
}
return sb.ToString ();
}
/// <summary>
/// Draws the text held by <see cref="TextFormatter"/> to <see cref="Application.Driver"/> using the colors specified.
/// Draws the text held by <see cref="TextFormatter"/> to <see cref="ConsoleDriver"/> using the colors specified.
/// </summary>
/// <param name="bounds">Specifies the screen-relative location and maximum size for drawing the text.</param>
/// <param name="normalColor">The color to use for all text except the hotkey</param>
/// <param name="hotColor">The color to use to draw the hotkey</param>
/// <param name="containerBounds">Specifies the screen-relative location and maximum container size.</param>
/// <param name="fillRemaining">Determines if the bounds width will be used (default) or only the text width will be used.</param>
public void Draw (Rect bounds, Attribute normalColor, Attribute hotColor, Rect containerBounds = default, bool fillRemaining = true)
/// <param name="driver">The console driver currently used by the application.</param>
public void Draw (Rect bounds, Attribute normalColor, Attribute hotColor, Rect containerBounds = default, bool fillRemaining = true, ConsoleDriver driver = null)
{
// With this check, we protect against subclasses with overrides of Text (like Button)
if (string.IsNullOrEmpty (_text)) {
return;
}
Application.Driver?.SetAttribute (normalColor);
if (driver == null) {
driver = Application.Driver;
}
driver?.SetAttribute (normalColor);
// Use "Lines" to ensure a Format (don't use "lines"))
@@ -1271,7 +1407,7 @@ namespace Terminal.Gui {
var isVertical = IsVerticalDirection (_textDirection);
var maxBounds = bounds;
if (Application.Driver != null) {
if (driver != null) {
maxBounds = containerBounds == default
? bounds
: new Rect (Math.Max (containerBounds.X, bounds.X),
@@ -1316,7 +1452,7 @@ namespace Terminal.Gui {
// Horizontal Alignment
if (_textAlignment == TextAlignment.Right || (_textAlignment == TextAlignment.Justified && !IsLeftToRight (_textDirection))) {
if (isVertical) {
var runesWidth = GetSumMaxCharWidth (Lines, line);
var runesWidth = GetSumMaxCharWidth (Lines, line, TabWidth);
x = bounds.Right - runesWidth;
CursorPosition = bounds.Width - runesWidth + (_hotKeyPos > -1 ? _hotKeyPos : 0);
} else {
@@ -1326,7 +1462,7 @@ namespace Terminal.Gui {
}
} else if (_textAlignment == TextAlignment.Left || _textAlignment == TextAlignment.Justified) {
if (isVertical) {
var runesWidth = line > 0 ? GetSumMaxCharWidth (Lines, 0, line) : 0;
var runesWidth = line > 0 ? GetSumMaxCharWidth (Lines, 0, line, TabWidth) : 0;
x = bounds.Left + runesWidth;
} else {
x = bounds.Left;
@@ -1334,7 +1470,7 @@ namespace Terminal.Gui {
CursorPosition = _hotKeyPos > -1 ? _hotKeyPos : 0;
} else if (_textAlignment == TextAlignment.Centered) {
if (isVertical) {
var runesWidth = GetSumMaxCharWidth (Lines, line);
var runesWidth = GetSumMaxCharWidth (Lines, line, TabWidth);
x = bounds.Left + line + ((bounds.Width - runesWidth) / 2);
CursorPosition = (bounds.Width - runesWidth) / 2 + (_hotKeyPos > -1 ? _hotKeyPos : 0);
} else {
@@ -1375,46 +1511,94 @@ namespace Terminal.Gui {
var start = isVertical ? bounds.Top : bounds.Left;
var size = isVertical ? bounds.Height : bounds.Width;
var current = start + colOffset;
List<Point?> lastZeroWidthPos = null;
Rune rune = default;
Rune lastRuneUsed;
var zeroLengthCount = isVertical ? runes.Sum (r => r.GetColumns () == 0 ? 1 : 0) : 0;
for (var idx = (isVertical ? start - y : start - x) + colOffset; current < start + size; idx++) {
if (idx < 0 || x + current + colOffset < 0) {
current++;
continue;
} else if (!fillRemaining && idx > runes.Length - 1) {
break;
for (var idx = (isVertical ? start - y : start - x) + colOffset; current < start + size + zeroLengthCount; idx++) {
lastRuneUsed = rune;
if (lastZeroWidthPos == null) {
if (idx < 0 || x + current + colOffset < 0) {
current++;
continue;
} else if (!fillRemaining && idx > runes.Length - 1) {
break;
}
if ((!isVertical && current - start > maxBounds.Left + maxBounds.Width - bounds.X + colOffset)
|| (isVertical && idx > maxBounds.Top + maxBounds.Height - bounds.Y)) {
break;
}
}
if ((!isVertical && idx > maxBounds.Left + maxBounds.Width - bounds.X + colOffset)
|| (isVertical && idx > maxBounds.Top + maxBounds.Height - bounds.Y))
//if ((!isVertical && idx > maxBounds.Left + maxBounds.Width - bounds.X + colOffset)
// || (isVertical && idx > maxBounds.Top + maxBounds.Height - bounds.Y))
break;
// break;
var rune = (Rune)' ';
rune = (Rune)' ';
if (isVertical) {
Application.Driver?.Move (x, current);
if (idx >= 0 && idx < runes.Length) {
rune = runes [idx];
}
if (lastZeroWidthPos == null) {
driver?.Move (x, current);
} else {
var foundIdx = lastZeroWidthPos.IndexOf (p => 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 {
Application.Driver?.Move (current, y);
driver?.Move (current, y);
if (idx >= 0 && idx < runes.Length) {
rune = runes [idx];
}
}
var runeWidth = GetRuneWidth (rune, TabWidth);
if (HotKeyPos > -1 && idx == HotKeyPos) {
if ((isVertical && _textVerticalAlignment == VerticalTextAlignment.Justified) ||
(!isVertical && _textAlignment == TextAlignment.Justified)) {
CursorPosition = idx - start;
}
Application.Driver?.SetAttribute (hotColor);
Application.Driver?.AddRune (rune);
Application.Driver?.SetAttribute (normalColor);
driver?.SetAttribute (hotColor);
driver?.AddRune (rune);
driver?.SetAttribute (normalColor);
} else {
Application.Driver?.AddRune (rune);
if (isVertical) {
if (runeWidth == 0) {
if (lastZeroWidthPos == null) {
lastZeroWidthPos = new List<Point?> ();
}
var foundIdx = lastZeroWidthPos.IndexOf (p => p.Value.Y == current);
if (foundIdx == -1) {
current--;
lastZeroWidthPos.Add ((new Point (x + 1, current)));
}
driver?.Move (x + 1, current);
}
}
driver?.AddRune (rune);
}
// BUGBUG: I think this is a bug. If rune is a combining mark current should not be incremented.
var runeWidth = rune.GetColumns (); //Math.Max (rune.GetColumns (), 1);
if (isVertical) {
current++;
if (runeWidth > 0) {
current++;
}
} else {
current += runeWidth;
}

View File

@@ -50,7 +50,7 @@ namespace Terminal.Gui {
_frame = new Rect (value.X, value.Y, Math.Max (value.Width, 0), Math.Max (value.Height, 0));
if (IsInitialized || LayoutStyle == LayoutStyle.Absolute) {
LayoutFrames ();
TextFormatter.Size = GetSizeNeededForTextAndHotKey ();
TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey ();
SetNeedsLayout ();
SetNeedsDisplay ();
}
@@ -213,9 +213,11 @@ namespace Terminal.Gui {
#if DEBUG
if (LayoutStyle == LayoutStyle.Computed && !IsInitialized) {
Debug.WriteLine ($"WARNING: Bounds is being accessed before the View has been initialized. This is likely a bug. View: {this}");
Debug.WriteLine ($"The Frame is set before the View has been initialized. So it isn't a bug.Is by design.");
}
#endif // DEBUG
var frameRelativeBounds = Padding?.Thickness.GetInside (Padding.Frame) ?? new Rect (default, Frame.Size);
//var frameRelativeBounds = Padding?.Thickness.GetInside (Padding.Frame) ?? new Rect (default, Frame.Size);
var frameRelativeBounds = FrameGetInsideBounds ();
return new Rect (default, frameRelativeBounds.Size);
}
set {
@@ -229,6 +231,16 @@ namespace Terminal.Gui {
}
}
private Rect FrameGetInsideBounds ()
{
if (Margin == null || Border == null || Padding == null) {
return new Rect (default, Frame.Size);
}
var width = Math.Max (0, Frame.Size.Width - Margin.Thickness.Horizontal - Border.Thickness.Horizontal - Padding.Thickness.Horizontal);
var height = Math.Max (0, Frame.Size.Height - Margin.Thickness.Vertical - Border.Thickness.Vertical - Padding.Thickness.Vertical);
return new Rect (Point.Empty, new Size (width, height));
}
// Diagnostics to highlight when X or Y is read before the view has been initialized
private Pos VerifyIsIntialized (Pos pos)
{
@@ -460,7 +472,7 @@ namespace Terminal.Gui {
if (IsInitialized || LayoutStyle == LayoutStyle.Absolute) {
SetMinWidthHeight ();
LayoutFrames ();
TextFormatter.Size = GetSizeNeededForTextAndHotKey ();
TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey ();
SetNeedsLayout ();
SetNeedsDisplay ();
}
@@ -657,7 +669,7 @@ namespace Terminal.Gui {
Frame = r;
// BUGBUG: Why is this AFTER setting Frame? Seems duplicative.
if (!SetMinWidthHeight ()) {
TextFormatter.Size = GetSizeNeededForTextAndHotKey ();
TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey ();
}
}
}
@@ -878,7 +890,7 @@ namespace Terminal.Gui {
var oldBounds = Bounds;
OnLayoutStarted (new LayoutEventArgs () { OldBounds = oldBounds });
TextFormatter.Size = GetSizeNeededForTextAndHotKey ();
TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey ();
// Sort out the dependencies of the X, Y, Width, Height properties
var nodes = new HashSet<View> ();
@@ -958,7 +970,7 @@ namespace Terminal.Gui {
}
}
// BUGBUG: This call may be redundant
TextFormatter.Size = GetSizeNeededForTextAndHotKey ();
TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey ();
return aSize;
}

View File

@@ -136,7 +136,7 @@ namespace Terminal.Gui {
} else {
SetMinWidthHeight ();
}
TextFormatter.Size = GetSizeNeededForTextAndHotKey ();
TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey ();
SetNeedsDisplay ();
}
@@ -159,7 +159,7 @@ namespace Terminal.Gui {
} else {
return TextFormatter.IsVerticalDirection (TextDirection) &&
TextFormatter.Text?.Contains ((char)HotKeySpecifier.Value) == true
? Math.Max (HotKeySpecifier.GetColumns(), 0) : 0;
? Math.Max (HotKeySpecifier.GetColumns (), 0) : 0;
}
}
@@ -177,7 +177,7 @@ namespace Terminal.Gui {
/// Gets the dimensions required for <see cref="Text"/> accounting for a <see cref="Terminal.Gui.TextFormatter.HotKeySpecifier"/> .
/// </summary>
/// <returns></returns>
public Size GetSizeNeededForTextAndHotKey ()
public Size GetTextFormatterSizeNeededForTextAndHotKey ()
{
if (string.IsNullOrEmpty (TextFormatter.Text)) {
@@ -188,9 +188,8 @@ namespace Terminal.Gui {
// BUGBUG: This IGNORES what Text is set to, using on only the current View size. This doesn't seem to make sense.
// BUGBUG: This uses Frame; in v2 it should be Bounds
return new Size (_frame.Size.Width + GetHotKeySpecifierLength (),
_frame.Size.Height + GetHotKeySpecifierLength (false));
return new Size (Bounds.Size.Width + GetHotKeySpecifierLength (),
Bounds.Size.Height + GetHotKeySpecifierLength (false));
}
}
}

View File

@@ -1,7 +1,5 @@
using System.Text;
using System;
using System;
using System.Collections.Generic;
using Terminal.Gui;
using static Terminal.Gui.ConfigurationManager;
namespace Terminal.Gui {
@@ -262,7 +260,7 @@ namespace Terminal.Gui {
}
}
Dialog d;
d = new Dialog (buttonList.ToArray ()) {
Title = title,
@@ -273,12 +271,12 @@ namespace Terminal.Gui {
if (width != 0) {
d.Width = width;
}
}
if (height != 0) {
d.Height = height;
}
if (useErrorColors) {
d.ColorScheme = Colors.Error;
} else {
@@ -286,7 +284,7 @@ namespace Terminal.Gui {
}
var messageLabel = new Label () {
AutoSize = false,
AutoSize = wrapMessage ? false : true,
Text = message,
TextAlignment = TextAlignment.Centered,
X = 0,
@@ -294,16 +292,19 @@ namespace Terminal.Gui {
Width = Dim.Fill (0),
Height = Dim.Fill (1)
};
messageLabel.TextFormatter.WordWrap = wrapMessage; // BUGBUG: This does nothing as it's not implemented by TextFormatter!
messageLabel.TextFormatter.WordWrap = wrapMessage;
messageLabel.TextFormatter.MultiLine = wrapMessage ? false : true;
d.Add (messageLabel);
d.Loaded += (s, e) => {
if (width != 0 || height != 0) {
return;
}
// TODO: replace with Dim.Fit when implemented
var maxBounds = d.SuperView?.Bounds ?? Application.Top.Bounds;
messageLabel.TextFormatter.Size = new Size (maxBounds.Size.Width - d.GetFramesThickness ().Horizontal, maxBounds.Size.Height - d.GetFramesThickness ().Vertical);
if (wrapMessage) {
messageLabel.TextFormatter.Size = new Size (maxBounds.Size.Width - d.GetFramesThickness ().Horizontal, maxBounds.Size.Height - d.GetFramesThickness ().Vertical);
}
var msg = messageLabel.TextFormatter.Format ();
var messageSize = messageLabel.TextFormatter.GetFormattedSize ();
@@ -314,7 +315,8 @@ namespace Terminal.Gui {
d.Width = newWidth;
}
// Ensure height fits the text + vspace + buttons
d.Height = Math.Max (height, messageSize.Height + 2 + d.GetFramesThickness ().Vertical);
var lastLine = messageLabel.TextFormatter.Lines [^1];
d.Height = Math.Max (height, messageSize.Height + (lastLine.EndsWith ("\r\n") || lastLine.EndsWith ('\n') ? 1 : 2) + d.GetFramesThickness ().Vertical);
d.SetRelativeLayout (d.SuperView?.Frame ?? Application.Top.Frame);
};

View File

@@ -439,40 +439,40 @@ namespace Terminal.Gui.DialogTests {
iterations++;
if (iterations == 0) {
MessageBox.Query (string.Empty, new string ('f', 50), defaultButton: 0, wrapMessage: true, "btn");
MessageBox.Query (string.Empty, new string ('f', 50), defaultButton: 0, wrapMessage: false, "btn");
Application.RequestStop ();
} else if (iterations == 1) {
Application.Refresh ();
TestHelpers.AssertDriverContentsWithFrameAre (@$"
╔══════════════════╗
┌────────────────┐
║│ffffffffffffffff│║
║│ffffffffffffffff│║
║│ffffffffffffffff│║
║│ ff │║
║│ │║
{btn} │
└────────────────┘
────────────────────
ffffffffffffffffffff
⟦► btn ◄⟧
────────────────────
╚══════════════════╝", output);
Application.RequestStop ();
// Really long text
MessageBox.Query (string.Empty, new string ('f', 500), defaultButton: 0, wrapMessage: true, "btn");
MessageBox.Query (string.Empty, new string ('f', 500), defaultButton: 0, wrapMessage: false, "btn");
} else if (iterations == 2) {
Application.Refresh ();
TestHelpers.AssertDriverContentsWithFrameAre (@$"
┌────────────────┐
│ffffffffffffffff│
║│ffffffffffffffff│║
║│ffffffffffffffff│║
║│ffffffffffffffff│║
║│ffffffffffffffff│║
║│ffffffffffffffff│║
│ffffffffffffffff│
{btn} │
└────────────────┘╝", output);
══════════════════
────────────────────
ffffffffffffffffffff
⟦► btn ◄⟧
────────────────────
══════════════════╝", output);
Application.RequestStop ();
}
@@ -505,14 +505,14 @@ namespace Terminal.Gui.DialogTests {
Application.Refresh ();
TestHelpers.AssertDriverContentsWithFrameAre (@$"
╔══════════════════╗
┌──────────────┐
║ │ff ff ff ff ff│ ║
║ │ff ff ff ff ff│ ║
║ │ff ff ff ff ff│ ║
║ │ ff ff │ ║
║ │ │ ║
{btn} │
└──────────────┘
────────────────────
ff ff ff ff ff ff ff
⟦► btn ◄⟧
────────────────────
╚══════════════════╝", output);
Application.RequestStop ();
@@ -521,16 +521,16 @@ namespace Terminal.Gui.DialogTests {
} else if (iterations == 2) {
Application.Refresh ();
TestHelpers.AssertDriverContentsWithFrameAre (@$"
┌────────────────┐
│ffffffffffffffff│
║│ffffffffffffffff│║
║│ffffffffffffffff│║
║│ffffffffffffffff│║
║│ffffffffffffffff│║
║│ffffffffffffffff│║
│ffffffffffffffff│
{btn} │
└────────────────┘╝", output);
══════════════════
────────────────────
ffffffffffffffffffff
⟦► btn ◄⟧
────────────────────
══════════════════╝", output);
Application.RequestStop ();
}
};
@@ -539,15 +539,15 @@ namespace Terminal.Gui.DialogTests {
}
[Theory, AutoInitShutdown]
[InlineData (" ", true)]
[InlineData (" ", false)]
[InlineData ("", true)]
[InlineData ("", false)]
[InlineData ("\n", true)]
[InlineData ("\n", false)]
[InlineData (" \n", true)]
[InlineData (" \n", false)]
public void Message_Empty_Or_A_NewLline_WrapMessagge_True_Or_False (string message, bool wrapMessage)
[InlineData (" ", true, 1)]
[InlineData (" ", false, 1)]
[InlineData ("", true, 1)]
[InlineData ("", false, 1)]
[InlineData ("\n", true, 1)]
[InlineData ("\n", false, 1)]
[InlineData (" \n", true, 1)]
[InlineData (" \n", false, 2)]
public void Message_Empty_Or_A_NewLline_WrapMessagge_True_Or_False (string message, bool wrapMessage, int linesLength)
{
var iterations = -1;
Application.Begin (Application.Top);
@@ -561,12 +561,22 @@ namespace Terminal.Gui.DialogTests {
Application.RequestStop ();
} else if (iterations == 1) {
Application.Refresh ();
TestHelpers.AssertDriverContentsWithFrameAre (@$"
if (linesLength == 1) {
TestHelpers.AssertDriverContentsWithFrameAre (@$"
┌──────────────────────────────────────────────┐
│ │
│ │
│ {CM.Glyphs.LeftBracket}{CM.Glyphs.LeftDefaultIndicator} ok {CM.Glyphs.RightDefaultIndicator}{CM.Glyphs.RightBracket} │
└──────────────────────────────────────────────┘", output);
} else {
TestHelpers.AssertDriverContentsWithFrameAre (@$"
┌──────────────────────────────────────────────┐
│ │
│ │
│ │
│ {CM.Glyphs.LeftBracket}{CM.Glyphs.LeftDefaultIndicator} ok {CM.Glyphs.RightDefaultIndicator}{CM.Glyphs.RightBracket} │
└──────────────────────────────────────────────┘", output);
}
Application.RequestStop ();
}
};
@@ -574,4 +584,4 @@ namespace Terminal.Gui.DialogTests {
Application.Run ();
}
}
}
}

View File

@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Terminal.Gui;
using Xunit;
using Xunit.Abstractions;
@@ -65,13 +64,130 @@ namespace Terminal.Gui.TextTests {
Assert.NotEmpty (tf.Lines);
}
[Fact]
public void TestSize_TextChange ()
[Theory]
[InlineData (TextDirection.LeftRight_TopBottom, false)]
[InlineData (TextDirection.LeftRight_TopBottom, true)]
[InlineData (TextDirection.TopBottom_LeftRight, false)]
[InlineData (TextDirection.TopBottom_LeftRight, true)]
public void TestSize_TextChange (TextDirection textDirection, bool autoSize)
{
var tf = new TextFormatter () { Text = "你" };
var tf = new TextFormatter () { Direction = textDirection, Text = "你", AutoSize = autoSize };
Assert.Equal (2, tf.Size.Width);
Assert.Equal (1, tf.Size.Height);
tf.Text = "你你";
Assert.Equal (4, tf.Size.Width);
if (autoSize) {
if (textDirection == TextDirection.LeftRight_TopBottom) {
Assert.Equal (4, tf.Size.Width);
Assert.Equal (1, tf.Size.Height);
} else {
Assert.Equal (2, tf.Size.Width);
Assert.Equal (2, tf.Size.Height);
}
} else {
Assert.Equal (2, tf.Size.Width);
Assert.Equal (1, tf.Size.Height);
}
}
[Theory]
[InlineData (TextDirection.LeftRight_TopBottom)]
[InlineData (TextDirection.TopBottom_LeftRight)]
public void TestSize_AutoSizeChange (TextDirection textDirection)
{
var tf = new TextFormatter () { Direction = textDirection, Text = "你你" };
if (textDirection == TextDirection.LeftRight_TopBottom) {
Assert.Equal (4, tf.Size.Width);
Assert.Equal (1, tf.Size.Height);
} else {
Assert.Equal (2, tf.Size.Width);
Assert.Equal (2, tf.Size.Height);
}
Assert.False (tf.AutoSize);
tf.Size = new Size (1, 1);
Assert.Equal (1, tf.Size.Width);
Assert.Equal (1, tf.Size.Height);
tf.AutoSize = true;
if (textDirection == TextDirection.LeftRight_TopBottom) {
Assert.Equal (4, tf.Size.Width);
Assert.Equal (1, tf.Size.Height);
} else {
Assert.Equal (2, tf.Size.Width);
Assert.Equal (2, tf.Size.Height);
}
}
[Theory]
[InlineData (TextDirection.LeftRight_TopBottom, false)]
[InlineData (TextDirection.LeftRight_TopBottom, true)]
[InlineData (TextDirection.TopBottom_LeftRight, false)]
[InlineData (TextDirection.TopBottom_LeftRight, true)]
public void TestSize_SizeChange_AutoSize_True_Or_False (TextDirection textDirection, bool autoSize)
{
var tf = new TextFormatter () { Direction = textDirection, Text = "你你", AutoSize = autoSize };
if (textDirection == TextDirection.LeftRight_TopBottom) {
Assert.Equal (4, tf.Size.Width);
Assert.Equal (1, tf.Size.Height);
} else {
Assert.Equal (2, tf.Size.Width);
Assert.Equal (2, tf.Size.Height);
}
tf.Size = new Size (1, 1);
if (autoSize) {
if (textDirection == TextDirection.LeftRight_TopBottom) {
Assert.Equal (4, tf.Size.Width);
Assert.Equal (1, tf.Size.Height);
} else {
Assert.Equal (2, tf.Size.Width);
Assert.Equal (2, tf.Size.Height);
}
} else {
Assert.Equal (1, tf.Size.Width);
Assert.Equal (1, tf.Size.Height);
}
}
[Theory]
[InlineData (TextAlignment.Left, false)]
[InlineData (TextAlignment.Centered, true)]
[InlineData (TextAlignment.Right, false)]
[InlineData (TextAlignment.Justified, true)]
public void TestSize_SizeChange_AutoSize_True_Or_False_Horizontal (TextAlignment textAlignment, bool autoSize)
{
var tf = new TextFormatter () { Direction = TextDirection.LeftRight_TopBottom, Text = "你你", Alignment = textAlignment, AutoSize = autoSize };
Assert.Equal (4, tf.Size.Width);
Assert.Equal (1, tf.Size.Height);
tf.Size = new Size (1, 1);
if (autoSize && textAlignment != TextAlignment.Justified) {
Assert.Equal (4, tf.Size.Width);
Assert.Equal (1, tf.Size.Height);
} else {
Assert.Equal (1, tf.Size.Width);
Assert.Equal (1, tf.Size.Height);
}
}
[Theory]
[InlineData (VerticalTextAlignment.Top, false)]
[InlineData (VerticalTextAlignment.Middle, true)]
[InlineData (VerticalTextAlignment.Bottom, false)]
[InlineData (VerticalTextAlignment.Justified, true)]
public void TestSize_SizeChange_AutoSize_True_Or_False_Vertical (VerticalTextAlignment textAlignment, bool autoSize)
{
var tf = new TextFormatter () { Direction = TextDirection.TopBottom_LeftRight, Text = "你你", VerticalAlignment = textAlignment, AutoSize = autoSize };
Assert.Equal (2, tf.Size.Width);
Assert.Equal (2, tf.Size.Height);
tf.Size = new Size (1, 1);
if (autoSize && textAlignment != VerticalTextAlignment.Justified) {
Assert.Equal (2, tf.Size.Width);
Assert.Equal (2, tf.Size.Height);
} else {
Assert.Equal (1, tf.Size.Width);
Assert.Equal (1, tf.Size.Height);
}
}
[Fact]
@@ -361,8 +477,8 @@ namespace Terminal.Gui.TextTests {
[InlineData ("A sentence has words.", "A sentence has words.", int.MaxValue)] // should fit
[InlineData ("A sentence has words.", "A sentence has words", 20)] // Should not fit
[InlineData ("A sentence has words.", "A sentence", 10)] // Should not fit
[InlineData ("A\tsentence\thas\twords.", "A\tsentence\thas\twords.", int.MaxValue)]
[InlineData ("A\tsentence\thas\twords.", "A\tsentence", 10)]
[InlineData ("A\tsentence\thas\twords.", "A sentence has words.", int.MaxValue)]
[InlineData ("A\tsentence\thas\twords.", "A sentence", 10)]
[InlineData ("line1\nline2\nline3long!", "line1\nline2\nline3long!", int.MaxValue)]
[InlineData ("line1\nline2\nline3long!", "line1\nline", 10)]
[InlineData (" ~  s  gui.cs   master ↑10", " ~  s  ", 10)] // Unicode
@@ -372,10 +488,12 @@ namespace Terminal.Gui.TextTests {
public void ClipAndJustify_Valid_Left (string text, string justifiedText, int maxWidth)
{
var align = TextAlignment.Left;
var textDirection = TextDirection.LeftRight_BottomTop;
var tabWidth = 1;
Assert.Equal (justifiedText, TextFormatter.ClipAndJustify (text, maxWidth, align));
Assert.Equal (justifiedText, TextFormatter.ClipAndJustify (text, maxWidth, align, textDirection, tabWidth));
var expectedClippedWidth = Math.Min (justifiedText.GetRuneCount (), maxWidth);
Assert.Equal (justifiedText, TextFormatter.ClipAndJustify (text, maxWidth, align));
Assert.Equal (justifiedText, TextFormatter.ClipAndJustify (text, maxWidth, align, textDirection, tabWidth));
Assert.True (justifiedText.GetRuneCount () <= maxWidth);
Assert.True (justifiedText.GetColumns () <= maxWidth);
Assert.Equal (expectedClippedWidth, justifiedText.GetRuneCount ());
@@ -393,8 +511,8 @@ namespace Terminal.Gui.TextTests {
[InlineData ("A sentence has words.", "A sentence has words.", int.MaxValue)] // should fit
[InlineData ("A sentence has words.", "A sentence has words", 20)] // Should not fit
[InlineData ("A sentence has words.", "A sentence", 10)] // Should not fit
[InlineData ("A\tsentence\thas\twords.", "A\tsentence\thas\twords.", int.MaxValue)]
[InlineData ("A\tsentence\thas\twords.", "A\tsentence", 10)]
[InlineData ("A\tsentence\thas\twords.", "A sentence has words.", int.MaxValue)]
[InlineData ("A\tsentence\thas\twords.", "A sentence", 10)]
[InlineData ("line1\nline2\nline3long!", "line1\nline2\nline3long!", int.MaxValue)]
[InlineData ("line1\nline2\nline3long!", "line1\nline", 10)]
[InlineData (" ~  s  gui.cs   master ↑10", " ~  s  ", 10)] // Unicode
@@ -404,10 +522,12 @@ namespace Terminal.Gui.TextTests {
public void ClipAndJustify_Valid_Right (string text, string justifiedText, int maxWidth)
{
var align = TextAlignment.Right;
var textDirection = TextDirection.LeftRight_BottomTop;
var tabWidth = 1;
Assert.Equal (justifiedText, TextFormatter.ClipAndJustify (text, maxWidth, align));
Assert.Equal (justifiedText, TextFormatter.ClipAndJustify (text, maxWidth, align, textDirection, tabWidth));
var expectedClippedWidth = Math.Min (justifiedText.GetRuneCount (), maxWidth);
Assert.Equal (justifiedText, TextFormatter.ClipAndJustify (text, maxWidth, align));
Assert.Equal (justifiedText, TextFormatter.ClipAndJustify (text, maxWidth, align, textDirection, tabWidth));
Assert.True (justifiedText.GetRuneCount () <= maxWidth);
Assert.True (justifiedText.GetColumns () <= maxWidth);
Assert.Equal (expectedClippedWidth, justifiedText.GetRuneCount ());
@@ -425,8 +545,8 @@ namespace Terminal.Gui.TextTests {
[InlineData ("A sentence has words.", "A sentence has words.", int.MaxValue)] // should fit
[InlineData ("A sentence has words.", "A sentence has words", 20)] // Should not fit
[InlineData ("A sentence has words.", "A sentence", 10)] // Should not fit
[InlineData ("A\tsentence\thas\twords.", "A\tsentence\thas\twords.", int.MaxValue)]
[InlineData ("A\tsentence\thas\twords.", "A\tsentence", 10)]
[InlineData ("A\tsentence\thas\twords.", "A sentence has words.", int.MaxValue)]
[InlineData ("A\tsentence\thas\twords.", "A sentence", 10)]
[InlineData ("line1\nline2\nline3long!", "line1\nline2\nline3long!", int.MaxValue)]
[InlineData ("line1\nline2\nline3long!", "line1\nline", 10)]
[InlineData (" ~  s  gui.cs   master ↑10", " ~  s  ", 10)] // Unicode
@@ -436,10 +556,12 @@ namespace Terminal.Gui.TextTests {
public void ClipAndJustify_Valid_Centered (string text, string justifiedText, int maxWidth)
{
var align = TextAlignment.Centered;
var textDirection = TextDirection.LeftRight_TopBottom;
var tabWidth = 1;
Assert.Equal (justifiedText, TextFormatter.ClipAndJustify (text, maxWidth, align));
Assert.Equal (justifiedText, TextFormatter.ClipAndJustify (text, maxWidth, align, textDirection, tabWidth));
var expectedClippedWidth = Math.Min (justifiedText.GetRuneCount (), maxWidth);
Assert.Equal (justifiedText, TextFormatter.ClipAndJustify (text, maxWidth, align));
Assert.Equal (justifiedText, TextFormatter.ClipAndJustify (text, maxWidth, align, textDirection, tabWidth));
Assert.True (justifiedText.GetRuneCount () <= maxWidth);
Assert.True (justifiedText.GetColumns () <= maxWidth);
Assert.Equal (expectedClippedWidth, justifiedText.GetRuneCount ());
@@ -451,15 +573,16 @@ namespace Terminal.Gui.TextTests {
[Theory]
[InlineData ("test", "", 0)]
[InlineData ("test", "te", 2)]
[InlineData ("test", "test", int.MaxValue)]
[InlineData ("test", "test", int.MaxValue)] // This doesn't throw because it only create a word with length 1
[InlineData ("A sentence has words.", "A sentence has words.", 22)] // should fit
[InlineData ("A sentence has words.", "A sentence has words.", 21)] // should fit
[InlineData ("A sentence has words.", "A sentence has words.", 500)] // should fit
[InlineData ("A sentence has words.", "A sentence has words", 20)] // Should not fit
[InlineData ("A sentence has words.", "A sentence", 10)] // Should not fit
[InlineData ("A\tsentence\thas\twords.", "A\tsentence\thas\twords.", int.MaxValue)]
[InlineData ("A\tsentence\thas\twords.", "A\tsentence", 10)]
[InlineData ("line1\nline2\nline3long!", "line1\nline2\nline3long!", int.MaxValue)]
// Now throw System.OutOfMemoryException. See https://stackoverflow.com/questions/20672920/maxcapacity-of-stringbuilder
//[InlineData ("A\tsentence\thas\twords.", "A sentence has words.", int.MaxValue)]
[InlineData ("A\tsentence\thas\twords.", "A sentence", 10)]
[InlineData ("line1\nline2\nline3long!", "line1\nline2\nline3long!", int.MaxValue)] // This doesn't throw because it only create a line with length 1
[InlineData ("line1\nline2\nline3long!", "line1\nline", 10)]
[InlineData (" ~  s  gui.cs   master ↑10", " ~  s  ", 10)] // Unicode
[InlineData ("Ð ÑÐ", "Ð ÑÐ", 5)] // should fit
@@ -468,10 +591,12 @@ namespace Terminal.Gui.TextTests {
public void ClipAndJustify_Valid_Justified (string text, string justifiedText, int maxWidth)
{
var align = TextAlignment.Justified;
var textDirection = TextDirection.LeftRight_TopBottom;
var tabWidth = 1;
Assert.Equal (justifiedText, TextFormatter.ClipAndJustify (text, maxWidth, align));
Assert.Equal (justifiedText, TextFormatter.ClipAndJustify (text, maxWidth, align, textDirection, tabWidth));
var expectedClippedWidth = Math.Min (justifiedText.GetRuneCount (), maxWidth);
Assert.Equal (justifiedText, TextFormatter.ClipAndJustify (text, maxWidth, align));
Assert.Equal (justifiedText, TextFormatter.ClipAndJustify (text, maxWidth, align, textDirection, tabWidth));
Assert.True (justifiedText.GetRuneCount () <= maxWidth);
Assert.True (justifiedText.GetColumns () <= maxWidth);
Assert.Equal (expectedClippedWidth, justifiedText.GetRuneCount ());
@@ -620,21 +745,24 @@ namespace Terminal.Gui.TextTests {
[Theory]
[InlineData ("กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำ", 51, 0, new string [] { "กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำ" })]
[InlineData ("กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำ", 50, -1, new string [] { "กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัา", "ำ" })]
[InlineData ("กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำ", 50, -1, new string [] { "กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำ" })]
[InlineData ("กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำ", 46, -5, new string [] { "กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮ", "ฯะัาำ" })]
[InlineData ("กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำ", 26, -25, new string [] { "กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบ", "ปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำ" })]
[InlineData ("กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำ", 17, -34, new string [] { "กขฃคฅฆงจฉชซฌญฎฏฐฑ", "ฒณดตถทธนบปผฝพฟภมย", "รฤลฦวศษสหฬอฮฯะัาำ" })]
[InlineData ("กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำ", 13, -38, new string [] { "กขฃคฅฆงจฉชซฌญ", "ฎฏฐฑฒณดตถทธนบ", "ปผฝพฟภมยรฤลฦว", "ศษสหฬอฮฯะัาำ" })]
[InlineData ("กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำ", 1, -50, new string [] { "ก", "ข", "ฃ", "ค", "ฅ", "ฆ", "ง", "จ", "ฉ", "ช", "ซ", "ฌ", "ญ", "ฎ", "ฏ", "ฐ", "ฑ", "ฒ", "ณ", "ด", "ต", "ถ", "ท", "ธ", "น", "บ", "ป", "ผ", "ฝ", "พ", "ฟ", "ภ", "ม", "ย", "ร", "ฤ", "ล", "ฦ", "ว", "ศ", "ษ", "ส", "ห", "ฬ", "อ", "ฮ", "ฯ", "ะ", "ั", "า", "ำ" })]
[InlineData ("กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำ", 1, -50, new string [] { "ก", "ข", "ฃ", "ค", "ฅ", "ฆ", "ง", "จ", "ฉ", "ช", "ซ", "ฌ", "ญ", "ฎ", "ฏ", "ฐ", "ฑ", "ฒ", "ณ", "ด", "ต", "ถ", "ท", "ธ", "น", "บ", "ป", "ผ", "ฝ", "พ", "ฟ", "ภ", "ม", "ย", "ร", "ฤ", "ล", "ฦ", "ว", "ศ", "ษ", "ส", "ห", "ฬ", "อ", "ฮ", "ฯ", "ะั", "า", "ำ" })]
public void WordWrap_Unicode_SingleWordLine (string text, int maxWidth, int widthOffset, IEnumerable<string> resultLines)
{
List<string> wrappedLines;
var zeroWidth = text.EnumerateRunes ().Where (r => r.GetColumns () == 0);
Assert.Single (zeroWidth);
Assert.Equal ('ั', zeroWidth.ElementAt (0).Value);
Assert.Equal (maxWidth, text.GetRuneCount () + widthOffset);
var expectedClippedWidth = Math.Min (text.GetRuneCount (), maxWidth);
wrappedLines = TextFormatter.WordWrapText (text, maxWidth);
Assert.Equal (wrappedLines.Count, resultLines.Count ());
Assert.True (expectedClippedWidth >= (wrappedLines.Count > 0 ? wrappedLines.Max (l => l.GetRuneCount ()) : 0));
Assert.True (expectedClippedWidth >= (wrappedLines.Count > 0 ? wrappedLines.Max (l => l.GetRuneCount () + zeroWidth.Count () - 1 + widthOffset) : 0));
Assert.True (expectedClippedWidth >= (wrappedLines.Count > 0 ? wrappedLines.Max (l => l.GetColumns ()) : 0));
Assert.Equal (resultLines, wrappedLines);
}
@@ -793,7 +921,7 @@ namespace Terminal.Gui.TextTests {
[InlineData ("文に は言葉 があり ます。", 14, 0, new string [] { "文に は言葉", "があり ます。" })]
[InlineData ("文に は言葉 があり ます。", 3, -11, new string [] { "文", "に", "は", "言", "葉", "が", "あ", "り", "ま", "す", "。" })]
[InlineData ("文に は言葉 があり ます。", 2, -12, new string [] { "文", "に", "は", "言", "葉", "が", "あ", "り", "ま", "す", "。" })]
[InlineData ("文に は言葉 があり ます。", 1, -13, new string [] { })]
[InlineData ("文に は言葉 があり ます。", 1, -13, new string [] { " ", " ", " " })] // Just Spaces; should result in a single space for each line
public void WordWrap_PreserveTrailingSpaces_False_Wide_Runes (string text, int maxWidth, int widthOffset, IEnumerable<string> resultLines)
{
List<string> wrappedLines;
@@ -1071,7 +1199,7 @@ namespace Terminal.Gui.TextTests {
[InlineData ("A sentence has words.\r\nLine 2.", 29, -1, TextAlignment.Left, false, 1, false, 1)]
[InlineData ("A sentence has words.\r\nLine 2.", 30, 0, TextAlignment.Left, false, 1, false)]
[InlineData ("A sentence has words.\r\nLine 2.", 31, 1, TextAlignment.Left, false, 1, false)]
public void Reformat_NoWordrap_NewLines (string text, int maxWidth, int widthOffset, TextAlignment textAlignment, bool wrap, int linesCount, bool stringEmpty, int clipWidthOffset = 0)
public void Reformat_NoWordrap_NewLines_MultiLine_False (string text, int maxWidth, int widthOffset, TextAlignment textAlignment, bool wrap, int linesCount, bool stringEmpty, int clipWidthOffset = 0)
{
Assert.Equal (maxWidth, text.GetRuneCount () + widthOffset);
var expectedClippedWidth = Math.Min (text.GetRuneCount (), maxWidth) + clipWidthOffset;
@@ -1092,6 +1220,64 @@ namespace Terminal.Gui.TextTests {
}
}
[Theory]
[InlineData ("A sentence has words.\nLine 2.", 0, -29, TextAlignment.Left, false, 1, true, new string [] { "" })]
[InlineData ("A sentence has words.\nLine 2.", 1, -28, TextAlignment.Left, false, 2, false, new string [] { "A", "L" })]
[InlineData ("A sentence has words.\nLine 2.", 5, -24, TextAlignment.Left, false, 2, false, new string [] { "A sen", "Line " })]
[InlineData ("A sentence has words.\nLine 2.", 28, -1, TextAlignment.Left, false, 2, false, new string [] { "A sentence has words.", "Line 2." })]
//// no clip
[InlineData ("A sentence has words.\nLine 2.", 29, 0, TextAlignment.Left, false, 2, false, new string [] { "A sentence has words.", "Line 2." })]
[InlineData ("A sentence has words.\nLine 2.", 30, 1, TextAlignment.Left, false, 2, false, new string [] { "A sentence has words.", "Line 2." })]
[InlineData ("A sentence has words.\r\nLine 2.", 0, -30, TextAlignment.Left, false, 1, true, new string [] { "" })]
[InlineData ("A sentence has words.\r\nLine 2.", 1, -29, TextAlignment.Left, false, 2, false, new string [] { "A", "L" })]
[InlineData ("A sentence has words.\r\nLine 2.", 5, -25, TextAlignment.Left, false, 2, false, new string [] { "A sen", "Line " })]
[InlineData ("A sentence has words.\r\nLine 2.", 29, -1, TextAlignment.Left, false, 2, false, new string [] { "A sentence has words.", "Line 2." })]
[InlineData ("A sentence has words.\r\nLine 2.", 30, 0, TextAlignment.Left, false, 2, false, new string [] { "A sentence has words.", "Line 2." })]
[InlineData ("A sentence has words.\r\nLine 2.", 31, 1, TextAlignment.Left, false, 2, false, new string [] { "A sentence has words.", "Line 2." })]
public void Reformat_NoWordrap_NewLines_MultiLine_True (string text, int maxWidth, int widthOffset, TextAlignment textAlignment, bool wrap, int linesCount, bool stringEmpty, IEnumerable<string> resultLines)
{
Assert.Equal (maxWidth, text.GetRuneCount () + widthOffset);
var list = TextFormatter.Format (text, maxWidth, textAlignment, wrap, false, 0, TextDirection.LeftRight_TopBottom, true);
Assert.NotEmpty (list);
Assert.True (list.Count == linesCount);
if (stringEmpty) {
Assert.Equal (string.Empty, list [0]);
} else {
Assert.NotEqual (string.Empty, list [0]);
}
Assert.Equal (list, resultLines);
}
[Theory]
[InlineData ("A sentence has words.\nLine 2.", 0, -29, TextAlignment.Left, false, 1, true, new string [] { "" })]
[InlineData ("A sentence has words.\nLine 2.", 1, -28, TextAlignment.Left, false, 2, false, new string [] { "A", "L" })]
[InlineData ("A sentence has words.\nLine 2.", 5, -24, TextAlignment.Left, false, 2, false, new string [] { "A sen", "Line " })]
[InlineData ("A sentence has words.\nLine 2.", 28, -1, TextAlignment.Left, false, 2, false, new string [] { "A sentence has words.", "Line 2." })]
//// no clip
[InlineData ("A sentence has words.\nLine 2.", 29, 0, TextAlignment.Left, false, 2, false, new string [] { "A sentence has words.", "Line 2." })]
[InlineData ("A sentence has words.\nLine 2.", 30, 1, TextAlignment.Left, false, 2, false, new string [] { "A sentence has words.", "Line 2." })]
[InlineData ("A sentence has words.\r\nLine 2.", 0, -30, TextAlignment.Left, false, 1, true, new string [] { "" })]
[InlineData ("A sentence has words.\r\nLine 2.", 1, -29, TextAlignment.Left, false, 2, false, new string [] { "A", "L" })]
[InlineData ("A sentence has words.\r\nLine 2.", 5, -25, TextAlignment.Left, false, 2, false, new string [] { "A sen", "Line " })]
[InlineData ("A sentence has words.\r\nLine 2.", 29, -1, TextAlignment.Left, false, 2, false, new string [] { "A sentence has words.", "Line 2." })]
[InlineData ("A sentence has words.\r\nLine 2.", 30, 0, TextAlignment.Left, false, 2, false, new string [] { "A sentence has words.", "Line 2." })]
[InlineData ("A sentence has words.\r\nLine 2.", 31, 1, TextAlignment.Left, false, 2, false, new string [] { "A sentence has words.", "Line 2." })]
public void Reformat_NoWordrap_NewLines_MultiLine_True_Vertical (string text, int maxWidth, int widthOffset, TextAlignment textAlignment, bool wrap, int linesCount, bool stringEmpty, IEnumerable<string> resultLines)
{
Assert.Equal (maxWidth, text.GetRuneCount () + widthOffset);
var list = TextFormatter.Format (text, maxWidth, textAlignment, wrap, false, 0, TextDirection.TopBottom_LeftRight, true);
Assert.NotEmpty (list);
Assert.True (list.Count == linesCount);
if (stringEmpty) {
Assert.Equal (string.Empty, list [0]);
} else {
Assert.NotEqual (string.Empty, list [0]);
}
Assert.Equal (list, resultLines);
}
[Theory]
// Even # of spaces
// 0123456789
@@ -1418,5 +1604,149 @@ namespace Terminal.Gui.TextTests {
Assert.Equal (expected, text [index].ToString ());
}
}
[Fact]
public void GetLengthThatFits_With_Combining_Runes ()
{
var text = "Les Mise\u0328\u0301rables";
Assert.Equal (16, TextFormatter.GetLengthThatFits (text, 14));
}
[Fact]
public void GetMaxColsForWidth_With_Combining_Runes ()
{
var text = new List<string> () { "Les Mis", "e\u0328\u0301", "rables" };
Assert.Equal (1, TextFormatter.GetMaxColsForWidth (text, 1));
}
[Fact]
public void GetSumMaxCharWidth_With_Combining_Runes ()
{
var text = "Les Mise\u0328\u0301rables";
Assert.Equal (1, TextFormatter.GetSumMaxCharWidth (text, 1, 1));
}
[Fact]
public void GetSumMaxCharWidth_List_With_Combining_Runes ()
{
var text = new List<string> () { "Les Mis", "e\u0328\u0301", "rables" };
Assert.Equal (1, TextFormatter.GetSumMaxCharWidth (text, 1, 1));
}
[Theory]
[InlineData (14, 1, TextDirection.LeftRight_TopBottom)]
[InlineData (1, 14, TextDirection.TopBottom_LeftRight)]
public void CalcRect_With_Combining_Runes (int width, int height, TextDirection textDirection)
{
var text = "Les Mise\u0328\u0301rables";
Assert.Equal (new Rect (0, 0, width, height), TextFormatter.CalcRect (0, 0, text, textDirection));
}
[Theory]
[InlineData (14, 1, TextDirection.LeftRight_TopBottom, "Les Misęrables")]
[InlineData (1, 14, TextDirection.TopBottom_LeftRight, "L\ne\ns\n \nM\ni\ns\nę\nr\na\nb\nl\ne\ns")]
[InlineData (4, 4, TextDirection.TopBottom_LeftRight, @"
LMre
eias
ssb
ęl ")]
public void Draw_With_Combining_Runes (int width, int height, TextDirection textDirection, string expected)
{
var driver = new FakeDriver ();
driver.Init ();
var text = "Les Mise\u0328\u0301rables";
var tf = new TextFormatter ();
tf.Direction = textDirection;
tf.Text = text;
Assert.True (tf.WordWrap);
if (textDirection == TextDirection.LeftRight_TopBottom) {
Assert.Equal (new Size (width, height), tf.Size);
} else {
Assert.Equal (new Size (1, text.GetColumns ()), tf.Size);
tf.Size = new Size (width, height);
}
tf.Draw (new Rect (0, 0, width, height), new Attribute (ColorName.White, ColorName.Black), new Attribute (ColorName.Blue, ColorName.Black), default, true, driver);
TestHelpers.AssertDriverContentsWithFrameAre (expected, output, driver);
driver.End ();
}
[Theory]
[InlineData (17, 1, TextDirection.LeftRight_TopBottom, 4, "This is a Tab")]
[InlineData (1, 17, TextDirection.TopBottom_LeftRight, 4, "T\nh\ni\ns\n \ni\ns\n \na\n \n \n \n \n \nT\na\nb")]
[InlineData (13, 1, TextDirection.LeftRight_TopBottom, 0, "This is a Tab")]
[InlineData (1, 13, TextDirection.TopBottom_LeftRight, 0, "T\nh\ni\ns\n \ni\ns\n \na\n \nT\na\nb")]
public void TabWith_PreserveTrailingSpaces_False (int width, int height, TextDirection textDirection, int tabWidth, string expected)
{
var driver = new FakeDriver ();
driver.Init ();
var text = "This is a \tTab";
var tf = new TextFormatter ();
tf.Direction = textDirection;
tf.TabWidth = tabWidth;
tf.Text = text;
Assert.True (tf.WordWrap);
Assert.False (tf.PreserveTrailingSpaces);
Assert.Equal (new Size (width, height), tf.Size);
tf.Draw (new Rect (0, 0, width, height), new Attribute (ColorName.White, ColorName.Black), new Attribute (ColorName.Blue, ColorName.Black), default, true, driver);
TestHelpers.AssertDriverContentsWithFrameAre (expected, output, driver);
driver.End ();
}
[Theory]
[InlineData (17, 1, TextDirection.LeftRight_TopBottom, 4, "This is a Tab")]
[InlineData (1, 17, TextDirection.TopBottom_LeftRight, 4, "T\nh\ni\ns\n \ni\ns\n \na\n \n \n \n \n \nT\na\nb")]
[InlineData (13, 1, TextDirection.LeftRight_TopBottom, 0, "This is a Tab")]
[InlineData (1, 13, TextDirection.TopBottom_LeftRight, 0, "T\nh\ni\ns\n \ni\ns\n \na\n \nT\na\nb")]
public void TabWith_PreserveTrailingSpaces_True (int width, int height, TextDirection textDirection, int tabWidth, string expected)
{
var driver = new FakeDriver ();
driver.Init ();
var text = "This is a \tTab";
var tf = new TextFormatter ();
tf.Direction = textDirection;
tf.TabWidth = tabWidth;
tf.PreserveTrailingSpaces = true;
tf.Text = text;
Assert.True (tf.WordWrap);
Assert.Equal (new Size (width, height), tf.Size);
tf.Draw (new Rect (0, 0, width, height), new Attribute (ColorName.White, ColorName.Black), new Attribute (ColorName.Blue, ColorName.Black), default, true, driver);
TestHelpers.AssertDriverContentsWithFrameAre (expected, output, driver);
driver.End ();
}
[Theory]
[InlineData (17, 1, TextDirection.LeftRight_TopBottom, 4, "This is a Tab")]
[InlineData (1, 17, TextDirection.TopBottom_LeftRight, 4, "T\nh\ni\ns\n \ni\ns\n \na\n \n \n \n \n \nT\na\nb")]
[InlineData (13, 1, TextDirection.LeftRight_TopBottom, 0, "This is a Tab")]
[InlineData (1, 13, TextDirection.TopBottom_LeftRight, 0, "T\nh\ni\ns\n \ni\ns\n \na\n \nT\na\nb")]
public void TabWith_WordWrap_True (int width, int height, TextDirection textDirection, int tabWidth, string expected)
{
var driver = new FakeDriver ();
driver.Init ();
var text = "This is a \tTab";
var tf = new TextFormatter ();
tf.Direction = textDirection;
tf.TabWidth = tabWidth;
tf.WordWrap = true;
tf.Text = text;
Assert.False (tf.PreserveTrailingSpaces);
Assert.Equal (new Size (width, height), tf.Size);
tf.Draw (new Rect (0, 0, width, height), new Attribute (ColorName.White, ColorName.Black), new Attribute (ColorName.Blue, ColorName.Black), default, true, driver);
TestHelpers.AssertDriverContentsWithFrameAre (expected, output, driver);
driver.End ();
}
}
}

View File

@@ -357,9 +357,9 @@ namespace Terminal.Gui.ViewTests {
win.Add (label);
Application.Top.Add (win);
// Text is empty so height=0
// Text is empty but height=1 by default, see Label view
Assert.False (label.AutoSize);
Assert.Equal ("(0,0,0,0)", label.Bounds.ToString ());
Assert.Equal ("(0,0,0,1)", label.Bounds.ToString ());
label.Text = "New text\nNew line";
Application.Top.LayoutSubviews ();
@@ -385,10 +385,11 @@ namespace Terminal.Gui.ViewTests {
Assert.True (label.IsAdded);
// Text is empty so height=0
// Text is empty but height=1 by default, see Label view
Assert.True (label.AutoSize);
// BUGBUG: LayoutSubviews has not been called, so this test is not really valid (pos/dim are indeterminate, not 0)
Assert.Equal ("(0,0,0,0)", label.Bounds.ToString ());
// Not really a bug because View call OnResizeNeeded method on the SetInitialProperties method
Assert.Equal ("(0,0,0,1)", label.Bounds.ToString ());
label.Text = "First line\nSecond line";
Application.Top.LayoutSubviews ();
@@ -420,9 +421,9 @@ namespace Terminal.Gui.ViewTests {
win.Add (label);
Application.Top.Add (win);
// Text is empty so height=0
// Text is empty but height=1 by default, see Label view
Assert.True (label.AutoSize);
Assert.Equal ("(0,0,0,0)", label.Bounds.ToString ());
Assert.Equal ("(0,0,0,1)", label.Bounds.ToString ());
var rs = Application.Begin (Application.Top);

View File

@@ -446,8 +446,10 @@ Test
Assert.Equal (new Rect (0, 0, width + 2, height + 2), pos);
}
[Fact, AutoInitShutdown]
public void Label_WordWrap_PreserveTrailingSpaces_Horizontal_With_Wide_Runes ()
[Theory, AutoInitShutdown]
[InlineData (false)]
[InlineData (true)]
public void Label_WordWrap_PreserveTrailingSpaces_Horizontal_With_Wide_Runes (bool autoSize)
{
var text = "文に は言葉 があり ます。";
var width = 6;
@@ -455,17 +457,29 @@ Test
var wrappedLines = TextFormatter.WordWrapText (text, width, true);
var breakLines = "";
foreach (var line in wrappedLines) breakLines += $"{line}{Environment.NewLine}";
var label = new Label (breakLines) { Width = Dim.Fill (), Height = Dim.Fill () };
var label = new Label (breakLines) { Width = Dim.Fill (), Height = Dim.Fill (), AutoSize = autoSize };
var frame = new FrameView () { Width = Dim.Fill (), Height = Dim.Fill () };
frame.Add (label);
Application.Top.Add (frame);
Application.Begin (Application.Top);
Assert.True (label.AutoSize == autoSize);
Assert.Equal (new Rect (0, 0, 78, 23), label.Frame);
if (autoSize) {
// The size of the wrappedLines [1]
Assert.Equal (new Size (width, height - 2), label.TextFormatter.Size);
} else {
Assert.Equal (new Size (78, 23), label.TextFormatter.Size);
}
((FakeDriver)Application.Driver).SetBufferSize (width + 2, height + 2);
Assert.True (label.AutoSize);
Assert.Equal (new Rect (0, 0, width, height), label.Frame);
Assert.Equal (new Size (width, height), label.TextFormatter.Size);
if (autoSize) {
Assert.Equal (new Size (width, height - 2), label.TextFormatter.Size);
} else {
Assert.Equal (new Size (width, height), label.TextFormatter.Size);
}
Assert.Equal (new Rect (0, 0, width + 2, height + 2), frame.Frame);
var expected = @"
@@ -670,15 +684,17 @@ e
Assert.Equal (new Rect (0, 0, 2, 7), pos);
}
[Fact, AutoInitShutdown]
public void Label_Draw_Horizontal_Simple_TextAlignments ()
[Theory, AutoInitShutdown]
[InlineData (true)]
[InlineData (false)]
public void Label_Draw_Horizontal_Simple_TextAlignments (bool autoSize)
{
var text = "Hello World";
var width = 20;
var lblLeft = new Label (text) { Width = width };
var lblCenter = new Label (text) { Y = 1, Width = width, TextAlignment = TextAlignment.Centered };
var lblRight = new Label (text) { Y = 2, Width = width, TextAlignment = TextAlignment.Right };
var lblJust = new Label (text) { Y = 3, Width = width, TextAlignment = TextAlignment.Justified };
var lblLeft = new Label (text) { Width = width, AutoSize = autoSize };
var lblCenter = new Label (text) { Y = 1, Width = width, TextAlignment = TextAlignment.Centered, AutoSize = autoSize };
var lblRight = new Label (text) { Y = 2, Width = width, TextAlignment = TextAlignment.Right, AutoSize = autoSize };
var lblJust = new Label (text) { Y = 3, Width = width, TextAlignment = TextAlignment.Justified, AutoSize = autoSize };
var frame = new FrameView () { Width = Dim.Fill (), Height = Dim.Fill () };
frame.Add (lblLeft, lblCenter, lblRight, lblJust);
@@ -686,14 +702,28 @@ e
Application.Begin (Application.Top);
((FakeDriver)Application.Driver).SetBufferSize (width + 2, 6);
Assert.True (lblLeft.AutoSize);
Assert.True (lblCenter.AutoSize);
Assert.True (lblRight.AutoSize);
Assert.True (lblJust.AutoSize);
Assert.True (lblLeft.AutoSize == autoSize);
Assert.True (lblCenter.AutoSize == autoSize);
Assert.True (lblRight.AutoSize == autoSize);
Assert.True (lblJust.AutoSize == autoSize);
Assert.True (lblLeft.TextFormatter.AutoSize == autoSize);
Assert.True (lblCenter.TextFormatter.AutoSize == autoSize);
Assert.True (lblRight.TextFormatter.AutoSize == autoSize);
Assert.True (lblJust.TextFormatter.AutoSize == autoSize);
Assert.Equal (new Rect (0, 0, width, 1), lblLeft.Frame);
Assert.Equal (new Rect (0, 1, width, 1), lblCenter.Frame);
Assert.Equal (new Rect (0, 2, width, 1), lblRight.Frame);
Assert.Equal (new Rect (0, 3, width, 1), lblJust.Frame);
if (autoSize) {
Assert.Equal (new Size (11, 1), lblLeft.TextFormatter.Size);
Assert.Equal (new Size (11, 1), lblCenter.TextFormatter.Size);
Assert.Equal (new Size (11, 1), lblRight.TextFormatter.Size);
} else {
Assert.Equal (new Size (width, 1), lblLeft.TextFormatter.Size);
Assert.Equal (new Size (width, 1), lblCenter.TextFormatter.Size);
Assert.Equal (new Size (width, 1), lblRight.TextFormatter.Size);
}
Assert.Equal (new Size (width, 1), lblJust.TextFormatter.Size);
Assert.Equal (new Rect (0, 0, width + 2, 6), frame.Frame);
var expected = @"
@@ -709,15 +739,17 @@ e
Assert.Equal (new Rect (0, 0, width + 2, 6), pos);
}
[Fact, AutoInitShutdown]
public void Label_Draw_Vertical_Simple_TextAlignments ()
[Theory, AutoInitShutdown]
[InlineData (true)]
[InlineData (false)]
public void Label_Draw_Vertical_Simple_TextAlignments (bool autoSize)
{
var text = "Hello World";
var height = 20;
var lblLeft = new Label (text, direction: TextDirection.TopBottom_LeftRight) { Height = height };
var lblCenter = new Label (text, direction: TextDirection.TopBottom_LeftRight) { X = 2, Height = height, VerticalTextAlignment = VerticalTextAlignment.Middle };
var lblRight = new Label (text, direction: TextDirection.TopBottom_LeftRight) { X = 4, Height = height, VerticalTextAlignment = VerticalTextAlignment.Bottom };
var lblJust = new Label (text, direction: TextDirection.TopBottom_LeftRight) { X = 6, Height = height, VerticalTextAlignment = VerticalTextAlignment.Justified };
var lblLeft = new Label (text, direction: TextDirection.TopBottom_LeftRight, autoSize) { Height = height };
var lblCenter = new Label (text, direction: TextDirection.TopBottom_LeftRight, autoSize) { X = 2, Height = height, VerticalTextAlignment = VerticalTextAlignment.Middle };
var lblRight = new Label (text, direction: TextDirection.TopBottom_LeftRight, autoSize) { X = 4, Height = height, VerticalTextAlignment = VerticalTextAlignment.Bottom };
var lblJust = new Label (text, direction: TextDirection.TopBottom_LeftRight, autoSize) { X = 6, Height = height, VerticalTextAlignment = VerticalTextAlignment.Justified };
var frame = new FrameView () { Width = Dim.Fill (), Height = Dim.Fill () };
frame.Add (lblLeft, lblCenter, lblRight, lblJust);
@@ -725,14 +757,28 @@ e
Application.Begin (Application.Top);
((FakeDriver)Application.Driver).SetBufferSize (9, height + 2);
Assert.True (lblLeft.AutoSize);
Assert.True (lblCenter.AutoSize);
Assert.True (lblRight.AutoSize);
Assert.True (lblJust.AutoSize);
Assert.True (lblLeft.AutoSize == autoSize);
Assert.True (lblCenter.AutoSize == autoSize);
Assert.True (lblRight.AutoSize == autoSize);
Assert.True (lblJust.AutoSize == autoSize);
Assert.True (lblLeft.TextFormatter.AutoSize == autoSize);
Assert.True (lblCenter.TextFormatter.AutoSize == autoSize);
Assert.True (lblRight.TextFormatter.AutoSize == autoSize);
Assert.True (lblJust.TextFormatter.AutoSize == autoSize);
Assert.Equal (new Rect (0, 0, 1, height), lblLeft.Frame);
Assert.Equal (new Rect (2, 0, 1, height), lblCenter.Frame);
Assert.Equal (new Rect (4, 0, 1, height), lblRight.Frame);
Assert.Equal (new Rect (6, 0, 1, height), lblJust.Frame);
if (autoSize) {
Assert.Equal (new Size (1, 11), lblLeft.TextFormatter.Size);
Assert.Equal (new Size (1, 11), lblCenter.TextFormatter.Size);
Assert.Equal (new Size (1, 11), lblRight.TextFormatter.Size);
} else {
Assert.Equal (new Size (1, height), lblLeft.TextFormatter.Size);
Assert.Equal (new Size (1, height), lblCenter.TextFormatter.Size);
Assert.Equal (new Size (1, height), lblRight.TextFormatter.Size);
}
Assert.Equal (new Size (1, height), lblJust.TextFormatter.Size);
Assert.Equal (new Rect (0, 0, 9, height + 2), frame.Frame);
var expected = @"