Преглед на файлове

Added text direction support for word-wrap and fix draw issues.

BDisp преди 3 години
родител
ревизия
933a1bda91
променени са 3 файла, в които са добавени 827 реда и са изтрити 77 реда
  1. 210 68
      Terminal.Gui/Core/TextFormatter.cs
  2. 14 5
      UnitTests/GraphViewTests.cs
  3. 603 4
      UnitTests/TextFormatterTests.cs

+ 210 - 68
Terminal.Gui/Core/TextFormatter.cs

@@ -303,7 +303,7 @@ namespace Terminal.Gui {
 		/// <remarks>
 		/// <para>
 		/// Upon a 'get' of this property, if the text needs to be formatted (if <see cref="NeedsFormat"/> is <c>true</c>)
-		/// <see cref="Format(ustring, int, bool, bool, bool, int)"/> will be called internally. 
+		/// <see cref="Format(ustring, int, bool, bool, bool, int, TextDirection)"/> will be called internally. 
 		/// </para>
 		/// </remarks>
 		public List<ustring> Lines {
@@ -328,12 +328,14 @@ namespace Terminal.Gui {
 					}
 
 					if (IsVerticalDirection (textDirection)) {
-						lines = Format (shown_text, Size.Height, textVerticalAlignment == VerticalTextAlignment.Justified, Size.Width > 1);
+						lines = Format (shown_text, Size.Height, textVerticalAlignment == VerticalTextAlignment.Justified, Size.Width > 1,
+							false, 0, textDirection);
 						if (!AutoSize && lines.Count > Size.Width) {
 							lines.RemoveRange (Size.Width, lines.Count - Size.Width);
 						}
 					} else {
-						lines = Format (shown_text, Size.Width, textAlignment == TextAlignment.Justified, Size.Height > 1);
+						lines = Format (shown_text, Size.Width, textAlignment == TextAlignment.Justified, Size.Height > 1,
+							false, 0, textDirection);
 						if (!AutoSize && lines.Count > Size.Height) {
 							lines.RemoveRange (Size.Height, lines.Count - Size.Height);
 						}
@@ -346,7 +348,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Gets or sets whether the <see cref="TextFormatter"/> needs to format the text when <see cref="Draw(Rect, Attribute, Attribute)"/> is called.
+		/// Gets or sets whether the <see cref="TextFormatter"/> needs to format the text when <see cref="Draw(Rect, Attribute, Attribute, Rect)"/> is called.
 		/// If it is <c>false</c> when Draw is called, the Draw call will be faster.
 		/// </summary>
 		/// <remarks>
@@ -436,6 +438,7 @@ namespace Terminal.Gui {
 		/// <param name="preserveTrailingSpaces">If <c>true</c>, the wrapped text will keep the trailing spaces.
 		///  If <c>false</c>, the trailing spaces will be trimmed.</param>
 		/// <param name="tabWidth">The tab width.</param>
+		/// <param name="textDirection">The text direction.</param>
 		/// <returns>Returns a list of word wrapped lines.</returns>
 		/// <remarks>
 		/// <para>
@@ -445,7 +448,8 @@ namespace Terminal.Gui {
 		/// This method strips Newline ('\n' and '\r\n') sequences before processing.
 		/// </para>
 		/// </remarks>
-		public static List<ustring> WordWrap (ustring text, int width, bool preserveTrailingSpaces = false, int tabWidth = 0)
+		public static List<ustring> WordWrap (ustring text, int width, bool preserveTrailingSpaces = false, int tabWidth = 0,
+			TextDirection textDirection = TextDirection.LeftRight_TopBottom)
 		{
 			if (width < 0) {
 				throw new ArgumentOutOfRangeException ("Width cannot be negative.");
@@ -460,15 +464,29 @@ namespace Terminal.Gui {
 
 			var runes = StripCRLF (text).ToRuneList ();
 			if (!preserveTrailingSpaces) {
-				while ((end = start + width) < runes.Count) {
-					while (runes [end] != ' ' && end > start)
-						end--;
-					if (end == start)
-						end = start + width;
-					lines.Add (ustring.Make (runes.GetRange (start, end - start)));
-					start = end;
-					if (runes [end] == ' ') {
-						start++;
+				if (IsHorizontalDirection (textDirection)) {
+					while ((end = start + GetMaxLengthForWidth (runes.GetRange (start, runes.Count - start), width)) < runes.Count) {
+						while (runes [end] != ' ' && end > start)
+							end--;
+						if (end == start)
+							end = start + GetMaxLengthForWidth (runes.GetRange (end, runes.Count - end), width);
+						lines.Add (ustring.Make (runes.GetRange (start, end - start)));
+						start = end;
+						if (runes [end] == ' ') {
+							start++;
+						}
+					}
+				} else {
+					while ((end = start + width) < runes.Count) {
+						while (runes [end] != ' ' && end > start)
+							end--;
+						if (end == start)
+							end = start + width;
+						lines.Add (ustring.Make (runes.GetRange (start, end - start)));
+						start = end;
+						if (runes [end] == ' ') {
+							start++;
+						}
 					}
 				}
 			} else {
@@ -486,7 +504,11 @@ namespace Terminal.Gui {
 
 				while (length < cWidth && to < runes.Count) {
 					var rune = runes [to];
-					length += Rune.ColumnWidth (rune);
+					if (IsHorizontalDirection (textDirection)) {
+						length += Rune.ColumnWidth (rune);
+					} else {
+						length++;
+					}
 					if (rune == ' ') {
 						if (length == cWidth) {
 							return to + 1;
@@ -527,10 +549,11 @@ namespace Terminal.Gui {
 		/// <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>
+		/// <param name="textDirection">The text direction.</param>
 		/// <returns>Justified and clipped text.</returns>
-		public static ustring ClipAndJustify (ustring text, int width, TextAlignment talign)
+		public static ustring ClipAndJustify (ustring text, int width, TextAlignment talign, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
 		{
-			return ClipAndJustify (text, width, talign == TextAlignment.Justified);
+			return ClipAndJustify (text, width, talign == TextAlignment.Justified, textDirection);
 		}
 
 		/// <summary>
@@ -539,8 +562,9 @@ namespace Terminal.Gui {
 		/// <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="justify">Justify.</param>
+		/// <param name="textDirection">The text direction.</param>
 		/// <returns>Justified and clipped text.</returns>
-		public static ustring ClipAndJustify (ustring text, int width, bool justify)
+		public static ustring ClipAndJustify (ustring text, int width, bool justify, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
 		{
 			if (width < 0) {
 				throw new ArgumentOutOfRangeException ("Width cannot be negative.");
@@ -552,10 +576,12 @@ namespace Terminal.Gui {
 			var runes = text.ToRuneList ();
 			int slen = runes.Count;
 			if (slen > width) {
-				return ustring.Make (runes.GetRange (0, width));
+				return ustring.Make (runes.GetRange (0, GetMaxLengthForWidth (text, width)));
 			} else {
 				if (justify) {
-					return Justify (text, width);
+					return Justify (text, width, ' ', textDirection);
+				} else if (GetTextWidth (text) > width && IsHorizontalDirection (textDirection)) {
+					return ustring.Make (runes.GetRange (0, GetMaxLengthForWidth (text, width)));
 				}
 				return text;
 			}
@@ -568,8 +594,9 @@ namespace Terminal.Gui {
 		/// <param name="text"></param>
 		/// <param name="width"></param>
 		/// <param name="spaceChar">Character to replace whitespace and pad with. For debugging purposes.</param>
+		/// <param name="textDirection">The text direction.</param>
 		/// <returns>The justified text.</returns>
-		public static ustring Justify (ustring text, int width, char spaceChar = ' ')
+		public static ustring Justify (ustring text, int width, char spaceChar = ' ', TextDirection textDirection = TextDirection.LeftRight_TopBottom)
 		{
 			if (width < 0) {
 				throw new ArgumentOutOfRangeException ("Width cannot be negative.");
@@ -579,8 +606,12 @@ namespace Terminal.Gui {
 			}
 
 			var words = text.Split (ustring.Make (' '));
-			int textCount = words.Sum (arg => arg.RuneCount);
-
+			int textCount;
+			if (IsHorizontalDirection (textDirection)) {
+				textCount = words.Sum (arg => GetTextWidth (arg));
+			} else {
+				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;
 
@@ -610,6 +641,7 @@ namespace Terminal.Gui {
 		/// <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>
 		/// <param name="preserveTrailingSpaces">If <c>true</c> and 'wordWrap' also true, the wrapped text will keep the trailing spaces. If <c>false</c>, the trailing spaces will be trimmed.</param>
 		/// <param name="tabWidth">The tab width.</param>
+		/// <param name="textDirection">The text direction.</param>
 		/// <returns>A list of word wrapped lines.</returns>
 		/// <remarks>
 		/// <para>
@@ -622,9 +654,9 @@ namespace Terminal.Gui {
 		/// If <c>width</c> is int.MaxValue, the text will be formatted to the maximum width possible. 
 		/// </para>
 		/// </remarks>
-		public static List<ustring> Format (ustring text, int width, TextAlignment talign, bool wordWrap, bool preserveTrailingSpaces = false, int tabWidth = 0)
+		public static List<ustring> Format (ustring text, int width, TextAlignment talign, bool wordWrap, bool preserveTrailingSpaces = false, int tabWidth = 0, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
 		{
-			return Format (text, width, talign == TextAlignment.Justified, wordWrap, preserveTrailingSpaces, tabWidth);
+			return Format (text, width, talign == TextAlignment.Justified, wordWrap, preserveTrailingSpaces, tabWidth, textDirection);
 		}
 
 		/// <summary>
@@ -636,6 +668,7 @@ namespace Terminal.Gui {
 		/// <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>
 		/// <param name="preserveTrailingSpaces">If <c>true</c> and 'wordWrap' also true, the wrapped text will keep the trailing spaces. If <c>false</c>, the trailing spaces will be trimmed.</param>
 		/// <param name="tabWidth">The tab width.</param>
+		/// <param name="textDirection">The text direction.</param>
 		/// <returns>A list of word wrapped lines.</returns>
 		/// <remarks>
 		/// <para>
@@ -649,7 +682,7 @@ namespace Terminal.Gui {
 		/// </para>
 		/// </remarks>
 		public static List<ustring> Format (ustring text, int width, bool justify, bool wordWrap,
-			bool preserveTrailingSpaces = false, int tabWidth = 0)
+			bool preserveTrailingSpaces = false, int tabWidth = 0, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
 		{
 			if (width < 0) {
 				throw new ArgumentOutOfRangeException ("width cannot be negative");
@@ -666,7 +699,7 @@ namespace Terminal.Gui {
 
 			if (wordWrap == false) {
 				text = ReplaceCRLFWithSpace (text);
-				lineResult.Add (ClipAndJustify (text, width, justify));
+				lineResult.Add (ClipAndJustify (text, width, justify, textDirection));
 				return lineResult;
 			}
 
@@ -676,9 +709,9 @@ namespace Terminal.Gui {
 			for (int i = 0; i < runeCount; i++) {
 				Rune c = runes [i];
 				if (c == '\n') {
-					var wrappedLines = WordWrap (ustring.Make (runes.GetRange (lp, i - lp)), width, preserveTrailingSpaces, tabWidth);
+					var wrappedLines = WordWrap (ustring.Make (runes.GetRange (lp, i - lp)), width, preserveTrailingSpaces, tabWidth, textDirection);
 					foreach (var line in wrappedLines) {
-						lineResult.Add (ClipAndJustify (line, width, justify));
+						lineResult.Add (ClipAndJustify (line, width, justify, textDirection));
 					}
 					if (wrappedLines.Count == 0) {
 						lineResult.Add (ustring.Empty);
@@ -686,8 +719,8 @@ namespace Terminal.Gui {
 					lp = i + 1;
 				}
 			}
-			foreach (var line in WordWrap (ustring.Make (runes.GetRange (lp, runeCount - lp)), width, preserveTrailingSpaces, tabWidth)) {
-				lineResult.Add (ClipAndJustify (line, width, justify));
+			foreach (var line in WordWrap (ustring.Make (runes.GetRange (lp, runeCount - lp)), width, preserveTrailingSpaces, tabWidth, textDirection)) {
+				lineResult.Add (ClipAndJustify (line, width, justify, textDirection));
 			}
 
 			return lineResult;
@@ -717,7 +750,7 @@ namespace Terminal.Gui {
 			var max = 0;
 			result.ForEach (s => {
 				var m = 0;
-				s.ToRuneList ().ForEach (r => m += Rune.ColumnWidth (r));
+				s.ToRuneList ().ForEach (r => m += Math.Max (Rune.ColumnWidth (r), 1));
 				if (m > max) {
 					max = m;
 				}
@@ -725,6 +758,94 @@ namespace Terminal.Gui {
 			return max;
 		}
 
+		/// <summary>
+		/// Gets the total width of the passed text.
+		/// </summary>
+		/// <param name="text"></param>
+		/// <returns>The text width.</returns>
+		public static int GetTextWidth (ustring text)
+		{
+			return text.ToRuneList ().Sum (r => Math.Max (Rune.ColumnWidth (r), 1));
+		}
+
+		/// <summary>
+		/// Gets the maximum characters width from the list based on the <paramref name="startIndex"/>
+		/// and the <paramref name="length"/>.
+		/// </summary>
+		/// <param name="lines">The lines.</param>
+		/// <param name="startIndex">The start index.</param>
+		/// <param name="length">The length.</param>
+		/// <returns>The maximum characters width.</returns>
+		public static int GetSumMaxCharWidth (List<ustring> lines, int startIndex = -1, int length = -1)
+		{
+			var max = 0;
+			for (int i = (startIndex == -1 ? 0 : startIndex); i < (length == -1 ? lines.Count : startIndex + length); i++) {
+				var runes = lines [i];
+				if (runes.Length > 0)
+					max += runes.Max (r => Math.Max (Rune.ColumnWidth (r), 1));
+			}
+			return max;
+		}
+
+		/// <summary>
+		/// Gets the maximum characters width from the text based on the <paramref name="startIndex"/>
+		/// and the <paramref name="length"/>.
+		/// </summary>
+		/// <param name="text">The text.</param>
+		/// <param name="startIndex">The start index.</param>
+		/// <param name="length">The length.</param>
+		/// <returns>The maximum characters width.</returns>
+		public static int GetSumMaxCharWidth (ustring text, int startIndex = -1, int length = -1)
+		{
+			var max = 0;
+			var runes = text.ToRunes ();
+			for (int i = (startIndex == -1 ? 0 : startIndex); i < (length == -1 ? runes.Length : startIndex + length); i++) {
+				max += Math.Max (Rune.ColumnWidth (runes [i]), 1);
+			}
+			return max;
+		}
+
+		/// <summary>
+		/// Gets the index position from the text based on the <paramref name="width"/>.
+		/// </summary>
+		/// <param name="text">The text.</param>
+		/// <param name="width">The width.</param>
+		/// <returns>The index of the text that fit the width.</returns>
+		public static int GetMaxLengthForWidth (ustring text, int width)
+		{
+			var runes = text.ToRuneList ();
+			var runesLength = 0;
+			var runeIdx = 0;
+			for (; runeIdx < runes.Count; runeIdx++) {
+				var runeWidth = Math.Max (Rune.ColumnWidth (runes [runeIdx]), 1);
+				if (runesLength + runeWidth > width) {
+					break;
+				}
+				runesLength += runeWidth;
+			}
+			return runeIdx;
+		}
+
+		/// <summary>
+		/// Gets the index position from the list based on the <paramref name="width"/>.
+		/// </summary>
+		/// <param name="runes">The runes.</param>
+		/// <param name="width">The width.</param>
+		/// <returns>The index of the list that fit the width.</returns>
+		public static int GetMaxLengthForWidth (List<Rune> runes, int width)
+		{
+			var runesLength = 0;
+			var runeIdx = 0;
+			for (; runeIdx < runes.Count; runeIdx++) {
+				var runeWidth = Math.Max (Rune.ColumnWidth (runes [runeIdx]), 1);
+				if (runesLength + runeWidth > width) {
+					break;
+				}
+				runesLength += runeWidth;
+			}
+			return runeIdx;
+		}
+
 		/// <summary>
 		///  Calculates the rectangle required to hold text, assuming no word wrapping.
 		/// </summary>
@@ -753,15 +874,13 @@ namespace Terminal.Gui {
 							mw = cols;
 						}
 						cols = 0;
-					} else {
-						if (rune != '\r') {
-							cols++;
-							var rw = Rune.ColumnWidth (rune);
-							if (rw > 0) {
-								rw--;
-							}
-							cols += rw;
+					} else if (rune != '\r') {
+						cols++;
+						var rw = Rune.ColumnWidth (rune);
+						if (rw > 0) {
+							rw--;
 						}
+						cols += rw;
 					}
 				}
 				if (cols > mw) {
@@ -781,16 +900,14 @@ namespace Terminal.Gui {
 							vh = rows;
 						}
 						rows = 0;
-					} else {
-						if (rune != '\r') {
-							rows++;
-							var rw = Rune.ColumnWidth (rune);
-							if (rw < 0) {
-								rw++;
-							}
-							if (rw > vw) {
-								vw = rw;
-							}
+					} else if (rune != '\r') {
+						rows++;
+						var rw = Rune.ColumnWidth (rune);
+						if (rw < 0) {
+							rw++;
+						}
+						if (rw > vw) {
+							vw = rw;
 						}
 					}
 				}
@@ -925,7 +1042,8 @@ namespace Terminal.Gui {
 		/// <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)
+		/// <param name="containerBounds">Specifies the screen-relative location and maximum container size.</param>
+		public void Draw (Rect bounds, Attribute normalColor, Attribute hotColor, Rect containerBounds = default)
 		{
 			// With this check, we protect against subclasses with overrides of Text (like Button)
 			if (ustring.IsNullOrEmpty (text)) {
@@ -969,26 +1087,31 @@ namespace Terminal.Gui {
 				// Horizontal Alignment
 				if (textAlignment == TextAlignment.Right || (textAlignment == TextAlignment.Justified && !IsLeftToRight (textDirection))) {
 					if (isVertical) {
-						x = bounds.Right - Lines.Count + line;
-						CursorPosition = bounds.Width - Lines.Count + hotKeyPos;
+						var runesWidth = GetSumMaxCharWidth (Lines, line);
+						x = bounds.Right - runesWidth;
+						CursorPosition = bounds.Width - runesWidth + hotKeyPos;
 					} else {
-						x = bounds.Right - runes.Length;
-						CursorPosition = bounds.Width - runes.Length + hotKeyPos;
+						var runesWidth = GetTextWidth (ustring.Make (runes));
+						x = bounds.Right - runesWidth;
+						CursorPosition = bounds.Width - runesWidth + hotKeyPos;
 					}
 				} else if (textAlignment == TextAlignment.Left || textAlignment == TextAlignment.Justified) {
 					if (isVertical) {
-						x = bounds.Left + line;
+						var runesWidth = line > 0 ? GetSumMaxCharWidth (Lines, 0, line) : 0;
+						x = bounds.Left + runesWidth;
 					} else {
 						x = bounds.Left;
 					}
 					CursorPosition = hotKeyPos;
 				} else if (textAlignment == TextAlignment.Centered) {
 					if (isVertical) {
-						x = bounds.Left + line + ((bounds.Width - Lines.Count) / 2);
-						CursorPosition = (bounds.Width - Lines.Count) / 2 + hotKeyPos;
+						var runesWidth = GetSumMaxCharWidth (Lines, line);
+						x = bounds.Left + line + ((bounds.Width - runesWidth) / 2);
+						CursorPosition = (bounds.Width - runesWidth) / 2 + hotKeyPos;
 					} else {
-						x = bounds.Left + (bounds.Width - runes.Length) / 2;
-						CursorPosition = (bounds.Width - runes.Length) / 2 + hotKeyPos;
+						var runesWidth = GetTextWidth (ustring.Make (runes));
+						x = bounds.Left + (bounds.Width - runesWidth) / 2;
+						CursorPosition = (bounds.Width - runesWidth) / 2 + hotKeyPos;
 					}
 				} else {
 					throw new ArgumentOutOfRangeException ();
@@ -1021,9 +1144,21 @@ namespace Terminal.Gui {
 
 				var start = isVertical ? bounds.Top : bounds.Left;
 				var size = isVertical ? bounds.Height : bounds.Width;
-
 				var current = start;
-				for (var idx = start; idx < start + size; idx++) {
+				var startX = start < 0
+					? start
+					: isVertical ? start - y : start - x;
+				var savedClip = Application.Driver?.Clip;
+				if (Application.Driver != null && containerBounds != default) {
+					Application.Driver.Clip = containerBounds == default
+						? bounds
+						: new Rect (Math.Max (containerBounds.X, bounds.X),
+						Math.Max (containerBounds.Y, bounds.Y),
+						Math.Min (containerBounds.Width, containerBounds.Right - bounds.Left),
+						Math.Min (containerBounds.Height, containerBounds.Bottom - bounds.Top));
+				}
+
+				for (var idx = startX; current < start + size; idx++) {
 					if (idx < 0) {
 						current++;
 						continue;
@@ -1031,13 +1166,13 @@ namespace Terminal.Gui {
 					var rune = (Rune)' ';
 					if (isVertical) {
 						Application.Driver?.Move (x, current);
-						if (idx >= y && idx < (y + runes.Length)) {
-							rune = runes [idx - y];
+						if (idx >= 0 && idx < runes.Length) {
+							rune = runes [idx];
 						}
 					} else {
 						Application.Driver?.Move (current, y);
-						if (idx >= x && idx < (x + runes.Length)) {
-							rune = runes [idx - x];
+						if (idx >= 0 && idx < runes.Length) {
+							rune = runes [idx];
 						}
 					}
 					if ((rune & HotKeyTagMask) == HotKeyTagMask) {
@@ -1051,11 +1186,18 @@ namespace Terminal.Gui {
 					} else {
 						Application.Driver?.AddRune (rune);
 					}
-					current += Rune.ColumnWidth (rune);
-					if (idx + 1 < runes.Length && current + Rune.ColumnWidth (runes [idx + 1]) > size) {
+					var runeWidth = Math.Max (Rune.ColumnWidth (rune), 1);
+					if (isVertical) {
+						current++;
+					} else {
+						current += runeWidth;
+					}
+					if (!isVertical && idx + 1 < runes.Length && current + Rune.ColumnWidth (runes [idx + 1]) > start + size) {
 						break;
 					}
 				}
+				if (Application.Driver != null)
+					Application.Driver.Clip = (Rect)savedClip;
 			}
 		}
 	}

+ 14 - 5
UnitTests/GraphViewTests.cs

@@ -9,6 +9,7 @@ using Attribute = Terminal.Gui.Attribute;
 using System.Text;
 using System.Text.RegularExpressions;
 using Xunit.Abstractions;
+using Rune = System.Rune;
 
 namespace Terminal.Gui.Views {
 
@@ -140,10 +141,13 @@ namespace Terminal.Gui.Views {
 								runes.InsertRange (i, new List<char> () { ' ' });
 							}
 						}
-						if (c > w) {
-							w = c;
+						if (Rune.ColumnWidth (rune) > 1) {
+							c++;
 						}
-						h = r - y;
+						if (c + 1 > w) {
+							w = c + 1;
+						}
+						h = r - y + 1;
 					}
 					if (x > -1) {
 						runes.Add (rune);
@@ -155,7 +159,7 @@ namespace Terminal.Gui.Views {
 			}
 
 			// Remove unnecessary empty lines
-			for (int r = lines.Count - 1; r > h; r--) {
+			for (int r = lines.Count - 1; r > h - 1; r--) {
 				lines.RemoveAt (r);
 			}
 
@@ -201,7 +205,7 @@ namespace Terminal.Gui.Views {
 
 				Assert.Equal (expectedLook, actualLook);
 			}
-			return new Rect (x, y, w > -1 ? w + 1 : 0, h > -1 ? h + 1 : 0);
+			return new Rect (x, y, w > -1 ? w : 0, h > -1 ? h : 0);
 		}
 
 #pragma warning disable xUnit1013 // Public method should be marked as test
@@ -1687,6 +1691,11 @@ namespace Terminal.Gui.Views {
 				//put label into view
 				mount.Add (lbl1);
 
+				//putting mount into toplevel since changing size
+				//also change AutoSize to false
+				Application.Top.Add (mount);
+				Application.Begin (Application.Top);
+
 				// render view
 				lbl1.ColorScheme = new ColorScheme ();
 				Assert.Equal (1, lbl1.Height);

+ 603 - 4
UnitTests/TextFormatterTests.cs

@@ -1,17 +1,22 @@
 using NStack;
 using System;
 using System.Collections.Generic;
-using System.ComponentModel;
-using System.IO;
 using System.Linq;
-using Terminal.Gui;
+using Terminal.Gui.Views;
 using Xunit;
+using Xunit.Abstractions;
 
 // Alias Console to MockConsole so we don't accidentally use Console
 using Console = Terminal.Gui.FakeConsole;
 
 namespace Terminal.Gui.Core {
 	public class TextFormatterTests {
+		readonly ITestOutputHelper output;
+
+		public TextFormatterTests (ITestOutputHelper output)
+		{
+			this.output = output;
+		}
 
 		[Fact]
 		public void Basic_Usage ()
@@ -1931,6 +1936,240 @@ namespace Terminal.Gui.Core {
 			Assert.True (wrappedLines.Count == text.Length);
 		}
 
+		[Fact]
+		public void WordWrap_preserveTrailingSpaces_Wide_Runes ()
+		{
+			var text = ustring.Empty;
+			int maxWidth = 1;
+			int expectedClippedWidth = 1;
+
+			List<ustring> wrappedLines;
+
+			text = "文に は言葉 があり ます。";
+			maxWidth = 14;
+			expectedClippedWidth = 14;
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth, true);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("文に は言葉 ", wrappedLines [0].ToString ());
+			Assert.Equal ("があり ます。", wrappedLines [1].ToString ());
+			Assert.True (wrappedLines.Count == 2);
+
+			maxWidth = 3;
+			expectedClippedWidth = 3;
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth, true);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("文に", wrappedLines [0].ToString ());
+			Assert.Equal (" ", wrappedLines [1].ToString ());
+			Assert.Equal ("は言", wrappedLines [2].ToString ());
+			Assert.Equal ("葉 ", wrappedLines [3].ToString ());
+			Assert.Equal ("があ", wrappedLines [4].ToString ());
+			Assert.Equal ("り ", wrappedLines [5].ToString ());
+			Assert.Equal ("ます", wrappedLines [6].ToString ());
+			Assert.Equal ("。", wrappedLines [^1].ToString ());
+			Assert.True (wrappedLines.Count == 8);
+
+			maxWidth = 2;
+			expectedClippedWidth = 2;
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth, true);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("文", wrappedLines [0].ToString ());
+			Assert.Equal ("に", wrappedLines [1].ToString ());
+			Assert.Equal (" ", wrappedLines [2].ToString ());
+			Assert.Equal ("は", wrappedLines [3].ToString ());
+			Assert.Equal ("言", wrappedLines [4].ToString ());
+			Assert.Equal ("葉", wrappedLines [5].ToString ());
+			Assert.Equal (" ", wrappedLines [6].ToString ());
+			Assert.Equal ("が", wrappedLines [7].ToString ());
+			Assert.Equal ("あ", wrappedLines [8].ToString ());
+			Assert.Equal ("り", wrappedLines [9].ToString ());
+			Assert.Equal (" ", wrappedLines [10].ToString ());
+			Assert.Equal ("ま", wrappedLines [11].ToString ());
+			Assert.Equal ("す", wrappedLines [12].ToString ());
+			Assert.Equal ("。", wrappedLines [^1].ToString ());
+			Assert.True (wrappedLines.Count == 14);
+
+			maxWidth = 1;
+			expectedClippedWidth = 1;
+			wrappedLines = TextFormatter.WordWrap (text, maxWidth, true);
+			Assert.True (expectedClippedWidth >= wrappedLines.Max (l => l.RuneCount));
+			Assert.Equal ("文", wrappedLines [0].ToString ());
+			Assert.Equal ("に", wrappedLines [1].ToString ());
+			Assert.Equal (" ", wrappedLines [2].ToString ());
+			Assert.Equal ("は", wrappedLines [3].ToString ());
+			Assert.Equal ("言", wrappedLines [4].ToString ());
+			Assert.Equal ("葉", wrappedLines [5].ToString ());
+			Assert.Equal (" ", wrappedLines [6].ToString ());
+			Assert.Equal ("が", wrappedLines [7].ToString ());
+			Assert.Equal ("あ", wrappedLines [8].ToString ());
+			Assert.Equal ("り", wrappedLines [9].ToString ());
+			Assert.Equal (" ", wrappedLines [10].ToString ());
+			Assert.Equal ("ま", wrappedLines [11].ToString ());
+			Assert.Equal ("す", wrappedLines [12].ToString ());
+			Assert.Equal ("。", wrappedLines [^1].ToString ());
+			Assert.False (wrappedLines.Count == text.Length);
+			Assert.True (wrappedLines.Count == text.RuneCount);
+			Assert.Equal (25, text.ConsoleWidth);
+			Assert.Equal (25, TextFormatter.GetTextWidth (text));
+		}
+
+		[Fact, AutoInitShutdown]
+		public void WordWrap_preserveTrailingSpaces_Horizontal_With_Simple_Runes ()
+		{
+			var text = "A sentence has words.";
+			var width = 3;
+			var height = 8;
+			var wrappedLines = TextFormatter.WordWrap (text, width, true);
+			var breakLines = "";
+			foreach (var line in wrappedLines) {
+				breakLines += $"{line}{Environment.NewLine}";
+			}
+			var label = new Label (breakLines) { Width = Dim.Fill (), Height = Dim.Fill () };
+			var frame = new FrameView () { Width = Dim.Fill (), Height = Dim.Fill () };
+
+			frame.Add (label);
+			Application.Top.Add (frame);
+			Application.Begin (Application.Top);
+			((FakeDriver)Application.Driver).SetBufferSize (width + 2, height + 2);
+
+			Assert.False (label.AutoSize);
+			Assert.Equal (new Rect (0, 0, width, height), label.Frame);
+			Assert.Equal (new Rect (0, 0, width + 2, height + 2), frame.Frame);
+
+			var expected = @"
+┌───┐
+│A  │
+│sen│
+│ten│
+│ce │
+│has│
+│   │
+│wor│
+│ds.│
+└───┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, width + 2, height + 2), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void WordWrap_preserveTrailingSpaces_Vertical_With_Simple_Runes ()
+		{
+			var text = "A sentence has words.";
+			var width = 8;
+			var height = 3;
+			var wrappedLines = TextFormatter.WordWrap (text, width, true);
+			var breakLines = "";
+			foreach (var line in wrappedLines) {
+				breakLines += $"{line}{Environment.NewLine}";
+			}
+			var label = new Label (breakLines) {
+				TextDirection = TextDirection.TopBottom_LeftRight,
+				Width = Dim.Fill (), Height = Dim.Fill () 
+			};
+			var frame = new FrameView () { Width = Dim.Fill (), Height = Dim.Fill () };
+
+			frame.Add (label);
+			Application.Top.Add (frame);
+			Application.Begin (Application.Top);
+			((FakeDriver)Application.Driver).SetBufferSize (width + 2, height + 2);
+
+			Assert.False (label.AutoSize);
+			Assert.Equal (new Rect (0, 0, width, height), label.Frame);
+			Assert.Equal (new Rect (0, 0, width + 2, height + 2), frame.Frame);
+
+			var expected = @"
+┌────────┐
+│Astc swd│
+│ eeeh os│
+│ nn a r.│
+└────────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, width + 2, height + 2), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void WordWrap_preserveTrailingSpaces_Horizontal_With_Wide_Runes ()
+		{
+			var text = "文に は言葉 があり ます。";
+			var width = 6;
+			var height = 8;
+			var wrappedLines = TextFormatter.WordWrap (text, width, true);
+			var breakLines = "";
+			foreach (var line in wrappedLines) {
+				breakLines += $"{line}{Environment.NewLine}";
+			}
+			var label = new Label (breakLines) { Width = Dim.Fill (), Height = Dim.Fill () };
+			var frame = new FrameView () { Width = Dim.Fill (), Height = Dim.Fill () };
+
+			frame.Add (label);
+			Application.Top.Add (frame);
+			Application.Begin (Application.Top);
+			((FakeDriver)Application.Driver).SetBufferSize (width + 2, height + 2);
+
+			Assert.False (label.AutoSize);
+			Assert.Equal (new Rect (0, 0, width, height), label.Frame);
+			Assert.Equal (new Rect (0, 0, width + 2, height + 2), frame.Frame);
+
+			var expected = @"
+┌──────┐
+│文に  │
+│は言葉│
+│ があ │
+│り    │
+│ ます │
+│。    │
+│      │
+│      │
+└──────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, width + 2, height + 2), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void WordWrap_preserveTrailingSpaces_Vertical_With_Wide_Runes ()
+		{
+			var text = "文に は言葉 があり ます。";
+			var width = 8;
+			var height = 4;
+			var wrappedLines = TextFormatter.WordWrap (text, width, true);
+			var breakLines = "";
+			foreach (var line in wrappedLines) {
+				breakLines += $"{line}{Environment.NewLine}";
+			}
+			var label = new Label (breakLines) {
+				TextDirection = TextDirection.TopBottom_LeftRight,
+				Width = Dim.Fill (),
+				Height = Dim.Fill ()
+			};
+			var frame = new FrameView () { Width = Dim.Fill (), Height = Dim.Fill () };
+
+			frame.Add (label);
+			Application.Top.Add (frame);
+			Application.Begin (Application.Top);
+			((FakeDriver)Application.Driver).SetBufferSize (width + 2, height + 2);
+
+			Assert.False (label.AutoSize);
+			Assert.Equal (new Rect (0, 0, width, height), label.Frame);
+			Assert.Equal (new Rect (0, 0, width + 2, height + 2), frame.Frame);
+
+			var expected = @"
+┌────────┐
+│文はがま│
+│に言あす│
+│  葉り。│
+│        │
+└────────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, width + 2, height + 2), pos);
+		}
+
 		[Fact]
 		public void WordWrap_preserveTrailingSpaces_With_Tab ()
 		{
@@ -2011,6 +2250,18 @@ namespace Terminal.Gui.Core {
 			Assert.True (wrappedLines.Count == text.Length);
 		}
 
+		[Fact]
+		public void WordWrap_Unicode_Wide_Runes ()
+		{
+			ustring text = "これが最初の行です。 こんにちは世界。 これが2行目です。";
+			var width = text.RuneCount;
+			var wrappedLines = TextFormatter.WordWrap (text, width);
+			Assert.Equal (3, wrappedLines.Count);
+			Assert.Equal ("これが最初の行です。", wrappedLines [0].ToString ());
+			Assert.Equal ("こんにちは世界。", wrappedLines [1].ToString ());
+			Assert.Equal ("これが2行目です。", wrappedLines [^1].ToString ());
+		}
+
 		[Fact]
 		public void ReplaceHotKeyWithTag ()
 		{
@@ -2539,7 +2790,7 @@ namespace Terminal.Gui.Core {
 		}
 
 		[Fact]
-		public void TestClipOrPad_ShortWord()
+		public void TestClipOrPad_ShortWord ()
 		{
 			// word is short but we want it to fill 6 so it should be padded
 			Assert.Equal ("fff   ", TextFormatter.ClipOrPad ("fff", 6));
@@ -2589,7 +2840,355 @@ namespace Terminal.Gui.Core {
 			Assert.Equal (Key.Null, tf.HotKey);
 			tf.HotKey = Key.CtrlMask | Key.Q;
 			Assert.Equal (Key.CtrlMask | Key.Q, tf.HotKey);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Draw_Horizontal_Simple_Runes ()
+		{
+			var label = new Label ("Demo Simple Rune");
+			Application.Top.Add (label);
+			Application.Begin (Application.Top);
+
+			Assert.True (label.AutoSize);
+			Assert.Equal (new Rect (0, 0, 16, 1), label.Frame);
+
+			var expected = @"
+Demo Simple Rune
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 16, 1), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Draw_Vertical_Simple_Runes ()
+		{
+			var label = new Label ("Demo Simple Rune") {
+				TextDirection = TextDirection.TopBottom_LeftRight
+			};
+			Application.Top.Add (label);
+			Application.Begin (Application.Top);
+
+			Assert.True (label.AutoSize);
+			Assert.Equal (new Rect (0, 0, 1, 16), label.Frame);
+
+			var expected = @"
+D
+e
+m
+o
+
+S
+i
+m
+p
+l
+e
+
+R
+u
+n
+e
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 1, 16), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Draw_Horizontal_Wide_Runes ()
+		{
+			var label = new Label ("デモエムポンズ");
+			Application.Top.Add (label);
+			Application.Begin (Application.Top);
+
+			Assert.True (label.AutoSize);
+			Assert.Equal (new Rect (0, 0, 14, 1), label.Frame);
+
+			var expected = @"
+デモエムポンズ
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 14, 1), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Draw_Vertical_Wide_Runes ()
+		{
+			var label = new Label ("デモエムポンズ") {
+				TextDirection = TextDirection.TopBottom_LeftRight
+			};
+			Application.Top.Add (label);
+			Application.Begin (Application.Top);
+
+			Assert.True (label.AutoSize);
+			Assert.Equal (new Rect (0, 0, 2, 7), label.Frame);
+
+			var expected = @"
+デ
+モ
+エ
+ム
+ポ
+ン
+ズ
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 2, 7), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Draw_Horizontal_Simple_TextAlignments ()
+		{
+			var text = "Hello World";
+			var width = 20;
+			var lblLeft = new Label (text) { Width = width };
+			var lblCenter = new Label (text) { Y = 1, Width = width, TextAlignment = TextAlignment.Centered };
+			var lblRight = new Label (text) { Y = 2, Width = width, TextAlignment = TextAlignment.Right };
+			var lblJust = new Label (text) { Y = 3, Width = width, TextAlignment = TextAlignment.Justified };
+			var frame = new FrameView () { Width = Dim.Fill (), Height = Dim.Fill () };
+
+			frame.Add (lblLeft, lblCenter, lblRight, lblJust);
+			Application.Top.Add (frame);
+			Application.Begin (Application.Top);
+			((FakeDriver)Application.Driver).SetBufferSize (width + 2, 6);
+
+			Assert.False (lblLeft.AutoSize);
+			Assert.False (lblCenter.AutoSize);
+			Assert.False (lblRight.AutoSize);
+			Assert.False (lblJust.AutoSize);
+			Assert.Equal (new Rect (0, 0, width, 1), lblLeft.Frame);
+			Assert.Equal (new Rect (0, 1, width, 1), lblCenter.Frame);
+			Assert.Equal (new Rect (0, 2, width, 1), lblRight.Frame);
+			Assert.Equal (new Rect (0, 3, width, 1), lblJust.Frame);
+			Assert.Equal (new Rect (0, 0, width + 2, 6), frame.Frame);
+
+			var expected = @"
+┌────────────────────┐
+│Hello World         │
+│    Hello World     │
+│         Hello World│
+│Hello          World│
+└────────────────────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, width + 2, 6), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Draw_Vertical_Simple_TextAlignments ()
+		{
+			var text = "Hello World";
+			var height = 20;
+			var lblLeft = new Label (text, direction: TextDirection.TopBottom_LeftRight) { Height = height };
+			var lblCenter = new Label (text, direction: TextDirection.TopBottom_LeftRight) { X = 2, Height = height, VerticalTextAlignment = VerticalTextAlignment.Middle };
+			var lblRight = new Label (text, direction: TextDirection.TopBottom_LeftRight) { X = 4, Height = height, VerticalTextAlignment = VerticalTextAlignment.Bottom };
+			var lblJust = new Label (text, direction: TextDirection.TopBottom_LeftRight) { X = 6, Height = height, VerticalTextAlignment = VerticalTextAlignment.Justified };
+			var frame = new FrameView () { Width = Dim.Fill (), Height = Dim.Fill () };
+
+			frame.Add (lblLeft, lblCenter, lblRight, lblJust);
+			Application.Top.Add (frame);
+			Application.Begin (Application.Top);
+			((FakeDriver)Application.Driver).SetBufferSize (9, height + 2);
+
+			Assert.False (lblLeft.AutoSize);
+			Assert.False (lblCenter.AutoSize);
+			Assert.False (lblRight.AutoSize);
+			Assert.False (lblJust.AutoSize);
+			Assert.Equal (new Rect (0, 0, 1, height), lblLeft.Frame);
+			Assert.Equal (new Rect (2, 0, 1, height), lblCenter.Frame);
+			Assert.Equal (new Rect (4, 0, 1, height), lblRight.Frame);
+			Assert.Equal (new Rect (6, 0, 1, height), lblJust.Frame);
+			Assert.Equal (new Rect (0, 0, 9, height + 2), frame.Frame);
+
+			var expected = @"
+┌───────┐
+│H     H│
+│e     e│
+│l     l│
+│l     l│
+│o H   o│
+│  e    │
+│W l    │
+│o l    │
+│r o    │
+│l   H  │
+│d W e  │
+│  o l  │
+│  r l  │
+│  l o  │
+│  d    │
+│    W W│
+│    o o│
+│    r r│
+│    l l│
+│    d d│
+└───────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 9, height + 2), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Draw_Horizontal_Wide_TextAlignments ()
+		{
+			var text = "こんにちは 世界";
+			var width = 25;
+			var lblLeft = new Label (text) { Width = width };
+			var lblCenter = new Label (text) { Y = 1, Width = width, TextAlignment = TextAlignment.Centered };
+			var lblRight = new Label (text) { Y = 2, Width = width, TextAlignment = TextAlignment.Right };
+			var lblJust = new Label (text) { Y = 3, Width = width, TextAlignment = TextAlignment.Justified };
+			var frame = new FrameView () { Width = Dim.Fill (), Height = Dim.Fill () };
+
+			frame.Add (lblLeft, lblCenter, lblRight, lblJust);
+			Application.Top.Add (frame);
+			Application.Begin (Application.Top);
+			((FakeDriver)Application.Driver).SetBufferSize (width + 2, 6);
+
+			Assert.False (lblLeft.AutoSize);
+			Assert.False (lblCenter.AutoSize);
+			Assert.False (lblRight.AutoSize);
+			Assert.False (lblJust.AutoSize);
+			Assert.Equal (new Rect (0, 0, width, 1), lblLeft.Frame);
+			Assert.Equal (new Rect (0, 1, width, 1), lblCenter.Frame);
+			Assert.Equal (new Rect (0, 2, width, 1), lblRight.Frame);
+			Assert.Equal (new Rect (0, 3, width, 1), lblJust.Frame);
+			Assert.Equal (new Rect (0, 0, width + 2, 6), frame.Frame);
+
+			var expected = @"
+┌─────────────────────────┐
+│こんにちは 世界          │
+│     こんにちは 世界     │
+│          こんにちは 世界│
+│こんにちは           世界│
+└─────────────────────────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, width + 2, 6), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Draw_Vertical_Wide_TextAlignments ()
+		{
+			var text = "こんにちは 世界";
+			var height = 23;
+			var lblLeft = new Label (text) { Width = 2, Height = height, TextDirection = TextDirection.TopBottom_LeftRight };
+			var lblCenter = new Label (text) { X = 3, Width = 2, Height = height, TextDirection = TextDirection.TopBottom_LeftRight, VerticalTextAlignment = VerticalTextAlignment.Middle };
+			var lblRight = new Label (text) { X = 6, Width = 2, Height = height, TextDirection = TextDirection.TopBottom_LeftRight, VerticalTextAlignment = VerticalTextAlignment.Bottom };
+			var lblJust = new Label (text) { X = 9, Width = 2, Height = height, TextDirection = TextDirection.TopBottom_LeftRight, VerticalTextAlignment = VerticalTextAlignment.Justified };
+			var frame = new FrameView () { Width = Dim.Fill (), Height = Dim.Fill () };
+
+			frame.Add (lblLeft, lblCenter, lblRight, lblJust);
+			Application.Top.Add (frame);
+			Application.Begin (Application.Top);
+			((FakeDriver)Application.Driver).SetBufferSize (13, height + 2);
+
+			Assert.False (lblLeft.AutoSize);
+			Assert.False (lblCenter.AutoSize);
+			Assert.False (lblRight.AutoSize);
+			Assert.False (lblJust.AutoSize);
+			Assert.Equal (new Rect (0, 0, 2, height), lblLeft.Frame);
+			Assert.Equal (new Rect (3, 0, 2, height), lblCenter.Frame);
+			Assert.Equal (new Rect (6, 0, 2, height), lblRight.Frame);
+			Assert.Equal (new Rect (9, 0, 2, height), lblJust.Frame);
+			Assert.Equal (new Rect (0, 0, 13, height + 2), frame.Frame);
+
+			var expected = @"
+┌───────────┐
+│こ       こ│
+│ん       ん│
+│に       に│
+│ち       ち│
+│は       は│
+│           │
+│世         │
+│界 こ      │
+│   ん      │
+│   に      │
+│   ち      │
+│   は      │
+│           │
+│   世      │
+│   界      │
+│      こ   │
+│      ん   │
+│      に   │
+│      ち   │
+│      は   │
+│           │
+│      世 世│
+│      界 界│
+└───────────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 13, height + 2), pos);
+		}
+
+		[Fact]
+		public void GetTextWidth_Simple_And_Wide_Runes ()
+		{
+			ustring text = "Hello World";
+			Assert.Equal (11, TextFormatter.GetTextWidth (text));
+			text = "こんにちは世界";
+			Assert.Equal (14, TextFormatter.GetTextWidth (text));
+		}
+
+		[Fact]
+		public void GetSumMaxCharWidth_Simple_And_Wide_Runes ()
+		{
+			ustring text = "Hello World";
+			Assert.Equal (11, TextFormatter.GetSumMaxCharWidth (text));
+			Assert.Equal (1, TextFormatter.GetSumMaxCharWidth (text, 6, 1));
+			text = "こんにちは 世界";
+			Assert.Equal (15, TextFormatter.GetSumMaxCharWidth (text));
+			Assert.Equal (2, TextFormatter.GetSumMaxCharWidth (text, 6, 1));
+		}
+
+		[Fact]
+		public void GetSumMaxCharWidth_List_Simple_And_Wide_Runes ()
+		{
+			List<ustring> text =new List<ustring>() { "Hello", "World" };
+			Assert.Equal (2, TextFormatter.GetSumMaxCharWidth (text));
+			Assert.Equal (1, TextFormatter.GetSumMaxCharWidth (text, 1, 1));
+			text = new List<ustring> () { "こんにちは", "世界" };
+			Assert.Equal (4, TextFormatter.GetSumMaxCharWidth (text));
+			Assert.Equal (2, TextFormatter.GetSumMaxCharWidth (text, 1, 1));
+		}
+
+		[Fact]
+		public void GetMaxLengthForWidth_Simple_And_Wide_Runes ()
+		{
+			ustring text = "Hello World";
+			Assert.Equal (6, TextFormatter.GetMaxLengthForWidth (text, 6));
+			text = "こんにちは 世界";
+			Assert.Equal (3, TextFormatter.GetMaxLengthForWidth (text, 6));
+		}
+
+		[Fact]
+		public void GetMaxLengthForWidth_List_Simple_And_Wide_Runes ()
+		{
+			var runes = ustring.Make ("Hello World").ToRuneList ();
+			Assert.Equal (6, TextFormatter.GetMaxLengthForWidth (runes, 6));
+			runes = ustring.Make ("こんにちは 世界").ToRuneList ();
+			Assert.Equal (3, TextFormatter.GetMaxLengthForWidth (runes, 6));
+		}
+
+		[Fact]
+		public void Format_Truncate_Simple_And_Wide_Runes ()
+		{
+			var text = "Truncate";
+			var list = TextFormatter.Format (text, 3, false, false);
+			Assert.Equal ("Tru", list [^1].ToString ());
 
+			text = "デモエムポンズ";
+			list = TextFormatter.Format (text, 3, false, false);
+			Assert.Equal ("デ", list [^1].ToString ());
 		}
 	}
 }