diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs index db13b5930b..8bfbbc4a34 100644 --- a/Terminal.Gui/Text/TextFormatter.cs +++ b/Terminal.Gui/Text/TextFormatter.cs @@ -304,7 +304,7 @@ public void Draw ( { if (isVertical) { - int runesWidth = GetWidestLineLength (linesFormatted, line, TabWidth); + int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, line, linesFormatted.Count - line, TabWidth); x = screen.Right - runesWidth; CursorPosition = screen.Width - runesWidth + (_hotKeyPos > -1 ? _hotKeyPos : 0); } @@ -320,7 +320,7 @@ public void Draw ( if (isVertical) { int runesWidth = line > 0 - ? GetWidestLineLength (linesFormatted, 0, line, TabWidth) + ? GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth) : 0; x = screen.Left + runesWidth; } @@ -335,8 +335,9 @@ public void Draw ( { if (isVertical) { - int runesWidth = GetWidestLineLength (linesFormatted, line, TabWidth); - x = screen.Left + line + (screen.Width - runesWidth) / 2; + int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth); + int linesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth); + x = screen.Left + linesWidth + (screen.Width - runesWidth) / 2; CursorPosition = (screen.Width - runesWidth) / 2 + (_hotKeyPos > -1 ? _hotKeyPos : 0); } @@ -410,7 +411,10 @@ public void Draw ( if (lastZeroWidthPos is null) { - if (idx < 0 || x + current + colOffset < 0) + if (idx < 0 + || (isVertical + ? VerticalAlignment != VerticalTextAlignment.Bottom && current < 0 + : Alignment != TextAlignment.Right && x + current + colOffset < 0)) { current++; @@ -423,7 +427,7 @@ public void Draw ( } if ((!isVertical && current - start > maxScreen.Left + maxScreen.Width - screen.X + colOffset) - || (isVertical && idx > maxScreen.Top + maxScreen.Height - screen.Y)) + || (isVertical && current > start + size + zeroLengthCount && idx > maxScreen.Top + maxScreen.Height - screen.Y)) { break; } @@ -642,7 +646,8 @@ public List GetLines () PreserveTrailingSpaces, TabWidth, Direction, - MultiLine + MultiLine, + this ); if (!AutoSize) @@ -665,7 +670,8 @@ public List GetLines () PreserveTrailingSpaces, TabWidth, Direction, - MultiLine + MultiLine, + this ); if (!AutoSize && _lines.Count > Size.Height) @@ -932,6 +938,7 @@ public static string ClipOrPad (string text, int width) /// /// The number of columns used for a tab. /// The text direction. + /// instance to access any of his objects. /// A list of word wrapped lines. /// /// This method does not do any justification. @@ -947,7 +954,8 @@ public static List WordWrapText ( int width, bool preserveTrailingSpaces = false, int tabWidth = 0, - TextDirection textDirection = TextDirection.LeftRight_TopBottom + TextDirection textDirection = TextDirection.LeftRight_TopBottom, + TextFormatter textFormatter = null ) { if (width < 0) @@ -955,7 +963,6 @@ public static List WordWrapText ( throw new ArgumentOutOfRangeException ($"{nameof (width)} cannot be negative."); } - int start = 0, end; List lines = new (); if (string.IsNullOrEmpty (text)) @@ -965,6 +972,13 @@ public static List WordWrapText ( List runes = StripCRLF (text).ToRuneList (); + int start = Math.Max ( + !runes.Contains ((Rune)' ') && textFormatter is { VerticalAlignment: VerticalTextAlignment.Bottom } && IsVerticalDirection (textDirection) + ? runes.Count - width + : 0, + 0); + int end; + if (preserveTrailingSpaces) { while ((end = start) < runes.Count) @@ -997,7 +1011,8 @@ public static List WordWrapText ( + GetLengthThatFits ( runes.GetRange (start, runes.Count - start), width, - tabWidth + tabWidth, + textDirection )) < runes.Count) { @@ -1012,13 +1027,15 @@ public static List WordWrapText ( + GetLengthThatFits ( runes.GetRange (end, runes.Count - end), width, - tabWidth + tabWidth, + textDirection ); } var str = StringExtensions.ToString (runes.GetRange (start, end - start)); + int zeroLength = text.EnumerateRunes ().Sum (r => r.GetColumns () == 0 ? 1 : 0); - if (end > start && GetRuneWidth (str, tabWidth) <= width) + if (end > start && GetRuneWidth (str, tabWidth, textDirection) <= width + zeroLength) { lines.Add (str); start = end; @@ -1177,16 +1194,18 @@ int GetNextWhiteSpace (int from, int cWidth, out bool incomplete, int cLength = /// Alignment. /// The text direction. /// The number of columns used for a tab. + /// instance to access any of his objects. /// Justified and clipped text. public static string ClipAndJustify ( string text, int width, TextAlignment talign, TextDirection textDirection = TextDirection.LeftRight_TopBottom, - int tabWidth = 0 + int tabWidth = 0, + TextFormatter textFormatter = null ) { - return ClipAndJustify (text, width, talign == TextAlignment.Justified, textDirection, tabWidth); + return ClipAndJustify (text, width, talign == TextAlignment.Justified, textDirection, tabWidth, textFormatter); } /// Justifies text within a specified width. @@ -1198,13 +1217,15 @@ public static string ClipAndJustify ( /// Justify. /// The text direction. /// The number of columns used for a tab. + /// instance to access any of his objects. /// Justified and clipped text. public static string ClipAndJustify ( string text, int width, bool justify, TextDirection textDirection = TextDirection.LeftRight_TopBottom, - int tabWidth = 0 + int tabWidth = 0, + TextFormatter textFormatter = null ) { if (width < 0) @@ -1219,20 +1240,39 @@ public static string ClipAndJustify ( text = ReplaceTABWithSpaces (text, tabWidth); List runes = text.ToRuneList (); + int zeroLength = runes.Sum (r => r.GetColumns () == 0 ? 1 : 0); - if (runes.Count > width) + if (runes.Count - zeroLength > width) { if (IsHorizontalDirection (textDirection)) { - return StringExtensions.ToString ( - runes.GetRange ( - 0, - GetLengthThatFits (text, width, tabWidth) - ) - ); + if (textFormatter is { Alignment: TextAlignment.Right }) + { + return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection); + } + + if (textFormatter is { Alignment: TextAlignment.Centered }) + { + return GetRangeThatFits (runes, Math.Max ((runes.Count - width) / 2, 0), text, width, tabWidth, textDirection); + } + + return GetRangeThatFits (runes, 0, text, width, tabWidth, textDirection); } - int zeroLength = runes.Sum (r => r.GetColumns () == 0 ? 1 : 0); + if (IsVerticalDirection (textDirection)) + { + if (textFormatter is { VerticalAlignment: VerticalTextAlignment.Bottom }) + { + return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection); + } + + if (textFormatter is { VerticalAlignment: VerticalTextAlignment.Middle }) + { + return GetRangeThatFits (runes, Math.Max((runes.Count - width) / 2, 0), text, width, tabWidth, textDirection); + } + + return GetRangeThatFits (runes, 0, text, width, tabWidth, textDirection); + } return StringExtensions.ToString (runes.GetRange (0, width + zeroLength)); } @@ -1242,19 +1282,57 @@ public static string ClipAndJustify ( return Justify (text, width, ' ', textDirection, tabWidth); } - if (IsHorizontalDirection (textDirection) && GetRuneWidth (text, tabWidth) > width) + if (IsHorizontalDirection (textDirection)) { - return StringExtensions.ToString ( - runes.GetRange ( - 0, - GetLengthThatFits (text, width, tabWidth) - ) - ); + if (textFormatter is { Alignment: TextAlignment.Right }) + { + if (GetRuneWidth (text, tabWidth, textDirection) > width) + { + return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection); + } + } + else if (textFormatter is { Alignment: TextAlignment.Centered }) + { + return GetRangeThatFits (runes, Math.Max(( runes.Count - width) / 2, 0), text, width, tabWidth, textDirection); + } + else if (GetRuneWidth (text, tabWidth, textDirection) > width) + { + return GetRangeThatFits (runes, 0, text, width, tabWidth, textDirection); + } + } + + if (IsVerticalDirection (textDirection)) + { + if (textFormatter is { VerticalAlignment: VerticalTextAlignment.Bottom }) + { + if (runes.Count - zeroLength > width) + { + return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection); + } + } + else if (textFormatter is { VerticalAlignment: VerticalTextAlignment.Middle }) + { + return GetRangeThatFits (runes, Math.Max ((runes.Count - width) / 2, 0), text, width, tabWidth, textDirection); + } + else if (runes.Count - zeroLength > width) + { + return GetRangeThatFits (runes, 0, text, width, tabWidth, textDirection); + } } return text; } + private static string GetRangeThatFits (List runes, int index, string text, int width, int tabWidth, TextDirection textDirection) + { + return StringExtensions.ToString ( + runes.GetRange ( + index, + GetLengthThatFits (text, width, tabWidth, textDirection) + ) + ); + } + /// /// Justifies the text to fill the width provided. Space will be added between words to make the text just fit /// width. Spaces will not be added to the start or end. @@ -1289,7 +1367,7 @@ public static string Justify ( if (IsHorizontalDirection (textDirection)) { - textCount = words.Sum (arg => GetRuneWidth (arg, tabWidth)); + textCount = words.Sum (arg => GetRuneWidth (arg, tabWidth, textDirection)); } else { @@ -1352,6 +1430,7 @@ public static string Justify ( /// The number of columns used for a tab. /// The text direction. /// If new lines are allowed. + /// instance to access any of his objects. /// A list of word wrapped lines. /// /// An empty string will result in one empty line. @@ -1366,7 +1445,8 @@ public static List Format ( bool preserveTrailingSpaces = false, int tabWidth = 0, TextDirection textDirection = TextDirection.LeftRight_TopBottom, - bool multiLine = false + bool multiLine = false, + TextFormatter textFormatter = null ) { return Format ( @@ -1377,7 +1457,8 @@ public static List Format ( preserveTrailingSpaces, tabWidth, textDirection, - multiLine + multiLine, + textFormatter ); } @@ -1397,6 +1478,7 @@ public static List Format ( /// The number of columns used for a tab. /// The text direction. /// If new lines are allowed. + /// instance to access any of his objects. /// A list of word wrapped lines. /// /// An empty string will result in one empty line. @@ -1411,7 +1493,8 @@ public static List Format ( bool preserveTrailingSpaces = false, int tabWidth = 0, TextDirection textDirection = TextDirection.LeftRight_TopBottom, - bool multiLine = false + bool multiLine = false, + TextFormatter textFormatter = null ) { if (width < 0) @@ -1457,14 +1540,14 @@ public static List Format ( foreach (string line in lines) { - lineResult.Add (ClipAndJustify (line, width, justify, textDirection, tabWidth)); + lineResult.Add (ClipAndJustify (line, width, justify, textDirection, tabWidth, textFormatter)); } return lineResult; } text = ReplaceCRLFWithSpace (text); - lineResult.Add (ClipAndJustify (text, width, justify, textDirection, tabWidth)); + lineResult.Add (ClipAndJustify (text, width, justify, textDirection, tabWidth, textFormatter)); return lineResult; } @@ -1485,7 +1568,8 @@ public static List Format ( width, preserveTrailingSpaces, tabWidth, - textDirection + textDirection, + textFormatter ); foreach (string line in wrappedLines) @@ -1507,7 +1591,8 @@ public static List Format ( width, preserveTrailingSpaces, tabWidth, - textDirection + textDirection, + textFormatter )) { lineResult.Add (ClipAndJustify (line, width, justify, textDirection, tabWidth)); @@ -1592,7 +1677,7 @@ public static int GetWidestLineLength (string text, int tabWidth = 0) /// The length. /// The number of columns used for a tab. /// The maximum characters width. - public static int GetWidestLineLength ( + public static int GetColumnsRequiredForVerticalText ( List lines, int startIndex = -1, int length = -1, @@ -1644,27 +1729,32 @@ public static int GetSumMaxCharWidth (string text, int startIndex = -1, int leng return max; } - /// Gets the number of the Runes in the text that will fit in . + /// Gets the number of the Runes in the text that will fit in . /// /// This API will return incorrect results if the text includes glyphs who's width is dependent on surrounding /// glyphs (e.g. Arabic). /// /// The text. - /// The width. - /// The number of columns used for a tab. + /// The width. + /// The width used for a tab. + /// The text direction. /// The index of the text that fit the width. - public static int GetLengthThatFits (string text, int columns, int tabWidth = 0) { return GetLengthThatFits (text?.ToRuneList (), columns, tabWidth); } + public static int GetLengthThatFits (string text, int width, int tabWidth = 0, TextDirection textDirection = TextDirection.LeftRight_TopBottom) + { + return GetLengthThatFits (text?.ToRuneList (), width, tabWidth, textDirection); + } - /// Gets the number of the Runes in a list of Runes that will fit in . + /// Gets the number of the Runes in a list of Runes that will fit in . /// /// This API will return incorrect results if the text includes glyphs who's width is dependent on surrounding /// glyphs (e.g. Arabic). /// /// 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, int tabWidth = 0) + /// The width. + /// The width used for a tab. + /// The text direction. + /// The index of the last Rune in that fit in . + public static int GetLengthThatFits (List runes, int width, int tabWidth = 0, TextDirection textDirection = TextDirection.LeftRight_TopBottom) { if (runes is null || runes.Count == 0) { @@ -1676,9 +1766,9 @@ public static int GetLengthThatFits (List runes, int columns, int tabWidth for (; runeIdx < runes.Count; runeIdx++) { - int runeWidth = GetRuneWidth (runes [runeIdx], tabWidth); + int runeWidth = GetRuneWidth (runes [runeIdx], tabWidth, textDirection); - if (runesLength + runeWidth > columns) + if (runesLength + runeWidth > width) { break; } @@ -1689,12 +1779,12 @@ public static int GetLengthThatFits (List runes, int columns, int tabWidth 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 (string str, int tabWidth, TextDirection textDirection = TextDirection.LeftRight_TopBottom) { return GetRuneWidth (str.EnumerateRunes ().ToList (), tabWidth, textDirection); } + private static int GetRuneWidth (List runes, int tabWidth, TextDirection textDirection = TextDirection.LeftRight_TopBottom) { return runes.Sum (r => GetRuneWidth (r, tabWidth, textDirection)); } - private static int GetRuneWidth (Rune rune, int tabWidth) + private static int GetRuneWidth (Rune rune, int tabWidth, TextDirection textDirection = TextDirection.LeftRight_TopBottom) { - int runeWidth = rune.GetColumns (); + int runeWidth = IsHorizontalDirection (textDirection) ? rune.GetColumns () : 1; if (rune.Value == '\t') { diff --git a/Terminal.Gui/View/ViewText.cs b/Terminal.Gui/View/ViewText.cs index 96c33e56e6..eb98e8e13b 100644 --- a/Terminal.Gui/View/ViewText.cs +++ b/Terminal.Gui/View/ViewText.cs @@ -334,7 +334,7 @@ bool GetMinimumSizeOfText (out Size sizeRequired) switch (TextFormatter.IsVerticalDirection (TextDirection)) { case true: - int colWidth = TextFormatter.GetWidestLineLength (new List { TextFormatter.Text }, 0, 1); + int colWidth = TextFormatter.GetColumnsRequiredForVerticalText (new List { TextFormatter.Text }, 0, 1); // TODO: v2 - This uses frame.Width; it should only use Viewport if (_frame.Width < colWidth diff --git a/UICatalog/Scenarios/TextAlignmentsAndDirection.cs b/UICatalog/Scenarios/TextAlignmentsAndDirection.cs index 52c7c5b5ed..32e5d4b360 100644 --- a/UICatalog/Scenarios/TextAlignmentsAndDirection.cs +++ b/UICatalog/Scenarios/TextAlignmentsAndDirection.cs @@ -268,7 +268,7 @@ public override void Main () X = 0, Y = Pos.Bottom (txtLabelHJ), Width = Dim.Fill (31), - Height = Dim.Fill (6), + Height = Dim.Fill (4), ColorScheme = color2 }; @@ -284,6 +284,7 @@ public override void Main () ColorScheme = color1, Text = txt }; + txtLabelTL.TextFormatter.MultiLine = true; var txtLabelTC = new Label { @@ -297,6 +298,7 @@ public override void Main () ColorScheme = color1, Text = txt }; + txtLabelTC.TextFormatter.MultiLine = true; var txtLabelTR = new Label { @@ -310,6 +312,7 @@ public override void Main () ColorScheme = color1, Text = txt }; + txtLabelTR.TextFormatter.MultiLine = true; var txtLabelML = new Label { @@ -323,6 +326,7 @@ public override void Main () ColorScheme = color1, Text = txt }; + txtLabelML.TextFormatter.MultiLine = true; var txtLabelMC = new Label { @@ -336,6 +340,7 @@ public override void Main () ColorScheme = color1, Text = txt }; + txtLabelMC.TextFormatter.MultiLine = true; var txtLabelMR = new Label { @@ -349,6 +354,7 @@ public override void Main () ColorScheme = color1, Text = txt }; + txtLabelMR.TextFormatter.MultiLine = true; var txtLabelBL = new Label { @@ -362,6 +368,7 @@ public override void Main () ColorScheme = color1, Text = txt }; + txtLabelBL.TextFormatter.MultiLine = true; var txtLabelBC = new Label { @@ -375,6 +382,7 @@ public override void Main () ColorScheme = color1, Text = txt }; + txtLabelBC.TextFormatter.MultiLine = true; var txtLabelBR = new Label { @@ -388,6 +396,7 @@ public override void Main () ColorScheme = color1, Text = txt }; + txtLabelBR.TextFormatter.MultiLine = true; mtxts.Add (txtLabelTL); mtxts.Add (txtLabelTC); @@ -473,36 +482,43 @@ public override void Main () Text = "Justify" }; - justifyCheckbox.Toggled += (s, e) => - { - if (e.OldValue == true) - { - foreach (Label t in mtxts) - { - t.TextAlignment = (TextAlignment)((dynamic)t.Data).h; - t.VerticalTextAlignment = (VerticalTextAlignment)((dynamic)t.Data).v; - } - } - else - { - foreach (Label t in mtxts) - { - if (TextFormatter.IsVerticalDirection (t.TextDirection)) - { - t.VerticalTextAlignment = VerticalTextAlignment.Justified; - t.TextAlignment = ((dynamic)t.Data).h; - } - else - { - t.TextAlignment = TextAlignment.Justified; - t.VerticalTextAlignment = ((dynamic)t.Data).v; - } - } - } - }; + justifyCheckbox.Toggled += (s, e) => ToggleJustify (e.OldValue is { } && (bool)e.OldValue); app.Add (justifyCheckbox); + // WRAP CHECKBOX + + var wrapCheckbox = new CheckBox + { + X = Pos.Right (container) + 1, + Y = Pos.Y (justifyCheckbox) + 1, + AutoSize = false, + Width = Dim.Fill (10), + Height = 1, + Text = "Word Wrap", + Checked = true + }; + + wrapCheckbox.Toggled += (s, e) => + { + if (e.OldValue == true) + { + foreach (Label t in mtxts) + { + t.TextFormatter.WordWrap = false; + } + } + else + { + foreach (Label t in mtxts) + { + t.TextFormatter.WordWrap = true; + } + } + }; + + app.Add (wrapCheckbox); + // Direction Options List directionsEnum = Enum.GetValues (typeof (TextDirection)).Cast ().ToList (); @@ -510,7 +526,7 @@ public override void Main () var directionOptions = new RadioGroup { X = Pos.Right (container) + 1, - Y = Pos.Bottom (justifyCheckbox) + 1, + Y = Pos.Bottom (wrapCheckbox) + 1, Width = Dim.Fill (10), Height = Dim.Fill (1), HotKeySpecifier = (Rune)'\xffff', @@ -519,15 +535,54 @@ public override void Main () directionOptions.SelectedItemChanged += (s, ev) => { + var justChecked = justifyCheckbox.Checked is { } && (bool)justifyCheckbox.Checked; + + if (justChecked) + { + ToggleJustify (true); + } foreach (Label v in mtxts) { v.TextDirection = (TextDirection)ev.SelectedItem; } + + if (justChecked) + { + ToggleJustify (false); + } }; app.Add (directionOptions); Application.Run (app); app.Dispose (); + + void ToggleJustify (bool oldValue) + { + if (oldValue == true) + { + foreach (Label t in mtxts) + { + t.TextAlignment = (TextAlignment)((dynamic)t.Data).h; + t.VerticalTextAlignment = (VerticalTextAlignment)((dynamic)t.Data).v; + } + } + else + { + foreach (Label t in mtxts) + { + if (TextFormatter.IsVerticalDirection (t.TextDirection)) + { + t.VerticalTextAlignment = VerticalTextAlignment.Justified; + t.TextAlignment = ((dynamic)t.Data).h; + } + else + { + t.TextAlignment = TextAlignment.Justified; + t.VerticalTextAlignment = ((dynamic)t.Data).v; + } + } + } + } } } diff --git a/UnitTests/Text/TextFormatterTests.cs b/UnitTests/Text/TextFormatterTests.cs index 6f16a22156..848abacda0 100644 --- a/UnitTests/Text/TextFormatterTests.cs +++ b/UnitTests/Text/TextFormatterTests.cs @@ -961,26 +961,36 @@ public void GetMaxColsForWidth_With_Combining_Runes () Assert.Equal (1, TextFormatter.GetMaxColsForWidth (text, 1)); } + [Theory] + [InlineData (new [] { "0123456789" }, 1)] + [InlineData (new [] { "Hello World" }, 1)] + [InlineData (new [] { "Hello", "World" }, 2)] + [InlineData (new [] { "こんにちは", "世界" }, 4)] + public void GetColumnsRequiredForVerticalText_List_GetsWidth (IEnumerable text, int expectedWidth) + { + Assert.Equal (expectedWidth, TextFormatter.GetColumnsRequiredForVerticalText (text.ToList ())); + } + [Theory] [InlineData (new [] { "Hello", "World" }, 2, 1, 1, 1)] [InlineData (new [] { "こんにちは", "世界" }, 4, 1, 1, 2)] - public void GetWidestLineLength_List_Simple_And_Wide_Runes ( + public void GetColumnsRequiredForVerticalText_List_Simple_And_Wide_Runes ( IEnumerable text, - int width, + int expectedWidth, int index, int length, - int indexWidth + int expectedIndexWidth ) { - Assert.Equal (width, TextFormatter.GetWidestLineLength (text.ToList ())); - Assert.Equal (indexWidth, TextFormatter.GetWidestLineLength (text.ToList (), index, length)); + Assert.Equal (expectedWidth, TextFormatter.GetColumnsRequiredForVerticalText (text.ToList ())); + Assert.Equal (expectedIndexWidth, TextFormatter.GetColumnsRequiredForVerticalText (text.ToList (), index, length)); } [Fact] - public void GetWidestLineLength_List_With_Combining_Runes () + public void GetColumnsRequiredForVerticalText_List_With_Combining_Runes () { List text = new () { "Les Mis", "e\u0328\u0301", "rables" }; - Assert.Equal (1, TextFormatter.GetWidestLineLength (text, 1, 1)); + Assert.Equal (1, TextFormatter.GetColumnsRequiredForVerticalText (text, 1, 1)); } [Fact] @@ -3298,4 +3308,455 @@ public void WordWrap_WithNewLines (string text, int maxWidth, int widthOffset, I ); Assert.Equal (resultLines, wrappedLines); } + + [SetupFakeDriver] + [Theory] + [InlineData ("Hello World", 15, 1, "Hello World")] + [InlineData ("Well Done\nNice Work", 15, 2, @" +Well Done +Nice Work")] + [InlineData ("你好 世界", 15, 1, "你好 世界")] + [InlineData ("做 得好\n幹 得好", 15, 2, @" +做 得好 +幹 得好")] + public void Justify_Horizontal (string text, int width, int height, string expectedText) + { + TextFormatter tf = new () + { + Text = text, + Alignment = TextAlignment.Justified, + Size = new Size (width, height), + MultiLine = true + }; + + tf.Draw (new Rectangle (0, 0, width, height), Attribute.Default, Attribute.Default); + + TestHelpers.AssertDriverContentsWithFrameAre (expectedText, _output); + } + + [SetupFakeDriver] + [Theory] + [InlineData ("Hello World", 1, 15, "H\ne\nl\nl\no\n \n \n \n \n \nW\no\nr\nl\nd")] + [InlineData ("Well Done\nNice Work", 2, 15, @" +WN +ei +lc +le + + + + + + + +DW +oo +nr +ek")] + [InlineData ("你好 世界", 2, 15, "你\n好\n \n \n \n \n \n \n \n \n \n \n \n世\n界")] + [InlineData ("做 得好\n幹 得好", 4, 15, @" +做幹 + + + + + + + + + + + + +得得 +好好")] + public void Justify_Vertical (string text, int width, int height, string expectedText) + { + TextFormatter tf = new () + { + Text = text, + Direction = TextDirection.TopBottom_LeftRight, + VerticalAlignment = VerticalTextAlignment.Justified, + Size = new Size (width, height), + MultiLine = true + }; + + tf.Draw (new Rectangle (0, 0, width, height), Attribute.Default, Attribute.Default); + + TestHelpers.AssertDriverContentsWithFrameAre (expectedText, _output); + } + + [SetupFakeDriver] + [Theory] + [InlineData ("A", 0, false, "")] + [InlineData ("A", 1, false, "A")] + [InlineData ("A", 2, false, "A")] + [InlineData ("AB", 1, false, "A")] + [InlineData ("AB", 2, false, "AB")] + [InlineData ("ABC", 3, false, "ABC")] + [InlineData ("ABC", 4, false, "ABC")] + [InlineData ("ABC", 6, false, "ABC")] + + [InlineData ("A", 0, true, "")] + [InlineData ("A", 1, true, "A")] + [InlineData ("A", 2, true, "A")] + [InlineData ("AB", 1, true, "A")] + [InlineData ("AB", 2, true, "AB")] + [InlineData ("ABC", 3, true, "ABC")] + [InlineData ("ABC", 4, true, "ABC")] + [InlineData ("ABC", 6, true, "ABC")] + public void Draw_Horizontal_Left (string text, int width, bool autoSize, string expectedText) + + { + TextFormatter tf = new () + { + Text = text, + Alignment = TextAlignment.Left, + AutoSize = autoSize, + }; + + if (!autoSize) + { + tf.Size = new Size (width, 1); + } + tf.Draw (new Rectangle (0, 0, width, 1), Attribute.Default, Attribute.Default); + + TestHelpers.AssertDriverContentsWithFrameAre (expectedText, _output); + } + + [SetupFakeDriver] + [Theory] + [InlineData ("A", 0, false, "")] + [InlineData ("A", 1, false, "A")] + [InlineData ("A", 2, false, " A")] + [InlineData ("AB", 1, false, "B")] + [InlineData ("AB", 2, false, "AB")] + [InlineData ("ABC", 3, false, "ABC")] + [InlineData ("ABC", 4, false, " ABC")] + [InlineData ("ABC", 6, false, " ABC")] + + [InlineData ("A", 0, true, "")] + [InlineData ("A", 1, true, "A")] + [InlineData ("A", 2, true, " A")] + [InlineData ("AB", 1, true, "B")] // FIXED: Should be "B". See https://github.com/gui-cs/Terminal.Gui/issues/3418#issuecomment-2067771418 for a partial fix + [InlineData ("AB", 2, true, "AB")] + [InlineData ("ABC", 3, true, "ABC")] + [InlineData ("ABC", 4, true, " ABC")] + [InlineData ("ABC", 6, true, " ABC")] + public void Draw_Horizontal_Right (string text, int width, bool autoSize, string expectedText) + { + TextFormatter tf = new () + { + Text = text, + Alignment = TextAlignment.Right, + AutoSize = autoSize, + }; + + if (!autoSize) + { + tf.Size = new Size (width, 1); + } + + tf.Draw (new Rectangle (Point.Empty, new (width, 1)), Attribute.Default, Attribute.Default); + TestHelpers.AssertDriverContentsWithFrameAre (expectedText, _output); + } + + [SetupFakeDriver] + [Theory] + [InlineData ("A", 0, false, "")] + [InlineData ("A", 1, false, "A")] + [InlineData ("A", 2, false, "A")] + [InlineData ("A", 3, false, " A")] + [InlineData ("AB", 1, false, "A")] + [InlineData ("AB", 2, false, "AB")] + [InlineData ("ABC", 2, false, "AB")] + [InlineData ("ABC", 3, false, "ABC")] + [InlineData ("ABC", 4, false, "ABC")] + [InlineData ("ABC", 5, false, " ABC")] + [InlineData ("ABC", 6, false, " ABC")] + [InlineData ("ABC", 9, false, " ABC")] + [InlineData ("ABCD", 2, false, "BC")] + + [InlineData ("A", 0, true, "")] + [InlineData ("A", 1, true, "A")] + [InlineData ("A", 2, true, "A")] + [InlineData ("A", 3, true, " A")] + [InlineData ("AB", 1, true, "A")] + [InlineData ("AB", 2, true, "AB")] + [InlineData ("ABC", 2, true, "AB")] + [InlineData ("ABC", 3, true, "ABC")] + [InlineData ("ABC", 4, true, "ABC")] + [InlineData ("ABC", 5, true, " ABC")] + [InlineData ("ABC", 6, true, " ABC")] + [InlineData ("ABC", 9, true, " ABC")] + [InlineData ("ABCD", 2, true, " C")] + public void Draw_Horizontal_Centered (string text, int width, bool autoSize, string expectedText) + { + TextFormatter tf = new () + { + Text = text, + Alignment = TextAlignment.Centered, + AutoSize = autoSize, + }; + + if (!autoSize) + { + tf.Size = new Size (width, 1); + } + tf.Draw (new Rectangle (0, 0, width, 1), Attribute.Default, Attribute.Default); + + TestHelpers.AssertDriverContentsWithFrameAre (expectedText, _output); + } + + [SetupFakeDriver] + [Theory] + [InlineData ("A", 0, false, "")] + [InlineData ("A", 1, false, "A")] + [InlineData ("A", 2, false, "A")] + [InlineData ("A B", 1, false, "A")] + [InlineData ("A B", 2, false, "A")] + [InlineData ("A B", 3, false, "A B")] + [InlineData ("A B", 4, false, "A B")] + [InlineData ("A B", 5, false, "A B")] + [InlineData ("A B", 6, false, "A B")] + [InlineData ("A B", 10, false, "A B")] + [InlineData ("ABC ABC", 10, false, "ABC ABC")] + + [InlineData ("A", 0, true, "")] + [InlineData ("A", 1, true, "A")] + [InlineData ("A", 2, true, "A")] + [InlineData ("A B", 1, true, "A")] + [InlineData ("A B", 2, true, "A")] + [InlineData ("A B", 3, true, "A B")] + [InlineData ("A B", 4, true, "A B")] + [InlineData ("A B", 5, true, "A B")] + [InlineData ("A B", 6, true, "A B")] + [InlineData ("A B", 10, true, "A B")] + [InlineData ("ABC ABC", 10, true, "ABC ABC")] + public void Draw_Horizontal_Justified (string text, int width, bool autoSize, string expectedText) + { + TextFormatter tf = new () + { + Text = text, + Alignment = TextAlignment.Justified, + AutoSize = autoSize, + }; + + if (!autoSize) + { + tf.Size = new Size (width, 1); + } + tf.Draw (new Rectangle (0, 0, width, 1), Attribute.Default, Attribute.Default); + + TestHelpers.AssertDriverContentsWithFrameAre (expectedText, _output); + } + + [SetupFakeDriver] + [Theory] + [InlineData ("A", 0, 1, false, "", 0)] + [InlineData ("A", 1, 1, false, "A", 0)] + [InlineData ("A", 2, 2, false, " A", 1)] + [InlineData ("AB", 1, 1, false, "B", 0)] + [InlineData ("AB", 2, 2, false, " A\n B", 0)] + [InlineData ("ABC", 3, 2, false, " B\n C", 0)] + [InlineData ("ABC", 4, 2, false, " B\n C", 0)] + [InlineData ("ABC", 6, 2, false, " B\n C", 0)] + [InlineData ("こんにちは", 0, 1, false, "", 0)] + [InlineData ("こんにちは", 1, 0, false, "", 0)] + [InlineData ("こんにちは", 1, 1, false, "", 0)] + [InlineData ("こんにちは", 2, 1, false, "は", 0)] + [InlineData ("こんにちは", 2, 2, false, "ち\nは", 0)] + [InlineData ("こんにちは", 2, 3, false, "に\nち\nは", 0)] + [InlineData ("こんにちは", 2, 4, false, "ん\nに\nち\nは", 0)] + [InlineData ("こんにちは", 2, 5, false, "こ\nん\nに\nち\nは", 0)] + [InlineData ("こんにちは", 2, 6, false, "こ\nん\nに\nち\nは", 1)] + [InlineData ("ABCD\nこんにちは", 4, 7, false, " こ\n Aん\n Bに\n Cち\n Dは", 2)] + [InlineData ("こんにちは\nABCD", 3, 7, false, "こ \nんA\nにB\nちC\nはD", 2)] + + [InlineData ("A", 0, 1, true, "", 0)] + [InlineData ("A", 1, 1, true, "A", 0)] + [InlineData ("A", 2, 2, true, " A", 1)] + [InlineData ("AB", 1, 1, true, "B", 0)] + [InlineData ("AB", 2, 2, true, " A\n B", 0)] + [InlineData ("ABC", 3, 2, true, " B\n C", 0)] + [InlineData ("ABC", 4, 2, true, " B\n C", 0)] + [InlineData ("ABC", 6, 2, true, " B\n C", 0)] + [InlineData ("こんにちは", 0, 1, true, "", 0)] + [InlineData ("こんにちは", 1, 0, true, "", 0)] + [InlineData ("こんにちは", 1, 1, true, "", 0)] + [InlineData ("こんにちは", 2, 1, true, "は", 0)] + [InlineData ("こんにちは", 2, 2, true, "ち\nは", 0)] + [InlineData ("こんにちは", 2, 3, true, "に\nち\nは", 0)] + [InlineData ("こんにちは", 2, 4, true, "ん\nに\nち\nは", 0)] + [InlineData ("こんにちは", 2, 5, true, "こ\nん\nに\nち\nは", 0)] + [InlineData ("こんにちは", 2, 6, true, "こ\nん\nに\nち\nは", 1)] + [InlineData ("ABCD\nこんにちは", 4, 7, true, " こ\n Aん\n Bに\n Cち\n Dは", 2)] + [InlineData ("こんにちは\nABCD", 3, 7, true, "こ \nんA\nにB\nちC\nはD", 2)] + public void Draw_Vertical_Bottom_Horizontal_Right (string text, int width, int height, bool autoSize, string expectedText, int expectedY) + { + TextFormatter tf = new () + { + Text = text, + Alignment = TextAlignment.Right, + Direction = TextDirection.TopBottom_LeftRight, + VerticalAlignment = VerticalTextAlignment.Bottom, + AutoSize = autoSize, + }; + + if (!autoSize) + { + tf.Size = new Size (width, height); + } + + tf.Draw (new Rectangle (Point.Empty, new (width, height)), Attribute.Default, Attribute.Default); + Rectangle rect = TestHelpers.AssertDriverContentsWithFrameAre (expectedText, _output); + Assert.Equal (expectedY, rect.Y); + } + + [SetupFakeDriver] + [Theory] + [InlineData ("A", 5, false, "A")] + [InlineData ("AB12", 5, false, @" +A +B +1 +2")] + [InlineData ("AB\n12", 5, false, @" +A1 +B2")] + [InlineData ("", 1, false, "")] + [InlineData ("AB1 2", 2, false, @" +A12 +B ")] + [InlineData ("こんにちは", 1, false, @" +こん")] + [InlineData ("こんにちは", 2, false, @" +こに +んち")] + [InlineData ("こんにちは", 5, false, @" +こ +ん +に +ち +は")] + + [InlineData ("A", 5, true, "A")] + [InlineData ("AB12", 5, true, @" +A +B +1 +2")] + [InlineData ("AB\n12", 5, true, @" +A1 +B2")] + [InlineData ("", 1, true, "")] + [InlineData ("AB1 2", 2, true, @" +A +B")] + [InlineData ("こんにちは", 1, true, @" +こ")] + [InlineData ("こんにちは", 2, true, @" +こ +ん")] + [InlineData ("こんにちは", 5, true, @" +こ +ん +に +ち +は")] + public void Draw_Vertical_TopBottom_LeftRight_Top (string text, int height, bool autoSize, string expectedText) + { + TextFormatter tf = new () + { + Text = text, + AutoSize = autoSize, + Direction = TextDirection.TopBottom_LeftRight, + }; + + if (!autoSize) + { + tf.Size = new Size (5, height); + } + tf.Draw (new Rectangle (0, 0, 5, height), Attribute.Default, Attribute.Default); + + TestHelpers.AssertDriverContentsWithFrameAre (expectedText, _output); + } + + [SetupFakeDriver] + [Theory] + + // The expectedY param is to probe that the expectedText param start at that Y coordinate + + [InlineData ("A", 0, false, "", 0)] + [InlineData ("A", 1, false, "A", 0)] + [InlineData ("A", 2, false, "A", 0)] + [InlineData ("A", 3, false, "A", 1)] + [InlineData ("AB", 1, false, "A", 0)] + [InlineData ("AB", 2, false, "A\nB", 0)] + [InlineData ("ABC", 2, false, "A\nB", 0)] + [InlineData ("ABC", 3, false, "A\nB\nC", 0)] + [InlineData ("ABC", 4, false, "A\nB\nC", 0)] + [InlineData ("ABC", 5, false, "A\nB\nC", 1)] + [InlineData ("ABC", 6, false, "A\nB\nC", 1)] + [InlineData ("ABC", 9, false, "A\nB\nC", 3)] + [InlineData ("ABCD", 2, false, "B\nC", 0)] + [InlineData ("こんにちは", 0, false, "", 0)] + [InlineData ("こんにちは", 1, false, "に", 0)] + [InlineData ("こんにちは", 2, false, "ん\nに", 0)] + [InlineData ("こんにちは", 3, false, "ん\nに\nち", 0)] + [InlineData ("こんにちは", 4, false, "こ\nん\nに\nち", 0)] + [InlineData ("こんにちは", 5, false, "こ\nん\nに\nち\nは", 0)] + [InlineData ("こんにちは", 6, false, "こ\nん\nに\nち\nは", 0)] + [InlineData ("ABCD\nこんにちは", 7, false, "Aこ\nBん\nCに\nDち\n は", 1)] + [InlineData ("こんにちは\nABCD", 7, false, "こA\nんB\nにC\nちD\nは ", 1)] + + [InlineData ("A", 0, true, "", 0)] + [InlineData ("A", 1, true, "A", 0)] + [InlineData ("A", 2, true, "A", 0)] + [InlineData ("A", 3, true, "A", 1)] + [InlineData ("AB", 1, true, "A", 0)] + [InlineData ("AB", 2, true, "A\nB", 0)] + [InlineData ("ABC", 2, true, "A\nB", 0)] + [InlineData ("ABC", 3, true, "A\nB\nC", 0)] + [InlineData ("ABC", 4, true, "A\nB\nC", 0)] + [InlineData ("ABC", 5, true, "A\nB\nC", 1)] + [InlineData ("ABC", 6, true, "A\nB\nC", 1)] + [InlineData ("ABC", 9, true, "A\nB\nC", 3)] + [InlineData ("ABCD", 2, true, "B\nC", 0)] + [InlineData ("こんにちは", 0, true, "", 0)] + [InlineData ("こんにちは", 1, true, "に", 0)] + [InlineData ("こんにちは", 2, true, "ん\nに", 0)] + [InlineData ("こんにちは", 3, true, "ん\nに\nち", 0)] + [InlineData ("こんにちは", 4, true, "こ\nん\nに\nち", 0)] + [InlineData ("こんにちは", 5, true, "こ\nん\nに\nち\nは", 0)] + [InlineData ("こんにちは", 6, true, "こ\nん\nに\nち\nは", 0)] + [InlineData ("こんにちは", 7, true, "こ\nん\nに\nち\nは", 1)] + [InlineData ("ABCD\nこんにちは", 7, true, "Aこ\nBん\nCに\nDち\n は", 1)] + [InlineData ("こんにちは\nABCD", 7, true, "こA\nんB\nにC\nちD\nは ", 1)] + public void Draw_Vertical_TopBottom_LeftRight_Middle (string text, int height, bool autoSize, string expectedText, int expectedY) + { + TextFormatter tf = new () + { + Text = text, + Direction = TextDirection.TopBottom_LeftRight, + VerticalAlignment = VerticalTextAlignment.Middle, + AutoSize = autoSize, + }; + + if (!autoSize) + { + int width = text.ToRunes ().Max (r => r.GetColumns ()); + + if (text.Contains ("\n")) + { + width++; + } + + tf.Size = new Size (width, height); + } + tf.Draw (new Rectangle (0, 0, 5, height), Attribute.Default, Attribute.Default); + + Rectangle rect = TestHelpers.AssertDriverContentsWithFrameAre (expectedText, _output); + Assert.Equal (expectedY, rect.Y); + } }