Charlie Kindel 5 лет назад
Родитель
Сommit
120991ec5b

+ 4 - 0
Terminal.Gui/Core/Range.cs

@@ -0,0 +1,4 @@
+namespace Terminal.Gui {
+	internal class Range {
+	}
+}

+ 38 - 16
Terminal.Gui/Core/TextFormatter.cs

@@ -5,6 +5,28 @@ 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>
@@ -177,21 +199,20 @@ namespace Terminal.Gui {
 				return lines;
 			}
 
-			var runes = StripCRLF (text).ToRunes ();
+			var runes = StripCRLF (text).ToRuneList();
 
-			while ((end = start + width) < runes.Length) {
+			while ((end = start + width) < runes.Count) {
 				while (runes [end] != ' ' && end > start)
 					end -= 1;
 				if (end == start)
 					end = start + width;
-
-
-				lines.Add (ustring.Make (runes [start..end]).TrimSpace ());
+				lines.Add (ustring.Make (runes.GetRange (start, end - start)).TrimSpace());
 				start = end;
 			}
 
-			if (start < text.RuneCount)
-				lines.Add (ustring.Make (runes [start..]).TrimSpace ());
+			if (start < text.RuneCount) {
+				lines.Add (ustring.Make (runes.GetRange (start, runes.Count - start)).TrimSpace ());
+			}
 
 			return lines;
 		}
@@ -212,10 +233,10 @@ namespace Terminal.Gui {
 				return text;
 			}
 
-			var runes = text.ToRunes ();
-			int slen = runes.Length;
+			var runes = text.ToRuneList ();
+			int slen = runes.Count;
 			if (slen > width) {
-				return ustring.Make (runes [0..width]); // text [0, width];
+				return ustring.Make (runes.GetRange(0, width));
 			} else {
 				if (talign == TextAlignment.Justified) {
 					return Justify (text, width);
@@ -302,13 +323,13 @@ namespace Terminal.Gui {
 				return lineResult;
 			}
 
-			var runes = text.ToRunes ();
-			int runeCount = runes.Length;
+			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 [lp..i]), width);
+					var wrappedLines = WordWrap (ustring.Make (runes.GetRange(lp, i - lp)), width);
 					foreach (var line in wrappedLines) {
 						lineResult.Add (ClipAndJustify (line, width, talign));
 					}
@@ -318,7 +339,7 @@ namespace Terminal.Gui {
 					lp = i + 1;
 				}
 			}
-			foreach (var line in WordWrap (ustring.Make (runes [lp..runeCount]), width)) {
+			foreach (var line in WordWrap (ustring.Make (runes.GetRange(lp, runeCount - lp)), width)) {
 				lineResult.Add (ClipAndJustify (line, width, talign));
 			}
 
@@ -516,7 +537,7 @@ namespace Terminal.Gui {
 
 			// 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)
+				if (line > bounds.Height)
 					continue;
 				var runes = lines [line].ToRunes ();
 				int x;
@@ -537,7 +558,7 @@ namespace Terminal.Gui {
 					throw new ArgumentOutOfRangeException ();
 				}
 				for (var col = bounds.Left; col < bounds.Left + bounds.Width; col++) {
-					Application.Driver.Move (col, bounds.Y + line);
+					Application.Driver.Move (col, bounds.Top + line);
 					var rune = (Rune)' ';
 					if (col >= x && col < (x + runes.Length)) {
 						rune = runes [col - x];
@@ -552,5 +573,6 @@ namespace Terminal.Gui {
 				}
 			}
 		}
+
 	}
 }

+ 27 - 35
Terminal.Gui/Core/View.cs

@@ -18,28 +18,6 @@ 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
@@ -172,12 +150,12 @@ namespace Terminal.Gui {
 		public Action<MouseEventArgs> MouseClick;
 
 		/// <summary>
-		/// 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.
+		/// 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; }
 
@@ -860,21 +838,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);
 			}
 		}
 
