Ross Ferguson преди 5 години
родител
ревизия
3d8ef9a2de
променени са 40 файла, в които са добавени 3576 реда и са изтрити 802 реда
  1. 0 1
      Example/demo.cs
  2. 14 21
      Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs
  3. 14 16
      Terminal.Gui/ConsoleDrivers/WindowsDriver.cs
  4. 1 1
      Terminal.Gui/Core/ConsoleDriver.cs
  5. 3 1
      Terminal.Gui/Core/MainLoop.cs
  6. 578 0
      Terminal.Gui/Core/TextFormatter.cs
  7. 188 185
      Terminal.Gui/Core/View.cs
  8. 31 16
      Terminal.Gui/Core/Window.cs
  9. 9 1
      Terminal.Gui/Terminal.Gui.csproj
  10. 54 84
      Terminal.Gui/Views/Button.cs
  11. 2 2
      Terminal.Gui/Views/Checkbox.cs
  12. 5 4
      Terminal.Gui/Views/ComboBox.cs
  13. 4 4
      Terminal.Gui/Views/DateField.cs
  14. 38 6
      Terminal.Gui/Views/FrameView.cs
  15. 41 320
      Terminal.Gui/Views/Label.cs
  16. 2 2
      Terminal.Gui/Views/Menu.cs
  17. 2 3
      Terminal.Gui/Views/RadioGroup.cs
  18. 13 12
      Terminal.Gui/Views/TextField.cs
  19. 30 31
      Terminal.Gui/Views/TextView.cs
  20. 6 4
      Terminal.Gui/Windows/Dialog.cs
  21. 2 2
      Terminal.Gui/Windows/FileDialog.cs
  22. 3 3
      Terminal.Gui/Windows/MessageBox.cs
  23. 1 1
      Terminal.sln
  24. 5 4
      UICatalog/Scenarios/AllViewsTester.cs
  25. 13 13
      UICatalog/Scenarios/Buttons.cs
  26. 2 1
      UICatalog/Scenarios/CharacterMap.cs
  27. 1 1
      UICatalog/Scenarios/Clipping.cs
  28. 300 0
      UICatalog/Scenarios/LabelsAsButtons.cs
  29. 1 1
      UICatalog/Scenarios/MessageBoxes.cs
  30. 0 1
      UICatalog/Scenarios/Mouse.cs
  31. 4 4
      UICatalog/Scenarios/Progress.cs
  32. 0 1
      UICatalog/Scenarios/Scrolling.cs
  33. 82 10
      UICatalog/Scenarios/TextAlignments.cs
  34. 78 0
      UICatalog/Scenarios/TextFormatterDemo.cs
  35. 26 31
      UICatalog/Scenarios/Unicode.cs
  36. 17 9
      UICatalog/Scenarios/WindowsAndFrameViews.cs
  37. 1 1
      UICatalog/UICatalog.cs
  38. 1994 0
      UnitTests/TextFormatterTests.cs
  39. 10 4
      UnitTests/UnitTests.csproj
  40. 1 1
      UnitTests/ViewTests.cs

+ 0 - 1
Example/demo.cs

@@ -631,7 +631,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++}";
 		};
 

+ 14 - 21
Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs

@@ -168,45 +168,38 @@ namespace Terminal.Gui {
 
 		bool IMainLoopDriver.EventsPending (bool wait)
 		{
-			int pollTimeout = 0;
-			int n;
-
-			if (CkeckTimeout (wait, ref pollTimeout))
+			if (CheckTimers (wait, out var pollTimeout)) {
 				return true;
+			}
 
 			UpdatePollMap ();
 
-			while (true) {
-				n = poll (pollmap, (uint)pollmap.Length, 0);
-				if (pollmap != null) {
-					break;
-				}
-				if (mainLoop.idleHandlers.Count > 0 || CkeckTimeout (wait, ref pollTimeout)) {
-					return true;
-				}
-			}
-			int ic;
-			lock (mainLoop.idleHandlers)
-				ic = mainLoop.idleHandlers.Count;
-			return n > 0 || mainLoop.timeouts.Count > 0 && ((mainLoop.timeouts.Keys [0] - DateTime.UtcNow.Ticks) < 0) || ic > 0;
+			var n = poll (pollmap, (uint)pollmap.Length, pollTimeout);
+			
+			return n > 0 || CheckTimers (wait, out pollTimeout);
 		}
 
-		bool CkeckTimeout (bool wait, ref int pollTimeout)
+		bool CheckTimers (bool wait, out int pollTimeout)
 		{
 			long now = DateTime.UtcNow.Ticks;
 
 			if (mainLoop.timeouts.Count > 0) {
 				pollTimeout = (int)((mainLoop.timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond);
-				if (pollTimeout < 0)
+				if (pollTimeout < 0) {
 					return true;
-
+				}
 			} else
 				pollTimeout = -1;
 
 			if (!wait)
 				pollTimeout = 0;
 
-			return false;
+			int ic;
+			lock (mainLoop.idleHandlers) {
+				ic = mainLoop.idleHandlers.Count;
+			}
+
+			return ic > 0;
 		}
 
 		void IMainLoopDriver.MainIteration ()

+ 14 - 16
Terminal.Gui/ConsoleDrivers/WindowsDriver.cs

@@ -615,24 +615,16 @@ namespace Terminal.Gui {
 
 		bool IMainLoopDriver.EventsPending (bool wait)
 		{
-			int waitTimeout = 0;
-
-			if (CkeckTimeout (wait, ref waitTimeout))
+			if (CheckTimers (wait, out var waitTimeout)) {
 				return true;
+			}
 
 			result = null;
 			waitForProbe.Set ();
 
 			try {
-				while (result == null) {
-					if (!tokenSource.IsCancellationRequested)
-						eventReady.Wait (0, tokenSource.Token);
-					if (result != null) {
-						break;
-					}
-					if (mainLoop.idleHandlers.Count > 0 || CkeckTimeout (wait, ref waitTimeout)) {
-						return true;
-					}
+				if (!tokenSource.IsCancellationRequested) {
+					eventReady.Wait (waitTimeout, tokenSource.Token);
 				}
 			} catch (OperationCanceledException) {
 				return true;
@@ -640,15 +632,16 @@ namespace Terminal.Gui {
 				eventReady.Reset ();
 			}
 
-			if (!tokenSource.IsCancellationRequested)
-				return result != null;
+			if (!tokenSource.IsCancellationRequested) {
+				return result != null || CheckTimers (wait, out waitTimeout);
+			}
 
 			tokenSource.Dispose ();
 			tokenSource = new CancellationTokenSource ();
 			return true;
 		}
 
-		bool CkeckTimeout (bool wait, ref int waitTimeout)
+		bool CheckTimers (bool wait, out int waitTimeout)
 		{
 			long now = DateTime.UtcNow.Ticks;
 
@@ -663,7 +656,12 @@ namespace Terminal.Gui {
 			if (!wait)
 				waitTimeout = 0;
 
-			return false;
+			int ic;
+			lock (mainLoop.idleHandlers) {
+				ic = mainLoop.idleHandlers.Count;
+			}
+
+			return ic > 0;
 		}
 
 		Action<KeyEvent> keyHandler;

+ 1 - 1
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 (' ');
 			}

+ 3 - 1
Terminal.Gui/Core/MainLoop.cs

@@ -95,9 +95,11 @@ namespace Terminal.Gui {
 		/// <param name="idleHandler">Token that can be used to remove the idle handler with <see cref="RemoveIdle(Func{bool})"/> .</param>
 		public Func<bool> AddIdle (Func<bool> idleHandler)
 		{
-			lock (idleHandlers)
+			lock (idleHandlers) {
 				idleHandlers.Add (idleHandler);
+			}
 
+			Driver.Wakeup ();
 			return idleHandler;
 		}
 

+ 578 - 0
Terminal.Gui/Core/TextFormatter.cs

@@ -0,0 +1,578 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using NStack;
+
+namespace Terminal.Gui {
+	/// <summary>
+	/// Text alignment enumeration, controls how text is displayed.
+	/// </summary>
+	public enum TextAlignment {
+		/// <summary>
+		/// Aligns the text to the left of the frame.
+		/// </summary>
+		Left,
+		/// <summary>
+		/// Aligns the text to the right side of the frame.
+		/// </summary>
+		Right,
+		/// <summary>
+		/// Centers the text in the frame.
+		/// </summary>
+		Centered,
+		/// <summary>
+		/// Shows the text as justified text in the frame.
+		/// </summary>
+		Justified
+	}
+
+	/// <summary>
+	/// Provides text formatting capabilites for console apps. Supports, hotkeys, horizontal alignment, multille lines, and word-based line wrap.
+	/// </summary>
+	public class TextFormatter {
+		List<ustring> lines = new List<ustring> ();
+		ustring text;
+		TextAlignment textAlignment;
+		Attribute textColor = -1;
+		bool needsFormat = true;
+		Key hotKey;
+		Size size;
+
+		/// <summary>
+		///   The text to be displayed. This text is never modified.
+		/// </summary>
+		public virtual ustring Text {
+			get => text;
+			set {
+				text = value;
+				needsFormat = true;
+			}
+		}
+
+		// TODO: Add Vertical Text Alignment
+		/// <summary>
+		/// Controls the horizontal text-alignment property. 
+		/// </summary>
+		/// <value>The text alignment.</value>
+		public TextAlignment Alignment {
+			get => textAlignment;
+			set {
+				textAlignment = value;
+				needsFormat = true;
+			}
+		}
+
+		/// <summary>
+		///  Gets the size of the area the text will be drawn in. 
+		/// </summary>
+		public Size Size {
+			get => size;
+			internal set {
+				size = value;
+				needsFormat = true;
+			}
+		}
+
+		/// <summary>
+		/// The specifier character for the hotkey (e.g. '_'). Set to '\xffff' to disable hotkey support for this View instance. The default is '\xffff'. 
+		/// </summary>
+		public Rune HotKeySpecifier { get; set; } = (Rune)0xFFFF;
+
+		/// <summary>
+		/// The position in the text of the hotkey. The hotkey will be rendered using the hot color.
+		/// </summary>
+		public int HotKeyPos { get => hotKeyPos; set => hotKeyPos = value; }
+
+		/// <summary>
+		/// Gets the hotkey. Will be an upper case letter or digit.
+		/// </summary>
+		public Key HotKey { get => hotKey; internal set => hotKey = value; }
+
+		/// <summary>
+		/// Specifies the mask to apply to the hotkey to tag it as the hotkey. The default value of <c>0x100000</c> causes
+		/// the underlying Rune to be identified as a "private use" Unicode character.
+		/// </summary>HotKeyTagMask
+		public uint HotKeyTagMask { get; set; } = 0x100000;
+
+		/// <summary>
+		/// Gets the formatted lines.
+		/// </summary>
+		public List<ustring> Lines {
+			get {
+				// With this check, we protect against subclasses with overrides of Text
+				if (ustring.IsNullOrEmpty (Text)) {
+					lines = new List<ustring> ();
+					lines.Add (ustring.Empty);
+					needsFormat = false;
+					return lines;
+				}
+
+				if (needsFormat) {
+					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);
+					}
+					lines = Format (shown_text, Size.Width, textAlignment, Size.Height > 1);
+				}
+				needsFormat = false;
+				return lines;
+			}
+		}
+
+		/// <summary>
+		/// Sets a flag indicating the text needs to be formatted. 
+		/// Subsequent calls to <see cref="Draw"/>, <see cref="Lines"/>, etc... will cause the formatting to happen.>
+		/// </summary>
+		public void SetNeedsFormat ()
+		{
+			needsFormat = true;
+		}
+
+
+		static ustring StripCRLF (ustring str)
+		{
+			var runes = str.ToRuneList ();
+			for (int i = 0; i < runes.Count; i++) {
+				switch (runes [i]) {
+				case '\n':
+					runes.RemoveAt (i);
+					break;
+
+				case '\r':
+					if ((i + 1) < runes.Count && runes [i + 1] == '\n') {
+						runes.RemoveAt (i);
+						runes.RemoveAt (i + 1);
+						i++;
+					}
+					break;
+				}
+			}
+			return ustring.Make (runes);
+		}
+		static ustring ReplaceCRLFWithSpace (ustring str)
+		{
+			var runes = str.ToRuneList ();
+			for (int i = 0; i < runes.Count; i++) {
+				switch (runes [i]) {
+				case '\n':
+					runes [i] = (Rune)' ';
+					break;
+
+				case '\r':
+					if ((i + 1) < runes.Count && runes [i + 1] == '\n') {
+						runes [i] = (Rune)' ';
+						runes.RemoveAt (i + 1);
+						i++;
+					}
+					break;
+				}
+			}
+			return ustring.Make (runes); ;
+		}
+
+		/// <summary>
+		/// Formats the provided text to fit within the width provided using word wrapping.
+		/// </summary>
+		/// <param name="text">The text to word warp</param>
+		/// <param name="width">The width to contrain the text to</param>
+		/// <returns>Returns a list of word wrapped lines.</returns>
+		/// <remarks>
+		/// <para>
+		/// This method does not do any justification.
+		/// </para>
+		/// <para>
+		/// Newlines ('\n' and '\r\n') sequences are honored, adding the appropriate lines to the output.
+		/// </para>
+		/// </remarks>
+		public static List<ustring> WordWrap (ustring text, int width)
+		{
+			if (width < 0) {
+				throw new ArgumentOutOfRangeException ("Width cannot be negative.");
+			}
+
+			int start = 0, end;
+			var lines = new List<ustring> ();
+
+			if (ustring.IsNullOrEmpty (text)) {
+				return lines;
+			}
+
+			var runes = StripCRLF (text).ToRuneList();
+
+			while ((end = start + width) < runes.Count) {
+				while (runes [end] != ' ' && end > start)
+					end -= 1;
+				if (end == start)
+					end = start + width;
+				lines.Add (ustring.Make (runes.GetRange (start, end - start)).TrimSpace());
+				start = end;
+			}
+
+			if (start < text.RuneCount) {
+				lines.Add (ustring.Make (runes.GetRange (start, runes.Count - start)).TrimSpace ());
+			}
+
+			return lines;
+		}
+
+		/// <summary>
+		/// Justifies text within a specified width. 
+		/// </summary>
+		/// <param name="text">The text to justify.</param>
+		/// <param name="width">If the text length is greater that <c>width</c> it will be clipped.</param>
+		/// <param name="talign">Alignment.</param>
+		/// <returns>Justified and clipped text.</returns>
+		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;
+			}
+
+			var runes = text.ToRuneList ();
+			int slen = runes.Count;
+			if (slen > width) {
+				return ustring.Make (runes.GetRange(0, width));
+			} else {
+				if (talign == TextAlignment.Justified) {
+					return Justify (text, width);
+				}
+				return text;
+			}
+		}
+
+		/// <summary>
+		/// 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 <c>width</c>. Spaces will not be added to the ends.
+		/// </summary>
+		/// <param name="text"></param>
+		/// <param name="width"></param>
+		/// <param name="spaceChar">Character to replace whitespace and pad with. For debugging purposes.</param>
+		/// <returns>The justifed text.</returns>
+		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.Split (ustring.Make (' '));// whitespace, StringSplitOptions.RemoveEmptyEntries);
+			int textCount = words.Sum (arg => arg.RuneCount);
+
+			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;
+
+		/// <summary>
+		/// Reformats text into lines, applying text alignment and optionally wrapping text to new lines on word boundaries.
+		/// </summary>
+		/// <param name="text"></param>
+		/// <param name="width">The width to bound the text to for word wrapping and clipping.</param>
+		/// <param name="talign">Specifies how the text will be aligned horizontally.</param>
+		/// <param name="wordWrap">If <c>true</c>, the text will be wrapped to new lines as need. If <c>false</c>, forces text to fit a single line. Line breaks are converted to spaces. The text will be clipped to <c>width</c></param>
+		/// <returns>A list of word wrapped lines.</returns>
+		/// <remarks>
+		/// <para>
+		/// An empty <c>text</c> string will result in one empty line.
+		/// </para>
+		/// <para>
+		/// If <c>width</c> is 0, a single, empty line will be returned.
+		/// </para>
+		/// </remarks>
+		public static List<ustring> Format (ustring text, int width, TextAlignment talign, bool wordWrap)
+		{
+			if (width < 0) {
+				throw new ArgumentOutOfRangeException ("width cannot be negative");
+			}
+
+			List<ustring> lineResult = new List<ustring> ();
+
+			if (ustring.IsNullOrEmpty (text) || width == 0) {
+				lineResult.Add (ustring.Empty);
+				return lineResult;
+			}
+
+			if (wordWrap == false) {
+				text = ReplaceCRLFWithSpace (text);
+				lineResult.Add (ClipAndJustify (text, width, talign));
+				return lineResult;
+			}
+
+			var runes = text.ToRuneList ();
+			int runeCount = runes.Count;
+			int lp = 0;
+			for (int i = 0; i < runeCount; i++) {
+				Rune c = text [i];
+				if (c == '\n') {
+					var wrappedLines = WordWrap (ustring.Make (runes.GetRange(lp, i - lp)), 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 (ustring.Make (runes.GetRange(lp, runeCount - lp)), width)) {
+				lineResult.Add (ClipAndJustify (line, width, talign));
+			}
+
+			return lineResult;
+		}
+
+		/// <summary>
+		/// Computes the number of lines needed to render the specified text given the width.
+		/// </summary>
+		/// <returns>Number of lines.</returns>
+		/// <param name="text">Text, may contain newlines.</param>
+		/// <param name="width">The minimum width for the text.</param>
+		public static int MaxLines (ustring text, int width)
+		{
+			var result = TextFormatter.Format (text, width, TextAlignment.Left, true);
+			return result.Count;
+		}
+
+		/// <summary>
+		/// Computes the maximum width needed to render the text (single line or multple lines).
+		/// </summary>
+		/// <returns>Max width of lines.</returns>
+		/// <param name="text">Text, may contain newlines.</param>
+		/// <param name="width">The minimum width for the text.</param>
+		public static int MaxWidth (ustring text, int width)
+		{
+			var result = TextFormatter.Format (text, width, TextAlignment.Left, true);
+			return result.Max (s => s.RuneCount);
+		}
+
+
+		/// <summary>
+		///  Calculates the rectangle requried to hold text, assuming no word wrapping.
+		/// </summary>
+		/// <param name="x">The x location of the rectangle</param>
+		/// <param name="y">The y location of the rectangle</param>
+		/// <param name="text">The text to measure</param>
+		/// <returns></returns>
+		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);
+		}
+
+		/// <summary>
+		/// Finds the hotkey and its location in text. 
+		/// </summary>
+		/// <param name="text">The text to look in.</param>
+		/// <param name="hotKeySpecifier">The hotkey specifier (e.g. '_') to look for.</param>
+		/// <param name="firstUpperCase">If <c>true</c> the legacy behavior of identifying the first upper case character as the hotkey will be eanbled. 
+		/// Regardless of the value of this parameter, <c>hotKeySpecifier</c> takes precidence.</param>
+		/// <param name="hotPos">Outputs the Rune index into <c>text</c>.</param>
+		/// <param name="hotKey">Outputs the hotKey.</param>
+		/// <returns><c>true</c> if a hotkey was found; <c>false</c> otherwise.</returns>
+		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;
+		}
+
+		/// <summary>
+		/// Replaces the Rune at the index specfiied by the <c>hotPos</c> parameter with a tag identifying 
+		/// it as the hotkey. 
+		/// </summary>
+		/// <param name="text">The text to tag the hotkey in.</param>
+		/// <param name="hotPos">The Rune index of the hotkey in <c>text</c>.</param>
+		/// <returns>The text with the hotkey tagged.</returns>
+		/// <remarks>
+		/// The returned string will not render correctly without first un-doing the tag. To undo the tag, search for 
+		/// Runes with a bitmask of <c>otKeyTagMask</c> and remove that bitmask.
+		/// </remarks>
+		public 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] | HotKeyTagMask);
+			}
+			return ustring.Make (runes);
+		}
+
+		/// <summary>
+		/// Removes the hotkey specifier from text.
+		/// </summary>
+		/// <param name="text">The text to manipulate.</param>
+		/// <param name="hotKeySpecifier">The hot-key specifier (e.g. '_') to look for.</param>
+		/// <param name="hotPos">Returns the postion of the hot-key in the text. -1 if not found.</param>
+		/// <returns>The input text with the hotkey specifier ('_') removed.</returns>
+		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;
+		}
+
+		/// <summary>
+		/// Draws the text held by <see cref="TextFormatter"/> to <see cref="Application.Driver"/> using the colors specified.
+		/// </summary>
+		/// <param name="bounds">Specifies the screen-relative location and maximum size for drawing the text.</param>
+		/// <param name="normalColor">The color to use for all text except the hotkey</param>
+		/// <param name="hotColor">The color to use to draw the hotkey</param>
+		public void Draw (Rect bounds, Attribute normalColor, Attribute hotColor)
+		{
+			// With this check, we protect against subclasses with overrides of Text
+			if (ustring.IsNullOrEmpty (text)) {
+				return;
+			}
+
+			Application.Driver.SetAttribute (normalColor);
+
+			// Use "Lines" to ensure a Format (don't use "lines"))
+			for (int line = 0; line < Lines.Count; line++) {
+				if (line > bounds.Height)
+					continue;
+				var runes = lines [line].ToRunes ();
+				int x;
+				switch (textAlignment) {
+				case TextAlignment.Left:
+					x = bounds.Left;
+					break;
+				case TextAlignment.Justified:
+					x = bounds.Left;
+					break;
+				case TextAlignment.Right:
+					x = bounds.Right - runes.Length;
+					break;
+				case TextAlignment.Centered:
+					x = bounds.Left + (bounds.Width - runes.Length) / 2;
+					break;
+				default:
+					throw new ArgumentOutOfRangeException ();
+				}
+				for (var col = bounds.Left; col < bounds.Left + bounds.Width; col++) {
+					Application.Driver.Move (col, bounds.Top + line);
+					var rune = (Rune)' ';
+					if (col >= x && col < (x + runes.Length)) {
+						rune = runes [col - x];
+					}
+					if ((rune & HotKeyTagMask) == HotKeyTagMask) {
+						Application.Driver.SetAttribute (hotColor);
+						Application.Driver.AddRune ((Rune)((uint)rune & ~HotKeyTagMask));
+						Application.Driver.SetAttribute (normalColor);
+					} else {
+						Application.Driver.AddRune (rune);
+					}
+				}
+			}
+		}
+
+	}
+}

