diff --git a/Terminal.Gui/Core/TextFormatter.cs b/Terminal.Gui/Core/TextFormatter.cs index f904f2099..3a8bfc461 100644 --- a/Terminal.Gui/Core/TextFormatter.cs +++ b/Terminal.Gui/Core/TextFormatter.cs @@ -27,6 +27,80 @@ namespace Terminal.Gui { Justified } + /// + /// Vertical text alignment enumeration, controls how text is displayed. + /// + public enum VerticalTextAlignment { + /// + /// Aligns the text to the top of the frame. + /// + Top, + /// + /// Aligns the text to the bottom of the frame. + /// + Bottom, + /// + /// Centers the text verticaly in the frame. + /// + Middle, + /// + /// Shows the text as justified text in the frame. + /// + Justified + } + + /// TextDirection [H] = Horizontal [V] = Vertical + /// ============= + /// LeftRight_TopBottom [H] Normal + /// TopBottom_LeftRight [V] Normal + /// + /// RightLeft_TopBottom [H] Invert Text + /// TopBottom_RightLeft [V] Invert Lines + /// + /// LeftRight_BottomTop [H] Invert Lines + /// BottomTop_LeftRight [V] Invert Text + /// + /// RightLeft_BottomTop [H] Invert Text + Invert Lines + /// BottomTop_RightLeft [V] Invert Text + Invert Lines + /// + /// + /// Text direction enumeration, controls how text is displayed. + /// + public enum TextDirection { + /// + /// Normal Horizontal + /// + LeftRight_TopBottom, + /// + /// Normal Vertical + /// + TopBottom_LeftRight, + /// + /// + /// + RightLeft_TopBottom, + /// + /// + /// + TopBottom_RightLeft, + /// + /// + /// + LeftRight_BottomTop, + /// + /// + /// + BottomTop_LeftRight, + /// + /// + /// + RightLeft_BottomTop, + /// + /// + /// + BottomTop_RightLeft + } + /// /// Provides text formatting capabilities for console apps. Supports, hotkeys, horizontal alignment, multiple lines, and word-based line wrap. /// @@ -34,6 +108,8 @@ namespace Terminal.Gui { List lines = new List (); ustring text; TextAlignment textAlignment; + VerticalTextAlignment textVerticalAlignment; + TextDirection textDirection; Attribute textColor = -1; bool needsFormat; Key hotKey; @@ -70,6 +146,90 @@ namespace Terminal.Gui { } } + /// + /// Controls the vertical text-alignment property. + /// + /// The text vertical alignment. + public VerticalTextAlignment VerticalAlignment { + get => textVerticalAlignment; + set { + textVerticalAlignment = value; + NeedsFormat = true; + } + } + + /// + /// Controls the text-direction property. + /// + /// The text vertical alignment. + public TextDirection Direction { + get => textDirection; + set { + textDirection = value; + NeedsFormat = true; + } + } + + /// + /// Check if it is a horizontal direction + /// + public static bool IsHorizontalDirection (TextDirection textDirection) + { + switch (textDirection) { + case TextDirection.LeftRight_TopBottom: + case TextDirection.LeftRight_BottomTop: + case TextDirection.RightLeft_TopBottom: + case TextDirection.RightLeft_BottomTop: + return true; + default: + return false; + } + } + + /// + /// Check if it is a vertical direction + /// + public static bool IsVerticalDirection (TextDirection textDirection) + { + switch (textDirection) { + case TextDirection.TopBottom_LeftRight: + case TextDirection.TopBottom_RightLeft: + case TextDirection.BottomTop_LeftRight: + case TextDirection.BottomTop_RightLeft: + return true; + default: + return false; + } + } + + /// + /// Check if it is Left to Right direction + /// + public static bool IsLeftToRight (TextDirection textDirection) + { + switch (textDirection) { + case TextDirection.LeftRight_TopBottom: + case TextDirection.LeftRight_BottomTop: + return true; + default: + return false; + } + } + + /// + /// Check if it is Top to Bottom direction + /// + public static bool IsTopToBottom (TextDirection textDirection) + { + switch (textDirection) { + case TextDirection.TopBottom_LeftRight: + case TextDirection.TopBottom_RightLeft: + return true; + default: + return false; + } + } + /// /// Gets or sets the size of the area the text will be constrained to when formatted. /// @@ -113,7 +273,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 { @@ -135,7 +295,13 @@ namespace Terminal.Gui { if (Size.IsEmpty) { throw new InvalidOperationException ("Size must be set before accessing Lines"); } - lines = Format (shown_text, Size.Width, textAlignment, Size.Height > 1); + + if (IsVerticalDirection (textDirection)) { + lines = Format (shown_text, Size.Height, textVerticalAlignment == VerticalTextAlignment.Justified, Size.Width > 1); + } else { + lines = Format (shown_text, Size.Width, textAlignment == TextAlignment.Justified, Size.Height > 1); + } + NeedsFormat = false; } return lines; @@ -256,6 +422,18 @@ namespace Terminal.Gui { /// Alignment. /// Justified and clipped text. public static ustring ClipAndJustify (ustring text, int width, TextAlignment talign) + { + return ClipAndJustify (text, width, talign == TextAlignment.Justified); + } + + /// + /// Justifies text within a specified width. + /// + /// The text to justify. + /// If the text length is greater that width it will be clipped. + /// Justify. + /// Justified and clipped text. + public static ustring ClipAndJustify (ustring text, int width, bool justify) { if (width < 0) { throw new ArgumentOutOfRangeException ("Width cannot be negative."); @@ -269,7 +447,7 @@ namespace Terminal.Gui { if (slen > width) { return ustring.Make (runes.GetRange (0, width)); } else { - if (talign == TextAlignment.Justified) { + if (justify) { return Justify (text, width); } return text; @@ -337,6 +515,31 @@ namespace Terminal.Gui { /// /// public static List Format (ustring text, int width, TextAlignment talign, bool wordWrap, bool preserveTrailingSpaces = false) + { + return Format (text, width, talign == TextAlignment.Justified, wordWrap, preserveTrailingSpaces); + } + + /// + /// Reformats text into lines, applying text alignment and optionally wrapping text to new lines on word boundaries. + /// + /// + /// The width to bound the text to for word wrapping and clipping. + /// Specifies whether the text should be justified. + /// If true, the text will be wrapped to new lines as need. If false, forces text to fit a single line. Line breaks are converted to spaces. The text will be clipped to width + /// If true and 'wordWrap' also true, the wrapped text will keep the trailing spaces. If false, the trailing spaces will be trimmed. + /// A list of word wrapped lines. + /// + /// + /// An empty text string will result in one empty line. + /// + /// + /// If width is 0, a single, empty line will be returned. + /// + /// + /// If width is int.MaxValue, the text will be formatted to the maximum width possible. + /// + /// + public static List Format (ustring text, int width, bool justify, bool wordWrap, bool preserveTrailingSpaces = false) { if (width < 0) { throw new ArgumentOutOfRangeException ("width cannot be negative"); @@ -353,7 +556,7 @@ namespace Terminal.Gui { if (wordWrap == false) { text = ReplaceCRLFWithSpace (text); - lineResult.Add (ClipAndJustify (text, width, talign)); + lineResult.Add (ClipAndJustify (text, width, justify)); return lineResult; } @@ -365,7 +568,7 @@ namespace Terminal.Gui { if (c == '\n') { var wrappedLines = WordWrap (ustring.Make (runes.GetRange (lp, i - lp)), width, preserveTrailingSpaces); foreach (var line in wrappedLines) { - lineResult.Add (ClipAndJustify (line, width, talign)); + lineResult.Add (ClipAndJustify (line, width, justify)); } if (wrappedLines.Count == 0) { lineResult.Add (ustring.Empty); @@ -374,7 +577,7 @@ namespace Terminal.Gui { } } foreach (var line in WordWrap (ustring.Make (runes.GetRange (lp, runeCount - lp)), width, preserveTrailingSpaces)) { - lineResult.Add (ClipAndJustify (line, width, talign)); + lineResult.Add (ClipAndJustify (line, width, justify)); } return lineResult; @@ -388,7 +591,7 @@ namespace Terminal.Gui { /// The minimum width for the text. public static int MaxLines (ustring text, int width) { - var result = TextFormatter.Format (text, width, TextAlignment.Left, true); + var result = TextFormatter.Format (text, width, false, true); return result.Count; } @@ -400,7 +603,7 @@ namespace Terminal.Gui { /// The minimum width for the text. public static int MaxWidth (ustring text, int width) { - var result = TextFormatter.Format (text, width, TextAlignment.Left, true); + var result = TextFormatter.Format (text, width, false, true); var max = 0; result.ForEach (s => { var m = 0; @@ -585,38 +788,111 @@ namespace Terminal.Gui { Application.Driver?.SetAttribute (normalColor); // Use "Lines" to ensure a Format (don't use "lines")) - for (int line = 0; line < Lines.Count; line++) { - if (line > bounds.Height) + + var linesFormated = Lines; + switch (textDirection) { + case TextDirection.TopBottom_RightLeft: + case TextDirection.LeftRight_BottomTop: + case TextDirection.RightLeft_BottomTop: + case TextDirection.BottomTop_RightLeft: + linesFormated.Reverse (); + break; + } + + for (int line = 0; line < linesFormated.Count; line++) { + var isVertical = IsVerticalDirection (textDirection); + + if ((isVertical && (line > bounds.Width)) || (!isVertical && (line > bounds.Height))) continue; + var runes = lines [line].ToRunes (); - int x; - switch (textAlignment) { - case TextAlignment.Left: - case TextAlignment.Justified: - x = bounds.Left; + + switch (textDirection) { + case TextDirection.RightLeft_BottomTop: + case TextDirection.RightLeft_TopBottom: + case TextDirection.BottomTop_LeftRight: + case TextDirection.BottomTop_RightLeft: + runes = runes.Reverse ().ToArray (); + break; + } + + // When text is justified, we lost left or right, so we use the direction to align. + + int x, y; + // Horizontal Alignment + if (textAlignment == TextAlignment.Right || (textAlignment == TextAlignment.Justified && !IsLeftToRight (textDirection))) { + if (isVertical) { + x = bounds.Right - Lines.Count + line; + CursorPosition = bounds.Width - Lines.Count + hotKeyPos; + } else { + x = bounds.Right - runes.Length; + CursorPosition = bounds.Width - runes.Length + hotKeyPos; + } + } else if (textAlignment == TextAlignment.Left || textAlignment == TextAlignment.Justified) { + if (isVertical) { + x = bounds.Left + line; + } else { + x = bounds.Left; + } CursorPosition = hotKeyPos; - break; - case TextAlignment.Right: - x = bounds.Right - runes.Length; - CursorPosition = bounds.Width - runes.Length + hotKeyPos; - break; - case TextAlignment.Centered: - x = bounds.Left + (bounds.Width - runes.Length) / 2; - CursorPosition = (bounds.Width - runes.Length) / 2 + hotKeyPos; - break; - default: + } else if (textAlignment == TextAlignment.Centered) { + if (isVertical) { + x = bounds.Left + line + ((bounds.Width - Lines.Count) / 2); + CursorPosition = (bounds.Width - Lines.Count) / 2 + hotKeyPos; + } else { + x = bounds.Left + (bounds.Width - runes.Length) / 2; + CursorPosition = (bounds.Width - runes.Length) / 2 + hotKeyPos; + } + } else { throw new ArgumentOutOfRangeException (); } - var col = bounds.Left; - for (var idx = bounds.Left; idx < bounds.Left + bounds.Width; idx++) { - Application.Driver?.Move (col, bounds.Top + line); + + // Vertical Alignment + if (textVerticalAlignment == VerticalTextAlignment.Bottom || (textVerticalAlignment == VerticalTextAlignment.Justified && !IsTopToBottom (textDirection))) { + if (isVertical) { + y = bounds.Bottom - runes.Length; + } else { + y = bounds.Bottom - Lines.Count + line; + } + } else if (textVerticalAlignment == VerticalTextAlignment.Top || textVerticalAlignment == VerticalTextAlignment.Justified) { + if (isVertical) { + y = bounds.Top; + } else { + y = bounds.Top + line; + } + } else if (textVerticalAlignment == VerticalTextAlignment.Middle) { + if (isVertical) { + var s = (bounds.Height - runes.Length) / 2; + y = bounds.Top + s; + } else { + var s = (bounds.Height - Lines.Count) / 2; + y = bounds.Top + line + s; + } + } else { + throw new ArgumentOutOfRangeException (); + } + + var start = isVertical ? bounds.Top : bounds.Left; + var size = isVertical ? bounds.Height : bounds.Width; + + var current = start; + for (var idx = start; idx < start + size; idx++) { var rune = (Rune)' '; - if (idx >= x && idx < (x + runes.Length)) { - rune = runes [idx - x]; + if (isVertical) { + Application.Driver?.Move (x, current); + if (idx >= y && idx < (y + runes.Length)) { + rune = runes [idx - y]; + } + } else { + Application.Driver?.Move (current, y); + if (idx >= x && idx < (x + runes.Length)) { + rune = runes [idx - x]; + } } if ((rune & HotKeyTagMask) == HotKeyTagMask) { - if (textAlignment == TextAlignment.Justified) { - CursorPosition = idx - bounds.Left; + if ((isVertical && textVerticalAlignment == VerticalTextAlignment.Justified) || + (!isVertical && textAlignment == TextAlignment.Justified)) { + CursorPosition = idx - start; } Application.Driver?.SetAttribute (hotColor); Application.Driver?.AddRune ((Rune)((uint)rune & ~HotKeyTagMask)); @@ -624,9 +900,8 @@ namespace Terminal.Gui { } else { Application.Driver?.AddRune (rune); } - col += Rune.ColumnWidth (rune); - if (idx + 1 > - 1 && idx + 1 < runes.Length && col - + Rune.ColumnWidth (runes [idx + 1]) > bounds.Width) { + current += Rune.ColumnWidth (rune); + if (idx + 1 < runes.Length && current + Rune.ColumnWidth (runes [idx + 1]) > size) { break; } } diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index 6d7a5ef4c..7925e5374 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -1995,6 +1995,30 @@ namespace Terminal.Gui { } } + /// + /// Gets or sets how the View's is aligned verticaly when drawn. Changing this property will redisplay the . + /// + /// The text alignment. + public virtual VerticalTextAlignment VerticalTextAlignment { + get => textFormatter.VerticalAlignment; + set { + textFormatter.VerticalAlignment = value; + SetNeedsDisplay (); + } + } + + /// + /// Gets or sets the direction of the View's . Changing this property will redisplay the . + /// + /// The text alignment. + public virtual TextDirection TextDirection { + get => textFormatter.Direction; + set { + textFormatter.Direction = value; + SetNeedsDisplay (); + } + } + /// /// Get or sets if the was already initialized. /// This derived from to allow notify all the views that are being initialized. diff --git a/UICatalog/Scenarios/TextAlignmentsAndDirection.cs b/UICatalog/Scenarios/TextAlignmentsAndDirection.cs new file mode 100644 index 000000000..e83e17fbe --- /dev/null +++ b/UICatalog/Scenarios/TextAlignmentsAndDirection.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Terminal.Gui; + +namespace UICatalog { + [ScenarioMetadata (Name: "Text Alignment and Direction", Description: "Demonstrates text alignment")] + [ScenarioCategory ("Text")] + class TextAlignmentsAndDirections : Scenario { + + public override void Setup () + { + // string txt = ".\n...\n.....\nHELLO\n.....\n...\n."; + // string txt = "┌──┴──┐\n┤HELLO├\n└──┬──┘"; + string txt = "HELLO WORLD"; + + var color1 = new ColorScheme { Normal = Application.Driver.MakeAttribute (Color.Black, Color.Gray) }; + var color2 = new ColorScheme { Normal = Application.Driver.MakeAttribute (Color.Black, Color.DarkGray) }; + + var txts = new List