diff --git a/Example/demo.cs b/Example/demo.cs
index ca9681517..222c93e89 100644
--- a/Example/demo.cs
+++ b/Example/demo.cs
@@ -628,7 +628,6 @@ static class Demo {
int count = 0;
ml = new Label (new Rect (3, 17, 47, 1), "Mouse: ");
Application.RootMouseEvent += delegate (MouseEvent me) {
- ml.TextColor = Colors.TopLevel.Normal;
ml.Text = $"Mouse: ({me.X},{me.Y}) - {me.Flags} {count++}";
};
diff --git a/Terminal.Gui/Core/ConsoleDriver.cs b/Terminal.Gui/Core/ConsoleDriver.cs
index dc709cd70..72f2be6fa 100644
--- a/Terminal.Gui/Core/ConsoleDriver.cs
+++ b/Terminal.Gui/Core/ConsoleDriver.cs
@@ -663,7 +663,7 @@ namespace Terminal.Gui {
if (!ustring.IsNullOrEmpty (title) && width > 4 && region.Y + paddingTop <= region.Y + paddingBottom) {
Move (region.X + 1 + paddingLeft, region.Y + paddingTop);
AddRune (' ');
- var str = title.Length >= width ? title [0, width - 2] : title;
+ var str = title.RuneCount >= width ? title [0, width - 2] : title;
AddStr (str);
AddRune (' ');
}
diff --git a/Terminal.Gui/Core/TextFormatter.cs b/Terminal.Gui/Core/TextFormatter.cs
new file mode 100644
index 000000000..4b9431f57
--- /dev/null
+++ b/Terminal.Gui/Core/TextFormatter.cs
@@ -0,0 +1,536 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using NStack;
+
+namespace Terminal.Gui {
+ ///
+ /// Suppports text formatting, including horizontal alignment and word wrap for .
+ ///
+ public class TextFormatter {
+ List lines = new List ();
+ ustring text;
+ TextAlignment textAlignment;
+ Attribute textColor = -1;
+ bool recalcPending = false;
+ Key hotKey;
+
+ ///
+ /// Inititalizes a new object.
+ ///
+ ///
+ public TextFormatter (View view)
+ {
+ recalcPending = true;
+ }
+
+ ///
+ /// The text to be displayed.
+ ///
+ public virtual ustring Text {
+ get => text;
+ set {
+ text = value;
+ recalcPending = true;
+ }
+ }
+
+ // TODO: Add Vertical Text Alignment
+ ///
+ /// Controls the horizontal text-alignment property.
+ ///
+ /// The text alignment.
+ public TextAlignment Alignment {
+ get => textAlignment;
+ set {
+ textAlignment = value;
+ recalcPending = true;
+ }
+ }
+
+ ///
+ /// Gets the size of the area the text will be drawn in.
+ ///
+ public Size Size { get; internal set; }
+
+
+ ///
+ /// The specifier character for the hotkey (e.g. '_'). Set to '\xffff' to disable hotkey support for this View instance. The default is '\xffff'.
+ ///
+ public Rune HotKeySpecifier { get; set; } = (Rune)0xFFFF;
+
+ ///
+ /// 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; }
+
+ ///
+ /// Gets the hotkey. Will be an upper case letter or digit.
+ ///
+ public Key HotKey { get => hotKey; internal set => hotKey = value; }
+
+ ///
+ /// Causes the Text to be formatted, based on and .
+ ///
+ public void ReFormat ()
+ {
+ // With this check, we protect against subclasses with overrides of Text
+ if (ustring.IsNullOrEmpty (Text)) {
+ return;
+ }
+ recalcPending = false;
+ var shown_text = text;
+ if (FindHotKey (text, HotKeySpecifier, true, out hotKeyPos, out hotKey)) {
+ shown_text = RemoveHotKeySpecifier (Text, hotKeyPos, HotKeySpecifier);
+ shown_text = ReplaceHotKeyWithTag (shown_text, hotKeyPos);
+ }
+ Reformat (shown_text, lines, Size.Width, textAlignment, Size.Height > 1);
+ }
+
+ static ustring StripWhiteCRLF (ustring str)
+ {
+ var runes = new List ();
+ foreach (var r in str.ToRunes ()) {
+ if (r != '\r' && r != '\n') {
+ runes.Add (r);
+ }
+ }
+ return ustring.Make (runes); ;
+ }
+ static ustring ReplaceCRLFWithSpace (ustring str)
+ {
+ var runes = new List ();
+ foreach (var r in str.ToRunes ()) {
+ if (r == '\r' || r == '\n') {
+ runes.Add (new Rune (' '));
+ } else {
+ runes.Add (r);
+ }
+ }
+ return ustring.Make (runes); ;
+ }
+
+ ///
+ /// Formats the provided text to fit within the width provided using word wrapping.
+ ///
+ /// The text to word warp
+ /// The width to contrain the text to
+ /// Returns a list of lines.
+ ///
+ /// Newlines ('\n' and '\r\n') sequences are honored.
+ ///
+ public static List WordWrap (ustring text, int width)
+ {
+ if (width < 0) {
+ throw new ArgumentOutOfRangeException ("Width cannot be negative.");
+ }
+
+ int start = 0, end;
+ var lines = new List ();
+
+ if (ustring.IsNullOrEmpty (text)) {
+ return lines;
+ }
+
+ text = StripWhiteCRLF (text);
+
+ while ((end = start + width) < text.RuneCount) {
+ while (text [end] != ' ' && end > start)
+ end -= 1;
+ if (end == start)
+ end = start + width;
+
+ lines.Add (text [start, end].TrimSpace ());
+ start = end;
+ }
+
+ if (start < text.RuneCount)
+ lines.Add (text.Substring (start).TrimSpace ());
+
+ return lines;
+ }
+
+ public static ustring ClipAndJustify (ustring text, int width, TextAlignment talign)
+ {
+ if (width < 0) {
+ throw new ArgumentOutOfRangeException ("Width cannot be negative.");
+ }
+ if (ustring.IsNullOrEmpty (text)) {
+ return text;
+ }
+
+ int slen = text.RuneCount;
+ if (slen > width) {
+ return text [0, width];
+ } else {
+ if (talign == TextAlignment.Justified) {
+ return Justify (text, width);
+ }
+ return text;
+ }
+ }
+
+ ///
+ /// Justifies the text to fill the width provided. Space will be added between words (demarked by spaces and tabs) to
+ /// make the text just fit width. Spaces will not be added to the ends.
+ ///
+ ///
+ ///
+ /// Character to replace whitespace and pad with. For debugging purposes.
+ /// The justifed text.
+ public static ustring Justify (ustring text, int width, char spaceChar = ' ')
+ {
+ if (width < 0) {
+ throw new ArgumentOutOfRangeException ("Width cannot be negative.");
+ }
+ if (ustring.IsNullOrEmpty (text)) {
+ return text;
+ }
+
+ // TODO: Use ustring
+ var words = text.ToString ().Split (whitespace, StringSplitOptions.RemoveEmptyEntries);
+ int textCount = words.Sum (arg => arg.Length);
+
+ var spaces = words.Length > 1 ? (width - textCount) / (words.Length - 1) : 0;
+ var extras = words.Length > 1 ? (width - textCount) % words.Length : 0;
+
+ var s = new System.Text.StringBuilder ();
+ //s.Append ($"tc={textCount} sp={spaces},x={extras} - ");
+ for (int w = 0; w < words.Length; w++) {
+ var x = words [w];
+ s.Append (x);
+ if (w + 1 < words.Length)
+ for (int i = 0; i < spaces; i++)
+ s.Append (spaceChar);
+ if (extras > 0) {
+ //s.Append ('_');
+ extras--;
+ }
+ }
+ return ustring.Make (s.ToString ());
+ }
+
+ static char [] whitespace = new char [] { ' ', '\t' };
+ private int hotKeyPos;
+
+ ///
+ /// Reformats text into lines, applying text alignment and word wraping.
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// if false, forces text to fit a single line. Line breaks are converted to spaces.
+ static void Reformat (ustring textStr, List lineResult, int width, TextAlignment talign, bool wordWrap)
+ {
+ lineResult.Clear ();
+
+ if (wordWrap == false) {
+ textStr = ReplaceCRLFWithSpace (textStr);
+ lineResult.Add (ClipAndJustify (textStr, width, talign));
+ return;
+ }
+
+ int runeCount = textStr.RuneCount;
+ int lp = 0;
+ for (int i = 0; i < runeCount; i++) {
+ Rune c = textStr [i];
+ if (c == '\n') {
+ var wrappedLines = WordWrap (textStr [lp, i], width);
+ foreach (var line in wrappedLines) {
+ lineResult.Add (ClipAndJustify (line, width, talign));
+ }
+ if (wrappedLines.Count == 0) {
+ lineResult.Add (ustring.Empty);
+ }
+ lp = i + 1;
+ }
+ }
+ foreach (var line in WordWrap (textStr [lp, runeCount], width)) {
+ lineResult.Add (ClipAndJustify (line, width, talign));
+ }
+ }
+
+ ///
+ /// Computes the number of lines needed to render the specified text given the width.
+ ///
+ /// Number of lines.
+ /// Text, may contain newlines.
+ /// The minimum width for the text.
+ public static int MaxLines (ustring text, int width)
+ {
+ var result = new List ();
+ TextFormatter.Reformat (text, result, width, TextAlignment.Left, true);
+ return result.Count;
+ }
+
+ ///
+ /// Computes the maximum width needed to render the text (single line or multple lines).
+ ///
+ /// Max width of lines.
+ /// Text, may contain newlines.
+ /// The minimum width for the text.
+ public static int MaxWidth (ustring text, int width)
+ {
+ var result = new List ();
+ TextFormatter.Reformat (text, result, width, TextAlignment.Left, true);
+ return result.Max (s => s.RuneCount);
+ }
+
+ internal void Draw (Rect bounds, Attribute normalColor, Attribute hotColor)
+ {
+ // With this check, we protect against subclasses with overrides of Text
+ if (ustring.IsNullOrEmpty (text)) {
+ return;
+ }
+
+ if (recalcPending) {
+ ReFormat ();
+ }
+
+ Application.Driver.SetAttribute (normalColor);
+
+ for (int line = 0; line < lines.Count; line++) {
+ if (line < (bounds.Height - bounds.Top) || line >= bounds.Height)
+ continue;
+ var str = lines [line];
+ int x;
+ switch (textAlignment) {
+ case TextAlignment.Left:
+ x = bounds.Left;
+ break;
+ case TextAlignment.Justified:
+ x = bounds.Left;
+ break;
+ case TextAlignment.Right:
+ x = bounds.Right - str.RuneCount;
+ break;
+ case TextAlignment.Centered:
+ x = bounds.Left + (bounds.Width - str.RuneCount) / 2;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException ();
+ }
+ int col = 0;
+ foreach (var rune in str) {
+ Application.Driver.Move (x + col, bounds.Y + line);
+ if ((rune & 0x100000) == 0x100000) {
+ Application.Driver.SetAttribute (hotColor);
+ Application.Driver.AddRune ((Rune)((uint)rune & ~0x100000));
+ Application.Driver.SetAttribute (normalColor);
+ } else {
+ Application.Driver.AddRune (rune);
+ }
+ col++;
+ }
+ }
+ }
+
+ ///
+ /// Calculates the rectangle requried to hold text, assuming no word wrapping.
+ ///
+ /// The x location of the rectangle
+ /// The y location of the rectangle
+ /// The text to measure
+ ///
+ public static Rect CalcRect (int x, int y, ustring text)
+ {
+ if (ustring.IsNullOrEmpty (text))
+ return Rect.Empty;
+
+ int mw = 0;
+ int ml = 1;
+
+ int cols = 0;
+ foreach (var rune in text) {
+ if (rune == '\n') {
+ ml++;
+ if (cols > mw)
+ mw = cols;
+ cols = 0;
+ } else {
+ if (rune != '\r') {
+ cols++;
+ }
+ }
+ }
+ if (cols > mw)
+ mw = cols;
+
+ return new Rect (x, y, mw, ml);
+ }
+
+ public static bool FindHotKey (ustring text, Rune hotKeySpecifier, bool firstUpperCase, out int hotPos, out Key hotKey)
+ {
+ if (ustring.IsNullOrEmpty (text) || hotKeySpecifier == (Rune)0xFFFF) {
+ hotPos = -1;
+ hotKey = Key.Unknown;
+ return false;
+ }
+
+ Rune hot_key = (Rune)0;
+ int hot_pos = -1;
+
+ // Use first hot_key char passed into 'hotKey'.
+ // TODO: Ignore hot_key of two are provided
+ // TODO: Do not support non-alphanumeric chars that can't be typed
+ int i = 0;
+ foreach (Rune c in text) {
+ if ((char)c != 0xFFFD) {
+ if (c == hotKeySpecifier) {
+ hot_pos = i;
+ } else if (hot_pos > -1) {
+ hot_key = c;
+ break;
+ }
+ }
+ i++;
+ }
+
+
+ // Legacy support - use first upper case char if the specifier was not found
+ if (hot_pos == -1 && firstUpperCase) {
+ i = 0;
+ foreach (Rune c in text) {
+ if ((char)c != 0xFFFD) {
+ if (Rune.IsUpper (c)) {
+ hot_key = c;
+ hot_pos = i;
+ break;
+ }
+ }
+ i++;
+ }
+ }
+
+ if (hot_key != (Rune)0 && hot_pos != -1) {
+ hotPos = hot_pos;
+
+ if (hot_key.IsValid && char.IsLetterOrDigit ((char)hot_key)) {
+ hotKey = (Key)char.ToUpperInvariant ((char)hot_key);
+ return true;
+ }
+ }
+
+ hotPos = -1;
+ hotKey = Key.Unknown;
+ return false;
+ }
+
+ public static ustring ReplaceHotKeyWithTag (ustring text, int hotPos)
+ {
+ // Set the high bit
+ var runes = text.ToRuneList ();
+ if (Rune.IsLetterOrNumber (runes [hotPos])) {
+ runes [hotPos] = new Rune ((uint)runes [hotPos] | 0x100000);
+ }
+ return ustring.Make (runes);
+ }
+
+ ///
+ /// Removes the hotkey specifier from text.
+ ///
+ /// The text to manipulate.
+ /// The hot-key specifier (e.g. '_') to look for.
+ /// Returns the postion of the hot-key in the text. -1 if not found.
+ /// The input text with the hotkey specifier ('_') removed.
+ public static ustring RemoveHotKeySpecifier (ustring text, int hotPos, Rune hotKeySpecifier)
+ {
+ if (ustring.IsNullOrEmpty (text)) {
+ return text;
+ }
+
+ // Scan
+ ustring start = ustring.Empty;
+ int i = 0;
+ foreach (Rune c in text) {
+ if (c == hotKeySpecifier && i == hotPos) {
+ i++;
+ continue;
+ }
+ start += ustring.Make (c);
+ i++;
+ }
+ return start;
+ }
+
+ ///
+ /// Formats a single line of text with a hot-key and .
+ ///
+ /// The text to align.
+ /// The maximum width for the text.
+ /// The hot-key position before reformatting.
+ /// The hot-key position after reformatting.
+ /// The to align to.
+ /// The aligned text.
+ public static ustring GetAlignedText (ustring shown_text, int width, int hot_pos, out int c_hot_pos, TextAlignment textAlignment)
+ {
+ int start;
+ var caption = shown_text;
+ c_hot_pos = hot_pos;
+
+ if (width > shown_text.RuneCount + 1) {
+ switch (textAlignment) {
+ case TextAlignment.Left:
+ caption += new string (' ', width - caption.RuneCount);
+ break;
+ case TextAlignment.Right:
+ start = width - caption.RuneCount;
+ caption = $"{new string (' ', width - caption.RuneCount)}{caption}";
+ if (c_hot_pos > -1) {
+ c_hot_pos += start;
+ }
+ break;
+ case TextAlignment.Centered:
+ start = width / 2 - caption.RuneCount / 2;
+ caption = $"{new string (' ', start)}{caption}{new string (' ', width - caption.RuneCount - start)}";
+ if (c_hot_pos > -1) {
+ c_hot_pos += start;
+ }
+ break;
+ case TextAlignment.Justified:
+ var words = caption.Split (" ");
+ var wLen = GetWordsLength (words, c_hot_pos, out int runeCount, out int w_hot_pos);
+ var space = (width - runeCount) / (caption.RuneCount - wLen);
+ caption = "";
+ for (int i = 0; i < words.Length; i++) {
+ if (i == words.Length - 1) {
+ caption += new string (' ', width - caption.RuneCount - 1);
+ caption += words [i];
+ } else {
+ caption += words [i];
+ }
+ if (i < words.Length - 1) {
+ caption += new string (' ', space);
+ }
+ }
+ if (c_hot_pos > -1) {
+ c_hot_pos += w_hot_pos * space - space - w_hot_pos + 1;
+ }
+ break;
+ }
+ }
+
+ return caption;
+ }
+
+ static int GetWordsLength (ustring [] words, int hotPos, out int runeCount, out int wordHotPos)
+ {
+ int length = 0;
+ int rCount = 0;
+ int wHotPos = -1;
+ for (int i = 0; i < words.Length; i++) {
+ if (wHotPos == -1 && rCount + words [i].RuneCount >= hotPos)
+ wHotPos = i;
+ length += words [i].Length;
+ rCount += words [i].RuneCount;
+ }
+ if (wHotPos == -1 && hotPos > -1)
+ wHotPos = words.Length;
+ runeCount = rCount;
+ wordHotPos = wHotPos;
+ return length;
+ }
+ }
+}
diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs
index 18b7abd8f..3ae35d9d0 100644
--- a/Terminal.Gui/Core/View.cs
+++ b/Terminal.Gui/Core/View.cs
@@ -132,482 +132,7 @@ namespace Terminal.Gui {
/// frames for the vies that use .
///
///
- public class View : Responder, IEnumerable {
- ///
- /// Suppports text formatting, including horizontal alignment and word wrap for .
- ///
- public class ViewText {
- List lines = new List ();
- ustring text;
- TextAlignment textAlignment;
- Attribute textColor = -1;
- View view;
-
- ///
- /// Inititalizes a new object.
- ///
- ///
- public ViewText (View view)
- {
- this.view = view;
- recalcPending = true;
- }
-
- ///
- /// The text to be displayed.
- ///
- public virtual ustring Text {
- get => text;
- set {
- text = value;
- recalcPending = true;
- view.SetNeedsDisplay ();
- }
- }
-
- // TODO: Add Vertical Text Alignment
- ///
- /// Controls the horizontal text-alignment property.
- ///
- /// The text alignment.
- public TextAlignment TextAlignment {
- get => textAlignment;
- set {
- textAlignment = value;
- recalcPending = true;
- view.SetNeedsDisplay ();
- }
- }
-
- ///
- /// The color used for the drawing of the .
- ///
- public Attribute TextColor {
- get => textColor;
- set {
- textColor = value;
- recalcPending = true;
- view.SetNeedsDisplay ();
- }
- }
-
- ///
- /// Gets the size of the area the text will be drawn in.
- ///
- public Size TextSize { get; internal set; }
-
- bool recalcPending = false;
-
- public int HotKeyPos { get => hotKeyPos; set => hotKeyPos = value; }
- public Rune HotKey { get => hotKey; set => hotKey = value; }
- Rune hotKey;
-
- ///
- /// The specifier character for the hotkey (e.g. '_'). Set to '\xffff' to disable hotkey support for this View instance. The default is '\xffff'.
- ///
- public Rune HotKeySpecifier { get; set; } = (Rune)0xFFFF;
-
- ///
- /// Causes the Text to be formatted, based on and .
- ///
- public void ReFormat ()
- {
- // With this check, we protect against subclasses with overrides of Text
- if (ustring.IsNullOrEmpty (Text)) {
- return;
- }
- recalcPending = false;
- var shown_text = ProcessHotKeyText (text, HotKeySpecifier, false, out hotKeyPos, out hotKey);
- Reformat (shown_text, lines, TextSize.Width, textAlignment, TextSize.Height > 1);
- }
-
- static ustring StripCRLF (ustring str)
- {
- var runes = new List ();
- foreach (var r in str.ToRunes ()) {
- if (r != '\r' && r != '\n') {
- runes.Add (r);
- }
- }
- return ustring.Make (runes); ;
- }
- static ustring ReplaceCRLFWithSpace (ustring str)
- {
- var runes = new List ();
- foreach (var r in str.ToRunes ()) {
- if (r == '\r' || r == '\n') {
- runes.Add (new Rune (' ')); // r + 0x2400)); // U+25A1 □ WHITE SQUARE
- } else {
- runes.Add (r);
- }
- }
- return ustring.Make (runes); ;
- }
-
- static List WordWrap (ustring text, int margin)
- {
- int start = 0, end;
- var lines = new List ();
-
- text = StripCRLF (text);
-
- while ((end = start + margin) < text.Length) {
- while (text [end] != ' ' && end > start)
- end -= 1;
- if (end == start)
- end = start + margin;
-
- lines.Add (text [start, end]);
- start = end + 1;
- }
-
- if (start < text.Length)
- lines.Add (text.Substring (start));
-
- return lines;
- }
-
- static ustring ClipAndJustify (ustring str, int width, TextAlignment talign)
- {
- int slen = str.RuneCount;
- if (slen > width) {
- var uints = str.ToRunes (width);
- var runes = new Rune [uints.Length];
- for (int i = 0; i < uints.Length; i++)
- runes [i] = uints [i];
- return ustring.Make (runes);
- } else {
- if (talign == TextAlignment.Justified) {
- // TODO: ustring needs this
- var words = str.ToString ().Split (whitespace, StringSplitOptions.RemoveEmptyEntries);
- int textCount = words.Sum (arg => arg.Length);
-
- var spaces = words.Length > 1 ? (width - textCount) / (words.Length - 1) : 0;
- var extras = words.Length > 1 ? (width - textCount) % words.Length : 0;
-
- var s = new System.Text.StringBuilder ();
- //s.Append ($"tc={textCount} sp={spaces},x={extras} - ");
- for (int w = 0; w < words.Length; w++) {
- var x = words [w];
- s.Append (x);
- if (w + 1 < words.Length)
- for (int i = 0; i < spaces; i++)
- s.Append (' ');
- if (extras > 0) {
- //s.Append ('_');
- extras--;
- }
- }
- return ustring.Make (s.ToString ());
- }
- return str;
- }
- }
-
- static char [] whitespace = new char [] { ' ', '\t' };
- private int hotKeyPos;
-
- ///
- /// Reformats text into lines, applying text alignment and word wraping.
- ///
- ///
- ///
- ///
- ///
- /// if false, forces text to fit a single line. Line breaks are converted to spaces.
- static void Reformat (ustring textStr, List lineResult, int width, TextAlignment talign, bool wordWrap)
- {
- lineResult.Clear ();
-
- if (wordWrap == false) {
- textStr = ReplaceCRLFWithSpace (textStr);
- lineResult.Add (ClipAndJustify (textStr, width, talign));
- return;
- }
-
- int textLen = textStr.Length;
- int lp = 0;
- for (int i = 0; i < textLen; i++) {
- Rune c = textStr [i];
- if (c == '\n') {
- var wrappedLines = WordWrap (textStr [lp, i], width);
- foreach (var line in wrappedLines) {
- lineResult.Add (ClipAndJustify (line, width, talign));
- }
- if (wrappedLines.Count == 0) {
- lineResult.Add (ustring.Empty);
- }
- lp = i + 1;
- }
- }
- foreach (var line in WordWrap (textStr [lp, textLen], width)) {
- lineResult.Add (ClipAndJustify (line, width, talign));
- }
- }
-
- ///
- /// Computes the number of lines needed to render the specified text given the width.
- ///
- /// Number of lines.
- /// Text, may contain newlines.
- /// The minimum width for the text.
- public static int MaxLines (ustring text, int width)
- {
- var result = new List ();
- ViewText.Reformat (text, result, width, TextAlignment.Left, true);
- return result.Count;
- }
-
- ///
- /// Computes the maximum width needed to render the text (single line or multple lines).
- ///
- /// Max width of lines.
- /// Text, may contain newlines.
- /// The minimum width for the text.
- public static int MaxWidth (ustring text, int width)
- {
- var result = new List ();
- ViewText.Reformat (text, result, width, TextAlignment.Left, true);
- return result.Max (s => s.RuneCount);
- }
-
- internal void Draw (Rect bounds)
- {
- // With this check, we protect against subclasses with overrides of Text
- if (ustring.IsNullOrEmpty (text)) {
- return;
- }
-
- if (recalcPending) {
- ReFormat ();
- }
-
- if (TextColor != -1)
- Driver.SetAttribute (TextColor);
- else
- Driver.SetAttribute (view.ColorScheme.Normal);
-
- view.Clear ();
- for (int line = 0; line < lines.Count; line++) {
- if (line < bounds.Top || line >= bounds.Bottom)
- continue;
- var str = lines [line];
- int x;
- switch (textAlignment) {
- case TextAlignment.Left:
- x = 0;
- break;
- case TextAlignment.Justified:
- x = bounds.Left;
- break;
- case TextAlignment.Right:
- x = bounds.Right - str.Length;
- break;
- case TextAlignment.Centered:
- x = bounds.Left + (bounds.Width - str.Length) / 2;
- break;
- default:
- throw new ArgumentOutOfRangeException ();
- }
- view.Move (x, line);
- Driver.AddStr (str);
- }
-
- if (HotKeyPos != -1) {
- _ = GetAlignedText (lines [0], TextSize.Width, hotKeyPos, out hotKeyPos, textAlignment);
-
- view.Move (HotKeyPos, 0);
- Driver.SetAttribute (view.HasFocus ? view.ColorScheme.HotFocus : view.ColorScheme.HotNormal);
- Driver.AddRune (hotKey);
- }
- }
-
- ///
- /// Calculates the rectangle requried to hold text, assuming no word wrapping.
- ///
- /// The x location of the rectangle
- /// The y location of the rectangle
- /// The text to measure
- ///
- public static Rect CalcRect (int x, int y, ustring text)
- {
- if (ustring.IsNullOrEmpty (text))
- return Rect.Empty;
-
- int mw = 0;
- int ml = 1;
-
- int cols = 0;
- foreach (var rune in text) {
- if (rune == '\n') {
- ml++;
- if (cols > mw)
- mw = cols;
- cols = 0;
- } else
- cols++;
- }
- if (cols > mw)
- mw = cols;
-
- return new Rect (x, y, mw, ml);
- }
-
-
- ///
- /// Gets the position and Rune for the hotkey in text and removes the hotkey specifier.
- ///
- /// The text to manipulate.
- /// The hot-key specifier (e.g. '_') to look for.
- /// If true and no hotkey is found via the hotkey specifier, the first upper case char found will be the hotkey.
- /// Returns the postion of the hot-key in the text. -1 if not found.
- /// Returns the Rune immediately to the right of the hot-key position
- /// The input text with the hotkey specifier ('_') removed.
- public static ustring ProcessHotKeyText (ustring text, Rune hotKeySpecifier, bool firstUpperCase, out int hotPos, out Rune showHotKey)
- {
- if (hotKeySpecifier == (Rune)0xFFFF) {
- hotPos = -1;
- showHotKey = (Rune)0xFFFF;
- return text;
- }
- Rune hot_key = (Rune)0;
- int hot_pos = -1;
- ustring shown_text = text;
-
- // Use first hot_key char passed into 'hotKey'.
- // TODO: Ignore hot_key of two are provided
- int i = 0;
- foreach (Rune c in shown_text) {
- if ((char)c != 0xFFFD) {
- if (c == hotKeySpecifier) {
- hot_pos = i;
- } else if (hot_pos > -1) {
- hot_key = c;
- break;
- }
- }
- i++;
- }
-
- // Legacy support - use first upper case char if the specifier was not found
- if (hot_pos == -1 && firstUpperCase) {
- i = 0;
- foreach (Rune c in shown_text) {
- if ((char)c != 0xFFFD) {
- if (Rune.IsUpper (c)) {
- hot_key = c;
- hot_pos = i;
- break;
- }
- }
- i++;
- }
- }
- else {
- if (hot_pos != -1) {
- // Use char after 'hotKey'
- ustring start = "";
- i = 0;
- foreach (Rune c in shown_text) {
- start += ustring.Make (c);
- i++;
- if (i == hot_pos)
- break;
- }
- var st = shown_text;
- shown_text = start;
- i = 0;
- foreach (Rune c in st) {
- i++;
- if (i > hot_pos + 1) {
- shown_text += ustring.Make (c);
- }
- }
- }
- }
- hotPos = hot_pos;
- showHotKey = hot_key;
- return shown_text;
- }
-
- ///
- /// Formats a single line of text with a hot-key and .
- ///
- /// The text to align.
- /// The maximum width for the text.
- /// The hot-key position before reformatting.
- /// The hot-key position after reformatting.
- /// The to align to.
- /// The aligned text.
- public static ustring GetAlignedText (ustring shown_text, int width, int hot_pos, out int c_hot_pos, TextAlignment textAlignment)
- {
- int start;
- var caption = shown_text;
- c_hot_pos = hot_pos;
-
- if (width > shown_text.Length + 1) {
- switch (textAlignment) {
- case TextAlignment.Left:
- caption += new string (' ', width - caption.RuneCount);
- break;
- case TextAlignment.Right:
- start = width - caption.RuneCount;
- caption = $"{new string (' ', width - caption.RuneCount)}{caption}";
- if (c_hot_pos > -1) {
- c_hot_pos += start;
- }
- break;
- case TextAlignment.Centered:
- start = width / 2 - caption.RuneCount / 2;
- caption = $"{new string (' ', start)}{caption}{new string (' ', width - caption.RuneCount - start)}";
- if (c_hot_pos > -1) {
- c_hot_pos += start;
- }
- break;
- case TextAlignment.Justified:
- var words = caption.Split (" ");
- var wLen = GetWordsLength (words, c_hot_pos, out int runeCount, out int w_hot_pos);
- var space = (width - runeCount) / (caption.Length - wLen);
- caption = "";
- for (int i = 0; i < words.Length; i++) {
- if (i == words.Length - 1) {
- caption += new string (' ', width - caption.RuneCount - 1);
- caption += words [i];
- } else {
- caption += words [i];
- }
- if (i < words.Length - 1) {
- caption += new string (' ', space);
- }
- }
- if (c_hot_pos > -1) {
- c_hot_pos += w_hot_pos * space - space - w_hot_pos + 1;
- }
- break;
- }
- }
-
- return caption;
- }
-
- static int GetWordsLength (ustring [] words, int hotPos, out int runeCount, out int wordHotPos)
- {
- int length = 0;
- int rCount = 0;
- int wHotPos = -1;
- for (int i = 0; i < words.Length; i++) {
- if (wHotPos == -1 && rCount + words [i].RuneCount >= hotPos)
- wHotPos = i;
- length += words [i].Length;
- rCount += words [i].RuneCount;
- }
- if (wHotPos == -1 && hotPos > -1)
- wHotPos = words.Length;
- runeCount = rCount;
- wordHotPos = wHotPos;
- return length;
- }
- }
+ public partial class View : Responder, IEnumerable {
internal enum Direction {
Forward,
@@ -619,7 +144,7 @@ namespace Terminal.Gui {
View focused = null;
Direction focusDirection;
- ViewText viewText;
+ TextFormatter viewText;
///
/// Event fired when the view gets focus.
@@ -649,7 +174,7 @@ namespace Terminal.Gui {
///
/// The HotKey defined for this view. A user pressing HotKey on the keyboard while this view has focus will cause the Clicked event to fire.
///
- public Rune HotKey { get => viewText.HotKey; set => viewText.HotKey = value; }
+ public Key HotKey { get => viewText.HotKey; set => viewText.HotKey = value; }
///
///
@@ -889,7 +414,7 @@ namespace Terminal.Gui {
///
public View (Rect frame)
{
- viewText = new ViewText (this);
+ viewText = new TextFormatter (this);
this.Text = ustring.Empty;
this.Frame = frame;
@@ -933,7 +458,7 @@ namespace Terminal.Gui {
/// column to locate the Label.
/// row to locate the Label.
/// text to initialize the property with.
- public View (int x, int y, ustring text) : this (ViewText.CalcRect (x, y, text), text) { }
+ public View (int x, int y, ustring text) : this (TextFormatter.CalcRect (x, y, text), text) { }
///
/// Initializes a new instance of using layout.
@@ -952,7 +477,7 @@ namespace Terminal.Gui {
/// text to initialize the property with.
public View (Rect rect, ustring text) : this (rect)
{
- viewText = new ViewText (this);
+ viewText = new TextFormatter (this);
this.Text = text;
}
@@ -972,12 +497,13 @@ namespace Terminal.Gui {
/// text to initialize the property with.
public View (ustring text) : base ()
{
- viewText = new ViewText (this);
+ viewText = new TextFormatter (this);
this.Text = text;
CanFocus = false;
LayoutStyle = LayoutStyle.Computed;
- var r = ViewText.CalcRect (0, 0, text);
+ // BUGBUG: CalcRect doesn't account for line wrapping
+ var r = TextFormatter.CalcRect (0, 0, text);
x = Pos.At (0);
y = Pos.At (0);
Width = r.Width;
@@ -1549,10 +1075,13 @@ namespace Terminal.Gui {
Driver.SetAttribute (HasFocus ? ColorScheme.Focus : ColorScheme.Normal);
- // Draw any Text
- // TODO: Figure out if this should go here or after OnDrawContent
- viewText?.ReFormat ();
- viewText?.Draw (bounds);
+ if (!ustring.IsNullOrEmpty (Text)) {
+ Clear ();
+ // Draw any Text
+ // TODO: Figure out if this should go here or after OnDrawContent
+ viewText?.ReFormat ();
+ viewText?.Draw (ViewToScreen (Bounds), ColorScheme.Normal, ColorScheme.HotNormal);
+ }
// Invoke DrawContentEvent
OnDrawContent (bounds);
@@ -1561,8 +1090,6 @@ namespace Terminal.Gui {
foreach (var view in subviews) {
if (view.NeedDisplay != null && (!view.NeedDisplay.IsEmpty || view.childNeedsDisplay)) {
if (view.Frame.IntersectsWith (clipRect) && (view.Frame.IntersectsWith (bounds) || bounds.X < 0 || bounds.Y < 0)) {
-
- // FIXED: optimize this by computing the intersection of region and view.Bounds
if (view.layoutNeeded)
view.LayoutSubviews ();
Application.CurrentView = view;
@@ -1570,7 +1097,6 @@ namespace Terminal.Gui {
// Draw the subview
// Use the view's bounds (view-relative; Location will always be (0,0) because
view.Redraw (view.Bounds);
-
}
view.NeedDisplay = Rect.Empty;
view.childNeedsDisplay = false;
@@ -1674,7 +1200,7 @@ namespace Terminal.Gui {
return true;
var c = keyEvent.KeyValue;
- if (c == '\n' || c == ' ' || Rune.ToUpper ((uint)c) == HotKey) {
+ if (c == '\n' || c == ' ' || keyEvent.Key == HotKey) {
Clicked?.Invoke ();
return true;
}
@@ -2018,7 +1544,7 @@ namespace Terminal.Gui {
if (!layoutNeeded)
return;
- viewText.TextSize = Bounds.Size;
+ viewText.Size = Bounds.Size;
viewText.ReFormat ();
Rect oldBounds = Bounds;
@@ -2080,20 +1606,9 @@ namespace Terminal.Gui {
///
/// The text alignment.
public virtual TextAlignment TextAlignment {
- get => viewText.TextAlignment;
+ get => viewText.Alignment;
set {
- viewText.TextAlignment = value;
- SetNeedsDisplay ();
- }
- }
-
- ///
- /// The color used for the .
- ///
- public virtual Attribute TextColor {
- get => viewText.TextColor;
- set {
- viewText.TextColor = value;
+ viewText.Alignment = value;
SetNeedsDisplay ();
}
}
@@ -2169,7 +1684,7 @@ namespace Terminal.Gui {
if (mouseEvent.Flags == MouseFlags.Button1Clicked) {
- if (!HasFocus) {
+ if (!HasFocus && SuperView != null) {
SuperView.SetFocus (this);
SetNeedsDisplay ();
}
diff --git a/Terminal.Gui/Core/Window.cs b/Terminal.Gui/Core/Window.cs
index d02a6c21f..4cbb15ed1 100644
--- a/Terminal.Gui/Core/Window.cs
+++ b/Terminal.Gui/Core/Window.cs
@@ -278,15 +278,5 @@ namespace Terminal.Gui {
base.TextAlignment = contentView.TextAlignment = value;
}
}
-
- ///
- /// The color used for the .
- ///
- public override Attribute TextColor {
- get => contentView.TextColor;
- set {
- base.TextColor = contentView.TextColor = value;
- }
- }
}
}
diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs
index e3acf8933..877d1be76 100644
--- a/Terminal.Gui/Views/Button.cs
+++ b/Terminal.Gui/Views/Button.cs
@@ -28,19 +28,6 @@ namespace Terminal.Gui {
ustring text;
bool is_default;
- ///
- /// Gets or sets whether the is the default action to activate in a dialog.
- ///
- /// true if is default; otherwise, false.
- public bool IsDefault {
- get => is_default;
- set {
- is_default = value;
- SetWidthHeight (Text, is_default);
- Update ();
- }
- }
-
///
/// Initializes a new instance of using layout.
///
@@ -94,7 +81,7 @@ namespace Terminal.Gui {
/// in a will implicitly activate this button.
///
public Button (int x, int y, ustring text, bool is_default)
- : base (new Rect (x, y, text.Length + 4 + (is_default ? 2 : 0), 1))
+ : base (new Rect (x, y, text.RuneCount + 4 + (is_default ? 2 : 0), 1))
{
Init (text, is_default);
}
@@ -114,20 +101,20 @@ namespace Terminal.Gui {
_rightDefault = new Rune (Driver != null ? Driver.RightDefaultIndicator : '>');
CanFocus = true;
- Text = text ?? string.Empty;
this.IsDefault = is_default;
- int w = SetWidthHeight (text, is_default);
- Frame = new Rect (Frame.Location, new Size (w, 1));
+ Text = text ?? string.Empty;
+ //int w = SetWidthHeight (text, is_default);
+ //Frame = new Rect (Frame.Location, new Size (w, 1));
}
- int SetWidthHeight (ustring text, bool is_default)
- {
- int w = text.Length + 4 + (is_default ? 2 : 0);
- Width = w;
- Height = 1;
- Frame = new Rect (Frame.Location, new Size (w, 1));
- return w;
- }
+ //int SetWidthHeight (ustring text, bool is_default)
+ //{
+ // int w = text.RuneCount;// + 4 + (is_default ? 2 : 0);
+ // Width = w;
+ // Height = 1;
+ // Frame = new Rect (Frame.Location, new Size (w, 1));
+ // return w;
+ //}
///
/// The text displayed by this .
@@ -138,12 +125,23 @@ namespace Terminal.Gui {
}
set {
- SetWidthHeight (value, is_default);
text = value;
Update ();
}
}
+ ///
+ /// Gets or sets whether the is the default action to activate in a dialog.
+ ///
+ /// true if is default; otherwise, false.
+ public bool IsDefault {
+ get => is_default;
+ set {
+ is_default = value;
+ Update ();
+ }
+ }
+
internal void Update ()
{
if (IsDefault)
@@ -151,12 +149,17 @@ namespace Terminal.Gui {
else
base.Text = ustring.Make (_leftBracket) + " " + text + " " + ustring.Make (_rightBracket);
+ int w = base.Text.RuneCount - (base.Text.Contains (HotKeySpecifier) ? 1 : 0);
+ Width = w;
+ Height = 1;
+ Frame = new Rect (Frame.Location, new Size (w, 1));
+
SetNeedsDisplay ();
}
bool CheckKey (KeyEvent key)
{
- if ((char)key.KeyValue == HotKey) {
+ if (key.Key == HotKey) {
this.SuperView.SetFocus (this);
Clicked?.Invoke ();
return true;
@@ -187,7 +190,7 @@ namespace Terminal.Gui {
public override bool ProcessKey (KeyEvent kb)
{
var c = kb.KeyValue;
- if (c == '\n' || c == ' ' || Rune.ToUpper ((uint)c) == HotKey) {
+ if (c == '\n' || c == ' ' || kb.Key == HotKey) {
Clicked?.Invoke ();
return true;
}
diff --git a/Terminal.Gui/Views/Checkbox.cs b/Terminal.Gui/Views/Checkbox.cs
index ebae828ba..44a9e82da 100644
--- a/Terminal.Gui/Views/Checkbox.cs
+++ b/Terminal.Gui/Views/Checkbox.cs
@@ -51,7 +51,7 @@ namespace Terminal.Gui {
Text = s;
CanFocus = true;
Height = 1;
- Width = s.Length + 4;
+ Width = s.RuneCount + 4;
}
///
diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs
index 846d61041..aff1f2c6f 100644
--- a/Terminal.Gui/Views/ComboBox.cs
+++ b/Terminal.Gui/Views/ComboBox.cs
@@ -189,7 +189,7 @@ namespace Terminal.Gui {
if (!search.HasFocus)
this.SetFocus (search);
- search.CursorPosition = search.Text.Length;
+ search.CursorPosition = search.Text.RuneCount;
return true;
}
@@ -222,7 +222,7 @@ namespace Terminal.Gui {
}
SetValue((string)searchset [listview.SelectedItem]);
- search.CursorPosition = search.Text.Length;
+ search.CursorPosition = search.Text.RuneCount;
Search_Changed (search.Text);
OnSelectedChanged ();
@@ -245,7 +245,7 @@ namespace Terminal.Gui {
if (e.Key == Key.CursorUp && listview.HasFocus && listview.SelectedItem == 0 && searchset.Count > 0) // jump back to search
{
- search.CursorPosition = search.Text.Length;
+ search.CursorPosition = search.Text.RuneCount;
this.SetFocus (search);
return true;
}
diff --git a/Terminal.Gui/Views/DateField.cs b/Terminal.Gui/Views/DateField.cs
index 235440510..8909a548c 100644
--- a/Terminal.Gui/Views/DateField.cs
+++ b/Terminal.Gui/Views/DateField.cs
@@ -99,11 +99,11 @@ namespace Terminal.Gui {
{
ustring [] frm = ustring.Make (lf).Split (ustring.Make (sepChar));
for (int i = 0; i < frm.Length; i++) {
- if (frm [i].Contains ("M") && frm [i].Length < 2)
+ if (frm [i].Contains ("M") && frm [i].RuneCount < 2)
lf = lf.Replace ("M", "MM");
- if (frm [i].Contains ("d") && frm [i].Length < 2)
+ if (frm [i].Contains ("d") && frm [i].RuneCount < 2)
lf = lf.Replace ("d", "dd");
- if (frm [i].Contains ("y") && frm [i].Length < 4)
+ if (frm [i].Contains ("y") && frm [i].RuneCount < 4)
lf = lf.Replace ("yy", "yyyy");
}
return $" {lf}";
@@ -248,7 +248,7 @@ namespace Terminal.Gui {
date [1] = vals [i].TrimSpace ();
} else {
var year = vals [i].TrimSpace ();
- if (year.Length == 2) {
+ if (year.RuneCount == 2) {
var y = DateTime.Now.Year.ToString ();
date [2] = y.Substring (0, 2) + year.ToString ();
} else {
diff --git a/Terminal.Gui/Views/FrameView.cs b/Terminal.Gui/Views/FrameView.cs
index b9c49f60d..9ea3e4d0b 100644
--- a/Terminal.Gui/Views/FrameView.cs
+++ b/Terminal.Gui/Views/FrameView.cs
@@ -190,16 +190,5 @@ namespace Terminal.Gui {
base.TextAlignment = contentView.TextAlignment = value;
}
}
-
- ///
- /// The color used for the .
- ///
- public override Attribute TextColor {
- get => contentView.TextColor;
- set {
- base.TextColor = contentView.TextColor = value;
- }
- }
-
}
}
diff --git a/Terminal.Gui/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs
index b0da9070c..554424f5e 100644
--- a/Terminal.Gui/Views/Menu.cs
+++ b/Terminal.Gui/Views/Menu.cs
@@ -132,7 +132,7 @@ namespace Terminal.Gui {
return CanExecute == null ? true : CanExecute ();
}
- internal int Width => Title.Length + Help.Length + 1 + 2 +
+ internal int Width => Title.RuneCount + Help.Length + 1 + 2 +
(Checked || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio) ? 2 : 0);
///
@@ -367,7 +367,7 @@ namespace Terminal.Gui {
i == current ? ColorScheme.Focus : ColorScheme.Normal);
// The help string
- var l = item.Help.Length;
+ var l = item.Help.RuneCount;
Move (Frame.Width - l - 2, 1 + i);
Driver.AddStr (item.Help);
}
diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs
index 50db7b3e2..2c2b21630 100644
--- a/Terminal.Gui/Views/RadioGroup.cs
+++ b/Terminal.Gui/Views/RadioGroup.cs
@@ -95,7 +95,7 @@ namespace Terminal.Gui {
}
foreach (var s in radioLabels)
- width = Math.Max (s.Length + 3, width);
+ width = Math.Max (s.RuneCount + 3, width);
return new Rect (x, y, width, radioLabels.Count);
}
@@ -126,7 +126,7 @@ namespace Terminal.Gui {
// for (int i = 0; i < radioLabels.Count; i++) {
// Move(0, i);
// Driver.SetAttribute(ColorScheme.Normal);
- // Driver.AddStr(ustring.Make(new string (' ', radioLabels[i].Length + 4)));
+ // Driver.AddStr(ustring.Make(new string (' ', radioLabels[i].RuneCount + 4)));
// }
// if (newRadioLabels.Count != radioLabels.Count) {
// SetWidthHeight(newRadioLabels);
diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs
index 82d93a178..2fa43f832 100644
--- a/Terminal.Gui/Views/TextField.cs
+++ b/Terminal.Gui/Views/TextField.cs
@@ -64,7 +64,7 @@ namespace Terminal.Gui {
public TextField (ustring text)
{
Initialize (text, 0);
- Width = text.Length + 1;
+ Width = text.RuneCount + 1;
}
///
@@ -85,7 +85,7 @@ namespace Terminal.Gui {
text = "";
this.text = TextModel.ToRunes (text);
- point = text.Length;
+ point = text.RuneCount;
first = point > w ? point - w : 0;
CanFocus = true;
Used = true;
@@ -771,13 +771,13 @@ namespace Terminal.Gui {
void DeleteSelectedText ()
{
- string actualText = Text.ToString ();
+ ustring actualText = Text;
int selStart = SelectedLength < 0 ? SelectedLength + SelectedStart : SelectedStart;
int selLength = Math.Abs (SelectedLength);
- Text = actualText.Substring (0, selStart) +
- actualText.Substring (selStart + selLength, actualText.Length - selStart - selLength);
+ Text = actualText[0, selStart] +
+ actualText[selStart + selLength, actualText.RuneCount - selLength];
ClearAllSelection ();
- CursorPosition = selStart >= Text.Length ? Text.Length : selStart;
+ CursorPosition = selStart >= Text.RuneCount ? Text.RuneCount : selStart;
SetNeedsDisplay ();
}
@@ -789,13 +789,13 @@ namespace Terminal.Gui {
if (ReadOnly)
return;
- string actualText = Text.ToString ();
+ ustring actualText = Text;
int start = SelectedStart == -1 ? CursorPosition : SelectedStart;
- ustring cbTxt = Clipboard.Contents?.ToString () ?? "";
- Text = actualText.Substring (0, start) +
+ ustring cbTxt = Clipboard.Contents ?? "";
+ Text = actualText[0, start] +
cbTxt +
- actualText.Substring (start + SelectedLength, actualText.Length - start - SelectedLength);
- point = start + cbTxt.Length;
+ actualText[start + SelectedLength, actualText.RuneCount - SelectedLength];
+ point = start + cbTxt.RuneCount;
SelectedLength = 0;
ClearAllSelection ();
SetNeedsDisplay ();
diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs
index f1d40a9cf..de0c5d8b7 100644
--- a/Terminal.Gui/Views/TextView.cs
+++ b/Terminal.Gui/Views/TextView.cs
@@ -77,6 +77,8 @@ namespace Terminal.Gui {
{
var lines = new List> ();
int start = 0, i = 0;
+ // BUGBUG: I think this is buggy w.r.t Unicode. content.Length is bytes, and content[i] is bytes
+ // and content[i] == 10 may be the middle of a Rune.
for (; i < content.Length; i++) {
if (content [i] == 10) {
if (i - start > 0)
diff --git a/Terminal.Gui/Windows/FileDialog.cs b/Terminal.Gui/Windows/FileDialog.cs
index 684565e29..2b3780d2d 100644
--- a/Terminal.Gui/Windows/FileDialog.cs
+++ b/Terminal.Gui/Windows/FileDialog.cs
@@ -437,10 +437,10 @@ namespace Terminal.Gui {
/// The prompt.
/// The name field label.
/// The message.
- public FileDialog (ustring title, ustring prompt, ustring nameFieldLabel, ustring message) : base (title, Driver.Cols - 20, Driver.Rows - 5, null)
+ public FileDialog (ustring title, ustring prompt, ustring nameFieldLabel, ustring message) : base (title)//, Driver.Cols - 20, Driver.Rows - 5, null)
{
this.message = new Label (Rect.Empty, "MESSAGE" + message);
- var msgLines = ViewText.MaxLines (message, Driver.Cols - 20);
+ var msgLines = TextFormatter.MaxLines (message, Driver.Cols - 20);
dirLabel = new Label ("Directory: ") {
X = 1,
diff --git a/Terminal.Gui/Windows/MessageBox.cs b/Terminal.Gui/Windows/MessageBox.cs
index 70122e934..fb5653665 100644
--- a/Terminal.Gui/Windows/MessageBox.cs
+++ b/Terminal.Gui/Windows/MessageBox.cs
@@ -94,8 +94,8 @@ namespace Terminal.Gui {
static int QueryFull (bool useErrorColors, int width, int height, ustring title, ustring message, params ustring [] buttons)
{
const int defaultWidth = 50;
- int textWidth = View.ViewText.MaxWidth (message, width == 0 ? defaultWidth : width);
- int textHeight = View.ViewText.MaxLines (message, textWidth); // message.Count (ustring.Make ('\n')) + 1;
+ int textWidth = TextFormatter.MaxWidth (message, width == 0 ? defaultWidth : width);
+ int textHeight = TextFormatter.MaxLines (message, textWidth); // message.Count (ustring.Make ('\n')) + 1;
int msgboxHeight = Math.Max (1, textHeight) + 3; // textHeight + (top + top padding + buttons + bottom)
// Create button array for Dialog
@@ -135,7 +135,7 @@ namespace Terminal.Gui {
}
// Dynamically size Width
- int msgboxWidth = Math.Max (defaultWidth, Math.Max (title.Length + 8, Math.Max (textWidth + 4, d.GetButtonsWidth ()) + 8)); // textWidth + (left + padding + padding + right)
+ int msgboxWidth = Math.Max (defaultWidth, Math.Max (title.RuneCount + 8, Math.Max (textWidth + 4, d.GetButtonsWidth ()) + 8)); // textWidth + (left + padding + padding + right)
d.Width = msgboxWidth;
// Setup actions
diff --git a/Terminal.GuiTests/Core/TextFormatterTests.cs b/Terminal.GuiTests/Core/TextFormatterTests.cs
new file mode 100644
index 000000000..3b699a676
--- /dev/null
+++ b/Terminal.GuiTests/Core/TextFormatterTests.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Terminal.GuiTests.Core
+{
+ class TextFormatterTests
+ {
+ }
+}
diff --git a/Terminal.GuiTests/Terminal.GuiTests.csproj b/Terminal.GuiTests/Terminal.GuiTests.csproj
new file mode 100644
index 000000000..bbc84bd85
--- /dev/null
+++ b/Terminal.GuiTests/Terminal.GuiTests.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net472
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Terminal.sln b/Terminal.sln
index 6df6d7c65..60f459e4b 100644
--- a/Terminal.sln
+++ b/Terminal.sln
@@ -10,7 +10,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Designer", "Designer\Design
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UICatalog", "UICatalog\UICatalog.csproj", "{88979F89-9A42-448F-AE3E-3060145F6375}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{8B901EDE-8974-4820-B100-5226917E2990}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "UnitTests\UnitTests.csproj", "{8B901EDE-8974-4820-B100-5226917E2990}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
diff --git a/UICatalog/Scenarios/Buttons.cs b/UICatalog/Scenarios/Buttons.cs
index ea80c561c..c81574917 100644
--- a/UICatalog/Scenarios/Buttons.cs
+++ b/UICatalog/Scenarios/Buttons.cs
@@ -191,17 +191,17 @@ namespace UICatalog {
ustring start = "";
if (i > -1)
start = txt [0, i];
- txt = start + txt [i + 1, txt.Length];
+ txt = start + txt [i + 1, txt.RuneCount];
// Move over one or go to start
i++;
- if (i >= txt.Length) {
+ if (i >= txt.RuneCount) {
i = 0;
}
// Slip in the '_'
start = txt [0, i];
- txt = start + ustring.Make ('_') + txt [i, txt.Length];
+ txt = start + ustring.Make ('_') + txt [i, txt.RuneCount];
return txt;
}
@@ -218,11 +218,11 @@ namespace UICatalog {
};
Win.Add (moveHotKeyBtn);
- var muhkb = " ~ s gui.cs master ↑10 = Сохранить";
+ var muhkb = ustring.Make(" ~ s gui.cs master ↑10 = Сохранить");
var moveUnicodeHotKeyBtn = new Button (muhkb) {
X = Pos.Left (absoluteFrame) + 1,
Y = Pos.Bottom (radioGroup) + 1,
- Width = muhkb.Length + 30,
+ Width = muhkb.RuneCount + 30,
ColorScheme = Colors.TopLevel,
};
moveUnicodeHotKeyBtn.Clicked = () => {
diff --git a/UICatalog/Scenarios/LabelsAsButtons.cs b/UICatalog/Scenarios/LabelsAsButtons.cs
index 60351a34d..9ab09f861 100644
--- a/UICatalog/Scenarios/LabelsAsButtons.cs
+++ b/UICatalog/Scenarios/LabelsAsButtons.cs
@@ -133,6 +133,7 @@ namespace UICatalog {
// Demonstrates how changing the View.Frame property can SIZE Views (#583)
var sizeBtn = new Label ("Size This \u263a Button _via Pos") {
+ //var sizeBtn = new Label ("Size This x Button _via Pos") {
X = 0,
Y = Pos.Center () + 1,
Width = 30,
@@ -191,17 +192,17 @@ namespace UICatalog {
ustring start = "";
if (i > -1)
start = txt [0, i];
- txt = start + txt [i + 1, txt.Length];
+ txt = start + txt [i + 1, txt.RuneCount];
// Move over one or go to start
i++;
- if (i >= txt.Length) {
+ if (i >= txt.RuneCount) {
i = 0;
}
// Slip in the '_'
start = txt [0, i];
- txt = start + ustring.Make ('_') + txt [i, txt.Length];
+ txt = start + ustring.Make ('_') + txt [i, txt.RuneCount];
return txt;
}
@@ -218,7 +219,7 @@ namespace UICatalog {
};
Win.Add (moveHotKeyBtn);
- var muhkb = " ~ s gui.cs master ↑10 = Сохранить";
+ ustring muhkb = " ~ s gui.cs master ↑10 = Сохранить";
var moveUnicodeHotKeyBtn = new Label (muhkb) {
X = Pos.Left (absoluteFrame) + 1,
Y = Pos.Bottom (radioGroup) + 1,
diff --git a/UICatalog/Scenarios/Mouse.cs b/UICatalog/Scenarios/Mouse.cs
index 6c267b235..7c4a1457f 100644
--- a/UICatalog/Scenarios/Mouse.cs
+++ b/UICatalog/Scenarios/Mouse.cs
@@ -28,7 +28,6 @@ namespace UICatalog {
Win.Add (rmeList);
Application.RootMouseEvent += delegate (MouseEvent me) {
- ml.TextColor = Colors.TopLevel.Normal;
ml.Text = $"Mouse: ({me.X},{me.Y}) - {me.Flags} {count}";
rme.Add ($"({me.X},{me.Y}) - {me.Flags} {count++}");
rmeList.MoveDown ();
diff --git a/UICatalog/Scenarios/Scrolling.cs b/UICatalog/Scenarios/Scrolling.cs
index 832a067e0..047ca4ba2 100644
--- a/UICatalog/Scenarios/Scrolling.cs
+++ b/UICatalog/Scenarios/Scrolling.cs
@@ -243,7 +243,6 @@ namespace UICatalog {
mousePos.Y = Pos.AnchorEnd (1);
mousePos.Width = 50;
Application.RootMouseEvent += delegate (MouseEvent me) {
- mousePos.TextColor = Colors.TopLevel.Normal;
mousePos.Text = $"Mouse: ({me.X},{me.Y}) - {me.Flags} {count++}";
};
diff --git a/UICatalog/Scenarios/TextAlignments.cs b/UICatalog/Scenarios/TextAlignments.cs
index db8eb147e..d7c776946 100644
--- a/UICatalog/Scenarios/TextAlignments.cs
+++ b/UICatalog/Scenarios/TextAlignments.cs
@@ -9,19 +9,71 @@ namespace UICatalog {
class TextAlignments : Scenario {
public override void Setup ()
{
-#if true
string txt = "Hello world, how are you today? Pretty neat!";
-#else
- string txt = "Hello world, how are you today? Unicode: ~ gui.cs . Neat?";
-#endif
+ string unicodeSampleText = "A Unicode sentence (пÑивеÑ) has words.";
+
var alignments = Enum.GetValues (typeof (Terminal.Gui.TextAlignment)).Cast ().ToList ();
- var label = new Label ($"Demonstrating single-line (should clip!):") { Y = 0 };
+ var singleLines = new Label [alignments.Count];
+ var multipleLines = new Label [alignments.Count];
+
+ var multiLineHeight = 5;
+
+ foreach (var alignment in alignments) {
+ singleLines[(int)alignment] = new Label (txt) { TextAlignment = alignment, Width = Dim.Fill (), Height = 1, ColorScheme = Colors.Dialog };
+ multipleLines [(int)alignment] = new Label (txt) { TextAlignment = alignment, Width = Dim.Fill (), Height = multiLineHeight, ColorScheme = Colors.Dialog };
+ }
+
+ // Add a label & text field so we can demo IsDefault
+ var editLabel = new Label ("Text:") {
+ X = 0,
+ Y = 0,
+ };
+ Win.Add (editLabel);
+ var edit = new TextView () {
+ X = Pos.Right (editLabel) + 1,
+ Y = Pos.Y (editLabel),
+ Width = Dim.Fill("Text:".Length + " Unicode Sample".Length + 2),
+ Height = 4,
+ ColorScheme = Colors.TopLevel,
+ Text = txt,
+ };
+ edit.TextChanged = () => {
+ foreach (var alignment in alignments) {
+ singleLines [(int)alignment].Text = edit.Text;
+ multipleLines [(int)alignment].Text = edit.Text;
+ }
+ };
+ Win.Add (edit);
+
+ var unicodeSample = new Button ("Unicode Sample") {
+ X = Pos.Right (edit) + 1,
+ Y = 0,
+ Clicked = () => {
+ edit.Text = unicodeSampleText;
+ }
+ };
+ Win.Add (unicodeSample);
+
+ var update = new Button ("_Update", is_default: true) {
+ X = Pos.Right (edit) + 1,
+ Y = Pos.Bottom (edit) - 1,
+ Clicked = () => {
+ foreach (var alignment in alignments) {
+ singleLines [(int)alignment].Text = edit.Text;
+ multipleLines [(int)alignment].Text = edit.Text;
+ }
+ }
+ };
+ Win.Add (update);
+
+ var label = new Label ($"Demonstrating single-line (should clip):") { Y = Pos.Bottom (edit) + 1 };
Win.Add (label);
foreach (var alignment in alignments) {
label = new Label ($"{alignment}:") { Y = Pos.Bottom (label) };
Win.Add (label);
- label = new Label (txt) { TextAlignment = alignment, Y = Pos.Bottom (label), Width = Dim.Fill (), Height = 1, ColorScheme = Colors.Dialog };
- Win.Add (label);
+ singleLines [(int)alignment].Y = Pos.Bottom (label);
+ Win.Add (singleLines [(int)alignment]);
+ label = singleLines [(int)alignment];
}
txt += "\nSecond line\n\nFourth Line.";
@@ -30,8 +82,9 @@ namespace UICatalog {
foreach (var alignment in alignments) {
label = new Label ($"{alignment}:") { Y = Pos.Bottom (label) };
Win.Add (label);
- label = new Label (txt) { TextAlignment = alignment, Width = Dim.Fill (), Height = 6, ColorScheme = Colors.Dialog, Y = Pos.Bottom (label) };
- Win.Add (label);
+ multipleLines [(int)alignment].Y = Pos.Bottom (label);
+ Win.Add (multipleLines [(int)alignment]);
+ label = multipleLines [(int)alignment];
}
}
}
diff --git a/UICatalog/Scenarios/ViewWithText.cs b/UICatalog/Scenarios/ViewWithText.cs
index fd88e9922..b9f78e904 100644
--- a/UICatalog/Scenarios/ViewWithText.cs
+++ b/UICatalog/Scenarios/ViewWithText.cs
@@ -13,7 +13,6 @@ namespace UICatalog {
Win.Text = "This is the Te_xt for the host Win object. TextAlignment.Centered was specified. It is intentionally very long to illustrate word wrap.\n" +
"<-- There is a new line here to show a hard line break. You should see this text bleed underneath the subviews, which start at Y = 3.";
Win.TextAlignment = TextAlignment.Centered;
- Win.TextColor = Application.Driver.MakeAttribute (Color.BrightGreen, Color.Black);
#if true
string txt = "Hello world, how are you today? Pretty neat!";
#else
@@ -31,7 +30,6 @@ namespace UICatalog {
Width = Dim.Fill (),
Height = 1,
ColorScheme = Colors.Dialog,
- TextColor = Application.Driver.MakeAttribute (Color.BrightRed, Color.White),
};
Win.Add (label);
}
diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs
index bdfa43b9f..f98414266 100644
--- a/UICatalog/UICatalog.cs
+++ b/UICatalog/UICatalog.cs
@@ -209,7 +209,7 @@ namespace UICatalog {
}),
new MenuBarItem ("_Color Scheme", CreateColorSchemeMenuItems()),
new MenuBarItem ("_Diagostics", CreateDiagnosticMenuItems()),
- new MenuBarItem ("_About...", "About this app", () => MessageBox.Query ("About UI Catalog", aboutMessage.ToString(), "Ok")),
+ new MenuBarItem ("_About...", "About this app", () => MessageBox.Query ("About UI Catalog", aboutMessage.ToString(), "_Ok")),
});
_leftPane = new FrameView ("Categories") {
diff --git a/UnitTests/TextFormatterTests.cs b/UnitTests/TextFormatterTests.cs
new file mode 100644
index 000000000..b60e86742
--- /dev/null
+++ b/UnitTests/TextFormatterTests.cs
@@ -0,0 +1,1017 @@
+using NStack;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+using Terminal.Gui;
+using Xunit;
+
+// Alais Console to MockConsole so we don't accidentally use Console
+using Console = Terminal.Gui.FakeConsole;
+
+namespace Terminal.Gui {
+ public class TextFormatterTests {
+ [Fact]
+ public void FindHotKey_Invalid_ReturnsFalse ()
+ {
+ var text = ustring.Empty;
+ Rune hotKeySpecifier = '_';
+ bool supportFirstUpperCase = false;
+ int hotPos = 0;
+ Key hotKey = Key.Unknown;
+ bool result = false;
+
+ text = null;
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.False (result);
+ Assert.Equal (-1, hotPos);
+ Assert.Equal (Key.Unknown, hotKey);
+
+ text = "";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.False (result);
+ Assert.Equal (-1, hotPos);
+ Assert.Equal (Key.Unknown, hotKey);
+
+ text = "no hotkey";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.False (result);
+ Assert.Equal (-1, hotPos);
+ Assert.Equal (Key.Unknown, hotKey);
+
+ text = "No hotkey, Upper Case";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.False (result);
+ Assert.Equal (-1, hotPos);
+ Assert.Equal (Key.Unknown, hotKey);
+
+ text = "Non-english: Сохранить";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.False (result);
+ Assert.Equal (-1, hotPos);
+ Assert.Equal (Key.Unknown, hotKey);
+ }
+
+ [Fact]
+ public void FindHotKey_AlphaUpperCase_Succeeds ()
+ {
+ var text = ustring.Empty;
+ Rune hotKeySpecifier = '_';
+ bool supportFirstUpperCase = false;
+ int hotPos = 0;
+ Key hotKey = Key.Unknown;
+ bool result = false;
+
+ text = "_K Before";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (0, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ text = "a_K Second";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (1, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ text = "Last _K";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (5, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ text = "After K_";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.False (result);
+ Assert.Equal (-1, hotPos);
+ Assert.Equal (Key.Unknown, hotKey);
+
+ text = "Multiple _K and _R";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (9, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ // Cryllic K (К)
+ text = "Non-english: _Кдать";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (13, hotPos);
+ Assert.Equal ((Key)'К', hotKey);
+
+ // Turn on FirstUpperCase and verify same results
+ supportFirstUpperCase = true;
+ text = "_K Before";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (0, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ text = "a_K Second";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (1, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ text = "Last _K";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (5, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ text = "After K_";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.False (result);
+ Assert.Equal (-1, hotPos);
+ Assert.Equal (Key.Unknown, hotKey);
+
+ text = "Multiple _K and _R";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (9, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ // Cryllic K (К)
+ text = "Non-english: _Кдать";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (13, hotPos);
+ Assert.Equal ((Key)'К', hotKey);
+ }
+ [Fact]
+ public void FindHotKey_AlphaLowerCase_Succeeds ()
+ {
+ var text = ustring.Empty;
+ Rune hotKeySpecifier = '_';
+ bool supportFirstUpperCase = false;
+ int hotPos = 0;
+ Key hotKey = Key.Unknown;
+ bool result = false;
+
+ // lower case should return uppercase Hotkey
+ text = "_k Before";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (0, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ text = "a_k Second";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (1, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ text = "Last _k";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (5, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ text = "After k_";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.False (result);
+ Assert.Equal (-1, hotPos);
+ Assert.Equal (Key.Unknown, hotKey);
+
+ text = "Multiple _k and _R";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (9, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ // Lower case Cryllic K (к)
+ text = "Non-english: _кдать";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (13, hotPos);
+ Assert.Equal ((Key)'К', hotKey);
+
+ // Turn on FirstUpperCase and verify same results
+ supportFirstUpperCase = true;
+ text = "_k Before";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (0, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ text = "a_k Second";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (1, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ text = "Last _k";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (5, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ text = "After k_";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.False (result);
+ Assert.Equal (-1, hotPos);
+ Assert.Equal (Key.Unknown, hotKey);
+
+ text = "Multiple _k and _R";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (9, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ // Lower case Cryllic K (к)
+ text = "Non-english: _кдать";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (13, hotPos);
+ Assert.Equal ((Key)'К', hotKey);
+ }
+
+ [Fact]
+ public void FindHotKey_Numeric_Succeeds ()
+ {
+ var text = ustring.Empty;
+ Rune hotKeySpecifier = '_';
+ bool supportFirstUpperCase = false;
+ int hotPos = 0;
+ Key hotKey = Key.Unknown;
+ bool result = false;
+ // Digits
+ text = "_1 Before";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (0, hotPos);
+ Assert.Equal ((Key)'1', hotKey);
+
+ text = "a_1 Second";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (1, hotPos);
+ Assert.Equal ((Key)'1', hotKey);
+
+ text = "Last _1";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (5, hotPos);
+ Assert.Equal ((Key)'1', hotKey);
+
+ text = "After 1_";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.False (result);
+ Assert.Equal (-1, hotPos);
+ Assert.Equal (Key.Unknown, hotKey);
+
+ text = "Multiple _1 and _2";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (9, hotPos);
+ Assert.Equal ((Key)'1', hotKey);
+
+ // Turn on FirstUpperCase and verify same results
+ supportFirstUpperCase = true;
+ text = "_1 Before";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (0, hotPos);
+ Assert.Equal ((Key)'1', hotKey);
+
+ text = "a_1 Second";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (1, hotPos);
+ Assert.Equal ((Key)'1', hotKey);
+
+ text = "Last _1";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (5, hotPos);
+ Assert.Equal ((Key)'1', hotKey);
+
+ text = "After 1_";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.False (result);
+ Assert.Equal (-1, hotPos);
+ Assert.Equal (Key.Unknown, hotKey);
+
+ text = "Multiple _1 and _2";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (9, hotPos);
+ Assert.Equal ((Key)'1', hotKey);
+ }
+
+ [Fact]
+ public void FindHotKey_Legacy_FirstUpperCase_Succeeds ()
+ {
+ bool supportFirstUpperCase = true;
+
+ var text = ustring.Empty;
+ Rune hotKeySpecifier = (Rune)0;
+ int hotPos = 0;
+ Key hotKey = Key.Unknown;
+ bool result = false;
+
+ text = "K Before";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (0, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ text = "aK Second";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (1, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ text = "last K";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (5, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ text = "multiple K and R";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (9, hotPos);
+ Assert.Equal ((Key)'K', hotKey);
+
+ // Cryllic K (К)
+ text = "non-english: Кдать";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.True (result);
+ Assert.Equal (13, hotPos);
+ Assert.Equal ((Key)'К', hotKey);
+ }
+
+ [Fact]
+ public void FindHotKey_Legacy_FirstUpperCase_NotFound_Returns_False ()
+ {
+ bool supportFirstUpperCase = true;
+
+ var text = ustring.Empty;
+ Rune hotKeySpecifier = (Rune)0;
+ int hotPos = 0;
+ Key hotKey = Key.Unknown;
+ bool result = false;
+
+ text = "k before";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.False (result);
+ Assert.Equal (-1, hotPos);
+ Assert.Equal (Key.Unknown, hotKey);
+
+ text = "ak second";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.False (result);
+ Assert.Equal (-1, hotPos);
+ Assert.Equal (Key.Unknown, hotKey);
+
+ text = "last k";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.False (result);
+ Assert.Equal (-1, hotPos);
+ Assert.Equal (Key.Unknown, hotKey);
+
+ text = "multiple k and r";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.False (result);
+ Assert.Equal (-1, hotPos);
+ Assert.Equal (Key.Unknown, hotKey);
+
+ text = "12345";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.False (result);
+ Assert.Equal (-1, hotPos);
+ Assert.Equal (Key.Unknown, hotKey);
+
+ // punctuation
+ text = "`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.False (result);
+ Assert.Equal (-1, hotPos);
+ Assert.Equal (Key.Unknown, hotKey);
+
+ // ~IsLetterOrDigit + Unicode
+ text = " ~ s gui.cs master ↑10";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.False (result);
+ Assert.Equal (-1, hotPos);
+ Assert.Equal (Key.Unknown, hotKey);
+
+ // Lower case Cryllic K (к)
+ text = "non-english: кдать";
+ result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey);
+ Assert.False (result);
+ Assert.Equal (-1, hotPos);
+ Assert.Equal (Key.Unknown, hotKey);
+ }
+
+ static ustring testHotKeyAtStart = "_K Before";
+ static ustring testHotKeyAtSecondPos = "a_K Second";
+ static ustring testHotKeyAtLastPos = "Last _K";
+ static ustring testHotKeyAfterLastChar = "After K_";
+ static ustring testMultiHotKeys = "Multiple _K and _R";
+ static ustring testNonEnglish = "Non-english: _Кдать";
+
+ [Fact]
+ public void RemoveHotKeySpecifier_InValid_ReturnsOriginal ()
+ {
+ Rune hotKeySpecifier = '_';
+
+ Assert.Null (TextFormatter.RemoveHotKeySpecifier (null, 0, hotKeySpecifier));
+ Assert.Equal ("", TextFormatter.RemoveHotKeySpecifier ("", 0, hotKeySpecifier));
+ Assert.Equal ("", TextFormatter.RemoveHotKeySpecifier ("", -1, hotKeySpecifier));
+ Assert.Equal ("", TextFormatter.RemoveHotKeySpecifier ("", 100, hotKeySpecifier));
+
+ Assert.Equal ("a", TextFormatter.RemoveHotKeySpecifier ("a", -1, hotKeySpecifier));
+ Assert.Equal ("a", TextFormatter.RemoveHotKeySpecifier ("a", 100, hotKeySpecifier));
+ }
+
+ [Fact]
+ public void RemoveHotKeySpecifier_Valid_ReturnsStripped ()
+ {
+ Rune hotKeySpecifier = '_';
+
+ Assert.Equal ("K Before", TextFormatter.RemoveHotKeySpecifier ("_K Before", 0, hotKeySpecifier));
+ Assert.Equal ("aK Second", TextFormatter.RemoveHotKeySpecifier ("a_K Second", 1, hotKeySpecifier));
+ Assert.Equal ("Last K", TextFormatter.RemoveHotKeySpecifier ("Last _K", 5, hotKeySpecifier));
+ Assert.Equal ("After K", TextFormatter.RemoveHotKeySpecifier ("After K_", 7, hotKeySpecifier));
+ Assert.Equal ("Multiple K and _R", TextFormatter.RemoveHotKeySpecifier ("Multiple _K and _R", 9, hotKeySpecifier));
+ Assert.Equal ("Non-english: Кдать", TextFormatter.RemoveHotKeySpecifier ("Non-english: _Кдать", 13, hotKeySpecifier));
+ }
+
+ [Fact]
+ public void RemoveHotKeySpecifier_Valid_Legacy_ReturnsOriginal ()
+ {
+ Rune hotKeySpecifier = '_';
+
+ Assert.Equal ("all lower case", TextFormatter.RemoveHotKeySpecifier ("all lower case", 0, hotKeySpecifier));
+ Assert.Equal ("K Before", TextFormatter.RemoveHotKeySpecifier ("K Before", 0, hotKeySpecifier));
+ Assert.Equal ("aK Second", TextFormatter.RemoveHotKeySpecifier ("aK Second", 1, hotKeySpecifier));
+ Assert.Equal ("Last K", TextFormatter.RemoveHotKeySpecifier ("Last K", 5, hotKeySpecifier));
+ Assert.Equal ("After K", TextFormatter.RemoveHotKeySpecifier ("After K", 7, hotKeySpecifier));
+ Assert.Equal ("Multiple K and R", TextFormatter.RemoveHotKeySpecifier ("Multiple K and R", 9, hotKeySpecifier));
+ Assert.Equal ("Non-english: Кдать", TextFormatter.RemoveHotKeySpecifier ("Non-english: Кдать", 13, hotKeySpecifier));
+ }
+
+ [Fact]
+ public void CalcRect_Invalid_Returns_Empty ()
+ {
+ Assert.Equal (Rect.Empty, TextFormatter.CalcRect (0, 0, null));
+ Assert.Equal (Rect.Empty, TextFormatter.CalcRect (0, 0, ""));
+ Assert.Equal (Rect.Empty, TextFormatter.CalcRect (1, 2, ""));
+ Assert.Equal (Rect.Empty, TextFormatter.CalcRect (-1, -2, ""));
+ }
+
+ [Fact]
+ public void CalcRect_SingleLine_Returns_1High ()
+ {
+ var text = ustring.Empty;
+
+ text = "test";
+ Assert.Equal (new Rect (0, 0, text.RuneCount, 1), TextFormatter.CalcRect (0, 0, text));
+ }
+
+ [Fact]
+ public void CalcRect_MultiLine_Returns_nHigh ()
+ {
+ var text = ustring.Empty;
+ var lines = 0;
+
+ text = "line1\nline2";
+ lines = 2;
+ Assert.Equal (new Rect (0, 0, 5, lines), TextFormatter.CalcRect (0, 0, text));
+
+ text = "line1\nline2\nline3long!";
+ lines = 3;
+ Assert.Equal (new Rect (0, 0, 10, lines), TextFormatter.CalcRect (0, 0, text));
+
+ text = "line1\nline2\n\n";
+ lines = 4;
+ Assert.Equal (new Rect (0, 0, 5, lines), TextFormatter.CalcRect (0, 0, text));
+
+ text = "line1\r\nline2";
+ lines = 2;
+ Assert.Equal (new Rect (0, 0, 5, lines), TextFormatter.CalcRect (0, 0, text));
+ }
+
+ [Fact]
+ public void ClipAndJustify_Invalid_Returns_Original ()
+ {
+ var text = ustring.Empty;
+
+ Assert.Equal (text, TextFormatter.ClipAndJustify (text, 0, TextAlignment.Left));
+
+ text = null;
+ Assert.Equal (text, TextFormatter.ClipAndJustify (text, 0, TextAlignment.Left));
+
+ text = "test";
+ Assert.Throws (() => TextFormatter.ClipAndJustify (text, -1, TextAlignment.Left));
+ }
+
+ [Fact]
+ public void ClipAndJustify_Valid_Left ()
+ {
+ var align = TextAlignment.Left;
+
+ var text = ustring.Empty;
+ var justifiedText = ustring.Empty;
+ int width = 0;
+
+ text = "test";
+ width = 0;
+ Assert.Equal (ustring.Empty, justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+
+ text = "test";
+ width = 2;
+ Assert.Equal (text [0, width], justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+
+ text = "test";
+ width = int.MaxValue;
+ Assert.Equal (text, justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A sentence has words.";
+ width = int.MaxValue;
+ Assert.Equal (text, justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A sentence has words.";
+ width = 10;
+ Assert.Equal (text [0, width], justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A\tsentence\thas\twords.";
+ width = int.MaxValue;
+ Assert.Equal (text, justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A\tsentence\thas\twords.";
+ width = 10;
+ Assert.Equal (text [0, width], justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "line1\nline2\nline3long!";
+ width = int.MaxValue;
+ Assert.Equal (text, justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "line1\nline2\nline3long!";
+ width = 10;
+ Assert.Equal (text [0, width], justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = " ~ s gui.cs master ↑10";
+ width = 10;
+ Assert.Equal (text [0, width], justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+ }
+
+ [Fact]
+ public void ClipAndJustify_Valid_Right ()
+ {
+ var align = TextAlignment.Right;
+
+ var text = ustring.Empty;
+ var justifiedText = ustring.Empty;
+ int width = 0;
+
+ text = "test";
+ width = 0;
+ Assert.Equal (ustring.Empty, justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+
+ text = "test";
+ width = 2;
+ Assert.Equal (text [0, width], justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+
+ text = "test";
+ width = int.MaxValue;
+ Assert.Equal (text, justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A sentence has words.";
+ width = int.MaxValue;
+ Assert.Equal (text, justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A sentence has words.";
+ width = 10;
+ Assert.Equal (text [0, width], justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A\tsentence\thas\twords.";
+ width = int.MaxValue;
+ Assert.Equal (text, justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A\tsentence\thas\twords.";
+ width = 10;
+ Assert.Equal (text [0, width], justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "line1\nline2\nline3long!";
+ width = int.MaxValue;
+ Assert.Equal (text, justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "line1\nline2\nline3long!";
+ width = 10;
+ Assert.Equal (text [0, width], justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = " ~ s gui.cs master ↑10";
+ width = 10;
+ Assert.Equal (text [0, width], justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+ }
+
+ [Fact]
+ public void ClipAndJustify_Valid_Centered ()
+ {
+ var align = TextAlignment.Centered;
+
+ var text = ustring.Empty;
+ var justifiedText = ustring.Empty;
+ int width = 0;
+
+ text = "test";
+ width = 0;
+ Assert.Equal (ustring.Empty, justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+
+ text = "test";
+ width = 2;
+ Assert.Equal (text [0, width], justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+
+ text = "test";
+ width = int.MaxValue;
+ Assert.Equal (text, justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A sentence has words.";
+ width = int.MaxValue;
+ Assert.Equal (text, justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A sentence has words.";
+ width = 10;
+ Assert.Equal (text [0, width], justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A\tsentence\thas\twords.";
+ width = int.MaxValue;
+ Assert.Equal (text, justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A\tsentence\thas\twords.";
+ width = 10;
+ Assert.Equal (text [0, width], justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "line1\nline2\nline3long!";
+ width = int.MaxValue;
+ Assert.Equal (text, justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "line1\nline2\nline3long!";
+ width = 10;
+ Assert.Equal (text [0, width], justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = " ~ s gui.cs master ↑10";
+ width = 10;
+ Assert.Equal (text [0, width], justifiedText = TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "";
+ width = text.RuneCount;
+ Assert.Equal (text, justifiedText = TextFormatter.ClipAndJustify (text, width, align)); ;
+ Assert.True (justifiedText.RuneCount <= width);
+ }
+
+
+ [Fact]
+ public void ClipAndJustify_Valid_Justified ()
+ {
+ var text = ustring.Empty;
+ int width = 0;
+ var align = TextAlignment.Justified;
+
+ text = "test";
+ width = 0;
+ Assert.Equal (ustring.Empty, TextFormatter.ClipAndJustify (text, width, align));
+
+ text = "test";
+ width = 2;
+ Assert.Equal (text [0, width], TextFormatter.ClipAndJustify (text, width, align));
+
+ text = "test";
+ width = int.MaxValue;
+ Assert.Equal (text, TextFormatter.ClipAndJustify (text, width, align));
+ Assert.True (text.RuneCount <= width);
+
+ // see Justify_ tests below
+
+ }
+
+ [Fact]
+ public void Justify_Invalid ()
+ {
+ var text = ustring.Empty;
+ Assert.Equal (text, TextFormatter.Justify (text, 0));
+
+ text = null;
+ Assert.Equal (text, TextFormatter.Justify (text, 0));
+
+ text = "test";
+ Assert.Throws (() => TextFormatter.Justify (text, -1));
+ }
+
+ [Fact]
+ public void Justify_SingleWord ()
+ {
+ var text = ustring.Empty;
+ var justifiedText = ustring.Empty;
+ int width = 0;
+ char fillChar = '+';
+
+ text = "word";
+ justifiedText = "word";
+ width = text.RuneCount;
+ Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+
+ text = "word";
+ justifiedText = "word";
+ width = text.RuneCount + 1;
+ Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+
+ text = "word";
+ justifiedText = "word";
+ width = text.RuneCount + 2;
+ Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+
+ text = "word";
+ justifiedText = "word";
+ width = text.RuneCount + 10;
+ Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+
+ text = "word";
+ justifiedText = "word";
+ width = text.RuneCount + 11;
+ Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+ }
+
+ [Fact]
+ public void Justify_Sentence ()
+ {
+ var text = ustring.Empty;
+ var justifiedText = ustring.Empty;
+ int width = 0;
+ char fillChar = '+';
+
+ text = "A sentence has words.";
+ justifiedText = "A+sentence+has+words.";
+ width = text.RuneCount;
+ Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A sentence has words.";
+ justifiedText = "A+sentence+has+words.";
+ width = text.RuneCount + 1;
+ Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A sentence has words.";
+ justifiedText = "A+sentence+has+words.";
+ width = text.RuneCount + 2;
+ Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A sentence has words.";
+ justifiedText = "A++sentence++has++words.";
+ width = text.RuneCount + 3;
+ Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A sentence has words.";
+ justifiedText = "A++sentence++has++words.";
+ width = text.RuneCount + 4;
+ Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A sentence has words.";
+ justifiedText = "A++sentence++has++words.";
+ width = text.RuneCount + 5;
+ Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A sentence has words.";
+ justifiedText = "A+++sentence+++has+++words.";
+ width = text.RuneCount + 6;
+ Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A sentence has words.";
+ justifiedText = "A+++++++sentence+++++++has+++++++words.";
+ width = text.RuneCount + 20;
+ Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+ Assert.True (justifiedText.RuneCount <= width);
+
+ text = "A sentence has words.";
+ justifiedText = "A++++++++sentence++++++++has++++++++words.";
+ width = text.RuneCount + 23;
+ Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+ Assert.True (justifiedText.RuneCount <= width);
+
+ //TODO: Unicode
+ }
+
+ [Fact]
+ public void WordWrap_Invalid ()
+ {
+ var text = ustring.Empty;
+ int width = 0;
+
+ Assert.Empty (TextFormatter.WordWrap (null, width));
+ Assert.Empty (TextFormatter.WordWrap (text, width));
+ Assert.Throws (() => TextFormatter.WordWrap (text, -1));
+ }
+
+ [Fact]
+ public void WordWrap_SingleWordLine ()
+ {
+ var text = ustring.Empty;
+ int width = 0;
+ List wrappedLines;
+
+ text = "Constantinople";
+ width = text.RuneCount;
+ wrappedLines = TextFormatter.WordWrap (text, width);
+ Assert.True (wrappedLines.Count == 1);
+
+ width = text.RuneCount - 1;
+ wrappedLines = TextFormatter.WordWrap (text, width);
+ Assert.Equal (2, wrappedLines.Count);
+ Assert.Equal (text [0, text.RuneCount - 1].ToString (), wrappedLines [0].ToString ());
+ Assert.Equal ("e", wrappedLines [1].ToString ());
+
+ width = text.RuneCount - 2;
+ wrappedLines = TextFormatter.WordWrap (text, width);
+ Assert.Equal (2, wrappedLines.Count);
+ Assert.Equal (text [0, text.RuneCount - 2].ToString (), wrappedLines [0].ToString ());
+
+ width = text.RuneCount - 5;
+ wrappedLines = TextFormatter.WordWrap (text, width);
+ Assert.Equal (2, wrappedLines.Count);
+
+ width = (int)Math.Ceiling ((double)(text.RuneCount / 2F));
+ wrappedLines = TextFormatter.WordWrap (text, width);
+ Assert.Equal (2, wrappedLines.Count);
+ Assert.Equal ("Constan", wrappedLines [0].ToString ());
+ Assert.Equal ("tinople", wrappedLines [1].ToString ());
+
+ width = (int)Math.Ceiling ((double)(text.RuneCount / 3F));
+ wrappedLines = TextFormatter.WordWrap (text, width);
+ Assert.Equal (3, wrappedLines.Count);
+ Assert.Equal ("Const", wrappedLines [0].ToString ());
+ Assert.Equal ("antin", wrappedLines [1].ToString ());
+ Assert.Equal ("ople", wrappedLines [2].ToString ());
+
+ width = (int)Math.Ceiling ((double)(text.RuneCount / 4F));
+ wrappedLines = TextFormatter.WordWrap (text, width);
+ Assert.Equal (4, wrappedLines.Count);
+
+ width = (int)Math.Ceiling ((double)text.RuneCount / text.RuneCount);
+ wrappedLines = TextFormatter.WordWrap (text, width);
+ Assert.Equal (text.RuneCount, wrappedLines.Count);
+ Assert.Equal ("C", wrappedLines [0].ToString ());
+ Assert.Equal ("o", wrappedLines [1].ToString ());
+ Assert.Equal ("n", wrappedLines [2].ToString ());
+ Assert.Equal ("s", wrappedLines [3].ToString ());
+ Assert.Equal ("t", wrappedLines [4].ToString ());
+ Assert.Equal ("a", wrappedLines [5].ToString ());
+ Assert.Equal ("n", wrappedLines [6].ToString ());
+ Assert.Equal ("t", wrappedLines [7].ToString ());
+ Assert.Equal ("i", wrappedLines [8].ToString ());
+ Assert.Equal ("n", wrappedLines [9].ToString ());
+ Assert.Equal ("o", wrappedLines [10].ToString ());
+ Assert.Equal ("p", wrappedLines [11].ToString ());
+ Assert.Equal ("l", wrappedLines [12].ToString ());
+ Assert.Equal ("e", wrappedLines [13].ToString ());
+ }
+
+ [Fact]
+ public void WordWrap_NoNewLines ()
+ {
+ var text = ustring.Empty;
+ int width = 0;
+ List wrappedLines;
+
+ text = "A sentence has words.";
+ width = text.RuneCount;
+ wrappedLines = TextFormatter.WordWrap (text, width);
+ Assert.True (wrappedLines.Count == 1);
+
+ width = text.RuneCount - 1;
+ wrappedLines = TextFormatter.WordWrap (text, width);
+ Assert.Equal (2, wrappedLines.Count);
+ Assert.Equal ("A sentence has", wrappedLines [0].ToString ());
+ Assert.Equal ("words.", wrappedLines [1].ToString ());
+
+ width = text.RuneCount - "words.".Length;
+ wrappedLines = TextFormatter.WordWrap (text, width);
+ Assert.Equal (2, wrappedLines.Count);
+ Assert.Equal ("A sentence has", wrappedLines [0].ToString ());
+ Assert.Equal ("words.", wrappedLines [1].ToString ());
+
+ width = text.RuneCount - " words.".Length;
+ wrappedLines = TextFormatter.WordWrap (text, width);
+ Assert.Equal (2, wrappedLines.Count);
+ Assert.Equal ("A sentence has", wrappedLines [0].ToString ());
+ Assert.Equal ("words.", wrappedLines [1].ToString ());
+
+ width = text.RuneCount - "s words.".Length;
+ wrappedLines = TextFormatter.WordWrap (text, width);
+ Assert.Equal (2, wrappedLines.Count);
+ Assert.Equal ("A sentence", wrappedLines [0].ToString ());
+ Assert.Equal ("has words.", wrappedLines [1].ToString ());
+
+ // Unicode
+ // TODO: Lots of bugs
+ //text = "A Unicode sentence (пÑивеÑ) has words.";
+ //width = text.RuneCount;
+ //wrappedLines = TextFormatter.WordWrap (text, width);
+ //Assert.True (wrappedLines.Count == 1);
+
+ //width = text.RuneCount - 1;
+ //wrappedLines = TextFormatter.WordWrap (text, width);
+ //Assert.Equal (2, wrappedLines.Count);
+ //Assert.Equal ("A Unicode sentence (пÑивеÑ) has", wrappedLines [0].ToString ());
+ //Assert.Equal ("words.", wrappedLines [1].ToString ());
+
+ //width = text.RuneCount - "words.".Length;
+ //wrappedLines = TextFormatter.WordWrap (text, width);
+ //Assert.Equal (2, wrappedLines.Count);
+ //Assert.Equal ("A Unicode sentence (пÑивеÑ) has", wrappedLines [0].ToString ());
+ //Assert.Equal ("words.", wrappedLines [1].ToString ());
+
+ //width = text.RuneCount - " words.".Length;
+ //wrappedLines = TextFormatter.WordWrap (text, width);
+ //Assert.Equal (2, wrappedLines.Count);
+ //Assert.Equal ("A Unicode sentence (пÑивеÑ) has", wrappedLines [0].ToString ());
+ //Assert.Equal ("words.", wrappedLines [1].ToString ());
+
+ //width = text.RuneCount - "s words.".Length;
+ //wrappedLines = TextFormatter.WordWrap (text, width);
+ //Assert.Equal (2, wrappedLines.Count);
+ //Assert.Equal ("A Unicode sentence (пÑивеÑ)", wrappedLines [0].ToString ());
+ //Assert.Equal ("has words.", wrappedLines [1].ToString ());
+
+ //width = text.RuneCount - "веÑ) has words.".Length;
+ //wrappedLines = TextFormatter.WordWrap (text, width);
+ //Assert.Equal (2, wrappedLines.Count);
+ //Assert.Equal ("A Unicode sentence", wrappedLines [0].ToString ());
+ //Assert.Equal ("(пÑивеÑ) has words.", wrappedLines [1].ToString ());
+
+ }
+
+ [Fact]
+ public void ReplaceHotKeyWithTag ()
+ {
+ ustring text = "test";
+ int hotPos = 0;
+ uint tag = 0x100000 | 't';
+
+ Assert.Equal (ustring.Make (new Rune [] { tag, 'e', 's', 't' }), TextFormatter.ReplaceHotKeyWithTag (text, hotPos));
+
+ tag = 0x100000 | 'e';
+ hotPos = 1;
+ Assert.Equal (ustring.Make (new Rune [] { 't', tag, 's', 't' }), TextFormatter.ReplaceHotKeyWithTag (text, hotPos));
+
+ var result = TextFormatter.ReplaceHotKeyWithTag (text, hotPos);
+ Assert.Equal ('e', (uint)(result.ToRunes () [1] & ~0x100000));
+
+ text = "Ok";
+ tag = 0x100000 | 'O';
+ hotPos = 0;
+ Assert.Equal (ustring.Make (new Rune [] { tag, 'k' }), result = TextFormatter.ReplaceHotKeyWithTag (text, hotPos));
+ Assert.Equal ('O', (uint)(result.ToRunes () [0] & ~0x100000));
+
+ text = "[◦ Ok ◦]";
+ text = ustring.Make(new Rune [] { '[', '◦', ' ', 'O', 'k', ' ', '◦', ']' });
+ var runes = text.ToRuneList ();
+ Assert.Equal (text.RuneCount, runes.Count);
+ Assert.Equal (text, ustring.Make(runes));
+ tag = 0x100000 | 'O';
+ hotPos = 3;
+ Assert.Equal (ustring.Make (new Rune [] { '[', '◦', ' ', tag, 'k', ' ', '◦', ']' }), result = TextFormatter.ReplaceHotKeyWithTag (text, hotPos));
+ Assert.Equal ('O', (uint)(result.ToRunes () [3] & ~0x100000));
+
+ text = "^k";
+ tag = '^';
+ hotPos = 0;
+ Assert.Equal (ustring.Make (new Rune [] { tag, 'k' }), result = TextFormatter.ReplaceHotKeyWithTag (text, hotPos));
+ Assert.Equal ('^', (uint)(result.ToRunes () [0] & ~0x100000));
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj
index 917fbcf59..8a8ecb0a6 100644
--- a/UnitTests/UnitTests.csproj
+++ b/UnitTests/UnitTests.csproj
@@ -6,11 +6,17 @@
-
+
-
-
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+