From 7b3d8eed2b98ef9a34318f507835851ce325a4b6 Mon Sep 17 00:00:00 2001 From: BDisp Date: Fri, 24 Nov 2023 19:50:16 +0000 Subject: [PATCH 1/4] Add horizontal and vertical support for combining glyphs. --- Terminal.Gui/Text/TextFormatter.cs | 118 +++++++++++++++++++++------ UnitTests/Text/TextFormatterTests.cs | 71 +++++++++++++++- 2 files changed, 163 insertions(+), 26 deletions(-) diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs index 4fdd45b1e..23fc28b56 100644 --- a/Terminal.Gui/Text/TextFormatter.cs +++ b/Terminal.Gui/Text/TextFormatter.cs @@ -368,7 +368,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++; @@ -472,12 +482,13 @@ namespace Terminal.Gui { if (IsHorizontalDirection (textDirection)) { return StringExtensions.ToString (runes.GetRange (0, GetLengthThatFits (text, width))); } 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) { + } else if (IsHorizontalDirection (textDirection) && GetRuneWidth (text.GetColumns ()) > width) { return StringExtensions.ToString (runes.GetRange (0, GetLengthThatFits (text, width))); } return text; @@ -654,7 +665,7 @@ namespace Terminal.Gui { 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.GetColumns ())); if (m > max) { max = m; } @@ -688,7 +699,7 @@ namespace Terminal.Gui { 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.GetColumns ())); } return max; } @@ -706,7 +717,7 @@ namespace Terminal.Gui { 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].GetColumns ()); } return max; } @@ -734,7 +745,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].GetColumns ()); if (runesLength + runeWidth > columns) { break; } @@ -743,6 +754,15 @@ namespace Terminal.Gui { return runeIdx; } + private static int GetRuneWidth (int runeWidth) + { + if (runeWidth < 0 || runeWidth > 0) { + return Math.Max (runeWidth, 1); + } + + return runeWidth; + } + /// /// Gets the index position from the list based on the . /// @@ -756,7 +776,7 @@ namespace Terminal.Gui { 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.GetColumns ())) : 1; if (runesLength + maxRruneWidth > width) { break; } @@ -798,6 +818,8 @@ namespace Terminal.Gui { var rw = ((Rune)rune).GetColumns (); if (rw > 0) { rw--; + } else if (rw == 0) { + cols--; } cols += rw; } @@ -823,7 +845,9 @@ namespace Terminal.Gui { } else if (rune.Value != '\r') { rows++; var rw = ((Rune)rune).GetColumns (); - if (cw < rw) { + if (rw == 0) { + rows--; + } else if (cw < rw) { cw = rw; vw++; } @@ -1375,31 +1399,64 @@ 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) { + Application.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)); + + Application.Driver?.Move (lastZeroWidthPos [foundIdx].Value.X, current); + } else if (!rune.IsCombiningMark () && lastRuneUsed.IsCombiningMark ()) { + current++; + Application.Driver?.Move (x, current); + } else { + Application.Driver?.Move (x, current); + } + } else { + Application.Driver?.Move (x, current); + } + } } else { Application.Driver?.Move (current, y); if (idx >= 0 && idx < runes.Length) { rune = runes [idx]; } } + + var runeWidth = GetRuneWidth (rune.GetColumns ()); + if (HotKeyPos > -1 && idx == HotKeyPos) { if ((isVertical && _textVerticalAlignment == VerticalTextAlignment.Justified) || (!isVertical && _textAlignment == TextAlignment.Justified)) { @@ -1409,12 +1466,27 @@ namespace Terminal.Gui { Application.Driver?.AddRune (rune); Application.Driver?.SetAttribute (normalColor); } else { + 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))); + } + Application.Driver?.Move (x + 1, current); + } + } + Application.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/UnitTests/Text/TextFormatterTests.cs b/UnitTests/Text/TextFormatterTests.cs index dac0c0cb6..9eb788cf2 100644 --- a/UnitTests/Text/TextFormatterTests.cs +++ b/UnitTests/Text/TextFormatterTests.cs @@ -620,21 +620,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); } @@ -1418,5 +1421,67 @@ 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, AutoInitShutdown] + [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 text = "Les Mise\u0328\u0301rables"; + + var tf = new TextFormatter (); + tf.Text = text; + tf.Direction = textDirection; + + if (textDirection == TextDirection.LeftRight_TopBottom) { + Assert.Equal (new Size (width, height), tf.Size); + } else { + 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)); + TestHelpers.AssertDriverContentsWithFrameAre (expected, output); + } } } \ No newline at end of file From b0e5ada1fb3ddb0fad001b870a41bf50dbd3e4ad Mon Sep 17 00:00:00 2001 From: BDisp Date: Fri, 24 Nov 2023 21:12:21 +0000 Subject: [PATCH 2/4] Fix text and auto size behavior. --- Terminal.Gui/Text/TextFormatter.cs | 16 ++++++++++------ UnitTests/Text/TextFormatterTests.cs | 21 ++++++++++++++++----- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs index 23fc28b56..7d83ee3f7 100644 --- a/Terminal.Gui/Text/TextFormatter.cs +++ b/Terminal.Gui/Text/TextFormatter.cs @@ -978,7 +978,7 @@ namespace Terminal.Gui { #endregion // Static Members List _lines = new List (); - string _text; + string _text = null; TextAlignment _textAlignment; VerticalTextAlignment _textVerticalAlignment; TextDirection _textDirection; @@ -997,13 +997,17 @@ namespace Terminal.Gui { public virtual string Text { get => _text; set { + if (AutoSize || (_text == null && value != null && Size.IsEmpty)) { + Size = CalcRect (0, 0, value, _textDirection).Size; + } + _text = 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 (_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); + //} NeedsFormat = true; } diff --git a/UnitTests/Text/TextFormatterTests.cs b/UnitTests/Text/TextFormatterTests.cs index 9eb788cf2..cfb31d44a 100644 --- a/UnitTests/Text/TextFormatterTests.cs +++ b/UnitTests/Text/TextFormatterTests.cs @@ -65,13 +65,23 @@ namespace Terminal.Gui.TextTests { Assert.NotEmpty (tf.Lines); } - [Fact] - public void TestSize_TextChange () + [Theory] + [InlineData (TextDirection.LeftRight_TopBottom, 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); tf.Text = "你你"; - Assert.Equal (4, tf.Size.Width); + if (autoSize) { + if (textDirection == TextDirection.LeftRight_TopBottom) { + Assert.Equal (4, tf.Size.Width); + } else { + Assert.Equal (2, tf.Size.Width); + } + } else { + Assert.Equal (2, tf.Size.Width); + } } [Fact] @@ -1472,12 +1482,13 @@ ssb var text = "Les Mise\u0328\u0301rables"; var tf = new TextFormatter (); - tf.Text = text; tf.Direction = textDirection; + tf.Text = text; 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)); From 063cea852670db87bedb76c9f20f60222b525a2f Mon Sep 17 00:00:00 2001 From: BDisp Date: Fri, 24 Nov 2023 23:45:32 +0000 Subject: [PATCH 3/4] Add TabWidth property. --- Terminal.Gui/Text/TextFormatter.cs | 59 ++++++++++++++++++++-------- UnitTests/Text/TextFormatterTests.cs | 38 ++++++++++++++++++ 2 files changed, 81 insertions(+), 16 deletions(-) diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs index 7d83ee3f7..496364e29 100644 --- a/Terminal.Gui/Text/TextFormatter.cs +++ b/Terminal.Gui/Text/TextFormatter.cs @@ -169,6 +169,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. @@ -613,6 +622,7 @@ namespace Terminal.Gui { if (wordWrap == false) { text = ReplaceCRLFWithSpace (text); + text = ReplaceTABWithSpaces (text, tabWidth); lineResult.Add (ClipAndJustify (text, width, justify, textDirection)); return lineResult; } @@ -634,7 +644,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 (ReplaceTABWithSpaces (line, tabWidth), width, justify, textDirection)); } return lineResult; @@ -792,8 +802,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); @@ -815,11 +826,16 @@ namespace Terminal.Gui { cols = 0; } else if (rune.Value != '\r') { cols++; - var rw = ((Rune)rune).GetColumns (); - if (rw > 0) { - rw--; - } else if (rw == 0) { - cols--; + 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; } @@ -844,12 +860,18 @@ namespace Terminal.Gui { cw = 1; } else if (rune.Value != '\r') { rows++; - var rw = ((Rune)rune).GetColumns (); - if (rw == 0) { - rows--; - } else 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++; + } } } } @@ -998,7 +1020,7 @@ namespace Terminal.Gui { get => _text; set { if (AutoSize || (_text == null && value != null && Size.IsEmpty)) { - Size = CalcRect (0, 0, value, _textDirection).Size; + Size = CalcRect (0, 0, value, _textDirection, TabWidth).Size; } _text = value; @@ -1222,7 +1244,7 @@ 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); + PreserveTrailingSpaces, TabWidth, _textDirection); if (!AutoSize) { colsWidth = GetMaxColsForWidth (_lines, Size.Width); if (_lines.Count > colsWidth) { @@ -1231,7 +1253,7 @@ namespace Terminal.Gui { } } else { _lines = Format (shown_text, Size.Width, _textAlignment == TextAlignment.Justified, Size.Height > 1, - PreserveTrailingSpaces, 0, _textDirection); + PreserveTrailingSpaces, TabWidth, _textDirection); if (!AutoSize && _lines.Count > Size.Height) { _lines.RemoveRange (Size.Height, _lines.Count - Size.Height); } @@ -1254,6 +1276,11 @@ namespace Terminal.Gui { /// public bool NeedsFormat { get; set; } + /// + /// Gets or sets the number of columns used for a tab. + /// + public int TabWidth { get; set; } = 4; + /// /// Causes the to reformat the text. /// diff --git a/UnitTests/Text/TextFormatterTests.cs b/UnitTests/Text/TextFormatterTests.cs index cfb31d44a..32f2a799a 100644 --- a/UnitTests/Text/TextFormatterTests.cs +++ b/UnitTests/Text/TextFormatterTests.cs @@ -1494,5 +1494,43 @@ ssb tf.Draw (new Rect (0, 0, width, height), new Attribute (ColorName.White, ColorName.Black), new Attribute (ColorName.Blue, ColorName.Black)); TestHelpers.AssertDriverContentsWithFrameAre (expected, output); } + + [Theory, AutoInitShutdown] + [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 text = "This is a \tTab"; + var tf = new TextFormatter (); + tf.Direction = textDirection; + tf.TabWidth = tabWidth; + 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)); + TestHelpers.AssertDriverContentsWithFrameAre (expected, output); + } + + [Theory, AutoInitShutdown] + [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 text = "This is a \tTab"; + var tf = new TextFormatter (); + tf.Direction = textDirection; + tf.TabWidth = tabWidth; + tf.PreserveTrailingSpaces = true; + tf.Text = text; + + 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)); + TestHelpers.AssertDriverContentsWithFrameAre (expected, output); + } } } \ No newline at end of file From b94ca727ea3a64d83ee5df7861ef956838d1eca9 Mon Sep 17 00:00:00 2001 From: BDisp Date: Fri, 24 Nov 2023 23:54:25 +0000 Subject: [PATCH 4/4] Add unit test for WordWrap. --- UnitTests/Text/TextFormatterTests.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/UnitTests/Text/TextFormatterTests.cs b/UnitTests/Text/TextFormatterTests.cs index 32f2a799a..740e847cf 100644 --- a/UnitTests/Text/TextFormatterTests.cs +++ b/UnitTests/Text/TextFormatterTests.cs @@ -1508,6 +1508,7 @@ ssb tf.TabWidth = tabWidth; tf.Text = text; + Assert.False (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)); @@ -1528,6 +1529,26 @@ ssb tf.PreserveTrailingSpaces = true; tf.Text = text; + Assert.False (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)); + TestHelpers.AssertDriverContentsWithFrameAre (expected, output); + } + + [Theory, AutoInitShutdown] + [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 text = "This is a \tTab"; + var tf = new TextFormatter (); + tf.Direction = textDirection; + tf.TabWidth = tabWidth; + tf.WordWrap = true; + tf.Text = text; + 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)); TestHelpers.AssertDriverContentsWithFrameAre (expected, output);