@@ -1078,9 +1059,8 @@ namespace Terminal.Gui {
 			if (!ustring.IsNullOrEmpty (Text)) {
 				Clear ();
 				// Draw any Text
-				// TODO: Figure out if this should go here or after OnDrawContent
 				viewText?.SetNeedsFormat ();
-				viewText?.Draw (ViewToScreen (Bounds), ColorScheme.Normal, ColorScheme.HotNormal);
+				viewText?.Draw (ViewToScreen (Bounds), HasFocus ? ColorScheme.Focus : ColorScheme.Normal, HasFocus ? ColorScheme.HotFocus : ColorScheme.HotNormal);
 			}
 
 			// Invoke DrawContentEvent
@@ -1594,7 +1574,19 @@ namespace Terminal.Gui {
 		///   The text displayed by the <see cref="View"/>.
 		/// </summary>
 		/// <remarks>
-		///  The text will only be displayed if the View has no subviews.
+		/// <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;
@@ -1605,7 +1597,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Controls the text-alignment property of the View. Changing this property will redisplay the <see cref="View"/>.
+		/// 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>
 		/// <value>The text alignment.</value>
 		public virtual TextAlignment TextAlignment {

+ 11 - 3
Terminal.Gui/Terminal.Gui.csproj

@@ -1,12 +1,11 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
-    <TargetFrameworks>netstandard2.1</TargetFrameworks>
+    <TargetFrameworks>net472;netstandard2.0</TargetFrameworks>
     <RootNamespace>Terminal.Gui</RootNamespace>
     <AssemblyName>Terminal.Gui</AssemblyName>
     <DocumentationFile>bin\Release\Terminal.Gui.xml</DocumentationFile>
     <GenerateDocumentationFile Condition=" '$(Configuration)' == 'Release' ">true</GenerateDocumentationFile>
     <AssemblyVersion>0.90.0.0</AssemblyVersion>
-    <LangVersion>8.0</LangVersion>
   </PropertyGroup>
   <PropertyGroup>
     <GeneratePackageOnBuild Condition=" '$(Configuration)' == 'Release' ">true</GeneratePackageOnBuild>
@@ -75,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!)
@@ -101,6 +100,15 @@
       * 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.
+        * Any view derived from View now gets mouse click (Clicked event) support for free.
+        * Label is now just an alias for View.
+        * 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

+ 8 - 15
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
@@ -103,19 +109,8 @@ namespace Terminal.Gui {
 			CanFocus = true;
 			this.IsDefault = is_default;
 			Text = text ?? string.Empty;
-			//int w = SetWidthHeight (text, is_default);
-			//Frame = new Rect (Frame.Location, new Size (w, 1));
 		}
 
-		//int SetWidthHeight (ustring text, bool is_default)
-		//{
-		//	int w = text.RuneCount;// + 4 + (is_default ? 2 : 0);
-		//	Width = w;
-		//	Height = 1;
-		//	Frame = new Rect (Frame.Location, new Size (w, 1));
-		//	return w;
-		//}
-
 		/// <summary>
 		///   The text displayed by this <see cref="Button"/>.
 		/// </summary>
@@ -153,7 +148,6 @@ namespace Terminal.Gui {
 			Width = w;
 			Height = 1;
 			Frame = new Rect (Frame.Location, new Size (w, 1));
-
 			SetNeedsDisplay ();
 		}
 
@@ -196,6 +190,5 @@ namespace Terminal.Gui {
 			}
 			return base.ProcessKey (kb);
 		}
-
 	}
 }

+ 3 - 3
UICatalog/Scenarios/AllViewsTester.cs

@@ -76,9 +76,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 +87,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,
 			};

