From 3f4d96bec763d5614b6cac1475634517a7a38373 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 26 Nov 2023 22:41:54 +0000 Subject: [PATCH] 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. --- Terminal.Gui/Text/TextFormatter.cs | 410 +++++++++++++++++++-------- Terminal.Gui/View/ViewLayout.cs | 24 +- Terminal.Gui/View/ViewText.cs | 11 +- Terminal.Gui/Views/MessageBox.cs | 26 +- UnitTests/Dialogs/MessageBoxTests.cs | 108 +++---- UnitTests/Text/TextFormatterTests.cs | 386 +++++++++++++++++++++++-- UnitTests/View/Layout/LayoutTests.cs | 13 +- UnitTests/Views/LabelTests.cs | 96 +++++-- 8 files changed, 829 insertions(+), 245 deletions(-) diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs index 4fdd45b1e..1b38430bb 100644 --- a/Terminal.Gui/Text/TextFormatter.cs +++ b/Terminal.Gui/Text/TextFormatter.cs @@ -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)); + } + /// /// Splits all newlines in the 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 { /// 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. /// Justified and clipped text. - 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); } /// @@ -456,8 +475,9 @@ namespace Terminal.Gui { /// 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. /// Justified and clipped text. - 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 { /// /// 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) + 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' }; /// /// 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 , 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. /// A list of word wrapped lines. /// /// @@ -558,9 +583,9 @@ namespace Terminal.Gui { /// If is int.MaxValue, the text will be formatted to the maximum width possible. /// /// - public static List Format (string text, int width, TextAlignment talign, bool wordWrap, bool preserveTrailingSpaces = false, int tabWidth = 0, TextDirection textDirection = TextDirection.LeftRight_TopBottom) + public static List 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); } /// @@ -575,6 +600,7 @@ namespace Terminal.Gui { /// 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. /// A list of word wrapped lines. /// /// @@ -588,7 +614,7 @@ namespace Terminal.Gui { /// /// public static List 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 { /// Width of the longest line after formatting the text constrained by . /// Text, may contain newlines. /// The number of columns to constrain the text to for formatting. - public static int MaxWidth (string text, int maxColumns) + /// The number of columns used for a tab. + 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 { /// if it contains newlines. /// /// Text, may contain newlines. + /// The number of columns used for a tab. /// The length of the longest line. - 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)); } /// @@ -681,14 +726,15 @@ namespace Terminal.Gui { /// The lines. /// The start index. /// The length. + /// The number of columns used for a tab. /// The maximum characters width. - public static int GetSumMaxCharWidth (List lines, int startIndex = -1, int length = -1) + public static int GetSumMaxCharWidth (List 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 { /// 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) + 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 { /// /// The text. /// The width. + /// The number of columns used for a tab. /// The index of the text that fit the width. - 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); /// /// Gets the number of the Runes in a list of Runes that will fit in . /// /// The list of runes. /// The width. + /// The number of columns used for a tab. /// The index of the last Rune in that fit in . - public static int GetLengthThatFits (List runes, int columns) + public static int GetLengthThatFits (List 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 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; + } + /// /// Gets the index position from the list based on the . /// /// 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) + public static int GetMaxColsForWidth (List 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 { /// The y location of the rectangle /// The text to measure /// The text direction. + /// The number of columns used for a tab. /// - 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 _lines = new List (); - 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; /// /// Event invoked when the 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 { /// values and doesn't work with layout, /// to avoid breaking the and settings. /// - public bool AutoSize { get; set; } + /// + /// Auto size is ignored if the and are used. + /// + 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; + } + } + } /// /// Gets or sets whether trailing spaces at the end of word-wrapped lines are preserved @@ -999,7 +1107,10 @@ namespace Terminal.Gui { /// If trailing spaces at the end of wrapped lines will be removed when /// is formatted for display. The default is . /// - public bool PreserveTrailingSpaces { get; set; } + public bool PreserveTrailingSpaces { + get => _preserveTrailingSpaces; + set => _preserveTrailingSpaces = EnableNeedsFormat (value); + } /// /// Controls the horizontal text-alignment property. @@ -1007,10 +1118,7 @@ namespace Terminal.Gui { /// The text alignment. public TextAlignment Alignment { get => _textAlignment; - set { - _textAlignment = value; - NeedsFormat = true; - } + set => _textAlignment = EnableNeedsFormat (value); } /// @@ -1019,10 +1127,7 @@ namespace Terminal.Gui { /// The text vertical alignment. public VerticalTextAlignment VerticalAlignment { get => _textVerticalAlignment; - set { - _textVerticalAlignment = value; - NeedsFormat = true; - } + set => _textVerticalAlignment = EnableNeedsFormat (value); } /// @@ -1031,10 +1136,7 @@ namespace Terminal.Gui { /// The text vertical alignment. public TextDirection Direction { get => _textDirection; - set { - _textDirection = value; - NeedsFormat = true; - } + set => _textDirection = EnableNeedsFormat (value); } /// @@ -1097,11 +1199,13 @@ namespace Terminal.Gui { } } - // TODO: This is not implemented! /// - /// + /// Allows word wrap the to fit the available container width. /// - public bool WordWrap { get; set; } = false; + public bool WordWrap { + get => _wordWrap; + set => _wordWrap = EnableNeedsFormat (value); + } /// /// 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. /// 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 { /// /// The position in the text of the hotkey. The hotkey will be rendered using the hot color. /// - public int HotKeyPos { get => _hotKeyPos; set => _hotKeyPos = value; } + public int HotKeyPos { get => _hotKeyPos; internal set => _hotKeyPos = value; } /// /// Gets the hotkey. Will be an upper case letter or digit. @@ -1146,7 +1251,7 @@ namespace Terminal.Gui { /// /// Gets the cursor position from . If the is defined, the cursor will be positioned over it. /// - public int CursorPosition { get; set; } + public int CursorPosition { get; internal set; } /// /// Gets the size required to hold the formatted text, given the constraints placed by . @@ -1169,7 +1274,7 @@ namespace Terminal.Gui { /// /// /// Upon a 'get' of this property, if the text needs to be formatted (if is true) - /// will be called internally. + /// will be called internally. /// /// public List 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 { } /// - /// Gets or sets whether the needs to format the text when is called. + /// Gets or sets whether the needs to format the text when is called. /// If it is false when Draw is called, the Draw call will be faster. /// /// @@ -1226,6 +1331,33 @@ namespace Terminal.Gui { /// public bool NeedsFormat { get; set; } + /// + /// Gets or sets the number of columns used for a tab. + /// + public int TabWidth { + get => _tabWidth; + set => _tabWidth = EnableNeedsFormat (value); + } + + /// + /// 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); + } + } + + private T EnableNeedsFormat (T value) + { + NeedsFormat = true; + return value; + } + /// /// Causes the to reformat the text. /// @@ -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 (); } /// - /// Draws the text held by to using the colors specified. + /// Draws the text held by to using the colors specified. /// /// 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. /// Determines if the bounds width will be used (default) or only the text width will be used. - public void Draw (Rect bounds, Attribute normalColor, Attribute hotColor, Rect containerBounds = default, bool fillRemaining = true) + /// The console driver currently used by the application. + 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 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 (); + } + 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; } diff --git a/Terminal.Gui/View/ViewLayout.cs b/Terminal.Gui/View/ViewLayout.cs index 48b5434cd..5f599c89a 100644 --- a/Terminal.Gui/View/ViewLayout.cs +++ b/Terminal.Gui/View/ViewLayout.cs @@ -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 (); @@ -958,7 +970,7 @@ namespace Terminal.Gui { } } // BUGBUG: This call may be redundant - TextFormatter.Size = GetSizeNeededForTextAndHotKey (); + TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey (); return aSize; } diff --git a/Terminal.Gui/View/ViewText.cs b/Terminal.Gui/View/ViewText.cs index 1457cfa62..72eb42f5b 100644 --- a/Terminal.Gui/View/ViewText.cs +++ b/Terminal.Gui/View/ViewText.cs @@ -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 accounting for a . /// /// - 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)); } - } } \ No newline at end of file diff --git a/Terminal.Gui/Views/MessageBox.cs b/Terminal.Gui/Views/MessageBox.cs index 1ab1bd567..78ac62e8a 100644 --- a/Terminal.Gui/Views/MessageBox.cs +++ b/Terminal.Gui/Views/MessageBox.cs @@ -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); }; diff --git a/UnitTests/Dialogs/MessageBoxTests.cs b/UnitTests/Dialogs/MessageBoxTests.cs index 624c58805..ee0bfdad8 100644 --- a/UnitTests/Dialogs/MessageBoxTests.cs +++ b/UnitTests/Dialogs/MessageBoxTests.cs @@ -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 (); } } -} \ No newline at end of file +} diff --git a/UnitTests/Text/TextFormatterTests.cs b/UnitTests/Text/TextFormatterTests.cs index dac0c0cb6..99756f0f1 100644 --- a/UnitTests/Text/TextFormatterTests.cs +++ b/UnitTests/Text/TextFormatterTests.cs @@ -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 resultLines) { List 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 resultLines) { List 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 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 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 () { "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 () { "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 (); + } } } \ No newline at end of file diff --git a/UnitTests/View/Layout/LayoutTests.cs b/UnitTests/View/Layout/LayoutTests.cs index 19d759425..c7dc9a6de 100644 --- a/UnitTests/View/Layout/LayoutTests.cs +++ b/UnitTests/View/Layout/LayoutTests.cs @@ -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); diff --git a/UnitTests/Views/LabelTests.cs b/UnitTests/Views/LabelTests.cs index eef07ba74..11881c3b4 100644 --- a/UnitTests/Views/LabelTests.cs +++ b/UnitTests/Views/LabelTests.cs @@ -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 = @"