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