From 49bfe6a6c23d4e5e34c185463f60c27fbbefa32d Mon Sep 17 00:00:00 2001 From: Charlie Kindel Date: Wed, 17 Jun 2020 08:50:38 -0700 Subject: [PATCH] progress...rabbit hole --- Example/demo.cs | 1 - Terminal.Gui/Core/ConsoleDriver.cs | 2 +- Terminal.Gui/Core/TextFormatter.cs | 536 +++++++++ Terminal.Gui/Core/View.cs | 527 +-------- Terminal.Gui/Core/Window.cs | 10 - Terminal.Gui/Views/Button.cs | 59 +- Terminal.Gui/Views/Checkbox.cs | 2 +- Terminal.Gui/Views/ComboBox.cs | 6 +- Terminal.Gui/Views/DateField.cs | 8 +- Terminal.Gui/Views/FrameView.cs | 11 - Terminal.Gui/Views/Menu.cs | 4 +- Terminal.Gui/Views/RadioGroup.cs | 4 +- Terminal.Gui/Views/TextField.cs | 22 +- Terminal.Gui/Views/TextView.cs | 2 + Terminal.Gui/Windows/FileDialog.cs | 4 +- Terminal.Gui/Windows/MessageBox.cs | 6 +- Terminal.GuiTests/Core/TextFormatterTests.cs | 12 + Terminal.GuiTests/Terminal.GuiTests.csproj | 20 + Terminal.sln | 2 +- UICatalog/Scenarios/Buttons.cs | 10 +- UICatalog/Scenarios/LabelsAsButtons.cs | 9 +- UICatalog/Scenarios/Mouse.cs | 1 - UICatalog/Scenarios/Scrolling.cs | 1 - UICatalog/Scenarios/TextAlignments.cs | 71 +- UICatalog/Scenarios/ViewWithText.cs | 2 - UICatalog/UICatalog.cs | 2 +- UnitTests/TextFormatterTests.cs | 1017 ++++++++++++++++++ UnitTests/UnitTests.csproj | 14 +- 28 files changed, 1752 insertions(+), 613 deletions(-) create mode 100644 Terminal.Gui/Core/TextFormatter.cs create mode 100644 Terminal.GuiTests/Core/TextFormatterTests.cs create mode 100644 Terminal.GuiTests/Terminal.GuiTests.csproj create mode 100644 UnitTests/TextFormatterTests.cs 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 +