+ 188 - 185
Terminal.Gui/Core/View.cs

@@ -1,4 +1,4 @@
-//
+//
 // Authors:
 //   Miguel de Icaza ([email protected])
 //
@@ -13,32 +13,11 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 using NStack;
 
 namespace Terminal.Gui {
-	/// <summary>
-	/// Text alignment enumeration, controls how text is displayed.
-	/// </summary>
-	public enum TextAlignment {
-		/// <summary>
-		/// Aligns the text to the left of the frame.
-		/// </summary>
-		Left,
-		/// <summary>
-		/// Aligns the text to the right side of the frame.
-		/// </summary>
-		Right,
-		/// <summary>
-		/// Centers the text in the frame.
-		/// </summary>
-		Centered,
-		/// <summary>
-		/// Shows the text as justified text in the frame.
-		/// </summary>
-		Justified
-	}
-
 	/// <summary>
 	/// Determines the LayoutStyle for a view, if Absolute, during LayoutSubviews, the
 	/// value from the Frame will be used, if the value is Computed, then the Frame
@@ -131,7 +110,8 @@ namespace Terminal.Gui {
 	///    frames for the vies that use <see cref="LayoutStyle.Computed"/>.
 	/// </para>
 	/// </remarks>
-	public class View : Responder, IEnumerable {
+	public partial class View : Responder, IEnumerable {
+
 		internal enum Direction {
 			Forward,
 			Backward
@@ -142,6 +122,8 @@ namespace Terminal.Gui {
 		View focused = null;
 		Direction focusDirection;
 
+		TextFormatter viewText;
+
 		/// <summary>
 		/// Event fired when the view gets focus.
 		/// </summary>
@@ -167,6 +149,16 @@ namespace Terminal.Gui {
 		/// </summary>
 		public Action<MouseEventArgs> MouseClick;
 
+		/// <summary>
+		/// Gets or sets 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.
+		/// </summary>
+		public Key HotKey { get => viewText.HotKey; set => viewText.HotKey = value; }
+
+		/// <summary>
+		/// Gets or sets the specifier character for the hotkey (e.g. '_'). Set to '\xffff' to disable hotkey support for this View instance. The default is '\xffff'. 
+		/// </summary>
+		public Rune HotKeySpecifier { get => viewText.HotKeySpecifier; set => viewText.HotKeySpecifier = value; }
+
 		internal Direction FocusDirection {
 			get => SuperView?.FocusDirection ?? focusDirection;
 			set {
@@ -389,29 +381,100 @@ namespace Terminal.Gui {
 		/// </remarks>
 		public View (Rect frame)
 		{
+			viewText = new TextFormatter ();
+			this.Text = ustring.Empty;
+
 			this.Frame = frame;
-			CanFocus = false;
 			LayoutStyle = LayoutStyle.Absolute;
 		}
 
 		/// <summary>
-		/// Initializes a new instance of <see cref="LayoutStyle.Computed"/> <see cref="View"/> class.
+		///   Initializes a new instance of <see cref="View"/> using <see cref="LayoutStyle.Computed"/> layout.
 		/// </summary>
 		/// <remarks>
+		/// <para>
 		///   Use <see cref="X"/>, <see cref="Y"/>, <see cref="Width"/>, and <see cref="Height"/> properties to dynamically control the size and location of the view.
-		/// </remarks>
-		/// <remarks>
+		///   The <see cref="Label"/> will be created using <see cref="LayoutStyle.Computed"/>
+		///   coordinates. The initial size (<see cref="View.Frame"/> will be 
+		///   adjusted to fit the contents of <see cref="Text"/>, including newlines ('\n') for multiple lines. 
+		/// </para>
+		/// <para>
+		///   If <c>Height</c> is greater than one, word wrapping is provided.
+		/// </para>
+		/// <para>
 		///   This constructor intitalize a View with a <see cref="LayoutStyle"/> of <see cref="LayoutStyle.Computed"/>. 
 		///   Use <see cref="X"/>, <see cref="Y"/>, <see cref="Width"/>, and <see cref="Height"/> properties to dynamically control the size and location of the view.
+		/// </para>
 		/// </remarks>
-		public View ()
+		public View () : this (text: string.Empty) { }
+
+
+		/// <summary>
+		///   Initializes a new instance of <see cref="View"/> using <see cref="LayoutStyle.Absolute"/> layout.
+		/// </summary>
+		/// <remarks>
+		/// <para>
+		///   The <see cref="View"/> will be created at the given
+		///   coordinates with the given string. The size (<see cref="View.Frame"/> will be 
+		///   adjusted to fit the contents of <see cref="Text"/>, including newlines ('\n') for multiple lines. 
+		/// </para>
+		/// <para>
+		///   No line wrapping is provided.
+		/// </para>
+		/// </remarks>
+		/// <param name="x">column to locate the Label.</param>
+		/// <param name="y">row to locate the Label.</param>
+		/// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
+		public View (int x, int y, ustring text) : this (TextFormatter.CalcRect (x, y, text), text) { }
+
+		/// <summary>
+		///   Initializes a new instance of <see cref="View"/> using <see cref="LayoutStyle.Absolute"/> layout.
+		/// </summary>
+		/// <remarks>
+		/// <para>
+		///   The <see cref="View"/> will be created at the given
+		///   coordinates with the given string. The initial size (<see cref="View.Frame"/> will be 
+		///   adjusted to fit the contents of <see cref="Text"/>, including newlines ('\n') for multiple lines. 
+		/// </para>
+		/// <para>
+		///   If <c>rect.Height</c> is greater than one, word wrapping is provided.
+		/// </para>
+		/// </remarks>
+		/// <param name="rect">Location.</param>
+		/// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
+		public View (Rect rect, ustring text) : this (rect)
+		{
+			viewText = new TextFormatter ();
+			this.Text = text;
+		}
+
+		/// <summary>
+		///   Initializes a new instance of <see cref="View"/> using <see cref="LayoutStyle.Computed"/> layout.
+		/// </summary>
+		/// <remarks>
+		/// <para>
+		///   The <see cref="View"/> will be created using <see cref="LayoutStyle.Computed"/>
+		///   coordinates with the given string. The initial size (<see cref="View.Frame"/> will be 
+		///   adjusted to fit the contents of <see cref="Text"/>, including newlines ('\n') for multiple lines. 
+		/// </para>
+		/// <para>
+		///   If <c>Height</c> is greater than one, word wrapping is provided.
+		/// </para>
+		/// </remarks>
+		/// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
+		public View (ustring text) : base ()
 		{
+			viewText = new TextFormatter ();
+			this.Text = text;
+
 			CanFocus = false;
 			LayoutStyle = LayoutStyle.Computed;
+			// BUGBUG: CalcRect doesn't account for line wrapping
+			var r = TextFormatter.CalcRect (0, 0, text);
 			x = Pos.At (0);
 			y = Pos.At (0);
-			Height = 0;
-			Width = 0;
+			Width = r.Width;
+			Height = r.Height;
 		}
 
 		/// <summary>
@@ -432,6 +495,7 @@ namespace Terminal.Gui {
 			if (SuperView == null)
 				return;
 			SuperView.SetNeedsLayout ();
+			viewText.SetNeedsFormat ();
 		}
 
 		/// <summary>
@@ -763,21 +827,24 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Utility function to draw strings that contain a hotkey.
 		/// </summary>
-		/// <param name="text">String to display, the underscoore before a letter flags the next letter as the hotkey.</param>
+		/// <param name="text">String to display, the hotkey specifier before a letter flags the next letter as the hotkey.</param>
 		/// <param name="hotColor">Hot color.</param>
 		/// <param name="normalColor">Normal color.</param>
 		/// <remarks>
-		/// The hotkey is any character following an underscore ('_') character.</remarks>
+		/// <para>The hotkey is any character following the hotkey specifier, which is the underscore ('_') character by default.</para>
+		/// <para>The hotkey specifier can be changed via <see cref="HotKeySpecifier"/></para>
+		/// </remarks>
 		public void DrawHotString (ustring text, Attribute hotColor, Attribute normalColor)
 		{
-			Driver.SetAttribute (normalColor);
+			var hotkeySpec = HotKeySpecifier == (Rune)0xffff ? (Rune)'_' : HotKeySpecifier;
+			Application.Driver.SetAttribute (normalColor);
 			foreach (var rune in text) {
-				if (rune == '_') {
-					Driver.SetAttribute (hotColor);
+				if (rune == hotkeySpec) {
+					Application.Driver.SetAttribute (hotColor);
 					continue;
 				}
-				Driver.AddRune (rune);
-				Driver.SetAttribute (normalColor);
+				Application.Driver.AddRune (rune);
+				Application.Driver.SetAttribute (normalColor);
 			}
 		}
 
@@ -819,8 +886,13 @@ namespace Terminal.Gui {
 		{
 			if (focused != null)
 				focused.PositionCursor ();
-			else
-				Move (frame.X, frame.Y);
+			else {
+				if (CanFocus && HasFocus) {
+					Move (viewText.HotKeyPos == -1 ? 1 : viewText.HotKeyPos, 0);
+				} else {
+					Move (frame.X, frame.Y);
+				}
+			}
 		}
 
 		/// <inheritdoc/>
@@ -971,6 +1043,15 @@ namespace Terminal.Gui {
 		{
 			var clipRect = new Rect (Point.Empty, frame.Size);
 
+			Driver.SetAttribute (HasFocus ? ColorScheme.Focus : ColorScheme.Normal);
+
+			if (!ustring.IsNullOrEmpty (Text)) {
+				Clear ();
+				// Draw any Text
+				viewText?.SetNeedsFormat ();
+				viewText?.Draw (ViewToScreen (Bounds), HasFocus ? ColorScheme.Focus : ColorScheme.Normal, HasFocus ? ColorScheme.HotFocus : ColorScheme.HotNormal);
+			}
+
 			// Invoke DrawContentEvent
 			OnDrawContent (bounds);
 
@@ -978,8 +1059,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;
@@ -987,7 +1066,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;
@@ -1083,7 +1161,6 @@ namespace Terminal.Gui {
 		/// <inheritdoc/>
 		public override bool ProcessKey (KeyEvent keyEvent)
 		{
-
 			KeyEventEventArgs args = new KeyEventEventArgs (keyEvent);
 			KeyPress?.Invoke (args);
 			if (args.Handled)
@@ -1104,7 +1181,7 @@ namespace Terminal.Gui {
 			if (subviews == null || subviews.Count == 0)
 				return false;
 			foreach (var view in subviews)
-				if (view.SuperView.IsCurrentTop && view.ProcessHotKey (keyEvent))
+				if (view.ProcessHotKey (keyEvent))
 					return true;
 			return false;
 		}
@@ -1119,7 +1196,7 @@ namespace Terminal.Gui {
 			if (subviews == null || subviews.Count == 0)
 				return false;
 			foreach (var view in subviews)
-				if (view.SuperView.IsCurrentTop && view.ProcessColdKey (keyEvent))
+				if (view.ProcessColdKey (keyEvent))
 					return true;
 			return false;
 		}
@@ -1139,7 +1216,7 @@ namespace Terminal.Gui {
 			if (subviews == null || subviews.Count == 0)
 				return false;
 			foreach (var view in subviews)
-				if (view.SuperView.IsCurrentTop && view.OnKeyDown (keyEvent))
+				if (view.HasFocus && view.OnKeyDown (keyEvent))
 					return true;
 
 			return false;
@@ -1160,7 +1237,7 @@ namespace Terminal.Gui {
 			if (subviews == null || subviews.Count == 0)
 				return false;
 			foreach (var view in subviews)
-				if (view.SuperView.IsCurrentTop && view.OnKeyUp (keyEvent))
+				if (view.HasFocus && view.OnKeyUp (keyEvent))
 					return true;
 
 			return false;
@@ -1385,7 +1462,7 @@ namespace Terminal.Gui {
 			}
 
 			if (edges.Any ()) {
-				if (!object.ReferenceEquals(edges.First ().From, edges.First ().To)) {
+				if (!object.ReferenceEquals (edges.First ().From, edges.First ().To)) {
 					throw new InvalidOperationException ($"TopologicalSort (for Pos/Dim) cannot find {edges.First ().From}. Did you forget to add it to {this}?");
 				} else {
 					throw new InvalidOperationException ("TopologicalSort encountered a recursive cycle in the relative Pos/Dim in the views of " + this);
@@ -1406,6 +1483,22 @@ namespace Terminal.Gui {
 			public Rect OldBounds { get; set; }
 		}
 
+		/// <summary>
+		/// Fired after the Views's <see cref="LayoutSubviews"/> method has completed. 
+		/// </summary>
+		/// <remarks>
+		/// Subscribe to this event to perform tasks when the <see cref="View"/> has been resized or the layout has otherwise changed.
+		/// </remarks>
+		public Action<LayoutEventArgs> LayoutStarted;
+
+		/// <summary>
+		/// Raises the <see cref="LayoutStarted"/> event. Called from  <see cref="LayoutSubviews"/> before any subviews have been laid out.
+		/// </summary>
+		internal virtual void OnLayoutStarted (LayoutEventArgs args)
+		{
+			LayoutStarted?.Invoke (args);
+		}
+
 		/// <summary>
 		/// Fired after the Views's <see cref="LayoutSubviews"/> method has completed. 
 		/// </summary>
@@ -1415,7 +1508,7 @@ namespace Terminal.Gui {
 		public Action<LayoutEventArgs> LayoutComplete;
 
 		/// <summary>
-		/// Raises the <see cref="LayoutComplete"/> event. Called from  <see cref="LayoutSubviews"/> after all sub-views have been laid out.
+		/// Raises the <see cref="LayoutComplete"/> event. Called from  <see cref="LayoutSubviews"/> before all sub-views have been laid out.
 		/// </summary>
 		internal virtual void OnLayoutComplete (LayoutEventArgs args)
 		{
@@ -1431,10 +1524,15 @@ namespace Terminal.Gui {
 		/// </remarks>
 		public virtual void LayoutSubviews ()
 		{
-			if (!layoutNeeded)
+			if (!layoutNeeded) {
 				return;
+			}
 
 			Rect oldBounds = Bounds;
+			OnLayoutStarted (new LayoutEventArgs () { OldBounds = oldBounds });
+
+			viewText.Size = Bounds.Size;
+
 
 			// Sort out the dependencies of the X, Y, Width, Height properties
 			var nodes = new HashSet<View> ();
@@ -1457,8 +1555,9 @@ namespace Terminal.Gui {
 			var ordered = TopologicalSort (nodes, edges);
 
 			foreach (var v in ordered) {
-				if (v.LayoutStyle == LayoutStyle.Computed)
+				if (v.LayoutStyle == LayoutStyle.Computed) {
 					v.SetRelativeLayout (Frame);
+				}
 
 				v.LayoutSubviews ();
 				v.layoutNeeded = false;
@@ -1475,146 +1574,41 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// A generic virtual method at the level of View to manipulate any hot-keys.
+		///   The text displayed by the <see cref="View"/>.
 		/// </summary>
-		/// <param name="text">The text to manipulate.</param>
-		/// <param name="hotKey">The hot-key to look for.</param>
-		/// <param name="hotPos">The returning hot-key position.</param>
-		/// <param name="showHotKey">The character immediately to the right relative to the hot-key position</param>
-		/// <returns>It aims to facilitate the preparation for <see cref="TextAlignment"/> procedures.</returns>
-		public virtual ustring GetTextFromHotKey (ustring text, Rune hotKey, out int hotPos, out Rune showHotKey)
-		{
-			Rune hot_key = (Rune)0;
-			int hot_pos = -1;
-			ustring shown_text = text;
-
-			// Use first hot_key char passed into 'hotKey'.
-			int i = 0;
-			foreach (Rune c in shown_text) {
-				if ((char)c != 0xFFFD) {
-					if (c == hotKey) {
-						hot_pos = i;
-					} else if (hot_pos > -1) {
-						hot_key = c;
-						break;
-					}
-				}
-				i++;
-			}
-
-			if (hot_pos == -1) {
-				// Use first upper-case char if there are no hot-key in the text.
-				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 {
-				// 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);
-					}
-				}
+		/// <remarks>
+		/// <para>
+		///  If provided, the text will be drawn before any subviews are drawn.
+		/// </para>
+		/// <para>
+		///  The text will be drawn starting at the view origin (0, 0) and will be formatted according
+		///  to the <see cref="TextAlignment"/> property. If the view's height is greater than 1, the
+		///  text will word-wrap to additional lines if it does not fit horizontally. If the view's height
+		///  is 1, the text will be clipped.
+		/// </para>
+		/// <para>
+		///  Set the <see cref="HotKeySpecifier"/> to enable hotkey support. To disable hotkey support set <see cref="HotKeySpecifier"/> to
+		///  <c>(Rune)0xffff</c>.
+		/// </para>
+		/// </remarks>
+		public virtual ustring Text {
+			get => viewText.Text;
+			set {
+				viewText.Text = value;
+				SetNeedsDisplay ();
 			}
-			hotPos = hot_pos;
-			showHotKey = hot_key;
-			return shown_text;
 		}
 
 		/// <summary>
-		/// A generic virtual method at the level of View to manipulate any hot-keys with <see cref="TextAlignment"/> process.
+		/// Gets or sets how the View's <see cref="Text"/> is aligned horizontally when drawn. Changing this property will redisplay the <see cref="View"/>.
 		/// </summary>
-		/// <param name="shown_text">The text to manipulate to align.</param>
-		/// <param name="hot_pos">The passed in hot-key position.</param>
-		/// <param name="c_hot_pos">The returning hot-key position.</param>
-		/// <param name="textAlignment">The <see cref="TextAlignment"/> to align to.</param>
-		/// <returns>It performs the <see cref="TextAlignment"/> process to the caller.</returns>
-		public virtual ustring GetTextAlignment (ustring shown_text, int hot_pos, out int c_hot_pos, TextAlignment textAlignment)
-		{
-			int start;
-			var caption = shown_text;
-			c_hot_pos = hot_pos;
-
-			if (Frame.Width > shown_text.Length + 1) {
-				switch (textAlignment) {
-				case TextAlignment.Left:
-					caption += new string (' ', Frame.Width - caption.RuneCount);
-					break;
-				case TextAlignment.Right:
-					start = Frame.Width - caption.RuneCount;
-					caption = $"{new string (' ', Frame.Width - caption.RuneCount)}{caption}";
-					if (c_hot_pos > -1) {
-						c_hot_pos += start;
-					}
-					break;
-				case TextAlignment.Centered:
-					start = Frame.Width / 2 - caption.RuneCount / 2;
-					caption = $"{new string (' ', start)}{caption}{new string (' ', Frame.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 = (Frame.Width - runeCount) / (caption.Length - wLen);
-					caption = "";
-					for (int i = 0; i < words.Length; i++) {
-						if (i == words.Length - 1) {
-							caption += new string (' ', Frame.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;
-		}
-
-		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;
+		/// <value>The text alignment.</value>
+		public virtual TextAlignment TextAlignment {
+			get => viewText.Alignment;
+			set {
+				viewText.Alignment = value;
+				SetNeedsDisplay ();
 			}
-			if (wHotPos == -1 && hotPos > -1)
-				wHotPos = words.Length;
-			runeCount = rCount;
-			wordHotPos = wHotPos;
-			return length;
 		}
 
 		/// <summary>
@@ -1686,6 +1680,15 @@ namespace Terminal.Gui {
 			if (MouseEvent (mouseEvent))
 				return true;
 
+
+			if (mouseEvent.Flags == MouseFlags.Button1Clicked) {
+				if (!HasFocus && SuperView != null) {
+					SuperView.SetFocus (this);
+					SetNeedsDisplay ();
+				}
+
+				return true;
+			}
 			return false;
 		}
 	}

+ 31 - 16
Terminal.Gui/Core/Window.cs

@@ -2,6 +2,13 @@
 // Authors:
 //   Miguel de Icaza ([email protected])
 //
+// NOTE: Window is functionally identical to FrameView with the following exceptions. 
+//  - Window is a Toplevel
+//  - FrameView Does not support padding (but should)
+//  - FrameView Does not support mouse dragging
+//  - FrameView Does not support IEnumerable
+// Any udpates done here should probably be done in FrameView as well; TODO: Merge these classes
+
 using System.Collections;
 using NStack;
 
@@ -29,7 +36,6 @@ namespace Terminal.Gui {
 			}
 		}
 
-
 		/// <summary>
 		/// ContentView is an internal implementation detail of Window. It is used to host Views added with <see cref="Add(View)"/>. 
 		/// Its ONLY reason for being is to provide a simple way for Window to expose to those SubViews that the Window's Bounds 
@@ -38,21 +44,6 @@ namespace Terminal.Gui {
 		class ContentView : View {
 			public ContentView (Rect frame) : base (frame) { }
 			public ContentView () : base () { }
-#if false
-			public override void Redraw (Rect bounds)
-			{
-				Driver.SetAttribute (ColorScheme.Focus);
-
-				for (int y = 0; y < Frame.Height; y++) {
-					Move (0, y);
-					for (int x = 0; x < Frame.Width; x++) {
-
-						Driver.AddRune ('x');
-					}
-				}
-				base.Redraw (region);
-			}
-#endif
 		}
 
 		/// <summary>
@@ -263,5 +254,29 @@ namespace Terminal.Gui {
 			//Demo.ml.Text = me.ToString ();
 			return false;
 		}
+
+		/// <summary>
+		///   The text displayed by the <see cref="Label"/>.
+		/// </summary>
+		public override ustring Text {
+			get => contentView.Text;
+			set {
+				base.Text = value;
+				if (contentView != null) {
+					contentView.Text = value;
+				}
+			}
+		}
+
+		/// <summary>
+		/// Controls the text-alignment property of the label, changing it will redisplay the <see cref="Label"/>.
+		/// </summary>
+		/// <value>The text alignment.</value>
+		public override TextAlignment TextAlignment {
+			get => contentView.TextAlignment;
+			set {
+				base.TextAlignment = contentView.TextAlignment = value;
+			}
+		}
 	}
 }

+ 9 - 1
Terminal.Gui/Terminal.Gui.csproj

@@ -74,7 +74,7 @@
       * Added a OpenSelectedItem event to the ListView #429. (Thanks @bdisp!)
       * Fixes the return value of the position cursor in the TextField. (Thanks @bdisp!)
       * Updates screen on Unix window resizing. (Thanks @bdisp!)
-      * Fixes the functions of the Edit-&gt;Copy-Cut-Paste menu for the TextField that was not working well. (Thanks @bdisp!)
+      * Fixes the functions of the Edit-Copy-Cut-Paste menu for the TextField that was not working well. (Thanks @bdisp!)
       * More robust error handing in Pos/Dim. Fixes #355 stack overflow with Pos based on the size of windows at startup. Added a OnResized action to set the Pos after the terminal are resized. (Thanks @bdisp!)
       * Fixes #389 Window layouting breaks when resizing. (Thanks @bdisp!)
       * Fixes #557 MessageBox needs to take ustrings (BREAKING CHANGE). (Thanks @tig!)
@@ -100,6 +100,14 @@
       * ConsoleDriver and Drivers have new standard glyph definitions for things like right arrow. (Thanks @tig!)
       * ScrollView updated to use pretty glyphs. (Thanks @tig!)
       * Menubar now uses pretty arrow glyph for sub-menus. (Thanks @tig!)
+      * The project now has a growing set of unit tests (over 100 tests). (Thanks @tig!)
+      * View now has a Text property, implemented via the new TextFormatting class. (Thanks @tig!)
+        * TextAlignment is implemented once across all Views that support it.
+        * Unicode support is now much more robust and complete; dozens of bugs fixed.
+        * Any view dervied from View now has a Text property with multi-line text formatting, including word-wrap and hotkey support.
+        * Label is now mostly just an alias for View; supports Clicked
+        * Button is now a very thin class derived from View (no API changes).
+        * Dozens of unit tests for TextAlignment are provided reducing the chance of regressions.
 
       0.81:
       * Fix ncurses engine for macOS/Linux, it works again

+ 54 - 84
Terminal.Gui/Views/Button.cs

@@ -15,8 +15,14 @@ namespace Terminal.Gui {
 	/// <remarks>
 	/// <para>
 	///   Provides a button showing text invokes an <see cref="Action"/> when clicked on with a mouse
-	///   or when the user presses SPACE, ENTER, or hotkey. The hotkey is specified by the first uppercase
-	///   letter in the button.
+	///   or when the user presses SPACE, ENTER, or hotkey. The hotkey is the first letter or digit following the first underscore ('_') 
+	///   in the button text. 
+	/// </para>
+	/// <para>
+	///   Use <see cref="View.HotKeySpecifier"/> to change the hotkey specifier from the default of ('_'). 
+	/// </para>
+	/// <para>
+	///   If no hotkey specifier is found, the first uppercase letter encountered will be used as the hotkey.
 	/// </para>
 	/// <para>
 	///   When the button is configured as the default (<see cref="IsDefault"/>) and the user presses
@@ -26,34 +32,7 @@ namespace Terminal.Gui {
 	/// </remarks>
 	public class Button : View {
 		ustring text;
-		ustring shown_text;
-		Rune hot_key;
-		int hot_pos = -1;
 		bool is_default;
-		TextAlignment textAlignment = TextAlignment.Centered;
-
-		/// <summary>
-		/// Gets or sets whether the <see cref="Button"/> is the default action to activate in a dialog.
-		/// </summary>
-		/// <value><c>true</c> if is default; otherwise, <c>false</c>.</value>
-		public bool IsDefault {
-			get => is_default;
-			set {
-				is_default = value;
-				SetWidthHeight (Text, is_default);
-				Update ();
-			}
-		}
-
-		/// <summary>
-		///   Clicked <see cref="Action"/>, raised when the button is clicked.
-		/// </summary>
-		/// <remarks>
-		///   Client code can hook up to this event, it is
-		///   raised when the button is activated either with
-		///   the mouse or the keyboard.
-		/// </remarks>
-		public Action Clicked;
 
 		/// <summary>
 		///   Initializes a new instance of <see cref="Button"/> using <see cref="LayoutStyle.Computed"/> layout.
@@ -108,7 +87,7 @@ namespace Terminal.Gui {
 		///   in a <see cref="Dialog"/> will implicitly activate this button.
 		/// </param>
 		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);
 		}
@@ -120,49 +99,40 @@ namespace Terminal.Gui {
 
 		void Init (ustring text, bool is_default)
 		{
+			HotKeySpecifier = new Rune ('_');
+
 			_leftBracket = new Rune (Driver != null ? Driver.LeftBracket : '[');
 			_rightBracket = new Rune (Driver != null ? Driver.RightBracket : ']');
 			_leftDefault = new Rune (Driver != null ? Driver.LeftDefaultIndicator : '<');
 			_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));
-		}
-
-		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;
+			Text = text ?? string.Empty;
 		}
 
 		/// <summary>
 		///   The text displayed by this <see cref="Button"/>.
 		/// </summary>
-		public ustring Text {
+		public new ustring Text {
 			get {
 				return text;
 			}
 
 			set {
-				SetWidthHeight (value, is_default);
 				text = value;
 				Update ();
 			}
 		}
 
 		/// <summary>
-		/// Sets or gets the text alignment for the <see cref="Button"/>.
+		/// Gets or sets whether the <see cref="Button"/> is the default action to activate in a dialog.
 		/// </summary>
-		public TextAlignment TextAlignment {
-			get => textAlignment;
+		/// <value><c>true</c> if is default; otherwise, <c>false</c>.</value>
+		public bool IsDefault {
+			get => is_default;
 			set {
-				textAlignment = value;
+				is_default = value;
 				Update ();
 			}
 		}
@@ -170,44 +140,20 @@ namespace Terminal.Gui {
 		internal void Update ()
 		{
 			if (IsDefault)
-				shown_text = ustring.Make (_leftBracket) + ustring.Make (_leftDefault) + " " + text + " " + ustring.Make (_rightDefault) + ustring.Make (_rightBracket);
+				base.Text = ustring.Make (_leftBracket) + ustring.Make (_leftDefault) + " " + text + " " + ustring.Make (_rightDefault) + ustring.Make (_rightBracket);
 			else
-				shown_text = ustring.Make (_leftBracket) + " " + text + " " + ustring.Make (_rightBracket);
-
-			shown_text = GetTextFromHotKey (shown_text, '_', out hot_pos, out hot_key);
+				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 ();
 		}
 
-		int c_hot_pos;
-
-		///<inheritdoc/>
-		public override void Redraw (Rect bounds)
-		{
-			Driver.SetAttribute (HasFocus ? ColorScheme.Focus : ColorScheme.Normal);
-			Move (0, 0);
-
-			var caption = GetTextAlignment (shown_text, hot_pos, out int s_hot_pos, TextAlignment);
-			c_hot_pos = s_hot_pos;
-
-			Driver.AddStr (caption);
-
-			if (c_hot_pos != -1) {
-				Move (c_hot_pos, 0);
-				Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : ColorScheme.HotNormal);
-				Driver.AddRune (hot_key);
-			}
-		}
-
-		///<inheritdoc/>
-		public override void PositionCursor ()
-		{
-			Move (c_hot_pos == -1 ? 1 : c_hot_pos, 0);
-		}
-
 		bool CheckKey (KeyEvent key)
 		{
-			if ((char)key.KeyValue == hot_key) {
+			if (key.Key == HotKey) {
 				this.SuperView.SetFocus (this);
 				Clicked?.Invoke ();
 				return true;
@@ -238,18 +184,42 @@ namespace Terminal.Gui {
 		public override bool ProcessKey (KeyEvent kb)
 		{
 			var c = kb.KeyValue;
-			if (c == '\n' || c == ' ' || Rune.ToUpper ((uint)c) == hot_key) {
+			if (c == '\n' || c == ' ' || kb.Key == HotKey) {
 				Clicked?.Invoke ();
 				return true;
 			}
 			return base.ProcessKey (kb);
 		}
 
-		///<inheritdoc/>
-		public override bool MouseEvent (MouseEvent me)
+
+		/// <summary>
+		///   Clicked <see cref="Action"/>, raised when the user clicks the primary mouse button within the Bounds of this <see cref="View"/>
+		///   or if the user presses the action key while this view is focused. (TODO: IsDefault)
+		/// </summary>
+		/// <remarks>
+		///   Client code can hook up to this event, it is
+		///   raised when the button is activated either with
+		///   the mouse or the keyboard.
+		/// </remarks>
+		public Action Clicked;
+
+		/// <summary>
+		/// Method invoked when a mouse event is generated
+		/// </summary>
+		/// <param name="mouseEvent"></param>
+		/// <returns><c>true</c>, if the event was handled, <c>false</c> otherwise.</returns>
+		public override bool OnMouseEvent (MouseEvent mouseEvent)
 		{
-			if (me.Flags == MouseFlags.Button1Clicked) {
-				if (!HasFocus) {
+			MouseEventArgs args = new MouseEventArgs (mouseEvent);
+			MouseClick?.Invoke (args);
+			if (args.Handled)
+				return true;
+			if (MouseEvent (mouseEvent))
+				return true;
+
+
+			if (mouseEvent.Flags == MouseFlags.Button1Clicked) {
+				if (!HasFocus && SuperView != null) {
 					SuperView.SetFocus (this);
 					SetNeedsDisplay ();
 				}

+ 2 - 2
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;
 		}
 
 		/// <summary>
@@ -88,7 +88,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		///   The text displayed by this <see cref="CheckBox"/>
 		/// </summary>
-		public ustring Text {
+		public new ustring Text {
 			get {
 				return text;
 			}

+ 5 - 4
Terminal.Gui/Views/ComboBox.cs

@@ -217,7 +217,7 @@ namespace Terminal.Gui {
 				this.SetFocus (search);
 			}
 
-			search.CursorPosition = search.Text.Length;
+			search.CursorPosition = search.Text.RuneCount;
 
 			return true;
 		}
@@ -272,8 +272,9 @@ namespace Terminal.Gui {
 				return true;
 			}
 
-			if (e.Key == Key.CursorUp && listview.HasFocus && listview.SelectedItem == 0 && searchset.Count > 0) { // jump back to search
-				search.CursorPosition = search.Text.Length;
+			if (e.Key == Key.CursorUp && listview.HasFocus && listview.SelectedItem == 0 && searchset.Count > 0) // jump back to search
+			{
+				search.CursorPosition = search.Text.RuneCount;
 				this.SetFocus (search);
 				return true;
 			}
@@ -297,7 +298,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// The currently selected list item
 		/// </summary>
-		public ustring Text 
+		public new ustring Text
 		{
 			get {
 				return text;

+ 4 - 4
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 {

+ 38 - 6
Terminal.Gui/Views/FrameView.cs

@@ -1,12 +1,14 @@
 //
-// FrameView.cs: Frame control
-//
 // Authors:
 //   Miguel de Icaza ([email protected])
 //
-using System;
-using System.Collections;
-using System.Collections.Generic;
+// NOTE: FrameView is functionally identical to Window with the following exceptions. 
+//  - Is not a Toplevel
+//  - Does not support mouse dragging
+//  - Does not support padding (but should)
+//  - Does not support IEnumerable
+// Any udpates done here should probably be done in Window as well; TODO: Merge these classes
+
 using NStack;
 
 namespace Terminal.Gui {
@@ -30,6 +32,11 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <summary>
+		/// ContentView is an internal implementation detail of Window. It is used to host Views added with <see cref="Add(View)"/>. 
+		/// Its ONLY reason for being is to provide a simple way for Window to expose to those SubViews that the Window's Bounds 
+		/// are actually deflated due to the border. 
+		/// </summary>
 		class ContentView : View {
 			public ContentView (Rect frame) : base (frame) { }
 			public ContentView () : base () { }
@@ -40,7 +47,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <param name="frame">Frame.</param>
 		/// <param name="title">Title.</param>
-		public FrameView (Rect frame, ustring title) : base (frame)
+		public FrameView (Rect frame, ustring title = null) : base (frame)
 		{
 			var cFrame = new Rect (1, 1, frame.Width - 2, frame.Height - 2);
 			this.title = title;
@@ -86,6 +93,7 @@ namespace Terminal.Gui {
 		void Initialize ()
 		{
 			base.Add (contentView);
+			contentView.Text = base.Text;
 		}
 
 		void DrawFrame ()
@@ -158,5 +166,29 @@ namespace Terminal.Gui {
 			Driver.DrawWindowTitle (scrRect, Title, padding, padding, padding, padding);
 			Driver.SetAttribute (ColorScheme.Normal);
 		}
+
+		/// <summary>
+		///   The text displayed by the <see cref="Label"/>.
+		/// </summary>
+		public override ustring Text {
+			get => contentView.Text;
+			set {
+				base.Text = value;
+				if (contentView != null) {
+					contentView.Text = value;
+				}
+			}
+		}
+
+		/// <summary>
+		/// Controls the text-alignment property of the label, changing it will redisplay the <see cref="Label"/>.
+		/// </summary>
+		/// <value>The text alignment.</value>
+		public override TextAlignment TextAlignment {
+			get => contentView.TextAlignment;
+			set {
+				base.TextAlignment = contentView.TextAlignment = value;
+			}
+		}
 	}
 }

+ 41 - 320
Terminal.Gui/Views/Label.cs

@@ -15,350 +15,71 @@ namespace Terminal.Gui {
 	/// <summary>
 	/// The Label <see cref="View"/> displays a string at a given position and supports multiple lines separted by newline characters. Multi-line Labels support word wrap.
 	/// </summary>
+	/// <remarks>
+	/// The <see cref="Label"/> view is functionality identical to <see cref="View"/> and is included for API backwards compatibilty.
+	/// </remarks>
 	public class Label : View {
-		List<ustring> lines = new List<ustring> ();
-		bool recalcPending = true;
-		ustring text;
-		TextAlignment textAlignment;
-
-		static Rect CalcRect (int x, int y, ustring s)
+		/// <inheritdoc/>
+		public Label ()
 		{
-			int mw = 0;
-			int ml = 1;
-
-			int cols = 0;
-			foreach (var rune in s) {
-				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);
 		}
 
-		/// <summary>
-		///   Initializes a new instance of <see cref="Label"/> using <see cref="LayoutStyle.Absolute"/> layout.
-		/// </summary>
-		/// <remarks>
-		/// <para>
-		///   The <see cref="Label"/> will be created at the given
-		///   coordinates with the given string. The size (<see cref="View.Frame"/> will be 
-		///   adjusted to fit the contents of <see cref="Text"/>, including newlines ('\n') for multiple lines. 
-		/// </para>
-		/// <para>
-		///   No line wrapping is provided.
-		/// </para>
-		/// </remarks>
-		/// <param name="x">column to locate the Label.</param>
-		/// <param name="y">row to locate the Label.</param>
-		/// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
-		public Label (int x, int y, ustring text) : this (CalcRect (x, y, text), text)
+		/// <inheritdoc/>
+		public Label (Rect frame) : base (frame)
 		{
 		}
 
-		/// <summary>
-		///   Initializes a new instance of <see cref="Label"/> using <see cref="LayoutStyle.Absolute"/> layout.
-		/// </summary>
-		/// <remarks>
-		/// <para>
-		///   The <see cref="Label"/> will be created at the given
-		///   coordinates with the given string. The initial size (<see cref="View.Frame"/> will be 
-		///   adjusted to fit the contents of <see cref="Text"/>, including newlines ('\n') for multiple lines. 
-		/// </para>
-		/// <para>
-		///   If <c>rect.Height</c> is greater than one, word wrapping is provided.
-		/// </para>
-		/// </remarks>
-		/// <param name="rect">Location.</param>
-		/// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
-		public Label (Rect rect, ustring text) : base (rect)
-		{
-			this.text = text;
-		}
-
-		/// <summary>
-		///   Initializes a new instance of <see cref="Label"/> using <see cref="LayoutStyle.Computed"/> layout.
-		/// </summary>
-		/// <remarks>
-		/// <para>
-		///   The <see cref="Label"/> will be created using <see cref="LayoutStyle.Computed"/>
-		///   coordinates with the given string. The initial size (<see cref="View.Frame"/> will be 
-		///   adjusted to fit the contents of <see cref="Text"/>, including newlines ('\n') for multiple lines. 
-		/// </para>
-		/// <para>
-		///   If <c>Height</c> is greater than one, word wrapping is provided.
-		/// </para>
-		/// </remarks>
-		/// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
-		public Label (ustring text) : base ()
-		{
-			this.text = text;
-			var r = CalcRect (0, 0, text);
-			Width = r.Width;
-			Height = r.Height;
-		}
-
-		/// <summary>
-		///   Initializes a new instance of <see cref="Label"/> using <see cref="LayoutStyle.Computed"/> layout.
-		/// </summary>
-		/// <remarks>
-		/// <para>
-		///   The <see cref="Label"/> will be created using <see cref="LayoutStyle.Computed"/>
-		///   coordinates. The initial size (<see cref="View.Frame"/> will be 
-		///   adjusted to fit the contents of <see cref="Text"/>, including newlines ('\n') for multiple lines. 
-		/// </para>
-		/// <para>
-		///   If <c>Height</c> is greater than one, word wrapping is provided.
-		/// </para>
-		/// </remarks>
-		public Label () : this (text: string.Empty) { }
-
-		static char [] whitespace = new char [] { ' ', '\t' };
-
-		static ustring ClipAndJustify (ustring str, int width, TextAlignment talign)
+		/// <inheritdoc/>
+		public Label (ustring text) : base (text)
 		{
-			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;
-			}
 		}
 
-		void Recalc ()
+		/// <inheritdoc/>
+		public Label (Rect rect, ustring text) : base (rect, text)
 		{
-			recalcPending = false;
-			Recalc (text, lines, Frame.Width, textAlignment, Bounds.Height > 1);
 		}
 
-		static ustring StripCRLF (ustring str)
+		/// <inheritdoc/>
+		public Label (int x, int y, ustring text) : base (x, y, text)
 		{
-			var runes = new List<Rune> ();
-			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<Rune> ();
-			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<ustring> WordWrap (ustring text, int margin)
-		{
-			int start = 0, end;
-			var lines = new List<ustring> ();
-
-			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 void Recalc (ustring textStr, List<ustring> 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));
-			}
-		}
-
-		///<inheritdoc/>
-		public override void LayoutSubviews ()
-		{
-			recalcPending = true;
-		}
-
-		///<inheritdoc/>
-		public override void Redraw (Rect bounds)
-		{
-			if (recalcPending)
-				Recalc ();
-
-			if (TextColor != -1)
-				Driver.SetAttribute (TextColor);
-			else
-				Driver.SetAttribute (ColorScheme.Normal);
-
-			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 ();
-				}
-				Move (x, line);
-				Driver.AddStr (str);
-			}
 		}
 
 		/// <summary>
-		/// Computes the number of lines needed to render the specified text by the <see cref="Label"/> view
+		///   Clicked <see cref="Action"/>, raised when the user clicks the primary mouse button within the Bounds of this <see cref="View"/>
+		///   or if the user presses the action key while this view is focused. (TODO: IsDefault)
 		/// </summary>
-		/// <returns>Number of lines.</returns>
-		/// <param name="text">Text, may contain newlines.</param>
-		/// <param name="width">The width for the text.</param>
-		public static int MeasureLines (ustring text, int width)
-		{
-			var result = new List<ustring> ();
-			Recalc (text, result, width, TextAlignment.Left, true);
-			return result.Count;
-		}
-
-		/// <summary>
-		/// Computes the max width of a line or multilines needed to render by the Label control
-		/// </summary>
-		/// <returns>Max width of lines.</returns>
-		/// <param name="text">Text, may contain newlines.</param>
-		/// <param name="width">The width for the text.</param>
-		public static int MaxWidth (ustring text, int width)
-		{
-			var result = new List<ustring> ();
-			Recalc (text, result, width, TextAlignment.Left, true);
-			return result.Max (s => s.RuneCount);
-		}
+		/// <remarks>
+		///   Client code can hook up to this event, it is
+		///   raised when the button is activated either with
+		///   the mouse or the keyboard.
+		/// </remarks>
+		public Action Clicked;
 
 		/// <summary>
-		/// Computes the max height of a line or multilines needed to render by the Label control
+		/// Method invoked when a mouse event is generated
 		/// </summary>
-		/// <returns>Max height of lines.</returns>
-		/// <param name="text">Text, may contain newlines.</param>
-		/// <param name="width">The width for the text.</param>
-		public static int MaxHeight (ustring text, int width)
+		/// <param name="mouseEvent"></param>
+		/// <returns><c>true</c>, if the event was handled, <c>false</c> otherwise.</returns>
+		public override bool OnMouseEvent (MouseEvent mouseEvent)
 		{
-			var result = new List<ustring> ();
-			Recalc (text, result, width, TextAlignment.Left, true);
-			return result.Count;
-		}
-
-		/// <summary>
-		///   The text displayed by the <see cref="Label"/>.
-		/// </summary>
-		public virtual ustring Text {
-			get => text;
-			set {
-				text = value;
-				recalcPending = true;
-				SetNeedsDisplay ();
-			}
-		}
-
-		/// <summary>
-		/// Controls the text-alignment property of the label, changing it will redisplay the <see cref="Label"/>.
-		/// </summary>
-		/// <value>The text alignment.</value>
-		public TextAlignment TextAlignment {
-			get => textAlignment;
-			set {
-				textAlignment = value;
-				SetNeedsDisplay ();
-			}
-		}
+			MouseEventArgs args = new MouseEventArgs (mouseEvent);
+			MouseClick?.Invoke (args);
+			if (args.Handled)
+				return true;
+			if (MouseEvent (mouseEvent))
+				return true;
+
+
+			if (mouseEvent.Flags == MouseFlags.Button1Clicked) {
+				if (!HasFocus && SuperView != null) {
+					SuperView.SetFocus (this);
+					SetNeedsDisplay ();
+				}
 
-		Attribute textColor = -1;
-		/// <summary>
-		///   The color used for the <see cref="Label"/>.
-		/// </summary>
-		public Attribute TextColor {
-			get => textColor;
-			set {
-				textColor = value;
-				SetNeedsDisplay ();
+				Clicked?.Invoke ();
+				return true;
 			}
+			return false;
 		}
 	}
-
 }

+ 2 - 2
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);
 
 		/// <summary>
@@ -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);
 			}

+ 2 - 3
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);
@@ -144,7 +144,6 @@ namespace Terminal.Gui {
 				Driver.AddStr (ustring.Make(new Rune[] { (i == selected ? Driver.Selected : Driver.UnSelected), ' '}));
 				DrawHotString (radioLabels [i], HasFocus && i == cursor, ColorScheme);
 			}
-			base.Redraw (bounds);
 		}
 
 		///<inheritdoc/>

+ 13 - 12
Terminal.Gui/Views/TextField.cs

@@ -63,7 +63,8 @@ namespace Terminal.Gui {
 		/// <param name="text">Initial text contents.</param>
 		public TextField (ustring text)
 		{
-			Initialize (text, Frame.Width);
+			Initialize (text, 0);
+			Width = text.RuneCount + 1;
 		}
 
 		/// <summary>
@@ -84,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;
@@ -122,7 +123,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <remarks>
 		/// </remarks>
-		public ustring Text {
+		public new ustring Text {
 			get {
 				return ustring.Make (text);
 			}
@@ -770,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 ();
 		}
 
@@ -788,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 ();

+ 30 - 31
Terminal.Gui/Views/TextView.cs

@@ -77,6 +77,8 @@ namespace Terminal.Gui {
 		{
 			var lines = new List<List<Rune>> ();
 			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)
@@ -126,10 +128,11 @@ namespace Terminal.Gui {
 		public override string ToString ()
 		{
 			var sb = new StringBuilder ();
-			foreach (var line in lines) 
-			{
-				sb.Append (ustring.Make(line));
-				sb.AppendLine ();
+			for (int i = 0; i < lines.Count; i++) {
+				sb.Append (ustring.Make (lines[i]));
+				if ((i + 1) < lines.Count) {
+					sb.AppendLine ();
+				}
 			}
 			return sb.ToString ();
 		}
@@ -146,7 +149,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <returns>The line.</returns>
 		/// <param name="line">Line number to retrieve.</param>
-		public List<Rune> GetLine (int line) => line < Count ? lines [line]: lines[Count-1];
+		public List<Rune> GetLine (int line) => line < Count ? lines [line] : lines [Count - 1];
 
 		/// <summary>
 		/// Adds a line to the model at the specified position.
@@ -326,7 +329,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <remarks>
 		/// </remarks>
-		public ustring Text {
+		public override ustring Text {
 			get {
 				return model.ToString ();
 			}
@@ -364,7 +367,7 @@ namespace Terminal.Gui {
 			if (stream == null)
 				throw new ArgumentNullException (nameof (stream));
 			ResetPosition ();
-			model.LoadStream(stream);
+			model.LoadStream (stream);
 			SetNeedsDisplay ();
 		}
 
@@ -372,7 +375,7 @@ namespace Terminal.Gui {
 		/// Closes the contents of the stream into the  <see cref="TextView"/>.
 		/// </summary>
 		/// <returns><c>true</c>, if stream was closed, <c>false</c> otherwise.</returns>
-		public bool CloseFile()
+		public bool CloseFile ()
 		{
 			ResetPosition ();
 			var res = model.CloseFile ();
@@ -397,7 +400,7 @@ namespace Terminal.Gui {
 		public override void PositionCursor ()
 		{
 			if (selecting) {
-				var minRow = Math.Min (Math.Max (Math.Min (selectionStartRow, currentRow)-topRow, 0), Frame.Height);
+				var minRow = Math.Min (Math.Max (Math.Min (selectionStartRow, currentRow) - topRow, 0), Frame.Height);
 				var maxRow = Math.Min (Math.Max (Math.Max (selectionStartRow, currentRow) - topRow, 0), Frame.Height);
 
 				SetNeedsDisplay (new Rect (0, minRow, Frame.Width, maxRow));
@@ -476,12 +479,12 @@ namespace Terminal.Gui {
 			var endCol = (int)(end & 0xffffffff);
 			var line = model.GetLine (startRow);
 
-			if (startRow == maxrow) 
+			if (startRow == maxrow)
 				return StringFromRunes (line.GetRange (startCol, endCol));
 
 			ustring res = StringFromRunes (line.GetRange (startCol, line.Count - startCol));
 
-			for (int row = startRow+1; row < maxrow; row++) {
+			for (int row = startRow + 1; row < maxrow; row++) {
 				res = res + ustring.Make ((Rune)10) + StringFromRunes (model.GetLine (row));
 			}
 			line = model.GetLine (maxrow);
@@ -514,7 +517,7 @@ namespace Terminal.Gui {
 			var line2 = model.GetLine (maxrow);
 			line.AddRange (line2.Skip (endCol));
 			for (int row = startRow + 1; row <= maxrow; row++) {
-				model.RemoveLine (startRow+1);
+				model.RemoveLine (startRow + 1);
 			}
 			if (currentEncoded == end) {
 				currentRow -= maxrow - (startRow);
@@ -531,33 +534,29 @@ namespace Terminal.Gui {
 
 			int bottom = bounds.Bottom;
 			int right = bounds.Right;
-			for (int row = bounds.Top; row < bottom; row++) 
-			{
+			for (int row = bounds.Top; row < bottom; row++) {
 				int textLine = topRow + row;
-				if (textLine >= model.Count) 
-				{
+				if (textLine >= model.Count) {
 					ColorNormal ();
 					ClearRegion (bounds.Left, row, bounds.Right, row + 1);
 					continue;
 				}
 				var line = model.GetLine (textLine);
 				int lineRuneCount = line.Count;
-				if (line.Count < bounds.Left)
-				{
+				if (line.Count < bounds.Left) {
 					ClearRegion (bounds.Left, row, bounds.Right, row + 1);
 					continue;
 				}
 
 				Move (bounds.Left, row);
-				for (int col = bounds.Left; col < right; col++) 
-				{
+				for (int col = bounds.Left; col < right; col++) {
 					var lineCol = leftColumn + col;
 					var rune = lineCol >= lineRuneCount ? ' ' : line [lineCol];
 					if (selecting && PointInSelection (col, row))
 						ColorSelection ();
 					else
 						ColorNormal ();
-					
+
 					AddRune (col, row, rune);
 				}
 			}
@@ -637,12 +636,12 @@ namespace Terminal.Gui {
 			for (int i = 1; i < lines.Count; i++)
 				model.AddLine (currentRow + i, lines [i]);
 
-			var last = model.GetLine (currentRow + lines.Count-1);
+			var last = model.GetLine (currentRow + lines.Count - 1);
 			var lastp = last.Count;
 			last.InsertRange (last.Count, rest);
 
 			// Now adjjust column and row positions
-			currentRow += lines.Count-1;
+			currentRow += lines.Count - 1;
 			currentColumn = lastp;
 			if (currentRow - topRow > Frame.Height) {
 				topRow = currentRow - Frame.Height + 1;
@@ -651,7 +650,7 @@ namespace Terminal.Gui {
 			}
 			if (currentColumn < leftColumn)
 				leftColumn = currentColumn;
-			if (currentColumn-leftColumn >= Frame.Width)
+			if (currentColumn - leftColumn >= Frame.Width)
 				leftColumn = currentColumn - Frame.Width + 1;
 			SetNeedsDisplay ();
 		}
@@ -968,7 +967,7 @@ namespace Terminal.Gui {
 
 				break;
 
-			case (Key)((int)'f' + Key.AltMask): 
+			case (Key)((int)'f' + Key.AltMask):
 				newPos = WordForward (currentColumn, currentRow);
 				if (newPos.HasValue) {
 					currentColumn = newPos.Value.col;
@@ -1094,8 +1093,8 @@ namespace Terminal.Gui {
 				col++;
 				rune = line [col];
 				return true;
-			} 
-			while (row + 1 < model.Count){
+			}
+			while (row + 1 < model.Count) {
 				col = 0;
 				row++;
 				line = model.GetLine (row);
@@ -1143,7 +1142,7 @@ namespace Terminal.Gui {
 
 			var srow = row;
 			if (Rune.IsPunctuation (rune) || Rune.IsWhiteSpace (rune)) {
-				while (MoveNext (ref col, ref row, out rune)){
+				while (MoveNext (ref col, ref row, out rune)) {
 					if (Rune.IsLetterOrDigit (rune))
 						break;
 				}
@@ -1166,18 +1165,18 @@ namespace Terminal.Gui {
 		{
 			if (fromRow == 0 && fromCol == 0)
 				return null;
-			
+
 			var col = fromCol;
 			var row = fromRow;
 			var line = GetCurrentLine ();
 			var rune = RuneAt (col, row);
 
 			if (Rune.IsPunctuation (rune) || Rune.IsSymbol (rune) || Rune.IsWhiteSpace (rune)) {
-				while (MovePrev (ref col, ref row, out rune)){
+				while (MovePrev (ref col, ref row, out rune)) {
 					if (Rune.IsLetterOrDigit (rune))
 						break;
 				}
-				while (MovePrev (ref col, ref row, out rune)){
+				while (MovePrev (ref col, ref row, out rune)) {
 					if (!Rune.IsLetterOrDigit (rune))
 						break;
 				}

+ 6 - 4
Terminal.Gui/Windows/Dialog.cs

@@ -61,6 +61,10 @@ namespace Terminal.Gui {
 					Add (b);
 				}
 			}
+
+			LayoutStarted += (args) => {
+				LayoutStartedHandler ();
+			};
 		}
 
 		/// <summary>
@@ -110,8 +114,8 @@ namespace Terminal.Gui {
 			}
 			return buttons.Select (b => b.Bounds.Width).Sum () + buttons.Count() - 1;
 		}
-		///<inheritdoc/>
-		public override void LayoutSubviews ()
+
+		void LayoutStartedHandler ()
 		{
 			int buttonsWidth = GetButtonsWidth ();
 
@@ -122,8 +126,6 @@ namespace Terminal.Gui {
 				button.X = Pos.AnchorEnd (shiftLeft);
 				button.Y = Pos.AnchorEnd (1);
 			}
-
-			base.LayoutSubviews ();
 		}
 
 		///<inheritdoc/>

+ 2 - 2
Terminal.Gui/Windows/FileDialog.cs

@@ -437,10 +437,10 @@ namespace Terminal.Gui {
 		/// <param name="prompt">The prompt.</param>
 		/// <param name="nameFieldLabel">The name field label.</param>
 		/// <param name="message">The message.</param>
-		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 = Label.MeasureLines (message, Driver.Cols - 20);
+			var msgLines = TextFormatter.MaxLines (message, Driver.Cols - 20);
 
 			dirLabel = new Label ("Directory: ") {
 				X = 1,

+ 3 - 3
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 = Label.MaxWidth (message, width);
-			int textHeight = Label.MaxHeight (message, width == 0 ? defaultWidth : width); // 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

+ 1 - 1
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

+ 5 - 4
UICatalog/Scenarios/AllViewsTester.cs

@@ -1,5 +1,6 @@
 using NStack;
 using System;
+using System.Collections;
 using System.Collections.Generic;
 using System.Linq;
 using System.Reflection;
@@ -76,9 +77,9 @@ namespace UICatalog {
 
 			_leftPane = new Window ("Classes") {
 				X = 0,
-				Y = 0, // for menu
+				Y = 0, 
 				Width = 15,
-				Height = Dim.Fill (),
+				Height = Dim.Fill (1), // for status bar
 				CanFocus = false,
 				ColorScheme = Colors.TopLevel,
 			};
@@ -87,7 +88,7 @@ namespace UICatalog {
 				X = 0,
 				Y = 0,
 				Width = Dim.Fill (0),
-				Height = Dim.Fill (), // for status bar
+				Height = Dim.Fill (0), 
 				AllowsMarking = false,
 				ColorScheme = Colors.TopLevel,
 			};
@@ -367,7 +368,7 @@ namespace UICatalog {
 			}
 
 			// If the view supports a Source property, set it so we have something to look at
-			if (view != null && view.GetType ().GetProperty ("Source") != null) {
+			if (view != null && view.GetType ().GetProperty ("Source") != null && view.GetType().GetProperty("Source").PropertyType == typeof(Terminal.Gui.IListDataSource)) {
 				var source = new ListWrapper (new List<ustring> () { ustring.Make ("List Item #1"), ustring.Make ("List Item #2"), ustring.Make ("List Item #3")});
 				view?.GetType ().GetProperty ("Source")?.GetSetMethod ()?.Invoke (view, new [] { source });
 			}

+ 13 - 13
UICatalog/Scenarios/Buttons.cs

@@ -70,9 +70,6 @@ namespace UICatalog {
 				//prev = colorButton;
 				x += colorButton.Frame.Width + 2;
 			}
-			// BUGBUG: For some reason these buttons don't move to correct locations initially. 
-			// This was the only way I find to resolves this with the View prev variable.
-			//Top.Ready += () => Top.Redraw (Top.Bounds);
 
 			Button button;
 			Win.Add (button = new Button ("A super long _Button that will probably expose a bug in clipping or wrapping of text. Will it?") {
@@ -187,23 +184,26 @@ namespace UICatalog {
 			ustring MoveHotkey (ustring txt)
 			{
 				// Remove the '_'
-				var i = txt.IndexOf ('_');
+				var runes = txt.ToRuneList ();
+
+				var i = runes.IndexOf ('_');
 				ustring start = "";
-				if (i > -1)
-					start = txt [0, i];
-				txt = start + txt [i + 1, txt.Length];
+				if (i > -1) {
+					start = ustring.Make (runes.GetRange (0, i));
+				}
+				txt = start + ustring.Make (runes.GetRange (i + 1, runes.Count - (i + 1)));
+
+				runes = txt.ToRuneList ();
 
 				// Move over one or go to start
 				i++;
-				if (i >= txt.Length) {
+				if (i >= runes.Count) {
 					i = 0;
 				}
 
 				// Slip in the '_'
-				start = txt [0, i];
-				txt = start + ustring.Make ('_') + txt [i, txt.Length];
-
-				return txt;
+				start = ustring.Make (runes.GetRange (0, i));
+				return start + ustring.Make ('_') + ustring.Make (runes.GetRange (i, runes.Count - i));
 			}
 
 			var mhkb = "Click to Change th_is Button's Hotkey";
@@ -218,7 +218,7 @@ 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,

+ 2 - 1
UICatalog/Scenarios/CharacterMap.cs

@@ -24,7 +24,8 @@ namespace UICatalog {
 				Width = CharMap.RowWidth + 2,
 				Height = Dim.Fill (),
 				Start = 0x2500,
-				ColorScheme = Colors.Dialog
+				ColorScheme = Colors.Dialog,
+				CanFocus = true,
 			};
 
 			Win.Add (charMap);

+ 1 - 1
UICatalog/Scenarios/Clipping.cs

@@ -34,7 +34,7 @@ namespace UICatalog {
 			//Win.Height = Dim.Fill () - 2;
 			var label = new Label ("ScrollView (new Rect (5, 5, 100, 60)) with a 200, 100 ContentSize...") {
 				X = 0, Y = 0,
-				ColorScheme = Colors.Dialog
+				//ColorScheme = Colors.Dialog
 			};
 			Top.Add (label);
 

+ 300 - 0
UICatalog/Scenarios/LabelsAsButtons.cs

@@ -0,0 +1,300 @@
+using NStack;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Terminal.Gui;
+
+namespace UICatalog {
+	[ScenarioMetadata (Name: "Labels As Buttons", Description: "Illustrates that Button is really just a Label++")]
+	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("POC")]
+	class LabelsAsLabels : Scenario {
+		public override void Setup ()
+		{
+			// Add a label & text field so we can demo IsDefault
+			var editLabel = new Label ("TextField (to demo IsDefault):") {
+				X = 0,
+				Y = 0,
+			};
+			Win.Add (editLabel);
+			// Add a TextField using Absolute layout. 
+			var edit = new TextField (31, 0, 15, "");
+			Win.Add (edit);
+
+			// This is the default Label (IsDefault = true); if user presses ENTER in the TextField
+			// the scenario will quit
+			var defaultLabel = new Label ("_Quit") {
+				X = Pos.Center (),
+				//TODO: Change to use Pos.AnchorEnd()
+				Y = Pos.Bottom (Win) - 3,
+				//IsDefault = true,
+				Clicked = () => Application.RequestStop (),
+				HotKeySpecifier = (System.Rune)'_',
+				CanFocus = true,
+			};
+			Win.Add (defaultLabel);
+
+			var swapLabel = new Label (50, 0, "S_wap Default (Absolute Layout)") {
+				HotKeySpecifier = (System.Rune)'_',
+				CanFocus = true,
+			};
+			swapLabel.Clicked = () => {
+				//defaultLabel.IsDefault = !defaultLabel.IsDefault;
+				//swapLabel.IsDefault = !swapLabel.IsDefault;
+			};
+			Win.Add (swapLabel);
+
+			static void DoMessage (Label Label, ustring txt)
+			{
+				Label.Clicked = () => {
+					var btnText = Label.Text.ToString ();
+					MessageBox.Query ("Message", $"Did you click {txt}?", "Yes", "No");
+				};
+			}
+
+			var colorLabelsLabel = new Label ("Color Labels:") {
+				X = 0,
+				Y = Pos.Bottom (editLabel) + 1,
+			};
+			Win.Add (colorLabelsLabel);
+
+			//With this method there is no need to call Top.Ready += () => Top.Redraw (Top.Bounds);
+			var x = Pos.Right (colorLabelsLabel) + 2;
+			foreach (var colorScheme in Colors.ColorSchemes) {
+				var colorLabel = new Label ($"{colorScheme.Key}") {
+					ColorScheme = colorScheme.Value,
+					X = x,
+					Y = Pos.Y (colorLabelsLabel),
+					HotKeySpecifier = (System.Rune)'_',
+					CanFocus = true,
+				};
+				DoMessage (colorLabel, colorLabel.Text);
+				Win.Add (colorLabel);
+				x += colorLabel.Text.Length + 2;
+			}
+			Top.Ready += () => Top.Redraw (Top.Bounds);
+
+			Label Label;
+			Win.Add (Label = new Label ("A super long _Label that will probably expose a bug in clipping or wrapping of text. Will it?") {
+				X = 2,
+				Y = Pos.Bottom (colorLabelsLabel) + 1,
+				HotKeySpecifier = (System.Rune)'_',
+				CanFocus = true,
+			});
+			DoMessage (Label, Label.Text);
+
+			// Note the 'N' in 'Newline' will be the hotkey
+			Win.Add (Label = new Label ("a Newline\nin the Label") {
+				X = 2,
+				Y = Pos.Bottom (Label) + 1,
+				Clicked = () => MessageBox.Query ("Message", "Question?", "Yes", "No"),
+				HotKeySpecifier = (System.Rune)'_',
+				CanFocus = true,
+			});
+
+			var textChanger = new Label ("Te_xt Changer") {
+				X = 2,
+				Y = Pos.Bottom (Label) + 1,
+				HotKeySpecifier = (System.Rune)'_',
+				CanFocus = true,
+			};
+			Win.Add (textChanger);
+			textChanger.Clicked = () => textChanger.Text += "!";
+
+			Win.Add (Label = new Label ("Lets see if this will move as \"Text Changer\" grows") {
+				X = Pos.Right (textChanger) + 2,
+				Y = Pos.Y (textChanger),
+				HotKeySpecifier = (System.Rune)'_',
+				CanFocus = true,
+			});
+
+			var removeLabel = new Label ("Remove this Label") {
+				X = 2,
+				Y = Pos.Bottom (Label) + 1,
+				ColorScheme = Colors.Error,
+				HotKeySpecifier = (System.Rune)'_',
+				CanFocus = true,
+			};
+			Win.Add (removeLabel);
+			// This in intresting test case because `moveBtn` and below are laid out relative to this one!
+			removeLabel.Clicked = () => Win.Remove (removeLabel);
+
+			var computedFrame = new FrameView ("Computed Layout") {
+				X = 0,
+				Y = Pos.Bottom (removeLabel) + 1,
+				Width = Dim.Percent (50),
+				Height = 5
+			};
+			Win.Add (computedFrame);
+
+			// Demonstrates how changing the View.Frame property can move Views
+			var moveBtn = new Label ("Move This \u263b Label _via Pos") {
+				X = 0,
+				Y = Pos.Center () - 1,
+				Width = 30,
+				ColorScheme = Colors.Error,
+				HotKeySpecifier = (System.Rune)'_',
+				CanFocus = true,
+			};
+			moveBtn.Clicked = () => {
+				moveBtn.X = moveBtn.Frame.X + 5;
+				// This is already fixed with the call to SetNeedDisplay() in the Pos Dim.
+				//computedFrame.LayoutSubviews (); // BUGBUG: This call should not be needed. View.X is not causing relayout correctly
+			};
+			computedFrame.Add (moveBtn);
+
+			// Demonstrates how changing the View.Frame property can SIZE Views (#583)
+			var sizeBtn = new Label ("Size This \u263a Label _via Pos") {
+				//var sizeBtn = new Label ("Size This x Label _via Pos") {
+				X = 0,
+				Y = Pos.Center () + 1,
+				Width = 30,
+				ColorScheme = Colors.Error,
+				HotKeySpecifier = (System.Rune)'_',
+				CanFocus = true,
+			};
+			sizeBtn.Clicked = () => {
+				sizeBtn.Width = sizeBtn.Frame.Width + 5;
+				//computedFrame.LayoutSubviews (); // FIXED: This call should not be needed. View.X is not causing relayout correctly
+			};
+			computedFrame.Add (sizeBtn);
+
+			var absoluteFrame = new FrameView ("Absolute Layout") {
+				X = Pos.Right (computedFrame),
+				Y = Pos.Bottom (removeLabel) + 1,
+				Width = Dim.Fill (),
+				Height = 5
+			};
+			Win.Add (absoluteFrame);
+
+			// Demonstrates how changing the View.Frame property can move Views
+			var moveBtnA = new Label (0, 0, "Move This Label via Frame") {
+				ColorScheme = Colors.Error,
+				HotKeySpecifier = (System.Rune)'_',
+				CanFocus = true,
+			};
+			moveBtnA.Clicked = () => {
+				moveBtnA.Frame = new Rect (moveBtnA.Frame.X + 5, moveBtnA.Frame.Y, moveBtnA.Frame.Width, moveBtnA.Frame.Height);
+			};
+			absoluteFrame.Add (moveBtnA);
+
+			// Demonstrates how changing the View.Frame property can SIZE Views (#583)
+			var sizeBtnA = new Label (0, 2, " ~  s  gui.cs   master ↑10 = Со_хранить") {
+				ColorScheme = Colors.Error,
+				HotKeySpecifier = (System.Rune)'_',
+				CanFocus = true,
+			};
+			sizeBtnA.Clicked = () => {
+				sizeBtnA.Frame = new Rect (sizeBtnA.Frame.X, sizeBtnA.Frame.Y, sizeBtnA.Frame.Width + 5, sizeBtnA.Frame.Height);
+			};
+			absoluteFrame.Add (sizeBtnA);
+
+			var label = new Label ("Text Alignment (changes the four Labels above): ") {
+				X = 2,
+				Y = Pos.Bottom (computedFrame) + 1,
+				HotKeySpecifier = (System.Rune)'_',
+				CanFocus = true,
+			};
+			Win.Add (label);
+
+			var radioGroup = new RadioGroup (new ustring [] { "Left", "Right", "Centered", "Justified" }) {
+				X = 4,
+				Y = Pos.Bottom (label) + 1,
+				SelectedItem = 2,
+			};
+			Win.Add (radioGroup);
+
+			// Demo changing hotkey
+			ustring MoveHotkey (ustring txt)
+			{
+				// Remove the '_'
+				var runes = txt.ToRuneList ();
+
+				var i = runes.IndexOf ('_');
+				ustring start = "";
+				if (i > -1) {
+					start = ustring.Make (runes.GetRange (0, i));
+				}
+				txt = start + ustring.Make (runes.GetRange (i + 1, runes.Count - (i + 1)));
+
+				runes = txt.ToRuneList ();
+
+				// Move over one or go to start
+				i++;
+				if (i >= runes.Count) {
+					i = 0;
+				}
+
+				// Slip in the '_'
+				start = ustring.Make (runes.GetRange (0, i));
+				return start + ustring.Make ('_') + ustring.Make (runes.GetRange (i, runes.Count - i));
+			}
+
+			var mhkb = "Click to Change th_is Label's Hotkey";
+			var moveHotKeyBtn = new Label (mhkb) {
+				X = 2,
+				Y = Pos.Bottom (radioGroup) + 1,
+				Width = mhkb.Length + 10,
+				ColorScheme = Colors.TopLevel,
+				HotKeySpecifier = (System.Rune)'_',
+				CanFocus = true,
+			};
+			moveHotKeyBtn.Clicked = () => {
+				moveHotKeyBtn.Text = MoveHotkey (moveHotKeyBtn.Text);
+			};
+			Win.Add (moveHotKeyBtn);
+
+			ustring muhkb = " ~  s  gui.cs   master ↑10 = Сохранить";
+			var moveUnicodeHotKeyBtn = new Label (muhkb) {
+				X = Pos.Left (absoluteFrame) + 1,
+				Y = Pos.Bottom (radioGroup) + 1,
+				Width = muhkb.Length + 30,
+				ColorScheme = Colors.TopLevel,
+				HotKeySpecifier = (System.Rune)'_',
+				CanFocus = true,
+			};
+			moveUnicodeHotKeyBtn.Clicked = () => {
+				moveUnicodeHotKeyBtn.Text = MoveHotkey (moveUnicodeHotKeyBtn.Text);
+			};
+			Win.Add (moveUnicodeHotKeyBtn);
+
+			radioGroup.SelectedItemChanged += (args) => {
+				switch (args.SelectedItem) {
+				case 0:
+					moveBtn.TextAlignment = TextAlignment.Left;
+					sizeBtn.TextAlignment = TextAlignment.Left;
+					moveBtnA.TextAlignment = TextAlignment.Left;
+					sizeBtnA.TextAlignment = TextAlignment.Left;
+					moveHotKeyBtn.TextAlignment = TextAlignment.Left;
+					moveUnicodeHotKeyBtn.TextAlignment = TextAlignment.Left;
+					break;
+				case 1:
+					moveBtn.TextAlignment = TextAlignment.Right;
+					sizeBtn.TextAlignment = TextAlignment.Right;
+					moveBtnA.TextAlignment = TextAlignment.Right;
+					sizeBtnA.TextAlignment = TextAlignment.Right;
+					moveHotKeyBtn.TextAlignment = TextAlignment.Right;
+					moveUnicodeHotKeyBtn.TextAlignment = TextAlignment.Right;
+					break;
+				case 2:
+					moveBtn.TextAlignment = TextAlignment.Centered;
+					sizeBtn.TextAlignment = TextAlignment.Centered;
+					moveBtnA.TextAlignment = TextAlignment.Centered;
+					sizeBtnA.TextAlignment = TextAlignment.Centered;
+					moveHotKeyBtn.TextAlignment = TextAlignment.Centered;
+					moveUnicodeHotKeyBtn.TextAlignment = TextAlignment.Centered;
+					break;
+				case 3:
+					moveBtn.TextAlignment = TextAlignment.Justified;
+					sizeBtn.TextAlignment = TextAlignment.Justified;
+					moveBtnA.TextAlignment = TextAlignment.Justified;
+					sizeBtnA.TextAlignment = TextAlignment.Justified;
+					moveHotKeyBtn.TextAlignment = TextAlignment.Justified;
+					moveUnicodeHotKeyBtn.TextAlignment = TextAlignment.Justified;
+					break;
+				}
+			};
+		}
+	}
+}

+ 1 - 1
UICatalog/Scenarios/MessageBoxes.cs

@@ -142,7 +142,7 @@ namespace UICatalog {
 				ColorScheme = Colors.Error,
 			};
 
-			var btnText = new [] { "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine" };
+			var btnText = new [] { "_Zero", "_One", "T_wo", "_Three", "_Four", "Fi_ve", "Si_x", "_Seven", "_Eight", "_Nine" };
 
 			var showMessageBoxButton = new Button ("Show MessageBox") {
 				X = Pos.Center(),

+ 0 - 1
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 ();

+ 4 - 4
UICatalog/Scenarios/Progress.cs

@@ -47,8 +47,8 @@ namespace UICatalog {
 				var lbl = new Label (1, 1, "Tick every (ms):");
 				LeftFrame.Add (lbl);
 				Speed = new TextField ("") {
-					X = Pos.Right (lbl) + 1,
-					Y = Pos.Y (lbl),
+					X = Pos.X (lbl),
+					Y = Pos.Bottom (lbl),
 					Width = 7,
 				};
 				LeftFrame.Add (Speed);
@@ -134,9 +134,9 @@ namespace UICatalog {
 		}
 
 		private Timer _systemTimer = null;
-		private uint _systemTimerTick = 1000; // ms
+		private uint _systemTimerTick = 100; // ms
 		private object _mainLoopTimeout = null;
-		private uint _mainLooopTimeoutTick = 1000; // ms
+		private uint _mainLooopTimeoutTick = 100; // ms
 		public override void Setup ()
 		{
 			// Demo #1 - Use System.Timer (and threading)

+ 0 - 1
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++}";
 			};
 

+ 82 - 10
UICatalog/Scenarios/TextAlignments.cs

@@ -9,30 +9,102 @@ namespace UICatalog {
 	class TextAlignments : Scenario {
 		public override void Setup ()
 		{
-#if true
+			Win.X = 10;
+			Win.Width = Dim.Fill (10);
+
 			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<Terminal.Gui.TextAlignment> ().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, X = 1, Width = Dim.Fill (1), Height = 1, ColorScheme = Colors.Dialog };
+				multipleLines [(int)alignment] = new Label (txt) { TextAlignment = alignment, X = 1, Width = Dim.Fill (1), 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") {
+				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 enableHotKeyCheckBox = new CheckBox ("Enable Hotkey (_)", false) {
+				X = 0,
+				Y = Pos.Bottom (edit),
+			};
+
+			Win.Add (enableHotKeyCheckBox);
+
+			var label = new Label ($"Demonstrating single-line (should clip):") { Y = Pos.Bottom (enableHotKeyCheckBox) + 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.";
-			label = new Label ($"Demonstrating multi-line and word wrap:") { Y = Pos.Bottom (label) + 1 };
+			label = new Label ($"Demonstrating multi-line and word wrap:") { Y = Pos.Bottom (label) };
 			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, 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];
 			}
+
+			enableHotKeyCheckBox.Toggled += (previous) => {
+				foreach (var alignment in alignments) {
+					singleLines [(int)alignment].HotKeySpecifier = previous ? (Rune)0xffff : (Rune)'_';
+					multipleLines [(int)alignment].HotKeySpecifier = previous ? (Rune)0xffff : (Rune)'_';
+				}
+				Win.SetNeedsDisplay ();
+				Win.LayoutSubviews ();
+			};
 		}
 	}
 }

+ 78 - 0
UICatalog/Scenarios/TextFormatterDemo.cs

@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Terminal.Gui;
+
+namespace UICatalog {
+	[ScenarioMetadata (Name: "TextFormatter Demo", Description: "Demos and tests the TextFormatter class.")]
+	[ScenarioCategory ("Text")]
+	[ScenarioCategory ("POC")]
+	class TextFormatterDemo : Scenario {
+		public override void Init (Toplevel top, ColorScheme colorScheme)
+		{
+			Application.Init ();
+
+			Top = top;
+			if (Top == null) {
+				Top = Application.Top;
+			}
+			Win = null;
+		}
+
+		public override void Setup ()
+		{
+			Top.Text = "Press CTRL-Q to Quit. This is the Text for the TopLevel View. 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.";
+			Top.TextAlignment = TextAlignment.Centered;
+			Top.ColorScheme = Colors.Base;
+
+			string text = "Hello world, how are you today? Pretty neat!\nSecond line\n\nFourth Line.";
+			string unicode = "Τὴ γλῶσσα μοῦ ἔδωσαν ἑλληνικὴ\nτὸ σπίτι φτωχικὸ στὶς ἀμμουδιὲς τοῦ Ὁμήρου.\nΜονάχη ἔγνοια ἡ γλῶσσα μου στὶς ἀμμουδιὲς τοῦ Ὁμήρου.";
+
+			var unicodeCheckBox = new CheckBox ("Unicode", Top.HotKeySpecifier == (Rune)' ') {
+				X = 0,
+				Y = 3,
+			};
+
+			Top.Add (unicodeCheckBox);
+
+			var alignments = Enum.GetValues (typeof (Terminal.Gui.TextAlignment)).Cast<Terminal.Gui.TextAlignment> ().ToList ();
+			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 (text) { TextAlignment = alignment, X = 0, Width = Dim.Fill (), Height = 1, ColorScheme = Colors.Dialog };
+				multipleLines [(int)alignment] = new Label (text) { TextAlignment = alignment, X = 0, Width = Dim.Fill (), Height = multiLineHeight, ColorScheme = Colors.Dialog };
+			}
+
+			var label = new Label ($"Demonstrating single-line (should clip):") { Y = Pos.Bottom (unicodeCheckBox) + 1 };
+			Top.Add (label);
+			foreach (var alignment in alignments) {
+				label = new Label ($"{alignment}:") { Y = Pos.Bottom (label) };
+				Top.Add (label);
+				singleLines [(int)alignment].Y = Pos.Bottom (label);
+				Top.Add (singleLines [(int)alignment]);
+				label = singleLines [(int)alignment];
+			}
+
+			label = new Label ($"Demonstrating multi-line and word wrap:") { Y = Pos.Bottom (label) };
+			Top.Add (label);
+			foreach (var alignment in alignments) {
+				label = new Label ($"{alignment}:") { Y = Pos.Bottom (label) };
+				Top.Add (label);
+				multipleLines [(int)alignment].Y = Pos.Bottom (label);
+				Top.Add (multipleLines [(int)alignment]);
+				label = multipleLines [(int)alignment];
+			}
+
+			unicodeCheckBox.Toggled += (previous) => {
+				foreach (var alignment in alignments) {
+					singleLines [(int)alignment].Text = previous ? text : unicode;
+					multipleLines [(int)alignment].Text = previous ? text : unicode;
+				}
+			};
+		}
+	}
+}

+ 26 - 31
UICatalog/Scenarios/Unicode.cs

@@ -10,6 +10,9 @@ namespace UICatalog {
 	class UnicodeInMenu : Scenario {
 		public override void Setup ()
 		{
+			//string text = "Hello world, how are you today? Pretty neat!\nSecond line\n\nFourth Line.";
+			string unicode = "Τὴ γλῶσσα μοῦ ἔδωσαν ἑλληνικὴ\nτὸ σπίτι φτωχικὸ στὶς ἀμμουδιὲς τοῦ Ὁμήρου.\nΜονάχη ἔγνοια ἡ γλῶσσα μου στὶς ἀμμουδιὲς τοῦ Ὁμήρου.";
+
 			var menu = new MenuBar (new MenuBarItem [] {
 				new MenuBarItem ("_Файл", new MenuItem [] {
 					new MenuItem ("_Создать", "Creates new file", null),
@@ -25,23 +28,32 @@ namespace UICatalog {
 			});
 			Top.Add (menu);
 
-			var label = new Label ("Button:") { X = 0, Y = 1 };
+			var label = new Label ("Label:") { X = 0, Y = 1 };
+			Win.Add (label);
+			var testlabel = new Label ("Стоял _он, дум великих полн") { X = 20, Y = Pos.Y (label), Width = Dim.Percent (50), };
+			Win.Add (testlabel);
+
+			label = new Label ("Label (CanFocus):") { X = Pos.X (label), Y = Pos.Bottom (label) + 1 };
 			Win.Add (label);
-			var button2 = new Button ("Со_хранить") { X = 15, Y = Pos.Y (label), Width = Dim.Percent (50), };
-			Win.Add (button2);
+			testlabel = new Label ("Стоял &он, дум великих полн") { X = 20, Y = Pos.Y (label), Width = Dim.Percent (50), CanFocus = true, HotKeySpecifier = new System.Rune('&') };
+			Win.Add (testlabel);
+
+			label = new Label ("Button:") { X = Pos.X (label), Y = Pos.Bottom (label) + 1 };
+			Win.Add (label);
+			var button = new Button ("A123456789♥♦♣♠JQK") { X = 20, Y = Pos.Y (label) };
+			Win.Add (button);
 
 			label = new Label ("CheckBox:") { X = Pos.X (label), Y = Pos.Bottom (label) + 1 };
 			Win.Add (label);
-			var checkBox = new CheckBox (" ~  s  gui.cs   master ↑10") { X = 15, Y = Pos.Y (label), Width = Dim.Percent (50) };
+			var checkBox = new CheckBox (" ~  s  gui.cs   master ↑10") { X = 20, Y = Pos.Y (label), Width = Dim.Percent (50) };
 			Win.Add (checkBox);
 
 			label = new Label ("ComboBox:") { X = Pos.X (label), Y = Pos.Bottom (label) + 1 };
 			Win.Add (label);
 			var comboBox = new ComboBox () {
-				X = 15,
+				X = 20,
 				Y = Pos.Y (label),
 				Width = Dim.Percent (50),
-				ColorScheme = Colors.Error
 			};
 			comboBox.SetSource (new List<string> () { "item #1", " ~  s  gui.cs   master ↑10", "Со_хранить" });
 
@@ -51,7 +63,7 @@ namespace UICatalog {
 			label = new Label ("HexView:") { X = Pos.X (label), Y = Pos.Bottom (label) + 2 };
 			Win.Add (label);
 			var hexView = new HexView (new System.IO.MemoryStream (Encoding.ASCII.GetBytes (" ~  s  gui.cs   master ↑10 Со_хранить"))) {
-				X = 15,
+				X = 20,
 				Y = Pos.Y (label),
 				Width = Dim.Percent (60),
 				Height = 5
@@ -60,56 +72,39 @@ namespace UICatalog {
 
 			label = new Label ("ListView:") { X = Pos.X (label), Y = Pos.Bottom (hexView) + 1 };
 			Win.Add (label);
-			var listView = new ListView (new List<string> () { "item #1", " ~  s  gui.cs   master ↑10", "Со_хранить" }) {
-				X = 15,
+			var listView = new ListView (new List<string> () { "item #1", " ~  s  gui.cs   master ↑10", "Со_хранить", unicode }) {
+				X = 20,
 				Y = Pos.Y (label),
 				Width = Dim.Percent (60),
 				Height = 3,
-				ColorScheme = Colors.Menu
 			};
 			Win.Add (listView);
 
 			label = new Label ("RadioGroup:") { X = Pos.X (label), Y = Pos.Bottom (listView) + 1 };
 			Win.Add (label);
 			var radioGroup = new RadioGroup (new ustring [] { "item #1", " ~  s  gui.cs   master ↑10", "Со_хранить" }, selected: 0) {
-				X = 15,
+				X = 20,
 				Y = Pos.Y (label),
 				Width = Dim.Percent (60),
-				ColorScheme = Colors.Menu
 			};
 			Win.Add (radioGroup);
 
 			label = new Label ("TextField:") { X = Pos.X (label), Y = Pos.Bottom (radioGroup) + 1 };
 			Win.Add (label);
-			var textField = new TextField (" ~  s  gui.cs   master ↑10 = Со_хранить") { X = 15, Y = Pos.Y (label), Width = Dim.Percent (60) };
+			var textField = new TextField (" ~  s  gui.cs   master ↑10 = Со_хранить") { X = 20, Y = Pos.Y (label), Width = Dim.Percent (60) };
 			Win.Add (textField);
 
 			label = new Label ("TextView:") { X = Pos.X (label), Y = Pos.Bottom (textField) + 1 };
 			Win.Add (label);
 			var textView = new TextView () {
-				X = 15,
+				X = 20,
 				Y = Pos.Y (label),
 				Width = Dim.Percent (60),
-				Height = 3,
-				ColorScheme = Colors.Menu,
-				Text = " ~  s  gui.cs   master ↑10\nСо_хранить",
+				Height = 5,
+				Text = unicode,
 			};
 			Win.Add (textView);
 
-			//label = new Label ("Charset:") { 
-			//	X = Pos.Percent(75) + 1, 
-			//	Y = 0,
-			//};
-			//Win.Add (label);
-			//var charset = new Label ("") { 
-			//	X = Pos.Percent(75) + 1, 
-			//	Y = Pos.Y (label) + 1,
-			//	Width = Dim.Fill (1),
-			//	Height = Dim.Fill (),
-			//	ColorScheme = Colors.Dialog
-			//};
-			//Win.Add (charset);
-
 			// Move Win down to row 1, below menu
 			Win.Y = 1;
 			Top.LayoutSubviews ();

+ 17 - 9
UICatalog/Scenarios/WindowsAndFrameViews.cs

@@ -64,11 +64,11 @@ namespace UICatalog {
 				X = Pos.Center (),
 				Y = 0,
 				ColorScheme = Colors.Error,
-				Clicked = () => About()
+				Clicked = () => About ()
 			});
 			Win.Add (new Button ("Press ME! (Y = Pos.AnchorEnd(1))") {
 				X = Pos.Center (),
-				Y = Pos.AnchorEnd(1),
+				Y = Pos.AnchorEnd (1),
 				ColorScheme = Colors.Error
 			});
 			Top.Add (Win);
@@ -80,7 +80,7 @@ namespace UICatalog {
 					X = margin,
 					Y = Pos.Bottom (listWin.Last ()) + (margin),
 					Width = Dim.Fill (margin),
-					Height = contentHeight + (i*2) + 2,
+					Height = contentHeight + (i * 2) + 2,
 				};
 				win.ColorScheme = Colors.Dialog;
 				win.Add (new Button ("Press me! (Y = 0)") {
@@ -95,8 +95,10 @@ namespace UICatalog {
 					Width = Dim.Percent (50),
 					Height = 5,
 					ColorScheme = Colors.Base,
+					Text = "The Text in the Window",
 				};
-				subWin.Add (new TextField (win.Title.ToString ()) {
+				subWin.Add (new TextField ("Edit me! " + win.Title.ToString ()) {
+					Y = 1,
 					ColorScheme = Colors.Error
 				});
 				win.Add (subWin);
@@ -106,8 +108,12 @@ namespace UICatalog {
 					Width = Dim.Percent (100),
 					Height = 5,
 					ColorScheme = Colors.Base,
+					Text = "The Text in the FrameView",
+
 				};
-				frameView.Add (new TextField ("Edit Me"));
+				frameView.Add (new TextField ("Edit Me!") {
+					Y = 1,
+				});
 				win.Add (frameView);
 
 				Top.Add (win);
@@ -135,6 +141,7 @@ namespace UICatalog {
 				Width = Dim.Percent (50),
 				Height = Dim.Fill () - 1,
 				ColorScheme = Colors.Base,
+				Text = "The Text in the Window",
 			};
 			subWinofFV.Add (new TextField ("Edit Me") {
 				ColorScheme = Colors.Error
@@ -150,8 +157,9 @@ namespace UICatalog {
 				Width = Dim.Percent (100),
 				Height = Dim.Fill () - 1,
 				ColorScheme = Colors.Base,
+				Text = "The Text in the FrameView",
 			};
-			subFrameViewofFV.Add (new TextField ("Edit Me"));
+			subFrameViewofFV.Add (new TextField (0, 0, 15, "Edit Me"));
 
 			subFrameViewofFV.Add (new CheckBox (0, 1, "Check me"));
 			// BUGBUG: This checkbox is not shown even though frameViewFV has 3 rows in 
@@ -160,12 +168,12 @@ namespace UICatalog {
 
 			frame.Add (new CheckBox ("Btn1 (Y = Pos.AnchorEnd (1))") {
 				X = 0,
-				Y = Pos.AnchorEnd (1), 
+				Y = Pos.AnchorEnd (1),
 			});
 			CheckBox c = new CheckBox ("Btn2 (Y = Pos.AnchorEnd (1))") {
-				Y = Pos.AnchorEnd (1), 
+				Y = Pos.AnchorEnd (1),
 			};
-			c.X = Pos.AnchorEnd () - (Pos.Right (c) - Pos.Left (c)); 
+			c.X = Pos.AnchorEnd () - (Pos.Right (c) - Pos.Left (c));
 			frame.Add (c);
 
 			frame.Add (subFrameViewofFV);

+ 1 - 1
UICatalog/UICatalog.cs

@@ -211,7 +211,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") {

+ 1994 - 0
UnitTests/TextFormatterTests.cs

@@ -0,0 +1,1994 @@
+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 Basic_Usage ()
+		{
+			var tf = new TextFormatter ();
+
+
+		}
+		[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));
+
+			text = " ~  s  gui.cs   master ↑10";
+			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 = "\nline2";
+			lines = 2;
+			Assert.Equal (new Rect (0, 0, 5, lines), TextFormatter.CalcRect (0, 0, text));
+
+			text = "\n\n";
+			lines = 3;
+			Assert.Equal (new Rect (0, 0, 0, lines), TextFormatter.CalcRect (0, 0, text));
+
+			text = "\n\n\n";
+			lines = 4;
+			Assert.Equal (new Rect (0, 0, 0, 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));
+
+			text = " ~  s  gui.cs   master ↑10\n";
+			lines = 2;
+			Assert.Equal (new Rect (0, 0, text.RuneCount - 1, lines), TextFormatter.CalcRect (0, 0, text));
+
+			text = "\n ~  s  gui.cs   master ↑10";
+			lines = 2;
+			Assert.Equal (new Rect (0, 0, text.RuneCount - 1, lines), TextFormatter.CalcRect (0, 0, text));
+
+			text = " ~  s  gui.cs   master\n↑10";
+			lines = 2;
+			Assert.Equal (new Rect (0, 0, ustring.Make (" ~  s  gui.cs   master\n").RuneCount - 1, 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<ArgumentOutOfRangeException> (() => 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 maxWidth = 0;
+			int expectedClippedWidth = 0;
+
+			text = "test";
+			maxWidth = 0;
+			Assert.Equal (ustring.Empty, justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align));
+
+			text = "test";
+			maxWidth = 2;
+			Assert.Equal (text.ToRunes () [0..maxWidth], justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align));
+
+			text = "test";
+			maxWidth = int.MaxValue;
+			Assert.Equal (text, justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align));
+			Assert.True (justifiedText.RuneCount <= maxWidth);
+
+			text = "A sentence has words.";
+			// should fit
+			maxWidth = text.RuneCount + 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should fit.
+			maxWidth = text.RuneCount + 0;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should fit.
+			maxWidth = int.MaxValue;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should not fit
+			maxWidth = text.RuneCount - 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should not fit
+			maxWidth = 10;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+
+			text = "A\tsentence\thas\twords.";
+			maxWidth = int.MaxValue;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			text = "A\tsentence\thas\twords.";
+			maxWidth = 10;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			text = "line1\nline2\nline3long!";
+			maxWidth = int.MaxValue;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			text = "line1\nline2\nline3long!";
+			maxWidth = 10;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Unicode
+			text = " ~  s  gui.cs   master ↑10";
+			maxWidth = 10;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// should fit
+			text = "Ð ÑÐ";
+			maxWidth = text.RuneCount + 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should fit.
+			maxWidth = text.RuneCount + 0;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should not fit
+			maxWidth = text.RuneCount - 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+		}
+
+		[Fact]
+		public void ClipAndJustify_Valid_Right ()
+		{
+			var align = TextAlignment.Right;
+
+			var text = ustring.Empty;
+			var justifiedText = ustring.Empty;
+			int maxWidth = 0;
+			int expectedClippedWidth = 0;
+
+			text = "test";
+			maxWidth = 0;
+			Assert.Equal (ustring.Empty, justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align));
+
+			text = "test";
+			maxWidth = 2;
+			Assert.Equal (text.ToRunes () [0..maxWidth], justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align));
+
+			text = "test";
+			maxWidth = int.MaxValue;
+			Assert.Equal (text, justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align));
+			Assert.True (justifiedText.RuneCount <= maxWidth);
+
+			text = "A sentence has words.";
+			// should fit
+			maxWidth = text.RuneCount + 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should fit.
+			maxWidth = text.RuneCount + 0;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should fit.
+			maxWidth = int.MaxValue;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should not fit
+			maxWidth = text.RuneCount - 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should not fit
+			maxWidth = 10;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+
+			text = "A\tsentence\thas\twords.";
+			maxWidth = int.MaxValue;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			text = "A\tsentence\thas\twords.";
+			maxWidth = 10;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			text = "line1\nline2\nline3long!";
+			maxWidth = int.MaxValue;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			text = "line1\nline2\nline3long!";
+			maxWidth = 10;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Unicode
+			text = " ~  s  gui.cs   master ↑10";
+			maxWidth = 10;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// should fit
+			text = "Ð ÑÐ";
+			maxWidth = text.RuneCount + 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should fit.
+			maxWidth = text.RuneCount + 0;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should not fit
+			maxWidth = text.RuneCount - 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+		}
+
+		[Fact]
+		public void ClipAndJustify_Valid_Centered ()
+		{
+			var align = TextAlignment.Centered;
+
+			var text = ustring.Empty;
+			var justifiedText = ustring.Empty;
+			int maxWidth = 0;
+			int expectedClippedWidth = 0;
+
+			text = "test";
+			maxWidth = 0;
+			Assert.Equal (ustring.Empty, justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align));
+
+			text = "test";
+			maxWidth = 2;
+			Assert.Equal (text.ToRunes () [0..maxWidth], justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align));
+
+			text = "test";
+			maxWidth = int.MaxValue;
+			Assert.Equal (text, justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align));
+			Assert.True (justifiedText.RuneCount <= maxWidth);
+
+			text = "A sentence has words.";
+			// should fit
+			maxWidth = text.RuneCount + 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should fit.
+			maxWidth = text.RuneCount + 0;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should fit.
+			maxWidth = int.MaxValue;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should not fit
+			maxWidth = text.RuneCount - 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should not fit
+			maxWidth = 10;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			text = "A\tsentence\thas\twords.";
+			maxWidth = int.MaxValue;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			text = "A\tsentence\thas\twords.";
+			maxWidth = 10;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			text = "line1\nline2\nline3long!";
+			maxWidth = int.MaxValue;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			text = "line1\nline2\nline3long!";
+			maxWidth = 10;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Unicode
+			text = " ~  s  gui.cs   master ↑10";
+			maxWidth = 10;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// should fit
+			text = "Ð ÑÐ";
+			maxWidth = text.RuneCount + 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should fit.
+			maxWidth = text.RuneCount + 0;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should not fit
+			maxWidth = text.RuneCount - 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+		}
+
+
+		[Fact]
+		public void ClipAndJustify_Valid_Justified ()
+		{
+			var align = TextAlignment.Justified;
+
+			var text = ustring.Empty;
+			var justifiedText = ustring.Empty;
+			int maxWidth = 0;
+			int expectedClippedWidth = 0;
+
+			text = "test";
+			maxWidth = 0;
+			Assert.Equal (ustring.Empty, justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align));
+
+			text = "test";
+			maxWidth = 2;
+			Assert.Equal (text.ToRunes () [0..maxWidth], justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align));
+
+			text = "test";
+			maxWidth = int.MaxValue;
+			Assert.Equal (text, justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align));
+			Assert.True (justifiedText.RuneCount <= maxWidth);
+
+			text = "A sentence has words.";
+			// should fit
+			maxWidth = text.RuneCount + 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			//Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should fit.
+			maxWidth = text.RuneCount + 0;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			//Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should fit.
+			maxWidth = 500;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			//Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			//Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should not fit
+			maxWidth = text.RuneCount - 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			//Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should not fit
+			maxWidth = 10;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			//Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+
+			text = "A\tsentence\thas\twords.";
+			maxWidth = int.MaxValue;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			//Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			text = "A\tsentence\thas\twords.";
+			maxWidth = 10;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			text = "line1\nline2\nline3long!";
+			maxWidth = int.MaxValue;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			text = "line1\nline2\nline3long!";
+			maxWidth = 10;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Unicode
+			text = " ~  s  gui.cs   master ↑10";
+			maxWidth = 10;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// should fit
+			text = "Ð ÑÐ";
+			maxWidth = text.RuneCount + 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			//Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			//Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should fit.
+			maxWidth = text.RuneCount + 0;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// Should not fit
+			maxWidth = text.RuneCount - 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			justifiedText = TextFormatter.ClipAndJustify (text, maxWidth, align);
+			Assert.Equal (expectedClippedWidth, justifiedText.RuneCount);
+			Assert.True (expectedClippedWidth <= maxWidth);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), justifiedText);
+
+			// 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<ArgumentOutOfRangeException> (() => TextFormatter.Justify (text, -1));
+		}
+
+		[Fact]
+		public void Justify_SingleWord ()
+		{
+			var text = ustring.Empty;
+			var justifiedText = ustring.Empty;
+			int width = 0;
+			char fillChar = '+';
+
+			// Even # of chars
+			text = "word";
+			justifiedText = text;
+
+			width = text.RuneCount;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+			width = text.RuneCount + 1;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+			width = text.RuneCount + 2;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+			width = text.RuneCount + 10;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+			width = text.RuneCount + 11;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+
+			// Odd # of chars
+			text = "word.";
+			justifiedText = text;
+
+			width = text.RuneCount;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+			width = text.RuneCount + 1;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+			width = text.RuneCount + 2;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+			width = text.RuneCount + 10;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+			width = text.RuneCount + 11;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+
+
+			// Unicode (even #)
+			text = "пÑивеÑ";
+			justifiedText = text;
+
+			width = text.RuneCount;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+			width = text.RuneCount + 1;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+			width = text.RuneCount + 2;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+			width = text.RuneCount + 10;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+			width = text.RuneCount + 11;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+
+			// Unicode (odd # of chars)
+			text = "пÑивеÑ.";
+			justifiedText = text;
+
+			width = text.RuneCount;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+			width = text.RuneCount + 1;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+			width = text.RuneCount + 2;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+			width = text.RuneCount + 10;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, width, fillChar).ToString ());
+			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 forceToWidth = 0;
+			char fillChar = '+';
+
+			// Even # of spaces
+			//      0123456789
+			text = "012 456 89";
+
+			forceToWidth = text.RuneCount;
+			justifiedText = text.Replace (" ", "+");
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "+");
+			forceToWidth = text.RuneCount + 1;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "++");
+			forceToWidth = text.RuneCount + 2;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "++");
+			forceToWidth = text.RuneCount + 3;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "+++");
+			forceToWidth = text.RuneCount + 4;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "+++");
+			forceToWidth = text.RuneCount + 5;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "++++");
+			forceToWidth = text.RuneCount + 6;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "+++++++++++");
+			forceToWidth = text.RuneCount + 20;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "++++++++++++");
+			forceToWidth = text.RuneCount + 23;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			// Odd # of spaces
+			//      0123456789
+			text = "012 456 89 end";
+
+			forceToWidth = text.RuneCount;
+			justifiedText = text.Replace (" ", "+");
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "+");
+			forceToWidth = text.RuneCount + 1;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "+");
+			forceToWidth = text.RuneCount + 2;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "++");
+			forceToWidth = text.RuneCount + 3;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "++");
+			forceToWidth = text.RuneCount + 4;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "++");
+			forceToWidth = text.RuneCount + 5;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "+++");
+			forceToWidth = text.RuneCount + 6;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "+++++++");
+			forceToWidth = text.RuneCount + 20;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "++++++++");
+			forceToWidth = text.RuneCount + 23;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			// Unicode
+			// Even # of chars
+			//      0123456789
+			text = "пÑРвРÑ";
+
+			forceToWidth = text.RuneCount;
+			justifiedText = text.Replace (" ", "+");
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "+");
+			forceToWidth = text.RuneCount + 1;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "++");
+			forceToWidth = text.RuneCount + 2;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "++");
+			forceToWidth = text.RuneCount + 3;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "+++");
+			forceToWidth = text.RuneCount + 4;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "+++");
+			forceToWidth = text.RuneCount + 5;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "++++");
+			forceToWidth = text.RuneCount + 6;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "+++++++++++");
+			forceToWidth = text.RuneCount + 20;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "++++++++++++");
+			forceToWidth = text.RuneCount + 23;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			// Unicode
+			// Odd # of chars
+			//      0123456789
+			text = "Ð ÑРвРÑ";
+
+			forceToWidth = text.RuneCount;
+			justifiedText = text.Replace (" ", "+");
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "+");
+			forceToWidth = text.RuneCount + 1;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "+");
+			forceToWidth = text.RuneCount + 2;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "++");
+			forceToWidth = text.RuneCount + 3;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "++");
+			forceToWidth = text.RuneCount + 4;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "++");
+			forceToWidth = text.RuneCount + 5;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "+++");
+			forceToWidth = text.RuneCount + 6;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "+++++++");
+			forceToWidth = text.RuneCount + 20;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+
+			justifiedText = text.Replace (" ", "++++++++");
+			forceToWidth = text.RuneCount + 23;
+			Assert.Equal (justifiedText.ToString (), TextFormatter.Justify (text, forceToWidth, fillChar).ToString ());
+			Assert.True (Math.Abs (forceToWidth - justifiedText.RuneCount) < text.Count (" "));
+		}
+
+		[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<ArgumentOutOfRangeException> (() => TextFormatter.WordWrap (text, -1));
+		}
+
+		[Fact]
+		public void WordWrap_SingleWordLine ()
+		{
+			var text = ustring.Empty;
+			int width = 0;
+			List<ustring> 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_Unicode_SingleWordLine ()
+		{
+			var text = ustring.Empty;
+			int width = 0;
+			List<ustring> wrappedLines;
+
+			text = "กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำ";
+			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 (ustring.Make (text.ToRunes () [0..(text.RuneCount - 1)]).ToString (), wrappedLines [0].ToString ());
+			Assert.Equal ("ำ", wrappedLines [1].ToString ());
+
+			width = text.RuneCount - 2;
+			wrappedLines = TextFormatter.WordWrap (text, width);
+			Assert.Equal (2, wrappedLines.Count);
+			Assert.Equal (ustring.Make (text.ToRunes () [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 ("กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบ", wrappedLines [0].ToString ());
+			Assert.Equal ("ปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำ", wrappedLines [1].ToString ());
+
+			width = (int)Math.Ceiling ((double)(text.RuneCount / 3F));
+			wrappedLines = TextFormatter.WordWrap (text, width);
+			Assert.Equal (3, wrappedLines.Count);
+			Assert.Equal ("กขฃคฅฆงจฉชซฌญฎฏฐฑ", wrappedLines [0].ToString ());
+			Assert.Equal ("ฒณดตถทธนบปผฝพฟภมย", wrappedLines [1].ToString ());
+			Assert.Equal ("รฤลฦวศษสหฬอฮฯะัาำ", 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 ("ก", wrappedLines [0].ToString ());
+			Assert.Equal ("ข", wrappedLines [1].ToString ());
+			Assert.Equal ("ฃ", wrappedLines [2].ToString ());
+			Assert.Equal ("ำ", wrappedLines [^1].ToString ());
+		}
+
+		[Fact]
+		public void WordWrap_NoNewLines ()
+		{
+			var text = ustring.Empty;
+			int maxWidth = 0;
+			int expectedClippedWidth = 0;
+
+			List<ustring> wrappedLines;
+
+			text = "A sentence has words.";
+			maxWidth = text.RuneCount;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth);
+			Assert.True (wrappedLines.Count == 1);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A sentence has words.", wrappedLines [0].ToString ());
+
+			maxWidth = text.RuneCount - 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth);
+			Assert.Equal (2, wrappedLines.Count);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A sentence has", wrappedLines [0].ToString ());
+			Assert.Equal ("words.", wrappedLines [1].ToString ());
+
+			maxWidth = text.RuneCount - "words.".Length;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth);
+			Assert.Equal (2, wrappedLines.Count);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A sentence has", wrappedLines [0].ToString ());
+			Assert.Equal ("words.", wrappedLines [1].ToString ());
+
+			maxWidth = text.RuneCount - " words.".Length;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth);
+			Assert.Equal (2, wrappedLines.Count);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A sentence has", wrappedLines [0].ToString ());
+			Assert.Equal ("words.", wrappedLines [1].ToString ());
+
+			maxWidth = text.RuneCount - "s words.".Length;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth);
+			Assert.Equal (2, wrappedLines.Count);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A sentence", wrappedLines [0].ToString ());
+			Assert.Equal ("has words.", wrappedLines [1].ToString ());
+
+			// Unicode 
+			text = "A Unicode sentence (пÑивеÑ) has words.";
+			maxWidth = text.RuneCount;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth);
+			Assert.True (wrappedLines.Count == 1);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A Unicode sentence (пÑивеÑ) has words.", wrappedLines [0].ToString ());
+
+			maxWidth = text.RuneCount - 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth);
+			Assert.Equal (2, wrappedLines.Count);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A Unicode sentence (пÑивеÑ) has", wrappedLines [0].ToString ());
+			Assert.Equal ("words.", wrappedLines [1].ToString ());
+
+			maxWidth = text.RuneCount - "words.".Length;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth);
+			Assert.Equal (2, wrappedLines.Count);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A Unicode sentence (пÑивеÑ) has", wrappedLines [0].ToString ());
+			Assert.Equal ("words.", wrappedLines [1].ToString ());
+
+			maxWidth = text.RuneCount - " words.".Length;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth);
+			Assert.Equal (2, wrappedLines.Count);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A Unicode sentence (пÑивеÑ) has", wrappedLines [0].ToString ());
+			Assert.Equal ("words.", wrappedLines [1].ToString ());
+
+			maxWidth = text.RuneCount - "s words.".Length;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth);
+			Assert.Equal (2, wrappedLines.Count);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A Unicode sentence (пÑивеÑ)", wrappedLines [0].ToString ());
+			Assert.Equal ("has words.", wrappedLines [1].ToString ());
+
+			maxWidth = text.RuneCount - "веÑ) has words.".Length;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth);
+			Assert.Equal (2, wrappedLines.Count);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A Unicode sentence", wrappedLines [0].ToString ());
+			Assert.Equal ("(пÑивеÑ) has words.", wrappedLines [1].ToString ());
+		}
+
+		[Fact]
+		public void WordWrap_Narrow ()
+		{
+			var text = ustring.Empty;
+			int maxWidth = 1;
+			int expectedClippedWidth = 1;
+
+			List<ustring> wrappedLines;
+
+			text = "A sentence has words.";
+			maxWidth = 3;
+			expectedClippedWidth = 3;
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth);
+			//Assert.True (wrappedLines.Count == );
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A", wrappedLines [0].ToString ());
+			// BUGBUG: WordWrap breaks down with small widths. It should not
+			// the following line should be "sen"...
+			Assert.Equal ("se", wrappedLines [1].ToString ());
+			Assert.Equal ("nte", wrappedLines [2].ToString ());
+			Assert.Equal ("nce", wrappedLines [3].ToString ());
+			Assert.Equal ("ha", wrappedLines [4].ToString ());
+			Assert.Equal ("s", wrappedLines [5].ToString ());
+			Assert.Equal ("wo", wrappedLines [6].ToString ());
+			Assert.Equal ("rds", wrappedLines [7].ToString ());
+			Assert.Equal (".", wrappedLines [^1].ToString ());
+
+			maxWidth = 2;
+			expectedClippedWidth = 2;
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth);
+			//Assert.True (wrappedLines.Count == );
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A", wrappedLines [0].ToString ());
+			Assert.Equal ("s", wrappedLines [1].ToString ());
+			Assert.Equal ("en", wrappedLines [2].ToString ());
+			Assert.Equal ("te", wrappedLines [3].ToString ());
+			Assert.Equal (".", wrappedLines [^1].ToString ());
+
+			maxWidth = 1;
+			expectedClippedWidth = 1;
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth);
+			//Assert.True (wrappedLines.Count == );
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("A", wrappedLines [0].ToString ());
+			// BUGBUG: WordWrap breaks down with a width of one. It should not
+			// provide blank lines like it does.
+			Assert.Equal ("", wrappedLines [1].ToString ());
+			Assert.Equal ("s", wrappedLines [2].ToString ());
+			Assert.Equal ("e", wrappedLines [3].ToString ());
+			Assert.Equal ("n", wrappedLines [4].ToString ());
+			Assert.Equal (".", wrappedLines [^1].ToString ());
+
+		}
+		[Fact]
+		public void ReplaceHotKeyWithTag ()
+		{
+			var tf = new TextFormatter ();
+			ustring text = "test";
+			int hotPos = 0;
+			uint tag = tf.HotKeyTagMask | 't';
+
+			Assert.Equal (ustring.Make (new Rune [] { tag, 'e', 's', 't' }), tf.ReplaceHotKeyWithTag (text, hotPos));
+
+			tag = tf.HotKeyTagMask | 'e';
+			hotPos = 1;
+			Assert.Equal (ustring.Make (new Rune [] { 't', tag, 's', 't' }), tf.ReplaceHotKeyWithTag (text, hotPos));
+
+			var result = tf.ReplaceHotKeyWithTag (text, hotPos);
+			Assert.Equal ('e', (uint)(result.ToRunes () [1] & ~tf.HotKeyTagMask));
+
+			text = "Ok";
+			tag = 0x100000 | 'O';
+			hotPos = 0;
+			Assert.Equal (ustring.Make (new Rune [] { tag, 'k' }), result = tf.ReplaceHotKeyWithTag (text, hotPos));
+			Assert.Equal ('O', (uint)(result.ToRunes () [0] & ~tf.HotKeyTagMask));
+
+			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 = tf.HotKeyTagMask | 'O';
+			hotPos = 3;
+			Assert.Equal (ustring.Make (new Rune [] { '[', '◦', ' ', tag, 'k', ' ', '◦', ']' }), result = tf.ReplaceHotKeyWithTag (text, hotPos));
+			Assert.Equal ('O', (uint)(result.ToRunes () [3] & ~tf.HotKeyTagMask));
+
+			text = "^k";
+			tag = '^';
+			hotPos = 0;
+			Assert.Equal (ustring.Make (new Rune [] { tag, 'k' }), result = tf.ReplaceHotKeyWithTag (text, hotPos));
+			Assert.Equal ('^', (uint)(result.ToRunes () [0] & ~tf.HotKeyTagMask));
+		}
+
+		[Fact]
+		public void Reformat_Invalid ()
+		{
+			var text = ustring.Empty;
+			var list = new List<ustring> ();
+
+			Assert.Throws<ArgumentOutOfRangeException> (() => TextFormatter.Format (text, -1, TextAlignment.Left, false));
+
+			list = TextFormatter.Format (text, 0, TextAlignment.Left, false);
+			Assert.NotEmpty (list);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Empty, list [0]);
+
+			text = null;
+			list = TextFormatter.Format (text, 0, TextAlignment.Left, false);
+			Assert.NotEmpty (list);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Empty, list [0]);
+
+			list = TextFormatter.Format (text, 0, TextAlignment.Left, true);
+			Assert.NotEmpty (list);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Empty, list [0]);
+		}
+
+		[Fact]
+		public void Reformat_NoWordrap_SingleLine ()
+		{
+			var text = ustring.Empty;
+			var list = new List<ustring> ();
+			var maxWidth = 0;
+			var expectedClippedWidth = 0;
+			var wrap = false;
+
+			maxWidth = 0;
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+
+			maxWidth = 1;
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+
+			text = "A sentence has words.";
+			maxWidth = 0;
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Empty, list [0]);
+
+			maxWidth = 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), list [0]);
+
+			maxWidth = 5;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), list[0]);
+
+			maxWidth = text.RuneCount - 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), list [0]);
+
+			// no clip
+			maxWidth = text.RuneCount + 0;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), list [0]);
+
+			maxWidth = text.RuneCount + 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), list [0]);
+		}
+
+		[Fact]
+		public void Reformat_NoWordrap_NewLines ()
+		{
+			var text = ustring.Empty;
+			var list = new List<ustring> ();
+			var maxWidth = 0;
+			var expectedClippedWidth = 0;
+			var wrap = false;
+
+			text = "A sentence has words.\nLine 2.";
+			maxWidth = 0;
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Empty, list [0]);
+
+			maxWidth = 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), list [0]);
+
+			maxWidth = 5;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), list [0]);
+
+			maxWidth = text.RuneCount - 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth); 
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]).Replace("\n", " "), list [0]);
+
+			// no clip
+			maxWidth = text.RuneCount + 0;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]).Replace ("\n", " "), list [0]);
+
+			maxWidth = text.RuneCount + 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]).Replace ("\n", " "), list [0]);
+
+			text = "A sentence has words.\r\nLine 2.";
+			maxWidth = 0;
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Empty, list [0]);
+
+			maxWidth = 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), list [0]);
+
+			maxWidth = 5;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), list [0]);
+
+			maxWidth = text.RuneCount - 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth) + 1;
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]).Replace ("\r\n", " ").ToString(), list [0].ToString());
+
+			// no clip
+			maxWidth = text.RuneCount + 0;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]).Replace ("\r\n", " "), list [0]);
+
+			maxWidth = text.RuneCount + 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]).Replace ("\r\n", " "), list [0]);
+		}
+
+		[Fact]
+		public void Reformat_Wrap_Spaces_No_NewLines ()
+		{
+			var text = ustring.Empty;
+			var list = new List<ustring> ();
+			var maxWidth = 0;
+			var expectedClippedWidth = 0;
+			var wrap = true;
+
+			// Even # of spaces
+			//      0123456789
+			text = "012 456 89";
+
+			// See WordWrap BUGBUGs above.
+			//maxWidth = 0;
+			//list = TextFormatter.Reformat (text, maxWidth, TextAlignment.Left, wrap);
+			//Assert.True (list.Count == 1);
+			//Assert.Equal (ustring.Empty, list [0]);
+
+			//maxWidth = 1;
+			//expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			//// remove 3 whitespace chars
+			//expectedLines = text.RuneCount;
+			//list = TextFormatter.Reformat (text, maxWidth, TextAlignment.Left, wrap);
+			//Assert.Equal (expectedLines, list.Count);
+			//Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), list [0]);
+
+			////width = (int)Math.Ceiling ((double)(text.RuneCount / 2F));
+
+			//maxWidth = 5;
+			//expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			//expectedLines = (int)Math.Ceiling ((double)(text.RuneCount / maxWidth));
+			//list = TextFormatter.Reformat (text, maxWidth, TextAlignment.Left, wrap);
+			//Assert.Equal (expectedLines, list.Count);
+			//Assert.Equal (ustring.Make (text.ToRunes () [0..expectedClippedWidth]), list [0]);
+
+			maxWidth = text.RuneCount - 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 2);
+			Assert.Equal ("012 456", list [0]);
+			Assert.Equal ("89", list [1]);
+
+			// no clip
+			maxWidth = text.RuneCount + 0;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal ("012 456 89", list [0]);
+
+			maxWidth = text.RuneCount + 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal ("012 456 89", list [0]);
+
+			// Odd # of spaces
+			//      0123456789
+			text = "012 456 89 end";
+			maxWidth = text.RuneCount - 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 2);
+			Assert.Equal ("012 456 89", list [0]);
+			Assert.Equal ("end", list [1]);
+
+			// no clip
+			maxWidth = text.RuneCount + 0;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal ("012 456 89 end", list [0]);
+
+			maxWidth = text.RuneCount + 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal ("012 456 89 end", list [0]);
+		
+		}
+
+		[Fact]
+		public void Reformat_Unicode_Wrap_Spaces_No_NewLines ()
+		{
+			var text = ustring.Empty;
+			var list = new List<ustring> ();
+			var maxWidth = 0;
+			var expectedClippedWidth = 0;
+			var wrap = true;
+
+			// Unicode
+			// Even # of chars
+			//      0123456789
+			text = "пÑРвРÑ";
+
+			maxWidth = text.RuneCount - 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 2);
+			Assert.Equal ("пÑРвÐ", list [0]);
+			Assert.Equal ("Ñ", list [1]);
+
+			// no clip
+			maxWidth = text.RuneCount + 0;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal ("пÑРвРÑ", list [0]);
+
+			maxWidth = text.RuneCount + 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal ("пÑРвРÑ", list [0]);
+
+			// Unicode
+			// Odd # of chars
+			//      0123456789
+			text = "Ð ÑРвРÑ";
+
+			maxWidth = text.RuneCount - 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 2);
+			Assert.Equal ("Ð ÑРвÐ", list [0]);
+			Assert.Equal ("Ñ", list [1]);
+
+			// no clip
+			maxWidth = text.RuneCount + 0;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal ("Ð ÑРвРÑ", list [0]);
+
+			maxWidth = text.RuneCount + 1;
+			expectedClippedWidth = Math.Min (text.RuneCount, maxWidth);
+			list = TextFormatter.Format (text, maxWidth, TextAlignment.Left, wrap);
+			Assert.True (list.Count == 1);
+			Assert.Equal ("Ð ÑРвРÑ", list [0]);
+
+		}
+	}
+}

+ 10 - 4
UnitTests/UnitTests.csproj

@@ -6,11 +6,17 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
     <PackageReference Include="System.Collections" Version="4.3.0" />
-    <PackageReference Include="xunit" Version="2.4.0" />
-    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
-    <PackageReference Include="coverlet.collector" Version="1.2.0" />
+    <PackageReference Include="xunit" Version="2.4.1" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+    </PackageReference>
+    <PackageReference Include="coverlet.collector" Version="1.3.0">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+    </PackageReference>
   </ItemGroup>
 
   <ItemGroup>

+ 1 - 1
UnitTests/ViewTests.cs

@@ -25,7 +25,7 @@ namespace Terminal.Gui {
 			Assert.Equal (new Rect (0, 0, 0, 0), r.Frame);
 			Assert.Null (r.Focused);
 			Assert.Null (r.ColorScheme);
-			Assert.Equal (Dim.Sized (0), r.Height);
+			Assert.Equal (Dim.Sized (0), r.Width);
 			Assert.Equal (Dim.Sized (0), r.Height);
 			// BUGBUG: Pos needs eqality implemented
 			//Assert.Equal (Pos.At (0), r.X);