+ 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.RuneCount];
+				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.RuneCount) {
+				if (i >= runes.Count) {
 					i = 0;
 				}
 
 				// Slip in the '_'
-				start = txt [0, i];
-				txt = start + ustring.Make ('_') + txt [i, txt.RuneCount];
-
-				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 = ustring.Make(" ~  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,

+ 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);
 

+ 87 - 59
UICatalog/Scenarios/LabelsAsButtons.cs

@@ -6,10 +6,10 @@ using System.Reflection;
 using Terminal.Gui;
 
 namespace UICatalog {
-	[ScenarioMetadata (Name: "LabelsAsButtons", Description: "POC to see how making Label more a base class would work")]
+	[ScenarioMetadata (Name: "Labels As Buttons", Description: "Illustrates that Button is really just a Label++")]
 	[ScenarioCategory ("Controls")]
 	[ScenarioCategory ("POC")]
-	class LabelsAsButtons : Scenario {
+	class LabelsAsLabels : Scenario {
 		public override void Setup ()
 		{
 			// Add a label & text field so we can demo IsDefault
@@ -22,107 +22,120 @@ namespace UICatalog {
 			var edit = new TextField (31, 0, 15, "");
 			Win.Add (edit);
 
-			// This is the default button (IsDefault = true); if user presses ENTER in the TextField
+			// This is the default Label (IsDefault = true); if user presses ENTER in the TextField
 			// the scenario will quit
-			var defaultButton = new Label ("_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 (defaultButton);
+			Win.Add (defaultLabel);
 
-			var swapButton = new Label (50, 0, "Swap Default (Absolute Layout)");
-			swapButton.Clicked = () => {
-				//defaultButton.IsDefault = !defaultButton.IsDefault;
-				//swapButton.IsDefault = !swapButton.IsDefault;
+			var swapLabel = new Label (50, 0, "S_wap Default (Absolute Layout)") {
+				HotKeySpecifier = (System.Rune)'_',
+				CanFocus = true,
 			};
-			Win.Add (swapButton);
+			swapLabel.Clicked = () => {
+				//defaultLabel.IsDefault = !defaultLabel.IsDefault;
+				//swapLabel.IsDefault = !swapLabel.IsDefault;
+			};
+			Win.Add (swapLabel);
 
-			static void DoMessage (Label button, ustring txt)
+			static void DoMessage (Label Label, ustring txt)
 			{
-				button.Clicked = () => {
-					var btnText = button.Text.ToString ();
+				Label.Clicked = () => {
+					var btnText = Label.Text.ToString ();
 					MessageBox.Query ("Message", $"Did you click {txt}?", "Yes", "No");
 				};
 			}
 
-			var colorButtonsLabel = new Label ("Color Buttons:") {
+			var colorLabelsLabel = new Label ("Color Labels:") {
 				X = 0,
 				Y = Pos.Bottom (editLabel) + 1,
 			};
-			Win.Add (colorButtonsLabel);
-
-			//View prev = colorButtonsLabel;
+			Win.Add (colorLabelsLabel);
 
 			//With this method there is no need to call Top.Ready += () => Top.Redraw (Top.Bounds);
-			var x = Pos.Right (colorButtonsLabel) + 2;
+			var x = Pos.Right (colorLabelsLabel) + 2;
 			foreach (var colorScheme in Colors.ColorSchemes) {
-				var colorButton = new Label ($"{colorScheme.Key}") {
+				var colorLabel = new Label ($"{colorScheme.Key}") {
 					ColorScheme = colorScheme.Value,
-					//X = Pos.Right (prev) + 2,
 					X = x,
-					Y = Pos.Y (colorButtonsLabel),
+					Y = Pos.Y (colorLabelsLabel),
+					HotKeySpecifier = (System.Rune)'_',
+					CanFocus = true,
 				};
-				DoMessage (colorButton, colorButton.Text);
-				Win.Add (colorButton);
-				//prev = colorButton;
-				x += colorButton.Frame.Width + 2;
+				DoMessage (colorLabel, colorLabel.Text);
+				Win.Add (colorLabel);
+				x += colorLabel.Text.Length + 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);
+			Top.Ready += () => Top.Redraw (Top.Bounds);
 
-			Label button;
-			Win.Add (button = new Label ("A super long _Button that will probably expose a bug in clipping or wrapping of text. Will it?") {
+			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 (colorButtonsLabel) + 1,
+				Y = Pos.Bottom (colorLabelsLabel) + 1,
+				HotKeySpecifier = (System.Rune)'_',
+				CanFocus = true,
 			});
-			DoMessage (button, button.Text);
+			DoMessage (Label, Label.Text);
 
 			// Note the 'N' in 'Newline' will be the hotkey
-			Win.Add (button = new Label ("a Newline\nin the button") {
+			Win.Add (Label = new Label ("a Newline\nin the Label") {
 				X = 2,
-				Y = Pos.Bottom (button) + 1,
-				Clicked = () => MessageBox.Query ("Message", "Question?", "Yes", "No")
+				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 (button) + 1,
+				Y = Pos.Bottom (Label) + 1,
+				HotKeySpecifier = (System.Rune)'_',
+				CanFocus = true,
 			};
 			Win.Add (textChanger);
 			textChanger.Clicked = () => textChanger.Text += "!";
 
-			Win.Add (button = new Label ("Lets see if this will move as \"Text Changer\" grows") {
+			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 removeButton = new Label ("Remove this button") {
+			var removeLabel = new Label ("Remove this Label") {
 				X = 2,
-				Y = Pos.Bottom (button) + 1,
-				ColorScheme = Colors.Error
+				Y = Pos.Bottom (Label) + 1,
+				ColorScheme = Colors.Error,
+				HotKeySpecifier = (System.Rune)'_',
+				CanFocus = true,
 			};
-			Win.Add (removeButton);
+			Win.Add (removeLabel);
 			// This in intresting test case because `moveBtn` and below are laid out relative to this one!
-			removeButton.Clicked = () => Win.Remove (removeButton);
+			removeLabel.Clicked = () => Win.Remove (removeLabel);
 
 			var computedFrame = new FrameView ("Computed Layout") {
 				X = 0,
-				Y = Pos.Bottom (removeButton) + 1,
+				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 Button _via Pos") {
+			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;
@@ -132,12 +145,14 @@ namespace UICatalog {
 			computedFrame.Add (moveBtn);
 
 			// Demonstrates how changing the View.Frame property can SIZE Views (#583)
-			var sizeBtn = new Label ("Size This \u263a Button _via Pos") {
-			//var sizeBtn = new Label ("Size This x Button _via Pos") {
+			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;
@@ -147,15 +162,17 @@ namespace UICatalog {
 
 			var absoluteFrame = new FrameView ("Absolute Layout") {
 				X = Pos.Right (computedFrame),
-				Y = Pos.Bottom (removeButton) + 1,
+				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 Button via Frame") {
+			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);
@@ -165,15 +182,19 @@ namespace UICatalog {
 			// 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 buttons above): ") {
+			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);
 
@@ -188,31 +209,36 @@ 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.RuneCount];
+				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.RuneCount) {
+				if (i >= runes.Count) {
 					i = 0;
 				}
 
 				// Slip in the '_'
-				start = txt [0, i];
-				txt = start + ustring.Make ('_') + txt [i, txt.RuneCount];
-
-				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";
+			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);
@@ -225,6 +251,8 @@ namespace UICatalog {
 				Y = Pos.Bottom (radioGroup) + 1,
 				Width = muhkb.Length + 30,
 				ColorScheme = Colors.TopLevel,
+				HotKeySpecifier = (System.Rune)'_',
+				CanFocus = true,
 			};
 			moveUnicodeHotKeyBtn.Clicked = () => {
 				moveUnicodeHotKeyBtn.Text = MoveHotkey (moveUnicodeHotKeyBtn.Text);

+ 23 - 7
UICatalog/Scenarios/TextAlignments.cs

@@ -10,7 +10,7 @@ namespace UICatalog {
 		public override void Setup ()
 		{
 			Win.X = 10;
-			Win.Width = Dim.Fill (20);
+			Win.Width = Dim.Fill (10);
 
 			string txt = "Hello world, how are you today? Pretty neat!";
 			string unicodeSampleText = "A Unicode sentence (пÑРвеÑ) has words.";
@@ -22,8 +22,8 @@ namespace UICatalog {
 			var multiLineHeight = 5;
 
 			foreach (var alignment in alignments) {
-				singleLines[(int)alignment] = new Label (txt) { TextAlignment = alignment, Width = Dim.Fill (), Height = 1, ColorScheme = Colors.Dialog };
-				multipleLines [(int)alignment] = new Label (txt) { TextAlignment = alignment, Width = Dim.Fill (), Height = multiLineHeight, ColorScheme = Colors.Dialog };
+				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
@@ -35,7 +35,7 @@ namespace UICatalog {
 			var edit = new TextView () {
 				X = Pos.Right (editLabel) + 1,
 				Y = Pos.Y (editLabel),
-				Width = Dim.Fill("Text:".Length + "  Unicode Sample".Length + 2),
+				Width = Dim.Fill ("Text:".Length + "  Unicode Sample".Length + 2),
 				Height = 4,
 				ColorScheme = Colors.TopLevel,
 				Text = txt,
@@ -57,7 +57,7 @@ namespace UICatalog {
 			};
 			Win.Add (unicodeSample);
 
-			var update = new Button ("_Update", is_default: true) {
+			var update = new Button ("_Update") {
 				X = Pos.Right (edit) + 1,
 				Y = Pos.Bottom (edit) - 1,
 				Clicked = () => {
@@ -69,7 +69,14 @@ namespace UICatalog {
 			};
 			Win.Add (update);
 
-			var label = new Label ($"Demonstrating single-line (should clip):") { Y = Pos.Bottom (edit) + 1 };
+			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) };
@@ -80,7 +87,7 @@ namespace UICatalog {
 			}
 
 			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) };
@@ -89,6 +96,15 @@ namespace UICatalog {
 				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 ();

+ 0 - 48
UICatalog/Scenarios/ViewWithText.cs

@@ -1,48 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Terminal.Gui;
-
-namespace UICatalog {
-	[ScenarioMetadata (Name: "View Text", Description: "Demos and tests View's Text capabilities.")]
-	[ScenarioCategory ("Text")]
-	[ScenarioCategory ("POC")]
-	class ViewWithText : Scenario {
-		public override void Setup ()
-		{
-			Win.Text = "This is the Te_xt for the host Win object. TextAlignment.Centered was specified. It is intentionally very long to illustrate word wrap.\n" +
-				"<-- There is a new line here to show a hard line break. You should see this text bleed underneath the subviews, which start at Y = 3.";
-			Win.TextAlignment = TextAlignment.Centered;
-#if true
-			string txt = "Hello world, how are you today? Pretty neat!";
-#else
-			string txt = "Hello world, how are you today? Unicode:  ~  gui.cs  . Neat?";
-#endif
-			var alignments = Enum.GetValues (typeof (Terminal.Gui.TextAlignment)).Cast<Terminal.Gui.TextAlignment> ().ToList ();
-			var label = new View ($"Demonstrating single-line (should clip!):") { Y = 3 };
-			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);
-			}
-
-			txt += "\nSecond line\n\nFourth Line.";
-			label = new View ($"Demonstrating multi-line and word wrap:") { Y = Pos.Bottom (label) + 1 };
-			Win.Add (label);
-			foreach (var alignment in alignments) {
-				label = new View ($"{alignment}:") { Y = Pos.Bottom (label) };
-				Win.Add (label);
-				label = new View (txt) { TextAlignment = alignment, Width = Dim.Fill (), Height = 6, ColorScheme = Colors.Dialog, Y = Pos.Bottom (label) };
-				Win.Add (label);
-			}
-		}
-	}
-}

+ 4 - 0
UnitTests/TextFormatterTests.cs

@@ -1613,6 +1613,10 @@ namespace Terminal.Gui {
 			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;