Browse Source

feature complete

Charlie Kindel 5 years ago
parent
commit
8272d8aff3

+ 202 - 182
Terminal.Gui/Core/TextFormatter.cs

@@ -6,33 +6,25 @@ using NStack;
 
 
 namespace Terminal.Gui {
 namespace Terminal.Gui {
 	/// <summary>
 	/// <summary>
-	/// Suppports text formatting, including horizontal alignment and word wrap for <see cref="View"/>.
+	/// Provides text formatting capabilites for console apps. Supports, hotkeys, horizontal alignment, multille lines, and word-based line wrap.
 	/// </summary>
 	/// </summary>
 	public class TextFormatter {
 	public class TextFormatter {
 		List<ustring> lines = new List<ustring> ();
 		List<ustring> lines = new List<ustring> ();
 		ustring text;
 		ustring text;
 		TextAlignment textAlignment;
 		TextAlignment textAlignment;
 		Attribute textColor = -1;
 		Attribute textColor = -1;
-		bool recalcPending = false;
+		bool needsFormat = true;
 		Key hotKey;
 		Key hotKey;
+		Size size;
 
 
 		/// <summary>
 		/// <summary>
-		///  Inititalizes a new <see cref="TextFormatter"/> object.
-		/// </summary>
-		/// <param name="view"></param>
-		public TextFormatter (View view)
-		{
-			recalcPending = true;
-		}
-
-		/// <summary>
-		///   The text to be displayed.
+		///   The text to be displayed. This text is never modified.
 		/// </summary>
 		/// </summary>
 		public virtual ustring Text {
 		public virtual ustring Text {
 			get => text;
 			get => text;
 			set {
 			set {
 				text = value;
 				text = value;
-				recalcPending = true;
+				needsFormat = true;
 			}
 			}
 		}
 		}
 
 
@@ -45,15 +37,20 @@ namespace Terminal.Gui {
 			get => textAlignment;
 			get => textAlignment;
 			set {
 			set {
 				textAlignment = value;
 				textAlignment = value;
-				recalcPending = true;
+				needsFormat = true;
 			}
 			}
 		}
 		}
 
 
 		/// <summary>
 		/// <summary>
 		///  Gets the size of the area the text will be drawn in. 
 		///  Gets the size of the area the text will be drawn in. 
 		/// </summary>
 		/// </summary>
-		public Size Size { get; internal set; }
-
+		public Size Size {
+			get => size;
+			internal set {
+				size = value;
+				needsFormat = true;
+			}
+		}
 
 
 		/// <summary>
 		/// <summary>
 		/// The specifier character for the hotkey (e.g. '_'). Set to '\xffff' to disable hotkey support for this View instance. The default is '\xffff'. 
 		/// The specifier character for the hotkey (e.g. '_'). Set to '\xffff' to disable hotkey support for this View instance. The default is '\xffff'. 
@@ -71,41 +68,83 @@ namespace Terminal.Gui {
 		public Key HotKey { get => hotKey; internal set => hotKey = value; }
 		public Key HotKey { get => hotKey; internal set => hotKey = value; }
 
 
 		/// <summary>
 		/// <summary>
-		/// Causes the Text to be formatted, based on <see cref="Alignment"/> and <see cref="Size"/>.
+		/// 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>
 		/// </summary>
-		public void ReFormat ()
-		{
-			// With this check, we protect against subclasses with overrides of Text
-			if (ustring.IsNullOrEmpty (Text)) {
-				return;
-			}
-			recalcPending = false;
-			var shown_text = text;
-			if (FindHotKey (text, HotKeySpecifier, true, out hotKeyPos, out hotKey)) {
-				shown_text = RemoveHotKeySpecifier (Text, hotKeyPos, HotKeySpecifier);
-				shown_text = ReplaceHotKeyWithTag (shown_text, hotKeyPos);
+		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;
 			}
 			}
-			Reformat (shown_text, lines, Size.Width, textAlignment, Size.Height > 1);
 		}
 		}
 
 
-		static ustring StripWhiteCRLF (ustring str)
+		/// <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 ()
 		{
 		{
-			var runes = new List<Rune> ();
-			foreach (var r in str.ToRunes ()) {
-				if (r != '\r' && r != '\n') {
-					runes.Add (r);
+			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); ;
+			return ustring.Make (runes);
 		}
 		}
 		static ustring ReplaceCRLFWithSpace (ustring str)
 		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 (' '));
-				} else {
-					runes.Add (r);
+			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); ;
 			return ustring.Make (runes); ;
@@ -116,9 +155,14 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// </summary>
 		/// <param name="text">The text to word warp</param>
 		/// <param name="text">The text to word warp</param>
 		/// <param name="width">The width to contrain the text to</param>
 		/// <param name="width">The width to contrain the text to</param>
-		/// <returns>Returns a list of lines.</returns>
+		/// <returns>Returns a list of word wrapped lines.</returns>
 		/// <remarks>
 		/// <remarks>
-		/// Newlines ('\n' and '\r\n') sequences are honored.
+		/// <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>
 		/// </remarks>
 		public static List<ustring> WordWrap (ustring text, int width)
 		public static List<ustring> WordWrap (ustring text, int width)
 		{
 		{
@@ -133,24 +177,32 @@ namespace Terminal.Gui {
 				return lines;
 				return lines;
 			}
 			}
 
 
-			text = StripWhiteCRLF (text);
+			var runes = StripCRLF (text).ToRunes ();
 
 
-			while ((end = start + width) < text.RuneCount) {
-				while (text [end] != ' ' && end > start)
+			while ((end = start + width) < runes.Length) {
+				while (runes [end] != ' ' && end > start)
 					end -= 1;
 					end -= 1;
 				if (end == start)
 				if (end == start)
 					end = start + width;
 					end = start + width;
 
 
-				lines.Add (text [start, end].TrimSpace ());
+
+				lines.Add (ustring.Make (runes [start..end]).TrimSpace ());
 				start = end;
 				start = end;
 			}
 			}
 
 
 			if (start < text.RuneCount)
 			if (start < text.RuneCount)
-				lines.Add (text.Substring (start).TrimSpace ());
+				lines.Add (ustring.Make (runes [start..]).TrimSpace ());
 
 
 			return lines;
 			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)
 		public static ustring ClipAndJustify (ustring text, int width, TextAlignment talign)
 		{
 		{
 			if (width < 0) {
 			if (width < 0) {
@@ -160,9 +212,10 @@ namespace Terminal.Gui {
 				return text;
 				return text;
 			}
 			}
 
 
-			int slen = text.RuneCount;
+			var runes = text.ToRunes ();
+			int slen = runes.Length;
 			if (slen > width) {
 			if (slen > width) {
-				return text [0, width];
+				return ustring.Make (runes [0..width]); // text [0, width];
 			} else {
 			} else {
 				if (talign == TextAlignment.Justified) {
 				if (talign == TextAlignment.Justified) {
 					return Justify (text, width);
 					return Justify (text, width);
@@ -189,8 +242,8 @@ namespace Terminal.Gui {
 			}
 			}
 
 
 			// TODO: Use ustring
 			// TODO: Use ustring
-			var words = text.ToString ().Split (whitespace, StringSplitOptions.RemoveEmptyEntries);
-			int textCount = words.Sum (arg => arg.Length);
+			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 spaces = words.Length > 1 ? (width - textCount) / (words.Length - 1) : 0;
 			var extras = words.Length > 1 ? (width - textCount) % words.Length : 0;
 			var extras = words.Length > 1 ? (width - textCount) % words.Length : 0;
@@ -215,29 +268,47 @@ namespace Terminal.Gui {
 		private int hotKeyPos;
 		private int hotKeyPos;
 
 
 		/// <summary>
 		/// <summary>
-		/// Reformats text into lines, applying text alignment and word wraping.
+		/// Reformats text into lines, applying text alignment and optionally wrapping text to new lines on word boundaries.
 		/// </summary>
 		/// </summary>
-		/// <param name="textStr"></param>
-		/// <param name="lineResult"></param>
-		/// <param name="width"></param>
-		/// <param name="talign"></param>
-		/// <param name="wordWrap">if <c>false</c>, forces text to fit a single line. Line breaks are converted to spaces.</param>
-		static void Reformat (ustring textStr, List<ustring> lineResult, int width, TextAlignment talign, bool wordWrap)
+		/// <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)
 		{
 		{
-			lineResult.Clear ();
+			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) {
 			if (wordWrap == false) {
-				textStr = ReplaceCRLFWithSpace (textStr);
-				lineResult.Add (ClipAndJustify (textStr, width, talign));
-				return;
+				text = ReplaceCRLFWithSpace (text);
+				lineResult.Add (ClipAndJustify (text, width, talign));
+				return lineResult;
 			}
 			}
 
 
-			int runeCount = textStr.RuneCount;
+			var runes = text.ToRunes ();
+			int runeCount = runes.Length;
 			int lp = 0;
 			int lp = 0;
 			for (int i = 0; i < runeCount; i++) {
 			for (int i = 0; i < runeCount; i++) {
-				Rune c = textStr [i];
+				Rune c = text [i];
 				if (c == '\n') {
 				if (c == '\n') {
-					var wrappedLines = WordWrap (textStr [lp, i], width);
+					var wrappedLines = WordWrap (ustring.Make (runes [lp..i]), width);
 					foreach (var line in wrappedLines) {
 					foreach (var line in wrappedLines) {
 						lineResult.Add (ClipAndJustify (line, width, talign));
 						lineResult.Add (ClipAndJustify (line, width, talign));
 					}
 					}
@@ -247,9 +318,11 @@ namespace Terminal.Gui {
 					lp = i + 1;
 					lp = i + 1;
 				}
 				}
 			}
 			}
-			foreach (var line in WordWrap (textStr [lp, runeCount], width)) {
+			foreach (var line in WordWrap (ustring.Make (runes [lp..runeCount]), width)) {
 				lineResult.Add (ClipAndJustify (line, width, talign));
 				lineResult.Add (ClipAndJustify (line, width, talign));
 			}
 			}
+
+			return lineResult;
 		}
 		}
 
 
 		/// <summary>
 		/// <summary>
@@ -260,8 +333,7 @@ namespace Terminal.Gui {
 		/// <param name="width">The minimum width for the text.</param>
 		/// <param name="width">The minimum width for the text.</param>
 		public static int MaxLines (ustring text, int width)
 		public static int MaxLines (ustring text, int width)
 		{
 		{
-			var result = new List<ustring> ();
-			TextFormatter.Reformat (text, result, width, TextAlignment.Left, true);
+			var result = TextFormatter.Format (text, width, TextAlignment.Left, true);
 			return result.Count;
 			return result.Count;
 		}
 		}
 
 
@@ -273,59 +345,10 @@ namespace Terminal.Gui {
 		/// <param name="width">The minimum width for the text.</param>
 		/// <param name="width">The minimum width for the text.</param>
 		public static int MaxWidth (ustring text, int width)
 		public static int MaxWidth (ustring text, int width)
 		{
 		{
-			var result = new List<ustring> ();
-			TextFormatter.Reformat (text, result, width, TextAlignment.Left, true);
+			var result = TextFormatter.Format (text, width, TextAlignment.Left, true);
 			return result.Max (s => s.RuneCount);
 			return result.Max (s => s.RuneCount);
 		}
 		}
 
 
-		internal void Draw (Rect bounds, Attribute normalColor, Attribute hotColor)
-		{
-			// With this check, we protect against subclasses with overrides of Text
-			if (ustring.IsNullOrEmpty (text)) {
-				return;
-			}
-
-			if (recalcPending) {
-				ReFormat ();
-			}
-
-			Application.Driver.SetAttribute (normalColor);
-
-			for (int line = 0; line < lines.Count; line++) {
-				if (line < (bounds.Height - bounds.Top) || line >= bounds.Height)
-					continue;
-				var str = lines [line];
-				int x;
-				switch (textAlignment) {
-				case TextAlignment.Left:
-					x = bounds.Left;
-					break;
-				case TextAlignment.Justified:
-					x = bounds.Left;
-					break;
-				case TextAlignment.Right:
-					x = bounds.Right - str.RuneCount;
-					break;
-				case TextAlignment.Centered:
-					x = bounds.Left + (bounds.Width - str.RuneCount) / 2;
-					break;
-				default:
-					throw new ArgumentOutOfRangeException ();
-				}
-				int col = 0;
-				foreach (var rune in str) {
-					Application.Driver.Move (x + col, bounds.Y + line);
-					if ((rune & 0x100000) == 0x100000) {
-						Application.Driver.SetAttribute (hotColor);
-						Application.Driver.AddRune ((Rune)((uint)rune & ~0x100000));
-						Application.Driver.SetAttribute (normalColor);
-					} else {
-						Application.Driver.AddRune (rune);
-					}
-					col++;
-				}
-			}
-		}
 
 
 		/// <summary>
 		/// <summary>
 		///  Calculates the rectangle requried to hold text, assuming no word wrapping.
 		///  Calculates the rectangle requried to hold text, assuming no word wrapping.
@@ -361,6 +384,16 @@ namespace Terminal.Gui {
 			return new Rect (x, y, mw, ml);
 			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)
 		public static bool FindHotKey (ustring text, Rune hotKeySpecifier, bool firstUpperCase, out int hotPos, out Key hotKey)
 		{
 		{
 			if (ustring.IsNullOrEmpty (text) || hotKeySpecifier == (Rune)0xFFFF) {
 			if (ustring.IsNullOrEmpty (text) || hotKeySpecifier == (Rune)0xFFFF) {
@@ -418,12 +451,23 @@ namespace Terminal.Gui {
 			return false;
 			return false;
 		}
 		}
 
 
-		public static ustring ReplaceHotKeyWithTag (ustring text, int hotPos)
+		/// <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
 			// Set the high bit
 			var runes = text.ToRuneList ();
 			var runes = text.ToRuneList ();
 			if (Rune.IsLetterOrNumber (runes [hotPos])) {
 			if (Rune.IsLetterOrNumber (runes [hotPos])) {
-				runes [hotPos] = new Rune ((uint)runes [hotPos] | 0x100000);
+				runes [hotPos] = new Rune ((uint)runes [hotPos] | HotKeyTagMask);
 			}
 			}
 			return ustring.Make (runes);
 			return ustring.Make (runes);
 		}
 		}
@@ -456,81 +500,57 @@ namespace Terminal.Gui {
 		}
 		}
 
 
 		/// <summary>
 		/// <summary>
-		/// Formats a single line of text with a hot-key and <see cref="Alignment"/>.
+		/// Draws the text held by <see cref="TextFormatter"/> to <see cref="Application.Driver"/> using the colors specified.
 		/// </summary>
 		/// </summary>
-		/// <param name="shown_text">The text to align.</param>
-		/// <param name="width">The maximum width for the text.</param>
-		/// <param name="hot_pos">The hot-key position before reformatting.</param>
-		/// <param name="c_hot_pos">The hot-key position after reformatting.</param>
-		/// <param name="textAlignment">The <see cref="Alignment"/> to align to.</param>
-		/// <returns>The aligned text.</returns>
-		public static ustring GetAlignedText (ustring shown_text, int width, int hot_pos, out int c_hot_pos, TextAlignment textAlignment)
+		/// <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)
 		{
 		{
-			int start;
-			var caption = shown_text;
-			c_hot_pos = hot_pos;
+			// With this check, we protect against subclasses with overrides of Text
+			if (ustring.IsNullOrEmpty (text)) {
+				return;
+			}
+
+			Application.Driver.SetAttribute (normalColor);
 
 
-			if (width > shown_text.RuneCount + 1) {
+			// Use "Lines" to ensure a Format (don't use "lines"))
+			for (int line = 0; line < Lines.Count; line++) {
+				if (line < (bounds.Height - bounds.Top) || line >= bounds.Height)
+					continue;
+				var runes = lines [line].ToRunes ();
+				int x;
 				switch (textAlignment) {
 				switch (textAlignment) {
 				case TextAlignment.Left:
 				case TextAlignment.Left:
-					caption += new string (' ', width - caption.RuneCount);
+					x = bounds.Left;
+					break;
+				case TextAlignment.Justified:
+					x = bounds.Left;
 					break;
 					break;
 				case TextAlignment.Right:
 				case TextAlignment.Right:
-					start = width - caption.RuneCount;
-					caption = $"{new string (' ', width - caption.RuneCount)}{caption}";
-					if (c_hot_pos > -1) {
-						c_hot_pos += start;
-					}
+					x = bounds.Right - runes.Length;
 					break;
 					break;
 				case TextAlignment.Centered:
 				case TextAlignment.Centered:
-					start = width / 2 - caption.RuneCount / 2;
-					caption = $"{new string (' ', start)}{caption}{new string (' ', width - caption.RuneCount - start)}";
-					if (c_hot_pos > -1) {
-						c_hot_pos += start;
-					}
+					x = bounds.Left + (bounds.Width - runes.Length) / 2;
 					break;
 					break;
-				case TextAlignment.Justified:
-					var words = caption.Split (" ");
-					var wLen = GetWordsLength (words, c_hot_pos, out int runeCount, out int w_hot_pos);
-					var space = (width - runeCount) / (caption.RuneCount - wLen);
-					caption = "";
-					for (int i = 0; i < words.Length; i++) {
-						if (i == words.Length - 1) {
-							caption += new string (' ', width - caption.RuneCount - 1);
-							caption += words [i];
-						} else {
-							caption += words [i];
-						}
-						if (i < words.Length - 1) {
-							caption += new string (' ', space);
-						}
+				default:
+					throw new ArgumentOutOfRangeException ();
+				}
+				for (var col = bounds.Left; col < bounds.Left + bounds.Width; col++) {
+					Application.Driver.Move (col, bounds.Y + line);
+					var rune = (Rune)' ';
+					if (col >= x && col < (x + runes.Length)) {
+						rune = runes [col - x];
 					}
 					}
-					if (c_hot_pos > -1) {
-						c_hot_pos += w_hot_pos * space - space - w_hot_pos + 1;
+					if ((rune & HotKeyTagMask) == HotKeyTagMask) {
+						Application.Driver.SetAttribute (hotColor);
+						Application.Driver.AddRune ((Rune)((uint)rune & ~HotKeyTagMask));
+						Application.Driver.SetAttribute (normalColor);
+					} else {
+						Application.Driver.AddRune (rune);
 					}
 					}
-					break;
 				}
 				}
 			}
 			}
-
-			return caption;
-		}
-
-		static int GetWordsLength (ustring [] words, int hotPos, out int runeCount, out int wordHotPos)
-		{
-			int length = 0;
-			int rCount = 0;
-			int wHotPos = -1;
-			for (int i = 0; i < words.Length; i++) {
-				if (wHotPos == -1 && rCount + words [i].RuneCount >= hotPos)
-					wHotPos = i;
-				length += words [i].Length;
-				rCount += words [i].RuneCount;
-			}
-			if (wHotPos == -1 && hotPos > -1)
-				wHotPos = words.Length;
-			runeCount = rCount;
-			wordHotPos = wHotPos;
-			return length;
 		}
 		}
 	}
 	}
 }
 }

+ 5 - 6
Terminal.Gui/Core/View.cs

@@ -414,7 +414,7 @@ namespace Terminal.Gui {
 		/// </remarks>
 		/// </remarks>
 		public View (Rect frame)
 		public View (Rect frame)
 		{
 		{
-			viewText = new TextFormatter (this);
+			viewText = new TextFormatter ();
 			this.Text = ustring.Empty;
 			this.Text = ustring.Empty;
 
 
 			this.Frame = frame;
 			this.Frame = frame;
@@ -477,7 +477,7 @@ namespace Terminal.Gui {
 		/// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
 		/// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
 		public View (Rect rect, ustring text) : this (rect)
 		public View (Rect rect, ustring text) : this (rect)
 		{
 		{
-			viewText = new TextFormatter (this);
+			viewText = new TextFormatter ();
 			this.Text = text;
 			this.Text = text;
 		}
 		}
 
 
@@ -497,7 +497,7 @@ namespace Terminal.Gui {
 		/// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
 		/// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
 		public View (ustring text) : base ()
 		public View (ustring text) : base ()
 		{
 		{
-			viewText = new TextFormatter (this);
+			viewText = new TextFormatter ();
 			this.Text = text;
 			this.Text = text;
 
 
 			CanFocus = false;
 			CanFocus = false;
@@ -528,7 +528,7 @@ namespace Terminal.Gui {
 			if (SuperView == null)
 			if (SuperView == null)
 				return;
 				return;
 			SuperView.SetNeedsLayout ();
 			SuperView.SetNeedsLayout ();
-			viewText.ReFormat ();
+			viewText.SetNeedsFormat ();
 		}
 		}
 
 
 		/// <summary>
 		/// <summary>
@@ -1079,7 +1079,7 @@ namespace Terminal.Gui {
 				Clear ();
 				Clear ();
 				// Draw any Text
 				// Draw any Text
 				// TODO: Figure out if this should go here or after OnDrawContent
 				// TODO: Figure out if this should go here or after OnDrawContent
-				viewText?.ReFormat ();
+				viewText?.SetNeedsFormat ();
 				viewText?.Draw (ViewToScreen (Bounds), ColorScheme.Normal, ColorScheme.HotNormal);
 				viewText?.Draw (ViewToScreen (Bounds), ColorScheme.Normal, ColorScheme.HotNormal);
 			}
 			}
 
 
@@ -1549,7 +1549,6 @@ namespace Terminal.Gui {
 				return;
 				return;
 
 
 			viewText.Size = Bounds.Size;
 			viewText.Size = Bounds.Size;
-			viewText.ReFormat ();
 
 
 			Rect oldBounds = Bounds;
 			Rect oldBounds = Bounds;
 
 

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

@@ -1,11 +1,12 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFrameworks>net472;netstandard2.0</TargetFrameworks>
+    <TargetFrameworks>netstandard2.1</TargetFrameworks>
     <RootNamespace>Terminal.Gui</RootNamespace>
     <RootNamespace>Terminal.Gui</RootNamespace>
     <AssemblyName>Terminal.Gui</AssemblyName>
     <AssemblyName>Terminal.Gui</AssemblyName>
     <DocumentationFile>bin\Release\Terminal.Gui.xml</DocumentationFile>
     <DocumentationFile>bin\Release\Terminal.Gui.xml</DocumentationFile>
     <GenerateDocumentationFile Condition=" '$(Configuration)' == 'Release' ">true</GenerateDocumentationFile>
     <GenerateDocumentationFile Condition=" '$(Configuration)' == 'Release' ">true</GenerateDocumentationFile>
     <AssemblyVersion>0.90.0.0</AssemblyVersion>
     <AssemblyVersion>0.90.0.0</AssemblyVersion>
+    <LangVersion>8.0</LangVersion>
   </PropertyGroup>
   </PropertyGroup>
   <PropertyGroup>
   <PropertyGroup>
     <GeneratePackageOnBuild Condition=" '$(Configuration)' == 'Release' ">true</GeneratePackageOnBuild>
     <GeneratePackageOnBuild Condition=" '$(Configuration)' == 'Release' ">true</GeneratePackageOnBuild>

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

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

+ 4 - 1
UICatalog/Scenarios/TextAlignments.cs

@@ -9,8 +9,11 @@ namespace UICatalog {
 	class TextAlignments : Scenario {
 	class TextAlignments : Scenario {
 		public override void Setup ()
 		public override void Setup ()
 		{
 		{
+			Win.X = 10;
+			Win.Width = Dim.Fill (20);
+
 			string txt = "Hello world, how are you today? Pretty neat!";
 			string txt = "Hello world, how are you today? Pretty neat!";
-			string unicodeSampleText = "A Unicode sentence (пÑивеÑ) has words.";
+			string unicodeSampleText = "A Unicode sentence (пÑРвеÑ) has words.";
 
 
 			var alignments = Enum.GetValues (typeof (Terminal.Gui.TextAlignment)).Cast<Terminal.Gui.TextAlignment> ().ToList ();
 			var alignments = Enum.GetValues (typeof (Terminal.Gui.TextAlignment)).Cast<Terminal.Gui.TextAlignment> ().ToList ();
 			var singleLines = new Label [alignments.Count];
 			var singleLines = new Label [alignments.Count];

File diff suppressed because it is too large
+ 710 - 169
UnitTests/TextFormatterTests.cs


Some files were not shown because too many files changed in this diff